diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index 27722d9207..bd0dbcdf3e 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -51,6 +51,12 @@ matterManufacturer: vendorId: 0x1533 productId: 0x0012 deviceProfileName: lock-user-pin-battery + #Kwikset + - id: "5153/66" + deviceLabel: Kwikset Halo Select Plus + vendorId: 0x1421 + productId: 0x0042 + deviceProfileName: lock-user-pin-battery #Level - id: "4767/1" deviceLabel: Level Lock Plus (Matter) diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index cbab5a4db0..0eee7edc40 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -82,7 +82,8 @@ local NEW_MATTER_LOCK_PRODUCTS = { {0x135D, 0x00B0}, -- Nuki, Smart Lock {0x15F2, 0x0001}, -- Viomi, AiSafety Smart Lock E100 {0x158B, 0x0001}, -- Deasino, DS-MT01 - {0x10E1, 0x2002} -- VDA + {0x10E1, 0x2002}, -- VDA + {0x1421, 0x0042}, -- Kwikset Halo Select Plus } local battery_support = { diff --git a/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery-illuminance.yml b/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery-illuminance.yml index b6c30d3421..3bac085178 100644 --- a/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery-illuminance.yml +++ b/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery-illuminance.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: matter-motion-battery-illuminance components: - id: main diff --git a/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery.yml b/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery.yml index 0e2b57c71e..66ea49c033 100644 --- a/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery.yml +++ b/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: matter-motion-battery components: - id: main diff --git a/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel-illuminance.yml b/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel-illuminance.yml index 87c7fdf701..f9f97b3360 100644 --- a/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel-illuminance.yml +++ b/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel-illuminance.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: matter-motion-batteryLevel-illuminance components: - id: main diff --git a/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel.yml b/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel.yml index afc752557f..fa497ba042 100644 --- a/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel.yml +++ b/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: matter-motion-batteryLevel components: - id: main diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index c67eec7bf2..0bc24ec7ca 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -905,6 +905,41 @@ matterManufacturer: vendorId: 0x1189 productId: 0x0AC3 deviceProfileName: light-color-level + - id: "4489/61444" + deviceLabel: OSRAM MATTER PLUG UK + vendorId: 0x1189 + productId: 0xF004 + deviceProfileName: plug-binary + - id: "4489/61443" + deviceLabel: OSRAM MATTER PLUG EU WH + vendorId: 0x1189 + productId: 0xF003 + deviceProfileName: plug-binary + - id: "4489/61441" + deviceLabel: OSRAM MATTER CLASSIC A 60W + vendorId: 0x1189 + productId: 0xF001 + deviceProfileName: light-color-level + - id: "4489/2353" + deviceLabel: SMART MATTER FLOORCORN200 MGC WT + vendorId: 0x1189 + productId: 0x0931 + deviceProfileName: light-color-level + - id: "4489/2350" + deviceLabel: SMART MATTER FLOORCORN140 MGC BK + vendorId: 0x1189 + productId: 0x092E + deviceProfileName: light-color-level + - id: "4489/2352" + deviceLabel: SMART MATTER FLOORCORN140 MGC WT + vendorId: 0x1189 + productId: 0x0930 + deviceProfileName: light-color-level + - id: "4489/2351" + deviceLabel: SMART MATTER FLOORCORN200 MGC BK + vendorId: 0x1189 + productId: 0x092F + deviceProfileName: light-color-level #Shelly - id: "5264/1" deviceLabel: Shelly Plug S MTR Gen3 diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua index 0e3fe5f843..484e4c7a15 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua @@ -396,15 +396,13 @@ end function CameraAttributeHandlers.camera_av_stream_management_attribute_list_handler(driver, device, ib, response) if not ib.data.elements then return end local status_light_enabled_present, status_light_brightness_present = false, false - local attribute_ids, capability_ids = {}, {} + local attribute_ids = {} for _, attr in ipairs(ib.data.elements) do if attr.value == clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID then status_light_enabled_present = true - table.insert(capability_ids, capabilities.switch.ID) table.insert(attribute_ids, clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID) elseif attr.value == clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID then status_light_brightness_present = true - table.insert(capability_ids, capabilities.mode.ID) table.insert(attribute_ids, clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) end end @@ -413,7 +411,6 @@ function CameraAttributeHandlers.camera_av_stream_management_attribute_list_hand endpoint_id = ib.endpoint_id, cluster_id = ib.cluster_id, attribute_ids = attribute_ids, - capability_ids = capability_ids } device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist=true}) camera_cfg.match_profile(device, status_light_enabled_present, status_light_brightness_present) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua index e307db8d15..90c5ee3498 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua @@ -49,15 +49,15 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr local microphone_component_capabilities = {} local doorbell_component_capabilities = {} + local function has_server_cluster_type(cluster) + return cluster.cluster_type == "SERVER" or cluster.cluster_type == "BOTH" + end + local camera_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) if #camera_endpoints > 0 then - if #device:get_endpoints(clusters.WebRTCTransportProvider.ID, {cluster_type = "SERVER"}) > 0 and - #device:get_endpoints(clusters.WebRTCTransportRequestor.ID, {cluster_type = "CLIENT"}) > 0 then - table.insert(main_component_capabilities, capabilities.webrtc.ID) - end local camera_ep = switch_utils.get_endpoint_info(device, camera_endpoints[1]) for _, ep_cluster in pairs(camera_ep.clusters or {}) do - if ep_cluster.cluster_id == clusters.CameraAvStreamManagement.ID then + if ep_cluster.cluster_id == clusters.CameraAvStreamManagement.ID and has_server_cluster_type(ep_cluster) then local clus_has_feature = function(feature_bitmap) return clusters.CameraAvStreamManagement.are_features_supported(feature_bitmap, ep_cluster.feature_map) end @@ -92,7 +92,7 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.NIGHT_VISION) then table.insert(main_component_capabilities, capabilities.nightVision.ID) end - elseif ep_cluster.cluster_id == clusters.CameraAvSettingsUserLevelManagement.ID then + elseif ep_cluster.cluster_id == clusters.CameraAvSettingsUserLevelManagement.ID and has_server_cluster_type(ep_cluster) then local clus_has_feature = function(feature_bitmap) return clusters.CameraAvSettingsUserLevelManagement.are_features_supported(feature_bitmap, ep_cluster.feature_map) end @@ -102,10 +102,13 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr table.insert(main_component_capabilities, capabilities.mechanicalPanTiltZoom.ID) end table.insert(main_component_capabilities, capabilities.videoStreamSettings.ID) - elseif ep_cluster.cluster_id == clusters.ZoneManagement.ID then + elseif ep_cluster.cluster_id == clusters.ZoneManagement.ID and has_server_cluster_type(ep_cluster) then table.insert(main_component_capabilities, capabilities.zoneManagement.ID) - elseif ep_cluster.cluster_id == clusters.OccupancySensing.ID then + elseif ep_cluster.cluster_id == clusters.OccupancySensing.ID and has_server_cluster_type(ep_cluster) then table.insert(main_component_capabilities, capabilities.motionSensor.ID) + elseif ep_cluster.cluster_id == clusters.WebRTCTransportProvider.ID and has_server_cluster_type(ep_cluster) and + #device:get_endpoints(clusters.WebRTCTransportRequestor.ID, {cluster_type = "CLIENT"}) > 0 then + table.insert(main_component_capabilities, capabilities.webrtc.ID) end end end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua index 4334d2a304..dcdd936950 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua @@ -35,10 +35,6 @@ function CameraUtils.update_camera_component_map(device) clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel.ID, clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel.ID, }, - capability_ids = { - capabilities.audioMute.ID, - capabilities.audioVolume.ID, - } } end if CameraUtils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then @@ -51,10 +47,6 @@ function CameraUtils.update_camera_component_map(device) clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel.ID, clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel.ID, }, - capability_ids = { - capabilities.audioMute.ID, - capabilities.audioVolume.ID, - } } end device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua index adc0da500e..eebe734fcd 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -25,7 +25,7 @@ function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_on -- per spec, the Switch device types support OnOff as CLIENT, though some vendors break spec and support it as SERVER. local primary_dt_id = switch_utils.find_max_subset_device_type(ep_info, fields.DEVICE_TYPE_ID.LIGHT) or switch_utils.find_max_subset_device_type(ep_info, fields.DEVICE_TYPE_ID.SWITCH) - or ep_info.device_types[1] and ep_info.device_types[1].device_type_id + or switch_utils.find_primary_device_type(ep_info) local generic_profile = fields.device_type_profile_map[primary_dt_id] diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index c350a7adaf..3a0bdc9fdb 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -36,6 +36,7 @@ SwitchFields.CURRENT_HUESAT_ATTR_MAX = 254 SwitchFields.DEVICE_TYPE_ID = { AGGREGATOR = 0x000E, + BRIDGED_NODE = 0x0013, CAMERA = 0x0142, CHIME = 0x0146, DIMMABLE_PLUG_IN_UNIT = 0x010B, diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index 718e186dcf..b6cc09e007 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -93,23 +93,33 @@ function utils.device_type_supports_button_switch_combination(device, endpoint_i return utils.tbl_contains(dimmable_eps, endpoint_id) end --- Some devices report multiple device types which are a subset of --- a superset device type (Ex. Dimmable Light is a superset of On/Off Light). --- We should map to the largest superset device type supported. --- This can be done by matching to the device type with the highest ID +--- Some devices report multiple device types which are a subset of a superset +--- device type (Ex. Dimmable Light is a superset of On/Off Light). We should map +--- to the largest superset device type supported. +--- This can be done by matching to the device type with the highest ID +--- note: that superset device types have a higher ID than those of their subset +--- is heuristic and could therefore break in the future, were the spec expanded function utils.find_max_subset_device_type(ep, device_type_set) if ep.endpoint_id == 0 then return end -- EP-scoped device types not permitted on Root Node - local primary_dt_id = ep.device_types[1] and ep.device_types[1].device_type_id - if utils.tbl_contains(device_type_set, primary_dt_id) then - for _, dt in ipairs(ep.device_types) do - -- only device types in the subset should be considered. - if utils.tbl_contains(device_type_set, dt.device_type_id) then - primary_dt_id = math.max(primary_dt_id, dt.device_type_id) - end + local primary_dt_id = -1 + for _, dt in ipairs(ep.device_types) do + -- only device types in the subset should be considered. + if utils.tbl_contains(device_type_set, dt.device_type_id) then + primary_dt_id = math.max(primary_dt_id, dt.device_type_id) + end + end + return (primary_dt_id > 0) and primary_dt_id or nil +end + +--- Lights and Switches are Device Types that have Superset-style functionality +--- For all other device types, this function should be used to identify the primary device type +function utils.find_primary_device_type(ep_info) + for _, dt in ipairs(ep_info.device_types) do + if dt.device_type_id ~= fields.DEVICE_TYPE_ID.BRIDGED_NODE then + -- if this is not a bridged node, return the first device type seen + return dt.device_type_id end - return primary_dt_id end - return nil end --- find_default_endpoint is a helper function to handle situations where @@ -168,60 +178,40 @@ function utils.component_to_endpoint(device, component) return utils.find_default_endpoint(device) end ---- An extension of the library function endpoint_to_component, to support a mapping scheme ---- that includes cluster and attribute id's so that we can use multiple components for a ---- single endpoint. +--- An extension of the library function endpoint_to_component, used to support a mapping scheme +--- that optionally includes cluster and attribute ids so that multiple components can be mapped +--- to a single endpoint. --- --- @param device any a Matter device object ---- @param opts number|table either is an ep_id or a table { endpoint_id, capability_id } +--- @param ep_info number|table either an ep_id or a table { endpoint_id, optional(cluster_id), optional(attribute_id) } +--- where cluster_id is required for an attribute_id to be handled. --- @return string component -function utils.endpoint_to_component(device, opts) - local ep_info = {} - if type(opts) == "number" then - ep_info.endpoint_id = opts - elseif type(opts) == "table" then - if opts.endpoint_info then - ep_info = opts.endpoint_info - else - ep_info = { - endpoint_id = opts.endpoint_id, - cluster_id = opts.cluster_id, - attribute_id = opts.attribute_id - } - end +function utils.endpoint_to_component(device, ep_info) + if type(ep_info) == "number" then + ep_info = { endpoint_id = ep_info } end for component, map_info in pairs(device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {}) do if type(map_info) == "number" and map_info == ep_info.endpoint_id then return component - elseif type(map_info) == "table" and map_info.endpoint_id == ep_info.endpoint_id then - if (not map_info.cluster_id or (map_info.cluster_id == ep_info.cluster_id - and utils.tbl_contains(map_info.attribute_ids, ep_info.attribute_id))) - and (not opts.capability_id or utils.tbl_contains(map_info.capability_ids, opts.capability_id)) then + elseif type(map_info) == "table" and map_info.endpoint_id == ep_info.endpoint_id + and (not map_info.cluster_id or (map_info.cluster_id == ep_info.cluster_id + and (not map_info.attribute_ids or utils.tbl_contains(map_info.attribute_ids, ep_info.attribute_id)))) then return component - end end end return "main" end ---- An extension of the library function emit_event_for_endpoint, to support devices with ---- multiple components defined for the same endpoint, since they can't be easily ---- differentiated based on a simple endpoint id to component mapping, but we can extend ---- this mapping to include the cluster and attribute id's so that we know which component ---- to route events to. +--- An extension of the library function emit_event_for_endpoint, used to support devices with +--- multiple components mapped to the same endpoint. This is handled by extending the parameters to optionally +--- include a cluster id and attribute id for more specific routing --- --- @param device any a Matter device object ---- @param ep_info number|table endpoint_id or ib (includes endpoint_id, cluster_id, attribute_id) +--- @param ep_info number|table endpoint_id or an ib (the ib data includes endpoint_id, cluster_id, and attribute_id fields) --- @param event any a capability event object function utils.emit_event_for_endpoint(device, ep_info, event) if type(ep_info) == "number" then ep_info = { endpoint_id = ep_info } - elseif type(ep_info) == "table" then - ep_info = { - endpoint_id = ep_info.endpoint_id, - cluster_id = ep_info.cluster_id, - attribute_id = ep_info.attribute_id - } end if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then local child = utils.find_child(device, ep_info.endpoint_id) @@ -230,8 +220,7 @@ function utils.emit_event_for_endpoint(device, ep_info, event) return end end - local opts = { endpoint_info = ep_info, capability_id = event.capability.ID } - local comp_id = utils.endpoint_to_component(device, opts) + local comp_id = utils.endpoint_to_component(device, ep_info) local comp = device.profile.components[comp_id] device:emit_component_event(comp, event) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua index d504107c79..b9804a2a76 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -156,7 +156,6 @@ local function update_device_profile() { "main", { - "webrtc", "videoCapture2", "cameraViewportSettings", "localMediaStorage", @@ -168,6 +167,7 @@ local function update_device_profile() "mechanicalPanTiltZoom", "videoStreamSettings", "zoneManagement", + "webrtc", "motionSensor", "sounds", } diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index 92a8ea3207..6cd56d3a24 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -50,6 +50,11 @@ matterManufacturer: vendorId: 0x1206 productId: 0x0001 deviceProfileName: thermostat-nostate-nobattery + - id: "4614/17" + deviceLabel: LUX TQX Smart Thermostat + vendorId: 0x1206 + productId: 0x0011 + deviceProfileName: thermostat-humidity-nostate-nobattery #Meross - id: "4933/57345" deviceLabel: Smart Wi-Fi Thermostat diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua deleted file mode 100644 index 320826dacc..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua +++ /dev/null @@ -1,76 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local AttributeList = { - ID = 0xFFFB, - NAME = "AttributeList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - -function AttributeList:augment_type(data_type_obj) - for i, v in ipairs(data_type_obj.elements) do - data_type_obj.elements[i] = data_types.validate_or_build_type(v, AttributeList.element_type) - end -end - -function AttributeList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function AttributeList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - - -function AttributeList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function AttributeList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function AttributeList:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function AttributeList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) -return AttributeList - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyExported.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyExported.lua deleted file mode 100644 index 22befec642..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyExported.lua +++ /dev/null @@ -1,68 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local CumulativeEnergyExported = { - ID = 0x0002, - NAME = "CumulativeEnergyExported", - base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", -} - -function CumulativeEnergyExported:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function CumulativeEnergyExported:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function CumulativeEnergyExported:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function CumulativeEnergyExported:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function CumulativeEnergyExported:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - self:augment_type(data) - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function CumulativeEnergyExported:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(CumulativeEnergyExported, {__call = CumulativeEnergyExported.new_value, __index = CumulativeEnergyExported.base_type}) -return CumulativeEnergyExported - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyExported.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyExported.lua deleted file mode 100644 index 4c1ee29274..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyExported.lua +++ /dev/null @@ -1,68 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local PeriodicEnergyExported = { - ID = 0x0004, - NAME = "PeriodicEnergyExported", - base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", -} - -function PeriodicEnergyExported:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function PeriodicEnergyExported:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function PeriodicEnergyExported:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function PeriodicEnergyExported:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function PeriodicEnergyExported:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - self:augment_type(data) - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function PeriodicEnergyExported:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(PeriodicEnergyExported, {__call = PeriodicEnergyExported.new_value, __index = PeriodicEnergyExported.base_type}) -return PeriodicEnergyExported - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/CumulativeEnergyMeasured.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/CumulativeEnergyMeasured.lua deleted file mode 100644 index 13136bd791..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/CumulativeEnergyMeasured.lua +++ /dev/null @@ -1,108 +0,0 @@ -local data_types = require "st.matter.data_types" -local cluster_base = require "st.matter.cluster_base" -local TLVParser = require "st.matter.TLV.TLVParser" -local StructureABC = require "st.matter.data_types.base_defs.StructureABC" - -local CumulativeEnergyMeasured = { - ID = 0x0000, - NAME = "CumulativeEnergyMeasured", - base_type = data_types.Structure, -} - -CumulativeEnergyMeasured.field_defs = { - { - name = "energy_imported", - field_id = 0, - is_nullable = false, - is_optional = true, - data_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", - }, - { - name = "energy_exported", - field_id = 1, - is_nullable = false, - is_optional = true, - data_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", - }, -} - -function CumulativeEnergyMeasured:augment_type(base_type_obj) - local elems = {} - for _, v in ipairs(base_type_obj.elements) do - for _, field_def in ipairs(self.field_defs) do - if field_def.field_id == v.field_id and not - ((field_def.is_nullable or field_def.is_optional) and v.elements == nil) then - elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) - if field_def.element_type ~= nil then - for i, e in ipairs(elems[field_def.name].elements) do - elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) - end - end - end - end - end - base_type_obj.elements = elems -end - -function CumulativeEnergyMeasured:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - nil, --attribute_id - self.ID - ) -end - -function CumulativeEnergyMeasured:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - nil, --attribute_id - self.ID - ) -end - -function CumulativeEnergyMeasured:build_test_event_report( - device, - endpoint_id, - fields, - status -) - local data = {} - data.elements = {} - data.num_elements = 0 - setmetatable(data, StructureABC.new_mt({NAME = "CumulativeEnergyMeasuredEventData", ID = 0x15})) - for idx, field_def in ipairs(self.field_defs) do --Note: idx is 1 when field_id is 0 - if (not field_def.is_optional and not field_def.is_nullable) and not fields[field_def.name] then - error("Missing non optional or non_nullable field: " .. field_def.name) - elseif fields[field_def.name] then - data.elements[field_def.name] = data_types.validate_or_build_type(fields[field_def.name], field_def.data_type, field_def.name) - data.elements[field_def.name].field_id = field_def.field_id - data.num_elements = data.num_elements + 1 - end - end - return cluster_base.build_test_event_report( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function CumulativeEnergyMeasured:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -function CumulativeEnergyMeasured:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -return CumulativeEnergyMeasured - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/PeriodicEnergyMeasured.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/PeriodicEnergyMeasured.lua deleted file mode 100644 index e2c4b1b577..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/PeriodicEnergyMeasured.lua +++ /dev/null @@ -1,108 +0,0 @@ -local data_types = require "st.matter.data_types" -local cluster_base = require "st.matter.cluster_base" -local TLVParser = require "st.matter.TLV.TLVParser" -local StructureABC = require "st.matter.data_types.base_defs.StructureABC" - -local PeriodicEnergyMeasured = { - ID = 0x0001, - NAME = "PeriodicEnergyMeasured", - base_type = data_types.Structure, -} - -PeriodicEnergyMeasured.field_defs = { - { - name = "energy_imported", - field_id = 0, - is_nullable = false, - is_optional = true, - data_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", - }, - { - name = "energy_exported", - field_id = 1, - is_nullable = false, - is_optional = true, - data_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", - }, -} - -function PeriodicEnergyMeasured:augment_type(base_type_obj) - local elems = {} - for _, v in ipairs(base_type_obj.elements) do - for _, field_def in ipairs(self.field_defs) do - if field_def.field_id == v.field_id and not - ((field_def.is_nullable or field_def.is_optional) and v.elements == nil) then - elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) - if field_def.element_type ~= nil then - for i, e in ipairs(elems[field_def.name].elements) do - elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) - end - end - end - end - end - base_type_obj.elements = elems -end - -function PeriodicEnergyMeasured:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - nil, --attribute_id - self.ID - ) -end - -function PeriodicEnergyMeasured:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - nil, --attribute_id - self.ID - ) -end - -function PeriodicEnergyMeasured:build_test_event_report( - device, - endpoint_id, - fields, - status -) - local data = {} - data.elements = {} - data.num_elements = 0 - setmetatable(data, StructureABC.new_mt({NAME = "PeriodicEnergyMeasuredEventData", ID = 0x15})) - for idx, field_def in ipairs(self.field_defs) do --Note: idx is 1 when field_id is 0 - if (not field_def.is_optional and not field_def.is_nullable) and not fields[field_def.name] then - error("Missing non optional or non_nullable field: " .. field_def.name) - elseif fields[field_def.name] then - data.elements[field_def.name] = data_types.validate_or_build_type(fields[field_def.name], field_def.data_type, field_def.name) - data.elements[field_def.name].field_id = field_def.field_id - data.num_elements = data.num_elements + 1 - end - end - return cluster_base.build_test_event_report( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function PeriodicEnergyMeasured:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -function PeriodicEnergyMeasured:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -return PeriodicEnergyMeasured - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/init.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/init.lua deleted file mode 100644 index 02b085583e..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/init.lua +++ /dev/null @@ -1,25 +0,0 @@ -local event_mt = {} -event_mt.__event_cache = {} -event_mt.__index = function(self, key) - if event_mt.__event_cache[key] == nil then - local req_loc = string.format("ElectricalEnergyMeasurement.server.events.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - event_mt.__event_cache[key] = raw_def - end - return event_mt.__event_cache[key] -end - - -local ElectricalEnergyMeasurementEvents = {} - -function ElectricalEnergyMeasurementEvents:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(ElectricalEnergyMeasurementEvents, event_mt) - -return ElectricalEnergyMeasurementEvents - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/Feature.lua deleted file mode 100644 index 717ba6a2f3..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/Feature.lua +++ /dev/null @@ -1,116 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.IMPORTED_ENERGY = 0x0001 -Feature.EXPORTED_ENERGY = 0x0002 -Feature.CUMULATIVE_ENERGY = 0x0004 -Feature.PERIODIC_ENERGY = 0x0008 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - IMPORTED_ENERGY = 0x0001, - EXPORTED_ENERGY = 0x0002, - CUMULATIVE_ENERGY = 0x0004, - PERIODIC_ENERGY = 0x0008, -} - -Feature.is_imported_energy_set = function(self) - return (self.value & self.IMPORTED_ENERGY) ~= 0 -end - -Feature.set_imported_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.IMPORTED_ENERGY - else - self.value = self.IMPORTED_ENERGY - end -end - -Feature.unset_imported_energy = function(self) - self.value = self.value & (~self.IMPORTED_ENERGY & self.BASE_MASK) -end -Feature.is_exported_energy_set = function(self) - return (self.value & self.EXPORTED_ENERGY) ~= 0 -end - -Feature.set_exported_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.EXPORTED_ENERGY - else - self.value = self.EXPORTED_ENERGY - end -end - -Feature.unset_exported_energy = function(self) - self.value = self.value & (~self.EXPORTED_ENERGY & self.BASE_MASK) -end -Feature.is_cumulative_energy_set = function(self) - return (self.value & self.CUMULATIVE_ENERGY) ~= 0 -end - -Feature.set_cumulative_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.CUMULATIVE_ENERGY - else - self.value = self.CUMULATIVE_ENERGY - end -end - -Feature.unset_cumulative_energy = function(self) - self.value = self.value & (~self.CUMULATIVE_ENERGY & self.BASE_MASK) -end -Feature.is_periodic_energy_set = function(self) - return (self.value & self.PERIODIC_ENERGY) ~= 0 -end - -Feature.set_periodic_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.PERIODIC_ENERGY - else - self.value = self.PERIODIC_ENERGY - end -end - -Feature.unset_periodic_energy = function(self) - self.value = self.value & (~self.PERIODIC_ENERGY & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.IMPORTED_ENERGY | - Feature.EXPORTED_ENERGY | - Feature.CUMULATIVE_ENERGY | - Feature.PERIODIC_ENERGY - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_imported_energy_set = Feature.is_imported_energy_set, - set_imported_energy = Feature.set_imported_energy, - unset_imported_energy = Feature.unset_imported_energy, - is_exported_energy_set = Feature.is_exported_energy_set, - set_exported_energy = Feature.set_exported_energy, - unset_exported_energy = Feature.unset_exported_energy, - is_cumulative_energy_set = Feature.is_cumulative_energy_set, - set_cumulative_energy = Feature.set_cumulative_energy, - unset_cumulative_energy = Feature.unset_cumulative_energy, - is_periodic_energy_set = Feature.is_periodic_energy_set, - set_periodic_energy = Feature.set_periodic_energy, - unset_periodic_energy = Feature.unset_periodic_energy, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/init.lua deleted file mode 100644 index 54785d16c6..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/init.lua +++ /dev/null @@ -1,94 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local ElectricalPowerMeasurementServerAttributes = require "ElectricalPowerMeasurement.server.attributes" -local ElectricalPowerMeasurementTypes = require "ElectricalPowerMeasurement.types" - -local ElectricalPowerMeasurement = {} - -ElectricalPowerMeasurement.ID = 0x0090 -ElectricalPowerMeasurement.NAME = "ElectricalPowerMeasurement" -ElectricalPowerMeasurement.server = {} -ElectricalPowerMeasurement.client = {} -ElectricalPowerMeasurement.server.attributes = ElectricalPowerMeasurementServerAttributes:set_parent_cluster(ElectricalPowerMeasurement) -ElectricalPowerMeasurement.types = ElectricalPowerMeasurementTypes - -function ElectricalPowerMeasurement:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "PowerMode", - [0x0001] = "NumberOfMeasurementTypes", - [0x0002] = "Accuracy", - [0x0003] = "Ranges", - [0x0004] = "Voltage", - [0x0005] = "ActiveCurrent", - [0x0006] = "ReactiveCurrent", - [0x0007] = "ApparentCurrent", - [0x0008] = "ActivePower", - [0x0009] = "ReactivePower", - [0x000A] = "ApparentPower", - [0x000B] = "RMSVoltage", - [0x000C] = "RMSCurrent", - [0x000D] = "RMSPower", - [0x000E] = "Frequency", - [0x000F] = "HarmonicCurrents", - [0x0010] = "HarmonicPhases", - [0x0011] = "PowerFactor", - [0x0012] = "NeutralCurrent", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - -ElectricalPowerMeasurement.attribute_direction_map = { - ["PowerMode"] = "server", - ["NumberOfMeasurementTypes"] = "server", - ["Accuracy"] = "server", - ["Ranges"] = "server", - ["Voltage"] = "server", - ["ActiveCurrent"] = "server", - ["ReactiveCurrent"] = "server", - ["ApparentCurrent"] = "server", - ["ActivePower"] = "server", - ["ReactivePower"] = "server", - ["ApparentPower"] = "server", - ["RMSVoltage"] = "server", - ["RMSCurrent"] = "server", - ["RMSPower"] = "server", - ["Frequency"] = "server", - ["HarmonicCurrents"] = "server", - ["HarmonicPhases"] = "server", - ["PowerFactor"] = "server", - ["NeutralCurrent"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - -ElectricalPowerMeasurement.FeatureMap = ElectricalPowerMeasurement.types.Feature - -function ElectricalPowerMeasurement.are_features_supported(feature, feature_map) - if (ElectricalPowerMeasurement.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = ElectricalPowerMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, ElectricalPowerMeasurement.NAME)) - end - return ElectricalPowerMeasurement[direction].attributes[key] -end -ElectricalPowerMeasurement.attributes = {} -setmetatable(ElectricalPowerMeasurement.attributes, attribute_helper_mt) - -setmetatable(ElectricalPowerMeasurement, {__index = cluster_base}) - -return ElectricalPowerMeasurement - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/Feature.lua deleted file mode 100644 index cbda4f3478..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/Feature.lua +++ /dev/null @@ -1,138 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.DIRECT_CURRENT = 0x0001 -Feature.ALTERNATING_CURRENT = 0x0002 -Feature.POLYPHASE_POWER = 0x0004 -Feature.HARMONICS = 0x0008 -Feature.POWER_QUALITY = 0x0010 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - DIRECT_CURRENT = 0x0001, - ALTERNATING_CURRENT = 0x0002, - POLYPHASE_POWER = 0x0004, - HARMONICS = 0x0008, - POWER_QUALITY = 0x0010, -} - -Feature.is_direct_current_set = function(self) - return (self.value & self.DIRECT_CURRENT) ~= 0 -end - -Feature.set_direct_current = function(self) - if self.value ~= nil then - self.value = self.value | self.DIRECT_CURRENT - else - self.value = self.DIRECT_CURRENT - end -end - -Feature.unset_direct_current = function(self) - self.value = self.value & (~self.DIRECT_CURRENT & self.BASE_MASK) -end -Feature.is_alternating_current_set = function(self) - return (self.value & self.ALTERNATING_CURRENT) ~= 0 -end - -Feature.set_alternating_current = function(self) - if self.value ~= nil then - self.value = self.value | self.ALTERNATING_CURRENT - else - self.value = self.ALTERNATING_CURRENT - end -end - -Feature.unset_alternating_current = function(self) - self.value = self.value & (~self.ALTERNATING_CURRENT & self.BASE_MASK) -end -Feature.is_polyphase_power_set = function(self) - return (self.value & self.POLYPHASE_POWER) ~= 0 -end - -Feature.set_polyphase_power = function(self) - if self.value ~= nil then - self.value = self.value | self.POLYPHASE_POWER - else - self.value = self.POLYPHASE_POWER - end -end - -Feature.unset_polyphase_power = function(self) - self.value = self.value & (~self.POLYPHASE_POWER & self.BASE_MASK) -end -Feature.is_harmonics_set = function(self) - return (self.value & self.HARMONICS) ~= 0 -end - -Feature.set_harmonics = function(self) - if self.value ~= nil then - self.value = self.value | self.HARMONICS - else - self.value = self.HARMONICS - end -end - -Feature.unset_harmonics = function(self) - self.value = self.value & (~self.HARMONICS & self.BASE_MASK) -end -Feature.is_power_quality_set = function(self) - return (self.value & self.POWER_QUALITY) ~= 0 -end - -Feature.set_power_quality = function(self) - if self.value ~= nil then - self.value = self.value | self.POWER_QUALITY - else - self.value = self.POWER_QUALITY - end -end - -Feature.unset_power_quality = function(self) - self.value = self.value & (~self.POWER_QUALITY & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.DIRECT_CURRENT | - Feature.ALTERNATING_CURRENT | - Feature.POLYPHASE_POWER | - Feature.HARMONICS | - Feature.POWER_QUALITY - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_direct_current_set = Feature.is_direct_current_set, - set_direct_current = Feature.set_direct_current, - unset_direct_current = Feature.unset_direct_current, - is_alternating_current_set = Feature.is_alternating_current_set, - set_alternating_current = Feature.set_alternating_current, - unset_alternating_current = Feature.unset_alternating_current, - is_polyphase_power_set = Feature.is_polyphase_power_set, - set_polyphase_power = Feature.set_polyphase_power, - unset_polyphase_power = Feature.unset_polyphase_power, - is_harmonics_set = Feature.is_harmonics_set, - set_harmonics = Feature.set_harmonics, - unset_harmonics = Feature.unset_harmonics, - is_power_quality_set = Feature.is_power_quality_set, - set_power_quality = Feature.set_power_quality, - unset_power_quality = Feature.unset_power_quality, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/init.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/init.lua deleted file mode 100644 index 16d13a0688..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/init.lua +++ /dev/null @@ -1,15 +0,0 @@ -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("ElectricalPowerMeasurement.types." .. key) - end - return types_mt.__types_cache[key] -end - -local ElectricalPowerMeasurementTypes = {} - -setmetatable(ElectricalPowerMeasurementTypes, types_mt) - -return ElectricalPowerMeasurementTypes - diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/AttributeList.lua deleted file mode 100644 index f2ca149f03..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/AttributeList.lua +++ /dev/null @@ -1,75 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local AttributeList = { - ID = 0xFFFB, - NAME = "AttributeList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - -function AttributeList:augment_type(data_type_obj) - for i, v in ipairs(data_type_obj.elements) do - data_type_obj.elements[i] = data_types.validate_or_build_type(v, AttributeList.element_type) - end -end - -function AttributeList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function AttributeList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function AttributeList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function AttributeList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function AttributeList:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function AttributeList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) -return AttributeList - diff --git a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/AcceptedCommandList.lua deleted file mode 100644 index 6a8d95df1d..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/AcceptedCommandList.lua +++ /dev/null @@ -1,75 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local data_types = require "st.matter.data_types" -local TLVParser = require "st.matter.TLV.TLVParser" - -local AcceptedCommandList = { - ID = 0xFFF9, - NAME = "AcceptedCommandList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - -function AcceptedCommandList:augment_type(data_type_obj) - for i, v in ipairs(data_type_obj.elements) do - data_type_obj.elements[i] = data_types.validate_or_build_type(v, AcceptedCommandList.element_type) - end -end - -function AcceptedCommandList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function AcceptedCommandList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AcceptedCommandList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AcceptedCommandList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function AcceptedCommandList:build_test_report_data( - device, - endpoint_id, - value, - status -) - local data = data_types.validate_or_build_type(value, self.base_type) - - return cluster_base.build_test_report_data( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function AcceptedCommandList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value, __index = AcceptedCommandList.base_type}) -return AcceptedCommandList - diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/init.lua similarity index 90% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/init.lua index 3fa5f37100..7bfdfd7248 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/init.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local ActivatedCarbonFilterMonitoringServerAttributes = require "ActivatedCarbonFilterMonitoring.server.attributes" -local ActivatedCarbonFilterMonitoringServerCommands = require "ActivatedCarbonFilterMonitoring.server.commands" -local ActivatedCarbonFilterMonitoringTypes = require "ActivatedCarbonFilterMonitoring.types" +local ActivatedCarbonFilterMonitoringServerAttributes = require "embedded_clusters.ActivatedCarbonFilterMonitoring.server.attributes" +local ActivatedCarbonFilterMonitoringServerCommands = require "embedded_clusters.ActivatedCarbonFilterMonitoring.server.commands" +local ActivatedCarbonFilterMonitoringTypes = require "embedded_clusters.ActivatedCarbonFilterMonitoring.types" local ActivatedCarbonFilterMonitoring = {} diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua index 4980356485..df65143e66 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local ChangeIndication = { ID = 0x0002, NAME = "ChangeIndication", - base_type = require "ActivatedCarbonFilterMonitoring.types.ChangeIndicationEnum", + base_type = require "embedded_clusters.ActivatedCarbonFilterMonitoring.types.ChangeIndicationEnum", } function ChangeIndication:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua similarity index 89% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua index e668aa4c48..361e423a1c 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua @@ -1,4 +1,7 @@ -local cluster_base = require "st.matter.cluster_base" +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/init.lua index a02378a50d..1ecdf6a931 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("ActivatedCarbonFilterMonitoring.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.ActivatedCarbonFilterMonitoring.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/ResetCondition.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua similarity index 96% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/ResetCondition.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua index 040ce653b4..1ac942f780 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/ResetCondition.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/init.lua index 7f641481d4..ec343da578 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local command_mt = {} command_mt.__command_cache = {} command_mt.__index = function(self, key) if command_mt.__command_cache[key] == nil then - local req_loc = string.format("ActivatedCarbonFilterMonitoring.server.commands.%s", key) + local req_loc = string.format("embedded_clusters.ActivatedCarbonFilterMonitoring.server.commands.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua similarity index 92% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua index 438de24c94..6b5ab62494 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/Feature.lua similarity index 96% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/Feature.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/Feature.lua index 88474d1b0f..906e769676 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/Feature.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/Feature.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/init.lua similarity index 62% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/init.lua index 2ff8e6e89a..7cdc2d1561 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("ActivatedCarbonFilterMonitoring.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.ActivatedCarbonFilterMonitoring.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/init.lua similarity index 86% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/init.lua index 78e77fce51..4f6d19cc9f 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local AirQualityServerAttributes = require "AirQuality.server.attributes" -local AirQualityTypes = require "AirQuality.types" +local AirQualityServerAttributes = require "embedded_clusters.AirQuality.server.attributes" +local AirQualityTypes = require "embedded_clusters.AirQuality.types" local AirQuality = {} diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AcceptedCommandList.lua similarity index 94% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AcceptedCommandList.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AcceptedCommandList.lua index 6a8d95df1d..a1e56c6597 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AcceptedCommandList.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AcceptedCommandList.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AirQuality.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AirQuality.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AirQuality.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AirQuality.lua index f92f43543a..1beb6218f9 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AirQuality.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AirQuality.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local AirQuality = { ID = 0x0000, NAME = "AirQuality", - base_type = require "AirQuality.types.AirQualityEnum", + base_type = require "embedded_clusters.AirQuality.types.AirQualityEnum", } function AirQuality:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AttributeList.lua similarity index 94% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AttributeList.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AttributeList.lua index 93e96817e6..238b50ade3 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AttributeList.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AttributeList.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/EventList.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/EventList.lua similarity index 94% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/EventList.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/EventList.lua index 69155cd7ca..719f17a231 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/EventList.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/EventList.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/init.lua similarity index 75% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/init.lua index aef3b476a9..50295b081a 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("AirQuality.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.AirQuality.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/AirQualityEnum.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/AirQualityEnum.lua similarity index 94% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/types/AirQualityEnum.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/AirQualityEnum.lua index 317a42dc9b..c2c255614a 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/AirQualityEnum.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/AirQualityEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/Feature.lua similarity index 96% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/types/Feature.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/Feature.lua index 86b90ce627..906a09a2bb 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/Feature.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/Feature.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/init.lua similarity index 60% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/types/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/init.lua index 88a2b861b7..b77d67de82 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("AirQuality.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.AirQuality.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/init.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/init.lua index f5109f8943..4de97147e4 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local CarbonDioxideConcentrationMeasurementServerAttributes = require "CarbonDioxideConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local CarbonDioxideConcentrationMeasurementServerAttributes = require "embedded_clusters.CarbonDioxideConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local CarbonDioxideConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua similarity index 81% rename from drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua index 50127a3537..cc8f7b47eb 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua @@ -1,9 +1,12 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" local LevelValue = { ID = 0x000A, NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", } function LevelValue:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua similarity index 89% rename from drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua index 763bbaa17b..ca0dafb0dc 100644 --- a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" local MeasuredValue = { diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua similarity index 82% rename from drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua index d18f14b09f..9597906d3a 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -1,11 +1,14 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" local MeasurementUnit = { ID = 0x0008, NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", } function MeasurementUnit:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua index 93583c2080..0206213e6f 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("CarbonDioxideConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.CarbonDioxideConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/init.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/init.lua index e8cdb487f5..a6e1f24d1d 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local CarbonMonoxideConcentrationMeasurementServerAttributes = require "CarbonMonoxideConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local CarbonMonoxideConcentrationMeasurementServerAttributes = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local CarbonMonoxideConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua similarity index 81% rename from drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua index 50127a3537..cc8f7b47eb 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua @@ -1,9 +1,12 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" local LevelValue = { ID = 0x000A, NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", } function LevelValue:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua similarity index 89% rename from drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua index 763bbaa17b..ca0dafb0dc 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" local MeasuredValue = { diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua similarity index 82% rename from drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua index d18f14b09f..9597906d3a 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -1,11 +1,14 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" local MeasurementUnit = { ID = 0x0008, NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", } function MeasurementUnit:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua index 2307e8977d..1a7e7b508c 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("CarbonMonoxideConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.CarbonMonoxideConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/init.lua similarity index 92% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/init.lua index 596d1bfe80..cb4cffa2d2 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local ConcentrationMeasurementServerAttributes = require "ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurementTypes = require "ConcentrationMeasurement.types" +local ConcentrationMeasurementServerAttributes = require "embedded_clusters.ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurementTypes = require "embedded_clusters.ConcentrationMeasurement.types" local ConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/LevelValue.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/LevelValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/LevelValue.lua index e7023a336c..e4f88c8491 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/LevelValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local LevelValue = { ID = 0x000A, NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", } function LevelValue:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasuredValue.lua similarity index 93% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasuredValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasuredValue.lua index c658d2d3aa..2ab739841f 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua index 3d50e8b97b..0fa14745c0 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local MeasurementUnit = { ID = 0x0008, NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", } function MeasurementUnit:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/init.lua index a1a0092151..19cde9aa55 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("ConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.ConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/Feature.lua similarity index 98% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/Feature.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/Feature.lua index 9aa2413903..0bb19bec62 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/Feature.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/Feature.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/LevelValueEnum.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/LevelValueEnum.lua similarity index 93% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/LevelValueEnum.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/LevelValueEnum.lua index b1264d72b8..02b4f727df 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/LevelValueEnum.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/LevelValueEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/MeasurementUnitEnum.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/MeasurementUnitEnum.lua similarity index 94% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/MeasurementUnitEnum.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/MeasurementUnitEnum.lua index c8302c5cc4..6efd90901a 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/MeasurementUnitEnum.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/MeasurementUnitEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/init.lua similarity index 62% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/init.lua index f6da1e6b62..c339f17414 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("ConcentrationMeasurement.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.ConcentrationMeasurement.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua similarity index 61% rename from drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua index bc5685ae75..0f564673a9 100644 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua @@ -1,7 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local ElectricalEnergyMeasurementServerAttributes = require "ElectricalEnergyMeasurement.server.attributes" -local ElectricalEnergyMeasurementEvents = require "ElectricalEnergyMeasurement.server.events" -local ElectricalEnergyMeasurementTypes = require "ElectricalEnergyMeasurement.types" +local ElectricalEnergyMeasurementServerAttributes = require "embedded_clusters.ElectricalEnergyMeasurement.server.attributes" +local ElectricalEnergyMeasurementTypes = require "embedded_clusters.ElectricalEnergyMeasurement.types" local ElectricalEnergyMeasurement = {} ElectricalEnergyMeasurement.ID = 0x0091 @@ -9,20 +11,12 @@ ElectricalEnergyMeasurement.NAME = "ElectricalEnergyMeasurement" ElectricalEnergyMeasurement.server = {} ElectricalEnergyMeasurement.client = {} ElectricalEnergyMeasurement.server.attributes = ElectricalEnergyMeasurementServerAttributes:set_parent_cluster(ElectricalEnergyMeasurement) -ElectricalEnergyMeasurement.server.events = ElectricalEnergyMeasurementEvents:set_parent_cluster(ElectricalEnergyMeasurement) ElectricalEnergyMeasurement.types = ElectricalEnergyMeasurementTypes function ElectricalEnergyMeasurement:get_attribute_by_id(attr_id) local attr_id_map = { - [0x0000] = "Accuracy", [0x0001] = "CumulativeEnergyImported", - [0x0002] = "CumulativeEnergyExported", [0x0003] = "PeriodicEnergyImported", - [0x0004] = "PeriodicEnergyExported", - [0x0005] = "CumulativeEnergyReset", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", } local attr_name = attr_id_map[attr_id] if attr_name ~= nil then @@ -32,15 +26,8 @@ function ElectricalEnergyMeasurement:get_attribute_by_id(attr_id) end ElectricalEnergyMeasurement.attribute_direction_map = { - ["Accuracy"] = "server", ["CumulativeEnergyImported"] = "server", - ["CumulativeEnergyExported"] = "server", ["PeriodicEnergyImported"] = "server", - ["PeriodicEnergyExported"] = "server", - ["CumulativeEnergyReset"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", } ElectricalEnergyMeasurement.FeatureMap = ElectricalEnergyMeasurement.types.Feature @@ -63,13 +50,6 @@ end ElectricalEnergyMeasurement.attributes = {} setmetatable(ElectricalEnergyMeasurement.attributes, attribute_helper_mt) -local event_helper_mt = {} -event_helper_mt.__index = function(self, key) - return ElectricalEnergyMeasurement.server.events[key] -end -ElectricalEnergyMeasurement.events = {} -setmetatable(ElectricalEnergyMeasurement.events, event_helper_mt) - setmetatable(ElectricalEnergyMeasurement, {__index = cluster_base}) return ElectricalEnergyMeasurement diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua index 3dc58635e1..2d41790440 100644 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local CumulativeEnergyImported = { ID = 0x0001, NAME = "CumulativeEnergyImported", - base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", + base_type = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", } function CumulativeEnergyImported:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua index 753b91ea2d..5daccf48ab 100644 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local PeriodicEnergyImported = { ID = 0x0003, NAME = "PeriodicEnergyImported", - base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", + base_type = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", } function PeriodicEnergyImported:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua index adfdf42bbf..57bc0d1f72 100644 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("ElectricalEnergyMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.ElectricalEnergyMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua similarity index 97% rename from drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua index 950b260227..a4c58a3646 100644 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local StructureABC = require "st.matter.data_types.base_defs.StructureABC" local EnergyMeasurementStruct = {} diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua new file mode 100644 index 0000000000..e3db76f49d --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua @@ -0,0 +1,31 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.IMPORTED_ENERGY = 0x0001 +Feature.EXPORTED_ENERGY = 0x0002 +Feature.CUMULATIVE_ENERGY = 0x0004 +Feature.PERIODIC_ENERGY = 0x0008 + +function Feature.bits_are_valid(feature) + local max = + Feature.IMPORTED_ENERGY | + Feature.EXPORTED_ENERGY | + Feature.CUMULATIVE_ENERGY | + Feature.PERIODIC_ENERGY + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua similarity index 62% rename from drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua index bb0c39fe0e..ec29b53e05 100644 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("ElectricalEnergyMeasurement.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.ElectricalEnergyMeasurement.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/init.lua new file mode 100644 index 0000000000..c9061e3cc4 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/init.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local ElectricalPowerMeasurementServerAttributes = require "embedded_clusters.ElectricalPowerMeasurement.server.attributes" + +local ElectricalPowerMeasurement = {} + +ElectricalPowerMeasurement.ID = 0x0090 +ElectricalPowerMeasurement.NAME = "ElectricalPowerMeasurement" +ElectricalPowerMeasurement.server = {} +ElectricalPowerMeasurement.client = {} +ElectricalPowerMeasurement.server.attributes = ElectricalPowerMeasurementServerAttributes:set_parent_cluster(ElectricalPowerMeasurement) + +function ElectricalPowerMeasurement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0008] = "ActivePower", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +ElectricalPowerMeasurement.attribute_direction_map = { + ["ActivePower"] = "server", +} + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ElectricalPowerMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ElectricalPowerMeasurement.NAME)) + end + return ElectricalPowerMeasurement[direction].attributes[key] +end +ElectricalPowerMeasurement.attributes = {} +setmetatable(ElectricalPowerMeasurement.attributes, attribute_helper_mt) + +setmetatable(ElectricalPowerMeasurement, {__index = cluster_base}) + +return ElectricalPowerMeasurement + diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua similarity index 93% rename from drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua index 6c34abd2f4..f1696509f5 100644 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -65,4 +68,3 @@ end setmetatable(ActivePower, {__call = ActivePower.new_value, __index = ActivePower.base_type}) return ActivePower - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua index 0c30fa8dd4..6de69b94ff 100644 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("ElectricalPowerMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.ElectricalPowerMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/init.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/init.lua index 5920a9dc66..cdfd1d597e 100644 --- a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local FormaldehydeConcentrationMeasurementServerAttributes = require "FormaldehydeConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local FormaldehydeConcentrationMeasurementServerAttributes = require "embedded_clusters.FormaldehydeConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local FormaldehydeConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua similarity index 81% rename from drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua index 50127a3537..cc8f7b47eb 100644 --- a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua @@ -1,9 +1,12 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" local LevelValue = { ID = 0x000A, NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", } function LevelValue:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua similarity index 89% rename from drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua index 763bbaa17b..ca0dafb0dc 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" local MeasuredValue = { diff --git a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua similarity index 82% rename from drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua index d18f14b09f..9597906d3a 100644 --- a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -1,11 +1,14 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" local MeasurementUnit = { ID = 0x0008, NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", } function MeasurementUnit:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/init.lua index 37900b0fb1..3dee13abe3 100644 --- a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("FormaldehydeConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.FormaldehydeConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/init.lua similarity index 89% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/init.lua index 84400d7833..7a36d3aa92 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/init.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local HepaFilterMonitoringServerAttributes = require "HepaFilterMonitoring.server.attributes" -local HepaFilterMonitoringServerCommands = require "HepaFilterMonitoring.server.commands" -local HepaFilterMonitoringTypes = require "HepaFilterMonitoring.types" +local HepaFilterMonitoringServerAttributes = require "embedded_clusters.HepaFilterMonitoring.server.attributes" +local HepaFilterMonitoringServerCommands = require "embedded_clusters.HepaFilterMonitoring.server.commands" +local HepaFilterMonitoringTypes = require "embedded_clusters.HepaFilterMonitoring.types" local HepaFilterMonitoring = {} diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/ChangeIndication.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/ChangeIndication.lua index 955b89eb88..9dca020204 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/ChangeIndication.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local ChangeIndication = { ID = 0x0002, NAME = "ChangeIndication", - base_type = require "HepaFilterMonitoring.types.ChangeIndicationEnum", + base_type = require "embedded_clusters.HepaFilterMonitoring.types.ChangeIndicationEnum", } function ChangeIndication:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/Condition.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/Condition.lua similarity index 93% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/Condition.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/Condition.lua index e668aa4c48..76a4fd9760 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/Condition.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/Condition.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/init.lua index 8d7ffe6c00..2590980846 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("HepaFilterMonitoring.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.HepaFilterMonitoring.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/ResetCondition.lua similarity index 96% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/ResetCondition.lua index 040ce653b4..1ac942f780 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/ResetCondition.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/init.lua index 55a4ea7a30..77ae141f77 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local command_mt = {} command_mt.__command_cache = {} command_mt.__index = function(self, key) if command_mt.__command_cache[key] == nil then - local req_loc = string.format("HepaFilterMonitoring.server.commands.%s", key) + local req_loc = string.format("embedded_clusters.HepaFilterMonitoring.server.commands.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/ChangeIndicationEnum.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/ChangeIndicationEnum.lua similarity index 92% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/ChangeIndicationEnum.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/ChangeIndicationEnum.lua index 438de24c94..6b5ab62494 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/ChangeIndicationEnum.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/ChangeIndicationEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/Feature.lua similarity index 96% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/Feature.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/Feature.lua index 88474d1b0f..906e769676 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/Feature.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/Feature.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/init.lua similarity index 61% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/init.lua index 77aca088ff..6c36af1aec 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("HepaFilterMonitoring.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.HepaFilterMonitoring.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/init.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/init.lua index b60e71050a..eae9be65f0 100644 --- a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local NitrogenDioxideConcentrationMeasurementServerAttributes = require "NitrogenDioxideConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local NitrogenDioxideConcentrationMeasurementServerAttributes = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local NitrogenDioxideConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua similarity index 81% rename from drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua index 50127a3537..cc8f7b47eb 100644 --- a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua @@ -1,9 +1,12 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" local LevelValue = { ID = 0x000A, NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", } function LevelValue:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua similarity index 89% rename from drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua index 763bbaa17b..ca0dafb0dc 100644 --- a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" local MeasuredValue = { diff --git a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua similarity index 82% rename from drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua index d18f14b09f..9597906d3a 100644 --- a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -1,11 +1,14 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" local MeasurementUnit = { ID = 0x0008, NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", } function MeasurementUnit:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua index 06c3d6dd55..c82517d362 100644 --- a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("NitrogenDioxideConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.NitrogenDioxideConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/init.lua similarity index 85% rename from drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/init.lua index 83fd04857e..c49ea94b39 100644 --- a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local OzoneConcentrationMeasurementServerAttributes = require "OzoneConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local OzoneConcentrationMeasurementServerAttributes = require "embedded_clusters.OzoneConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local OzoneConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/init.lua index fe0048cd99..918b680495 100644 --- a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("OzoneConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.OzoneConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/init.lua similarity index 85% rename from drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/init.lua index 98eebd407e..3b333b5417 100644 --- a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local Pm10ConcentrationMeasurementServerAttributes = require "Pm10ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local Pm10ConcentrationMeasurementServerAttributes = require "embedded_clusters.Pm10ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local Pm10ConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/init.lua index 55f08c7e43..3b1e6617a4 100644 --- a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("Pm10ConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.Pm10ConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/init.lua similarity index 85% rename from drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/init.lua index 0b3caa3bd2..b2e6656a9a 100644 --- a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local Pm1ConcentrationMeasurementServerAttributes = require "Pm1ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local Pm1ConcentrationMeasurementServerAttributes = require "embedded_clusters.Pm1ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local Pm1ConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/init.lua index f668e41a07..2635da32a6 100644 --- a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("Pm1ConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.Pm1ConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/init.lua similarity index 85% rename from drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/init.lua index 5234346d60..e6e6144f94 100644 --- a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local Pm25ConcentrationMeasurementServerAttributes = require "Pm25ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local Pm25ConcentrationMeasurementServerAttributes = require "embedded_clusters.Pm25ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local Pm25ConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/init.lua index 2c7d5fce7b..5c432da0ec 100644 --- a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("Pm25ConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.Pm25ConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/init.lua similarity index 85% rename from drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/init.lua index 2a4cc04a0d..3d4b21d602 100644 --- a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local RadonConcentrationMeasurementServerAttributes = require "RadonConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local RadonConcentrationMeasurementServerAttributes = require "embedded_clusters.RadonConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local RadonConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/init.lua index b83ef67bfc..8aa225e510 100644 --- a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("RadonConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.RadonConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua similarity index 89% rename from drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua index a99c1dea50..cad49a14e1 100644 --- a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes = require "TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local TotalVolatileOrganicCompoundsConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua index c0c1b65c37..b67cbcd7b6 100644 --- a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/init.lua similarity index 87% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/init.lua index 1155cfd636..3a1c2f1bfb 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/init.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local WaterHeaterModeServerAttributes = require "WaterHeaterMode.server.attributes" -local WaterHeaterModeServerCommands = require "WaterHeaterMode.server.commands" -local WaterHeaterModeTypes = require "WaterHeaterMode.types" +local WaterHeaterModeServerAttributes = require "embedded_clusters.WaterHeaterMode.server.attributes" +local WaterHeaterModeServerCommands = require "embedded_clusters.WaterHeaterMode.server.commands" +local WaterHeaterModeTypes = require "embedded_clusters.WaterHeaterMode.types" local WaterHeaterMode = {} diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/CurrentMode.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/CurrentMode.lua similarity index 93% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/CurrentMode.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/CurrentMode.lua index aa20156f74..165b906df5 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/CurrentMode.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/CurrentMode.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -24,7 +27,6 @@ function CurrentMode:read(device, endpoint_id) ) end - function CurrentMode:subscribe(device, endpoint_id) return cluster_base.subscribe( device, diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/SupportedModes.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/SupportedModes.lua similarity index 90% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/SupportedModes.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/SupportedModes.lua index 1f393a17d4..903a374d43 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/SupportedModes.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/SupportedModes.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" @@ -6,7 +9,7 @@ local SupportedModes = { ID = 0x0000, NAME = "SupportedModes", base_type = require "st.matter.data_types.Array", - element_type = require "WaterHeaterMode.types.ModeOptionStruct", + element_type = require "embedded_clusters.WaterHeaterMode.types.ModeOptionStruct", } function SupportedModes:augment_type(data_type_obj) diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/init.lua similarity index 75% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/init.lua index 020a4125ce..fb7bed9828 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) if attr_mt.__attr_cache[key] == nil then - local req_loc = string.format("WaterHeaterMode.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.WaterHeaterMode.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/ChangeToMode.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/ChangeToMode.lua similarity index 96% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/ChangeToMode.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/ChangeToMode.lua index 73ddbd1029..726352b448 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/ChangeToMode.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/ChangeToMode.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/init.lua index 9736c4577f..64660ed336 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local command_mt = {} command_mt.__command_cache = {} command_mt.__index = function(self, key) if command_mt.__command_cache[key] == nil then - local req_loc = string.format("WaterHeaterMode.server.commands.%s", key) + local req_loc = string.format("embedded_clusters.WaterHeaterMode.server.commands.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/Feature.lua similarity index 92% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/Feature.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/Feature.lua index da49bf4115..c7e987399d 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/Feature.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/Feature.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeOptionStruct.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeOptionStruct.lua similarity index 94% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeOptionStruct.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeOptionStruct.lua index f770a7916c..1db8c5b634 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeOptionStruct.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeOptionStruct.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local StructureABC = require "st.matter.data_types.base_defs.StructureABC" @@ -24,7 +27,7 @@ ModeOptionStruct.field_defs = { is_nullable = false, is_optional = false, data_type = require "st.matter.data_types.Array", - element_type = require "WaterHeaterMode.types.ModeTagStruct", + element_type = require "embedded_clusters.WaterHeaterMode.types.ModeTagStruct", }, } diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTag.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTag.lua similarity index 90% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTag.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTag.lua index 009e70a40e..7d37d4374d 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTag.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTag.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTagStruct.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTagStruct.lua similarity index 96% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTagStruct.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTagStruct.lua index 1c41eb320e..17dfd269d6 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTagStruct.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTagStruct.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local StructureABC = require "st.matter.data_types.base_defs.StructureABC" diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/init.lua similarity index 61% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/init.lua index f0198ff8a0..bae583dfb4 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("WaterHeaterMode.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.WaterHeaterMode.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua index 58f0d75344..6697c80fb6 100644 --- a/drivers/SmartThings/matter-thermostat/src/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/init.lua @@ -1,2263 +1,516 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -local capabilities = require "st.capabilities" -local log = require "log" -local clusters = require "st.matter.clusters" -local embedded_cluster_utils = require "embedded-cluster-utils" -local im = require "st.matter.interaction_model" - -local MatterDriver = require "st.matter.driver" -local utils = require "st.utils" - -local SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities" --- declare match_profile function for use throughout file -local match_profile - --- Include driver-side definitions when lua libs api version is < 10 -local version = require "version" -if version.api < 10 then - clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" - clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" - -- new modes add in Matter 1.2 - clusters.Thermostat.types.ThermostatSystemMode.DRY = 0x8 - clusters.Thermostat.types.ThermostatSystemMode.SLEEP = 0x9 -end - -local SAVED_SYSTEM_MODE_IB = "__saved_system_mode_ib" -local DISALLOWED_THERMOSTAT_MODES = "__DISALLOWED_CONTROL_OPERATIONS" -local OPTIONAL_THERMOSTAT_MODES_SEEN = "__OPTIONAL_THERMOSTAT_MODES_SEEN" - -if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" -end - -if version.api < 13 then - clusters.WaterHeaterMode = require "WaterHeaterMode" -end - -local THERMOSTAT_MODE_MAP = { - [clusters.Thermostat.types.ThermostatSystemMode.OFF] = capabilities.thermostatMode.thermostatMode.off, - [clusters.Thermostat.types.ThermostatSystemMode.AUTO] = capabilities.thermostatMode.thermostatMode.auto, - [clusters.Thermostat.types.ThermostatSystemMode.COOL] = capabilities.thermostatMode.thermostatMode.cool, - [clusters.Thermostat.types.ThermostatSystemMode.HEAT] = capabilities.thermostatMode.thermostatMode.heat, - [clusters.Thermostat.types.ThermostatSystemMode.EMERGENCY_HEATING] = capabilities.thermostatMode.thermostatMode.emergency_heat, - [clusters.Thermostat.types.ThermostatSystemMode.PRECOOLING] = capabilities.thermostatMode.thermostatMode.precooling, - [clusters.Thermostat.types.ThermostatSystemMode.FAN_ONLY] = capabilities.thermostatMode.thermostatMode.fanonly, - [clusters.Thermostat.types.ThermostatSystemMode.DRY] = capabilities.thermostatMode.thermostatMode.dryair, - [clusters.Thermostat.types.ThermostatSystemMode.SLEEP] = capabilities.thermostatMode.thermostatMode.asleep, -} - -local THERMOSTAT_OPERATING_MODE_MAP = { - [0] = capabilities.thermostatOperatingState.thermostatOperatingState.heating, - [1] = capabilities.thermostatOperatingState.thermostatOperatingState.cooling, - [2] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, - [3] = capabilities.thermostatOperatingState.thermostatOperatingState.heating, - [4] = capabilities.thermostatOperatingState.thermostatOperatingState.cooling, - [5] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, - [6] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, -} - -local WIND_MODE_MAP = { - [0] = capabilities.windMode.windMode.sleepWind, - [1] = capabilities.windMode.windMode.naturalWind -} - -local ROCK_MODE_MAP = { - [0] = capabilities.fanOscillationMode.fanOscillationMode.horizontal, - [1] = capabilities.fanOscillationMode.fanOscillationMode.vertical, - [2] = capabilities.fanOscillationMode.fanOscillationMode.swing -} - -local RAC_DEVICE_TYPE_ID = 0x0072 -local AP_DEVICE_TYPE_ID = 0x002D -local FAN_DEVICE_TYPE_ID = 0x002B -local WATER_HEATER_DEVICE_TYPE_ID = 0x050F -local HEAT_PUMP_DEVICE_TYPE_ID = 0x0309 -local THERMOSTAT_DEVICE_TYPE_ID = 0x0301 -local ELECTRICAL_SENSOR_DEVICE_TYPE_ID = 0x0510 - -local MIN_ALLOWED_PERCENT_VALUE = 0 -local MAX_ALLOWED_PERCENT_VALUE = 100 - -local CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" -local LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" -local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds - -local TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP = "__total_cumulative_energy_imported_map" -local SUPPORTED_WATER_HEATER_MODES_WITH_IDX = "__supported_water_heater_modes_with_idx" -local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -local MGM3_PPM_CONVERSION_FACTOR = 24.45 - --- For RPC version >= 6, we can always assume that the values received from temperatureSetpoint --- are in Celsius, but we still limit the setpoint range to somewhat reasonable values. --- For RPC <= 5, this is a work around to handle when units for temperatureSetpoint is changed for the App. --- When units are switched, we will never know the units of the received command value as the arguments don't contain the unit. --- So to handle this we assume the following ranges considering usual thermostat/water-heater temperatures: --- Thermostat: --- 1. if the received setpoint command value is in range 5 ~ 40, it is inferred as *C --- 2. if the received setpoint command value is in range 41 ~ 104, it is inferred as *F -local THERMOSTAT_MAX_TEMP_IN_C = version.rpc >= 6 and 100.0 or 40.0 -local THERMOSTAT_MIN_TEMP_IN_C = version.rpc >= 6 and 0.0 or 5.0 --- Water Heater: --- 1. if the received setpoint command value is in range 30 ~ 80, it is inferred as *C --- 2. if the received setpoint command value is in range 86 ~ 176, it is inferred as *F -local WATER_HEATER_MAX_TEMP_IN_C = version.rpc >= 6 and 100.0 or 80.0 -local WATER_HEATER_MIN_TEMP_IN_C = version.rpc >= 6 and 0.0 or 30.0 - -local setpoint_limit_device_field = { - MIN_SETPOINT_DEADBAND_CHECKED = "MIN_SETPOINT_DEADBAND_CHECKED", - MIN_HEAT = "MIN_HEAT", - MAX_HEAT = "MAX_HEAT", - MIN_COOL = "MIN_COOL", - MAX_COOL = "MAX_COOL", - MIN_DEADBAND = "MIN_DEADBAND", - MIN_TEMP = "MIN_TEMP", - MAX_TEMP = "MAX_TEMP" -} - -local battery_support = { - NO_BATTERY = "NO_BATTERY", - BATTERY_LEVEL = "BATTERY_LEVEL", - BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" -} - -local profiling_data = { - BATTERY_SUPPORT = "__BATTERY_SUPPORT", - THERMOSTAT_RUNNING_STATE_SUPPORT = "__THERMOSTAT_RUNNING_STATE_SUPPORT" -} - -local subscribed_attributes = { - [capabilities.switch.ID] = { - clusters.OnOff.attributes.OnOff - }, - [capabilities.temperatureMeasurement.ID] = { - clusters.Thermostat.attributes.LocalTemperature, - clusters.TemperatureMeasurement.attributes.MeasuredValue, - clusters.TemperatureMeasurement.attributes.MinMeasuredValue, - clusters.TemperatureMeasurement.attributes.MaxMeasuredValue - }, - [capabilities.relativeHumidityMeasurement.ID] = { - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue - }, - [capabilities.thermostatMode.ID] = { - clusters.Thermostat.attributes.SystemMode, - clusters.Thermostat.attributes.ControlSequenceOfOperation - }, - [capabilities.thermostatOperatingState.ID] = { - clusters.Thermostat.attributes.ThermostatRunningState - }, - [capabilities.thermostatFanMode.ID] = { - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.FanMode - }, - [capabilities.thermostatCoolingSetpoint.ID] = { - clusters.Thermostat.attributes.OccupiedCoolingSetpoint, - clusters.Thermostat.attributes.AbsMinCoolSetpointLimit, - clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit - }, - [capabilities.thermostatHeatingSetpoint.ID] = { - clusters.Thermostat.attributes.OccupiedHeatingSetpoint, - clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, - clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit - }, - [capabilities.airConditionerFanMode.ID] = { - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.FanMode - }, - [capabilities.airPurifierFanMode.ID] = { - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.FanMode - }, - [capabilities.fanMode.ID] = { - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.FanMode - }, - [capabilities.fanSpeedPercent.ID] = { - clusters.FanControl.attributes.PercentCurrent - }, - [capabilities.windMode.ID] = { - clusters.FanControl.attributes.WindSupport, - clusters.FanControl.attributes.WindSetting - }, - [capabilities.fanOscillationMode.ID] = { - clusters.FanControl.attributes.RockSupport, - clusters.FanControl.attributes.RockSetting - }, - [capabilities.battery.ID] = { - clusters.PowerSource.attributes.BatPercentRemaining - }, - [capabilities.batteryLevel.ID] = { - clusters.PowerSource.attributes.BatChargeLevel - }, - [capabilities.filterState.ID] = { - clusters.HepaFilterMonitoring.attributes.Condition, - clusters.ActivatedCarbonFilterMonitoring.attributes.Condition - }, - [capabilities.filterStatus.ID] = { - clusters.HepaFilterMonitoring.attributes.ChangeIndication, - clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication - }, - [capabilities.airQualityHealthConcern.ID] = { - clusters.AirQuality.attributes.AirQuality - }, - [capabilities.carbonMonoxideMeasurement.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.carbonMonoxideHealthConcern.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.carbonDioxideMeasurement.ID] = { - clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.carbonDioxideHealthConcern.ID] = { - clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.nitrogenDioxideMeasurement.ID] = { - clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit - }, - [capabilities.nitrogenDioxideHealthConcern.ID] = { - clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.ozoneMeasurement.ID] = { - clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue, - clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit - }, - [capabilities.ozoneHealthConcern.ID] = { - clusters.OzoneConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.formaldehydeMeasurement.ID] = { - clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue, - clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.formaldehydeHealthConcern.ID] = { - clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.veryFineDustSensor.ID] = { - clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.veryFineDustHealthConcern.ID] = { - clusters.Pm1ConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.fineDustHealthConcern.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.fineDustSensor.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.dustSensor.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, - clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.dustHealthConcern.ID] = { - clusters.Pm10ConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.radonMeasurement.ID] = { - clusters.RadonConcentrationMeasurement.attributes.MeasuredValue, - clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.radonHealthConcern.ID] = { - clusters.RadonConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.tvocMeasurement.ID] = { - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue, - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.tvocHealthConcern.ID] = { - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue - }, - [capabilities.powerMeter.ID] = { - clusters.ElectricalPowerMeasurement.attributes.ActivePower - }, - [capabilities.mode.ID] = { - clusters.WaterHeaterMode.attributes.CurrentMode, - clusters.WaterHeaterMode.attributes.SupportedModes - }, - [capabilities.powerConsumptionReport.ID] = { - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported - }, - [capabilities.energyMeter.ID] = { - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported - }, -} - -local function supports_capability_by_id_modular(device, capability, component) - if not device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - device.log.warn_with({hub_logs = true}, "Device has overriden supports_capability_by_id, but does not have supported capabilities set.") - return false - end - for _, component_capabilities in ipairs(device:get_field(SUPPORTED_COMPONENT_CAPABILITIES)) do - local comp_id = component_capabilities[1] - local capability_ids = component_capabilities[2] - if (component == nil) or (component == comp_id) then - for _, cap in ipairs(capability_ids) do - if cap == capability then - return true - end - end - end - end - return false -end - -local function epoch_to_iso8601(time) - return os.date("!%Y-%m-%dT%H:%M:%SZ", time) -end - -local get_total_cumulative_energy_imported = function(device) - local total_cumulative_energy_imported = device:get_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} - local total_energy = 0 - for _, energyWh in pairs(total_cumulative_energy_imported) do - total_energy = total_energy + energyWh - end - return total_energy -end - -local function report_power_consumption_to_st_energy(device, latest_total_imported_energy_wh) - local current_time = os.time() - local last_time = device:get_field(LAST_IMPORTED_REPORT_TIMESTAMP) or 0 - - -- Ensure that the previous report was sent at least 15 minutes ago - if MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then - return - end - - device:set_field(LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) - - -- Calculate the energy delta between reports - local energy_delta_wh = 0.0 - local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, - capabilities.powerConsumptionReport.powerConsumption.NAME) - if previous_imported_report and previous_imported_report.energy then - energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) - end - - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - device:emit_component_event(device.profile.components["main"], capabilities.powerConsumptionReport.powerConsumption({ - start = epoch_to_iso8601(last_time), - ["end"] = epoch_to_iso8601(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) -end - -local function device_removed(driver, device) - device.log.info("device removed") -end - -local function tbl_contains(array, value) - for idx, element in ipairs(array) do - if element == value then - return true, idx - end - end - return false, nil -end - -local function get_field_for_endpoint(device, field, endpoint) - return device:get_field(string.format("%s_%d", field, endpoint)) -end - -local function set_field_for_endpoint(device, field, endpoint, value, additional_params) - device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) -end - -local function find_default_endpoint(device, cluster) - local res = device.MATTER_DEFAULT_ENDPOINT - local eps = embedded_cluster_utils.get_endpoints(device, cluster) - table.sort(eps) - for _, v in ipairs(eps) do - if v ~= 0 then --0 is the matter RootNode endpoint - return v - end - end - device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) - return res -end - -local function component_to_endpoint(device, component_name, cluster_id) - -- Use the find_default_endpoint function to return the first endpoint that - -- supports a given cluster. - local component_to_endpoint_map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) - if component_to_endpoint_map ~= nil and component_to_endpoint_map[component_name] ~= nil then - return component_to_endpoint_map[component_name] - end - if not cluster_id then return device.MATTER_DEFAULT_ENDPOINT end - return find_default_endpoint(device, cluster_id) -end - -local endpoint_to_component = function (device, endpoint_id) - local component_to_endpoint_map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) - if component_to_endpoint_map ~= nil then - for comp, ep in pairs(component_to_endpoint_map) do - if ep == endpoint_id then - return comp - end - end - end - return "main" -end - -local function device_init(driver, device) - if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) and (version.api < 15 or version.rpc < 9) then - -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id - -- library function to utilize optional capabilities - device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) - end - device:subscribe() - device:set_component_to_endpoint_fn(component_to_endpoint) - device:set_endpoint_to_component_fn(endpoint_to_component) - if not device:get_field(setpoint_limit_device_field.MIN_SETPOINT_DEADBAND_CHECKED) then - local auto_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}) - --Query min setpoint deadband if needed - if #auto_eps ~= 0 and device:get_field(setpoint_limit_device_field.MIN_DEADBAND) == nil then - local deadband_read = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - deadband_read:merge(clusters.Thermostat.attributes.MinSetpointDeadBand:read()) - device:send(deadband_read) - end - end - - -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. - -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. - local electrical_energy_measurement_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) - if #electrical_energy_measurement_eps > 0 then - local cumulative_energy_eps = embedded_cluster_utils.get_endpoints( - device, - clusters.ElectricalEnergyMeasurement.ID, - {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY} - ) - if #cumulative_energy_eps == 0 then device:set_field(CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = false}) end - end -end - -local function info_changed(driver, device, event, args) - if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - -- This indicates the device should be using a modular profile, so - -- re-up subscription with new capabilities using the modular supports_capability override - device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) - end - - if device.profile.id ~= args.old_st_store.profile.id then - device:subscribe() - end -end - -local function get_endpoints_for_dt(device, device_type) - local endpoints = {} - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == device_type then - table.insert(endpoints, ep.endpoint_id) - break - end - end - end - table.sort(endpoints) - return endpoints -end - -local function get_device_type(device) - -- For cases where a device has multiple device types, this list indicates which - -- device type will be the "main" device type for purposes of selecting a profile - -- with an appropriate category. This is done to promote consistency between - -- devices with similar device type compositions that may report their device types - -- listed in different orders - local device_type_priority = { - [HEAT_PUMP_DEVICE_TYPE_ID] = 1, - [RAC_DEVICE_TYPE_ID] = 2, - [AP_DEVICE_TYPE_ID] = 3, - [THERMOSTAT_DEVICE_TYPE_ID] = 4, - [FAN_DEVICE_TYPE_ID] = 5, - [WATER_HEATER_DEVICE_TYPE_ID] = 6, - } - - local main_device_type = false - - for _, ep in ipairs(device.endpoints) do - if ep.device_types ~= nil then - for _, dt in ipairs(ep.device_types) do - if not device_type_priority[main_device_type] or (device_type_priority[dt.device_type_id] and - device_type_priority[dt.device_type_id] < device_type_priority[main_device_type]) then - main_device_type = dt.device_type_id - end - end - end - end - - return main_device_type -end - -local AIR_QUALITY_MAP = { - {capabilities.carbonDioxideMeasurement.ID, "-co2", clusters.CarbonDioxideConcentrationMeasurement}, - {capabilities.carbonDioxideHealthConcern.ID, "-co2", clusters.CarbonDioxideConcentrationMeasurement}, - {capabilities.carbonMonoxideMeasurement.ID, "-co", clusters.CarbonMonoxideConcentrationMeasurement}, - {capabilities.carbonMonoxideHealthConcern.ID, "-co", clusters.CarbonMonoxideConcentrationMeasurement}, - {capabilities.dustSensor.ID, "-pm10", clusters.Pm10ConcentrationMeasurement}, - {capabilities.dustHealthConcern.ID, "-pm10", clusters.Pm10ConcentrationMeasurement}, - {capabilities.fineDustSensor.ID, "-pm25", clusters.Pm25ConcentrationMeasurement}, - {capabilities.fineDustHealthConcern.ID, "-pm25", clusters.Pm25ConcentrationMeasurement}, - {capabilities.formaldehydeMeasurement.ID, "-ch2o", clusters.FormaldehydeConcentrationMeasurement}, - {capabilities.formaldehydeHealthConcern.ID, "-ch2o", clusters.FormaldehydeConcentrationMeasurement}, - {capabilities.nitrogenDioxideHealthConcern.ID, "-no2", clusters.NitrogenDioxideConcentrationMeasurement}, - {capabilities.nitrogenDioxideMeasurement.ID, "-no2", clusters.NitrogenDioxideConcentrationMeasurement}, - {capabilities.ozoneHealthConcern.ID, "-ozone", clusters.OzoneConcentrationMeasurement}, - {capabilities.ozoneMeasurement.ID, "-ozone", clusters.OzoneConcentrationMeasurement}, - {capabilities.radonHealthConcern.ID, "-radon", clusters.RadonConcentrationMeasurement}, - {capabilities.radonMeasurement.ID, "-radon", clusters.RadonConcentrationMeasurement}, - {capabilities.tvocHealthConcern.ID, "-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement}, - {capabilities.tvocMeasurement.ID, "-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement}, - {capabilities.veryFineDustHealthConcern.ID, "-pm1", clusters.Pm1ConcentrationMeasurement}, - {capabilities.veryFineDustSensor.ID, "-pm1", clusters.Pm1ConcentrationMeasurement}, -} - -local function create_level_measurement_profile(device) - local meas_name, level_name = "", "" - for _, details in ipairs(AIR_QUALITY_MAP) do - local cap_id = details[1] - local cluster = details[3] - -- capability describes either a HealthConcern or Measurement/Sensor - if (cap_id:match("HealthConcern$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) - if #attr_eps > 0 then - level_name = level_name .. details[2] - end - elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) - if #attr_eps > 0 then - meas_name = meas_name .. details[2] - end - end - end - return meas_name, level_name -end - -local function supported_level_measurements(device) - local measurement_caps, level_caps = {}, {} - for _, details in ipairs(AIR_QUALITY_MAP) do - local cap_id = details[1] - local cluster = details[3] - -- capability describes either a HealthConcern or Measurement/Sensor - if (cap_id:match("HealthConcern$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) - if #attr_eps > 0 then - table.insert(level_caps, cap_id) - end - elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) - if #attr_eps > 0 then - table.insert(measurement_caps, cap_id) - end - end - end - return measurement_caps, level_caps -end - -local function create_air_quality_sensor_profile(device) - local aqs_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) - local profile_name = "" - if #aqs_eps > 0 then - profile_name = profile_name .. "-aqs" - end - local meas_name, level_name = create_level_measurement_profile(device) - if meas_name ~= "" then - profile_name = profile_name .. meas_name .. "-meas" - end - if level_name ~= "" then - profile_name = profile_name .. level_name .. "-level" - end - return profile_name -end - -local function create_fan_profile(device) - local fan_eps = device:get_endpoints(clusters.FanControl.ID) - local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) - local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) - local profile_name = "" - if #fan_eps > 0 then - profile_name = profile_name .. "-fan" - end - if #rock_eps > 0 then - profile_name = profile_name .. "-rock" - end - if #wind_eps > 0 then - profile_name = profile_name .. "-wind" - end - return profile_name -end - -local function create_air_purifier_profile(device) - local hepa_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID) - local ac_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID) - local fan_eps_seen = false - local profile_name = "air-purifier" - if #hepa_filter_eps > 0 then - profile_name = profile_name .. "-hepa" - end - if #ac_filter_eps > 0 then - profile_name = profile_name .. "-ac" - end - - -- air purifier profiles include -fan later in the name for historical reasons. - -- save this information for use at that point. - local fan_profile = create_fan_profile(device) - if fan_profile ~= "" then - fan_eps_seen = true - end - fan_profile = string.gsub(fan_profile, "-fan", "") - profile_name = profile_name .. fan_profile - - return profile_name, fan_eps_seen -end - -local function create_thermostat_modes_profile(device) - local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) - local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) - - local thermostat_modes = "" - if #heat_eps == 0 and #cool_eps == 0 then - return "No Heating nor Cooling Support" - elseif #heat_eps > 0 and #cool_eps == 0 then - thermostat_modes = thermostat_modes .. "-heating-only" - elseif #cool_eps > 0 and #heat_eps == 0 then - thermostat_modes = thermostat_modes .. "-cooling-only" - end - return thermostat_modes -end - -local function profiling_data_still_required(device) - for _, field in pairs(profiling_data) do - if device:get_field(field) == nil then - return true -- data still required if a field is nil - end - end - return false -end - -local function match_profile_switch(driver, device) - if profiling_data_still_required(device) then return end - - local running_state_supported = device:get_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) - local battery_supported = device:get_field(profiling_data.BATTERY_SUPPORT) - - local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) - local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) - local device_type = get_device_type(device) - local profile_name - if device_type == RAC_DEVICE_TYPE_ID then - profile_name = "room-air-conditioner" - - if #humidity_eps > 0 then - profile_name = profile_name .. "-humidity" - end - - -- Room AC does not support the rocking feature of FanControl. - local fan_name = create_fan_profile(device) - fan_name = string.gsub(fan_name, "-rock", "") - profile_name = profile_name .. fan_name - - local thermostat_modes = create_thermostat_modes_profile(device) - if thermostat_modes == "" then - profile_name = profile_name .. "-heating-cooling" - else - device.log.warn_with({hub_logs=true}, "Device does not support both heating and cooling. No matching profile") - return - end - - if profile_name == "room-air-conditioner-humidity-fan-wind-heating-cooling" then - profile_name = "room-air-conditioner" - end - - if not running_state_supported and profile_name == "room-air-conditioner-fan-heating-cooling" then - profile_name = profile_name .. "-nostate" - end - - elseif device_type == FAN_DEVICE_TYPE_ID then - profile_name = create_fan_profile(device) - -- remove leading "-" - profile_name = string.sub(profile_name, 2) - if profile_name == "fan" then - profile_name = "fan-generic" - end - - elseif device_type == AP_DEVICE_TYPE_ID then - local fan_eps_found - profile_name, fan_eps_found = create_air_purifier_profile(device) - if #thermostat_eps > 0 then - profile_name = profile_name .. "-thermostat" - - if #humidity_eps > 0 then - profile_name = profile_name .. "-humidity" - end - - if fan_eps_found then - profile_name = profile_name .. "-fan" - end - - local thermostat_modes = create_thermostat_modes_profile(device) - if thermostat_modes ~= "No Heating nor Cooling Support" then - profile_name = profile_name .. thermostat_modes - end - - if not running_state_supported then - profile_name = profile_name .. "-nostate" - end - - if battery_supported == battery_support.BATTERY_LEVEL then - profile_name = profile_name .. "-batteryLevel" - elseif battery_supported == battery_support.NO_BATTERY then - profile_name = profile_name .. "-nobattery" - end - elseif #device:get_endpoints(clusters.TemperatureMeasurement.ID) > 0 then - profile_name = profile_name .. "-temperature" - - if #humidity_eps > 0 then - profile_name = profile_name .. "-humidity" - end - - if fan_eps_found then - profile_name = profile_name .. "-fan" - end - end - profile_name = profile_name .. create_air_quality_sensor_profile(device) - elseif device_type == WATER_HEATER_DEVICE_TYPE_ID then - -- If a Water Heater is composed of Electrical Sensor device type, it must support both ElectricalEnergyMeasurement and - -- ElectricalPowerMeasurement clusters. - local electrical_sensor_eps = get_endpoints_for_dt(device, ELECTRICAL_SENSOR_DEVICE_TYPE_ID) or {} - if #electrical_sensor_eps > 0 then - profile_name = "water-heater-power-energy-powerConsumption" - end - elseif device_type == HEAT_PUMP_DEVICE_TYPE_ID then - profile_name = "heat-pump" - local MAX_HEAT_PUMP_THERMOSTAT_COMPONENTS = 2 - for i = 1, math.min(MAX_HEAT_PUMP_THERMOSTAT_COMPONENTS, #thermostat_eps) do - profile_name = profile_name .. "-thermostat" - if tbl_contains(humidity_eps, thermostat_eps[i]) then - profile_name = profile_name .. "-humidity" - end - end - elseif #thermostat_eps > 0 then - profile_name = "thermostat" - - if #humidity_eps > 0 then - profile_name = profile_name .. "-humidity" - end - - -- thermostat profiles support neither wind nor rocking FanControl attributes - local fan_name = create_fan_profile(device) - if fan_name ~= "" then - profile_name = profile_name .. "-fan" - end - - local thermostat_modes = create_thermostat_modes_profile(device) - if thermostat_modes == "No Heating nor Cooling Support" then - device.log.warn_with({hub_logs=true}, "Device does not support either heating or cooling. No matching profile") - return - else - profile_name = profile_name .. thermostat_modes - end - - if not running_state_supported then - profile_name = profile_name .. "-nostate" - end - - if battery_supported == battery_support.BATTERY_LEVEL then - profile_name = profile_name .. "-batteryLevel" - elseif battery_supported == battery_support.NO_BATTERY then - profile_name = profile_name .. "-nobattery" - end - else - device.log.warn_with({hub_logs=true}, "Device type is not supported in thermostat driver") - return - end - - if profile_name then - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) - end - -- clear all profiling data fields after profiling is complete. - for _, field in pairs(profiling_data) do - device:set_field(field, nil) - end -end - -local function get_thermostat_optional_capabilities(device) - local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) - local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) - local running_state_supported = device:get_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) - - local supported_thermostat_capabilities = {} - - if #heat_eps > 0 then - table.insert(supported_thermostat_capabilities, capabilities.thermostatHeatingSetpoint.ID) - end - if #cool_eps > 0 then - table.insert(supported_thermostat_capabilities, capabilities.thermostatCoolingSetpoint.ID) - end - - if running_state_supported then - table.insert(supported_thermostat_capabilities, capabilities.thermostatOperatingState.ID) - end - - return supported_thermostat_capabilities -end - -local function get_air_quality_optional_capabilities(device) - local supported_air_quality_capabilities = {} - - local measurement_caps, level_caps = supported_level_measurements(device) - - for _, cap_id in ipairs(measurement_caps) do - table.insert(supported_air_quality_capabilities, cap_id) - end - - for _, cap_id in ipairs(level_caps) do - table.insert(supported_air_quality_capabilities, cap_id) - end - - return supported_air_quality_capabilities -end - -local function match_modular_profile_air_purifer(driver, device) - local optional_supported_component_capabilities = {} - local main_component_capabilities = {} - local hepa_filter_component_capabilities = {} - local ac_filter_component_capabilties = {} - local profile_name = "air-purifier-modular" - - local MAIN_COMPONENT_IDX = 1 - local CAPABILITIES_LIST_IDX = 2 - - local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) - local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID) - if #humidity_eps > 0 then - table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) - end - if #temp_eps > 0 then - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - end - - local hepa_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID) - local ac_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID) - - if #hepa_filter_eps > 0 then - local filter_state_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID, {feature_bitmap = clusters.HepaFilterMonitoring.types.Feature.CONDITION}) - if #filter_state_eps > 0 then - table.insert(hepa_filter_component_capabilities, capabilities.filterState.ID) - end - - table.insert(hepa_filter_component_capabilities, capabilities.filterStatus.ID) - end - if #ac_filter_eps > 0 then - local filter_state_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID, {feature_bitmap = clusters.ActivatedCarbonFilterMonitoring.types.Feature.CONDITION}) - if #filter_state_eps > 0 then - table.insert(ac_filter_component_capabilties, capabilities.filterState.ID) - end - - table.insert(ac_filter_component_capabilties, capabilities.filterStatus.ID) - end - - -- determine fan capabilities, note that airPurifierFanMode is already mandatory - local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) - local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) - - if #rock_eps > 0 then - table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID) - end - if #wind_eps > 0 then - table.insert(main_component_capabilities, capabilities.windMode.ID) - end - - local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) - - if #thermostat_eps > 0 then - -- thermostatMode and temperatureMeasurement should be expected if thermostat is present - table.insert(main_component_capabilities, capabilities.thermostatMode.ID) - - -- only add temperatureMeasurement if it is not already added via TemperatureMeasurement cluster support - if #temp_eps == 0 then - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - end - local thermostat_capabilities = get_thermostat_optional_capabilities(device) - for _, capability_id in pairs(thermostat_capabilities) do - table.insert(main_component_capabilities, capability_id) - end - end - - local aqs_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) - if #aqs_eps > 0 then - table.insert(main_component_capabilities, capabilities.airQualityHealthConcern.ID) - end - - local supported_air_quality_capabilities = get_air_quality_optional_capabilities(device) - for _, capability_id in pairs(supported_air_quality_capabilities) do - table.insert(main_component_capabilities, capability_id) - end - - table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) - if #ac_filter_component_capabilties > 0 then - table.insert(optional_supported_component_capabilities, {"activatedCarbonFilter", ac_filter_component_capabilties}) - end - if #hepa_filter_component_capabilities > 0 then - table.insert(optional_supported_component_capabilities, {"hepaFilter", hepa_filter_component_capabilities}) - end - - device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) - - -- earlier modular profile gating (min api v14, rpc 8) ensures we are running >= 0.57 FW. - -- This gating specifies a workaround required only for 0.57 FW, which is not needed for 0.58 and higher. - if version.api < 15 or version.rpc < 9 then - -- add mandatory capabilities for subscription - local total_supported_capabilities = optional_supported_component_capabilities - table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airPurifierFanMode.ID) - table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.fanSpeedPercent.ID) - table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.refresh.ID) - table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.firmwareUpdate.ID) - - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) - end -end - -local function match_modular_profile_thermostat(driver, device) - local optional_supported_component_capabilities = {} - local main_component_capabilities = {} - local profile_name = "thermostat-modular" - - local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) - if #humidity_eps > 0 then - table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) - end - - -- determine fan capabilities - local fan_eps = device:get_endpoints(clusters.FanControl.ID) - local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) - local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) - - if #fan_eps > 0 then - table.insert(main_component_capabilities, capabilities.fanMode.ID) - end - if #rock_eps > 0 then - table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID) - end - if #wind_eps > 0 then - table.insert(main_component_capabilities, capabilities.windMode.ID) - end - - local thermostat_capabilities = get_thermostat_optional_capabilities(device) - for _, capability_id in pairs(thermostat_capabilities) do - table.insert(main_component_capabilities, capability_id) - end - - local battery_supported = device:get_field(profiling_data.BATTERY_SUPPORT) - if battery_supported == battery_support.BATTERY_LEVEL then - table.insert(main_component_capabilities, capabilities.batteryLevel.ID) - elseif battery_supported == battery_support.BATTERY_PERCENTAGE then - table.insert(main_component_capabilities, capabilities.battery.ID) - end - - table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) - device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) - - -- earlier modular profile gating (min api v14, rpc 8) ensures we are running >= 0.57 FW. - -- This gating specifies a workaround required only for 0.57 FW, which is not needed for 0.58 and higher. - if version.api < 15 or version.rpc < 9 then - -- add mandatory capabilities for subscription - local total_supported_capabilities = optional_supported_component_capabilities - table.insert(main_component_capabilities, capabilities.thermostatMode.ID) - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - table.insert(main_component_capabilities, capabilities.refresh.ID) - table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) - - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) - end -end - -local function match_modular_profile_room_ac(driver, device) - local running_state_supported = device:get_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) - local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) - local optional_supported_component_capabilities = {} - local main_component_capabilities = {} - local profile_name = "room-air-conditioner-modular" - - if #humidity_eps > 0 then - table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) - end - - -- determine fan capabilities - local fan_eps = device:get_endpoints(clusters.FanControl.ID) - local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) - -- Note: Room AC does not support the rocking feature of FanControl. - - if #fan_eps > 0 then - table.insert(main_component_capabilities, capabilities.airConditionerFanMode.ID) - table.insert(main_component_capabilities, capabilities.fanSpeedPercent.ID) - end - if #wind_eps > 0 then - table.insert(main_component_capabilities, capabilities.windMode.ID) - end - - local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) - local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) - - if #heat_eps > 0 then - table.insert(main_component_capabilities, capabilities.thermostatHeatingSetpoint.ID) - end - if #cool_eps > 0 then - table.insert(main_component_capabilities, capabilities.thermostatCoolingSetpoint.ID) - end - - if running_state_supported then - table.insert(main_component_capabilities, capabilities.thermostatOperatingState.ID) - end - - table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) - device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) - - -- earlier modular profile gating (min api v14, rpc 8) ensures we are running >= 0.57 FW. - -- This gating specifies a workaround required only for 0.57 FW, which is not needed for 0.58 and higher. - if version.api < 15 or version.rpc < 9 then - -- add mandatory capabilities for subscription - local total_supported_capabilities = optional_supported_component_capabilities - table.insert(main_component_capabilities, capabilities.switch.ID) - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - table.insert(main_component_capabilities, capabilities.thermostatMode.ID) - table.insert(main_component_capabilities, capabilities.refresh.ID) - table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) - - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) - end -end - -local function match_modular_profile(driver, device, device_type) - if profiling_data_still_required(device) then return end - - if device_type == AP_DEVICE_TYPE_ID then - match_modular_profile_air_purifer(driver, device) - elseif device_type == RAC_DEVICE_TYPE_ID then - match_modular_profile_room_ac(driver, device) - elseif device_type == THERMOSTAT_DEVICE_TYPE_ID then - match_modular_profile_thermostat(driver, device) - else - device.log.warn_with({hub_logs=true}, "Device type is not supported by modular profile in thermostat driver, trying profile switch instead") - match_profile_switch(driver, device) - return - end - - -- clear all profiling data fields after profiling is complete. - for _, field in pairs(profiling_data) do - device:set_field(field, nil) - end -end - -local function supports_modular_profile(device) - local supported_modular_device_types = { - AP_DEVICE_TYPE_ID, - RAC_DEVICE_TYPE_ID, - THERMOSTAT_DEVICE_TYPE_ID, - } - local device_type = get_device_type(device) - if not tbl_contains(supported_modular_device_types, device_type) then - device_type = false - end - return version.api >= 14 and version.rpc >= 8 and device_type -end - -function match_profile(driver, device) - local modular_device_type = supports_modular_profile(device) - if modular_device_type then - match_modular_profile(driver, device, modular_device_type) - else - match_profile_switch(driver, device) - end -end - -local function do_configure(driver, device) - match_profile(driver, device) -end - -local function driver_switched(driver, device) - match_profile(driver, device) -end - -local function device_added(driver, device) - local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - req:merge(clusters.Thermostat.attributes.ControlSequenceOfOperation:read(device)) - req:merge(clusters.FanControl.attributes.FanModeSequence:read(device)) - req:merge(clusters.FanControl.attributes.WindSupport:read(device)) - req:merge(clusters.FanControl.attributes.RockSupport:read(device)) - - local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) - if #thermostat_eps > 0 then - req:merge(clusters.Thermostat.attributes.AttributeList:read(device)) - else - device:set_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, false) - end - local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) - if #battery_feature_eps > 0 then - req:merge(clusters.PowerSource.attributes.AttributeList:read(device)) - else - device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.NO_BATTERY) - end - device:send(req) - local heat_pump_eps = get_endpoints_for_dt(device, HEAT_PUMP_DEVICE_TYPE_ID) or {} - if #heat_pump_eps > 0 then - local thermostat_eps = get_endpoints_for_dt(device, THERMOSTAT_DEVICE_TYPE_ID) or {} - local component_to_endpoint_map = { - ["thermostatOne"] = thermostat_eps[1], - ["thermostatTwo"] = thermostat_eps[2], - } - device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, {persist = true}) - end -end - -local function store_unit_factory(capability_name) - return function(driver, device, ib, response) - device:set_field(capability_name.."_unit", ib.data.value, {persist = true}) - end -end - -local units = { - PPM = 0, - PPB = 1, - PPT = 2, - MGM3 = 3, - UGM3 = 4, - NGM3 = 5, - PM3 = 6, - BQM3 = 7, - PCIL = 0xFF -- not in matter spec -} - -local unit_strings = { - [units.PPM] = "ppm", - [units.PPB] = "ppb", - [units.PPT] = "ppt", - [units.MGM3] = "mg/m^3", - [units.NGM3] = "ng/m^3", - [units.UGM3] = "μg/m^3", - [units.BQM3] = "Bq/m^3", - [units.PCIL] = "pCi/L" -} - -local unit_default = { - [capabilities.carbonMonoxideMeasurement.NAME] = units.PPM, - [capabilities.carbonDioxideMeasurement.NAME] = units.PPM, - [capabilities.nitrogenDioxideMeasurement.NAME] = units.PPM, - [capabilities.ozoneMeasurement.NAME] = units.PPM, - [capabilities.formaldehydeMeasurement.NAME] = units.PPM, - [capabilities.veryFineDustSensor.NAME] = units.UGM3, - [capabilities.fineDustSensor.NAME] = units.UGM3, - [capabilities.dustSensor.NAME] = units.UGM3, - [capabilities.radonMeasurement.NAME] = units.BQM3, - [capabilities.tvocMeasurement.NAME] = units.PPB -- TVOC is typically within the range of 0-5500 ppb, with good to moderate values being < 660 ppb -} - --- All ConcentrationMesurement clusters inherit from the same base cluster definitions, --- so CarbonMonoxideConcentratinMeasurement is used below but the same enum types exist --- in all ConcentrationMeasurement clusters -local level_strings = { - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.UNKNOWN] = "unknown", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.LOW] = "good", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.MEDIUM] = "moderate", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.HIGH] = "unhealthy", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.CRITICAL] = "hazardous", -} - --- measured in g/mol -local molecular_weights = { - [capabilities.carbonDioxideMeasurement.NAME] = 44.010, - [capabilities.nitrogenDioxideMeasurement.NAME] = 28.014, - [capabilities.ozoneMeasurement.NAME] = 48.0, - [capabilities.formaldehydeMeasurement.NAME] = 30.031, - [capabilities.veryFineDustSensor.NAME] = "N/A", - [capabilities.fineDustSensor.NAME] = "N/A", - [capabilities.dustSensor.NAME] = "N/A", - [capabilities.radonMeasurement.NAME] = 222.018, - [capabilities.tvocMeasurement.NAME] = "N/A", -} - -local conversion_tables = { - [units.PPM] = { - [units.PPM] = function(value) return utils.round(value) end, - [units.PPB] = function(value) return utils.round(value * (10^3)) end, - [units.UGM3] = function(value, molecular_weight) return utils.round((value * molecular_weight * 10^3) / MGM3_PPM_CONVERSION_FACTOR) end, - [units.MGM3] = function(value, molecular_weight) return utils.round((value * molecular_weight) / MGM3_PPM_CONVERSION_FACTOR) end, - }, - [units.PPB] = { - [units.PPM] = function(value) return utils.round(value/(10^3)) end, - [units.PPB] = function(value) return utils.round(value) end, - }, - [units.PPT] = { - [units.PPM] = function(value) return utils.round(value/(10^6)) end - }, - [units.MGM3] = { - [units.UGM3] = function(value) return utils.round(value * (10^3)) end, - [units.PPM] = function(value, molecular_weight) return utils.round((value * MGM3_PPM_CONVERSION_FACTOR) / molecular_weight) end, - }, - [units.UGM3] = { - [units.UGM3] = function(value) return utils.round(value) end, - [units.PPM] = function(value, molecular_weight) return utils.round((value * MGM3_PPM_CONVERSION_FACTOR) / (molecular_weight * 10^3)) end, - }, - [units.NGM3] = { - [units.UGM3] = function(value) return utils.round(value/(10^3)) end - }, - [units.BQM3] = { - [units.PCIL] = function(value) return utils.round(value/37) end - }, -} - -local function unit_conversion(value, from_unit, to_unit, capability_name) - local conversion_function = conversion_tables[from_unit][to_unit] - if not conversion_function then - log.info_with( {hub_logs = true} , string.format("Unsupported unit conversion from %s to %s", unit_strings[from_unit], unit_strings[to_unit])) - return - end - - if not value then - log.info_with( {hub_logs = true} , "unit conversion value is nil") - return - end - - return conversion_function(value, molecular_weights[capability_name]) -end - -local function measurementHandlerFactory(capability_name, attribute, target_unit) - return function(driver, device, ib, response) - local reporting_unit = device:get_field(capability_name.."_unit") - - if not reporting_unit then - reporting_unit = unit_default[capability_name] - device:set_field(capability_name.."_unit", reporting_unit, {persist = true}) - end - - local value = nil - if reporting_unit then - value = unit_conversion(ib.data.value, reporting_unit, target_unit, capability_name) - end - - if value then - device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = value, unit = unit_strings[target_unit]})) - -- handle case where device profile supports both fineDustLevel and dustLevel - if capability_name == capabilities.fineDustSensor.NAME and device:supports_capability(capabilities.dustSensor) then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.dustSensor.fineDustLevel({value = value, unit = unit_strings[target_unit]})) - end - end - end -end - -local function levelHandlerFactory(attribute) - return function(driver, device, ib, response) - device:emit_event_for_endpoint(ib.endpoint_id, attribute(level_strings[ib.data.value])) - end -end - --- handlers -local function air_quality_attr_handler(driver, device, ib, response) - local state = ib.data.value - if state == 0 then -- Unknown - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unknown()) - elseif state == 1 then -- Good - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.good()) - elseif state == 2 then -- Fair - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.moderate()) - elseif state == 3 then -- Moderate - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.slightlyUnhealthy()) - elseif state == 4 then -- Poor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unhealthy()) - elseif state == 5 then -- VeryPoor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.veryUnhealthy()) - elseif state == 6 then -- ExtremelyPoor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.hazardous()) - end -end - -local function on_off_attr_handler(driver, device, ib, response) - if ib.data.value then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.on()) - else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) - end -end - -local function temp_event_handler(attribute) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local unit = "C" - - -- Only emit the capability for RPC version >= 5, since unit conversion for - -- range capabilities is only supported in that case. - if version.rpc >= 5 then - local event - if attribute == capabilities.thermostatCoolingSetpoint.coolingSetpoint then - local range = { - minimum = device:get_field(setpoint_limit_device_field.MIN_COOL) or THERMOSTAT_MIN_TEMP_IN_C, - maximum = device:get_field(setpoint_limit_device_field.MAX_COOL) or THERMOSTAT_MAX_TEMP_IN_C, - step = 0.1 - } - event = capabilities.thermostatCoolingSetpoint.coolingSetpointRange({value = range, unit = unit}) - device:emit_event_for_endpoint(ib.endpoint_id, event) - elseif attribute == capabilities.thermostatHeatingSetpoint.heatingSetpoint then - local MAX_TEMP_IN_C = THERMOSTAT_MAX_TEMP_IN_C - local MIN_TEMP_IN_C = THERMOSTAT_MIN_TEMP_IN_C - local is_water_heater_device = get_device_type(device) == WATER_HEATER_DEVICE_TYPE_ID - if is_water_heater_device then - MAX_TEMP_IN_C = WATER_HEATER_MAX_TEMP_IN_C - MIN_TEMP_IN_C = WATER_HEATER_MIN_TEMP_IN_C - end - - local range = { - minimum = device:get_field(setpoint_limit_device_field.MIN_HEAT) or MIN_TEMP_IN_C, - maximum = device:get_field(setpoint_limit_device_field.MAX_HEAT) or MAX_TEMP_IN_C, - step = 0.1 - } - event = capabilities.thermostatHeatingSetpoint.heatingSetpointRange({value = range, unit = unit}) - device:emit_event_for_endpoint(ib.endpoint_id, event) - end - end - - local temp = ib.data.value / 100.0 - device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = temp, unit = unit})) - end -end - -local temp_attr_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local temp = ib.data.value / 100.0 - local unit = "C" - temp = utils.clamp_value(temp, THERMOSTAT_MIN_TEMP_IN_C, THERMOSTAT_MAX_TEMP_IN_C) - set_field_for_endpoint(device, minOrMax, ib.endpoint_id, temp) - local min = get_field_for_endpoint(device, setpoint_limit_device_field.MIN_TEMP, ib.endpoint_id) - local max = get_field_for_endpoint(device, setpoint_limit_device_field.MAX_TEMP, ib.endpoint_id) - if min ~= nil and max ~= nil then - if min < max then - -- Only emit the capability for RPC version >= 5 (unit conversion for - -- temperature range capability is only supported for RPC >= 5) - if version.rpc >= 5 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = min, maximum = max }, unit = unit })) - end - set_field_for_endpoint(device, setpoint_limit_device_field.MIN_TEMP, ib.endpoint_id, nil) - set_field_for_endpoint(device, setpoint_limit_device_field.MAX_TEMP, ib.endpoint_id, nil) - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min temperature %d that is not lower than the reported max temperature %d", min, max)) - end - end - end -end - -local function humidity_attr_handler(driver, device, ib, response) - local humidity = math.floor(ib.data.value / 100.0) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(humidity)) -end - -local function system_mode_handler(driver, device, ib, response) - if device:get_field(OPTIONAL_THERMOSTAT_MODES_SEEN) == nil then -- this being nil means the sequence_of_operation_handler hasn't run. - device.log.info_with({hub_logs = true}, "In the SystemMode handler: ControlSequenceOfOperation has not run yet. Exiting early.") - device:set_field(SAVED_SYSTEM_MODE_IB, ib) - return - end - - local supported_modes = device:get_latest_state(device:endpoint_to_component(ib.endpoint_id), capabilities.thermostatMode.ID, capabilities.thermostatMode.supportedThermostatModes.NAME) or {} - -- check that the given mode was in the supported modes list - if tbl_contains(supported_modes, THERMOSTAT_MODE_MAP[ib.data.value].NAME) then - device:emit_event_for_endpoint(ib.endpoint_id, THERMOSTAT_MODE_MAP[ib.data.value]()) - return - end - -- if the value is not found in the supported modes list, check if it's disallowed and early return if so. - local disallowed_thermostat_modes = device:get_field(DISALLOWED_THERMOSTAT_MODES) or {} - if tbl_contains(disallowed_thermostat_modes, THERMOSTAT_MODE_MAP[ib.data.value].NAME) then - return - end - -- if we get here, then the reported mode is allowed and not in our mode map - -- add the mode to the OPTIONAL_THERMOSTAT_MODES_SEEN and supportedThermostatModes tables - local optional_modes_seen = utils.deep_copy(device:get_field(OPTIONAL_THERMOSTAT_MODES_SEEN)) or {} - table.insert(optional_modes_seen, THERMOSTAT_MODE_MAP[ib.data.value].NAME) - device:set_field(OPTIONAL_THERMOSTAT_MODES_SEEN, optional_modes_seen, {persist=true}) - local sm_copy = utils.deep_copy(supported_modes) - table.insert(sm_copy, THERMOSTAT_MODE_MAP[ib.data.value].NAME) - local supported_modes_event = capabilities.thermostatMode.supportedThermostatModes(sm_copy, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, supported_modes_event) - device:emit_event_for_endpoint(ib.endpoint_id, THERMOSTAT_MODE_MAP[ib.data.value]()) -end - -local function running_state_handler(driver, device, ib, response) - for mode, operating_state in pairs(THERMOSTAT_OPERATING_MODE_MAP) do - if ((ib.data.value >> mode) & 1) > 0 then - device:emit_event_for_endpoint(ib.endpoint_id, operating_state()) - return - end - end - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatOperatingState.thermostatOperatingState.idle()) -end - -local function sequence_of_operation_handler(driver, device, ib, response) - -- The ControlSequenceOfOperation attribute only directly specifies what can't be operated by the operating environment, not what can. - -- However, we assert here that a Cooling enum value implies that SystemMode supports cooling, and the same for a Heating enum. - -- We also assert that Off is supported, though per spec this is optional. - if device:get_field(OPTIONAL_THERMOSTAT_MODES_SEEN) == nil then - device:set_field(OPTIONAL_THERMOSTAT_MODES_SEEN, {capabilities.thermostatMode.thermostatMode.off.NAME}, {persist=true}) - end - local supported_modes = utils.deep_copy(device:get_field(OPTIONAL_THERMOSTAT_MODES_SEEN)) - local disallowed_mode_operations = {} - - local modes_for_inclusion = {} - if ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.COOLING_WITH_REHEAT then - local _, found_idx = tbl_contains(supported_modes, capabilities.thermostatMode.thermostatMode.emergency_heat.NAME) - if found_idx then - table.remove(supported_modes, found_idx) -- if seen before, remove now - end - table.insert(supported_modes, capabilities.thermostatMode.thermostatMode.cool.NAME) - table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.heat.NAME) - table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.emergency_heat.NAME) - elseif ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.HEATING_WITH_REHEAT then - local _, found_idx = tbl_contains(supported_modes, capabilities.thermostatMode.thermostatMode.precooling.NAME) - if found_idx then - table.remove(supported_modes, found_idx) -- if seen before, remove now - end - table.insert(supported_modes, capabilities.thermostatMode.thermostatMode.heat.NAME) - table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.cool.NAME) - table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.precooling.NAME) - elseif ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.COOLING_AND_HEATING_WITH_REHEAT then - table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.cool.NAME) - table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.heat.NAME) - end - - -- check whether the Auto Mode should be supported in SystemMode, though this is unrelated to ControlSequenceOfOperation - local auto = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}) - if #auto > 0 then - table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.auto.NAME) - else - table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.auto.NAME) - end - - -- if a disallowed value was once allowed and added, it should be removed now. - for index, mode in pairs(supported_modes) do - if tbl_contains(disallowed_mode_operations, mode) then - table.remove(supported_modes, index) - end - end - -- do not include any values twice - for _, mode in pairs(modes_for_inclusion) do - if not tbl_contains(supported_modes, mode) then - table.insert(supported_modes, mode) - end - end - device:set_field(DISALLOWED_THERMOSTAT_MODES, disallowed_mode_operations) - local event = capabilities.thermostatMode.supportedThermostatModes(supported_modes, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, event) - - -- will be set by the SystemMode handler if this handler hasn't run yet. - if device:get_field(SAVED_SYSTEM_MODE_IB) then - system_mode_handler(driver, device, device:get_field(SAVED_SYSTEM_MODE_IB), response) - device:set_field(SAVED_SYSTEM_MODE_IB, nil) - end -end - -local function min_deadband_limit_handler(driver, device, ib, response) - local val = ib.data.value / 10.0 - log.info("Setting " .. setpoint_limit_device_field.MIN_DEADBAND .. " to " .. string.format("%s", val)) - device:set_field(setpoint_limit_device_field.MIN_DEADBAND, val, { persist = true }) - device:set_field(setpoint_limit_device_field.MIN_SETPOINT_DEADBAND_CHECKED, true, {persist = true}) -end - -local function fan_mode_handler(driver, device, ib, response) - local fan_mode_event = { - [clusters.FanControl.attributes.FanMode.OFF] = { capabilities.fanMode.fanMode.off(), - capabilities.airConditionerFanMode.fanMode("off"), - capabilities.airPurifierFanMode.airPurifierFanMode.off(), - nil }, -- 'OFF' is not supported by thermostatFanMode - [clusters.FanControl.attributes.FanMode.LOW] = { capabilities.fanMode.fanMode.low(), - capabilities.airConditionerFanMode.fanMode("low"), - capabilities.airPurifierFanMode.airPurifierFanMode.low(), - capabilities.thermostatFanMode.thermostatFanMode.on() }, - [clusters.FanControl.attributes.FanMode.MEDIUM] = { capabilities.fanMode.fanMode.medium(), - capabilities.airConditionerFanMode.fanMode("medium"), - capabilities.airPurifierFanMode.airPurifierFanMode.medium(), - capabilities.thermostatFanMode.thermostatFanMode.on() }, - [clusters.FanControl.attributes.FanMode.HIGH] = { capabilities.fanMode.fanMode.high(), - capabilities.airConditionerFanMode.fanMode("high"), - capabilities.airPurifierFanMode.airPurifierFanMode.high(), - capabilities.thermostatFanMode.thermostatFanMode.on() }, - [clusters.FanControl.attributes.FanMode.ON] = { capabilities.fanMode.fanMode.auto(), - capabilities.airConditionerFanMode.fanMode("auto"), - capabilities.airPurifierFanMode.airPurifierFanMode.auto(), - capabilities.thermostatFanMode.thermostatFanMode.on() }, - [clusters.FanControl.attributes.FanMode.AUTO] = { capabilities.fanMode.fanMode.auto(), - capabilities.airConditionerFanMode.fanMode("auto"), - capabilities.airPurifierFanMode.airPurifierFanMode.auto(), - capabilities.thermostatFanMode.thermostatFanMode.auto() }, - [clusters.FanControl.attributes.FanMode.SMART] = { capabilities.fanMode.fanMode.auto(), - capabilities.airConditionerFanMode.fanMode("auto"), - capabilities.airPurifierFanMode.airPurifierFanMode.auto(), - capabilities.thermostatFanMode.thermostatFanMode.auto() } - } - local fan_mode_idx = device:supports_capability_by_id(capabilities.fanMode.ID) and 1 or - device:supports_capability_by_id(capabilities.airConditionerFanMode.ID) and 2 or - device:supports_capability_by_id(capabilities.airPurifierFanMode.ID) and 3 or - device:supports_capability_by_id(capabilities.thermostatFanMode.ID) and 4 - if fan_mode_idx ~= false and fan_mode_event[ib.data.value][fan_mode_idx] then - device:emit_event_for_endpoint(ib.endpoint_id, fan_mode_event[ib.data.value][fan_mode_idx]) - else - log.warn(string.format("Invalid Fan Mode (%s)", ib.data.value)) - end -end - -local function fan_mode_sequence_handler(driver, device, ib, response) - local supportedFanModes, supported_fan_modes_attribute - if ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH then - supportedFanModes = { "off", "low", "medium", "high" } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH then - supportedFanModes = { "off", "low", "high" } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO then - supportedFanModes = { "off", "low", "medium", "high", "auto" } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH_AUTO then - supportedFanModes = { "off", "low", "high", "auto" } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_HIGH_AUTO then - supportedFanModes = { "off", "high", "auto" } - else - supportedFanModes = { "off", "high" } - end - - if device:supports_capability_by_id(capabilities.airPurifierFanMode.ID) then - supported_fan_modes_attribute = capabilities.airPurifierFanMode.supportedAirPurifierFanModes - elseif device:supports_capability_by_id(capabilities.airConditionerFanMode.ID) then - supported_fan_modes_attribute = capabilities.airConditionerFanMode.supportedAcFanModes - elseif device:supports_capability_by_id(capabilities.thermostatFanMode.ID) then - supported_fan_modes_attribute = capabilities.thermostatFanMode.supportedThermostatFanModes - -- Our thermostat fan mode control is not granular enough to handle all of the supported modes - if ib.data.value >= clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO and - ib.data.value <= clusters.FanControl.attributes.FanModeSequence.OFF_ON_AUTO then - supportedFanModes = { "auto", "on" } - else - supportedFanModes = { "on" } - end - else - supported_fan_modes_attribute = capabilities.fanMode.supportedFanModes - end - - local event = supported_fan_modes_attribute(supportedFanModes, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, event) -end - -local function fan_speed_percent_attr_handler(driver, device, ib, response) - local speed = 0 - if ib.data.value ~= nil then - speed = utils.clamp_value(ib.data.value, MIN_ALLOWED_PERCENT_VALUE, MAX_ALLOWED_PERCENT_VALUE) - end - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(speed)) -end - -local function wind_support_handler(driver, device, ib, response) - local supported_wind_modes = {capabilities.windMode.windMode.noWind.NAME} - for mode, wind_mode in pairs(WIND_MODE_MAP) do - if ((ib.data.value >> mode) & 1) > 0 then - table.insert(supported_wind_modes, wind_mode.NAME) - end - end - local event = capabilities.windMode.supportedWindModes(supported_wind_modes, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, event) -end - -local function wind_setting_handler(driver, device, ib, response) - for index, wind_mode in pairs(WIND_MODE_MAP) do - if ((ib.data.value >> index) & 1) > 0 then - device:emit_event_for_endpoint(ib.endpoint_id, wind_mode()) - return - end - end - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windMode.windMode.noWind()) -end - -local function rock_support_handler(driver, device, ib, response) - local supported_rock_modes = {capabilities.fanOscillationMode.fanOscillationMode.off.NAME} - for mode, rock_mode in pairs(ROCK_MODE_MAP) do - if ((ib.data.value >> mode) & 1) > 0 then - table.insert(supported_rock_modes, rock_mode.NAME) - end - end - local event = capabilities.fanOscillationMode.supportedFanOscillationModes(supported_rock_modes, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, event) -end - -local function rock_setting_handler(driver, device, ib, response) - for index, rock_mode in pairs(ROCK_MODE_MAP) do - if ((ib.data.value >> index) & 1) > 0 then - device:emit_event_for_endpoint(ib.endpoint_id, rock_mode()) - return - end - end - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanOscillationMode.fanOscillationMode.off()) -end - -local function hepa_filter_condition_handler(driver, device, ib, response) - local component = device.profile.components["hepaFilter"] - local condition = ib.data.value - device:emit_component_event(component, capabilities.filterState.filterLifeRemaining(condition)) -end - -local function hepa_filter_change_indication_handler(driver, device, ib, response) - local component = device.profile.components["hepaFilter"] - if ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.OK then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) - elseif ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.WARNING then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) - elseif ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.CRITICAL then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.replace()) - end -end - -local function activated_carbon_filter_condition_handler(driver, device, ib, response) - local component = device.profile.components["activatedCarbonFilter"] - local condition = ib.data.value - device:emit_component_event(component, capabilities.filterState.filterLifeRemaining(condition)) -end - -local function activated_carbon_filter_change_indication_handler(driver, device, ib, response) - local component = device.profile.components["activatedCarbonFilter"] - if ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.OK then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) - elseif ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.WARNING then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) - elseif ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.CRITICAL then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.replace()) - end -end - -local function handle_switch_on(driver, device, cmd) - local endpoint_id = component_to_endpoint(device, cmd.component, clusters.OnOff.ID) - local req = clusters.OnOff.server.commands.On(device, endpoint_id) - device:send(req) -end - -local function handle_switch_off(driver, device, cmd) - local endpoint_id = component_to_endpoint(device, cmd.component, clusters.OnOff.ID) - local req = clusters.OnOff.server.commands.Off(device, endpoint_id) - device:send(req) -end - -local function set_thermostat_mode(driver, device, cmd) - local mode_id = nil - for value, mode in pairs(THERMOSTAT_MODE_MAP) do - if mode.NAME == cmd.args.mode then - mode_id = value - break - end - end - if mode_id then - device:send(clusters.Thermostat.attributes.SystemMode:write(device, component_to_endpoint(device, cmd.component, clusters.Thermostat.ID), mode_id)) - end -end - -local thermostat_mode_setter = function(mode_name) - return function(driver, device, cmd) - return set_thermostat_mode(driver, device, {component = cmd.component, args = {mode = mode_name}}) - end -end - -local function set_setpoint(setpoint) - return function(driver, device, cmd) - local endpoint_id = component_to_endpoint(device, cmd.component, clusters.Thermostat.ID) - local MAX_TEMP_IN_C = THERMOSTAT_MAX_TEMP_IN_C - local MIN_TEMP_IN_C = THERMOSTAT_MIN_TEMP_IN_C - local is_water_heater_device = get_device_type(device) == WATER_HEATER_DEVICE_TYPE_ID - if is_water_heater_device then - MAX_TEMP_IN_C = WATER_HEATER_MAX_TEMP_IN_C - MIN_TEMP_IN_C = WATER_HEATER_MIN_TEMP_IN_C - end - local value = cmd.args.setpoint - if version.rpc <= 5 and value > MAX_TEMP_IN_C then - value = utils.f_to_c(value) - end - - -- Gather cached setpoint values when considering setpoint limits - -- Note: cached values should always exist, but defaults are chosen just in case to prevent - -- nil operation errors, and deadband logic from triggering. - local cached_cooling_val, cooling_setpoint = device:get_latest_state( - cmd.component, capabilities.thermostatCoolingSetpoint.ID, - capabilities.thermostatCoolingSetpoint.coolingSetpoint.NAME, - MAX_TEMP_IN_C, { value = MAX_TEMP_IN_C, unit = "C" } - ) - if cooling_setpoint and cooling_setpoint.unit == "F" then - cached_cooling_val = utils.f_to_c(cached_cooling_val) - end - local cached_heating_val, heating_setpoint = device:get_latest_state( - cmd.component, capabilities.thermostatHeatingSetpoint.ID, - capabilities.thermostatHeatingSetpoint.heatingSetpoint.NAME, - MIN_TEMP_IN_C, { value = MIN_TEMP_IN_C, unit = "C" } - ) - if heating_setpoint and heating_setpoint.unit == "F" then - cached_heating_val = utils.f_to_c(cached_heating_val) - end - local is_auto_capable = #device:get_endpoints( - clusters.Thermostat.ID, - {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE} - ) > 0 - - --Check setpoint limits for the device - local setpoint_type = string.match(setpoint.NAME, "Heat") or "Cool" - local deadband = device:get_field(setpoint_limit_device_field.MIN_DEADBAND) or 2.5 --spec default - if setpoint_type == "Heat" then - local min = device:get_field(setpoint_limit_device_field.MIN_HEAT) or MIN_TEMP_IN_C - local max = device:get_field(setpoint_limit_device_field.MAX_HEAT) or MAX_TEMP_IN_C - if value < min or value > max then - log.warn(string.format( - "Invalid setpoint (%s) outside the min (%s) and the max (%s)", - value, min, max - )) - device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpoint(heating_setpoint, {state_change = true})) - return - end - if is_auto_capable and value > (cached_cooling_val - deadband) then - log.warn(string.format( - "Invalid setpoint (%s) is greater than the cooling setpoint (%s) with the deadband (%s)", - value, cooling_setpoint, deadband - )) - device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpoint(heating_setpoint, {state_change = true})) - return - end - else - local min = device:get_field(setpoint_limit_device_field.MIN_COOL) or MIN_TEMP_IN_C - local max = device:get_field(setpoint_limit_device_field.MAX_COOL) or MAX_TEMP_IN_C - if value < min or value > max then - log.warn(string.format( - "Invalid setpoint (%s) outside the min (%s) and the max (%s)", - value, min, max - )) - device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpoint(cooling_setpoint, {state_change = true})) - return - end - if is_auto_capable and value < (cached_heating_val + deadband) then - log.warn(string.format( - "Invalid setpoint (%s) is less than the heating setpoint (%s) with the deadband (%s)", - value, heating_setpoint, deadband - )) - device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpoint(cooling_setpoint, {state_change = true})) - return - end - end - device:send(setpoint:write(device, component_to_endpoint(device, cmd.component, clusters.Thermostat.ID), utils.round(value * 100.0))) - end -end - -local heating_setpoint_limit_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local MAX_TEMP_IN_C = THERMOSTAT_MAX_TEMP_IN_C - local MIN_TEMP_IN_C = THERMOSTAT_MIN_TEMP_IN_C - local is_water_heater_device = (get_device_type(device) == WATER_HEATER_DEVICE_TYPE_ID) - if is_water_heater_device then - MAX_TEMP_IN_C = WATER_HEATER_MAX_TEMP_IN_C - MIN_TEMP_IN_C = WATER_HEATER_MIN_TEMP_IN_C - end - local val = ib.data.value / 100.0 - val = utils.clamp_value(val, MIN_TEMP_IN_C, MAX_TEMP_IN_C) - device:set_field(minOrMax, val) - local min = device:get_field(setpoint_limit_device_field.MIN_HEAT) - local max = device:get_field(setpoint_limit_device_field.MAX_HEAT) - if min ~= nil and max ~= nil then - if min < max then - -- Only emit the capability for RPC version >= 5 (unit conversion for - -- heating setpoint range capability is only supported for RPC >= 5) - if version.rpc >= 5 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = min, maximum = max, step = 0.1 }, unit = "C" })) - end - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min heating setpoint %d that is not lower than the reported max %d", min, max)) - end - end - end -end - -local cooling_setpoint_limit_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local val = ib.data.value / 100.0 - val = utils.clamp_value(val, THERMOSTAT_MIN_TEMP_IN_C, THERMOSTAT_MAX_TEMP_IN_C) - device:set_field(minOrMax, val) - local min = device:get_field(setpoint_limit_device_field.MIN_COOL) - local max = device:get_field(setpoint_limit_device_field.MAX_COOL) - if min ~= nil and max ~= nil then - if min < max then - -- Only emit the capability for RPC version >= 5 (unit conversion for - -- cooling setpoint range capability is only supported for RPC >= 5) - if version.rpc >= 5 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { minimum = min, maximum = max, step = 0.1 }, unit = "C" })) - end - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min cooling setpoint %d that is not lower than the reported max %d", min, max)) - end - end - end -end - -local function set_fan_mode(device, cmd, fan_mode_capability) - local command_argument = cmd.args.fanMode - if fan_mode_capability == capabilities.airPurifierFanMode then - command_argument = cmd.args.airPurifierFanMode - elseif fan_mode_capability == capabilities.thermostatFanMode then - command_argument = cmd.args.mode - end - local fan_mode_id - if command_argument == "off" then - fan_mode_id = clusters.FanControl.attributes.FanMode.OFF - elseif command_argument == "on" then - fan_mode_id = clusters.FanControl.attributes.FanMode.ON - elseif command_argument == "auto" then - fan_mode_id = clusters.FanControl.attributes.FanMode.AUTO - elseif command_argument == "high" then - fan_mode_id = clusters.FanControl.attributes.FanMode.HIGH - elseif command_argument == "medium" then - fan_mode_id = clusters.FanControl.attributes.FanMode.MEDIUM - elseif tbl_contains({ "low", "sleep", "quiet", "windFree" }, command_argument) then - fan_mode_id = clusters.FanControl.attributes.FanMode.LOW - else - device.log.warn(string.format("Invalid Fan Mode (%s) received from capability command", command_argument)) - return - end - device:send(clusters.FanControl.attributes.FanMode:write(device, component_to_endpoint(device, cmd.component, clusters.FanControl.ID), fan_mode_id)) -end - -local set_fan_mode_factory = function(fan_mode_capability) - return function(driver, device, cmd) - set_fan_mode(device, cmd, fan_mode_capability) - end -end +local log = require "log" +local version = require "version" +local MatterDriver = require "st.matter.driver" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local im = require "st.matter.interaction_model" +local attribute_handlers = require "thermostat_handlers.attribute_handlers" +local capability_handlers = require "thermostat_handlers.capability_handlers" +local fields = require "thermostat_utils.fields" +local thermostat_utils = require "thermostat_utils.utils" +local embedded_cluster_utils = require "thermostat_utils.embedded_cluster_utils" -local function thermostat_fan_mode_setter(mode_name) - return function(driver, device, cmd) - set_fan_mode(device, {component = cmd.component, args = {mode = mode_name}}, capabilities.thermostatFanMode) - end +if version.api < 10 then + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" end -local function set_fan_speed_percent(driver, device, cmd) - local speed = math.floor(cmd.args.percent) - device:send(clusters.FanControl.attributes.PercentSetting:write(device, component_to_endpoint(device, cmd.component, clusters.FanControl.ID), speed)) +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" end -local function set_wind_mode(driver, device, cmd) - local wind_mode = 0 - if cmd.args.windMode == capabilities.windMode.windMode.sleepWind.NAME then - wind_mode = clusters.FanControl.types.WindSupportMask.SLEEP_WIND - elseif cmd.args.windMode == capabilities.windMode.windMode.naturalWind.NAME then - wind_mode = clusters.FanControl.types.WindSupportMask.NATURAL_WIND - end - device:send(clusters.FanControl.attributes.WindSetting:write(device, component_to_endpoint(device, cmd.component, clusters.FanControl.ID), wind_mode)) +if version.api < 13 then + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" end -local function set_rock_mode(driver, device, cmd) - local rock_mode = 0 - if cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.horizontal.NAME then - rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_LEFT_RIGHT - elseif cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.vertical.NAME then - rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_UP_DOWN - elseif cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.swing.NAME then - rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_ROUND - end - device:send(clusters.FanControl.attributes.RockSetting:write(device, component_to_endpoint(device, cmd.component, clusters.FanControl.ID), rock_mode)) -end +local ThermostatLifecycleHandlers = {} -local function set_water_heater_mode(driver, device, cmd) - device.log.info(string.format("set_water_heater_mode mode: %s", cmd.args.mode)) - local endpoint_id = component_to_endpoint(device, cmd.component, clusters.Thermostat.ID) - local supportedWaterHeaterModesWithIdx = device:get_field(SUPPORTED_WATER_HEATER_MODES_WITH_IDX) or {} - for i, mode in ipairs(supportedWaterHeaterModesWithIdx) do - if cmd.args.mode == mode[2] then - device:send(clusters.WaterHeaterMode.commands.ChangeToMode(device, endpoint_id, mode[1])) - return - end - end -end +function ThermostatLifecycleHandlers.device_added(driver, device) + local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + req:merge(clusters.Thermostat.attributes.ControlSequenceOfOperation:read(device)) + req:merge(clusters.FanControl.attributes.FanModeSequence:read(device)) + req:merge(clusters.FanControl.attributes.WindSupport:read(device)) + req:merge(clusters.FanControl.attributes.RockSupport:read(device)) -local function reset_filter_state(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - if cmd.component == "hepaFilter" then - device:send(clusters.HepaFilterMonitoring.server.commands.ResetCondition(device, endpoint_id)) + local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) + if #thermostat_eps > 0 then + req:merge(clusters.Thermostat.attributes.AttributeList:read(device)) else - device:send(clusters.ActivatedCarbonFilterMonitoring.server.commands.ResetCondition(device, endpoint_id)) + device:set_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, false) end -end - -local function battery_percent_remaining_attr_handler(driver, device, ib, response) - if ib.data.value then - device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) + local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) + if #battery_feature_eps > 0 then + req:merge(clusters.PowerSource.attributes.AttributeList:read(device)) + else + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY) end -end - -local function active_power_handler(driver, device, ib, response) - if ib.data.value then - local watt_value = ib.data.value / 1000 - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W" })) - if type(device.register_native_capability_attr_handler) == "function" then - device:register_native_capability_attr_handler("powerMeter","power") - end + device:send(req) + local heat_pump_eps = thermostat_utils.get_endpoints_by_device_type(device, fields.HEAT_PUMP_DEVICE_TYPE_ID) or {} + if #heat_pump_eps > 0 then + local thermostat_eps = thermostat_utils.get_endpoints_by_device_type(device, fields.THERMOSTAT_DEVICE_TYPE_ID) or {} + local component_to_endpoint_map = { + ["thermostatOne"] = thermostat_eps[1], + ["thermostatTwo"] = thermostat_eps[2], + } + device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, {persist = true}) end end -local function periodic_energy_imported_handler(driver, device, ib, response) - if ib.data then - if version.api < 11 then - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:augment_type(ib.data) - end - local endpoint_id = string.format(ib.endpoint_id) - local energy_imported_Wh = utils.round(ib.data.elements.energy.value / 1000) --convert mWh to Wh - local cumulative_energy_imported = device:get_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} - cumulative_energy_imported[endpoint_id] = cumulative_energy_imported[endpoint_id] or 0 - cumulative_energy_imported[endpoint_id] = cumulative_energy_imported[endpoint_id] + energy_imported_Wh - device:set_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP, cumulative_energy_imported, { persist = true }) - local total_cumulative_energy_imported = get_total_cumulative_energy_imported(device) - device:emit_component_event(device.profile.components["main"], ib.endpoint_id, capabilities.energyMeter.energy({value = total_cumulative_energy_imported, unit = "Wh"})) - report_power_consumption_to_st_energy(device, total_cumulative_energy_imported) - end +function ThermostatLifecycleHandlers.do_configure(driver, device) + local device_cfg = require "thermostat_utils.device_configuration" + device_cfg.match_profile(device) end -local function cumulative_energy_imported_handler(driver, device, ib, response) - if ib.data then - if version.api < 11 then - clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:augment_type(ib.data) - end - local endpoint_id = string.format(ib.endpoint_id) - local cumulative_energy_imported = device:get_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} - local cumulative_energy_imported_Wh = utils.round( ib.data.elements.energy.value / 1000) -- convert mWh to Wh - cumulative_energy_imported[endpoint_id] = cumulative_energy_imported_Wh - device:set_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP, cumulative_energy_imported, { persist = true }) - local total_cumulative_energy_imported = get_total_cumulative_energy_imported(device) - device:emit_component_event(device.profile.components["main"], capabilities.energyMeter.energy({ value = total_cumulative_energy_imported, unit = "Wh" })) - report_power_consumption_to_st_energy(device, total_cumulative_energy_imported) - end +function ThermostatLifecycleHandlers.driver_switched(driver, device) + local device_cfg = require "thermostat_utils.device_configuration" + device_cfg.match_profile(device) end -local function energy_report_handler_factory(is_cumulative_report) - return function(driver, device, ib, response) - if is_cumulative_report then - cumulative_energy_imported_handler(driver, device, ib, response) - elseif device:get_field(CUMULATIVE_REPORTS_NOT_SUPPORTED) then - periodic_energy_imported_handler(driver, device, ib, response) - end +function ThermostatLifecycleHandlers.device_init(driver, device) + if device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) and (version.api < 15 or version.rpc < 9) then + -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id + -- library function to utilize optional capabilities + device:extend_device("supports_capability_by_id", thermostat_utils.supports_capability_by_id_modular) end -end - -local function water_heater_supported_modes_attr_handler(driver, device, ib, response) - local supportWaterHeaterModes = {} - local supportWaterHeaterModesWithIdx = {} - for _, mode in ipairs(ib.data.elements) do - if version.api < 13 then - clusters.WaterHeaterMode.types.ModeOptionStruct:augment_type(mode) + device:subscribe() + device:set_component_to_endpoint_fn(thermostat_utils.component_to_endpoint) + device:set_endpoint_to_component_fn(thermostat_utils.endpoint_to_component) + if not device:get_field(fields.setpoint_limit_device_field.MIN_SETPOINT_DEADBAND_CHECKED) then + local auto_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}) + --Query min setpoint deadband if needed + if #auto_eps ~= 0 and device:get_field(fields.setpoint_limit_device_field.MIN_DEADBAND) == nil then + device:send(clusters.Thermostat.attributes.MinSetpointDeadBand:read()) end - table.insert(supportWaterHeaterModes, mode.elements.label.value) - table.insert(supportWaterHeaterModesWithIdx, {mode.elements.mode.value, mode.elements.label.value}) end - device:set_field(SUPPORTED_WATER_HEATER_MODES_WITH_IDX, supportWaterHeaterModesWithIdx, { persist = true }) - local event = capabilities.mode.supportedModes(supportWaterHeaterModes, { visibility = { displayed = false } }) - device:emit_event_for_endpoint(ib.endpoint_id, event) - event = capabilities.mode.supportedArguments(supportWaterHeaterModes, { visibility = { displayed = false } }) - device:emit_event_for_endpoint(ib.endpoint_id, event) -end -local function water_heater_mode_handler(driver, device, ib, response) - device.log.info(string.format("water_heater_mode_handler mode: %s", ib.data.value)) - local supportWaterHeaterModesWithIdx = device:get_field(SUPPORTED_WATER_HEATER_MODES_WITH_IDX) or {} - local currentMode = ib.data.value - for i, mode in ipairs(supportWaterHeaterModesWithIdx) do - if mode[1] == currentMode then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.mode.mode(mode[2])) - break - end + -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. + -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. + local electrical_energy_measurement_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) + if #electrical_energy_measurement_eps > 0 then + local cumulative_energy_eps = embedded_cluster_utils.get_endpoints( + device, + clusters.ElectricalEnergyMeasurement.ID, + {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY} + ) + if #cumulative_energy_eps == 0 then device:set_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = false}) end end end -local function battery_charge_level_attr_handler(driver, device, ib, response) - if ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.OK then - device:emit_event(capabilities.batteryLevel.battery.normal()) - elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.WARNING then - device:emit_event(capabilities.batteryLevel.battery.warning()) - elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.CRITICAL then - device:emit_event(capabilities.batteryLevel.battery.critical()) +function ThermostatLifecycleHandlers.info_changed(driver, device, event, args) + if device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) then + -- This indicates the device should be using a modular profile, so + -- re-up subscription with new capabilities using the modular supports_capability override + device:extend_device("supports_capability_by_id", thermostat_utils.supports_capability_by_id_modular) end -end -local function power_source_attribute_list_handler(driver, device, ib, response) - for _, attr in ipairs(ib.data.elements) do - -- mark if the device if BatPercentRemaining (Attribute ID 0x0C) or - -- BatChargeLevel (Attribute ID 0x0E) is present and try profiling. - if attr.value == 0x0C then - device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_PERCENTAGE) - match_profile(driver, device) - return - elseif attr.value == 0x0E then - device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_LEVEL) - match_profile(driver, device) - return - end + if device.profile.id ~= args.old_st_store.profile.id then + device:subscribe() end end -local function thermostat_attribute_list_handler(driver, device, ib, response) - for _, attr in ipairs(ib.data.elements) do - -- mark whether the optional attribute ThermostatRunningState (0x029) is present and try profiling - if attr.value == 0x029 then - device:set_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, true) - match_profile(driver, device) - return - end - end - device:set_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, false) - match_profile(driver, device) +function ThermostatLifecycleHandlers.device_removed(driver, device) + device.log.info("device removed") end local matter_driver_template = { lifecycle_handlers = { - init = device_init, - added = device_added, - doConfigure = do_configure, - infoChanged = info_changed, - removed = device_removed, - driverSwitched = driver_switched + added = ThermostatLifecycleHandlers.device_added, + doConfigure = ThermostatLifecycleHandlers.do_configure, + driverSwitched = ThermostatLifecycleHandlers.driver_switched, + infoChanged = ThermostatLifecycleHandlers.info_changed, + init = ThermostatLifecycleHandlers.device_init, + removed = ThermostatLifecycleHandlers.device_removed, }, matter_handlers = { attr = { - [clusters.OnOff.ID] = { - [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, + [clusters.ActivatedCarbonFilterMonitoring.ID] = { + [clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.ID] = attribute_handlers.activated_carbon_filter_change_indication_handler, + [clusters.ActivatedCarbonFilterMonitoring.attributes.Condition.ID] = attribute_handlers.activated_carbon_filter_condition_handler, }, - [clusters.Thermostat.ID] = { - [clusters.Thermostat.attributes.LocalTemperature.ID] = temp_event_handler(capabilities.temperatureMeasurement.temperature), - [clusters.Thermostat.attributes.OccupiedCoolingSetpoint.ID] = temp_event_handler(capabilities.thermostatCoolingSetpoint.coolingSetpoint), - [clusters.Thermostat.attributes.OccupiedHeatingSetpoint.ID] = temp_event_handler(capabilities.thermostatHeatingSetpoint.heatingSetpoint), - [clusters.Thermostat.attributes.SystemMode.ID] = system_mode_handler, - [clusters.Thermostat.attributes.ThermostatRunningState.ID] = running_state_handler, - [clusters.Thermostat.attributes.ControlSequenceOfOperation.ID] = sequence_of_operation_handler, - [clusters.Thermostat.attributes.AbsMinHeatSetpointLimit.ID] = heating_setpoint_limit_handler_factory(setpoint_limit_device_field.MIN_HEAT), - [clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit.ID] = heating_setpoint_limit_handler_factory(setpoint_limit_device_field.MAX_HEAT), - [clusters.Thermostat.attributes.AbsMinCoolSetpointLimit.ID] = cooling_setpoint_limit_handler_factory(setpoint_limit_device_field.MIN_COOL), - [clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit.ID] = cooling_setpoint_limit_handler_factory(setpoint_limit_device_field.MAX_COOL), - [clusters.Thermostat.attributes.MinSetpointDeadBand.ID] = min_deadband_limit_handler, - [clusters.Thermostat.attributes.AttributeList.ID] = thermostat_attribute_list_handler, + [clusters.AirQuality.ID] = { + [clusters.AirQuality.attributes.AirQuality.ID] = attribute_handlers.air_quality_handler, + }, + [clusters.ElectricalEnergyMeasurement.ID] = { + [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = attribute_handlers.energy_imported_factory(true), + [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = attribute_handlers.energy_imported_factory(false), + }, + [clusters.ElectricalPowerMeasurement.ID] = { + [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = attribute_handlers.active_power_handler }, [clusters.FanControl.ID] = { - [clusters.FanControl.attributes.FanModeSequence.ID] = fan_mode_sequence_handler, - [clusters.FanControl.attributes.FanMode.ID] = fan_mode_handler, - [clusters.FanControl.attributes.PercentCurrent.ID] = fan_speed_percent_attr_handler, - [clusters.FanControl.attributes.WindSupport.ID] = wind_support_handler, - [clusters.FanControl.attributes.WindSetting.ID] = wind_setting_handler, - [clusters.FanControl.attributes.RockSupport.ID] = rock_support_handler, - [clusters.FanControl.attributes.RockSetting.ID] = rock_setting_handler, + [clusters.FanControl.attributes.FanMode.ID] = attribute_handlers.fan_mode_handler, + [clusters.FanControl.attributes.FanModeSequence.ID] = attribute_handlers.fan_mode_sequence_handler, + [clusters.FanControl.attributes.PercentCurrent.ID] = attribute_handlers.percent_current_handler, + [clusters.FanControl.attributes.RockSetting.ID] = attribute_handlers.rock_setting_handler, + [clusters.FanControl.attributes.RockSupport.ID] = attribute_handlers.rock_support_handler, + [clusters.FanControl.attributes.WindSetting.ID] = attribute_handlers.wind_setting_handler, + [clusters.FanControl.attributes.WindSupport.ID] = attribute_handlers.wind_support_handler, }, - [clusters.TemperatureMeasurement.ID] = { - [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = temp_event_handler(capabilities.temperatureMeasurement.temperature), - [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = temp_attr_handler_factory(setpoint_limit_device_field.MIN_TEMP), - [clusters.TemperatureMeasurement.attributes.MaxMeasuredValue.ID] = temp_attr_handler_factory(setpoint_limit_device_field.MAX_TEMP), + [clusters.HepaFilterMonitoring.ID] = { + [clusters.HepaFilterMonitoring.attributes.ChangeIndication.ID] = attribute_handlers.hepa_filter_change_indication_handler, + [clusters.HepaFilterMonitoring.attributes.Condition.ID] = attribute_handlers.hepa_filter_condition_handler, }, - [clusters.RelativeHumidityMeasurement.ID] = { - [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = humidity_attr_handler + [clusters.OnOff.ID] = { + [clusters.OnOff.attributes.OnOff.ID] = attribute_handlers.on_off_handler, }, [clusters.PowerSource.ID] = { - [clusters.PowerSource.attributes.AttributeList.ID] = power_source_attribute_list_handler, - [clusters.PowerSource.attributes.BatChargeLevel.ID] = battery_charge_level_attr_handler, - [clusters.PowerSource.attributes.BatPercentRemaining.ID] = battery_percent_remaining_attr_handler, + [clusters.PowerSource.attributes.AttributeList.ID] = attribute_handlers.power_source_attribute_list_handler, + [clusters.PowerSource.attributes.BatChargeLevel.ID] = attribute_handlers.bat_charge_level_handler, + [clusters.PowerSource.attributes.BatPercentRemaining.ID] = attribute_handlers.bat_percent_remaining_handler, }, - [clusters.HepaFilterMonitoring.ID] = { - [clusters.HepaFilterMonitoring.attributes.Condition.ID] = hepa_filter_condition_handler, - [clusters.HepaFilterMonitoring.attributes.ChangeIndication.ID] = hepa_filter_change_indication_handler + [clusters.RelativeHumidityMeasurement.ID] = { + [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.relative_humidity_measured_value_handler }, - [clusters.ActivatedCarbonFilterMonitoring.ID] = { - [clusters.ActivatedCarbonFilterMonitoring.attributes.Condition.ID] = activated_carbon_filter_condition_handler, - [clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.ID] = activated_carbon_filter_change_indication_handler + [clusters.TemperatureMeasurement.ID] = { + [clusters.TemperatureMeasurement.attributes.MaxMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.setpoint_limit_device_field.MAX_TEMP), + [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.temperature_handler_factory(capabilities.temperatureMeasurement.temperature), + [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.setpoint_limit_device_field.MIN_TEMP), }, - [clusters.AirQuality.ID] = { - [clusters.AirQuality.attributes.AirQuality.ID] = air_quality_attr_handler, + [clusters.Thermostat.ID] = { + [clusters.Thermostat.attributes.AttributeList.ID] = attribute_handlers.thermostat_attribute_list_handler, + [clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit.ID] = attribute_handlers.abs_cool_setpoint_limit_factory(fields.setpoint_limit_device_field.MAX_COOL), + [clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit.ID] = attribute_handlers.abs_heat_setpoint_limit_factory(fields.setpoint_limit_device_field.MAX_HEAT), + [clusters.Thermostat.attributes.AbsMinCoolSetpointLimit.ID] = attribute_handlers.abs_cool_setpoint_limit_factory(fields.setpoint_limit_device_field.MIN_COOL), + [clusters.Thermostat.attributes.AbsMinHeatSetpointLimit.ID] = attribute_handlers.abs_heat_setpoint_limit_factory(fields.setpoint_limit_device_field.MIN_HEAT), + [clusters.Thermostat.attributes.ControlSequenceOfOperation.ID] = attribute_handlers.control_sequence_of_operation_handler, + [clusters.Thermostat.attributes.LocalTemperature.ID] = attribute_handlers.temperature_handler_factory(capabilities.temperatureMeasurement.temperature), + [clusters.Thermostat.attributes.MinSetpointDeadBand.ID] = attribute_handlers.min_setpoint_deadband_handler, + [clusters.Thermostat.attributes.OccupiedCoolingSetpoint.ID] = attribute_handlers.temperature_handler_factory(capabilities.thermostatCoolingSetpoint.coolingSetpoint), + [clusters.Thermostat.attributes.OccupiedHeatingSetpoint.ID] = attribute_handlers.temperature_handler_factory(capabilities.thermostatHeatingSetpoint.heatingSetpoint), + [clusters.Thermostat.attributes.SystemMode.ID] = attribute_handlers.system_mode_handler, + [clusters.Thermostat.attributes.ThermostatRunningState.ID] = attribute_handlers.thermostat_running_state_handler, }, - [clusters.CarbonMonoxideConcentrationMeasurement.ID] = { - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.carbonMonoxideMeasurement.NAME, capabilities.carbonMonoxideMeasurement.carbonMonoxideLevel, units.PPM), - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.carbonMonoxideMeasurement.NAME), - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.carbonMonoxideHealthConcern.carbonMonoxideHealthConcern), + [clusters.WaterHeaterMode.ID] = { + [clusters.WaterHeaterMode.attributes.CurrentMode.ID] = attribute_handlers.water_heater_current_mode_handler, + [clusters.WaterHeaterMode.attributes.SupportedModes.ID] = attribute_handlers.water_heater_supported_modes_handler }, + -- CONCENTRATION MEASUREMENT CLUSTERS -- [clusters.CarbonDioxideConcentrationMeasurement.ID] = { - [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.carbonDioxideMeasurement.NAME, capabilities.carbonDioxideMeasurement.carbonDioxide, units.PPM), - [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.carbonDioxideMeasurement.NAME), - [clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern), + [clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern), + [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.carbonDioxideMeasurement.NAME, capabilities.carbonDioxideMeasurement.carbonDioxide, fields.units.PPM), + [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.carbonDioxideMeasurement.NAME), + }, + [clusters.CarbonMonoxideConcentrationMeasurement.ID] = { + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.carbonMonoxideHealthConcern.carbonMonoxideHealthConcern), + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.carbonMonoxideMeasurement.NAME, capabilities.carbonMonoxideMeasurement.carbonMonoxideLevel, fields.units.PPM), + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.carbonMonoxideMeasurement.NAME), + }, + [clusters.FormaldehydeConcentrationMeasurement.ID] = { + [clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.formaldehydeHealthConcern.formaldehydeHealthConcern), + [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.formaldehydeMeasurement.NAME, capabilities.formaldehydeMeasurement.formaldehydeLevel, fields.units.PPM), + [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.formaldehydeMeasurement.NAME), }, [clusters.NitrogenDioxideConcentrationMeasurement.ID] = { - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.nitrogenDioxideMeasurement.NAME, capabilities.nitrogenDioxideMeasurement.nitrogenDioxide, units.PPM), - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.nitrogenDioxideMeasurement.NAME), - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.nitrogenDioxideHealthConcern.nitrogenDioxideHealthConcern) + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.nitrogenDioxideHealthConcern.nitrogenDioxideHealthConcern), + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.nitrogenDioxideMeasurement.NAME, capabilities.nitrogenDioxideMeasurement.nitrogenDioxide, fields.units.PPM), + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.nitrogenDioxideMeasurement.NAME), }, [clusters.OzoneConcentrationMeasurement.ID] = { - [clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.ozoneMeasurement.NAME, capabilities.ozoneMeasurement.ozone, units.PPM), - [clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.ozoneMeasurement.NAME), - [clusters.OzoneConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.ozoneHealthConcern.ozoneHealthConcern) - }, - [clusters.FormaldehydeConcentrationMeasurement.ID] = { - [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.formaldehydeMeasurement.NAME, capabilities.formaldehydeMeasurement.formaldehydeLevel, units.PPM), - [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.formaldehydeMeasurement.NAME), - [clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.formaldehydeHealthConcern.formaldehydeHealthConcern), + [clusters.OzoneConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.ozoneHealthConcern.ozoneHealthConcern), + [clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.ozoneMeasurement.NAME, capabilities.ozoneMeasurement.ozone, fields.units.PPM), + [clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.ozoneMeasurement.NAME), }, [clusters.Pm1ConcentrationMeasurement.ID] = { - [clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.veryFineDustSensor.NAME, capabilities.veryFineDustSensor.veryFineDustLevel, units.UGM3), - [clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.veryFineDustSensor.NAME), - [clusters.Pm1ConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern), - }, - [clusters.Pm25ConcentrationMeasurement.ID] = { - [clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.fineDustSensor.NAME, capabilities.fineDustSensor.fineDustLevel, units.UGM3), - [clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.fineDustSensor.NAME), - [clusters.Pm25ConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.fineDustHealthConcern.fineDustHealthConcern), + [clusters.Pm1ConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern), + [clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.veryFineDustSensor.NAME, capabilities.veryFineDustSensor.veryFineDustLevel, fields.units.UGM3), + [clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.veryFineDustSensor.NAME), }, [clusters.Pm10ConcentrationMeasurement.ID] = { - [clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.dustSensor.NAME, capabilities.dustSensor.dustLevel, units.UGM3), - [clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.dustSensor.NAME), - [clusters.Pm10ConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.dustHealthConcern.dustHealthConcern), + [clusters.Pm10ConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.dustHealthConcern.dustHealthConcern), + [clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.dustSensor.NAME, capabilities.dustSensor.dustLevel, fields.units.UGM3), + [clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.dustSensor.NAME), + }, + [clusters.Pm25ConcentrationMeasurement.ID] = { + [clusters.Pm25ConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.fineDustHealthConcern.fineDustHealthConcern), + [clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.fineDustSensor.NAME, capabilities.fineDustSensor.fineDustLevel, fields.units.UGM3), + [clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.fineDustSensor.NAME), }, [clusters.RadonConcentrationMeasurement.ID] = { - [clusters.RadonConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.radonMeasurement.NAME, capabilities.radonMeasurement.radonLevel, units.PCIL), - [clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.radonMeasurement.NAME), - [clusters.RadonConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.radonHealthConcern.radonHealthConcern) + [clusters.RadonConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.radonHealthConcern.radonHealthConcern), + [clusters.RadonConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.radonMeasurement.NAME, capabilities.radonMeasurement.radonLevel, fields.units.PCIL), + [clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.radonMeasurement.NAME), }, [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.ID] = { - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.tvocMeasurement.NAME, capabilities.tvocMeasurement.tvocLevel, units.PPB), - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.tvocMeasurement.NAME), - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.tvocHealthConcern.tvocHealthConcern) - }, - [clusters.ElectricalPowerMeasurement.ID] = { - [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = active_power_handler - }, - [clusters.ElectricalEnergyMeasurement.ID] = { - [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = energy_report_handler_factory(true), - [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = energy_report_handler_factory(false), - }, - [clusters.WaterHeaterMode.ID] = { - [clusters.WaterHeaterMode.attributes.CurrentMode.ID] = water_heater_mode_handler, - [clusters.WaterHeaterMode.attributes.SupportedModes.ID] = water_heater_supported_modes_attr_handler + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.tvocHealthConcern.tvocHealthConcern), + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.tvocMeasurement.NAME, capabilities.tvocMeasurement.tvocLevel, fields.units.PPB), + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.tvocMeasurement.NAME), }, }, }, - subscribed_attributes = subscribed_attributes, capability_handlers = { - [capabilities.switch.ID] = { - [capabilities.switch.commands.on.NAME] = handle_switch_on, - [capabilities.switch.commands.off.NAME] = handle_switch_off, + [capabilities.airConditionerFanMode.ID] = { + [capabilities.airConditionerFanMode.commands.setFanMode.NAME] = capability_handlers.fan_mode_command_factory(capabilities.airConditionerFanMode) }, - [capabilities.thermostatMode.ID] = { - [capabilities.thermostatMode.commands.setThermostatMode.NAME] = set_thermostat_mode, - [capabilities.thermostatMode.commands.auto.NAME] = thermostat_mode_setter(capabilities.thermostatMode.thermostatMode.auto.NAME), - [capabilities.thermostatMode.commands.off.NAME] = thermostat_mode_setter(capabilities.thermostatMode.thermostatMode.off.NAME), - [capabilities.thermostatMode.commands.cool.NAME] = thermostat_mode_setter(capabilities.thermostatMode.thermostatMode.cool.NAME), - [capabilities.thermostatMode.commands.heat.NAME] = thermostat_mode_setter(capabilities.thermostatMode.thermostatMode.heat.NAME), - [capabilities.thermostatMode.commands.emergencyHeat.NAME] = thermostat_mode_setter(capabilities.thermostatMode.thermostatMode.emergency_heat.NAME) + [capabilities.airPurifierFanMode.ID] = { + [capabilities.airPurifierFanMode.commands.setAirPurifierFanMode.NAME] = capability_handlers.fan_mode_command_factory(capabilities.airPurifierFanMode) }, - [capabilities.thermostatFanMode.ID] = { - [capabilities.thermostatFanMode.commands.setThermostatFanMode.NAME] = set_fan_mode_factory(capabilities.thermostatFanMode), - [capabilities.thermostatFanMode.commands.fanAuto.NAME] = thermostat_fan_mode_setter(capabilities.thermostatFanMode.thermostatFanMode.auto.NAME), - [capabilities.thermostatFanMode.commands.fanOn.NAME] = thermostat_fan_mode_setter(capabilities.thermostatFanMode.thermostatFanMode.on.NAME) + [capabilities.fanMode.ID] = { + [capabilities.fanMode.commands.setFanMode.NAME] = capability_handlers.fan_mode_command_factory(capabilities.fanMode) + }, + [capabilities.fanOscillationMode.ID] = { + [capabilities.fanOscillationMode.commands.setFanOscillationMode.NAME] = capability_handlers.handle_set_fan_oscillation_mode, + }, + [capabilities.fanSpeedPercent.ID] = { + [capabilities.fanSpeedPercent.commands.setPercent.NAME] = capability_handlers.handle_fan_speed_set_percent, + }, + [capabilities.filterState.ID] = { + [capabilities.filterState.commands.resetFilter.NAME] = capability_handlers.handle_filter_state_reset_filter, + }, + [capabilities.mode.ID] = { + [capabilities.mode.commands.setMode.NAME] = capability_handlers.handle_set_mode, + }, + [capabilities.switch.ID] = { + [capabilities.switch.commands.off.NAME] = capability_handlers.handle_switch_off, + [capabilities.switch.commands.on.NAME] = capability_handlers.handle_switch_on, }, [capabilities.thermostatCoolingSetpoint.ID] = { - [capabilities.thermostatCoolingSetpoint.commands.setCoolingSetpoint.NAME] = set_setpoint(clusters.Thermostat.attributes.OccupiedCoolingSetpoint) + [capabilities.thermostatCoolingSetpoint.commands.setCoolingSetpoint.NAME] = capability_handlers.thermostat_set_setpoint_factory(clusters.Thermostat.attributes.OccupiedCoolingSetpoint) }, [capabilities.thermostatHeatingSetpoint.ID] = { - [capabilities.thermostatHeatingSetpoint.commands.setHeatingSetpoint.NAME] = set_setpoint(clusters.Thermostat.attributes.OccupiedHeatingSetpoint) + [capabilities.thermostatHeatingSetpoint.commands.setHeatingSetpoint.NAME] = capability_handlers.thermostat_set_setpoint_factory(clusters.Thermostat.attributes.OccupiedHeatingSetpoint) + }, + [capabilities.thermostatFanMode.ID] = { + [capabilities.thermostatFanMode.commands.fanAuto.NAME] = capability_handlers.thermostat_fan_mode_command_factory(capabilities.thermostatFanMode.thermostatFanMode.auto.NAME), + [capabilities.thermostatFanMode.commands.fanOn.NAME] = capability_handlers.thermostat_fan_mode_command_factory(capabilities.thermostatFanMode.thermostatFanMode.on.NAME), + [capabilities.thermostatFanMode.commands.setThermostatFanMode.NAME] = capability_handlers.fan_mode_command_factory(capabilities.thermostatFanMode), + }, + [capabilities.thermostatMode.ID] = { + [capabilities.thermostatMode.commands.auto.NAME] = capability_handlers.thermostat_mode_command_factory(capabilities.thermostatMode.thermostatMode.auto.NAME), + [capabilities.thermostatMode.commands.cool.NAME] = capability_handlers.thermostat_mode_command_factory(capabilities.thermostatMode.thermostatMode.cool.NAME), + [capabilities.thermostatMode.commands.emergencyHeat.NAME] = capability_handlers.thermostat_mode_command_factory(capabilities.thermostatMode.thermostatMode.emergency_heat.NAME), + [capabilities.thermostatMode.commands.heat.NAME] = capability_handlers.thermostat_mode_command_factory(capabilities.thermostatMode.thermostatMode.heat.NAME), + [capabilities.thermostatMode.commands.off.NAME] = capability_handlers.thermostat_mode_command_factory(capabilities.thermostatMode.thermostatMode.off.NAME), + [capabilities.thermostatMode.commands.setThermostatMode.NAME] = capability_handlers.handle_set_thermostat_mode, + }, + [capabilities.windMode.ID] = { + [capabilities.windMode.commands.setWindMode.NAME] = capability_handlers.handle_set_wind_mode, }, + }, + subscribed_attributes = { [capabilities.airConditionerFanMode.ID] = { - [capabilities.airConditionerFanMode.commands.setFanMode.NAME] = set_fan_mode_factory(capabilities.airConditionerFanMode) + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode }, [capabilities.airPurifierFanMode.ID] = { - [capabilities.airPurifierFanMode.commands.setAirPurifierFanMode.NAME] = set_fan_mode_factory(capabilities.airPurifierFanMode) + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode + }, + [capabilities.battery.ID] = { + clusters.PowerSource.attributes.BatPercentRemaining + }, + [capabilities.batteryLevel.ID] = { + clusters.PowerSource.attributes.BatChargeLevel + }, + [capabilities.energyMeter.ID] = { + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported }, [capabilities.fanMode.ID] = { - [capabilities.fanMode.commands.setFanMode.NAME] = set_fan_mode_factory(capabilities.fanMode) + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode + }, + [capabilities.fanOscillationMode.ID] = { + clusters.FanControl.attributes.RockSupport, + clusters.FanControl.attributes.RockSetting }, [capabilities.fanSpeedPercent.ID] = { - [capabilities.fanSpeedPercent.commands.setPercent.NAME] = set_fan_speed_percent, + clusters.FanControl.attributes.PercentCurrent }, - [capabilities.windMode.ID] = { - [capabilities.windMode.commands.setWindMode.NAME] = set_wind_mode, + [capabilities.filterState.ID] = { + clusters.HepaFilterMonitoring.attributes.Condition, + clusters.ActivatedCarbonFilterMonitoring.attributes.Condition }, - [capabilities.fanOscillationMode.ID] = { - [capabilities.fanOscillationMode.commands.setFanOscillationMode.NAME] = set_rock_mode, + [capabilities.filterStatus.ID] = { + clusters.HepaFilterMonitoring.attributes.ChangeIndication, + clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication }, [capabilities.mode.ID] = { - [capabilities.mode.commands.setMode.NAME] = set_water_heater_mode, + clusters.WaterHeaterMode.attributes.CurrentMode, + clusters.WaterHeaterMode.attributes.SupportedModes + }, + [capabilities.powerConsumptionReport.ID] = { + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported + }, + [capabilities.powerMeter.ID] = { + clusters.ElectricalPowerMeasurement.attributes.ActivePower + }, + [capabilities.relativeHumidityMeasurement.ID] = { + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue + }, + [capabilities.switch.ID] = { + clusters.OnOff.attributes.OnOff + }, + [capabilities.temperatureMeasurement.ID] = { + clusters.Thermostat.attributes.LocalTemperature, + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue + }, + [capabilities.thermostatCoolingSetpoint.ID] = { + clusters.Thermostat.attributes.OccupiedCoolingSetpoint, + clusters.Thermostat.attributes.AbsMinCoolSetpointLimit, + clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit + }, + [capabilities.thermostatFanMode.ID] = { + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode + }, + [capabilities.thermostatHeatingSetpoint.ID] = { + clusters.Thermostat.attributes.OccupiedHeatingSetpoint, + clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, + clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit + }, + [capabilities.thermostatMode.ID] = { + clusters.Thermostat.attributes.SystemMode, + clusters.Thermostat.attributes.ControlSequenceOfOperation + }, + [capabilities.thermostatOperatingState.ID] = { + clusters.Thermostat.attributes.ThermostatRunningState + }, + [capabilities.windMode.ID] = { + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.WindSetting + }, + -- AIR QUALITY SENSOR DEVICE TYPE SPECIFIC CAPABILITIES -- + [capabilities.airQualityHealthConcern.ID] = { + clusters.AirQuality.attributes.AirQuality + }, + [capabilities.atmosphericPressureMeasurement.ID] = { + clusters.PressureMeasurement.attributes.MeasuredValue + }, + [capabilities.carbonDioxideHealthConcern.ID] = { + clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.carbonDioxideMeasurement.ID] = { + clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue, + clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.carbonMonoxideHealthConcern.ID] = { + clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.carbonMonoxideMeasurement.ID] = { + clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, + clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.dustHealthConcern.ID] = { + clusters.Pm10ConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.dustSensor.ID] = { + clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, + clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.fineDustHealthConcern.ID] = { + clusters.Pm25ConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.fineDustSensor.ID] = { + clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.formaldehydeHealthConcern.ID] = { + clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.formaldehydeMeasurement.ID] = { + clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue, + clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.nitrogenDioxideHealthConcern.ID] = { + clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.nitrogenDioxideMeasurement.ID] = { + clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue, + clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit + }, + [capabilities.ozoneHealthConcern.ID] = { + clusters.OzoneConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.ozoneMeasurement.ID] = { + clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue, + clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit + }, + [capabilities.radonHealthConcern.ID] = { + clusters.RadonConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.radonMeasurement.ID] = { + clusters.RadonConcentrationMeasurement.attributes.MeasuredValue, + clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.relativeHumidityMeasurement.ID] = { + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue + }, + [capabilities.tvocHealthConcern.ID] = { + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue + }, + [capabilities.tvocMeasurement.ID] = { + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue, + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.veryFineDustHealthConcern.ID] = { + clusters.Pm1ConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.veryFineDustSensor.ID] = { + clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.filterState.ID] = { - [capabilities.filterState.commands.resetFilter.NAME] = reset_filter_state, - } }, supported_capabilities = { - capabilities.thermostatMode, - capabilities.thermostatHeatingSetpoint, - capabilities.thermostatCoolingSetpoint, - capabilities.thermostatFanMode, - capabilities.thermostatOperatingState, capabilities.airConditionerFanMode, - capabilities.fanMode, - capabilities.fanSpeedPercent, + capabilities.airQualityHealthConcern, capabilities.airPurifierFanMode, - capabilities.windMode, - capabilities.fanOscillationMode, capabilities.battery, capabilities.batteryLevel, - capabilities.filterState, - capabilities.filterStatus, - capabilities.airQualityHealthConcern, capabilities.carbonDioxideHealthConcern, capabilities.carbonDioxideMeasurement, capabilities.carbonMonoxideHealthConcern, capabilities.carbonMonoxideMeasurement, + capabilities.dustHealthConcern, + capabilities.dustSensor, + capabilities.energyMeter, + capabilities.fanMode, + capabilities.fanOscillationMode, + capabilities.fanSpeedPercent, + capabilities.filterState, + capabilities.filterStatus, + capabilities.fineDustHealthConcern, + capabilities.fineDustSensor, + capabilities.formaldehydeHealthConcern, + capabilities.formaldehydeMeasurement, + capabilities.mode, capabilities.nitrogenDioxideHealthConcern, capabilities.nitrogenDioxideMeasurement, capabilities.ozoneHealthConcern, capabilities.ozoneMeasurement, - capabilities.formaldehydeHealthConcern, - capabilities.formaldehydeMeasurement, - capabilities.veryFineDustHealthConcern, - capabilities.veryFineDustSensor, - capabilities.fineDustHealthConcern, - capabilities.fineDustSensor, - capabilities.dustSensor, - capabilities.dustHealthConcern, + capabilities.powerConsumptionReport, + capabilities.powerMeter, capabilities.radonHealthConcern, capabilities.radonMeasurement, + capabilities.thermostatCoolingSetpoint, + capabilities.thermostatFanMode, + capabilities.thermostatHeatingSetpoint, + capabilities.thermostatMode, + capabilities.thermostatOperatingState, capabilities.tvocHealthConcern, capabilities.tvocMeasurement, - capabilities.powerMeter, - capabilities.energyMeter, - capabilities.powerConsumptionReport, - capabilities.mode + capabilities.veryFineDustHealthConcern, + capabilities.veryFineDustSensor, + capabilities.windMode, }, } diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua index d9cb8c6309..ca7fddcdd9 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua @@ -1,16 +1,6 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(0) local capabilities = require "st.capabilities" @@ -20,19 +10,19 @@ local clusters = require "st.matter.clusters" local version = require "version" if version.api < 10 then - clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" - clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" end local mock_device = test.mock_device.build_test_matter_device({ diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua index 871693d5a6..fd68c958d9 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua @@ -1,16 +1,6 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -21,19 +11,19 @@ local version = require "version" version.api = 9 -- include driver-side cluster definitions to test embedded clusters on lower api versions -clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" -clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" -clusters.AirQuality = require "AirQuality" -clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" -clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" -clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" -clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" -clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" -clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" -clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" -clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" -clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" -clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" +clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" +clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" +clusters.AirQuality = require "embedded_clusters.AirQuality" +clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" +clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" +clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" +clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" +clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" +clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" +clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" +clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" +clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" +clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("air-purifier-hepa-ac-wind.yml"), diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua index 4f2b5bd0f4..745076cd04 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua @@ -1,16 +1,5 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -23,19 +12,19 @@ local version = require "version" test.disable_startup_messages() if version.api < 10 then - clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" - clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" end local mock_device_basic = test.mock_device.build_test_matter_device({ @@ -315,10 +304,6 @@ test.register_coroutine_test( "Test profile change on init for basic Air Purifier device", function() test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "doConfigure" }) - test.socket.matter:__queue_receive({ - mock_device_basic.id, - clusters.Thermostat.attributes.AttributeList:build_test_report_data(mock_device_basic, 1, {uint32(0)}) - }) mock_device_basic:expect_metadata_update(expected_update_metadata) mock_device_basic:expect_metadata_update({ provisioning_state = "PROVISIONED" }) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua index 4a94a9a7e9..8eefceb23b 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua @@ -1,16 +1,6 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_heat_pump.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_heat_pump.lua index ba097d0451..e96bfb69fc 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_heat_pump.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_heat_pump.lua @@ -1,16 +1,5 @@ --- Copyright 2024 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.matter.clusters" @@ -27,8 +16,8 @@ local HEAT_PUMP_DEVICE_TYPE_ID = 0x0309 local THERMOSTAT_DEVICE_TYPE_ID = 0x0301 if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" end local device_desc = { diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac.lua index cec0cb9ffc..35728e628d 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac.lua @@ -1,16 +1,6 @@ --- Copyright 2024 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(0) local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac_modular.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac_modular.lua index 2da9e5c023..96577d72a3 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac_modular.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac_modular.lua @@ -1,16 +1,6 @@ --- Copyright 2024 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_battery.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_battery.lua index 35f312c67e..9fb8536c23 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_battery.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_battery.lua @@ -1,16 +1,5 @@ --- Copyright 2023 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_featuremap.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_featuremap.lua index 37f73d0e50..c898fab362 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_featuremap.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_featuremap.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua index 7f9577fa8f..a8acdc253e 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua @@ -1,16 +1,5 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits.lua index b28d11dcc7..ae9c25940f 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits.lua @@ -1,16 +1,5 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits_rpc.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits_rpc.lua index 72b46af4f3..bbd084cb72 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits_rpc.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits_rpc.lua @@ -1,15 +1,5 @@ --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua index 44ed395797..e40572ff5d 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_composed_bridged.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_composed_bridged.lua index e901ac59a4..1f1a4e6cb4 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_composed_bridged.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_composed_bridged.lua @@ -1,16 +1,5 @@ --- Copyright 2023 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua index 6645d39692..73d180da7e 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua @@ -1,16 +1,5 @@ --- Copyright 2025 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_rpc5.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_rpc5.lua index b52b08027e..03e7df8ae2 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_rpc5.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_rpc5.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.matter.clusters" local test = require "integration_test" diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_water_heater.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_water_heater.lua index cbd6464a48..b39db4136b 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_water_heater.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_water_heater.lua @@ -1,16 +1,5 @@ --- Copyright 2022 SmartThings --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -19,11 +8,12 @@ local version = require "version" local clusters = require "st.matter.clusters" if version.api < 13 then - clusters.WaterHeaterMode = require "WaterHeaterMode" + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" end if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" end local WATER_HEATER_EP = 10 diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua new file mode 100644 index 0000000000..78f5ee3bf2 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua @@ -0,0 +1,661 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local log = require "log" +local version = require "version" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local st_utils = require "st.utils" +local fields = require "thermostat_utils.fields" +local thermostat_utils = require "thermostat_utils.utils" + +if version.api < 10 then + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" +end + +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" +end + +if version.api < 13 then + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" +end + +local AttributeHandlers = {} + + +-- [[ THERMOSTAT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.thermostat_attribute_list_handler(driver, device, ib, response) + local device_cfg = require "thermostat_utils.device_configuration" + for _, attr in ipairs(ib.data.elements) do + -- mark whether the optional attribute ThermostatRunningState (0x029) is present and try profiling + if attr.value == 0x029 then + device:set_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, true) + device_cfg.match_profile(device) + return + end + end + device:set_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, false) + device_cfg.match_profile(device) +end + +function AttributeHandlers.system_mode_handler(driver, device, ib, response) + if device:get_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN) == nil then -- this being nil means the control_sequence_of_operation_handler hasn't run. + device.log.info_with({hub_logs = true}, "In the SystemMode handler: ControlSequenceOfOperation has not run yet. Exiting early.") + device:set_field(fields.SAVED_SYSTEM_MODE_IB, ib) + return + end + + local supported_modes = device:get_latest_state(device:endpoint_to_component(ib.endpoint_id), capabilities.thermostatMode.ID, capabilities.thermostatMode.supportedThermostatModes.NAME) or {} + -- check that the given mode was in the supported modes list + if thermostat_utils.tbl_contains(supported_modes, fields.THERMOSTAT_MODE_MAP[ib.data.value].NAME) then + device:emit_event_for_endpoint(ib.endpoint_id, fields.THERMOSTAT_MODE_MAP[ib.data.value]()) + return + end + -- if the value is not found in the supported modes list, check if it's disallowed and early return if so. + local disallowed_thermostat_modes = device:get_field(fields.DISALLOWED_THERMOSTAT_MODES) or {} + if thermostat_utils.tbl_contains(disallowed_thermostat_modes, fields.THERMOSTAT_MODE_MAP[ib.data.value].NAME) then + return + end + -- if we get here, then the reported mode is allowed and not in our mode map + -- add the mode to the OPTIONAL_THERMOSTAT_MODES_SEEN and supportedThermostatModes tables + local optional_modes_seen = st_utils.deep_copy(device:get_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN)) or {} + table.insert(optional_modes_seen, fields.THERMOSTAT_MODE_MAP[ib.data.value].NAME) + device:set_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN, optional_modes_seen, {persist=true}) + local sm_copy = st_utils.deep_copy(supported_modes) + table.insert(sm_copy, fields.THERMOSTAT_MODE_MAP[ib.data.value].NAME) + local supported_modes_event = capabilities.thermostatMode.supportedThermostatModes(sm_copy, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, supported_modes_event) + device:emit_event_for_endpoint(ib.endpoint_id, fields.THERMOSTAT_MODE_MAP[ib.data.value]()) +end + +function AttributeHandlers.thermostat_running_state_handler(driver, device, ib, response) + for mode, operating_state in pairs(fields.THERMOSTAT_OPERATING_MODE_MAP) do + if ((ib.data.value >> mode) & 1) > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, operating_state()) + return + end + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatOperatingState.thermostatOperatingState.idle()) +end + +function AttributeHandlers.control_sequence_of_operation_handler(driver, device, ib, response) + -- The ControlSequenceOfOperation attribute only directly specifies what can't be operated by the operating environment, not what can. + -- However, we assert here that a Cooling enum value implies that SystemMode supports cooling, and the same for a Heating enum. + -- We also assert that Off is supported, though per spec this is optional. + if device:get_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN) == nil then + device:set_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN, {capabilities.thermostatMode.thermostatMode.off.NAME}, {persist=true}) + end + local supported_modes = st_utils.deep_copy(device:get_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN)) + local disallowed_mode_operations = {} + + local modes_for_inclusion = {} + if ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.COOLING_WITH_REHEAT then + local _, found_idx = thermostat_utils.tbl_contains(supported_modes, capabilities.thermostatMode.thermostatMode.emergency_heat.NAME) + if found_idx then + table.remove(supported_modes, found_idx) -- if seen before, remove now + end + table.insert(supported_modes, capabilities.thermostatMode.thermostatMode.cool.NAME) + table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.heat.NAME) + table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.emergency_heat.NAME) + elseif ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.HEATING_WITH_REHEAT then + local _, found_idx = thermostat_utils.tbl_contains(supported_modes, capabilities.thermostatMode.thermostatMode.precooling.NAME) + if found_idx then + table.remove(supported_modes, found_idx) -- if seen before, remove now + end + table.insert(supported_modes, capabilities.thermostatMode.thermostatMode.heat.NAME) + table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.cool.NAME) + table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.precooling.NAME) + elseif ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.COOLING_AND_HEATING_WITH_REHEAT then + table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.cool.NAME) + table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.heat.NAME) + end + + -- check whether the Auto Mode should be supported in SystemMode, though this is unrelated to ControlSequenceOfOperation + local auto = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}) + if #auto > 0 then + table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.auto.NAME) + else + table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.auto.NAME) + end + + -- if a disallowed value was once allowed and added, it should be removed now. + for index, mode in pairs(supported_modes) do + if thermostat_utils.tbl_contains(disallowed_mode_operations, mode) then + table.remove(supported_modes, index) + end + end + -- do not include any values twice + for _, mode in pairs(modes_for_inclusion) do + if not thermostat_utils.tbl_contains(supported_modes, mode) then + table.insert(supported_modes, mode) + end + end + device:set_field(fields.DISALLOWED_THERMOSTAT_MODES, disallowed_mode_operations) + local event = capabilities.thermostatMode.supportedThermostatModes(supported_modes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) + + -- will be set by the SystemMode handler if this handler hasn't run yet. + if device:get_field(fields.SAVED_SYSTEM_MODE_IB) then + AttributeHandlers.system_mode_handler(driver, device, device:get_field(fields.SAVED_SYSTEM_MODE_IB), response) + device:set_field(fields.SAVED_SYSTEM_MODE_IB, nil) + end +end + +function AttributeHandlers.min_setpoint_deadband_handler(driver, device, ib, response) + local val = ib.data.value / 10.0 + log.info("Setting " .. fields.setpoint_limit_device_field.MIN_DEADBAND .. " to " .. string.format("%s", val)) + device:set_field(fields.setpoint_limit_device_field.MIN_DEADBAND, val, { persist = true }) + device:set_field(fields.setpoint_limit_device_field.MIN_SETPOINT_DEADBAND_CHECKED, true, {persist = true}) +end + +function AttributeHandlers.abs_heat_setpoint_limit_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local MAX_TEMP_IN_C = fields.THERMOSTAT_MAX_TEMP_IN_C + local MIN_TEMP_IN_C = fields.THERMOSTAT_MIN_TEMP_IN_C + local is_water_heater_device = (thermostat_utils.get_device_type(device) == fields.WATER_HEATER_DEVICE_TYPE_ID) + if is_water_heater_device then + MAX_TEMP_IN_C = fields.WATER_HEATER_MAX_TEMP_IN_C + MIN_TEMP_IN_C = fields.WATER_HEATER_MIN_TEMP_IN_C + end + local val = ib.data.value / 100.0 + val = st_utils.clamp_value(val, MIN_TEMP_IN_C, MAX_TEMP_IN_C) + device:set_field(minOrMax, val) + local min = device:get_field(fields.setpoint_limit_device_field.MIN_HEAT) + local max = device:get_field(fields.setpoint_limit_device_field.MAX_HEAT) + if min ~= nil and max ~= nil then + if min < max then + -- Only emit the capability for RPC version >= 5 (unit conversion for + -- heating setpoint range capability is only supported for RPC >= 5) + if version.rpc >= 5 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = min, maximum = max, step = 0.1 }, unit = "C" })) + end + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min heating setpoint %d that is not lower than the reported max %d", min, max)) + end + end + end +end + +function AttributeHandlers.abs_cool_setpoint_limit_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local val = ib.data.value / 100.0 + val = st_utils.clamp_value(val, fields.THERMOSTAT_MIN_TEMP_IN_C, fields.THERMOSTAT_MAX_TEMP_IN_C) + device:set_field(minOrMax, val) + local min = device:get_field(fields.setpoint_limit_device_field.MIN_COOL) + local max = device:get_field(fields.setpoint_limit_device_field.MAX_COOL) + if min ~= nil and max ~= nil then + if min < max then + -- Only emit the capability for RPC version >= 5 (unit conversion for + -- cooling setpoint range capability is only supported for RPC >= 5) + if version.rpc >= 5 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { minimum = min, maximum = max, step = 0.1 }, unit = "C" })) + end + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min cooling setpoint %d that is not lower than the reported max %d", min, max)) + end + end + end +end + + +-- [[ TEMPERATURE MEASUREMENT CLUSER ATTRIBUTES ]] -- + +function AttributeHandlers.temperature_handler_factory(attribute) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local unit = "C" + + -- Only emit the capability for RPC version >= 5, since unit conversion for + -- range capabilities is only supported in that case. + if version.rpc >= 5 then + local event + if attribute == capabilities.thermostatCoolingSetpoint.coolingSetpoint then + local range = { + minimum = device:get_field(fields.setpoint_limit_device_field.MIN_COOL) or fields.THERMOSTAT_MIN_TEMP_IN_C, + maximum = device:get_field(fields.setpoint_limit_device_field.MAX_COOL) or fields.THERMOSTAT_MAX_TEMP_IN_C, + step = 0.1 + } + event = capabilities.thermostatCoolingSetpoint.coolingSetpointRange({value = range, unit = unit}) + device:emit_event_for_endpoint(ib.endpoint_id, event) + elseif attribute == capabilities.thermostatHeatingSetpoint.heatingSetpoint then + local MAX_TEMP_IN_C = fields.THERMOSTAT_MAX_TEMP_IN_C + local MIN_TEMP_IN_C = fields.THERMOSTAT_MIN_TEMP_IN_C + local is_water_heater_device = thermostat_utils.get_device_type(device) == fields.WATER_HEATER_DEVICE_TYPE_ID + if is_water_heater_device then + MAX_TEMP_IN_C = fields.WATER_HEATER_MAX_TEMP_IN_C + MIN_TEMP_IN_C = fields.WATER_HEATER_MIN_TEMP_IN_C + end + + local range = { + minimum = device:get_field(fields.setpoint_limit_device_field.MIN_HEAT) or MIN_TEMP_IN_C, + maximum = device:get_field(fields.setpoint_limit_device_field.MAX_HEAT) or MAX_TEMP_IN_C, + step = 0.1 + } + event = capabilities.thermostatHeatingSetpoint.heatingSetpointRange({value = range, unit = unit}) + device:emit_event_for_endpoint(ib.endpoint_id, event) + end + end + + local temp = ib.data.value / 100.0 + device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = temp, unit = unit})) + end +end + +function AttributeHandlers.temperature_measured_value_bounds_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local temp = ib.data.value / 100.0 + local unit = "C" + temp = st_utils.clamp_value(temp, fields.THERMOSTAT_MIN_TEMP_IN_C, fields.THERMOSTAT_MAX_TEMP_IN_C) + thermostat_utils.set_field_for_endpoint(device, minOrMax, ib.endpoint_id, temp) + local min = thermostat_utils.get_field_for_endpoint(device, fields.setpoint_limit_device_field.MIN_TEMP, ib.endpoint_id) + local max = thermostat_utils.get_field_for_endpoint(device, fields.setpoint_limit_device_field.MAX_TEMP, ib.endpoint_id) + if min ~= nil and max ~= nil then + if min < max then + -- Only emit the capability for RPC version >= 5 (unit conversion for + -- temperature range capability is only supported for RPC >= 5) + if version.rpc >= 5 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = min, maximum = max }, unit = unit })) + end + thermostat_utils.set_field_for_endpoint(device, fields.setpoint_limit_device_field.MIN_TEMP, ib.endpoint_id, nil) + thermostat_utils.set_field_for_endpoint(device, fields.setpoint_limit_device_field.MAX_TEMP, ib.endpoint_id, nil) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min temperature %d that is not lower than the reported max temperature %d", min, max)) + end + end + end +end + + +--[[ FAN CONTROL CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.fan_mode_handler(driver, device, ib, response) + local fan_mode_event = { + [clusters.FanControl.attributes.FanMode.OFF] = { capabilities.fanMode.fanMode.off(), + capabilities.airConditionerFanMode.fanMode("off"), + capabilities.airPurifierFanMode.airPurifierFanMode.off(), + nil }, -- 'OFF' is not supported by thermostatFanMode + [clusters.FanControl.attributes.FanMode.LOW] = { capabilities.fanMode.fanMode.low(), + capabilities.airConditionerFanMode.fanMode("low"), + capabilities.airPurifierFanMode.airPurifierFanMode.low(), + capabilities.thermostatFanMode.thermostatFanMode.on() }, + [clusters.FanControl.attributes.FanMode.MEDIUM] = { capabilities.fanMode.fanMode.medium(), + capabilities.airConditionerFanMode.fanMode("medium"), + capabilities.airPurifierFanMode.airPurifierFanMode.medium(), + capabilities.thermostatFanMode.thermostatFanMode.on() }, + [clusters.FanControl.attributes.FanMode.HIGH] = { capabilities.fanMode.fanMode.high(), + capabilities.airConditionerFanMode.fanMode("high"), + capabilities.airPurifierFanMode.airPurifierFanMode.high(), + capabilities.thermostatFanMode.thermostatFanMode.on() }, + [clusters.FanControl.attributes.FanMode.ON] = { capabilities.fanMode.fanMode.auto(), + capabilities.airConditionerFanMode.fanMode("auto"), + capabilities.airPurifierFanMode.airPurifierFanMode.auto(), + capabilities.thermostatFanMode.thermostatFanMode.on() }, + [clusters.FanControl.attributes.FanMode.AUTO] = { capabilities.fanMode.fanMode.auto(), + capabilities.airConditionerFanMode.fanMode("auto"), + capabilities.airPurifierFanMode.airPurifierFanMode.auto(), + capabilities.thermostatFanMode.thermostatFanMode.auto() }, + [clusters.FanControl.attributes.FanMode.SMART] = { capabilities.fanMode.fanMode.auto(), + capabilities.airConditionerFanMode.fanMode("auto"), + capabilities.airPurifierFanMode.airPurifierFanMode.auto(), + capabilities.thermostatFanMode.thermostatFanMode.auto() } + } + local fan_mode_idx = device:supports_capability_by_id(capabilities.fanMode.ID) and 1 or + device:supports_capability_by_id(capabilities.airConditionerFanMode.ID) and 2 or + device:supports_capability_by_id(capabilities.airPurifierFanMode.ID) and 3 or + device:supports_capability_by_id(capabilities.thermostatFanMode.ID) and 4 + if fan_mode_idx ~= false and fan_mode_event[ib.data.value][fan_mode_idx] then + device:emit_event_for_endpoint(ib.endpoint_id, fan_mode_event[ib.data.value][fan_mode_idx]) + else + log.warn(string.format("Invalid Fan Mode (%s)", ib.data.value)) + end +end + +function AttributeHandlers.fan_mode_sequence_handler(driver, device, ib, response) + local supportedFanModes, supported_fan_modes_attribute + if ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH then + supportedFanModes = { "off", "low", "medium", "high" } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH then + supportedFanModes = { "off", "low", "high" } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO then + supportedFanModes = { "off", "low", "medium", "high", "auto" } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH_AUTO then + supportedFanModes = { "off", "low", "high", "auto" } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_HIGH_AUTO then + supportedFanModes = { "off", "high", "auto" } + else + supportedFanModes = { "off", "high" } + end + + if device:supports_capability_by_id(capabilities.airPurifierFanMode.ID) then + supported_fan_modes_attribute = capabilities.airPurifierFanMode.supportedAirPurifierFanModes + elseif device:supports_capability_by_id(capabilities.airConditionerFanMode.ID) then + supported_fan_modes_attribute = capabilities.airConditionerFanMode.supportedAcFanModes + elseif device:supports_capability_by_id(capabilities.thermostatFanMode.ID) then + supported_fan_modes_attribute = capabilities.thermostatFanMode.supportedThermostatFanModes + -- Our thermostat fan mode control is not granular enough to handle all of the supported modes + if ib.data.value >= clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO and + ib.data.value <= clusters.FanControl.attributes.FanModeSequence.OFF_ON_AUTO then + supportedFanModes = { "auto", "on" } + else + supportedFanModes = { "on" } + end + else + supported_fan_modes_attribute = capabilities.fanMode.supportedFanModes + end + + local event = supported_fan_modes_attribute(supportedFanModes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.percent_current_handler(driver, device, ib, response) + local speed = 0 + if ib.data.value ~= nil then + speed = st_utils.clamp_value(ib.data.value, fields.MIN_ALLOWED_PERCENT_VALUE, fields.MAX_ALLOWED_PERCENT_VALUE) + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(speed)) +end + +function AttributeHandlers.wind_support_handler(driver, device, ib, response) + local supported_wind_modes = {capabilities.windMode.windMode.noWind.NAME} + for mode, wind_mode in pairs(fields.WIND_MODE_MAP) do + if ((ib.data.value >> mode) & 1) > 0 then + table.insert(supported_wind_modes, wind_mode.NAME) + end + end + local event = capabilities.windMode.supportedWindModes(supported_wind_modes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.wind_setting_handler(driver, device, ib, response) + for index, wind_mode in pairs(fields.WIND_MODE_MAP) do + if ((ib.data.value >> index) & 1) > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, wind_mode()) + return + end + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windMode.windMode.noWind()) +end + +function AttributeHandlers.rock_support_handler(driver, device, ib, response) + local supported_rock_modes = {capabilities.fanOscillationMode.fanOscillationMode.off.NAME} + for mode, rock_mode in pairs(fields.ROCK_MODE_MAP) do + if ((ib.data.value >> mode) & 1) > 0 then + table.insert(supported_rock_modes, rock_mode.NAME) + end + end + local event = capabilities.fanOscillationMode.supportedFanOscillationModes(supported_rock_modes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.rock_setting_handler(driver, device, ib, response) + for index, rock_mode in pairs(fields.ROCK_MODE_MAP) do + if ((ib.data.value >> index) & 1) > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, rock_mode()) + return + end + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanOscillationMode.fanOscillationMode.off()) +end + + +-- [[ HEPA FILTER MONITORING CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.hepa_filter_condition_handler(driver, device, ib, response) + local component = device.profile.components["hepaFilter"] + local condition = ib.data.value + device:emit_component_event(component, capabilities.filterState.filterLifeRemaining(condition)) +end + +function AttributeHandlers.hepa_filter_change_indication_handler(driver, device, ib, response) + local component = device.profile.components["hepaFilter"] + if ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.OK then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) + elseif ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.WARNING then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) + elseif ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.CRITICAL then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.replace()) + end +end + + +-- [[ ACTIVATED CARBON FILTER MONITORING CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.activated_carbon_filter_condition_handler(driver, device, ib, response) + local component = device.profile.components["activatedCarbonFilter"] + local condition = ib.data.value + device:emit_component_event(component, capabilities.filterState.filterLifeRemaining(condition)) +end + +function AttributeHandlers.activated_carbon_filter_change_indication_handler(driver, device, ib, response) + local component = device.profile.components["activatedCarbonFilter"] + if ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.OK then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) + elseif ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.WARNING then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) + elseif ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.CRITICAL then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.replace()) + end +end + + +--[[ AIR QUALITY SENSOR CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.air_quality_handler(driver, device, ib, response) + local state = ib.data.value + if state == 0 then -- Unknown + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unknown()) + elseif state == 1 then -- Good + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.good()) + elseif state == 2 then -- Fair + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.moderate()) + elseif state == 3 then -- Moderate + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.slightlyUnhealthy()) + elseif state == 4 then -- Poor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unhealthy()) + elseif state == 5 then -- VeryPoor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.veryUnhealthy()) + elseif state == 6 then -- ExtremelyPoor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.hazardous()) + end +end + + +-- [[ CONCENTRATION CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.concentration_measurement_unit_factory(capability_name) + return function(driver, device, ib, response) + device:set_field(capability_name.."_unit", ib.data.value, {persist = true}) + end +end + +function AttributeHandlers.concentration_level_value_factory(attribute) + return function(driver, device, ib, response) + device:emit_event_for_endpoint(ib.endpoint_id, attribute(fields.level_strings[ib.data.value])) + end +end + +function AttributeHandlers.concentration_measured_value_factory(capability_name, attribute, target_unit) + return function(driver, device, ib, response) + local reporting_unit = device:get_field(capability_name.."_unit") + + if not reporting_unit then + reporting_unit = fields.unit_default[capability_name] + device:set_field(capability_name.."_unit", reporting_unit, {persist = true}) + end + + local value = nil + if reporting_unit then + value = thermostat_utils.unit_conversion(ib.data.value, reporting_unit, target_unit, capability_name) + end + + if value then + device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = value, unit = fields.unit_strings[target_unit]})) + -- handle case where device profile supports both fineDustLevel and dustLevel + if capability_name == capabilities.fineDustSensor.NAME and device:supports_capability(capabilities.dustSensor) then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.dustSensor.fineDustLevel({value = value, unit = fields.unit_strings[target_unit]})) + end + end + end +end + + +-- [[ POWER SOURCE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.bat_percent_remaining_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) + end +end + +function AttributeHandlers.bat_charge_level_handler(driver, device, ib, response) + if ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.OK then + device:emit_event(capabilities.batteryLevel.battery.normal()) + elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.WARNING then + device:emit_event(capabilities.batteryLevel.battery.warning()) + elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.CRITICAL then + device:emit_event(capabilities.batteryLevel.battery.critical()) + end +end + +function AttributeHandlers.power_source_attribute_list_handler(driver, device, ib, response) + local device_cfg = require "thermostat_utils.device_configuration" + for _, attr in ipairs(ib.data.elements) do + -- mark if the device if BatPercentRemaining (Attribute ID 0x0C) or + -- BatChargeLevel (Attribute ID 0x0E) is present and try profiling. + if attr.value == 0x0C then + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_PERCENTAGE) + device_cfg.match_profile(device) + return + elseif attr.value == 0x0E then + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_LEVEL) + device_cfg.match_profile(device) + return + end + end +end + + +-- [[ ELECTRICAL POWER MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.active_power_handler(driver, device, ib, response) + if ib.data.value then + local watt_value = ib.data.value / 1000 + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W" })) + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("powerMeter","power") + end + end +end + + +-- [[ ELECTRICAL ENERGY MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +local function periodic_energy_imported_handler(driver, device, ib, response) + if ib.data then + if version.api < 11 then + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:augment_type(ib.data) + end + local endpoint_id = string.format(ib.endpoint_id) + local energy_imported_Wh = st_utils.round(ib.data.elements.energy.value / 1000) --convert mWh to Wh + local cumulative_energy_imported = device:get_field(fields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} + cumulative_energy_imported[endpoint_id] = cumulative_energy_imported[endpoint_id] or 0 + cumulative_energy_imported[endpoint_id] = cumulative_energy_imported[endpoint_id] + energy_imported_Wh + device:set_field(fields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP, cumulative_energy_imported, { persist = true }) + local total_cumulative_energy_imported = thermostat_utils.get_total_cumulative_energy_imported(device) + device:emit_component_event(device.profile.components["main"], ib.endpoint_id, capabilities.energyMeter.energy({value = total_cumulative_energy_imported, unit = "Wh"})) + thermostat_utils.report_power_consumption_to_st_energy(device, total_cumulative_energy_imported) + end +end + +local function cumulative_energy_imported_handler(driver, device, ib, response) + if ib.data then + if version.api < 11 then + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:augment_type(ib.data) + end + local endpoint_id = string.format(ib.endpoint_id) + local cumulative_energy_imported = device:get_field(fields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} + local cumulative_energy_imported_Wh = st_utils.round( ib.data.elements.energy.value / 1000) -- convert mWh to Wh + cumulative_energy_imported[endpoint_id] = cumulative_energy_imported_Wh + device:set_field(fields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP, cumulative_energy_imported, { persist = true }) + local total_cumulative_energy_imported = thermostat_utils.get_total_cumulative_energy_imported(device) + device:emit_component_event(device.profile.components["main"], capabilities.energyMeter.energy({ value = total_cumulative_energy_imported, unit = "Wh" })) + thermostat_utils.report_power_consumption_to_st_energy(device, total_cumulative_energy_imported) + end +end + +function AttributeHandlers.energy_imported_factory(is_cumulative_report) + return function(driver, device, ib, response) + if is_cumulative_report then + cumulative_energy_imported_handler(driver, device, ib, response) + elseif device:get_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED) then + periodic_energy_imported_handler(driver, device, ib, response) + end + end +end + + +-- [[ WATER HEATER MODE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.water_heater_supported_modes_handler(driver, device, ib, response) + local supportWaterHeaterModes = {} + local supportWaterHeaterModesWithIdx = {} + for _, mode in ipairs(ib.data.elements) do + if version.api < 13 then + clusters.WaterHeaterMode.types.ModeOptionStruct:augment_type(mode) + end + table.insert(supportWaterHeaterModes, mode.elements.label.value) + table.insert(supportWaterHeaterModesWithIdx, {mode.elements.mode.value, mode.elements.label.value}) + end + device:set_field(fields.SUPPORTED_WATER_HEATER_MODES_WITH_IDX, supportWaterHeaterModesWithIdx, { persist = true }) + local event = capabilities.mode.supportedModes(supportWaterHeaterModes, { visibility = { displayed = false } }) + device:emit_event_for_endpoint(ib.endpoint_id, event) + event = capabilities.mode.supportedArguments(supportWaterHeaterModes, { visibility = { displayed = false } }) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.water_heater_current_mode_handler(driver, device, ib, response) + device.log.info(string.format("water_heater_current_mode_handler mode: %s", ib.data.value)) + local supportWaterHeaterModesWithIdx = device:get_field(fields.SUPPORTED_WATER_HEATER_MODES_WITH_IDX) or {} + local currentMode = ib.data.value + for i, mode in ipairs(supportWaterHeaterModesWithIdx) do + if mode[1] == currentMode then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.mode.mode(mode[2])) + break + end + end +end + + +-- [[ RELATIVE HUMIDITY MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.relative_humidity_measured_value_handler(driver, device, ib, response) + local humidity = math.floor(ib.data.value / 100.0) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(humidity)) +end + + +-- [[ ON OFF CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.on_off_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.on()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) + end +end + +return AttributeHandlers diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/capability_handlers.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/capability_handlers.lua new file mode 100644 index 0000000000..4a32b7a700 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/capability_handlers.lua @@ -0,0 +1,256 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local log = require "log" +local version = require "version" +local st_utils = require "st.utils" +local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" +local fields = require "thermostat_utils.fields" +local thermostat_utils = require "thermostat_utils.utils" + +if version.api < 10 then + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" +end + +if version.api < 13 then + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" +end + +local CapabilityHandlers = {} + + +-- [[ FAN SPEED PERCENT CAPABILITY HANDLERS ]] -- + +function CapabilityHandlers.handle_fan_speed_set_percent(driver, device, cmd) + local speed = math.floor(cmd.args.percent) + device:send(clusters.FanControl.attributes.PercentSetting:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.FanControl.ID), speed)) +end + + +-- [[ WIND MODE CAPABILITY HANDLERS ]] -- + +function CapabilityHandlers.handle_set_wind_mode(driver, device, cmd) + local wind_mode = 0 + if cmd.args.windMode == capabilities.windMode.windMode.sleepWind.NAME then + wind_mode = clusters.FanControl.types.WindSupportMask.SLEEP_WIND + elseif cmd.args.windMode == capabilities.windMode.windMode.naturalWind.NAME then + wind_mode = clusters.FanControl.types.WindSupportMask.NATURAL_WIND + end + device:send(clusters.FanControl.attributes.WindSetting:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.FanControl.ID), wind_mode)) +end + + +-- [[ FAN OSCILLATION MODE HANDLERS ]] -- + +function CapabilityHandlers.handle_set_fan_oscillation_mode(driver, device, cmd) + local rock_mode = 0 + if cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.horizontal.NAME then + rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_LEFT_RIGHT + elseif cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.vertical.NAME then + rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_UP_DOWN + elseif cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.swing.NAME then + rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_ROUND + end + device:send(clusters.FanControl.attributes.RockSetting:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.FanControl.ID), rock_mode)) +end + + +-- [[ MODE CAPABILITY HANDLERS ]] -- + +function CapabilityHandlers.handle_set_mode(driver, device, cmd) + device.log.info(string.format("set_water_heater_mode mode: %s", cmd.args.mode)) + local endpoint_id = thermostat_utils.component_to_endpoint(device, cmd.component, clusters.Thermostat.ID) + local supportedWaterHeaterModesWithIdx = device:get_field(fields.SUPPORTED_WATER_HEATER_MODES_WITH_IDX) or {} + for i, mode in ipairs(supportedWaterHeaterModesWithIdx) do + if cmd.args.mode == mode[2] then + device:send(clusters.WaterHeaterMode.commands.ChangeToMode(device, endpoint_id, mode[1])) + return + end + end +end + + +-- [[ FILTER STATE CAPABLITY HANDLERS ]] -- + +function CapabilityHandlers.handle_filter_state_reset_filter(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + if cmd.component == "hepaFilter" then + device:send(clusters.HepaFilterMonitoring.server.commands.ResetCondition(device, endpoint_id)) + else + device:send(clusters.ActivatedCarbonFilterMonitoring.server.commands.ResetCondition(device, endpoint_id)) + end +end + + +-- [[ SWITCH CAPABLITY HANDLERS ]] -- + +function CapabilityHandlers.handle_switch_on(driver, device, cmd) + local endpoint_id = thermostat_utils.component_to_endpoint(device, cmd.component, clusters.OnOff.ID) + local req = clusters.OnOff.server.commands.On(device, endpoint_id) + device:send(req) +end + +function CapabilityHandlers.handle_switch_off(driver, device, cmd) + local endpoint_id = thermostat_utils.component_to_endpoint(device, cmd.component, clusters.OnOff.ID) + local req = clusters.OnOff.server.commands.Off(device, endpoint_id) + device:send(req) +end + + +-- [[ THERMOSTAT MODE CAPABLITY HANDLERS ]] -- + +function CapabilityHandlers.handle_set_thermostat_mode(driver, device, cmd) + local mode_id = nil + for value, mode in pairs(fields.THERMOSTAT_MODE_MAP) do + if mode.NAME == cmd.args.mode then + mode_id = value + break + end + end + if mode_id then + device:send(clusters.Thermostat.attributes.SystemMode:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.Thermostat.ID), mode_id)) + end +end + +function CapabilityHandlers.thermostat_mode_command_factory(mode_name) + return function(driver, device, cmd) + return CapabilityHandlers.handle_set_thermostat_mode(driver, device, {component = cmd.component, args = {mode = mode_name}}) + end +end + + +-- [[ FAN MODE CAPABILITY HANDLERS ]] -- + +local function set_fan_mode(device, cmd, fan_mode_capability) + local command_argument = cmd.args.fanMode + if fan_mode_capability == capabilities.airPurifierFanMode then + command_argument = cmd.args.airPurifierFanMode + elseif fan_mode_capability == capabilities.thermostatFanMode then + command_argument = cmd.args.mode + end + local fan_mode_id + if command_argument == "off" then + fan_mode_id = clusters.FanControl.attributes.FanMode.OFF + elseif command_argument == "on" then + fan_mode_id = clusters.FanControl.attributes.FanMode.ON + elseif command_argument == "auto" then + fan_mode_id = clusters.FanControl.attributes.FanMode.AUTO + elseif command_argument == "high" then + fan_mode_id = clusters.FanControl.attributes.FanMode.HIGH + elseif command_argument == "medium" then + fan_mode_id = clusters.FanControl.attributes.FanMode.MEDIUM + elseif thermostat_utils.tbl_contains({ "low", "sleep", "quiet", "windFree" }, command_argument) then + fan_mode_id = clusters.FanControl.attributes.FanMode.LOW + else + device.log.warn(string.format("Invalid Fan Mode (%s) received from capability command", command_argument)) + return + end + device:send(clusters.FanControl.attributes.FanMode:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.FanControl.ID), fan_mode_id)) +end + +function CapabilityHandlers.fan_mode_command_factory(fan_mode_capability) + return function(driver, device, cmd) + set_fan_mode(device, cmd, fan_mode_capability) + end +end + + +-- [[ THERMOSTAT FAN MODE CAPABILITY HANDLERS ]] -- + +function CapabilityHandlers.thermostat_fan_mode_command_factory(mode_name) + return function(driver, device, cmd) + set_fan_mode(device, {component = cmd.component, args = {mode = mode_name}}, capabilities.thermostatFanMode) + end +end + + +-- [[ THERMOSTAT HEATING/COOLING CAPABILITY HANDLERS ]] -- + +function CapabilityHandlers.thermostat_set_setpoint_factory(setpoint) + return function(driver, device, cmd) + local endpoint_id = thermostat_utils.component_to_endpoint(device, cmd.component, clusters.Thermostat.ID) + local MAX_TEMP_IN_C = fields.THERMOSTAT_MAX_TEMP_IN_C + local MIN_TEMP_IN_C = fields.THERMOSTAT_MIN_TEMP_IN_C + local is_water_heater_device = thermostat_utils.get_device_type(device) == fields.WATER_HEATER_DEVICE_TYPE_ID + if is_water_heater_device then + MAX_TEMP_IN_C = fields.WATER_HEATER_MAX_TEMP_IN_C + MIN_TEMP_IN_C = fields.WATER_HEATER_MIN_TEMP_IN_C + end + local value = cmd.args.setpoint + if version.rpc <= 5 and value > MAX_TEMP_IN_C then + value = st_utils.f_to_c(value) + end + + -- Gather cached setpoint values when considering setpoint limits + -- Note: cached values should always exist, but defaults are chosen just in case to prevent + -- nil operation errors, and deadband logic from triggering. + local cached_cooling_val, cooling_setpoint = device:get_latest_state( + cmd.component, capabilities.thermostatCoolingSetpoint.ID, + capabilities.thermostatCoolingSetpoint.coolingSetpoint.NAME, + MAX_TEMP_IN_C, { value = MAX_TEMP_IN_C, unit = "C" } + ) + if cooling_setpoint and cooling_setpoint.unit == "F" then + cached_cooling_val = st_utils.f_to_c(cached_cooling_val) + end + local cached_heating_val, heating_setpoint = device:get_latest_state( + cmd.component, capabilities.thermostatHeatingSetpoint.ID, + capabilities.thermostatHeatingSetpoint.heatingSetpoint.NAME, + MIN_TEMP_IN_C, { value = MIN_TEMP_IN_C, unit = "C" } + ) + if heating_setpoint and heating_setpoint.unit == "F" then + cached_heating_val = st_utils.f_to_c(cached_heating_val) + end + local is_auto_capable = #device:get_endpoints( + clusters.Thermostat.ID, + {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE} + ) > 0 + + --Check setpoint limits for the device + local setpoint_type = string.match(setpoint.NAME, "Heat") or "Cool" + local deadband = device:get_field(fields.setpoint_limit_device_field.MIN_DEADBAND) or 2.5 --spec default + if setpoint_type == "Heat" then + local min = device:get_field(fields.setpoint_limit_device_field.MIN_HEAT) or MIN_TEMP_IN_C + local max = device:get_field(fields.setpoint_limit_device_field.MAX_HEAT) or MAX_TEMP_IN_C + if value < min or value > max then + log.warn(string.format( + "Invalid setpoint (%s) outside the min (%s) and the max (%s)", + value, min, max + )) + device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpoint(heating_setpoint, {state_change = true})) + return + end + if is_auto_capable and value > (cached_cooling_val - deadband) then + log.warn(string.format( + "Invalid setpoint (%s) is greater than the cooling setpoint (%s) with the deadband (%s)", + value, cooling_setpoint, deadband + )) + device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpoint(heating_setpoint, {state_change = true})) + return + end + else + local min = device:get_field(fields.setpoint_limit_device_field.MIN_COOL) or MIN_TEMP_IN_C + local max = device:get_field(fields.setpoint_limit_device_field.MAX_COOL) or MAX_TEMP_IN_C + if value < min or value > max then + log.warn(string.format( + "Invalid setpoint (%s) outside the min (%s) and the max (%s)", + value, min, max + )) + device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpoint(cooling_setpoint, {state_change = true})) + return + end + if is_auto_capable and value < (cached_heating_val + deadband) then + log.warn(string.format( + "Invalid setpoint (%s) is less than the heating setpoint (%s) with the deadband (%s)", + value, heating_setpoint, deadband + )) + device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpoint(cooling_setpoint, {state_change = true})) + return + end + end + device:send(setpoint:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.Thermostat.ID), st_utils.round(value * 100.0))) + end +end + +return CapabilityHandlers diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua new file mode 100644 index 0000000000..102b6e8d3b --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua @@ -0,0 +1,324 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "thermostat_utils.embedded_cluster_utils" +local version = require "version" +local fields = require "thermostat_utils.fields" +local thermostat_utils = require "thermostat_utils.utils" + +if version.api < 10 then + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" +end + +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" +end + +if version.api < 13 then + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" +end + +local DeviceConfigurationHelpers = {} + +function DeviceConfigurationHelpers.supported_level_measurements(device) + local measurement_caps, level_caps = {}, {} + for _, details in ipairs(fields.AIR_QUALITY_MAP) do + local cap_id = details[1] + local cluster = details[3] + -- capability describes either a HealthConcern or Measurement/Sensor + if (cap_id:match("HealthConcern$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) + if #attr_eps > 0 then + table.insert(level_caps, cap_id) + end + elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) + if #attr_eps > 0 then + table.insert(measurement_caps, cap_id) + end + end + end + return measurement_caps, level_caps +end + +function DeviceConfigurationHelpers.get_thermostat_optional_capabilities(device) + local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) + local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) + local running_state_supported = device:get_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) + + local supported_thermostat_capabilities = {} + + if #heat_eps > 0 then + table.insert(supported_thermostat_capabilities, capabilities.thermostatHeatingSetpoint.ID) + end + if #cool_eps > 0 then + table.insert(supported_thermostat_capabilities, capabilities.thermostatCoolingSetpoint.ID) + end + + if running_state_supported then + table.insert(supported_thermostat_capabilities, capabilities.thermostatOperatingState.ID) + end + + return supported_thermostat_capabilities +end + +function DeviceConfigurationHelpers.get_air_quality_optional_capabilities(device) + local supported_air_quality_capabilities = {} + + local measurement_caps, level_caps = DeviceConfigurationHelpers.supported_level_measurements(device) + + for _, cap_id in ipairs(measurement_caps) do + table.insert(supported_air_quality_capabilities, cap_id) + end + + for _, cap_id in ipairs(level_caps) do + table.insert(supported_air_quality_capabilities, cap_id) + end + + return supported_air_quality_capabilities +end + + +local DeviceConfiguration = {} + +function DeviceConfiguration.match_modular_profile_air_purifer(device) + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + local hepa_filter_component_capabilities = {} + local ac_filter_component_capabilties = {} + local profile_name = "air-purifier-modular" + + local MAIN_COMPONENT_IDX = 1 + local CAPABILITIES_LIST_IDX = 2 + + local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) + local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID) + if #humidity_eps > 0 then + table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) + end + if #temp_eps > 0 then + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + end + + local hepa_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID) + local ac_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID) + + if #hepa_filter_eps > 0 then + local filter_state_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID, {feature_bitmap = clusters.HepaFilterMonitoring.types.Feature.CONDITION}) + if #filter_state_eps > 0 then + table.insert(hepa_filter_component_capabilities, capabilities.filterState.ID) + end + + table.insert(hepa_filter_component_capabilities, capabilities.filterStatus.ID) + end + if #ac_filter_eps > 0 then + local filter_state_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID, {feature_bitmap = clusters.ActivatedCarbonFilterMonitoring.types.Feature.CONDITION}) + if #filter_state_eps > 0 then + table.insert(ac_filter_component_capabilties, capabilities.filterState.ID) + end + + table.insert(ac_filter_component_capabilties, capabilities.filterStatus.ID) + end + + -- determine fan capabilities, note that airPurifierFanMode is already mandatory + local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) + local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) + + if #rock_eps > 0 then + table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID) + end + if #wind_eps > 0 then + table.insert(main_component_capabilities, capabilities.windMode.ID) + end + + local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) + + if #thermostat_eps > 0 then + -- thermostatMode and temperatureMeasurement should be expected if thermostat is present + table.insert(main_component_capabilities, capabilities.thermostatMode.ID) + + -- only add temperatureMeasurement if it is not already added via TemperatureMeasurement cluster support + if #temp_eps == 0 then + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + end + local thermostat_capabilities = DeviceConfigurationHelpers.get_thermostat_optional_capabilities(device) + for _, capability_id in pairs(thermostat_capabilities) do + table.insert(main_component_capabilities, capability_id) + end + end + + local aqs_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) + if #aqs_eps > 0 then + table.insert(main_component_capabilities, capabilities.airQualityHealthConcern.ID) + end + + local supported_air_quality_capabilities = DeviceConfigurationHelpers.get_air_quality_optional_capabilities(device) + for _, capability_id in pairs(supported_air_quality_capabilities) do + table.insert(main_component_capabilities, capability_id) + end + + table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) + if #ac_filter_component_capabilties > 0 then + table.insert(optional_supported_component_capabilities, {"activatedCarbonFilter", ac_filter_component_capabilties}) + end + if #hepa_filter_component_capabilities > 0 then + table.insert(optional_supported_component_capabilities, {"hepaFilter", hepa_filter_component_capabilities}) + end + + device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) + + -- earlier modular profile gating (min api v14, rpc 8) ensures we are running >= 0.57 FW. + -- This gating specifies a workaround required only for 0.57 FW, which is not needed for 0.58 and higher. + if version.api < 15 or version.rpc < 9 then + -- add mandatory capabilities for subscription + local total_supported_capabilities = optional_supported_component_capabilities + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airPurifierFanMode.ID) + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.fanSpeedPercent.ID) + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.refresh.ID) + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.firmwareUpdate.ID) + + device:set_field(fields.SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + end +end + +function DeviceConfiguration.match_modular_profile_thermostat(device) + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + local profile_name = "thermostat-modular" + + local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) + if #humidity_eps > 0 then + table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) + end + + -- determine fan capabilities + local fan_eps = device:get_endpoints(clusters.FanControl.ID) + local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) + local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) + + if #fan_eps > 0 then + table.insert(main_component_capabilities, capabilities.fanMode.ID) + end + if #rock_eps > 0 then + table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID) + end + if #wind_eps > 0 then + table.insert(main_component_capabilities, capabilities.windMode.ID) + end + + local thermostat_capabilities = DeviceConfigurationHelpers.get_thermostat_optional_capabilities(device) + for _, capability_id in pairs(thermostat_capabilities) do + table.insert(main_component_capabilities, capability_id) + end + + local battery_supported = device:get_field(fields.profiling_data.BATTERY_SUPPORT) + if battery_supported == fields.battery_support.BATTERY_LEVEL then + table.insert(main_component_capabilities, capabilities.batteryLevel.ID) + elseif battery_supported == fields.battery_support.BATTERY_PERCENTAGE then + table.insert(main_component_capabilities, capabilities.battery.ID) + end + + table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) + device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) + + -- earlier modular profile gating (min api v14, rpc 8) ensures we are running >= 0.57 FW. + -- This gating specifies a workaround required only for 0.57 FW, which is not needed for 0.58 and higher. + if version.api < 15 or version.rpc < 9 then + -- add mandatory capabilities for subscription + local total_supported_capabilities = optional_supported_component_capabilities + table.insert(main_component_capabilities, capabilities.thermostatMode.ID) + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + table.insert(main_component_capabilities, capabilities.refresh.ID) + table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) + + device:set_field(fields.SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + end +end + +function DeviceConfiguration.match_modular_profile_room_ac(device) + local running_state_supported = device:get_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) + local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + local profile_name = "room-air-conditioner-modular" + + if #humidity_eps > 0 then + table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) + end + + -- determine fan capabilities + local fan_eps = device:get_endpoints(clusters.FanControl.ID) + local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) + -- Note: Room AC does not support the rocking feature of FanControl. + + if #fan_eps > 0 then + table.insert(main_component_capabilities, capabilities.airConditionerFanMode.ID) + table.insert(main_component_capabilities, capabilities.fanSpeedPercent.ID) + end + if #wind_eps > 0 then + table.insert(main_component_capabilities, capabilities.windMode.ID) + end + + local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) + local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) + + if #heat_eps > 0 then + table.insert(main_component_capabilities, capabilities.thermostatHeatingSetpoint.ID) + end + if #cool_eps > 0 then + table.insert(main_component_capabilities, capabilities.thermostatCoolingSetpoint.ID) + end + + if running_state_supported then + table.insert(main_component_capabilities, capabilities.thermostatOperatingState.ID) + end + + table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) + device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) + + -- earlier modular profile gating (min api v14, rpc 8) ensures we are running >= 0.57 FW. + -- This gating specifies a workaround required only for 0.57 FW, which is not needed for 0.58 and higher. + if version.api < 15 or version.rpc < 9 then + -- add mandatory capabilities for subscription + local total_supported_capabilities = optional_supported_component_capabilities + table.insert(main_component_capabilities, capabilities.switch.ID) + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + table.insert(main_component_capabilities, capabilities.thermostatMode.ID) + table.insert(main_component_capabilities, capabilities.refresh.ID) + table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) + + device:set_field(fields.SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + end +end + +local match_modular_device_type = { + [fields.AP_DEVICE_TYPE_ID] = DeviceConfiguration.match_modular_profile_air_purifer, + [fields.RAC_DEVICE_TYPE_ID] = DeviceConfiguration.match_modular_profile_room_ac, + [fields.THERMOSTAT_DEVICE_TYPE_ID] = DeviceConfiguration.match_modular_profile_thermostat, +} + +local function profiling_data_still_required(device) + for _, field in pairs(fields.profiling_data) do + if device:get_field(field) == nil then + return true -- data still required if a field is nil + end + end + return false +end + +function DeviceConfiguration.match_profile(device) + if profiling_data_still_required(device) then return end + local primary_device_type = thermostat_utils.get_device_type(device) + if version.api >= 14 and version.rpc >= 8 and match_modular_device_type[primary_device_type] then + match_modular_device_type[primary_device_type](device) + return + else + local legacy_device_cfg = require "thermostat_utils.legacy_device_configuration" + legacy_device_cfg.match_profile(device) + end +end + +return DeviceConfiguration diff --git a/drivers/SmartThings/matter-thermostat/src/embedded-cluster-utils.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/embedded_cluster_utils.lua similarity index 67% rename from drivers/SmartThings/matter-thermostat/src/embedded-cluster-utils.lua rename to drivers/SmartThings/matter-thermostat/src/thermostat_utils/embedded_cluster_utils.lua index 3c38ef55d0..3368fe2fa4 100644 --- a/drivers/SmartThings/matter-thermostat/src/embedded-cluster-utils.lua +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/embedded_cluster_utils.lua @@ -1,31 +1,32 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +local version = require "version" local clusters = require "st.matter.clusters" local utils = require "st.utils" --- Include driver-side definitions when lua libs api version is < 10 -local version = require "version" if version.api < 10 then - clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" - clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" end if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" end if version.api < 13 then - clusters.WaterHeaterMode = require "WaterHeaterMode" + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" end local embedded_cluster_utils = {} diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/fields.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/fields.lua new file mode 100644 index 0000000000..770fd7d65e --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/fields.lua @@ -0,0 +1,238 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local version = require "version" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local st_utils = require "st.utils" + +if version.api < 10 then + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Thermostat.types.ThermostatSystemMode.DRY = 0x8 -- ThermostatSystemMode added in Matter 1.2 + clusters.Thermostat.types.ThermostatSystemMode.SLEEP = 0x9 -- ThermostatSystemMode added in Matter 1.2 +end + +local ThermostatFields = {} + +ThermostatFields.SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities" + +ThermostatFields.SAVED_SYSTEM_MODE_IB = "__saved_system_mode_ib" +ThermostatFields.DISALLOWED_THERMOSTAT_MODES = "__DISALLOWED_CONTROL_OPERATIONS" +ThermostatFields.OPTIONAL_THERMOSTAT_MODES_SEEN = "__OPTIONAL_THERMOSTAT_MODES_SEEN" + +ThermostatFields.RAC_DEVICE_TYPE_ID = 0x0072 +ThermostatFields.AP_DEVICE_TYPE_ID = 0x002D +ThermostatFields.FAN_DEVICE_TYPE_ID = 0x002B +ThermostatFields.WATER_HEATER_DEVICE_TYPE_ID = 0x050F +ThermostatFields.HEAT_PUMP_DEVICE_TYPE_ID = 0x0309 +ThermostatFields.THERMOSTAT_DEVICE_TYPE_ID = 0x0301 +ThermostatFields.ELECTRICAL_SENSOR_DEVICE_TYPE_ID = 0x0510 + +ThermostatFields.MIN_ALLOWED_PERCENT_VALUE = 0 +ThermostatFields.MAX_ALLOWED_PERCENT_VALUE = 100 +ThermostatFields.CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" +ThermostatFields.LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" +ThermostatFields.MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds +ThermostatFields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP = "__total_cumulative_energy_imported_map" +ThermostatFields.SUPPORTED_WATER_HEATER_MODES_WITH_IDX = "__supported_water_heater_modes_with_idx" +ThermostatFields.COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" +ThermostatFields.MGM3_PPM_CONVERSION_FACTOR = 24.45 + +-- For RPC version < 6: +-- issue context: driver cannot know a setpoint capability's unit (whether Celsius or Farenheit) +-- when a command is received, as the received arguments do not contain the unit. +-- workaround: map the following temperature ranges to either Celsius or Farenheit: +-- For Thermostats: +-- 1. if the received setpoint command value is in the range 5 ~ 40, it is inferred as *C +-- 2. if the received setpoint command value is in the range 41 ~ 104, it is inferred as *F +-- For Water Heaters: +-- 1. if the received setpoint command value is in the range 30 ~ 80, it is inferred as *C +-- 2. if the received setpoint command value is in the range 86 ~ 176, it is inferred as *F +-- For RPC version >= 6: +-- temperatureSetpoint always reports in Celsius, removing the need for the above workaround. +-- In this case, we use these fields simply to limit the setpoint's range to "reasonable" values on the platform. +ThermostatFields.THERMOSTAT_MAX_TEMP_IN_C = version.rpc >= 6 and 100.0 or 40.0 +ThermostatFields.THERMOSTAT_MIN_TEMP_IN_C = version.rpc >= 6 and 0.0 or 5.0 +ThermostatFields.WATER_HEATER_MAX_TEMP_IN_C = version.rpc >= 6 and 100.0 or 80.0 +ThermostatFields.WATER_HEATER_MIN_TEMP_IN_C = version.rpc >= 6 and 0.0 or 30.0 + +ThermostatFields.setpoint_limit_device_field = { + MIN_SETPOINT_DEADBAND_CHECKED = "MIN_SETPOINT_DEADBAND_CHECKED", + MIN_HEAT = "MIN_HEAT", + MAX_HEAT = "MAX_HEAT", + MIN_COOL = "MIN_COOL", + MAX_COOL = "MAX_COOL", + MIN_DEADBAND = "MIN_DEADBAND", + MIN_TEMP = "MIN_TEMP", + MAX_TEMP = "MAX_TEMP" +} + +ThermostatFields.battery_support = { + NO_BATTERY = "NO_BATTERY", + BATTERY_LEVEL = "BATTERY_LEVEL", + BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" +} + +ThermostatFields.profiling_data = { + BATTERY_SUPPORT = "__BATTERY_SUPPORT", + THERMOSTAT_RUNNING_STATE_SUPPORT = "__THERMOSTAT_RUNNING_STATE_SUPPORT" +} + +ThermostatFields.THERMOSTAT_MODE_MAP = { + [clusters.Thermostat.types.ThermostatSystemMode.OFF] = capabilities.thermostatMode.thermostatMode.off, + [clusters.Thermostat.types.ThermostatSystemMode.AUTO] = capabilities.thermostatMode.thermostatMode.auto, + [clusters.Thermostat.types.ThermostatSystemMode.COOL] = capabilities.thermostatMode.thermostatMode.cool, + [clusters.Thermostat.types.ThermostatSystemMode.HEAT] = capabilities.thermostatMode.thermostatMode.heat, + [clusters.Thermostat.types.ThermostatSystemMode.EMERGENCY_HEATING] = capabilities.thermostatMode.thermostatMode.emergency_heat, + [clusters.Thermostat.types.ThermostatSystemMode.PRECOOLING] = capabilities.thermostatMode.thermostatMode.precooling, + [clusters.Thermostat.types.ThermostatSystemMode.FAN_ONLY] = capabilities.thermostatMode.thermostatMode.fanonly, + [clusters.Thermostat.types.ThermostatSystemMode.DRY] = capabilities.thermostatMode.thermostatMode.dryair, + [clusters.Thermostat.types.ThermostatSystemMode.SLEEP] = capabilities.thermostatMode.thermostatMode.asleep, +} + +ThermostatFields.THERMOSTAT_OPERATING_MODE_MAP = { + [0] = capabilities.thermostatOperatingState.thermostatOperatingState.heating, + [1] = capabilities.thermostatOperatingState.thermostatOperatingState.cooling, + [2] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, + [3] = capabilities.thermostatOperatingState.thermostatOperatingState.heating, + [4] = capabilities.thermostatOperatingState.thermostatOperatingState.cooling, + [5] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, + [6] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, +} + +ThermostatFields.WIND_MODE_MAP = { + [0] = capabilities.windMode.windMode.sleepWind, + [1] = capabilities.windMode.windMode.naturalWind +} + +ThermostatFields.ROCK_MODE_MAP = { + [0] = capabilities.fanOscillationMode.fanOscillationMode.horizontal, + [1] = capabilities.fanOscillationMode.fanOscillationMode.vertical, + [2] = capabilities.fanOscillationMode.fanOscillationMode.swing +} + +ThermostatFields.AIR_QUALITY_MAP = { + {capabilities.carbonDioxideMeasurement.ID, "-co2", clusters.CarbonDioxideConcentrationMeasurement}, + {capabilities.carbonDioxideHealthConcern.ID, "-co2", clusters.CarbonDioxideConcentrationMeasurement}, + {capabilities.carbonMonoxideMeasurement.ID, "-co", clusters.CarbonMonoxideConcentrationMeasurement}, + {capabilities.carbonMonoxideHealthConcern.ID, "-co", clusters.CarbonMonoxideConcentrationMeasurement}, + {capabilities.dustSensor.ID, "-pm10", clusters.Pm10ConcentrationMeasurement}, + {capabilities.dustHealthConcern.ID, "-pm10", clusters.Pm10ConcentrationMeasurement}, + {capabilities.fineDustSensor.ID, "-pm25", clusters.Pm25ConcentrationMeasurement}, + {capabilities.fineDustHealthConcern.ID, "-pm25", clusters.Pm25ConcentrationMeasurement}, + {capabilities.formaldehydeMeasurement.ID, "-ch2o", clusters.FormaldehydeConcentrationMeasurement}, + {capabilities.formaldehydeHealthConcern.ID, "-ch2o", clusters.FormaldehydeConcentrationMeasurement}, + {capabilities.nitrogenDioxideHealthConcern.ID, "-no2", clusters.NitrogenDioxideConcentrationMeasurement}, + {capabilities.nitrogenDioxideMeasurement.ID, "-no2", clusters.NitrogenDioxideConcentrationMeasurement}, + {capabilities.ozoneHealthConcern.ID, "-ozone", clusters.OzoneConcentrationMeasurement}, + {capabilities.ozoneMeasurement.ID, "-ozone", clusters.OzoneConcentrationMeasurement}, + {capabilities.radonHealthConcern.ID, "-radon", clusters.RadonConcentrationMeasurement}, + {capabilities.radonMeasurement.ID, "-radon", clusters.RadonConcentrationMeasurement}, + {capabilities.tvocHealthConcern.ID, "-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement}, + {capabilities.tvocMeasurement.ID, "-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement}, + {capabilities.veryFineDustHealthConcern.ID, "-pm1", clusters.Pm1ConcentrationMeasurement}, + {capabilities.veryFineDustSensor.ID, "-pm1", clusters.Pm1ConcentrationMeasurement}, +} + +ThermostatFields.units = { + PPM = 0, + PPB = 1, + PPT = 2, + MGM3 = 3, + UGM3 = 4, + NGM3 = 5, + PM3 = 6, + BQM3 = 7, + PCIL = 0xFF -- not in matter spec +} + +local units = ThermostatFields.units -- copy units to avoid references below + +ThermostatFields.unit_strings = { + [units.PPM] = "ppm", + [units.PPB] = "ppb", + [units.PPT] = "ppt", + [units.MGM3] = "mg/m^3", + [units.NGM3] = "ng/m^3", + [units.UGM3] = "μg/m^3", + [units.BQM3] = "Bq/m^3", + [units.PCIL] = "pCi/L" +} + +ThermostatFields.unit_default = { + [capabilities.carbonMonoxideMeasurement.NAME] = units.PPM, + [capabilities.carbonDioxideMeasurement.NAME] = units.PPM, + [capabilities.nitrogenDioxideMeasurement.NAME] = units.PPM, + [capabilities.ozoneMeasurement.NAME] = units.PPM, + [capabilities.formaldehydeMeasurement.NAME] = units.PPM, + [capabilities.veryFineDustSensor.NAME] = units.UGM3, + [capabilities.fineDustSensor.NAME] = units.UGM3, + [capabilities.dustSensor.NAME] = units.UGM3, + [capabilities.radonMeasurement.NAME] = units.BQM3, + [capabilities.tvocMeasurement.NAME] = units.PPB -- TVOC is typically within the range of 0-5500 ppb, with good to moderate values being < 660 ppb +} + +-- All ConcentrationMesurement clusters inherit from the same base cluster definitions, +-- so CarbonMonoxideConcentratinMeasurement is used below but the same enum types exist +-- in all ConcentrationMeasurement clusters +ThermostatFields.level_strings = { + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.UNKNOWN] = "unknown", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.LOW] = "good", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.MEDIUM] = "moderate", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.HIGH] = "unhealthy", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.CRITICAL] = "hazardous", +} + +-- measured in g/mol +ThermostatFields.molecular_weights = { + [capabilities.carbonDioxideMeasurement.NAME] = 44.010, + [capabilities.nitrogenDioxideMeasurement.NAME] = 28.014, + [capabilities.ozoneMeasurement.NAME] = 48.0, + [capabilities.formaldehydeMeasurement.NAME] = 30.031, + [capabilities.veryFineDustSensor.NAME] = "N/A", + [capabilities.fineDustSensor.NAME] = "N/A", + [capabilities.dustSensor.NAME] = "N/A", + [capabilities.radonMeasurement.NAME] = 222.018, + [capabilities.tvocMeasurement.NAME] = "N/A", +} + +ThermostatFields.conversion_tables = { + [units.PPM] = { + [units.PPM] = function(value) return st_utils.round(value) end, + [units.PPB] = function(value) return st_utils.round(value * (10^3)) end, + [units.UGM3] = function(value, molecular_weight) return st_utils.round((value * molecular_weight * 10^3) / ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) end, + [units.MGM3] = function(value, molecular_weight) return st_utils.round((value * molecular_weight) / ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) end, + }, + [units.PPB] = { + [units.PPM] = function(value) return st_utils.round(value/(10^3)) end, + [units.PPB] = function(value) return st_utils.round(value) end, + }, + [units.PPT] = { + [units.PPM] = function(value) return st_utils.round(value/(10^6)) end + }, + [units.MGM3] = { + [units.UGM3] = function(value) return st_utils.round(value * (10^3)) end, + [units.PPM] = function(value, molecular_weight) return st_utils.round((value * ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) / molecular_weight) end, + }, + [units.UGM3] = { + [units.UGM3] = function(value) return st_utils.round(value) end, + [units.PPM] = function(value, molecular_weight) return st_utils.round((value * ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) / (molecular_weight * 10^3)) end, + }, + [units.NGM3] = { + [units.UGM3] = function(value) return st_utils.round(value/(10^3)) end + }, + [units.BQM3] = { + [units.PCIL] = function(value) return st_utils.round(value/37) end + }, +} + +return ThermostatFields \ No newline at end of file diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/legacy_device_configuration.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/legacy_device_configuration.lua new file mode 100644 index 0000000000..6cdda2a53a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/legacy_device_configuration.lua @@ -0,0 +1,259 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local version = require "version" +local clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "thermostat_utils.embedded_cluster_utils" +local fields = require "thermostat_utils.fields" +local thermostat_utils = require "thermostat_utils.utils" + +if version.api < 10 then + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" + clusters.AirQuality = require "embedded_clusters.AirQuality" +end + +local LegacyConfigurationHelpers = {} + +function LegacyConfigurationHelpers.create_level_measurement_profile(device) + local meas_name, level_name = "", "" + for _, details in ipairs(fields.AIR_QUALITY_MAP) do + local cap_id = details[1] + local cluster = details[3] + -- capability describes either a HealthConcern or Measurement/Sensor + if (cap_id:match("HealthConcern$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) + if #attr_eps > 0 then + level_name = level_name .. details[2] + end + elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) + if #attr_eps > 0 then + meas_name = meas_name .. details[2] + end + end + end + return meas_name, level_name +end + +function LegacyConfigurationHelpers.create_air_quality_sensor_profile(device) + local aqs_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) + local profile_name = "" + if #aqs_eps > 0 then + profile_name = profile_name .. "-aqs" + end + local meas_name, level_name = LegacyConfigurationHelpers.create_level_measurement_profile(device) + if meas_name ~= "" then + profile_name = profile_name .. meas_name .. "-meas" + end + if level_name ~= "" then + profile_name = profile_name .. level_name .. "-level" + end + return profile_name +end + +function LegacyConfigurationHelpers.create_fan_profile(device) + local fan_eps = device:get_endpoints(clusters.FanControl.ID) + local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) + local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) + local profile_name = "" + if #fan_eps > 0 then + profile_name = profile_name .. "-fan" + end + if #rock_eps > 0 then + profile_name = profile_name .. "-rock" + end + if #wind_eps > 0 then + profile_name = profile_name .. "-wind" + end + return profile_name +end + +function LegacyConfigurationHelpers.create_air_purifier_profile(device) + local hepa_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID) + local ac_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID) + local fan_eps_seen = false + local profile_name = "air-purifier" + if #hepa_filter_eps > 0 then + profile_name = profile_name .. "-hepa" + end + if #ac_filter_eps > 0 then + profile_name = profile_name .. "-ac" + end + + -- air purifier profiles include -fan later in the name for historical reasons. + -- save this information for use at that point. + local fan_profile = LegacyConfigurationHelpers.create_fan_profile(device) + if fan_profile ~= "" then + fan_eps_seen = true + end + fan_profile = string.gsub(fan_profile, "-fan", "") + profile_name = profile_name .. fan_profile + + return profile_name, fan_eps_seen +end + +function LegacyConfigurationHelpers.create_thermostat_modes_profile(device) + local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) + local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) + + local thermostat_modes = "" + if #heat_eps == 0 and #cool_eps == 0 then + return "No Heating nor Cooling Support" + elseif #heat_eps > 0 and #cool_eps == 0 then + thermostat_modes = thermostat_modes .. "-heating-only" + elseif #cool_eps > 0 and #heat_eps == 0 then + thermostat_modes = thermostat_modes .. "-cooling-only" + end + return thermostat_modes +end + + +local LegacyDeviceConfiguration = {} + +function LegacyDeviceConfiguration.match_profile(device) + local running_state_supported = device:get_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) + local battery_supported = device:get_field(fields.profiling_data.BATTERY_SUPPORT) + + local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) + local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) + local device_type = thermostat_utils.get_device_type(device) + local profile_name + if device_type == fields.RAC_DEVICE_TYPE_ID then + profile_name = "room-air-conditioner" + + if #humidity_eps > 0 then + profile_name = profile_name .. "-humidity" + end + + -- Room AC does not support the rocking feature of FanControl. + local fan_name = LegacyConfigurationHelpers.create_fan_profile(device) + fan_name = string.gsub(fan_name, "-rock", "") + profile_name = profile_name .. fan_name + + local thermostat_modes = LegacyConfigurationHelpers.create_thermostat_modes_profile(device) + if thermostat_modes == "" then + profile_name = profile_name .. "-heating-cooling" + else + device.log.warn_with({hub_logs=true}, "Device does not support both heating and cooling. No matching profile") + return + end + + if profile_name == "room-air-conditioner-humidity-fan-wind-heating-cooling" then + profile_name = "room-air-conditioner" + end + + if not running_state_supported and profile_name == "room-air-conditioner-fan-heating-cooling" then + profile_name = profile_name .. "-nostate" + end + + elseif device_type == fields.FAN_DEVICE_TYPE_ID then + profile_name = LegacyConfigurationHelpers.create_fan_profile(device) + -- remove leading "-" + profile_name = string.sub(profile_name, 2) + if profile_name == "fan" then + profile_name = "fan-generic" + end + + elseif device_type == fields.AP_DEVICE_TYPE_ID then + local fan_eps_found + profile_name, fan_eps_found = LegacyConfigurationHelpers.create_air_purifier_profile(device) + if #thermostat_eps > 0 then + profile_name = profile_name .. "-thermostat" + + if #humidity_eps > 0 then + profile_name = profile_name .. "-humidity" + end + + if fan_eps_found then + profile_name = profile_name .. "-fan" + end + + local thermostat_modes = LegacyConfigurationHelpers.create_thermostat_modes_profile(device) + if thermostat_modes ~= "No Heating nor Cooling Support" then + profile_name = profile_name .. thermostat_modes + end + + if not running_state_supported then + profile_name = profile_name .. "-nostate" + end + + if battery_supported == fields.battery_support.BATTERY_LEVEL then + profile_name = profile_name .. "-batteryLevel" + elseif battery_supported == fields.battery_support.NO_BATTERY then + profile_name = profile_name .. "-nobattery" + end + elseif #device:get_endpoints(clusters.TemperatureMeasurement.ID) > 0 then + profile_name = profile_name .. "-temperature" + + if #humidity_eps > 0 then + profile_name = profile_name .. "-humidity" + end + + if fan_eps_found then + profile_name = profile_name .. "-fan" + end + end + profile_name = profile_name .. LegacyConfigurationHelpers.create_air_quality_sensor_profile(device) + elseif device_type == fields.WATER_HEATER_DEVICE_TYPE_ID then + -- If a Water Heater is composed of Electrical Sensor device type, it must support both ElectricalEnergyMeasurement and + -- ElectricalPowerMeasurement clusters. + local electrical_sensor_eps = thermostat_utils.get_endpoints_by_device_type(device, fields.ELECTRICAL_SENSOR_DEVICE_TYPE_ID) or {} + if #electrical_sensor_eps > 0 then + profile_name = "water-heater-power-energy-powerConsumption" + end + elseif device_type == fields.HEAT_PUMP_DEVICE_TYPE_ID then + profile_name = "heat-pump" + local MAX_HEAT_PUMP_THERMOSTAT_COMPONENTS = 2 + for i = 1, math.min(MAX_HEAT_PUMP_THERMOSTAT_COMPONENTS, #thermostat_eps) do + profile_name = profile_name .. "-thermostat" + if thermostat_utils.tbl_contains(humidity_eps, thermostat_eps[i]) then + profile_name = profile_name .. "-humidity" + end + end + elseif #thermostat_eps > 0 then + profile_name = "thermostat" + + if #humidity_eps > 0 then + profile_name = profile_name .. "-humidity" + end + + -- thermostat profiles support neither wind nor rocking FanControl attributes + local fan_name = LegacyConfigurationHelpers.create_fan_profile(device) + if fan_name ~= "" then + profile_name = profile_name .. "-fan" + end + + local thermostat_modes = LegacyConfigurationHelpers.create_thermostat_modes_profile(device) + if thermostat_modes == "No Heating nor Cooling Support" then + device.log.warn_with({hub_logs=true}, "Device does not support either heating or cooling. No matching profile") + return + else + profile_name = profile_name .. thermostat_modes + end + + if not running_state_supported then + profile_name = profile_name .. "-nostate" + end + + if battery_supported == fields.battery_support.BATTERY_LEVEL then + profile_name = profile_name .. "-batteryLevel" + elseif battery_supported == fields.battery_support.NO_BATTERY then + profile_name = profile_name .. "-nobattery" + end + else + device.log.warn_with({hub_logs=true}, "Device type is not supported in thermostat driver") + return + end + + if profile_name then + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) + end + -- clear all profiling data fields after profiling is complete. + for _, field in pairs(fields.profiling_data) do + device:set_field(field, nil) + end +end + +return LegacyDeviceConfiguration diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua new file mode 100644 index 0000000000..8194f08c48 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua @@ -0,0 +1,183 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local log = require "log" +local capabilities = require "st.capabilities" +local embedded_cluster_utils = require "thermostat_utils.embedded_cluster_utils" +local fields = require "thermostat_utils.fields" + +local ThermostatUtils = {} + +function ThermostatUtils.tbl_contains(array, value) + if value == nil then return false end + for _, element in pairs(array or {}) do + if element == value then + return true + end + end + return false +end + +function ThermostatUtils.get_field_for_endpoint(device, field, endpoint) + return device:get_field(string.format("%s_%d", field, endpoint)) +end + +function ThermostatUtils.set_field_for_endpoint(device, field, endpoint, value, additional_params) + device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) +end + +function ThermostatUtils.find_default_endpoint(device, cluster) + local res = device.MATTER_DEFAULT_ENDPOINT + local eps = embedded_cluster_utils.get_endpoints(device, cluster) + table.sort(eps) + for _, v in ipairs(eps) do + if v ~= 0 then --0 is the matter RootNode endpoint + return v + end + end + device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) + return res +end + +function ThermostatUtils.component_to_endpoint(device, component_name, cluster_id) + -- Use the find_default_endpoint function to return the first endpoint that + -- supports a given cluster. + local component_to_endpoint_map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) + if component_to_endpoint_map ~= nil and component_to_endpoint_map[component_name] ~= nil then + return component_to_endpoint_map[component_name] + end + if not cluster_id then return device.MATTER_DEFAULT_ENDPOINT end + return ThermostatUtils.find_default_endpoint(device, cluster_id) +end + +function ThermostatUtils.endpoint_to_component(device, endpoint_id) + local component_to_endpoint_map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) + if component_to_endpoint_map ~= nil then + for comp, ep in pairs(component_to_endpoint_map) do + if ep == endpoint_id then + return comp + end + end + end + return "main" +end + +function ThermostatUtils.get_total_cumulative_energy_imported(device) + local total_cumulative_energy_imported = device:get_field(fields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} + local total_energy = 0 + for _, energyWh in pairs(total_cumulative_energy_imported) do + total_energy = total_energy + energyWh + end + return total_energy +end + +function ThermostatUtils.get_endpoints_by_device_type(device, device_type) + local endpoints = {} + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == device_type then + table.insert(endpoints, ep.endpoint_id) + break + end + end + end + table.sort(endpoints) + return endpoints +end + +function ThermostatUtils.get_device_type(device) + -- For cases where a device has multiple device types, this list indicates which + -- device type will be the "main" device type for purposes of selecting a profile + -- with an appropriate category. This is done to promote consistency between + -- devices with similar device type compositions that may report their device types + -- listed in different orders + local device_type_priority = { + [fields.HEAT_PUMP_DEVICE_TYPE_ID] = 1, + [fields.RAC_DEVICE_TYPE_ID] = 2, + [fields.AP_DEVICE_TYPE_ID] = 3, + [fields.THERMOSTAT_DEVICE_TYPE_ID] = 4, + [fields.FAN_DEVICE_TYPE_ID] = 5, + [fields.WATER_HEATER_DEVICE_TYPE_ID] = 6, + } + + local main_device_type = false + + for _, ep in ipairs(device.endpoints) do + if ep.device_types ~= nil then + for _, dt in ipairs(ep.device_types) do + if not device_type_priority[main_device_type] or (device_type_priority[dt.device_type_id] and + device_type_priority[dt.device_type_id] < device_type_priority[main_device_type]) then + main_device_type = dt.device_type_id + end + end + end + end + + return main_device_type +end + +function ThermostatUtils.unit_conversion(value, from_unit, to_unit, capability_name) + local conversion_function = fields.conversion_tables[from_unit] and fields.conversion_tables[from_unit][to_unit] or nil + if not conversion_function then + log.info_with( {hub_logs = true} , string.format("Unsupported unit conversion from %s to %s", fields.unit_strings[from_unit], fields.unit_strings[to_unit])) + return + end + + if not value then + log.info_with( {hub_logs = true} , "unit conversion value is nil") + return + end + + return conversion_function(value, fields.molecular_weights[capability_name]) +end + +function ThermostatUtils.supports_capability_by_id_modular(device, capability, component) + if not device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) then + device.log.warn_with({hub_logs = true}, "Device has overriden supports_capability_by_id, but does not have supported capabilities set.") + return false + end + for _, component_capabilities in ipairs(device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES)) do + local comp_id = component_capabilities[1] + local capability_ids = component_capabilities[2] + if (component == nil) or (component == comp_id) then + for _, cap in ipairs(capability_ids) do + if cap == capability then + return true + end + end + end + end + return false +end + +function ThermostatUtils.report_power_consumption_to_st_energy(device, latest_total_imported_energy_wh) + local current_time = os.time() + local last_time = device:get_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP) or 0 + + -- Ensure that the previous report was sent at least 15 minutes ago + if fields.MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then + return + end + + device:set_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) + + -- Calculate the energy delta between reports + local energy_delta_wh = 0.0 + local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, + capabilities.powerConsumptionReport.powerConsumption.NAME) + if previous_imported_report and previous_imported_report.energy then + energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) + end + + local epoch_to_iso8601 = function(time) return os.date("!%Y-%m-%dT%H:%M:%SZ", time) end + + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' + device:emit_component_event(device.profile.components["main"], capabilities.powerConsumptionReport.powerConsumption({ + start = epoch_to_iso8601(last_time), + ["end"] = epoch_to_iso8601(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) +end + +return ThermostatUtils diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index 63c3aa3254..3f84380085 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -162,6 +162,21 @@ matterManufacturer: vendorId: 0x1457 productId: 0x0002 deviceProfileName: window-covering-battery + - id: "5207/22" + deviceLabel: WISTAR WSCMXH Smart Vertical Blind Motor + vendorId: 0x1457 + productId: 0x0016 + deviceProfileName: window-covering-tilt + - id: "5207/23" + deviceLabel: WISTAR WSCMXF Smart Vertical Blind Motor + vendorId: 0x1457 + productId: 0x0017 + deviceProfileName: window-covering-tilt + - id: "5207/24" + deviceLabel: WISTAR WSCMXF-LED Smart Vertical Blind Motor + vendorId: 0x1457 + productId: 0x0018 + deviceProfileName: window-covering-tilt #Yooksmart - id: "5411/1052" deviceLabel: Smart WindowCovering Series diff --git a/drivers/SmartThings/matter-window-covering/src/init.lua b/drivers/SmartThings/matter-window-covering/src/init.lua index 289c164e64..6759560c50 100644 --- a/drivers/SmartThings/matter-window-covering/src/init.lua +++ b/drivers/SmartThings/matter-window-covering/src/init.lua @@ -29,7 +29,7 @@ local battery_support = { } local REVERSE_POLARITY = "__reverse_polarity" local PRESET_LEVEL_KEY = "__preset_level_key" -local PRESET_LEVEL = 50 +local DEFAULT_PRESET_LEVEL = 50 local function find_default_endpoint(device, cluster) local res = device.MATTER_DEFAULT_ENDPOINT @@ -78,7 +78,7 @@ local function device_init(driver, device) device:emit_event(capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed = false}})) local preset_position = device:get_field(PRESET_LEVEL_KEY) or (device.preferences ~= nil and device.preferences.presetPosition) or - PRESET_LEVEL + DEFAULT_PRESET_LEVEL device:emit_event(capabilities.windowShadePreset.position(preset_position, {visibility = {displayed = false}})) device:set_field(PRESET_LEVEL_KEY, preset_position, {persist = true}) end @@ -131,25 +131,14 @@ local function device_removed(driver, device) log.info("device removed") end -- capability handlers local function handle_preset(driver, device, cmd) + local lift_value = device:get_latest_state( + "main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.position.NAME + ) or DEFAULT_PRESET_LEVEL + local hundredths_lift_percent = (100 - lift_value) * 100 local endpoint_id = device:component_to_endpoint(cmd.component) - local lift_eps = device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.LIFT}) - local tilt_eps = device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.TILT}) - if #lift_eps > 0 then - local lift_value = device:get_latest_state( - "main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.position.NAME - ) or PRESET_LEVEL - local hundredths_lift_percent = (100 - lift_value) * 100 - local req = clusters.WindowCovering.server.commands.GoToLiftPercentage( - device, endpoint_id, hundredths_lift_percent - ) - device:send(req) - end - if #tilt_eps > 0 then - local req = clusters.WindowCovering.server.commands.GoToTiltPercentage( - device, endpoint_id, PRESET_LEVEL * 100 - ) - device:send(req) - end + device:send(clusters.WindowCovering.server.commands.GoToLiftPercentage( + device, endpoint_id, hundredths_lift_percent + )) end local function handle_set_preset(driver, device, cmd) diff --git a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua index b30f2ea4bf..409ebfcb09 100644 --- a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua +++ b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua @@ -789,9 +789,6 @@ test.register_coroutine_test( test.socket.matter:__expect_send( {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, (100 - PRESET_LEVEL) * 100)} ) - test.socket.matter:__expect_send( - {mock_device.id, WindowCovering.server.commands.GoToTiltPercentage(mock_device, 10, 5000)} - ) end ) diff --git a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua index 97a79ac209..1055d53020 100644 --- a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua +++ b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua @@ -165,6 +165,7 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) { hub_logs = true }, string.format("Driver wasn't able to spin up SSDP task, cannot initialize devices.") ) + cosock.socket.sleep(30) end end end, diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 5b9977e50d..1f63ef21d3 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -479,6 +479,17 @@ local function make_ssdp_event_handler( end function SonosDriver:start_ssdp_event_task() + if self.ssdp_task ~= nil then + return + end + cosock.spawn(function () + while self:start_ssdp_event_task_inner() == false do + cosock.socket.sleep(30) + end + end) +end + +function SonosDriver:start_ssdp_event_task_inner() local ssdp_task, err = sonos_ssdp.spawn_persistent_ssdp_task() if err then log.error_with({ hub_logs = true }, string.format("Unable to create SSDP task: %s", err)) @@ -492,7 +503,9 @@ function SonosDriver:start_ssdp_event_task() end self.ssdp_event_thread_handle = cosock.spawn(make_ssdp_event_handler(self, ssdp_task_subscription, oauth_token_subscription)) + return true end + return false end ---@param api_key string diff --git a/drivers/SmartThings/zigbee-bed/src/shus-mattress/init.lua b/drivers/SmartThings/zigbee-bed/src/shus-mattress/init.lua index c11354a8d2..08109f35ce 100755 --- a/drivers/SmartThings/zigbee-bed/src/shus-mattress/init.lua +++ b/drivers/SmartThings/zigbee-bed/src/shus-mattress/init.lua @@ -36,7 +36,7 @@ end local function process_control_attr_factory(cmd) return function(driver, device, value, zb_rx) - device:emit_event(cmd.idle()) + device:emit_event(cmd("idle", { visibility = { displayed = false }})) end end @@ -129,7 +129,7 @@ local function process_capabilities_hardness_factory(cap,attrs,cap_attr) ) --A button that can be triggered continuously local evt_ctrl = cap_attr.soft() - local evt_idle = cap_attr.idle() + local evt_idle = cap_attr("idle", { visibility = { displayed = false }}) if cmd.args[cap] == "hard" then evt_ctrl = cap_attr.hard() end @@ -148,7 +148,7 @@ local function device_init(driver, device) end local function device_added(driver, device) - device:emit_event(custom_capabilities.yoga.supportedYogaState({"stop", "left", "right"})) + device:emit_event(custom_capabilities.yoga.supportedYogaState({"stop", "left", "right"}, { visibility = { displayed = false }})) do_refresh(driver, device) end diff --git a/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua b/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua index 0ba0441719..d3922a3bae 100755 --- a/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua +++ b/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua @@ -59,7 +59,7 @@ test.register_coroutine_test( function() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.yoga.supportedYogaState({"stop", "left", "right"}) )) + custom_capabilities.yoga.supportedYogaState({"stop", "left", "right"}, { visibility = { displayed = false }}) )) local read_0x0006_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0006, MFG_CODE) local read_0x0007_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0007, MFG_CODE) local read_0x0009_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0009, MFG_CODE) @@ -140,7 +140,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "Device reported leftback 0 and driver emit custom_capabilities.left_control.leftback.idle()", + "Device reported leftback 0 and driver emit custom_capabilities.left_control.leftback.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0000, data_types.Uint8.ID, 0 } @@ -150,12 +150,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.leftback.idle())) + custom_capabilities.left_control.leftback.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported leftback 1 and driver emit custom_capabilities.left_control.leftback.idle()", + "Device reported leftback 1 and driver emit custom_capabilities.left_control.leftback.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0000, data_types.Uint8.ID, 1 } @@ -165,12 +165,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.leftback.idle())) + custom_capabilities.left_control.leftback.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported leftwaist 0 and driver emit custom_capabilities.left_control.leftwaist.idle()", + "Device reported leftwaist 0 and driver emit custom_capabilities.left_control.leftwaist.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0001, data_types.Uint8.ID, 0 } @@ -180,12 +180,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.leftwaist.idle())) + custom_capabilities.left_control.leftwaist.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported leftwaist 1 and driver emit custom_capabilities.left_control.leftwaist.idle()", + "Device reported leftwaist 1 and driver emit custom_capabilities.left_control.leftwaist.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0001, data_types.Uint8.ID, 1 } @@ -195,12 +195,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.leftwaist.idle())) + custom_capabilities.left_control.leftwaist.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported lefthip 0 and driver emit custom_capabilities.left_control.lefthip.idle()", + "Device reported lefthip 0 and driver emit custom_capabilities.left_control.lefthip.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0002, data_types.Uint8.ID, 0 } @@ -210,12 +210,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.lefthip.idle())) + custom_capabilities.left_control.lefthip.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported lefthip 1 and driver emit custom_capabilities.left_control.lefthip.idle()", + "Device reported lefthip 1 and driver emit custom_capabilities.left_control.lefthip.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0002, data_types.Uint8.ID, 1 } @@ -225,12 +225,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.lefthip.idle())) + custom_capabilities.left_control.lefthip.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported rightback 0 and driver emit custom_capabilities.right_control.rightback.idle()", + "Device reported rightback 0 and driver emit custom_capabilities.right_control.rightback.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0003, data_types.Uint8.ID, 0 } @@ -240,12 +240,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.rightback.idle())) + custom_capabilities.right_control.rightback.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported rightback 1 and driver emit custom_capabilities.right_control.rightback.idle()", + "Device reported rightback 1 and driver emit custom_capabilities.right_control.rightback.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0003, data_types.Uint8.ID, 1 } @@ -255,12 +255,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.rightback.idle())) + custom_capabilities.right_control.rightback.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported rightwaist 0 and driver emit custom_capabilities.right_control.rightwaist.idle()", + "Device reported rightwaist 0 and driver emit custom_capabilities.right_control.rightwaist.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0004, data_types.Uint8.ID, 0 } @@ -270,12 +270,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.rightwaist.idle())) + custom_capabilities.right_control.rightwaist.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported rightwaist 1 and driver emit custom_capabilities.right_control.rightwaist.idle()", + "Device reported rightwaist 1 and driver emit custom_capabilities.right_control.rightwaist.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0004, data_types.Uint8.ID, 1 } @@ -285,12 +285,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.rightwaist.idle())) + custom_capabilities.right_control.rightwaist.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported righthip 0 and driver emit custom_capabilities.right_control.righthip.idle()", + "Device reported righthip 0 and driver emit custom_capabilities.right_control.righthip.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0005, data_types.Uint8.ID, 0 } @@ -300,12 +300,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.righthip.idle())) + custom_capabilities.right_control.righthip.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported righthip 1 and driver emit custom_capabilities.right_control.righthip.idle()", + "Device reported righthip 1 and driver emit custom_capabilities.right_control.righthip.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0005, data_types.Uint8.ID, 1 } @@ -315,7 +315,7 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.righthip.idle())) + custom_capabilities.right_control.righthip.idle({ visibility = { displayed = false }}))) end ) diff --git a/drivers/SmartThings/zigbee-button/fingerprints.yml b/drivers/SmartThings/zigbee-button/fingerprints.yml index 61285448f7..c70cae5eac 100644 --- a/drivers/SmartThings/zigbee-button/fingerprints.yml +++ b/drivers/SmartThings/zigbee-button/fingerprints.yml @@ -24,6 +24,16 @@ zigbeeManufacturer: manufacturer: LUMI model: lumi.remote.b286acn03 deviceProfileName: aqara-double-buttons + - id: "LUMI/lumi.remote.b18ac1" + deviceLabel: Aqara Wireless Remote Switch H1 (Single Rocker) + manufacturer: LUMI + model: lumi.remote.b18ac1 + deviceProfileName: aqara-single-button-mode + - id: "LUMI/lumi.remote.b28ac1" + deviceLabel: Aqara Wireless Remote Switch H1 (Double Rocker) + manufacturer: LUMI + model: lumi.remote.b28ac1 + deviceProfileName: aqara-double-buttons-mode - id: "HEIMAN/SOS-EM" deviceLabel: HEIMAN Button manufacturer: HEIMAN diff --git a/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons-mode.yml b/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons-mode.yml new file mode 100644 index 0000000000..f19f19da57 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons-mode.yml @@ -0,0 +1,35 @@ +name: aqara-double-buttons-mode +components: + - id: main + capabilities: + - id: button + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button1 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: all + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +preferences: + - preferenceId: stse.allowOperationModeChange + explicit: true diff --git a/drivers/SmartThings/zigbee-button/profiles/aqara-single-button-mode.yml b/drivers/SmartThings/zigbee-button/profiles/aqara-single-button-mode.yml new file mode 100644 index 0000000000..904de72281 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/aqara-single-button-mode.yml @@ -0,0 +1,17 @@ +name: aqara-single-button-mode +components: + - id: main + capabilities: + - id: button + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Button +preferences: + - preferenceId: stse.allowOperationModeChange + explicit: true diff --git a/drivers/SmartThings/zigbee-button/src/aqara/init.lua b/drivers/SmartThings/zigbee-button/src/aqara/init.lua index c54e34be42..baa03c4f34 100644 --- a/drivers/SmartThings/zigbee-button/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-button/src/aqara/init.lua @@ -19,11 +19,15 @@ local data_types = require "st.zigbee.data_types" local capabilities = require "st.capabilities" local button_utils = require "button_utils" +local MODE = "devicemode" +local MODE_CHANGE = "stse.allowOperationModeChange" +local SUPPORTED_BUTTON = { { "pushed" }, { "pushed", "held", "double" } } local PowerConfiguration = clusters.PowerConfiguration local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID_T1 = 0x0009 local PRIVATE_ATTRIBUTE_ID_E1 = 0x0125 +local PRIVATE_ATTRIBUTE_ID_ALIVE = 0x00F7 local MFG_CODE = 0x115F local MULTISTATE_INPUT_CLUSTER_ID = 0x0012 @@ -34,7 +38,9 @@ local FINGERPRINTS = { ["lumi.remote.b1acn02"] = { mfr = "LUMI", btn_cnt = 1 }, ["lumi.remote.acn003"] = { mfr = "LUMI", btn_cnt = 1 }, ["lumi.remote.b186acn03"] = { mfr = "LUMI", btn_cnt = 1 }, - ["lumi.remote.b286acn03"] = { mfr = "LUMI", btn_cnt = 3 } + ["lumi.remote.b286acn03"] = { mfr = "LUMI", btn_cnt = 3 }, + ["lumi.remote.b18ac1"] = { mfr = "LUMI", btn_cnt = 1 }, + ["lumi.remote.b28ac1"] = { mfr = "LUMI", btn_cnt = 3 } } local configuration = { @@ -103,6 +109,36 @@ local function battery_level_handler(driver, device, value, zb_rx) end end +local function mode_switching_handler(driver, device, value, zb_rx) + local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 + local allow = device.preferences[MODE_CHANGE] or false + if allow then + local mode = device:get_field(MODE) or 1 + mode = 3 - mode + device:set_field(MODE, mode, { persist = true }) + device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, + MFG_CODE, data_types.Uint8, mode)) + device:emit_event(capabilities.button.supportedButtonValues(SUPPORTED_BUTTON[mode], + { visibility = { displayed = false } })) + device:emit_event(capabilities.button.numberOfButtons({ value = 1 })) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, + capabilities.button.button.pushed({ state_change = false })) + if btn_evt_cnt > 1 then + for i = 1, btn_evt_cnt do + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.supportedButtonValues(SUPPORTED_BUTTON[mode], + { visibility = { displayed = false } })) + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.numberOfButtons({ value = 1 })) + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.button.pushed({ state_change = false })) + button_utils.emit_event_if_latest_state_missing(device, COMP_LIST[i], capabilities.button, + capabilities.button.button.NAME, capabilities.button.button.pushed({ state_change = false })) + end + end + end +end + local is_aqara_products = function(opts, driver, device) local isAqaraProducts = false if FINGERPRINTS[device:get_model()] and FINGERPRINTS[device:get_model()].mfr == device:get_manufacturer() then @@ -122,8 +158,18 @@ end local function added_handler(self, device) local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 - - device:emit_event(capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, + local mode = device:get_field(MODE) or 0 + local model = device:get_model() + + if mode == 0 then + if model == "lumi.remote.b18ac1" or model == "lumi.remote.b28ac1" then + mode = 1 + else + mode = 2 + end + end + device:set_field(MODE, mode, { persist = true }) + device:emit_event(capabilities.button.supportedButtonValues(SUPPORTED_BUTTON[mode], { visibility = { displayed = false } })) device:emit_event(capabilities.button.numberOfButtons({ value = 1 })) button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, @@ -135,7 +181,7 @@ local function added_handler(self, device) if btn_evt_cnt > 1 then for i = 1, btn_evt_cnt do device:emit_component_event(device.profile.components[COMP_LIST[i]], - capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, + capabilities.button.supportedButtonValues(SUPPORTED_BUTTON[mode], { visibility = { displayed = false } })) device:emit_component_event(device.profile.components[COMP_LIST[i]], capabilities.button.numberOfButtons({ value = 1 })) @@ -179,6 +225,9 @@ local aqara_wireless_switch_handler = { }, [PowerConfiguration.ID] = { [PowerConfiguration.attributes.BatteryVoltage.ID] = battery_level_handler + }, + [PRIVATE_CLUSTER_ID] = { + [PRIVATE_ATTRIBUTE_ID_ALIVE] = mode_switching_handler } } }, diff --git a/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua index 1d7b6d090c..838b9545fb 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua @@ -28,6 +28,8 @@ local MFG_CODE = 0x115F local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID_T1 = 0x0009 local PRIVATE_ATTRIBUTE_ID_E1 = 0x0125 +local PRIVATE_ATTRIBUTE_ID_ALIVE = 0x00F7 +local MODE_CHANGE = "stse.allowOperationModeChange" local COMP_LIST = { "button1", "button2", "all" } local mock_device_e1 = test.mock_device.build_test_zigbee_device( @@ -44,9 +46,9 @@ local mock_device_e1 = test.mock_device.build_test_zigbee_device( } ) -local mock_device_t1_double_rocker = test.mock_device.build_test_zigbee_device( +local mock_device_h1_double_rocker = test.mock_device.build_test_zigbee_device( { - profile = t_utils.get_profile_definition("aqara-double-buttons.yml"), + profile = t_utils.get_profile_definition("aqara-double-buttons-mode.yml"), zigbee_endpoints = { [1] = { id = 1, @@ -61,7 +63,7 @@ local mock_device_t1_double_rocker = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() test.mock_device.add_test_device(mock_device_e1) - test.mock_device.add_test_device(mock_device_t1_double_rocker) + test.mock_device.add_test_device(mock_device_h1_double_rocker) end test.set_test_init_function(test_init) @@ -69,28 +71,27 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Handle added lifecycle - T1 double rocker", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_t1_double_rocker.id, "added" }) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.device_lifecycle:__queue_receive({ mock_device_h1_double_rocker.id, "added" }) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.button.numberOfButtons({ value = 1 }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.button.button.pushed({ state_change = false }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.batteryLevel.battery.normal())) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.batteryLevel.type("CR2032"))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.batteryLevel.quantity(1))) for i = 1, 3 do - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message(COMP_LIST[i], + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message(COMP_LIST[i], + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], capabilities.button.numberOfButtons({ value = 1 }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message(COMP_LIST[i], + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], capabilities.button.button.pushed({ state_change = false }))) - end - + end end ) @@ -118,8 +119,8 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device_e1.id, cluster_base.write_manufacturer_specific_attribute(mock_device_e1, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, MFG_CODE, - data_types.Uint8, 2) }) - mock_device_e1:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + data_types.Uint8, 2) }) + mock_device_e1:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) @@ -127,31 +128,31 @@ test.register_coroutine_test( test.register_coroutine_test( "Handle doConfigure lifecycle -- t1", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_t1_double_rocker.id, "doConfigure" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_h1_double_rocker.id, "doConfigure" }) test.socket.zigbee:__expect_send({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_bind_request(mock_device_t1_double_rocker, zigbee_test_utils.mock_hub_eui, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_bind_request(mock_device_h1_double_rocker, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID) }) test.socket.zigbee:__expect_send({ - mock_device_t1_double_rocker.id, - PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device_t1_double_rocker, 30, 3600, 1) + mock_device_h1_double_rocker.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device_h1_double_rocker, 30, 3600, 1) }) test.socket.zigbee:__expect_send({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_bind_request(mock_device_t1_double_rocker, zigbee_test_utils.mock_hub_eui, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_bind_request(mock_device_h1_double_rocker, zigbee_test_utils.mock_hub_eui, MULTISTATE_INPUT_CLUSTER_ID) }) test.socket.zigbee:__expect_send({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_attr_config(mock_device_t1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, PRESENT_ATTRIBUTE_ID, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attr_config(mock_device_h1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, PRESENT_ATTRIBUTE_ID, 0x0003, 0x1C20, data_types.Uint16, 0x0001) }) - test.socket.zigbee:__expect_send({ mock_device_t1_double_rocker.id, - cluster_base.write_manufacturer_specific_attribute(mock_device_t1_double_rocker, PRIVATE_CLUSTER_ID, + test.socket.zigbee:__expect_send({ mock_device_h1_double_rocker.id, + cluster_base.write_manufacturer_specific_attribute(mock_device_h1_double_rocker, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_T1, MFG_CODE, - data_types.Uint8, 1) }) - mock_device_t1_double_rocker:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + data_types.Uint8, 1) }) + mock_device_h1_double_rocker:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) @@ -162,14 +163,14 @@ test.register_coroutine_test( { PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0001 } } test.socket.zigbee:__queue_receive({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_attribute_report(mock_device_t1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.pushed({ state_change = true }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("button1", - capabilities.button.button.pushed({state_change = true}))) end ) @@ -180,14 +181,14 @@ test.register_coroutine_test( { PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0002 } } test.socket.zigbee:__queue_receive({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_attribute_report(mock_device_t1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.double({ state_change = true }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("button1", capabilities.button.button.double({ state_change = true }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("button1", - capabilities.button.button.double({state_change = true}))) end ) @@ -198,13 +199,13 @@ test.register_coroutine_test( { PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0000 } } test.socket.zigbee:__queue_receive({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_attribute_report(mock_device_t1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", - capabilities.button.button.held({state_change = true}))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("button1", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.held({ state_change = true }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("button1", capabilities.button.button.held({ state_change = true }))) end ) @@ -254,4 +255,46 @@ test.register_message_test( } } ) + +test.register_coroutine_test( + "Wireless Remote Switch H1 Mode Change", + function() + local mode = 2 + local updates = { + preferences = { + [MODE_CHANGE] = true + } + } + test.socket.device_lifecycle:__queue_receive(mock_device_h1_double_rocker:generate_info_changed(updates)) + mock_device_h1_double_rocker:set_field("devicemode", 1, { persist = true }) + local attr_report_data = { + { PRIVATE_ATTRIBUTE_ID_ALIVE, data_types.OctetString.ID, "\x01\x21\xB8\x0B\x03\x28\x19\x04\x21\xA8\x13\x05\x21\x45\x08\x06\x24\x07\x00\x00\x00\x00\x08\x21\x15\x01\x0A\x21\xF5\x65\x0C\x20\x01\x64\x20\x01\x66\x20\x03\x67\x20\x01\x68\x21\xA8\x00" } + } + test.wait_for_events() + test.socket.zigbee:__queue_receive({ + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, PRIVATE_CLUSTER_ID, attr_report_data, + MFG_CODE) + }) + test.socket.zigbee:__expect_send({ mock_device_h1_double_rocker.id, cluster_base + .write_manufacturer_specific_attribute(mock_device_h1_double_rocker, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, + MFG_CODE, data_types.Uint8, mode) }) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 1 }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.pushed({ state_change = false }))) + + for i = 1, 3 do + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.numberOfButtons({ value = 1 }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.button.pushed({ state_change = false }))) + end + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml index 5096f9cb25..68359710fe 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml +++ b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml @@ -58,6 +58,11 @@ zigbeeManufacturer: manufacturer: HEIMAN model: HT-EF-3.0 deviceProfileName: humidity-temp-battery + - id: frient/AQSZB-110 + deviceLabel: Air Quality Sensor + manufacturer: frient A/S + model: AQSZB-110 + deviceProfileName: frient-airquality-humidity-temperature-battery - id: frient/HMSZB-110 deviceLabel: frient Humidity Sensor manufacturer: frient A/S diff --git a/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-airquality-humidity-temperature-battery.yml b/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-airquality-humidity-temperature-battery.yml new file mode 100644 index 0000000000..08774c0d31 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-airquality-humidity-temperature-battery.yml @@ -0,0 +1,52 @@ +name: frient-airquality-humidity-temperature-battery +components: +- id: main + capabilities: + - id: airQualitySensor + version: 1 + - id: tvocMeasurement + version: 1 + - id: tvocHealthConcern + version: 1 + config: + values: + - key: "tvocHealthConcern.value" + enabledValues: + - good + - moderate + - slightlyUnhealthy + - unhealthy + - veryUnhealthy + - id: relativeHumidityMeasurement + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 +preferences: + - preferenceId: humidityOffset + explicit: true + - title: "Humidity Sensitivity (%)" + name: humiditySensitivity + description: "Minimum change in humidity level to report" + required: false + preferenceType: number + definition: + minimum: 1 + maximum: 50 + default: 3 + - preferenceId: tempOffset + explicit: true + - title: "Temperature Sensitivity (°)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua index 3a4e495e3e..b040a4f73f 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua @@ -23,7 +23,8 @@ local devices = { FRIENT_HUMIDITY_TEMP_SENSOR = { FINGERPRINTS = { { mfr = "frient A/S", model = "HMSZB-110" }, - { mfr = "frient A/S", model = "HMSZB-120" } + { mfr = "frient A/S", model = "HMSZB-120" }, + { mfr = "frient A/S", model = "AQSZB-110" } }, CONFIGURATION = { { diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua new file mode 100644 index 0000000000..e33c1b07e7 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua @@ -0,0 +1,156 @@ +local capabilities = require "st.capabilities" +local util = require "st.utils" +local data_types = require "st.zigbee.data_types" +local zcl_clusters = require "st.zigbee.zcl.clusters" +local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement +local HumidityMeasurement = zcl_clusters.RelativeHumidity +local PowerConfiguration = zcl_clusters.PowerConfiguration +local device_management = require "st.zigbee.device_management" +local cluster_base = require "st.zigbee.cluster_base" +local battery_defaults = require "st.zigbee.defaults.battery_defaults" +local configurationMap = require "configurations" + +local FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS = { + { mfr = "frient A/S", model = "AQSZB-110", subdriver = "airquality" } +} + +local function can_handle_frient(opts, driver, device, ...) + for _, fingerprint in ipairs(FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model and fingerprint.subdriver == "airquality" then + return true + end + end + return false +end + +local Frient_VOCMeasurement = { + ID = 0xFC03, + ManufacturerSpecificCode = 0x1015, + attributes = { + MeasuredValue = { ID = 0x0000, base_type = data_types.Uint16 }, + MinMeasuredValue = { ID = 0x0001, base_type = data_types.Uint16 }, + MaxMeasuredValue = { ID = 0x0002, base_type = data_types.Uint16 }, + Resolution = { ID = 0x0003, base_type = data_types.Uint16 }, + }, +} + +Frient_VOCMeasurement.attributes.MeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.MinMeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.MaxMeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.Resolution._cluster = Frient_VOCMeasurement + +local MAX_VOC_REPORTABLE_VALUE = 5500 -- Max VOC reportable value + +--- Table to map VOC (ppb) to HealthConcern +local VOC_TO_HEALTHCONCERN_MAPPING = { + [2201] = "veryUnhealthy", + [661] = "unhealthy", + [221] = "slightlyUnhealthy", + [66] = "moderate", + [0] = "good", +} + +--- Map VOC (ppb) to HealthConcern +local function voc_to_healthconcern(raw_voc) + for voc, perc in util.rkeys(VOC_TO_HEALTHCONCERN_MAPPING) do + if raw_voc >= voc then + return perc + end + end +end +--- Map VOC (ppb) to CAQI +local function voc_to_caqi(raw_voc) + if (raw_voc > 5500) then + return 100 + else + return math.floor(raw_voc*99/5500) + end +end + +-- May take around 8 minutes for the first valid VOC measurement to be reported after the device is powered on +local function voc_measure_value_attr_handler(driver, device, attr_val, zb_rx) + local voc_value = attr_val.value + if (voc_value < 65535) then -- ignore it if it's outside the limits + voc_value = util.clamp_value(voc_value, 0, MAX_VOC_REPORTABLE_VALUE) + device:emit_event(capabilities.airQualitySensor.airQuality({ value = voc_to_caqi(voc_value)})) + device:emit_event(capabilities.tvocHealthConcern.tvocHealthConcern(voc_to_healthconcern(voc_value))) + device:emit_event(capabilities.tvocMeasurement.tvocLevel({ value = voc_value, unit = "ppb" })) + end +end + +-- The device sends the value of MeasuredValue to be 0x8000, which corresponds to -327.68C, until it gets the first valid measurement. Therefore we don't emit event before the value is correct. It may take up to 4 minutes +local function temperatureHandler(driver, device, attr_val, zb_rx) + local temp_value = attr_val.value + if (temp_value > -32768) then + device:emit_event(capabilities.temperatureMeasurement.temperature({ value = temp_value / 100, unit = "C" })) + end +end + +local function device_init(driver, device) + battery_defaults.build_linear_voltage_init(2.3, 3.0)(driver, device) + local configuration = configurationMap.get_device_configuration(device) + if configuration ~= nil then + for _, attribute in ipairs(configuration) do + device:add_configured_attribute(attribute) + end + end +end + +local function device_added(driver, device) + device:emit_event(capabilities.airQualitySensor.airQuality(voc_to_caqi(0))) + device:emit_event(capabilities.tvocHealthConcern.tvocHealthConcern(voc_to_healthconcern(0))) + device:emit_event(capabilities.tvocMeasurement.tvocLevel({ value = 0, unit = "ppb" })) +end + +local function do_refresh(driver, device) + for _, fingerprint in ipairs(FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + device:send(cluster_base.read_manufacturer_specific_attribute(device, Frient_VOCMeasurement.ID, Frient_VOCMeasurement.attributes.MeasuredValue.ID, Frient_VOCMeasurement.ManufacturerSpecificCode):to_endpoint(0x26)) + device:send(TemperatureMeasurement.attributes.MeasuredValue:read(device):to_endpoint(0x26)) + device:send(HumidityMeasurement.attributes.MeasuredValue:read(device):to_endpoint(0x26)) + device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) + end + end +end + +local function do_configure(driver, device) + device:configure() + device:send(device_management.build_bind_request(device, Frient_VOCMeasurement.ID, driver.environment_info.hub_zigbee_eui, 0x26)) + + device:send( + cluster_base.configure_reporting( + device, + data_types.ClusterId(Frient_VOCMeasurement.ID), + Frient_VOCMeasurement.attributes.MeasuredValue.ID, + Frient_VOCMeasurement.attributes.MeasuredValue.base_type.ID, + 60, 600, 10 + ):to_endpoint(0x26) + ) + + device.thread:call_with_delay(5, function() + do_refresh(driver, device) + end) +end + +local frient_airquality_sensor = { + NAME = "frient Air Quality Sensor", + lifecycle_handlers = { + init = device_init, + added = device_added, + doConfigure = do_configure, + }, + zigbee_handlers = { + cluster = {}, + attr = { + [Frient_VOCMeasurement.ID] = { + [Frient_VOCMeasurement.attributes.MeasuredValue.ID] = voc_measure_value_attr_handler, + }, + [TemperatureMeasurement.ID] = { + [TemperatureMeasurement.attributes.MeasuredValue.ID] = temperatureHandler, + }, + } + }, + can_handle = can_handle_frient +} + +return frient_airquality_sensor \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua index 7791e6752b..1356345ff2 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua @@ -20,7 +20,8 @@ local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement local FRIENT_TEMP_HUMUDITY_SENSOR_FINGERPRINTS = { { mfr = "frient A/S", model = "HMSZB-110" }, - { mfr = "frient A/S", model = "HMSZB-120" } + { mfr = "frient A/S", model = "HMSZB-120" }, + { mfr = "frient A/S", model = "AQSZB-110" } } local function can_handle_frient_sensor(opts, driver, device) @@ -73,6 +74,9 @@ local frient_sensor = { doConfigure = do_configure, infoChanged = info_changed }, + sub_drivers = { + require("frient-sensor/air-quality") + }, can_handle = can_handle_frient_sensor } diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_air_quality_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_air_quality_sensor.lua new file mode 100644 index 0000000000..607a8aa19e --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_air_quality_sensor.lua @@ -0,0 +1,305 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" + +local PowerConfiguration = clusters.PowerConfiguration +local TemperatureMeasurement = clusters.TemperatureMeasurement +local HumidityMeasurement = clusters.RelativeHumidity + +local Frient_VOCMeasurement = { + ID = 0xFC03, + ManufacturerSpecificCode = 0x1015, + attributes = { + MeasuredValue = { ID = 0x0000, base_type = data_types.Uint16 }, + MinMeasuredValue = { ID = 0x0001, base_type = data_types.Uint16 }, + MaxMeasuredValue = { ID = 0x0002, base_type = data_types.Uint16 }, + Resolution = { ID = 0x0003, base_type = data_types.Uint16 }, + }, +} + +Frient_VOCMeasurement.attributes.MeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.MinMeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.MaxMeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.Resolution._cluster = Frient_VOCMeasurement + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("frient-airquality-humidity-temperature-battery.yml"), + zigbee_endpoints = { + [0x26] = { + id = 0x26, + manufacturer = "frient A/S", + model = "AQSZB-110", + server_clusters = {0x0001, 0x0402, 0x0405, 0xFC03} + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "Refresh should read all necessary attributes", + { + { + channel = "capability", + direction = "receive", + message = {mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_message_test( + "Min battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 23) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +test.register_message_test( + "Max battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 30) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + +test.register_coroutine_test( + "Configure should configure all necessary attributes", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device, 30, 21600, 1) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + TemperatureMeasurement.ID + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 0x001E, 0x0E10, 100) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + HumidityMeasurement.ID + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 60, 3600, 300) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + Frient_VOCMeasurement.ID, + 38 + ):to_endpoint(0x26) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.configure_reporting( + mock_device, + data_types.ClusterId(Frient_VOCMeasurement.ID), + Frient_VOCMeasurement.attributes.MeasuredValue.ID, + Frient_VOCMeasurement.attributes.MeasuredValue.base_type.ID, + 60, 600, 10 + ):to_endpoint(0x26) + }) + + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_message_test( + "Humidity report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 0x1950) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 65 })) + } + } +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + } + } +) + +test.register_coroutine_test( + "info_changed to check for necessary preferences settings: Temperature Sensitivity", + function() + local updates = { + preferences = { + temperatureSensitivity = 0.9, + humiditySensitivity = 10 + } + } + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + local temperatureSensitivity = math.floor(0.9 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 3600, + temperatureSensitivity + ) + }) + local humiditySensitivity = math.floor(10 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 60, + 3600, + humiditySensitivity + ) + }) + test.wait_for_events() + end +) + +test.register_message_test( + "VOC measurement report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, cluster_base.build_test_attr_report(Frient_VOCMeasurement.attributes.MeasuredValue, mock_device, 300) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.airQualitySensor.airQuality({ value = 5 })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tvocHealthConcern.tvocHealthConcern({ value = "slightlyUnhealthy" })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tvocMeasurement.tvocLevel({ value = 300, unit = "ppb" })) + } + }, + { + inner_block_ordering = "relaxed" + } +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index d5f2e4d8df..3dfa9970b5 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2359,6 +2359,11 @@ zigbeeManufacturer: model: NEXENTRO Dimming Actuator deviceProfileName: on-off-level # Inovelli + - id: "Inovelli/VZM30-SN" + deviceLabel: "Inovelli On/Off Blue Series" + manufacturer: Inovelli + model: VZM30-SN + deviceProfileName: inovelli-vzm30-sn - id: "Inovelli/VZM31-SN" deviceLabel: "Inovelli 2-in-1 Blue Series" manufacturer: Inovelli diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml new file mode 100644 index 0000000000..ff670c0097 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml @@ -0,0 +1,231 @@ +name: inovelli-vzm30-sn +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + - id: temperatureMeasurement + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + - id: firmwareUpdate + version: 1 + categories: + - name: Switch + - id: button1 + label: Down Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + label: Up Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + label: Config Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +preferences: + - name: "notificationChild" + title: "Add Child Device - Notification" + description: "Create Separate Child Device for Notification Control" + required: false + preferenceType: boolean + definition: + default: false + - name: "notificationType" + title: "Notification Effect" + description: "This is the notification effect used by the notification child device" + required: false + preferenceType: enumeration + definition: + options: + "255": "Clear" + "1": "Solid" + "2": "Fast Blink" + "3": "Slow Blink" + "4": "Pulse" + "5": "Chase" + "6": "Open/Close" + "7": "Small-to-Big" + "8": "Aurora" + "9": "Slow Falling" + "10": "Medium Falling" + "11": "Fast Falling" + "12": "Slow Rising" + "13": "Medium Rising" + "14": "Fast Rising" + "15": "Medium Blink" + "16": "Slow Chase" + "17": "Fast Chase" + "18": "Fast Siren" + "19": "Slow Siren" + default: 1 + - name: "parameter258" + title: "258. Switch Mode" + description: "Use as a Dimmer or an On/Off switch" + required: false + preferenceType: enumeration + definition: + options: + "0": "Dimmer" + "1": "On/Off (default)" + default: 1 + - name: "parameter22" + title: "22. Aux Switch Type" + description: "Set the Aux switch type. Smart Bulb Mode does not work in Dumb 3-Way Switch mode." + required: false + preferenceType: enumeration + definition: + options: + "0": "None" + "1": "3-Way Aux Switch (default)" + default: 1 + - name: "parameter52" + title: "52. Smart Bulb Mode" + description: "For use with Smart Bulbs that need constant power and are controlled via commands rather than power. Smart Bulb Mode does not work in Dumb 3-Way Switch mode." + required: false + preferenceType: enumeration + definition: + options: + "0": "Disabled (default)" + "1": "Smart Bulb Mode" + default: 0 + - name: "parameter1" + title: "1. Dimming Speed (Remote)" + description: "This changes the speed that the light dims up when controlled from the hub. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + Default=25 (2500ms or 2.5s)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 126 + default: 25 + - name: "parameter2" + title: "2. Dimming Speed (Local)" + description: "This changes the speed that the light dims up when controlled at the switch. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=127 (Sync with parameter 1)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 127 + default: 127 + - name: "parameter3" + title: "3. Ramp Rate (Remote)" + description: "This changes the speed that the light turns on when controlled from the hub. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=0" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 127 + default: 0 + - name: "parameter4" + title: "4. Ramp Rate (Local)" + description: "This changes the speed that the light turns on when controlled at the switch. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=127 (Sync with parameter 3)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 127 + default: 127 + - name: "parameter11" + title: "11. Invert Switch" + description: "Inverts the orientation of the switch. Useful when the switch is installed upside down. Essentially up becomes down and down becomes up." + required: false + preferenceType: enumeration + definition: + options: + "0": "No (default)" + "1": "Yes" + default: 0 + - name: "parameter15" + title: "15. Level After Power Restored" + description: "The level the switch will return to when power is restored after power failure. + 0=Off + 1-100=Set Level + 101=Use previous level." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 101 + default: 101 + - name: "parameter95" + title: "95. LED Indicator Color (w/On)" + description: "Set the color of the Full LED Indicator when the load is on." + required: false + preferenceType: enumeration + definition: + options: + "0": "Red" + "7": "Orange" + "28": "Lemon" + "64": "Lime" + "85": "Green" + "106": "Teal" + "127": "Cyan" + "148": "Aqua" + "170": "Blue (default)" + "190": "Violet" + "212": "Magenta" + "234": "Pink" + "255": "White" + default: 170 + - name: "parameter96" + title: "96. LED Indicator Color (w/Off)" + description: "Set the color of the Full LED Indicator when the load is off." + required: false + preferenceType: enumeration + definition: + options: + "0": "Red" + "7": "Orange" + "28": "Lemon" + "64": "Lime" + "85": "Green" + "106": "Teal" + "127": "Cyan" + "148": "Aqua" + "170": "Blue (default)" + "190": "Violet" + "212": "Magenta" + "234": "Pink" + "255": "White" + default: 170 + - name: "parameter97" + title: "97. LED Indicator Intensity (w/On)" + description: "Set the intensity of the Full LED Indicator when the load is on." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 100 + default: 50 + - name: "parameter98" + title: "98. LED Indicator Intensity (w/Off)" + description: "Set the intensity of the Full LED Indicator when the load is off." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 100 + default: 5 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index 696ff8ada9..1b694d2a5e 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -66,6 +66,8 @@ local zigbee_switch_driver_template = { capabilities.energyMeter, capabilities.motionSensor, capabilities.illuminanceMeasurement, + capabilities.relativeHumidityMeasurement, + capabilities.temperatureMeasurement, }, sub_drivers = { lazy_load_if_possible("non_zigbee_devices"), diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua index 473c1cb807..d9b676e307 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua @@ -4,6 +4,7 @@ return function(opts, driver, device) local INOVELLI_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM30-SN" }, { mfr = "Inovelli", model = "VZM31-SN" }, { mfr = "Inovelli", model = "VZM32-SN" } } diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua index 33d3ff577c..c402731328 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua @@ -4,9 +4,27 @@ local clusters = require "st.zigbee.zcl.clusters" local device_management = require "st.zigbee.device_management" local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade +local zigbee_constants = require "st.zigbee.constants" +local capabilities = require "st.capabilities" local M = {} +M.supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} +} + +-- Utility function to check if device is VZM32-SN +function M.is_vzm32(device) + return device:get_model() == "VZM32-SN" +end + +-- Utility function to check if device is VZM32-SN +function M.is_vzm30(device) + return device:get_model() == "VZM30-SN" +end + -- Sends a generic configure for Inovelli devices (all models): -- - device:configure -- - send OTA ImageNotify @@ -25,10 +43,31 @@ function M.base_device_configure(driver, device, private_cluster_id, mfg_code) device:send(device_management.build_bind_request(device, private_cluster_id, driver.environment_info.hub_zigbee_eui, 2)) -- Read divisors/multipliers for power/energy reporting - device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) + -- Set default divisor to 1000 for VZM32-SN and VZM30-SN. In initial firmware the divisor is incorrectly set to 100. + if M.is_vzm32(device) or M.is_vzm30(device) then + device:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 1000, {persist = true}) + else + device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) + end device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) device:send(clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(device)) device:send(clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(device)) + + for _, component in pairs(device.profile.components) do + if component.id ~= "main" then + device:emit_component_event( + component, + capabilities.button.supportedButtonValues( + M.supported_button_values[component.id], + { visibility = { displayed = false } } + ) + ) + device:emit_component_event( + component, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + end + end end return M \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua index 73967b41f3..a83a343663 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua @@ -30,8 +30,6 @@ local base_preference_map = { parameter2 = {parameter_number = 2, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter3 = {parameter_number = 3, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter4 = {parameter_number = 4, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, - parameter9 = {parameter_number = 9, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, - parameter10 = {parameter_number = 10, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter15 = {parameter_number = 15, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter95 = {parameter_number = 95, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter96 = {parameter_number = 96, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, @@ -41,12 +39,20 @@ local base_preference_map = { -- Model-specific overrides/additions local model_preference_overrides = { + ["VZM30-SN"] = { + parameter11 = {parameter_number = 11, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, + parameter22 = {parameter_number = 22, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + }, ["VZM31-SN"] = { + parameter9 = {parameter_number = 9, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter10 = {parameter_number = 10, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter11 = {parameter_number = 11, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, parameter17 = {parameter_number = 17, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter22 = {parameter_number = 22, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, }, ["VZM32-SN"] = { + parameter9 = {parameter_number = 9, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter10 = {parameter_number = 10, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter34 = {parameter_number = 34, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter101 = {parameter_number = 101, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, parameter102 = {parameter_number = 102, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, @@ -126,9 +132,55 @@ local function scene_handler(driver, device, zb_rx) local capability_attribute = map_key_attribute_to_capability[bytes:byte(2)] local additional_fields = { state_change = true } local event = capability_attribute and capability_attribute(additional_fields) or nil - local comp = device.profile.components[button_to_component(button_number)] + local comp_name = button_to_component(button_number) + local comp = device.profile.components[comp_name] if comp ~= nil and event ~= nil then - device:emit_component_event(comp, event) + -- Check if the event is in the supportedButtonValues before emitting + -- This ensures backward compatibility with devices installed with previous driver versions + local expected_values = inovelli_common.supported_button_values[comp_name] + local supportedEvents = device:get_latest_state( + comp_name, + capabilities.button.ID, + capabilities.button.supportedButtonValues.NAME, + {capabilities.button.button.pushed.NAME} -- default fallback for older devices + ) + + -- Check if supportedButtonValues needs to be updated + -- This handles devices installed with previous driver versions that don't have + -- the updated supportedButtonValues attribute. If the current value only contains + -- "pushed" (the fallback), update it to the full list. + local needs_update = false + if expected_values then + -- Check if current supportedEvents is exactly the fallback (only "pushed") + -- This indicates the state was never set and we're using the fallback value + if #supportedEvents == 1 and supportedEvents[1] == capabilities.button.button.pushed.NAME then + needs_update = true + end + + if needs_update then + device:emit_component_event( + comp, + capabilities.button.supportedButtonValues( + expected_values, + { visibility = { displayed = false } } + ) + ) + supportedEvents = expected_values -- Update local reference for event check + end + end + + -- Check if the event is supported + local event_supported = false + for _, event_name in pairs(supportedEvents) do + if event.value.value == event_name then + event_supported = true + break + end + end + + if event_supported then + device:emit_component_event(comp, event) + end end end @@ -200,18 +252,6 @@ local function device_configure(driver, device) end end -local function energy_meter_handler(driver, device, value, zb_rx) - local raw_value = value.value - raw_value = raw_value / 100 - device:emit_event(capabilities.energyMeter.energy({value = raw_value, unit = "kWh" })) -end - -local function power_meter_handler(driver, device, value, zb_rx) - local raw_value = value.value - raw_value = raw_value / 10 - device:emit_event(capabilities.powerMeter.power({value = raw_value, unit = "W" })) -end - local function huePercentToValue(value) if value <= 2 then return 0 elseif value >= 98 then return 255 @@ -328,7 +368,7 @@ local function handle_resetEnergyMeter(self, device) end local inovelli = { - NAME = "inovelli combined handler", + NAME = "Inovelli Zigbee Switch", lifecycle_handlers = { doConfigure = device_configure, infoChanged = info_changed, @@ -336,13 +376,6 @@ local inovelli = { }, zigbee_handlers = { attr = { - [clusters.SimpleMetering.ID] = { - [clusters.SimpleMetering.attributes.InstantaneousDemand.ID] = power_meter_handler, - [clusters.SimpleMetering.attributes.CurrentSummationDelivered.ID] = energy_meter_handler - }, - [clusters.ElectricalMeasurement.ID] = { - [clusters.ElectricalMeasurement.attributes.ActivePower.ID] = power_meter_handler - }, [OccupancySensing.ID] = { [OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler }, diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua index 20677e4764..2fc65221be 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua @@ -4,5 +4,6 @@ local lazy_load = require "lazy_load_subdriver" return { + lazy_load("inovelli.vzm30-sn"), lazy_load("inovelli.vzm32-sn") } diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/can_handle.lua new file mode 100644 index 0000000000..9d1b285b45 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(opts, driver, device) + local INOVELLI_VZM30_SN_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM30-SN" }, + } + for _, fp in ipairs(INOVELLI_VZM30_SN_FINGERPRINTS) do + if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then + local sub_driver = require("inovelli.vzm30-sn") + return true, sub_driver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua new file mode 100644 index 0000000000..6ad3ec758d --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua @@ -0,0 +1,53 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local clusters = require "st.zigbee.zcl.clusters" +local st_device = require "st.device" +local inovelli_common = require "inovelli.common" + +local TemperatureMeasurement = clusters.TemperatureMeasurement +local RelativeHumidity = clusters.RelativeHumidity + +local PRIVATE_CLUSTER_ID = 0xFC31 +local MFG_CODE = 0x122F + +local function configure_temperature_reporting(device) + local min_temp_change = 50 -- 0.5°C in 0.01°C units + device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 3600, min_temp_change)) +end + +local function configure_humidity_reporting(device) + local min_humidity_change = 50 -- 0.5% in 0.01% units + device:send(RelativeHumidity.attributes.MeasuredValue:configure_reporting(device, 30, 3600, min_humidity_change)) +end + +local function device_configure(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + inovelli_common.base_device_configure(driver, device, PRIVATE_CLUSTER_ID, MFG_CODE) + configure_temperature_reporting(device) + configure_humidity_reporting(device) + else + device:configure() + end +end + +local vzm30_sn = { + NAME = "Inovelli VZM30-SN Zigbee Switch", + can_handle = require("inovelli.vzm30-sn.can_handle"), + lifecycle_handlers = { + doConfigure = device_configure, + }, +} + +return vzm30_sn \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua index edadf85b90..4e0151aeb6 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua @@ -52,7 +52,7 @@ local function device_configure(driver, device) end local vzm32_sn = { - NAME = "inovelli vzm32-sn device-specific", + NAME = "Inovelli VZM32-SN mmWave Dimmer", can_handle = require("inovelli.vzm32-sn.can_handle"), lifecycle_handlers = { added = device_added, diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua new file mode 100644 index 0000000000..79e7a66b54 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua @@ -0,0 +1,537 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" +local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade +local device_management = require "st.zigbee.device_management" +local zigbee_constants = require "st.zigbee.constants" + +local OnOff = clusters.OnOff +local Level = clusters.Level +local TemperatureMeasurement = clusters.TemperatureMeasurement +local RelativeHumidity = clusters.RelativeHumidity + +-- Inovelli VZM30-SN device identifiers +local INOVELLI_MANUFACTURER_ID = "Inovelli" +local INOVELLI_VZM30_SN_MODEL = "VZM30-SN" + +-- Device endpoints with supported clusters +local inovelli_vzm30_sn_endpoints = { + [1] = { + id = 1, + manufacturer = INOVELLI_MANUFACTURER_ID, + model = INOVELLI_VZM30_SN_MODEL, + server_clusters = {0x0006, 0x0008, 0x0300, 0x0402, 0x0405} -- OnOff, Level, ColorControl, TemperatureMeasurement, RelativeHumidity + } +} + +local mock_inovelli_vzm30_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm30-sn.yml"), + zigbee_endpoints = inovelli_vzm30_sn_endpoints, + fingerprinted_endpoint_id = 0x01 +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzm30_sn) +end +test.set_test_init_function(test_init) + +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} + } + +-- Test device initialization +test.register_message_test( + "Device should initialize properly on added lifecycle event", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_inovelli_vzm30_sn.id, "added" }, + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + Level.attributes.CurrentLevel:read(mock_inovelli_vzm30_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + OnOff.attributes.OnOff:read(mock_inovelli_vzm30_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_inovelli_vzm30_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + RelativeHumidity.attributes.MeasuredValue:read(mock_inovelli_vzm30_sn) + } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test refresh capability +test.register_message_test( + "Refresh capability should send read commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + { capability = "refresh", command = "refresh", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, OnOff.attributes.OnOff:read(mock_inovelli_vzm30_sn) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, Level.attributes.CurrentLevel:read(mock_inovelli_vzm30_sn) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_inovelli_vzm30_sn) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, RelativeHumidity.attributes.MeasuredValue:read(mock_inovelli_vzm30_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch on command +test.register_message_test( + "Switch on command should send OnOff On command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + { capability = "switch", command = "on", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, clusters.OnOff.server.commands.On(mock_inovelli_vzm30_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch off command +test.register_message_test( + "Switch off command should send OnOff Off command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + { capability = "switch", command = "off", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, clusters.OnOff.server.commands.Off(mock_inovelli_vzm30_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch level command +test.register_message_test( + "Switch level command should send Level MoveToLevelWithOnOff command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + { capability = "switchLevel", command = "setLevel", args = { 50 } } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + clusters.Level.server.commands.MoveToLevelWithOnOff(mock_inovelli_vzm30_sn, math.floor(50/100.0 * 254), 0xFFFF) + } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Build test message for Inovelli private cluster button press +local function build_inovelli_button_message(device, button_number, key_attribute) + local messages = require "st.zigbee.messages" + local zcl_messages = require "st.zigbee.zcl" + local zb_const = require "st.zigbee.constants" + local data_types = require "st.zigbee.data_types" + local frameCtrl = require "st.zigbee.zcl.frame_ctrl" + + -- Combine button_number and key_attribute into a single value + -- button_number in lower byte, key_attribute in upper byte + local combined_value = (key_attribute * 256) + button_number + + -- Create the command body using serialize_int + local command_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_messages.ZclHeader({ + frame_ctrl = frameCtrl(0x15), -- Manufacturer specific, client to server + mfg_code = data_types.Uint16(0x122F), -- Inovelli manufacturer code + seqno = data_types.Uint8(0x6D), + cmd = data_types.ZCLCommandId(0x00) -- Scene command + }), + zcl_body = data_types.Uint16(combined_value) + }) + + local addrh = messages.AddressHeader( + device:get_short_address(), + 0x02, -- src_endpoint from real device log + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + 0xFC31 -- PRIVATE_CLUSTER_ID + ) + + return messages.ZigbeeMessageRx({ + address_header = addrh, + body = command_body + }) +end + +-- Test button1 pushed +test.register_message_test( + "Button1 pushed should emit button event and update supportedButtonValues", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_inovelli_vzm30_sn.id, build_inovelli_button_message(mock_inovelli_vzm30_sn, 0x01, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + } + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test button2 pressed 4 times +test.register_message_test( + "Button2 pressed 4 times should emit button event and update supportedButtonValues", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_inovelli_vzm30_sn.id, build_inovelli_button_message(mock_inovelli_vzm30_sn, 0x02, 0x05) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message( + "button2", + capabilities.button.supportedButtonValues( + supported_button_values["button2"], + { visibility = { displayed = false } } + ) + ) + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) + } + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test consecutive button events - supportedButtonValues should only be sent on first event +test.register_coroutine_test( + "Consecutive button events should only send supportedButtonValues on first event", + function() + test.socket.capability:__set_channel_ordering("relaxed") + + -- First button event: button1 pushed - should send supportedButtonValues + button event + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm30_sn.id, + build_inovelli_button_message(mock_inovelli_vzm30_sn, 0x01, 0x00) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + ) + + -- Second button event: button1 pushed_2x - should only send button event, NOT supportedButtonValues + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm30_sn.id, + build_inovelli_button_message(mock_inovelli_vzm30_sn, 0x01, 0x03) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message("button1", capabilities.button.button.pushed_2x({ state_change = true })) + ) + end +) + +-- Test temperature measurement +test.register_message_test( + "Temperature measurement should emit temperature events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_inovelli_vzm30_sn, 2500) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message("main", capabilities.temperatureMeasurement.temperature({value = 25.0, unit = "C"})) + } + } +) + +-- Test humidity measurement +test.register_message_test( + "Humidity measurement should emit humidity events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + RelativeHumidity.attributes.MeasuredValue:build_test_attr_report(mock_inovelli_vzm30_sn, 6500) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity(65)) + } + } +) + +-- Test power meter from ElectricalMeasurement +test.register_coroutine_test( + "Power meter from ElectricalMeasurement should emit power events", + function() + -- Set the divisor field (default handlers use 10 if not set, but we can set it for consistency) + -- The default handler will use 10 if ELECTRICAL_MEASUREMENT_DIVISOR_KEY is not set + -- Since the test expects 2000 -> 200.0 W, that means divisor of 10 is being used + mock_inovelli_vzm30_sn:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 10, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm30_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm30_sn, 2000) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) + ) + end +) + +-- Test energy meter +test.register_coroutine_test( + "Energy meter should emit energy events", + function() + -- Set the divisor field as the device does during configuration + -- For VZM30-SN, the divisor is set to 1000 (like VZM32-SN) + mock_inovelli_vzm30_sn:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 1000, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm30_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm30_sn, 212) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 0.212, unit = "kWh"})) + ) + end +) + +-- Test energy meter reset command +test.register_message_test( + "Energy meter reset command should send reset commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + { capability = "energyMeter", command = "resetEnergyMeter", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + cluster_base.build_manufacturer_specific_command( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x02, -- PRIVATE_CMD_ENERGY_RESET_ID + 0x122F, -- MFG_CODE + utils.serialize_int(0, 1, false, false) + ) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:read(mock_inovelli_vzm30_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:read(mock_inovelli_vzm30_sn) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + + +test.register_coroutine_test( + "doConfigure runs base + VZM30 extras", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzm30_sn.id, "doConfigure" }) + + -- Button capability messages from base_device_configure + for _, component in pairs(mock_inovelli_vzm30_sn.profile.components) do + if component.id ~= "main" then + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message( + component.id, + capabilities.button.supportedButtonValues( + supported_button_values[component.id], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message( + component.id, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + ) + end + end + + -- device:configure() sends bind requests and configure reporting (default handler) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm30_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm30_sn, 1, 3600, 1) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, device_management.build_bind_request(mock_inovelli_vzm30_sn, RelativeHumidity.ID, require("integration_test.zigbee_test_utils").mock_hub_eui) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, RelativeHumidity.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm30_sn, 30, 3600, 100) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm30_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm30_sn, 0, 300) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, device_management.build_bind_request(mock_inovelli_vzm30_sn, TemperatureMeasurement.ID, require("integration_test.zigbee_test_utils").mock_hub_eui) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm30_sn, 30, 600, 100) }) + + -- base_device_configure sends OTA ImageNotify and private cluster bind + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, OTAUpgrade.commands.ImageNotify(mock_inovelli_vzm30_sn, 0x00, 100, 0x122F, 0xFFFF, 0xFFFFFFFF) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, device_management.build_bind_request(mock_inovelli_vzm30_sn, 0xFC31, require("integration_test.zigbee_test_utils").mock_hub_eui, 2) }) + + -- Read divisors/multipliers + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.SimpleMetering.attributes.Multiplier:read(mock_inovelli_vzm30_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_inovelli_vzm30_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_inovelli_vzm30_sn) }) + + -- VZM30-specific: temperature and humidity reporting configuration + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm30_sn, 30, 3600, 50) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, RelativeHumidity.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm30_sn, 30, 3600, 50) }) + + mock_inovelli_vzm30_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_child.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_child.lua new file mode 100644 index 0000000000..e40ead721a --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_child.lua @@ -0,0 +1,356 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" + +-- Device endpoints with supported clusters +local inovelli_vzm30_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM30-SN", + server_clusters = {0x0006, 0x0008, 0x0300} -- OnOff, Level, ColorControl + } +} + +local mock_parent_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm30-sn.yml"), + zigbee_endpoints = inovelli_vzm30_sn_endpoints, + fingerprinted_endpoint_id = 0x01 +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local mock_child_device = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("rgbw-bulb.yml"), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = "notification" +}) + +local function test_init() + test.mock_device.add_test_device(mock_parent_device) + test.mock_device.add_test_device(mock_child_device) +end +test.set_test_init_function(test_init) + +-- Test child device initialization +test.register_message_test( + "Child device should initialize with default color values", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_child_device.id, "added" }, + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.hue(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(6500)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switchLevel.level(100)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test child device switch on command +test.register_coroutine_test( + "Child device switch on should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = 100 -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "on", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device switch off command +test.register_coroutine_test( + "Child device switch off should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "off", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(0, 4, false, false) + ) + }) + end +) + +-- Test child device level command +test.register_coroutine_test( + "Child device level command should emit events and send configuration to parent", + function() + local level = math.random(1, 99) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local effect = 1 -- Default notificationType + local color = 100 -- Default color for child devices (since device starts with no hue state) + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) -- Use the actual level from command + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switchLevel", command = "setLevel", args = { level } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switchLevel.level(level)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device color command +test.register_coroutine_test( + "Child device color command should emit events and send configuration to parent", + function() + local color = math.random(0, 100) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorControl", command = "setColor", args = {{ hue = color, saturation = 100 }} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(color)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device color temperature command +test.register_coroutine_test( + "Child device color temperature command should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = 100 -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorTemperature", command = "setColorTemperature", args = { 3000 } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(3000)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua new file mode 100644 index 0000000000..0726a2d575 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua @@ -0,0 +1,210 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" + +-- Device endpoints with supported clusters +local inovelli_vzm30_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM30-SN", + server_clusters = {0x0006, 0x0008} -- OnOff, Level + } +} + +local mock_inovelli_vzm30_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm30-sn.yml"), + zigbee_endpoints = inovelli_vzm30_sn_endpoints, + fingerprinted_endpoint_id = 0x01, + label = "Inovelli VZM30-SN" +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzm30_sn) +end +test.set_test_init_function(test_init) + +-- Test parameter1 preference change +test.register_coroutine_test( + "parameter1 preference should send configuration command", + function() + local new_param_value = 50 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter1 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 1, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test parameter9 preference change +test.register_coroutine_test( + "parameter15 preference should send configuration command", + function() + local new_param_value = 10 + local expected_value = utils.round(new_param_value / 100 * 254) + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter15 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 15, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + expected_value + ) + }) + end +) + +-- Test parameter52 preference change +test.register_coroutine_test( + "parameter52 preference should send configuration command", + function() + local new_param_value = true + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter52 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 52, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter258 preference change +test.register_coroutine_test( + "parameter258 preference should send configuration command", + function() + local new_param_value = false + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter258 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 258, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter11 preference change (VZM30-only, same as VZM31) +test.register_coroutine_test( + "parameter11 preference should send configuration command", + function() + local new_param_value = true + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter11 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 11, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter17 preference change (VZM30-only, same as VZM31) +test.register_coroutine_test( + "parameter95 preference should send configuration command", + function() + local new_param_value = 64 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter95 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 95, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test parameter22 preference change (VZM30-only, same as VZM31) +test.register_coroutine_test( + "parameter22 preference should send configuration command", + function() + local new_param_value = 2 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter22 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 22, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test notificationChild preference change +test.register_coroutine_test( + "notificationChild preference should create child device when enabled", + function() + mock_inovelli_vzm30_sn:expect_device_create({ + type = "EDGE_CHILD", + label = "Inovelli VZM30-SN Notification", + profile = "rgbw-bulb-2700K-6500K", + parent_device_id = mock_inovelli_vzm30_sn.id, + parent_assigned_child_key = "notification" + }) + + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {notificationChild = true}})) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua index d70159dde5..a3d57415b1 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua @@ -10,6 +10,7 @@ local cluster_base = require "st.zigbee.cluster_base" local utils = require "st.utils" local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade local device_management = require "st.zigbee.device_management" +local zigbee_constants = require "st.zigbee.constants" -- Inovelli VZM31-SN device identifiers local INOVELLI_MANUFACTURER_ID = "Inovelli" @@ -38,6 +39,12 @@ local function test_init() end test.set_test_init_function(test_init) +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} +} + -- Test device initialization test.register_message_test( "Device should initialize properly on added lifecycle event", @@ -181,24 +188,38 @@ end -- Test button1 pushed test.register_message_test( - "Button1 pushed should emit button event", + "Button1 pushed should emit button event and update supportedButtonValues", { { channel = "zigbee", direction = "receive", message = { mock_inovelli_vzm31_sn.id, build_inovelli_button_message(mock_inovelli_vzm31_sn, 0x01, 0x00) } }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm31_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + }, { channel = "capability", direction = "send", message = mock_inovelli_vzm31_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) } + }, + { + inner_block_ordering = "relaxed" } ) -- Test button2 pressed 4 times test.register_message_test( - "Button2 pressed 4 times should emit button event", + "Button2 pressed 4 times should emit button event and update supportedButtonValues", { { channel = "zigbee", @@ -208,69 +229,96 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_vzm31_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) - } - } -) - --- Test power meter from SimpleMetering -test.register_message_test( - "Power meter from SimpleMetering should emit power events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm31_sn.id, - clusters.SimpleMetering.attributes.InstantaneousDemand:build_test_attr_report(mock_inovelli_vzm31_sn, 1500) - } + message = mock_inovelli_vzm31_sn:generate_test_message( + "button2", + capabilities.button.supportedButtonValues( + supported_button_values["button2"], + { visibility = { displayed = false } } + ) + ) }, { channel = "capability", direction = "send", - message = mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.powerMeter.power({value = 150.0, unit = "W"})) + message = mock_inovelli_vzm31_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) } + }, + { + inner_block_ordering = "relaxed" } ) +-- Test consecutive button events - supportedButtonValues should only be sent on first event +test.register_coroutine_test( + "Consecutive button events should only send supportedButtonValues on first event", + function() + test.socket.capability:__set_channel_ordering("relaxed") + + -- First button event: button1 pushed - should send supportedButtonValues + button event + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm31_sn.id, + build_inovelli_button_message(mock_inovelli_vzm31_sn, 0x01, 0x00) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + ) + + -- Second button event: button1 pushed_2x - should only send button event, NOT supportedButtonValues + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm31_sn.id, + build_inovelli_button_message(mock_inovelli_vzm31_sn, 0x01, 0x03) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message("button1", capabilities.button.button.pushed_2x({ state_change = true })) + ) + end +) + -- Test power meter from ElectricalMeasurement -test.register_message_test( +test.register_coroutine_test( "Power meter from ElectricalMeasurement should emit power events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm31_sn.id, - clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm31_sn, 2000) - } - }, - { - channel = "capability", - direction = "send", - message = mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) - } - } + function() + -- Set the divisor field (default handlers use 10 if not set, but we can set it for consistency) + -- The default handler will use 10 if ELECTRICAL_MEASUREMENT_DIVISOR_KEY is not set + -- Since the test expects 2000 -> 200.0 W, that means divisor of 10 is being used + mock_inovelli_vzm31_sn:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 10, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm31_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm31_sn, 2000) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) + ) + end ) -- Test energy meter -test.register_message_test( +test.register_coroutine_test( "Energy meter should emit energy events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm31_sn.id, - clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm31_sn, 50000) - } - }, - { - channel = "capability", - direction = "send", - message = mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 500.0, unit = "kWh"})) - } - } + function() + -- Set the divisor field as the device reads during configuration + -- For VZM31, the divisor is read from the device, but for testing we need to set it + -- The test expects 50000 -> 500.0 kWh, which means divisor of 100 + mock_inovelli_vzm31_sn:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 100, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm31_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm31_sn, 50000) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 500.0, unit = "kWh"})) + ) + end ) -- Test energy meter reset command @@ -325,21 +373,49 @@ test.register_message_test( test.register_coroutine_test( "doConfigure runs base config (VZM31)", function() + test.socket.capability:__set_channel_ordering("relaxed") test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzm31_sn.id, "doConfigure" }) + + -- Button capability messages from base_device_configure + for _, component in pairs(mock_inovelli_vzm31_sn.profile.components) do + if component.id ~= "main" then + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message( + component.id, + capabilities.button.supportedButtonValues( + supported_button_values[component.id], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message( + component.id, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + ) + end + end + + -- device:configure() sends bind requests and configure reporting (default handler) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm31_sn, 0, 300) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm31_sn, 1, 3600, 1) }) + + -- base_device_configure sends OTA ImageNotify and private cluster bind test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, OTAUpgrade.commands.ImageNotify(mock_inovelli_vzm31_sn, 0x00, 100, 0x122F, 0xFFFF, 0xFFFFFFFF) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, device_management.build_bind_request(mock_inovelli_vzm31_sn, 0xFC31, require("integration_test.zigbee_test_utils").mock_hub_eui, 2) }) + + -- Read divisors/multipliers test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.SimpleMetering.attributes.Divisor:read(mock_inovelli_vzm31_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.SimpleMetering.attributes.Multiplier:read(mock_inovelli_vzm31_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_inovelli_vzm31_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_inovelli_vzm31_sn) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm31_sn, 0, 300) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm31_sn, 1, 3600, 1) }) + mock_inovelli_vzm31_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) test.run_registered_tests() - diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua index debdeba110..0a288f27d4 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -10,6 +10,7 @@ local cluster_base = require "st.zigbee.cluster_base" local utils = require "st.utils" local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade local device_management = require "st.zigbee.device_management" +local zigbee_constants = require "st.zigbee.constants" local OnOff = clusters.OnOff local Level = clusters.Level @@ -41,6 +42,12 @@ local function test_init() end test.set_test_init_function(test_init) +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} +} + -- Test device initialization test.register_message_test( "Device should initialize properly on added lifecycle event", @@ -225,38 +232,101 @@ end -- Test button1 pushed test.register_message_test( - "Button1 pushed should emit button event", + "Button1 pushed should emit button event and update supportedButtonValues", { { channel = "zigbee", direction = "receive", message = { mock_inovelli_vzm32_sn.id, build_inovelli_button_message(mock_inovelli_vzm32_sn, 0x01, 0x00) } }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + }, { channel = "capability", direction = "send", message = mock_inovelli_vzm32_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) } + }, + { + inner_block_ordering = "relaxed" } ) -- Test button2 pressed 4 times test.register_message_test( - "Button2 pressed 4 times should emit button event", + "Button2 pressed 4 times should emit button event and update supportedButtonValues", { { channel = "zigbee", direction = "receive", message = { mock_inovelli_vzm32_sn.id, build_inovelli_button_message(mock_inovelli_vzm32_sn, 0x02, 0x05) } }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message( + "button2", + capabilities.button.supportedButtonValues( + supported_button_values["button2"], + { visibility = { displayed = false } } + ) + ) + }, { channel = "capability", direction = "send", message = mock_inovelli_vzm32_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) } + }, + { + inner_block_ordering = "relaxed" } ) +-- Test consecutive button events - supportedButtonValues should only be sent on first event +test.register_coroutine_test( + "Consecutive button events should only send supportedButtonValues on first event", + function() + test.socket.capability:__set_channel_ordering("relaxed") + + -- First button event: button1 pushed - should send supportedButtonValues + button event + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm32_sn.id, + build_inovelli_button_message(mock_inovelli_vzm32_sn, 0x01, 0x00) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + ) + + -- Second button event: button1 pushed_2x - should only send button event, NOT supportedButtonValues + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm32_sn.id, + build_inovelli_button_message(mock_inovelli_vzm32_sn, 0x01, 0x03) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message("button1", capabilities.button.button.pushed_2x({ state_change = true })) + ) + end +) + -- Test illuminance measurement test.register_message_test( "Illuminance measurement should emit illuminance events", @@ -317,64 +387,41 @@ test.register_message_test( } ) --- Test power meter from SimpleMetering -test.register_message_test( - "Power meter from SimpleMetering should emit power events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm32_sn.id, - clusters.SimpleMetering.attributes.InstantaneousDemand:build_test_attr_report(mock_inovelli_vzm32_sn, 1500) - } - }, - { - channel = "capability", - direction = "send", - message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.powerMeter.power({value = 150.0, unit = "W"})) - } - } -) - -- Test power meter from ElectricalMeasurement -test.register_message_test( +test.register_coroutine_test( "Power meter from ElectricalMeasurement should emit power events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm32_sn.id, - clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm32_sn, 2000) - } - }, - { - channel = "capability", - direction = "send", - message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) - } - } + function() + -- Set the divisor field (default handlers use 10 if not set, but we can set it for consistency) + -- The default handler will use 10 if ELECTRICAL_MEASUREMENT_DIVISOR_KEY is not set + -- Since the test expects 2000 -> 200.0 W, that means divisor of 10 is being used + -- For VZM32, the actual device reads ACPowerDivisor, but default is 10 + mock_inovelli_vzm32_sn:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 10, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm32_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm32_sn, 2000) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) + ) + end ) -- Test energy meter -test.register_message_test( +test.register_coroutine_test( "Energy meter should emit energy events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm32_sn.id, - clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm32_sn, 50000) - } - }, - { - channel = "capability", - direction = "send", - message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 500.0, unit = "kWh"})) - } - } + function() + -- Set the divisor field as the device does during configuration + mock_inovelli_vzm32_sn:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 1000, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm32_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm32_sn, 212) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 0.212, unit = "kWh"})) + ) + end ) -- Test energy meter reset command @@ -429,22 +476,52 @@ test.register_message_test( test.register_coroutine_test( "doConfigure runs base + VZM32 extras", function() + test.socket.capability:__set_channel_ordering("relaxed") test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzm32_sn.id, "doConfigure" }) + + -- Button capability messages from base_device_configure + for _, component in pairs(mock_inovelli_vzm32_sn.profile.components) do + if component.id ~= "main" then + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message( + component.id, + capabilities.button.supportedButtonValues( + supported_button_values[component.id], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message( + component.id, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + ) + end + end + + -- device:configure() sends bind requests and configure reporting (default handler) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm32_sn, 0, 300) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm32_sn, 1, 3600, 1) }) + + -- base_device_configure sends OTA ImageNotify and private cluster bind test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, OTAUpgrade.commands.ImageNotify(mock_inovelli_vzm32_sn, 0x00, 100, 0x122F, 0xFFFF, 0xFFFFFFFF) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, device_management.build_bind_request(mock_inovelli_vzm32_sn, 0xFC31, require("integration_test.zigbee_test_utils").mock_hub_eui, 2) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.SimpleMetering.attributes.Divisor:read(mock_inovelli_vzm32_sn) }) + + -- Read divisors/multipliers test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.SimpleMetering.attributes.Multiplier:read(mock_inovelli_vzm32_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_inovelli_vzm32_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_inovelli_vzm32_sn) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm32_sn, 0, 300) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm32_sn, 1, 3600, 1) }) + + -- VZM32-specific: occupancy and illuminance reporting configuration test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, device_management.build_bind_request(mock_inovelli_vzm32_sn, clusters.OccupancySensing.ID, require("integration_test.zigbee_test_utils").mock_hub_eui) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm32_sn, 10, 600, 11761) }) + mock_inovelli_vzm32_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-thermostat/fingerprints.yml b/drivers/SmartThings/zigbee-thermostat/fingerprints.yml index 1dc993ea26..d88b3ff89a 100644 --- a/drivers/SmartThings/zigbee-thermostat/fingerprints.yml +++ b/drivers/SmartThings/zigbee-thermostat/fingerprints.yml @@ -124,6 +124,11 @@ zigbeeManufacturer: manufacturer: Resideo Korea model: DT300ST-M000 deviceProfileName: thermostat-resideo-dt300st-m000 + - id: "Resideo Korea/MC200ST" + deviceLabel: Valve Controller + manufacturer: Resideo Korea + model: MC200ST + deviceProfileName: thermostat-resideo-dt300st-m000 - id: "LUMI/lumi.airrtc.agl001" deviceLabel: Aqara Smart Radiator Thermostat E1 manufacturer: LUMI diff --git a/drivers/SmartThings/zwave-switch/fingerprints.yml b/drivers/SmartThings/zwave-switch/fingerprints.yml index 7154dc11c7..d90a0c6b76 100644 --- a/drivers/SmartThings/zwave-switch/fingerprints.yml +++ b/drivers/SmartThings/zwave-switch/fingerprints.yml @@ -45,17 +45,23 @@ zwaveManufacturer: productId: 0x0000 deviceProfileName: switch-level - id: "Inovelli/Dimmer/Power/Energy" - deviceLabel: Inovelli Dimmer Switch + deviceLabel: Inovelli Dimmer Red Series manufacturerId: 0x031E productType: 0x0001 productId: 0x0001 deviceProfileName: inovelli-dimmer-power-energy - id: "Inovelli/Dimmer" - deviceLabel: Inovelli Dimmer Switch + deviceLabel: Inovelli Dimmer Black Series manufacturerId: 0x031E productType: 0x0003 productId: 0x0001 deviceProfileName: inovelli-dimmer + - id: "Inovelli/VZW32-SN" + deviceLabel: Inovelli mmWave Dimmer Red Series + manufacturerId: 0x031E + productType: 0x0017 + productId: 0x0001 + deviceProfileName: inovelli-mmwave-dimmer-vzw32-sn - id: "010F/0403" deviceLabel: Fibaro Single Switch manufacturerId: 0x010F diff --git a/drivers/SmartThings/zwave-switch/profiles/inovelli-dimmer-power-energy.yml b/drivers/SmartThings/zwave-switch/profiles/inovelli-dimmer-power-energy.yml index c3ee571c10..077c80e3a3 100644 --- a/drivers/SmartThings/zwave-switch/profiles/inovelli-dimmer-power-energy.yml +++ b/drivers/SmartThings/zwave-switch/profiles/inovelli-dimmer-power-energy.yml @@ -27,24 +27,48 @@ components: categories: - name: Light - id: button1 + label: Down Button capabilities: - id: button version: 1 categories: - name: RemoteController - id: button2 + label: Up Button capabilities: - id: button version: 1 categories: - name: RemoteController - id: button3 + label: Config Button capabilities: - id: button version: 1 categories: - name: RemoteController preferences: + - name: "notificationChild" + title: "Add Child Device - Notification" + description: "Create Separate Child Device for Notification Control" + required: false + preferenceType: boolean + definition: + default: false + - name: "notificationType" + title: "Notification Effect" + description: "This is the notification effect used by the notification child device" + required: false + preferenceType: enumeration + definition: + options: + "0": "Clear" + "1": "Solid" + "2": "Chase" + "3": "Fast Blink" + "4": "Slow Blink" + "5": "Pulse" + default: 1 - name: "dimmingSpeed" title: "Dimming Speed" description: "How fast or slow the light changes state when you hold the switch. diff --git a/drivers/SmartThings/zwave-switch/profiles/inovelli-mmwave-dimmer-vzw32-sn.yml b/drivers/SmartThings/zwave-switch/profiles/inovelli-mmwave-dimmer-vzw32-sn.yml new file mode 100644 index 0000000000..d85a661839 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/profiles/inovelli-mmwave-dimmer-vzw32-sn.yml @@ -0,0 +1,400 @@ +name: inovelli-mmwave-dimmer-vzw32-sn +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + - id: motionSensor + version: 1 + - id: illuminanceMeasurement + version: 1 + config: + values: + - key: "illuminance.value" + range: [0, 5000] + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: Switch +- id: button1 + label: Down Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +- id: button2 + label: Up Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +- id: button3 + label: Config Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +preferences: + - name: "notificationChild" + title: "Add Child Device - Notification" + description: "Create Separate Child Device for Notification Control" + required: false + preferenceType: boolean + definition: + default: false + - name: "notificationType" + title: "Notification Effect" + description: "This is the notification effect used by the notification child device" + required: false + preferenceType: enumeration + definition: + options: + "255": "Clear" + "1": "Solid" + "2": "Fast Blink" + "3": "Slow Blink" + "4": "Pulse" + "5": "Chase" + "6": "Open/Close" + "7": "Small-to-Big" + "8": "Aurora" + "9": "Slow Falling" + "10": "Medium Falling" + "11": "Fast Falling" + "12": "Slow Rising" + "13": "Medium Rising" + "14": "Fast Rising" + "15": "Medium Blink" + "16": "Slow Chase" + "17": "Fast Chase" + "18": "Fast Siren" + "19": "Slow Siren" + default: 1 + - name: "parameter158" + title: "158. Switch Mode" + description: "Use as a Dimmer or an On/Off switch" + required: true + preferenceType: enumeration + definition: + options: + "0": "Dimmer (default)" + "1": "On/Off" + default: 0 + - name: "parameter52" + title: "52. Smart Bulb Mode" + description: "For use with Smart Bulbs that need constant power and are controlled via commands rather than power. Smart Bulb Mode does not work in Dumb 3-Way Switch mode." + required: true + preferenceType: enumeration + definition: + options: + "0": "Disabled (default)" + "1": "Smart Bulb Mode" + default: 0 + - name: "parameter1" + title: "1. Dimming Speed (Remote)" + description: "This changes the speed that the light dims up when controlled from the hub. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + Default=25 (2500ms or 2.5s)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 255 + default: 25 + - name: "parameter2" + title: "2. Dimming Speed (Local)" + description: "This changes the speed that the light dims up when controlled at the switch. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=255 (Sync with parameter 1)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 255 + default: 255 + - name: "parameter3" + title: "3. Ramp Rate (Remote)" + description: "This changes the speed that the light turns on when controlled from the hub. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=255 (Sync with parameter 1)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 255 + default: 255 + - name: "parameter4" + title: "4. Ramp Rate (Local)" + description: "This changes the speed that the light turns on when controlled at the switch. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=255 (Sync with parameter 3)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 255 + default: 255 + - name: "parameter9" + title: "9. Minimum Level" + description: "The minimum level that the light can be dimmed. Useful when the user has a light that does not turn on or flickers at a lower level." + required: true + preferenceType: number + definition: + minimum: 1 + maximum: 99 + default: 1 + - name: "parameter10" + title: "10. Maximum Level" + description: "The maximum level that the light can be dimmed. Useful when the user wants to limit the maximum brighness." + required: true + preferenceType: number + definition: + minimum: 2 + maximum: 100 + default: 100 + - name: "parameter15" + title: "15. Level After Power Restored" + description: "The level the switch will return to when power is restored after power failure. + 0=Off + 1-100=Set Level + 101=Use previous level." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 101 + default: 101 + - name: "parameter18" + title: "18. Active Power Reports" + description: "Power level change that will result in a new power report being sent. + 0 = Disabled + 1-32767 = 0.1W-3276.7W." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 32767 + default: 100 + - name: "parameter19" + title: "19. Periodic Power & Energy Reports" + description: "Time period between consecutive power & energy reports being sent (in seconds). The timer is reset after each report is sent." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 32767 + default: 3600 + - name: "parameter20" + title: "20. Active Energy Reports" + description: "Energy level change that will result in a new energy report being sent. + 0 = Disabled + 1-32767 = 0.01kWh-327.67kWh." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 32767 + default: 100 + - name: "parameter50" + title: "50. Button Press Delay" + description: "Adjust the delay used in scene control. 0=no delay (disables multi-tap scenes), 1=100ms, 2=200ms, 3=300ms, etc." + required: true + preferenceType: enumeration + definition: + options: + "0": "0ms" + "1": "100ms" + "2": "200ms" + "3": "300ms" + "4": "400ms" + "5": "500ms (default)" + "6": "600ms" + "7": "700ms" + "8": "800ms" + "9": "900ms" + default: 5 + - name: "parameter95" + title: "95. LED Indicator Color (w/On)" + description: "Set the color of the Full LED Indicator when the load is on." + required: true + preferenceType: enumeration + definition: + options: + "0": "Red" + "7": "Orange" + "28": "Lemon" + "64": "Lime" + "85": "Green" + "106": "Teal" + "127": "Cyan" + "148": "Aqua" + "170": "Blue (default)" + "190": "Violet" + "212": "Magenta" + "234": "Pink" + "255": "White" + default: 170 + - name: "parameter96" + title: "96. LED Indicator Color (w/Off)" + description: "Set the color of the Full LED Indicator when the load is off." + required: true + preferenceType: enumeration + definition: + options: + "0": "Red" + "7": "Orange" + "28": "Lemon" + "64": "Lime" + "85": "Green" + "106": "Teal" + "127": "Cyan" + "148": "Aqua" + "170": "Blue (default)" + "190": "Violet" + "212": "Magenta" + "234": "Pink" + "255": "White" + default: 170 + - name: "parameter97" + title: "97. LED Indicator Intensity (w/On)" + description: "Set the intensity of the Full LED Indicator when the load is on." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 100 + default: 50 + - name: "parameter98" + title: "98. LED Indicator Intensity (w/Off)" + description: "Set the intensity of the Full LED Indicator when the load is off." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 100 + default: 5 + - name: "parameter101" + title: "101. mmWave Height Minimum (Floor)" + description: "Minimum range of the Z-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: -300 + - name: "parameter102" + title: "102. mmWave Height Maximum (Ceiling)" + description: "Maximum range of the Z-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: 300 + - name: "parameter103" + title: "103. mmWave Width Minimum (Left)" + description: "Minimum range of the X-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: -600 + - name: "parameter104" + title: "104. mmWave Width Maximum (Right)" + description: "Maximum range of the X-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: 600 + - name: "parameter105" + title: "105. mmWave Depth Minimum (Near)" + description: "Minimum range of the Y-Axis in cm" + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 600 + default: 0 + - name: "parameter106" + title: "106. mmWave Depth Maximum (Far)" + description: "Maximum range of the Y-Axis in cm" + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 600 + default: 600 + - name: "parameter108" + title: "108. mmWave Stay Life" + description: "Optimize detection in areas where user may be still for a long time. The delay time of the stay area is set to 50ms when it is set to 1, to 1 second when it is set to 20, and the default value is 300, that is, 15 seconds" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 4294967295 + default: 300 + - name: "parameter110" + title: "Light On Presence Behavior" + description: "When presence is detected, choose how to control the light load" + required: true + preferenceType: enumeration + definition: + options: + "0": "Disabled" + "1": "Auto On/Off when occupied (default)" + "2": "Auto Off when vacant" + "3": "Auto On when occupied" + "4": "Auto On/Off when Vacant" + "5": "Auto On when Vacant" + "6": "Auto Off when Occupied" + default: 1 + - name: "parameter111" + title: "111. mmWave Control Commands" + description: "Advanced commands to send to the mmWave Module (Please see documentation)" + required: false + preferenceType: enumeration + definition: + options: + "1": "Set Interference Area" + "3": "Clear Interference Area" + "0": "Factory Reset Module" + default: 3 + - name: "parameter112" + title: "112. mmWave Sensitivity" + description: "Adjust the sensitivity of the mmWave sensor. 0-Low, 1-Medium, 2-High." + required: false + preferenceType: enumeration + definition: + options: + "0": "Low" + "1": "Medium" + "2": "High (default)" + default: 2 + - name: "parameter113" + title: "113. mmWave Detection Delay" + description: "The time from detecting a person to triggering an action. 0-Low (5s), 1-Medium (1s), 2-Fast (0.2s)." + required: false + preferenceType: enumeration + definition: + options: + "0": "5 seconds" + "1": "1 second" + "2": "0.2 seconds (default)" + default: 2 + - name: "parameter114" + title: "mmWave Detection Timeout" + description: "Adjust the timeout after presence is no longer detected. After the timeout the load will turn off." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 4294967296 + default: 30 \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/profiles/rgbw-bulb.yml b/drivers/SmartThings/zwave-switch/profiles/rgbw-bulb.yml new file mode 100644 index 0000000000..d87d32adc6 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/profiles/rgbw-bulb.yml @@ -0,0 +1,16 @@ +name: rgbw-bulb +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + - id: colorTemperature + version: 1 + - id: colorControl + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/zwave-switch/src/init.lua b/drivers/SmartThings/zwave-switch/src/init.lua index 5814945884..405600e962 100644 --- a/drivers/SmartThings/zwave-switch/src/init.lua +++ b/drivers/SmartThings/zwave-switch/src/init.lua @@ -122,7 +122,7 @@ local driver_template = { }, sub_drivers = { lazy_load_if_possible("eaton-accessory-dimmer"), - lazy_load_if_possible("inovelli-LED"), + lazy_load_if_possible("inovelli"), lazy_load_if_possible("dawon-smart-plug"), lazy_load_if_possible("inovelli-2-channel-smart-plug"), lazy_load_if_possible("zwave-dual-switch"), diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua deleted file mode 100644 index eed5127205..0000000000 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua +++ /dev/null @@ -1,21 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local INOVELLI_MANUFACTURER_ID = 0x031E -local INOVELLI_LZW31SN_PRODUCT_TYPE = 0x0001 -local INOVELLI_LZW31_PRODUCT_TYPE = 0x0003 -local INOVELLI_DIMMER_PRODUCT_ID = 0x0001 - -local function can_handle_inovelli_led(opts, driver, device, ...) - if device:id_match( - INOVELLI_MANUFACTURER_ID, - {INOVELLI_LZW31SN_PRODUCT_TYPE, INOVELLI_LZW31_PRODUCT_TYPE}, - INOVELLI_DIMMER_PRODUCT_ID - ) then - local subdriver = require("inovelli-LED") - return true, subdriver - end - return false -end - -return can_handle_inovelli_led diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua deleted file mode 100644 index 23fafda05d..0000000000 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua +++ /dev/null @@ -1,82 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local capabilities = require "st.capabilities" ---- @type st.utils -local utils = require "st.utils" ---- @type st.zwave.constants -local constants = require "st.zwave.constants" ---- @type st.zwave.CommandClass -local cc = require "st.zwave.CommandClass" ---- @type st.zwave.CommandClass.Configuration -local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) - -local LED_COLOR_CONTROL_PARAMETER_NUMBER = 13 -local LED_GENERIC_SATURATION = 100 -local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" - -local function huePercentToZwaveValue(value) - if value <= 2 then - return 0 - elseif value >= 98 then - return 255 - else - return utils.round(value / 100 * 255) - end -end - -local function zwaveValueToHuePercent(value) - if value <= 2 then - return 0 - elseif value >= 254 then - return 100 - else - return utils.round(value / 255 * 100) - end -end - -local function configuration_report(driver, device, cmd) - if cmd.args.parameter_number == LED_COLOR_CONTROL_PARAMETER_NUMBER then - local hue = zwaveValueToHuePercent(cmd.args.configuration_value) - - local ledBarComponent = device.profile.components[LED_BAR_COMPONENT_NAME] - if ledBarComponent ~= nil then - device:emit_component_event(ledBarComponent, capabilities.colorControl.hue(hue)) - device:emit_component_event(ledBarComponent, capabilities.colorControl.saturation(LED_GENERIC_SATURATION)) - end - end -end - -local function set_color(driver, device, cmd) - local value = huePercentToZwaveValue(cmd.args.color.hue) - local config = Configuration:Set({ - parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER, - configuration_value=value, - size=2 - }) - device:send(config) - - local query_configuration = function() - device:send(Configuration:Get({ parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER })) - end - - device.thread:call_with_delay(constants.DEFAULT_GET_STATUS_DELAY, query_configuration) -end - -local inovelli_led = { - NAME = "Inovelli LED", - zwave_handlers = { - [cc.CONFIGURATION] = { - [Configuration.REPORT] = configuration_report - } - }, - capability_handlers = { - [capabilities.colorControl.ID] = { - [capabilities.colorControl.commands.setColor.NAME] = set_color - } - }, - can_handle = require("inovelli-LED.can_handle"), - sub_drivers = require("inovelli-LED.sub_drivers"), -} - -return inovelli_led diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua deleted file mode 100644 index fd0ff5d844..0000000000 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua +++ /dev/null @@ -1,87 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local capabilities = require "st.capabilities" ---- @type st.zwave.CommandClass -local cc = require "st.zwave.CommandClass" ---- @type st.zwave.CommandClass.CentralScene -local CentralScene = (require "st.zwave.CommandClass.CentralScene")({version=3}) - -local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" - -local supported_button_values = { - ["button1"] = {"pushed", "pushed_2x", "pushed_3x", "pushed_4x", "pushed_5x"}, - ["button2"] = {"pushed", "pushed_2x", "pushed_3x", "pushed_4x", "pushed_5x"}, - ["button3"] = {"pushed"} -} - -local function device_added(driver, device) - for _, component in pairs(device.profile.components) do - if component.id ~= "main" and component.id ~= LED_BAR_COMPONENT_NAME then - device:emit_component_event( - component, - capabilities.button.supportedButtonValues( - supported_button_values[component.id], - { visibility = { displayed = false } } - ) - ) - device:emit_component_event( - component, - capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) - ) - end - end - device:refresh() -end - -local map_scene_number_to_component = { - [1] = "button2", - [2] = "button1", - [3] = "button3" -} - - -local map_key_attribute_to_capability = { - [CentralScene.key_attributes.KEY_PRESSED_1_TIME] = capabilities.button.button.pushed, - [CentralScene.key_attributes.KEY_PRESSED_2_TIMES] = capabilities.button.button.pushed_2x, - [CentralScene.key_attributes.KEY_PRESSED_3_TIMES] = capabilities.button.button.pushed_3x, - [CentralScene.key_attributes.KEY_PRESSED_4_TIMES] = capabilities.button.button.pushed_4x, - [CentralScene.key_attributes.KEY_PRESSED_5_TIMES] = capabilities.button.button.pushed_5x, -} - -local function central_scene_notification_handler(self, device, cmd) - if ( cmd.args.scene_number ~= nil and cmd.args.scene_number ~= 0 ) then - local capability_attribute = map_key_attribute_to_capability[cmd.args.key_attributes] - local additional_fields = { - state_change = true - } - - local event - if capability_attribute ~= nil then - event = capability_attribute(additional_fields) - end - - if event ~= nil then - -- device reports scene notifications from endpoint 0 (main) but central scene events have to be emitted for button components: 1,2,3 - local comp = device.profile.components[map_scene_number_to_component[cmd.args.scene_number]] - if comp ~= nil then - device:emit_component_event(comp, event) - end - end - end -end - -local inovelli_led_lzw31sn = { - NAME = "Inovelli LED LZW 31SN", - zwave_handlers = { - [cc.CENTRAL_SCENE] = { - [CentralScene.NOTIFICATION] = central_scene_notification_handler - } - }, - lifecycle_handlers = { - added = device_added - }, - can_handle = require("inovelli-LED.inovelli-lzw31sn.can_handle") -} - -return inovelli_led_lzw31sn diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli/can_handle.lua new file mode 100644 index 0000000000..7c0f6be77b --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/can_handle.lua @@ -0,0 +1,20 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local INOVELLI_FINGERPRINTS = { + { mfr = 0x031E, prod = 0x0017, model = 0x0001 }, -- Inovelli VZW32-SN + { mfr = 0x031E, prod = 0x0001, model = 0x0001 }, -- Inovelli LZW31SN + { mfr = 0x031E, prod = 0x0003, model = 0x0001 }, -- Inovelli LZW31 +} + +local function can_handle_inovelli(opts, driver, device, ...) + for _, fingerprint in ipairs(INOVELLI_FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("inovelli") + return true, subdriver + end + end + return false +end + +return can_handle_inovelli diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli/init.lua new file mode 100644 index 0000000000..84ed2c7df7 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/init.lua @@ -0,0 +1,501 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass.Configuration +local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) +--- @type st.zwave.CommandClass.Association +local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) +--- @type st.zwave.CommandClass.SwitchBinary +local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({ version = 2 }) +--- @type st.zwave.CommandClass.Basic +local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) +--- @type st.zwave.CommandClass.SwitchMultilevel +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) +--- @type st.zwave.CommandClass.Meter +local Meter = (require "st.zwave.CommandClass.Meter")({ version = 3 }) +local preferencesMap = require "preferences" + +--- @type st.utils +local utils = require "st.utils" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +local log = require "log" +local st_device = require "st.device" + +--- @type st.zwave.CommandClass.CentralScene +local CentralScene = (require "st.zwave.CommandClass.CentralScene")({version=3}) +--- @type st.zwave.constants +local constants = require "st.zwave.constants" + +local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" + +local GEN3_NOTIFICATION_PARAMETER_NUMBER = 99 +local GEN2_NOTIFICATION_PARAMETER_NUMBER = 16 +local LED_COLOR_CONTROL_PARAMETER_NUMBER = 13 +local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" +local LED_GENERIC_SATURATION = 100 + +-- TODO: Remove after transition period - supportedButtonValues initialization +-- This table defines the supported button values for each button component. +-- Used to initialize supportedButtonValues on device_added and update devices with old values. +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed"} +} + +-- Device type detection helpers +local function is_gen2(device) + return device:id_match(0x031E, {0x0001, 0x0003}, 0x0001) +end + +local function is_gen3(device) + return device:id_match(0x031E, {0x0015, 0x0017}, 0x0001) +end + +-- Helper function to get the correct notification parameter number based on device type +local function get_notification_parameter_number(device) + -- For child devices, check the parent device type + local device_to_check = device + if device.network_type == st_device.NETWORK_TYPE_CHILD then + device_to_check = device:get_parent_device() + end + + if is_gen3(device_to_check) then + return GEN3_NOTIFICATION_PARAMETER_NUMBER + else + return GEN2_NOTIFICATION_PARAMETER_NUMBER + end +end + +local function button_to_component(buttonId) + if buttonId > 0 then + return string.format("button%d", buttonId) + end +end + +local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return utils.round(value / 100 * 255) + end +end + +local function valueToHuePercent(value) + if value <= 2 then + return 0 + elseif value >= 254 then + return 100 + else + return utils.round(value / 255 * 100) + end +end + +local preferences_to_numeric_value = function(new_value) + local numeric = tonumber(new_value) + if numeric == nil then -- in case the value is boolean + numeric = new_value and 1 or 0 + end + return numeric +end + +local preferences_calculate_parameter = function(new_value, type, number) + if type == 4 and new_value > 2147483647 then + return ((4294967296 - new_value) * -1) + elseif type == 2 and new_value > 32767 then + return ((65536 - new_value) * -1) + elseif type == 1 and new_value > 127 then + return ((256 - new_value) * -1) + else + return new_value + end +end + +local function add_child(driver,parent,profile,child_type) + local child_metadata = { + type = "EDGE_CHILD", + label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)), + profile = profile, + parent_device_id = parent.id, + parent_assigned_child_key = child_type, + vendor_provided_label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)) + } + driver:try_create_device(child_metadata) +end + +local function getNotificationValue(device, value) + local level = device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) or 100 + local color = utils.round(device:get_latest_state("main", capabilities.colorControl.ID, capabilities.colorControl.hue.NAME) or 100) + local effect = device:get_parent_device().preferences.notificationType or 1 + local duration = 255 -- Default duration + + -- Get the parent device to check generation for child devices + local device_to_check = device + if device.network_type == st_device.NETWORK_TYPE_CHILD then + device_to_check = device:get_parent_device() + end + + local colorValue = huePercentToValue(value or color) + local notificationValue = 0 + + if is_gen3(device_to_check) then + -- Gen3 order: duration, level, color, effect (bytes 0-3 from low to high) + notificationValue = notificationValue + (effect * 16777216) -- byte 3 (highest) + notificationValue = notificationValue + (colorValue * 65536) -- byte 2 + notificationValue = notificationValue + (level * 256) -- byte 1 + notificationValue = notificationValue + (duration * 1) -- byte 0 (lowest) + else + -- Gen2 order: color, level, duration, effect (bytes 0-3 from low to high) + notificationValue = notificationValue + (effect * 16777216) -- byte 3 (highest) + notificationValue = notificationValue + (duration * 65536) -- byte 2 + notificationValue = notificationValue + (level * 256) -- byte 1 + notificationValue = notificationValue + (colorValue * 1) -- byte 0 (lowest) + end + + return notificationValue +end + +local function set_color(driver, device, command) + if device.network_type == st_device.NETWORK_TYPE_CHILD then + device:emit_event(capabilities.colorControl.hue(command.args.color.hue)) + device:emit_event(capabilities.colorControl.saturation(command.args.color.saturation)) + device:emit_event(capabilities.switch.switch("on")) + local dev = device:get_parent_device() + local config = Configuration:Set({ + parameter_number=get_notification_parameter_number(device), + configuration_value=getNotificationValue(device), + size=4 + }) + local send_configuration = function() + dev:send(config) + end + device.thread:call_with_delay(1,send_configuration) + else + local value = huePercentToValue(command.args.color.hue) + local config = Configuration:Set({ + parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER, + configuration_value=value, + size=2 + }) + device:send(config) + + local query_configuration = function() + device:send(Configuration:Get({ parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER })) + end + + device.thread:call_with_delay(constants.DEFAULT_GET_STATUS_DELAY, query_configuration) + end +end + +local function set_color_temperature(driver, device, command) + if device.network_type == st_device.NETWORK_TYPE_CHILD then + device:emit_event(capabilities.colorControl.hue(100)) + device:emit_event(capabilities.colorTemperature.colorTemperature(command.args.temperature)) + device:emit_event(capabilities.switch.switch("on")) + local dev = device:get_parent_device() + local config = Configuration:Set({ + parameter_number=get_notification_parameter_number(device), + configuration_value=getNotificationValue(device, 100), + size=4 + }) + local send_configuration = function() + dev:send(config) + end + device.thread:call_with_delay(1,send_configuration) + else + local value = huePercentToValue(100) + local config = Configuration:Set({ + parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER, + configuration_value=value, + size=2 + }) + device:send(config) + + local query_configuration = function() + device:send(Configuration:Get({ parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER })) + end + + device.thread:call_with_delay(constants.DEFAULT_GET_STATUS_DELAY, query_configuration) + end +end + +local function switch_level_set(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + local level = utils.round(command.args.level) + level = utils.clamp_value(level, 0, 99) + + device:send(SwitchMultilevel:Set({ value=level, duration=command.args.rate or "default" })) + + device.thread:call_with_delay(3, function(d) + device:send(SwitchMultilevel:Get({})) + end) + else + device:emit_event(capabilities.switchLevel.level(command.args.level)) + device:emit_event(capabilities.switch.switch(command.args.level ~= 0 and "on" or "off")) + local dev = device:get_parent_device() + local config = Configuration:Set({ + parameter_number=get_notification_parameter_number(device), + configuration_value=getNotificationValue(device), + size=4 + }) + local send_configuration = function() + dev:send(config) + end + device.thread:call_with_delay(1,send_configuration) + end +end + +local function refresh_handler(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(SwitchMultilevel:Get({})) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.WATTS })) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS })) + end +end + +local function device_added(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(Association:Set({grouping_identifier = 1, node_ids = {driver.environment_info.hub_zwave_id}})) + refresh_handler(driver, device) + if is_gen2(device) then + local ledBarComponent = device.profile.components[LED_BAR_COMPONENT_NAME] + if ledBarComponent ~= nil then + device:emit_component_event(ledBarComponent, capabilities.colorControl.hue(1)) + device:emit_component_event(ledBarComponent, capabilities.colorControl.saturation(1)) + end + end + else + device:emit_event(capabilities.colorControl.hue(1)) + device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + device:emit_event(capabilities.switchLevel.level(100)) + device:emit_event(capabilities.switch.switch("off")) + end +end + +local function info_changed(driver, device, event, args) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + local time_diff = 3 + local last_clock_set_time = device:get_field(LATEST_CLOCK_SET_TIMESTAMP) + if last_clock_set_time ~= nil then + time_diff = os.difftime(os.time(), last_clock_set_time) + end + device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time(), {persist = true}) + if time_diff > 2 then + local preferences = preferencesMap.get_device_parameters(device) + if args.old_st_store.preferences["notificationChild"] ~= device.preferences.notificationChild and args.old_st_store.preferences["notificationChild"] == false and device.preferences.notificationChild == true then + if not device:get_child_by_parent_assigned_key('notification') then + add_child(driver,device,'rgbw-bulb','notification') + end + end + + for id, value in pairs(device.preferences) do + if args.old_st_store.preferences[id] ~= value and preferences and preferences[id] then + local new_parameter_value = preferences_calculate_parameter(preferences_to_numeric_value(device.preferences[id]), preferences[id].size, id) + device:send(Configuration:Set({parameter_number = preferences[id].parameter_number, size = preferences[id].size, configuration_value = new_parameter_value})) + end + end + else + log.info("info_changed running more than once. Cancelling this run. Time diff: " .. time_diff) + end + end +end + +local function switch_set_on_off_handler(value) + return function(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(Basic:Set({ value = value })) + device.thread:call_with_delay(3, function(d) + device:send(SwitchMultilevel:Get({})) + end) + else + device:emit_event(capabilities.switch.switch(value == 0 and "off" or "on")) + local dev = device:get_parent_device() + local config = Configuration:Set({ + parameter_number=get_notification_parameter_number(device), + configuration_value=(value == 0 and 0 or getNotificationValue(device)), + size=4 + }) + local send_configuration = function() + dev:send(config) + end + device.thread:call_with_delay(1,send_configuration) + end + end +end + +local function configuration_report(driver, device, cmd) + if cmd.args.parameter_number == LED_COLOR_CONTROL_PARAMETER_NUMBER and is_gen2(device) then + local hue = valueToHuePercent(cmd.args.configuration_value) + + local ledBarComponent = device.profile.components[LED_BAR_COMPONENT_NAME] + if ledBarComponent ~= nil then + device:emit_component_event(ledBarComponent, capabilities.colorControl.hue(hue)) + device:emit_component_event(ledBarComponent, capabilities.colorControl.saturation(LED_GENERIC_SATURATION)) + end + end +end + +local map_key_attribute_to_capability = { + [CentralScene.key_attributes.KEY_PRESSED_1_TIME] = capabilities.button.button.pushed, + [CentralScene.key_attributes.KEY_RELEASED] = capabilities.button.button.held, + [CentralScene.key_attributes.KEY_HELD_DOWN] = capabilities.button.button.down_hold, + [CentralScene.key_attributes.KEY_PRESSED_2_TIMES] = capabilities.button.button.pushed_2x, + [CentralScene.key_attributes.KEY_PRESSED_3_TIMES] = capabilities.button.button.pushed_3x, + [CentralScene.key_attributes.KEY_PRESSED_4_TIMES] = capabilities.button.button.pushed_4x, + [CentralScene.key_attributes.KEY_PRESSED_5_TIMES] = capabilities.button.button.pushed_5x, +} + +-- Map key attributes to their button value strings for support checking +-- TODO: This mapping and the support check below can likely be removed after a transition period. +-- Once users have interacted with their devices and the supportedButtonValues gets properly +-- set during device initialization, the driver will know which values are supported and +-- won't attempt to emit unsupported events. This code is a temporary safeguard to prevent +-- errors during the transition period. +local map_key_attribute_to_value = { + [CentralScene.key_attributes.KEY_RELEASED] = "held", + [CentralScene.key_attributes.KEY_HELD_DOWN] = "down_hold", +} + +-- TODO: Remove after transition period - button value support checking +-- Helper function to check if a button value is supported. +-- This function can likely be removed after a transition period once devices have +-- their supportedButtonValues properly set. See comment above map_key_attribute_to_value. +local function is_button_value_supported(device, component, value) + if value == nil then + return true -- If no value to check, assume supported + end + + local supported_values_state = device:get_latest_state( + component.id, + capabilities.button.ID, + capabilities.button.supportedButtonValues.NAME + ) + + -- Check multiple possible structures for supportedButtonValues + -- In SmartThings Edge, get_latest_state returns a state object + -- For supportedButtonValues, the array could be in: state.value, or state itself IS the array + local supported_values = nil + if supported_values_state ~= nil then + -- First check .value property (most common structure) + if supported_values_state.value ~= nil then + supported_values = supported_values_state.value + -- Check if state itself is an array (the state IS the array) + -- Check if index 1 exists - if it does and .value doesn't exist, the state itself is the array + elseif type(supported_values_state) == "table" and supported_values_state[1] ~= nil then + supported_values = supported_values_state + end + + -- Check .state.value structure (nested structure) + if supported_values == nil and supported_values_state.state ~= nil and supported_values_state.state.value ~= nil then + supported_values = supported_values_state.state.value + end + end + + if supported_values == nil then + return true -- If no supported values set, assume all are supported (backward compatibility) + end + + -- Check if the value is in the supported values array + if type(supported_values) == "table" then + for _, supported_value in ipairs(supported_values) do + if supported_value == value then + return true + end + end + end + + return false +end + +local function central_scene_notification_handler(self, device, cmd) + if ( cmd.args.scene_number ~= nil and cmd.args.scene_number ~= 0 ) then + local button_number = cmd.args.scene_number + local capability_attribute = map_key_attribute_to_capability[cmd.args.key_attributes] + local additional_fields = { + state_change = true + } + + local event + if capability_attribute ~= nil then + event = capability_attribute(additional_fields) + end + + if event ~= nil then + -- device reports scene notifications from endpoint 0 (main) but central scene events have to be emitted for button components: 1,2,3 + local component_name = button_to_component(button_number) + local comp = device.profile.components[component_name] + if comp ~= nil then + -- TODO: Remove after transition period - button value support checking + -- Check if held or down_hold are supported before emitting. + -- This support check can likely be removed after a transition period once devices + -- have their supportedButtonValues properly set. The driver will then only emit events + -- for values that are actually supported, preventing errors. See comment above map_key_attribute_to_value. + local button_value = map_key_attribute_to_value[cmd.args.key_attributes] + local is_supported = is_button_value_supported(device, comp, button_value) + if button_value == nil or is_supported then + device:emit_component_event(comp, event) + else + -- TODO: Remove after transition period - supportedButtonValues update for old devices + -- Update supportedButtonValues for devices with old values from previous driver versions. + -- After updating, emit the event since the value is now supported. + if supported_button_values[comp.id] ~= nil then + device:emit_component_event( + comp, + capabilities.button.supportedButtonValues( + supported_button_values[comp.id], + { visibility = { displayed = false } } + ) + ) + device:emit_component_event(comp, event) + end + end + end + end + end +end + +------------------------------------------------------------------------------------------- +-- Register message handlers and run driver +------------------------------------------------------------------------------------------- +local inovelli = { + NAME = "Inovelli Z-Wave Switch", + lifecycle_handlers = { + infoChanged = info_changed, + added = device_added, + }, + zwave_handlers = { + [cc.CENTRAL_SCENE] = { + [CentralScene.NOTIFICATION] = central_scene_notification_handler + }, + [cc.CONFIGURATION] = { + [Configuration.REPORT] = configuration_report + } + }, + capability_handlers = { + [capabilities.switch.ID] = { + [capabilities.switch.switch.on.NAME] = switch_set_on_off_handler(SwitchBinary.value.ON_ENABLE), + [capabilities.switch.switch.off.NAME] = switch_set_on_off_handler(SwitchBinary.value.OFF_DISABLE) + }, + [capabilities.colorControl.ID] = { + [capabilities.colorControl.commands.setColor.NAME] = set_color + }, + [capabilities.colorTemperature.ID] = { + [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temperature + }, + [capabilities.switchLevel.ID] = { + [capabilities.switchLevel.commands.setLevel.NAME] = switch_level_set + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh_handler + } + }, + can_handle = require("inovelli.can_handle"), + sub_drivers = require("inovelli.sub_drivers"), +} + +return inovelli \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/can_handle.lua similarity index 69% rename from drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua rename to drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/can_handle.lua index 611a8881c2..6b70eb3c27 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/can_handle.lua @@ -5,15 +5,15 @@ local INOVELLI_MANUFACTURER_ID = 0x031E local INOVELLI_LZW31SN_PRODUCT_TYPE = 0x0001 local INOVELLI_DIMMER_PRODUCT_ID = 0x0001 -local function can_handle_inovelli_lzw31sn(opts, driver, device, ...) +local function can_handle_lzw31sn(opts, driver, device, ...) if device:id_match( INOVELLI_MANUFACTURER_ID, INOVELLI_LZW31SN_PRODUCT_TYPE, INOVELLI_DIMMER_PRODUCT_ID ) then - return true, require("inovelli-LED.inovelli-lzw31sn") + return true, require("inovelli.lzw31-sn") end return false end -return can_handle_inovelli_lzw31sn +return can_handle_lzw31sn diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/init.lua new file mode 100644 index 0000000000..77a5430681 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/init.lua @@ -0,0 +1,74 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass.SwitchMultilevel +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) +--- @type st.zwave.CommandClass.Meter +local Meter = (require "st.zwave.CommandClass.Meter")({ version = 3 }) +--- @type st.zwave.CommandClass.Association +local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) +--- @type st.device +local st_device = require "st.device" + +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed"} +} + +local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" + +local function refresh_handler(driver, device) + device:send(SwitchMultilevel:Get({})) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.WATTS })) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS })) +end + +local function device_added(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(Association:Set({grouping_identifier = 1, node_ids = {driver.environment_info.hub_zwave_id}})) + for _, component in pairs(device.profile.components) do + if component.id ~= "main" and component.id ~= LED_BAR_COMPONENT_NAME then + device:emit_component_event( + component, + capabilities.button.supportedButtonValues( + supported_button_values[component.id], + { visibility = { displayed = false } } + ) + ) + device:emit_component_event( + component, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + end + end + refresh_handler(driver, device) + local ledBarComponent = device.profile.components[LED_BAR_COMPONENT_NAME] + if ledBarComponent ~= nil then + device:emit_component_event(ledBarComponent, capabilities.colorControl.hue(1)) + device:emit_component_event(ledBarComponent, capabilities.colorControl.saturation(1)) + end + else + device:emit_event(capabilities.colorControl.hue(1)) + device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + device:emit_event(capabilities.switchLevel.level(100)) + device:emit_event(capabilities.switch.switch("off")) + end +end + +local lzw31_sn = { + NAME = "Inovelli LZW31-SN Z-Wave Dimmer", + lifecycle_handlers = { + added = device_added, + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh_handler + } + }, + can_handle = require("inovelli.lzw31-sn.can_handle") +} + +return lzw31_sn \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/inovelli/sub_drivers.lua similarity index 67% rename from drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua rename to drivers/SmartThings/zwave-switch/src/inovelli/sub_drivers.lua index 6a51cd350b..e182120ece 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli/sub_drivers.lua @@ -4,5 +4,6 @@ local lazy_load = require "lazy_load_subdriver" return { - lazy_load("inovelli-LED.inovelli-lzw31sn"), + lazy_load("inovelli.lzw31-sn"), + lazy_load("inovelli.vzw32-sn") } diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/can_handle.lua new file mode 100644 index 0000000000..5ed4e272e0 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local INOVELLI_MANUFACTURER_ID = 0x031E +local INOVELLI_VZW32_SN_PRODUCT_TYPE = 0x0017 +local INOVELLI_DIMMER_PRODUCT_ID = 0x0001 + +local function can_handle_vzw32_sn(opts, driver, device, ...) + if device:id_match( + INOVELLI_MANUFACTURER_ID, + INOVELLI_VZW32_SN_PRODUCT_TYPE, + INOVELLI_DIMMER_PRODUCT_ID + ) then + return true, require("inovelli.vzw32-sn") + end + return false +end + +return can_handle_vzw32_sn diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/init.lua new file mode 100644 index 0000000000..0ad1d31430 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/init.lua @@ -0,0 +1,73 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass.SwitchMultilevel +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) +--- @type st.zwave.CommandClass.SensorMultilevel +local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ version = 7 }) +--- @type st.zwave.CommandClass.Meter +local Meter = (require "st.zwave.CommandClass.Meter")({ version = 3 }) +--- @type st.zwave.CommandClass.Notification +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +--- @type st.zwave.CommandClass.Association +local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) +--- @type st.device +local st_device = require "st.device" + +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} +} + +local function refresh_handler(driver, device) + device:send(SwitchMultilevel:Get({})) + device:send(SensorMultilevel:Get({sensor_type = SensorMultilevel.sensor_type.ILLUMINANCE})) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.WATTS })) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS })) + device:send(Notification:Get({notification_type = Notification.notification_type.HOME_SECURITY, event = Notification.event.home_security.MOTION_DETECTION})) +end + +local function device_added(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(Association:Set({grouping_identifier = 1, node_ids = {driver.environment_info.hub_zwave_id}})) + for _, component in pairs(device.profile.components) do + if component.id ~= "main" and component.id ~= "LEDColorConfiguration" then + device:emit_component_event( + component, + capabilities.button.supportedButtonValues( + supported_button_values[component.id], + { visibility = { displayed = false } } + ) + ) + device:emit_component_event( + component, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + end + end + refresh_handler(driver, device) + else + device:emit_event(capabilities.colorControl.hue(1)) + device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + device:emit_event(capabilities.switchLevel.level(100)) + device:emit_event(capabilities.switch.switch("off")) + end +end + +local vzw32_sn = { + NAME = "Inovelli VZW32-SN mmWave Dimmer", + lifecycle_handlers = { + added = device_added, + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh_handler + } + }, + can_handle = require("inovelli.vzw32-sn.can_handle") +} + +return vzw32_sn \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/preferences.lua b/drivers/SmartThings/zwave-switch/src/preferences.lua index af8e8cf772..824e570070 100644 --- a/drivers/SmartThings/zwave-switch/src/preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/preferences.lua @@ -59,6 +59,44 @@ local devices = { switchType = {parameter_number = 22, size = 1} } }, + INOVELLI_VZW32_SN = { + MATCHING_MATRIX = { + mfrs = 0x031E, + product_types = {0x0017}, + product_ids = 0x0001 + }, + PARAMETERS = { + parameter158 = {parameter_number = 158, size = 1}, + parameter52 = {parameter_number = 52, size = 1}, + parameter1 = {parameter_number = 1, size = 1}, + parameter2 = {parameter_number = 2, size = 1}, + parameter3 = {parameter_number = 3, size = 1}, + parameter4 = {parameter_number = 4, size = 1}, + parameter9 = {parameter_number = 9, size = 1}, + parameter10 = {parameter_number = 10, size = 1}, + parameter15 = {parameter_number = 15, size = 1}, + parameter18 = {parameter_number = 18, size = 1}, + parameter19 = {parameter_number = 19, size = 2}, + parameter20 = {parameter_number = 20, size = 2}, + parameter50 = {parameter_number = 50, size = 1}, + parameter95 = {parameter_number = 95, size = 1}, + parameter96 = {parameter_number = 96, size = 1}, + parameter97 = {parameter_number = 97, size = 1}, + parameter98 = {parameter_number = 98, size = 1}, + parameter101 = {parameter_number = 101, size = 2}, + parameter102 = {parameter_number = 102, size = 2}, + parameter103 = {parameter_number = 103, size = 2}, + parameter104 = {parameter_number = 104, size = 2}, + parameter105 = {parameter_number = 105, size = 2}, + parameter106 = {parameter_number = 106, size = 2}, + parameter108 = {parameter_number = 108, size = 4}, + parameter110 = {parameter_number = 110, size = 2}, + parameter111 = {parameter_number = 111, size = 1}, + parameter112 = {parameter_number = 112, size = 1}, + parameter113 = {parameter_number = 113, size = 1}, + parameter114 = {parameter_number = 114, size = 4} + } + }, QUBINO_FLUSH_DIMMER = { MATCHING_MATRIX = { mfrs = 0x0159, diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_button.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_button.lua index 4391102104..0863f9eb43 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_button.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_button.lua @@ -5,8 +5,10 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local zw = require "st.zwave" local zw_test_utils = require "integration_test.zwave_test_utils" -local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) local CentralScene = (require "st.zwave.CommandClass.CentralScene")({version=1}) +local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) +local Meter = (require "st.zwave.CommandClass.Meter")({ version = 3 }) local t_utils = require "integration_test.utils" local INOVELLI_MANUFACTURER_ID = 0x031E @@ -55,8 +57,8 @@ end test.set_test_init_function(test_init) local supported_button_values = { - ["button1"] = {"pushed", "pushed_2x", "pushed_3x", "pushed_4x", "pushed_5x"}, - ["button2"] = {"pushed", "pushed_2x", "pushed_3x", "pushed_4x", "pushed_5x"}, + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, ["button3"] = {"pushed"} } @@ -89,9 +91,44 @@ test.register_coroutine_test( test.socket.zwave:__expect_send( zw_test_utils.zwave_test_build_send_command( mock_inovelli_dimmer, - Basic:Get({}) + Association:Set({ grouping_identifier = 1, node_ids = {}, payload = "\x01" }) ) ) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_dimmer, + SwitchMultilevel:Get({}) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_dimmer, + Meter:Get({ scale = Meter.scale.electric_meter.WATTS }) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_dimmer, + Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS }) + ) + ) + + local ledBarComponent = mock_inovelli_dimmer.profile.components[LED_BAR_COMPONENT_NAME] + if ledBarComponent ~= nil then + test.socket.capability:__expect_send( + mock_inovelli_dimmer:generate_test_message( + LED_BAR_COMPONENT_NAME, + capabilities.colorControl.hue(1) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_dimmer:generate_test_message( + LED_BAR_COMPONENT_NAME, + capabilities.colorControl.saturation(1) + ) + ) + end end ) @@ -115,7 +152,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed({state_change = true})) + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed({state_change = true})) } } ) @@ -139,7 +176,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_4x({ state_change = true })) } } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_scenes.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_scenes.lua index 1202b187e2..316b9b1254 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_scenes.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_scenes.lua @@ -54,7 +54,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed({ + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed({ state_change = true })) } } @@ -74,7 +74,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_2x({ + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_2x({ state_change = true })) } } @@ -94,7 +94,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_3x({ + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_3x({ state_change = true })) } } @@ -114,7 +114,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_4x({ + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) } } @@ -134,7 +134,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_5x({ + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_5x({ state_change = true })) } } @@ -154,7 +154,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed({ + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) } } @@ -174,7 +174,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_2x({ + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_2x({ state_change = true })) } } @@ -194,7 +194,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_3x({ + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_3x({ state_change = true })) } } @@ -214,7 +214,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_4x({ + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_4x({ state_change = true })) } } @@ -234,7 +234,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_5x({ + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_5x({ state_change = true })) } } diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn.lua new file mode 100644 index 0000000000..5593ced043 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn.lua @@ -0,0 +1,325 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +local zw_test_utils = require "integration_test.zwave_test_utils" +local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({version=2}) +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) +local Basic = (require "st.zwave.CommandClass.Basic")({version=1}) +local CentralScene = (require "st.zwave.CommandClass.CentralScene")({version=3}) +local Association = (require "st.zwave.CommandClass.Association")({version=1}) +local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({version=7}) +local Meter = (require "st.zwave.CommandClass.Meter")({version=3}) +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local t_utils = require "integration_test.utils" + +-- Inovelli VZW32-SN device identifiers +local INOVELLI_MANUFACTURER_ID = 0x031E +local INOVELLI_VZW32_SN_PRODUCT_TYPE = 0x0017 +local INOVELLI_VZW32_SN_PRODUCT_ID = 0x0001 +local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" + +-- Device endpoints with supported command classes +local inovelli_vzw32_sn_endpoints = { + { + command_classes = { + {value = zw.SWITCH_BINARY}, + {value = zw.SWITCH_MULTILEVEL}, + {value = zw.BASIC}, + {value = zw.CONFIGURATION}, + {value = zw.CENTRAL_SCENE}, + {value = zw.ASSOCIATION}, + {value = zw.SENSOR_MULTILEVEL}, + {value = zw.METER}, + {value = zw.NOTIFICATION}, + } + } +} + +-- Create mock device +local mock_inovelli_vzw32_sn = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("inovelli-mmwave-dimmer-vzw32-sn.yml"), + zwave_endpoints = inovelli_vzw32_sn_endpoints, + zwave_manufacturer_id = INOVELLI_MANUFACTURER_ID, + zwave_product_type = INOVELLI_VZW32_SN_PRODUCT_TYPE, + zwave_product_id = INOVELLI_VZW32_SN_PRODUCT_ID +}) + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzw32_sn) +end +test.set_test_init_function(test_init) + +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} +} + +-- Test device initialization +test.register_coroutine_test( + "Device should initialize properly on added lifecycle event", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzw32_sn.id, "added" }) + + for button_name, _ in pairs(mock_inovelli_vzw32_sn.profile.components) do + if button_name ~= "main" and button_name ~= LED_BAR_COMPONENT_NAME then + test.socket.capability:__expect_send( + mock_inovelli_vzw32_sn:generate_test_message( + button_name, + capabilities.button.supportedButtonValues( + supported_button_values[button_name], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzw32_sn:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + end + end + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Association:Set({ + grouping_identifier = 1, + node_ids = {}, -- Mock hub Z-Wave ID + payload = "\x01", -- Should contain grouping_identifier = 1 + }) + ) + ) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SwitchMultilevel:Get({}) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SensorMultilevel:Get({sensor_type = SensorMultilevel.sensor_type.ILLUMINANCE}) + ) + ) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Meter:Get({ scale = Meter.scale.electric_meter.WATTS }) + ) + ) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS }) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Notification:Get({notification_type = Notification.notification_type.HOME_SECURITY, event = Notification.event.home_security.MOTION_DETECTION}) + ) + ) + end +) + +-- Test switch on command +test.register_coroutine_test( + "Switch on command should send Basic Set with ON value", + function() + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + test.socket.capability:__queue_receive({ + mock_inovelli_vzw32_sn.id, + { capability = "switch", command = "on", args = {} } + }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Basic:Set({ value = SwitchBinary.value.ON_ENABLE }) + ) + ) + test.wait_for_events() + test.mock_time.advance_time(3) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SwitchMultilevel:Get({}) + ) + ) + end +) + +-- Test switch off command +test.register_coroutine_test( + "Switch off command should send Basic Set with OFF value", + function() + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + test.socket.capability:__queue_receive({ + mock_inovelli_vzw32_sn.id, + { capability = "switch", command = "off", args = {} } + }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Basic:Set({ value = SwitchBinary.value.OFF_DISABLE }) + ) + ) + test.wait_for_events() + test.mock_time.advance_time(3) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SwitchMultilevel:Get({}) + ) + ) + end +) + +-- Test switch level command +test.register_coroutine_test( + "Switch level command should send SwitchMultilevel Set", + function() + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + + test.socket.capability:__queue_receive({ + mock_inovelli_vzw32_sn.id, + { capability = "switchLevel", command = "setLevel", args = { 50 } } + }) + + local expected_command = SwitchMultilevel:Set({ value = 50, duration = "default" }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + expected_command + ) + ) + + test.wait_for_events() + test.mock_time.advance_time(3) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SwitchMultilevel:Get({}) + ) + ) + end +) + +-- Test central scene notifications +test.register_message_test( + "Central scene notification should emit button events", + { + { + channel = "zwave", + direction = "receive", + message = { mock_inovelli_vzw32_sn.id, zw_test_utils.zwave_test_build_receive_command(CentralScene:Notification({ + scene_number = 1, + key_attributes=CentralScene.key_attributes.KEY_PRESSED_1_TIME + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzw32_sn:generate_test_message("button1", capabilities.button.button.pushed({ + state_change = true + })) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test central scene notifications - button2 pressed 4 times +test.register_message_test( + "Central scene notification button2 pressed 4 times should emit button events", + { + { + channel = "zwave", + direction = "receive", + message = { mock_inovelli_vzw32_sn.id, zw_test_utils.zwave_test_build_receive_command(CentralScene:Notification({ + scene_number = 2, + key_attributes=CentralScene.key_attributes.KEY_PRESSED_4_TIMES + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzw32_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ + state_change = true + })) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test refresh capability +test.register_message_test( + "Refresh capability should request switch level", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzw32_sn.id, + { capability = "refresh", command = "refresh", args = {} } + } + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SwitchMultilevel:Get({}) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SensorMultilevel:Get({sensor_type = SensorMultilevel.sensor_type.ILLUMINANCE}) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Meter:Get({ scale = Meter.scale.electric_meter.WATTS }) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS }) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Notification:Get({notification_type = Notification.notification_type.HOME_SECURITY, event = Notification.event.home_security.MOTION_DETECTION}) + ) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_child.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_child.lua new file mode 100644 index 0000000000..40213851c7 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_child.lua @@ -0,0 +1,335 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +local zw_test_utils = require "integration_test.zwave_test_utils" +local Configuration = (require "st.zwave.CommandClass.Configuration")({version=4}) +local t_utils = require "integration_test.utils" +local st_device = require "st.device" + +-- Inovelli VZW32-SN device identifiers +local INOVELLI_MANUFACTURER_ID = 0x031E +local INOVELLI_VZW32_SN_PRODUCT_TYPE = 0x0017 +local INOVELLI_VZW32_SN_PRODUCT_ID = 0x0001 + +-- Device endpoints with supported command classes +local inovelli_vzw32_sn_endpoints = { + { + command_classes = { + {value = zw.SWITCH_BINARY}, + {value = zw.SWITCH_MULTILEVEL}, + {value = zw.BASIC}, + {value = zw.CONFIGURATION}, + {value = zw.CENTRAL_SCENE}, + {value = zw.ASSOCIATION}, + } + } +} + +-- Create mock parent device +local mock_parent_device = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("inovelli-mmwave-dimmer-vzw32-sn.yml"), + zwave_endpoints = inovelli_vzw32_sn_endpoints, + zwave_manufacturer_id = INOVELLI_MANUFACTURER_ID, + zwave_product_type = INOVELLI_VZW32_SN_PRODUCT_TYPE, + zwave_product_id = INOVELLI_VZW32_SN_PRODUCT_ID +}) + +-- Create mock child device (notification device) +local mock_child_device = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("rgbw-bulb.yml"), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = "notification" +}) + +-- Set child device network type +mock_child_device.network_type = st_device.NETWORK_TYPE_CHILD + +local function test_init() + test.mock_device.add_test_device(mock_parent_device) + test.mock_device.add_test_device(mock_child_device) +end +test.set_test_init_function(test_init) + +-- Test child device initialization +test.register_message_test( + "Child device should initialize with default color values", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_child_device.id, "added" }, + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.hue(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switchLevel.level(100)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test child device switch on command +test.register_coroutine_test( + "Child device switch on should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = 100 -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "on", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_parent_device, + Configuration:Set({ + parameter_number = 99, + configuration_value = notificationValue, + size = 4 + }) + ) + ) + end +) + +-- Test child device switch off command +test.register_coroutine_test( + "Child device switch off should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "off", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_parent_device, + Configuration:Set({ + parameter_number = 99, + configuration_value = 0, -- Switch off sends 0 + size = 4 + }) + ) + ) + end +) + +-- Test child device level command +test.register_coroutine_test( + "Child device level command should emit events and send configuration to parent", + function() + local level = math.random(1, 99) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local effect = 1 -- Default notificationType + local color = 100 -- Default color for child devices (since device starts with no hue state) + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) -- Use the actual level from command + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switchLevel", command = "setLevel", args = { level } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switchLevel.level(level)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_parent_device, + Configuration:Set({ + parameter_number = 99, + configuration_value = notificationValue, + size = 4 + }) + ) + ) + end +) + +-- Test child device color command +test.register_coroutine_test( + "Child device color command should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = math.random(0, 100) -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorControl", command = "setColor", args = {{ hue = color, saturation = 100 }} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(color)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_parent_device, + Configuration:Set({ + parameter_number = 99, + configuration_value = notificationValue, + size = 4 + }) + ) + ) + end +) + +-- Test child device color temperature command +test.register_coroutine_test( + "Child device color temperature command should emit events and send configuration to parent", + function() + local temp = math.random(2700, 6500) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorTemperature", command = "setColorTemperature", args = { temp } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(temp)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_parent_device, + Configuration:Set({ + parameter_number = 99, + configuration_value = 33514751, -- Calculated: effect(1)*16777216 + hue(255)*65536 + level(100)*256 + 255 + size = 4 + }) + ) + ) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_preferences.lua new file mode 100644 index 0000000000..8cccb5726e --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_preferences.lua @@ -0,0 +1,151 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local zw = require "st.zwave" +local zw_test_utils = require "integration_test.zwave_test_utils" +local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) +local t_utils = require "integration_test.utils" + +-- Inovelli VZW32-SN device identifiers +local INOVELLI_MANUFACTURER_ID = 0x031E +local INOVELLI_VZW32_SN_PRODUCT_TYPE = 0x0017 +local INOVELLI_VZW32_SN_PRODUCT_ID = 0x0001 + +-- Device endpoints with supported command classes +local inovelli_vzw32_sn_endpoints = { + { + command_classes = { + { value = zw.SWITCH_BINARY }, + { value = zw.SWITCH_MULTILEVEL }, + { value = zw.BASIC }, + { value = zw.CONFIGURATION }, + { value = zw.CENTRAL_SCENE }, + { value = zw.ASSOCIATION }, + } + } +} + +-- Create mock device +local mock_inovelli_vzw32_sn = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("inovelli-mmwave-dimmer-vzw32-sn.yml"), + zwave_endpoints = inovelli_vzw32_sn_endpoints, + zwave_manufacturer_id = INOVELLI_MANUFACTURER_ID, + zwave_product_type = INOVELLI_VZW32_SN_PRODUCT_TYPE, + zwave_product_id = INOVELLI_VZW32_SN_PRODUCT_ID +}) + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzw32_sn) +end +test.set_test_init_function(test_init) + +-- Test parameter 1 (example preference) +do + local new_param_value = 10 + test.register_coroutine_test( + "Parameter 1 should be updated in the device configuration after change", + function() + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzw32_sn:generate_info_changed({preferences = {parameter1 = new_param_value}})) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Configuration:Set({ + parameter_number = 1, + configuration_value = new_param_value, + size = 1 + }) + ) + ) + end + ) +end + +-- Test parameter 52 (example preference) +do + local new_param_value = 25 + test.register_coroutine_test( + "Parameter 52 should be updated in the device configuration after change", + function() + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzw32_sn:generate_info_changed({preferences = {parameter52 = new_param_value}})) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Configuration:Set({ + parameter_number = 52, + configuration_value = new_param_value, + size = 1 + }) + ) + ) + end + ) +end + +-- Test parameter 158 (example preference) +do + local new_param_value = 5 + test.register_coroutine_test( + "Parameter 158 should be updated in the device configuration after change", + function() + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzw32_sn:generate_info_changed({preferences = {parameter158 = new_param_value}})) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Configuration:Set({ + parameter_number = 158, + configuration_value = new_param_value, + size = 1 + }) + ) + ) + end + ) +end + +-- Test parameter 101 (2-byte parameter) +do + local new_param_value = -400 + test.register_coroutine_test( + "Parameter 101 should be updated in the device configuration after change", + function() + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzw32_sn:generate_info_changed({preferences = {parameter101 = new_param_value}})) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Configuration:Set({ + parameter_number = 101, + configuration_value = new_param_value, + size = 2 + }) + ) + ) + end + ) +end + +-- Test notificationChild preference (special case for child device creation) +do + local new_param_value = true + test.register_coroutine_test( + "notificationChild preference should create child device when enabled", + function() + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzw32_sn:generate_info_changed({preferences = {notificationChild = new_param_value}})) + + -- Expect child device creation + mock_inovelli_vzw32_sn:expect_device_create({ + type = "EDGE_CHILD", + label = "nil Notification", -- This will be the parent label + "Notification" + profile = "rgbw-bulb", + parent_device_id = mock_inovelli_vzw32_sn.id, + parent_assigned_child_key = "notification" + }) + end + ) +end + +test.run_registered_tests() \ No newline at end of file diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index fcf79cb144..afa2fbc811 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -116,6 +116,9 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "WISTAR WSERD50-L Smart Tubular Motor",威仕达智能管状电机 WSERD50-L "WISTAR WSERD50-T Smart Tubular Motor",威仕达智能管状电机 WSERD50-T "WISTAR WSER60 Smart Tubular Motor",威仕达智能管状电机 WSER60 +"WISTAR WSCMXH Smart Vertical Blind Motor",威仕达智能梦幻帘电机 WSCMXH +"WISTAR WSCMXF Smart Vertical Blind Motor",威仕达智能梦幻帘电机 WSCMXF +"WISTAR WSCMXF-LED Smart Vertical Blind Motor",威仕达智能梦幻帘电机 WSCMXF-LED "VIVIDSTORM Smart Screen VWSDSTUST120H",VIVIDSTORM智能幕布 VWSDSTUST120H "HOPOsmart Window Opener A2230011",HOPOsmart链式开窗器 A2230011 "Yanmi Switch (3 Way)",岩米三位智能开关面板