diff --git a/.github/workflows/jenkins-driver-tests.yml b/.github/workflows/jenkins-driver-tests.yml index 153b8f6913..f04d237442 100644 --- a/.github/workflows/jenkins-driver-tests.yml +++ b/.github/workflows/jenkins-driver-tests.yml @@ -1,16 +1,9 @@ name: Run Jenkins driver tests on: - pull_request: - paths: - - 'drivers/**' - pull_request_target: paths: - 'drivers/**' -permissions: - statuses: write - jobs: trigger-driver-test: strategy: diff --git a/drivers/SmartThings/matter-appliance/src/init.lua b/drivers/SmartThings/matter-appliance/src/init.lua index 044da51e7c..8f91ebcd0c 100644 --- a/drivers/SmartThings/matter-appliance/src/init.lua +++ b/drivers/SmartThings/matter-appliance/src/init.lua @@ -297,6 +297,7 @@ local matter_driver_template = { capabilities.windMode }, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } local matter_driver = MatterDriver("matter-appliance", matter_driver_template) diff --git a/drivers/SmartThings/matter-energy/src/init.lua b/drivers/SmartThings/matter-energy/src/init.lua index 51d361753a..c6e44008d6 100644 --- a/drivers/SmartThings/matter-energy/src/init.lua +++ b/drivers/SmartThings/matter-energy/src/init.lua @@ -750,6 +750,7 @@ matter_driver_template = { capabilities.battery, capabilities.chargingState }, + shared_device_thread_enabled = true, } local matter_driver = MatterDriver("matter-energy", matter_driver_template) diff --git a/drivers/SmartThings/matter-lock/src/init.lua b/drivers/SmartThings/matter-lock/src/init.lua index b3403863ec..4f58e960e9 100755 --- a/drivers/SmartThings/matter-lock/src/init.lua +++ b/drivers/SmartThings/matter-lock/src/init.lua @@ -714,6 +714,7 @@ local matter_lock_driver = { doConfigure = do_configure, infoChanged = info_changed, }, + shared_device_thread_enabled = true, } ----------------------------------------------------------------------------------------------------------------------------- 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 fbea3ebfb5..adcbc04283 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -259,7 +259,8 @@ local function match_profile_modular(driver, device) end table.insert(enabled_optional_component_capability_pairs, {"main", main_component_capabilities}) - if lock_utils.optional_capabilities_list_changed(enabled_optional_component_capability_pairs, device.profile.components) then + if modular_profile_name == "lock-modular-embedded-unlatch" -- the embedded config that may be needed is not checked by an optional capability comparison + or lock_utils.optional_capabilities_list_changed(enabled_optional_component_capability_pairs, device.profile.components) then device:try_update_metadata({profile = modular_profile_name, optional_component_capabilities = enabled_optional_component_capability_pairs}) end end 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 cb22abdf7d..4b875fd1bc 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 @@ -623,4 +623,77 @@ test.register_coroutine_test( } ) + +local mock_nuki_smart_lock_ultra = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock-nocodes-notamper.yml"), + manufacturer_info = { + vendor_id = 0x135D, + product_id = 0x00A1, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.BasicInformation.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = DoorLock.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0x1000, -- UNBOLT + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = 10 + }, + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock + } + } + } +}) + +local battery_support = { + NO_BATTERY = "NO_BATTERY", + BATTERY_LEVEL = "BATTERY_LEVEL", + BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" +} + +local profiling_data = { + BATTERY_SUPPORT = "__BATTERY_SUPPORT", +} + +test.register_coroutine_test( + "Test Nuki Smart Lock Ultra profile change with user and pin supported", + function() + -- technically, since power source attributes must be read, this wouldn't be running via doConfigure, but this is straightforward. + test.socket.device_lifecycle:__queue_receive({ mock_nuki_smart_lock_ultra.id, "doConfigure" }) + test.socket.capability:__expect_send( + mock_nuki_smart_lock_ultra:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_nuki_smart_lock_ultra:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + mock_nuki_smart_lock_ultra:expect_metadata_update({ profile = "lock-modular-embedded-unlatch", optional_component_capabilities = {{"main", {"battery"}}}}) + mock_nuki_smart_lock_ultra:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + test_init = function() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_nuki_smart_lock_ultra) + mock_nuki_smart_lock_ultra:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_PERCENTAGE, {persist = true}) -- assume this has been set previously + end, + min_api_version = 17 + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/matter-pump/src/init.lua b/drivers/SmartThings/matter-pump/src/init.lua index db43d3b1df..0e79f66cbb 100644 --- a/drivers/SmartThings/matter-pump/src/init.lua +++ b/drivers/SmartThings/matter-pump/src/init.lua @@ -1,328 +1,329 @@ --- 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 log = require "log" -local clusters = require "st.matter.clusters" -local embedded_cluster_utils = require "embedded-cluster-utils" -local MatterDriver = require "st.matter.driver" - -local IS_LOCAL_OVERRIDE = "__is_local_override" --- Per matter spec, the pump level is in steps of 0.5% and the --- max level value is 200. Anything above is considered 100% -local MAX_PUMP_ATTR_LEVEL = 200 -local MAX_CAP_SWITCH_LEVEL = 100 - --- Include driver-side definitions when lua libs api version is < 10 -local version = require "version" -if version.api < 10 then - clusters.PumpConfigurationAndControl = require "PumpConfigurationAndControl" -end - -local pumpOperationMode = capabilities.pumpOperationMode -local pumpControlMode = capabilities.pumpControlMode - -local PUMP_OPERATION_MODE_MAP = { - [clusters.PumpConfigurationAndControl.types.OperationModeEnum.NORMAL] = pumpOperationMode.operationMode.normal, - [clusters.PumpConfigurationAndControl.types.OperationModeEnum.MINIMUM] = pumpOperationMode.operationMode.minimum, - [clusters.PumpConfigurationAndControl.types.OperationModeEnum.MAXIMUM] = pumpOperationMode.operationMode.maximum, - [clusters.PumpConfigurationAndControl.types.OperationModeEnum.LOCAL] = pumpOperationMode.operationMode.localSetting, -} - -local PUMP_CONTROL_MODE_MAP = { - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_SPEED] = pumpControlMode.controlMode.constantSpeed, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_PRESSURE] = pumpControlMode.controlMode.constantPressure, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.PROPORTIONAL_PRESSURE] = pumpControlMode.controlMode.proportionalPressure, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_FLOW] = pumpControlMode.controlMode.constantFlow, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_TEMPERATURE] = pumpControlMode.controlMode.constantTemperature, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.AUTOMATIC] = pumpControlMode.controlMode.automatic, -} - -local PUMP_CURRENT_CONTROL_MODE_MAP = { - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_SPEED] = pumpControlMode.currentControlMode.constantSpeed, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_PRESSURE] = pumpControlMode.currentControlMode.constantPressure, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.PROPORTIONAL_PRESSURE] = pumpControlMode.currentControlMode.proportionalPressure, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_FLOW] = pumpControlMode.currentControlMode.constantFlow, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_TEMPERATURE] = pumpControlMode.currentControlMode.constantTemperature, - [clusters.PumpConfigurationAndControl.types.ControlModeEnum.AUTOMATIC] = pumpControlMode.currentControlMode.automatic, -} - -local subscribed_attributes = { - [capabilities.switch.ID] = { - clusters.OnOff.attributes.OnOff, - }, - [capabilities.switchLevel.ID] = { - clusters.LevelControl.attributes.CurrentLevel - }, - [capabilities.pumpOperationMode.ID]={ - clusters.PumpConfigurationAndControl.attributes.OperationMode, - clusters.PumpConfigurationAndControl.attributes.EffectiveOperationMode, - clusters.PumpConfigurationAndControl.attributes.PumpStatus, - }, - [capabilities.pumpControlMode.ID]={ - clusters.PumpConfigurationAndControl.attributes.EffectiveControlMode, - }, -} - -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) - -- Use the find_default_endpoint function to return the first endpoint that - -- supports a given cluster. - return find_default_endpoint(device, clusters.PumpConfigurationAndControl.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) - --Note this is needed because device:subscribe() does not recalculate - -- the subscribed attributes each time it is run, that only happens at init. - -- This will change in the 0.48.x release of the lua libs. - 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 set_supported_op_mode(driver, device) - local spd_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_SPEED}) - local local_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.LOCAL_OPERATION}) - local supported_op_modes = {pumpOperationMode.operationMode.normal.NAME} - if #spd_eps > 0 then - table.insert(supported_op_modes, pumpOperationMode.operationMode.minimum.NAME) - table.insert(supported_op_modes, pumpOperationMode.operationMode.maximum.NAME) - end - if #local_eps > 0 then - table.insert(supported_op_modes, pumpOperationMode.operationMode.localSetting.NAME) - end - device:emit_event(pumpOperationMode.supportedOperationModes(supported_op_modes)) -end - -local function set_supported_control_mode(driver, device) - local spd_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_SPEED}) - local prsconst_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_PRESSURE}) - local prscomp_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.COMPENSATED_PRESSURE}) - local flw_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_FLOW}) - local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_TEMPERATURE}) - local auto_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.AUTOMATIC}) - local supported_control_modes = {} - if #spd_eps > 0 then - table.insert(supported_control_modes, pumpControlMode.controlMode.constantSpeed.NAME) - end - if #prsconst_eps > 0 then - table.insert(supported_control_modes, pumpControlMode.controlMode.constantPressure.NAME) - end - if #prscomp_eps > 0 then - table.insert(supported_control_modes, pumpControlMode.controlMode.proportionalPressure.NAME) - end - if #flw_eps > 0 then - table.insert(supported_control_modes, pumpControlMode.controlMode.constantFlow.NAME) - end - if #temp_eps > 0 then - table.insert(supported_control_modes, pumpControlMode.controlMode.constantTemperature.NAME) - end - if #auto_eps > 0 then - table.insert(supported_control_modes, pumpControlMode.controlMode.automatic.NAME) - end - device:emit_event(pumpControlMode.supportedControlModes(supported_control_modes)) -end - -local function do_configure(driver, device) - local pump_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID) - local level_eps = embedded_cluster_utils.get_endpoints(device, clusters.LevelControl.ID) - local profile_name = "pump" - if #pump_eps == 1 then - if #level_eps > 0 then - profile_name = profile_name .. "-level" - else - profile_name = profile_name .. "-only" - end - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) - else - device.log.warn_with({hub_logs=true}, "Device does not support pump configuration and control cluster") - end - set_supported_op_mode(driver, device) - set_supported_control_mode(driver, device) -end - --- Matter Handlers -- -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 level_attr_handler(driver, device, ib, response) - if ib.data.value ~= nil then - local level = math.floor((ib.data.value / MAX_PUMP_ATTR_LEVEL * MAX_CAP_SWITCH_LEVEL) + 0.5) - level = math.min(level, MAX_CAP_SWITCH_LEVEL) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switchLevel.level(level)) - end -end - -local function effective_operation_mode_handler(driver, device, ib, response) - local modeEnum = clusters.PumpConfigurationAndControl.types.OperationModeEnum - local supported_control_modes = {} - local local_override = device:get_field(IS_LOCAL_OVERRIDE) - if not local_override then - set_supported_op_mode(driver, device) - end - if ib.data.value == modeEnum.NORMAL then - device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.normal()) - set_supported_control_mode(driver, device) - elseif ib.data.value == modeEnum.MINIMUM then - device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.minimum()) - device:emit_event_for_endpoint(ib.endpoint_id, pumpControlMode.supportedControlModes(supported_control_modes)) - elseif ib.data.value == modeEnum.MAXIMUM then - device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.maximum()) - device:emit_event_for_endpoint(ib.endpoint_id, pumpControlMode.supportedControlModes(supported_control_modes)) - elseif ib.data.value == modeEnum.LOCAL then - device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.localSetting()) - device:emit_event_for_endpoint(ib.endpoint_id, pumpControlMode.supportedControlModes(supported_control_modes)) - end -end - -local function effective_control_mode_handler(driver, device, ib, response) - device:emit_event_for_endpoint(ib.endpoint_id, PUMP_CURRENT_CONTROL_MODE_MAP[ib.data.value]()) -end - -local function pump_status_handler(driver, device, ib, response) - if ib.data.value == clusters.PumpConfigurationAndControl.types.PumpStatusBitmap.LOCAL_OVERRIDE then - device:set_field(IS_LOCAL_OVERRIDE, true, {persist = true}) - device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.localSetting()) - local supported_op_modes = {} - local supported_control_modes = {} - device:emit_event(pumpOperationMode.supportedOperationModes(supported_op_modes)) - device:emit_event(pumpControlMode.supportedControlModes(supported_control_modes)) - elseif ib.data.value == clusters.PumpConfigurationAndControl.types.PumpStatusBitmap.RUNNING then - device:set_field(IS_LOCAL_OVERRIDE, false, {persist = true}) - device:send(clusters.PumpConfigurationAndControl.attributes.EffectiveOperationMode:read(device)) - end -end - --- Capability Handlers -- -local function handle_switch_on(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - 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 = device:component_to_endpoint(cmd.component) - local req = clusters.OnOff.server.commands.Off(device, endpoint_id) - device:send(req) -end - -local function handle_set_level(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local level = math.floor(cmd.args.level / MAX_CAP_SWITCH_LEVEL * MAX_PUMP_ATTR_LEVEL) - local req = clusters.LevelControl.server.commands.MoveToLevelWithOnOff(device, endpoint_id, level, cmd.args.rate or 0, 0 ,0) - device:send(req) -end - -local function set_operation_mode(driver, device, cmd) - local mode_id = nil - for id, mode in pairs(PUMP_OPERATION_MODE_MAP) do - if mode.NAME == cmd.args.operationMode then - mode_id = id - break - end - end - if mode_id then - device:send(clusters.PumpConfigurationAndControl.attributes.OperationMode:write(device, device:component_to_endpoint(cmd.component), mode_id)) - end -end - -local function set_control_mode(driver, device, cmd) - local mode_id = nil - for id, mode in pairs(PUMP_CONTROL_MODE_MAP) do - if mode.NAME == cmd.args.controlMode then - mode_id = id - break - end - end - if mode_id then - device:send(clusters.PumpConfigurationAndControl.attributes.ControlMode:write(device, device:component_to_endpoint(cmd.component), mode_id)) - end -end - -local matter_driver_template = { - lifecycle_handlers = { - init = device_init, - doConfigure = do_configure, - infoChanged = info_changed, - }, - matter_handlers = { - attr = { - [clusters.OnOff.ID] = { - [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, - }, - [clusters.LevelControl.ID] = { - [clusters.LevelControl.attributes.CurrentLevel.ID] = level_attr_handler - }, - [clusters.PumpConfigurationAndControl.ID] = { - [clusters.PumpConfigurationAndControl.attributes.EffectiveOperationMode.ID] = effective_operation_mode_handler, - [clusters.PumpConfigurationAndControl.attributes.EffectiveControlMode.ID] = effective_control_mode_handler, - [clusters.PumpConfigurationAndControl.attributes.PumpStatus.ID] = pump_status_handler, - }, - }, - }, - 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.switchLevel.ID] = { - [capabilities.switchLevel.commands.setLevel.NAME] = handle_set_level, - }, - [capabilities.pumpOperationMode.ID] = { - [capabilities.pumpOperationMode.commands.setOperationMode.NAME] = set_operation_mode, - }, - [capabilities.pumpControlMode.ID] = { - [capabilities.pumpControlMode.commands.setControlMode.NAME] = set_control_mode, - }, - }, - supported_capabilities = { - capabilities.switch, - capabilities.switchLevel, - capabilities.pumpOperationMode, - capabilities.pumpControlMode, - }, -} - -local matter_driver = MatterDriver("matter-pump", 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() \ No newline at end of file +-- 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 log = require "log" +local clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "embedded-cluster-utils" +local MatterDriver = require "st.matter.driver" + +local IS_LOCAL_OVERRIDE = "__is_local_override" +-- Per matter spec, the pump level is in steps of 0.5% and the +-- max level value is 200. Anything above is considered 100% +local MAX_PUMP_ATTR_LEVEL = 200 +local MAX_CAP_SWITCH_LEVEL = 100 + +-- Include driver-side definitions when lua libs api version is < 10 +local version = require "version" +if version.api < 10 then + clusters.PumpConfigurationAndControl = require "PumpConfigurationAndControl" +end + +local pumpOperationMode = capabilities.pumpOperationMode +local pumpControlMode = capabilities.pumpControlMode + +local PUMP_OPERATION_MODE_MAP = { + [clusters.PumpConfigurationAndControl.types.OperationModeEnum.NORMAL] = pumpOperationMode.operationMode.normal, + [clusters.PumpConfigurationAndControl.types.OperationModeEnum.MINIMUM] = pumpOperationMode.operationMode.minimum, + [clusters.PumpConfigurationAndControl.types.OperationModeEnum.MAXIMUM] = pumpOperationMode.operationMode.maximum, + [clusters.PumpConfigurationAndControl.types.OperationModeEnum.LOCAL] = pumpOperationMode.operationMode.localSetting, +} + +local PUMP_CONTROL_MODE_MAP = { + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_SPEED] = pumpControlMode.controlMode.constantSpeed, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_PRESSURE] = pumpControlMode.controlMode.constantPressure, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.PROPORTIONAL_PRESSURE] = pumpControlMode.controlMode.proportionalPressure, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_FLOW] = pumpControlMode.controlMode.constantFlow, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_TEMPERATURE] = pumpControlMode.controlMode.constantTemperature, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.AUTOMATIC] = pumpControlMode.controlMode.automatic, +} + +local PUMP_CURRENT_CONTROL_MODE_MAP = { + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_SPEED] = pumpControlMode.currentControlMode.constantSpeed, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_PRESSURE] = pumpControlMode.currentControlMode.constantPressure, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.PROPORTIONAL_PRESSURE] = pumpControlMode.currentControlMode.proportionalPressure, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_FLOW] = pumpControlMode.currentControlMode.constantFlow, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.CONSTANT_TEMPERATURE] = pumpControlMode.currentControlMode.constantTemperature, + [clusters.PumpConfigurationAndControl.types.ControlModeEnum.AUTOMATIC] = pumpControlMode.currentControlMode.automatic, +} + +local subscribed_attributes = { + [capabilities.switch.ID] = { + clusters.OnOff.attributes.OnOff, + }, + [capabilities.switchLevel.ID] = { + clusters.LevelControl.attributes.CurrentLevel + }, + [capabilities.pumpOperationMode.ID]={ + clusters.PumpConfigurationAndControl.attributes.OperationMode, + clusters.PumpConfigurationAndControl.attributes.EffectiveOperationMode, + clusters.PumpConfigurationAndControl.attributes.PumpStatus, + }, + [capabilities.pumpControlMode.ID]={ + clusters.PumpConfigurationAndControl.attributes.EffectiveControlMode, + }, +} + +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) + -- Use the find_default_endpoint function to return the first endpoint that + -- supports a given cluster. + return find_default_endpoint(device, clusters.PumpConfigurationAndControl.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) + --Note this is needed because device:subscribe() does not recalculate + -- the subscribed attributes each time it is run, that only happens at init. + -- This will change in the 0.48.x release of the lua libs. + 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 set_supported_op_mode(driver, device) + local spd_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_SPEED}) + local local_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.LOCAL_OPERATION}) + local supported_op_modes = {pumpOperationMode.operationMode.normal.NAME} + if #spd_eps > 0 then + table.insert(supported_op_modes, pumpOperationMode.operationMode.minimum.NAME) + table.insert(supported_op_modes, pumpOperationMode.operationMode.maximum.NAME) + end + if #local_eps > 0 then + table.insert(supported_op_modes, pumpOperationMode.operationMode.localSetting.NAME) + end + device:emit_event(pumpOperationMode.supportedOperationModes(supported_op_modes)) +end + +local function set_supported_control_mode(driver, device) + local spd_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_SPEED}) + local prsconst_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_PRESSURE}) + local prscomp_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.COMPENSATED_PRESSURE}) + local flw_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_FLOW}) + local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.CONSTANT_TEMPERATURE}) + local auto_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID, {feature_bitmap = clusters.PumpConfigurationAndControl.types.Feature.AUTOMATIC}) + local supported_control_modes = {} + if #spd_eps > 0 then + table.insert(supported_control_modes, pumpControlMode.controlMode.constantSpeed.NAME) + end + if #prsconst_eps > 0 then + table.insert(supported_control_modes, pumpControlMode.controlMode.constantPressure.NAME) + end + if #prscomp_eps > 0 then + table.insert(supported_control_modes, pumpControlMode.controlMode.proportionalPressure.NAME) + end + if #flw_eps > 0 then + table.insert(supported_control_modes, pumpControlMode.controlMode.constantFlow.NAME) + end + if #temp_eps > 0 then + table.insert(supported_control_modes, pumpControlMode.controlMode.constantTemperature.NAME) + end + if #auto_eps > 0 then + table.insert(supported_control_modes, pumpControlMode.controlMode.automatic.NAME) + end + device:emit_event(pumpControlMode.supportedControlModes(supported_control_modes)) +end + +local function do_configure(driver, device) + local pump_eps = embedded_cluster_utils.get_endpoints(device, clusters.PumpConfigurationAndControl.ID) + local level_eps = embedded_cluster_utils.get_endpoints(device, clusters.LevelControl.ID) + local profile_name = "pump" + if #pump_eps == 1 then + if #level_eps > 0 then + profile_name = profile_name .. "-level" + else + profile_name = profile_name .. "-only" + end + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) + else + device.log.warn_with({hub_logs=true}, "Device does not support pump configuration and control cluster") + end + set_supported_op_mode(driver, device) + set_supported_control_mode(driver, device) +end + +-- Matter Handlers -- +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 level_attr_handler(driver, device, ib, response) + if ib.data.value ~= nil then + local level = math.floor((ib.data.value / MAX_PUMP_ATTR_LEVEL * MAX_CAP_SWITCH_LEVEL) + 0.5) + level = math.min(level, MAX_CAP_SWITCH_LEVEL) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switchLevel.level(level)) + end +end + +local function effective_operation_mode_handler(driver, device, ib, response) + local modeEnum = clusters.PumpConfigurationAndControl.types.OperationModeEnum + local supported_control_modes = {} + local local_override = device:get_field(IS_LOCAL_OVERRIDE) + if not local_override then + set_supported_op_mode(driver, device) + end + if ib.data.value == modeEnum.NORMAL then + device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.normal()) + set_supported_control_mode(driver, device) + elseif ib.data.value == modeEnum.MINIMUM then + device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.minimum()) + device:emit_event_for_endpoint(ib.endpoint_id, pumpControlMode.supportedControlModes(supported_control_modes)) + elseif ib.data.value == modeEnum.MAXIMUM then + device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.maximum()) + device:emit_event_for_endpoint(ib.endpoint_id, pumpControlMode.supportedControlModes(supported_control_modes)) + elseif ib.data.value == modeEnum.LOCAL then + device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.localSetting()) + device:emit_event_for_endpoint(ib.endpoint_id, pumpControlMode.supportedControlModes(supported_control_modes)) + end +end + +local function effective_control_mode_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib.endpoint_id, PUMP_CURRENT_CONTROL_MODE_MAP[ib.data.value]()) +end + +local function pump_status_handler(driver, device, ib, response) + if ib.data.value == clusters.PumpConfigurationAndControl.types.PumpStatusBitmap.LOCAL_OVERRIDE then + device:set_field(IS_LOCAL_OVERRIDE, true, {persist = true}) + device:emit_event_for_endpoint(ib.endpoint_id, pumpOperationMode.currentOperationMode.localSetting()) + local supported_op_modes = {} + local supported_control_modes = {} + device:emit_event(pumpOperationMode.supportedOperationModes(supported_op_modes)) + device:emit_event(pumpControlMode.supportedControlModes(supported_control_modes)) + elseif ib.data.value == clusters.PumpConfigurationAndControl.types.PumpStatusBitmap.RUNNING then + device:set_field(IS_LOCAL_OVERRIDE, false, {persist = true}) + device:send(clusters.PumpConfigurationAndControl.attributes.EffectiveOperationMode:read(device)) + end +end + +-- Capability Handlers -- +local function handle_switch_on(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + 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 = device:component_to_endpoint(cmd.component) + local req = clusters.OnOff.server.commands.Off(device, endpoint_id) + device:send(req) +end + +local function handle_set_level(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local level = math.floor(cmd.args.level / MAX_CAP_SWITCH_LEVEL * MAX_PUMP_ATTR_LEVEL) + local req = clusters.LevelControl.server.commands.MoveToLevelWithOnOff(device, endpoint_id, level, cmd.args.rate or 0, 0 ,0) + device:send(req) +end + +local function set_operation_mode(driver, device, cmd) + local mode_id = nil + for id, mode in pairs(PUMP_OPERATION_MODE_MAP) do + if mode.NAME == cmd.args.operationMode then + mode_id = id + break + end + end + if mode_id then + device:send(clusters.PumpConfigurationAndControl.attributes.OperationMode:write(device, device:component_to_endpoint(cmd.component), mode_id)) + end +end + +local function set_control_mode(driver, device, cmd) + local mode_id = nil + for id, mode in pairs(PUMP_CONTROL_MODE_MAP) do + if mode.NAME == cmd.args.controlMode then + mode_id = id + break + end + end + if mode_id then + device:send(clusters.PumpConfigurationAndControl.attributes.ControlMode:write(device, device:component_to_endpoint(cmd.component), mode_id)) + end +end + +local matter_driver_template = { + lifecycle_handlers = { + init = device_init, + doConfigure = do_configure, + infoChanged = info_changed, + }, + matter_handlers = { + attr = { + [clusters.OnOff.ID] = { + [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, + }, + [clusters.LevelControl.ID] = { + [clusters.LevelControl.attributes.CurrentLevel.ID] = level_attr_handler + }, + [clusters.PumpConfigurationAndControl.ID] = { + [clusters.PumpConfigurationAndControl.attributes.EffectiveOperationMode.ID] = effective_operation_mode_handler, + [clusters.PumpConfigurationAndControl.attributes.EffectiveControlMode.ID] = effective_control_mode_handler, + [clusters.PumpConfigurationAndControl.attributes.PumpStatus.ID] = pump_status_handler, + }, + }, + }, + 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.switchLevel.ID] = { + [capabilities.switchLevel.commands.setLevel.NAME] = handle_set_level, + }, + [capabilities.pumpOperationMode.ID] = { + [capabilities.pumpOperationMode.commands.setOperationMode.NAME] = set_operation_mode, + }, + [capabilities.pumpControlMode.ID] = { + [capabilities.pumpControlMode.commands.setControlMode.NAME] = set_control_mode, + }, + }, + supported_capabilities = { + capabilities.switch, + capabilities.switchLevel, + capabilities.pumpOperationMode, + capabilities.pumpControlMode, + }, + shared_device_thread_enabled = true, +} + +local matter_driver = MatterDriver("matter-pump", 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-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index e3b483cad5..483b92fc21 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -350,3 +350,8 @@ matterGeneric: deviceTypes: - id: 0x0306 # Flow Sensor deviceProfileName: flow-battery + - id: "matter/soil/sensor" + deviceLabel: Matter Soil Sensor + deviceTypes: + - id: 0x0045 # Soil Sensor + deviceProfileName: soil-sensor-battery diff --git a/drivers/SmartThings/matter-sensor/profiles/soil-sensor-battery.yml b/drivers/SmartThings/matter-sensor/profiles/soil-sensor-battery.yml new file mode 100644 index 0000000000..07c9162e4d --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/soil-sensor-battery.yml @@ -0,0 +1,17 @@ +name: soil-sensor-battery +components: +- id: main + capabilities: + - id: relativeHumidityMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: HumiditySensor +preferences: + - preferenceId: humidityOffset + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/soil-sensor-batteryLevel.yml b/drivers/SmartThings/matter-sensor/profiles/soil-sensor-batteryLevel.yml new file mode 100644 index 0000000000..cfda9b9e95 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/soil-sensor-batteryLevel.yml @@ -0,0 +1,17 @@ +name: soil-sensor-batteryLevel +components: +- id: main + capabilities: + - id: relativeHumidityMeasurement + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: HumiditySensor +preferences: + - preferenceId: humidityOffset + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/soil-sensor.yml b/drivers/SmartThings/matter-sensor/profiles/soil-sensor.yml new file mode 100644 index 0000000000..280edc80c4 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/soil-sensor.yml @@ -0,0 +1,15 @@ +name: soil-sensor +components: +- id: main + capabilities: + - id: relativeHumidityMeasurement + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: HumiditySensor +preferences: + - preferenceId: humidityOffset + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor-battery.yml b/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor-battery.yml new file mode 100644 index 0000000000..73db53e7c1 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor-battery.yml @@ -0,0 +1,21 @@ +name: temperature-soil-sensor-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: HumiditySensor +preferences: + - preferenceId: tempOffset + explicit: true + - preferenceId: humidityOffset + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor-batteryLevel.yml b/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor-batteryLevel.yml new file mode 100644 index 0000000000..55ccc8fb25 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor-batteryLevel.yml @@ -0,0 +1,21 @@ +name: temperature-soil-sensor-batteryLevel +components: +- id: main + capabilities: + - id: relativeHumidityMeasurement + version: 1 + - id: temperatureMeasurement + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: HumiditySensor +preferences: + - preferenceId: tempOffset + explicit: true + - preferenceId: humidityOffset + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor.yml b/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor.yml new file mode 100644 index 0000000000..bc327f01b0 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/temperature-soil-sensor.yml @@ -0,0 +1,19 @@ +name: temperature-soil-sensor +components: +- id: main + capabilities: + - id: relativeHumidityMeasurement + version: 1 + - id: temperatureMeasurement + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: HumiditySensor +preferences: + - preferenceId: tempOffset + explicit: true + - preferenceId: humidityOffset + explicit: true diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/init.lua new file mode 100644 index 0000000000..98c23abef4 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/init.lua @@ -0,0 +1,5 @@ +local GlobalTypes = require "embedded_clusters.Global.types" + +local Global = {} +Global.types = GlobalTypes +return Global diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/LevelValueEnum.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/LevelValueEnum.lua new file mode 100644 index 0000000000..ed8b969b49 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/LevelValueEnum.lua @@ -0,0 +1,36 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local LevelValueEnum = {} +local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.UNKNOWN] = "UNKNOWN", + [self.LOW] = "LOW", + [self.MEDIUM] = "MEDIUM", + [self.HIGH] = "HIGH", + [self.CRITICAL] = "CRITICAL", + } + 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.UNKNOWN = 0x00 +new_mt.__index.LOW = 0x01 +new_mt.__index.MEDIUM = 0x02 +new_mt.__index.HIGH = 0x03 +new_mt.__index.CRITICAL = 0x04 + +LevelValueEnum.UNKNOWN = 0x00 +LevelValueEnum.LOW = 0x01 +LevelValueEnum.MEDIUM = 0x02 +LevelValueEnum.HIGH = 0x03 +LevelValueEnum.CRITICAL = 0x04 + +LevelValueEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(LevelValueEnum, new_mt) + +return LevelValueEnum diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementAccuracyRangeStruct.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementAccuracyRangeStruct.lua new file mode 100644 index 0000000000..b298a66961 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementAccuracyRangeStruct.lua @@ -0,0 +1,119 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local MeasurementAccuracyRangeStruct = {} +local new_mt = StructureABC.new_mt({NAME = "MeasurementAccuracyRangeStruct", ID = data_types.name_to_id_map["Structure"]}) + +MeasurementAccuracyRangeStruct.field_defs = { + { + name = "range_min", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Int64", + }, + { + name = "range_max", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Int64", + }, + { + name = "percent_max", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "percent_min", + field_id = 3, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "percent_typical", + field_id = 4, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "fixed_max", + field_id = 5, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint64", + }, + { + name = "fixed_min", + field_id = 6, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint64", + }, + { + name = "fixed_typical", + field_id = 7, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint64", + }, +} + +MeasurementAccuracyRangeStruct.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 + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + elseif not (field_def.is_optional and tbl[field_def.name] == nil) then + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +MeasurementAccuracyRangeStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = MeasurementAccuracyRangeStruct.init +new_mt.__index.serialize = MeasurementAccuracyRangeStruct.serialize + +MeasurementAccuracyRangeStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(MeasurementAccuracyRangeStruct, new_mt) + +return MeasurementAccuracyRangeStruct diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementAccuracyStruct.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementAccuracyStruct.lua new file mode 100644 index 0000000000..4da8857164 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementAccuracyStruct.lua @@ -0,0 +1,99 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local MeasurementAccuracyStruct = {} +local new_mt = StructureABC.new_mt({NAME = "MeasurementAccuracyStruct", ID = data_types.name_to_id_map["Structure"]}) + +MeasurementAccuracyStruct.field_defs = { + { + name = "measurement_type", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "embedded_clusters.Global.types.MeasurementTypeEnum", + }, + { + name = "measured", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Boolean", + }, + { + name = "min_measured_value", + field_id = 2, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Int64", + }, + { + name = "max_measured_value", + field_id = 3, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Int64", + }, + { + name = "accuracy_ranges", + field_id = 4, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Array", + element_type = require "embedded_clusters.Global.types.MeasurementAccuracyRangeStruct", + }, +} + +MeasurementAccuracyStruct.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 + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + elseif not (field_def.is_optional and tbl[field_def.name] == nil) then + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +MeasurementAccuracyStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = MeasurementAccuracyStruct.init +new_mt.__index.serialize = MeasurementAccuracyStruct.serialize + +MeasurementAccuracyStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(MeasurementAccuracyStruct, new_mt) + +return MeasurementAccuracyStruct diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementTypeEnum.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementTypeEnum.lua new file mode 100644 index 0000000000..2e749ebacd --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementTypeEnum.lua @@ -0,0 +1,75 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local MeasurementTypeEnum = {} +local new_mt = UintABC.new_mt({NAME = "MeasurementTypeEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.UNSPECIFIED] = "UNSPECIFIED", + [self.VOLTAGE] = "VOLTAGE", + [self.ACTIVE_CURRENT] = "ACTIVE_CURRENT", + [self.REACTIVE_CURRENT] = "REACTIVE_CURRENT", + [self.APPARENT_CURRENT] = "APPARENT_CURRENT", + [self.ACTIVE_POWER] = "ACTIVE_POWER", + [self.REACTIVE_POWER] = "REACTIVE_POWER", + [self.APPARENT_POWER] = "APPARENT_POWER", + [self.RMS_VOLTAGE] = "RMS_VOLTAGE", + [self.RMS_CURRENT] = "RMS_CURRENT", + [self.RMS_POWER] = "RMS_POWER", + [self.FREQUENCY] = "FREQUENCY", + [self.POWER_FACTOR] = "POWER_FACTOR", + [self.NEUTRAL_CURRENT] = "NEUTRAL_CURRENT", + [self.ELECTRICAL_ENERGY] = "ELECTRICAL_ENERGY", + [self.REACTIVE_ENERGY] = "REACTIVE_ENERGY", + [self.APPARENT_ENERGY] = "APPARENT_ENERGY", + [self.SOIL_MOISTURE] = "SOIL_MOISTURE", + } + 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.UNSPECIFIED = 0x00 +new_mt.__index.VOLTAGE = 0x01 +new_mt.__index.ACTIVE_CURRENT = 0x02 +new_mt.__index.REACTIVE_CURRENT = 0x03 +new_mt.__index.APPARENT_CURRENT = 0x04 +new_mt.__index.ACTIVE_POWER = 0x05 +new_mt.__index.REACTIVE_POWER = 0x06 +new_mt.__index.APPARENT_POWER = 0x07 +new_mt.__index.RMS_VOLTAGE = 0x08 +new_mt.__index.RMS_CURRENT = 0x09 +new_mt.__index.RMS_POWER = 0x0A +new_mt.__index.FREQUENCY = 0x0B +new_mt.__index.POWER_FACTOR = 0x0C +new_mt.__index.NEUTRAL_CURRENT = 0x0D +new_mt.__index.ELECTRICAL_ENERGY = 0x0E +new_mt.__index.REACTIVE_ENERGY = 0x0F +new_mt.__index.APPARENT_ENERGY = 0x10 +new_mt.__index.SOIL_MOISTURE = 0x11 + +MeasurementTypeEnum.UNSPECIFIED = 0x00 +MeasurementTypeEnum.VOLTAGE = 0x01 +MeasurementTypeEnum.ACTIVE_CURRENT = 0x02 +MeasurementTypeEnum.REACTIVE_CURRENT = 0x03 +MeasurementTypeEnum.APPARENT_CURRENT = 0x04 +MeasurementTypeEnum.ACTIVE_POWER = 0x05 +MeasurementTypeEnum.REACTIVE_POWER = 0x06 +MeasurementTypeEnum.APPARENT_POWER = 0x07 +MeasurementTypeEnum.RMS_VOLTAGE = 0x08 +MeasurementTypeEnum.RMS_CURRENT = 0x09 +MeasurementTypeEnum.RMS_POWER = 0x0A +MeasurementTypeEnum.FREQUENCY = 0x0B +MeasurementTypeEnum.POWER_FACTOR = 0x0C +MeasurementTypeEnum.NEUTRAL_CURRENT = 0x0D +MeasurementTypeEnum.ELECTRICAL_ENERGY = 0x0E +MeasurementTypeEnum.REACTIVE_ENERGY = 0x0F +MeasurementTypeEnum.APPARENT_ENERGY = 0x10 +MeasurementTypeEnum.SOIL_MOISTURE = 0x11 + +MeasurementTypeEnum.augment_type = function(_cls, val) + setmetatable(val, new_mt) +end + +setmetatable(MeasurementTypeEnum, new_mt) + +return MeasurementTypeEnum diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/init.lua new file mode 100644 index 0000000000..984c5e02d8 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/init.lua @@ -0,0 +1,14 @@ +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("embedded_clusters.Global.types." .. key) + end + return types_mt.__types_cache[key] +end + +local GlobalTypes = {} + +setmetatable(GlobalTypes, types_mt) + +return GlobalTypes diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/init.lua new file mode 100644 index 0000000000..a7db91daea --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/init.lua @@ -0,0 +1,46 @@ +local cluster_base = require "st.matter.cluster_base" +local SoilMeasurementServerAttributes = require "embedded_clusters.SoilMeasurement.server.attributes" + +local SoilMeasurement = {} + +SoilMeasurement.ID = 0x0430 +SoilMeasurement.NAME = "SoilMeasurement" +SoilMeasurement.server = {} +SoilMeasurement.client = {} +SoilMeasurement.server.attributes = SoilMeasurementServerAttributes:set_parent_cluster(SoilMeasurement) + +function SoilMeasurement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "SoilMoistureMeasurementLimits", + [0x0001] = "SoilMoistureMeasuredValue", + [0xFFF9] = "AcceptedCommandList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +SoilMeasurement.attribute_direction_map = { + ["SoilMoistureMeasurementLimits"] = "server", + ["SoilMoistureMeasuredValue"] = "server", + ["AcceptedCommandList"] = "server", + ["AttributeList"] = "server", +} + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = SoilMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, SoilMeasurement.NAME)) + end + return SoilMeasurement[direction].attributes[key] +end +SoilMeasurement.attributes = {} +setmetatable(SoilMeasurement.attributes, attribute_helper_mt) + +setmetatable(SoilMeasurement, {__index = cluster_base}) + +return SoilMeasurement diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/SoilMoistureMeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/SoilMoistureMeasuredValue.lua new file mode 100644 index 0000000000..5df92ec62a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/SoilMoistureMeasuredValue.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SoilMoistureMeasuredValue = { + ID = 0x0001, + NAME = "SoilMoistureMeasuredValue", + base_type = require "st.matter.data_types.Uint8", +} + +function SoilMoistureMeasuredValue:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function SoilMoistureMeasuredValue:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function SoilMoistureMeasuredValue:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function SoilMoistureMeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SoilMoistureMeasuredValue: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 SoilMoistureMeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(SoilMoistureMeasuredValue, {__call = SoilMoistureMeasuredValue.new_value, __index = SoilMoistureMeasuredValue.base_type}) +return SoilMoistureMeasuredValue diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/SoilMoistureMeasurementLimits.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/SoilMoistureMeasurementLimits.lua new file mode 100644 index 0000000000..6fbf1a8c8a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/SoilMoistureMeasurementLimits.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SoilMoistureMeasurementLimits = { + ID = 0x0000, + NAME = "SoilMoistureMeasurementLimits", + base_type = require "embedded_clusters.Global.types.MeasurementAccuracyStruct", +} + +function SoilMoistureMeasurementLimits:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function SoilMoistureMeasurementLimits:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function SoilMoistureMeasurementLimits:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function SoilMoistureMeasurementLimits:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SoilMoistureMeasurementLimits: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 SoilMoistureMeasurementLimits:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SoilMoistureMeasurementLimits, {__call = SoilMoistureMeasurementLimits.new_value, __index = SoilMoistureMeasurementLimits.base_type}) +return SoilMoistureMeasurementLimits diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..3741efd72a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SoilMeasurement/server/attributes/init.lua @@ -0,0 +1,19 @@ +local attr_mt = {} +attr_mt.__index = function(self, key) + local req_loc = string.format("embedded_clusters.SoilMeasurement.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 SoilMeasurementServerAttributes = {} + +function SoilMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(SoilMeasurementServerAttributes, attr_mt) + +return SoilMeasurementServerAttributes diff --git a/drivers/SmartThings/matter-sensor/src/init.lua b/drivers/SmartThings/matter-sensor/src/init.lua index 73e34fcd56..6dbd6d9811 100644 --- a/drivers/SmartThings/matter-sensor/src/init.lua +++ b/drivers/SmartThings/matter-sensor/src/init.lua @@ -37,6 +37,12 @@ if version.api < 11 then clusters.BooleanStateConfiguration = require "embedded_clusters.BooleanStateConfiguration" end +-- Include driver-side definitions when lua libs api version is < 21 +-- TODO: change this to < 20 once the lua libs have been updated for hub-core 61 +if version.api < 21 then + clusters.SoilMeasurement = require "embedded_clusters.SoilMeasurement" +end + local SensorLifecycleHandlers = {} function SensorLifecycleHandlers.do_configure(driver, device) @@ -117,6 +123,10 @@ local matter_driver_template = { [clusters.RelativeHumidityMeasurement.ID] = { [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.humidity_measured_value_handler }, + [clusters.SoilMeasurement.ID] = { + [clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue.ID] = attribute_handlers.soil_moisture_measured_value_handler, + [clusters.SoilMeasurement.attributes.SoilMoistureMeasurementLimits.ID] = attribute_handlers.soil_moisture_measurement_limits_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), @@ -164,7 +174,9 @@ local matter_driver_template = { clusters.BooleanState.attributes.StateValue, }, [capabilities.relativeHumidityMeasurement.ID] = { - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, + clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue, + clusters.SoilMeasurement.attributes.SoilMoistureMeasurementLimits }, [capabilities.temperatureAlarm.ID] = { clusters.BooleanState.attributes.StateValue, @@ -243,9 +255,6 @@ local matter_driver_template = { clusters.RadonConcentrationMeasurement.attributes.MeasuredValue, clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.relativeHumidityMeasurement.ID] = { - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue - }, [capabilities.tvocHealthConcern.ID] = { clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue }, @@ -296,6 +305,7 @@ local matter_driver_template = { capabilities.flowMeasurement, }, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } local matter_driver = MatterDriver("matter-sensor", matter_driver_template) diff --git a/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua index 9bba480855..a81a03b4a6 100644 --- a/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua @@ -9,6 +9,10 @@ local fields = require "sensor_utils.fields" local device_cfg = require "sensor_utils.device_configuration" local version = require "version" +if version.api < 13 then + clusters.Global = require "embedded_clusters.Global" +end + local AttributeHandlers = {} @@ -69,16 +73,39 @@ function AttributeHandlers.humidity_measured_value_handler(driver, device, ib, r end +-- [[ SOIL MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.soil_moisture_measured_value_handler(driver, device, ib, response) + if ib.data.value == nil then return end + local min = sensor_utils.get_field_for_endpoint(device, fields.SOIL_LIMIT_MIN, ib.endpoint_id) or sensor_utils.SOIL_MOISTURE_MIN + local max = sensor_utils.get_field_for_endpoint(device, fields.SOIL_LIMIT_MAX, ib.endpoint_id) or sensor_utils.SOIL_MOISTURE_MAX + local soil_moisture = st_utils.clamp_value(ib.data.value, min, max) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(soil_moisture)) +end + +function AttributeHandlers.soil_moisture_measurement_limits_handler(driver, device, ib, response) + local MeasurementAccuracyStruct = require "embedded_clusters.Global.types.MeasurementAccuracyStruct" + MeasurementAccuracyStruct:augment_type(ib.data) + local min_val = ib.data.elements and ib.data.elements.min_measured_value and ib.data.elements.min_measured_value.value + local max_val = ib.data.elements and ib.data.elements.max_measured_value and ib.data.elements.max_measured_value.value + if not (min_val and max_val) or (min_val >= max_val) or (min_val < sensor_utils.SOIL_MOISTURE_MIN) or (max_val > sensor_utils.SOIL_MOISTURE_MAX) then + device.log.warn_with({hub_logs = true}, string.format("Device reported invalid soil moisture limits: min=%d, max=%d", min_val, max_val)) + end + sensor_utils.set_field_for_endpoint(device, fields.SOIL_LIMIT_MIN, ib.endpoint_id, min_val) + sensor_utils.set_field_for_endpoint(device, fields.SOIL_LIMIT_MAX, ib.endpoint_id, max_val) +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 + 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]) diff --git a/drivers/SmartThings/matter-sensor/src/sensor_utils/device_configuration.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/device_configuration.lua index 2c5f38524a..4690a3e356 100644 --- a/drivers/SmartThings/matter-sensor/src/sensor_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/device_configuration.lua @@ -11,18 +11,22 @@ if version.api < 11 then clusters.BooleanStateConfiguration = require "embedded_clusters.BooleanStateConfiguration" end +if version.api < 21 then + clusters.SoilMeasurement = require "embedded_clusters.SoilMeasurement" +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 + 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 @@ -53,12 +57,18 @@ function DeviceConfiguration.match_profile(driver, device, battery_supported) profile_name = profile_name .. "-illuminance" end - if device:supports_capability(capabilities.temperatureMeasurement) then + if device:supports_capability(capabilities.temperatureMeasurement) or + #device:get_endpoints(clusters.TemperatureMeasurement.ID) > 0 then profile_name = profile_name .. "-temperature" end if device:supports_capability(capabilities.relativeHumidityMeasurement) then - profile_name = profile_name .. "-humidity" + if #embedded_cluster_utils.get_endpoints(device, clusters.SoilMeasurement.ID) > 0 then + -- TODO: Update soil sensor profiles to use the SoilSensor category once it is available. + profile_name = profile_name .. "-soil-sensor" + else + profile_name = profile_name .. "-humidity" + end end if device:supports_capability(capabilities.atmosphericPressureMeasurement) then @@ -107,7 +117,7 @@ function DeviceConfiguration.match_profile(driver, device, battery_supported) 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 + #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.VISION}) > 0 then occupancy_support = "-presence" end end @@ -117,8 +127,7 @@ function DeviceConfiguration.match_profile(driver, device, battery_supported) -- 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 +return DeviceConfiguration diff --git a/drivers/SmartThings/matter-sensor/src/sensor_utils/embedded_cluster_utils.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/embedded_cluster_utils.lua index e2384098d9..37623d57d2 100644 --- a/drivers/SmartThings/matter-sensor/src/sensor_utils/embedded_cluster_utils.lua +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/embedded_cluster_utils.lua @@ -25,6 +25,10 @@ if version.api < 11 then clusters.BooleanStateConfiguration = require "embedded_clusters.BooleanStateConfiguration" end +if version.api < 21 then + clusters.SoilMeasurement = require "embedded_clusters.SoilMeasurement" +end + local embedded_cluster_utils = {} local embedded_clusters_api_10 = { @@ -46,38 +50,44 @@ local embedded_clusters_api_11 = { [clusters.BooleanStateConfiguration.ID] = clusters.BooleanStateConfiguration } +local embedded_clusters_api_21 = { + [clusters.SoilMeasurement.ID] = clusters.SoilMeasurement +} + 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 < 10 and embedded_clusters_api_10[cluster_id] ~= nil or - version.api < 11 and embedded_clusters_api_11[cluster_id] ~= nil then - local embedded_cluster = embedded_clusters_api_10[cluster_id] or embedded_clusters_api_11[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 + -- 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 < 10 and embedded_clusters_api_10[cluster_id] ~= nil or + version.api < 11 and embedded_clusters_api_11[cluster_id] ~= nil or + version.api < 21 and embedded_clusters_api_21[cluster_id] ~= nil then + local embedded_cluster = embedded_clusters_api_10[cluster_id] or embedded_clusters_api_11[cluster_id] or + embedded_clusters_api_21[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 - return eps - else - return device:get_endpoints(cluster_id, opts) end + return eps + else + return device:get_endpoints(cluster_id, opts) end +end - return embedded_cluster_utils \ No newline at end of file +return 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 index f0b2a5c7a6..b31b1b5c5b 100644 --- a/drivers/SmartThings/matter-sensor/src/sensor_utils/fields.lua +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/fields.lua @@ -11,6 +11,8 @@ SensorFields.TEMP_MAX = "__temp_max" SensorFields.FLOW_BOUND_RECEIVED = "__flow_bound_received" SensorFields.FLOW_MIN = "__flow_min" SensorFields.FLOW_MAX = "__flow_max" +SensorFields.SOIL_LIMIT_MIN = "__soil_limit_min" +SensorFields.SOIL_LIMIT_MAX = "__soil_limit_max" SensorFields.battery_support = { NO_BATTERY = "NO_BATTERY", diff --git a/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua index 5a0421fb0c..d5437410a5 100644 --- a/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua @@ -3,6 +3,10 @@ local utils = {} +-- Sanity check bounds for soil moisture measurement limits (percent) +utils.SOIL_MOISTURE_MIN = 0 +utils.SOIL_MOISTURE_MAX = 100 + function utils.get_field_for_endpoint(device, field, endpoint) return device:get_field(string.format("%s_%d", field, endpoint)) end diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_soil_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_soil_sensor.lua new file mode 100644 index 0000000000..0c845fa47e --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_soil_sensor.lua @@ -0,0 +1,221 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local test = require "integration_test" +local version = require "version" + +clusters.Global = require "embedded_clusters.Global" + +if version.api < 21 then + clusters.SoilMeasurement = require "embedded_clusters.SoilMeasurement" +end + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("temperature-soil-sensor.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.SoilMeasurement.ID, cluster_type = "SERVER" }, + { cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0045, device_type_revision = 1 } -- Soil Sensor + } + }, + } +}) + +local subscribe_request + +local cluster_subscribe_list = { + clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue, + clusters.SoilMeasurement.attributes.SoilMoistureMeasurementLimits, + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue +} + +local function test_init() + 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.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" }) + 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 = "temperature-soil-sensor" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Test infoChanged lifecycle event", + function() + local updated_device_profile = t_utils.get_profile_definition("temperature-soil-sensor.yml") + 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.register_coroutine_test( + "Relative humidity reports should generate correct messages", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.RelativeHumidityMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 4049) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 40 })) + ) + + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.RelativeHumidityMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 4050) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 41 })) + ) + end +) + +test.register_coroutine_test( + "Temperature reports should generate correct messages", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.TemperatureMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 40*100) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 40.0, unit = "C" })) + ) + end +) + +test.register_coroutine_test( + "Min and max temperature attributes set capability constraint", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue:build_test_report_data(mock_device, 1, 500) + } + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue:build_test_report_data(mock_device, 1, 4000) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = 5.00, maximum = 40.00 }, unit = "C" }) + ) + ) + end +) + +test.register_coroutine_test( + "Soil moisture is reported raw when no limits are set", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue:build_test_report_data(mock_device, 1, 55) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 55 })) + ) + end +) + +local function build_soil_moisture_limits(min_value, max_value) + return clusters.Global.types.MeasurementAccuracyStruct({ + measurement_type = clusters.Global.types.MeasurementTypeEnum.SOIL_MOISTURE, + measured = true, + min_measured_value = min_value, + max_measured_value = max_value, + accuracy_ranges = {clusters.Global.types.MeasurementAccuracyRangeStruct({range_min = min_value, range_max = max_value})} + }) +end + +test.register_coroutine_test( + "Soil moisture is scaled 0-100% when min and max limits are set", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.SoilMeasurement.attributes.SoilMoistureMeasurementLimits:build_test_report_data(mock_device, 1, build_soil_moisture_limits(0, 50)) + } + ) + -- Receive a measured value of 25, which is 50% when scaled between 0 and 50 + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue:build_test_report_data(mock_device, 1, 25) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 25 })) + ) + end +) + +test.register_coroutine_test( + "Soil moisture scaling rounds correctly", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.SoilMeasurement.attributes.SoilMoistureMeasurementLimits:build_test_report_data(mock_device, 1, build_soil_moisture_limits(10, 90)) + } + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue:build_test_report_data(mock_device, 1, 10) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 10 })) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.SoilMeasurement.attributes.SoilMoistureMeasuredValue:build_test_report_data(mock_device, 1, 90) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 90 })) + ) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 06070f0484..b73ac0e98f 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -155,7 +155,7 @@ matterManufacturer: deviceLabel: OREIN Bath Fan OLO5S vendorId: 0x1396 productId: 0x1001 - deviceProfileName: light-color-level-fan + deviceProfileName: fan-modular - id: "5014/4214" deviceLabel: Linkind Smart Light Bulb vendorId: 0x1396 @@ -4165,6 +4165,11 @@ matterGeneric: deviceTypes: - id: 0x0110 # Mounted Dimmable Load Control deviceProfileName: switch-level + - id: "matter/irrigation-system" + deviceLabel: Matter Irrigation System + deviceTypes: + - id: 0x0040 # Irrigation System + deviceProfileName: irrigation-system - id: "matter/water-valve" deviceLabel: Matter Water Valve deviceTypes: diff --git a/drivers/SmartThings/matter-switch/profiles/irrigation-system.yml b/drivers/SmartThings/matter-switch/profiles/irrigation-system.yml new file mode 100644 index 0000000000..15896fbfee --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/irrigation-system.yml @@ -0,0 +1,26 @@ +name: irrigation-system +components: +- id: main + capabilities: + - id: valve + version: 1 + - id: level + version: 1 + config: + values: + - key: "level.value" + range: [0, 100] + optional: true + - id: flowMeasurement + version: 1 + optional: true + - id: operationalState + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Irrigation + 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 2f91bcb04e..1b1129e8be 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-fan.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-fan.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-color-level-fan components: - id: main diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index b1c6a8df8b..d560013a17 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -8,9 +8,9 @@ local clusters = require "st.matter.clusters" local log = require "log" local version = require "version" local cfg = require "switch_utils.device_configuration" +local button_cfg = cfg.ButtonCfg local device_cfg = cfg.DeviceCfg local switch_cfg = cfg.SwitchCfg -local button_cfg = cfg.ButtonCfg local fields = require "switch_utils.fields" local switch_utils = require "switch_utils.utils" local attribute_handlers = require "switch_handlers.attribute_handlers" @@ -47,6 +47,7 @@ function SwitchLifecycleHandlers.do_configure(driver, device) switch_cfg.set_device_control_options(device) device_cfg.match_profile(driver, device) elseif device.network_type == device_lib.NETWORK_TYPE_CHILD then + device_cfg.match_child_profile(driver, device) -- 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() @@ -148,6 +149,11 @@ local matter_driver_template = { [clusters.FanControl.attributes.FanModeSequence.ID] = attribute_handlers.fan_mode_sequence_handler, [clusters.FanControl.attributes.PercentCurrent.ID] = attribute_handlers.percent_current_handler }, + [clusters.FlowMeasurement.ID] = { + [clusters.FlowMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.flow_attr_handler, + [clusters.FlowMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.flow_attr_handler_factory(fields.FLOW_MIN), + [clusters.FlowMeasurement.attributes.MaxMeasuredValue.ID] = attribute_handlers.flow_attr_handler_factory(fields.FLOW_MAX) + }, [clusters.IlluminanceMeasurement.ID] = { [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.illuminance_measured_value_handler }, @@ -159,6 +165,11 @@ local matter_driver_template = { [clusters.OccupancySensing.ID] = { [clusters.OccupancySensing.attributes.Occupancy.ID] = attribute_handlers.occupancy_handler, }, + [clusters.OperationalState.ID] = { + [clusters.OperationalState.attributes.AcceptedCommandList.ID] = attribute_handlers.operational_state_accepted_command_list_attr_handler, + [clusters.OperationalState.attributes.OperationalState.ID] = attribute_handlers.operational_state_attr_handler, + [clusters.OperationalState.attributes.OperationalError.ID] = attribute_handlers.operational_error_attr_handler + }, [clusters.OnOff.ID] = { [clusters.OnOff.attributes.OnOff.ID] = attribute_handlers.on_off_attr_handler, }, @@ -226,17 +237,24 @@ local matter_driver_template = { [capabilities.fanSpeedPercent.ID] = { clusters.FanControl.attributes.PercentCurrent }, + [capabilities.flowMeasurement.ID] = { + clusters.FlowMeasurement.attributes.MeasuredValue, + clusters.FlowMeasurement.attributes.MinMeasuredValue, + clusters.FlowMeasurement.attributes.MaxMeasuredValue + }, [capabilities.illuminanceMeasurement.ID] = { clusters.IlluminanceMeasurement.attributes.MeasuredValue }, - [capabilities.motionSensor.ID] = { - clusters.OccupancySensing.attributes.Occupancy - }, [capabilities.level.ID] = { clusters.ValveConfigurationAndControl.attributes.CurrentLevel }, - [capabilities.switch.ID] = { - clusters.OnOff.attributes.OnOff + [capabilities.motionSensor.ID] = { + clusters.OccupancySensing.attributes.Occupancy + }, + [capabilities.operationalState.ID] = { + clusters.OperationalState.attributes.AcceptedCommandList, + clusters.OperationalState.attributes.OperationalState, + clusters.OperationalState.attributes.OperationalError }, [capabilities.powerMeter.ID] = { clusters.ElectricalPowerMeasurement.attributes.ActivePower @@ -244,6 +262,9 @@ local matter_driver_template = { [capabilities.relativeHumidityMeasurement.ID] = { clusters.RelativeHumidityMeasurement.attributes.MeasuredValue }, + [capabilities.switch.ID] = { + clusters.OnOff.attributes.OnOff + }, [capabilities.switchLevel.ID] = { clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, @@ -287,6 +308,10 @@ local matter_driver_template = { [capabilities.level.ID] = { [capabilities.level.commands.setLevel.NAME] = capability_handlers.handle_set_level }, + [capabilities.operationalState.ID] = { + [capabilities.operationalState.commands.pause.NAME] = capability_handlers.handle_operational_state_pause, + [capabilities.operationalState.commands.resume.NAME] = capability_handlers.handle_operational_state_resume + }, [capabilities.statelessColorTemperatureStep.ID] = { [capabilities.statelessColorTemperatureStep.commands.stepColorTemperatureByPercent.NAME] = capability_handlers.handle_step_color_temperature_by_percent, }, @@ -319,6 +344,7 @@ local matter_driver_template = { capabilities.energyMeter, capabilities.fanMode, capabilities.fanSpeedPercent, + capabilities.flowMeasurement, capabilities.hdr, capabilities.illuminanceMeasurement, capabilities.imageControl, @@ -327,6 +353,7 @@ local matter_driver_template = { capabilities.mechanicalPanTiltZoom, capabilities.motionSensor, capabilities.nightVision, + capabilities.operationalState, capabilities.powerMeter, capabilities.powerConsumptionReport, capabilities.relativeHumidityMeasurement, @@ -345,7 +372,8 @@ local matter_driver_template = { 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") - } + }, + shared_device_thread_enabled = true, } local matter_driver = MatterDriver("matter-switch", matter_driver_template) 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 20bd92881e..0fdc9cc822 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -550,4 +550,79 @@ function AttributeHandlers.percent_current_handler(driver, device, ib, response) device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(ib.data.value)) end + +-- [[ OPERATIONAL STATE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.operational_state_accepted_command_list_attr_handler(driver, device, ib, response) + if ib.data.elements == nil then return end + local accepted_command_list = {} + for _, accepted_command in ipairs(ib.data.elements) do + local accepted_command_id = accepted_command.value + if fields.operational_state_command_map[accepted_command_id] ~= nil then + table.insert(accepted_command_list, fields.operational_state_command_map[accepted_command_id]) + end + end + local event = capabilities.operationalState.supportedCommands(accepted_command_list, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.operational_state_attr_handler(driver, device, ib, response) + if ib.data.value == clusters.OperationalState.types.OperationalStateEnum.STOPPED then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.stopped()) + elseif ib.data.value == clusters.OperationalState.types.OperationalStateEnum.RUNNING then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.running()) + elseif ib.data.value == clusters.OperationalState.types.OperationalStateEnum.PAUSED then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.paused()) + end +end + +function AttributeHandlers.operational_error_attr_handler(driver, device, ib, response) + if ib.data.elements == nil or ib.data.elements.error_state_id == nil or ib.data.elements.error_state_id.value == nil then return end + if version.api < 10 then + clusters.OperationalState.types.ErrorStateStruct:augment_type(ib.data) + end + local operationalError = ib.data.elements.error_state_id.value + if operationalError == clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_START_OR_RESUME then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.unableToStartOrResume()) + elseif operationalError == clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_COMPLETE_OPERATION then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.unableToCompleteOperation()) + elseif operationalError == clusters.OperationalState.types.ErrorStateEnum.COMMAND_INVALID_IN_STATE then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.commandInvalidInCurrentState()) + end +end + + +-- [[ FLOW MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.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 + +function AttributeHandlers.flow_attr_handler_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" + switch_utils.set_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..minOrMax, ib.endpoint_id, flow_bound) + local min = switch_utils.get_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MIN, ib.endpoint_id) + local max = switch_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 })) + switch_utils.set_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MIN, ib.endpoint_id, nil) + switch_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 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 eec323a335..6d035c3dc7 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua @@ -50,11 +50,15 @@ end -- [[ STATELESS SWITCH LEVEL STEP CAPABILITY COMMANDS ]] -- function CapabilityHandlers.handle_step_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 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)) + local transition_time = device:get_field(fields.TRANSITION_TIME.SWITCH_LEVEL_STEP) or fields.DEFAULT_STEP_TRANSITION_TIME + device:send(clusters.LevelControl.server.commands.Step(device, endpoint_id, step_mode, math.abs(step_size), transition_time, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF)) end @@ -67,10 +71,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.HANDLE_COMMAND_IF_OFF) + req = clusters.ColorControl.server.commands.MoveToHueAndSaturation(device, endpoint_id, hue, sat, fields.ZERO_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.HANDLE_COMMAND_IF_OFF) + req = clusters.ColorControl.server.commands.MoveToColor(device, endpoint_id, x, y, fields.ZERO_TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) end device:send(req) end @@ -80,7 +84,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.HANDLE_COMMAND_IF_OFF) + local req = clusters.ColorControl.server.commands.MoveToHue(device, endpoint_id, hue, 0, fields.ZERO_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") @@ -92,7 +96,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.HANDLE_COMMAND_IF_OFF) + local req = clusters.ColorControl.server.commands.MoveToSaturation(device, endpoint_id, sat, fields.ZERO_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") @@ -105,16 +109,18 @@ end 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) + -- note: the field containing the color temp bounds will be associated with a parent device + local field_device = device:get_parent_device() or device + local min_temp_kelvin = switch_utils.get_field_for_endpoint(field_device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MIN, endpoint_id) + local max_temp_kelvin = switch_utils.get_field_for_endpoint(field_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) + temp_in_mired = switch_utils.get_field_for_endpoint(field_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) + temp_in_mired = switch_utils.get_field_for_endpoint(field_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.HANDLE_COMMAND_IF_OFF) + local req = clusters.ColorControl.server.commands.MoveToColorTemperature(device, endpoint_id, temp_in_mired, fields.ZERO_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 @@ -123,19 +129,29 @@ end -- [[ STATELESS COLOR TEMPERATURE STEP CAPABILITY COMMANDS ]] -- function CapabilityHandlers.handle_step_color_temperature_by_percent(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 step_percent_change = cmd.args and cmd.args.stepSize or 0 if step_percent_change == 0 then return end step_percent_change = st_utils.clamp_value(step_percent_change, -100, 100) 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.DEFAULT_MIRED_MIN - local max_mireds = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MAX, endpoint_id) or fields.DEFAULT_MIRED_MAX + -- note: the field containing the color temp bounds will be associated with a parent device + local field_device = device:get_parent_device() or device + local min_mireds = switch_utils.get_field_for_endpoint(field_device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MIN, endpoint_id) + local max_mireds = switch_utils.get_field_for_endpoint(field_device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MAX, endpoint_id) + -- since colorTemperatureRange is only set after both custom bounds are, use defaults if any custom bound is missing + if not (min_mireds and max_mireds) then + min_mireds = fields.DEFAULT_MIRED_MIN + max_mireds = fields.DEFAULT_MIRED_MAX + end 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)) + local transition_time = device:get_field(fields.TRANSITION_TIME.COLOR_TEMP_STEP) or fields.DEFAULT_STEP_TRANSITION_TIME + device:send(clusters.ColorControl.server.commands.StepColorTemperature(device, endpoint_id, step_mode, step_size_in_mireds, transition_time, min_mireds, max_mireds, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF)) end - -- [[ VALVE CAPABILITY COMMANDS ]] -- function CapabilityHandlers.handle_valve_open(driver, device, cmd) @@ -216,4 +232,21 @@ function CapabilityHandlers.handle_reset_energy_meter(driver, device, cmd) end end + +-- [[ OPERATIONAL STATE CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_operational_state_resume(driver, device, cmd) + local endpoint_id = device:get_endpoints(clusters.OperationalState.ID)[1] + device:send(clusters.OperationalState.server.commands.Resume(device, endpoint_id)) + device:send(clusters.OperationalState.attributes.OperationalState:read(device, endpoint_id)) + device:send(clusters.OperationalState.attributes.OperationalError:read(device, endpoint_id)) +end + +function CapabilityHandlers.handle_operational_state_pause(driver, device, cmd) + local endpoint_id = device:get_endpoints(clusters.OperationalState.ID)[1] + device:send(clusters.OperationalState.server.commands.Pause(device, endpoint_id)) + device:send(clusters.OperationalState.attributes.OperationalState:read(device, endpoint_id)) + device:send(clusters.OperationalState.attributes.OperationalError:read(device, endpoint_id)) +end + return CapabilityHandlers 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 ab2e075eb7..085de2d351 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -20,17 +20,18 @@ local ChildConfiguration = {} local SwitchDeviceConfiguration = {} local ButtonDeviceConfiguration = {} local FanDeviceConfiguration = {} +local ValveDeviceConfiguration = {} 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 + 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 child_profile, optional_component_capabilities = assign_profile_fn(device, ep_id, true) local existing_child_device = device:get_field(fields.IS_PARENT_CHILD_DEVICE) and switch_utils.find_child(device, ep_id) if not existing_child_device then driver:try_create_device({ @@ -43,7 +44,8 @@ function ChildConfiguration.create_or_update_child_devices(driver, device, serve }) else existing_child_device:try_update_metadata({ - profile = child_profile + profile = child_profile, + optional_component_capabilities = optional_component_capabilities }) end end @@ -74,7 +76,6 @@ function FanDeviceConfiguration.assign_profile_for_fan_ep(device, server_fan_ep_ 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) @@ -190,9 +191,56 @@ function ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep end end +function ValveDeviceConfiguration.assign_profile_for_irrigation_system_ep(device, irrigation_system_ep_id, is_child_device) + local main_component_capabilities = {} + local profile_name = "irrigation-system" + + local valve_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.WATER_VALVE) + table.sort(valve_ep_ids) + local supports_level = switch_utils.find_cluster_on_ep( + switch_utils.get_endpoint_info(device, is_child_device and irrigation_system_ep_id or valve_ep_ids[1]), + clusters.ValveConfigurationAndControl.ID, + {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL} + ) + if supports_level then + table.insert(main_component_capabilities, capabilities.level.ID) + end + + if is_child_device then + return profile_name, {{"main", main_component_capabilities}} + end + + local irrigation_system_ep_info = switch_utils.get_endpoint_info(device, irrigation_system_ep_id) + if switch_utils.find_cluster_on_ep(irrigation_system_ep_info, clusters.FlowMeasurement.ID) then + table.insert(main_component_capabilities, capabilities.flowMeasurement.ID) + end + if switch_utils.find_cluster_on_ep(irrigation_system_ep_info, clusters.OperationalState.ID) then + table.insert(main_component_capabilities, capabilities.operationalState.ID) + end + + return profile_name, {{"main", main_component_capabilities}} +end + -- [[ PROFILE MATCHING AND CONFIGURATIONS ]] -- +function DeviceConfiguration.match_child_profile(driver, device) + local parent_device = device:get_parent_device() + local irrigation_system_ep_ids = switch_utils.get_endpoints_by_device_type( + parent_device, + fields.DEVICE_TYPE_ID.IRRIGATION_SYSTEM + ) + if #irrigation_system_ep_ids > 0 then + ChildConfiguration.create_or_update_child_devices( + driver, + parent_device, + {device:get_endpoint()}, + nil, + ValveDeviceConfiguration.assign_profile_for_irrigation_system_ep + ) + end +end + local function profiling_data_still_required(device) for _, field in pairs(fields.profiling_data) do if device:get_field(field) == nil then @@ -209,14 +257,6 @@ function DeviceConfiguration.match_profile(driver, device) local optional_component_capabilities local updated_profile - 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 - 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_or_update_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id, SwitchDeviceConfiguration.assign_profile_for_onoff_ep) @@ -238,8 +278,20 @@ 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 + local irrigation_system_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.IRRIGATION_SYSTEM) + local valve_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.WATER_VALVE) + if #irrigation_system_ep_ids > 0 then + updated_profile, optional_component_capabilities = ValveDeviceConfiguration.assign_profile_for_irrigation_system_ep(device, irrigation_system_ep_ids[1], false) + ChildConfiguration.create_or_update_child_devices(driver, device, valve_ep_ids, default_endpoint_id, ValveDeviceConfiguration.assign_profile_for_irrigation_system_ep) + elseif #valve_ep_ids > 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 + + if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN) > 0 then updated_profile, optional_component_capabilities = FanDeviceConfiguration.assign_profile_for_fan_ep(device, default_endpoint_id) end @@ -256,7 +308,9 @@ function DeviceConfiguration.match_profile(driver, device) end return { + ButtonCfg = ButtonDeviceConfiguration, + ChildCfg = ChildConfiguration, DeviceCfg = DeviceConfiguration, SwitchCfg = SwitchDeviceConfiguration, - ButtonCfg = ButtonDeviceConfiguration + ValveCfg = ValveDeviceConfiguration } diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index 1432b18c74..875589f227 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -1,7 +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 SwitchFields = {} @@ -13,16 +13,12 @@ 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 = st_utils.round(SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MIN) -- 1000 Mireds -SwitchFields.COLOR_TEMPERATURE_MIRED_MIN = st_utils.round(SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MAX) -- 67 Mireds +SwitchFields.COLOR_TEMPERATURE_MIRED_MIN = 67 -- 15000 Kelvin +SwitchFields.COLOR_TEMPERATURE_MIRED_MAX = 1000 -- 1000 Kelvin -- These values are the config bounds in the default Matter profiles (e.g. light-level-colorTemperature, light-color-level) -local DEFAULT_KELVIN_MIN = 2200 -local DEFAULT_KELVIN_MAX = 6500 -SwitchFields.DEFAULT_MIRED_MIN = st_utils.round(SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT/DEFAULT_KELVIN_MAX) -- 154 Mireds -SwitchFields.DEFAULT_MIRED_MAX = st_utils.round(SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT/DEFAULT_KELVIN_MIN) -- 455 Mireds +SwitchFields.DEFAULT_MIRED_MIN = 154 -- 6500 Kelvin +SwitchFields.DEFAULT_MIRED_MAX = 455 -- 2200 Kelvin SwitchFields.SWITCH_LEVEL_LIGHTING_MIN = 1 SwitchFields.CURRENT_HUESAT_ATTR_MIN = 0 @@ -38,6 +34,7 @@ SwitchFields.DEVICE_TYPE_ID = { ELECTRICAL_SENSOR = 0x0510, FAN = 0x002B, GENERIC_SWITCH = 0x000F, + IRRIGATION_SYSTEM = 0x0040, MOUNTED_ON_OFF_CONTROL = 0x010F, MOUNTED_DIMMABLE_LOAD_CONTROL = 0x0110, ON_OFF_PLUG_IN_UNIT = 0x010A, @@ -52,6 +49,7 @@ SwitchFields.DEVICE_TYPE_ID = { DIMMER = 0x0104, COLOR_DIMMER = 0x0105, }, + WATER_VALVE = 0x0042, } SwitchFields.device_type_profile_map = { @@ -90,6 +88,9 @@ SwitchFields.LEVEL_BOUND_RECEIVED = "__level_bound_received" SwitchFields.LEVEL_MIN = "__level_min" SwitchFields.LEVEL_MAX = "__level_max" SwitchFields.COLOR_MODE = "__color_mode" +SwitchFields.FLOW_BOUND_RECEIVED = "__flow_bound_received" +SwitchFields.FLOW_MIN = "__flow_min" +SwitchFields.FLOW_MAX = "__flow_max" SwitchFields.SUBSCRIBED_ATTRIBUTES_KEY = "__subscribed_attributes" @@ -146,6 +147,11 @@ SwitchFields.switch_category_vendor_overrides = { {0xEEE2, 0xAB08, 0xAB31, 0xAB04, 0xAB01, 0xAB43, 0xAB02, 0xAB03, 0xAB05} } +SwitchFields.operational_state_command_map = { + [clusters.OperationalState.commands.Pause.ID] = "pause", + [clusters.OperationalState.commands.Resume.ID] = "resume" +} + --- 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" @@ -193,8 +199,13 @@ SwitchFields.TEMP_BOUND_RECEIVED = "__temp_bound_received" SwitchFields.TEMP_MIN = "__temp_min" SwitchFields.TEMP_MAX = "__temp_max" -SwitchFields.TRANSITION_TIME = 0 -- number of 10ths of a second -SwitchFields.TRANSITION_TIME_FAST = 3 -- 0.3 seconds +SwitchFields.ZERO_TRANSITION_TIME = 0 -- 0.0 seconds +SwitchFields.DEFAULT_STEP_TRANSITION_TIME = 3 -- 0.3 seconds, measured in tenths of a second as per the Matter spec + +SwitchFields.TRANSITION_TIME = { + SWITCH_LEVEL_STEP = "__switch_level_step_transition_time", + COLOR_TEMP_STEP = "__color_temp_step_transition_time", +} -- 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 diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index f949a15a56..e7f5a96187 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -151,10 +151,6 @@ function utils.find_default_endpoint(device) return device.MATTER_DEFAULT_ENDPOINT end - 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) for _,ep in ipairs(endpoints) do @@ -166,23 +162,24 @@ function utils.find_default_endpoint(device) end -- Return the first fan endpoint as the default endpoint if any is found + local fan_endpoint_ids = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN) if #fan_endpoint_ids > 0 then return get_first_non_zero_endpoint(fan_endpoint_ids) end - -- 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 - - -- 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) + -- Return the first water valve endpoint as the default endpoint if any is found + local water_valve_endpoint_ids = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.WATER_VALVE) + if #water_valve_endpoint_ids > 0 then + return get_first_non_zero_endpoint(water_valve_endpoint_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. + -- default endpoint. Else return the first onoff endpoint as the default endpoint. + -- + -- If only one of the two types of endpoints are present, return the first endpoint of the present type. + 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}) 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 @@ -191,6 +188,10 @@ function utils.find_default_endpoint(device) 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(momentary_switch_ep_ids) end + elseif #onoff_ep_ids > 0 then + return get_first_non_zero_endpoint(onoff_ep_ids) + elseif #momentary_switch_ep_ids > 0 then + return get_first_non_zero_endpoint(momentary_switch_ep_ids) end device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) 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 ccf952a824..02feaa245b 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 @@ -656,4 +656,83 @@ test.register_message_test( } ) +local generic_manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000 } +local generic_matter_version = { hardware = 1, software = 1 } +local root_endpoint = { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } +} + +local mock_device_light_level_motion = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("light-level-motion.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} + }, + device_types = { + {device_type_id = 0x0101, device_type_revision = 1} -- Dimmable Light + } + }, + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.OccupancySensing.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0107, device_type_revision = 1} -- Occupancy Sensor + } + } + } +}) + +local function test_init_light_level_motion() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_light_level_motion) +end + +test.register_coroutine_test( + "Test init and doConfigure for Dimmable Light device type with Occupancy Sensor", + function() + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.OccupancySensing.attributes.Occupancy + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_light_level_motion) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_light_level_motion)) + end + end + + 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" }) + 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, + { + test_init = test_init_light_level_motion, + min_api_version = 17 + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_irrigation_system.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_irrigation_system.lua new file mode 100644 index 0000000000..241f17c53e --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_irrigation_system.lua @@ -0,0 +1,477 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local test = require "integration_test" +local version = require "version" + +if version.api < 11 then + clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" +end + +local endpoints = { + ROOT_EP = 0, + IRRIGATION_SYSTEM_EP = 1, + VALVE_1_EP = 2, + VALVE_2_EP = 3, + VALVE_3_EP = 4 +} + +-- Mock device representing an irrigation system with 3 valve endpoints +local mock_irrigation_system = test.mock_device.build_test_matter_device({ + label = "Matter Irrigation System", + profile = t_utils.get_profile_definition("irrigation-system.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = endpoints.ROOT_EP, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = endpoints.IRRIGATION_SYSTEM_EP, + clusters = { + {cluster_id = clusters.Descriptor.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.FlowMeasurement.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.OperationalState.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0040, device_type_revision = 1} -- Irrigation System + } + }, + { + endpoint_id = endpoints.VALVE_1_EP, + clusters = { + { + cluster_id = clusters.ValveConfigurationAndControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 2 -- LEVEL feature + }, + }, + device_types = { + {device_type_id = 0x0042, device_type_revision = 1} -- Water Valve + } + }, + { + endpoint_id = endpoints.VALVE_2_EP, + clusters = { + { + cluster_id = clusters.ValveConfigurationAndControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 2 -- LEVEL feature + }, + }, + device_types = { + {device_type_id = 0x0042, device_type_revision = 1} -- Water Valve + } + }, + { + endpoint_id = endpoints.VALVE_3_EP, + clusters = { + { + cluster_id = clusters.ValveConfigurationAndControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 2 -- LEVEL feature + }, + }, + device_types = { + {device_type_id = 0x0042, device_type_revision = 1} -- Water Valve + } + } + } +}) + +local mock_children = {} +for _, endpoint in ipairs(mock_irrigation_system.endpoints) do + if endpoint.endpoint_id == 3 or endpoint.endpoint_id == 4 then + local child_data = { + profile = t_utils.get_profile_definition("water-valve-level.yml"), + device_network_id = string.format("%s:%d", mock_irrigation_system.id, endpoint.endpoint_id), + parent_device_id = mock_irrigation_system.id, + parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) + } + mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) + end +end + +local subscribe_request + +local expected_metadata = { + optional_component_capabilities = { { "main", { "level", "flowMeasurement", "operationalState", } } }, + profile = "irrigation-system" +} + +local function test_init() + test.mock_device.add_test_device(mock_irrigation_system) + local cluster_subscribe_list = { + clusters.ValveConfigurationAndControl.attributes.CurrentState, + clusters.ValveConfigurationAndControl.attributes.CurrentLevel, + } + subscribe_request = cluster_subscribe_list[1]:subscribe(mock_irrigation_system) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_irrigation_system)) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_irrigation_system.id, "added" }) + test.socket.matter:__expect_send({mock_irrigation_system.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_irrigation_system.id, "init" }) + test.socket.matter:__expect_send({mock_irrigation_system.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_irrigation_system.id, "doConfigure" }) + mock_irrigation_system:expect_metadata_update(expected_metadata) + mock_irrigation_system:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + for _, child in pairs(mock_children) do + test.mock_device.add_test_device(child) + end + for i = 3,4 do + mock_irrigation_system:expect_device_create({ + type = "EDGE_CHILD", + label = string.format("Matter Irrigation System %d", i - 1), + profile = "irrigation-system", + parent_device_id = mock_irrigation_system.id, + parent_assigned_child_key = string.format("%d", i) + }) + end + test.socket.matter:__expect_send({mock_irrigation_system.id, subscribe_request}) +end +test.set_test_init_function(test_init) + + +local additional_subscribed_attributes = { + clusters.FlowMeasurement.attributes.MeasuredValue, + clusters.FlowMeasurement.attributes.MaxMeasuredValue, + clusters.FlowMeasurement.attributes.MinMeasuredValue, + clusters.OperationalState.attributes.AcceptedCommandList, + clusters.OperationalState.attributes.OperationalError, + clusters.OperationalState.attributes.OperationalState, +} + +local function update_device_profile() + local updated_device_profile = t_utils.get_profile_definition( + "irrigation-system.yml", { enabled_optional_capabilities = expected_metadata.optional_component_capabilities } + ) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_irrigation_system:generate_info_changed({ profile = updated_device_profile })) + for _, attr in ipairs(additional_subscribed_attributes) do + subscribe_request:merge(attr:subscribe(mock_irrigation_system)) + end + test.socket.matter:__expect_send({mock_irrigation_system.id, subscribe_request}) +end + +test.register_coroutine_test( + "Parent device: Open command should send the appropriate commands", + function() + test.socket.capability:__queue_receive({ + mock_irrigation_system.id, + { capability = "valve", component = "main", command = "open", args = { } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Open(mock_irrigation_system, endpoints.VALVE_1_EP) + }) + end +) + +test.register_coroutine_test( + "Parent device: Close command should send the appropriate commands", + function() + test.socket.capability:__queue_receive({ + mock_irrigation_system.id, + { capability = "valve", component = "main", command = "close", args = { } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Close(mock_irrigation_system, endpoints.VALVE_1_EP) + }) + end +) + +test.register_coroutine_test( + "Parent device: Set level command should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + + test.socket.capability:__queue_receive({ + mock_irrigation_system.id, + { capability = "level", component = "main", command = "setLevel", args = { 75 } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Open(mock_irrigation_system, endpoints.VALVE_1_EP, nil, 75) + }) + end +) + +test.register_coroutine_test( + "Parent device: Current state closed should generate closed event", + function() + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.attributes.CurrentState:build_test_report_data( + mock_irrigation_system, + endpoints.VALVE_1_EP, + 0 + ) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message("main", capabilities.valve.valve.closed()) + ) + end +) + +test.register_coroutine_test( + "Parent device: Current level reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.attributes.CurrentLevel:build_test_report_data( + mock_irrigation_system, + endpoints.VALVE_1_EP, + 60 + ) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message("main", capabilities.level.level(60)) + ) + end +) + +test.register_coroutine_test( + "Flow reports should generate correct messages", + function() + update_device_profile() + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.FlowMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_irrigation_system, 1, 20 * 10) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message( + "main", + capabilities.flowMeasurement.flow({ value = 20.0, unit = "m^3/h" }) + ) + ) + end +) + +test.register_coroutine_test( + "Min and max flow attributes set capability constraint", + function() + update_device_profile() + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.FlowMeasurement.attributes.MinMeasuredValue:build_test_report_data(mock_irrigation_system, 1, 20) + }) + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.FlowMeasurement.attributes.MaxMeasuredValue:build_test_report_data(mock_irrigation_system, 1, 5000) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message( + "main", + capabilities.flowMeasurement.flowRange({ + value = { minimum = 2.0, maximum = 500.0 }, + unit = "m^3/h" + }) + ) + ) + end +) + +test.register_coroutine_test( + "Child device valve 2: Open command should send the appropriate commands", + function() + test.socket.capability:__queue_receive({ + mock_children[endpoints.VALVE_2_EP].id, + { capability = "valve", component = "main", command = "open", args = { } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Open(mock_irrigation_system, endpoints.VALVE_2_EP) + }) + end +) + +test.register_coroutine_test( + "Child device valve 2: Set level command should send the appropriate commands", + function() + test.socket.capability:__queue_receive({ + mock_children[endpoints.VALVE_2_EP].id, + { capability = "level", component = "main", command = "setLevel", args = { 40 } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Open(mock_irrigation_system, endpoints.VALVE_2_EP, nil, 40) + }) + end +) + +test.register_coroutine_test( + "Child device valve 2: Current state closed should generate closed event", + function() + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.attributes.CurrentState:build_test_report_data( + mock_irrigation_system, + endpoints.VALVE_2_EP, + 0 + ) + }) + test.socket.capability:__expect_send( + mock_children[endpoints.VALVE_2_EP]:generate_test_message("main", capabilities.valve.valve.closed()) + ) + end +) + +test.register_coroutine_test( + "Child device valve 3: Close command should send the appropriate commands", + function() + test.socket.capability:__queue_receive({ + mock_children[endpoints.VALVE_3_EP].id, + { capability = "valve", component = "main", command = "close", args = { } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.commands.Close(mock_irrigation_system, endpoints.VALVE_3_EP) + }) + end +) + +test.register_coroutine_test( + "Child device valve 3: Current level reports should generate appropriate events", + function() + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.ValveConfigurationAndControl.server.attributes.CurrentLevel:build_test_report_data( + mock_irrigation_system, + endpoints.VALVE_3_EP, + 100 + ) + }) + test.socket.capability:__expect_send( + mock_children[endpoints.VALVE_3_EP]:generate_test_message("main", capabilities.level.level(100)) + ) + end +) + +test.register_coroutine_test( + "OperationalState attribute running should generate running event", + function() + update_device_profile() + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.OperationalState.attributes.OperationalState:build_test_report_data( + mock_irrigation_system, + endpoints.IRRIGATION_SYSTEM_EP, + clusters.OperationalState.types.OperationalStateEnum.RUNNING + ) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message("main", capabilities.operationalState.operationalState.running()) + ) + end +) + +test.register_coroutine_test( + "OperationalState OperationalError UNABLE_TO_COMPLETE_OPERATION should generate unableToCompleteOperation event", + function() + update_device_profile() + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_irrigation_system.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data( + mock_irrigation_system, + endpoints.IRRIGATION_SYSTEM_EP, { error_state_id = clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_COMPLETE_OPERATION } + ) + }) + test.socket.capability:__expect_send( + mock_irrigation_system:generate_test_message("main", capabilities.operationalState.operationalState.unableToCompleteOperation()) + ) + end +) + +test.register_coroutine_test( + "OperationalState resume command should send Resume and re-read state/error to irrigation system EP", + function() + update_device_profile() + test.wait_for_events() + + test.socket.capability:__queue_receive({ + mock_irrigation_system.id, + { capability = "operationalState", component = "main", command = "resume", args = { } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.OperationalState.server.commands.Resume(mock_irrigation_system, endpoints.IRRIGATION_SYSTEM_EP) + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.OperationalState.attributes.OperationalState:read(mock_irrigation_system, endpoints.IRRIGATION_SYSTEM_EP) + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.OperationalState.attributes.OperationalError:read(mock_irrigation_system, endpoints.IRRIGATION_SYSTEM_EP) + }) + end +) + +test.register_coroutine_test( + "OperationalState pause command should send Pause and re-read state/error to irrigation system EP", + function() + update_device_profile() + test.wait_for_events() + + test.socket.capability:__queue_receive({ + mock_irrigation_system.id, + { capability = "operationalState", component = "main", command = "pause", args = { } } + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.OperationalState.server.commands.Pause(mock_irrigation_system, endpoints.IRRIGATION_SYSTEM_EP) + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.OperationalState.attributes.OperationalState:read(mock_irrigation_system, endpoints.IRRIGATION_SYSTEM_EP) + }) + test.socket.matter:__expect_send({ + mock_irrigation_system.id, + clusters.OperationalState.attributes.OperationalError:read(mock_irrigation_system, endpoints.IRRIGATION_SYSTEM_EP) + }) + end +) + +test.register_coroutine_test( + "Child device doConfigure: update child profile based on its endpoint configuration", + function() + test.socket.device_lifecycle:__queue_receive({ mock_children[endpoints.VALVE_2_EP].id, "doConfigure" }) + mock_children[endpoints.VALVE_2_EP]:expect_metadata_update({ + profile = "irrigation-system", + optional_component_capabilities = {{"main", {"level"}}} + }) + mock_children[endpoints.VALVE_2_EP]:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.matter:__expect_send({mock_irrigation_system.id, subscribe_request}) + end +) + +test.run_registered_tests() + diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_device_configuration.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_device_configuration.lua new file mode 100644 index 0000000000..6c3d168c91 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_device_configuration.lua @@ -0,0 +1,306 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local clusters = require "st.matter.clusters" + +test.disable_startup_messages() + + +local generic_manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000 } +local generic_matter_version = { hardware = 1, software = 1 } +local root_endpoint = { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } +} + + +local mock_device_onoff_switch_as_server = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("matter-thing.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0103, device_type_revision = 1} -- On/Off Light Switch + } + } + } +}) + +test.register_coroutine_test( + "Test profile change on init for onoff parent cluster as server", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_switch_as_server.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_switch_as_server.id, "init" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_switch_as_server.id, "doConfigure" }) + mock_device_onoff_switch_as_server:expect_metadata_update({ profile = "switch-binary" }) + mock_device_onoff_switch_as_server:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + test_init = function() test.mock_device.add_test_device(mock_device_onoff_switch_as_server) end, + min_api_version = 17 + } +) + + +local mock_device_onoff_switch_as_client = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("matter-thing.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "CLIENT", cluster_revision = 1, feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0103, device_type_revision = 1} -- On/Off Light Switch + } + } + } +}) + +test.register_coroutine_test( + "Test init for onoff parent cluster as client", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_switch_as_client.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_switch_as_client.id, "init" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_switch_as_client.id, "doConfigure" }) + mock_device_onoff_switch_as_client:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + test_init = function() test.mock_device.add_test_device(mock_device_onoff_switch_as_client) end, + min_api_version = 17 + } +) + + +local mock_device_dimmer_switch_as_server = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("matter-thing.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + endpoint_id = 1, + 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 = 0x0104, device_type_revision = 1} -- Dimmer Switch + } + } + } +}) + +test.register_coroutine_test( + "Test profile change on init for dimmer parent cluster as server", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer_switch_as_server.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer_switch_as_server.id, "init" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer_switch_as_server.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_dimmer_switch_as_server.id, + clusters.LevelControl.attributes.Options:write(mock_device_dimmer_switch_as_server, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + mock_device_dimmer_switch_as_server:expect_metadata_update({ profile = "switch-level" }) + mock_device_dimmer_switch_as_server:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + test_init = function() test.mock_device.add_test_device(mock_device_dimmer_switch_as_server) end, + min_api_version = 17 + } +) + + +local mock_device_plug_with_switch_profile_vendor_override = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("switch-binary.yml"), + manufacturer_info = { vendor_id = 0x142B, product_id = 0x1003}, -- this device has a vendor override to join as a switch instead of a plug + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + 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 + } + } + } +}) + +test.register_coroutine_test( + "Test init for device with requiring the switch category as a vendor override", + function() + local mock_device = mock_device_plug_with_switch_profile_vendor_override + local subscribe_request = clusters.OnOff.attributes.OnOff: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({ profile = "switch-binary" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + test_init = function() test.mock_device.add_test_device(mock_device_plug_with_switch_profile_vendor_override) end, + min_api_version = 17 + } +) + + +local mock_device_color_dimmer = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("matter-thing.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + 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.ColorControl.ID, cluster_type = "CLIENT", feature_map = 31}, + + }, + device_types = { + {device_type_id = 0x0105, device_type_revision = 1} -- Color Dimmer Switch + } + } + } +}) + +test.register_coroutine_test( + "Test profile change on init for color dimmer device type as server", + function() + 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, + { + test_init = function() test.mock_device.add_test_device(mock_device_color_dimmer) end, + min_api_version = 17 + } +) + + +local mock_device_mounted_on_off_control = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("switch-binary.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + 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 = "SERVER", feature_map = 2}, + + }, + device_types = { + {device_type_id = 0x010F, device_type_revision = 1} -- Mounted On/Off Control + } + } + } +}) + +test.register_coroutine_test( + "Test init for mounted onoff control", + function() + local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_mounted_on_off_control) + + 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" }) + 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, + { + test_init = function() test.mock_device.add_test_device(mock_device_mounted_on_off_control) end, + min_api_version = 17 + } +) + + +local mock_device_mounted_dimmable_load_control = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("switch-level.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + 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 = "SERVER", feature_map = 2}, + + }, + device_types = { + {device_type_id = 0x0110, device_type_revision = 1} -- Mounted Dimmable Load Control + } + } + } +}) + +test.register_coroutine_test( + "Test init for mounted dimmable load control", + function() + 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 + if i > 1 then + 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" }) + 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, + { + test_init = function() test.mock_device.add_test_device(mock_device_mounted_dimmable_load_control) end, + min_api_version = 17 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_parent_child.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_parent_child.lua new file mode 100644 index 0000000000..1531632d24 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_parent_child.lua @@ -0,0 +1,869 @@ +-- 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 capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" + +test.disable_startup_messages() + +local generic_manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000 } +local generic_matter_version = { hardware = 1, software = 1 } +local root_endpoint = { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } +} + +local parent_ep_id = 10 +local dimmable_ep_id = 30 +local extended_color_ep_id = 50 + +-- this parent device would fingerprint as light-color-level, since the most feature-rich endpoint is the extended color one, +-- but it should re-configure to light-binary in doConfigure +local mock_device = test.mock_device.build_test_matter_device({ + label = "Matter Switch", + profile = t_utils.get_profile_definition("light-color-level.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + endpoint_id = parent_ep_id, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 2} -- On/Off Light + } + }, + { + endpoint_id = extended_color_ep_id, + 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 = dimmable_ep_id, + 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 = 0x0100, device_type_revision = 2}, -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light + } + }, + } +}) + +local child_profiles = { + [dimmable_ep_id] = t_utils.get_profile_definition("light-level.yml"), + [extended_color_ep_id] = 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_id and endpoint.endpoint_id ~= 0 then + local child_data = { + 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) + } + mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) + end +end + +local function handle_init_event(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, + clusters.ColorControl.attributes.ColorMode, + } + local expected_subscriptions = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + expected_subscriptions: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, expected_subscriptions}) +end + +local function handle_do_configure_event(mock_device) + 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, extended_color_ep_id, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, extended_color_ep_id, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, dimmable_ep_id, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + + mock_device:expect_metadata_update({ profile = "light-binary" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 2", + profile = "light-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", dimmable_ep_id) + }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 3", + profile = "light-color-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", extended_color_ep_id) + }) +end + +local function test_init_for_lifecycle_tests() + test.mock_device.add_test_device(mock_device) + for _, child in pairs(mock_children) do + test.mock_device.add_test_device(child) + end +end + +-- due to device copy logic in the integration tests, we need to handle init and doConfigure before generating an infoChanged event +local function test_init_for_generate_info_changed_tests() + test.mock_device.add_test_device(mock_device) + for _, child in pairs(mock_children) do + test.mock_device.add_test_device(child) + end + handle_init_event(mock_device) + handle_do_configure_event(mock_device) +end + +local function test_init_for_post_configure_tests() + test.mock_device.add_test_device(mock_device) + for _, child in pairs(mock_children) do + test.mock_device.add_test_device(child) + end + local FIND_CHILD_KEY = "__find_child_fn" + mock_device:set_field(FIND_CHILD_KEY, switch_utils.find_child, { persist = false }) + mock_device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, { persist = false }) +end + +test.set_test_init_function(test_init_for_post_configure_tests) + +test.register_coroutine_test( + "Handle initial init lifecycle event, before children are created", + function() + handle_init_event(mock_device) + test.wait_for_events() + assert(mock_device:get_field(fields.profiling_data.POWER_TOPOLOGY) == false, "Device should be marked as not needing to configure power topology") + assert(mock_device:get_field(fields.profiling_data.BATTERY_SUPPORT) == fields.battery_support.NO_BATTERY, "Device should be marked as having no battery") + end, + { + test_init = test_init_for_lifecycle_tests, + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Handle doConfigure lifecycle event", + function() + mock_device:set_field(fields.profiling_data.BATTERY_SUPPORT, false, { persist = true }) + mock_device:set_field(fields.profiling_data.POWER_TOPOLOGY, false, { persist = true }) + handle_do_configure_event(mock_device) + test.wait_for_events() + local FIND_CHILD_KEY = "__find_child_fn" + assert(type(mock_device:get_field(FIND_CHILD_KEY)) == "function", "Child find function should be stored in doConfigure") + end, + { + test_init = test_init_for_lifecycle_tests, + min_api_version = 17 + } +) + +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[dimmable_ep_id]:expect_metadata_update({ profile = "light-level" }) + mock_children[extended_color_ep_id]:expect_metadata_update({ profile = "light-color-level" }) + mock_device:expect_metadata_update({ profile = "light-binary" }) + end, + { + test_init = test_init_for_generate_info_changed_tests, + min_api_version = 17 + } +) + + +test.register_message_test( + "Dimmable Child: Current level cluster reports generate switch level events appropriately", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.attributes.CurrentLevel:build_test_report_data(mock_device, dimmable_ep_id, 50) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[dimmable_ep_id]: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" } + } + }, + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Children: Level Control Min and max attributes set switch level constraints appropriately", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, dimmable_ep_id, 1) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, dimmable_ep_id, 254) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[dimmable_ep_id]: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, extended_color_ep_id, 127) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, extended_color_ep_id, 203) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[extended_color_ep_id]:generate_test_message("main", capabilities.switchLevel.levelRange({minimum = 50, maximum = 80})) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Extended Color Child: 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, extended_color_ep_id, 15091) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, extended_color_ep_id, 21547) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[extended_color_ep_id]:generate_test_message("main", capabilities.colorControl.hue(50)) + }, + { + channel = "capability", + direction = "send", + message = mock_children[extended_color_ep_id]:generate_test_message("main", capabilities.colorControl.saturation(72)) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Extended Color Child: colorTemperatureRange, setColorTemperature, stepColorTemperatureByPercent handled appropriately", + { + -- setColorTemperature before a color temperature range is set + { + channel = "capability", + direction = "receive", + message = { + mock_children[extended_color_ep_id].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, extended_color_ep_id, 15182, 21547, fields.ZERO_TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColor:build_test_command_response(mock_device, extended_color_ep_id) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, extended_color_ep_id, 15091) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, extended_color_ep_id, 21547) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[extended_color_ep_id]:generate_test_message("main", capabilities.colorControl.hue(50)) + }, + { + channel = "capability", + direction = "send", + message = mock_children[extended_color_ep_id]:generate_test_message("main", capabilities.colorControl.saturation(72)) + }, + + -- colorTemperatureRange testing + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, extended_color_ep_id, 153) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, extended_color_ep_id, 555) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[extended_color_ep_id]:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 1800, maximum = 6500})) + }, + + -- setColorTemperature testing + { + channel = "capability", + direction = "receive", + message = { + mock_children[extended_color_ep_id].id, + { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, extended_color_ep_id, 555, fields.ZERO_TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) + } + }, -- 555 is expected since it is re-bounded by the given range + + -- stepColorTemperatureByPercent testing + { + channel = "capability", + direction = "receive", + message = { + mock_children[extended_color_ep_id].id, + { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 20 } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_children[extended_color_ep_id].id, capability_id = "statelessColorTemperatureStep", capability_cmd_id = "stepColorTemperatureByPercent" } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ColorControl.server.commands.StepColorTemperature(mock_device, extended_color_ep_id, clusters.ColorControl.types.StepModeEnum.DOWN, 80, fields.DEFAULT_STEP_TRANSITION_TIME, 153, 555, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + }, + }, + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Parent: switch capability <-> On Off cluster should handle events appropriately", + { + { + 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 = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, parent_ep_id) + }, + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, parent_ep_id, 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" } + } + }, + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Children: switch capability <-> On Off Cluster should handle events appropriately", + { + { + channel = "capability", + direction = "receive", + message = { + mock_children[dimmable_ep_id].id, + { capability = "switch", component = "main", command = "on", args = { } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_children[dimmable_ep_id].id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, dimmable_ep_id) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, dimmable_ep_id, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[dimmable_ep_id]: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" } + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_children[extended_color_ep_id].id, + { capability = "switch", component = "main", command = "on", args = { } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_children[extended_color_ep_id].id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, extended_color_ep_id) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, extended_color_ep_id, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[extended_color_ep_id]: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" } + } + }, + }, + { + min_api_version = 17 + } +) + +local dimmable_child_plug_ep_id = 30 + +local mock_plug = test.mock_device.build_test_matter_device({ + label = "Matter Plug", + profile = t_utils.get_profile_definition("plug-level.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + endpoint_id = parent_ep_id, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.ON_OFF_PLUG_IN_UNIT, device_type_revision = 2} + } + }, + { + endpoint_id = dimmable_child_plug_ep_id, + 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 = fields.DEVICE_TYPE_ID.DIMMABLE_PLUG_IN_UNIT, device_type_revision = 2} + } + }, + } +}) + +local function handle_init_event_for_plug(mock_device) + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel + } + local expected_subscriptions = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + expected_subscriptions: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, expected_subscriptions}) +end + +local function handle_do_configure_event_for_plug(mock_device) + 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, dimmable_child_plug_ep_id, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + mock_device:expect_metadata_update({ profile = "plug-binary" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Plug 2", + profile = "plug-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", dimmable_child_plug_ep_id) + }) +end + +test.register_coroutine_test( + "Plug: Handle initial init lifecycle event, before children are created", + function() + handle_init_event_for_plug(mock_plug) + test.wait_for_events() + assert(mock_plug:get_field(fields.profiling_data.POWER_TOPOLOGY) == false, "Device should be marked as not needing to configure power topology") + assert(mock_plug:get_field(fields.profiling_data.BATTERY_SUPPORT) == fields.battery_support.NO_BATTERY, "Device should be marked as having no battery") + end, + { + test_init = function() + test.mock_device.add_test_device(mock_plug) + end, + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Plug: Handle doConfigure lifecycle event", + function() + mock_plug:set_field(fields.profiling_data.BATTERY_SUPPORT, false, { persist = true }) + mock_plug:set_field(fields.profiling_data.POWER_TOPOLOGY, false, { persist = true }) + handle_do_configure_event_for_plug(mock_plug) + test.wait_for_events() + local FIND_CHILD_KEY = "__find_child_fn" + assert(type(mock_plug:get_field(FIND_CHILD_KEY)) == "function", "Child find function should be stored in doConfigure") + end, + { + test_init = function() + test.mock_device.add_test_device(mock_plug) + end, + min_api_version = 17 + } +) + + +local overriden_plug_child_ep_id = 30 + +-- This device overrides both its parent and child profiles to become the Switch category +local mock_plug_profile_override = test.mock_device.build_test_matter_device({ + label = "Matter Plug", + profile = t_utils.get_profile_definition("switch-binary.yml"), + manufacturer_info = { vendor_id = 0x1321, product_id = 0x000C }, -- this Sonoff device has an overloaded profile only for its children + endpoints = { + root_endpoint, + { + endpoint_id = parent_ep_id, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.ON_OFF_PLUG_IN_UNIT, device_type_revision = 2} + } + }, + { + endpoint_id = overriden_plug_child_ep_id, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.ON_OFF_PLUG_IN_UNIT, device_type_revision = 2} + } + }, + } +}) + + +local function handle_do_configure_event_for_plug_with_profile_override(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "switch-binary" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Plug 2", + profile = "switch-binary", -- overriden profile for Sonoff device + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", overriden_plug_child_ep_id) + }) +end + +test.register_coroutine_test( + "Plug with Overriden Profile: Handle doConfigure lifecycle event", + function() + mock_plug_profile_override:set_field(fields.profiling_data.BATTERY_SUPPORT, false, { persist = true }) + mock_plug_profile_override:set_field(fields.profiling_data.POWER_TOPOLOGY, false, { persist = true }) + handle_do_configure_event_for_plug_with_profile_override(mock_plug_profile_override) + end, + { + test_init = function() + test.mock_device.add_test_device(mock_plug_profile_override) + end, + min_api_version = 17 + } +) + +local mock_switch = test.mock_device.build_test_matter_device({ + label = "Matter Switch", + profile = t_utils.get_profile_definition("matter-thing.yml"), + manufacturer_info = generic_manufacturer_info, + matter_version = generic_matter_version, + endpoints = { + root_endpoint, + { + 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 = "SERVER", feature_map = 2} + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.SWITCH.DIMMER, device_type_revision = 1} + } + }, + { + endpoint_id = 10, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.SWITCH.ON_OFF_LIGHT, device_type_revision = 1} + } + }, + { + endpoint_id = 20, -- this endpoint should not generate a child device since it only has a client OnOff cluster + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "CLIENT", cluster_revision = 1, feature_map = 0}, + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.SWITCH.ON_OFF_LIGHT, device_type_revision = 1} + } + }, + { + endpoint_id = 30, -- this endpoint should profile correctly, though it is not a switch + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = fields.DEVICE_TYPE_ID.LIGHT.ON_OFF, device_type_revision = 2} + } + }, + { + endpoint_id = 40, -- this endpoint should generate a switch-binary child device since it has a SERVER OnOff cluster,though the device type is unknown. + 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 = 0x0304, device_type_revision = 2} -- Pump Controller + } + } + } +}) + +test.register_coroutine_test( + "Switch Profile: Handle doConfigure lifecycle event", + function() + mock_switch:set_field(fields.profiling_data.BATTERY_SUPPORT, false, { persist = true }) + mock_switch:set_field(fields.profiling_data.POWER_TOPOLOGY, false, { persist = true }) + test.socket.device_lifecycle:__queue_receive({ mock_switch.id, "doConfigure" }) + test.socket.matter:__expect_send({ mock_switch.id, clusters.LevelControl.attributes.Options:write(mock_switch, 7, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) }) + test.socket.matter:__expect_send({ mock_switch.id, clusters.LevelControl.attributes.Options:write(mock_switch, 40, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) }) + mock_switch:expect_metadata_update({ profile = "switch-level" }) + mock_switch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + mock_switch:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 2", + profile = "switch-binary", + parent_device_id = mock_switch.id, + parent_assigned_child_key = string.format("%d", 10) + }) + + -- client cluster only endpoint (20) should not generate a child device + + mock_switch:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 3", + profile = "light-binary", + parent_device_id = mock_switch.id, + parent_assigned_child_key = string.format("%d", 30) + }) + + mock_switch:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 4", + profile = "switch-binary", + parent_device_id = mock_switch.id, + parent_assigned_child_key = string.format("%d", 40) + }) + end, + { + test_init = function() test.mock_device.add_test_device(mock_switch) end, + min_api_version = 17 + } +) + +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 deleted file mode 100644 index 37b5ec8ca5..0000000000 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ /dev/null @@ -1,804 +0,0 @@ --- Copyright © 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local test = require "integration_test" -local t_utils = require "integration_test.utils" -local clusters = require "st.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 = { - 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.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0103, device_type_revision = 1} -- On/Off Light Switch - } - } - } -}) - -local mock_device_onoff_client = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("matter-thing.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.OnOff.ID, cluster_type = "CLIENT", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0103, device_type_revision = 1} -- On/Off Light Switch - } - } - } -}) - -local mock_device_dimmer = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("matter-thing.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 5, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} - }, - device_types = { - {device_type_id = 0x0104, device_type_revision = 1} -- Dimmer Switch - } - } - } -}) - -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 = { - 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 = 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.ColorControl.ID, cluster_type = "CLIENT", feature_map = 31}, - - }, - device_types = { - {device_type_id = 0x0105, device_type_revision = 1} -- Color Dimmer Switch - } - } - } -}) - -local mock_device_mounted_on_off_control = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("switch-binary.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 = 7, - 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 = 0x010F, device_type_revision = 1} -- Mounted On/Off Control - } - } - } -}) - -local mock_device_mounted_dimmable_load_control = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("switch-level.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 = 7, - 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 = 0x0110, device_type_revision = 1} -- Mounted Dimmable Load Control - } - } - } -}) - -local mock_device_water_valve = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("matter-thing.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.ValveConfigurationAndControl.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 2}, - }, - device_types = { - {device_type_id = 0x0042, device_type_revision = 1} -- Water Valve - } - } - } -}) - -local mock_device_parent_client_child_server = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("matter-thing.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 7, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0103, device_type_revision = 1} -- OnOff Switch - } - }, - { - endpoint_id = 10, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "CLIENT", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0103, device_type_revision = 1} -- OnOff Switch - } - }, - } -}) - -local mock_device_parent_child_switch_types = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("matter-thing.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - 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 = "SERVER", feature_map = 2} - }, - device_types = { - {device_type_id = 0x0104, device_type_revision = 1} -- Dimmer Switch - } - }, - { - endpoint_id = 10, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0103, device_type_revision = 1} -- OnOff Switch - } - }, - } -}) - -local mock_device_parent_child_different_types = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("switch-binary.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 7, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0103, device_type_revision = 1} -- OnOff Switch - } - }, - { - endpoint_id = 10, - 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 - } - } - } -}) - -local mock_device_parent_child_unsupported_device_type = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("matter-thing.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0016, device_type_revision = 1} -- RootNode - } - }, - { - endpoint_id = 7, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0103, device_type_revision = 1} -- OnOff Switch - } - }, - { - endpoint_id = 10, - 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 = 0x0304, device_type_revision = 2} -- Pump Controller - } - } - } -}) - -local mock_device_light_level_motion = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("light-level-motion.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.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} - }, - device_types = { - {device_type_id = 0x0101, device_type_revision = 1} -- Dimmable Light - } - }, - { - endpoint_id = 2, - clusters = { - {cluster_id = clusters.OccupancySensing.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0107, device_type_revision = 1} -- Occupancy Sensor - } - } - } -}) - -local function test_init_parent_child_switch_types() - test.mock_device.add_test_device(mock_device_parent_child_switch_types) - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "init" }) - 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" }) - - mock_device_parent_child_switch_types:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "switch-binary", - parent_device_id = mock_device_parent_child_switch_types.id, - parent_assigned_child_key = string.format("%d", 10) - }) -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" }) -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() - test.mock_device.add_test_device(mock_device_parent_client_child_server) - - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "added" }) - - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "init" }) - - 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" }) -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, - 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 - -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_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 = { - clusters.OnOff.attributes.OnOff, - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_mounted_on_off_control) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - 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" }) - 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 - -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 - if i > 1 then - 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" }) - 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 - -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 - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_parent_child_different_types) - 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" }) - 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" }) - - mock_device_parent_child_different_types:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "light-color-level", - parent_device_id = mock_device_parent_child_different_types.id, - parent_assigned_child_key = string.format("%d", 10) - }) -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" }) - 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({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "switch-binary", - parent_device_id = mock_device_parent_child_unsupported_device_type.id, - parent_assigned_child_key = string.format("%d", 10) - }) -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, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.OccupancySensing.attributes.Occupancy - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_light_level_motion) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - 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.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" }) - 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 - -test.register_coroutine_test( - "Test profile change on init for onoff parent cluster as server", - function() - end, - { - test_init = test_init_onoff, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test profile change on init for dimmer parent cluster as server", - function() - end, - { - test_init = test_init_dimmer, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test profile change on init for color dimmer parent cluster as server", - function() - end, - { - test_init = test_init_color_dimmer, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test init for onoff parent cluster as client", - function() - end, - { - test_init = test_init_onoff_client, - min_api_version = 17 - } -) - -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, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test init for mounted onoff control parent cluster as server", - function() - end, - { - test_init = test_init_mounted_on_off_control, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test init for mounted dimmable load control parent cluster as server", - function() - end, - { - test_init = test_init_mounted_dimmable_load_control, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test profile change on init for water valve parent cluster as server", - function() - end, - { - test_init = test_init_water_valve, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test profile change on init for onoff parent cluster as client and onoff child as server", - function() - end, - { - test_init = test_init_parent_client_child_server, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test profile change on init for onoff device when parent and child are both server", - function() - end, - { - test_init = test_init_parent_child_switch_types, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test child device attribute subscriptions when parent device has clusters that are not a superset of child device clusters", - function() - end, - { - test_init = test_init_parent_child_different_types, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test child device attributes not subscribed to for unsupported device type for child device", - function() - end, - { - test_init = test_init_parent_child_unsupported_device_type, - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test init for light with motion sensor", - function() - end, - { - test_init = test_init_light_level_motion, - min_api_version = 17 - } -) - -test.run_registered_tests() - 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 338acd58c7..ced13d6eb8 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 @@ -66,6 +66,19 @@ local function test_init() end test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Test profile change on init for water valve parent cluster as server", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "water-valve-level" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 17 + } +) + test.register_message_test( "Open command should send the appropriate commands", { 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 deleted file mode 100644 index 8102c61865..0000000000 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ /dev/null @@ -1,740 +0,0 @@ --- 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 clusters = require "st.matter.clusters" - -test.disable_startup_messages() - -local TRANSITION_TIME = 0 -local OPTIONS_MASK = 0x01 -local HANDLE_COMMAND_IF_OFF = 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("light-level-colorTemperature.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 = parent_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - }, - device_types = { - {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 = 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 = 0x010D, device_type_revision = 2} -- Extended Color Light - } - }, - } -}) - -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("light-level-colorTemperature.yml"), - manufacturer_info = { - vendor_id = 0x1321, - product_id = 0x000C, - }, - 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 = parent_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 2} -- On/Off Light - } - }, - { - 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 = 0x0100, device_type_revision = 2}, -- On/Off Light - {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light - } - }, - { - 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"}, - }, - device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug - } - }, - } -}) - -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_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) - } - mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) - end -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, - 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 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, 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 - test.mock_device.add_test_device(child) - end - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "light-level", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child1_ep) - }) - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 3", - profile = "light-color-level", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child2_ep) - }) -end - -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_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_non_sequential[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) - end -end - -local function test_init_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, - 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 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.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({ 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)}) - - 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 - test.mock_device.add_test_device(child) - end - - unsup_mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "light-color-level", - 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 - unsup_mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 3", - profile = "switch-binary", - parent_device_id = unsup_mock_device.id, - parent_assigned_child_key = string.format("%d", child3_ep_non_sequential) - }) - - unsup_mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 4", - profile = "light-level", - parent_device_id = unsup_mock_device.id, - parent_assigned_child_key = string.format("%d", child1_ep_non_sequential) - }) -end - -test.set_test_init_function(test_init) - -test.register_message_test( - "Parent device: switch capability should send the appropriate commands", - { - { - 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 = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, parent_ep) - }, - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, parent_ep, 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" } - } - }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "First child device: switch capability switch should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_children[child1_ep].id, - { capability = "switch", component = "main", command = "on", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_children[child1_ep].id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, child1_ep) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, child1_ep, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child1_ep]: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" } - } - }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Second child device: switch capability should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_children[child2_ep].id, - { capability = "switch", component = "main", command = "on", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_children[child2_ep].id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, child2_ep) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, child2_ep, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]: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" } - } - }, - }, - { - min_api_version = 17 - } -) - -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" } - } - }, - }, - { - min_api_version = 17 - } -) - -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, HANDLE_COMMAND_IF_OFF) - } - }, - { - 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)) - }, - }, - { - min_api_version = 17 - } -) - -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)) - } - }, - { - min_api_version = 17 - } -) - -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, HANDLE_COMMAND_IF_OFF) - } - }, - { - 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)) - } - }, - { - min_api_version = 17 - } -) - -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})) - } - }, - { - min_api_version = 17 - } -) - -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})) - } - }, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test child devices are created in order of their endpoints", - function() - end, - { - test_init = test_init_parent_child_endpoints_non_sequential, - min_api_version = 17 - } -) - -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, - { - min_api_version = 17 - } -) - -test.run_registered_tests() - 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 deleted file mode 100644 index 9beed1805e..0000000000 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ /dev/null @@ -1,720 +0,0 @@ --- 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 capabilities = require "st.capabilities" -local clusters = require "st.matter.clusters" - -test.disable_startup_messages() - -local TRANSITION_TIME = 0 -local OPTIONS_MASK = 0x01 -local HANDLE_COMMAND_IF_OFF = 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("light-level-colorTemperature.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 = parent_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - }, - device_types = { - {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 = 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 = 0x010D, device_type_revision = 2} -- Extended Color Light - } - }, - } -}) - -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("light-level-colorTemperature.yml"), - manufacturer_info = { - vendor_id = 0x1321, - product_id = 0x000C, - }, - 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 = parent_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0100, device_type_revision = 2} -- On/Off Light - } - }, - { - 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 = 0x0100, device_type_revision = 2}, -- On/Off Light - {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light - } - }, - { - 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"}, - }, - device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug - } - }, - } -}) - -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_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) - } - mock_children[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) - end -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, - 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 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, 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 - test.mock_device.add_test_device(child) - end - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "light-level", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child1_ep) - }) - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 3", - profile = "light-color-level", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child2_ep) - }) -end - -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_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_non_sequential[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) - end -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, - 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 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_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" }) - 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 = "switch-binary" }) - mock_device_parent_child_endpoints_non_sequential: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({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - 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) - }) - - -- 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_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 - -test.set_test_init_function(test_init) - -test.register_message_test( - "Parent device: switch capability should send the appropriate commands", - { - { - 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 = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, parent_ep) - }, - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, parent_ep, 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" } - } - }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "First child device: switch capability switch should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_children[child1_ep].id, - { capability = "switch", component = "main", command = "on", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_children[child1_ep].id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, child1_ep) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, child1_ep, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child1_ep]: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" } - } - }, - }, - { - min_api_version = 17 - } -) - -test.register_message_test( - "Second child device: switch capability should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_children[child2_ep].id, - { capability = "switch", component = "main", command = "on", args = { } } - } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_children[child2_ep].id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, child2_ep) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, child2_ep, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_children[child2_ep]: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" } - } - }, - }, - { - min_api_version = 17 - } -) - -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" } - } - }, - }, - { - min_api_version = 17 - } -) - -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, HANDLE_COMMAND_IF_OFF) - } - }, - { - 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)) - }, - }, - { - min_api_version = 17 - } -) - -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)) - } - }, - { - min_api_version = 17 - } -) - -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, HANDLE_COMMAND_IF_OFF) - } - }, - { - 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)) - } - }, - { - min_api_version = 17 - } -) - -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})) - } - }, - { - min_api_version = 17 - } -) - -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})) - } - }, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Test child devices are created in order of their endpoints", - function() - end, - { - test_init = test_init_parent_child_endpoints_non_sequential, - min_api_version = 17 - } -) - -test.run_registered_tests() - diff --git a/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua b/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua index 8d05070efc..a92c547ac7 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua @@ -62,12 +62,20 @@ test.register_message_test( { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 20 } } } }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device_color_temp.id, capability_id = "statelessColorTemperatureStep", capability_cmd_id = "stepColorTemperatureByPercent" } + } + }, { 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, 60, fields.TRANSITION_TIME_FAST, fields.DEFAULT_MIRED_MIN, fields.DEFAULT_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + clusters.ColorControl.server.commands.StepColorTemperature(mock_device_color_temp, 1, clusters.ColorControl.types.StepModeEnum.DOWN, 60, fields.DEFAULT_STEP_TRANSITION_TIME, fields.DEFAULT_MIRED_MIN, fields.DEFAULT_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) }, }, { @@ -78,12 +86,20 @@ test.register_message_test( { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 90 } } } }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device_color_temp.id, capability_id = "statelessColorTemperatureStep", capability_cmd_id = "stepColorTemperatureByPercent" } + } + }, { 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, 271, fields.TRANSITION_TIME_FAST, fields.DEFAULT_MIRED_MIN, fields.DEFAULT_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + clusters.ColorControl.server.commands.StepColorTemperature(mock_device_color_temp, 1, clusters.ColorControl.types.StepModeEnum.DOWN, 271, fields.DEFAULT_STEP_TRANSITION_TIME, fields.DEFAULT_MIRED_MIN, fields.DEFAULT_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) }, }, { @@ -94,12 +110,20 @@ test.register_message_test( { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { -50 } } } }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device_color_temp.id, capability_id = "statelessColorTemperatureStep", capability_cmd_id = "stepColorTemperatureByPercent" } + } + }, { 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, 151, fields.TRANSITION_TIME_FAST, fields.DEFAULT_MIRED_MIN, fields.DEFAULT_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + clusters.ColorControl.server.commands.StepColorTemperature(mock_device_color_temp, 1, clusters.ColorControl.types.StepModeEnum.UP, 151, fields.DEFAULT_STEP_TRANSITION_TIME, fields.DEFAULT_MIRED_MIN, fields.DEFAULT_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) }, } }, @@ -120,12 +144,20 @@ test.register_message_test( { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } } }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device_color_temp.id, capability_id = "statelessSwitchLevelStep", capability_cmd_id = "stepLevel" } + } + }, { 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) + clusters.LevelControl.server.commands.Step(mock_device_color_temp, 1, clusters.LevelControl.types.StepModeEnum.UP, 64, fields.DEFAULT_STEP_TRANSITION_TIME, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) }, }, { @@ -136,12 +168,20 @@ test.register_message_test( { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { -50 } } } }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device_color_temp.id, capability_id = "statelessSwitchLevelStep", capability_cmd_id = "stepLevel" } + } + }, { 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) + clusters.LevelControl.server.commands.Step(mock_device_color_temp, 1, clusters.LevelControl.types.StepModeEnum.DOWN, 127, fields.DEFAULT_STEP_TRANSITION_TIME, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) }, }, { @@ -152,12 +192,20 @@ test.register_message_test( { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 100 } } } }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device_color_temp.id, capability_id = "statelessSwitchLevelStep", capability_cmd_id = "stepLevel" } + } + }, { 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) + clusters.LevelControl.server.commands.Step(mock_device_color_temp, 1, clusters.LevelControl.types.StepModeEnum.UP, 254, fields.DEFAULT_STEP_TRANSITION_TIME, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) }, } }, diff --git a/drivers/SmartThings/matter-window-covering/src/init.lua b/drivers/SmartThings/matter-window-covering/src/init.lua index 6759560c50..a98f8a321d 100644 --- a/drivers/SmartThings/matter-window-covering/src/init.lua +++ b/drivers/SmartThings/matter-window-covering/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 + --Note: Currently only support for window shades with the PositionallyAware Feature --Note: No support for setting device into calibration mode, it must be done manually @@ -376,11 +366,9 @@ local matter_driver_template = { capabilities.battery, capabilities.batteryLevel, }, - sub_drivers = { - -- for devices sending a position update while device is in motion - require("matter-window-covering-position-updates-while-moving") - } + sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } local matter_driver = MatterDriver("matter-window-covering", matter_driver_template) -matter_driver:run() \ No newline at end of file +matter_driver:run() diff --git a/drivers/SmartThings/matter-window-covering/src/lazy_load_subdriver.lua b/drivers/SmartThings/matter-window-covering/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..a04740d267 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/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-window-covering/src/matter-window-covering-position-updates-while-moving/can_handle.lua b/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/can_handle.lua new file mode 100644 index 0000000000..ba9207f3d2 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_matter_window_covering_position_updates_while_moving(opts, driver, device) + local device_lib = require "st.device" + if device.network_type ~= device_lib.NETWORK_TYPE_MATTER then + return false + end + local FINGERPRINTS = require("matter-window-covering-position-updates-while-moving.fingerprints") + for i, v in ipairs(FINGERPRINTS) do + if device.manufacturer_info.vendor_id == v[1] and + device.manufacturer_info.product_id == v[2] then + return true, require("matter-window-covering-position-updates-while-moving") + end + end + return false +end + +return is_matter_window_covering_position_updates_while_moving diff --git a/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/fingerprints.lua b/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/fingerprints.lua new file mode 100644 index 0000000000..37800fc680 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SUB_WINDOW_COVERING_VID_PID = { + {0x10e1, 0x1005} -- VDA +} + +return SUB_WINDOW_COVERING_VID_PID diff --git a/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/init.lua b/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/init.lua index 11ad2d8ef9..56407c6667 100644 --- a/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/init.lua +++ b/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/init.lua @@ -1,20 +1,9 @@ --- 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" -local device_lib = require "st.device" local DEFAULT_LEVEL = 0 local STATE_MACHINE = "__state_machine" @@ -27,22 +16,7 @@ local StateMachineEnum = { STATE_CURRENT_POSITION_FIRED = 0x03 } -local SUB_WINDOW_COVERING_VID_PID = { - {0x10e1, 0x1005} -- VDA -} -local function is_matter_window_covering_position_updates_while_moving(opts, driver, device) - if device.network_type ~= device_lib.NETWORK_TYPE_MATTER then - return false - end - for i, v in ipairs(SUB_WINDOW_COVERING_VID_PID) do - if device.manufacturer_info.vendor_id == v[1] and - device.manufacturer_info.product_id == v[2] then - return true - end - end - return false -end local function device_init(driver, device) device:subscribe() @@ -145,7 +119,8 @@ local matter_window_covering_position_updates_while_moving_handler = { }, capability_handlers = { }, - can_handle = is_matter_window_covering_position_updates_while_moving, + can_handle = require("matter-window-covering-position-updates-while-moving.can_handle"), + shared_device_thread_enabled = true, } return matter_window_covering_position_updates_while_moving_handler diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers.lua new file mode 100644 index 0000000000..ff048340d0 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/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("matter-window-covering-position-updates-while-moving"), +} +return sub_drivers 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 062b199ea1..9f273037f3 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 @@ -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/philips-hue/profiles/legacy-color.yml b/drivers/SmartThings/philips-hue/profiles/legacy-color.yml index fa3adedb6d..2d906dab24 100644 --- a/drivers/SmartThings/philips-hue/profiles/legacy-color.yml +++ b/drivers/SmartThings/philips-hue/profiles/legacy-color.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorControl version: 1 - id: samsungim.hueSyncMode diff --git a/drivers/SmartThings/philips-hue/profiles/white-ambiance.yml b/drivers/SmartThings/philips-hue/profiles/white-ambiance.yml index b7c6efc7eb..354b8bbc2e 100644 --- a/drivers/SmartThings/philips-hue/profiles/white-ambiance.yml +++ b/drivers/SmartThings/philips-hue/profiles/white-ambiance.yml @@ -6,8 +6,12 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 + - id: statelessColorTemperatureStep + version: 1 - id: samsungim.hueSyncMode version: 1 - id: refresh diff --git a/drivers/SmartThings/philips-hue/profiles/white-and-color-ambiance.yml b/drivers/SmartThings/philips-hue/profiles/white-and-color-ambiance.yml index 35fa5550bd..7fe8797be2 100644 --- a/drivers/SmartThings/philips-hue/profiles/white-and-color-ambiance.yml +++ b/drivers/SmartThings/philips-hue/profiles/white-and-color-ambiance.yml @@ -6,10 +6,14 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorControl version: 1 - id: colorTemperature version: 1 + - id: statelessColorTemperatureStep + version: 1 - id: samsungim.hueSyncMode version: 1 - id: refresh diff --git a/drivers/SmartThings/philips-hue/profiles/white.yml b/drivers/SmartThings/philips-hue/profiles/white.yml index 447ddcad81..18312c48e2 100644 --- a/drivers/SmartThings/philips-hue/profiles/white.yml +++ b/drivers/SmartThings/philips-hue/profiles/white.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: samsungim.hueSyncMode version: 1 - id: refresh diff --git a/drivers/SmartThings/philips-hue/src/handlers/commands.lua b/drivers/SmartThings/philips-hue/src/handlers/commands.lua index cf30834a2c..6bee932239 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/commands.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/commands.lua @@ -19,9 +19,7 @@ local CommandHandlers = {} ---@param driver HueDriver ---@param device HueChildDevice ----@param args table -local function do_switch_action(driver, device, args) - local on = args.command == "on" +local function get_light_device_id_and_hue_api_module(driver, device) local id = device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) local bridge_device = utils.get_hue_bridge_for_device(driver, device, id) @@ -49,14 +47,19 @@ local function do_switch_action(driver, device, args) return end - local resp, err = hue_api:set_light_on_state(light_id, on) + return light_id, hue_api +end - if not resp or (resp.errors and #resp.errors == 0) then +---@param response table? Command response from the Hue API, expected to have an 'errors' field if there were issues +---@param err string? Error message returned from the Hue API call, if any +---@param action_desc string Description of the action being performed, for logging purposes +local function log_command_response_errors(response, err, action_desc) + if not response or (response.errors and #response.errors == 0) then if err ~= nil then - log.error_with({ hub_logs = true }, "Error performing on/off action: " .. err) - elseif resp and #resp.errors > 0 then - for _, error in ipairs(resp.errors) do - log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description) + log.error_with({ hub_logs = true }, "Error performing " .. action_desc .. ": " .. err) + elseif response and #response.errors > 0 then + for _, error in ipairs(response.errors) do + log.error_with({ hub_logs = true }, "Error returned in Hue response for " .. action_desc .. ": " .. error.description) end end end @@ -65,60 +68,30 @@ end ---@param driver HueDriver ---@param device HueChildDevice ---@param args table -local function do_switch_level_action(driver, device, args) - local level = st_utils.clamp_value(args.args.level, 1, 100) - local id = device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) - local bridge_device = utils.get_hue_bridge_for_device(driver, device, id) - - if not bridge_device then - log.warn( - "Couldn't get a bridge for light with Child Key " .. - (device.parent_assigned_child_key or "unexpected nil parent_assigned_child_key")) - return - end +local function do_switch_action(driver, device, args) + local on = args.command == "on" + local light_id, hue_api = get_light_device_id_and_hue_api_module(driver, device) + if not (light_id and hue_api) then return end - local light_id = utils.get_hue_rid(device) - local hue_api = bridge_device:get_field(Fields.BRIDGE_API) --[[@as PhilipsHueApi]] + local resp, err = hue_api:set_light_on_state(light_id, on) + log_command_response_errors(resp, err, "on/off action") +end - if not (light_id and hue_api) then - log.warn( - string.format( - "Could not get a proper light resource ID or API instance for %s" .. - "\n\tLight Resource ID: %s" .. - "\n\tHue API nil? %s", - (device.label or device.id or "unknown device"), - light_id, - (hue_api == nil) - ) - ) - return - end +---@param driver HueDriver +---@param device HueChildDevice +---@param args table +local function do_switch_level_action(driver, device, args) + local level = st_utils.clamp_value(args.args.level, 1, 100) + local light_id, hue_api = get_light_device_id_and_hue_api_module(driver, device) + if not (light_id and hue_api) then return end local is_off = device:get_field(Fields.SWITCH_STATE) == "off" - if is_off then local resp, err = hue_api:set_light_on_state(light_id, true) - if not resp or (resp.errors and #resp.errors == 0) then - if err ~= nil then - log.error_with({ hub_logs = true }, "Error performing on/off action: " .. err) - elseif resp and #resp.errors > 0 then - for _, error in ipairs(resp.errors) do - log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description) - end - end - end + log_command_response_errors(resp, err, "on/off action") end - local resp, err = hue_api:set_light_level(light_id, level) - if not resp or (resp.errors and #resp.errors == 0) then - if err ~= nil then - log.error_with({ hub_logs = true }, "Error performing switch level action: " .. err) - elseif resp and #resp.errors > 0 then - for _, error in ipairs(resp.errors) do - log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description) - end - end - end + log_command_response_errors(resp, err, "switch level action") end ---@param driver HueDriver @@ -130,46 +103,13 @@ local function do_color_action(driver, device, args) hue = 0 device:set_field(Fields.WRAPPED_HUE, true) end - local id = device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) - local bridge_device = utils.get_hue_bridge_for_device(driver, device, id) - - if not bridge_device then - log.warn( - "Couldn't get a bridge for light with Child Key " .. - (device.parent_assigned_child_key or "unexpected nil parent_assigned_child_key")) - return - end - - local light_id = utils.get_hue_rid(device) - local hue_api = bridge_device:get_field(Fields.BRIDGE_API) --[[@as PhilipsHueApi]] - - if not (light_id and hue_api) then - log.warn( - string.format( - "Could not get a proper light resource ID or API instance for %s" .. - "\n\tLight Resource ID: %s" .. - "\n\tHue API nil? %s", - (device.label or device.id or "unknown device"), - light_id, - (hue_api == nil) - ) - ) - return - end + local light_id, hue_api = get_light_device_id_and_hue_api_module(driver, device) + if not (light_id and hue_api) then return end local red, green, blue = st_utils.hsv_to_rgb(hue, sat) local xy = HueColorUtils.safe_rgb_to_xy(red, green, blue, device:get_field(Fields.GAMUT)) - local resp, err = hue_api:set_light_color_xy(light_id, xy) - if not resp or (resp.errors and #resp.errors == 0) then - if err ~= nil then - log.error_with({ hub_logs = true }, "Error performing color action: " .. err) - elseif resp and #resp.errors > 0 then - for _, error in ipairs(resp.errors) do - log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description) - end - end - end + log_command_response_errors(resp, err, "color action") end -- Function to allow changes to "setHue" attribute to Philips Hue light devices @@ -207,51 +147,58 @@ end ---@param args table local function do_color_temp_action(driver, device, args) local kelvin = args.args.temperature - local id = device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) - local bridge_device = utils.get_hue_bridge_for_device(driver, device, id) - - if not bridge_device then - log.warn( - "Couldn't get a bridge for light with Child Key " .. - (device.parent_assigned_child_key or "unexpected nil parent_assigned_child_key")) - return - end - - local light_id = utils.get_hue_rid(device) - local hue_api = bridge_device:get_field(Fields.BRIDGE_API) --[[@as PhilipsHueApi]] - - if not (light_id and hue_api) then - log.warn( - string.format( - "Could not get a proper light resource ID or API instance for %s" .. - "\n\tLight Resource ID: %s" .. - "\n\tHue API nil? %s", - (device.label or device.id or "unknown device"), - light_id, - (hue_api == nil) - ) - ) - return - end + local light_id, hue_api = get_light_device_id_and_hue_api_module(driver, device) + if not (light_id and hue_api) then return end local min = device:get_field(Fields.MIN_KELVIN) or Consts.MIN_TEMP_KELVIN_WHITE_AMBIANCE local clamped_kelvin = st_utils.clamp_value(kelvin, min, Consts.MAX_TEMP_KELVIN) local mirek = math.floor(utils.kelvin_to_mirek(clamped_kelvin)) local resp, err = hue_api:set_light_color_temp(light_id, mirek) - - if not resp or (resp.errors and #resp.errors == 0) then - if err ~= nil then - log.error_with({ hub_logs = true }, "Error performing color temp action: " .. err) - elseif resp and #resp.errors > 0 then - for _, error in ipairs(resp.errors) do - log.error_with({ hub_logs = true }, "Error returned in Hue response: " .. error.description) - end - end - end + log_command_response_errors(resp, err, "color temp action") device:set_field(Fields.COLOR_TEMP_SETPOINT, clamped_kelvin); end + +---@param driver HueDriver +---@param device HueChildDevice +---@param args table +local function do_step_level_action(driver, device, args) + local step_percent = args.args and args.args.stepSize or 0 + if step_percent == 0 then return end + local light_id, hue_api = get_light_device_id_and_hue_api_module(driver, device) + if not (light_id and hue_api) then return end + + -- stepSize is already in percent; Hue brightness_delta is also in percent + local action = (step_percent > 0) and "up" or "down" + local brightness_delta = math.abs(step_percent) + local resp, err = hue_api:set_light_level_delta(light_id, brightness_delta, action) + log_command_response_errors(resp, err, "step level action") +end + +---@param driver HueDriver +---@param device HueChildDevice +---@param args table +local function do_step_color_temp_action(driver, device, args) + local step_percent = args.args and args.args.stepSize or 0 + if step_percent == 0 then return end + local light_id, hue_api = get_light_device_id_and_hue_api_module(driver, device) + if not (light_id and hue_api) then return end + + -- Reminder, stepSize > 0 == Kelvin UP == Mireds DOWN. stepSize < 0 == Kelvin DOWN == Mireds UP + local action = (step_percent > 0) and "down" or "up" + + -- Derive the mirek range from stored Kelvin bounds (note: higher Kelvin = lower mirek) + local min_kelvin = device:get_field(Fields.MIN_KELVIN) or Consts.MIN_TEMP_KELVIN_WHITE_AMBIANCE + local max_kelvin = device:get_field(Fields.MAX_KELVIN) or Consts.MAX_TEMP_KELVIN + local min_mirek = math.floor(utils.kelvin_to_mirek(max_kelvin)) + local max_mirek = math.ceil(utils.kelvin_to_mirek(min_kelvin)) + local mirek_delta = st_utils.round((max_mirek - min_mirek) * (math.abs(step_percent) / 100.0)) + + local resp, err = hue_api:set_light_color_temp_delta(light_id, mirek_delta, action) + log_command_response_errors(resp, err, "step color temp action") +end + ---@param driver HueDriver ---@param device HueChildDevice ---@param args table @@ -301,6 +248,20 @@ function CommandHandlers.set_color_temp_handler(driver, device, args) do_color_temp_action(driver, device, args) end +---@param driver HueDriver +---@param device HueChildDevice +---@param args table +function CommandHandlers.step_level_handler(driver, device, args) + do_step_level_action(driver, device, args) +end + +---@param driver HueDriver +---@param device HueChildDevice +---@param args table +function CommandHandlers.step_color_temp_handler(driver, device, args) + do_step_color_temp_action(driver, device, args) +end + local refresh_handlers = require "handlers.refresh_handlers" ---@param driver HueDriver diff --git a/drivers/SmartThings/philips-hue/src/hue/api.lua b/drivers/SmartThings/philips-hue/src/hue/api.lua index e05fa3d0a0..6039b395cc 100644 --- a/drivers/SmartThings/philips-hue/src/hue/api.lua +++ b/drivers/SmartThings/philips-hue/src/hue/api.lua @@ -507,4 +507,52 @@ function PhilipsHueApi:set_light_color_temp_by_device_type(id, mirek, device_typ end end +---@param id string +---@param brightness_delta number absolute brightness percentage delta +---@param action "up"|"down" +---@return { errors: table[], [string]: any }? response json payload in response to the request, nil on error +---@return string? err error, nil on successful HTTP request but the response may indicate a problem with the request itself. +function PhilipsHueApi:set_light_level_delta(id, brightness_delta, action) + return self:set_light_level_delta_by_device_type(id, brightness_delta, action, HueDeviceTypes.LIGHT) +end + +function PhilipsHueApi:set_grouped_light_level_delta(id, brightness_delta, action) + return self:set_light_level_delta_by_device_type(id, brightness_delta, action, GROUPED_LIGHT) +end + +function PhilipsHueApi:set_light_level_delta_by_device_type(id, brightness_delta, action, device_type) + if type(brightness_delta) == "number" then + local url = string.format("/clip/v2/resource/%s/%s", device_type, id) + local payload = json.encode { dimming_delta = { action = action, brightness_delta = brightness_delta } } + return do_put(self, url, payload) + else + return nil, + string.format("Expected number for brightness delta, received %s", st_utils.stringify_table(brightness_delta, nil, false)) + end +end + +---@param id string +---@param mirek_delta number absolute mirek delta +---@param action "up"|"down" +---@return { errors: table[], [string]: any }? response json payload in response to the request, nil on error +---@return string? err error, nil on successful HTTP request but the response may indicate a problem with the request itself. +function PhilipsHueApi:set_light_color_temp_delta(id, mirek_delta, action) + return self:set_light_color_temp_delta_by_device_type(id, mirek_delta, action, HueDeviceTypes.LIGHT) +end + +function PhilipsHueApi:set_grouped_light_color_temp_delta(id, mirek_delta, action) + return self:set_light_color_temp_delta_by_device_type(id, mirek_delta, action, GROUPED_LIGHT) +end + +function PhilipsHueApi:set_light_color_temp_delta_by_device_type(id, mirek_delta, action, device_type) + if type(mirek_delta) == "number" then + local url = string.format("/clip/v2/resource/%s/%s", device_type, id) + local payload = json.encode { color_temperature_delta = { action = action, mirek_delta = mirek_delta } } + return do_put(self, url, payload) + else + return nil, + string.format("Expected number for color temp mirek delta, received %s", st_utils.stringify_table(mirek_delta, nil, false)) + end +end + return PhilipsHueApi diff --git a/drivers/SmartThings/philips-hue/src/hue_driver_template.lua b/drivers/SmartThings/philips-hue/src/hue_driver_template.lua index 3ffd02b3b6..1c32b260e9 100644 --- a/drivers/SmartThings/philips-hue/src/hue_driver_template.lua +++ b/drivers/SmartThings/philips-hue/src/hue_driver_template.lua @@ -59,6 +59,8 @@ local set_color_handler = utils.safe_wrap_handler(command_handlers.set_color_han local set_hue_handler = utils.safe_wrap_handler(command_handlers.set_hue_handler) local set_saturation_handler = utils.safe_wrap_handler(command_handlers.set_saturation_handler) local set_color_temp_handler = utils.safe_wrap_handler(command_handlers.set_color_temp_handler) +local step_level_handler = utils.safe_wrap_handler(command_handlers.step_level_handler) +local step_color_temp_handler = utils.safe_wrap_handler(command_handlers.step_color_temp_handler) --- @class HueDriverDatastore --- @field public bridge_netinfo table @@ -105,6 +107,12 @@ function HueDriver.new_driver_template(dbg_config) [capabilities.colorTemperature.ID] = { [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temp_handler, }, + [capabilities.statelessSwitchLevelStep.ID] = { + [capabilities.statelessSwitchLevelStep.commands.stepLevel.NAME] = step_level_handler, + }, + [capabilities.statelessColorTemperatureStep.ID] = { + [capabilities.statelessColorTemperatureStep.commands.stepColorTemperatureByPercent.NAME] = step_color_temp_handler, + }, }, -- override the default capability message handler if batched receives are supported diff --git a/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua b/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua index 993ba2a96d..49e2f79a88 100755 --- a/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua +++ b/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua @@ -33,7 +33,8 @@ local zigbee_air_quality_detector_template = { capabilities.tvocMeasurement, capabilities.tvocHealthConcern }, - sub_drivers = { require("MultiIR") } + sub_drivers = { require("MultiIR") }, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_air_quality_detector_template, zigbee_air_quality_detector_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-bed/src/init.lua b/drivers/SmartThings/zigbee-bed/src/init.lua index 9f464c38ce..9d3e798b8b 100755 --- a/drivers/SmartThings/zigbee-bed/src/init.lua +++ b/drivers/SmartThings/zigbee-bed/src/init.lua @@ -12,6 +12,7 @@ local zigbee_bed_template = { }, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_bed_template, zigbee_bed_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-button/src/init.lua b/drivers/SmartThings/zigbee-button/src/init.lua index 8ed0db27db..cb1565ea74 100644 --- a/drivers/SmartThings/zigbee-button/src/init.lua +++ b/drivers/SmartThings/zigbee-button/src/init.lua @@ -136,6 +136,7 @@ local zigbee_button_driver_template = { }, ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_button_driver_template, zigbee_button_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua index 4ddb66aa4a..8a4f5b5c01 100644 --- a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua @@ -21,6 +21,7 @@ local zigbee_carbon_monoxide_driver_template = { ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, health_check = false, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_carbon_monoxide_driver_template, diff --git a/drivers/SmartThings/zigbee-contact/src/init.lua b/drivers/SmartThings/zigbee-contact/src/init.lua index 63a7bf7565..0258d5360a 100644 --- a/drivers/SmartThings/zigbee-contact/src/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/init.lua @@ -90,6 +90,7 @@ local zigbee_contact_driver_template = { sub_drivers = require("sub_drivers"), ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_contact_driver_template, diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/init.lua index be7f8ad147..ed6969cbc8 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/init.lua @@ -32,6 +32,7 @@ local zigbee_dimmer_remote_driver_template = { }, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_dimmer_remote_driver_template, zigbee_dimmer_remote_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-fan/src/init.lua b/drivers/SmartThings/zigbee-fan/src/init.lua index c4a7185838..33af1cb320 100644 --- a/drivers/SmartThings/zigbee-fan/src/init.lua +++ b/drivers/SmartThings/zigbee-fan/src/init.lua @@ -27,6 +27,7 @@ local zigbee_fan_driver = { init = device_init }, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_fan_driver,zigbee_fan_driver.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua index 9ca7cd734d..6bde23ed25 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua @@ -70,6 +70,7 @@ local zigbee_humidity_driver = { }, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_humidity_driver, zigbee_humidity_driver.supported_capabilities, {native_capability_attrs_enabled = true}) diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua index 7f84eb68dc..6f9d3c5554 100644 --- a/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua @@ -21,6 +21,7 @@ local zigbee_illuminance_driver = { }, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_illuminance_driver, zigbee_illuminance_driver.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index 94f5adc0c4..1ac2598e2f 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -442,6 +442,7 @@ local zigbee_lock_driver = { init = init, }, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_lock_driver, zigbee_lock_driver.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/init.lua index 660948720b..9c3f33ad14 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/init.lua @@ -125,6 +125,7 @@ local zigbee_motion_driver = { }, ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_motion_driver, zigbee_motion_driver.supported_capabilities, {native_capability_attrs_enabled = true}) diff --git a/drivers/SmartThings/zigbee-power-meter/src/init.lua b/drivers/SmartThings/zigbee-power-meter/src/init.lua index 6aa3d4b8c1..4f69f0e72e 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/init.lua @@ -60,6 +60,7 @@ local zigbee_power_meter_driver_template = { doConfigure = do_configure, }, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_power_meter_driver_template, zigbee_power_meter_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/init.lua b/drivers/SmartThings/zigbee-presence-sensor/src/init.lua index 261bc90922..d797a09b62 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/init.lua @@ -196,6 +196,7 @@ local zigbee_presence_driver = { zigbee_message_handler = all_zigbee_message_handler, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_presence_driver, zigbee_presence_driver.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-range-extender/src/init.lua b/drivers/SmartThings/zigbee-range-extender/src/init.lua index 565aa74a28..f6ba3c1ffc 100644 --- a/drivers/SmartThings/zigbee-range-extender/src/init.lua +++ b/drivers/SmartThings/zigbee-range-extender/src/init.lua @@ -23,6 +23,7 @@ local zigbee_range_driver_template = { }, health_check = false, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_range_driver_template, zigbee_range_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-sensor/src/init.lua b/drivers/SmartThings/zigbee-sensor/src/init.lua index 487c19a733..348d186613 100644 --- a/drivers/SmartThings/zigbee-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-sensor/src/init.lua @@ -103,8 +103,9 @@ local zigbee_generic_sensor_template = { }, ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_generic_sensor_template, zigbee_generic_sensor_template.supported_capabilities) local zigbee_sensor = ZigbeeDriver("zigbee-sensor", zigbee_generic_sensor_template) -zigbee_sensor:run() \ No newline at end of file +zigbee_sensor:run() diff --git a/drivers/SmartThings/zigbee-siren/src/init.lua b/drivers/SmartThings/zigbee-siren/src/init.lua index b1ad81c9c6..8e4812c3bd 100644 --- a/drivers/SmartThings/zigbee-siren/src/init.lua +++ b/drivers/SmartThings/zigbee-siren/src/init.lua @@ -197,8 +197,9 @@ local zigbee_siren_driver_template = { } }, health_check = false, + shared_device_thread_enabled = true, } 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() \ No newline at end of file +zigbee_siren:run() diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/init.lua index fe64260c11..0f1ba0720b 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/init.lua @@ -18,6 +18,7 @@ local zigbee_smoke_driver_template = { sub_drivers = require("sub_drivers"), ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_smoke_driver_template, diff --git a/drivers/SmartThings/zigbee-switch/profiles/abl-light-z-001-bulb.yml b/drivers/SmartThings/zigbee-switch/profiles/abl-light-z-001-bulb.yml index 8a0d62b1f6..28d233316c 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/abl-light-z-001-bulb.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/abl-light-z-001-bulb.yml @@ -6,8 +6,12 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/aqara-led-bulb.yml b/drivers/SmartThings/zigbee-switch/profiles/aqara-led-bulb.yml index 65ee11beb0..c3d8f2f16d 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/aqara-led-bulb.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/aqara-led-bulb.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: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/aqara-light.yml b/drivers/SmartThings/zigbee-switch/profiles/aqara-light.yml index 6c2da08393..07cc4b4904 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/aqara-light.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/aqara-light.yml @@ -10,8 +10,12 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-bulb.yml b/drivers/SmartThings/zigbee-switch/profiles/color-bulb.yml index fbe4243f6a..8395432142 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-bulb.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-bulb.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2000K-6500K.yml b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2000K-6500K.yml index fae09d20cb..43b1953caf 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2000K-6500K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2000K-6500K.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: [ 2000, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-4000K.yml b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-4000K.yml index 0e542eff43..4a81aac14c 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-4000K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-4000K.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, 4000 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-5000K.yml b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-5000K.yml index e8495a5b6c..420f43c959 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-5000K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-5000K.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, 5000 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-6500K.yml b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-6500K.yml index 985ec05a4f..221be95e5d 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-6500K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2200K-6500K.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/zigbee-switch/profiles/color-temp-bulb-2500K-6000K.yml b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2500K-6000K.yml index e6ffe1a46f..7715942435 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2500K-6000K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2500K-6000K.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: [ 2500, 6000 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2700K-5000K.yml b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2700K-5000K.yml index 15677d1307..a1b8fa2b7c 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2700K-5000K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2700K-5000K.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, 5000 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2700K-6500K.yml b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2700K-6500K.yml index b56cb5f84e..2cbe79b906 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2700K-6500K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb-2700K-6500K.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: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb.yml b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb.yml index c30ba1d25f..e882f23098 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/color-temp-bulb.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: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/profiles/ge-link-bulb.yml b/drivers/SmartThings/zigbee-switch/profiles/ge-link-bulb.yml index e0c09885c4..0381032825 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/ge-link-bulb.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/ge-link-bulb.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/zigbee-switch/profiles/inovelli-vzm30-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml index ff670c0097..6a41cae14b 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: temperatureMeasurement version: 1 - id: relativeHumidityMeasurement diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm31-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm31-sn.yml index f39f8324eb..47368ca7e3 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm31-sn.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm31-sn.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/zigbee-switch/profiles/inovelli-vzm32-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml index 746890a15a..fe3b3de11b 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: motionSensor version: 1 - id: illuminanceMeasurement diff --git a/drivers/SmartThings/zigbee-switch/profiles/light-level-power-energy.yml b/drivers/SmartThings/zigbee-switch/profiles/light-level-power-energy.yml index b45fc5e0e8..ac516c23c0 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/light-level-power-energy.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/light-level-power-energy.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/zigbee-switch/profiles/light-level-power.yml b/drivers/SmartThings/zigbee-switch/profiles/light-level-power.yml index 6eca96ab18..a07047269b 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/light-level-power.yml +++ b/drivers/SmartThings/zigbee-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/zigbee-switch/profiles/on-off-level-intensity.yml b/drivers/SmartThings/zigbee-switch/profiles/on-off-level-intensity.yml index 0d8688cb6b..dc5e6e0ca2 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/on-off-level-intensity.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/on-off-level-intensity.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/zigbee-switch/profiles/on-off-level-motion-sensor.yml b/drivers/SmartThings/zigbee-switch/profiles/on-off-level-motion-sensor.yml index 248bd66e7f..f344f32a34 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/on-off-level-motion-sensor.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/on-off-level-motion-sensor.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/zigbee-switch/profiles/on-off-level-no-firmware-update.yml b/drivers/SmartThings/zigbee-switch/profiles/on-off-level-no-firmware-update.yml index a25ef4aa2c..f5ef30908f 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/on-off-level-no-firmware-update.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/on-off-level-no-firmware-update.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: refresh version: 1 categories: diff --git a/drivers/SmartThings/zigbee-switch/profiles/on-off-level.yml b/drivers/SmartThings/zigbee-switch/profiles/on-off-level.yml index 350c51c722..67f27f7289 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/on-off-level.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/on-off-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/zigbee-switch/profiles/plug-level-power.yml b/drivers/SmartThings/zigbee-switch/profiles/plug-level-power.yml index d6ac987d50..a234f15409 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/plug-level-power.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/plug-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/zigbee-switch/profiles/rgbw-bulb-1800K-6500K.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-1800K-6500K.yml index c95d6c4b16..6f6a78a4b5 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-1800K-6500K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-1800K-6500K.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: [ 1800, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2000K-6500K.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2000K-6500K.yml index 89af9a7f94..bf7f81832d 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2000K-6500K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2000K-6500K.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: [ 2000, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-4000K.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-4000K.yml index 2e17ba527d..b1c7d3e379 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-4000K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-4000K.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, 4000 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-5000K.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-5000K.yml index d83b671f12..9032ba0fe0 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-5000K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-5000K.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, 5000 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-6500K.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-6500K.yml index 0d334ca62e..1a13390cf5 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-6500K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2200K-6500K.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/zigbee-switch/profiles/rgbw-bulb-2500K-6000K.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2500K-6000K.yml index 3bd54e3a59..c74ba232c0 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2500K-6000K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2500K-6000K.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: [ 2500, 6000 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2700K-5000K.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2700K-5000K.yml index 740a002b83..466a34c06a 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2700K-5000K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2700K-5000K.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, 5000 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2700K-6500K.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2700K-6500K.yml index 8878a04a99..dcab0e8224 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2700K-6500K.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb-2700K-6500K.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: [ 2700, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb.yml b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb.yml index b863f9e587..57f566bdfb 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/rgbw-bulb.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: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-switch/profiles/switch-dimmer-power-energy.yml b/drivers/SmartThings/zigbee-switch/profiles/switch-dimmer-power-energy.yml index 4507ab5282..623156bf6c 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/switch-dimmer-power-energy.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/switch-dimmer-power-energy.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/zigbee-switch/profiles/switch-level-power.yml b/drivers/SmartThings/zigbee-switch/profiles/switch-level-power.yml index 2042471bf3..43b2b26581 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/switch-level-power.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/switch-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/zigbee-switch/profiles/switch-level.yml b/drivers/SmartThings/zigbee-switch/profiles/switch-level.yml index 96166ef0d9..abcaba3e21 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/switch-level.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/switch-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/zigbee-switch/src/color_temp_range_handlers/init.lua b/drivers/SmartThings/zigbee-switch/src/color_temp_range_handlers/init.lua index 761ac49736..84185557af 100644 --- a/drivers/SmartThings/zigbee-switch/src/color_temp_range_handlers/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/color_temp_range_handlers/init.lua @@ -3,56 +3,59 @@ local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" -local utils = require "st.utils" -local KELVIN_MAX = "_max_kelvin" -local KELVIN_MIN = "_min_kelvin" -local MIREDS_CONVERSION_CONSTANT = 1000000 -local COLOR_TEMPERATURE_KELVIN_MAX = 15000 -local COLOR_TEMPERATURE_KELVIN_MIN = 1000 -local COLOR_TEMPERATURE_MIRED_MAX = utils.round(MIREDS_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MIN) -- 1000 -local COLOR_TEMPERATURE_MIRED_MIN = utils.round(MIREDS_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MAX) -- 67 +local switch_utils = require "switch_utils" + +-- These values are a "sanity check" to ensure that max/min values we are getting are reasonable +local COLOR_TEMPERATURE_MIRED_MAX = 1000 -- 1000 Kelvin +local COLOR_TEMPERATURE_MIRED_MIN = 67 -- 15000 Kelvin local function color_temp_min_mireds_handler(driver, device, value, zb_rx) - local temp_in_mired = value.value - local endpoint = zb_rx.address_header.src_endpoint.value - if temp_in_mired == nil then + -- if mired value is nil or outside of sane bounds, log and ignore. Else, save value + local min_mired_bound = value.value + if min_mired_bound == 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)) + elseif (min_mired_bound < COLOR_TEMPERATURE_MIRED_MIN or min_mired_bound > 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", min_mired_bound, COLOR_TEMPERATURE_MIRED_MIN, COLOR_TEMPERATURE_MIRED_MAX)) return end - local temp_in_kelvin = utils.round(MIREDS_CONVERSION_CONSTANT / temp_in_mired) - device:set_field(KELVIN_MAX..endpoint, temp_in_kelvin) - local min = device:get_field(KELVIN_MIN..endpoint) - if min ~= nil then - if temp_in_kelvin > min then - device:emit_event_for_endpoint(endpoint, capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = min, maximum = temp_in_kelvin}})) - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a max color temperature %d K that is not higher than the reported min color temperature %d K", min, temp_in_kelvin)) - end + device:set_field(switch_utils.MIRED_MIN_BOUND, min_mired_bound, {persist = true}) + + -- if we have already received a valid max mired bound, emit a colorTemperatureRange event + local max_mired_bound = device:get_field(switch_utils.MIRED_MAX_BOUND) + if max_mired_bound == nil then + return + elseif min_mired_bound < max_mired_bound then + local endpoint = zb_rx.address_header.src_endpoint.value + local max_kelvin_bound = switch_utils.convert_mired_to_kelvin(min_mired_bound) + local min_kelvin_bound = switch_utils.convert_mired_to_kelvin(max_mired_bound) + device:emit_event_for_endpoint(endpoint, capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = min_kelvin_bound, maximum = max_kelvin_bound}})) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a max color temperature %d Mireds that is not higher than the reported min color temperature %d Mireds", max_mired_bound, min_mired_bound)) end end local function color_temp_max_mireds_handler(driver, device, value, zb_rx) - local temp_in_mired = value.value - local endpoint = zb_rx.address_header.src_endpoint.value - if temp_in_mired == nil then + -- if mired value is nil or outside of sane bounds, log and ignore. Else, save value + local max_mired_bound = value.value + if max_mired_bound == 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)) + elseif (max_mired_bound < COLOR_TEMPERATURE_MIRED_MIN or max_mired_bound > 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", max_mired_bound, COLOR_TEMPERATURE_MIRED_MIN, COLOR_TEMPERATURE_MIRED_MAX)) return end - local temp_in_kelvin = utils.round(MIREDS_CONVERSION_CONSTANT / temp_in_mired) - device:set_field(KELVIN_MIN..endpoint, temp_in_kelvin) - local max = device:get_field(KELVIN_MAX..endpoint) - if max ~= nil then - if temp_in_kelvin < max then - device:emit_event_for_endpoint(endpoint, capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = temp_in_kelvin, 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", temp_in_kelvin, max)) - end + device:set_field(switch_utils.MIRED_MAX_BOUND, max_mired_bound, {persist = true}) + + -- if we have already received a valid min mired bound, emit a colorTemperatureRange event + local min_mired_bound = device:get_field(switch_utils.MIRED_MIN_BOUND) + if min_mired_bound == nil then + return + elseif max_mired_bound > min_mired_bound then + local endpoint = zb_rx.address_header.src_endpoint.value + local max_kelvin_bound = switch_utils.convert_mired_to_kelvin(min_mired_bound) + local min_kelvin_bound = switch_utils.convert_mired_to_kelvin(max_mired_bound) + device:emit_event_for_endpoint(endpoint, capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = min_kelvin_bound, maximum = max_kelvin_bound}})) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min color temperature %d Mireds that is not lower than the reported max color temperature %d Mireds", min_mired_bound, max_mired_bound)) end end diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index 062ac68782..9fbb09119e 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -86,6 +86,7 @@ local zigbee_switch_driver_template = { doConfigure = lazy_handler("lifecycle_handlers.do_configure"), }, health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_switch_driver_template, zigbee_switch_driver_template.supported_capabilities, diff --git a/drivers/SmartThings/zigbee-switch/src/stateless_handlers/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/stateless_handlers/can_handle.lua new file mode 100644 index 0000000000..845bd33156 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/stateless_handlers/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" + +return function(opts, driver, device) + local can_handle = device:supports_capability(capabilities.statelessColorTemperatureStep) + or device:supports_capability(capabilities.statelessSwitchLevelStep) + if can_handle then + local subdriver = require("stateless_handlers") + return true, subdriver + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/stateless_handlers/init.lua b/drivers/SmartThings/zigbee-switch/src/stateless_handlers/init.lua new file mode 100644 index 0000000000..82be544f4a --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/stateless_handlers/init.lua @@ -0,0 +1,66 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local st_utils = require "st.utils" +local clusters = require "st.zigbee.zcl.clusters" +local switch_utils = require "switch_utils" + +-- These values are the mired versions of the config bounds in the default profile (e.g. color-temp-bulb) +local DEFAULT_MIRED_MAX_BOUND = 370 -- 2700 Kelvin (Mireds are the inverse of Kelvin) +local DEFAULT_MIRED_MIN_BOUND = 154 -- 6500 Kelvin (Mireds are the inverse of Kelvin) + +-- Transition Time: The time that shall be taken to perform the step change, in units of 1/10ths of a second. +local DEFAULT_STEP_TRANSITION_TIME = 3 -- 0.3 seconds + +-- Options Mask & Override: Indicates which options are being overridden by the Level/ColorControl cluster commands +local OPTIONS_MASK = 0x01 -- default: The `ExecuteIfOff` option is overriden +local IGNORE_COMMAND_IF_OFF = 0x00 -- default: the command will not be executed if the device is off + +local function step_color_temperature_by_percent_handler(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 step_percent_change = cmd.args and cmd.args.stepSize or 0 + if step_percent_change == 0 then return end + local transition_time = device:get_field(switch_utils.COLOR_TEMP_STEP_TRANSITION_TIME) or DEFAULT_STEP_TRANSITION_TIME + -- Reminder, stepSize > 0 == Kelvin UP == Mireds DOWN. stepSize < 0 == Kelvin DOWN == Mireds UP + local step_mode = (step_percent_change > 0) and clusters.ColorControl.types.CcStepMode.DOWN or clusters.ColorControl.types.CcStepMode.UP + -- note: the field containing the color temp bounds will be associated with a parent device + local field_device = device:get_parent_device() or device + local min_mireds = field_device:get_field(switch_utils.MIRED_MIN_BOUND) + local max_mireds = field_device:get_field(switch_utils.MIRED_MAX_BOUND) + -- since colorTemperatureRange is only set after both custom bounds are, use defaults if any custom bound is missing + if not (min_mireds and max_mireds) then + min_mireds = DEFAULT_MIRED_MIN_BOUND + max_mireds = DEFAULT_MIRED_MAX_BOUND + end + 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, step_mode, step_size_in_mireds, transition_time, min_mireds, max_mireds, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF)) +end + +local function step_level_handler(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 step_size = st_utils.round((cmd.args and cmd.args.stepSize or 0)/100.0 * 254) + if step_size == 0 then return end + local transition_time = device:get_field(switch_utils.SWITCH_LEVEL_STEP_TRANSITION_TIME) or DEFAULT_STEP_TRANSITION_TIME + local step_mode = (step_size > 0) and clusters.Level.types.MoveStepMode.UP or clusters.Level.types.MoveStepMode.DOWN + device:send(clusters.Level.server.commands.Step(device, step_mode, math.abs(step_size), transition_time, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF)) +end + +local stateless_handlers = { + NAME = "Zigbee Stateless Step Handlers", + capability_handlers = { + [capabilities.statelessColorTemperatureStep.ID] = { + [capabilities.statelessColorTemperatureStep.commands.stepColorTemperatureByPercent.NAME] = step_color_temperature_by_percent_handler, + }, + [capabilities.statelessSwitchLevelStep.ID] = { + [capabilities.statelessSwitchLevelStep.commands.stepLevel.NAME] = step_level_handler, + }, + }, + can_handle = require("stateless_handlers.can_handle") +} + +return stateless_handlers diff --git a/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua index 07d1fd9c7a..736c2a0464 100644 --- a/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua @@ -36,5 +36,6 @@ return { lazy_load_if_possible("tuya-multi"), lazy_load_if_possible("frient"), lazy_load_if_possible("frient-IO"), - lazy_load_if_possible("color_temp_range_handlers") + lazy_load_if_possible("color_temp_range_handlers"), + lazy_load_if_possible("stateless_handlers") } diff --git a/drivers/SmartThings/zigbee-switch/src/switch_utils.lua b/drivers/SmartThings/zigbee-switch/src/switch_utils.lua index 66ad4715f9..e974d52474 100644 --- a/drivers/SmartThings/zigbee-switch/src/switch_utils.lua +++ b/drivers/SmartThings/zigbee-switch/src/switch_utils.lua @@ -1,8 +1,24 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local st_utils = require "st.utils" + local switch_utils = {} +switch_utils.MIRED_MAX_BOUND = "__max_mired_bound" +switch_utils.MIRED_MIN_BOUND = "__min_mired_bound" + +-- Fields to store the transition times for the stateless capabilities, +-- in case native handler implementations need to be re-configured in the future +switch_utils.SWITCH_LEVEL_STEP_TRANSITION_TIME = "__switch_level_step_transition_time" +switch_utils.COLOR_TEMP_STEP_TRANSITION_TIME = "__color_temp_step_transition_time" + +switch_utils.MIREDS_CONVERSION_CONSTANT = 1000000 + +switch_utils.convert_mired_to_kelvin = function(mired) + return st_utils.round(switch_utils.MIREDS_CONVERSION_CONSTANT / mired) +end + 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) 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 5e27983718..8826ca535d 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 @@ -25,8 +25,10 @@ local zigbee_bulb_all_caps = { capabilities = { [capabilities.switch.ID] = { id = capabilities.switch.ID }, [capabilities.switchLevel.ID] = { id = capabilities.switchLevel.ID }, + [capabilities.statelessSwitchLevelStep.ID] = { id = capabilities.statelessSwitchLevelStep.ID }, [capabilities.colorControl.ID] = { id = capabilities.colorControl.ID }, [capabilities.colorTemperature.ID] = { id = capabilities.colorTemperature.ID }, + [capabilities.statelessColorTemperatureStep.ID] = { id = capabilities.statelessColorTemperatureStep.ID }, [capabilities.powerMeter.ID] = { id = capabilities.powerMeter.ID }, [capabilities.energyMeter.ID] = { id = capabilities.energyMeter.ID }, [capabilities.refresh.ID] = { id = capabilities.refresh.ID }, @@ -294,6 +296,174 @@ test.register_message_test( } ) +local DEFAULT_MIRED_MAX = 370 +local DEFAULT_MIRED_MIN = 154 +local TRANSITION_TIME = 3 +local OPTIONS_MASK = 0x01 +local IGNORE_COMMAND_IF_OFF = 0x00 + +test.register_message_test( + "Step ColorTemperature command test", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 20 } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "statelessColorTemperatureStep", capability_cmd_id = "stepColorTemperatureByPercent" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + ColorControl.server.commands.StepColorTemperature(mock_device, ColorControl.types.CcStepMode.DOWN, 43, TRANSITION_TIME, DEFAULT_MIRED_MIN, DEFAULT_MIRED_MAX, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + }, + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 90 } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "statelessColorTemperatureStep", capability_cmd_id = "stepColorTemperatureByPercent" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + ColorControl.server.commands.StepColorTemperature(mock_device, ColorControl.types.CcStepMode.DOWN, 194, TRANSITION_TIME, DEFAULT_MIRED_MIN, DEFAULT_MIRED_MAX, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + }, + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { -50 } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "statelessColorTemperatureStep", capability_cmd_id = "stepColorTemperatureByPercent" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + ColorControl.server.commands.StepColorTemperature(mock_device, ColorControl.types.CcStepMode.UP, 108, TRANSITION_TIME, DEFAULT_MIRED_MIN, DEFAULT_MIRED_MAX, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + }, + } + }, + { + min_api_version = 19 + } +) + +test.register_message_test( + "Step Level command test", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "statelessSwitchLevelStep", capability_cmd_id = "stepLevel" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + Level.server.commands.Step(mock_device, Level.types.MoveStepMode.UP, 64, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + }, + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { -50 } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "statelessSwitchLevelStep", capability_cmd_id = "stepLevel" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + Level.server.commands.Step(mock_device, Level.types.MoveStepMode.DOWN, 127, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 100 } } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "statelessSwitchLevelStep", capability_cmd_id = "stepLevel" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + Level.server.commands.Step(mock_device, Level.types.MoveStepMode.UP, 254, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + } + } + }, + { + min_api_version = 19 + } +) + test.register_coroutine_test( "lifecycle configure event should configure device", function () 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 356d7dbe31..80241b4ca9 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 @@ -4,6 +4,7 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local OnOff = clusters.OnOff @@ -272,4 +273,51 @@ test.register_coroutine_test( } ) +local TRANSITION_TIME = 3 +local OPTIONS_MASK = 0x01 +local IGNORE_COMMAND_IF_OFF = 0x00 +local REPORTED_MIRED_MIN = 160 +local REPORTED_MIRED_MAX = 370 + +test.register_coroutine_test( + "Step Color Temperature command with device-reported mired range test", + function() + -- Report non-default range values to verify subsequent step commands do not use defaults. + test.socket.zigbee:__queue_receive({mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_attr_report(mock_device, REPORTED_MIRED_MAX)}) + test.socket.zigbee:__queue_receive({mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_attr_report(mock_device, REPORTED_MIRED_MIN)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 2703, maximum = 6250}))) + test.wait_for_events() + + test.socket.capability:__queue_receive({mock_device.id, { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 20 } } }) + mock_device:expect_native_cmd_handler_registration("statelessColorTemperatureStep", "stepColorTemperatureByPercent") + test.socket.zigbee:__expect_send( + { + mock_device.id, + ColorControl.server.commands.StepColorTemperature(mock_device, ColorControl.types.CcStepMode.DOWN, 42, TRANSITION_TIME, REPORTED_MIRED_MIN, REPORTED_MIRED_MAX, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + } + ) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test( + "Step Level command test", + function() + test.socket.capability:__queue_receive({mock_device.id, { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } }) + mock_device:expect_native_cmd_handler_registration("statelessSwitchLevelStep", "stepLevel") + test.socket.zigbee:__expect_send( + { + mock_device.id, + Level.commands.Step(mock_device, Level.types.MoveStepMode.UP, 64, TRANSITION_TIME, OPTIONS_MASK, IGNORE_COMMAND_IF_OFF) + } + ) + test.wait_for_events() + end, + { + min_api_version = 19 + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-thermostat/src/init.lua b/drivers/SmartThings/zigbee-thermostat/src/init.lua index b1766e7892..a72b3c9107 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/init.lua @@ -354,6 +354,7 @@ local zigbee_thermostat_driver = { }, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_thermostat_driver, zigbee_thermostat_driver.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-valve/src/init.lua b/drivers/SmartThings/zigbee-valve/src/init.lua index 1840b55be0..717012e2bb 100644 --- a/drivers/SmartThings/zigbee-valve/src/init.lua +++ b/drivers/SmartThings/zigbee-valve/src/init.lua @@ -43,6 +43,7 @@ local zigbee_valve_driver_template = { }, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_valve_driver_template, zigbee_valve_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua index 4ded4e195c..6f4b879108 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua @@ -81,6 +81,7 @@ local zigbee_water_driver_template = { ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_water_driver_template, diff --git a/drivers/SmartThings/zigbee-watering-kit/src/init.lua b/drivers/SmartThings/zigbee-watering-kit/src/init.lua index 7dd35e6f09..7e23c2fef9 100644 --- a/drivers/SmartThings/zigbee-watering-kit/src/init.lua +++ b/drivers/SmartThings/zigbee-watering-kit/src/init.lua @@ -15,6 +15,7 @@ local zigbee_water_driver_template = { }, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_water_driver_template, zigbee_water_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/init.lua index 16783a8726..0a093645f8 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/init.lua @@ -48,6 +48,7 @@ local zigbee_window_treatment_driver_template = { }, sub_drivers = require("sub_drivers"), health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_window_treatment_driver_template, zigbee_window_treatment_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-bulb/src/init.lua b/drivers/SmartThings/zwave-bulb/src/init.lua index 58e2f32a7e..fd6c9a9b8f 100644 --- a/drivers/SmartThings/zwave-bulb/src/init.lua +++ b/drivers/SmartThings/zwave-bulb/src/init.lua @@ -21,6 +21,7 @@ local driver_template = { capabilities.powerMeter }, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities, {native_capability_cmds_enabled = true}) diff --git a/drivers/SmartThings/zwave-button/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-button/src/apiv6_bugfix/can_handle.lua new file mode 100644 index 0000000000..8a9b8cc6cc --- /dev/null +++ b/drivers/SmartThings/zwave-button/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-button/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-button/src/apiv6_bugfix/init.lua index 0204b7b2d5..fd309d8e29 100644 --- a/drivers/SmartThings/zwave-button/src/apiv6_bugfix/init.lua +++ b/drivers/SmartThings/zwave-button/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,8 @@ local apiv6_bugfix = { } }, NAME = "apiv6_bugfix", - can_handle = can_handle + can_handle = require("apiv6_bugfix.can_handle"), + shared_device_thread_enabled = true, } return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-button/src/configurations.lua b/drivers/SmartThings/zwave-button/src/configurations.lua index 5dc1b96e0f..2c93b5075c 100644 --- a/drivers/SmartThings/zwave-button/src/configurations.lua +++ b/drivers/SmartThings/zwave-button/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 = { AEOTEC_NANOMOTE_ONE = { diff --git a/drivers/SmartThings/zwave-button/src/init.lua b/drivers/SmartThings/zwave-button/src/init.lua index b369197a5b..18469ca56b 100644 --- a/drivers/SmartThings/zwave-button/src/init.lua +++ b/drivers/SmartThings/zwave-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" --- @type st.zwave.defaults @@ -41,10 +31,8 @@ local driver_template = { lifecycle_handlers = { added = added_handler, }, - sub_drivers = { - require("zwave-multi-button"), - require("apiv6_bugfix"), - } + sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-button/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-button/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-button/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-button/src/sub_drivers.lua b/drivers/SmartThings/zwave-button/src/sub_drivers.lua new file mode 100644 index 0000000000..57e87ad8ad --- /dev/null +++ b/drivers/SmartThings/zwave-button/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("zwave-multi-button"), + lazy_load_if_possible("apiv6_bugfix"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_minimote.lua b/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_minimote.lua index 6876b7026f..88bf890686 100644 --- a/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_minimote.lua +++ b/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_minimote.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-button/src/test/test_zwave_aeotec_nanomote_one.lua b/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_nanomote_one.lua index 4d8be4dad2..7df9c6cada 100644 --- a/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_nanomote_one.lua +++ b/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_nanomote_one.lua @@ -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-button/src/test/test_zwave_button.lua b/drivers/SmartThings/zwave-button/src/test/test_zwave_button.lua index f5e16ba158..664d048010 100644 --- a/drivers/SmartThings/zwave-button/src/test/test_zwave_button.lua +++ b/drivers/SmartThings/zwave-button/src/test/test_zwave_button.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-button/src/test/test_zwave_fibaro_button.lua b/drivers/SmartThings/zwave-button/src/test/test_zwave_fibaro_button.lua index e03471594f..5418917ee5 100644 --- a/drivers/SmartThings/zwave-button/src/test/test_zwave_fibaro_button.lua +++ b/drivers/SmartThings/zwave-button/src/test/test_zwave_fibaro_button.lua @@ -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-button/src/test/test_zwave_multi_button.lua b/drivers/SmartThings/zwave-button/src/test/test_zwave_multi_button.lua index 07132534c5..b88a85a800 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 @@ -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-button/src/zwave-multi-button/aeotec-keyfob/can_handle.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/can_handle.lua new file mode 100644 index 0000000000..5a2fec217c --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeotec_keyfob(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-multi-button.aeotec-keyfob.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("zwave-multi-button.aeotec-keyfob") + end + end + return false +end + +return can_handle_aeotec_keyfob diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/fingerprints.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/fingerprints.lua new file mode 100644 index 0000000000..dd6a9c8219 --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZWAVE_AEOTEC_KEYFOB_FINGERPRINTS = { + {mfr = 0x0086, prod = 0x0101, model = 0x0058}, -- Aeotec KeyFob US + {mfr = 0x0086, prod = 0x0001, model = 0x0058}, -- Aeotec KeyFob EU + {mfr = 0x0086, prod = 0x0001, model = 0x0026} -- Aeotec Panic Button +} + +return ZWAVE_AEOTEC_KEYFOB_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/init.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/init.lua index c8b655bff2..13096c0579 100644 --- a/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/init.lua +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-keyfob/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 --- @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 }) -local ZWAVE_AEOTEC_KEYFOB_FINGERPRINTS = { - {mfr = 0x0086, prod = 0x0101, model = 0x0058}, -- Aeotec KeyFob US - {mfr = 0x0086, prod = 0x0001, model = 0x0058}, -- Aeotec KeyFob EU - {mfr = 0x0086, prod = 0x0001, model = 0x0026} -- Aeotec Panic Button -} - -local function can_handle_aeotec_keyfob(opts, driver, device, ...) - for _, fingerprint in ipairs(ZWAVE_AEOTEC_KEYFOB_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:refresh() device:send(Configuration:Set({ configuration_value = 1, parameter_number = 250, size = 1 })) @@ -43,7 +17,8 @@ local aeotec_keyfob = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_aeotec_keyfob, + can_handle = require("zwave-multi-button.aeotec-keyfob.can_handle"), + shared_device_thread_enabled = true, } return aeotec_keyfob diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/can_handle.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/can_handle.lua new file mode 100644 index 0000000000..fb39fa8f70 --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeotec_minimote(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-multi-button.aeotec-minimote.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("zwave-multi-button.aeotec-minimote") + end + end + return false +end + +return can_handle_aeotec_minimote diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/fingerprints.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/fingerprints.lua new file mode 100644 index 0000000000..b63e217394 --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZWAVE_AEOTEC_MINIMOTE_FINGERPRINTS = { + {mfr = 0x0086, prod = 0x0001, model = 0x0003} -- Aeotec Mimimote +} + +return ZWAVE_AEOTEC_MINIMOTE_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/init.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/init.lua index 814bcb775b..3b8a04d90c 100644 --- a/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/init.lua +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/aeotec-minimote/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.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) -local ZWAVE_AEOTEC_MINIMOTE_FINGERPRINTS = { - {mfr = 0x0086, prod = 0x0001, model = 0x0003} -- Aeotec Mimimote -} -local function can_handle_aeotec_minimote(opts, driver, device, ...) - for _, fingerprint in ipairs(ZWAVE_AEOTEC_MINIMOTE_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local function basic_set_handler(self, device, cmd) local button = cmd.args.value // 40 + 1 @@ -59,7 +38,8 @@ local aeotec_minimote = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_aeotec_minimote, + can_handle = require("zwave-multi-button.aeotec-minimote.can_handle"), + shared_device_thread_enabled = true, } return aeotec_minimote diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/can_handle.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/can_handle.lua new file mode 100644 index 0000000000..a9d7d24be2 --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-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_zwave_multi_button(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-multi-button.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("zwave-multi-button") + end + end + return false +end + +return can_handle_zwave_multi_button diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/can_handle.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/can_handle.lua new file mode 100644 index 0000000000..055e4f1eff --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_keyfob(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-multi-button.fibaro-keyfob.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("zwave-multi-button.fibaro-keyfob") + end + end + return false +end + +return can_handle_fibaro_keyfob diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/fingerprints.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/fingerprints.lua new file mode 100644 index 0000000000..269d136689 --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZWAVE_FIBARO_KEYFOB_FINGERPRINTS = { + {mfr = 0x010F, prod = 0x1001, model = 0x1000}, -- Fibaro KeyFob EU + {mfr = 0x010F, prod = 0x1001, model = 0x2000}, -- Fibaro KeyFob US + {mfr = 0x010F, prod = 0x1001, model = 0x3000} -- Fibaro KeyFob AU +} + +return ZWAVE_FIBARO_KEYFOB_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/init.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/init.lua index 653cd21ddd..178d440783 100644 --- a/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/init.lua +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/fibaro-keyfob/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 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 }) -local ZWAVE_FIBARO_KEYFOB_FINGERPRINTS = { - {mfr = 0x010F, prod = 0x1001, model = 0x1000}, -- Fibaro KeyFob EU - {mfr = 0x010F, prod = 0x1001, model = 0x2000}, -- Fibaro KeyFob US - {mfr = 0x010F, prod = 0x1001, model = 0x3000} -- Fibaro KeyFob AU -} -local function can_handle_fibaro_keyfob(opts, driver, device, ...) - for _, fingerprint in ipairs(ZWAVE_FIBARO_KEYFOB_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:refresh() @@ -46,7 +23,8 @@ local fibaro_keyfob = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_fibaro_keyfob, + can_handle = require("zwave-multi-button.fibaro-keyfob.can_handle"), + shared_device_thread_enabled = true, } return fibaro_keyfob diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/fingerprints.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/fingerprints.lua new file mode 100644 index 0000000000..4e539c90ab --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/fingerprints.lua @@ -0,0 +1,23 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZWAVE_MULTI_BUTTON_FINGERPRINTS = { + {mfr = 0x010F, prod = 0x1001, model = 0x1000}, -- Fibaro KeyFob EU + {mfr = 0x010F, prod = 0x1001, model = 0x2000}, -- Fibaro KeyFob US + {mfr = 0x010F, prod = 0x1001, model = 0x3000}, -- Fibaro KeyFob AU + {mfr = 0x0371, prod = 0x0002, model = 0x0003}, -- Aeotec NanoMote Quad EU + {mfr = 0x0371, prod = 0x0102, model = 0x0003}, -- Aeotec NanoMote Quad US + {mfr = 0x0086, prod = 0x0001, model = 0x0058}, -- Aeotec KeyFob EU + {mfr = 0x0086, prod = 0x0101, model = 0x0058}, -- Aeotec KeyFob US + {mfr = 0x0086, prod = 0x0002, model = 0x0082}, -- Aeotec Wallmote Quad EU + {mfr = 0x0086, prod = 0x0102, model = 0x0082}, -- Aeotec Wallmote Quad US + {mfr = 0x0086, prod = 0x0002, model = 0x0081}, -- Aeotec Wallmote EU + {mfr = 0x0086, prod = 0x0102, model = 0x0081}, -- Aeotec Wallmote US + {mfr = 0x0060, prod = 0x000A, model = 0x0003}, -- Everspring Remote Control + {mfr = 0x0086, prod = 0x0001, model = 0x0003}, -- Aeotec Mimimote, + {mfr = 0x0371, prod = 0x0102, model = 0x0016}, -- Aeotec illumino Wallmote 7, + {mfr = 0x0460, prod = 0x0009, model = 0x0081}, -- Shelly Wave i4, + {mfr = 0x0460, prod = 0x0009, model = 0x0082} -- Shelly Wave i4DC, +} + +return ZWAVE_MULTI_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/init.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/init.lua index 9094dc4111..57b9d42be4 100644 --- a/drivers/SmartThings/zwave-button/src/zwave-multi-button/init.lua +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/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,33 +11,7 @@ local CentralScene = (require "st.zwave.CommandClass.CentralScene")({ version=1 --- @type st.zwave.CommandClass.SceneActivation local SceneActivation = (require "st.zwave.CommandClass.SceneActivation")({ version=1 }) -local ZWAVE_MULTI_BUTTON_FINGERPRINTS = { - {mfr = 0x010F, prod = 0x1001, model = 0x1000}, -- Fibaro KeyFob EU - {mfr = 0x010F, prod = 0x1001, model = 0x2000}, -- Fibaro KeyFob US - {mfr = 0x010F, prod = 0x1001, model = 0x3000}, -- Fibaro KeyFob AU - {mfr = 0x0371, prod = 0x0002, model = 0x0003}, -- Aeotec NanoMote Quad EU - {mfr = 0x0371, prod = 0x0102, model = 0x0003}, -- Aeotec NanoMote Quad US - {mfr = 0x0086, prod = 0x0001, model = 0x0058}, -- Aeotec KeyFob EU - {mfr = 0x0086, prod = 0x0101, model = 0x0058}, -- Aeotec KeyFob US - {mfr = 0x0086, prod = 0x0002, model = 0x0082}, -- Aeotec Wallmote Quad EU - {mfr = 0x0086, prod = 0x0102, model = 0x0082}, -- Aeotec Wallmote Quad US - {mfr = 0x0086, prod = 0x0002, model = 0x0081}, -- Aeotec Wallmote EU - {mfr = 0x0086, prod = 0x0102, model = 0x0081}, -- Aeotec Wallmote US - {mfr = 0x0060, prod = 0x000A, model = 0x0003}, -- Everspring Remote Control - {mfr = 0x0086, prod = 0x0001, model = 0x0003}, -- Aeotec Mimimote, - {mfr = 0x0371, prod = 0x0102, model = 0x0016}, -- Aeotec illumino Wallmote 7, - {mfr = 0x0460, prod = 0x0009, model = 0x0081}, -- Shelly Wave i4, - {mfr = 0x0460, prod = 0x0009, model = 0x0082} -- Shelly Wave i4DC, -} -local function can_handle_zwave_multi_button(opts, driver, device, ...) - for _, fingerprint in ipairs(ZWAVE_MULTI_BUTTON_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local map_key_attribute_to_capability = { [CentralScene.key_attributes.KEY_PRESSED_1_TIME] = capabilities.button.button.pushed, @@ -115,12 +80,9 @@ local zwave_multi_button = { lifecycle_handlers = { init = device_init }, - can_handle = can_handle_zwave_multi_button, - sub_drivers = { - require("zwave-multi-button/aeotec-keyfob"), - require("zwave-multi-button/fibaro-keyfob"), - require("zwave-multi-button/aeotec-minimote") - } + can_handle = require("zwave-multi-button.can_handle"), + sub_drivers = require("zwave-multi-button.sub_drivers"), + shared_device_thread_enabled = true, } return zwave_multi_button diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/can_handle.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/can_handle.lua new file mode 100644 index 0000000000..6f532ab2af --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_shelly_wave_i4(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-multi-button.shelly_wave_i4.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("zwave-multi-button.shelly_wave_i4") + end + end + return false +end + +return can_handle_shelly_wave_i4 diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/fingerprints.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/fingerprints.lua new file mode 100644 index 0000000000..459a811d9a --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SHELLY_WAVE_i4_FINGERPRINTS = { + {mfr = 0x0460, prod = 0x0009, model = 0x0081}, -- Shelly Wave i4 + {mfr = 0x0460, prod = 0x0009, model = 0x0082} -- Shelly Wave i4 DC +} + +return SHELLY_WAVE_i4_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/init.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/init.lua index 4e962cebb1..4b78cffac1 100644 --- a/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/init.lua +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/shelly_wave_i4/init.lua @@ -1,35 +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 + -- @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=2 }) -local SHELLY_WAVE_i4_FINGERPRINTS = { - {mfr = 0x0460, prod = 0x0009, model = 0x0081}, -- Shelly Wave i4 - {mfr = 0x0460, prod = 0x0009, model = 0x0082} -- Shelly Wave i4 DC -} -local function can_handle_shelly_wave_i4(opts, driver, device, ...) - for _, fingerprint in ipairs(SHELLY_WAVE_i4_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:refresh() @@ -45,7 +23,8 @@ local shelly_wave_i4 = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_shelly_wave_i4, + can_handle = require("zwave-multi-button.shelly_wave_i4.can_handle"), + shared_device_thread_enabled = true, } return shelly_wave_i4 diff --git a/drivers/SmartThings/zwave-button/src/zwave-multi-button/sub_drivers.lua b/drivers/SmartThings/zwave-button/src/zwave-multi-button/sub_drivers.lua new file mode 100644 index 0000000000..7ec5622dea --- /dev/null +++ b/drivers/SmartThings/zwave-button/src/zwave-multi-button/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-multi-button/aeotec-keyfob"), + lazy_load_if_possible("zwave-multi-button/fibaro-keyfob"), + lazy_load_if_possible("zwave-multi-button/aeotec-minimote"), + lazy_load_if_possible("zwave-multi-button/shelly_wave_i4"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-electric-meter/src/init.lua b/drivers/SmartThings/zwave-electric-meter/src/init.lua index 851de3096f..53c91cfa90 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/init.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/init.lua @@ -43,6 +43,7 @@ local driver_template = { added = device_added }, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-fan/src/init.lua b/drivers/SmartThings/zwave-fan/src/init.lua index acdb34ae76..d3b51d8344 100644 --- a/drivers/SmartThings/zwave-fan/src/init.lua +++ b/drivers/SmartThings/zwave-fan/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 @@ -27,10 +17,8 @@ local driver_template = { capabilities.switch, capabilities.fanSpeed, }, - sub_drivers = { - require("zwave-fan-3-speed"), - require("zwave-fan-4-speed") - }, + sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-fan/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-fan/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-fan/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-fan/src/sub_drivers.lua b/drivers/SmartThings/zwave-fan/src/sub_drivers.lua new file mode 100644 index 0000000000..373e4daf52 --- /dev/null +++ b/drivers/SmartThings/zwave-fan/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("zwave-fan-3-speed"), + lazy_load_if_possible("zwave-fan-4-speed"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_3_speed.lua b/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_3_speed.lua index 01c5a2b856..534c48698e 100644 --- a/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_3_speed.lua +++ b/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_3_speed.lua @@ -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-fan/src/test/test_zwave_fan_4_speed.lua b/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_4_speed.lua index 4c9b252877..80ccef948b 100644 --- a/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_4_speed.lua +++ b/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_4_speed.lua @@ -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-fan/src/zwave-fan-3-speed/can_handle.lua b/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/can_handle.lua new file mode 100644 index 0000000000..66a04b41a9 --- /dev/null +++ b/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_fan_3_speed(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-fan-3-speed.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("zwave-fan-3-speed") + end + end + return false +end + +return is_fan_3_speed diff --git a/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/fingerprints.lua b/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/fingerprints.lua new file mode 100644 index 0000000000..7241769dcd --- /dev/null +++ b/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/fingerprints.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FAN_3_SPEED_FINGERPRINTS = { + {mfr = 0x001D, prod = 0x1001, model = 0x0334}, -- Leviton 3-Speed Fan Controller + {mfr = 0x0063, prod = 0x4944, model = 0x3034}, -- GE In-Wall Smart Fan Control + {mfr = 0x0063, prod = 0x4944, model = 0x3131}, -- GE In-Wall Smart Fan Control + {mfr = 0x0039, prod = 0x4944, model = 0x3131}, -- Honeywell Z-Wave Plus In-Wall Fan Speed Control + {mfr = 0x0063, prod = 0x4944, model = 0x3337}, -- GE In-Wall Smart Fan Control +} + +return FAN_3_SPEED_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/init.lua b/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/init.lua index 282f2e35db..ad629ca54c 100644 --- a/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/init.lua +++ b/drivers/SmartThings/zwave-fan/src/zwave-fan-3-speed/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 log = require "log" local capabilities = require "st.capabilities" @@ -22,13 +12,6 @@ local Basic = (require "st.zwave.CommandClass.Basic")({ version=1 }) local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version=4 }) local fan_speed_helper = (require "zwave_fan_helpers") -local FAN_3_SPEED_FINGERPRINTS = { - {mfr = 0x001D, prod = 0x1001, model = 0x0334}, -- Leviton 3-Speed Fan Controller - {mfr = 0x0063, prod = 0x4944, model = 0x3034}, -- GE In-Wall Smart Fan Control - {mfr = 0x0063, prod = 0x4944, model = 0x3131}, -- GE In-Wall Smart Fan Control - {mfr = 0x0039, prod = 0x4944, model = 0x3131}, -- Honeywell Z-Wave Plus In-Wall Fan Speed Control - {mfr = 0x0063, prod = 0x4944, model = 0x3337}, -- GE In-Wall Smart Fan Control -} local function map_fan_3_speed_to_switch_level (speed) if speed == fan_speed_helper.fan_speed.OFF then @@ -63,14 +46,6 @@ end --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is an 3-speed fan, else false -local function is_fan_3_speed(opts, driver, device, ...) - for _, fingerprint in ipairs(FAN_3_SPEED_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local capability_handlers = {} @@ -110,7 +85,8 @@ local zwave_fan_3_speed = { } }, NAME = "Z-Wave fan 3 speed", - can_handle = is_fan_3_speed, + can_handle = require("zwave-fan-3-speed.can_handle"), + shared_device_thread_enabled = true, } return zwave_fan_3_speed diff --git a/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/can_handle.lua b/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/can_handle.lua new file mode 100644 index 0000000000..b67f3c2ec7 --- /dev/null +++ b/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fan_4_speed(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-fan-4-speed.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("zwave-fan-4-speed") + end + end + return false +end + +return can_handle_fan_4_speed diff --git a/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/fingerprints.lua b/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/fingerprints.lua new file mode 100644 index 0000000000..2e2f2b13fb --- /dev/null +++ b/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FAN_4_SPEED_FINGERPRINTS = { + {mfr = 0x001D, prod = 0x0038, model = 0x0002}, -- Leviton 4-Speed Fan Controller +} + +return FAN_4_SPEED_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/init.lua b/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/init.lua index f714a56b1b..62ec23bfa0 100644 --- a/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/init.lua +++ b/drivers/SmartThings/zwave-fan/src/zwave-fan-4-speed/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 log = require "log" local capabilities = require "st.capabilities" @@ -22,9 +12,6 @@ local Basic = (require "st.zwave.CommandClass.Basic")({ version=1 }) local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version=4 }) local fan_speed_helper = (require "zwave_fan_helpers") -local FAN_4_SPEED_FINGERPRINTS = { - {mfr = 0x001D, prod = 0x0038, model = 0x0002}, -- Leviton 4-Speed Fan Controller -} local function map_fan_4_speed_to_switch_level (speed) if speed == fan_speed_helper.fan_speed.OFF then @@ -64,14 +51,6 @@ end --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is 4-speed fan, else false -local function can_handle_fan_4_speed(opts, driver, device, ...) - for _, fingerprint in ipairs(FAN_4_SPEED_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local capability_handlers = {} @@ -111,7 +90,8 @@ local zwave_fan_4_speed = { } }, NAME = "Z-Wave fan 4 speed", - can_handle = can_handle_fan_4_speed, + can_handle = require("zwave-fan-4-speed.can_handle"), + shared_device_thread_enabled = true, } return zwave_fan_4_speed diff --git a/drivers/SmartThings/zwave-fan/src/zwave_fan_helpers.lua b/drivers/SmartThings/zwave-fan/src/zwave_fan_helpers.lua index bb056cf06a..38c26fa78c 100644 --- a/drivers/SmartThings/zwave-fan/src/zwave_fan_helpers.lua +++ b/drivers/SmartThings/zwave-fan/src/zwave_fan_helpers.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-garage-door-opener/src/ecolink-zw-gdo/can_handle.lua b/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/can_handle.lua new file mode 100644 index 0000000000..571ba6bce1 --- /dev/null +++ b/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_ecolink_garage_door(opts, driver, device, ...) + local FINGERPRINTS = require("ecolink-zw-gdo.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("ecolink-zw-gdo") + end + end + return false +end + +return can_handle_ecolink_garage_door diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/fingerprints.lua b/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/fingerprints.lua new file mode 100644 index 0000000000..15de27ad0b --- /dev/null +++ b/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Ecolink garage door operator +local ECOLINK_GARAGE_DOOR_FINGERPRINTS = { + {manufacturerId = 0x014A, productType = 0x0007, productId = 0x4731}, +} + +return ECOLINK_GARAGE_DOOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/init.lua b/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/init.lua index e6638841be..1019056330 100644 --- a/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/init.lua +++ b/drivers/SmartThings/zwave-garage-door-opener/src/ecolink-zw-gdo/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.capabilities local capabilities = require "st.capabilities" @@ -28,11 +17,6 @@ local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ ve --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 8 }) --- Ecolink garage door operator -local ECOLINK_GARAGE_DOOR_FINGERPRINTS = { - manufacturerId = 0x014A, productType = 0x0007, productId = 0x4731 -} - local GDO_ENDPOINT_NAME = "main" local CONTACTSENSOR_ENDPOINT_NAME = "sensor" local GDO_ENDPOINT_NUMBER = 1 @@ -55,11 +39,6 @@ local GDO_CONFIG_PARAMS = { --- @param driver Driver driver instance --- @param device Device device isntance --- @return boolean true if the device proper, else false -local function can_handle_ecolink_garage_door(opts, driver, device, ...) - return device:id_match(ECOLINK_GARAGE_DOOR_FINGERPRINTS.manufacturerId, - ECOLINK_GARAGE_DOOR_FINGERPRINTS.productType, - ECOLINK_GARAGE_DOOR_FINGERPRINTS.productId) -end local function component_to_endpoint(device, component_id) if (CONTACTSENSOR_ENDPOINT_NAME == component_id) then @@ -282,7 +261,8 @@ local ecolink_garage_door_operator = { doConfigure = configure_device_with_updated_config, infoChanged = configure_device_with_updated_config }, - can_handle = can_handle_ecolink_garage_door + can_handle = require("ecolink-zw-gdo.can_handle"), + shared_device_thread_enabled = true, } return ecolink_garage_door_operator diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/init.lua b/drivers/SmartThings/zwave-garage-door-opener/src/init.lua index 6d0a0a880c..3286471d83 100644 --- a/drivers/SmartThings/zwave-garage-door-opener/src/init.lua +++ b/drivers/SmartThings/zwave-garage-door-opener/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 @@ -24,10 +14,8 @@ local driver_template = { capabilities.doorControl, capabilities.contactSensor, }, - sub_drivers = { - require("mimolite-garage-door"), - require("ecolink-zw-gdo") - } + sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-garage-door-opener/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-garage-door-opener/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-garage-door-opener/src/mimolite-garage-door/can_handle.lua b/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/can_handle.lua new file mode 100644 index 0000000000..e515aae646 --- /dev/null +++ b/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_mimolite_garage_door(opts, driver, device, ...) + local FINGERPRINTS = require("mimolite-garage-door.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("mimolite-garage-door") + end + end + return false +end + +return can_handle_mimolite_garage_door diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/fingerprints.lua b/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/fingerprints.lua new file mode 100644 index 0000000000..52f0969e84 --- /dev/null +++ b/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local MIMOLITE_GARAGE_DOOR_FINGERPRINTS = { + { manufacturerId = 0x0084, productType = 0x0453, productId = 0x0111 } -- mimolite garage door +} + +return MIMOLITE_GARAGE_DOOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/init.lua b/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/init.lua index 1fd1b4362b..138a4f1b1d 100644 --- a/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/init.lua +++ b/drivers/SmartThings/zwave-garage-door-opener/src/mimolite-garage-door/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,23 +18,12 @@ local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = --- @type st.zwave.CommandClass.SwitchBinary local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({ version = 2 }) -local MIMOLITE_GARAGE_DOOR_FINGERPRINTS = { - { manufacturerId = 0x0084, productType = 0x0453, productId = 0x0111 } -- mimolite garage door -} --- Determine whether the passed device is mimolite garage door --- --- @param driver Driver driver instance --- @param device Device device isntance --- @return boolean true if the device proper, else false -local function can_handle_mimolite_garage_door(opts, driver, device, ...) - for _, fingerprint in ipairs(MIMOLITE_GARAGE_DOOR_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local function door_event_helper(device, value) device:emit_event(value == 0x00 and capabilities.doorControl.door.closed() or capabilities.doorControl.door.open()) @@ -118,7 +97,8 @@ local mimolite_garage_door = { doConfigure = do_configure }, NAME = "mimolite garage door", - can_handle = can_handle_mimolite_garage_door + can_handle = require("mimolite-garage-door.can_handle"), + shared_device_thread_enabled = true, } return mimolite_garage_door diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/sub_drivers.lua b/drivers/SmartThings/zwave-garage-door-opener/src/sub_drivers.lua new file mode 100644 index 0000000000..8de6edfd56 --- /dev/null +++ b/drivers/SmartThings/zwave-garage-door-opener/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("mimolite-garage-door"), + lazy_load_if_possible("ecolink-zw-gdo"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/test/test_ecolink_garage_door_operator.lua b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_ecolink_garage_door_operator.lua index 8d0ebfb7a9..0608ca2ef2 100644 --- a/drivers/SmartThings/zwave-garage-door-opener/src/test/test_ecolink_garage_door_operator.lua +++ b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_ecolink_garage_door_operator.lua @@ -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-garage-door-opener/src/test/test_mimolite_garage_door.lua b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_mimolite_garage_door.lua index 319a823daf..cc33391a4c 100644 --- a/drivers/SmartThings/zwave-garage-door-opener/src/test/test_mimolite_garage_door.lua +++ b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_mimolite_garage_door.lua @@ -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-garage-door-opener/src/test/test_zwave_garage_door_opener.lua b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_zwave_garage_door_opener.lua index 1fd7c8a3d6..0d5da468e8 100644 --- a/drivers/SmartThings/zwave-garage-door-opener/src/test/test_zwave_garage_door_opener.lua +++ b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_zwave_garage_door_opener.lua @@ -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/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index 925452c431..c3506c5005 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -172,6 +172,7 @@ local driver_template = { } }, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) 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..d9956517dd --- /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 + 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..947f2fea2b 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,10 @@ 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"), + shared_device_thread_enabled = true, } 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..f8d001cdf4 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,8 @@ local multisensor_6 = { } }, NAME = "aeotec multisensor 6", - can_handle = can_handle_multisensor_6 + can_handle = require("aeotec-multisensor.multisensor-6.can_handle"), + shared_device_thread_enabled = true, } 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..d97d65759c 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,8 @@ local multisensor_7 = { } }, NAME = "aeotec multisensor 7", - can_handle = can_handle_multisensor_7 + can_handle = require("aeotec-multisensor.multisensor-7.can_handle"), + shared_device_thread_enabled = true, } 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..1b87febb74 --- /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 + 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..509e38ac9e 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,8 @@ 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"), + shared_device_thread_enabled = true, } 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..38b8a6612f 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,8 @@ local apiv6_bugfix = { } }, NAME = "apiv6_bugfix", - can_handle = can_handle + can_handle = require("apiv6_bugfix.can_handle"), + shared_device_thread_enabled = true, } 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..8ab0bac6bc --- /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 + 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..53ef77c4be 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,8 @@ 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"), + shared_device_thread_enabled = true, } 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..64f34ffd96 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,8 @@ 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"), + shared_device_thread_enabled = true, } -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..6cf9ebba98 --- /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 + 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..abe2a8b6dc 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,8 @@ 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"), + shared_device_thread_enabled = true, } 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..a1f8ccee82 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,8 @@ 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"), + shared_device_thread_enabled = true, } 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..dc26fcddcd 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,9 @@ 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"), + shared_device_thread_enabled = true, } 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..75fc982511 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,8 @@ local fibaro_flood_sensor = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_fibaro_flood_sensor + can_handle = require("fibaro-flood-sensor.can_handle"), + shared_device_thread_enabled = true, } 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..b184001935 --- /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 + 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..e1aa2ccaf8 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,8 @@ local fibaro_motion_sensor = { [SensorAlarm.REPORT] = sensor_alarm_report } }, - can_handle = can_handle_fibaro_motion_sensor + can_handle = require("fibaro-motion-sensor.can_handle"), + shared_device_thread_enabled = true, } -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..8fe58a7167 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, @@ -85,7 +59,8 @@ local firmware_version = { [cc.WAKE_UP] = { [WakeUp.NOTIFICATION] = wakeup_notification } - } + }, + shared_device_thread_enabled = true, } -return firmware_version \ No newline at end of file +return firmware_version 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..cc6c476658 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,8 @@ 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"), + shared_device_thread_enabled = true, } 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..07835ac7d4 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,8 @@ 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"), + shared_device_thread_enabled = true, } return homeseer_multi_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/init.lua b/drivers/SmartThings/zwave-sensor/src/init.lua index 213aa8c389..9ab4d0077d 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, @@ -169,6 +125,7 @@ local driver_template = { [WakeUp.NOTIFICATION] = wakeup_notification } }, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, 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..9b515bae9f --- /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 + 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..89085c00c9 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,8 @@ local sensative_strip = { doConfigure = do_configure }, NAME = "sensative_strip", - can_handle = can_handle_sensative_strip + can_handle = require("sensative-strip.can_handle"), + shared_device_thread_enabled = true, } 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 934235ae24..472932fdfb 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 29938eb5ce..323c0d4807 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 ed0e6312b5..13442ec635 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 63671f073b..b6ede32bd5 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 836b5a9480..e38ce96f37 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 bdb0f60308..0f3dc972de 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 a1e9f5691e..3559656e3a 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 853729bdd5..a33f9b97ec 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 428a4883b0..8479a5f74f 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 0e537bf123..3c288c0be4 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 48c3d2e609..ab124e8f80 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 5f9adaf70c..0a31f97de5 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 6e5f1d25ed..fb2138c0d5 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 1b4ba1cd9c..05e306b6c7 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 16f46f0756..6707dd7ae0 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 c784afb875..8fd23b208b 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 cd986cdc94..4d3e6e0660 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 51533e76da..0ac7e57418 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 d9fd3c08c3..530d1c03bb 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 53ce347428..3dbfd89bc7 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 a4931e34f3..ac2022e069 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 1315ffe638..3496ebb6d5 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 b78bc8df64..926558036b 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 2549508083..743ffc5298 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 0ee1fe63e6..c88527f0cd 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" @@ -115,4 +105,3 @@ test.register_message_test( ) 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 72e0e59d07..fb9b519a42 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 7fdd26954c..e80e28dbd9 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 422347f770..4cfd906636 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 1b372a9162..3cd16b6fa3 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 9737b0d863..fde2d5ebfe 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 df28af97d1..7a8adbeb10 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 56549ac78e..8e463e7625 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 7f234a05fb..677204e5d1 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 ab296f8fed..775130d87e 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..8554dab28f 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,8 @@ local timed_tamper_clear = { } }, NAME = "timed tamper clear", - can_handle = can_handle_tamper_event + can_handle = require("timed-tamper-clear.can_handle"), + shared_device_thread_enabled = true, } 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..492a72dc74 --- /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 + 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..78b3beefcb 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,8 @@ local v1_contact_event = { } }, NAME = "v1 contact event", - can_handle = can_handle_v1_contact_event + can_handle = require("v1-contact-event.can_handle"), + shared_device_thread_enabled = true, } 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..f9e8f8a04e 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,8 @@ 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"), + shared_device_thread_enabled = true, } 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..42163aeaae 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,8 @@ local wakeup_no_poll = { [WakeUp.NOTIFICATION] = wakeup_notification } }, - can_handle = can_handle + can_handle = require("wakeup-no-poll.can_handle"), + shared_device_thread_enabled = true, } -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..e979e32153 --- /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 + 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..2a4053302b 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,8 @@ 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"), + shared_device_thread_enabled = true, } 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..63da2ae39c --- /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 + 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..6a956a4343 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,8 @@ 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"), + shared_device_thread_enabled = true, } return water_leak_sensor diff --git a/drivers/SmartThings/zwave-siren/src/init.lua b/drivers/SmartThings/zwave-siren/src/init.lua index 52ccaba6b9..94e8b723b9 100644 --- a/drivers/SmartThings/zwave-siren/src/init.lua +++ b/drivers/SmartThings/zwave-siren/src/init.lua @@ -88,7 +88,8 @@ local driver_template = { infoChanged = info_changed, doConfigure = do_configure, added = added_handler - } + }, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/init.lua index 7968983f63..1ee506f453 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/init.lua @@ -79,7 +79,8 @@ local driver_template = { infoChanged = info_changed, doConfigure = do_configure, added = device_added - } + }, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-switch/src/init.lua b/drivers/SmartThings/zwave-switch/src/init.lua index 9f40fd84e4..7d622e586f 100644 --- a/drivers/SmartThings/zwave-switch/src/init.lua +++ b/drivers/SmartThings/zwave-switch/src/init.lua @@ -125,7 +125,8 @@ local driver_template = { infoChanged = info_changed, doConfigure = do_configure, added = device_added - } + }, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, diff --git a/drivers/SmartThings/zwave-thermostat/src/init.lua b/drivers/SmartThings/zwave-thermostat/src/init.lua index 3668085b1a..6a79c1f4e1 100755 --- a/drivers/SmartThings/zwave-thermostat/src/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/init.lua @@ -106,6 +106,7 @@ local driver_template = { added = device_added }, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities, {native_capability_attrs_enabled = true}) diff --git a/drivers/SmartThings/zwave-valve/src/init.lua b/drivers/SmartThings/zwave-valve/src/init.lua index cea5b877c1..f430c19a4a 100644 --- a/drivers/SmartThings/zwave-valve/src/init.lua +++ b/drivers/SmartThings/zwave-valve/src/init.lua @@ -17,6 +17,7 @@ local driver_template = { capabilities.valve, }, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-window-treatment/src/init.lua b/drivers/SmartThings/zwave-window-treatment/src/init.lua index b2597d9f61..8b8f32c49b 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/init.lua @@ -75,6 +75,7 @@ local driver_template = { } }, sub_drivers = require("sub_drivers"), + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities)