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/ElectricalEnergyMeasurement/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua index f2a812055b..8c96de0563 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua @@ -12,15 +12,8 @@ ElectricalEnergyMeasurement.types = ElectricalEnergyMeasurementTypes function ElectricalEnergyMeasurement:get_attribute_by_id(attr_id) local attr_id_map = { - [0x0000] = "Accuracy", [0x0001] = "CumulativeEnergyImported", - [0x0002] = "CumulativeEnergyExported", [0x0003] = "PeriodicEnergyImported", - [0x0004] = "PeriodicEnergyExported", - [0x0005] = "CumulativeEnergyReset", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", } local attr_name = attr_id_map[attr_id] if attr_name ~= nil then @@ -30,15 +23,8 @@ function ElectricalEnergyMeasurement:get_attribute_by_id(attr_id) end ElectricalEnergyMeasurement.attribute_direction_map = { - ["Accuracy"] = "server", ["CumulativeEnergyImported"] = "server", - ["CumulativeEnergyExported"] = "server", ["PeriodicEnergyImported"] = "server", - ["PeriodicEnergyExported"] = "server", - ["CumulativeEnergyReset"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", } ElectricalEnergyMeasurement.FeatureMap = ElectricalEnergyMeasurement.types.Feature diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua index 717ba6a2f3..7b9bc03714 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua @@ -9,75 +9,6 @@ Feature.EXPORTED_ENERGY = 0x0002 Feature.CUMULATIVE_ENERGY = 0x0004 Feature.PERIODIC_ENERGY = 0x0008 -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - IMPORTED_ENERGY = 0x0001, - EXPORTED_ENERGY = 0x0002, - CUMULATIVE_ENERGY = 0x0004, - PERIODIC_ENERGY = 0x0008, -} - -Feature.is_imported_energy_set = function(self) - return (self.value & self.IMPORTED_ENERGY) ~= 0 -end - -Feature.set_imported_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.IMPORTED_ENERGY - else - self.value = self.IMPORTED_ENERGY - end -end - -Feature.unset_imported_energy = function(self) - self.value = self.value & (~self.IMPORTED_ENERGY & self.BASE_MASK) -end -Feature.is_exported_energy_set = function(self) - return (self.value & self.EXPORTED_ENERGY) ~= 0 -end - -Feature.set_exported_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.EXPORTED_ENERGY - else - self.value = self.EXPORTED_ENERGY - end -end - -Feature.unset_exported_energy = function(self) - self.value = self.value & (~self.EXPORTED_ENERGY & self.BASE_MASK) -end -Feature.is_cumulative_energy_set = function(self) - return (self.value & self.CUMULATIVE_ENERGY) ~= 0 -end - -Feature.set_cumulative_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.CUMULATIVE_ENERGY - else - self.value = self.CUMULATIVE_ENERGY - end -end - -Feature.unset_cumulative_energy = function(self) - self.value = self.value & (~self.CUMULATIVE_ENERGY & self.BASE_MASK) -end -Feature.is_periodic_energy_set = function(self) - return (self.value & self.PERIODIC_ENERGY) ~= 0 -end - -Feature.set_periodic_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.PERIODIC_ENERGY - else - self.value = self.PERIODIC_ENERGY - end -end - -Feature.unset_periodic_energy = function(self) - self.value = self.value & (~self.PERIODIC_ENERGY & self.BASE_MASK) -end - function Feature.bits_are_valid(feature) local max = Feature.IMPORTED_ENERGY | @@ -91,25 +22,6 @@ function Feature.bits_are_valid(feature) end end -Feature.mask_methods = { - is_imported_energy_set = Feature.is_imported_energy_set, - set_imported_energy = Feature.set_imported_energy, - unset_imported_energy = Feature.unset_imported_energy, - is_exported_energy_set = Feature.is_exported_energy_set, - set_exported_energy = Feature.set_exported_energy, - unset_exported_energy = Feature.unset_exported_energy, - is_cumulative_energy_set = Feature.is_cumulative_energy_set, - set_cumulative_energy = Feature.set_cumulative_energy, - unset_cumulative_energy = Feature.unset_cumulative_energy, - is_periodic_energy_set = Feature.is_periodic_energy_set, - set_periodic_energy = Feature.set_periodic_energy, - unset_periodic_energy = Feature.unset_periodic_energy, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - setmetatable(Feature, new_mt) return Feature diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua index c9f401dd5c..222c80090b 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua @@ -1,6 +1,5 @@ local cluster_base = require "st.matter.cluster_base" local ElectricalPowerMeasurementServerAttributes = require "embedded_clusters.ElectricalPowerMeasurement.server.attributes" -local ElectricalPowerMeasurementTypes = require "embedded_clusters.ElectricalPowerMeasurement.types" local ElectricalPowerMeasurement = {} @@ -9,32 +8,10 @@ ElectricalPowerMeasurement.NAME = "ElectricalPowerMeasurement" ElectricalPowerMeasurement.server = {} ElectricalPowerMeasurement.client = {} ElectricalPowerMeasurement.server.attributes = ElectricalPowerMeasurementServerAttributes:set_parent_cluster(ElectricalPowerMeasurement) -ElectricalPowerMeasurement.types = ElectricalPowerMeasurementTypes function ElectricalPowerMeasurement:get_attribute_by_id(attr_id) local attr_id_map = { - [0x0000] = "PowerMode", - [0x0001] = "NumberOfMeasurementTypes", - [0x0002] = "Accuracy", - [0x0003] = "Ranges", - [0x0004] = "Voltage", - [0x0005] = "ActiveCurrent", - [0x0006] = "ReactiveCurrent", - [0x0007] = "ApparentCurrent", [0x0008] = "ActivePower", - [0x0009] = "ReactivePower", - [0x000A] = "ApparentPower", - [0x000B] = "RMSVoltage", - [0x000C] = "RMSCurrent", - [0x000D] = "RMSPower", - [0x000E] = "Frequency", - [0x000F] = "HarmonicCurrents", - [0x0010] = "HarmonicPhases", - [0x0011] = "PowerFactor", - [0x0012] = "NeutralCurrent", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", } local attr_name = attr_id_map[attr_id] if attr_name ~= nil then @@ -44,39 +21,9 @@ function ElectricalPowerMeasurement:get_attribute_by_id(attr_id) end ElectricalPowerMeasurement.attribute_direction_map = { - ["PowerMode"] = "server", - ["NumberOfMeasurementTypes"] = "server", - ["Accuracy"] = "server", - ["Ranges"] = "server", - ["Voltage"] = "server", - ["ActiveCurrent"] = "server", - ["ReactiveCurrent"] = "server", - ["ApparentCurrent"] = "server", ["ActivePower"] = "server", - ["ReactivePower"] = "server", - ["ApparentPower"] = "server", - ["RMSVoltage"] = "server", - ["RMSCurrent"] = "server", - ["RMSPower"] = "server", - ["Frequency"] = "server", - ["HarmonicCurrents"] = "server", - ["HarmonicPhases"] = "server", - ["PowerFactor"] = "server", - ["NeutralCurrent"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", } -ElectricalPowerMeasurement.FeatureMap = ElectricalPowerMeasurement.types.Feature - -function ElectricalPowerMeasurement.are_features_supported(feature, feature_map) - if (ElectricalPowerMeasurement.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - local attribute_helper_mt = {} attribute_helper_mt.__index = function(self, key) local direction = ElectricalPowerMeasurement.attribute_direction_map[key] diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua index 6c34abd2f4..457f6484af 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua @@ -65,4 +65,3 @@ end setmetatable(ActivePower, {__call = ActivePower.new_value, __index = ActivePower.base_type}) return ActivePower - diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/Feature.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/Feature.lua deleted file mode 100644 index cbda4f3478..0000000000 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/Feature.lua +++ /dev/null @@ -1,138 +0,0 @@ -local data_types = require "st.matter.data_types" -local UintABC = require "st.matter.data_types.base_defs.UintABC" - -local Feature = {} -local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) - -Feature.BASE_MASK = 0xFFFF -Feature.DIRECT_CURRENT = 0x0001 -Feature.ALTERNATING_CURRENT = 0x0002 -Feature.POLYPHASE_POWER = 0x0004 -Feature.HARMONICS = 0x0008 -Feature.POWER_QUALITY = 0x0010 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - DIRECT_CURRENT = 0x0001, - ALTERNATING_CURRENT = 0x0002, - POLYPHASE_POWER = 0x0004, - HARMONICS = 0x0008, - POWER_QUALITY = 0x0010, -} - -Feature.is_direct_current_set = function(self) - return (self.value & self.DIRECT_CURRENT) ~= 0 -end - -Feature.set_direct_current = function(self) - if self.value ~= nil then - self.value = self.value | self.DIRECT_CURRENT - else - self.value = self.DIRECT_CURRENT - end -end - -Feature.unset_direct_current = function(self) - self.value = self.value & (~self.DIRECT_CURRENT & self.BASE_MASK) -end -Feature.is_alternating_current_set = function(self) - return (self.value & self.ALTERNATING_CURRENT) ~= 0 -end - -Feature.set_alternating_current = function(self) - if self.value ~= nil then - self.value = self.value | self.ALTERNATING_CURRENT - else - self.value = self.ALTERNATING_CURRENT - end -end - -Feature.unset_alternating_current = function(self) - self.value = self.value & (~self.ALTERNATING_CURRENT & self.BASE_MASK) -end -Feature.is_polyphase_power_set = function(self) - return (self.value & self.POLYPHASE_POWER) ~= 0 -end - -Feature.set_polyphase_power = function(self) - if self.value ~= nil then - self.value = self.value | self.POLYPHASE_POWER - else - self.value = self.POLYPHASE_POWER - end -end - -Feature.unset_polyphase_power = function(self) - self.value = self.value & (~self.POLYPHASE_POWER & self.BASE_MASK) -end -Feature.is_harmonics_set = function(self) - return (self.value & self.HARMONICS) ~= 0 -end - -Feature.set_harmonics = function(self) - if self.value ~= nil then - self.value = self.value | self.HARMONICS - else - self.value = self.HARMONICS - end -end - -Feature.unset_harmonics = function(self) - self.value = self.value & (~self.HARMONICS & self.BASE_MASK) -end -Feature.is_power_quality_set = function(self) - return (self.value & self.POWER_QUALITY) ~= 0 -end - -Feature.set_power_quality = function(self) - if self.value ~= nil then - self.value = self.value | self.POWER_QUALITY - else - self.value = self.POWER_QUALITY - end -end - -Feature.unset_power_quality = function(self) - self.value = self.value & (~self.POWER_QUALITY & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.DIRECT_CURRENT | - Feature.ALTERNATING_CURRENT | - Feature.POLYPHASE_POWER | - Feature.HARMONICS | - Feature.POWER_QUALITY - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_direct_current_set = Feature.is_direct_current_set, - set_direct_current = Feature.set_direct_current, - unset_direct_current = Feature.unset_direct_current, - is_alternating_current_set = Feature.is_alternating_current_set, - set_alternating_current = Feature.set_alternating_current, - unset_alternating_current = Feature.unset_alternating_current, - is_polyphase_power_set = Feature.is_polyphase_power_set, - set_polyphase_power = Feature.set_polyphase_power, - unset_polyphase_power = Feature.unset_polyphase_power, - is_harmonics_set = Feature.is_harmonics_set, - set_harmonics = Feature.set_harmonics, - unset_harmonics = Feature.unset_harmonics, - is_power_quality_set = Feature.is_power_quality_set, - set_power_quality = Feature.set_power_quality, - unset_power_quality = Feature.unset_power_quality, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/init.lua deleted file mode 100644 index 8016a487c3..0000000000 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/init.lua +++ /dev/null @@ -1,15 +0,0 @@ -local types_mt = {} -types_mt.__types_cache = {} -types_mt.__index = function(self, key) - if types_mt.__types_cache[key] == nil then - types_mt.__types_cache[key] = require("embedded_clusters.ElectricalPowerMeasurement.types." .. key) - end - return types_mt.__types_cache[key] -end - -local ElectricalPowerMeasurementTypes = {} - -setmetatable(ElectricalPowerMeasurementTypes, types_mt) - -return ElectricalPowerMeasurementTypes - diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/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/embedded_clusters/ValveConfigurationAndControl/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua index 0535e3267c..696c324a81 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua @@ -14,20 +14,8 @@ ValveConfigurationAndControl.types = ValveConfigurationAndControlTypes function ValveConfigurationAndControl:get_attribute_by_id(attr_id) local attr_id_map = { - [0x0000] = "OpenDuration", - [0x0001] = "DefaultOpenDuration", - [0x0002] = "AutoCloseTime", - [0x0003] = "RemainingDuration", [0x0004] = "CurrentState", - [0x0005] = "TargetState", [0x0006] = "CurrentLevel", - [0x0007] = "TargetLevel", - [0x0008] = "DefaultOpenLevel", - [0x0009] = "ValveFault", - [0x000A] = "LevelStep", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", } local attr_name = attr_id_map[attr_id] if attr_name ~= nil then @@ -47,32 +35,9 @@ function ValveConfigurationAndControl:get_server_command_by_id(command_id) return nil end -function ValveConfigurationAndControl:get_event_by_id(event_id) - local event_id_map = { - [0x0000] = "ValveStateChanged", - [0x0001] = "ValveFault", - } - if event_id_map[event_id] ~= nil then - return self.server.events[event_id_map[event_id]] - end - return nil -end - ValveConfigurationAndControl.attribute_direction_map = { - ["OpenDuration"] = "server", - ["DefaultOpenDuration"] = "server", - ["AutoCloseTime"] = "server", - ["RemainingDuration"] = "server", ["CurrentState"] = "server", - ["TargetState"] = "server", ["CurrentLevel"] = "server", - ["TargetLevel"] = "server", - ["DefaultOpenLevel"] = "server", - ["ValveFault"] = "server", - ["LevelStep"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", } ValveConfigurationAndControl.command_direction_map = { @@ -111,13 +76,6 @@ end ValveConfigurationAndControl.commands = {} setmetatable(ValveConfigurationAndControl.commands, command_helper_mt) -local event_helper_mt = {} -event_helper_mt.__index = function(self, key) - return ValveConfigurationAndControl.server.events[key] -end -ValveConfigurationAndControl.events = {} -setmetatable(ValveConfigurationAndControl.events, event_helper_mt) - setmetatable(ValveConfigurationAndControl, {__index = cluster_base}) return ValveConfigurationAndControl diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua index 7433a99c9e..ea3ebeb7c1 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua @@ -7,44 +7,6 @@ Feature.BASE_MASK = 0xFFFF Feature.TIME_SYNC = 0x0001 Feature.LEVEL = 0x0002 -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - TIME_SYNC = 0x0001, - LEVEL = 0x0002, -} - -Feature.is_time_sync_set = function(self) - return (self.value & self.TIME_SYNC) ~= 0 -end - -Feature.set_time_sync = function(self) - if self.value ~= nil then - self.value = self.value | self.TIME_SYNC - else - self.value = self.TIME_SYNC - end -end - -Feature.unset_time_sync = function(self) - self.value = self.value & (~self.TIME_SYNC & self.BASE_MASK) -end - -Feature.is_level_set = function(self) - return (self.value & self.LEVEL) ~= 0 -end - -Feature.set_level = function(self) - if self.value ~= nil then - self.value = self.value | self.LEVEL - else - self.value = self.LEVEL - end -end - -Feature.unset_level = function(self) - self.value = self.value & (~self.LEVEL & self.BASE_MASK) -end - function Feature.bits_are_valid(feature) local max = Feature.TIME_SYNC | @@ -56,19 +18,6 @@ function Feature.bits_are_valid(feature) end end -Feature.mask_methods = { - is_time_sync_set = Feature.is_time_sync_set, - set_time_sync = Feature.set_time_sync, - unset_time_sync = Feature.unset_time_sync, - is_level_set = Feature.is_level_set, - set_level = Feature.set_level, - unset_level = Feature.unset_level, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - setmetatable(Feature, new_mt) return Feature diff --git a/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua index ad8a74a00f..bb0ccfd746 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.ID = 0x0091 + clusters.ElectricalPowerMeasurement.ID = 0x0090 + 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_CHILD_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_CHILD_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))) @@ -360,9 +414,7 @@ function AttributeHandlers.power_source_attribute_list_handler(driver, device, i if #button_eps > 1 then profile_name = string.format("%d-", #button_eps) .. profile_name end - - if device.manufacturer_info.vendor_id == fields.AQARA_MANUFACTURER_ID and - device.manufacturer_info.product_id == fields.AQARA_CLIMATE_SENSOR_W100_ID then + if switch_utils.check_vendor_overrides(device.manufacturer_info, "is_climate_sensor_w100") then profile_name = profile_name .. "-temperature-humidity" end device:try_update_metadata({ profile = profile_name }) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index bbf9ca24ee..beed80625d 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 @@ -87,17 +93,15 @@ function SwitchLifecycleHandlers.device_init(driver, device) end local main_endpoint = switch_utils.find_default_endpoint(device) -- ensure subscription to all endpoint attributes- including those mapped to child devices - for idx, ep in ipairs(device.endpoints) do + for _, ep in ipairs(device.endpoints) do if ep.endpoint_id ~= main_endpoint then - if device:supports_server_cluster(clusters.OnOff.ID, ep) then - local child_profile = 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 + if dt.device_type_id == fields.ELECTRICAL_SENSOR_ID then + for _, attr in pairs(fields.device_type_attribute_map[fields.ELECTRICAL_SENSOR_ID]) do + device:add_subscribed_attribute(attr) + end + end id = math.max(id, dt.device_type_id) end for _, attr in pairs(fields.device_type_attribute_map[id] or {}) do @@ -122,7 +126,7 @@ function SwitchLifecycleHandlers.device_init(driver, 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 #cumulative_energy_eps > 0 then device:set_field(fields.CUMULATIVE_REPORTS_SUPPORTED, true, {persist = false}) end end end end @@ -149,9 +153,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 +187,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 27fe47f11a..f3b95a4729 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua @@ -19,6 +19,17 @@ local utils = require "st.utils" local dkjson = require "dkjson" local clusters = require "st.matter.clusters" local button_attr = capabilities.button.button +local version = require "version" + +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" +end + +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +end local aqara_parent_ep = 4 local aqara_child1_ep = 1 @@ -35,7 +46,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 @@ -271,10 +283,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) } ) @@ -288,7 +298,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) } ) @@ -308,7 +318,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) } ) @@ -325,7 +335,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 ) } ) @@ -338,7 +348,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 d981af3110..77355c2881 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 @@ -56,9 +59,29 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} }, device_types = { - { device_type_id = 0x010A, device_type_revision = 1 } -- OnOff Plug + { device_type_id = 0x010B, device_type_revision = 1 }, -- OnOff Dimmable Plug } - } + }, + { + endpoint_id = 3, + clusters = { + { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, }, -- SET_TOPOLOGY + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor + } + }, + { + endpoint_id = 4, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, + { cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + }, + device_types = { + { device_type_id = 0x010B, device_type_revision = 1 }, -- OnOff Dimmable Plug + } + }, }, }) @@ -82,18 +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 } }, }, }) local subscribed_attributes_periodic = { - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + clusters.OnOff.attributes.OnOff, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, } local subscribed_attributes = { clusters.OnOff.attributes.OnOff, @@ -138,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) @@ -157,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( @@ -336,6 +370,7 @@ test.register_message_test( } }, } + -- { test_init = test_init_periodic } ) test.register_coroutine_test( @@ -400,9 +435,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 } ) @@ -411,8 +457,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_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index 0046244bcd..f2274e6555 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 @@ -473,6 +473,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" }) @@ -505,6 +507,7 @@ local function test_init_mounted_on_off_control() test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "doConfigure" }) + mock_device_mounted_on_off_control:expect_metadata_update({ profile = "switch-binary" }) mock_device_mounted_on_off_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -526,6 +529,7 @@ local function test_init_mounted_dimmable_load_control() test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "doConfigure" }) + mock_device_mounted_dimmable_load_control:expect_metadata_update({ profile = "switch-level" }) mock_device_mounted_dimmable_load_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -566,6 +570,7 @@ local function test_init_parent_child_different_types() test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "doConfigure" }) + mock_device_parent_child_different_types:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_child_different_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) mock_device_parent_child_different_types:expect_device_create({ @@ -617,6 +622,7 @@ local function test_init_light_level_motion() test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "doConfigure" }) + mock_device_light_level_motion:expect_metadata_update({ profile = "light-level-motion" }) mock_device_light_level_motion:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -704,4 +710,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_matter_water_valve.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua index d4ead24458..16f0ea3103 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua @@ -17,6 +17,11 @@ local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" +local version = require "version" + +if version.api < 11 then + clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" +end local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("water-valve-level.yml"), diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index 21c9e1087d..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 @@ -189,6 +189,7 @@ local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "light-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children) do @@ -260,6 +261,7 @@ local function test_init_parent_child_endpoints_non_sequential() test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "doConfigure" }) + mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "light-binary" }) mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children_non_sequential) do @@ -687,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 03c898b7f5..f09ab3c858 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 @@ -24,6 +24,8 @@ local child_profile_override = t_utils.get_profile_definition("switch-binary.yml local parent_ep = 10 local child1_ep = 20 local child2_ep = 30 +local child3_ep = 40 +local child4_ep = 50 local mock_device = test.mock_device.build_test_matter_device({ label = "Matter Switch", @@ -69,6 +71,24 @@ local mock_device = test.mock_device.build_test_matter_device({ {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug } }, + { + endpoint_id = child3_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + } + }, + { + endpoint_id = child4_ep, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + } + } } }) @@ -146,6 +166,7 @@ local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "plug-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children) do @@ -167,6 +188,22 @@ local function test_init() parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", child2_ep) }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 4", + profile = "plug-binary", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", child3_ep) + }) + + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 5", + profile = "plug-binary", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", child4_ep) + }) end local mock_children_child_profile_override = {} @@ -196,6 +233,7 @@ local function test_init_child_profile_override() test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "doConfigure" }) + mock_device_child_profile_override:expect_metadata_update({ profile = "plug-binary" }) mock_device_child_profile_override:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children_child_profile_override) do @@ -389,4 +427,4 @@ test.register_coroutine_test( { test_init = test_init_child_profile_override } ) -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 feb21ac193..196ea22fe0 100644 --- a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua @@ -31,43 +31,40 @@ local DeviceConfiguration = {} local SwitchDeviceConfiguration = {} local ButtonDeviceConfiguration = {} -function SwitchDeviceConfiguration.assign_child_profile(device, child_ep) +function SwitchDeviceConfiguration.assign_switch_profile(device, switch_ep, opts) local profile for _, ep in ipairs(device.endpoints) do - if ep.endpoint_id == child_ep then + if ep.endpoint_id == switch_ep then -- Some devices report multiple device types which are a subset of -- a superset device type (For example, Dimmable Light is a superset of -- On/Off light). This mostly applies to the four light types, so we will want -- to match the profile for the superset device type. This can be done by -- matching to the device type with the highest ID + -- Note: Electrical Sensor does not follow the above logic, so it's ignored local id = 0 for _, dt in ipairs(ep.device_types) do - id = math.max(id, dt.device_type_id) + if dt.device_type_id ~= fields.ELECTRICAL_SENSOR_ID then + id = math.max(id, dt.device_type_id) + end end profile = fields.device_type_profile_map[id] break end end - -- Check if device has an overridden child profile that differs from the profile that would match - -- the child's device type for the following two cases: - -- 1. To add Electrical Sensor only to the first EDGE_CHILD (light-power-energy-powerConsumption) - -- for the Aqara Light Switch H2. The profile of the second EDGE_CHILD for this device is - -- determined in the "for" loop above (e.g., light-binary) - -- 2. The selected profile for the child device matches the initial profile defined in - -- child_device_profile_overrides - for id, vendor in pairs(fields.child_device_profile_overrides_per_vendor_id) do - for _, fingerprint in ipairs(vendor) do - if device.manufacturer_info.product_id == fingerprint.product_id and - ((device.manufacturer_info.vendor_id == fields.AQARA_MANUFACTURER_ID and child_ep == 1) or profile == fingerprint.initial_profile) then - return fingerprint.target_profile - end - end + local electrical_tags = switch_utils.get_field_for_endpoint(device, fields.ELECTRICAL_TAGS, switch_ep) + if electrical_tags ~= nil and (profile == "plug-binary" or profile == "plug-level" or profile == "light-binary") then + profile = string.gsub(profile, "-binary", "") .. electrical_tags end - -- default to "switch-binary" if no profile is found - return profile or "switch-binary" + if opts and opts.is_child_device then + -- Check if device has a profile override that differs from its generically chosen profile + return switch_utils.check_vendor_overrides(device.manufacturer_info, "initial_profile", profile, "target_profile") + or profile + or "switch-binary" -- default to "switch-binary" if no child profile is found + end + return profile end function SwitchDeviceConfiguration.create_child_switch_devices(driver, device, main_endpoint) @@ -75,12 +72,12 @@ function SwitchDeviceConfiguration.create_child_switch_devices(driver, device, m local parent_child_device = false local switch_eps = device:get_endpoints(clusters.OnOff.ID) table.sort(switch_eps) - for idx, ep in ipairs(switch_eps) do + for _, ep in ipairs(switch_eps) do if device:supports_server_cluster(clusters.OnOff.ID, ep) then num_switch_server_eps = num_switch_server_eps + 1 if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint local name = string.format("%s %d", device.label, num_switch_server_eps) - local child_profile = SwitchDeviceConfiguration.assign_child_profile(device, ep) + local child_profile = SwitchDeviceConfiguration.assign_switch_profile(device, ep, { is_child_device = true }) driver:try_create_device( { type = "EDGE_CHILD", @@ -92,10 +89,6 @@ function SwitchDeviceConfiguration.create_child_switch_devices(driver, device, m } ) parent_child_device = true - if idx == 1 and string.find(child_profile, "energy") then - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) - end end end end @@ -225,34 +218,37 @@ function DeviceConfiguration.initialize_buttons_and_switches(driver, device, mai return profile_found end +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 main_endpoint = switch_utils.find_default_endpoint(device) -- initialize the main device card with buttons if applicable, and create child devices as needed for multi-switch devices. local profile_found = DeviceConfiguration.initialize_buttons_and_switches(driver, device, main_endpoint) if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then device:set_find_child(switch_utils.find_child) + else + for _, ep in ipairs(device.endpoints) do + -- this may be persist-set during initial device interview. If not PARENT/CHILD, we can drop this data. + switch_utils.set_field_for_endpoint(device, fields.PRIMARY_CHILD_EP, ep.endpoint_id, nil) + end end if profile_found then return end local fan_eps = device:get_endpoints(clusters.FanControl.ID) - local level_eps = device:get_endpoints(clusters.LevelControl.ID) - local energy_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) - local power_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) local valve_eps = embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) local profile_name = nil - local level_support = "" - if #level_eps > 0 then - level_support = "-level" - end - if #energy_eps > 0 and #power_eps > 0 then - profile_name = "plug" .. level_support .. "-power-energy-powerConsumption" - elseif #energy_eps > 0 then - profile_name = "plug" .. level_support .. "-energy-powerConsumption" - elseif #power_eps > 0 then - profile_name = "plug" .. level_support .. "-power" - elseif #valve_eps > 0 then + if #valve_eps > 0 then profile_name = "water-valve" if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID, {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then @@ -263,6 +259,18 @@ function DeviceConfiguration.match_profile(driver, device) end if profile_name then device:try_update_metadata({ profile = profile_name }) + return + end + + -- after doing all previous profiling steps, attempt to re-profile main/parent switch/plug device + profile_name = SwitchDeviceConfiguration.assign_switch_profile(device, main_endpoint) + -- ignore attempts to dynamically profile light-level-colorTemperature and light-color-level devices for now, since + -- these may lose fingerprinted Kelvin ranges when dynamically profiled. + if profile_name and profile_name ~= "light-level-colorTemperature" and profile_name ~= "light-color-level" then + if profile_name == "light-level" and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then + profile_name = "light-level-motion" + end + device:try_update_metadata({profile = profile_name}) end end diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua index 2244eab661..314d2c47e3 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua @@ -76,18 +76,14 @@ SwitchFields.device_type_profile_map = { [SwitchFields.MOUNTED_DIMMABLE_LOAD_CONTROL_ID] = "switch-level", } - -SwitchFields.CONVERSION_CONST_MILLIWATT_TO_WATT = 1000 -- A milliwatt is 1/1000th of a watt - - -- COMPONENT_TO_ENDPOINT_MAP is here to preserve the endpoint mapping for -- devices that were joined to this driver as MCD devices before the transition -- to join switch devices as parent-child. This value will exist in the device -- table for devices that joined prior to this transition, and is also used for -- button devices that require component mapping. SwitchFields.COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -SwitchFields.ENERGY_MANAGEMENT_ENDPOINT = "__energy_management_endpoint" SwitchFields.IS_PARENT_CHILD_DEVICE = "__is_parent_child_device" +SwitchFields.PRIMARY_CHILD_EP = "__PRIMARY_CHILD_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" @@ -99,30 +95,31 @@ 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.child_device_profile_overrides_per_vendor_id = { - [0x1321] = { - { product_id = 0x000C, target_profile = "switch-binary", initial_profile = "plug-binary" }, - { product_id = 0x000D, target_profile = "switch-binary", initial_profile = "plug-binary" }, +SwitchFields.vendor_overrides = { + [0x1321] = { -- SONOFF_MANUFACTURER_ID + [0x000C] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, + [0x000D] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, }, - [0x115F] = { - { product_id = 0x1003, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - { product_id = 0x1004, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 2 Channels(On/Off Light) - { product_id = 0x1005, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 3 Channels(On/Off Light) - { product_id = 0x1006, target_profile = "light-level-power-energy-powerConsumption" }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) - { product_id = 0x1008, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - { product_id = 0x1009, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 2 Channels(On/Off Light) - { product_id = 0x100A, target_profile = "light-level-power-energy-powerConsumption" }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) + [0x115F] = { -- AQARA_MANUFACTURER_ID + [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 } } -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 @@ -150,9 +147,6 @@ SwitchFields.TEMP_BOUND_RECEIVED = "__temp_bound_received" SwitchFields.TEMP_MIN = "__temp_min" SwitchFields.TEMP_MAX = "__temp_max" -SwitchFields.AQARA_MANUFACTURER_ID = 0x115F -SwitchFields.AQARA_CLIMATE_SENSOR_W100_ID = 0x2004 - SwitchFields.TRANSITION_TIME = 0 --1/10ths of a second -- When sent with a command, these options mask and override bitmaps cause the command -- to take effect when the switch/light is off. diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua index 86368e9208..e329c62cbe 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.ID = 0x0091 + clusters.ElectricalPowerMeasurement.ID = 0x0090 + 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) @@ -41,6 +54,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 @@ -74,33 +91,30 @@ function utils.check_field_name_updates(device) end end +function utils.check_vendor_overrides(manufacturer_info, override_key, device_key, return_value) + for product_id, overrides in pairs(fields.vendor_overrides[manufacturer_info.vendor_id] or {}) do + if product_id == manufacturer_info.product_id and (device_key or true) == overrides[override_key] then + return overrides[return_value] or true + end + end +end + --- device_type_supports_button_switch_combination helper function used to check --- whether the device type for an endpoint is currently supported by a profile for --- combination button/switch devices. function utils.device_type_supports_button_switch_combination(device, endpoint_id) - for _, ep in ipairs(device.endpoints) do - if ep.endpoint_id == endpoint_id then - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == fields.DIMMABLE_LIGHT_DEVICE_TYPE_ID then - for _, fingerprint in ipairs(fields.child_device_profile_overrides_per_vendor_id[0x115F]) do - if device.manufacturer_info.product_id == fingerprint.product_id then - return false -- For Aqara Dimmer Switch with Button. - end - end - return true - end - end - end + if utils.check_vendor_overrides(device.manufacturer_info, "ignore_combo_switch_button") then + return false end - return false + local dimmable_eps = utils.get_endpoints_by_dt(device, fields.DIMMABLE_LIGHT_DEVICE_TYPE_ID) + return utils.tbl_contains(dimmable_eps, endpoint_id) end --- find_default_endpoint is a helper function to handle situations where --- device does not have endpoint ids in sequential order from 1 function utils.find_default_endpoint(device) - if device.manufacturer_info.vendor_id == fields.AQARA_MANUFACTURER_ID and - device.manufacturer_info.product_id == fields.AQARA_CLIMATE_SENSOR_W100_ID then - -- In case of Aqara Climate Sensor W100, in order to sequentially set the button name to button 1, 2, 3 + -- Buttons should not be set on the main component for the Aqara Climate Sensor W100, + if utils.check_vendor_overrides(device.manufacturer_info, "is_climate_sensor_w100") then return device.MATTER_DEFAULT_ENDPOINT end @@ -162,8 +176,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_CHILD_EP, ep_id) or ep_id + return parent_device:get_child_by_parent_assigned_key(string.format("%d", primary_ep_key)) end -- Fallback handler for responses that dont have their own handler @@ -182,15 +197,21 @@ function utils.create_multi_press_values_list(size, supportsHeld) return list end -function utils.detect_bridge(device) +-- get a list of endpoints for a specified device type. +function utils.get_endpoints_by_dt(device, device_type_id) + local dt_eps = {} for _, ep in ipairs(device.endpoints) do for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == fields.AGGREGATOR_DEVICE_TYPE_ID then - return true + if dt.device_type_id == device_type_id then + table.insert(dt_eps, ep.endpoint_id) end end end - return false + return dt_eps +end + +function utils.detect_bridge(device) + return #utils.get_endpoints_by_dt(device, fields.AGGREGATOR_DEVICE_TYPE_ID) > 0 end function utils.detect_matter_thing(device) @@ -202,7 +223,7 @@ function utils.detect_matter_thing(device) return device:supports_capability(capabilities.refresh) end -function utils.report_power_consumption_to_st_energy(device, latest_total_imported_energy_wh) +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 @@ -210,35 +231,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_dt(device, fields.ELECTRICAL_SENSOR_ID) + 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_CHILD_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