From 6c955bbc655a2e31892276ecfb4256ec6ae6400c Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Thu, 8 Jan 2026 15:00:43 -0600 Subject: [PATCH] move sensor handlers into switch subdriver --- .../SmartThings/matter-switch/src/init.lua | 33 +---- .../src/sub_drivers/camera/can_handle.lua | 11 +- .../attribute_handlers.lua | 6 +- .../capability_handlers.lua | 4 +- .../event_handlers.lua | 2 +- .../src/sub_drivers/camera/init.lua | 17 ++- .../device_configuration.lua | 4 +- .../camera/{camera_utils => utils}/fields.lua | 0 .../camera/utils/subscriptions.lua | 99 +++++++++++++ .../camera/{camera_utils => utils}/utils.lua | 139 +----------------- .../src/sub_drivers/sensor/can_handle.lua | 27 ++++ .../sensor/handlers/attribute_handlers.lua | 80 ++++++++++ .../src/sub_drivers/sensor/init.lua | 31 ++++ .../src/sub_drivers/sensor/utils/fields.lua | 10 ++ .../sensor/utils/subscriptions.lua | 54 +++++++ .../switch_handlers/attribute_handlers.lua | 64 -------- .../src/switch_utils/device_configuration.lua | 5 +- .../matter-switch/src/switch_utils/fields.lua | 14 +- .../matter-switch/src/switch_utils/utils.lua | 85 +++++++++-- .../test_matter_sensor_offset_preferences.lua | 123 ---------------- 20 files changed, 413 insertions(+), 395 deletions(-) rename drivers/SmartThings/matter-switch/src/sub_drivers/camera/{camera_handlers => handlers}/attribute_handlers.lua (98%) rename drivers/SmartThings/matter-switch/src/sub_drivers/camera/{camera_handlers => handlers}/capability_handlers.lua (99%) rename drivers/SmartThings/matter-switch/src/sub_drivers/camera/{camera_handlers => handlers}/event_handlers.lua (94%) rename drivers/SmartThings/matter-switch/src/sub_drivers/camera/{camera_utils => utils}/device_configuration.lua (99%) rename drivers/SmartThings/matter-switch/src/sub_drivers/camera/{camera_utils => utils}/fields.lua (100%) create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/camera/utils/subscriptions.lua rename drivers/SmartThings/matter-switch/src/sub_drivers/camera/{camera_utils => utils}/utils.lua (52%) create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/sensor/can_handle.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/sensor/handlers/attribute_handlers.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/sensor/init.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/sensor/utils/fields.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/sensor/utils/subscriptions.lua delete mode 100644 drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index e8d1b8330a..343f379cae 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -82,7 +82,6 @@ function SwitchLifecycleHandlers.info_changed(driver, device, event, args) end end - -- instant update of values after offset preference change for name, info in pairs(device.preferences or {}) do if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then if name == "tempOffset" then @@ -92,7 +91,6 @@ function SwitchLifecycleHandlers.info_changed(driver, device, event, args) end end end - end function SwitchLifecycleHandlers.device_init(driver, device) @@ -159,17 +157,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.IlluminanceMeasurement.ID] = { - [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.illuminance_measured_value_handler - }, [clusters.LevelControl.ID] = { [clusters.LevelControl.attributes.CurrentLevel.ID] = attribute_handlers.level_control_current_level_handler, [clusters.LevelControl.attributes.MaxLevel.ID] = attribute_handlers.level_bounds_handler_factory(fields.LEVEL_MAX), [clusters.LevelControl.attributes.MinLevel.ID] = attribute_handlers.level_bounds_handler_factory(fields.LEVEL_MIN), }, - [clusters.OccupancySensing.ID] = { - [clusters.OccupancySensing.attributes.Occupancy.ID] = attribute_handlers.occupancy_handler, - }, [clusters.OnOff.ID] = { [clusters.OnOff.attributes.OnOff.ID] = attribute_handlers.on_off_attr_handler, }, @@ -181,17 +173,9 @@ local matter_driver_template = { [clusters.PowerTopology.ID] = { [clusters.PowerTopology.attributes.AvailableEndpoints.ID] = attribute_handlers.available_endpoints_handler, }, - [clusters.RelativeHumidityMeasurement.ID] = { - [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.relative_humidity_measured_value_handler - }, [clusters.Switch.ID] = { [clusters.Switch.attributes.MultiPressMax.ID] = attribute_handlers.multi_press_max_handler }, - [clusters.TemperatureMeasurement.ID] = { - [clusters.TemperatureMeasurement.attributes.MaxMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.TEMP_MAX), - [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.temperature_measured_value_handler, - [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.TEMP_MIN), - }, [clusters.ValveConfigurationAndControl.ID] = { [clusters.ValveConfigurationAndControl.attributes.CurrentLevel.ID] = attribute_handlers.valve_configuration_current_level_handler, [clusters.ValveConfigurationAndControl.attributes.CurrentState.ID] = attribute_handlers.valve_configuration_current_state_handler, @@ -237,12 +221,6 @@ local matter_driver_template = { [capabilities.fanSpeedPercent.ID] = { clusters.FanControl.attributes.PercentCurrent }, - [capabilities.illuminanceMeasurement.ID] = { - clusters.IlluminanceMeasurement.attributes.MeasuredValue - }, - [capabilities.motionSensor.ID] = { - clusters.OccupancySensing.attributes.Occupancy - }, [capabilities.level.ID] = { clusters.ValveConfigurationAndControl.attributes.CurrentLevel }, @@ -252,19 +230,11 @@ local matter_driver_template = { [capabilities.powerMeter.ID] = { clusters.ElectricalPowerMeasurement.attributes.ActivePower }, - [capabilities.relativeHumidityMeasurement.ID] = { - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue - }, [capabilities.switchLevel.ID] = { clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, clusters.LevelControl.attributes.MinLevel, }, - [capabilities.temperatureMeasurement.ID] = { - clusters.TemperatureMeasurement.attributes.MeasuredValue, - clusters.TemperatureMeasurement.attributes.MinMeasuredValue, - clusters.TemperatureMeasurement.attributes.MaxMeasuredValue - }, [capabilities.valve.ID] = { clusters.ValveConfigurationAndControl.attributes.CurrentState }, @@ -349,7 +319,8 @@ local matter_driver_template = { switch_utils.lazy_load("sub_drivers.camera"), switch_utils.lazy_load_if_possible("sub_drivers.eve_energy"), switch_utils.lazy_load_if_possible("sub_drivers.ikea_scroll"), - switch_utils.lazy_load_if_possible("sub_drivers.third_reality_mk1") + switch_utils.lazy_load_if_possible("sub_drivers.third_reality_mk1"), + switch_utils.lazy_load_if_possible("sub_drivers.sensor"), } } diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/can_handle.lua index 25a441d641..e9e9b87c1b 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/can_handle.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/can_handle.lua @@ -3,13 +3,14 @@ return function(opts, driver, device, ...) local device_lib = require "st.device" - local fields = require "switch_utils.fields" - local switch_utils = require "switch_utils.utils" if device.network_type == device_lib.NETWORK_TYPE_MATTER then local version = require "version" - if version.rpc >= 10 and version.api >= 16 and - #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) > 0 then - return true, require("sub_drivers.camera") + if version.rpc >= 10 and version.api >= 16 then + local fields = require "switch_utils.fields" + local switch_utils = require "switch_utils.utils" + if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) > 0 then + return true, require("sub_drivers.camera") + end end end return false diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/handlers/attribute_handlers.lua similarity index 98% rename from drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua rename to drivers/SmartThings/matter-switch/src/sub_drivers/camera/handlers/attribute_handlers.lua index 484e4c7a15..4e0ad8d9e3 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/handlers/attribute_handlers.lua @@ -1,11 +1,11 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local camera_fields = require "sub_drivers.camera.camera_utils.fields" -local camera_utils = require "sub_drivers.camera.camera_utils.utils" +local camera_fields = require "sub_drivers.camera.utils.fields" +local camera_utils = require "sub_drivers.camera.utils.utils" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" -local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" +local camera_cfg = require "sub_drivers.camera.utils.device_configuration" local fields = require "switch_utils.fields" local utils = require "st.utils" diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/handlers/capability_handlers.lua similarity index 99% rename from drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua rename to drivers/SmartThings/matter-switch/src/sub_drivers/camera/handlers/capability_handlers.lua index fb85eb863f..09cd693639 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/handlers/capability_handlers.lua @@ -1,8 +1,8 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local camera_fields = require "sub_drivers.camera.camera_utils.fields" -local camera_utils = require "sub_drivers.camera.camera_utils.utils" +local camera_fields = require "sub_drivers.camera.utils.fields" +local camera_utils = require "sub_drivers.camera.utils.utils" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local utils = require "st.utils" diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/event_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/handlers/event_handlers.lua similarity index 94% rename from drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/event_handlers.lua rename to drivers/SmartThings/matter-switch/src/sub_drivers/camera/handlers/event_handlers.lua index 02b63bb37f..16944155fe 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/event_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/handlers/event_handlers.lua @@ -1,7 +1,7 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local camera_fields = require "sub_drivers.camera.camera_utils.fields" +local camera_fields = require "sub_drivers.camera.utils.fields" local capabilities = require "st.capabilities" local switch_utils = require "switch_utils.utils" diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua index f13589ff41..8e198684ae 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua @@ -5,15 +5,15 @@ -- Matter Camera Sub Driver ------------------------------------------------------------------------------------- -local attribute_handlers = require "sub_drivers.camera.camera_handlers.attribute_handlers" +local attribute_handlers = require "sub_drivers.camera.handlers.attribute_handlers" local button_cfg = require("switch_utils.device_configuration").ButtonCfg -local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" -local camera_fields = require "sub_drivers.camera.camera_utils.fields" -local camera_utils = require "sub_drivers.camera.camera_utils.utils" +local camera_cfg = require "sub_drivers.camera.utils.device_configuration" +local camera_fields = require "sub_drivers.camera.utils.fields" +local camera_utils = require "sub_drivers.camera.utils.utils" local capabilities = require "st.capabilities" -local capability_handlers = require "sub_drivers.camera.camera_handlers.capability_handlers" +local capability_handlers = require "sub_drivers.camera.handlers.capability_handlers" local clusters = require "st.matter.clusters" -local event_handlers = require "sub_drivers.camera.camera_handlers.event_handlers" +local event_handlers = require "sub_drivers.camera.handlers.event_handlers" local fields = require "switch_utils.fields" local switch_utils = require "switch_utils.utils" @@ -26,7 +26,10 @@ function CameraLifecycleHandlers.device_init(driver, device) if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then device:set_find_child(switch_utils.find_child) end - device:extend_device("subscribe", camera_utils.subscribe) + if #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) == 0 then + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist = true}) + end + device:extend_device("subscribe", switch_utils.subscribe) device:subscribe() end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/utils/device_configuration.lua similarity index 99% rename from drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua rename to drivers/SmartThings/matter-switch/src/sub_drivers/camera/utils/device_configuration.lua index 20641ebd33..c1be20569d 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/utils/device_configuration.lua @@ -2,8 +2,8 @@ -- Licensed under the Apache License, Version 2.0 local button_cfg = require("switch_utils.device_configuration").ButtonCfg -local camera_fields = require "sub_drivers.camera.camera_utils.fields" -local camera_utils = require "sub_drivers.camera.camera_utils.utils" +local camera_fields = require "sub_drivers.camera.utils.fields" +local camera_utils = require "sub_drivers.camera.utils.utils" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local device_cfg = require "switch_utils.device_configuration" diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/utils/fields.lua similarity index 100% rename from drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua rename to drivers/SmartThings/matter-switch/src/sub_drivers/camera/utils/fields.lua diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/utils/subscriptions.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/utils/subscriptions.lua new file mode 100644 index 0000000000..dc4c09a8c3 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/utils/subscriptions.lua @@ -0,0 +1,99 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" + +local SubscriptionMap = { + subscribed_attributes = { + [capabilities.audioMute.ID] = { + clusters.CameraAvStreamManagement.attributes.SpeakerMuted, + clusters.CameraAvStreamManagement.attributes.MicrophoneMuted + }, + [capabilities.audioVolume.ID] = { + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel, + clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel, + clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel + }, + [capabilities.cameraPrivacyMode.ID] = { + clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled, + clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled, + clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn + }, + [capabilities.cameraViewportSettings.ID] = { + clusters.CameraAvStreamManagement.attributes.MinViewportResolution, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams, + clusters.CameraAvStreamManagement.attributes.Viewport + }, + [capabilities.hdr.ID] = { + clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, + clusters.CameraAvStreamManagement.attributes.ImageRotation + }, + [capabilities.imageControl.ID] = { + clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal, + clusters.CameraAvStreamManagement.attributes.ImageFlipVertical + }, + [capabilities.localMediaStorage.ID] = { + clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled, + clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled + }, + [capabilities.mechanicalPanTiltZoom.ID] = { + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPresets, + clusters.CameraAvSettingsUserLevelManagement.attributes.MaxPresets, + clusters.CameraAvSettingsUserLevelManagement.attributes.ZoomMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.PanMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.PanMin, + clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMin + }, + [capabilities.mode.ID] = { + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness + }, + [capabilities.nightVision.ID] = { + clusters.CameraAvStreamManagement.attributes.NightVision, + clusters.CameraAvStreamManagement.attributes.NightVisionIllum + }, + [capabilities.sounds.ID] = { + clusters.Chime.attributes.InstalledChimeSounds, + clusters.Chime.attributes.SelectedChime + }, + [capabilities.switch.ID] = { + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled + }, + [capabilities.videoStreamSettings.ID] = { + clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints, + clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams, + clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams + }, + [capabilities.webrtc.ID] = { + clusters.CameraAvStreamManagement.attributes.TwoWayTalkSupport + }, + [capabilities.zoneManagement.ID] = { + clusters.ZoneManagement.attributes.MaxZones, + clusters.ZoneManagement.attributes.Zones, + clusters.ZoneManagement.attributes.Triggers, + clusters.ZoneManagement.attributes.SensitivityMax, + clusters.ZoneManagement.attributes.Sensitivity + }, + }, + subscribed_events = { + [capabilities.zoneManagement.ID] = { + clusters.ZoneManagement.events.ZoneTriggered, + clusters.ZoneManagement.events.ZoneStopped + } + }, + conditional_subscriptions = { + [function(device) + local fields = require "switch_utils.fields" + local switch_utils = require "switch_utils.utils" + return #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) > 0 + end] = { clusters.CameraAvStreamManagement.attributes.AttributeList } + } +} + +return SubscriptionMap diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/utils/utils.lua similarity index 52% rename from drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua rename to drivers/SmartThings/matter-switch/src/sub_drivers/camera/utils/utils.lua index a93e757c16..435f354da0 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/utils/utils.lua @@ -1,7 +1,7 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local camera_fields = require "sub_drivers.camera.camera_utils.fields" +local camera_fields = require "sub_drivers.camera.utils.fields" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local fields = require "switch_utils.fields" @@ -166,141 +166,4 @@ function CameraUtils.optional_capabilities_list_changed(new_component_capability return false end -function CameraUtils.subscribe(device) - local camera_subscribed_attributes = { - [capabilities.hdr.ID] = { - clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, - clusters.CameraAvStreamManagement.attributes.ImageRotation - }, - [capabilities.nightVision.ID] = { - clusters.CameraAvStreamManagement.attributes.NightVision, - clusters.CameraAvStreamManagement.attributes.NightVisionIllum - }, - [capabilities.imageControl.ID] = { - clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal, - clusters.CameraAvStreamManagement.attributes.ImageFlipVertical - }, - [capabilities.cameraPrivacyMode.ID] = { - clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled, - clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled, - clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn - }, - [capabilities.webrtc.ID] = { - clusters.CameraAvStreamManagement.attributes.TwoWayTalkSupport - }, - [capabilities.mechanicalPanTiltZoom.ID] = { - clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition, - clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPresets, - clusters.CameraAvSettingsUserLevelManagement.attributes.MaxPresets, - clusters.CameraAvSettingsUserLevelManagement.attributes.ZoomMax, - clusters.CameraAvSettingsUserLevelManagement.attributes.PanMax, - clusters.CameraAvSettingsUserLevelManagement.attributes.PanMin, - clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMax, - clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMin - }, - [capabilities.audioMute.ID] = { - clusters.CameraAvStreamManagement.attributes.SpeakerMuted, - clusters.CameraAvStreamManagement.attributes.MicrophoneMuted - }, - [capabilities.audioVolume.ID] = { - clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel, - clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel, - clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel, - clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel, - clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel, - clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel - }, - [capabilities.mode.ID] = { - clusters.CameraAvStreamManagement.attributes.StatusLightBrightness - }, - [capabilities.switch.ID] = { - clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, - clusters.OnOff.attributes.OnOff - }, - [capabilities.videoStreamSettings.ID] = { - clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints, - clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate, - clusters.CameraAvStreamManagement.attributes.VideoSensorParams, - clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams - }, - [capabilities.zoneManagement.ID] = { - clusters.ZoneManagement.attributes.MaxZones, - clusters.ZoneManagement.attributes.Zones, - clusters.ZoneManagement.attributes.Triggers, - clusters.ZoneManagement.attributes.SensitivityMax, - clusters.ZoneManagement.attributes.Sensitivity - }, - [capabilities.sounds.ID] = { - clusters.Chime.attributes.InstalledChimeSounds, - clusters.Chime.attributes.SelectedChime - }, - [capabilities.localMediaStorage.ID] = { - clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled, - clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled - }, - [capabilities.cameraViewportSettings.ID] = { - clusters.CameraAvStreamManagement.attributes.MinViewportResolution, - clusters.CameraAvStreamManagement.attributes.VideoSensorParams, - clusters.CameraAvStreamManagement.attributes.Viewport - }, - [capabilities.motionSensor.ID] = { - clusters.OccupancySensing.attributes.Occupancy - }, - [capabilities.switchLevel.ID] = { - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - }, - [capabilities.colorControl.ID] = { - clusters.ColorControl.attributes.ColorMode, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - }, - [capabilities.colorTemperature.ID] = { - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - }, - } - - local camera_subscribed_events = { - [capabilities.zoneManagement.ID] = { - clusters.ZoneManagement.events.ZoneTriggered, - clusters.ZoneManagement.events.ZoneStopped - }, - [capabilities.button.ID] = { - clusters.Switch.events.InitialPress, - clusters.Switch.events.LongPress, - clusters.Switch.events.ShortRelease, - clusters.Switch.events.MultiPressComplete - } - } - - local im = require "st.matter.interaction_model" - - local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {}) - local devices_seen, capabilities_seen, attributes_seen, events_seen = {}, {}, {}, {} - - if #device:get_endpoints(clusters.CameraAvStreamManagement.ID) > 0 then - local ib = im.InteractionInfoBlock(nil, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.attributes.AttributeList.ID) - subscribe_request:with_info_block(ib) - end - - for _, endpoint_info in ipairs(device.endpoints) do - local checked_device = switch_utils.find_child(device, endpoint_info.endpoint_id) or device - if not devices_seen[checked_device.id] then - switch_utils.populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, events_seen, - camera_subscribed_attributes, camera_subscribed_events - ) - devices_seen[checked_device.id] = true -- only loop through any device once - end - end - - if #subscribe_request.info_blocks > 0 then - device:send(subscribe_request) - end -end - return CameraUtils diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/sensor/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/sensor/can_handle.lua new file mode 100644 index 0000000000..1444e94097 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/sensor/can_handle.lua @@ -0,0 +1,27 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ib) + local capabilities = require "st.capabilities" + local supported_capabilities = { + capabilities.illuminanceMeasurement, + capabilities.motionSensor, + capabilities.relativeHumidityMeasurement, + capabilities.temperatureMeasurement, + } + -- MatterMessageDispatcher handles all events through a MATTER typed device. + -- Therefore, find and check if an endpoint-mapped EDGE_CHILD device exists, + -- Note: parameter ib should always be populated here by MatterMessageDispatcher. + if opts and opts.dispatcher_class == "MatterMessageDispatcher" then + local switch_utils = require "switch_utils.utils" + if ib and ib.info_block and ib.info_block.endpoint_id then + device = switch_utils.find_child(device, ib.info_block.endpoint_id) or device + end + end + for _, capability in ipairs(supported_capabilities) do + if device:supports_capability(capability) then + return true, require("sub_drivers.sensor") + end + end + return false +end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/sensor/handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/sensor/handlers/attribute_handlers.lua new file mode 100644 index 0000000000..896346a398 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/sensor/handlers/attribute_handlers.lua @@ -0,0 +1,80 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local st_utils = require "st.utils" +local capabilities = require "st.capabilities" +local switch_utils = require "switch_utils.utils" + +local SensorAttributeHandlers = {} + + +-- [[ ILLUMINANCE CLUSTER ATTRIBUTES ]] -- + +function SensorAttributeHandlers.illuminance_measured_value_handler(driver, device, ib, response) + local lux = math.floor(10 ^ ((ib.data.value - 1) / 10000)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.illuminanceMeasurement.illuminance(lux)) +end + + +-- [[ TEMPERATURE MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function SensorAttributeHandlers.temperature_measured_value_handler(driver, device, ib, response) + local measured_value = ib.data.value + if measured_value ~= nil then + local temp = measured_value / 100.0 + local unit = "C" + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperature({value = temp, unit = unit})) + end +end + +function SensorAttributeHandlers.temperature_measured_value_bounds_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local sensor_fields = require "sub_drivers.sensor.utils.fields" + local temp = ib.data.value / 100.0 + local unit = "C" + switch_utils.set_field_for_endpoint(device, sensor_fields.TEMP_BOUND_RECEIVED..minOrMax, ib.endpoint_id, temp) + local min = switch_utils.get_field_for_endpoint(device, sensor_fields.TEMP_BOUND_RECEIVED..sensor_fields.TEMP_MIN, ib.endpoint_id) + local max = switch_utils.get_field_for_endpoint(device, sensor_fields.TEMP_BOUND_RECEIVED..sensor_fields.TEMP_MAX, ib.endpoint_id) + if min ~= nil and max ~= nil then + if min < max then + -- Only emit the capability for RPC version >= 5 (unit conversion for + -- temperature range capability is only supported for RPC >= 5) + local version = require "version" + if version.rpc >= 5 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = min, maximum = max }, unit = unit })) + end + switch_utils.set_field_for_endpoint(device, sensor_fields.TEMP_BOUND_RECEIVED..sensor_fields.TEMP_MIN, ib.endpoint_id, nil) + switch_utils.set_field_for_endpoint(device, sensor_fields.TEMP_BOUND_RECEIVED..sensor_fields.TEMP_MAX, ib.endpoint_id, nil) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min temperature %d that is not lower than the reported max temperature %d", min, max)) + end + end + end +end + + +-- [[ RELATIVE HUMIDITY MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function SensorAttributeHandlers.humidity_measured_value_handler(driver, device, ib, response) + local measured_value = ib.data.value + if measured_value ~= nil then + local humidity = st_utils.round(measured_value / 100.0) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(humidity)) + end +end + + +-- [[ OCCUPANCY SENSING CLUSTER ATTRIBUTES ]] -- + +function SensorAttributeHandlers.occupancy_measured_value_handler(driver, device, ib, response) + if device:supports_capability(capabilities.motionSensor) then + device:emit_event(ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) + else + device:emit_event(ib.data.value == 0x01 and capabilities.presenceSensor.presence("present") or capabilities.presenceSensor.presence("not present")) + end +end + +return SensorAttributeHandlers diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/sensor/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/sensor/init.lua new file mode 100644 index 0000000000..1c87a1672b --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/sensor/init.lua @@ -0,0 +1,31 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" +local sensor_fields = require "sub_drivers.sensor.utils.fields" +local attribute_handlers = require "sub_drivers.sensor.handlers.attribute_handlers" + +local sensor_handler = { + NAME = "Matter Sensor Handlers", + can_handle = require("sub_drivers.sensor.can_handle"), + matter_handlers = { + attr = { + [clusters.IlluminanceMeasurement.ID] = { + [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.illuminance_measured_value_handler + }, + [clusters.OccupancySensing.ID] = { + [clusters.OccupancySensing.attributes.Occupancy.ID] = attribute_handlers.occupancy_measured_value_handler, + }, + [clusters.RelativeHumidityMeasurement.ID] = { + [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.humidity_measured_value_handler + }, + [clusters.TemperatureMeasurement.ID] = { + [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.temperature_measured_value_handler, + [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(sensor_fields.TEMP_MIN), + [clusters.TemperatureMeasurement.attributes.MaxMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(sensor_fields.TEMP_MAX), + }, + } + } +} + +return sensor_handler diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/sensor/utils/fields.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/sensor/utils/fields.lua new file mode 100644 index 0000000000..1603ee1f51 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/sensor/utils/fields.lua @@ -0,0 +1,10 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SensorFields = {} + +SensorFields.TEMP_BOUND_RECEIVED = "__temp_bound_received" +SensorFields.TEMP_MIN = "__temp_min" +SensorFields.TEMP_MAX = "__temp_max" + +return SensorFields diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/sensor/utils/subscriptions.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/sensor/utils/subscriptions.lua new file mode 100644 index 0000000000..86341aa8cd --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/sensor/utils/subscriptions.lua @@ -0,0 +1,54 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" + +local SubscriptionMap = { + subscribed_attributes = { + [capabilities.atmosphericPressureMeasurement.ID] = { + clusters.PressureMeasurement.attributes.MeasuredValue + }, + [capabilities.contactSensor.ID] = { + clusters.BooleanState.attributes.StateValue + }, + [capabilities.flowMeasurement.ID] = { + clusters.FlowMeasurement.attributes.MeasuredValue, + clusters.FlowMeasurement.attributes.MinMeasuredValue, + clusters.FlowMeasurement.attributes.MaxMeasuredValue + }, + [capabilities.hardwareFault.ID] = { + clusters.BooleanStateConfiguration.attributes.SensorFault, + clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels + }, + [capabilities.illuminanceMeasurement.ID] = { + clusters.IlluminanceMeasurement.attributes.MeasuredValue + }, + [capabilities.motionSensor.ID] = { + clusters.OccupancySensing.attributes.Occupancy + }, + [capabilities.presenceSensor.ID] = { + clusters.OccupancySensing.attributes.Occupancy + }, + [capabilities.rainSensor.ID] = { + clusters.BooleanState.attributes.StateValue, + }, + [capabilities.relativeHumidityMeasurement.ID] = { + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue + }, + [capabilities.temperatureAlarm.ID] = { + clusters.BooleanState.attributes.StateValue, + }, + [capabilities.temperatureMeasurement.ID] = { + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, + clusters.Thermostat.attributes.LocalTemperature + }, + [capabilities.waterSensor.ID] = { + clusters.BooleanState.attributes.StateValue, + }, + } +} + +return SubscriptionMap 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 ae76709be8..f64168146c 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -232,21 +232,6 @@ function AttributeHandlers.color_temp_physical_mireds_bounds_factory(minOrMax) end --- [[ ILLUMINANCE CLUSTER ATTRIBUTES ]] -- - -function AttributeHandlers.illuminance_measured_value_handler(driver, device, ib, response) - local lux = math.floor(10 ^ ((ib.data.value - 1) / 10000)) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.illuminanceMeasurement.illuminance(lux)) -end - - --- [[ OCCUPANCY CLUSTER ATTRIBUTES ]] -- - -function AttributeHandlers.occupancy_handler(driver, device, ib, response) - device:emit_event(ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) -end - - -- [[ ELECTRICAL POWER MEASUREMENT CLUSTER ATTRIBUTES ]] -- function AttributeHandlers.active_power_handler(driver, device, ib, response) @@ -416,55 +401,6 @@ function AttributeHandlers.multi_press_max_handler(driver, device, ib, response) end --- [[ TEMPERATURE MEASUREMENT CLUSTER ATTRIBUTES ]] -- - -function AttributeHandlers.temperature_measured_value_handler(driver, device, ib, response) - local measured_value = ib.data.value - if measured_value ~= nil then - local temp = measured_value / 100.0 - local unit = "C" - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperature({value = temp, unit = unit})) - end -end - -function AttributeHandlers.temperature_measured_value_bounds_factory(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local temp = ib.data.value / 100.0 - local unit = "C" - switch_utils.set_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..minOrMax, ib.endpoint_id, temp) - local min = switch_utils.get_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MIN, ib.endpoint_id) - local max = switch_utils.get_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MAX, ib.endpoint_id) - if min ~= nil and max ~= nil then - if min < max then - -- Only emit the capability for RPC version >= 5 (unit conversion for - -- temperature range capability is only supported for RPC >= 5) - if version.rpc >= 5 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = min, maximum = max }, unit = unit })) - end - switch_utils.set_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MIN, ib.endpoint_id, nil) - switch_utils.set_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MAX, ib.endpoint_id, nil) - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min temperature %d that is not lower than the reported max temperature %d", min, max)) - end - end - end -end - - --- [[ RELATIVE HUMIDITY MEASUREMENT CLUSTER ATTRIBUTES ]] -- - -function AttributeHandlers.relative_humidity_measured_value_handler(driver, device, ib, response) - local measured_value = ib.data.value - if measured_value ~= nil then - local humidity = st_utils.round(measured_value / 100.0) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(humidity)) - end -end - - -- [[ FAN CONTROL CLUSTER ATTRIBUTES ]] -- function AttributeHandlers.fan_mode_handler(driver, device, ib, response) diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua index 8542972320..352c8fb166 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -16,12 +16,11 @@ if version.api < 11 then end local DeviceConfiguration = {} -local ChildConfiguration = {} local SwitchDeviceConfiguration = {} local ButtonDeviceConfiguration = {} local FanDeviceConfiguration = {} -function ChildConfiguration.create_or_update_child_devices(driver, device, server_cluster_ep_ids, default_endpoint_id, assign_profile_fn) +function DeviceConfiguration.create_or_update_child_devices(driver, device, server_cluster_ep_ids, default_endpoint_id, assign_profile_fn) if #server_cluster_ep_ids == 1 and server_cluster_ep_ids[1] == default_endpoint_id then -- no children will be created return end @@ -216,7 +215,7 @@ function DeviceConfiguration.match_profile(driver, device) local server_onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) -- get_endpoints defaults to return EPs supporting SERVER or BOTH if #server_onoff_ep_ids > 0 then - ChildConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id, SwitchDeviceConfiguration.assign_profile_for_onoff_ep) + DeviceConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id, SwitchDeviceConfiguration.assign_profile_for_onoff_ep) end if switch_utils.tbl_contains(server_onoff_ep_ids, default_endpoint_id) then diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index dcc3403780..f89c0e9742 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -25,14 +25,24 @@ SwitchFields.DEVICE_TYPE_ID = { BRIDGED_NODE = 0x0013, CAMERA = 0x0142, CHIME = 0x0146, + CONTACT_SENSOR = 0x0015, DIMMABLE_PLUG_IN_UNIT = 0x010B, DOORBELL = 0x0143, ELECTRICAL_SENSOR = 0x0510, FAN = 0x002B, + FLOW_SENSOR = 0x0306, GENERIC_SWITCH = 0x000F, + HUMIDITY_SENSOR = 0x0307, + LIGHT_SENSOR = 0x0106, MOUNTED_ON_OFF_CONTROL = 0x010F, MOUNTED_DIMMABLE_LOAD_CONTROL = 0x0110, + OCCUPANCY_SENSOR = 0x0107, ON_OFF_PLUG_IN_UNIT = 0x010A, + PRESSURE_SENSOR = 0x0305, + RAIN_SENSOR = 0x0044, + TEMPERATURE_SENSOR = 0x0302, + WATER_FREEZE_DETECTOR = 0x0041, + WATER_LEAK_DETECTOR = 0x0043, LIGHT = { ON_OFF = 0x0100, DIMMABLE = 0x0101, @@ -182,10 +192,6 @@ SwitchFields.EMULATE_HELD = "__emulate_held" -- for non-MSR (MomentarySwitchRele SwitchFields.SUPPORTS_MULTI_PRESS = "__multi_button" -- for MSM devices (MomentarySwitchMultiPress), create an event on receipt of MultiPressComplete SwitchFields.INITIAL_PRESS_ONLY = "__initial_press_only" -- for devices that support MS (MomentarySwitch), but not MSR (MomentarySwitchRelease) -SwitchFields.TEMP_BOUND_RECEIVED = "__temp_bound_received" -SwitchFields.TEMP_MIN = "__temp_min" -SwitchFields.TEMP_MAX = "__temp_max" - SwitchFields.TRANSITION_TIME = 0 --1/10ths of a second -- When sent with a command, these options mask and override bitmaps cause the command -- to take effect when the switch/light is off. diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index da5376f031..9ce41b1ee0 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -162,7 +162,7 @@ function utils.find_default_endpoint(device) return ep end end - return nil + return device.MATTER_DEFAULT_ENDPOINT end -- Return the first fan endpoint as the default endpoint if any is found @@ -442,7 +442,7 @@ end --- @param events_seen table a list of events that have already been checked --- @param subscribed_attributes table key-value pairs mapping capability ids to subscribed attributes --- @param subscribed_events table key-value pairs mapping capability ids to subscribed events -function utils.populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, events_seen, subscribed_attributes, subscribed_events) +local function populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, events_seen, subscribed_attributes, subscribed_events) for _, component in pairs(checked_device.st_store.profile.components) do for _, capability in pairs(component.capabilities) do if not capabilities_seen[capability.id] then @@ -450,20 +450,32 @@ function utils.populate_subscribe_request_for_device(checked_device, subscribe_r local cluster_id = attr.cluster or attr._cluster.ID local attr_id = attr.ID or attr.attribute if not attributes_seen[cluster_id] or not attributes_seen[cluster_id][attr_id] then - local ib = im.InteractionInfoBlock(nil, cluster_id, attr_id) - subscribe_request:with_info_block(ib) - attributes_seen[cluster_id] = attributes_seen[cluster_id] or {} - attributes_seen[cluster_id][attr_id] = ib + local parent_device = checked_device:get_parent_device() or checked_device + local supporting_eps = parent_device:get_endpoints(cluster_id, { attribute_id = nil }) + if #supporting_eps == 0 then + log.warn_with({ hub_logs = true }, string.format("Device does not support cluster 0x%04X not adding subscribed attribute", cluster_id)) + else + local ib = im.InteractionInfoBlock(nil, cluster_id, attr_id) + subscribe_request:with_info_block(ib) + attributes_seen[cluster_id] = attributes_seen[cluster_id] or {} + attributes_seen[cluster_id][attr_id] = ib + end end end for _, event in ipairs(subscribed_events[capability.id] or {}) do local cluster_id = event.cluster or event._cluster.ID local event_id = event.ID or event.event if not events_seen[cluster_id] or not events_seen[cluster_id][event_id] then - local ib = im.InteractionInfoBlock(nil, cluster_id, nil, event_id) - subscribe_request:with_info_block(ib) - events_seen[cluster_id] = events_seen[cluster_id] or {} - events_seen[cluster_id][event_id] = ib + local parent_device = checked_device:get_parent_device() or checked_device + local supporting_eps = parent_device:get_endpoints(cluster_id, { event_id = nil }) + if #supporting_eps == 0 then + log.warn_with({ hub_logs = true }, string.format("Device does not support cluster 0x%04X, not adding subscribed event", cluster_id)) + else + local ib = im.InteractionInfoBlock(nil, cluster_id, nil, event_id) + subscribe_request:with_info_block(ib) + events_seen[cluster_id] = events_seen[cluster_id] or {} + events_seen[cluster_id][event_id] = ib + end end end capabilities_seen[capability.id] = true -- only loop through any capability once @@ -472,6 +484,53 @@ function utils.populate_subscribe_request_for_device(checked_device, subscribe_r end end +--- aggregate the subscribed_attributes and subscribed_events tables with capability-subscription tables found in sub-drivers +--- +--- @param device any a Matter device object +--- @param checked_device any a Matter device object, either a parent or child device, so not necessarily the same as device +--- @param subscribe_request any a subscribe request that will be appended to as needed for the device +--- @param subscribed_attributes any table key-value pairs mapping capability ids to subscribed attributes, which will be appended to as needed for the device +--- @param subscribed_events any table key-value pairs mapping capability ids to subscribed events, which will be appended to as needed for the device +local function aggregate_sub_driver_subscriptions(device, checked_device, subscribe_request, subscribed_attributes, subscribed_events) + for _, sub_driver in ipairs(device.driver.sub_drivers) do + if sub_driver.can_handle({}, device.driver, checked_device) then + local sub_driver_subscriptions = require(string.format("%s%s", sub_driver.NAME, ".utils.subscriptions")) + for capability, cluster_attributes in pairs(sub_driver_subscriptions.subscribed_attributes or {}) do + if subscribed_attributes[capability] then + for _, attr in pairs(cluster_attributes or {}) do + if not utils.tbl_contains(subscribed_attributes[capability], attr) then + table.insert(subscribed_attributes[capability], attr) + end + end + else + subscribed_attributes[capability] = cluster_attributes + end + end + for capability, cluster_events in pairs(sub_driver_subscriptions.subscribed_events or {}) do + if subscribed_events[capability] then + for _, event in pairs(cluster_events or {}) do + if not utils.tbl_contains(subscribed_events[capability], event) then + table.insert(subscribed_events[capability], event) + end + end + else + subscribed_events[capability] = cluster_events + end + end + for condition_fn, cluster_attributes in pairs(sub_driver_subscriptions.conditional_subscriptions or {}) do + if condition_fn(checked_device) then + for _, cluster_attribute in pairs(cluster_attributes or {}) do + local cluster_id = cluster_attribute.cluster or cluster_attribute._cluster.ID + local attr_id = cluster_attribute.ID or cluster_attribute.attribute + local ib = im.InteractionInfoBlock(nil, cluster_id, attr_id) + subscribe_request:with_info_block(ib) + end + end + end + end + end +end + --- create and send a subscription request by checking all devices, accounting for both parent and child devices --- --- @param device any a Matter device object @@ -482,8 +541,10 @@ function utils.subscribe(device) for _, endpoint_info in ipairs(device.endpoints) do local checked_device = utils.find_child(device, endpoint_info.endpoint_id) or device if not devices_seen[checked_device.id] then - utils.populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, events_seen, - device.driver.subscribed_attributes, device.driver.subscribed_events + local subscribed_attributes, subscribed_events = device.driver.subscribed_attributes, device.driver.subscribed_events + aggregate_sub_driver_subscriptions(device, checked_device, subscribe_request, subscribed_attributes, subscribed_events) + populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, events_seen, + subscribed_attributes, subscribed_events ) devices_seen[checked_device.id] = true -- only loop through any device once end diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua deleted file mode 100644 index de2afe3bd3..0000000000 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua +++ /dev/null @@ -1,123 +0,0 @@ -local test = require "integration_test" -local capabilities = require "st.capabilities" -local t_utils = require "integration_test.utils" -local utils = require "st.utils" -local dkjson = require "dkjson" -local clusters = require "st.matter.clusters" - -local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("3-button-battery-temperature-humidity.yml"), - matter_version = {hardware = 1, software = 1}, - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - endpoints = { - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x0302, device_type_revision = 1}, - } - }, - { - endpoint_id = 2, - clusters = { - {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "BOTH"}, - }, - device_types = { - {device_type_id = 0x0307, device_type_revision = 1}, - } - }, - } -}) - -local function test_init() - test.disable_startup_messages() - test.mock_device.add_test_device(mock_device) - - local cluster_subscribe_list = { - clusters.Switch.events.InitialPress, - clusters.Switch.events.LongPress, - clusters.Switch.events.ShortRelease, - clusters.Switch.events.MultiPressComplete, - - clusters.TemperatureMeasurement.attributes.MeasuredValue, - clusters.TemperatureMeasurement.attributes.MinMeasuredValue, - clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, - - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, - clusters.PowerSource.attributes.BatPercentRemaining - } - - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "3-button-battery-temperature-humidity" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json}) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - -end - -test.register_coroutine_test("Read appropriate attribute values after tempOffset preference change", function() - local report = clusters.TemperatureMeasurement.attributes.MeasuredValue:build_test_report_data(mock_device,1, 2000) - mock_device.st_store.preferences = {tempOffset = "0"} - - test.socket.matter:__queue_receive({mock_device.id, report}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.temperatureMeasurement.temperature({ - value = 20.0, - unit = "C" - }))) - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {tempOffset = "5"}})) - test.socket.matter:__expect_send({mock_device.id, clusters.TemperatureMeasurement.attributes.MeasuredValue:read(mock_device)}) - - test.wait_for_events() - - test.socket.matter:__queue_receive({mock_device.id, report}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.temperatureMeasurement.temperature({ - value = 20.0, - unit = "C" - }))) -end) - -test.register_coroutine_test("Read appropriate attribute values after humidityOffset preference change", function() - local report = clusters.RelativeHumidityMeasurement.attributes.MeasuredValue:build_test_report_data(mock_device,2, 2000) - mock_device.st_store.preferences = {humidityOffset = "0"} - - test.socket.matter:__queue_receive({mock_device.id, report}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.relativeHumidityMeasurement.humidity({ - value = 20 - }))) - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {humidityOffset = "5"}})) - test.socket.matter:__expect_send({mock_device.id, clusters.RelativeHumidityMeasurement.attributes.MeasuredValue:read(mock_device)}) - - test.wait_for_events() - - test.socket.matter:__queue_receive({mock_device.id, report}) - test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.relativeHumidityMeasurement.humidity({ - value = 20 - }))) -end) - -test.set_test_init_function(test_init) - -test.run_registered_tests()