diff --git a/drivers/SmartThings/matter-switch/profiles/light-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/light-energy-powerConsumption.yml new file mode 100644 index 0000000000..1f62938b9d --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-energy-powerConsumption.yml @@ -0,0 +1,16 @@ +name: light-energy-powerConsumption +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/light-level-energy-powerConsumption.yml new file mode 100644 index 0000000000..03963ccbd2 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-level-energy-powerConsumption.yml @@ -0,0 +1,22 @@ +name: light-level-energy-powerConsumption +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-power.yml b/drivers/SmartThings/matter-switch/profiles/light-level-power.yml new file mode 100644 index 0000000000..23625ada16 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-level-power.yml @@ -0,0 +1,20 @@ +name: light-level-power +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: powerMeter + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/profiles/light-power.yml b/drivers/SmartThings/matter-switch/profiles/light-power.yml new file mode 100644 index 0000000000..27e07d0a49 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-power.yml @@ -0,0 +1,14 @@ +name: light-power +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: powerMeter + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/init.lua new file mode 100644 index 0000000000..9691cef4d4 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/init.lua @@ -0,0 +1,50 @@ +local cluster_base = require "st.matter.cluster_base" +local DescriptorServerAttributes = require "embedded_clusters.Descriptor.server.attributes" + +local Descriptor = {} + +Descriptor.ID = 0x001D +Descriptor.NAME = "Descriptor" +Descriptor.server = {} +Descriptor.client = {} +Descriptor.server.attributes = DescriptorServerAttributes:set_parent_cluster(Descriptor) + +function Descriptor:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0003] = "PartsList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function Descriptor:get_server_command_by_id(command_id) + local server_id_map = { + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +Descriptor.attribute_direction_map = { + ["PartsList"] = "server", +} + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = Descriptor.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, Descriptor.NAME)) + end + return Descriptor[direction].attributes[key] +end +Descriptor.attributes = {} +setmetatable(Descriptor.attributes, attribute_helper_mt) + +setmetatable(Descriptor, {__index = cluster_base}) + +return Descriptor + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/PartsList.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/PartsList.lua new file mode 100644 index 0000000000..083f74a995 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/PartsList.lua @@ -0,0 +1,75 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local PartsList = { + ID = 0x0003, + NAME = "PartsList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint16", +} + +function PartsList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, PartsList.element_type) + end +end + +function PartsList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function PartsList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function PartsList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function PartsList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function PartsList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function PartsList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(PartsList, {__call = PartsList.new_value, __index = PartsList.base_type}) +return PartsList + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/init.lua new file mode 100644 index 0000000000..fb2d3706b5 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/init.lua @@ -0,0 +1,24 @@ +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.Descriptor.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local DescriptorServerAttributes = {} + +function DescriptorServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(DescriptorServerAttributes, attr_mt) + +return DescriptorServerAttributes + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/init.lua new file mode 100644 index 0000000000..de1a20007b --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/init.lua @@ -0,0 +1,52 @@ +local cluster_base = require "st.matter.cluster_base" +local PowerTopologyServerAttributes = require "embedded_clusters.PowerTopology.server.attributes" +local PowerTopologyTypes = require "embedded_clusters.PowerTopology.types" + +local PowerTopology = {} + +PowerTopology.ID = 0x009C +PowerTopology.NAME = "PowerTopology" +PowerTopology.server = {} +PowerTopology.client = {} +PowerTopology.server.attributes = PowerTopologyServerAttributes:set_parent_cluster(PowerTopology) +PowerTopology.types = PowerTopologyTypes + +function PowerTopology:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "AvailableEndpoints", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +PowerTopology.attribute_direction_map = { + ["AvailableEndpoints"] = "server", +} + +PowerTopology.FeatureMap = PowerTopology.types.Feature + +function PowerTopology.are_features_supported(feature, feature_map) + if (PowerTopology.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = PowerTopology.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, PowerTopology.NAME)) + end + return PowerTopology[direction].attributes[key] +end +PowerTopology.attributes = {} +setmetatable(PowerTopology.attributes, attribute_helper_mt) + +setmetatable(PowerTopology, {__index = cluster_base}) + +return PowerTopology + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/AvailableEndpoints.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/AvailableEndpoints.lua new file mode 100644 index 0000000000..d7946212f5 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/AvailableEndpoints.lua @@ -0,0 +1,69 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AvailableEndpoints = { + ID = 0x0000, + NAME = "AvailableEndpoints", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint16", +} + +function AvailableEndpoints:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function AvailableEndpoints:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AvailableEndpoints:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AvailableEndpoints:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AvailableEndpoints:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AvailableEndpoints:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AvailableEndpoints, {__call = AvailableEndpoints.new_value, __index = AvailableEndpoints.base_type}) +return AvailableEndpoints + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/init.lua new file mode 100644 index 0000000000..13edc98283 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/init.lua @@ -0,0 +1,20 @@ +local attr_mt = {} +attr_mt.__index = function(self, key) + local req_loc = string.format("embedded_clusters.PowerTopology.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + return raw_def +end + +local PowerTopologyServerAttributes = {} + +function PowerTopologyServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(PowerTopologyServerAttributes, attr_mt) + +return PowerTopologyServerAttributes + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/Feature.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/Feature.lua new file mode 100644 index 0000000000..af0339483e --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/Feature.lua @@ -0,0 +1,29 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.NODE_TOPOLOGY = 0x0001 +Feature.TREE_TOPOLOGY = 0x0002 +Feature.SET_TOPOLOGY = 0x0004 +Feature.DYNAMIC_POWER_FLOW = 0x0008 + +function Feature.bits_are_valid(feature) + local max = + Feature.NODE_TOPOLOGY | + Feature.TREE_TOPOLOGY | + Feature.SET_TOPOLOGY | + Feature.DYNAMIC_POWER_FLOW + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/init.lua new file mode 100644 index 0000000000..600a89845c --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/init.lua @@ -0,0 +1,11 @@ +local types_mt = {} +types_mt.__index = function(self, key) + return require("embedded_clusters.PowerTopology.types." .. key) +end + +local PowerTopologyTypes = {} + +setmetatable(PowerTopologyTypes, types_mt) + +return PowerTopologyTypes + diff --git a/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua index 73844f2fb7..8fc3acd78e 100644 --- a/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua @@ -22,6 +22,20 @@ local fields = require "utils.switch_fields" local switch_utils = require "utils.switch_utils" local color_utils = require "utils.color_utils" +local cfg = require "utils.device_configuration" +local device_cfg = cfg.DeviceCfg + +-- Include driver-side definitions when lua libs api version is < 11 +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" +end + +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +end + local AttributeHandlers = {} -- [[ ON OFF CLUSTER ATTRIBUTES ]] -- @@ -247,15 +261,10 @@ end function AttributeHandlers.active_power_handler(driver, device, ib, response) if ib.data.value then local watt_value = ib.data.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT - if ib.endpoint_id ~= 0 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) - else - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT), capabilities.powerMeter.power({ value = watt_value, unit = "W"})) - end - if type(device.register_native_capability_attr_handler) == "function" then - device:register_native_capability_attr_handler("powerMeter","power") - end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) + end + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("powerMeter","power") end end @@ -279,52 +288,97 @@ end -- [[ ELECTRICAL ENERGY MEASUREMENT CLUSTER ATTRIBUTES ]] -- -function AttributeHandlers.cumul_energy_imported_handler(driver, device, ib, response) - if ib.data.elements.energy then - local watt_hour_value = ib.data.elements.energy.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT - device:set_field(fields.TOTAL_IMPORTED_ENERGY, watt_hour_value, {persist = true}) - if ib.endpoint_id ~= 0 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) +function AttributeHandlers.energy_imported_factory(is_periodic_report) + return function(driver, device, ib, response) + -- workaround: ignore devices supporting Eve's private energy cluster AND the ElectricalEnergyMeasurement cluster + local EVE_MANUFACTURER_ID, EVE_PRIVATE_CLUSTER_ID = 0x130A, 0x130AFC01 + local eve_private_energy_eps = device:get_endpoints(EVE_PRIVATE_CLUSTER_ID) + if device.manufacturer_info.vendor_id == EVE_MANUFACTURER_ID and #eve_private_energy_eps > 0 then + return + end + local state_device = switch_utils.find_child(device, ib.endpoint_id) or device + local energy_meter_latest_state = state_device:get_latest_state( + "main", capabilities.energyMeter.ID, capabilities.energyMeter.energy.NAME, 0 -- 0 as the default if state is nil + ) + if version.api < 11 then + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct:augment_type(ib.data) + end + if ib.data.elements.energy then + local energy_imported_wh = ib.data.elements.energy.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT + if is_periodic_report then + -- handle this report only if cumulative reports are not supported + if device:get_field(fields.CUMULATIVE_REPORTS_SUPPORTED) then return end + energy_imported_wh = energy_imported_wh + energy_meter_latest_state + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = energy_imported_wh, unit = "Wh" })) + local energy_delta_wh = energy_imported_wh - energy_meter_latest_state + switch_utils.increment_field(device, fields.TOTAL_IMPORTED_ENERGY, energy_delta_wh, true) + switch_utils.report_power_consumption_to_st_energy(device) else - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT), capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) + device.log.warn("Received data from the energy imported attribute does not include a numerical energy value") end - switch_utils.report_power_consumption_to_st_energy(device, device:get_field(fields.TOTAL_IMPORTED_ENERGY)) end end -function AttributeHandlers.per_energy_imported_handler(driver, device, ib, response) - if ib.data.elements.energy then - local watt_hour_value = ib.data.elements.energy.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT - local latest_energy_report = device:get_field(fields.TOTAL_IMPORTED_ENERGY) or 0 - local summed_energy_report = latest_energy_report + watt_hour_value - device:set_field(fields.TOTAL_IMPORTED_ENERGY, summed_energy_report, {persist = true}) - device:emit_event(capabilities.energyMeter.energy({ value = summed_energy_report, unit = "Wh" })) - switch_utils.report_power_consumption_to_st_energy(device, device:get_field(fields.TOTAL_IMPORTED_ENERGY)) + +-- [[ POWER TOPOLOGY CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.available_endpoints_handler(driver, device, ib, response) + local set_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS) + for i, ep in pairs(set_topology_eps or {}) do + if ep.endpoint_id == ib.endpoint_id then + set_topology_eps[i] = nil -- seen, remove from list + local tags = "" + if ep[clusters.ElectricalPowerMeasurement.ID] then tags = tags.."-power" end + if ep[clusters.ElectricalEnergyMeasurement.ID] then tags = tags.."-energy-powerConsumption" end + table.sort(ib.data.elements) + local primary_available_ep = ib.data.elements[1].value -- for consistency, associate data with first listed EP + switch_utils.set_field_for_endpoint(device, fields.ELECTRICAL_TAGS, primary_available_ep, tags) + switch_utils.set_field_for_endpoint(device, fields.PRIMARY_ASSOCIATED_EP, ib.endpoint_id, primary_available_ep, { persist = true }) + break + end + end + + if #set_topology_eps ~= 0 then -- we have not handled all eps + device:set_field(fields.ELECTRICAL_SENSOR_EPS, set_topology_eps) -- permanently remove deleted ep + return end + + device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.SET_TOPOLOGY, {persist=true}) + device_cfg.match_profile(driver, device) end -function AttributeHandlers.energy_imported_factory(is_cumulative_report) - return function(driver, device, ib, response) - -- workaround: ignore devices supporting Eve's private energy cluster AND the ElectricalEnergyMeasurement cluster - local EVE_MANUFACTURER_ID, EVE_PRIVATE_CLUSTER_ID = 0x130A, 0x130AFC01 - local eve_private_energy_eps = device:get_endpoints(EVE_PRIVATE_CLUSTER_ID) - if device.manufacturer_info.vendor_id == EVE_MANUFACTURER_ID and #eve_private_energy_eps > 0 then - return - end - if is_cumulative_report then - AttributeHandlers.cumul_energy_imported_handler(driver, device, ib, response) - elseif device:get_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED) then - AttributeHandlers.per_energy_imported_handler(driver, device, ib, response) +-- [[ DESCRIPTOR CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.parts_list_handler(driver, device, ib, response) + local tree_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS) + for i, ep in pairs(tree_topology_eps or {}) do + if ep.endpoint_id == ib.endpoint_id then + tree_topology_eps[i] = nil -- seen, remove from list + local tags = "" + if ep[clusters.ElectricalPowerMeasurement.ID] then tags = tags.."-power" end + if ep[clusters.ElectricalEnergyMeasurement.ID] then tags = tags.."-energy-powerConsumption" end + table.sort(ib.data.elements) + local primary_available_ep = ib.data.elements[1].value -- for consistency, associate data with first listed EP + switch_utils.set_field_for_endpoint(device, fields.ELECTRICAL_TAGS, primary_available_ep, tags) + switch_utils.set_field_for_endpoint(device, fields.PRIMARY_ASSOCIATED_EP, ib.endpoint_id, primary_available_ep, { persist = true }) + break end end + + if #tree_topology_eps ~= 0 then -- we have not handled all eps + device:set_field(fields.ELECTRICAL_SENSOR_EPS, tree_topology_eps) -- permanently remove deleted ep + return + end + + device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.TREE_TOPOLOGY, {persist=true}) + device_cfg.match_profile(driver, device) end -- [[ POWER SOURCE CLUSTER ATTRIBUTES ]] -- - function AttributeHandlers.bat_percent_remaining_handler(driver, device, ib, response) if ib.data.value then device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index c7ac1f4d4f..b7071950d2 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -24,7 +24,6 @@ local fields = require "utils.switch_fields" local switch_utils = require "utils.switch_utils" local cfg = require "utils.device_configuration" local device_cfg = cfg.DeviceCfg -local switch_cfg = cfg.SwitchCfg local button_cfg = cfg.ButtonCfg local attribute_handlers = require "generic_handlers.attribute_handlers" @@ -35,9 +34,14 @@ local capability_handlers = require "generic_handlers.capability_handlers" if version.api < 11 then clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" end +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +end + local SwitchLifecycleHandlers = {} function SwitchLifecycleHandlers.device_added(driver, device) @@ -45,6 +49,8 @@ function SwitchLifecycleHandlers.device_added(driver, device) -- was created after the initial subscription report if device.network_type == device_lib.NETWORK_TYPE_CHILD then device:send(clusters.OnOff.attributes.OnOff:read(device)) + elseif device.network_type == device_lib.NETWORK_TYPE_MATTER then + switch_utils.handle_electrical_sensor_info(device) end -- call device init in case init is not called after added due to device caching @@ -80,6 +86,7 @@ end function SwitchLifecycleHandlers.device_init(driver, device) if device.network_type == device_lib.NETWORK_TYPE_MATTER then switch_utils.check_field_name_updates(device) + device:set_component_to_endpoint_fn(switch_utils.component_to_endpoint) device:set_endpoint_to_component_fn(switch_utils.endpoint_to_component) if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then @@ -87,15 +94,8 @@ function SwitchLifecycleHandlers.device_init(driver, device) end local default_endpoint_id = switch_utils.find_default_endpoint(device) -- ensure subscription to all endpoint attributes- including those mapped to child devices - for idx, ep in ipairs(device.endpoints) do + for _, ep in ipairs(device.endpoints) do if ep.endpoint_id ~= default_endpoint_id then - if device:supports_server_cluster(clusters.OnOff.ID, ep) then - local child_profile = switch_cfg.assign_child_profile(device, ep) - if idx == 1 and string.find(child_profile, "energy") then - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) - end - end local id = 0 for _, dt in ipairs(ep.device_types) do id = math.max(id, dt.device_type_id) @@ -115,14 +115,9 @@ function SwitchLifecycleHandlers.device_init(driver, device) -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. - local electrical_energy_measurement_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) - if #electrical_energy_measurement_eps > 0 then - local cumulative_energy_eps = embedded_cluster_utils.get_endpoints( - device, - clusters.ElectricalEnergyMeasurement.ID, - {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY} - ) - if #cumulative_energy_eps == 0 then device:set_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = false}) end + if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID, + {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY}) > 0 then + device:set_field(fields.CUMULATIVE_REPORTS_SUPPORTED, true, {persist = false}) end end end @@ -149,9 +144,12 @@ local matter_driver_template = { [clusters.ColorControl.attributes.CurrentX.ID] = attribute_handlers.current_x_handler, [clusters.ColorControl.attributes.CurrentY.ID] = attribute_handlers.current_y_handler, }, + [clusters.Descriptor.ID] = { + [clusters.Descriptor.attributes.PartsList.ID] = attribute_handlers.parts_list_handler, + }, [clusters.ElectricalEnergyMeasurement.ID] = { - [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = attribute_handlers.energy_imported_factory(true), - [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = attribute_handlers.energy_imported_factory(false), + [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = attribute_handlers.energy_imported_factory(false), + [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = attribute_handlers.energy_imported_factory(true), }, [clusters.ElectricalPowerMeasurement.ID] = { [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = attribute_handlers.active_power_handler, @@ -180,6 +178,9 @@ local matter_driver_template = { [clusters.PowerSource.attributes.BatChargeLevel.ID] = attribute_handlers.bat_charge_level_handler, [clusters.PowerSource.attributes.BatPercentRemaining.ID] = attribute_handlers.bat_percent_remaining_handler, }, + [clusters.PowerTopology.ID] = { + [clusters.PowerTopology.attributes.AvailableEndpoints.ID] = attribute_handlers.available_endpoints_handler, + }, [clusters.RelativeHumidityMeasurement.ID] = { [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.relative_humidity_measured_value_handler }, diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua index da8066f828..26c918fda8 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua @@ -42,7 +42,8 @@ local aqara_mock_device = test.mock_device.build_test_matter_device({ clusters = { {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, {cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 2 }, - {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 5 } + {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 5 }, + {cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 1 } -- NODE_TOPOLOGY }, device_types = { {device_type_id = 0x0016, device_type_revision = 1}, -- RootNode @@ -278,10 +279,8 @@ test.register_coroutine_test( function() test.socket.matter:__queue_receive( { - -- don't use "aqara_mock_children[aqara_child1_ep].id," - -- because energy management is at the root endpoint. aqara_mock_device.id, - clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(aqara_mock_device, 1, 17000) + clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(aqara_mock_device, 0, 17000) } ) @@ -295,7 +294,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive( { aqara_mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_19) + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 0, cumulative_report_val_19) } ) @@ -315,7 +314,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive( { aqara_mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_29) + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 0, cumulative_report_val_29) } ) @@ -332,7 +331,7 @@ test.register_coroutine_test( { aqara_mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - aqara_mock_device, 1, cumulative_report_val_39 + aqara_mock_device, 0, cumulative_report_val_39 ) } ) @@ -345,7 +344,7 @@ test.register_coroutine_test( aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ start = "1970-01-01T00:15:01Z", ["end"] = "1970-01-01T00:40:00Z", - deltaEnergy = 0.0, + deltaEnergy = 20.0, energy = 39.0 })) ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua similarity index 84% rename from drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua rename to drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua index 06ec130878..c5a211bd50 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua @@ -16,11 +16,13 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local t_utils = require "integration_test.utils" +local uint32 = require "st.matter.data_types.Uint32" local version = require "version" if version.api < 11 then clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" end local mock_device = test.mock_device.build_test_matter_device({ @@ -44,6 +46,7 @@ local mock_device = test.mock_device.build_test_matter_device({ clusters = { { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, { cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", feature_map = 0, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, }, -- SET_TOPOLOGY }, device_types = { { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor @@ -58,7 +61,27 @@ local mock_device = test.mock_device.build_test_matter_device({ device_types = { { device_type_id = 0x010B, device_type_revision = 1 }, -- Dimmable Plug In Unit } - } + }, + { + endpoint_id = 3, + clusters = { + { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, }, -- SET_TOPOLOGY + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor + } + }, + { + endpoint_id = 4, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, + { cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + }, + device_types = { + { device_type_id = 0x010B, device_type_revision = 1 }, -- Dimmable Plug In Unit + } + }, }, }) @@ -82,28 +105,22 @@ local mock_device_periodic = test.mock_device.build_test_matter_device({ { endpoint_id = 1, clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 10, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, } -- SET_TOPOLOGY }, device_types = { - { device_type_id = 0x0510, device_type_revision = 1 } -- Electrical Sensor + { device_type_id = 0x010A, device_type_revision = 1 }, -- OnOff Plug + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor } }, - { - endpoint_id = 2, - clusters = { - { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, - }, - device_types = { - { device_type_id = 0x010A, device_type_revision = 1 }, -- On Off Plug In Unit - } - } }, }) local subscribed_attributes_periodic = { clusters.OnOff.attributes.OnOff, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, } local subscribed_attributes = { clusters.OnOff.attributes.OnOff, @@ -148,14 +165,19 @@ local periodic_report_val_23 = { } local function test_init() + test.mock_device.add_test_device(mock_device) local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) for i, cluster in ipairs(subscribed_attributes) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + local read_req = clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device.id, 1) + read_req:merge(clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device.id, 3)) + test.socket.matter:__expect_send({ mock_device.id, read_req }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) end test.set_test_init_function(test_init) @@ -167,11 +189,13 @@ local function test_init_periodic() subscribe_request:merge(cluster:subscribe(mock_device_periodic)) end end - test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "added" }) + local read_req = clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device_periodic.id, 1) + test.socket.matter:__expect_send({ mock_device_periodic.id, read_req }) test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "init" }) test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) + test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) end test.register_message_test( @@ -326,26 +350,26 @@ test.register_coroutine_test( end ) -test.register_message_test( +test.register_coroutine_test( "Periodic Energy as subordinate to Cumulative Energy measurement should not generate any messages", - { - { - channel = "matter", - direction = "receive", - message = { + function() + test.socket.matter:__queue_receive( + { mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data(mock_device, 1, periodic_report_val_23) + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device, 1, periodic_report_val_23 + ) } - }, - { - channel = "matter", - direction = "receive", - message = { + ) + test.socket.matter:__queue_receive( + { mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data(mock_device, 1, periodic_report_val_23) + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device, 1, periodic_report_val_23 + ) } - }, - } + ) + end ) test.register_coroutine_test( @@ -410,9 +434,20 @@ test.register_coroutine_test( test.register_coroutine_test( "Test profile change on init for Electrical Sensor device type", function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 1, {uint32(2)})}) + test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 3, {uint32(4)})}) + mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "nil 2", + profile = "plug-level-energy-powerConsumption", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", 4) + }) end, { test_init = test_init } ) @@ -421,8 +456,10 @@ test.register_coroutine_test( "Test profile change on init for only Periodic Electrical Sensor device type", function() test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "doConfigure" }) - mock_device_periodic:expect_metadata_update({ profile = "plug-energy-powerConsumption" }) mock_device_periodic:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device_periodic.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device_periodic, 1, {uint32(1)})}) + mock_device_periodic:expect_metadata_update({ profile = "plug-energy-powerConsumption" }) end, { test_init = test_init_periodic } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua new file mode 100644 index 0000000000..f30c6f0cba --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua @@ -0,0 +1,396 @@ +-- Copyright 2025 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local uint32 = require "st.matter.data_types.Uint32" +local version = require "version" + +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" +end + +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +end + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("plug-level-power-energy-powerConsumption.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, + { cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", feature_map = 0, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 2, }, -- TREE_TOPOLOGY + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor + } + }, + { + endpoint_id = 2, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + { device_type_id = 0x010B, device_type_revision = 1 }, -- OnOff Dimmable Plug + } + }, + { + endpoint_id = 3, + clusters = { + { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 2, }, -- TREE_TOPOLOGY + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor + } + }, + { + endpoint_id = 4, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, + { cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + }, + device_types = { + { device_type_id = 0x010B, device_type_revision = 1 }, -- OnOff Dimmable Plug + } + }, + }, +}) + +local subscribed_attributes = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ElectricalPowerMeasurement.attributes.ActivePower, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, +} + +local cumulative_report_val_19 = { + energy = 19000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, +} + +local cumulative_report_val_29 = { + energy = 29000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, +} + +local cumulative_report_val_39 = { + energy = 39000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, +} + +local function test_init() + test.mock_device.add_test_device(mock_device) + local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) + for i, cluster in ipairs(subscribed_attributes) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + local read_req = clusters.Descriptor.attributes.PartsList:read(mock_device.id, 1) + read_req:merge(clusters.Descriptor.attributes.PartsList:read(mock_device.id, 3)) + test.socket.matter:__expect_send({ mock_device.id, read_req }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) +end +test.set_test_init_function(test_init) + +test.register_message_test( + "On command should send the appropriate commands", + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "main", command = "on", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, 2) + } + } + } +) + +test.register_message_test( + "Off command should send the appropriate commands", + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "off" } + } + }, + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "main", command = "off", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.Off(mock_device, 2) + } + } + } +) + +test.register_message_test( + "Active power measurement should generate correct messages", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ElectricalPowerMeasurement.server.attributes.ActivePower:build_test_report_data(mock_device, 1, 17000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerMeter.power({value = 17.0, unit="W"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } + } + } +) + +test.register_coroutine_test( + "Cumulative Energy measurement should generate correct messages", + function() + + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) + + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_19 + ) + } + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 19.0 + })) + ) + + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_29 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) + ) + + test.wait_for_events() + test.mock_time.advance_time(1500) + + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_39 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 39.0, unit = "Wh" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:40:00Z", + deltaEnergy = 20.0, + energy = 39.0 + })) + ) + end +) + +test.register_coroutine_test( + "Test profile change on init for Electrical Sensor device type", + function() + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device.id, clusters.Descriptor.attributes.PartsList:build_test_report_data(mock_device, 1, {uint32(2)})}) + test.socket.matter:__queue_receive({ mock_device.id, clusters.Descriptor.attributes.PartsList:build_test_report_data(mock_device, 3, {uint32(4)})}) + mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "nil 2", + profile = "plug-level-energy-powerConsumption", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", 4) + }) + end, + { test_init = test_init } +) + +test.register_message_test( + "Set level command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switchLevel", component = "main", command = "setLevel", args = {20,20} } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "switchLevel", capability_cmd_id = "setLevel" } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 2, math.floor(20/100.0 * 254), 20, 0 ,0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 2) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 2, 50) + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switchLevel.level(20)) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 2, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua index d8b0116101..d2a3ea39e6 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua @@ -10,13 +10,15 @@ local button_attr = capabilities.button.button -- Mock a 5-button device using endpoints non-consecutive endpoints local mock_device = test.mock_device.build_test_matter_device( { - profile = t_utils.get_profile_definition("5-button-battery.yml"), -- on a real device we would switch to this, rather than fingerprint to it + profile = t_utils.get_profile_definition("5-button-battery.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, endpoints = { { endpoint_id = 0, clusters = {}, - device_types = {} + device_types = { + {device_type_id = 0x0016, device_type_revision = 1}, -- RootNode + } }, { endpoint_id = 10, diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua index af7511100e..edaa7017c4 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua @@ -411,40 +411,44 @@ test.register_coroutine_test( clusters.Switch.server.events.ShortRelease, clusters.Switch.server.events.MultiPressComplete, } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_mcd_unsupported_switch_device_type) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_mcd_unsupported_switch_device_type)) - end + local unsup_mock_device = mock_device_mcd_unsupported_switch_device_type + local subscribe_request = cluster_subscribe_list[1]:subscribe(unsup_mock_device) + for _, cluster in ipairs(cluster_subscribe_list) do + subscribe_request:merge(cluster:subscribe(unsup_mock_device)) end - test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "added" }) - test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "added" }) + test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) - -- init results in subscription interaction - test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "init" }) - test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "init" }) + test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) - -- doConfigure sets the provisioning state to provisioned - mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ profile = "2-button" }) - mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - mock_device_mcd_unsupported_switch_device_type:expect_device_create({ + test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "doConfigure" }) + unsup_mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 1", profile = "switch-binary", - parent_device_id = mock_device_mcd_unsupported_switch_device_type.id, + parent_device_id = unsup_mock_device.id, parent_assigned_child_key = string.format("%d", 7) }) - test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "doConfigure" }) + unsup_mock_device:expect_metadata_update({ profile = "2-button" }) + unsup_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.wait_for_events() - -- simulate the profile change update taking affect and the device info changing - local device_info_copy = utils.deep_copy(mock_device_mcd_unsupported_switch_device_type.raw_st_data) - device_info_copy.profile.id = "5-buttons-battery" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "infoChanged", device_info_json }) - test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) + local updated_device_profile = t_utils.get_profile_definition("2-button.yml") + test.socket.device_lifecycle:__queue_receive(unsup_mock_device:generate_info_changed({ profile = updated_device_profile })) + local subscribe_request = cluster_subscribe_list[1]:subscribe(unsup_mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(unsup_mock_device)) + end + end + test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) + + test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) + + test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) end, { test_init = test_init_mcd_unsupported_switch_device_type } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index 056c103eed..6566b1378d 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -396,12 +396,7 @@ local mock_device_light_level_motion = test.mock_device.build_test_matter_device { endpoint_id = 1, clusters = { - { - cluster_id = clusters.OnOff.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = 0, --u32 bitmap - }, + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} }, device_types = { @@ -454,6 +449,10 @@ end local function test_init_onoff_client() test.mock_device.add_test_device(mock_device_onoff_client) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_client.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_client.id, "init" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_client.id, "doConfigure" }) + mock_device_onoff_client:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_parent_client_child_server() @@ -473,6 +472,8 @@ end local function test_init_dimmer() test.mock_device.add_test_device(mock_device_dimmer) + test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "doConfigure" }) mock_device_dimmer:expect_metadata_update({ profile = "switch-level" }) mock_device_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -708,4 +709,4 @@ test.register_coroutine_test( { test_init = test_init_light_level_motion } ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index cff4c7b60b..15ba79e061 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -689,4 +689,4 @@ test.register_coroutine_test( { test_init = test_init_parent_child_endpoints_non_sequential } ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua index 323efea478..15ba79e061 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua @@ -1,4 +1,4 @@ --- Copyright 2023 SmartThings +-- 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. @@ -19,15 +19,17 @@ local clusters = require "st.matter.clusters" test.disable_startup_messages() -local child_profile = t_utils.get_profile_definition("plug-binary.yml") -local child_profile_override = t_utils.get_profile_definition("switch-binary.yml") +local TRANSITION_TIME = 0 +local OPTIONS_MASK = 0x01 +local OPTIONS_OVERRIDE = 0x01 + local parent_ep = 10 local child1_ep = 20 local child2_ep = 30 local mock_device = test.mock_device.build_test_matter_device({ label = "Matter Switch", - profile = t_utils.get_profile_definition("plug-binary.yml"), + profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -48,36 +50,44 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, }, device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + {device_type_id = 0x0100, device_type_revision = 2} -- On/Off Light } }, { endpoint_id = child1_ep, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} }, device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light } }, { endpoint_id = child2_ep, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, }, device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light } }, } }) -local mock_device_child_profile_override = test.mock_device.build_test_matter_device({ +local child1_ep_non_sequential = 50 +local child2_ep_non_sequential = 30 +local child3_ep_non_sequential = 40 + +local mock_device_parent_child_endpoints_non_sequential = test.mock_device.build_test_matter_device({ label = "Matter Switch", - profile = t_utils.get_profile_definition("switch-binary.yml"), + profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), manufacturer_info = { vendor_id = 0x1321, - product_id = 0x000D, + product_id = 0x000C, }, endpoints = { { @@ -95,20 +105,33 @@ local mock_device_child_profile_override = test.mock_device.build_test_matter_de {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, }, device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + {device_type_id = 0x0100, device_type_revision = 2} -- On/Off Light } }, { - endpoint_id = child1_ep, + endpoint_id = child1_ep_non_sequential, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} }, device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light } }, { - endpoint_id = child2_ep, + endpoint_id = child2_ep_non_sequential, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, + }, + device_types = { + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light + } + }, + { + endpoint_id = child3_ep_non_sequential, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, }, @@ -119,11 +142,16 @@ local mock_device_child_profile_override = test.mock_device.build_test_matter_de } }) +local child_profiles = { + [child1_ep] = t_utils.get_profile_definition("light-level.yml"), + [child2_ep] = t_utils.get_profile_definition("light-color-level.yml"), +} + local mock_children = {} for i, endpoint in ipairs(mock_device.endpoints) do if endpoint.endpoint_id ~= parent_ep and endpoint.endpoint_id ~= 0 then local child_data = { - profile = child_profile, + profile = child_profiles[endpoint.endpoint_id], device_network_id = string.format("%s:%d", mock_device.id, endpoint.endpoint_id), parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) @@ -136,8 +164,23 @@ local function test_init() test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) @@ -146,7 +189,7 @@ local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "plug-binary" }) + mock_device:expect_metadata_update({ profile = "light-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children) do @@ -156,7 +199,7 @@ local function test_init() mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", - profile = "plug-binary", + profile = "light-level", parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", child1_ep) }) @@ -164,60 +207,90 @@ local function test_init() mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 3", - profile = "plug-binary", + profile = "light-color-level", parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", child2_ep) }) end -local mock_children_child_profile_override = {} -for i, endpoint in ipairs(mock_device_child_profile_override.endpoints) do +local child_profiles_non_sequential = { + [child1_ep_non_sequential] = t_utils.get_profile_definition("light-level.yml"), + [child2_ep_non_sequential] = t_utils.get_profile_definition("light-color-level.yml"), + [child3_ep_non_sequential] = t_utils.get_profile_definition("light-color-level.yml"), +} + +local mock_children_non_sequential = {} +for i, endpoint in ipairs(mock_device_parent_child_endpoints_non_sequential.endpoints) do if endpoint.endpoint_id ~= parent_ep and endpoint.endpoint_id ~= 0 then local child_data = { - profile = child_profile_override, - device_network_id = string.format("%s:%d", mock_device_child_profile_override.id, endpoint.endpoint_id), - parent_device_id = mock_device_child_profile_override.id, + profile = child_profiles_non_sequential[endpoint.endpoint_id], + device_network_id = string.format("%s:%d", mock_device_parent_child_endpoints_non_sequential.id, endpoint.endpoint_id), + parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) } - mock_children_child_profile_override[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) + mock_children_non_sequential[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) end end -local function test_init_child_profile_override() - test.mock_device.add_test_device(mock_device_child_profile_override) +local function test_init_parent_child_endpoints_non_sequential() + test.mock_device.add_test_device(mock_device_parent_child_endpoints_non_sequential) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_child_profile_override) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_parent_child_endpoints_non_sequential) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_parent_child_endpoints_non_sequential)) + end + end - test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "added" }) - test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "added" }) + test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "init" }) - test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "init" }) + test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "doConfigure" }) - mock_device_child_profile_override:expect_metadata_update({ profile = "switch-binary" }) - mock_device_child_profile_override:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "doConfigure" }) + mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "light-binary" }) + mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - for _, child in pairs(mock_children_child_profile_override) do + for _, child in pairs(mock_children_non_sequential) do test.mock_device.add_test_device(child) end - mock_device:expect_device_create({ + mock_device_parent_child_endpoints_non_sequential:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", - profile = "switch-binary", - parent_device_id = mock_device_child_profile_override.id, - parent_assigned_child_key = string.format("%d", child1_ep) + profile = "light-color-level", + parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, + parent_assigned_child_key = string.format("%d", child2_ep_non_sequential) }) - mock_device:expect_device_create({ + -- switch-binary will be selected as an overridden child device profile + mock_device_parent_child_endpoints_non_sequential:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 3", profile = "switch-binary", - parent_device_id = mock_device_child_profile_override.id, - parent_assigned_child_key = string.format("%d", child2_ep) + parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, + parent_assigned_child_key = string.format("%d", child3_ep_non_sequential) + }) + + mock_device_parent_child_endpoints_non_sequential:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 4", + profile = "light-level", + parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, + parent_assigned_child_key = string.format("%d", child1_ep_non_sequential) }) end @@ -376,19 +449,244 @@ test.register_message_test( } ) -test.register_coroutine_test( - "Added should call refresh for child devices", function() - test.socket.matter:__set_channel_ordering("relaxed") - test.socket.device_lifecycle:__queue_receive({ mock_children[child1_ep].id, "added" }) - local req = clusters.OnOff.attributes.OnOff:read(mock_children[child1_ep]) - test.socket.matter:__expect_send({mock_device.id, req}) - end +test.register_message_test( + "Current level reports should generate appropriate events", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.attributes.CurrentLevel:build_test_report_data(mock_device, child1_ep, 50) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child1_ep]:generate_test_message("main", capabilities.switchLevel.level(math.floor((50 / 254.0 * 100) + 0.5))) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } + } + }, + } +) + +test.register_message_test( + "Set color temperature should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_children[child2_ep].id, + { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child2_ep, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, child2_ep) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, child2_ep, 556) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) + }, + } +) + +test.register_message_test( + "X and Y color values should report hue and saturation once both have been received", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, child2_ep, 15091) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, child2_ep, 21547) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.hue(50)) + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.saturation(72)) + } + } +) + +test.register_message_test( + "Set color command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_children[child2_ep].id, + { capability = "colorControl", component = "main", command = "setColor", args = { { hue = 50, saturation = 72 } } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColor(mock_device, child2_ep, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColor:build_test_command_response(mock_device, child2_ep) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, child2_ep, 15091) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, child2_ep, 21547) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.hue(50)) + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.saturation(72)) + } + } +) + +test.register_message_test( + "Min and max level attributes set capability constraint for child devices", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, child1_ep, 1) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, child1_ep, 254) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child1_ep]:generate_test_message("main", capabilities.switchLevel.levelRange({minimum = 1, maximum = 100})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, child2_ep, 127) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, child2_ep, 203) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.switchLevel.levelRange({minimum = 50, maximum = 80})) + } + } +) + +test.register_message_test( + "Min and max color temp attributes set capability constraint for child devices", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, child2_ep, 153) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, child2_ep, 555) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 1800, maximum = 6500})) + } + } ) test.register_coroutine_test( - "Child device profiles should be overriden for specific devices", function() - end, - { test_init = test_init_child_profile_override } + "Test child devices are created in order of their endpoints", + function() + end, + { test_init = test_init_parent_child_endpoints_non_sequential } ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua index 23b5ca6873..56312a9e13 100644 --- a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua @@ -41,10 +41,12 @@ function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_on local generic_profile = fields.device_type_profile_map[primary_dt_id] - if is_child_device and ( - server_onoff_ep_id == switch_utils.get_product_override_field(device, "ep_id") or - generic_profile == switch_utils.get_product_override_field(device, "initial_profile") - ) then + local static_electrical_tags = switch_utils.get_field_for_endpoint(device, fields.ELECTRICAL_TAGS, server_onoff_ep_id) + if static_electrical_tags ~= nil then + generic_profile = string.gsub(generic_profile, "-binary", "") .. static_electrical_tags + end + + if is_child_device and generic_profile == switch_utils.get_product_override_field(device, "initial_profile") then generic_profile = switch_utils.get_product_override_field(device, "target_profile") or generic_profile end @@ -62,20 +64,17 @@ function SwitchDeviceConfiguration.create_child_devices(driver, device, server_o for idx, ep_id in ipairs(server_onoff_ep_ids) do device_num = device_num + 1 if ep_id ~= default_endpoint_id then -- don't create a child device that maps to the main endpoint - local name = string.format("%s %d", device.label, device_num) - local child_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, ep_id, true) - driver:try_create_device({ - type = "EDGE_CHILD", - label = name, - profile = child_profile, - parent_device_id = device.id, - parent_assigned_child_key = string.format("%d", ep_id), - vendor_provided_label = name - }) - if idx == 1 and string.find(child_profile, "energy") then - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep_id, {persist = true}) - end + local label_and_name = string.format("%s %d", device.label, device_num) + driver:try_create_device( + { + type = "EDGE_CHILD", + label = label_and_name, + profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, ep_id, true), + parent_device_id = device.id, + parent_assigned_child_key = string.format("%d", ep_id), + vendor_provided_label = label_and_name + } + ) end end @@ -157,9 +156,20 @@ end -- [[ PROFILE MATCHING AND CONFIGURATIONS ]] -- +local function profiling_data_still_required(device) + for _, field in pairs(fields.profiling_data) do + if device:get_field(field) == nil then + return true -- data still required if a field is nil + end + end + return false +end + function DeviceConfiguration.match_profile(driver, device) + if profiling_data_still_required(device) then return end + local default_endpoint_id = switch_utils.find_default_endpoint(device) - local updated_profile = nil + local updated_profile if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) > 0 then updated_profile = "water-valve" @@ -177,16 +187,7 @@ function DeviceConfiguration.match_profile(driver, device) if switch_utils.tbl_contains(server_onoff_ep_ids, default_endpoint_id) then updated_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, default_endpoint_id) local generic_profile = function(s) return string.find(updated_profile or "", s, 1, true) end - if generic_profile("plug-binary") or generic_profile("plug-level") then - if switch_utils.check_switch_category_vendor_overrides(device) then - updated_profile = string.gsub(updated_profile, "plug", "switch") - else - local electrical_tags = "" - if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) > 0 then electrical_tags = electrical_tags .. "-power" end - if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) > 0 then electrical_tags = electrical_tags .. "-energy-powerConsumption" end - if electrical_tags ~= "" then updated_profile = string.gsub(updated_profile, "-binary", "") .. electrical_tags end - end - elseif generic_profile("light-color-level") and #device:get_endpoints(clusters.FanControl.ID) > 0 then + if generic_profile("light-color-level") and #device:get_endpoints(clusters.FanControl.ID) > 0 then updated_profile = "light-color-level-fan" elseif generic_profile("light-level") and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then updated_profile = "light-level-motion" diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua index dad46d4c92..f07f18d6d9 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua @@ -47,12 +47,12 @@ SwitchFields.CURRENT_HUESAT_ATTR_MAX = 254 SwitchFields.DEVICE_TYPE_ID = { AGGREGATOR = 0x000E, + ON_OFF_PLUG_IN_UNIT = 0x010A, DIMMABLE_PLUG_IN_UNIT = 0x010B, - ELECTRICAL_SENSOR = 0x0510, - GENERIC_SWITCH = 0x000F, MOUNTED_ON_OFF_CONTROL = 0x010F, MOUNTED_DIMMABLE_LOAD_CONTROL = 0x0110, - ON_OFF_PLUG_IN_UNIT = 0x010A, + GENERIC_SWITCH = 0x000F, + ELECTRICAL_SENSOR = 0x0510, LIGHT = { ON_OFF = 0x0100, DIMMABLE = 0x0101, @@ -80,18 +80,14 @@ SwitchFields.device_type_profile_map = { [SwitchFields.DEVICE_TYPE_ID.MOUNTED_DIMMABLE_LOAD_CONTROL] = "switch-level", } - -SwitchFields.CONVERSION_CONST_MILLIWATT_TO_WATT = 1000 -- A milliwatt is 1/1000th of a watt - - -- COMPONENT_TO_ENDPOINT_MAP is here to preserve the endpoint mapping for -- devices that were joined to this driver as MCD devices before the transition -- to join switch devices as parent-child. This value will exist in the device -- table for devices that joined prior to this transition, and is also used for -- button devices that require component mapping. SwitchFields.COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -SwitchFields.ENERGY_MANAGEMENT_ENDPOINT = "__energy_management_endpoint" SwitchFields.IS_PARENT_CHILD_DEVICE = "__is_parent_child_device" +SwitchFields.PRIMARY_ASSOCIATED_EP = "__PRIMARY_ASSOCIATED_EP" SwitchFields.COLOR_TEMP_BOUND_RECEIVED_KELVIN = "__colorTemp_bound_received_kelvin" SwitchFields.COLOR_TEMP_BOUND_RECEIVED_MIRED = "__colorTemp_bound_received_mired" SwitchFields.COLOR_TEMP_MIN = "__color_temp_min" @@ -103,26 +99,18 @@ SwitchFields.COLOR_MODE = "__color_mode" SwitchFields.updated_fields = { { current_field_name = "__component_to_endpoint_map_button", updated_field_name = SwitchFields.COMPONENT_TO_ENDPOINT_MAP }, - { current_field_name = "__switch_intialized", updated_field_name = nil } + { current_field_name = "__switch_intialized", updated_field_name = nil }, + { current_field_name = "__energy_management_endpoint", updated_field_name = nil } } -SwitchFields.HUE_SAT_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION -SwitchFields.X_Y_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY - - SwitchFields.vendor_overrides = { - [0x1321] = { + [0x1321] = { -- SONOFF_MANUFACTURER_ID [0x000C] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, [0x000D] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, }, [0x115F] = { -- AQARA_MANUFACTURER_ID - [0x1003] = { target_profile = "light-power-energy-powerConsumption", ep_id = 1 }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - [0x1004] = { target_profile = "light-power-energy-powerConsumption", ep_id = 1 }, -- 2 Buttons(Generic Switch), 2 Channels(On/Off Light) - [0x1005] = { target_profile = "light-power-energy-powerConsumption", ep_id = 1 }, -- 4 Buttons(Generic Switch), 3 Channels(On/Off Light) - [0x1008] = { target_profile = "light-power-energy-powerConsumption", ep_id = 1 }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - [0x1009] = { target_profile = "light-power-energy-powerConsumption", ep_id = 1 }, -- 4 Buttons(Generic Switch), 2 Channels(On/Off Light) - [0x1006] = { ignore_combo_switch_button = true, target_profile = "light-level-power-energy-powerConsumption", ep_id = 1 }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) - [0x100A] = { ignore_combo_switch_button = true, target_profile = "light-level-power-energy-powerConsumption", ep_id = 1 }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) + [0x1006] = { ignore_combo_switch_button = true }, -- 3 Buttons(Generic Switch), 1 Channel (Dimmable Light) + [0x100A] = { ignore_combo_switch_button = true }, -- 1 Buttons(Generic Switch), 1 Channel (Dimmable Light) [0x2004] = { is_climate_sensor_w100 = true }, -- Climate Sensor W100, requires unique profile } } @@ -152,7 +140,15 @@ SwitchFields.switch_category_vendor_overrides = { {0xEEE2, 0xAB08, 0xAB31, 0xAB04, 0xAB01, 0xAB43, 0xAB02, 0xAB03, 0xAB05} } -SwitchFields.CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" +SwitchFields.CONVERSION_CONST_MILLIWATT_TO_WATT = 1000 -- A milliwatt is 1/1000th of a watt +SwitchFields.POWER_CONSUMPTION_REPORT_EP = "__POWER_CONSUMPTION_REPORT_EP" +SwitchFields.ELECTRICAL_SENSOR_EPS = "__ELECTRICAL_SENSOR_EPS" +SwitchFields.ELECTRICAL_TAGS = "__ELECTRICAL_TAGS" +SwitchFields.profiling_data = { + POWER_TOPOLOGY = "__POWER_TOPOLOGY", +} + +SwitchFields.CUMULATIVE_REPORTS_SUPPORTED = "__cumulative_reports_supported" SwitchFields.TOTAL_IMPORTED_ENERGY = "__total_imported_energy" SwitchFields.LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" SwitchFields.MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua index 2715f2e242..033283dbd8 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua @@ -14,10 +14,23 @@ local fields = require "utils.switch_fields" local st_utils = require "st.utils" +local version = require "version" local clusters = require "st.matter.clusters" local capabilities = require "st.capabilities" +local im = require "st.matter.interaction_model" local log = require "log" +-- Include driver-side definitions when lua libs api version is < 11 +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" +end + +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +end + local utils = {} function utils.tbl_contains(array, value) @@ -42,6 +55,10 @@ function utils.set_field_for_endpoint(device, field, endpoint, value, additional device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) end +function utils.increment_field(device, field, increment, persist) + device:set_field(field, (device:get_field(field) or 0) + increment, { persist = persist }) +end + function utils.mired_to_kelvin(value, minOrMax) if value == 0 then -- shouldn't happen, but has value = 1 @@ -187,8 +204,9 @@ function utils.endpoint_to_component(device, ep) return "main" end -function utils.find_child(parent, ep_id) - return parent:get_child_by_parent_assigned_key(string.format("%d", ep_id)) +function utils.find_child(parent_device, ep_id) + local primary_ep_key = utils.get_field_for_endpoint(parent_device, fields.PRIMARY_ASSOCIATED_EP, ep_id) or ep_id + return parent_device:get_child_by_parent_assigned_key(string.format("%d", primary_ep_key)) end function utils.get_endpoint_info(device, endpoint_id) @@ -203,6 +221,17 @@ function utils.matter_handler(driver, device, response_block) device.log.info(string.format("Fallback handler for %s", response_block)) end +--helper function to create list of multi press values +function utils.create_multi_press_values_list(size, supportsHeld) + local list = {"pushed", "double"} + if supportsHeld then table.insert(list, "held") end + -- add multi press values of 3 or greater to the list + for i=3, size do + table.insert(list, string.format("pushed_%dx", i)) + end + return list +end + -- get a list of endpoints for a specified device type. function utils.get_endpoints_by_device_type(device, device_type_id) local dt_eps = {} @@ -216,31 +245,19 @@ function utils.get_endpoints_by_device_type(device, device_type_id) return dt_eps end ---helper function to create list of multi press values -function utils.create_multi_press_values_list(size, supportsHeld) - local list = {"pushed", "double"} - if supportsHeld then table.insert(list, "held") end - -- add multi press values of 3 or greater to the list - for i=3, size do - table.insert(list, string.format("pushed_%dx", i)) - end - return list -end - function utils.detect_bridge(device) return #utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.AGGREGATOR) > 0 end function utils.detect_matter_thing(device) - for _, capability in ipairs(fields.supported_capabilities) do - if device:supports_capability(capability) then - return false - end + -- every profile except for matter-thing supports at least 2 capabilities (refresh, firmwareUpdate) + for i, _ in pairs(device.profile.components.main.capabilities) do + if i > 1 then return false end end - return device:supports_capability(capabilities.refresh) + return true end -function utils.report_power_consumption_to_st_energy(device, latest_total_imported_energy_wh) +function utils.report_power_consumption_to_st_energy(device) local current_time = os.time() local last_time = device:get_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP) or 0 @@ -248,35 +265,84 @@ function utils.report_power_consumption_to_st_energy(device, latest_total_import if fields.MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then return end - device:set_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) - -- Calculate the energy delta between reports - local energy_delta_wh = 0.0 - local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, - capabilities.powerConsumptionReport.powerConsumption.NAME) - if previous_imported_report and previous_imported_report.energy then - energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) - end + local total_imported_energy_wh = device:get_field(fields.TOTAL_IMPORTED_ENERGY) + local state_device = utils.find_child(device, device:get_field(fields.POWER_CONSUMPTION_REPORT_EP)) or device + local previous_imported_report = state_device:get_latest_state("main", capabilities.powerConsumptionReport.ID, + capabilities.powerConsumptionReport.powerConsumption.NAME, { energy = total_imported_energy_wh }) -- default value if nil + local energy_delta_wh = total_imported_energy_wh - previous_imported_report.energy -- Calculate the energy delta between reports + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' local epoch_to_iso8601 = function(time) return os.date("!%Y-%m-%dT%H:%M:%SZ", time) end -- Return an ISO-8061 timestamp from UTC + device:emit_event_for_endpoint(device:get_field(fields.POWER_CONSUMPTION_REPORT_EP), capabilities.powerConsumptionReport.powerConsumption({ + start = epoch_to_iso8601(last_time), + ["end"] = epoch_to_iso8601(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = total_imported_energy_wh + })) +end - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - if not device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT) then - device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ - start = epoch_to_iso8601(last_time), - ["end"] = epoch_to_iso8601(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) - else - device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT),capabilities.powerConsumptionReport.powerConsumption({ - start = epoch_to_iso8601(last_time), - ["end"] = epoch_to_iso8601(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) +function utils.handle_electrical_sensor_info(device) + local el_dt_eps = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.ELECTRICAL_SENSOR) + local electrical_sensor_eps = {} + local available_eps_req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + local parts_list_req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + for _, ep in ipairs(device.endpoints) do + if utils.tbl_contains(el_dt_eps, ep.endpoint_id) then + local el_ep_info = { endpoint_id = ep.endpoint_id } + for _, cluster in ipairs(ep.clusters) do + el_ep_info[cluster.cluster_id] = cluster.feature_map -- key the cluster's feature map on each supported cluster id + end + table.insert(electrical_sensor_eps, el_ep_info) + -- these read requests will ONLY be sent if the device supports the TREE_TOPOLOGY or SET_TOPOLOGY features, respectively + parts_list_req:merge(clusters.Descriptor.attributes.PartsList:read(device, ep.endpoint_id)) -- TREE read + available_eps_req:merge(clusters.PowerTopology.attributes.AvailableEndpoints:read(device, ep.endpoint_id)) -- SET read + end end + + local electrical_ep = electrical_sensor_eps[1] or {} + device:set_field(fields.POWER_CONSUMPTION_REPORT_EP, electrical_ep.endpoint_id, { persist = true }) + + local electrical_ep_has_feature = function(feature_name) + local feature = clusters.PowerTopology.types.Feature[feature_name] + if feature then + return clusters.PowerTopology.are_features_supported(feature, electrical_ep[clusters.PowerTopology.ID] or 0) + end + end + + if electrical_ep_has_feature("SET_TOPOLOGY") then + device:set_field(fields.ELECTRICAL_SENSOR_EPS, electrical_sensor_eps) -- assume any other stored EPs also have a SET topology + device:send(available_eps_req) + return + end + + if electrical_ep_has_feature("TREE_TOPOLOGY") then + device:set_field(fields.ELECTRICAL_SENSOR_EPS, electrical_sensor_eps) -- assume any other stored EPs also have a TREE topology + device:send(parts_list_req) + return + end + + if electrical_ep_has_feature("NODE_TOPOLOGY") then + -- ElectricalSensor EP has a NODE topology, so this is the ONLY Electrical Sensor EP + device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.NODE_TOPOLOGY, {persist=true}) + -- associate this EP's electrical tags with the first OnOff EP. These are not necessarily the same EP. + local tags = "" + if electrical_ep[clusters.ElectricalPowerMeasurement.ID] then tags = tags.."-power" end + if electrical_ep[clusters.ElectricalEnergyMeasurement.ID] then tags = tags.."-energy-powerConsumption" end + local switch_eps = device:get_endpoints(clusters.OnOff.ID) + table.sort(switch_eps) + if switch_eps[1] then + utils.set_field_for_endpoint(device, fields.PRIMARY_ASSOCIATED_EP, electrical_ep.endpoint_id, switch_eps[1], { persist = true }) + utils.set_field_for_endpoint(device, fields.ELECTRICAL_TAGS, switch_eps[1], tags) + else + device.log.warn("Electrical Sensor EP with NODE topology found, but no OnOff EPs exist. Electrical Sensor capabilities will not be exposed.") + end + return + end + + -- no Electrical Sensor EPs are supported + device:set_field(fields.profiling_data.POWER_TOPOLOGY, false, {persist=true}) end return utils