diff --git a/drivers/SmartThings/matter-appliance/fingerprints.yml b/drivers/SmartThings/matter-appliance/fingerprints.yml index f487ddd381..891ce2619f 100644 --- a/drivers/SmartThings/matter-appliance/fingerprints.yml +++ b/drivers/SmartThings/matter-appliance/fingerprints.yml @@ -32,3 +32,29 @@ matterGeneric: deviceTypes: - id: 0x0075 deviceProfileName: dishwasher-tn-tl + - id: "matter/extractor-hood" + deviceLabel: Matter Extractor Hood + deviceTypes: + - id: 0x007A + deviceProfileName: extractor-hood + - id: "matter/laundry-dryer" + deviceLabel: Matter Laundry Dryer + deviceTypes: + - id: 0x007C + deviceProfileName: laundry-dryer + - id: "matter/cook-top" + deviceLabel: Matter Cook Top + deviceTypes: + - id: 0x0078 + deviceProfileName: cook-top + - id: "matter/microwave-oven" + deviceLabel: Matter Microwave Oven + deviceTypes: + - id: 0x0079 + deviceProfileName: microwave-oven + - id: "matter/oven" + deviceLabel: Matter Oven + deviceTypes: + - id: 0x007B + - id: 0x0071 + deviceProfileName: oven-cabinet-one-tn-cabinet-two-tl-cook-top-cook-surface-one-tl-cook-surface-two-tl diff --git a/drivers/SmartThings/matter-appliance/profiles/cook-surface-one-cook-surface-two-tl.yml b/drivers/SmartThings/matter-appliance/profiles/cook-surface-one-cook-surface-two-tl.yml new file mode 100644 index 0000000000..b36eccc624 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/cook-surface-one-cook-surface-two-tl.yml @@ -0,0 +1,24 @@ +name: cook-surface-one-cook-surface-two-tl +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Cooktop +- id: cookSurfaceOne + label: Cook Surface 1 + capabilities: + - id: temperatureMeasurement + version: 1 +- id: cookSurfaceTwo + label: Cook Surface 2 + capabilities: + - id: temperatureLevel + version: 1 + - id: temperatureMeasurement + version: 1 diff --git a/drivers/SmartThings/matter-appliance/profiles/cook-surface-one-cook-surface-two.yml b/drivers/SmartThings/matter-appliance/profiles/cook-surface-one-cook-surface-two.yml new file mode 100644 index 0000000000..83d6298bb1 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/cook-surface-one-cook-surface-two.yml @@ -0,0 +1,22 @@ +name: cook-surface-one-cook-surface-two +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Cooktop +- id: cookSurfaceOne + label: Cook Surface 1 + capabilities: + - id: temperatureMeasurement + version: 1 +- id: cookSurfaceTwo + label: Cook Surface 2 + capabilities: + - id: temperatureMeasurement + version: 1 diff --git a/drivers/SmartThings/matter-appliance/profiles/cook-surface-one-tl-cook-surface-two-tl.yml b/drivers/SmartThings/matter-appliance/profiles/cook-surface-one-tl-cook-surface-two-tl.yml new file mode 100644 index 0000000000..4d73638a7d --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/cook-surface-one-tl-cook-surface-two-tl.yml @@ -0,0 +1,26 @@ +name: cook-surface-one-tl-cook-surface-two-tl +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Cooktop +- id: cookSurfaceOne + label: Cook Surface 1 + capabilities: + - id: temperatureLevel + version: 1 + - id: temperatureMeasurement + version: 1 +- id: cookSurfaceTwo + label: Cook Surface 2 + capabilities: + - id: temperatureLevel + version: 1 + - id: temperatureMeasurement + version: 1 diff --git a/drivers/SmartThings/matter-appliance/profiles/cook-surface-one-tl-cook-surface-two.yml b/drivers/SmartThings/matter-appliance/profiles/cook-surface-one-tl-cook-surface-two.yml new file mode 100644 index 0000000000..4a6464030d --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/cook-surface-one-tl-cook-surface-two.yml @@ -0,0 +1,24 @@ +name: cook-surface-one-tl-cook-surface-two +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Cooktop +- id: cookSurfaceOne + label: Cook Surface 1 + capabilities: + - id: temperatureLevel + version: 1 + - id: temperatureMeasurement + version: 1 +- id: cookSurfaceTwo + label: Cook Surface 2 + capabilities: + - id: temperatureMeasurement + version: 1 diff --git a/drivers/SmartThings/matter-appliance/profiles/cook-surface-one-tl.yml b/drivers/SmartThings/matter-appliance/profiles/cook-surface-one-tl.yml new file mode 100644 index 0000000000..bb206ac8d3 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/cook-surface-one-tl.yml @@ -0,0 +1,19 @@ +name: cook-surface-one-tl +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Cooktop +- id: cookSurfaceOne + label: Cook Surface 1 + capabilities: + - id: temperatureLevel + version: 1 + - id: temperatureMeasurement + version: 1 diff --git a/drivers/SmartThings/matter-appliance/profiles/cook-surface-one.yml b/drivers/SmartThings/matter-appliance/profiles/cook-surface-one.yml new file mode 100644 index 0000000000..44fee23b77 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/cook-surface-one.yml @@ -0,0 +1,17 @@ +name: cook-surface-one +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Cooktop +- id: cookSurfaceOne + label: Cook Surface 1 + capabilities: + - id: temperatureMeasurement + version: 1 diff --git a/drivers/SmartThings/matter-appliance/profiles/cook-top.yml b/drivers/SmartThings/matter-appliance/profiles/cook-top.yml new file mode 100644 index 0000000000..9ecb6d9566 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/cook-top.yml @@ -0,0 +1,13 @@ +name: cook-top +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Cooktop + \ No newline at end of file diff --git a/drivers/SmartThings/matter-appliance/profiles/extractor-hood-ac-wind.yml b/drivers/SmartThings/matter-appliance/profiles/extractor-hood-ac-wind.yml new file mode 100644 index 0000000000..34aadcb458 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/extractor-hood-ac-wind.yml @@ -0,0 +1,26 @@ +name: extractor-hood-ac-wind +components: +- id: main + label: Main + capabilities: + - id: fanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: windMode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: KitchenHood +- id: activatedCarbonFilter + label: Activated carbon filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: KitchenHood diff --git a/drivers/SmartThings/matter-appliance/profiles/extractor-hood-ac.yml b/drivers/SmartThings/matter-appliance/profiles/extractor-hood-ac.yml new file mode 100644 index 0000000000..23f3aedfe5 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/extractor-hood-ac.yml @@ -0,0 +1,24 @@ +name: extractor-hood-ac +components: +- id: main + label: Main + capabilities: + - id: fanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: KitchenHood +- id: activatedCarbonFilter + label: Activated carbon filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: KitchenHood diff --git a/drivers/SmartThings/matter-appliance/profiles/extractor-hood-hepa-ac-wind.yml b/drivers/SmartThings/matter-appliance/profiles/extractor-hood-hepa-ac-wind.yml new file mode 100644 index 0000000000..53a143569d --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/extractor-hood-hepa-ac-wind.yml @@ -0,0 +1,35 @@ +name: extractor-hood-hepa-ac-wind +components: +- id: main + label: Main + capabilities: + - id: fanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: windMode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: KitchenHood +- id: hepaFilter + label: Hepa filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: KitchenHood +- id: activatedCarbonFilter + label: Activated carbon filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: KitchenHood diff --git a/drivers/SmartThings/matter-appliance/profiles/extractor-hood-hepa-ac.yml b/drivers/SmartThings/matter-appliance/profiles/extractor-hood-hepa-ac.yml new file mode 100644 index 0000000000..a605ca2013 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/extractor-hood-hepa-ac.yml @@ -0,0 +1,33 @@ +name: extractor-hood-hepa-ac +components: +- id: main + label: Main + capabilities: + - id: fanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: KitchenHood +- id: hepaFilter + label: Hepa filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: KitchenHood +- id: activatedCarbonFilter + label: Activated carbon filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: KitchenHood diff --git a/drivers/SmartThings/matter-appliance/profiles/extractor-hood-hepa-wind.yml b/drivers/SmartThings/matter-appliance/profiles/extractor-hood-hepa-wind.yml new file mode 100644 index 0000000000..af88b84ee2 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/extractor-hood-hepa-wind.yml @@ -0,0 +1,26 @@ +name: extractor-hood-hepa-wind +components: +- id: main + label: Main + capabilities: + - id: fanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: windMode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: KitchenHood +- id: hepaFilter + label: Hepa filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: KitchenHood diff --git a/drivers/SmartThings/matter-appliance/profiles/extractor-hood-hepa.yml b/drivers/SmartThings/matter-appliance/profiles/extractor-hood-hepa.yml new file mode 100644 index 0000000000..a69cb17507 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/extractor-hood-hepa.yml @@ -0,0 +1,24 @@ +name: extractor-hood-hepa +components: +- id: main + label: Main + capabilities: + - id: fanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: KitchenHood +- id: hepaFilter + label: Hepa filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: KitchenHood diff --git a/drivers/SmartThings/matter-appliance/profiles/extractor-hood-wind.yml b/drivers/SmartThings/matter-appliance/profiles/extractor-hood-wind.yml new file mode 100644 index 0000000000..074a20c78e --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/extractor-hood-wind.yml @@ -0,0 +1,16 @@ +name: extractor-hood-wind +components: +- id: main + capabilities: + - id: fanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: windMode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: KitchenHood diff --git a/drivers/SmartThings/matter-appliance/profiles/extractor-hood.yml b/drivers/SmartThings/matter-appliance/profiles/extractor-hood.yml new file mode 100644 index 0000000000..c07813afcf --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/extractor-hood.yml @@ -0,0 +1,14 @@ +name: extractor-hood +components: +- id: main + capabilities: + - id: fanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: KitchenHood diff --git a/drivers/SmartThings/matter-appliance/profiles/laundry-dryer-tl.yml b/drivers/SmartThings/matter-appliance/profiles/laundry-dryer-tl.yml new file mode 100644 index 0000000000..6edd6edc01 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/laundry-dryer-tl.yml @@ -0,0 +1,21 @@ +name: laundry-dryer-tl +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: mode + version: 1 + - id: operationalState + version: 1 + - id: temperatureLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Dryer +metadata: + mnmn: SmartThingsEdge + vid: generic-laundry-dryer diff --git a/drivers/SmartThings/matter-appliance/profiles/laundry-dryer-tn-tl.yml b/drivers/SmartThings/matter-appliance/profiles/laundry-dryer-tn-tl.yml new file mode 100644 index 0000000000..ea54dfe097 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/laundry-dryer-tn-tl.yml @@ -0,0 +1,23 @@ +name: laundry-dryer-tn-tl +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: mode + version: 1 + - id: temperatureSetpoint + version: 1 + - id: operationalState + version: 1 + - id: temperatureLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Dryer +metadata: + mnmn: SmartThingsEdge + vid: generic-laundry-dryer diff --git a/drivers/SmartThings/matter-appliance/profiles/laundry-dryer-tn.yml b/drivers/SmartThings/matter-appliance/profiles/laundry-dryer-tn.yml new file mode 100644 index 0000000000..8182eded36 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/laundry-dryer-tn.yml @@ -0,0 +1,21 @@ +name: laundry-dryer-tn +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: mode + version: 1 + - id: temperatureSetpoint + version: 1 + - id: operationalState + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Dryer +metadata: + mnmn: SmartThingsEdge + vid: generic-laundry-dryer diff --git a/drivers/SmartThings/matter-appliance/profiles/laundry-dryer.yml b/drivers/SmartThings/matter-appliance/profiles/laundry-dryer.yml new file mode 100644 index 0000000000..d04fe0f74b --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/laundry-dryer.yml @@ -0,0 +1,19 @@ +name: laundry-dryer +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: mode + version: 1 + - id: operationalState + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Dryer +metadata: + mnmn: SmartThingsEdge + vid: generic-laundry-dryer \ No newline at end of file diff --git a/drivers/SmartThings/matter-appliance/profiles/microwave-oven.yml b/drivers/SmartThings/matter-appliance/profiles/microwave-oven.yml new file mode 100644 index 0000000000..5f15869b65 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/microwave-oven.yml @@ -0,0 +1,19 @@ +name: microwave-oven +components: +- id: main + capabilities: + - id: mode + version: 1 + - id: operationalState + version: 1 + - id: cookTime + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Microwave +metadata: + mnmn: SmartThingsEdge + vid: generic-microwave-oven diff --git a/drivers/SmartThings/matter-appliance/profiles/oven-cabinet-one-tn-cabinet-two-tl-cook-top-cook-surface-one-tl-cook-surface-two-tl.yml b/drivers/SmartThings/matter-appliance/profiles/oven-cabinet-one-tn-cabinet-two-tl-cook-top-cook-surface-one-tl-cook-surface-two-tl.yml new file mode 100644 index 0000000000..b5a17e129f --- /dev/null +++ b/drivers/SmartThings/matter-appliance/profiles/oven-cabinet-one-tn-cabinet-two-tl-cook-top-cook-surface-one-tl-cook-surface-two-tl.yml @@ -0,0 +1,50 @@ +name: oven-cabinet-one-tn-cabinet-two-tl-cook-top-cook-surface-one-tl-cook-surface-two-tl +components: +- id: main + capabilities: + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Oven +- id: tccOne + label: Oven Cabinet 1 + capabilities: + - id: mode + version: 1 + - id: temperatureMeasurement + version: 1 + - id: temperatureSetpoint + version: 1 +- id: tccTwo + label: Oven Cabinet 2 + capabilities: + - id: mode + version: 1 + - id: temperatureMeasurement + version: 1 + - id: temperatureLevel + version: 1 +- id: cookTop + label: Cook Top + capabilities: + - id: switch + version: 1 +- id: cookSurfaceOne + label: Cook Surface 1 + capabilities: + - id: temperatureLevel + version: 1 + - id: temperatureMeasurement + version: 1 +- id: cookSurfaceTwo + label: Cook Surface 2 + capabilities: + - id: temperatureLevel + version: 1 + - id: temperatureMeasurement + version: 1 +metadata: + mnmn: SmartThingsEdge + vid: generic-oven diff --git a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/init.lua b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/init.lua new file mode 100644 index 0000000000..c7057f82e4 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/init.lua @@ -0,0 +1,79 @@ +local cluster_base = require "st.matter.cluster_base" +local ActivatedCarbonFilterMonitoringServerAttributes = require "ActivatedCarbonFilterMonitoring.server.attributes" +local ActivatedCarbonFilterMonitoringTypes = require "ActivatedCarbonFilterMonitoring.types" + +local ActivatedCarbonFilterMonitoring = {} + +ActivatedCarbonFilterMonitoring.ID = 0x0072 +ActivatedCarbonFilterMonitoring.NAME = "ActivatedCarbonFilterMonitoring" +ActivatedCarbonFilterMonitoring.server = {} +ActivatedCarbonFilterMonitoring.client = {} +ActivatedCarbonFilterMonitoring.server.attributes = ActivatedCarbonFilterMonitoringServerAttributes:set_parent_cluster(ActivatedCarbonFilterMonitoring) +ActivatedCarbonFilterMonitoring.types = ActivatedCarbonFilterMonitoringTypes + +function ActivatedCarbonFilterMonitoring:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "Condition", + [0x0001] = "DegradationDirection", + [0x0002] = "ChangeIndication", + [0x0003] = "InPlaceIndicator", + [0x0004] = "LastChangedTime", + [0x0005] = "ReplacementProductList", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function ActivatedCarbonFilterMonitoring:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "ResetCondition", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +ActivatedCarbonFilterMonitoring.attribute_direction_map = { + ["Condition"] = "server", + ["DegradationDirection"] = "server", + ["ChangeIndication"] = "server", + ["InPlaceIndicator"] = "server", + ["LastChangedTime"] = "server", + ["ReplacementProductList"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + + +ActivatedCarbonFilterMonitoring.FeatureMap = ActivatedCarbonFilterMonitoring.types.Feature + +function ActivatedCarbonFilterMonitoring.are_features_supported(feature, feature_map) + if (ActivatedCarbonFilterMonitoring.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 = ActivatedCarbonFilterMonitoring.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ActivatedCarbonFilterMonitoring.NAME)) + end + return ActivatedCarbonFilterMonitoring[direction].attributes[key] +end +ActivatedCarbonFilterMonitoring.attributes = {} +setmetatable(ActivatedCarbonFilterMonitoring.attributes, attribute_helper_mt) + +setmetatable(ActivatedCarbonFilterMonitoring, {__index = cluster_base}) + +return ActivatedCarbonFilterMonitoring + diff --git a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua new file mode 100644 index 0000000000..ef3fb00a6a --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua @@ -0,0 +1,126 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +--- @class st.matter.clusters.ActivatedCarbonFilterMonitoring.AttributeList +--- @alias AttributeList +--- +--- @field public ID number 0xFFFB the ID of this attribute +--- @field public NAME string "AttributeList" the name of this attribute +--- @field public data_type st.matter.data_types.Array the data type of this attribute + +local AttributeList = { + ID = 0xFFFB, + NAME = "AttributeList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +--- Add additional functionality to the base type object +--- +--- @param base_type_obj st.matter.data_types.Array the base data type object to add functionality to +function AttributeList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, AttributeList.element_type) + end +end + +--- Create a Array object of this attribute with any additional features provided for the attribute +--- This is also usable with the AttributeList(...) syntax +--- +--- @vararg vararg the values needed to construct a Array +--- @return st.matter.data_types.Array +function AttributeList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +--- Constructs an st.matter.interaction_model.InteractionRequest to read +--- this attribute from a device +--- @param device st.matter.Device +--- @param endpoint_id number|nil +--- @return st.matter.interaction_model.InteractionRequest containing an Interaction Request +function AttributeList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + + +--- Reporting policy: AttributeList => true => mandatory + +--- Sets up a Subscribe Interaction +--- +--- @param device any +--- @param endpoint_id number|nil +--- @return any +function AttributeList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AttributeList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +--- Builds an AttributeList test attribute reponse for the driver integration testing framework +--- +--- @param device st.matter.Device the device to build this message for +--- @param endpoint_id number|nil +--- @param value any +--- @param status string Interaction status associated with the path +--- @return st.matter.interaction_model.InteractionResponse of type REPORT_DATA +function AttributeList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AttributeList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) +return AttributeList + diff --git a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua new file mode 100644 index 0000000000..dd6fe1ecfb --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua @@ -0,0 +1,116 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +--- @class st.matter.clusters.ActivatedCarbonFilterMonitoring.ChangeIndication +--- @alias ChangeIndication +--- +--- @field public ID number 0x0002 the ID of this attribute +--- @field public NAME string "ChangeIndication" the name of this attribute +--- @field public data_type ActivatedCarbonFilterMonitoring.types.ChangeIndicationEnum the data type of this attribute + +local ChangeIndication = { + ID = 0x0002, + NAME = "ChangeIndication", + base_type = require "ActivatedCarbonFilterMonitoring.types.ChangeIndicationEnum", +} + +--- Create a ChangeIndicationEnum object of this attribute with any additional features provided for the attribute +--- This is also usable with the ChangeIndication(...) syntax +--- +--- @vararg vararg the values needed to construct a ChangeIndicationEnum +--- @return ActivatedCarbonFilterMonitoring.types.ChangeIndicationEnum +function ChangeIndication:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +--- Constructs an st.matter.interaction_model.InteractionRequest to read +--- this attribute from a device +--- @param device st.matter.Device +--- @param endpoint_id number|nil +--- @return st.matter.interaction_model.InteractionRequest containing an Interaction Request +function ChangeIndication:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + + +--- Reporting policy: ChangeIndication => true => mandatory + +--- Sets up a Subscribe Interaction +--- +--- @param device any +--- @param endpoint_id number|nil +--- @return any +function ChangeIndication:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function ChangeIndication:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +--- Builds an ChangeIndication test attribute reponse for the driver integration testing framework +--- +--- @param device st.matter.Device the device to build this message for +--- @param endpoint_id number|nil +--- @param value any +--- @param status string Interaction status associated with the path +--- @return st.matter.interaction_model.InteractionResponse of type REPORT_DATA +function ChangeIndication:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function ChangeIndication:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(ChangeIndication, {__call = ChangeIndication.new_value, __index = ChangeIndication.base_type}) +return ChangeIndication + diff --git a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua new file mode 100644 index 0000000000..e668aa4c48 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local Condition = { + ID = 0x0000, + NAME = "Condition", + base_type = require "st.matter.data_types.Uint8", +} + +function Condition:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function Condition:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function Condition:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function Condition:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function Condition: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 Condition:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(Condition, {__call = Condition.new_value, __index = Condition.base_type}) +return Condition + diff --git a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/init.lua b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/init.lua new file mode 100644 index 0000000000..a02378a50d --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/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("ActivatedCarbonFilterMonitoring.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 ActivatedCarbonFilterMonitoringServerAttributes = {} + +function ActivatedCarbonFilterMonitoringServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ActivatedCarbonFilterMonitoringServerAttributes, attr_mt) + +return ActivatedCarbonFilterMonitoringServerAttributes + diff --git a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua new file mode 100644 index 0000000000..438de24c94 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua @@ -0,0 +1,33 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local ChangeIndicationEnum = {} +-- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility +-- with how types were handled in api < 10. +local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.OK] = "OK", + [self.WARNING] = "WARNING", + [self.CRITICAL] = "CRITICAL", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.OK = 0x00 +new_mt.__index.WARNING = 0x01 +new_mt.__index.CRITICAL = 0x02 + +ChangeIndicationEnum.OK = 0x00 +ChangeIndicationEnum.WARNING = 0x01 +ChangeIndicationEnum.CRITICAL = 0x02 + +ChangeIndicationEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(ChangeIndicationEnum, new_mt) + +return ChangeIndicationEnum + diff --git a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/Feature.lua b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/Feature.lua new file mode 100644 index 0000000000..88474d1b0f --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/Feature.lua @@ -0,0 +1,98 @@ +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.CONDITION = 0x0001 +Feature.WARNING = 0x0002 +Feature.REPLACEMENT_PRODUCT_LIST = 0x0004 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + CONDITION = 0x0001, + WARNING = 0x0002, + REPLACEMENT_PRODUCT_LIST = 0x0004, +} + +Feature.is_condition_set = function(self) + return (self.value & self.CONDITION) ~= 0 +end + +Feature.set_condition = function(self) + if self.value ~= nil then + self.value = self.value | self.CONDITION + else + self.value = self.CONDITION + end +end + +Feature.unset_condition = function(self) + self.value = self.value & (~self.CONDITION & self.BASE_MASK) +end + +Feature.is_warning_set = function(self) + return (self.value & self.WARNING) ~= 0 +end + +Feature.set_warning = function(self) + if self.value ~= nil then + self.value = self.value | self.WARNING + else + self.value = self.WARNING + end +end + +Feature.unset_warning = function(self) + self.value = self.value & (~self.WARNING & self.BASE_MASK) +end + +Feature.is_replacement_product_list_set = function(self) + return (self.value & self.REPLACEMENT_PRODUCT_LIST) ~= 0 +end + +Feature.set_replacement_product_list = function(self) + if self.value ~= nil then + self.value = self.value | self.REPLACEMENT_PRODUCT_LIST + else + self.value = self.REPLACEMENT_PRODUCT_LIST + end +end + +Feature.unset_replacement_product_list = function(self) + self.value = self.value & (~self.REPLACEMENT_PRODUCT_LIST & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.CONDITION | + Feature.WARNING | + Feature.REPLACEMENT_PRODUCT_LIST + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_condition_set = Feature.is_condition_set, + set_condition = Feature.set_condition, + unset_condition = Feature.unset_condition, + is_warning_set = Feature.is_warning_set, + set_warning = Feature.set_warning, + unset_warning = Feature.unset_warning, + is_replacement_product_list_set = Feature.is_replacement_product_list_set, + set_replacement_product_list = Feature.set_replacement_product_list, + unset_replacement_product_list = Feature.unset_replacement_product_list, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/init.lua b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/init.lua new file mode 100644 index 0000000000..2ff8e6e89a --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/init.lua @@ -0,0 +1,15 @@ +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("ActivatedCarbonFilterMonitoring.types." .. key) + end + return types_mt.__types_cache[key] +end + +local ActivatedCarbonFilterMonitoringTypes = {} + +setmetatable(ActivatedCarbonFilterMonitoringTypes, types_mt) + +return ActivatedCarbonFilterMonitoringTypes + diff --git a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/init.lua b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/init.lua new file mode 100644 index 0000000000..21795104b7 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/init.lua @@ -0,0 +1,79 @@ +local cluster_base = require "st.matter.cluster_base" +local HepaFilterMonitoringServerAttributes = require "HepaFilterMonitoring.server.attributes" +local HepaFilterMonitoringTypes = require "HepaFilterMonitoring.types" + +local HepaFilterMonitoring = {} + +HepaFilterMonitoring.ID = 0x0071 +HepaFilterMonitoring.NAME = "HepaFilterMonitoring" +HepaFilterMonitoring.server = {} +HepaFilterMonitoring.client = {} +HepaFilterMonitoring.server.attributes = HepaFilterMonitoringServerAttributes:set_parent_cluster(HepaFilterMonitoring) +HepaFilterMonitoring.types = HepaFilterMonitoringTypes + +function HepaFilterMonitoring:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "Condition", + [0x0001] = "DegradationDirection", + [0x0002] = "ChangeIndication", + [0x0003] = "InPlaceIndicator", + [0x0004] = "LastChangedTime", + [0x0005] = "ReplacementProductList", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function HepaFilterMonitoring:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "ResetCondition", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +HepaFilterMonitoring.attribute_direction_map = { + ["Condition"] = "server", + ["DegradationDirection"] = "server", + ["ChangeIndication"] = "server", + ["InPlaceIndicator"] = "server", + ["LastChangedTime"] = "server", + ["ReplacementProductList"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + + +HepaFilterMonitoring.FeatureMap = HepaFilterMonitoring.types.Feature + +function HepaFilterMonitoring.are_features_supported(feature, feature_map) + if (HepaFilterMonitoring.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 = HepaFilterMonitoring.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, HepaFilterMonitoring.NAME)) + end + return HepaFilterMonitoring[direction].attributes[key] +end +HepaFilterMonitoring.attributes = {} +setmetatable(HepaFilterMonitoring.attributes, attribute_helper_mt) + +setmetatable(HepaFilterMonitoring, {__index = cluster_base}) + +return HepaFilterMonitoring + diff --git a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/AttributeList.lua new file mode 100644 index 0000000000..c4f817e428 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/AttributeList.lua @@ -0,0 +1,126 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +--- @class st.matter.clusters.HepaFilterMonitoring.AttributeList +--- @alias AttributeList +--- +--- @field public ID number 0xFFFB the ID of this attribute +--- @field public NAME string "AttributeList" the name of this attribute +--- @field public data_type st.matter.data_types.Array the data type of this attribute + +local AttributeList = { + ID = 0xFFFB, + NAME = "AttributeList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +--- Add additional functionality to the base type object +--- +--- @param base_type_obj st.matter.data_types.Array the base data type object to add functionality to +function AttributeList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, AttributeList.element_type) + end +end + +--- Create a Array object of this attribute with any additional features provided for the attribute +--- This is also usable with the AttributeList(...) syntax +--- +--- @vararg vararg the values needed to construct a Array +--- @return st.matter.data_types.Array +function AttributeList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +--- Constructs an st.matter.interaction_model.InteractionRequest to read +--- this attribute from a device +--- @param device st.matter.Device +--- @param endpoint_id number|nil +--- @return st.matter.interaction_model.InteractionRequest containing an Interaction Request +function AttributeList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + + +--- Reporting policy: AttributeList => true => mandatory + +--- Sets up a Subscribe Interaction +--- +--- @param device any +--- @param endpoint_id number|nil +--- @return any +function AttributeList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AttributeList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +--- Builds an AttributeList test attribute reponse for the driver integration testing framework +--- +--- @param device st.matter.Device the device to build this message for +--- @param endpoint_id number|nil +--- @param value any +--- @param status string Interaction status associated with the path +--- @return st.matter.interaction_model.InteractionResponse of type REPORT_DATA +function AttributeList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AttributeList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) +return AttributeList + diff --git a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua new file mode 100644 index 0000000000..06a80153e3 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua @@ -0,0 +1,116 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +--- @class st.matter.clusters.HepaFilterMonitoring.ChangeIndication +--- @alias ChangeIndication +--- +--- @field public ID number 0x0002 the ID of this attribute +--- @field public NAME string "ChangeIndication" the name of this attribute +--- @field public data_type HepaFilterMonitoring.types.ChangeIndicationEnum the data type of this attribute + +local ChangeIndication = { + ID = 0x0002, + NAME = "ChangeIndication", + base_type = require "HepaFilterMonitoring.types.ChangeIndicationEnum", +} + +--- Create a ChangeIndicationEnum object of this attribute with any additional features provided for the attribute +--- This is also usable with the ChangeIndication(...) syntax +--- +--- @vararg vararg the values needed to construct a ChangeIndicationEnum +--- @return HepaFilterMonitoring.types.ChangeIndicationEnum +function ChangeIndication:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +--- Constructs an st.matter.interaction_model.InteractionRequest to read +--- this attribute from a device +--- @param device st.matter.Device +--- @param endpoint_id number|nil +--- @return st.matter.interaction_model.InteractionRequest containing an Interaction Request +function ChangeIndication:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + + +--- Reporting policy: ChangeIndication => true => mandatory + +--- Sets up a Subscribe Interaction +--- +--- @param device any +--- @param endpoint_id number|nil +--- @return any +function ChangeIndication:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function ChangeIndication:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +--- Builds an ChangeIndication test attribute reponse for the driver integration testing framework +--- +--- @param device st.matter.Device the device to build this message for +--- @param endpoint_id number|nil +--- @param value any +--- @param status string Interaction status associated with the path +--- @return st.matter.interaction_model.InteractionResponse of type REPORT_DATA +function ChangeIndication:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function ChangeIndication:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(ChangeIndication, {__call = ChangeIndication.new_value, __index = ChangeIndication.base_type}) +return ChangeIndication + diff --git a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/Condition.lua b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/Condition.lua new file mode 100644 index 0000000000..e668aa4c48 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/Condition.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local Condition = { + ID = 0x0000, + NAME = "Condition", + base_type = require "st.matter.data_types.Uint8", +} + +function Condition:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function Condition:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function Condition:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function Condition:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function Condition: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 Condition:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(Condition, {__call = Condition.new_value, __index = Condition.base_type}) +return Condition + diff --git a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/init.lua b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/init.lua new file mode 100644 index 0000000000..8d7ffe6c00 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/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("HepaFilterMonitoring.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 HepaFilterMonitoringServerAttributes = {} + +function HepaFilterMonitoringServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(HepaFilterMonitoringServerAttributes, attr_mt) + +return HepaFilterMonitoringServerAttributes + diff --git a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/ChangeIndicationEnum.lua b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/ChangeIndicationEnum.lua new file mode 100644 index 0000000000..438de24c94 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/ChangeIndicationEnum.lua @@ -0,0 +1,33 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local ChangeIndicationEnum = {} +-- Note: the name here is intentionally set to Uint8 to maintain backwards compatibility +-- with how types were handled in api < 10. +local new_mt = UintABC.new_mt({NAME = "Uint8", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.OK] = "OK", + [self.WARNING] = "WARNING", + [self.CRITICAL] = "CRITICAL", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.OK = 0x00 +new_mt.__index.WARNING = 0x01 +new_mt.__index.CRITICAL = 0x02 + +ChangeIndicationEnum.OK = 0x00 +ChangeIndicationEnum.WARNING = 0x01 +ChangeIndicationEnum.CRITICAL = 0x02 + +ChangeIndicationEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(ChangeIndicationEnum, new_mt) + +return ChangeIndicationEnum + diff --git a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/Feature.lua b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/Feature.lua new file mode 100644 index 0000000000..88474d1b0f --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/Feature.lua @@ -0,0 +1,98 @@ +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.CONDITION = 0x0001 +Feature.WARNING = 0x0002 +Feature.REPLACEMENT_PRODUCT_LIST = 0x0004 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + CONDITION = 0x0001, + WARNING = 0x0002, + REPLACEMENT_PRODUCT_LIST = 0x0004, +} + +Feature.is_condition_set = function(self) + return (self.value & self.CONDITION) ~= 0 +end + +Feature.set_condition = function(self) + if self.value ~= nil then + self.value = self.value | self.CONDITION + else + self.value = self.CONDITION + end +end + +Feature.unset_condition = function(self) + self.value = self.value & (~self.CONDITION & self.BASE_MASK) +end + +Feature.is_warning_set = function(self) + return (self.value & self.WARNING) ~= 0 +end + +Feature.set_warning = function(self) + if self.value ~= nil then + self.value = self.value | self.WARNING + else + self.value = self.WARNING + end +end + +Feature.unset_warning = function(self) + self.value = self.value & (~self.WARNING & self.BASE_MASK) +end + +Feature.is_replacement_product_list_set = function(self) + return (self.value & self.REPLACEMENT_PRODUCT_LIST) ~= 0 +end + +Feature.set_replacement_product_list = function(self) + if self.value ~= nil then + self.value = self.value | self.REPLACEMENT_PRODUCT_LIST + else + self.value = self.REPLACEMENT_PRODUCT_LIST + end +end + +Feature.unset_replacement_product_list = function(self) + self.value = self.value & (~self.REPLACEMENT_PRODUCT_LIST & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.CONDITION | + Feature.WARNING | + Feature.REPLACEMENT_PRODUCT_LIST + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_condition_set = Feature.is_condition_set, + set_condition = Feature.set_condition, + unset_condition = Feature.unset_condition, + is_warning_set = Feature.is_warning_set, + set_warning = Feature.set_warning, + unset_warning = Feature.unset_warning, + is_replacement_product_list_set = Feature.is_replacement_product_list_set, + set_replacement_product_list = Feature.set_replacement_product_list, + unset_replacement_product_list = Feature.unset_replacement_product_list, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/init.lua b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/init.lua new file mode 100644 index 0000000000..77aca088ff --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/init.lua @@ -0,0 +1,15 @@ +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("HepaFilterMonitoring.types." .. key) + end + return types_mt.__types_cache[key] +end + +local HepaFilterMonitoringTypes = {} + +setmetatable(HepaFilterMonitoringTypes, types_mt) + +return HepaFilterMonitoringTypes + diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/init.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/init.lua new file mode 100644 index 0000000000..697aa3fd05 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/init.lua @@ -0,0 +1,84 @@ +local cluster_base = require "st.matter.cluster_base" +local MicrowaveOvenControlServerAttributes = require "MicrowaveOvenControl.server.attributes" +local MicrowaveOvenControlServerCommands = require "MicrowaveOvenControl.server.commands" +local MicrowaveOvenControlTypes = require "MicrowaveOvenControl.types" + +local MicrowaveOvenControl = {} + +MicrowaveOvenControl.ID = 0x005F +MicrowaveOvenControl.NAME = "MicrowaveOvenControl" +MicrowaveOvenControl.server = {} +MicrowaveOvenControl.client = {} +MicrowaveOvenControl.server.attributes = MicrowaveOvenControlServerAttributes:set_parent_cluster(MicrowaveOvenControl) +MicrowaveOvenControl.server.commands = MicrowaveOvenControlServerCommands:set_parent_cluster(MicrowaveOvenControl) +MicrowaveOvenControl.types = MicrowaveOvenControlTypes + +function MicrowaveOvenControl:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "CookTime", + [0x0001] = "MaxCookTime", + [0xFFF9] = "AcceptedCommandList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function MicrowaveOvenControl:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "SetCookingParameters", + [0x0001] = "AddMoreTime", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +MicrowaveOvenControl.attribute_direction_map = { + ["CookTime"] = "server", + ["MaxCookTime"] = "server", + ["AcceptedCommandList"] = "server", +} + +MicrowaveOvenControl.command_direction_map = { + ["SetCookingParameters"] = "server", + ["AddMoreTime"] = "server", +} + +MicrowaveOvenControl.FeatureMap = MicrowaveOvenControl.types.Feature + +function MicrowaveOvenControl.are_features_supported(feature, feature_map) + if (MicrowaveOvenControl.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 = MicrowaveOvenControl.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, MicrowaveOvenControl.NAME)) + end + return MicrowaveOvenControl[direction].attributes[key] +end +MicrowaveOvenControl.attributes = {} +setmetatable(MicrowaveOvenControl.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = MicrowaveOvenControl.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, MicrowaveOvenControl.NAME)) + end + return MicrowaveOvenControl[direction].commands[key] +end +MicrowaveOvenControl.commands = {} +setmetatable(MicrowaveOvenControl.commands, command_helper_mt) + +setmetatable(MicrowaveOvenControl, {__index = cluster_base}) + +return MicrowaveOvenControl \ No newline at end of file diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/attributes/AcceptedCommandList.lua new file mode 100644 index 0000000000..1c41ddafa1 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/attributes/AcceptedCommandList.lua @@ -0,0 +1,74 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AcceptedCommandList = { + ID = 0xFFF9, + NAME = "AcceptedCommandList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +function AcceptedCommandList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, AcceptedCommandList.element_type) + end +end + +function AcceptedCommandList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function AcceptedCommandList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AcceptedCommandList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AcceptedCommandList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value, __index = AcceptedCommandList.base_type}) +return AcceptedCommandList diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/attributes/CookTime.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/attributes/CookTime.lua new file mode 100644 index 0000000000..7af3f4a3da --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/attributes/CookTime.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local CookTime = { + ID = 0x0000, + NAME = "CookTime", + base_type = require "st.matter.data_types.Uint32", +} + +function CookTime:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function CookTime:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function CookTime:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function CookTime:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CookTime: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 CookTime:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(CookTime, {__call = CookTime.new_value, __index = CookTime.base_type}) +return CookTime + diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/attributes/MaxCookTime.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/attributes/MaxCookTime.lua new file mode 100644 index 0000000000..1eba1d229f --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/attributes/MaxCookTime.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local MaxCookTime = { + ID = 0x0001, + NAME = "MaxCookTime", + base_type = require "st.matter.data_types.Uint32", +} + +function MaxCookTime:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function MaxCookTime:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function MaxCookTime:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function MaxCookTime:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MaxCookTime: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 MaxCookTime:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MaxCookTime, {__call = MaxCookTime.new_value, __index = MaxCookTime.base_type}) +return MaxCookTime + diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/attributes/init.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/attributes/init.lua new file mode 100644 index 0000000000..7dadc923c6 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/attributes/init.lua @@ -0,0 +1,23 @@ +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("MicrowaveOvenControl.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 MicrowaveOvenControlServerAttributes = {} + +function MicrowaveOvenControlServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(MicrowaveOvenControlServerAttributes, attr_mt) + +return MicrowaveOvenControlServerAttributes diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/commands/AddMoreTime.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/commands/AddMoreTime.lua new file mode 100644 index 0000000000..d9372ff1ec --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/commands/AddMoreTime.lua @@ -0,0 +1,97 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AddMoreTime = {} + +AddMoreTime.NAME = "AddMoreTime" +AddMoreTime.ID = 0x0001 +AddMoreTime.field_defs = { + { + name = "time_to_add", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint32", + }, +} + +function AddMoreTime:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, --tlv + status + ) +end + +function AddMoreTime:init(device, endpoint_id, time_to_add) + local out = {} + local args = {time_to_add} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = AddMoreTime, + __tostring = AddMoreTime.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function AddMoreTime:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AddMoreTime:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + base_type_obj.elements = elems +end + +function AddMoreTime:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(AddMoreTime, {__call = AddMoreTime.init}) + +return AddMoreTime \ No newline at end of file diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/commands/SetCookingParameters.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/commands/SetCookingParameters.lua new file mode 100644 index 0000000000..5ef5e16190 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/commands/SetCookingParameters.lua @@ -0,0 +1,125 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SetCookingParameters = {} + +SetCookingParameters.NAME = "SetCookingParameters" +SetCookingParameters.ID = 0x0000 +SetCookingParameters.field_defs = { + { + name = "cook_mode", + field_id = 0, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint8", + }, + { + name = "cook_time", + field_id = 1, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint32", + }, + { + name = "power_setting", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint8", + }, + { + name = "watt_setting_index", + field_id = 3, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint8", + }, + { + name = "start_after_setting", + field_id = 4, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Boolean", + }, +} + +function SetCookingParameters:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, --tlv + status + ) +end + +function SetCookingParameters:init(device, endpoint_id, cook_mode, cook_time, power_setting, watt_setting_index, start_after_setting) + local out = {} + local args = {cook_mode, cook_time, power_setting, watt_setting_index, start_after_setting} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = SetCookingParameters, + __tostring = SetCookingParameters.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function SetCookingParameters:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SetCookingParameters:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + base_type_obj.elements = elems +end + +function SetCookingParameters:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SetCookingParameters, {__call = SetCookingParameters.init}) + +return SetCookingParameters \ No newline at end of file diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/commands/init.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/commands/init.lua new file mode 100644 index 0000000000..1b345a90d6 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/server/commands/init.lua @@ -0,0 +1,22 @@ +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("MicrowaveOvenControl.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local MicrowaveOvenControlServerCommands = {} + +function MicrowaveOvenControlServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(MicrowaveOvenControlServerCommands, command_mt) + +return MicrowaveOvenControlServerCommands diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/types/Feature.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/types/Feature.lua new file mode 100644 index 0000000000..31a881aff6 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/types/Feature.lua @@ -0,0 +1,97 @@ +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.POWER_AS_NUMBER = 0x0001 +Feature.POWER_IN_WATTS = 0x0002 +Feature.POWER_NUMBER_LIMITS = 0x0004 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + POWER_AS_NUMBER = 0x0001, + POWER_IN_WATTS = 0x0002, + POWER_NUMBER_LIMITS = 0x0004, +} + +Feature.is_power_as_number_set = function(self) + return (self.value & self.POWER_AS_NUMBER) ~= 0 +end + +Feature.set_power_as_number = function(self) + if self.value ~= nil then + self.value = self.value | self.POWER_AS_NUMBER + else + self.value = self.POWER_AS_NUMBER + end +end + +Feature.unset_power_as_number = function(self) + self.value = self.value & (~self.POWER_AS_NUMBER & self.BASE_MASK) +end + +Feature.is_power_in_watts_set = function(self) + return (self.value & self.POWER_IN_WATTS) ~= 0 +end + +Feature.set_power_in_watts = function(self) + if self.value ~= nil then + self.value = self.value | self.POWER_IN_WATTS + else + self.value = self.POWER_IN_WATTS + end +end + +Feature.unset_power_in_watts = function(self) + self.value = self.value & (~self.POWER_IN_WATTS & self.BASE_MASK) +end + +Feature.is_power_number_limits_set = function(self) + return (self.value & self.POWER_NUMBER_LIMITS) ~= 0 +end + +Feature.set_power_number_limits = function(self) + if self.value ~= nil then + self.value = self.value | self.POWER_NUMBER_LIMITS + else + self.value = self.POWER_NUMBER_LIMITS + end +end + +Feature.unset_power_number_limits = function(self) + self.value = self.value & (~self.POWER_NUMBER_LIMITS & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.POWER_AS_NUMBER | + Feature.POWER_IN_WATTS | + Feature.POWER_NUMBER_LIMITS + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_power_as_number_set = Feature.is_power_as_number_set, + set_power_as_number = Feature.set_power_as_number, + unset_power_as_number = Feature.unset_power_as_number, + is_power_in_watts_set = Feature.is_power_in_watts_set, + set_power_in_watts = Feature.set_power_in_watts, + unset_power_in_watts = Feature.unset_power_in_watts, + is_power_number_limits_set = Feature.is_power_number_limits_set, + set_power_number_limits = Feature.set_power_number_limits, + unset_power_number_limits = Feature.unset_power_number_limits, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature \ No newline at end of file diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/types/init.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/types/init.lua new file mode 100644 index 0000000000..29c27b80ae --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/types/init.lua @@ -0,0 +1,15 @@ +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("MicrowaveOvenControl.types." .. key) + end + return types_mt.__types_cache[key] +end + +local MicrowaveOvenControlTypes = {} + +setmetatable(MicrowaveOvenControlTypes, types_mt) + +return MicrowaveOvenControlTypes + diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/init.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/init.lua new file mode 100644 index 0000000000..fdbbf4c443 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/init.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local MicrowaveOvenModeServerAttributes = require "MicrowaveOvenMode.server.attributes" +local MicrowaveOvenModeTypes = require "MicrowaveOvenMode.types" + +local MicrowaveOvenMode = {} + +MicrowaveOvenMode.ID = 0x005E +MicrowaveOvenMode.NAME = "MicrowaveOvenMode" +MicrowaveOvenMode.server = {} +MicrowaveOvenMode.client = {} +MicrowaveOvenMode.server.attributes = MicrowaveOvenModeServerAttributes:set_parent_cluster(MicrowaveOvenMode) +MicrowaveOvenMode.types = MicrowaveOvenModeTypes + +function MicrowaveOvenMode:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "SupportedModes", + [0x0001] = "CurrentMode", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function MicrowaveOvenMode: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 + +MicrowaveOvenMode.attribute_direction_map = { + ["SupportedModes"] = "server", + ["CurrentMode"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +MicrowaveOvenMode.FeatureMap = MicrowaveOvenMode.types.Feature + +function MicrowaveOvenMode.are_features_supported(feature, feature_map) + if (MicrowaveOvenMode.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 = MicrowaveOvenMode.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, MicrowaveOvenMode.NAME)) + end + return MicrowaveOvenMode[direction].attributes[key] +end +MicrowaveOvenMode.attributes = {} +setmetatable(MicrowaveOvenMode.attributes, attribute_helper_mt) + +setmetatable(MicrowaveOvenMode, {__index = cluster_base}) + +return MicrowaveOvenMode diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/server/attributes/AcceptedCommandList.lua new file mode 100644 index 0000000000..1c41ddafa1 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/server/attributes/AcceptedCommandList.lua @@ -0,0 +1,74 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AcceptedCommandList = { + ID = 0xFFF9, + NAME = "AcceptedCommandList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +function AcceptedCommandList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, AcceptedCommandList.element_type) + end +end + +function AcceptedCommandList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function AcceptedCommandList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AcceptedCommandList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AcceptedCommandList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value, __index = AcceptedCommandList.base_type}) +return AcceptedCommandList diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/server/attributes/CurrentMode.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/server/attributes/CurrentMode.lua new file mode 100644 index 0000000000..242a598bfd --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/server/attributes/CurrentMode.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local CurrentMode = { + ID = 0x0001, + NAME = "CurrentMode", + base_type = require "st.matter.data_types.Uint8", +} + +function CurrentMode:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function CurrentMode:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function CurrentMode:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function CurrentMode:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CurrentMode: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 CurrentMode:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(CurrentMode, {__call = CurrentMode.new_value, __index = CurrentMode.base_type}) +return CurrentMode + diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/server/attributes/SupportedModes.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/server/attributes/SupportedModes.lua new file mode 100644 index 0000000000..c6c19b685e --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/server/attributes/SupportedModes.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 SupportedModes = { + ID = 0x0000, + NAME = "SupportedModes", + base_type = require "st.matter.data_types.Array", + element_type = require "MicrowaveOvenMode.types.ModeOptionStruct", +} + +function SupportedModes: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, SupportedModes.element_type) + end +end + +function SupportedModes:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function SupportedModes:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function SupportedModes:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function SupportedModes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SupportedModes:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function SupportedModes:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SupportedModes, {__call = SupportedModes.new_value, __index = SupportedModes.base_type}) +return SupportedModes + diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/server/attributes/init.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/server/attributes/init.lua new file mode 100644 index 0000000000..a7fd9d169c --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/server/attributes/init.lua @@ -0,0 +1,23 @@ +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("MicrowaveOvenMode.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 MicrowaveOvenModeServerAttributes = {} + +function MicrowaveOvenModeServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(MicrowaveOvenModeServerAttributes, attr_mt) + +return MicrowaveOvenModeServerAttributes diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/Feature.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/Feature.lua new file mode 100644 index 0000000000..16f8292349 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/Feature.lua @@ -0,0 +1,53 @@ +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.ON_OFF = 0x0001 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + ON_OFF = 0x0001, +} + +Feature.is_on_off_set = function(self) + return (self.value & self.ON_OFF) ~= 0 +end + +Feature.set_on_off = function(self) + if self.value ~= nil then + self.value = self.value | self.ON_OFF + else + self.value = self.ON_OFF + end +end + +Feature.unset_on_off = function(self) + self.value = self.value & (~self.ON_OFF & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.ON_OFF + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_on_off_set = Feature.is_on_off_set, + set_on_off = Feature.set_on_off, + unset_on_off = Feature.unset_on_off, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature \ No newline at end of file diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/ModeOptionStruct.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/ModeOptionStruct.lua new file mode 100644 index 0000000000..f834d4be6e --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/ModeOptionStruct.lua @@ -0,0 +1,85 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +local ModeOptionStruct = {} +local new_mt = StructureABC.new_mt({NAME = "ModeOptionStruct", ID = data_types.name_to_id_map["Structure"]}) + +ModeOptionStruct.field_defs = { + { + name = "label", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.UTF8String1", + }, + { + name = "mode", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint8", + }, + { + name = "mode_tags", + field_id = 2, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Array", + element_type = require "MicrowaveOvenMode.types.ModeTagStruct", + }, +} + +ModeOptionStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do --Note: idx is 1 when field_id is 0 + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +ModeOptionStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = ModeOptionStruct.init +new_mt.__index.serialize = ModeOptionStruct.serialize + +ModeOptionStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(ModeOptionStruct, new_mt) + +return ModeOptionStruct + diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/ModeTag.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/ModeTag.lua new file mode 100644 index 0000000000..1ede9e638a --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/ModeTag.lua @@ -0,0 +1,27 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local ModeTag = {} +local new_mt = UintABC.new_mt({NAME = "ModeTag", ID = data_types.name_to_id_map["Uint16"]}, 2) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.NORMAL] = "NORMAL", + [self.DEFROST] = "DEFROST", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.NORMAL = 0x4000 +new_mt.__index.DEFROST = 0x4001 + +ModeTag.NORMAL = 0x4000 +ModeTag.DEFROST = 0x4001 + +ModeTag.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(ModeTag, new_mt) + +return ModeTag diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/ModeTagStruct.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/ModeTagStruct.lua new file mode 100644 index 0000000000..700ad7f597 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/ModeTagStruct.lua @@ -0,0 +1,78 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local ModeTagStruct = {} +local new_mt = StructureABC.new_mt({NAME = "ModeTagStruct", ID = data_types.name_to_id_map["Structure"]}) + +ModeTagStruct.field_defs = { + { + name = "mfg_code", + field_id = 0, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "value", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint16", + }, +} + +ModeTagStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do --Note: idx is 1 when field_id is 0 + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +ModeTagStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = ModeTagStruct.init +new_mt.__index.serialize = ModeTagStruct.serialize + +ModeTagStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(ModeTagStruct, new_mt) + +return ModeTagStruct + diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/init.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/init.lua new file mode 100644 index 0000000000..412d69cabd --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/init.lua @@ -0,0 +1,17 @@ +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + local req_loc = string.format("MicrowaveOvenMode.types.%s", key) + local cluster_type = require(req_loc) + types_mt.__types_cache[key] = cluster_type + end + return types_mt.__types_cache[key] +end + +local MicrowaveOvenModeTypes = {} + +setmetatable(MicrowaveOvenModeTypes, types_mt) + +return MicrowaveOvenModeTypes + diff --git a/drivers/SmartThings/matter-appliance/src/OvenMode/init.lua b/drivers/SmartThings/matter-appliance/src/OvenMode/init.lua new file mode 100644 index 0000000000..76e1817dc1 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/OvenMode/init.lua @@ -0,0 +1,80 @@ +local cluster_base = require "st.matter.cluster_base" +local OvenModeServerAttributes = require "OvenMode.server.attributes" +local OvenModeServerCommands = require "OvenMode.server.commands" +local OvenModeTypes = require "OvenMode.types" + +local OvenMode = {} + +OvenMode.ID = 0x0049 +OvenMode.NAME = "OvenMode" +OvenMode.server = {} +OvenMode.client = {} +OvenMode.server.attributes = OvenModeServerAttributes:set_parent_cluster(OvenMode) +OvenMode.server.commands = OvenModeServerCommands:set_parent_cluster(OvenMode) +OvenMode.types = OvenModeTypes + +function OvenMode:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "SupportedModes", + [0x0001] = "CurrentMode", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function OvenMode:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "ChangeToMode", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +OvenMode.attribute_direction_map = { + ["SupportedModes"] = "server", + ["CurrentMode"] = "server", +} + +OvenMode.command_direction_map = { + ["ChangeToMode"] = "server", +} + +OvenMode.FeatureMap = OvenMode.types.Feature + +function OvenMode.are_features_supported(feature, feature_map) + if (OvenMode.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 = OvenMode.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, OvenMode.NAME)) + end + return OvenMode[direction].attributes[key] +end +OvenMode.attributes = {} +setmetatable(OvenMode.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = OvenMode.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, OvenMode.NAME)) + end + return OvenMode[direction].commands[key] +end +OvenMode.commands = {} +setmetatable(OvenMode.commands, command_helper_mt) + +setmetatable(OvenMode, {__index = cluster_base}) + +return OvenMode diff --git a/drivers/SmartThings/matter-appliance/src/OvenMode/server/attributes/CurrentMode.lua b/drivers/SmartThings/matter-appliance/src/OvenMode/server/attributes/CurrentMode.lua new file mode 100644 index 0000000000..f2987e294d --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/OvenMode/server/attributes/CurrentMode.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local CurrentMode = { + ID = 0x0001, + NAME = "CurrentMode", + base_type = require "st.matter.data_types.Uint8", +} + +function CurrentMode:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function CurrentMode:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CurrentMode:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CurrentMode:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CurrentMode: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 CurrentMode:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(CurrentMode, {__call = CurrentMode.new_value, __index = CurrentMode.base_type}) +return CurrentMode diff --git a/drivers/SmartThings/matter-appliance/src/OvenMode/server/attributes/SupportedModes.lua b/drivers/SmartThings/matter-appliance/src/OvenMode/server/attributes/SupportedModes.lua new file mode 100644 index 0000000000..3803e1cd01 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/OvenMode/server/attributes/SupportedModes.lua @@ -0,0 +1,74 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SupportedModes = { + ID = 0x0000, + NAME = "SupportedModes", + base_type = require "st.matter.data_types.Array", + element_type = require "OvenMode.types.ModeOptionStruct", +} + +function SupportedModes: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, SupportedModes.element_type) + end +end + +function SupportedModes:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function SupportedModes:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function SupportedModes:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function SupportedModes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SupportedModes:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function SupportedModes:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SupportedModes, {__call = SupportedModes.new_value, __index = SupportedModes.base_type}) +return SupportedModes diff --git a/drivers/SmartThings/matter-appliance/src/OvenMode/server/attributes/init.lua b/drivers/SmartThings/matter-appliance/src/OvenMode/server/attributes/init.lua new file mode 100644 index 0000000000..6527039dfc --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/OvenMode/server/attributes/init.lua @@ -0,0 +1,23 @@ +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("OvenMode.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 OvenModeServerAttributes = {} + +function OvenModeServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(OvenModeServerAttributes, attr_mt) + +return OvenModeServerAttributes diff --git a/drivers/SmartThings/matter-appliance/src/OvenMode/server/commands/ChangeToMode.lua b/drivers/SmartThings/matter-appliance/src/OvenMode/server/commands/ChangeToMode.lua new file mode 100644 index 0000000000..4e492640a9 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/OvenMode/server/commands/ChangeToMode.lua @@ -0,0 +1,86 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ChangeToMode = {} + +ChangeToMode.NAME = "ChangeToMode" +ChangeToMode.ID = 0x0000 +ChangeToMode.field_defs = { + { + name = "new_mode", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint8", + }, +} + +function ChangeToMode:init(device, endpoint_id, new_mode) + local out = {} + local args = {new_mode} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = ChangeToMode, + __tostring = ChangeToMode.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function ChangeToMode:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ChangeToMode:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + base_type_obj.elements = elems +end + +function ChangeToMode:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(ChangeToMode, {__call = ChangeToMode.init}) + +return ChangeToMode diff --git a/drivers/SmartThings/matter-appliance/src/OvenMode/server/commands/init.lua b/drivers/SmartThings/matter-appliance/src/OvenMode/server/commands/init.lua new file mode 100644 index 0000000000..78434b8acd --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/OvenMode/server/commands/init.lua @@ -0,0 +1,22 @@ +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("OvenMode.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local OvenModeServerCommands = {} + +function OvenModeServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(OvenModeServerCommands, command_mt) + +return OvenModeServerCommands diff --git a/drivers/SmartThings/matter-appliance/src/OvenMode/types/Feature.lua b/drivers/SmartThings/matter-appliance/src/OvenMode/types/Feature.lua new file mode 100644 index 0000000000..da49bf4115 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/OvenMode/types/Feature.lua @@ -0,0 +1,54 @@ +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.ON_OFF = 0x0001 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + ON_OFF = 0x0001, +} + +Feature.is_on_off_set = function(self) + return (self.value & self.ON_OFF) ~= 0 +end + +Feature.set_on_off = function(self) + if self.value ~= nil then + self.value = self.value | self.ON_OFF + else + self.value = self.ON_OFF + end +end + +Feature.unset_on_off = function(self) + self.value = self.value & (~self.ON_OFF & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.ON_OFF + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_on_off_set = Feature.is_on_off_set, + set_on_off = Feature.set_on_off, + unset_on_off = Feature.unset_on_off, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-appliance/src/OvenMode/types/ModeOptionStruct.lua b/drivers/SmartThings/matter-appliance/src/OvenMode/types/ModeOptionStruct.lua new file mode 100644 index 0000000000..5278300221 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/OvenMode/types/ModeOptionStruct.lua @@ -0,0 +1,84 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local ModeOptionStruct = {} +local new_mt = StructureABC.new_mt({NAME = "ModeOptionStruct", ID = data_types.name_to_id_map["Structure"]}) +ModeOptionStruct.field_defs = { + { + name = "label", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.UTF8String1", + }, + { + name = "mode", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint8", + }, + { + name = "mode_tags", + field_id = 2, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Array", + element_type = require "OvenMode.types.ModeTagStruct", + }, +} + +ModeOptionStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +ModeOptionStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = ModeOptionStruct.init +new_mt.__index.serialize = ModeOptionStruct.serialize + +ModeOptionStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(ModeOptionStruct, new_mt) + +return ModeOptionStruct diff --git a/drivers/SmartThings/matter-appliance/src/OvenMode/types/ModeTag.lua b/drivers/SmartThings/matter-appliance/src/OvenMode/types/ModeTag.lua new file mode 100644 index 0000000000..233b2a0905 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/OvenMode/types/ModeTag.lua @@ -0,0 +1,48 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local ModeTag = {} +local new_mt = UintABC.new_mt({NAME = "ModeTag", ID = data_types.name_to_id_map["Uint16"]}, 2) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.BAKE] = "BAKE", + [self.CONVECTION] = "CONVECTION", + [self.GRILL] = "GRILL", + [self.ROAST] = "ROAST", + [self.CLEAN] = "CLEAN", + [self.CONVECTION_BAKE] = "CONVECTION_BAKE", + [self.CONVECTION_ROAST] = "CONVECTION_ROAST", + [self.WARMING] = "WARMING", + [self.PROOFING] = "PROOFING", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.BAKE = 0x4000 +new_mt.__index.CONVECTION = 0x4001 +new_mt.__index.GRILL = 0x4002 +new_mt.__index.ROAST = 0x4003 +new_mt.__index.CLEAN = 0x4004 +new_mt.__index.CONVECTION_BAKE = 0x4005 +new_mt.__index.CONVECTION_ROAST = 0x4006 +new_mt.__index.WARMING = 0x4007 +new_mt.__index.PROOFING = 0x4008 + +ModeTag.BAKE = 0x4000 +ModeTag.CONVECTION = 0x4001 +ModeTag.GRILL = 0x4002 +ModeTag.ROAST = 0x4003 +ModeTag.CLEAN = 0x4004 +ModeTag.CONVECTION_BAKE = 0x4005 +ModeTag.CONVECTION_ROAST = 0x4006 +ModeTag.WARMING = 0x4007 +ModeTag.PROOFING = 0x4008 + +ModeTag.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(ModeTag, new_mt) + +return ModeTag diff --git a/drivers/SmartThings/matter-appliance/src/OvenMode/types/ModeTagStruct.lua b/drivers/SmartThings/matter-appliance/src/OvenMode/types/ModeTagStruct.lua new file mode 100644 index 0000000000..610fb90ad7 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/OvenMode/types/ModeTagStruct.lua @@ -0,0 +1,77 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local ModeTagStruct = {} +local new_mt = StructureABC.new_mt({NAME = "ModeTagStruct", ID = data_types.name_to_id_map["Structure"]}) + +ModeTagStruct.field_defs = { + { + name = "mfg_code", + field_id = 0, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "value", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint16", + }, +} + +ModeTagStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +ModeTagStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = ModeTagStruct.init +new_mt.__index.serialize = ModeTagStruct.serialize + +ModeTagStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(ModeTagStruct, new_mt) + +return ModeTagStruct diff --git a/drivers/SmartThings/matter-appliance/src/OvenMode/types/init.lua b/drivers/SmartThings/matter-appliance/src/OvenMode/types/init.lua new file mode 100644 index 0000000000..8bd0777339 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/OvenMode/types/init.lua @@ -0,0 +1,15 @@ +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("OvenMode.types." .. key) + end + return types_mt.__types_cache[key] +end + +local OvenModeTypes = {} + +setmetatable(OvenModeTypes, types_mt) + +return OvenModeTypes + diff --git a/drivers/SmartThings/matter-appliance/src/embedded-cluster-utils.lua b/drivers/SmartThings/matter-appliance/src/embedded-cluster-utils.lua index febc27b569..c08a4cc44f 100644 --- a/drivers/SmartThings/matter-appliance/src/embedded-cluster-utils.lua +++ b/drivers/SmartThings/matter-appliance/src/embedded-cluster-utils.lua @@ -3,8 +3,10 @@ local utils = require "st.utils" local version = require "version" if version.api < 10 then + clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" clusters.DishwasherAlarm = require "DishwasherAlarm" clusters.DishwasherMode = require "DishwasherMode" + clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" clusters.LaundryWasherControls = require "LaundryWasherControls" clusters.LaundryWasherMode = require "LaundryWasherMode" clusters.OperationalState = require "OperationalState" @@ -13,17 +15,30 @@ if version.api < 10 then clusters.TemperatureControl = require "TemperatureControl" end +if version.api < 11 then + clusters.MicrowaveOvenControl = require "MicrowaveOvenControl" + clusters.MicrowaveOvenMode = require "MicrowaveOvenMode" +end + +-- this cluster is not supported in any release of the lua libs +clusters.OvenMode = require "OvenMode" + local embedded_cluster_utils = {} local embedded_clusters = { + [clusters.ActivatedCarbonFilterMonitoring.ID] = clusters.ActivatedCarbonFilterMonitoring, [clusters.DishwasherAlarm.ID] = clusters.DishwasherAlarm, [clusters.DishwasherMode.ID] = clusters.DishwasherMode, + [clusters.HepaFilterMonitoring.ID] = clusters.HepaFilterMonitoring, [clusters.LaundryWasherControls.ID] = clusters.LaundryWasherControls, [clusters.LaundryWasherMode.ID] = clusters.LaundryWasherMode, [clusters.OperationalState.ID] = clusters.OperationalState, [clusters.RefrigeratorAlarm.ID] = clusters.RefrigeratorAlarm, [clusters.RefrigeratorAndTemperatureControlledCabinetMode.ID] = clusters.RefrigeratorAndTemperatureControlledCabinetMode, [clusters.TemperatureControl.ID] = clusters.TemperatureControl, + [clusters.MicrowaveOvenControl.ID] = clusters.MicrowaveOvenControl, + [clusters.MicrowaveOvenMode.ID] = clusters.MicrowaveOvenMode, + [clusters.OvenMode.ID] = clusters.OvenMode, } function embedded_cluster_utils.get_endpoints(device, cluster_id, opts) diff --git a/drivers/SmartThings/matter-appliance/src/init.lua b/drivers/SmartThings/matter-appliance/src/init.lua index e6a46b3579..fa87f7177c 100644 --- a/drivers/SmartThings/matter-appliance/src/init.lua +++ b/drivers/SmartThings/matter-appliance/src/init.lua @@ -17,14 +17,15 @@ local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local im = require "st.matter.interaction_model" local embedded_cluster_utils = require "embedded-cluster-utils" - local log = require "log" local utils = require "st.utils" - local version = require "version" + if version.api < 10 then + clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" clusters.DishwasherAlarm = require "DishwasherAlarm" clusters.DishwasherMode = require "DishwasherMode" + clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" clusters.LaundryWasherControls = require "LaundryWasherControls" clusters.LaundryWasherMode = require "LaundryWasherMode" clusters.OperationalState = require "OperationalState" @@ -33,14 +34,27 @@ if version.api < 10 then clusters.TemperatureControl = require "TemperatureControl" end +if version.api < 11 then + clusters.MicrowaveOvenControl = require "MicrowaveOvenControl" + clusters.MicrowaveOvenMode = require "MicrowaveOvenMode" +end + +--this cluster is not supported in any releases of the lua libs +clusters.OvenMode = require "OvenMode" + local dishwasher = require("matter-dishwasher") -local laundryWasher = require("matter-laundry-washer") +local laundry = require("matter-laundry") local refrigerator = require("matter-refrigerator") +local extractor_hood = require("matter-extractor-hood") +local cook_top = require("matter-cook-top") +local microwave_oven = require("matter-microwave-oven") +local oven = require("matter-oven") local setpoint_limit_device_field = { MIN_TEMP = "MIN_TEMP", MAX_TEMP = "MAX_TEMP", } +local LAUNDRY_WASHER_DEVICE_TYPE_ID = 0x0073 local subscribed_attributes = { [capabilities.switch.ID] = { @@ -67,6 +81,10 @@ local subscribed_attributes = { clusters.LaundryWasherMode.attributes.CurrentMode, clusters.RefrigeratorAndTemperatureControlledCabinetMode.attributes.SupportedModes, clusters.RefrigeratorAndTemperatureControlledCabinetMode.attributes.CurrentMode, + clusters.MicrowaveOvenMode.attributes.CurrentMode, + clusters.MicrowaveOvenMode.attributes.SupportedModes, + clusters.OvenMode.attributes.SupportedModes, + clusters.OvenMode.attributes.CurrentMode, }, [capabilities.laundryWasherRinseMode.ID] = { clusters.LaundryWasherControls.attributes.NumberOfRinses, @@ -89,6 +107,29 @@ local subscribed_attributes = { [capabilities.temperatureAlarm.ID] = { clusters.DishwasherAlarm.attributes.State }, + [capabilities.fanMode.ID] = { + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode + }, + [capabilities.fanSpeedPercent.ID] = { + clusters.FanControl.attributes.PercentCurrent + }, + [capabilities.windMode.ID] = { + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.WindSetting + }, + [capabilities.filterState.ID] = { + clusters.HepaFilterMonitoring.attributes.Condition, + clusters.ActivatedCarbonFilterMonitoring.attributes.Condition + }, + [capabilities.filterStatus.ID] = { + clusters.HepaFilterMonitoring.attributes.ChangeIndication, + clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication + }, + [capabilities.cookTime.ID] = { + clusters.MicrowaveOvenControl.attributes.MaxCookTime, + clusters.MicrowaveOvenControl.attributes.CookTime + } } local function device_init(driver, device) @@ -98,41 +139,60 @@ end local function do_configure(driver, device) local tn_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, {feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_NUMBER}) local tl_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, {feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_LEVEL}) + local hepa_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID) + local ac_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID) + local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) if dishwasher.can_handle({}, driver, device) then local profile_name = "dishwasher" - if #tn_eps > 0 and #tl_eps > 0 then - profile_name = profile_name .. "-tn" .. "-tl" - elseif #tn_eps > 0 then + if #tn_eps > 0 then profile_name = profile_name .. "-tn" - elseif #tl_eps > 0 then + end + if #tl_eps > 0 then profile_name = profile_name .. "-tl" end device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) device:try_update_metadata({profile = profile_name}) - elseif laundryWasher.can_handle({}, driver, device) then - local profile_name = "laundry-washer" - if #tn_eps > 0 and #tl_eps > 0 then - profile_name = profile_name .. "-tn" .. "-tl" - elseif #tn_eps > 0 then + elseif laundry.can_handle({}, driver, device) then + local device_type = laundry.can_handle({}, driver, device) + local profile_name = "laundry" + if (device_type == LAUNDRY_WASHER_DEVICE_TYPE_ID) then + profile_name = profile_name.."-washer" + else + profile_name = profile_name.."-dryer" + end + if #tn_eps > 0 then profile_name = profile_name .. "-tn" - elseif #tl_eps > 0 then + end + if #tl_eps > 0 then profile_name = profile_name .. "-tl" end device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) device:try_update_metadata({profile = profile_name}) elseif refrigerator.can_handle({}, driver, device) then local profile_name = "refrigerator-freezer" - if #tn_eps > 0 and #tl_eps > 0 then - profile_name = profile_name .. "-tn" .. "-tl" - elseif #tn_eps > 0 then + if #tn_eps > 0 then profile_name = profile_name .. "-tn" - elseif #tl_eps > 0 then + end + if #tl_eps > 0 then profile_name = profile_name .. "-tl" end device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) device:try_update_metadata({profile = profile_name}) + elseif extractor_hood.can_handle({}, driver, device) then + local profile_name = "extractor-hood" + if #hepa_filter_eps > 0 then + profile_name = profile_name .. "-hepa" + end + if #ac_filter_eps > 0 then + profile_name = profile_name .. "-ac" + end + if #wind_eps > 0 then + profile_name = profile_name .. "-wind" + end + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) else - device.log.warn_with({hub_logs=true}, "Device has not sub driver") + device.log.warn_with({hub_logs=true}, "Device has no sub driver") end --Query setpoint limits if needed @@ -176,10 +236,10 @@ end local function temperature_setpoint_attr_handler(driver, device, ib, response) local tn_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, {feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_NUMBER}) if #tn_eps == 0 then - device.log.warn_with({ hub_logs = true }, string.format("Device does not support TEMPERATURE_NUMBER feature")) + device.log.warn_with({hub_logs = true}, string.format("Device does not support TEMPERATURE_NUMBER feature")) return end - device.log.info_with({ hub_logs = true }, + device.log.info_with({hub_logs = true}, string.format("temperature_setpoint_attr_handler: %d", ib.data.value)) local min = device:get_field(setpoint_limit_device_field.MIN_TEMP) or 0 @@ -199,7 +259,7 @@ local function setpoint_limit_handler(limit_field) return function(driver, device, ib, response) local tn_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, {feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_NUMBER}) if #tn_eps == 0 then - device.log.warn_with({ hub_logs = true }, string.format("Device does not support TEMPERATURE_NUMBER feature")) + device.log.warn_with({hub_logs = true}, string.format("Device does not support TEMPERATURE_NUMBER feature")) return end local val = ib.data.value / 100.0 @@ -224,11 +284,10 @@ end local function handle_temperature_setpoint(driver, device, cmd) local tn_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, {feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_NUMBER}) if #tn_eps == 0 then - device.log.warn_with({ hub_logs = true }, string.format("Device does not support TEMPERATURE_NUMBER feature")) + device.log.warn_with({hub_logs = true}, string.format("Device does not support TEMPERATURE_NUMBER feature")) return end - device.log.info_with({ hub_logs = true }, - string.format("handle_temperature_setpoint: %s", cmd.args.setpoint)) + device.log.info(string.format("handle_temperature_setpoint: %s", cmd.args.setpoint)) local value = cmd.args.setpoint local _, temp_setpoint = device:get_latest_state( @@ -289,11 +348,20 @@ local matter_driver_template = { capabilities.temperatureMeasurement, capabilities.waterFlowAlarm, capabilities.temperatureAlarm, + capabilities.filterState, + capabilities.filterStatus, + capabilities.fanMode, + capabilities.fanSpeedPercent, + capabilities.windMode }, sub_drivers = { dishwasher, - laundryWasher, - refrigerator + laundry, + refrigerator, + cook_top, + microwave_oven, + extractor_hood, + oven, } } diff --git a/drivers/SmartThings/matter-appliance/src/matter-cook-top/init.lua b/drivers/SmartThings/matter-appliance/src/matter-cook-top/init.lua new file mode 100644 index 0000000000..a153ba007b --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-cook-top/init.lua @@ -0,0 +1,227 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "embedded-cluster-utils" + +local log = require "log" + +local version = require "version" +if version.api < 10 then + clusters.TemperatureControl = require "TemperatureControl" +end + +local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" +local COOK_SURFACE_DEVICE_TYPE_ID = 0x0077 +local COOK_TOP_DEVICE_TYPE_ID = 0x0078 +local OVEN_DEVICE_ID = 0x007B + +local SUPPORTED_TEMPERATURE_LEVELS_MAP = "__supported_temperature_levels_map" + +local function get_endpoints_for_dt(device, device_type) + local endpoints = {} + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == device_type then + table.insert(endpoints, ep.endpoint_id) + break + end + end + end + table.sort(endpoints) + return endpoints +end + +local function table_contains(tab, val) + for _, tab_val in ipairs(tab) do + if tab_val == val then + return true + end + end + return false +end + +local function endpoint_to_component(device, ep) + local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} + for component, endpoint in pairs(map) do + if endpoint == ep then + return component + end + end + return "main" +end + +local function component_to_endpoint(device, component) + local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} + if map[component] then + return map[component] + end + return device.MATTER_DEFAULT_ENDPOINT +end + +local function device_init(driver, device) + device:subscribe() + device:set_endpoint_to_component_fn(endpoint_to_component) + device:set_component_to_endpoint_fn(component_to_endpoint) +end + +local function device_added(driver, device) + local cook_surface_endpoints = get_endpoints_for_dt(device, COOK_SURFACE_DEVICE_TYPE_ID) + local componentToEndpointMap = { + ["cookSurfaceOne"] = cook_surface_endpoints[1], + ["cookSurfaceTwo"] = cook_surface_endpoints[2] + } + device:set_field(COMPONENT_TO_ENDPOINT_MAP, componentToEndpointMap, { persist = true }) +end + +local function do_configure(driver, device) + local cook_surface_endpoints = get_endpoints_for_dt(device, COOK_SURFACE_DEVICE_TYPE_ID) + + local tl_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, + { feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_LEVEL }) + + local profile_name + if #cook_surface_endpoints > 0 then + profile_name = "cook-surface-one" + if table_contains(tl_eps, cook_surface_endpoints[1]) then + profile_name = profile_name .. "-tl" + end + + -- we only support up to two cook surfaces + if #cook_surface_endpoints > 1 then + profile_name = profile_name .. "-cook-surface-two" + if table_contains(tl_eps, cook_surface_endpoints[2]) then + profile_name = profile_name .. "-tl" + end + end + end + + if profile_name then + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({ profile = profile_name }) + end +end + +local function is_cook_top_device(opts, driver, device, ...) + local cook_top_eps = get_endpoints_for_dt(device, COOK_TOP_DEVICE_TYPE_ID) + local oven_eps = get_endpoints_for_dt(device, OVEN_DEVICE_ID) + -- we want to skip lifecycle events in cases where the device is an oven with a composed cook-top device + if (#oven_eps > 0) and opts.dispatcher_class == "DeviceLifecycleDispatcher" then + return false + end + if #cook_top_eps > 0 then + return true + end + return false +end + +-- Matter Handlers -- +local function selected_temperature_level_attr_handler(driver, device, ib, response) + local tl_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, + { feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_LEVEL }) + if #tl_eps == 0 then + log.warn_with({hub_logs = true}, string.format("Device does not support TEMPERATURE_LEVEL feature")) + return + end + log.info(string.format("selected_temperature_level_attr_handler: %s", ib.data.value)) + + local temperatureLevel = ib.data.value + local supportedTemperatureLevelsMap = device:get_field(SUPPORTED_TEMPERATURE_LEVELS_MAP) or {} + local supportedTemperatureLevels = supportedTemperatureLevelsMap[ib.endpoint_id] or {} + if supportedTemperatureLevels[temperatureLevel+1] then + local tempLevel = supportedTemperatureLevels[temperatureLevel+1] + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureLevel.temperatureLevel(tempLevel)) + end + log.warn("Received unsupported temperature level for endpoint "..(ib.endpoint_id)) +end + +local function supported_temperature_levels_attr_handler(driver, device, ib, response) + local tl_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, + { feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_LEVEL }) + if #tl_eps == 0 then + log.warn_with({hub_logs = true}, string.format("Device does not support TEMPERATURE_LEVEL feature")) + return + end + + local supportedTemperatureLevelsMap = device:get_field(SUPPORTED_TEMPERATURE_LEVELS_MAP) or {} + local supportedTemperatureLevels = {} + for _, tempLevel in ipairs(ib.data.elements) do + log.info(string.format("supported_temperature_levels_attr_handler: %s", tempLevel.value)) + table.insert(supportedTemperatureLevels, tempLevel.value) + end + supportedTemperatureLevelsMap[ib.endpoint_id] = supportedTemperatureLevels + device:set_field(SUPPORTED_TEMPERATURE_LEVELS_MAP, supportedTemperatureLevelsMap, { persist = true }) + local event = capabilities.temperatureLevel.supportedTemperatureLevels(supportedTemperatureLevels, + { visibility = { displayed = false } }) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +local function temp_event_handler(driver, device, ib, response) + device.log.info(string.format("temp_event_handler: %s", ib.data.value)) + + local temp + local unit = "C" + if ib.data.value == nil then + temp = 0 + else + temp = ib.data.value / 100.0 + end + device:emit_event_for_endpoint(ib.endpoint_id, + capabilities.temperatureMeasurement.temperature({ value = temp, unit = unit })) +end + +local function handle_temperature_level(driver, device, cmd) + log.info(string.format("handle_temperature_level: %s", cmd.args.temperatureLevel)) + + local endpoint_id = device:component_to_endpoint(cmd.component) + local supportedTemperatureLevelsMap = device:get_field(SUPPORTED_TEMPERATURE_LEVELS_MAP) or {} + local supportedTemperatureLevels = supportedTemperatureLevelsMap[endpoint_id] or {} + for i, tempLevel in ipairs(supportedTemperatureLevels) do + if cmd.args.temperatureLevel == tempLevel then + device:send(clusters.TemperatureControl.commands.SetTemperature(device, endpoint_id, nil, i - 1)) + return + end + end +end + +local matter_cook_top_handler = { + NAME = "matter-cook-top", + lifecycle_handlers = { + init = device_init, + added = device_added, + doConfigure = do_configure + }, + matter_handlers = { + attr = { + [clusters.TemperatureControl.ID] = { + [clusters.TemperatureControl.attributes.SelectedTemperatureLevel.ID] = + selected_temperature_level_attr_handler, + [clusters.TemperatureControl.attributes.SupportedTemperatureLevels.ID] = + supported_temperature_levels_attr_handler, + }, + [clusters.TemperatureMeasurement.ID] = { + [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = temp_event_handler, + }, + } + }, + capability_handlers = { + [capabilities.temperatureLevel.ID] = { + [capabilities.temperatureLevel.commands.setTemperatureLevel.NAME] = handle_temperature_level, + } + }, + can_handle = is_cook_top_device, +} + +return matter_cook_top_handler diff --git a/drivers/SmartThings/matter-appliance/src/matter-dishwasher/init.lua b/drivers/SmartThings/matter-appliance/src/matter-dishwasher/init.lua index 6e3100ae1b..165aae7d1e 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-dishwasher/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-dishwasher/init.lua @@ -99,6 +99,8 @@ local function dishwasher_supported_modes_attr_handler(driver, device, ib, respo device:set_field(SUPPORTED_DISHWASHER_MODES, supportedDishwasherModes, { persist = true }) local event = capabilities.mode.supportedModes(supportedDishwasherModes, {visibility = {displayed = false}}) device:emit_event_for_endpoint(ib.endpoint_id, event) + event = capabilities.mode.supportedArguments(supportedDishwasherModes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) end local function dishwasher_mode_attr_handler(driver, device, ib, response) diff --git a/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/init.lua b/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/init.lua new file mode 100644 index 0000000000..21a79da52e --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/init.lua @@ -0,0 +1,240 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" + +local EXTRACTOR_HOOD_DEVICE_TYPE_ID = 0x007A +local version = require "version" +if version.api < 10 then + clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" + clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" +end + +local WIND_MODE_MAP = { + [0] = capabilities.windMode.windMode.sleepWind, + [1] = capabilities.windMode.windMode.naturalWind +} + +local function device_init(driver, device) + device:subscribe() +end + +-- Matter Handlers -- +local function is_matter_extractor_hood(opts, driver, device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == EXTRACTOR_HOOD_DEVICE_TYPE_ID then + return true + end + end + end + return false +end + +local function fan_mode_handler(driver, device, ib, response) + if ib.data.value == clusters.FanControl.attributes.FanMode.OFF then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode.off()) + elseif ib.data.value == clusters.FanControl.attributes.FanMode.LOW then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode.low()) + elseif ib.data.value == clusters.FanControl.attributes.FanMode.MEDIUM then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode.medium()) + elseif ib.data.value == clusters.FanControl.attributes.FanMode.HIGH then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode.high()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode.auto()) + end +end + +local function fan_mode_sequence_handler(driver, device, ib, response) + local supportedFanModes + if ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH then + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.low.NAME, + capabilities.fanMode.fanMode.medium.NAME, + capabilities.fanMode.fanMode.high.NAME + } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH then + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.low.NAME, + capabilities.fanMode.fanMode.high.NAME + } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO then + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.low.NAME, + capabilities.fanMode.fanMode.medium.NAME, + capabilities.fanMode.fanMode.high.NAME, + capabilities.fanMode.fanMode.auto.NAME + } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH_AUTO then + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.low.NAME, + capabilities.fanMode.fanMode.high.NAME, + capabilities.fanMode.fanMode.auto.NAME + } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_HIGH_AUTO then + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.high.NAME, + capabilities.fanMode.fanMode.auto.NAME + } + else + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.high.NAME + } + end + local event = capabilities.fanMode.supportedFanModes(supportedFanModes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +local function fan_speed_percent_attr_handler(driver, device, ib, response) + local speed = ib.data.value + if speed ~= nil and speed <= 100 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(speed)) + end +end + +local function wind_support_handler(driver, device, ib, response) + local supported_wind_modes = {capabilities.windMode.windMode.noWind.NAME} + for mode, wind_mode in pairs(WIND_MODE_MAP) do + if ((ib.data.value >> mode) & 1) > 0 then + table.insert(supported_wind_modes, wind_mode.NAME) + end + end + local event = capabilities.windMode.supportedWindModes(supported_wind_modes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +local function wind_setting_handler(driver, device, ib, response) + for index, wind_mode in pairs(WIND_MODE_MAP) do + if ((ib.data.value >> index) & 1) > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, wind_mode()) + return + end + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windMode.windMode.noWind()) +end + +local function hepa_filter_condition_handler(driver, device, ib, response) + local component = device.profile.components["hepaFilter"] + local condition = ib.data.value + device:emit_component_event(component, capabilities.filterState.filterLifeRemaining(condition)) +end + +local function hepa_filter_change_indication_handler(driver, device, ib, response) + local component = device.profile.components["hepaFilter"] + if ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.OK then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) + elseif ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.WARNING then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) + elseif ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.CRITICAL then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.replace()) + end +end + +local function activated_carbon_filter_condition_handler(driver, device, ib, response) + local component = device.profile.components["activatedCarbonFilter"] + local condition = ib.data.value + device:emit_component_event(component, capabilities.filterState.filterLifeRemaining(condition)) +end + +local function activated_carbon_filter_change_indication_handler(driver, device, ib, response) + local component = device.profile.components["activatedCarbonFilter"] + if ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.OK then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) + elseif ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.WARNING then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) + elseif ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.CRITICAL then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.replace()) + end +end + +-- Capability Handlers -- +local function set_fan_mode(driver, device, cmd) + local fan_mode_id + if cmd.args.fanMode == capabilities.fanMode.fanMode.low.NAME then + fan_mode_id = clusters.FanControl.attributes.FanMode.LOW + elseif cmd.args.fanMode == capabilities.fanMode.fanMode.medium.NAME then + fan_mode_id = clusters.FanControl.attributes.FanMode.MEDIUM + elseif cmd.args.fanMode == capabilities.fanMode.fanMode.high.NAME then + fan_mode_id = clusters.FanControl.attributes.FanMode.HIGH + elseif cmd.args.fanMode == capabilities.fanMode.fanMode.auto.NAME then + fan_mode_id = clusters.FanControl.attributes.FanMode.AUTO + else + fan_mode_id = clusters.FanControl.attributes.FanMode.OFF + end + if fan_mode_id then + device:send(clusters.FanControl.attributes.FanMode:write(device, device:component_to_endpoint(cmd.component), fan_mode_id)) + end +end + +local function set_fan_speed_percent(driver, device, cmd) + local speed = math.floor(cmd.args.percent) + device:send(clusters.FanControl.attributes.PercentSetting:write(device, device:component_to_endpoint(cmd.component), speed)) +end + +local function set_wind_mode(driver, device, cmd) + local wind_mode = 0 + if cmd.args.windMode == capabilities.windMode.windMode.sleepWind.NAME then + wind_mode = clusters.FanControl.types.WindSupportMask.SLEEP_WIND + elseif cmd.args.windMode == capabilities.windMode.windMode.naturalWind.NAME then + wind_mode = clusters.FanControl.types.WindSupportMask.NATURAL_WIND + end + device:send(clusters.FanControl.attributes.WindSetting:write(device, device:component_to_endpoint(cmd.component), wind_mode)) +end + +local matter_extractor_hood_handler = { + NAME = "matter-extractor-hood", + lifecycle_handlers = { + init = device_init + }, + matter_handlers = { + attr = { + [clusters.HepaFilterMonitoring.ID] = { + [clusters.HepaFilterMonitoring.attributes.Condition.ID] = hepa_filter_condition_handler, + [clusters.HepaFilterMonitoring.attributes.ChangeIndication.ID] = hepa_filter_change_indication_handler + }, + [clusters.ActivatedCarbonFilterMonitoring.ID] = { + [clusters.ActivatedCarbonFilterMonitoring.attributes.Condition.ID] = activated_carbon_filter_condition_handler, + [clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.ID] = activated_carbon_filter_change_indication_handler + }, + [clusters.FanControl.ID] = { + [clusters.FanControl.attributes.FanModeSequence.ID] = fan_mode_sequence_handler, + [clusters.FanControl.attributes.FanMode.ID] = fan_mode_handler, + [clusters.FanControl.attributes.PercentCurrent.ID] = fan_speed_percent_attr_handler, + [clusters.FanControl.attributes.WindSupport.ID] = wind_support_handler, + [clusters.FanControl.attributes.WindSetting.ID] = wind_setting_handler + }, + } + }, + capability_handlers = { + [capabilities.fanMode.ID] = { + [capabilities.fanMode.commands.setFanMode.NAME] = set_fan_mode + }, + [capabilities.fanSpeedPercent.ID] = { + [capabilities.fanSpeedPercent.commands.setPercent.NAME] = set_fan_speed_percent + }, + [capabilities.windMode.ID] = { + [capabilities.windMode.commands.setWindMode.NAME] = set_wind_mode + } + }, + can_handle = is_matter_extractor_hood +} + +return matter_extractor_hood_handler diff --git a/drivers/SmartThings/matter-appliance/src/matter-laundry-washer/init.lua b/drivers/SmartThings/matter-appliance/src/matter-laundry/init.lua similarity index 96% rename from drivers/SmartThings/matter-appliance/src/matter-laundry-washer/init.lua rename to drivers/SmartThings/matter-appliance/src/matter-laundry/init.lua index 4dcf571cfb..8e65e3d652 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-laundry-washer/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-laundry/init.lua @@ -1,4 +1,4 @@ --- Copyright 2023 SmartThings +-- Copyright 2024 SmartThings -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ if version.api < 10 then end local LAUNDRY_WASHER_DEVICE_TYPE_ID = 0x0073 +local LAUNDRY_DRYER_DEVICE_TYPE_ID = 0x007C local LAUNDRY_WASHER_RINSE_MODE_MAP = { [clusters.LaundryWasherControls.types.NumberOfRinsesEnum.NONE] = capabilities.laundryWasherRinseMode.rinseMode.none, @@ -51,11 +52,11 @@ local function device_init(driver, device) end -- Matter Handlers -- -local function is_matter_laundry_washer(opts, driver, device) +local function is_matter_laundry_device(opts, driver, device) for _, ep in ipairs(device.endpoints) do for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == LAUNDRY_WASHER_DEVICE_TYPE_ID then - return true + if dt.device_type_id == LAUNDRY_WASHER_DEVICE_TYPE_ID or dt.device_type_id == LAUNDRY_DRYER_DEVICE_TYPE_ID then + return dt.device_type_id end end end @@ -112,6 +113,8 @@ local function laundry_washer_supported_modes_attr_handler(driver, device, ib, r device:set_field(SUPPORTED_LAUNDRY_WASHER_MODES, supportedLaundryWasherModes, {persist = true}) local event = capabilities.mode.supportedModes(supportedLaundryWasherModes, {visibility = {displayed = false}}) device:emit_component_event(component, event) + event = capabilities.mode.supportedArguments(supportedLaundryWasherModes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) end local function laundry_washer_mode_attr_handler(driver, device, ib, response) @@ -307,8 +310,8 @@ local function handle_operational_state_pause(driver, device, cmd) device:send(clusters.OperationalState.attributes.OperationalError:read(device, endpoint_id)) end -local matter_laundry_washer_handler = { - NAME = "matter-laundry-washer", +local matter_laundry_handler = { + NAME = "matter-laundry", lifecycle_handlers = { init = device_init, }, @@ -355,7 +358,7 @@ local matter_laundry_washer_handler = { [capabilities.operationalState.commands.resume.NAME] = handle_operational_state_resume, }, }, - can_handle = is_matter_laundry_washer, + can_handle = is_matter_laundry_device, } -return matter_laundry_washer_handler +return matter_laundry_handler diff --git a/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/init.lua b/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/init.lua new file mode 100644 index 0000000000..5b5174fcd1 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/init.lua @@ -0,0 +1,275 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local log = require "log" +local version = require "version" + +if version.api < 10 then + clusters.OperationalState = require "OperationalState" +end + +if version.api < 11 then + clusters.MicrowaveOvenControl = require "MicrowaveOvenControl" + clusters.MicrowaveOvenMode = require "MicrowaveOvenMode" +end + +local OPERATIONAL_STATE_COMMAND_MAP = { + [clusters.OperationalState.commands.Pause.ID] = "pause", + [clusters.OperationalState.commands.Stop.ID] = "stop", + [clusters.OperationalState.commands.Start.ID] = "start", + [clusters.OperationalState.commands.Resume.ID] = "resume", +} + +local MICROWAVE_OVEN_DEVICE_TYPE_ID = 0x0079 +local DEFAULT_COOKING_MODE = 0 +local DEFAULT_COOKING_TIME = 30 +local MICROWAVE_OVEN_SUPPORTED_MODES_KEY = "__microwave_oven_supported_modes__" + +local function device_init(driver, device) + device:subscribe() + device:send(clusters.MicrowaveOvenControl.attributes.MaxCookTime:read(device, device.MATTER_DEFAULT_ENDPOINT)) +end + +local function is_matter_mircowave_oven(opts, driver, device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == MICROWAVE_OVEN_DEVICE_TYPE_ID then + return true + end + end + end + return false +end + +local function get_last_set_cooking_parameters(device) + local cookingTime = device:get_latest_state("main", capabilities.cookTime.ID, capabilities.cookTime.cookTime.NAME) or DEFAULT_COOKING_TIME + local cookingMode = device:get_latest_state("main", capabilities.mode.ID, capabilities.mode.mode.ID) + local cookingModeId = DEFAULT_COOKING_MODE + local microwaveOvenModeSupportedModes = device:get_field(MICROWAVE_OVEN_SUPPORTED_MODES_KEY) or {} + for i, mode in ipairs(microwaveOvenModeSupportedModes) do + if cookingMode == mode then + cookingModeId = i - 1 + break + end + end + return cookingTime, cookingModeId +end + +local function operational_state_accepted_command_list_attr_handler(driver, device, ib, response) + log.info_with({ hub_logs = true }, + string.format("operational_state_accepted_command_list_attr_handler: %s", ib.data.elements)) + + local accepted_command_list = {} + for _, accepted_command in ipairs(ib.data.elements) do + local accepted_command_id = accepted_command.value + if OPERATIONAL_STATE_COMMAND_MAP[accepted_command_id] then + log.info_with({ hub_logs = true }, + string.format("AcceptedCommand: %s => %s", accepted_command_id, + OPERATIONAL_STATE_COMMAND_MAP[accepted_command_id])) + table.insert(accepted_command_list, OPERATIONAL_STATE_COMMAND_MAP[accepted_command_id]) + end + end + local event = capabilities.operationalState.supportedCommands(accepted_command_list, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +local function operational_state_attr_handler(driver, device, ib, response) + log.info_with({ hub_logs = true }, + string.format("operational_state_attr_handler operationalState: %s", ib.data.value)) + + local supported_mode = {} + if ib.data.value == clusters.OperationalState.types.OperationalStateEnum.STOPPED then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.stopped()) + supported_mode = device:get_field(MICROWAVE_OVEN_SUPPORTED_MODES_KEY) + elseif ib.data.value == clusters.OperationalState.types.OperationalStateEnum.RUNNING then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.running()) + elseif ib.data.value == clusters.OperationalState.types.OperationalStateEnum.PAUSED then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.paused()) + end + local event = capabilities.mode.supportedModes(supported_mode, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(device.MATTER_DEFAULT_ENDPOINT, event) + event = capabilities.mode.supportedArguments(supported_mode, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(device.MATTER_DEFAULT_ENDPOINT, event) +end + +local function operational_error_attr_handler(driver, device, ib, response) + if version.api < 10 then + clusters.OperationalState.types.ErrorStateStruct:augment_type(ib.data) + end + log.info_with({ hub_logs = true }, + string.format("operational_error_attr_handler errorStateID: %s", ib.data.elements.error_state_id.value)) + local operationalError = ib.data.elements.error_state_id.value + if operationalError == clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_START_OR_RESUME then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.unableToStartOrResume()) + elseif operationalError == clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_COMPLETE_OPERATION then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.unableToCompleteOperation()) + elseif operationalError == clusters.OperationalState.types.ErrorStateEnum.COMMAND_INVALID_IN_STATE then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.commandInvalidInCurrentState()) + end + if operationalError ~= clusters.OperationalState.types.ErrorStateEnum.NO_ERROR then + local event = capabilities.mode.supportedModes({}, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(device.MATTER_DEFAULT_ENDPOINT, event) + event = capabilities.mode.supportedArguments({}, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(device.MATTER_DEFAULT_ENDPOINT, event) + end +end + +local function microwave_oven_supported_modes_handler(driver, device, ib, response) + local microwaveOvenModeSupportedModes = {} + for _, mode in ipairs(ib.data.elements) do + if version.api < 11 then + clusters.MicrowaveOvenMode.types.ModeOptionStruct:augment_type(mode) + end + log.info_with({hub_logs=true},"Inserting supported microwave mode:", mode.elements.label.value) + table.insert(microwaveOvenModeSupportedModes, mode.elements.label.value) + end + local event = capabilities.mode.supportedModes(microwaveOvenModeSupportedModes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(device.MATTER_DEFAULT_ENDPOINT, event) + event = capabilities.mode.supportedArguments(microwaveOvenModeSupportedModes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(device.MATTER_DEFAULT_ENDPOINT, event) + device:set_field(MICROWAVE_OVEN_SUPPORTED_MODES_KEY, microwaveOvenModeSupportedModes) +end + +local function microwave_oven_current_mode_handler(driver, device, ib, response) + log.info_with({ hub_logs = true }, + string.format("microwave_oven_current_mode_handler currentMode: %s", ib.data.value)) + + local currentMode = ib.data.value + local microwaveOvenModeSupportedModes = device:get_field(MICROWAVE_OVEN_SUPPORTED_MODES_KEY) or {} + + if microwaveOvenModeSupportedModes[currentMode + 1] then + local mode = microwaveOvenModeSupportedModes[currentMode + 1] + device:emit_event_for_endpoint(device.MATTER_DEFAULT_ENDPOINT, capabilities.mode.mode(mode)) + return + end + log.warn(string.format("Microwave oven mode %s not found in supported microwave oven modes", currentMode)) +end + +local function microwave_oven_cook_time_handler(driver, device, ib, response) + local cookingTime = (ib.data.value == 0) and DEFAULT_COOKING_TIME or ib.data.value + device:emit_event_for_endpoint(device.MATTER_DEFAULT_ENDPOINT, capabilities.cookTime.cookTime(cookingTime)) +end + +local function microwave_oven_max_cook_time_handler(driver, device, ib, response) + local cook_time_range = { + minimum = 1, + maximum = ib.data.value + } + local event = capabilities.cookTime.cookTimeRange(cook_time_range, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(device.MATTER_DEFAULT_ENDPOINT, event) +end + +--------------------- +--capability handlers +--------------------- +local function update_device_state(device, endpoint) + device:send(clusters.OperationalState.attributes.OperationalState:read(device, endpoint)) + device:send(clusters.OperationalState.attributes.OperationalError:read(device, endpoint)) +end + +local function handle_operational_state_start(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.OperationalState.server.commands.Start(device, endpoint_id)) + update_device_state(device, endpoint_id) +end + +local function handle_operational_state_stop(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.OperationalState.server.commands.Stop(device, endpoint_id)) + update_device_state(device, endpoint_id) +end + +local function handle_operational_state_resume(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.OperationalState.server.commands.Resume(device, endpoint_id)) + update_device_state(device, endpoint_id) +end + +local function handle_operational_state_pause(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.OperationalState.server.commands.Pause(device, endpoint_id)) + update_device_state(device, endpoint_id) +end + +local function handle_microwave_oven_mode(driver, device, cmd) + log.info_with({ hub_logs = true }, + string.format("microwave_oven_mode[%s] mode: %s", cmd.component, cmd.args.mode)) + local microwaveOvenModeSupportedModes = device:get_field(MICROWAVE_OVEN_SUPPORTED_MODES_KEY) or {} + for i, mode in ipairs(microwaveOvenModeSupportedModes) do + if cmd.args.mode == mode then + local cookingTime, _ = get_last_set_cooking_parameters(device) + device:send(clusters.MicrowaveOvenControl.commands.SetCookingParameters(device, device.MATTER_DEFAULT_ENDPOINT, + i - 1, + cookingTime)) + return + end + end + log.warn(string.format("Microwave oven mode %s not found in supported modes", cmd.args.mode)) +end + +local function handle_set_cooking_time(driver, device, cmd) + local cookingTime = cmd.args.time + local _, mode_id = get_last_set_cooking_parameters(device) + device:send(clusters.MicrowaveOvenControl.commands.SetCookingParameters(device, device.MATTER_DEFAULT_ENDPOINT, mode_id, + cookingTime)) +end + +local matter_microwave_oven = { + NAME = "matter-microwave-oven", + lifecycle_handlers = { + init = device_init, + }, + matter_handlers = { + attr = { + [clusters.OperationalState.ID] = { + [clusters.OperationalState.attributes.AcceptedCommandList.ID] = + operational_state_accepted_command_list_attr_handler, + [clusters.OperationalState.attributes.OperationalState.ID] = operational_state_attr_handler, + [clusters.OperationalState.attributes.OperationalError.ID] = operational_error_attr_handler, + }, + [clusters.MicrowaveOvenMode.ID] = { + [clusters.MicrowaveOvenMode.attributes.SupportedModes.ID] = microwave_oven_supported_modes_handler, + [clusters.MicrowaveOvenMode.attributes.CurrentMode.ID] = microwave_oven_current_mode_handler, + }, + [clusters.MicrowaveOvenControl.ID] = { + [clusters.MicrowaveOvenControl.attributes.MaxCookTime.ID] = microwave_oven_max_cook_time_handler, + [clusters.MicrowaveOvenControl.attributes.CookTime.ID] = microwave_oven_cook_time_handler, + } + }, + }, + capability_handlers = { + [capabilities.operationalState.ID] = { + [capabilities.operationalState.commands.start.NAME] = handle_operational_state_start, + [capabilities.operationalState.commands.stop.NAME] = handle_operational_state_stop, + [capabilities.operationalState.commands.pause.NAME] = handle_operational_state_pause, + [capabilities.operationalState.commands.resume.NAME] = handle_operational_state_resume, + }, + [capabilities.mode.ID] = { + [capabilities.mode.commands.setMode.NAME] = handle_microwave_oven_mode, + }, + [capabilities.cookTime.ID] = { + [capabilities.cookTime.commands.setCookTime.NAME] = handle_set_cooking_time, + }, + }, + supported_capabilities = { + capabilities.operationalState, + capabilities.mode, + capabilities.cookTime + }, + can_handle = is_matter_mircowave_oven, +} + +return matter_microwave_oven diff --git a/drivers/SmartThings/matter-appliance/src/matter-oven/init.lua b/drivers/SmartThings/matter-appliance/src/matter-oven/init.lua new file mode 100644 index 0000000000..d284501551 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-oven/init.lua @@ -0,0 +1,286 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local log = require "log" +local version = require "version" +local embedded_cluster_utils = require "embedded-cluster-utils" +local utils = require "st.utils" + +if version.api < 10 then + clusters.TemperatureControl = require "TemperatureControl" +end + +--this cluster is not supported in any releases of the lua libs +clusters.OvenMode = require "OvenMode" + +local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" +local SUPPORTED_OVEN_MODES_MAP = "__supported_oven_modes_map_key_" +local CURRENT_CONFIGURED_UNIT = "__current_configured_unit" + +local OVEN_DEVICE_ID = 0x007B +local COOK_SURFACE_DEVICE_TYPE_ID = 0x0077 +local COOK_TOP_DEVICE_TYPE_ID = 0x0078 +local TCC_DEVICE_TYPE_ID = 0x0071 + +-- This is a work around to handle when units for temperatureSetpoint is changed for the App. +-- When units are switched, we will never know the recevied command value is for what unit as the arguments don't contain the unit. +-- So to handle this we assume the following ranges considering usual oven temperatures: +-- 1. if the recieved setpoint command value is in range 127 ~ 260, it is inferred as *C +-- 2. if the received setpoint command value is in range 261 ~ 500, it is inferred as *F +local OVEN_MAX_TEMP_IN_C = 260 +local OVEN_MIN_TEMP_IN_C = 127 + +local setpoint_limit_device_field = { + MIN_TEMP = "MIN_TEMP", + MAX_TEMP = "MAX_TEMP", +} + +local function get_endpoints_for_dt(device, device_type) + local endpoints = {} + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == device_type then + table.insert(endpoints, ep.endpoint_id) + break + end + end + end + table.sort(endpoints) + return endpoints +end + +local function endpoint_to_component(device, ep) + local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} + for component, endpoint in pairs(map) do + if endpoint == ep then + return component + end + end + return "main" +end + +local function component_to_endpoint(device, component) + local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} + if map[component] then + return map[component] + end + return device.MATTER_DEFAULT_ENDPOINT +end + +-- Lifecycle Handlers -- +local function device_init(driver, device) + device:subscribe() + device:set_endpoint_to_component_fn(endpoint_to_component) + device:set_component_to_endpoint_fn(component_to_endpoint) + device:set_field(CURRENT_CONFIGURED_UNIT, "C") +end + +local function device_added(driver, device) + -- We assume the following endpoint structure of oven device for now + local cook_surface_endpoints = get_endpoints_for_dt(device, COOK_SURFACE_DEVICE_TYPE_ID) + local cook_top_endpoint = get_endpoints_for_dt(device, COOK_TOP_DEVICE_TYPE_ID)[1] or device.MATTER_DEFAULT_ENDPOINT + local tcc_endpoints = get_endpoints_for_dt(device, TCC_DEVICE_TYPE_ID) + local componentToEndpointMap = { + ["tccOne"] = tcc_endpoints[1], + ["tccTwo"] = tcc_endpoints[2], + ["cookTop"] = cook_top_endpoint, + ["cookSurfaceOne"] = cook_surface_endpoints[1], + ["cookSurfaceTwo"] = cook_surface_endpoints[2] + } + device:set_field(COMPONENT_TO_ENDPOINT_MAP, componentToEndpointMap, { persist = true }) +end + +local function is_oven_device(opts, driver, device) + local oven_eps = get_endpoints_for_dt(device, OVEN_DEVICE_ID) + if #oven_eps > 0 then + return true + end + return false +end + +-- Matter Handlers -- +local function oven_supported_modes_attr_handler(driver, device, ib, response) + local supportedOvenModesMap = device:get_field(SUPPORTED_OVEN_MODES_MAP) or {} + local supportedOvenModes = {} + for _, mode in ipairs(ib.data.elements) do + clusters.OvenMode.types.ModeOptionStruct:augment_type(mode) + local modeLabel = mode.elements.label.value + log.info("Inserting supported oven mode: "..modeLabel) + table.insert(supportedOvenModes, modeLabel) + end + supportedOvenModesMap[string.format(ib.endpoint_id)] = supportedOvenModes + device:set_field(SUPPORTED_OVEN_MODES_MAP, supportedOvenModesMap, {persist = true}) + local event = capabilities.mode.supportedModes(supportedOvenModes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) + local event = capabilities.mode.supportedArguments(supportedOvenModes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +local function oven_mode_attr_handler(driver, device, ib, response) + log.info(string.format("oven_mode_attr_handler currentMode: %s", ib.data.value)) + + local supportedOvenModesMap = device:get_field(SUPPORTED_OVEN_MODES_MAP) or {} + local supportedOvenModes = supportedOvenModesMap[string.format(ib.endpoint_id)] or {} + local currentMode = ib.data.value + if supportedOvenModes[currentMode+1] then + local mode = supportedOvenModes[currentMode+1] + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.mode.mode(mode)) + return + end + log.warn("oven_mode_attr_handler received unsupported mode for endpoint"..ib.endpoint_id) +end + +local function temperature_setpoint_attr_handler(driver, device, ib, response) + local tn_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, + { feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_NUMBER }) + if #tn_eps == 0 then + device.log.warn(string.format("Device does not support TEMPERATURE_NUMBER feature")) + return + end + device.log.info(string.format("temperature_setpoint_attr_handler: %d", ib.data.value)) + + local min_field = string.format("%s-%d", setpoint_limit_device_field.MIN_TEMP, ib.endpoint_id) + local max_field = string.format("%s-%d", setpoint_limit_device_field.MAX_TEMP, ib.endpoint_id) + local min = device:get_field(min_field) or OVEN_MIN_TEMP_IN_C + local max = device:get_field(max_field) or OVEN_MAX_TEMP_IN_C + local temp = ib.data.value / 100.0 + local unit = "C" + local range = { + minimum = min, + maximum = max, + } + + -- Only emit the capability for RPC version >= 5, since unit conversion for + -- range capabilities is only supported in that case. + if version.rpc >= 5 then + device:emit_event_for_endpoint(ib.endpoint_id, + capabilities.temperatureSetpoint.temperatureSetpointRange({value = range, unit = unit},{visibility = {displayed = false}})) + end + + device:emit_event_for_endpoint(ib.endpoint_id, + capabilities.temperatureSetpoint.temperatureSetpoint({value = temp, unit = unit})) +end + +local function setpoint_limit_handler(limit_field) + return function(driver, device, ib, response) + local tn_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, + { feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_NUMBER }) + if #tn_eps == 0 then + device.log.warn(string.format("Device does not support TEMPERATURE_NUMBER feature")) + return + end + local field = string.format("%s-%d", limit_field, ib.endpoint_id) + local val = ib.data.value / 100.0 + + -- We clamp the max and min values as per the assumed oven temperature ranges. + if val < OVEN_MIN_TEMP_IN_C or val > OVEN_MAX_TEMP_IN_C then + if limit_field == setpoint_limit_device_field.MIN_TEMP then + val = OVEN_MIN_TEMP_IN_C + else + val = OVEN_MAX_TEMP_IN_C + end + end + + log.info("Setting " .. field .. " to " .. string.format("%s", val)) + device:set_field(field, val, { persist = true }) + end +end + +-- Capability Handlers -- +local function handle_oven_mode(driver, device, cmd) + log.info(string.format("handle_oven_mode mode: %s", cmd.args.mode)) + local ep = component_to_endpoint(device, cmd.component) + local supportedOvenModesMap = device:get_field(SUPPORTED_OVEN_MODES_MAP) or {} + local supportedOvenModes = supportedOvenModesMap[string.format(ep)] or {} + for i, mode in ipairs(supportedOvenModes) do + if cmd.args.mode == mode then + device:send(clusters.OvenMode.commands.ChangeToMode(device, ep, i - 1)) + return + end + end + log.warn("handle_oven_mode received unsupported mode: ".." for endpoint: "..ep) +end + +local function handle_temperature_setpoint(driver, device, cmd) + local tn_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, + { feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_NUMBER }) + if #tn_eps == 0 then + device.log.warn(string.format("Device does not support TEMPERATURE_NUMBER feature")) + return + end + device.log.info(string.format("handle_temperature_setpoint: %s", cmd.args.setpoint)) + + local value = cmd.args.setpoint + local _, temp_setpoint = device:get_latest_state( + cmd.component, capabilities.temperatureSetpoint.ID, + capabilities.temperatureSetpoint.temperatureSetpoint.NAME, + 0, { value = 0, unit = "C" } + ) + local ep = component_to_endpoint(device, cmd.component) + local min_field = string.format("%s-%d", setpoint_limit_device_field.MIN_TEMP, ep) + local max_field = string.format("%s-%d", setpoint_limit_device_field.MAX_TEMP, ep) + local min = device:get_field(min_field) or OVEN_MIN_TEMP_IN_C + local max = device:get_field(max_field) or OVEN_MAX_TEMP_IN_C + + if value > OVEN_MAX_TEMP_IN_C then + value = utils.f_to_c(value) + end + if value < min or value > max then + log.warn(string.format( + "Invalid setpoint (%s) outside the min (%s) and the max (%s)", + value, min, max + )) + device:emit_event_for_endpoint(ep, capabilities.temperatureSetpoint.temperatureSetpoint(temp_setpoint)) + return + end + + local ep = component_to_endpoint(device, cmd.component) + device:send(clusters.TemperatureControl.commands.SetTemperature(device, ep, utils.round(value * 100), nil)) +end + +local matter_oven_handler = { + NAME = "matter-oven", + lifecycle_handlers = { + init = device_init, + added = device_added, + }, + matter_handlers = { + attr = { + [clusters.TemperatureControl.ID] = { + [clusters.TemperatureControl.attributes.TemperatureSetpoint.ID] = temperature_setpoint_attr_handler, + [clusters.TemperatureControl.attributes.MinTemperature.ID] = setpoint_limit_handler( + setpoint_limit_device_field.MIN_TEMP), + [clusters.TemperatureControl.attributes.MaxTemperature.ID] = setpoint_limit_handler( + setpoint_limit_device_field.MAX_TEMP), + }, + [clusters.OvenMode.ID] = { + [clusters.OvenMode.attributes.SupportedModes.ID] = oven_supported_modes_attr_handler, + [clusters.OvenMode.attributes.CurrentMode.ID] = oven_mode_attr_handler, + }, + }, + }, + capability_handlers = { + [capabilities.mode.ID] = { + [capabilities.mode.commands.setMode.NAME] = handle_oven_mode, + }, + [capabilities.temperatureSetpoint.ID] = { + [capabilities.temperatureSetpoint.commands.setTemperatureSetpoint.NAME] = handle_temperature_setpoint, + } + }, + can_handle = is_oven_device, +} + +return matter_oven_handler diff --git a/drivers/SmartThings/matter-appliance/src/matter-refrigerator/init.lua b/drivers/SmartThings/matter-appliance/src/matter-refrigerator/init.lua index b0ffa57d31..487c60b515 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-refrigerator/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-refrigerator/init.lua @@ -142,7 +142,7 @@ local function temperature_setpoint_attr_handler(driver, device, ib, response) minimum = min, maximum = max, } - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureSetpoint.temperatureSetpointRange({value = range, unit = unit})) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureSetpoint.temperatureSetpointRange({value = range, unit = unit}), { visibility = { displayed = false } }) local temp = ib.data.value / 100.0 device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureSetpoint.temperatureSetpoint({value = temp, unit = unit})) @@ -214,6 +214,8 @@ local function refrigerator_tcc_supported_modes_attr_handler(driver, device, ib, device:set_field(SUPPORTED_REFRIGERATOR_TCC_MODES_MAP, supportedRefrigeratorTccModesMap, {persist = true}) local event = capabilities.mode.supportedModes(supportedRefrigeratorTccModes, {visibility = {displayed = false}}) device:emit_event_for_endpoint(ib.endpoint_id, event) + event = capabilities.mode.supportedArguments(supportedRefrigeratorTccModes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) end local function refrigerator_tcc_mode_attr_handler(driver, device, ib, response) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua b/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua new file mode 100644 index 0000000000..37129ba025 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua @@ -0,0 +1,202 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" + +local COOK_TOP_ENDPOINT = 1 +local COOK_SURFACE_ONE_ENDPOINT = 2 +local COOK_SURFACE_TWO_ENDPOINT = 3 + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("cook-surface-one-tl-cook-surface-two-tl.yml"), --on an actual device we would switch to this over doConfigure. + 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 = COOK_TOP_ENDPOINT, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 4}, --OffOnly feature + }, + device_types = { + { device_type_id = 0x0078, device_type_revision = 1 } -- Cook Top + } + }, + { + endpoint_id = COOK_SURFACE_ONE_ENDPOINT, + clusters = { + { cluster_id = clusters.TemperatureControl.ID, cluster_type = "SERVER", feature_map = 2 }, --Temperature Level + { cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, + }, + device_types = { + { device_type_id = 0x0077, device_type_revision = 1 } -- Cook Surface + } + }, + { + endpoint_id = COOK_SURFACE_TWO_ENDPOINT, + clusters = { + { cluster_id = clusters.TemperatureControl.ID, cluster_type = "SERVER", feature_map = 2}, --Temperature Level + { cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0077, device_type_revision = 1 } -- Cook Surface + } + } + } +}) + +local function test_init() + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureControl.attributes.SelectedTemperatureLevel, + clusters.TemperatureControl.attributes.SupportedTemperatureLevels + } + test.socket.matter:__set_channel_ordering("relaxed") + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Verify device profile update", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ profile = "cook-surface-one-tl-cook-surface-two-tl" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_coroutine_test( + "Assert component to endpoint map", + function() + local component_to_endpoint_map = mock_device:get_field("__component_to_endpoint_map") + assert(component_to_endpoint_map["cookSurfaceOne"] == COOK_SURFACE_ONE_ENDPOINT, "Cook Surface One Endpoint must be 2") + assert(component_to_endpoint_map["cookSurfaceTwo"] == COOK_SURFACE_TWO_ENDPOINT, "Cook Surface Two Endpoint must be 3") + end +) + +test.register_message_test( + "Off command should send appropriate commands", + -- we do not test "on" command, as cook-top is supposed to have offOnly feature. + { + { + 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, COOK_TOP_ENDPOINT) + } + } + } +) + +local utf1 = require "st.matter.data_types.UTF8String1" + +test.register_message_test( + "Cook Surface Two: TemperatureControl Supported Levels must be registered and setTemperatureLevel level command should send appropriate command", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureControl.attributes.SupportedTemperatureLevels:build_test_report_data(mock_device, COOK_SURFACE_TWO_ENDPOINT, {utf1("Level 1"), utf1("Level 2"), utf1("Level 3")}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("cookSurfaceTwo", capabilities.temperatureLevel.supportedTemperatureLevels({"Level 1", "Level 2", "Level 3"}, {visibility = {displayed = false}})) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "temperatureLevel", component = "cookSurfaceTwo", command = "setTemperatureLevel", args = {"Level 1"}} + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.TemperatureControl.server.commands.SetTemperature(mock_device, COOK_SURFACE_TWO_ENDPOINT, nil, 0) --0 is the index where Level1 is stored. + } + }, + } +) + +test.register_message_test( + "MeasuredValue of TemperatureMeasurement clusters should be reported correctly.", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 2, 40*100) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("cookSurfaceOne", capabilities.temperatureMeasurement.temperature({ value = 40.0, unit = "C" })) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 3, 20*100) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("cookSurfaceTwo", capabilities.temperatureMeasurement.temperature({ value = 20.0, unit = "C" })) + } + } +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua b/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua new file mode 100644 index 0000000000..7fd12ff1f9 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua @@ -0,0 +1,538 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local version = require "version" + +if version.api < 10 then + clusters.LaundryWasherMode = require "LaundryWasherMode" + clusters.OperationalState = require "OperationalState" + clusters.TemperatureControl = require "TemperatureControl" +end + +local APPLICATION_ENDPOINT = 1 + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("laundry-dryer-tn-tl.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 }, -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + }, + { cluster_id = clusters.LaundryWasherMode.ID, cluster_type = "SERVER" }, + { cluster_id = clusters.OperationalState.ID, cluster_type = "SERVER" }, + { cluster_id = clusters.TemperatureControl.ID, cluster_type = "SERVER", feature_map = 3}, + }, + device_types = { + { device_type_id = 0x007C, device_type_revision = 1 } -- LaundryDryer + } + } + } +}) + +local function test_init() + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LaundryWasherMode.attributes.CurrentMode, + clusters.LaundryWasherMode.attributes.SupportedModes, + clusters.OperationalState.attributes.OperationalState, + clusters.OperationalState.attributes.OperationalError, + clusters.OperationalState.attributes.AcceptedCommandList, + clusters.TemperatureControl.attributes.TemperatureSetpoint, + clusters.TemperatureControl.attributes.MaxTemperature, + clusters.TemperatureControl.attributes.MinTemperature, + clusters.TemperatureControl.attributes.SelectedTemperatureLevel, + clusters.TemperatureControl.attributes.SupportedTemperatureLevels + } + test.socket.matter:__set_channel_ordering("relaxed") + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +test.register_message_test( + "Off command should send appropriate commands", + -- we do not test "on" command, as laundry devices are supposed to have offOnly feature. + { + { + 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, APPLICATION_ENDPOINT) + } + } + } +) + +test.register_message_test( + "Setting operationalState command to 'start' should send appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "operationalState", component = "main", command = "start", args = {} } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.server.commands.Start(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalState:read(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:read(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalState:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.OperationalStateEnum.RUNNING) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.operationalState.operationalState.running()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.ErrorStateStruct({ + ["error_state_id"] = clusters.OperationalState.types.ErrorStateEnum.NO_ERROR, + ["error_state_label"] = "", + ["error_state_details"] = "" + })) + } + }, -- on receiving NO ERROR we don't do anything. + } +) + +test.register_message_test( + "Setting operationalState command to 'stop' should send appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "operationalState", component = "main", command = "stop", args = {} } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.server.commands.Stop(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalState:read(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:read(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalState:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.OperationalStateEnum.STOPPED) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.operationalState.operationalState.stopped()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.ErrorStateStruct({ + ["error_state_id"] = clusters.OperationalState.types.ErrorStateEnum.NO_ERROR, + ["error_state_label"] = "", + ["error_state_details"] = "" + })) + } + }, -- on receiving NO ERROR we don't do anything. + } +) + +test.register_message_test( + "Setting operationalState command to 'pause' should send appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "operationalState", component = "main", command = "pause", args = {} } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.server.commands.Pause(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalState:read(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:read(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalState:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.OperationalStateEnum.PAUSED) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.operationalState.operationalState.paused()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.ErrorStateStruct({ + ["error_state_id"] = clusters.OperationalState.types.ErrorStateEnum.NO_ERROR, + ["error_state_label"] = "", + ["error_state_details"] = "" + })) + } + }, -- on receiving NO ERROR we don't do anything. + } +) + +test.register_message_test( + "On receiving OperationalError, the appropriate operationalState event must be emitted", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.ErrorStateStruct({ + ["error_state_id"] = clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_START_OR_RESUME, + ["error_state_label"] = "", + ["error_state_details"] = "" + })) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.operationalState.operationalState.unableToStartOrResume()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.ErrorStateStruct({ + ["error_state_id"] = clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_COMPLETE_OPERATION, + ["error_state_label"] = "", + ["error_state_details"] = "" + })) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.operationalState.operationalState.unableToCompleteOperation()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.ErrorStateStruct({ + ["error_state_id"] = clusters.OperationalState.types.ErrorStateEnum.COMMAND_INVALID_IN_STATE, + ["error_state_label"] = "", + ["error_state_details"] = "" + })) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.operationalState.operationalState.commandInvalidInCurrentState()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.ErrorStateStruct({ + ["error_state_id"] = clusters.OperationalState.types.ErrorStateEnum.NO_ERROR, + ["error_state_label"] = "", + ["error_state_details"] = "" + })) + } + }, -- on receiving NO ERROR we don't do anything. + } +) + +test.register_message_test( + "Laundry Washer Suppoerted Modes must be registered and Laundry Washer Mode command should send appropriate commands", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LaundryWasherMode.attributes.SupportedModes:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + { + clusters.LaundryWasherMode.types.ModeOptionStruct({ ["label"] = "Quick", ["mode"] = 0, ["mode_tags"] = {} }), + clusters.LaundryWasherMode.types.ModeOptionStruct({ ["label"] = "Super Dry", ["mode"] = 1, ["mode_tags"] = {} }) + } + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.mode.supportedModes({"Quick", "Super Dry"}, {visibility = {displayed = false}})) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.mode.supportedArguments({"Quick", "Super Dry"}, {visibility = {displayed = false}})) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "mode", component = "main", command = "setMode", args = {"Quick"}} + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LaundryWasherMode.server.commands.ChangeToMode(mock_device, APPLICATION_ENDPOINT, 0) --0 is the index where Quick is stored. + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "mode", component = "main", command = "setMode", args = {"Super Dry"}} + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LaundryWasherMode.server.commands.ChangeToMode(mock_device, APPLICATION_ENDPOINT, 1) --1 is the index where Super Dry is stored. + } + } + } +) + +local utf1 = require "st.matter.data_types.UTF8String1" + +test.register_message_test( + "TemperatureControl Supported Levels must be registered and setTemperatureLevel level command should send appropriate commands", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureControl.attributes.SupportedTemperatureLevels:build_test_report_data(mock_device, APPLICATION_ENDPOINT, {utf1("Level 1"), utf1("Level 2"), utf1("Level 3")}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureLevel.supportedTemperatureLevels({"Level 1", "Level 2", "Level 3"}, {visibility = {displayed = false}})) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "temperatureLevel", component = "main", command = "setTemperatureLevel", args = {"Level 1"}} + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.TemperatureControl.server.commands.SetTemperature(mock_device, APPLICATION_ENDPOINT, nil, 0) --0 is the index where Level1 is stored. + } + }, + } +) + +test.register_message_test( + "temperatureSetpoint command should send appropriate commands", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureControl.attributes.MinTemperature:build_test_report_data(mock_device, APPLICATION_ENDPOINT, 0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureControl.attributes.MaxTemperature:build_test_report_data(mock_device, APPLICATION_ENDPOINT, 10000) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureControl.attributes.TemperatureSetpoint:build_test_report_data(mock_device, APPLICATION_ENDPOINT, 9000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureSetpoint.temperatureSetpointRange({value = {minimum=0.0,maximum=100.0}, unit = "C"})) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureSetpoint.temperatureSetpoint({value = 90.0, unit = "C"})) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "temperatureSetpoint", component = "main", command = "setTemperatureSetpoint", args = {40.0}} + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.TemperatureControl.commands.SetTemperature(mock_device, APPLICATION_ENDPOINT, 40 * 100, nil) + } + }, + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-appliance/src/test/test_matter_extractor_hood.lua b/drivers/SmartThings/matter-appliance/src/test/test_matter_extractor_hood.lua new file mode 100644 index 0000000000..e8aa17162c --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/test/test_matter_extractor_hood.lua @@ -0,0 +1,580 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" + +local clusters = require "st.matter.clusters" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("extractor-hood-hepa-ac-wind.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.HepaFilterMonitoring.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ActivatedCarbonFilterMonitoring.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = clusters.FanControl.types.FanControlFeature.WIND}, + }, + device_types = { + {device_type_id = 0x007A, device_type_revision = 1} -- Extractor Hood + } + } + } +}) + +local function test_init() + local subscribed_attributes = { + [capabilities.fanMode.ID] = { + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode + }, + [capabilities.fanSpeedPercent.ID] = { + clusters.FanControl.attributes.PercentCurrent + }, + [capabilities.windMode.ID] = { + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.WindSetting + }, + [capabilities.filterState.ID] = { + clusters.HepaFilterMonitoring.attributes.Condition, + clusters.ActivatedCarbonFilterMonitoring.attributes.Condition + }, + [capabilities.filterStatus.ID] = { + clusters.HepaFilterMonitoring.attributes.ChangeIndication, + clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication + } + } + local subscribe_request = nil + for _, attributes in pairs(subscribed_attributes) do + for _, attribute in ipairs(attributes) do + if subscribe_request == nil then + subscribe_request = attribute:subscribe(mock_device) + else + subscribe_request:merge(attribute:subscribe(mock_device)) + end + end + end + + 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) + +test.register_message_test( + "Test fan percent", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.FanControl.server.attributes.PercentCurrent:build_test_report_data(mock_device, 1, 10) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanSpeedPercent.percent(10)) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "fanSpeedPercent", component = "main", command = "setPercent", args = { 50 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.FanControl.attributes.PercentSetting:write(mock_device, 1, 50) + } + } + } +) + +test.register_message_test( + "Test fan mode matter handler", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.FanControl.attributes.FanMode:build_test_report_data(mock_device, 1, clusters.FanControl.types.FanModeEnum.OFF) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.fanMode("off")) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.FanControl.attributes.FanMode:build_test_report_data(mock_device, 1, clusters.FanControl.types.FanModeEnum.LOW) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.fanMode("low")) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.FanControl.attributes.FanMode:build_test_report_data(mock_device, 1, clusters.FanControl.types.FanModeEnum.MEDIUM) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.fanMode("medium")) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.FanControl.attributes.FanMode:build_test_report_data(mock_device, 1, clusters.FanControl.types.FanModeEnum.HIGH) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.fanMode("high")) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.FanControl.attributes.FanMode:build_test_report_data(mock_device, 1, clusters.FanControl.types.FanModeEnum.AUTO) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.fanMode("auto")) + } + } +) + +test.register_message_test( + "Test fan mode capability handler", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "fanMode", component = "main", command = "setFanMode", args = { capabilities.fanMode.fanMode.off.NAME } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.FanControl.attributes.FanMode:write(mock_device, 1, clusters.FanControl.types.FanModeEnum.OFF) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "fanMode", component = "main", command = "setFanMode", args = { capabilities.fanMode.fanMode.low.NAME } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.FanControl.attributes.FanMode:write(mock_device, 1, clusters.FanControl.types.FanModeEnum.LOW) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "fanMode", component = "main", command = "setFanMode", args = { capabilities.fanMode.fanMode.medium.NAME } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.FanControl.attributes.FanMode:write(mock_device, 1, clusters.FanControl.types.FanModeEnum.MEDIUM) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "fanMode", component = "main", command = "setFanMode", args = { capabilities.fanMode.fanMode.high.NAME } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.FanControl.attributes.FanMode:write(mock_device, 1, clusters.FanControl.types.FanModeEnum.HIGH) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "fanMode", component = "main", command = "setFanMode", args = { capabilities.fanMode.fanMode.auto.NAME } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.FanControl.attributes.FanMode:write(mock_device, 1, clusters.FanControl.types.FanModeEnum.AUTO) + } + } + } +) +test.register_message_test( + "Test setting fan mode sequence", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.FanControl.attributes.FanModeSequence:build_test_report_data(mock_device, 1, clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.supportedFanModes({ + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.low.NAME, + capabilities.fanMode.fanMode.medium.NAME, + capabilities.fanMode.fanMode.high.NAME + }, {visibility={displayed=false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.FanControl.attributes.FanModeSequence:build_test_report_data(mock_device, 1, clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.supportedFanModes({ + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.low.NAME, + capabilities.fanMode.fanMode.high.NAME + }, {visibility={displayed=false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.FanControl.attributes.FanModeSequence:build_test_report_data(mock_device, 1, clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.supportedFanModes({ + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.low.NAME, + capabilities.fanMode.fanMode.medium.NAME, + capabilities.fanMode.fanMode.high.NAME, + capabilities.fanMode.fanMode.auto.NAME + }, {visibility={displayed=false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.FanControl.attributes.FanModeSequence:build_test_report_data(mock_device, 1, clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH_AUTO) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.supportedFanModes({ + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.low.NAME, + capabilities.fanMode.fanMode.high.NAME, + capabilities.fanMode.fanMode.auto.NAME + }, {visibility={displayed=false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.FanControl.attributes.FanModeSequence:build_test_report_data(mock_device, 1, clusters.FanControl.attributes.FanModeSequence.OFF_HIGH_AUTO) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.supportedFanModes({ + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.high.NAME, + capabilities.fanMode.fanMode.auto.NAME + }, {visibility={displayed=false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.FanControl.attributes.FanModeSequence:build_test_report_data(mock_device, 1, clusters.FanControl.attributes.FanModeSequence.OFF_HIGH) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.supportedFanModes({ + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.high.NAME + }, {visibility={displayed=false}})) + } + } +) + +test.register_message_test( + "Test wind mode matter handler", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.FanControl.attributes.WindSupport:build_test_report_data(mock_device, 1, 0x02) -- NoWind and NaturalWind + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windMode.supportedWindModes({ + capabilities.windMode.windMode.noWind.NAME, + capabilities.windMode.windMode.naturalWind.NAME + }, {visibility={displayed=false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.FanControl.attributes.WindSetting:build_test_report_data(mock_device, 1, clusters.FanControl.types.WindSettingMask.NATURAL_WIND) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windMode.windMode.naturalWind()) + } + } +) + +test.register_message_test( + "Test wind mode capability handler", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "windMode", component = "main", command = "setWindMode", args = { "sleepWind" } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.FanControl.attributes.WindSetting:write(mock_device, 1, clusters.FanControl.types.WindSettingMask.SLEEP_WIND) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "windMode", component = "main", command = "setWindMode", args = { "naturalWind" } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.FanControl.attributes.WindSetting:write(mock_device, 1, clusters.FanControl.types.WindSettingMask.NATURAL_WIND) + } + } + } +) + +test.register_message_test( + "Test HEPA filter handlers", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.HepaFilterMonitoring.attributes.Condition:build_test_report_data(mock_device, 1, 3) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("hepaFilter", capabilities.filterState.filterLifeRemaining(3)) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.HepaFilterMonitoring.attributes.ChangeIndication:build_test_report_data(mock_device, 1, clusters.HepaFilterMonitoring.attributes.ChangeIndication.OK) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("hepaFilter", capabilities.filterStatus.filterStatus.normal()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.HepaFilterMonitoring.attributes.ChangeIndication:build_test_report_data(mock_device, 1, clusters.HepaFilterMonitoring.attributes.ChangeIndication.WARNING) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("hepaFilter", capabilities.filterStatus.filterStatus.normal()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.HepaFilterMonitoring.attributes.ChangeIndication:build_test_report_data(mock_device, 1, clusters.HepaFilterMonitoring.attributes.ChangeIndication.CRITICAL) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("hepaFilter", capabilities.filterStatus.filterStatus.replace()) + }, + } +) + +test.register_message_test( + "Test Activated Carbon filter handlers", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ActivatedCarbonFilterMonitoring.attributes.Condition:build_test_report_data(mock_device, 1, 5) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("activatedCarbonFilter", capabilities.filterState.filterLifeRemaining(5)) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication:build_test_report_data(mock_device, 1, clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.OK) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("activatedCarbonFilter", capabilities.filterStatus.filterStatus.normal()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication:build_test_report_data(mock_device, 1, clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.WARNING) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("activatedCarbonFilter", capabilities.filterStatus.filterStatus.normal()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication:build_test_report_data(mock_device, 1, clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.CRITICAL) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("activatedCarbonFilter", capabilities.filterStatus.filterStatus.replace()) + }, + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua b/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua new file mode 100644 index 0000000000..0f364e4994 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua @@ -0,0 +1,569 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" + +clusters.OperationalState = require "OperationalState" +clusters.MicrowaveOvenControl = require "MicrowaveOvenControl" +clusters.MicrowaveOvenMode = require "MicrowaveOvenMode" + +local APPLICATION_ENDPOINT = 1 + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("microwave-oven.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.OperationalState.ID, cluster_type = "SERVER" }, + { cluster_id = clusters.MicrowaveOvenControl.ID, cluster_type = "SERVER" }, + { cluster_id = clusters.MicrowaveOvenMode.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0079, device_type_revision = 1 } -- Microwave Oven + } + } + } +}) + +local function test_init() + local cluster_subscribe_list = { + clusters.OperationalState.attributes.OperationalState, + clusters.OperationalState.attributes.OperationalError, + clusters.OperationalState.attributes.AcceptedCommandList, + clusters.MicrowaveOvenMode.attributes.SupportedModes, + clusters.MicrowaveOvenMode.attributes.CurrentMode, + clusters.MicrowaveOvenControl.attributes.MaxCookTime, + clusters.MicrowaveOvenControl.attributes.CookTime + } + test.socket.matter:__set_channel_ordering("relaxed") + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) + test.socket.matter:__expect_send({ mock_device.id, clusters.MicrowaveOvenControl.attributes.MaxCookTime:read( + mock_device, APPLICATION_ENDPOINT) }) + test.mock_device.add_test_device(mock_device) +end + +local function init_supported_microwave_oven_modes() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.MicrowaveOvenMode.attributes.SupportedModes:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + { + clusters.MicrowaveOvenMode.types.ModeOptionStruct({ + ["label"] = "Grill", + ["mode"] = 0, + ["mode_tags"] = { + clusters.MicrowaveOvenMode.types.ModeTagStruct({ ["mfg_code"] = 256, ["value"] = 0 }) + } + }), + clusters.MicrowaveOvenMode.types.ModeOptionStruct({ + ["label"] = "Pre Heat", + ["mode"] = 1, + ["mode_tags"] = { + clusters.MicrowaveOvenMode.types.ModeTagStruct({ ["mfg_code"] = 256, ["value"] = 1 }) + } + }) + } + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.mode.supportedModes({ "Grill", "Pre Heat" }, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.mode.supportedArguments({ "Grill", "Pre Heat" }, {visibility={displayed=false}}))) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "Setting operationalState command to 'start' should send appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "operationalState", component = "main", command = "start", args = {} } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.server.commands.Start(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalState:read(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:read(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalState:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.OperationalStateEnum.RUNNING) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.operationalState.operationalState.running()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.mode.supportedModes({},{visibility={displayed=false}})) + -- Prevent user from changing modes in between an operation. + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.mode.supportedArguments({},{visibility={displayed=false}})) + -- Prevent user from changing modes in between an operation. + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.ErrorStateStruct({ + ["error_state_id"] = clusters.OperationalState.types.ErrorStateEnum.NO_ERROR, + ["error_state_label"] = "", + ["error_state_details"] = "" + })) + } + }, -- on receiving NO ERROR we don't do anything. + } +) + +test.register_message_test( + "Setting operationalState command to 'stop' should send appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "operationalState", component = "main", command = "stop", args = {} } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.server.commands.Stop(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalState:read(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:read(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalState:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.OperationalStateEnum.STOPPED) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.operationalState.operationalState.stopped()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.mode.supportedModes({ "Grill", "Pre Heat" },{visibility={displayed=false}})) + --When operation is stopped, enable mode options for user to choose. + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.mode.supportedArguments({ "Grill", "Pre Heat" },{visibility={displayed=false}})) + --When operation is stopped, enable mode options for user to choose. + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.ErrorStateStruct({ + ["error_state_id"] = clusters.OperationalState.types.ErrorStateEnum.NO_ERROR, + ["error_state_label"] = "", + ["error_state_details"] = "" + })) + } + }, -- on receiving NO ERROR we don't do anything. + }, + { + test_init = function() + test_init() + init_supported_microwave_oven_modes() + end + } +) + +test.register_message_test( + "Setting operationalState command to 'pause' should send appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "operationalState", component = "main", command = "pause", args = {} } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.server.commands.Pause(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalState:read(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:read(mock_device, APPLICATION_ENDPOINT) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalState:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.OperationalStateEnum.PAUSED) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.operationalState.operationalState.paused()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.mode.supportedModes({},{visibility={displayed=false}})) + -- Prevent user from changing modes in between an operation. + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.mode.supportedArguments({},{visibility={displayed=false}})) + -- Prevent user from changing modes in between an operation. + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.ErrorStateStruct({ + ["error_state_id"] = clusters.OperationalState.types.ErrorStateEnum.NO_ERROR, + ["error_state_label"] = "", + ["error_state_details"] = "" + })) + } + }, -- on receiving NO ERROR we don't do anything. + } +) + +test.register_message_test( + "On receiving OperationalError, the appropriate operationalState event must be emitted", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.ErrorStateStruct({ + ["error_state_id"] = clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_START_OR_RESUME, + ["error_state_label"] = "", + ["error_state_details"] = "" + })) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.operationalState.operationalState.unableToStartOrResume()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.mode.supportedModes({},{visibility={displayed=false}})) + -- Prevent user from changing mode in event of error. + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.mode.supportedArguments({},{visibility={displayed=false}})) + -- Prevent user from changing mode in event of error. + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.ErrorStateStruct({ + ["error_state_id"] = clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_COMPLETE_OPERATION, + ["error_state_label"] = "", + ["error_state_details"] = "" + })) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.operationalState.operationalState.unableToCompleteOperation()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.mode.supportedModes({},{visibility={displayed=false}})) + -- Prevent user from changing mode in event of error. + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.mode.supportedArguments({},{visibility={displayed=false}})) + -- Prevent user from changing mode in event of error. + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.ErrorStateStruct({ + ["error_state_id"] = clusters.OperationalState.types.ErrorStateEnum.COMMAND_INVALID_IN_STATE, + ["error_state_label"] = "", + ["error_state_details"] = "" + })) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.operationalState.operationalState.commandInvalidInCurrentState()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.mode.supportedModes({},{visibility={displayed=false}})) + -- Prevent user from changing mode in event of error. + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.mode.supportedArguments({},{visibility={displayed=false}})) + -- Prevent user from changing mode in event of error. + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OperationalState.attributes.OperationalError:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.OperationalState.types.ErrorStateStruct({ + ["error_state_id"] = clusters.OperationalState.types.ErrorStateEnum.NO_ERROR, + ["error_state_label"] = "", + ["error_state_details"] = "" + })) + } + }, -- on receiving NO ERROR we don't do anything. + } +) + +test.register_message_test( + "The cookTimeRange value should be set on receiving MaxCookTime", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.MicrowaveOvenControl.attributes.MaxCookTime:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + 900) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.cookTime.cookTimeRange({ + minimum = 1, --minimum should be 1. + maximum = 900 + },{visibility={displayed=false}})) + }, + } +) + +test.register_message_test( + "This test case checks for the following events:\n1. Report cookTime value of 30 seconds.\n2. MicrowaveOven supportedModes must be registered.\n3. Setting oven mode and cookTime should send appropriate commands", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.MicrowaveOvenControl.attributes.CookTime:build_test_report_data(mock_device, APPLICATION_ENDPOINT, 30) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.cookTime.cookTime(30)) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.MicrowaveOvenMode.attributes.SupportedModes:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + { + clusters.MicrowaveOvenMode.types.ModeOptionStruct({ + ["label"] = "Grill", + ["mode"] = 0, + ["mode_tags"] = { + clusters.MicrowaveOvenMode.types.ModeTagStruct({ ["mfg_code"] = 256, ["value"] = 0 }) + } + }), + clusters.MicrowaveOvenMode.types.ModeOptionStruct({ + ["label"] = "Pre Heat", + ["mode"] = 1, + ["mode_tags"] = { + clusters.MicrowaveOvenMode.types.ModeTagStruct({ ["mfg_code"] = 256, ["value"] = 1 }) + } + }) + } + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.mode.supportedModes({ "Grill", "Pre Heat" }, {visibility={displayed=false}})) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.mode.supportedArguments({ "Grill", "Pre Heat" }, {visibility={displayed=false}})) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "mode", component = "main", command = "setMode", args = { "Grill" } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.MicrowaveOvenControl.commands.SetCookingParameters(mock_device, APPLICATION_ENDPOINT, + 0, --Index where Grill is stored + 30) --30 since that was the last received cookTime. + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "cookTime", component = "main", command = "setCookTime", args = { 300 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.MicrowaveOvenControl.commands.SetCookingParameters(mock_device, APPLICATION_ENDPOINT, + 0, --> Grill, as this was the last set microwave oven mode. + 300) + } + }, + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-appliance/src/test/test_oven.lua b/drivers/SmartThings/matter-appliance/src/test/test_oven.lua new file mode 100644 index 0000000000..e57e149c68 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/test/test_oven.lua @@ -0,0 +1,528 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" + +local OVEN_ENDPOINT = 1 +local OVEN_TCC_ONE_ENDPOINT = 2 +local OVEN_TCC_TWO_ENDPOINT = 3 +local COOK_TOP_ENDPOINT = 4 +local COOK_SURFACE_ONE_ENDPOINT = 5 +local COOK_SURFACE_TWO_ENDPOINT = 6 + +clusters.OvenMode = require "OvenMode" +clusters.TemperatureControl = require "TemperatureControl" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("oven-cabinet-one-tn-cabinet-two-tl-cook-top-cook-surface-one-tl-cook-surface-two-tl.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 = OVEN_ENDPOINT, + clusters = {}, + device_types = { + { device_type_id = 0x007B, device_type_revision = 1 } -- Oven + } + }, + { + endpoint_id = OVEN_TCC_ONE_ENDPOINT, + clusters = { + { cluster_id = clusters.OvenMode.ID, cluster_type = "SERVER" }, + { cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER" }, + { cluster_id = clusters.TemperatureControl.ID, cluster_type = "SERVER", feature_map = 1 }, --Temperature Number + }, + device_types = { + { device_type_id = 0x0071, device_type_revision = 1 } -- Oven TCC + } + }, + { + endpoint_id = OVEN_TCC_TWO_ENDPOINT, + clusters = { + { cluster_id = clusters.OvenMode.ID, cluster_type = "SERVER" }, + { cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER" }, + { cluster_id = clusters.TemperatureControl.ID, cluster_type = "SERVER", feature_map = 2 }, --Temperature Level + }, + device_types = { + { device_type_id = 0x0071, device_type_revision = 1 } -- Oven TCC + } + }, + { + endpoint_id = COOK_TOP_ENDPOINT, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 4 }, --OffOnly feature + }, + device_types = { + { device_type_id = 0x0078, device_type_revision = 1 } -- Cook Top + } + }, + { + endpoint_id = COOK_SURFACE_ONE_ENDPOINT, + clusters = { + { cluster_id = clusters.TemperatureControl.ID, cluster_type = "SERVER", feature_map = 2 }, + { cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0077, device_type_revision = 1 } -- Cook Surface + } + }, + { + endpoint_id = COOK_SURFACE_TWO_ENDPOINT, + clusters = { + { cluster_id = clusters.TemperatureControl.ID, cluster_type = "SERVER", feature_map = 2 }, + { cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0077, device_type_revision = 1 } -- Cook Surface + } + } + } +}) + +local function test_init() + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureControl.attributes.TemperatureSetpoint, + clusters.TemperatureControl.attributes.MaxTemperature, + clusters.TemperatureControl.attributes.MinTemperature, + clusters.TemperatureControl.attributes.SelectedTemperatureLevel, + clusters.TemperatureControl.attributes.SupportedTemperatureLevels, + clusters.OvenMode.attributes.CurrentMode, + clusters.OvenMode.attributes.SupportedModes, + } + test.socket.matter:__set_channel_ordering("relaxed") + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.set_rpc_version(5) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Assert component to endpoint map", + function() + local component_to_endpoint_map = mock_device:get_field("__component_to_endpoint_map") + assert(component_to_endpoint_map["tccOne"] == OVEN_TCC_ONE_ENDPOINT, "Oven TCC One Endpoint must be 2") + assert(component_to_endpoint_map["tccTwo"] == OVEN_TCC_TWO_ENDPOINT, "Oven TCC Two Endpoint must be 3") + assert(component_to_endpoint_map["cookTop"] == COOK_TOP_ENDPOINT, "Cook Top Endpoint must be 4") + assert(component_to_endpoint_map["cookSurfaceOne"] == COOK_SURFACE_ONE_ENDPOINT, + "Cook Surface One Endpoint must be 5") + assert(component_to_endpoint_map["cookSurfaceTwo"] == COOK_SURFACE_TWO_ENDPOINT, + "Cook Surface Two Endpoint must be 6") + end +) + + +test.register_message_test( + "Oven TCC One: This test case checks for the following events:\n1. Oven supportedModes must be registered.\n2. Setting Oven mode should send appropriate commands", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OvenMode.attributes.SupportedModes:build_test_report_data(mock_device, OVEN_TCC_ONE_ENDPOINT, + { + clusters.OvenMode.types.ModeOptionStruct({ + ["label"] = "Grill", + ["mode"] = 0, + ["mode_tags"] = { + clusters.OvenMode.types.ModeTagStruct({ ["mfg_code"] = 256, ["value"] = 0 }) + } + }), + clusters.OvenMode.types.ModeOptionStruct({ + ["label"] = "Pre Heat", + ["mode"] = 1, + ["mode_tags"] = { + clusters.OvenMode.types.ModeTagStruct({ ["mfg_code"] = 256, ["value"] = 1 }) + } + }) + } + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("tccOne", + capabilities.mode.supportedModes({ "Grill", "Pre Heat" }, { visibility = { displayed = false } })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("tccOne", + capabilities.mode.supportedArguments({ "Grill", "Pre Heat" }, { visibility = { displayed = false } })) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "mode", component = "tccOne", command = "setMode", args = { "Grill" } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OvenMode.commands.ChangeToMode(mock_device, OVEN_TCC_ONE_ENDPOINT, 0) --Index where Grill is stored) + } + } + } +) + +test.register_message_test( + "First Oven TCC: MeasuredValue of TemperatureMeasurement clusters should be reported correctly.", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, OVEN_TCC_ONE_ENDPOINT, 40*100) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("tccOne", capabilities.temperatureMeasurement.temperature({ value = 40.0, unit = "C" })) + } + } +) + +test.register_message_test( + "First Oven TCC: Verify temperatureSetpoint command sends the appropriate commands.", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureControl.attributes.MinTemperature:build_test_report_data(mock_device, OVEN_TCC_ONE_ENDPOINT, 12800) --128*C + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureControl.attributes.MaxTemperature:build_test_report_data(mock_device, OVEN_TCC_ONE_ENDPOINT, 20000) --200*C + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureControl.attributes.TemperatureSetpoint:build_test_report_data(mock_device, OVEN_TCC_ONE_ENDPOINT, 13000) --130*C + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("tccOne", capabilities.temperatureSetpoint.temperatureSetpointRange({value = {minimum=128.0,maximum=200.0}, unit = "C"}, {visibility = {displayed = false}})) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("tccOne", capabilities.temperatureSetpoint.temperatureSetpoint({value = 130.0, unit = "C"})) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "temperatureSetpoint", component = "tccOne", command = "setTemperatureSetpoint", args = {130.0}} + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.TemperatureControl.commands.SetTemperature(mock_device, OVEN_TCC_ONE_ENDPOINT, 130 * 100, nil) + } + }, + } +) + +test.register_message_test( + "Oven TCC Two: This test case checks for the following events:\n1. Oven supportedModes must be registered.\n2. Setting Oven mode should send appropriate commands", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OvenMode.attributes.SupportedModes:build_test_report_data(mock_device, OVEN_TCC_TWO_ENDPOINT, + { + clusters.OvenMode.types.ModeOptionStruct({ + ["label"] = "Grill", + ["mode"] = 0, + ["mode_tags"] = { + clusters.OvenMode.types.ModeTagStruct({ ["mfg_code"] = 256, ["value"] = 0 }) + } + }), + clusters.OvenMode.types.ModeOptionStruct({ + ["label"] = "Pre Heat", + ["mode"] = 1, + ["mode_tags"] = { + clusters.OvenMode.types.ModeTagStruct({ ["mfg_code"] = 256, ["value"] = 1 }) + } + }) + } + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("tccTwo", + capabilities.mode.supportedModes({ "Grill", "Pre Heat" }, { visibility = { displayed = false } })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("tccTwo", + capabilities.mode.supportedArguments({ "Grill", "Pre Heat" }, { visibility = { displayed = false } })) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "mode", component = "tccTwo", command = "setMode", args = { "Pre Heat" } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OvenMode.commands.ChangeToMode(mock_device, OVEN_TCC_TWO_ENDPOINT, 1) --Index where Pre Heat is stored + } + } + } +) + +test.register_message_test( + "Oven TCC Two: MeasuredValue of TemperatureMeasurement clusters should be reported correctly.", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, OVEN_TCC_TWO_ENDPOINT, 50*100) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("tccTwo", capabilities.temperatureMeasurement.temperature({ value = 50.0, unit = "C" })) + } + } +) + +local utf1 = require "st.matter.data_types.UTF8String1" + +test.register_message_test( + "Second Oven TCC: TemperatureControl Supported Levels must be registered and setTemperatureLevel level command should send appropriate command", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureControl.attributes.SupportedTemperatureLevels:build_test_report_data(mock_device, OVEN_TCC_TWO_ENDPOINT, {utf1("Level 1"), utf1("Level 2"), utf1("Level 3")}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("tccTwo", capabilities.temperatureLevel.supportedTemperatureLevels({"Level 1", "Level 2", "Level 3"}, {visibility = {displayed = false}})) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "temperatureLevel", component = "tccTwo", command = "setTemperatureLevel", args = {"Level 1"}} + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.TemperatureControl.server.commands.SetTemperature(mock_device, OVEN_TCC_TWO_ENDPOINT, nil, 0) --0 is the index where Level1 is stored. + } + }, + } +) + +test.register_message_test( + "Cook Top: Off command should send appropriate commands", + -- we do not test "on" command, as cook-top is supposed to have offOnly feature. + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "cookTop", command = "off", args = {} } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.Off(mock_device, COOK_TOP_ENDPOINT) + } + } + } +) + +test.register_message_test( + "Cook Surface One: TemperatureControl Supported Levels must be registered and setTemperatureLevel level command should send appropriate command", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureControl.attributes.SupportedTemperatureLevels:build_test_report_data(mock_device, COOK_SURFACE_ONE_ENDPOINT, {utf1("Level 2"), utf1("Level 4"), utf1("Level 5")}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("cookSurfaceOne", capabilities.temperatureLevel.supportedTemperatureLevels({"Level 2", "Level 4", "Level 5"}, {visibility = {displayed = false}})) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "temperatureLevel", component = "cookSurfaceOne", command = "setTemperatureLevel", args = {"Level 5"}} + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.TemperatureControl.server.commands.SetTemperature(mock_device, COOK_SURFACE_ONE_ENDPOINT, nil, 2) -- 2 is the index where Level 5 is stored. + } + }, + } +) + +test.register_message_test( + "Cook Surface Two: TemperatureControl Supported Levels must be registered and setTemperatureLevel level command should send appropriate command", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureControl.attributes.SupportedTemperatureLevels:build_test_report_data(mock_device, COOK_SURFACE_TWO_ENDPOINT, {utf1("Level 3"), utf1("Level 4"), utf1("Level 5")}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("cookSurfaceTwo", capabilities.temperatureLevel.supportedTemperatureLevels({"Level 3", "Level 4", "Level 5"}, {visibility = {displayed = false}})) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "temperatureLevel", component = "cookSurfaceTwo", command = "setTemperatureLevel", args = {"Level 4"}} + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.TemperatureControl.server.commands.SetTemperature(mock_device, COOK_SURFACE_TWO_ENDPOINT, nil, 1) -- 1 is the index where Level 4 is stored. + } + }, + } +) + +test.register_message_test( + "Cook Surface One: MeasuredValue of TemperatureMeasurement clusters should be reported correctly.", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, COOK_SURFACE_ONE_ENDPOINT, 40*100) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("cookSurfaceOne", capabilities.temperatureMeasurement.temperature({ value = 40.0, unit = "C" })) + } + } +) + +test.register_message_test( + "Cook Surface Two: MeasuredValue of TemperatureMeasurement clusters should be reported correctly.", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, COOK_SURFACE_TWO_ENDPOINT, 20*100) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("cookSurfaceTwo", capabilities.temperatureMeasurement.temperature({ value = 20.0, unit = "C" })) + } + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-evse/config.yml b/drivers/SmartThings/matter-evse/config.yml new file mode 100644 index 0000000000..611e385a09 --- /dev/null +++ b/drivers/SmartThings/matter-evse/config.yml @@ -0,0 +1,6 @@ +name: 'Matter Evse' +packageKey: 'matter-evse' +permissions: + matter: {} +description: "SmartThings driver for Matter EVSE devices" +vendorSupportInformation: "https://support.smartthings.com" diff --git a/drivers/SmartThings/matter-evse/fingerprints.yml b/drivers/SmartThings/matter-evse/fingerprints.yml new file mode 100644 index 0000000000..6da5bcfe7c --- /dev/null +++ b/drivers/SmartThings/matter-evse/fingerprints.yml @@ -0,0 +1,7 @@ +matterGeneric: + - id: "matter/evse" + deviceLabel: Matter EVSE + deviceTypes: + - id: 0x0510 + - id: 0x050C + deviceProfileName: evse diff --git a/drivers/SmartThings/matter-evse/profiles/evse-energy-meas-energy-mgmt-mode.yml b/drivers/SmartThings/matter-evse/profiles/evse-energy-meas-energy-mgmt-mode.yml new file mode 100644 index 0000000000..8270dd9b3a --- /dev/null +++ b/drivers/SmartThings/matter-evse/profiles/evse-energy-meas-energy-mgmt-mode.yml @@ -0,0 +1,27 @@ +#EVSE with Electrical Energy Measurement Cluster in Electrical Sensor DT and Device Energy Manage Mode in Energy Management DT +name: evse-energy-meas-energy-mgmt-mode +components: +- id: main + capabilities: + - id: evseState + version: 1 + - id: evseChargingSession + version: 1 + - id: mode + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: ElectricVehicleCharger +- id: deviceEnergyManagement + label: Energy Manager + capabilities: + - id: mode + version: 1 +metadata: + mnmn: SmartThingsEdge + vid: generic-evse diff --git a/drivers/SmartThings/matter-evse/profiles/evse-energy-meas-power-meas-energy-mgmt-mode.yml b/drivers/SmartThings/matter-evse/profiles/evse-energy-meas-power-meas-energy-mgmt-mode.yml new file mode 100644 index 0000000000..49a6ce9814 --- /dev/null +++ b/drivers/SmartThings/matter-evse/profiles/evse-energy-meas-power-meas-energy-mgmt-mode.yml @@ -0,0 +1,32 @@ +#EVSE with Electrical Power Measurement & Electrical Energy Measurement Cluster in Electrical Sensor DT and Device Energy Management mode in Energy Management DT +name: evse-energy-meas-power-meas-energy-mgmt-mode +components: +- id: main + capabilities: + - id: evseState + version: 1 + - id: evseChargingSession + version: 1 + - id: mode + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: ElectricVehicleCharger +- id: electricalSensor + label: Electrical Sensor + capabilities: + - id: powerSource + version: 1 +- id: deviceEnergyManagement + label: Energy Manager + capabilities: + - id: mode + version: 1 +metadata: + mnmn: SmartThingsEdge + vid: generic-evse diff --git a/drivers/SmartThings/matter-evse/profiles/evse-energy-meas.yml b/drivers/SmartThings/matter-evse/profiles/evse-energy-meas.yml new file mode 100644 index 0000000000..3cb332168c --- /dev/null +++ b/drivers/SmartThings/matter-evse/profiles/evse-energy-meas.yml @@ -0,0 +1,22 @@ +#EVSE with Electrical Energy Measurement Cluster in Electrical Sensor +name: evse-energy-meas +components: +- id: main + capabilities: + - id: evseState + version: 1 + - id: evseChargingSession + version: 1 + - id: mode + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: ElectricVehicleCharger +metadata: + mnmn: SmartThingsEdge + vid: generic-evse diff --git a/drivers/SmartThings/matter-evse/profiles/evse-power-meas-energy-mgmt-mode.yml b/drivers/SmartThings/matter-evse/profiles/evse-power-meas-energy-mgmt-mode.yml new file mode 100644 index 0000000000..e389346d82 --- /dev/null +++ b/drivers/SmartThings/matter-evse/profiles/evse-power-meas-energy-mgmt-mode.yml @@ -0,0 +1,30 @@ +#EVSE with Electrical Power Measurement Cluster in Electrical Sensor DT and Device Energy Management mode in Energy Management DT +name: evse-power-meas-energy-mgmt-mode +components: +- id: main + capabilities: + - id: evseState + version: 1 + - id: evseChargingSession + version: 1 + - id: mode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: ElectricVehicleCharger +- id: electricalSensor + label: Electrical Sensor + capabilities: + - id: powerSource + version: 1 +- id: deviceEnergyManagement + label: Energy Manager + capabilities: + - id: mode + version: 1 +metadata: + mnmn: SmartThingsEdge + vid: generic-evse diff --git a/drivers/SmartThings/matter-evse/profiles/evse-power-meas.yml b/drivers/SmartThings/matter-evse/profiles/evse-power-meas.yml new file mode 100644 index 0000000000..9ec54d4140 --- /dev/null +++ b/drivers/SmartThings/matter-evse/profiles/evse-power-meas.yml @@ -0,0 +1,25 @@ +#EVSE with Electrical Power Measurement Cluster in Electrical Sensor DT +name: evse-power-meas +components: +- id: main + capabilities: + - id: evseState + version: 1 + - id: evseChargingSession + version: 1 + - id: mode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: ElectricVehicleCharger +- id: electricalSensor + label: Electrical Sensor + capabilities: + - id: powerSource + version: 1 +metadata: + mnmn: SmartThingsEdge + vid: generic-evse diff --git a/drivers/SmartThings/matter-evse/profiles/evse.yml b/drivers/SmartThings/matter-evse/profiles/evse.yml new file mode 100644 index 0000000000..e20aed42c9 --- /dev/null +++ b/drivers/SmartThings/matter-evse/profiles/evse.yml @@ -0,0 +1,19 @@ +name: evse +components: +- id: main + capabilities: + - id: evseState + version: 1 + - id: evseChargingSession + version: 1 + - id: mode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: ElectricVehicleCharger +metadata: + mnmn: SmartThingsEdge + vid: generic-evse diff --git a/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/init.lua b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/init.lua new file mode 100644 index 0000000000..2893282bb2 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/init.lua @@ -0,0 +1,86 @@ +local cluster_base = require "st.matter.cluster_base" +local DeviceEnergyManagementModeServerAttributes = require "DeviceEnergyManagementMode.server.attributes" +local DeviceEnergyManagementModeServerCommands = require "DeviceEnergyManagementMode.server.commands" +local DeviceEnergyManagementModeTypes = require "DeviceEnergyManagementMode.types" + + +local DeviceEnergyManagementMode = {} + +DeviceEnergyManagementMode.ID = 0x009F +DeviceEnergyManagementMode.NAME = "DeviceEnergyManagementMode" +DeviceEnergyManagementMode.server = {} +DeviceEnergyManagementMode.server.attributes = DeviceEnergyManagementModeServerAttributes:set_parent_cluster(DeviceEnergyManagementMode) +DeviceEnergyManagementMode.server.commands = DeviceEnergyManagementModeServerCommands:set_parent_cluster(DeviceEnergyManagementMode) +DeviceEnergyManagementMode.types = DeviceEnergyManagementModeTypes +DeviceEnergyManagementMode.FeatureMap = DeviceEnergyManagementMode.types.Feature + +function DeviceEnergyManagementMode.are_features_supported(feature, feature_map) + if (DeviceEnergyManagementMode.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +function DeviceEnergyManagementMode:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "SupportedModes", + [0x0001] = "CurrentMode", + [0xFFF9] = "AcceptedCommandList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function DeviceEnergyManagementMode:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "ChangeToMode", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +-- Attribute Mapping +DeviceEnergyManagementMode.attribute_direction_map = { + ["SupportedModes"] = "server", + ["CurrentMode"] = "server", + ["AcceptedCommandList"] = "server", + ["AttributeList"] = "server", +} + +-- Command Mapping +DeviceEnergyManagementMode.command_direction_map = { + ["ChangeToMode"] = "server", +} + +-- Cluster Completion +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = DeviceEnergyManagementMode.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, DeviceEnergyManagementMode.NAME)) + end + return DeviceEnergyManagementMode[direction].attributes[key] +end +DeviceEnergyManagementMode.attributes = {} +setmetatable(DeviceEnergyManagementMode.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = DeviceEnergyManagementMode.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, DeviceEnergyManagementMode.NAME)) + end + return DeviceEnergyManagementMode[direction].commands[key] +end +DeviceEnergyManagementMode.commands = {} +setmetatable(DeviceEnergyManagementMode.commands, command_helper_mt) + +setmetatable(DeviceEnergyManagementMode, {__index = cluster_base}) + +return DeviceEnergyManagementMode \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/attributes/AcceptedCommandList.lua new file mode 100644 index 0000000000..ad407e23bf --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/attributes/AcceptedCommandList.lua @@ -0,0 +1,81 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + + +local AcceptedCommandList = { + ID = 0xFFF9, + NAME = "AcceptedCommandList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +AcceptedCommandList.enum_fields = {} + +function AcceptedCommandList:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function AcceptedCommandList.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, AcceptedCommandList.enum_fields[value_obj.value]) +end + +function AcceptedCommandList:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function AcceptedCommandList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AcceptedCommandList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AcceptedCommandList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value}) +return AcceptedCommandList + diff --git a/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/attributes/AttributeList.lua new file mode 100644 index 0000000000..a441e90e0b --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/attributes/AttributeList.lua @@ -0,0 +1,81 @@ + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + + +local AttributeList = { + ID = 0xFFFB, + NAME = "AttributeList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +AttributeList.enum_fields = {} + +function AttributeList:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function AttributeList.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, AttributeList.enum_fields[value_obj.value]) +end + +function AttributeList:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function AttributeList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AttributeList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AttributeList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AttributeList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AttributeList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(AttributeList, {__call = AttributeList.new_value}) +return AttributeList diff --git a/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/attributes/CurrentMode.lua b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/attributes/CurrentMode.lua new file mode 100644 index 0000000000..f2987e294d --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/attributes/CurrentMode.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local CurrentMode = { + ID = 0x0001, + NAME = "CurrentMode", + base_type = require "st.matter.data_types.Uint8", +} + +function CurrentMode:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function CurrentMode:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CurrentMode:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CurrentMode:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CurrentMode: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 CurrentMode:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(CurrentMode, {__call = CurrentMode.new_value, __index = CurrentMode.base_type}) +return CurrentMode diff --git a/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/attributes/SupportedModes.lua b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/attributes/SupportedModes.lua new file mode 100644 index 0000000000..69b7e2969a --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/attributes/SupportedModes.lua @@ -0,0 +1,74 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SupportedModes = { + ID = 0x0000, + NAME = "SupportedModes", + base_type = require "st.matter.data_types.Array", + element_type = require "DeviceEnergyManagementMode.types.ModeOptionStruct", +} + +function SupportedModes: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, SupportedModes.element_type) + end +end + +function SupportedModes:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function SupportedModes:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function SupportedModes:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function SupportedModes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SupportedModes:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function SupportedModes:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SupportedModes, {__call = SupportedModes.new_value, __index = SupportedModes.base_type}) +return SupportedModes diff --git a/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/attributes/init.lua b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/attributes/init.lua new file mode 100644 index 0000000000..e814310f7e --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/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("DeviceEnergyManagementMode.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 DeviceEnergyManagementModeServerAttributes = {} + +function DeviceEnergyManagementModeServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(DeviceEnergyManagementModeServerAttributes, attr_mt) + +return DeviceEnergyManagementModeServerAttributes + diff --git a/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/commands/ChangeToMode.lua b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/commands/ChangeToMode.lua new file mode 100644 index 0000000000..488443c91c --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/commands/ChangeToMode.lua @@ -0,0 +1,79 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ChangeToMode = {} + +ChangeToMode.NAME = "ChangeToMode" +ChangeToMode.ID = 0x0000 +ChangeToMode.field_defs = { + { + name = "new_mode", + field_id = 0, + optional = false, + nullable = false, + data_type = data_types.Uint8, + }, +} + +function ChangeToMode:init(device, endpoint_id, new_mode) + local out = {} + local args = {new_mode} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.optional and args[i] == nil then + out[v.name] = nil + elseif v.nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = ChangeToMode, + __tostring = ChangeToMode.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function ChangeToMode:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ChangeToMode:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + end + end + end + base_type_obj.elements = elems +end + +function ChangeToMode:deserialize(tlv_buf) + return TLVParser.decode_tlv(tlv_buf) +end + +setmetatable(ChangeToMode, {__call = ChangeToMode.init}) + +return ChangeToMode \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/commands/init.lua b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/commands/init.lua new file mode 100644 index 0000000000..0ab178d9aa --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/server/commands/init.lua @@ -0,0 +1,23 @@ +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("DeviceEnergyManagementMode.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local DeviceEnergyManagementModeServerCommands = {} + +function DeviceEnergyManagementModeServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(DeviceEnergyManagementModeServerCommands, command_mt) + +return DeviceEnergyManagementModeServerCommands + diff --git a/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/types/Feature.lua b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/types/Feature.lua new file mode 100644 index 0000000000..4feed93356 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/types/Feature.lua @@ -0,0 +1,54 @@ +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.ON_OFF = 0x0001 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + ON_OFF = 0x0001, +} + +Feature.is_on_off_set = function(self) + return (self.value & self.ON_OFF) ~= 0 +end + +Feature.set_on_off = function(self) + if self.value ~= nil then + self.value = self.value | self.ON_OFF + else + self.value = self.ON_OFF + end +end + +Feature.unset_on_off = function(self) + self.value = self.value & (~self.ON_OFF & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.ON_OFF + + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_on_off_set = Feature.is_on_off_set, + set_on_off = Feature.set_on_off, + unset_on_off = Feature.unset_on_off, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature diff --git a/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/types/ModeOptionStruct.lua b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/types/ModeOptionStruct.lua new file mode 100644 index 0000000000..caa7734d7c --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/types/ModeOptionStruct.lua @@ -0,0 +1,85 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local ModeOptionStruct = {} +local new_mt = StructureABC.new_mt({NAME = "ModeOptionStruct", ID = data_types.name_to_id_map["Structure"]}) + +ModeOptionStruct.field_defs = { + { + name = "label", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.UTF8String1", + }, + { + name = "mode", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint8", + }, + { + name = "mode_tags", + field_id = 2, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Array", + element_type = require "DeviceEnergyManagementMode.types.ModeTagStruct", + }, +} + +ModeOptionStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +ModeOptionStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = ModeOptionStruct.init +new_mt.__index.serialize = ModeOptionStruct.serialize + +ModeOptionStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(ModeOptionStruct, new_mt) + +return ModeOptionStruct diff --git a/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/types/ModeTag.lua b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/types/ModeTag.lua new file mode 100644 index 0000000000..1f6d83578a --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/types/ModeTag.lua @@ -0,0 +1,33 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local ModeTag = {} +local new_mt = UintABC.new_mt({NAME = "ModeTag", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.NO_OPTIMIZATION] = "NO_OPTIMIZATION", + [self.DEVICE_OPTIMIZATION] = "DEVICE_OPTIMIZATION", + [self.LOCAL_OPTIMIZATION] = "LOCAL_OPTIMIZATION", + [self.GRID_OPTIMIZATION] = "GRID_OPTIMIZATION", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.NO_OPTIMIZATION = 0x4000 +new_mt.__index.DEVICE_OPTIMIZATION = 0x4001 +new_mt.__index.LOCAL_OPTIMIZATION = 0x4002 +new_mt.__index.GRID_OPTIMIZATION = 0x4003 + +ModeTag.NO_OPTIMIZATION = 0x4000 +ModeTag.DEVICE_OPTIMIZATION = 0x4001 +ModeTag.LOCAL_OPTIMIZATION = 0x4002 +ModeTag.GRID_OPTIMIZATION = 0x4003 + +ModeTag.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(ModeTag, new_mt) + +return ModeTag diff --git a/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/types/ModeTagStruct.lua b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/types/ModeTagStruct.lua new file mode 100644 index 0000000000..610fb90ad7 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/types/ModeTagStruct.lua @@ -0,0 +1,77 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local ModeTagStruct = {} +local new_mt = StructureABC.new_mt({NAME = "ModeTagStruct", ID = data_types.name_to_id_map["Structure"]}) + +ModeTagStruct.field_defs = { + { + name = "mfg_code", + field_id = 0, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "value", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint16", + }, +} + +ModeTagStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +ModeTagStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = ModeTagStruct.init +new_mt.__index.serialize = ModeTagStruct.serialize + +ModeTagStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(ModeTagStruct, new_mt) + +return ModeTagStruct diff --git a/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/types/init.lua b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/types/init.lua new file mode 100644 index 0000000000..5f2578800c --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/DeviceEnergyManagementMode/types/init.lua @@ -0,0 +1,17 @@ +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + local req_loc = string.format("DeviceEnergyManagementMode.types.%s", key) + local cluster_type = require(req_loc) + types_mt.__types_cache[key] = cluster_type + end + return types_mt.__types_cache[key] +end + +local DeviceEnergyManagementModeTypes = {} + +setmetatable(DeviceEnergyManagementModeTypes, types_mt) + +return DeviceEnergyManagementModeTypes + diff --git a/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/init.lua b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/init.lua new file mode 100644 index 0000000000..e50259f69c --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/init.lua @@ -0,0 +1,60 @@ +local cluster_base = require "st.matter.cluster_base" +local ElectricalEnergyMeasurementServerAttributes = require "ElectricalEnergyMeasurement.server.attributes" +local ElectricalEnergyMeasurementTypes = require "ElectricalEnergyMeasurement.types" + +local ElectricalEnergyMeasurement = {} + +ElectricalEnergyMeasurement.ID = 0x0091 +ElectricalEnergyMeasurement.NAME = "ElectricalEnergyMeasurement" +ElectricalEnergyMeasurement.server = {} +ElectricalEnergyMeasurement.server.attributes = ElectricalEnergyMeasurementServerAttributes:set_parent_cluster(ElectricalEnergyMeasurement) +ElectricalEnergyMeasurement.types = ElectricalEnergyMeasurementTypes +ElectricalEnergyMeasurement.FeatureMap = ElectricalEnergyMeasurement.types.Feature + +function ElectricalEnergyMeasurement.are_features_supported(feature, feature_map) + if (ElectricalEnergyMeasurement.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +function ElectricalEnergyMeasurement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0002] = "CumulativeEnergyExported", + [0x0004] = "PeriodicEnergyExported", + [0xFFF9] = "AcceptedCommandList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +-- Attribute Mapping +ElectricalEnergyMeasurement.attribute_direction_map = { + ["CumulativeEnergyImported"] = "server", + ["PeriodicEnergyImported"] = "server", + ["AcceptedCommandList"] = "server", + ["AttributeList"] = "server", +} + +-- Command Mapping +ElectricalEnergyMeasurement.command_direction_map = {} + +-- Cluster Completion +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ElectricalEnergyMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ElectricalEnergyMeasurement.NAME)) + end + return ElectricalEnergyMeasurement[direction].attributes[key] +end +ElectricalEnergyMeasurement.attributes = {} +setmetatable(ElectricalEnergyMeasurement.attributes, attribute_helper_mt) + +setmetatable(ElectricalEnergyMeasurement, {__index = cluster_base}) + +return ElectricalEnergyMeasurement \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/server/attributes/AcceptedCommandList.lua new file mode 100644 index 0000000000..4c51c12e6a --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/server/attributes/AcceptedCommandList.lua @@ -0,0 +1,80 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AcceptedCommandList = { + ID = 0xFFF9, + NAME = "AcceptedCommandList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +AcceptedCommandList.enum_fields = {} + +function AcceptedCommandList:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function AcceptedCommandList.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, AcceptedCommandList.enum_fields[value_obj.value]) +end + +function AcceptedCommandList:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function AcceptedCommandList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AcceptedCommandList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AcceptedCommandList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value}) +return AcceptedCommandList + diff --git a/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/server/attributes/AttributeList.lua new file mode 100644 index 0000000000..e3a71bab0d --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/server/attributes/AttributeList.lua @@ -0,0 +1,80 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + + +local AttributeList = { + ID = 0xFFFB, + NAME = "AttributeList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +AttributeList.enum_fields = {} + +function AttributeList:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function AttributeList.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, AttributeList.enum_fields[value_obj.value]) +end +function AttributeList:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function AttributeList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AttributeList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AttributeList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AttributeList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AttributeList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(AttributeList, {__call = AttributeList.new_value}) +return AttributeList + diff --git a/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua new file mode 100644 index 0000000000..db834c3424 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua @@ -0,0 +1,78 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local CumulativeEnergyImported = { + ID = 0x0001, + NAME = "CumulativeEnergyImported", + base_type = data_types.Structure, +} + +CumulativeEnergyImported.enum_fields = {} + +function CumulativeEnergyImported:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function CumulativeEnergyImported.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, CumulativeEnergyImported.enum_fields[value_obj.value]) +end +function CumulativeEnergyImported:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function CumulativeEnergyImported:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function CumulativeEnergyImported:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function CumulativeEnergyImported:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CumulativeEnergyImported:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function CumulativeEnergyImported:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(CumulativeEnergyImported, {__call = CumulativeEnergyImported.new_value}) +return CumulativeEnergyImported + diff --git a/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua new file mode 100644 index 0000000000..9a70554a7d --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua @@ -0,0 +1,78 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local PeriodicEnergyImported = { + ID = 0x0003, + NAME = "PeriodicEnergyImported", + base_type = data_types.Structure, +} + +PeriodicEnergyImported.enum_fields = {} + +function PeriodicEnergyImported:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function PeriodicEnergyImported.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, PeriodicEnergyImported.enum_fields[value_obj.value]) +end + +function PeriodicEnergyImported:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function PeriodicEnergyImported:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function PeriodicEnergyImported:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function PeriodicEnergyImported:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function PeriodicEnergyImported:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function PeriodicEnergyImported:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(PeriodicEnergyImported, {__call = PeriodicEnergyImported.new_value}) +return PeriodicEnergyImported diff --git a/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..adfdf42bbf --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/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("ElectricalEnergyMeasurement.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 ElectricalEnergyMeasurementServerAttributes = {} + +function ElectricalEnergyMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ElectricalEnergyMeasurementServerAttributes, attr_mt) + +return ElectricalEnergyMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua new file mode 100644 index 0000000000..99dd2dd52c --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua @@ -0,0 +1,93 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +local energy_mwh = require "st.matter.data_types.Int64" + + +local systime_ms = require "st.matter.data_types.Uint64" + +local EnergyMeasurementStruct = {} +local new_mt = StructureABC.new_mt({NAME = "EnergyMeasurementStruct", ID = data_types.name_to_id_map["Structure"]}) + +EnergyMeasurementStruct.field_defs = { + { + data_type = energy_mwh, + field_id = 0, + name = "energy", + }, + { + data_type = data_types.Uint32, + field_id = 1, + name = "start_timestamp", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Uint32, + field_id = 2, + name = "end_timestamp", + is_nullable = false, + is_optional = false, + }, + { + data_type = systime_ms, + field_id = 3, + name = "start_systime", + }, + { + data_type = systime_ms, + field_id = 4, + name = "end_systime", + }, +} + +EnergyMeasurementStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do --Note: idx is 1 when field_id is 0 + if (not field_def.is_optional or not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +EnergyMeasurementStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = EnergyMeasurementStruct.init +new_mt.__index.serialize = EnergyMeasurementStruct.serialize + +EnergyMeasurementStruct.augment_type = function(self, val) + local elems = {} + for _, v in ipairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.array_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.array_type) + end + end + end + end + end + val.elements = elems + setmetatable(val, new_mt) +end + +setmetatable(EnergyMeasurementStruct, new_mt) + +return EnergyMeasurementStruct + diff --git a/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/types/Feature.lua b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/types/Feature.lua new file mode 100644 index 0000000000..f8fa8cb516 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/types/Feature.lua @@ -0,0 +1,103 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.IMPORTED_ENERGY = 0x0001 +Feature.EXPORTED_ENERGY = 0x0002 +Feature.CUMULATIVE_ENERGY = 0x0004 +Feature.PERIODIC_ENERGY = 0x0008 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + IMPORTED_ENERGY = 0x0001, + EXPORTED_ENERGY = 0x0002, + CUMULATIVE_ENERGY = 0x0004, + PERIODIC_ENERGY = 0x0008, +} + +Feature.is_imported_energy_set = function(self) + return (self.value & self.IMPORTED_ENERGY) ~= 0 +end + +Feature.set_imported_energy = function(self) + if self.value ~= nil then + self.value = self.value | self.IMPORTED_ENERGY + else + self.value = self.IMPORTED_ENERGY + end +end +Feature.unset_imported_energy = function(self) + self.value = self.value & (~self.IMPORTED_ENERGY & self.BASE_MASK) +end + +Feature.is_exported_energy_set = function(self) + return (self.value & self.EXPORTED_ENERGY) ~= 0 +end + +Feature.set_exported_energy = function(self) + if self.value ~= nil then + self.value = self.value | self.EXPORTED_ENERGY + else + self.value = self.EXPORTED_ENERGY + end +end +Feature.unset_exported_energy = function(self) + self.value = self.value & (~self.EXPORTED_ENERGY & self.BASE_MASK) +end + +Feature.is_cumulative_energy_set = function(self) + return (self.value & self.CUMULATIVE_ENERGY) ~= 0 +end + +Feature.set_cumulative_energy = function(self) + if self.value ~= nil then + self.value = self.value | self.CUMULATIVE_ENERGY + else + self.value = self.CUMULATIVE_ENERGY + end +end + +Feature.unset_cumulative_energy = function(self) + self.value = self.value & (~self.CUMULATIVE_ENERGY & self.BASE_MASK) +end + +Feature.is_periodic_energy_set = function(self) + return (self.value & self.PERIODIC_ENERGY) ~= 0 +end + +Feature.set_periodic_energy = function(self) + if self.value ~= nil then + self.value = self.value | self.PERIODIC_ENERGY + else + self.value = self.PERIODIC_ENERGY + end +end +Feature.unset_periodic_energy = function(self) + self.value = self.value & (~self.PERIODIC_ENERGY & self.BASE_MASK) +end + +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-evse/src/ElectricalEnergyMeasurement/types/MeasurementTypeEnum.lua b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/types/MeasurementTypeEnum.lua new file mode 100644 index 0000000000..984df9c286 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/types/MeasurementTypeEnum.lua @@ -0,0 +1,51 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local MeasurementTypeEnum = {} +local new_mt = UintABC.new_mt({NAME = "MeasurementTypeEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.UNSPECIFIED] = "UNSPECIFIED", + [self.VOLTAGE] = "VOLTAGE", + [self.ACTIVE_CURRENT] = "ACTIVE_CURRENT", + [self.REACTIVE_CURRENT] = "REACTIVE_CURRENT", + [self.APPARENT_CURRENT] = "APPARENT_CURRENT", + [self.ACTIVE_POWER] = "ACTIVE_POWER", + [self.REACTIVE_POWER] = "REACTIVE_POWER", + [self.APPARENT_POWER] = "APPARENT_POWER", + [self.RMS_VOLTAGE] = "RMS_VOLTAGE", + [self.RMS_CURRENT] = "RMS_CURRENT", + [self.RMS_POWER] = "RMS_POWER", + [self.FREQUENCY] = "FREQUENCY", + [self.POWER_FACTOR] = "POWER_FACTOR", + [self.NEUTRAL_CURRENT] = "NEUTRAL_CURRENT", + [self.ELECTRICAL_ENERGY] = "ELECTRICAL_ENERGY", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.UNSPECIFIED = 0x00 +new_mt.__index.VOLTAGE = 0x01 +new_mt.__index.ACTIVE_CURRENT = 0x02 +new_mt.__index.REACTIVE_CURRENT = 0x03 +new_mt.__index.APPARENT_CURRENT = 0x04 +new_mt.__index.ACTIVE_POWER = 0x05 +new_mt.__index.REACTIVE_POWER = 0x06 +new_mt.__index.APPARENT_POWER = 0x07 +new_mt.__index.RMS_VOLTAGE = 0x08 +new_mt.__index.RMS_CURRENT = 0x09 +new_mt.__index.RMS_POWER = 0x0A +new_mt.__index.FREQUENCY = 0x0B +new_mt.__index.POWER_FACTOR = 0x0C +new_mt.__index.NEUTRAL_CURRENT = 0x0D +new_mt.__index.ELECTRICAL_ENERGY = 0x0E + +MeasurementTypeEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(MeasurementTypeEnum, new_mt) + +return MeasurementTypeEnum + diff --git a/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/types/init.lua b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/types/init.lua new file mode 100644 index 0000000000..db0e4230bf --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalEnergyMeasurement/types/init.lua @@ -0,0 +1,17 @@ +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + local req_loc = string.format("ElectricalEnergyMeasurement.types.%s", key) + local cluster_type = require(req_loc) + types_mt.__types_cache[key] = cluster_type + end + return types_mt.__types_cache[key] +end + +local ElectricalEnergyMeasurementTypes = {} + +setmetatable(ElectricalEnergyMeasurementTypes, types_mt) + +return ElectricalEnergyMeasurementTypes + diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/init.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/init.lua new file mode 100644 index 0000000000..cd28c586e2 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/init.lua @@ -0,0 +1,95 @@ +local cluster_base = require "st.matter.cluster_base" +local ElectricalPowerMeasurementServerAttributes = require "ElectricalPowerMeasurement.server.attributes" +local ElectricalPowerMeasurementServerCommands = require "ElectricalPowerMeasurement.server.commands" +local ElectricalPowerMeasurementTypes = require "ElectricalPowerMeasurement.types" + +local ElectricalPowerMeasurement = {} + +ElectricalPowerMeasurement.ID = 0x0090 +ElectricalPowerMeasurement.NAME = "ElectricalPowerMeasurement" +ElectricalPowerMeasurement.server = {} +ElectricalPowerMeasurement.client = {} +ElectricalPowerMeasurement.server.attributes = ElectricalPowerMeasurementServerAttributes:set_parent_cluster(ElectricalPowerMeasurement) +ElectricalPowerMeasurement.server.commands = ElectricalPowerMeasurementServerCommands:set_parent_cluster(ElectricalPowerMeasurement) +ElectricalPowerMeasurement.types = ElectricalPowerMeasurementTypes +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 + +function ElectricalPowerMeasurement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "PowerMode", + [0x0008] = "ActivePower", + [0x000A] = "ApparentPower", + [0xFFF9] = "AcceptedCommandList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function ElectricalPowerMeasurement: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 + +function ElectricalPowerMeasurement:get_event_by_id(event_id) + local event_id_map = { + [0x0000] = "MeasurementPeriodRanges", + } + if event_id_map[event_id] ~= nil then + return self.server.events[event_id_map[event_id]] + end + return nil +end +-- Attribute Mapping +ElectricalPowerMeasurement.attribute_direction_map = { + ["PowerMode"] = "server", + ["ActivePower"] = "server", + ["ApparentPower"] = "server", + ["AcceptedCommandList"] = "server", + ["AttributeList"] = "server", +} + +-- Command Mapping +ElectricalPowerMeasurement.command_direction_map = { +} + +-- Cluster Completion +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ElectricalPowerMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ElectricalPowerMeasurement.NAME)) + end + return ElectricalPowerMeasurement[direction].attributes[key] +end +ElectricalPowerMeasurement.attributes = {} +setmetatable(ElectricalPowerMeasurement.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = ElectricalPowerMeasurement.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, ElectricalPowerMeasurement.NAME)) + end + return ElectricalPowerMeasurement[direction].commands[key] +end +ElectricalPowerMeasurement.commands = {} +setmetatable(ElectricalPowerMeasurement.commands, command_helper_mt) + +setmetatable(ElectricalPowerMeasurement, {__index = cluster_base}) + +return ElectricalPowerMeasurement \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/AcceptedCommandList.lua new file mode 100644 index 0000000000..fd4855a236 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/AcceptedCommandList.lua @@ -0,0 +1,79 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AcceptedCommandList = { + ID = 0xFFF9, + NAME = "AcceptedCommandList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +AcceptedCommandList.enum_fields = {} + +function AcceptedCommandList:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function AcceptedCommandList.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, AcceptedCommandList.enum_fields[value_obj.value]) +end + +function AcceptedCommandList:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function AcceptedCommandList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AcceptedCommandList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AcceptedCommandList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AcceptedCommandList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AcceptedCommandList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value}) +return AcceptedCommandList diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua new file mode 100644 index 0000000000..16774ae1d6 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua @@ -0,0 +1,80 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + + +local ActivePower = { + ID = 0x0008, + NAME = "ActivePower", + base_type = data_types.Int64, +} + +ActivePower.enum_fields = { +} + +function ActivePower:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function ActivePower.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, ActivePower.enum_fields[value_obj.value]) +end + +function ActivePower:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function ActivePower:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function ActivePower:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function ActivePower:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ActivePower:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function ActivePower:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(ActivePower, {__call = ActivePower.new_value}) +return ActivePower diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/ApparentPower.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/ApparentPower.lua new file mode 100644 index 0000000000..40cbcef7eb --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/ApparentPower.lua @@ -0,0 +1,78 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ApparentPower = { + ID = 0x000A, + NAME = "ApparentPower", + base_type = data_types.Int64, +} + +ApparentPower.enum_fields = {} + +function ApparentPower:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function ApparentPower.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, ApparentPower.enum_fields[value_obj.value]) +end + +function ApparentPower:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function ApparentPower:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function ApparentPower:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function ApparentPower:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ApparentPower:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function ApparentPower:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(ApparentPower, {__call = ApparentPower.new_value}) +return ApparentPower diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/AttributeList.lua new file mode 100644 index 0000000000..eea5f89c63 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/AttributeList.lua @@ -0,0 +1,80 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AttributeList = { + ID = 0xFFFB, + NAME = "AttributeList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +AttributeList.enum_fields = { +} + +function AttributeList:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function AttributeList.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, AttributeList.enum_fields[value_obj.value]) +end + +function AttributeList:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function AttributeList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AttributeList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AttributeList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AttributeList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AttributeList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(AttributeList, {__call = AttributeList.new_value}) +return AttributeList diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/PowerMode.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/PowerMode.lua new file mode 100644 index 0000000000..4d96c91bd8 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/PowerMode.lua @@ -0,0 +1,86 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local PowerMode = { + ID = 0x0000, + NAME = "PowerMode", + base_type = data_types.Uint8, +} +PowerMode.UNKNOWN = 0x00 +PowerMode.DC = 0x01 +PowerMode.AC = 0x02 + +PowerMode.enum_fields = { + [PowerMode.UNKNOWN] = "UNKNOWN", + [PowerMode.DC] = "DC", + [PowerMode.AC] = "AC", +} + +function PowerMode:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function PowerMode.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, PowerMode.enum_fields[value_obj.value]) +end + +function PowerMode:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function PowerMode:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function PowerMode:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function PowerMode:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function PowerMode:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function PowerMode:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(PowerMode, {__call = PowerMode.new_value}) +return PowerMode + diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..0c30fa8dd4 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/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("ElectricalPowerMeasurement.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 ElectricalPowerMeasurementServerAttributes = {} + +function ElectricalPowerMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ElectricalPowerMeasurementServerAttributes, attr_mt) + +return ElectricalPowerMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/commands/init.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/commands/init.lua new file mode 100644 index 0000000000..087bb47a9e --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/commands/init.lua @@ -0,0 +1,23 @@ +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("ElectricalPowerMeasurement.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local ElectricalPowerMeasurementServerCommands = {} + +function ElectricalPowerMeasurementServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ElectricalPowerMeasurementServerCommands, command_mt) + +return ElectricalPowerMeasurementServerCommands + diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/events/MeasurementPeriodRanges.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/events/MeasurementPeriodRanges.lua new file mode 100644 index 0000000000..414f64e3e8 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/events/MeasurementPeriodRanges.lua @@ -0,0 +1,98 @@ +local data_types = require "st.matter.data_types" +local cluster_base = require "st.matter.cluster_base" +local TLVParser = require "st.matter.TLV.TLVParser" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +local MeasurementRangeStruct = require "ElectricalPowerMeasurement.types.MeasurementRangeStruct" + +local MeasurementPeriodRanges = { + ID = 0x0000, + NAME = "MeasurementPeriodRanges", + base_type = data_types.Structure, +} + +MeasurementPeriodRanges.field_defs = { + { + data_type = MeasurementRangeStruct, + field_id = 0, + is_array = true, + name = "ranges", + is_nullable = false, + is_optional = false, + }, +} + +function MeasurementPeriodRanges:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and not + ((field_def.is_nullable or field_def.is_optional) and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + elems[field_def.name].field_name = field_def.name + end + end + end + base_type_obj.elements = elems +end + +function MeasurementPeriodRanges:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + nil, --attribute_id + self.ID + ) +end + +function MeasurementPeriodRanges:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + nil, --attribute_id + self.ID + ) +end + +function MeasurementPeriodRanges:build_test_event_report( + device, + endpoint_id, + fields, + status +) + local data = {} + data.elements = {} + data.num_elements = 0 + setmetatable(data, StructureABC.new_mt({NAME = "MeasurementPeriodRangesEventData", ID = 0x15})) + for idx, field_def in ipairs(self.field_defs) do --Note: idx is 1 when field_id is 0 + if (not field_def.is_optional or not field_def.is_nullable) and not fields[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + elseif fields[field_def.name] then + data.elements[field_def.name] = data_types.validate_or_build_type(fields[field_def.name], field_def.data_type, field_def.name) + data.elements[field_def.name].field_id = field_def.field_id + data.num_elements = data.num_elements + 1 + end + end + return cluster_base.build_test_event_report( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MeasurementPeriodRanges:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +function MeasurementPeriodRanges:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +return MeasurementPeriodRanges diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/events/init.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/events/init.lua new file mode 100644 index 0000000000..eeec54b6f3 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/server/events/init.lua @@ -0,0 +1,24 @@ +local event_mt = {} +event_mt.__event_cache = {} +event_mt.__index = function(self, key) + if event_mt.__event_cache[key] == nil then + local req_loc = string.format("ElectricalPowerMeasurement.server.events.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + event_mt.__event_cache[key] = raw_def + end + return event_mt.__event_cache[key] +end + +local ElectricalPowerMeasurementEvents = {} + +function ElectricalPowerMeasurementEvents:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ElectricalPowerMeasurementEvents, event_mt) + +return ElectricalPowerMeasurementEvents + diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/Feature.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/Feature.lua new file mode 100644 index 0000000000..2229aebe0c --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/Feature.lua @@ -0,0 +1,138 @@ +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 \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/HarmonicMeasurementStruct.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/HarmonicMeasurementStruct.lua new file mode 100644 index 0000000000..1398736a19 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/HarmonicMeasurementStruct.lua @@ -0,0 +1,73 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local HarmonicMeasurementStruct = {} +local new_mt = StructureABC.new_mt({NAME = "HarmonicMeasurementStruct", ID = data_types.name_to_id_map["Structure"]}) + +HarmonicMeasurementStruct.field_defs = { + { + data_type = data_types.Uint8, + field_id = 0, + name = "order", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Int64, + field_id = 1, + name = "measurement", + is_nullable = true, + is_optional = true, + }, +} + +HarmonicMeasurementStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do --Note: idx is 1 when field_id is 0 + if (not field_def.is_optional or not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +HarmonicMeasurementStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = HarmonicMeasurementStruct.init +new_mt.__index.serialize = HarmonicMeasurementStruct.serialize + +HarmonicMeasurementStruct.augment_type = function(self, val) + local elems = {} + for _, v in ipairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.array_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.array_type) + end + end + end + end + end + val.elements = elems + setmetatable(val, new_mt) +end + +setmetatable(HarmonicMeasurementStruct, new_mt) + +return HarmonicMeasurementStruct diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/MeasurementAccuracyRangeStruct.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/MeasurementAccuracyRangeStruct.lua new file mode 100644 index 0000000000..ba03a8b793 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/MeasurementAccuracyRangeStruct.lua @@ -0,0 +1,115 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local MeasurementAccuracyRangeStruct = {} +local new_mt = StructureABC.new_mt({NAME = "MeasurementAccuracyRangeStruct", ID = data_types.name_to_id_map["Structure"]}) + +MeasurementAccuracyRangeStruct.field_defs = { + { + data_type = data_types.Int64, + field_id = 0, + name = "range_min", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Int64, + field_id = 1, + name = "range_max", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Uint16, + field_id = 2, + name = "percent_max", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Uint16, + field_id = 3, + name = "percent_min", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Uint16, + field_id = 4, + name = "percent_typical", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Uint64, + field_id = 5, + name = "fixed_max", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Uint64, + field_id = 6, + name = "fixed_min", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Uint64, + field_id = 7, + name = "fixed_typical", + is_nullable = false, + is_optional = false, + }, +} + +MeasurementAccuracyRangeStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do --Note: idx is 1 when field_id is 0 + if (not field_def.is_optional or not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +MeasurementAccuracyRangeStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = MeasurementAccuracyRangeStruct.init +new_mt.__index.serialize = MeasurementAccuracyRangeStruct.serialize + +MeasurementAccuracyRangeStruct.augment_type = function(self, val) + local elems = {} + for _, v in ipairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.array_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.array_type) + end + end + end + end + end + val.elements = elems + setmetatable(val, new_mt) +end + +setmetatable(MeasurementAccuracyRangeStruct, new_mt) + +return MeasurementAccuracyRangeStruct diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/MeasurementAccuracyStruct.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/MeasurementAccuracyStruct.lua new file mode 100644 index 0000000000..be596893f6 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/MeasurementAccuracyStruct.lua @@ -0,0 +1,91 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +local MeasurementTypeEnum = require "ElectricalPowerMeasurement.types.MeasurementTypeEnum" +local MeasurementAccuracyRangeStruct = require "ElectricalPowerMeasurement.types.MeasurementAccuracyRangeStruct" +local MeasurementAccuracyStruct = {} +local new_mt = StructureABC.new_mt({NAME = "MeasurementAccuracyStruct", ID = data_types.name_to_id_map["Structure"]}) + +MeasurementAccuracyStruct.field_defs = { + { + data_type = MeasurementTypeEnum, + field_id = 0, + name = "measurement_type", + }, + { + data_type = data_types.Boolean, + field_id = 1, + name = "measured", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Int64, + field_id = 2, + name = "min_measured_value", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Int64, + field_id = 3, + name = "max_measured_value", + is_nullable = false, + is_optional = false, + }, + { + data_type = MeasurementAccuracyRangeStruct, + field_id = 4, + name = "accuracy_ranges", + }, +} + +MeasurementAccuracyStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do --Note: idx is 1 when field_id is 0 + if (not field_def.is_optional or not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +MeasurementAccuracyStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = MeasurementAccuracyStruct.init +new_mt.__index.serialize = MeasurementAccuracyStruct.serialize + +MeasurementAccuracyStruct.augment_type = function(self, val) + local elems = {} + for _, v in ipairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.array_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.array_type) + end + end + end + end + end + val.elements = elems + setmetatable(val, new_mt) +end + +setmetatable(MeasurementAccuracyStruct, new_mt) + +return MeasurementAccuracyStruct diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/MeasurementRangeStruct.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/MeasurementRangeStruct.lua new file mode 100644 index 0000000000..d1d7cfd371 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/MeasurementRangeStruct.lua @@ -0,0 +1,126 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +local MeasurementTypeEnum = require "ElectricalPowerMeasurement.types.MeasurementTypeEnum" +local MeasurementRangeStruct = {} +local new_mt = StructureABC.new_mt({NAME = "MeasurementRangeStruct", ID = data_types.name_to_id_map["Structure"]}) + +MeasurementRangeStruct.field_defs = { + { + data_type = MeasurementTypeEnum, + field_id = 0, + name = "measurement_type", + }, + { + data_type = data_types.Int64, + field_id = 1, + name = "min", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Int64, + field_id = 2, + name = "max", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Uint32, + field_id = 3, + name = "start_timestamp", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Uint32, + field_id = 4, + name = "end_timestamp", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Uint32, + field_id = 5, + name = "min_timestamp", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Uint32, + field_id = 6, + name = "max_timestamp", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Uint64, + field_id = 7, + name = "start_systime", + }, + { + data_type = data_types.Uint64, + field_id = 8, + name = "end_systime", + }, + { + data_type = data_types.Uint64, + field_id = 9, + name = "min_systime", + }, + { + data_type = data_types.Uint64, + field_id = 10, + name = "max_systime", + }, +} + +MeasurementRangeStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do --Note: idx is 1 when field_id is 0 + if (not field_def.is_optional or not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +MeasurementRangeStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = MeasurementRangeStruct.init +new_mt.__index.serialize = MeasurementRangeStruct.serialize + +MeasurementRangeStruct.augment_type = function(self, val) + local elems = {} + for _, v in ipairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.array_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.array_type) + end + end + end + end + end + val.elements = elems + setmetatable(val, new_mt) +end + +setmetatable(MeasurementRangeStruct, new_mt) + +return MeasurementRangeStruct \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/MeasurementTypeEnum.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/MeasurementTypeEnum.lua new file mode 100644 index 0000000000..984df9c286 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/MeasurementTypeEnum.lua @@ -0,0 +1,51 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local MeasurementTypeEnum = {} +local new_mt = UintABC.new_mt({NAME = "MeasurementTypeEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.UNSPECIFIED] = "UNSPECIFIED", + [self.VOLTAGE] = "VOLTAGE", + [self.ACTIVE_CURRENT] = "ACTIVE_CURRENT", + [self.REACTIVE_CURRENT] = "REACTIVE_CURRENT", + [self.APPARENT_CURRENT] = "APPARENT_CURRENT", + [self.ACTIVE_POWER] = "ACTIVE_POWER", + [self.REACTIVE_POWER] = "REACTIVE_POWER", + [self.APPARENT_POWER] = "APPARENT_POWER", + [self.RMS_VOLTAGE] = "RMS_VOLTAGE", + [self.RMS_CURRENT] = "RMS_CURRENT", + [self.RMS_POWER] = "RMS_POWER", + [self.FREQUENCY] = "FREQUENCY", + [self.POWER_FACTOR] = "POWER_FACTOR", + [self.NEUTRAL_CURRENT] = "NEUTRAL_CURRENT", + [self.ELECTRICAL_ENERGY] = "ELECTRICAL_ENERGY", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.UNSPECIFIED = 0x00 +new_mt.__index.VOLTAGE = 0x01 +new_mt.__index.ACTIVE_CURRENT = 0x02 +new_mt.__index.REACTIVE_CURRENT = 0x03 +new_mt.__index.APPARENT_CURRENT = 0x04 +new_mt.__index.ACTIVE_POWER = 0x05 +new_mt.__index.REACTIVE_POWER = 0x06 +new_mt.__index.APPARENT_POWER = 0x07 +new_mt.__index.RMS_VOLTAGE = 0x08 +new_mt.__index.RMS_CURRENT = 0x09 +new_mt.__index.RMS_POWER = 0x0A +new_mt.__index.FREQUENCY = 0x0B +new_mt.__index.POWER_FACTOR = 0x0C +new_mt.__index.NEUTRAL_CURRENT = 0x0D +new_mt.__index.ELECTRICAL_ENERGY = 0x0E + +MeasurementTypeEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(MeasurementTypeEnum, new_mt) + +return MeasurementTypeEnum + diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/PowerModeEnum.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/PowerModeEnum.lua new file mode 100644 index 0000000000..f76370acd7 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/PowerModeEnum.lua @@ -0,0 +1,27 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + + +local PowerModeEnum = {} +local new_mt = UintABC.new_mt({NAME = "PowerModeEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.UNKNOWN] = "UNKNOWN", + [self.DC] = "DC", + [self.AC] = "AC", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.UNKNOWN = 0x00 +new_mt.__index.DC = 0x01 +new_mt.__index.AC = 0x02 + +PowerModeEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(PowerModeEnum, new_mt) + +return PowerModeEnum diff --git a/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/init.lua b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/init.lua new file mode 100644 index 0000000000..658256b316 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/ElectricalPowerMeasurement/types/init.lua @@ -0,0 +1,17 @@ +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + local req_loc = string.format("ElectricalPowerMeasurement.types.%s", key) + local cluster_type = require(req_loc) + types_mt.__types_cache[key] = cluster_type + end + return types_mt.__types_cache[key] +end + +local ElectricalPowerMeasurementTypes = {} + +setmetatable(ElectricalPowerMeasurementTypes, types_mt) + +return ElectricalPowerMeasurementTypes + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/init.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/init.lua new file mode 100644 index 0000000000..85e3ddced2 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/init.lua @@ -0,0 +1,162 @@ +local cluster_base = require "st.matter.cluster_base" +local EnergyEvseServerAttributes = require "EnergyEvse.server.attributes" +local EnergyEvseServerCommands = require "EnergyEvse.server.commands" +local EnergyEvseEvents = require "EnergyEvse.server.events" +local EnergyEvseTypes = require "EnergyEvse.types" + +local EnergyEvse = {} + +EnergyEvse.ID = 0x0099 +EnergyEvse.NAME = "EnergyEvse" +EnergyEvse.server = {} +EnergyEvse.client = {} +EnergyEvse.server.attributes = EnergyEvseServerAttributes:set_parent_cluster(EnergyEvse) +EnergyEvse.server.commands = EnergyEvseServerCommands:set_parent_cluster(EnergyEvse) +EnergyEvse.server.events = EnergyEvseEvents:set_parent_cluster(EnergyEvse) +EnergyEvse.types = EnergyEvseTypes +EnergyEvse.FeatureMap = EnergyEvse.types.Feature + +function EnergyEvse.are_features_supported(feature, feature_map) + if (EnergyEvse.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +function EnergyEvse:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "State", + [0x0001] = "SupplyState", + [0x0002] = "FaultState", + [0x0003] = "ChargingEnabledUntil", + [0x0004] = "DischargingEnabledUntil", + [0x0005] = "CircuitCapacity", + [0x0006] = "MinimumChargeCurrent", + [0x0007] = "MaximumChargeCurrent", + [0x0008] = "MaximumDischargeCurrent", + [0x0009] = "UserMaximumChargeCurrent", + [0x000A] = "RandomizationDelayWindow", + [0x0023] = "NextChargeStartTime", + [0x0024] = "NextChargeTargetTime", + [0x0025] = "NextChargeRequiredEnergy", + [0x0026] = "NextChargeTargetSoC", + [0x0027] = "ApproximateEVEfficiency", + [0x0030] = "StateOfCharge", + [0x0031] = "BatteryCapacity", + [0x0032] = "VehicleID", + [0x0040] = "SessionID", + [0x0041] = "SessionDuration", + [0x0042] = "SessionEnergyCharged", + [0x0043] = "SessionEnergyDischarged", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function EnergyEvse:get_server_command_by_id(command_id) + local server_id_map = { + [0x0001] = "Disable", + [0x0002] = "EnableCharging", + [0x0003] = "EnableDischarging", + [0x0004] = "StartDiagnostics", + [0x0005] = "SetTargets", + [0x0006] = "GetTargets", + [0x0007] = "ClearTargets", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +function EnergyEvse:get_event_by_id(event_id) + local event_id_map = { + [0x0000] = "EVConnected", + [0x0001] = "EVNotDetected", + [0x0002] = "EnergyTransferStarted", + [0x0003] = "EnergyTransferStopped", + [0x0004] = "Fault", + [0x0005] = "Rfid", + } + if event_id_map[event_id] ~= nil then + return self.server.events[event_id_map[event_id]] + end + return nil +end +EnergyEvse.attribute_direction_map = { + ["State"] = "server", + ["SupplyState"] = "server", + ["FaultState"] = "server", + ["ChargingEnabledUntil"] = "server", + ["DischargingEnabledUntil"] = "server", + ["CircuitCapacity"] = "server", + ["MinimumChargeCurrent"] = "server", + ["MaximumChargeCurrent"] = "server", + ["MaximumDischargeCurrent"] = "server", + ["UserMaximumChargeCurrent"] = "server", + ["RandomizationDelayWindow"] = "server", + ["NextChargeStartTime"] = "server", + ["NextChargeTargetTime"] = "server", + ["NextChargeRequiredEnergy"] = "server", + ["NextChargeTargetSoC"] = "server", + ["ApproximateEVEfficiency"] = "server", + ["StateOfCharge"] = "server", + ["BatteryCapacity"] = "server", + ["VehicleID"] = "server", + ["SessionID"] = "server", + ["SessionDuration"] = "server", + ["SessionEnergyCharged"] = "server", + ["SessionEnergyDischarged"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +EnergyEvse.command_direction_map = { + ["Disable"] = "server", + ["EnableCharging"] = "server", + ["EnableDischarging"] = "server", + ["StartDiagnostics"] = "server", + ["SetTargets"] = "server", + ["GetTargets"] = "server", + ["ClearTargets"] = "server", +} + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = EnergyEvse.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, EnergyEvse.NAME)) + end + return EnergyEvse[direction].attributes[key] +end +EnergyEvse.attributes = {} +setmetatable(EnergyEvse.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = EnergyEvse.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, EnergyEvse.NAME)) + end + return EnergyEvse[direction].commands[key] +end +EnergyEvse.commands = {} +setmetatable(EnergyEvse.commands, command_helper_mt) + +local event_helper_mt = {} +event_helper_mt.__index = function(self, key) + return EnergyEvse.server.events[key] +end +EnergyEvse.events = {} +setmetatable(EnergyEvse.events, event_helper_mt) + +setmetatable(EnergyEvse, {__index = cluster_base}) + +return EnergyEvse \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/AcceptedCommandList.lua new file mode 100644 index 0000000000..2d1bdd9f6a --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/AcceptedCommandList.lua @@ -0,0 +1,79 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AcceptedCommandList = { + ID = 0xFFF9, + NAME = "AcceptedCommandList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32" +} + +AcceptedCommandList.enum_fields = {} + +function AcceptedCommandList:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function AcceptedCommandList.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, AcceptedCommandList.enum_fields[value_obj.value]) +end + +function AcceptedCommandList:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function AcceptedCommandList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AcceptedCommandList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AcceptedCommandList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value}) +return AcceptedCommandList diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/ApproximateEVEfficiency.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/ApproximateEVEfficiency.lua new file mode 100644 index 0000000000..878ebd222b --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/ApproximateEVEfficiency.lua @@ -0,0 +1,76 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ApproximateEVEfficiency = { + ID = 0x0027, + NAME = "ApproximateEVEfficiency", + base_type = data_types.Uint16, +} + +function ApproximateEVEfficiency:new_value(...) + local o = self.base_type(table.unpack({...})) + return o +end + +function ApproximateEVEfficiency:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function ApproximateEVEfficiency:write(device, endpoint_id, value) + local data = data_types.validate_or_build_type(value, self.base_type) + return cluster_base.write( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, --event_id + data + ) +end + +function ApproximateEVEfficiency:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function ApproximateEVEfficiency:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ApproximateEVEfficiency: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 ApproximateEVEfficiency:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + return data +end + +setmetatable(ApproximateEVEfficiency, {__call = ApproximateEVEfficiency.new_value}) +return ApproximateEVEfficiency \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/AttributeList.lua new file mode 100644 index 0000000000..0f66af7c79 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/AttributeList.lua @@ -0,0 +1,81 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AttributeList = { + ID = 0xFFFB, + NAME = "AttributeList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32" +} + +AttributeList.enum_fields = { +} + +function AttributeList:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function AttributeList.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, AttributeList.enum_fields[value_obj.value]) +end + +function AttributeList:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function AttributeList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AttributeList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AttributeList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AttributeList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AttributeList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(AttributeList, {__call = AttributeList.new_value}) +return AttributeList + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/BatteryCapacity.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/BatteryCapacity.lua new file mode 100644 index 0000000000..e9bb2c905b --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/BatteryCapacity.lua @@ -0,0 +1,79 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local BatteryCapacity = { + ID = 0x0031, + NAME = "BatteryCapacity", + base_type = data_types.Int64, +} + +BatteryCapacity.enum_fields = { +} + +function BatteryCapacity:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function BatteryCapacity.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, BatteryCapacity.enum_fields[value_obj.value]) +end + +function BatteryCapacity:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function BatteryCapacity:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function BatteryCapacity:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function BatteryCapacity:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function BatteryCapacity:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function BatteryCapacity:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(BatteryCapacity, {__call = BatteryCapacity.new_value}) +return BatteryCapacity diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/ChargingEnabledUntil.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/ChargingEnabledUntil.lua new file mode 100644 index 0000000000..30288fba35 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/ChargingEnabledUntil.lua @@ -0,0 +1,64 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ChargingEnabledUntil = { + ID = 0x0003, + NAME = "ChargingEnabledUntil", + base_type = data_types.Uint32, +} + +function ChargingEnabledUntil:new_value(...) + local o = self.base_type(table.unpack({...})) + return o +end + +function ChargingEnabledUntil:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function ChargingEnabledUntil:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function ChargingEnabledUntil:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ChargingEnabledUntil: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 ChargingEnabledUntil:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + return data +end + +setmetatable(ChargingEnabledUntil, {__call = ChargingEnabledUntil.new_value}) +return ChargingEnabledUntil diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/CircuitCapacity.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/CircuitCapacity.lua new file mode 100644 index 0000000000..58cc7cc911 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/CircuitCapacity.lua @@ -0,0 +1,80 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local CircuitCapacity = { + ID = 0x0005, + NAME = "CircuitCapacity", + base_type = data_types.Int64, +} + +CircuitCapacity.enum_fields = { +} + +function CircuitCapacity:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function CircuitCapacity.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, CircuitCapacity.enum_fields[value_obj.value]) +end + +function CircuitCapacity:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function CircuitCapacity:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function CircuitCapacity:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function CircuitCapacity:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CircuitCapacity:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function CircuitCapacity:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(CircuitCapacity, {__call = CircuitCapacity.new_value}) +return CircuitCapacity + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/DischargingEnabledUntil.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/DischargingEnabledUntil.lua new file mode 100644 index 0000000000..502dd8b1fb --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/DischargingEnabledUntil.lua @@ -0,0 +1,64 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local DischargingEnabledUntil = { + ID = 0x0004, + NAME = "DischargingEnabledUntil", + base_type = data_types.Uint32, +} + +function DischargingEnabledUntil:new_value(...) + local o = self.base_type(table.unpack({...})) + return o +end + +function DischargingEnabledUntil:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function DischargingEnabledUntil:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function DischargingEnabledUntil:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function DischargingEnabledUntil: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 DischargingEnabledUntil:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + return data +end + +setmetatable(DischargingEnabledUntil, {__call = DischargingEnabledUntil.new_value}) +return DischargingEnabledUntil \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/EventList.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/EventList.lua new file mode 100644 index 0000000000..ec5a5c5426 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/EventList.lua @@ -0,0 +1,80 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local EventList = { + ID = 0xFFFA, + NAME = "EventList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32" +} + +EventList.enum_fields = { +} + +function EventList:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function EventList.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, EventList.enum_fields[value_obj.value]) +end + +function EventList:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function EventList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function EventList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function EventList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function EventList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function EventList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(EventList, {__call = EventList.new_value}) +return EventList diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/FaultState.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/FaultState.lua new file mode 100644 index 0000000000..c0b4db8f5a --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/FaultState.lua @@ -0,0 +1,114 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local FaultState = { + ID = 0x0002, + NAME = "FaultState", + base_type = data_types.Uint8, +} +FaultState.NO_ERROR = 0x00 +FaultState.METER_FAILURE = 0x01 +FaultState.OVER_VOLTAGE = 0x02 +FaultState.UNDER_VOLTAGE = 0x03 +FaultState.OVER_CURRENT = 0x04 +FaultState.CONTACT_WET_FAILURE = 0x05 +FaultState.CONTACT_DRY_FAILURE = 0x06 +FaultState.GROUND_FAULT = 0x07 +FaultState.POWER_LOSS = 0x08 +FaultState.POWER_QUALITY = 0x09 +FaultState.PILOT_SHORT_CIRCUIT = 0x0A +FaultState.EMERGENCY_STOP = 0x0B +FaultState.EV_DISCONNECTED = 0x0C +FaultState.WRONG_POWER_SUPPLY = 0x0D +FaultState.LIVE_NEUTRAL_SWAP = 0x0E +FaultState.OVER_TEMPERATURE = 0x0F +FaultState.OTHER = 0xFF + +FaultState.enum_fields = { + [FaultState.NO_ERROR] = "NO_ERROR", + [FaultState.METER_FAILURE] = "METER_FAILURE", + [FaultState.OVER_VOLTAGE] = "OVER_VOLTAGE", + [FaultState.UNDER_VOLTAGE] = "UNDER_VOLTAGE", + [FaultState.OVER_CURRENT] = "OVER_CURRENT", + [FaultState.CONTACT_WET_FAILURE] = "CONTACT_WET_FAILURE", + [FaultState.CONTACT_DRY_FAILURE] = "CONTACT_DRY_FAILURE", + [FaultState.GROUND_FAULT] = "GROUND_FAULT", + [FaultState.POWER_LOSS] = "POWER_LOSS", + [FaultState.POWER_QUALITY] = "POWER_QUALITY", + [FaultState.PILOT_SHORT_CIRCUIT] = "PILOT_SHORT_CIRCUIT", + [FaultState.EMERGENCY_STOP] = "EMERGENCY_STOP", + [FaultState.EV_DISCONNECTED] = "EV_DISCONNECTED", + [FaultState.WRONG_POWER_SUPPLY] = "WRONG_POWER_SUPPLY", + [FaultState.LIVE_NEUTRAL_SWAP] = "LIVE_NEUTRAL_SWAP", + [FaultState.OVER_TEMPERATURE] = "OVER_TEMPERATURE", + [FaultState.OTHER] = "OTHER", +} + +function FaultState:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function FaultState.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, FaultState.enum_fields[value_obj.value]) +end + +function FaultState:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function FaultState:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function FaultState:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function FaultState:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function FaultState:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function FaultState:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(FaultState, {__call = FaultState.new_value}) +return FaultState + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/MaximumChargeCurrent.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/MaximumChargeCurrent.lua new file mode 100644 index 0000000000..cbfe635d66 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/MaximumChargeCurrent.lua @@ -0,0 +1,80 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local MaximumChargeCurrent = { + ID = 0x0007, + NAME = "MaximumChargeCurrent", + base_type = data_types.Int64, +} + +MaximumChargeCurrent.enum_fields = { +} + +function MaximumChargeCurrent:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function MaximumChargeCurrent.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, MaximumChargeCurrent.enum_fields[value_obj.value]) +end + +function MaximumChargeCurrent:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function MaximumChargeCurrent:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function MaximumChargeCurrent:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function MaximumChargeCurrent:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MaximumChargeCurrent:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MaximumChargeCurrent:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MaximumChargeCurrent, {__call = MaximumChargeCurrent.new_value}) +return MaximumChargeCurrent + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/MaximumDischargeCurrent.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/MaximumDischargeCurrent.lua new file mode 100644 index 0000000000..c3866fa9d5 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/MaximumDischargeCurrent.lua @@ -0,0 +1,80 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local MaximumDischargeCurrent = { + ID = 0x0008, + NAME = "MaximumDischargeCurrent", + base_type = data_types.Int64, +} + +MaximumDischargeCurrent.enum_fields = { +} + +function MaximumDischargeCurrent:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function MaximumDischargeCurrent.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, MaximumDischargeCurrent.enum_fields[value_obj.value]) +end + +function MaximumDischargeCurrent:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function MaximumDischargeCurrent:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function MaximumDischargeCurrent:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function MaximumDischargeCurrent:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MaximumDischargeCurrent:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MaximumDischargeCurrent:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MaximumDischargeCurrent, {__call = MaximumDischargeCurrent.new_value}) +return MaximumDischargeCurrent + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/MinimumChargeCurrent.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/MinimumChargeCurrent.lua new file mode 100644 index 0000000000..ed9572388e --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/MinimumChargeCurrent.lua @@ -0,0 +1,80 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + + +local MinimumChargeCurrent = { + ID = 0x0006, + NAME = "MinimumChargeCurrent", + base_type = data_types.Int64, +} + +MinimumChargeCurrent.enum_fields = {} + +function MinimumChargeCurrent:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function MinimumChargeCurrent.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, MinimumChargeCurrent.enum_fields[value_obj.value]) +end + +function MinimumChargeCurrent:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function MinimumChargeCurrent:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function MinimumChargeCurrent:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function MinimumChargeCurrent:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MinimumChargeCurrent:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function MinimumChargeCurrent:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MinimumChargeCurrent, {__call = MinimumChargeCurrent.new_value}) +return MinimumChargeCurrent + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/NextChargeRequiredEnergy.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/NextChargeRequiredEnergy.lua new file mode 100644 index 0000000000..61a1a664f1 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/NextChargeRequiredEnergy.lua @@ -0,0 +1,78 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local NextChargeRequiredEnergy = { + ID = 0x0025, + NAME = "NextChargeRequiredEnergy", + base_type = data_types.Uint32, +} + +NextChargeRequiredEnergy.enum_fields = {} + +function NextChargeRequiredEnergy:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function NextChargeRequiredEnergy.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, NextChargeRequiredEnergy.enum_fields[value_obj.value]) +end + +function NextChargeRequiredEnergy:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function NextChargeRequiredEnergy:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function NextChargeRequiredEnergy:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function NextChargeRequiredEnergy:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function NextChargeRequiredEnergy:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function NextChargeRequiredEnergy:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(NextChargeRequiredEnergy, {__call = NextChargeRequiredEnergy.new_value}) +return NextChargeRequiredEnergy diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/NextChargeStartTime.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/NextChargeStartTime.lua new file mode 100644 index 0000000000..0125d1e4f0 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/NextChargeStartTime.lua @@ -0,0 +1,64 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local NextChargeStartTime = { + ID = 0x0023, + NAME = "NextChargeStartTime", + base_type = data_types.Uint32, +} + +function NextChargeStartTime:new_value(...) + local o = self.base_type(table.unpack({...})) + return o +end + +function NextChargeStartTime:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function NextChargeStartTime:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function NextChargeStartTime:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function NextChargeStartTime: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 NextChargeStartTime:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + return data +end + +setmetatable(NextChargeStartTime, {__call = NextChargeStartTime.new_value}) +return NextChargeStartTime diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/NextChargeTargetSoC.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/NextChargeTargetSoC.lua new file mode 100644 index 0000000000..50cf6db6e0 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/NextChargeTargetSoC.lua @@ -0,0 +1,64 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local NextChargeTargetSoC = { + ID = 0x0026, + NAME = "NextChargeTargetSoC", + base_type = data_types.Uint8, +} + +function NextChargeTargetSoC:new_value(...) + local o = self.base_type(table.unpack({...})) + return o +end + +function NextChargeTargetSoC:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function NextChargeTargetSoC:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function NextChargeTargetSoC:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function NextChargeTargetSoC: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 NextChargeTargetSoC:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + return data +end + +setmetatable(NextChargeTargetSoC, {__call = NextChargeTargetSoC.new_value}) +return NextChargeTargetSoC \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/NextChargeTargetTime.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/NextChargeTargetTime.lua new file mode 100644 index 0000000000..6462d42978 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/NextChargeTargetTime.lua @@ -0,0 +1,64 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local NextChargeTargetTime = { + ID = 0x0024, + NAME = "NextChargeTargetTime", + base_type = data_types.Uint32, +} + +function NextChargeTargetTime:new_value(...) + local o = self.base_type(table.unpack({...})) + return o +end + +function NextChargeTargetTime:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function NextChargeTargetTime:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function NextChargeTargetTime:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function NextChargeTargetTime: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 NextChargeTargetTime:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + return data +end + +setmetatable(NextChargeTargetTime, {__call = NextChargeTargetTime.new_value}) +return NextChargeTargetTime \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/RandomizationDelayWindow.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/RandomizationDelayWindow.lua new file mode 100644 index 0000000000..d9036e9a29 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/RandomizationDelayWindow.lua @@ -0,0 +1,91 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local RandomizationDelayWindow = { + ID = 0x000A, + NAME = "RandomizationDelayWindow", + base_type = data_types.Uint32, +} + +RandomizationDelayWindow.enum_fields = {} + +function RandomizationDelayWindow:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function RandomizationDelayWindow.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, RandomizationDelayWindow.enum_fields[value_obj.value]) +end + +function RandomizationDelayWindow:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function RandomizationDelayWindow:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function RandomizationDelayWindow:write(device, endpoint_id, value) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.write( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, --event_id + data + ) +end + +function RandomizationDelayWindow:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function RandomizationDelayWindow:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function RandomizationDelayWindow:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function RandomizationDelayWindow:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(RandomizationDelayWindow, {__call = RandomizationDelayWindow.new_value}) +return RandomizationDelayWindow diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/SessionDuration.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/SessionDuration.lua new file mode 100644 index 0000000000..f87e0d59ad --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/SessionDuration.lua @@ -0,0 +1,78 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SessionDuration = { + ID = 0x0041, + NAME = "SessionDuration", + base_type = data_types.Uint32, +} + +SessionDuration.enum_fields = {} + +function SessionDuration:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function SessionDuration.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, SessionDuration.enum_fields[value_obj.value]) +end + +function SessionDuration:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function SessionDuration:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function SessionDuration:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function SessionDuration:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SessionDuration:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function SessionDuration:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SessionDuration, {__call = SessionDuration.new_value}) +return SessionDuration diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/SessionEnergyCharged.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/SessionEnergyCharged.lua new file mode 100644 index 0000000000..b7c4275961 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/SessionEnergyCharged.lua @@ -0,0 +1,78 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SessionEnergyCharged = { + ID = 0x0042, + NAME = "SessionEnergyCharged", + base_type = data_types.Int64, +} + +SessionEnergyCharged.enum_fields = {} + +function SessionEnergyCharged:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function SessionEnergyCharged.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, SessionEnergyCharged.enum_fields[value_obj.value]) +end + +function SessionEnergyCharged:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function SessionEnergyCharged:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function SessionEnergyCharged:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function SessionEnergyCharged:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SessionEnergyCharged:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function SessionEnergyCharged:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SessionEnergyCharged, {__call = SessionEnergyCharged.new_value}) +return SessionEnergyCharged diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/SessionEnergyDischarged.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/SessionEnergyDischarged.lua new file mode 100644 index 0000000000..cfcee83351 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/SessionEnergyDischarged.lua @@ -0,0 +1,78 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SessionEnergyDischarged = { + ID = 0x0043, + NAME = "SessionEnergyDischarged", + base_type = data_types.Int64, +} + +SessionEnergyDischarged.enum_fields = {} + +function SessionEnergyDischarged:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function SessionEnergyDischarged.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, SessionEnergyDischarged.enum_fields[value_obj.value]) +end + +function SessionEnergyDischarged:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function SessionEnergyDischarged:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function SessionEnergyDischarged:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function SessionEnergyDischarged:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SessionEnergyDischarged:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function SessionEnergyDischarged:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SessionEnergyDischarged, {__call = SessionEnergyDischarged.new_value}) +return SessionEnergyDischarged diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/SessionID.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/SessionID.lua new file mode 100644 index 0000000000..f8de5a2a9c --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/SessionID.lua @@ -0,0 +1,65 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SessionID = { + ID = 0x0040, + NAME = "SessionID", + base_type = data_types.Uint32, +} + +function SessionID:new_value(...) + local o = self.base_type(table.unpack({...})) + return o +end + +function SessionID:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function SessionID:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function SessionID:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SessionID: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 SessionID:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + return data +end + +setmetatable(SessionID, {__call = SessionID.new_value}) +return SessionID + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/State.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/State.lua new file mode 100644 index 0000000000..d063ed4e0d --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/State.lua @@ -0,0 +1,93 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local State = { + ID = 0x0000, + NAME = "State", + base_type = data_types.Uint8, +} +State.NOT_PLUGGED_IN = 0x00 +State.PLUGGED_IN_NO_DEMAND = 0x01 +State.PLUGGED_IN_DEMAND = 0x02 +State.PLUGGED_IN_CHARGING = 0x03 +State.PLUGGED_IN_DISCHARGING = 0x04 +State.SESSION_ENDING = 0x05 +State.FAULT = 0x06 + +State.enum_fields = { + [State.NOT_PLUGGED_IN] = "NOT_PLUGGED_IN", + [State.PLUGGED_IN_NO_DEMAND] = "PLUGGED_IN_NO_DEMAND", + [State.PLUGGED_IN_DEMAND] = "PLUGGED_IN_DEMAND", + [State.PLUGGED_IN_CHARGING] = "PLUGGED_IN_CHARGING", + [State.PLUGGED_IN_DISCHARGING] = "PLUGGED_IN_DISCHARGING", + [State.SESSION_ENDING] = "SESSION_ENDING", + [State.FAULT] = "FAULT", +} + +function State:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function State.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, State.enum_fields[value_obj.value]) +end + +function State:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function State:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function State:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function State:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function State:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function State:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(State, {__call = State.new_value}) +return State diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/StateOfCharge.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/StateOfCharge.lua new file mode 100644 index 0000000000..6ca2b9d789 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/StateOfCharge.lua @@ -0,0 +1,64 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local StateOfCharge = { + ID = 0x0030, + NAME = "StateOfCharge", + base_type = data_types.Uint8, +} + +function StateOfCharge:new_value(...) + local o = self.base_type(table.unpack({...})) + return o +end + +function StateOfCharge:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function StateOfCharge:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function StateOfCharge:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function StateOfCharge: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 StateOfCharge:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + return data +end + +setmetatable(StateOfCharge, {__call = StateOfCharge.new_value}) +return StateOfCharge \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/SupplyState.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/SupplyState.lua new file mode 100644 index 0000000000..8b86968725 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/SupplyState.lua @@ -0,0 +1,89 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SupplyState = { + ID = 0x0001, + NAME = "SupplyState", + base_type = data_types.Uint8, +} +SupplyState.DISABLED = 0x00 +SupplyState.CHARGING_ENABLED = 0x01 +SupplyState.DISCHARGING_ENABLED = 0x02 +SupplyState.DISABLED_ERROR = 0x03 +SupplyState.DISABLED_DIAGNOSTICS = 0x04 + +SupplyState.enum_fields = { + [SupplyState.DISABLED] = "DISABLED", + [SupplyState.CHARGING_ENABLED] = "CHARGING_ENABLED", + [SupplyState.DISCHARGING_ENABLED] = "DISCHARGING_ENABLED", + [SupplyState.DISABLED_ERROR] = "DISABLED_ERROR", + [SupplyState.DISABLED_DIAGNOSTICS] = "DISABLED_DIAGNOSTICS", +} + +function SupplyState:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function SupplyState.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, SupplyState.enum_fields[value_obj.value]) +end + +function SupplyState:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function SupplyState:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function SupplyState:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function SupplyState:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SupplyState:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function SupplyState:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SupplyState, {__call = SupplyState.new_value}) +return SupplyState diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/UserMaximumChargeCurrent.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/UserMaximumChargeCurrent.lua new file mode 100644 index 0000000000..9241457b2d --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/UserMaximumChargeCurrent.lua @@ -0,0 +1,93 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local UserMaximumChargeCurrent = { + ID = 0x0009, + NAME = "UserMaximumChargeCurrent", + base_type = data_types.Int64, +} + +UserMaximumChargeCurrent.enum_fields = { +} + +function UserMaximumChargeCurrent:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function UserMaximumChargeCurrent.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, UserMaximumChargeCurrent.enum_fields[value_obj.value]) +end + +function UserMaximumChargeCurrent:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function UserMaximumChargeCurrent:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function UserMaximumChargeCurrent:write(device, endpoint_id, value) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.write( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, --event_id + data + ) +end + +function UserMaximumChargeCurrent:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function UserMaximumChargeCurrent:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function UserMaximumChargeCurrent:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function UserMaximumChargeCurrent:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(UserMaximumChargeCurrent, {__call = UserMaximumChargeCurrent.new_value}) +return UserMaximumChargeCurrent + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/VehicleID.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/VehicleID.lua new file mode 100644 index 0000000000..9e6c7652f0 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/VehicleID.lua @@ -0,0 +1,64 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local VehicleID = { + ID = 0x0032, + NAME = "VehicleID", + base_type = data_types.UTF8String1, +} + +function VehicleID:new_value(...) + local o = self.base_type(table.unpack({...})) + return o +end + +function VehicleID:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function VehicleID:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function VehicleID:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function VehicleID: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 VehicleID:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + return data +end + +setmetatable(VehicleID, {__call = VehicleID.new_value}) +return VehicleID \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/init.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/attributes/init.lua new file mode 100644 index 0000000000..859f762e0e --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/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("EnergyEvse.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 EnergyEvseServerAttributes = {} + +function EnergyEvseServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(EnergyEvseServerAttributes, attr_mt) + +return EnergyEvseServerAttributes + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/ClearTargets.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/ClearTargets.lua new file mode 100644 index 0000000000..c6ee9f6fd4 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/ClearTargets.lua @@ -0,0 +1,84 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ClearTargets = {} + +ClearTargets.NAME = "ClearTargets" +ClearTargets.ID = 0x0007 +ClearTargets.field_defs = { +} + +function ClearTargets:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, --tlv + status + ) +end + +function ClearTargets:init(device, endpoint_id) + local out = {} + local args = {} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.optional and args[i] == nil then + out[v.name] = nil + elseif v.nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = ClearTargets, + __tostring = ClearTargets.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + true + ) +end + +function ClearTargets:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ClearTargets:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + end + end + end + base_type_obj.elements = elems +end + +function ClearTargets:deserialize(tlv_buf) + return TLVParser.decode_tlv(tlv_buf) +end + +setmetatable(ClearTargets, {__call = ClearTargets.init}) + +return ClearTargets \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/Disable.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/Disable.lua new file mode 100644 index 0000000000..bbbdc44735 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/Disable.lua @@ -0,0 +1,84 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local Disable = {} + +Disable.NAME = "Disable" +Disable.ID = 0x0001 +Disable.field_defs = { +} + +function Disable:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, --tlv + status + ) +end + +function Disable:init(device, endpoint_id) + local out = {} + local args = {} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.optional and args[i] == nil then + out[v.name] = nil + elseif v.nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = Disable, + __tostring = Disable.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + true + ) +end + +function Disable:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function Disable:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + end + end + end + base_type_obj.elements = elems +end + +function Disable:deserialize(tlv_buf) + return TLVParser.decode_tlv(tlv_buf) +end + +setmetatable(Disable, {__call = Disable.init}) + +return Disable \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/EnableCharging.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/EnableCharging.lua new file mode 100644 index 0000000000..328dd34692 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/EnableCharging.lua @@ -0,0 +1,106 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local amperage_maType = require "st.matter.data_types.Int64" + +local EnableCharging = {} + +EnableCharging.NAME = "EnableCharging" +EnableCharging.ID = 0x0002 +EnableCharging.field_defs = { + { + name = "charging_enabled_until", + field_id = 0, + optional = false, + nullable = true, + data_type = data_types.Uint32, + }, + { + name = "minimum_charge_current", + field_id = 1, + optional = false, + nullable = false, + data_type = amperage_maType, + }, + { + name = "maximum_charge_current", + field_id = 2, + optional = false, + nullable = false, + data_type = amperage_maType, + }, +} + +function EnableCharging:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, --tlv + status + ) +end + +function EnableCharging:init(device, endpoint_id, charging_enabled_until, minimum_charge_current, maximum_charge_current) + local out = {} + local args = {charging_enabled_until, minimum_charge_current, maximum_charge_current} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.optional and args[i] == nil then + out[v.name] = nil + elseif v.nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = EnableCharging, + __tostring = EnableCharging.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + true + ) +end + +function EnableCharging:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function EnableCharging:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + end + end + end + base_type_obj.elements = elems +end + +function EnableCharging:deserialize(tlv_buf) + return TLVParser.decode_tlv(tlv_buf) +end + +setmetatable(EnableCharging, {__call = EnableCharging.init}) + +return EnableCharging \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/EnableDischarging.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/EnableDischarging.lua new file mode 100644 index 0000000000..74c4f3f30a --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/EnableDischarging.lua @@ -0,0 +1,99 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local amperage_maType = require "st.matter.data_types.Int64" + +local EnableDischarging = {} + +EnableDischarging.NAME = "EnableDischarging" +EnableDischarging.ID = 0x0003 +EnableDischarging.field_defs = { + { + name = "discharging_enabled_until", + field_id = 0, + optional = false, + nullable = true, + data_type = data_types.Uint32, + }, + { + name = "maximum_discharge_current", + field_id = 1, + optional = false, + nullable = false, + data_type = amperage_maType, + }, +} + +function EnableDischarging:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, --tlv + status + ) +end + +function EnableDischarging:init(device, endpoint_id, discharging_enabled_until, maximum_discharge_current) + local out = {} + local args = {discharging_enabled_until, maximum_discharge_current} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.optional and args[i] == nil then + out[v.name] = nil + elseif v.nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = EnableDischarging, + __tostring = EnableDischarging.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + true + ) +end + +function EnableDischarging:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function EnableDischarging:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + end + end + end + base_type_obj.elements = elems +end + +function EnableDischarging:deserialize(tlv_buf) + return TLVParser.decode_tlv(tlv_buf) +end + +setmetatable(EnableDischarging, {__call = EnableDischarging.init}) + +return EnableDischarging \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/GetTargets.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/GetTargets.lua new file mode 100644 index 0000000000..44c9085f88 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/GetTargets.lua @@ -0,0 +1,72 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local GetTargets = {} + +GetTargets.NAME = "GetTargets" +GetTargets.ID = 0x0006 +GetTargets.field_defs = {} + +function GetTargets:init(device, endpoint_id) + local out = {} + local args = {} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.optional and args[i] == nil then + out[v.name] = nil + elseif v.nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = GetTargets, + __tostring = GetTargets.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + true + ) +end + +function GetTargets:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function GetTargets:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + end + end + end + base_type_obj.elements = elems +end + +function GetTargets:deserialize(tlv_buf) + return TLVParser.decode_tlv(tlv_buf) +end + +setmetatable(GetTargets, {__call = GetTargets.init}) + +return GetTargets \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/SetTargets.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/SetTargets.lua new file mode 100644 index 0000000000..65895b5053 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/SetTargets.lua @@ -0,0 +1,93 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local ChargingTargetScheduleStructType = require "EnergyEvsetypes.ChargingTargetScheduleStruct" + +local SetTargets = {} + +SetTargets.NAME = "SetTargets" +SetTargets.ID = 0x0005 +SetTargets.field_defs = { + { + name = "charging_target_schedules", + field_id = 0, + optional = false, + nullable = false, + data_type = data_types.Array, + array_type = ChargingTargetScheduleStructType, + }, +} + +function SetTargets:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, --tlv + status + ) +end + +function SetTargets:init(device, endpoint_id, charging_target_schedules) + local out = {} + local args = {charging_target_schedules} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.optional and args[i] == nil then + out[v.name] = nil + elseif v.nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = SetTargets, + __tostring = SetTargets.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + true + ) +end + +function SetTargets:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SetTargets:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + end + end + end + base_type_obj.elements = elems +end + +function SetTargets:deserialize(tlv_buf) + return TLVParser.decode_tlv(tlv_buf) +end + +setmetatable(SetTargets, {__call = SetTargets.init}) + +return SetTargets \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/StartDiagnostics.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/StartDiagnostics.lua new file mode 100644 index 0000000000..e8c6bd2c5e --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/StartDiagnostics.lua @@ -0,0 +1,84 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local StartDiagnostics = {} + +StartDiagnostics.NAME = "StartDiagnostics" +StartDiagnostics.ID = 0x0004 +StartDiagnostics.field_defs = { +} + +function StartDiagnostics:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, --tlv + status + ) +end + +function StartDiagnostics:init(device, endpoint_id) + local out = {} + local args = {} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.optional and args[i] == nil then + out[v.name] = nil + elseif v.nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = StartDiagnostics, + __tostring = StartDiagnostics.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + true + ) +end + +function StartDiagnostics:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function StartDiagnostics:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + end + end + end + base_type_obj.elements = elems +end + +function StartDiagnostics:deserialize(tlv_buf) + return TLVParser.decode_tlv(tlv_buf) +end + +setmetatable(StartDiagnostics, {__call = StartDiagnostics.init}) + +return StartDiagnostics \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/init.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/init.lua new file mode 100644 index 0000000000..dbe3f22200 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/commands/init.lua @@ -0,0 +1,23 @@ +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("EnergyEvse.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local EnergyEvseServerCommands = {} + +function EnergyEvseServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(EnergyEvseServerCommands, command_mt) + +return EnergyEvseServerCommands + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/EVConnected.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/EVConnected.lua new file mode 100644 index 0000000000..3790cec4a3 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/EVConnected.lua @@ -0,0 +1,97 @@ +local data_types = require "st.matter.data_types" +local cluster_base = require "st.matter.cluster_base" +local TLVParser = require "st.matter.TLV.TLVParser" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local EVConnected = { + ID = 0x0000, + NAME = "EVConnected", + base_type = data_types.Structure, +} + +EVConnected.field_defs = { + { + data_type = data_types.Uint32, + field_id = 0, + is_array = false, + name = "session_id", + is_nullable = false, + is_optional = false, + }, +} + +function EVConnected:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and not + ((field_def.is_nullable or field_def.is_optional) and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + elems[field_def.name].field_name = field_def.name + end + end + end + base_type_obj.elements = elems +end + +function EVConnected:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + nil, --attribute_id + self.ID + ) +end + +function EVConnected:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + nil, --attribute_id + self.ID + ) +end + +function EVConnected:build_test_event_report( + device, + endpoint_id, + fields, + status +) + local data = {} + data.elements = {} + data.num_elements = 0 + setmetatable(data, StructureABC.new_mt({NAME = "EVConnectedEventData", ID = 0x15})) + for idx, field_def in ipairs(self.field_defs) do --Note: idx is 1 when field_id is 0 + if (not field_def.is_optional or not field_def.is_nullable) and not fields[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + elseif fields[field_def.name] then + data.elements[field_def.name] = data_types.validate_or_build_type(fields[field_def.name], field_def.data_type, field_def.name) + data.elements[field_def.name].field_id = field_def.field_id + data.num_elements = data.num_elements + 1 + end + end + return cluster_base.build_test_event_report( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function EVConnected:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +function EVConnected:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +return EVConnected diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/EVNotDetected.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/EVNotDetected.lua new file mode 100644 index 0000000000..d4796814cd --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/EVNotDetected.lua @@ -0,0 +1,129 @@ +local data_types = require "st.matter.data_types" +local cluster_base = require "st.matter.cluster_base" +local TLVParser = require "st.matter.TLV.TLVParser" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +local StateEnum = require "EnergyEvsetypes.StateEnum" +local EVNotDetected = { + ID = 0x0001, + NAME = "EVNotDetected", + base_type = data_types.Structure, +} + +EVNotDetected.field_defs = { + { + data_type = data_types.Uint32, + field_id = 0, + is_array = false, + name = "session_id", + is_nullable = false, + is_optional = false, + }, + { + data_type = StateEnum, + field_id = 1, + is_array = false, + name = "state", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Uint32, + field_id = 2, + is_array = false, + name = "session_duration", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Int64, + field_id = 3, + is_array = false, + name = "session_energy_charged", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Int64, + field_id = 4, + is_array = false, + name = "session_energy_discharged", + is_nullable = false, + is_optional = false, + }, +} + +function EVNotDetected:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and not + ((field_def.is_nullable or field_def.is_optional) and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + elems[field_def.name].field_name = field_def.name + end + end + end + base_type_obj.elements = elems +end + +function EVNotDetected:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + nil, --attribute_id + self.ID + ) +end + +function EVNotDetected:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + nil, --attribute_id + self.ID + ) +end + +function EVNotDetected:build_test_event_report( + device, + endpoint_id, + fields, + status +) + local data = {} + data.elements = {} + data.num_elements = 0 + setmetatable(data, StructureABC.new_mt({NAME = "EVNotDetectedEventData", ID = 0x15})) + for idx, field_def in ipairs(self.field_defs) do --Note: idx is 1 when field_id is 0 + if (not field_def.is_optional or not field_def.is_nullable) and not fields[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + elseif fields[field_def.name] then + data.elements[field_def.name] = data_types.validate_or_build_type(fields[field_def.name], field_def.data_type, field_def.name) + data.elements[field_def.name].field_id = field_def.field_id + data.num_elements = data.num_elements + 1 + end + end + return cluster_base.build_test_event_report( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function EVNotDetected:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +function EVNotDetected:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +return EVNotDetected diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/EnergyTransferStarted.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/EnergyTransferStarted.lua new file mode 100644 index 0000000000..dc39ca0ca8 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/EnergyTransferStarted.lua @@ -0,0 +1,114 @@ +local data_types = require "st.matter.data_types" +local cluster_base = require "st.matter.cluster_base" +local TLVParser = require "st.matter.TLV.TLVParser" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +local StateEnum = require "EnergyEvsetypes.StateEnum" + +local EnergyTransferStarted = { + ID = 0x0002, + NAME = "EnergyTransferStarted", + base_type = data_types.Structure, +} + +EnergyTransferStarted.field_defs = { + { + data_type = data_types.Uint32, + field_id = 0, + is_array = false, + name = "session_id", + is_nullable = false, + is_optional = false, + }, + { + data_type = StateEnum, + field_id = 1, + is_array = false, + name = "state", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Int64, + field_id = 2, + is_array = false, + name = "maximum_current", + is_nullable = false, + is_optional = false, + }, +} + +function EnergyTransferStarted:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and not + ((field_def.is_nullable or field_def.is_optional) and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + elems[field_def.name].field_name = field_def.name + end + end + end + base_type_obj.elements = elems +end + +function EnergyTransferStarted:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + nil, --attribute_id + self.ID + ) +end + +function EnergyTransferStarted:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + nil, --attribute_id + self.ID + ) +end + +function EnergyTransferStarted:build_test_event_report( + device, + endpoint_id, + fields, + status +) + local data = {} + data.elements = {} + data.num_elements = 0 + setmetatable(data, StructureABC.new_mt({NAME = "EnergyTransferStartedEventData", ID = 0x15})) + for idx, field_def in ipairs(self.field_defs) do --Note: idx is 1 when field_id is 0 + if (not field_def.is_optional or not field_def.is_nullable) and not fields[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + elseif fields[field_def.name] then + data.elements[field_def.name] = data_types.validate_or_build_type(fields[field_def.name], field_def.data_type, field_def.name) + data.elements[field_def.name].field_id = field_def.field_id + data.num_elements = data.num_elements + 1 + end + end + return cluster_base.build_test_event_report( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function EnergyTransferStarted:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +function EnergyTransferStarted:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +return EnergyTransferStarted \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/EnergyTransferStopped.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/EnergyTransferStopped.lua new file mode 100644 index 0000000000..884dc90e51 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/EnergyTransferStopped.lua @@ -0,0 +1,124 @@ +local data_types = require "st.matter.data_types" +local cluster_base = require "st.matter.cluster_base" +local TLVParser = require "st.matter.TLV.TLVParser" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +local StateEnum = require "EnergyEvsetypes.StateEnum" +local EnergyTransferStoppedReasonEnum = require "EnergyEvsetypes.EnergyTransferStoppedReasonEnum" + +local EnergyTransferStopped = { + ID = 0x0003, + NAME = "EnergyTransferStopped", + base_type = data_types.Structure, +} + +EnergyTransferStopped.field_defs = { + { + data_type = data_types.Uint32, + field_id = 0, + is_array = false, + name = "session_id", + is_nullable = false, + is_optional = false, + }, + { + data_type = StateEnum, + field_id = 1, + is_array = false, + name = "state", + is_nullable = false, + is_optional = false, + }, + { + data_type = EnergyTransferStoppedReasonEnum, + field_id = 2, + is_array = false, + name = "reason", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Int64, + field_id = 4, + is_array = false, + name = "energy_transferred", + is_nullable = false, + is_optional = false, + }, +} + +function EnergyTransferStopped:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and not + ((field_def.is_nullable or field_def.is_optional) and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + elems[field_def.name].field_name = field_def.name + end + end + end + base_type_obj.elements = elems +end + +function EnergyTransferStopped:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + nil, --attribute_id + self.ID + ) +end + +function EnergyTransferStopped:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + nil, --attribute_id + self.ID + ) +end + +function EnergyTransferStopped:build_test_event_report( + device, + endpoint_id, + fields, + status +) + local data = {} + data.elements = {} + data.num_elements = 0 + setmetatable(data, StructureABC.new_mt({NAME = "EnergyTransferStoppedEventData", ID = 0x15})) + for idx, field_def in ipairs(self.field_defs) do --Note: idx is 1 when field_id is 0 + if (not field_def.is_optional or not field_def.is_nullable) and not fields[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + elseif fields[field_def.name] then + data.elements[field_def.name] = data_types.validate_or_build_type(fields[field_def.name], field_def.data_type, field_def.name) + data.elements[field_def.name].field_id = field_def.field_id + data.num_elements = data.num_elements + 1 + end + end + return cluster_base.build_test_event_report( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function EnergyTransferStopped:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +function EnergyTransferStopped:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +return EnergyTransferStopped + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/Fault.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/Fault.lua new file mode 100644 index 0000000000..86fb42e3f0 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/Fault.lua @@ -0,0 +1,123 @@ +local data_types = require "st.matter.data_types" +local cluster_base = require "st.matter.cluster_base" +local TLVParser = require "st.matter.TLV.TLVParser" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +local StateEnum = require "EnergyEvsetypes.StateEnum" +local FaultStateEnum = require "EnergyEvsetypes.FaultStateEnum" + +local Fault = { + ID = 0x0004, + NAME = "Fault", + base_type = data_types.Structure, +} + +Fault.field_defs = { + { + data_type = data_types.Uint32, + field_id = 0, + is_array = false, + name = "session_id", + is_nullable = true, + is_optional = true, + }, + { + data_type = StateEnum, + field_id = 1, + is_array = false, + name = "state", + is_nullable = false, + is_optional = false, + }, + { + data_type = FaultStateEnum, + field_id = 2, + is_array = false, + name = "fault_state_previous_state", + is_nullable = false, + is_optional = false, + }, + { + data_type = FaultStateEnum, + field_id = 4, + is_array = false, + name = "fault_state_current_state", + is_nullable = false, + is_optional = false, + }, +} + +function Fault:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and not + ((field_def.is_nullable or field_def.is_optional) and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + elems[field_def.name].field_name = field_def.name + end + end + end + base_type_obj.elements = elems +end + +function Fault:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + nil, --attribute_id + self.ID + ) +end + +function Fault:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + nil, --attribute_id + self.ID + ) +end + +function Fault:build_test_event_report( + device, + endpoint_id, + fields, + status +) + local data = {} + data.elements = {} + data.num_elements = 0 + setmetatable(data, StructureABC.new_mt({NAME = "FaultEventData", ID = 0x15})) + for idx, field_def in ipairs(self.field_defs) do --Note: idx is 1 when field_id is 0 + if (not field_def.is_optional or not field_def.is_nullable) and not fields[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + elseif fields[field_def.name] then + data.elements[field_def.name] = data_types.validate_or_build_type(fields[field_def.name], field_def.data_type, field_def.name) + data.elements[field_def.name].field_id = field_def.field_id + data.num_elements = data.num_elements + 1 + end + end + return cluster_base.build_test_event_report( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function Fault:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +function Fault:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +return Fault \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/Rfid.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/Rfid.lua new file mode 100644 index 0000000000..e5bfb998fd --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/Rfid.lua @@ -0,0 +1,98 @@ +local data_types = require "st.matter.data_types" +local cluster_base = require "st.matter.cluster_base" +local TLVParser = require "st.matter.TLV.TLVParser" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local Rfid = { + ID = 0x0005, + NAME = "Rfid", + base_type = data_types.Structure, +} + +Rfid.field_defs = { + { + data_type = data_types.OctetString1, + field_id = 0, + is_array = false, + name = "uid", + is_nullable = false, + is_optional = false, + }, +} + +function Rfid:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and not + ((field_def.is_nullable or field_def.is_optional) and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + elems[field_def.name].field_name = field_def.name + end + end + end + base_type_obj.elements = elems +end + +function Rfid:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + nil, --attribute_id + self.ID + ) +end + +function Rfid:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + nil, --attribute_id + self.ID + ) +end + +function Rfid:build_test_event_report( + device, + endpoint_id, + fields, + status +) + local data = {} + data.elements = {} + data.num_elements = 0 + setmetatable(data, StructureABC.new_mt({NAME = "RfidEventData", ID = 0x15})) + for idx, field_def in ipairs(self.field_defs) do --Note: idx is 1 when field_id is 0 + if (not field_def.is_optional or not field_def.is_nullable) and not fields[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + elseif fields[field_def.name] then + data.elements[field_def.name] = data_types.validate_or_build_type(fields[field_def.name], field_def.data_type, field_def.name) + data.elements[field_def.name].field_id = field_def.field_id + data.num_elements = data.num_elements + 1 + end + end + return cluster_base.build_test_event_report( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function Rfid:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +function Rfid:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +return Rfid + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/init.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/init.lua new file mode 100644 index 0000000000..12268886f1 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/server/events/init.lua @@ -0,0 +1,24 @@ +local event_mt = {} +event_mt.__event_cache = {} +event_mt.__index = function(self, key) + if event_mt.__event_cache[key] == nil then + local req_loc = string.format("EnergyEvse.server.events.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + event_mt.__event_cache[key] = raw_def + end + return event_mt.__event_cache[key] +end + +local EnergyEvseEvents = {} + +function EnergyEvseEvents:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(EnergyEvseEvents, event_mt) + +return EnergyEvseEvents + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/types/ChargingTargetScheduleStruct.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/ChargingTargetScheduleStruct.lua new file mode 100644 index 0000000000..86a641e1b8 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/ChargingTargetScheduleStruct.lua @@ -0,0 +1,71 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +local TargetDayOfWeekBitmap = require "EnergyEvsetypes.TargetDayOfWeekBitmap" +local ChargingTargetStruct = require "EnergyEvsetypes.ChargingTargetStruct" + +local ChargingTargetScheduleStruct = {} +local new_mt = StructureABC.new_mt({NAME = "ChargingTargetScheduleStruct", ID = data_types.name_to_id_map["Structure"]}) + +ChargingTargetScheduleStruct.field_defs = { + { + data_type = TargetDayOfWeekBitmap, + field_id = 0, + name = "day_of_week_for_sequence", + }, + { + data_type = ChargingTargetStruct, + field_id = 1, + name = "charging_targets", + }, +} + +ChargingTargetScheduleStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do --Note: idx is 1 when field_id is 0 + if (not field_def.is_optional or not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +ChargingTargetScheduleStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = ChargingTargetScheduleStruct.init +new_mt.__index.serialize = ChargingTargetScheduleStruct.serialize + +ChargingTargetScheduleStruct.augment_type = function(self, val) + local elems = {} + for _, v in ipairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.array_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.array_type) + end + end + end + end + end + val.elements = elems + setmetatable(val, new_mt) +end + +setmetatable(ChargingTargetScheduleStruct, new_mt) + +return ChargingTargetScheduleStruct \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/types/ChargingTargetStruct.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/ChargingTargetStruct.lua new file mode 100644 index 0000000000..f9c6f7e8c8 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/ChargingTargetStruct.lua @@ -0,0 +1,79 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local ChargingTargetStruct = {} +local new_mt = StructureABC.new_mt({NAME = "ChargingTargetStruct", ID = data_types.name_to_id_map["Structure"]}) + +ChargingTargetStruct.field_defs = { + { + data_type = data_types.Uint16, + field_id = 0, + name = "target_time_minutes_past_midnight", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Uint8, + field_id = 1, + name = "target_so_c", + is_nullable = false, + is_optional = false, + }, + { + data_type = data_types.Int64, + field_id = 2, + name = "added_energy", + }, +} + +ChargingTargetStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do --Note: idx is 1 when field_id is 0 + if (not field_def.is_optional or not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +ChargingTargetStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = ChargingTargetStruct.init +new_mt.__index.serialize = ChargingTargetStruct.serialize + +ChargingTargetStruct.augment_type = function(self, val) + local elems = {} + for _, v in ipairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.array_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.array_type) + end + end + end + end + end + val.elements = elems + setmetatable(val, new_mt) +end + +setmetatable(ChargingTargetStruct, new_mt) + +return ChargingTargetStruct + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/types/EnergyTransferStoppedReasonEnum.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/EnergyTransferStoppedReasonEnum.lua new file mode 100644 index 0000000000..76e0958be3 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/EnergyTransferStoppedReasonEnum.lua @@ -0,0 +1,26 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local EnergyTransferStoppedReasonEnum = {} +local new_mt = UintABC.new_mt({NAME = "EnergyTransferStoppedReasonEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.EV_STOPPED] = "EV_STOPPED", + [self.EVSE_STOPPED] = "EVSE_STOPPED", + [self.OTHER] = "OTHER", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.EV_STOPPED = 0x00 +new_mt.__index.EVSE_STOPPED = 0x01 +new_mt.__index.OTHER = 0x02 + +EnergyTransferStoppedReasonEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(EnergyTransferStoppedReasonEnum, new_mt) + +return EnergyTransferStoppedReasonEnum diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/types/FaultStateEnum.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/FaultStateEnum.lua new file mode 100644 index 0000000000..6013c0a251 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/FaultStateEnum.lua @@ -0,0 +1,54 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local FaultStateEnum = {} +local new_mt = UintABC.new_mt({NAME = "FaultStateEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.NO_ERROR] = "NO_ERROR", + [self.METER_FAILURE] = "METER_FAILURE", + [self.OVER_VOLTAGE] = "OVER_VOLTAGE", + [self.UNDER_VOLTAGE] = "UNDER_VOLTAGE", + [self.OVER_CURRENT] = "OVER_CURRENT", + [self.CONTACT_WET_FAILURE] = "CONTACT_WET_FAILURE", + [self.CONTACT_DRY_FAILURE] = "CONTACT_DRY_FAILURE", + [self.GROUND_FAULT] = "GROUND_FAULT", + [self.POWER_LOSS] = "POWER_LOSS", + [self.POWER_QUALITY] = "POWER_QUALITY", + [self.PILOT_SHORT_CIRCUIT] = "PILOT_SHORT_CIRCUIT", + [self.EMERGENCY_STOP] = "EMERGENCY_STOP", + [self.EV_DISCONNECTED] = "EV_DISCONNECTED", + [self.WRONG_POWER_SUPPLY] = "WRONG_POWER_SUPPLY", + [self.LIVE_NEUTRAL_SWAP] = "LIVE_NEUTRAL_SWAP", + [self.OVER_TEMPERATURE] = "OVER_TEMPERATURE", + [self.OTHER] = "OTHER", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.NO_ERROR = 0x00 +new_mt.__index.METER_FAILURE = 0x01 +new_mt.__index.OVER_VOLTAGE = 0x02 +new_mt.__index.UNDER_VOLTAGE = 0x03 +new_mt.__index.OVER_CURRENT = 0x04 +new_mt.__index.CONTACT_WET_FAILURE = 0x05 +new_mt.__index.CONTACT_DRY_FAILURE = 0x06 +new_mt.__index.GROUND_FAULT = 0x07 +new_mt.__index.POWER_LOSS = 0x08 +new_mt.__index.POWER_QUALITY = 0x09 +new_mt.__index.PILOT_SHORT_CIRCUIT = 0x0A +new_mt.__index.EMERGENCY_STOP = 0x0B +new_mt.__index.EV_DISCONNECTED = 0x0C +new_mt.__index.WRONG_POWER_SUPPLY = 0x0D +new_mt.__index.LIVE_NEUTRAL_SWAP = 0x0E +new_mt.__index.OVER_TEMPERATURE = 0x0F +new_mt.__index.OTHER = 0xFF + +FaultStateEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(FaultStateEnum, new_mt) + +return FaultStateEnum diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/types/Feature.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/Feature.lua new file mode 100644 index 0000000000..9e3e10b643 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/Feature.lua @@ -0,0 +1,138 @@ +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.CHARGING_PREFERENCES = 0x0001 +Feature.SOC_REPORTING = 0x0002 +Feature.PLUG_AND_CHARGE = 0x0004 +Feature.RFID = 0x0008 +Feature.V2X = 0x0010 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + CHARGING_PREFERENCES = 0x0001, + SOC_REPORTING = 0x0002, + PLUG_AND_CHARGE = 0x0004, + RFID = 0x0008, + V2X = 0x0010, +} + +Feature.is_charging_preferences_set = function(self) + return (self.value & self.CHARGING_PREFERENCES) ~= 0 +end + +Feature.set_charging_preferences = function(self) + if self.value ~= nil then + self.value = self.value | self.CHARGING_PREFERENCES + else + self.value = self.CHARGING_PREFERENCES + end +end + +Feature.unset_charging_preferences = function(self) + self.value = self.value & (~self.CHARGING_PREFERENCES & self.BASE_MASK) +end + +Feature.is_soc_reporting_set = function(self) + return (self.value & self.SOC_REPORTING) ~= 0 +end + +Feature.set_soc_reporting = function(self) + if self.value ~= nil then + self.value = self.value | self.SOC_REPORTING + else + self.value = self.SOC_REPORTING + end +end + +Feature.unset_soc_reporting = function(self) + self.value = self.value & (~self.SOC_REPORTING & self.BASE_MASK) +end + +Feature.is_plug_and_charge_set = function(self) + return (self.value & self.PLUG_AND_CHARGE) ~= 0 +end + +Feature.set_plug_and_charge = function(self) + if self.value ~= nil then + self.value = self.value | self.PLUG_AND_CHARGE + else + self.value = self.PLUG_AND_CHARGE + end +end + +Feature.unset_plug_and_charge = function(self) + self.value = self.value & (~self.PLUG_AND_CHARGE & self.BASE_MASK) +end + +Feature.is_rfid_set = function(self) + return (self.value & self.RFID) ~= 0 +end + +Feature.set_rfid = function(self) + if self.value ~= nil then + self.value = self.value | self.RFID + else + self.value = self.RFID + end +end + +Feature.unset_rfid = function(self) + self.value = self.value & (~self.RFID & self.BASE_MASK) +end + +Feature.is_v2x_set = function(self) + return (self.value & self.V2X) ~= 0 +end + +Feature.set_v2x = function(self) + if self.value ~= nil then + self.value = self.value | self.V2X + else + self.value = self.V2X + end +end + +Feature.unset_v2x = function(self) + self.value = self.value & (~self.V2X & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.CHARGING_PREFERENCES | Feature.SOC_REPORTING | Feature.PLUG_AND_CHARGE | Feature.RFID | Feature.V2X + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_charging_preferences_set = Feature.is_charging_preferences_set, + set_charging_preferences = Feature.set_charging_preferences, + unset_charging_preferences = Feature.unset_charging_preferences, + is_soc_reporting_set = Feature.is_soc_reporting_set, + set_soc_reporting = Feature.set_soc_reporting, + unset_soc_reporting = Feature.unset_soc_reporting, + is_plug_and_charge_set = Feature.is_plug_and_charge_set, + set_plug_and_charge = Feature.set_plug_and_charge, + unset_plug_and_charge = Feature.unset_plug_and_charge, + is_rfid_set = Feature.is_rfid_set, + set_rfid = Feature.set_rfid, + unset_rfid = Feature.unset_rfid, + is_v2x_set = Feature.is_v2x_set, + set_v2x = Feature.set_v2x, + unset_v2x = Feature.unset_v2x, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/types/StateEnum.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/StateEnum.lua new file mode 100644 index 0000000000..3f01167718 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/StateEnum.lua @@ -0,0 +1,34 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local StateEnum = {} +local new_mt = UintABC.new_mt({NAME = "StateEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.NOT_PLUGGED_IN] = "NOT_PLUGGED_IN", + [self.PLUGGED_IN_NO_DEMAND] = "PLUGGED_IN_NO_DEMAND", + [self.PLUGGED_IN_DEMAND] = "PLUGGED_IN_DEMAND", + [self.PLUGGED_IN_CHARGING] = "PLUGGED_IN_CHARGING", + [self.PLUGGED_IN_DISCHARGING] = "PLUGGED_IN_DISCHARGING", + [self.SESSION_ENDING] = "SESSION_ENDING", + [self.FAULT] = "FAULT", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.NOT_PLUGGED_IN = 0x00 +new_mt.__index.PLUGGED_IN_NO_DEMAND = 0x01 +new_mt.__index.PLUGGED_IN_DEMAND = 0x02 +new_mt.__index.PLUGGED_IN_CHARGING = 0x03 +new_mt.__index.PLUGGED_IN_DISCHARGING = 0x04 +new_mt.__index.SESSION_ENDING = 0x05 +new_mt.__index.FAULT = 0x06 + +StateEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(StateEnum, new_mt) + +return StateEnum diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/types/SupplyStateEnum.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/SupplyStateEnum.lua new file mode 100644 index 0000000000..cf7764f8cc --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/SupplyStateEnum.lua @@ -0,0 +1,30 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local SupplyStateEnum = {} +local new_mt = UintABC.new_mt({NAME = "SupplyStateEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.DISABLED] = "DISABLED", + [self.CHARGING_ENABLED] = "CHARGING_ENABLED", + [self.DISCHARGING_ENABLED] = "DISCHARGING_ENABLED", + [self.DISABLED_ERROR] = "DISABLED_ERROR", + [self.DISABLED_DIAGNOSTICS] = "DISABLED_DIAGNOSTICS", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.DISABLED = 0x00 +new_mt.__index.CHARGING_ENABLED = 0x01 +new_mt.__index.DISCHARGING_ENABLED = 0x02 +new_mt.__index.DISABLED_ERROR = 0x03 +new_mt.__index.DISABLED_DIAGNOSTICS = 0x04 + +SupplyStateEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(SupplyStateEnum, new_mt) + +return SupplyStateEnum diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/types/TargetDayOfWeekBitmap.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/TargetDayOfWeekBitmap.lua new file mode 100644 index 0000000000..4a874f721b --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/TargetDayOfWeekBitmap.lua @@ -0,0 +1,171 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local TargetDayOfWeekBitmap = {} +local new_mt = UintABC.new_mt({NAME = "TargetDayOfWeekBitmap", ID = data_types.name_to_id_map["Uint8"]}, 1) + +TargetDayOfWeekBitmap.BASE_MASK = 0xFFFF +TargetDayOfWeekBitmap.SUNDAY = 0x0001 +TargetDayOfWeekBitmap.MONDAY = 0x0002 +TargetDayOfWeekBitmap.TUESDAY = 0x0004 +TargetDayOfWeekBitmap.WEDNESDAY = 0x0008 +TargetDayOfWeekBitmap.THURSDAY = 0x0010 +TargetDayOfWeekBitmap.FRIDAY = 0x0020 +TargetDayOfWeekBitmap.SATURDAY = 0x0040 + +TargetDayOfWeekBitmap.mask_fields = { + BASE_MASK = 0xFFFF, + SUNDAY = 0x0001, + MONDAY = 0x0002, + TUESDAY = 0x0004, + WEDNESDAY = 0x0008, + THURSDAY = 0x0010, + FRIDAY = 0x0020, + SATURDAY = 0x0040, +} + +TargetDayOfWeekBitmap.is_sunday_set = function(self) + return (self.value & self.SUNDAY) ~= 0 +end + +TargetDayOfWeekBitmap.set_sunday = function(self) + if self.value ~= nil then + self.value = self.value | self.SUNDAY + else + self.value = self.SUNDAY + end +end + +TargetDayOfWeekBitmap.unset_sunday = function(self) + self.value = self.value & (~self.SUNDAY & self.BASE_MASK) +end + +TargetDayOfWeekBitmap.is_monday_set = function(self) + return (self.value & self.MONDAY) ~= 0 +end + +TargetDayOfWeekBitmap.set_monday = function(self) + if self.value ~= nil then + self.value = self.value | self.MONDAY + else + self.value = self.MONDAY + end +end + +TargetDayOfWeekBitmap.unset_monday = function(self) + self.value = self.value & (~self.MONDAY & self.BASE_MASK) +end + +TargetDayOfWeekBitmap.is_tuesday_set = function(self) + return (self.value & self.TUESDAY) ~= 0 +end + +TargetDayOfWeekBitmap.set_tuesday = function(self) + if self.value ~= nil then + self.value = self.value | self.TUESDAY + else + self.value = self.TUESDAY + end +end + +TargetDayOfWeekBitmap.unset_tuesday = function(self) + self.value = self.value & (~self.TUESDAY & self.BASE_MASK) +end + +TargetDayOfWeekBitmap.is_wednesday_set = function(self) + return (self.value & self.WEDNESDAY) ~= 0 +end + +TargetDayOfWeekBitmap.set_wednesday = function(self) + if self.value ~= nil then + self.value = self.value | self.WEDNESDAY + else + self.value = self.WEDNESDAY + end +end + +TargetDayOfWeekBitmap.unset_wednesday = function(self) + self.value = self.value & (~self.WEDNESDAY & self.BASE_MASK) +end + +TargetDayOfWeekBitmap.is_thursday_set = function(self) + return (self.value & self.THURSDAY) ~= 0 +end + +TargetDayOfWeekBitmap.set_thursday = function(self) + if self.value ~= nil then + self.value = self.value | self.THURSDAY + else + self.value = self.THURSDAY + end +end + +TargetDayOfWeekBitmap.unset_thursday = function(self) + self.value = self.value & (~self.THURSDAY & self.BASE_MASK) +end + +TargetDayOfWeekBitmap.is_friday_set = function(self) + return (self.value & self.FRIDAY) ~= 0 +end + +TargetDayOfWeekBitmap.set_friday = function(self) + if self.value ~= nil then + self.value = self.value | self.FRIDAY + else + self.value = self.FRIDAY + end +end + +TargetDayOfWeekBitmap.unset_friday = function(self) + self.value = self.value & (~self.FRIDAY & self.BASE_MASK) +end + +TargetDayOfWeekBitmap.is_saturday_set = function(self) + return (self.value & self.SATURDAY) ~= 0 +end + +TargetDayOfWeekBitmap.set_saturday = function(self) + if self.value ~= nil then + self.value = self.value | self.SATURDAY + else + self.value = self.SATURDAY + end +end + +TargetDayOfWeekBitmap.unset_saturday = function(self) + self.value = self.value & (~self.SATURDAY & self.BASE_MASK) +end + + +TargetDayOfWeekBitmap.mask_methods = { + is_sunday_set = TargetDayOfWeekBitmap.is_sunday_set, + set_sunday = TargetDayOfWeekBitmap.set_sunday, + unset_sunday = TargetDayOfWeekBitmap.unset_sunday, + is_monday_set = TargetDayOfWeekBitmap.is_monday_set, + set_monday = TargetDayOfWeekBitmap.set_monday, + unset_monday = TargetDayOfWeekBitmap.unset_monday, + is_tuesday_set = TargetDayOfWeekBitmap.is_tuesday_set, + set_tuesday = TargetDayOfWeekBitmap.set_tuesday, + unset_tuesday = TargetDayOfWeekBitmap.unset_tuesday, + is_wednesday_set = TargetDayOfWeekBitmap.is_wednesday_set, + set_wednesday = TargetDayOfWeekBitmap.set_wednesday, + unset_wednesday = TargetDayOfWeekBitmap.unset_wednesday, + is_thursday_set = TargetDayOfWeekBitmap.is_thursday_set, + set_thursday = TargetDayOfWeekBitmap.set_thursday, + unset_thursday = TargetDayOfWeekBitmap.unset_thursday, + is_friday_set = TargetDayOfWeekBitmap.is_friday_set, + set_friday = TargetDayOfWeekBitmap.set_friday, + unset_friday = TargetDayOfWeekBitmap.unset_friday, + is_saturday_set = TargetDayOfWeekBitmap.is_saturday_set, + set_saturday = TargetDayOfWeekBitmap.set_saturday, + unset_saturday = TargetDayOfWeekBitmap.unset_saturday, +} + +TargetDayOfWeekBitmap.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(TargetDayOfWeekBitmap, new_mt) + +return TargetDayOfWeekBitmap + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvse/types/init.lua b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/init.lua new file mode 100644 index 0000000000..495c5fa250 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvse/types/init.lua @@ -0,0 +1,17 @@ +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + local req_loc = string.format("EnergyEvse.types.%s", key) + local cluster_type = require(req_loc) + types_mt.__types_cache[key] = cluster_type + end + return types_mt.__types_cache[key] +end + +local EnergyEvseTypes = {} + +setmetatable(EnergyEvseTypes, types_mt) + +return EnergyEvseTypes + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvseMode/init.lua b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/init.lua new file mode 100644 index 0000000000..e40b17452a --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/init.lua @@ -0,0 +1,96 @@ +local cluster_base = require "st.matter.cluster_base" +local EnergyEvseModeServerAttributes = require "EnergyEvseMode.server.attributes" +local EnergyEvseModeServerCommands = require "EnergyEvseMode.server.commands" +local EnergyEvseModeTypes = require "EnergyEvseMode.types" + +local EnergyEvseMode = {} + +EnergyEvseMode.ID = 0x009D +EnergyEvseMode.NAME = "EnergyEvseMode" +EnergyEvseMode.server = {} +EnergyEvseMode.server.attributes = EnergyEvseModeServerAttributes:set_parent_cluster(EnergyEvseMode) +EnergyEvseMode.server.commands = EnergyEvseModeServerCommands:set_parent_cluster(EnergyEvseMode) +EnergyEvseMode.types = EnergyEvseModeTypes +EnergyEvseMode.FeatureMap = EnergyEvseMode.types.Feature + +function EnergyEvseMode.are_features_supported(feature, feature_map) + if (EnergyEvseMode.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +function EnergyEvseMode:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "SupportedModes", + [0x0001] = "CurrentMode", + [0xFFF9] = "AcceptedCommandList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function EnergyEvseMode:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "ChangeToMode", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +function EnergyEvseMode:get_client_command_by_id(command_id) + local client_id_map = { + [0x0001] = "ChangeToModeResponse", + } + if client_id_map[command_id] ~= nil then + return self.client.commands[client_id_map[command_id]] + end + return nil +end + +-- Attribute Mapping +EnergyEvseMode.attribute_direction_map = { + ["SupportedModes"] = "server", + ["CurrentMode"] = "server", + ["AcceptedCommandList"] = "server", + ["AttributeList"] = "server", +} + +-- Command Mapping +EnergyEvseMode.command_direction_map = { + ["ChangeToMode"] = "server", + ["ChangeToModeResponse"] = "client", +} + +-- Cluster Completion +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = EnergyEvseMode.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, EnergyEvseMode.NAME)) + end + return EnergyEvseMode[direction].attributes[key] +end +EnergyEvseMode.attributes = {} +setmetatable(EnergyEvseMode.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = EnergyEvseMode.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, EnergyEvseMode.NAME)) + end + return EnergyEvseMode[direction].commands[key] +end +EnergyEvseMode.commands = {} +setmetatable(EnergyEvseMode.commands, command_helper_mt) + +setmetatable(EnergyEvseMode, {__index = cluster_base}) + +return EnergyEvseMode \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/attributes/AcceptedCommandList.lua new file mode 100644 index 0000000000..ad407e23bf --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/attributes/AcceptedCommandList.lua @@ -0,0 +1,81 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + + +local AcceptedCommandList = { + ID = 0xFFF9, + NAME = "AcceptedCommandList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +AcceptedCommandList.enum_fields = {} + +function AcceptedCommandList:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function AcceptedCommandList.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, AcceptedCommandList.enum_fields[value_obj.value]) +end + +function AcceptedCommandList:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function AcceptedCommandList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AcceptedCommandList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AcceptedCommandList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value}) +return AcceptedCommandList + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/attributes/AttributeList.lua new file mode 100644 index 0000000000..c747c15169 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/attributes/AttributeList.lua @@ -0,0 +1,81 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + + +local AttributeList = { + ID = 0xFFFB, + NAME = "AttributeList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +AttributeList.enum_fields = {} + +function AttributeList:augment_type(base_type_obj) + base_type_obj.field_name = self.NAME + base_type_obj.pretty_print = self.pretty_print +end + +function AttributeList.pretty_print(value_obj) + return string.format("%s.%s", value_obj.field_name or value_obj.NAME, AttributeList.enum_fields[value_obj.value]) +end + +function AttributeList:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function AttributeList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AttributeList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AttributeList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AttributeList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AttributeList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(AttributeList, {__call = AttributeList.new_value}) +return AttributeList + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/attributes/CurrentMode.lua b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/attributes/CurrentMode.lua new file mode 100644 index 0000000000..e7b5a885b6 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/attributes/CurrentMode.lua @@ -0,0 +1,64 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local CurrentMode = { + ID = 0x0001, + NAME = "CurrentMode", + base_type = require "st.matter.data_types.Uint8", +} + +function CurrentMode:new_value(...) + local o = self.base_type(table.unpack({...})) + return o +end + +function CurrentMode:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CurrentMode:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CurrentMode:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CurrentMode: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 CurrentMode:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + return data +end + +setmetatable(CurrentMode, {__call = CurrentMode.new_value, __index = CurrentMode.base_type}) +return CurrentMode diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/attributes/SupportedModes.lua b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/attributes/SupportedModes.lua new file mode 100644 index 0000000000..d217cdf658 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/attributes/SupportedModes.lua @@ -0,0 +1,74 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SupportedModes = { + ID = 0x0000, + NAME = "SupportedModes", + base_type = require "st.matter.data_types.Array", + element_type = require "EnergyEvseMode.types.ModeOptionStruct", +} + +function SupportedModes: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, SupportedModes.element_type) + end +end + +function SupportedModes:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function SupportedModes:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function SupportedModes:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function SupportedModes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SupportedModes:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function SupportedModes:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SupportedModes, {__call = SupportedModes.new_value, __index = SupportedModes.base_type}) +return SupportedModes diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/attributes/init.lua b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/attributes/init.lua new file mode 100644 index 0000000000..167eda541d --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/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("EnergyEvseMode.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 EnergyEvseModeServerAttributes = {} + +function EnergyEvseModeServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(EnergyEvseModeServerAttributes, attr_mt) + +return EnergyEvseModeServerAttributes + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/commands/ChangeToMode.lua b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/commands/ChangeToMode.lua new file mode 100644 index 0000000000..7e6ee5f9f5 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/commands/ChangeToMode.lua @@ -0,0 +1,79 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ChangeToMode = {} + +ChangeToMode.NAME = "ChangeToMode" +ChangeToMode.ID = 0x0000 +ChangeToMode.field_defs = { + { + name = "new_mode", + field_id = 0, + optional = false, + nullable = false, + data_type = data_types.Uint8, + }, +} + +function ChangeToMode:init(device, endpoint_id, new_mode) + local out = {} + local args = {new_mode} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.optional and args[i] == nil then + out[v.name] = nil + elseif v.nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = ChangeToMode, + __tostring = ChangeToMode.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function ChangeToMode:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ChangeToMode:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + end + end + end + base_type_obj.elements = elems +end + +function ChangeToMode:deserialize(tlv_buf) + return TLVParser.decode_tlv(tlv_buf) +end + +setmetatable(ChangeToMode, {__call = ChangeToMode.init}) + +return ChangeToMode diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/commands/init.lua b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/commands/init.lua new file mode 100644 index 0000000000..bea9b81a30 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/server/commands/init.lua @@ -0,0 +1,23 @@ +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("EnergyEvseMode.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local EnergyEvseModeServerCommands = {} + +function EnergyEvseModeServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(EnergyEvseModeServerCommands, command_mt) + +return EnergyEvseModeServerCommands + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvseMode/types/Feature.lua b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/types/Feature.lua new file mode 100644 index 0000000000..b542363cf9 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/types/Feature.lua @@ -0,0 +1,56 @@ +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.ON_OFF = 0x0001 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + ON_OFF = 0x0001, +} + +Feature.is_on_off_set = function(self) + return (self.value & self.ON_OFF) ~= 0 +end + +Feature.set_on_off = function(self) + if self.value ~= nil then + self.value = self.value | self.ON_OFF + else + self.value = self.ON_OFF + end +end + +Feature.unset_on_off = function(self) + self.value = self.value & (~self.ON_OFF & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.ON_OFF + + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_on_off_set = Feature.is_on_off_set, + set_on_off = Feature.set_on_off, + unset_on_off = Feature.unset_on_off, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvseMode/types/ModeOptionStruct.lua b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/types/ModeOptionStruct.lua new file mode 100644 index 0000000000..72465b9893 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/types/ModeOptionStruct.lua @@ -0,0 +1,85 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local ModeOptionStruct = {} +local new_mt = StructureABC.new_mt({NAME = "ModeOptionStruct", ID = data_types.name_to_id_map["Structure"]}) + +ModeOptionStruct.field_defs = { + { + name = "label", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.UTF8String1", + }, + { + name = "mode", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint8", + }, + { + name = "mode_tags", + field_id = 2, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Array", + element_type = require "EnergyEvseMode.types.ModeTagStruct", + }, +} + +ModeOptionStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +ModeOptionStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = ModeOptionStruct.init +new_mt.__index.serialize = ModeOptionStruct.serialize + +ModeOptionStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(ModeOptionStruct, new_mt) + +return ModeOptionStruct diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvseMode/types/ModeTag.lua b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/types/ModeTag.lua new file mode 100644 index 0000000000..71b2c43960 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/types/ModeTag.lua @@ -0,0 +1,30 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local ModeTag = {} +local new_mt = UintABC.new_mt({NAME = "ModeTag", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.MANUAL] = "MANUAL", + [self.TIME_OF_USE] = "TIME_OF_USE", + [self.SOLAR_CHARGING] = "SOLAR_CHARGING", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.MANUAL = 0x4000 +new_mt.__index.TIME_OF_USE = 0x4001 +new_mt.__index.SOLAR_CHARGING = 0x4002 + +ModeTag.MANUAL = 0x4000 +ModeTag.TIME_OF_USE = 0x4001 +ModeTag.SOLAR_CHARGING = 0x4002 + +ModeTag.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(ModeTag, new_mt) + +return ModeTag diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvseMode/types/ModeTagStruct.lua b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/types/ModeTagStruct.lua new file mode 100644 index 0000000000..610fb90ad7 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/types/ModeTagStruct.lua @@ -0,0 +1,77 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local ModeTagStruct = {} +local new_mt = StructureABC.new_mt({NAME = "ModeTagStruct", ID = data_types.name_to_id_map["Structure"]}) + +ModeTagStruct.field_defs = { + { + name = "mfg_code", + field_id = 0, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "value", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint16", + }, +} + +ModeTagStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +ModeTagStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = ModeTagStruct.init +new_mt.__index.serialize = ModeTagStruct.serialize + +ModeTagStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(ModeTagStruct, new_mt) + +return ModeTagStruct diff --git a/drivers/SmartThings/matter-evse/src/EnergyEvseMode/types/init.lua b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/types/init.lua new file mode 100644 index 0000000000..4197eaa9a6 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/EnergyEvseMode/types/init.lua @@ -0,0 +1,17 @@ +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + local req_loc = string.format("EnergyEvseMode.types.%s", key) + local cluster_type = require(req_loc) + types_mt.__types_cache[key] = cluster_type + end + return types_mt.__types_cache[key] +end + +local EnergyEvseModeTypes = {} + +setmetatable(EnergyEvseModeTypes, types_mt) + +return EnergyEvseModeTypes + diff --git a/drivers/SmartThings/matter-evse/src/embedded_cluster_utils.lua b/drivers/SmartThings/matter-evse/src/embedded_cluster_utils.lua new file mode 100644 index 0000000000..afad08d086 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/embedded_cluster_utils.lua @@ -0,0 +1,59 @@ +local clusters = require "st.matter.clusters" +local utils = require "st.utils" +local version = require "version" + +if version.api < 11 then + clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" + clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" + clusters.EnergyEvse = require "EnergyEvse" + clusters.EnergyEvseMode = require "EnergyEvseMode" +end + +--this cluster is not supported in any releases of the lua libs +clusters.DeviceEnergyManagementMode = require "DeviceEnergyManagementMode" + +local embedded_cluster_utils = {} + +local embedded_clusters = { + [clusters.ElectricalPowerMeasurement.ID] = clusters.ElectricalPowerMeasurement, + [clusters.EnergyEvse.ID] = clusters.EnergyEvse, + [clusters.DeviceEnergyManagementMode.ID] = clusters.DeviceEnergyManagementMode, + [clusters.ElectricalEnergyMeasurement.ID] = clusters.ElectricalEnergyMeasurement, + [clusters.EnergyEvseMode.ID] = clusters.EnergyEvseMode, +} + +function embedded_cluster_utils.get_endpoints(device, cluster_id, opts) + if embedded_clusters[cluster_id] ~= nil then + local embedded_cluster = embedded_clusters[cluster_id] + local opts = opts or {} + if utils.table_size(opts) > 1 then + device.log.warn_with({hub_logs = true}, "Invalid options for get_endpoints") + return + end + local clus_has_features = function(clus, feature_bitmap) + if not feature_bitmap or not clus then return false end + return embedded_cluster.are_features_supported(feature_bitmap, clus.feature_map) + end + local eps = {} + for _, ep in ipairs(device.endpoints) do + for _, clus in ipairs(ep.clusters) do + if ((clus.cluster_id == cluster_id) + and (opts.feature_bitmap == nil or clus_has_features(clus, opts.feature_bitmap)) + and ((opts.cluster_type == nil and clus.cluster_type == "SERVER" or clus.cluster_type == "BOTH") + or (opts.cluster_type == clus.cluster_type)) + or (cluster_id == nil)) then + table.insert(eps, ep.endpoint_id) + if cluster_id == nil then break end + end + end + end + table.sort(eps) + return eps + else + local eps = device:get_endpoints(cluster_id, opts) + table.sort(eps) + return eps + end + end + + return embedded_cluster_utils \ No newline at end of file diff --git a/drivers/SmartThings/matter-evse/src/init.lua b/drivers/SmartThings/matter-evse/src/init.lua new file mode 100644 index 0000000000..04b3b6a64f --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/init.lua @@ -0,0 +1,699 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +local MatterDriver = require "st.matter.driver" +local clusters = require "st.matter.clusters" +local log = require "log" +local utils = require "st.utils" +local matter_driver_template = {} +local embedded_cluster_utils = require "embedded_cluster_utils" + +local version = require "version" + +if version.api < 11 then + clusters.EnergyEvse = require "EnergyEvse" + clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" + clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" + clusters.EnergyEvseMode = require "EnergyEvseMode" +end + +--this cluster is not supported in any releases of the lua libs +clusters.DeviceEnergyManagementMode = require "DeviceEnergyManagementMode" + +local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" +local SUPPORTED_EVSE_MODES_MAP = "__supported_evse_modes_map" +local SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP = "__supported_device_energy_management_modes_map" +local RECURRING_REPORT_POLL_TIMER = "__recurring_report_poll_timer" +local RECURRING_POLL_TIMER = "__recurring_poll_timer" +local LAST_REPORTED_TIME = "__last_reported_time" +local POWER_CONSUMPTION_REPORT_TIME_INTERVAL = "__pcr_time_interval" +local DEVICE_REPORTED_TIME_INTERVAL_CONSIDERED = "__timer_interval_considered" +-- total in case there are multiple electrical sensors +local TOTAL_CUMULATIVE_ENERGY_IMPORTED = "__total_cumulative_energy_imported" + +local TIMER_REPEAT = (1 * 60) -- 1 minute +local REPORT_TIMEOUT = (15 * 60) -- Report the value each 15 minutes +local MAX_CHARGING_CURRENT_CONSTRAINT = 80000 -- In v1.3 release of stack, this check for 80 A is performed. + +local ELECTRICAL_SENSOR_DEVICE_ID = 0x0510 +local DEVICE_ENERGY_MANAGEMENT_DEVICE_ID = 0x050D + +local function endpoint_to_component(device, ep) + local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} + for component, endpoint in pairs(map) do + if endpoint == ep then + return component + end + end + return "main" +end + +local function component_to_endpoint(device, component) + local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} + if map[component] then + return map[component] + else + return device.MATTER_DEFAULT_ENDPOINT + end +end + +local function get_endpoints_for_dt(device, device_type) + local endpoints = {} + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == device_type then + table.insert(endpoints, ep.endpoint_id) + break + end + end + end + table.sort(endpoints) + return endpoints +end + +local function time_zone_offset() + return os.difftime(os.time(), os.time(os.date("!*t", os.time()))) +end + +local function iso8601_to_epoch(iso8061Timestamp) + local pattern = "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)" + local year, month, day, hour, mins, sec = iso8061Timestamp:match(pattern) + local time_tab = { + year = tonumber(year), + month = tonumber(month), + day = tonumber(day), + hour = tonumber(hour), + min = tonumber(mins), + sec = tonumber(sec), + isdst = false + } + return math.floor(os.time(time_tab) + time_zone_offset()) +end + +local function epoch_to_iso8601(time) + return os.date("!%Y-%m-%dT%H:%M:%SZ", time) +end + +local function tbl_contains(array, value) + for _, element in ipairs(array) do + if element == value then + return true + end + end + return false +end + +-- MAPS -- +local EVSE_STATE_ENUM_MAP = { + -- Since PLUGGED_IN_DISCHARGING is not to be supported, it is not checked. + [clusters.EnergyEvse.types.StateEnum.NOT_PLUGGED_IN] = capabilities.evseState.state.notPluggedIn, + [clusters.EnergyEvse.types.StateEnum.PLUGGED_IN_NO_DEMAND] = capabilities.evseState.state.pluggedInNoDemand, + [clusters.EnergyEvse.types.StateEnum.PLUGGED_IN_DEMAND] = capabilities.evseState.state.pluggedInDemand, + [clusters.EnergyEvse.types.StateEnum.PLUGGED_IN_CHARGING] = capabilities.evseState.state.pluggedInCharging, + [clusters.EnergyEvse.types.StateEnum.SESSION_ENDING] = capabilities.evseState.state.sessionEnding, + [clusters.EnergyEvse.types.StateEnum.FAULT] = capabilities.evseState.state.fault, +} + +local EVSE_SUPPLY_STATE_ENUM_MAP = { + [clusters.EnergyEvse.types.SupplyStateEnum.DISABLED] = capabilities.evseState.supplyState.disabled, + [clusters.EnergyEvse.types.SupplyStateEnum.CHARGING_ENABLED] = capabilities.evseState.supplyState.chargingEnabled, + [clusters.EnergyEvse.types.SupplyStateEnum.DISCHARGING_ENABLED] = capabilities.evseState.supplyState.dischargingEnabled, + [clusters.EnergyEvse.types.SupplyStateEnum.DISABLED_ERROR] = capabilities.evseState.supplyState.disabledError, + [clusters.EnergyEvse.types.SupplyStateEnum.DISABLED_DIAGNOSTICS] = capabilities.evseState.supplyState.disabledDiagnostics, +} + +local EVSE_FAULT_STATE_ENUM_MAP = { + [clusters.EnergyEvse.types.FaultStateEnum.NO_ERROR] = capabilities.evseState.faultState.noError, + [clusters.EnergyEvse.types.FaultStateEnum.METER_FAILURE] = capabilities.evseState.faultState.meterFailure, + [clusters.EnergyEvse.types.FaultStateEnum.OVER_VOLTAGE] = capabilities.evseState.faultState.overVoltage, + [clusters.EnergyEvse.types.FaultStateEnum.UNDER_VOLTAGE] = capabilities.evseState.faultState.underVoltage, + [clusters.EnergyEvse.types.FaultStateEnum.OVER_CURRENT] = capabilities.evseState.faultState.overCurrent, + [clusters.EnergyEvse.types.FaultStateEnum.CONTACT_WET_FAILURE] = capabilities.evseState.faultState.contactWetFailure, + [clusters.EnergyEvse.types.FaultStateEnum.CONTACT_DRY_FAILURE] = capabilities.evseState.faultState.contactDryFailure, + [clusters.EnergyEvse.types.FaultStateEnum.GROUND_FAULT] = capabilities.evseState.faultState.groundFault, + [clusters.EnergyEvse.types.FaultStateEnum.POWER_LOSS] = capabilities.evseState.faultState.powerLoss, + [clusters.EnergyEvse.types.FaultStateEnum.POWER_QUALITY] = capabilities.evseState.faultState.powerQuality, + [clusters.EnergyEvse.types.FaultStateEnum.PILOT_SHORT_CIRCUIT] = capabilities.evseState.faultState.pilotShortCircuit, + [clusters.EnergyEvse.types.FaultStateEnum.EMERGENCY_STOP] = capabilities.evseState.faultState.emergencyStop, + [clusters.EnergyEvse.types.FaultStateEnum.EV_DISCONNECTED] = capabilities.evseState.faultState.eVDisconnected, + [clusters.EnergyEvse.types.FaultStateEnum.WRONG_POWER_SUPPLY] = capabilities.evseState.faultState.wrongPowerSupply, + [clusters.EnergyEvse.types.FaultStateEnum.LIVE_NEUTRAL_SWAP] = capabilities.evseState.faultState.liveNeutralSwap, + [clusters.EnergyEvse.types.FaultStateEnum.OVER_TEMPERATURE] = capabilities.evseState.faultState.overTemperature, + [clusters.EnergyEvse.types.FaultStateEnum.OTHER] = capabilities.evseState.faultState.other, +} + +local function read_cumulative_energy_imported(device) + local electrical_energy_meas_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) + if electrical_energy_meas_eps and #electrical_energy_meas_eps > 0 then + local read_req = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(device, electrical_energy_meas_eps[1]) + for i, ep in ipairs(electrical_energy_meas_eps) do + if i > 1 then + read_req:merge(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(device, ep)) + end + end + device:send(read_req) + end +end + +local function create_poll_schedule(device) + local poll_timer = device:get_field(RECURRING_POLL_TIMER) + if poll_timer ~= nil then + return + end + + -- The powerConsumption report needs to be updated at least every 15 minutes in order to be included in SmartThings Energy + local timer = device.thread:call_on_schedule(TIMER_REPEAT, function() + read_cumulative_energy_imported(device) + end, "polling_schedule_timer") + + device:set_field(RECURRING_POLL_TIMER, timer) +end + +local function create_poll_report_schedule(device) + local polling_schedule_timer = device:get_field(RECURRING_REPORT_POLL_TIMER) + if polling_schedule_timer ~= nil then + return + end + + -- The powerConsumption report needs to be updated at least every 15 minutes in order to be included in SmartThings Energy + local pcr_interval = device:get_field(POWER_CONSUMPTION_REPORT_TIME_INTERVAL) or REPORT_TIMEOUT + local timer = device.thread:call_on_schedule(pcr_interval, function() + local current_time = os.time() + local last_time = device:get_field(LAST_REPORTED_TIME) or 0 + local total_cumulative_energy_imported = device:get_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED) or {} + local total_energy = 0 + + -- we sum up total cumulative energy across all electrical sensor endpoints + for _, energyWh in pairs(total_cumulative_energy_imported) do + total_energy = total_energy + energyWh + end + + device:set_field(LAST_REPORTED_TIME, current_time, { persist = true }) + + -- Calculate the energy consumed between the start and the end time + local previousTotalConsumptionWh = device:get_latest_state("main", capabilities.powerConsumptionReport + .ID, + capabilities.powerConsumptionReport.powerConsumption.NAME) or { energy = 0 } + + local deltaEnergyWh = math.max(total_energy - previousTotalConsumptionWh.energy, 0.0) + local startTime = epoch_to_iso8601(last_time) + local endTime = epoch_to_iso8601(current_time - 1) + + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' + device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ + start = startTime, + ["end"] = endTime, + deltaEnergy = deltaEnergyWh, + energy = total_energy + })) + end, "polling_report_schedule_timer") + + device:set_field(RECURRING_REPORT_POLL_TIMER, timer) +end + +local function create_poll_schedules_for_cumulative_energy_imported(device) + if not device:supports_capability(capabilities.powerConsumptionReport) then + return + end + create_poll_schedule(device) + create_poll_report_schedule(device) +end + +local function delete_poll_schedules(device) + local poll_timer = device:get_field(RECURRING_POLL_TIMER) + local reporting_poll_timer = device:get_field(RECURRING_REPORT_POLL_TIMER) + if poll_timer ~= nil then + device.thread:cancel_timer(poll_timer) + device:set_field(RECURRING_POLL_TIMER, nil) + end + if reporting_poll_timer ~= nil then + device.thread:cancel_timer(reporting_poll_timer) + device:set_field(RECURRING_REPORT_POLL_TIMER, nil) + end +end + +-- Lifecycle Handlers -- +local function device_init(driver, device) + device:subscribe() + device:set_endpoint_to_component_fn(endpoint_to_component) + device:set_component_to_endpoint_fn(component_to_endpoint) + create_poll_schedules_for_cumulative_energy_imported(device) + local current_time = os.time() + local current_time_iso8601 = epoch_to_iso8601(current_time) + -- emit current time by default + device:emit_event(capabilities.evseChargingSession.targetEndTime(current_time_iso8601)) +end + +local function device_added(driver, device) + local electrical_sensor_eps = get_endpoints_for_dt(device, ELECTRICAL_SENSOR_DEVICE_ID) or {} + local device_energy_mgmt_eps = get_endpoints_for_dt(device, DEVICE_ENERGY_MANAGEMENT_DEVICE_ID) or {} + local component_to_endpoint_map = { + ["main"] = device.MATTER_DEFAULT_ENDPOINT, + ["electricalSensor"] = electrical_sensor_eps[1], + ["deviceEnergyManagement"] = device_energy_mgmt_eps[1] + } + log.debug("component_to_endpoint_map " .. utils.stringify_table(component_to_endpoint_map)) + device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, { persist = true }) +end + +local function do_configure(driver, device) + local power_meas_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) or {} + local energy_meas_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) or {} + local device_energy_mgmt_eps = embedded_cluster_utils.get_endpoints(device, clusters.DeviceEnergyManagementMode) or {} + local profile_name = "evse" + + -- As per spec, at least one of the electrical energy measurement or electrical power measurement clusters are to be supported. + if #energy_meas_eps > 0 then + profile_name = profile_name .. "-energy-meas" + end + if #power_meas_eps > 0 then + profile_name = profile_name .. "-power-meas" + end + + if #device_energy_mgmt_eps > 0 then + profile_name = profile_name .. "-energy-mgmt-mode" + end + + device.log.info_with({ hub_logs = true }, string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({ profile = profile_name }) +end + +local function info_changed(driver, device) + for cap_id, attributes in pairs(matter_driver_template.subscribed_attributes) do + if device:supports_capability_by_id(cap_id) then + for _, attr in ipairs(attributes) do + device:add_subscribed_attribute(attr) + end + end + end + device:subscribe() + create_poll_schedules_for_cumulative_energy_imported(device) +end + +local function device_removed(driver, device) + delete_poll_schedules(device) +end + +-- Matter Handlers -- +local function charging_readiness_state_handler(driver, device, evse_state, evse_supply_state) + local event = capabilities.evseChargingSession.chargingState.stopped({state_change = true}) + if evse_supply_state.NAME == capabilities.evseState.supplyState.disabledError.NAME or + evse_supply_state.NAME == capabilities.evseState.supplyState.disabledDiagnostics.NAME or + evse_state.NAME == capabilities.evseState.state.fault.NAME then + event = capabilities.evseChargingSession.chargingState.disabled({state_change = true}) + elseif evse_supply_state.NAME == capabilities.evseState.supplyState.chargingEnabled.NAME then + event = capabilities.evseChargingSession.chargingState.charging({state_change = true}) + end + device:emit_event(event) +end + +local function evse_state_handler(driver, device, ib, response) + local evse_state = ib.data.value + local latest_supply_state = device:get_latest_state( + "main", + capabilities.evseState.ID, + capabilities.evseState.supplyState.NAME + ) + local event = EVSE_STATE_ENUM_MAP[evse_state] + if event then + device:emit_event_for_endpoint(ib.endpoint_id, event()) + charging_readiness_state_handler(driver, device, event, {NAME=latest_supply_state}) + else + log.warn("evse_state_handler invalid EVSE State: " .. evse_state) + end +end + +local function evse_supply_state_handler(driver, device, ib, response) + local evse_supply_state = ib.data.value + + local latest_evse_state = device:get_latest_state( + device:endpoint_to_component(ib.endopint_id), + capabilities.evseState.ID, + capabilities.evseState.state.NAME + ) + local event = EVSE_SUPPLY_STATE_ENUM_MAP[evse_supply_state] + if event then + device:emit_event_for_endpoint(ib.endpoint_id, event()) + charging_readiness_state_handler(driver, device, {NAME=latest_evse_state}, event) + else + log.warn("evse_supply_state_handler invalid EVSE Supply State: " .. evse_supply_state) + end +end + +local function evse_fault_state_handler(driver, device, ib, response) + local evse_fault_state = ib.data.value + local event = EVSE_FAULT_STATE_ENUM_MAP[evse_fault_state] + if event then + device:emit_event_for_endpoint(ib.endpoint_id, event()) + return + end + log.warn("Invalid EVSE fault state received: " .. evse_fault_state) +end + +local function evse_charging_enabled_until_handler(driver, device, ib, response) + local ep = ib.endpoint_id + local targetEndTime = ib.data.value + if targetEndTime ~= nil then + if targetEndTime == 0 then --if we get 0 we update with current time. + targetEndTime = os.time() + end + targetEndTime = epoch_to_iso8601(targetEndTime) + device:emit_event_for_endpoint(ep, capabilities.evseChargingSession.targetEndTime(targetEndTime)) + return + end + log.warn("Charging enabled handler received an invalid target end time, not reporting") +end + +local function evse_current_limit_handler(event) + return function(driver, device, ib, response) + local data = ib.data.value + local ep = ib.endpoint_id + if data then + device:emit_event_for_endpoint(ep, event(data)) + return + end + log.warn("Failed to emit capability for " .. event.NAME) + end +end + +local function evse_session_duration_handler(driver, device, ib, response) + local session_duration = ib.data.value + local endpoint_id = ib.endpoint_id + if session_duration then + device:emit_event_for_endpoint(endpoint_id, capabilities.evseChargingSession.sessionTime(session_duration)) + return + end + log.warn("evse_session_duration_handler received invalid EVSE Session Duration") +end + +local function evse_session_energy_charged_handler(driver, device, ib, response) + local charged_energy = ib.data.value + local endpoint_id = ib.endpoint_id + if charged_energy then + device:emit_event_for_endpoint(endpoint_id, capabilities.evseChargingSession.energyDelivered(charged_energy)) + return + end + log.warn("evse_session_energy_charged_handler received invalid EVSE Session Energy Charged") +end + +local function power_mode_handler(driver, device, ib, response) + local power_mode = ib.data.value + local endpoint_id = ib.endpoint_id + + if power_mode == clusters.ElectricalPowerMeasurement.types.PowerModeEnum.AC then + device:emit_event_for_endpoint(endpoint_id, capabilities.powerSource.powerSource.mains()) + elseif power_mode == clusters.ElectricalPowerMeasurement.types.PowerModeEnum.DC then + device:emit_event_for_endpoint(endpoint_id, capabilities.powerSource.powerSource.dc()) + else + device:emit_event_for_endpoint(endpoint_id, capabilities.powerSource.powerSource.unknown()) + end +end + +local function energy_evse_supported_modes_attr_handler(driver, device, ib, response) + local supportedEvseModesMap = device:get_field(SUPPORTED_EVSE_MODES_MAP) or {} + local supportedEvseModes = {} + for _, mode in ipairs(ib.data.elements) do + clusters.EnergyEvseMode.types.ModeOptionStruct:augment_type(mode) + table.insert(supportedEvseModes, mode.elements.label.value) + end + supportedEvseModesMap[ib.endpoint_id] = supportedEvseModes + device:set_field(SUPPORTED_EVSE_MODES_MAP, supportedEvseModesMap, { persist = true }) + local event = capabilities.mode.supportedModes(supportedEvseModes, { visibility = { displayed = false } }) + device:emit_event_for_endpoint(ib.endpoint_id, event) + event = capabilities.mode.supportedArguments(supportedEvseModes, { visibility = { displayed = false } }) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +local function energy_evse_mode_attr_handler(driver, device, ib, response) + device.log.info(string.format("energy_evse_modes_attr_handler currentMode: %s", ib.data.value)) + + local supportedEvseModesMap = device:get_field(SUPPORTED_EVSE_MODES_MAP) or {} + local supportedEvseModes = supportedEvseModesMap[ib.endpoint_id] or {} + local currentMode = ib.data.value + for i, mode in ipairs(supportedEvseModes) do + if i - 1 == currentMode then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.mode.mode(mode)) + break + end + end +end + +local function device_energy_mgmt_supported_modes_attr_handler(driver, device, ib, response) + local supportedDeviceEnergyMgmtModesMap = device:get_field(SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP) or {} + local supportedDeviceEnergyMgmtModes = {} + for _, mode in ipairs(ib.data.elements) do + clusters.EnergyEvseMode.types.ModeOptionStruct:augment_type(mode) + table.insert(supportedDeviceEnergyMgmtModes, mode.elements.label.value) + end + supportedDeviceEnergyMgmtModesMap[ib.endpoint_id] = supportedDeviceEnergyMgmtModes + device:set_field(SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP, supportedDeviceEnergyMgmtModesMap, { persist = true }) + local event = capabilities.mode.supportedModes(supportedDeviceEnergyMgmtModes, { visibility = { displayed = false } }) + device:emit_event_for_endpoint(ib.endpoint_id, event) + event = capabilities.mode.supportedArguments(supportedDeviceEnergyMgmtModes, { visibility = { displayed = false } }) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +local function device_energy_mgmt_mode_attr_handler(driver, device, ib, response) + device.log.info(string.format("device_energy_mgmt_mode_attr_handler currentMode: %s", ib.data.value)) + + local supportedDeviceEnergyMgmtModesMap = device:get_field(SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP) or {} + local supportedDeviceEnergyMgmtModes = supportedDeviceEnergyMgmtModesMap[ib.endpoint_id] or {} + local currentMode = ib.data.value + for i, mode in ipairs(supportedDeviceEnergyMgmtModes) do + if i - 1 == currentMode then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.mode.mode(mode)) + break + end + end +end + +local function cumulative_energy_imported_handler(driver, device, ib, response) + if ib.data then + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct:augment_type(ib.data) + local cumulative_energy_imported = ib.data.elements.energy.value + local endpoint_id = string.format(ib.endpoint_id) + local cumulative_energy_imported_Wh = utils.round(cumulative_energy_imported / 1000) + local total_cumulative_energy_imported = device:get_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED) or {} + + -- in case there are multiple electrical sensors store them in a table. + total_cumulative_energy_imported[endpoint_id] = cumulative_energy_imported_Wh + device:set_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED, total_cumulative_energy_imported, { persist = true }) + end +end + +local function periodic_energy_imported_handler(driver, device, ib, response) + local endpoint_id = ib.endpoint_id + local cumul_eps = embedded_cluster_utils.get_endpoints(device, + clusters.ElectricalEnergyMeasurement.ID, + {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY }) + + if ib.data then + if version.api < 11 then + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct:augment_type(ib.data) + end + + local start_timestamp = ib.data.elements.start_timestamp.value or 0 + local end_timestamp = ib.data.elements.end_timestamp.value or 0 + + local device_reporting_time_interval = end_timestamp - start_timestamp + if not device:get_field(DEVICE_REPORTED_TIME_INTERVAL_CONSIDERED) and device_reporting_time_interval > REPORT_TIMEOUT then + -- This is a one time setup in order to consider a larger time interval if the interval the device chooses to report is greater than 15 minutes. + device:set_field(DEVICE_REPORTED_TIME_INTERVAL_CONSIDERED, true, {persist=true}) + local polling_schedule_timer = device:get_field(RECURRING_REPORT_POLL_TIMER) + if polling_schedule_timer ~= nil then + device.thread:cancel_timer(polling_schedule_timer) + end + device:set_field(POWER_CONSUMPTION_REPORT_TIME_INTERVAL, device_reporting_time_interval, {persist = true}) + create_poll_report_schedule(device) + end + + if tbl_contains(cumul_eps, endpoint_id) then + -- Since cluster in this endpoint supports both CUME & PERE features, we will prefer + -- cumulative_energy_imported_handler to handle the energy report for this endpoint over PeriodicEnergyImported. + return + end + + local energy_imported = ib.data.elements.energy.value + endpoint_id = string.format(ib.endpoint_id) + local energy_imported_Wh = utils.round(energy_imported / 1000) + local total_cumulative_energy_imported = device:get_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED) or {} + + -- in case there are multiple electrical sensors store them in a table. + total_cumulative_energy_imported[endpoint_id] = total_cumulative_energy_imported[endpoint_id] or 0 + total_cumulative_energy_imported[endpoint_id] = total_cumulative_energy_imported[endpoint_id] + energy_imported_Wh + device:set_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED, total_cumulative_energy_imported, { persist = true }) + end +end + +-- Capability Handlers -- +local function get_latest_charging_parameters(device) + local min_charging_current = device:get_latest_state("main", capabilities.evseChargingSession.ID, + capabilities.evseChargingSession.minCurrent.NAME) or 0 + local max_charging_current = device:get_latest_state("main", capabilities.evseChargingSession.ID, + capabilities.evseChargingSession.maxCurrent.NAME) + local target_end_time_iso8601 = device:get_latest_state("main", capabilities.evseChargingSession.ID, + capabilities.evseChargingSession.targetEndTime.NAME) + return min_charging_current, max_charging_current, target_end_time_iso8601 +end + +local function handle_enable_charging(driver, device, cmd) + local ep = component_to_endpoint(device, cmd.component) + local default_min_current, default_max_current, default_charging_enabled_until_iso8601 = get_latest_charging_parameters( + device) + local charging_enabled_until_iso8601 = cmd.args.time or default_charging_enabled_until_iso8601 + local minimum_current = cmd.args.minCurrent or default_min_current + local maximum_current = cmd.args.maxCurrent or default_max_current + local charging_enabled_until_epoch_s = iso8601_to_epoch(charging_enabled_until_iso8601) + device:send(clusters.EnergyEvse.commands.EnableCharging(device, ep, charging_enabled_until_epoch_s, minimum_current, + maximum_current)) +end + +local handle_set_charging_parameters = function(cap, arg) + return function(driver, device, cmd) + if arg == "maxCurrent" and cmd.args[arg] > MAX_CHARGING_CURRENT_CONSTRAINT then + cmd.args[arg] = MAX_CHARGING_CURRENT_CONSTRAINT + log.warn_with({hub_logs=true}, "Clipping Max Current as it cannot be greater than 80A") + end + local capability_event = cap(cmd.args[arg]) + log.info("Setting value " .. (cmd.args[arg]) .. " for " .. (cap.NAME)) + device:emit_event(capability_event) + end +end + +local function handle_disable_charging(driver, device, cmd) + local ep = component_to_endpoint(device, cmd.component) + device:send(clusters.EnergyEvse.commands.Disable(device, ep)) +end + +local function handle_set_mode_command(driver, device, cmd) + local set_mode_handlers = { + ["main"] = function( ... ) + local ep = component_to_endpoint(device, cmd.component) + local supportedEvseModesMap = device:get_field(SUPPORTED_EVSE_MODES_MAP) + local supportedEvseModes = supportedEvseModesMap[ep] or {} + for i, mode in ipairs(supportedEvseModes) do + if cmd.args.mode == mode then + device:send(clusters.EnergyEvseMode.commands.ChangeToMode(device, ep, i - 1)) + return + end + end + log.warn("Received request to set unsupported mode for EnergyEvseMode.") + end, + ["deviceEnergyManagement"] = function( ... ) + local ep = component_to_endpoint(device, cmd.component) + local supportedDeviceEnergyMgmtModesMap = device:get_field(SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP) + local supportedDeviceEnergyMgmtModes = supportedDeviceEnergyMgmtModesMap[ep] or {} + for i, mode in ipairs(supportedDeviceEnergyMgmtModes) do + if cmd.args.mode == mode then + device:send(clusters.DeviceEnergyManagementMode.commands.ChangeToMode(device, ep, i - 1)) + return + end + end + log.warn("Received request to set unsupported mode for DeviceEnergyManagementMode.") + end + } + set_mode_handlers[cmd.component]() +end + +matter_driver_template = { + NAME = "matter-evse", + lifecycle_handlers = { + init = device_init, + added = device_added, + doConfigure = do_configure, + infoChanged = info_changed, + removed = device_removed + }, + matter_handlers = { + attr = { + [clusters.EnergyEvse.ID] = { + [clusters.EnergyEvse.attributes.State.ID] = evse_state_handler, + [clusters.EnergyEvse.attributes.SupplyState.ID] = evse_supply_state_handler, + [clusters.EnergyEvse.attributes.FaultState.ID] = evse_fault_state_handler, + [clusters.EnergyEvse.attributes.ChargingEnabledUntil.ID] = evse_charging_enabled_until_handler, + [clusters.EnergyEvse.attributes.MinimumChargeCurrent.ID] = evse_current_limit_handler(capabilities + .evseChargingSession.minCurrent), + [clusters.EnergyEvse.attributes.MaximumChargeCurrent.ID] = evse_current_limit_handler(capabilities + .evseChargingSession.maxCurrent), + [clusters.EnergyEvse.attributes.SessionDuration.ID] = evse_session_duration_handler, + [clusters.EnergyEvse.attributes.SessionEnergyCharged.ID] = evse_session_energy_charged_handler, + }, + [clusters.ElectricalPowerMeasurement.ID] = { + [clusters.ElectricalPowerMeasurement.attributes.PowerMode.ID] = power_mode_handler, + }, + [clusters.EnergyEvseMode.ID] = { + [clusters.EnergyEvseMode.attributes.SupportedModes.ID] = energy_evse_supported_modes_attr_handler, + [clusters.EnergyEvseMode.attributes.CurrentMode.ID] = energy_evse_mode_attr_handler, + }, + [clusters.DeviceEnergyManagementMode.ID] = { + [clusters.DeviceEnergyManagementMode.attributes.SupportedModes.ID] = device_energy_mgmt_supported_modes_attr_handler, + [clusters.DeviceEnergyManagementMode.attributes.CurrentMode.ID] = device_energy_mgmt_mode_attr_handler, + }, + [clusters.ElectricalEnergyMeasurement.ID] = { + [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = cumulative_energy_imported_handler, + [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = periodic_energy_imported_handler + }, + }, + }, + subscribed_attributes = { + [capabilities.evseState.ID] = { + clusters.EnergyEvse.attributes.State, + clusters.EnergyEvse.attributes.SupplyState, + clusters.EnergyEvse.attributes.FaultState, + }, + [capabilities.evseChargingSession.ID] = { + clusters.EnergyEvse.attributes.ChargingEnabledUntil, + clusters.EnergyEvse.attributes.MinimumChargeCurrent, + clusters.EnergyEvse.attributes.MaximumChargeCurrent, + clusters.EnergyEvse.attributes.SessionDuration, + clusters.EnergyEvse.attributes.SessionEnergyCharged, + }, + [capabilities.powerSource.ID] = { + clusters.ElectricalPowerMeasurement.attributes.PowerMode, + }, + [capabilities.powerConsumptionReport.ID] = { + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + }, + [capabilities.mode.ID] = { + clusters.EnergyEvseMode.attributes.SupportedModes, + clusters.EnergyEvseMode.attributes.CurrentMode, + clusters.DeviceEnergyManagementMode.attributes.CurrentMode, + clusters.DeviceEnergyManagementMode.attributes.SupportedModes + } + }, + capability_handlers = { + [capabilities.evseChargingSession.ID] = { + [capabilities.evseChargingSession.commands.enableCharging.NAME] = handle_enable_charging, + [capabilities.evseChargingSession.commands.disableCharging.NAME] = handle_disable_charging, + [capabilities.evseChargingSession.commands.setTargetEndTime.NAME] = handle_set_charging_parameters(capabilities.evseChargingSession.targetEndTime, "time"), + [capabilities.evseChargingSession.commands.setMinCurrent.NAME] = handle_set_charging_parameters(capabilities.evseChargingSession.minCurrent, "minCurrent"), + [capabilities.evseChargingSession.commands.setMaxCurrent.NAME] = handle_set_charging_parameters(capabilities.evseChargingSession.maxCurrent, "maxCurrent"), + }, + [capabilities.mode.ID] = { + [capabilities.mode.commands.setMode.NAME] = handle_set_mode_command, + }, + }, + supported_capabilities = { + capabilities.evseState, + capabilities.evseChargingSession, + capabilities.powerSource, + capabilities.powerConsumptionReport, + capabilities.mode + }, +} + +local matter_driver = MatterDriver("matter-evse", matter_driver_template) +log.info(string.format("Starting %s driver, with dispatcher: %s", matter_driver.NAME, matter_driver.matter_dispatcher)) +matter_driver:run() diff --git a/drivers/SmartThings/matter-evse/src/test/test_evse.lua b/drivers/SmartThings/matter-evse/src/test/test_evse.lua new file mode 100644 index 0000000000..c4c3640917 --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/test/test_evse.lua @@ -0,0 +1,511 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local log = require "log" + +local EVSE_EP = 1 +local ELECTRICAL_SENSOR_EP = 2 +local DEVICE_ENERGY_MANAGEMENT_DEVICE_EP = 3 + +clusters.EnergyEvse = require "EnergyEvse" +clusters.EnergyEvseMode = require "EnergyEvseMode" +clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" +clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" +clusters.DeviceEnergyManagementMode = require "DeviceEnergyManagementMode" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("evse-power-meas-energy-mgmt-mode.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 = EVSE_EP, + clusters = { + { cluster_id = clusters.EnergyEvse.ID, cluster_type = "SERVER" }, + { cluster_id = clusters.EnergyEvseMode.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x050C, device_type_revision = 1 } -- EVSE + } + }, + { + endpoint_id = ELECTRICAL_SENSOR_EP, + clusters = { + { cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 } -- Electrical Sensor + } + }, + { + endpoint_id = DEVICE_ENERGY_MANAGEMENT_DEVICE_EP, + clusters = { + { cluster_id = clusters.DeviceEnergyManagementMode.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x050D, device_type_revision = 1 } -- Device Energy Management + } + }, + } +}) + +local function test_init() + local cluster_subscribe_list = { + clusters.EnergyEvse.attributes.State, + clusters.EnergyEvse.attributes.SupplyState, + clusters.EnergyEvse.attributes.FaultState, + clusters.EnergyEvse.attributes.ChargingEnabledUntil, + clusters.EnergyEvse.attributes.MinimumChargeCurrent, + clusters.EnergyEvse.attributes.MaximumChargeCurrent, + clusters.EnergyEvse.attributes.SessionDuration, + clusters.EnergyEvse.attributes.SessionEnergyCharged, + clusters.ElectricalPowerMeasurement.attributes.PowerMode, + clusters.EnergyEvseMode.attributes.SupportedModes, + clusters.EnergyEvseMode.attributes.CurrentMode, + clusters.DeviceEnergyManagementMode.attributes.CurrentMode, + clusters.DeviceEnergyManagementMode.attributes.SupportedModes, + } + log.info("In test init", os.time()) + test.socket.matter:__set_channel_ordering("relaxed") + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.evseChargingSession.targetEndTime("1970-01-01T00:00:00Z"))) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Assert component to endpoint map", + function() + local component_to_endpoint_map = mock_device:get_field("__component_to_endpoint_map") + assert(component_to_endpoint_map["electricalSensor"] == ELECTRICAL_SENSOR_EP, "Electrical Sensor Endpoint must be 2") + assert(component_to_endpoint_map["deviceEnergyManagement"] == DEVICE_ENERGY_MANAGEMENT_DEVICE_EP, + "Device Energy Management Endpoint must be 3") + end +) + +test.register_message_test( + "EnergyEvse Supply State must trigger appropriate evseState supplystate capability event", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.EnergyEvse.attributes.SupplyState:build_test_report_data(mock_device, EVSE_EP, + clusters.EnergyEvse.attributes.SupplyState.CHARGING_ENABLED) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.evseState.supplyState.chargingEnabled()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.evseChargingSession.chargingState.charging({state_change = true})) + } + } +) + +test.register_message_test( + "EnergyEvse State must trigger appropriate evseState EvseState capability event", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.EnergyEvse.attributes.SupplyState:build_test_report_data(mock_device, EVSE_EP, + clusters.EnergyEvse.attributes.SupplyState.CHARGING_ENABLED) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.evseState.supplyState.chargingEnabled()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.evseChargingSession.chargingState.charging({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.EnergyEvse.attributes.State:build_test_report_data(mock_device, EVSE_EP, + clusters.EnergyEvse.attributes.State.PLUGGED_IN_DEMAND) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.evseState.state.pluggedInDemand()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.evseChargingSession.chargingState.charging({state_change = true})) + }, + } +) + +test.register_message_test( + "EnergyEvse Fault State must trigger appropriate evseState faultState capability event", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.EnergyEvse.attributes.FaultState:build_test_report_data(mock_device, EVSE_EP, + clusters.EnergyEvse.attributes.FaultState.GROUND_FAULT) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.evseState.faultState.groundFault()) + } + } +) + +test.register_message_test( + "EnergyEvse ChargingEnabledUntil in epoch must trigger appropriate evseChargingSession targetEndTime capability event in iso8601 format", --1724399242 in epoch to 2024-08-23T07:47:22Z in iso8601 + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.EnergyEvse.attributes.ChargingEnabledUntil:build_test_report_data(mock_device, EVSE_EP, 1724399242) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.evseChargingSession.targetEndTime("2024-08-23T07:47:22Z")) + } + } +) + +test.register_message_test( + "EnergyEvse MinimumChargeCurrent constraint must trigger appropriate evseChargingSession minCurrent capability event", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.EnergyEvse.attributes.MinimumChargeCurrent:build_test_report_data(mock_device, EVSE_EP, 0) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.evseChargingSession.minCurrent(0)) + } + } +) + +test.register_message_test( + "EnergyEvse MaximumChargeCurrent constraint must trigger appropriate evseChargingSession maxCurrent capability event", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.EnergyEvse.attributes.MaximumChargeCurrent:build_test_report_data(mock_device, EVSE_EP, 10000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.evseChargingSession.maxCurrent(10000)) + } + } +) + +test.register_message_test( + "EnergyEvse SessionDuration must trigger appropriate evseChargingSession sessionTime capability event", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.EnergyEvse.attributes.SessionDuration:build_test_report_data(mock_device, EVSE_EP, 9000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.evseChargingSession.sessionTime(9000)) + } + } +) + +test.register_message_test( + "EnergyEvse SessionEnergyCharged must trigger appropriate evseChargingSession energyDelivered capability event", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.EnergyEvse.attributes.SessionEnergyCharged:build_test_report_data(mock_device, EVSE_EP, 900000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.evseChargingSession.energyDelivered(900000)) + } + } +) + +test.register_message_test( + "ElectricalPowerMeasurement PowerMode must trigger appropriate powerSource capability event", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ElectricalPowerMeasurement.attributes.PowerMode:build_test_report_data(mock_device, ELECTRICAL_SENSOR_EP, + clusters.ElectricalPowerMeasurement.attributes.PowerMode.AC) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("electricalSensor", + capabilities.powerSource.powerSource.mains()) + } + } +) + +test.register_message_test( + "EnergyEvseMode SupportedModes must be registered.\n2.CurrentMode must trigger approriate mode capability event.\n3.Command to setMode should trigger appropriate changeToMode matter command", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.EnergyEvseMode.attributes.SupportedModes:build_test_report_data(mock_device, EVSE_EP, { + clusters.EnergyEvseMode.types.ModeOptionStruct({ + ["label"] = "Auto-Scheduled", + ["mode"] = 0, + ["mode_tags"] = { + clusters.EnergyEvseMode.types.ModeTagStruct({ ["mfg_code"] = 256, ["value"] = 0 }) + } + }), + clusters.EnergyEvseMode.types.ModeOptionStruct({ + ["label"] = "Manual", + ["mode"] = 1, + ["mode_tags"] = { + clusters.EnergyEvseMode.types.ModeTagStruct({ ["mfg_code"] = 256, ["value"] = 1 }) + } + }) + }) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.mode.supportedModes({ "Auto-Scheduled", "Manual" }, { visibility = { displayed = false } })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.mode.supportedArguments({ "Auto-Scheduled", "Manual" }, { visibility = { displayed = false } })) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.EnergyEvseMode.attributes.CurrentMode:build_test_report_data(mock_device, EVSE_EP, 1) --1 is the index for Manual EnergyEvse mode + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.mode.mode("Manual")) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "mode", component = "main", command = "setMode", args = { "Auto-Scheduled" } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.EnergyEvseMode.commands.ChangeToMode(mock_device, EVSE_EP, 0) --Index is Auto-Scheduled + } + } + } +) + +test.register_message_test( + "DeviceEnergyManagementMode SupportedModes must be registered.\n2.CurrentMode must trigger approriate mode capability event.\n3.Command to setMode should trigger appropriate changeToMode matter command", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.DeviceEnergyManagementMode.attributes.SupportedModes:build_test_report_data(mock_device, + DEVICE_ENERGY_MANAGEMENT_DEVICE_EP, { + clusters.DeviceEnergyManagementMode.types.ModeOptionStruct({ + ["label"] = "Grid Energy Management", + ["mode"] = 0, + ["mode_tags"] = { + clusters.DeviceEnergyManagementMode.types.ModeTagStruct({ ["mfg_code"] = 256, ["value"] = 0 }) + } + }), + clusters.DeviceEnergyManagementMode.types.ModeOptionStruct({ + ["label"] = "Home Energy Management", + ["mode"] = 1, + ["mode_tags"] = { + clusters.DeviceEnergyManagementMode.types.ModeTagStruct({ ["mfg_code"] = 256, ["value"] = 1 }) + } + }), + clusters.DeviceEnergyManagementMode.types.ModeOptionStruct({ + ["label"] = "Full Energy Management", + ["mode"] = 2, + ["mode_tags"] = { + clusters.DeviceEnergyManagementMode.types.ModeTagStruct({ ["mfg_code"] = 256, ["value"] = 2 }) + } + }) + }) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("deviceEnergyManagement", + capabilities.mode.supportedModes( + { "Grid Energy Management", "Home Energy Management", "Full Energy Management" }, + { visibility = { displayed = false } })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("deviceEnergyManagement", + capabilities.mode.supportedArguments( + { "Grid Energy Management", "Home Energy Management", "Full Energy Management" }, + { visibility = { displayed = false } })) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.DeviceEnergyManagementMode.attributes.CurrentMode:build_test_report_data(mock_device, + DEVICE_ENERGY_MANAGEMENT_DEVICE_EP, 1) --1 is the index for Home Energy Management mode + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("deviceEnergyManagement", + capabilities.mode.mode("Home Energy Management")) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "mode", component = "deviceEnergyManagement", command = "setMode", args = { "Full Energy Management" } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.DeviceEnergyManagementMode.commands.ChangeToMode(mock_device, DEVICE_ENERGY_MANAGEMENT_DEVICE_EP, 2) --Index is Full Energy Management + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "mode", component = "deviceEnergyManagement", command = "setMode", args = { "Grid Energy Management" } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.DeviceEnergyManagementMode.commands.ChangeToMode(mock_device, DEVICE_ENERGY_MANAGEMENT_DEVICE_EP, 0) --Index is Grid Energy Management + } + } + } +) + +--TODO: Include tests for evseChargingSession capability commands. This is anyhow tested with EVSE virtual device app. + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-evse/src/test/test_evse_energy_meas.lua b/drivers/SmartThings/matter-evse/src/test/test_evse_energy_meas.lua new file mode 100644 index 0000000000..b5b51236bd --- /dev/null +++ b/drivers/SmartThings/matter-evse/src/test/test_evse_energy_meas.lua @@ -0,0 +1,216 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" + +local EVSE_EP = 1 +local ELECTRICAL_SENSOR_EP_ONE = 2 +local ELECTRICAL_SENSOR_EP_TWO = 3 + +clusters.EnergyEvse = require "EnergyEvse" +clusters.EnergyEvseMode = require "EnergyEvseMode" +clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" +clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" +clusters.DeviceEnergyManagementMode = require "DeviceEnergyManagementMode" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("evse-energy-meas.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 = EVSE_EP, + clusters = { + { cluster_id = clusters.EnergyEvse.ID, cluster_type = "SERVER" }, + { cluster_id = clusters.EnergyEvseMode.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x050C, device_type_revision = 1 } -- EVSE + } + }, + { + endpoint_id = ELECTRICAL_SENSOR_EP_ONE, + clusters = { + { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 } -- Electrical Sensor + } + }, + { + endpoint_id = ELECTRICAL_SENSOR_EP_TWO, + clusters = { + { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 } -- Electrical Sensor + } + }, + } +}) + +local function test_init() + local cluster_subscribe_list = { + clusters.EnergyEvse.attributes.State, + clusters.EnergyEvse.attributes.SupplyState, + clusters.EnergyEvse.attributes.FaultState, + clusters.EnergyEvse.attributes.ChargingEnabledUntil, + clusters.EnergyEvse.attributes.MinimumChargeCurrent, + clusters.EnergyEvse.attributes.MaximumChargeCurrent, + clusters.EnergyEvse.attributes.SessionDuration, + clusters.EnergyEvse.attributes.SessionEnergyCharged, + clusters.EnergyEvseMode.attributes.SupportedModes, + clusters.EnergyEvseMode.attributes.CurrentMode, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + } + test.socket.matter:__set_channel_ordering("relaxed") + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.evseChargingSession.targetEndTime("1970-01-01T00:00:00Z"))) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Assert profile applied over doConfigure", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "evse-energy-meas" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_coroutine_test( + "Ensure timers are created for the device", + function() + local poll_timer = mock_device:get_field("__recurring_poll_timer") + assert(poll_timer ~= nil, "poll_timer should exist") + + local report_poll_timer = mock_device:get_field("__recurring_report_poll_timer") + assert(report_poll_timer ~= nil, "report_poll_timer should exist") + end +) + +test.register_coroutine_test( + "Ensure timers are created for the device", + function() + test.socket.matter:__set_channel_ordering("relaxed") + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "removed" }) + test.wait_for_events() + + local poll_timer = mock_device:get_field("__recurring_poll_timer") + assert(poll_timer == nil, "poll_timer should not exist") + + local report_poll_timer = mock_device:get_field("__recurring_report_poll_timer") + assert(report_poll_timer == nil, "report_poll_timer should not exist") + end +) + +test.register_coroutine_test( + "Ensure that every 60 seconds the driver reads the CumulativeEnergyImported attribute for both endpoints", + function() + test.mock_time.advance_time(60) + test.socket.matter:__set_channel_ordering("relaxed") + local CumulativeEnergyImportedReadReq = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported + :read(mock_device, ELECTRICAL_SENSOR_EP_ONE) + CumulativeEnergyImportedReadReq:merge(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read( + mock_device, ELECTRICAL_SENSOR_EP_TWO)) + test.socket.matter:__expect_send({ + mock_device.id, + CumulativeEnergyImportedReadReq + }) + test.wait_for_events() + end, + { + test_init = function() + test_init() + test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") + end + } +) + +test.register_coroutine_test( + "Ensure the total accumulated powerConsumption for both endpoints is reported every 15 minutes", + function() + test.socket.matter:__set_channel_ordering("relaxed") + test.socket.capability:__set_channel_ordering("relaxed") + + local CumulativeEnergyImportedReadReq = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported + :read(mock_device, ELECTRICAL_SENSOR_EP_ONE) + CumulativeEnergyImportedReadReq:merge(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read( + mock_device, ELECTRICAL_SENSOR_EP_TWO)) + + test.socket.matter:__expect_send({ + mock_device.id, + CumulativeEnergyImportedReadReq + }) + + test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes + .CumulativeEnergyImported:build_test_report_data(mock_device, + ELECTRICAL_SENSOR_EP_ONE, + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 100000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --100Wh + + test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes + .CumulativeEnergyImported:build_test_report_data(mock_device, + ELECTRICAL_SENSOR_EP_TWO, + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 150000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --150Wh + + test.wait_for_events() + test.mock_time.advance_time(60 * 15) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.powerConsumptionReport.powerConsumption({ + energy = 250, + deltaEnergy = 250, + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:14:59Z" + })) + ) + + test.wait_for_events() + end, + { + test_init = function() + test_init() + test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "create_poll_report_schedule") + test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") + end + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index 27fa823d6d..75d091db8e 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -148,3 +148,18 @@ matterGeneric: deviceTypes: - id: 0x0076 deviceProfileName: smoke-co + - id: "matter/rain/sensor" + deviceLabel: Matter Rain Sensor + deviceTypes: + - id: 0x0044 + deviceProfileName: rain-battery + - id: "matter/freeze/detector" + deviceLabel: Matter Water Freeze Detector + deviceTypes: + - id: 0x0041 + deviceProfileName: freeze-battery + - id: "matter/leak/detector" + deviceLabel: Matter Water Leak Detector + deviceTypes: + - id: 0x0043 + deviceProfileName: leak-battery diff --git a/drivers/SmartThings/matter-sensor/profiles/freeze-battery-fault-freezeSensitivity.yml b/drivers/SmartThings/matter-sensor/profiles/freeze-battery-fault-freezeSensitivity.yml new file mode 100644 index 0000000000..d01dac215c --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/freeze-battery-fault-freezeSensitivity.yml @@ -0,0 +1,25 @@ +name: freeze-battery-fault-freezeSensitivity +components: +- id: main + capabilities: + - id: temperatureAlarm + version: 1 + config: + values: + - key: "temperatureAlarm.value" + enabledValues: + - cleared + - freeze + - id: battery + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: WaterFreezeDetector +preferences: + - preferenceId: freezeSensitivity + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/freeze-battery-fault.yml b/drivers/SmartThings/matter-sensor/profiles/freeze-battery-fault.yml new file mode 100644 index 0000000000..4c14b5e4d7 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/freeze-battery-fault.yml @@ -0,0 +1,23 @@ +name: freeze-battery-fault +components: +- id: main + capabilities: + - id: temperatureAlarm + version: 1 + config: + values: + - key: "temperatureAlarm.value" + enabledValues: + - cleared + - freeze + - id: battery + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: WaterFreezeDetector + diff --git a/drivers/SmartThings/matter-sensor/profiles/freeze-battery-freezeSensitivity.yml b/drivers/SmartThings/matter-sensor/profiles/freeze-battery-freezeSensitivity.yml new file mode 100644 index 0000000000..5fb72e6cf7 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/freeze-battery-freezeSensitivity.yml @@ -0,0 +1,23 @@ +name: freeze-battery-freezeSensitivity +components: +- id: main + capabilities: + - id: temperatureAlarm + version: 1 + config: + values: + - key: "temperatureAlarm.value" + enabledValues: + - cleared + - freeze + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: WaterFreezeDetector +preferences: + - preferenceId: freezeSensitivity + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/freeze-battery.yml b/drivers/SmartThings/matter-sensor/profiles/freeze-battery.yml new file mode 100644 index 0000000000..c0cea79d51 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/freeze-battery.yml @@ -0,0 +1,20 @@ +name: freeze-battery +components: +- id: main + capabilities: + - id: temperatureAlarm + version: 1 + config: + values: + - key: "temperatureAlarm.value" + enabledValues: + - cleared + - freeze + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: WaterFreezeDetector diff --git a/drivers/SmartThings/matter-sensor/profiles/freeze-fault-freezeSensitivity.yml b/drivers/SmartThings/matter-sensor/profiles/freeze-fault-freezeSensitivity.yml new file mode 100644 index 0000000000..2d270fc29e --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/freeze-fault-freezeSensitivity.yml @@ -0,0 +1,23 @@ +name: freeze-fault-freezeSensitivity +components: +- id: main + capabilities: + - id: temperatureAlarm + version: 1 + config: + values: + - key: "temperatureAlarm.value" + enabledValues: + - cleared + - freeze + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: WaterFreezeDetector +preferences: + - preferenceId: freezeSensitivity + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/freeze-fault.yml b/drivers/SmartThings/matter-sensor/profiles/freeze-fault.yml new file mode 100644 index 0000000000..de91fc5034 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/freeze-fault.yml @@ -0,0 +1,20 @@ +name: freeze-fault +components: +- id: main + capabilities: + - id: temperatureAlarm + version: 1 + config: + values: + - key: "temperatureAlarm.value" + enabledValues: + - cleared + - freeze + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: WaterFreezeDetector diff --git a/drivers/SmartThings/matter-sensor/profiles/freeze-leak-fault-freezeSensitivity-leakSensitivity.yml b/drivers/SmartThings/matter-sensor/profiles/freeze-leak-fault-freezeSensitivity-leakSensitivity.yml new file mode 100644 index 0000000000..7211714c1e --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/freeze-leak-fault-freezeSensitivity-leakSensitivity.yml @@ -0,0 +1,27 @@ +name: freeze-leak-fault-freezeSensitivity-leakSensitivity +components: +- id: main + capabilities: + - id: temperatureAlarm + version: 1 + config: + values: + - key: "temperatureAlarm.value" + enabledValues: + - cleared + - freeze + - id: waterSensor + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: LeakSensor +preferences: + - preferenceId: freezeSensitivity + explicit: true + - preferenceId: leakSensitivity + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/leak-battery-fault-leakSensitivity.yml b/drivers/SmartThings/matter-sensor/profiles/leak-battery-fault-leakSensitivity.yml new file mode 100644 index 0000000000..0071ff9687 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/leak-battery-fault-leakSensitivity.yml @@ -0,0 +1,19 @@ +name: leak-battery-fault-leakSensitivity +components: +- id: main + capabilities: + - id: waterSensor + version: 1 + - id: battery + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: LeakSensor +preferences: + - preferenceId: leakSensitivity + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/leak-battery-fault.yml b/drivers/SmartThings/matter-sensor/profiles/leak-battery-fault.yml new file mode 100644 index 0000000000..6fd911c331 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/leak-battery-fault.yml @@ -0,0 +1,16 @@ +name: leak-battery-fault +components: +- id: main + capabilities: + - id: waterSensor + version: 1 + - id: battery + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: LeakSensor diff --git a/drivers/SmartThings/matter-sensor/profiles/leak-battery-leakSensitivity.yml b/drivers/SmartThings/matter-sensor/profiles/leak-battery-leakSensitivity.yml new file mode 100644 index 0000000000..d7eec73cc7 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/leak-battery-leakSensitivity.yml @@ -0,0 +1,17 @@ +name: leak-battery-leakSensitivity +components: +- id: main + capabilities: + - id: waterSensor + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: LeakSensor +preferences: + - preferenceId: leakSensitivity + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/leak-battery.yml b/drivers/SmartThings/matter-sensor/profiles/leak-battery.yml new file mode 100644 index 0000000000..6b90147bea --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/leak-battery.yml @@ -0,0 +1,14 @@ +name: leak-battery +components: +- id: main + capabilities: + - id: waterSensor + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: LeakSensor diff --git a/drivers/SmartThings/matter-sensor/profiles/leak-fault-leakSensitivity.yml b/drivers/SmartThings/matter-sensor/profiles/leak-fault-leakSensitivity.yml new file mode 100644 index 0000000000..51697afe81 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/leak-fault-leakSensitivity.yml @@ -0,0 +1,17 @@ +name: leak-fault-leakSensitivity +components: +- id: main + capabilities: + - id: waterSensor + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: LeakSensor +preferences: + - preferenceId: leakSensitivity + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/leak-fault.yml b/drivers/SmartThings/matter-sensor/profiles/leak-fault.yml new file mode 100644 index 0000000000..3b79709f3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/leak-fault.yml @@ -0,0 +1,14 @@ +name: leak-fault +components: +- id: main + capabilities: + - id: waterSensor + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: LeakSensor diff --git a/drivers/SmartThings/matter-sensor/profiles/rain-battery-fault-rainSensitivity.yml b/drivers/SmartThings/matter-sensor/profiles/rain-battery-fault-rainSensitivity.yml new file mode 100644 index 0000000000..9418abc845 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/rain-battery-fault-rainSensitivity.yml @@ -0,0 +1,19 @@ +name: rain-battery-fault-rainSensitivity +components: +- id: main + capabilities: + - id: rainSensor + version: 1 + - id: battery + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RainSensor +preferences: + - preferenceId: rainSensitivity + explicit: true \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/profiles/rain-battery-fault.yml b/drivers/SmartThings/matter-sensor/profiles/rain-battery-fault.yml new file mode 100644 index 0000000000..f1db9cafb8 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/rain-battery-fault.yml @@ -0,0 +1,16 @@ +name: rain-battery-fault +components: +- id: main + capabilities: + - id: rainSensor + version: 1 + - id: battery + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RainSensor diff --git a/drivers/SmartThings/matter-sensor/profiles/rain-battery-rainSensitivity.yml b/drivers/SmartThings/matter-sensor/profiles/rain-battery-rainSensitivity.yml new file mode 100644 index 0000000000..ee3c7c1a65 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/rain-battery-rainSensitivity.yml @@ -0,0 +1,17 @@ +name: rain-battery-rainSensitivity +components: +- id: main + capabilities: + - id: rainSensor + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RainSensor +preferences: + - preferenceId: rainSensitivity + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/rain-battery.yml b/drivers/SmartThings/matter-sensor/profiles/rain-battery.yml new file mode 100644 index 0000000000..ca794aeaaa --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/rain-battery.yml @@ -0,0 +1,14 @@ +name: rain-battery +components: +- id: main + capabilities: + - id: rainSensor + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RainSensor diff --git a/drivers/SmartThings/matter-sensor/profiles/rain-fault-rainSensitivity.yml b/drivers/SmartThings/matter-sensor/profiles/rain-fault-rainSensitivity.yml new file mode 100644 index 0000000000..9f080c12b6 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/rain-fault-rainSensitivity.yml @@ -0,0 +1,17 @@ +name: rain-fault-rainSensitivity +components: +- id: main + capabilities: + - id: rainSensor + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RainSensor +preferences: + - preferenceId: rainSensitivity + explicit: true \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/profiles/rain-fault.yml b/drivers/SmartThings/matter-sensor/profiles/rain-fault.yml new file mode 100644 index 0000000000..c5a3a8866a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/rain-fault.yml @@ -0,0 +1,14 @@ +name: rain-fault +components: +- id: main + capabilities: + - id: rainSensor + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RainSensor diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/init.lua b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/init.lua new file mode 100644 index 0000000000..f5258619fa --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/init.lua @@ -0,0 +1,81 @@ +local cluster_base = require "st.matter.cluster_base" +local BooleanStateConfigurationServerAttributes = require "BooleanStateConfiguration.server.attributes" +local BooleanStateConfigurationTypes = require "BooleanStateConfiguration.types" + +local BooleanStateConfiguration = {} + +BooleanStateConfiguration.ID = 0x0080 +BooleanStateConfiguration.NAME = "BooleanStateConfiguration" +BooleanStateConfiguration.server = {} +BooleanStateConfiguration.client = {} +BooleanStateConfiguration.server.attributes = BooleanStateConfigurationServerAttributes:set_parent_cluster(BooleanStateConfiguration) +BooleanStateConfiguration.types = BooleanStateConfigurationTypes + +function BooleanStateConfiguration:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "CurrentSensitivityLevel", + [0x0001] = "SupportedSensitivityLevels", + [0x0002] = "DefaultSensitivityLevel", + [0x0003] = "AlarmsActive", + [0x0004] = "AlarmsSuppressed", + [0x0005] = "AlarmsEnabled", + [0x0006] = "AlarmsSupported", + [0x0007] = "SensorFault", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +BooleanStateConfiguration.attribute_direction_map = { + ["CurrentSensitivityLevel"] = "server", + ["SupportedSensitivityLevels"] = "server", + ["DefaultSensitivityLevel"] = "server", + ["AlarmsActive"] = "server", + ["AlarmsSuppressed"] = "server", + ["AlarmsEnabled"] = "server", + ["AlarmsSupported"] = "server", + ["SensorFault"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +do + local has_aliases, aliases = pcall(require, "BooleanStateConfiguration.server.attributes") + if has_aliases then + for alias, _ in pairs(aliases) do + BooleanStateConfiguration.attribute_direction_map[alias] = "server" + end + end +end + +BooleanStateConfiguration.FeatureMap = BooleanStateConfiguration.types.Feature + +function BooleanStateConfiguration.are_features_supported(feature, feature_map) + if (BooleanStateConfiguration.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 = BooleanStateConfiguration.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, BooleanStateConfiguration.NAME)) + end + return BooleanStateConfiguration[direction].attributes[key] +end +BooleanStateConfiguration.attributes = {} +setmetatable(BooleanStateConfiguration.attributes, attribute_helper_mt) + +setmetatable(BooleanStateConfiguration, {__index = cluster_base}) + +return BooleanStateConfiguration + diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/CurrentSensitivityLevel.lua b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/CurrentSensitivityLevel.lua new file mode 100644 index 0000000000..ea28550422 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/CurrentSensitivityLevel.lua @@ -0,0 +1,80 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local CurrentSensitivityLevel = { + ID = 0x0000, + NAME = "CurrentSensitivityLevel", + base_type = require "st.matter.data_types.Uint8", +} + +function CurrentSensitivityLevel:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function CurrentSensitivityLevel:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function CurrentSensitivityLevel:write(device, endpoint_id, value) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.write( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, --event_id + data + ) +end + +function CurrentSensitivityLevel:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function CurrentSensitivityLevel:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CurrentSensitivityLevel: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 CurrentSensitivityLevel:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(CurrentSensitivityLevel, {__call = CurrentSensitivityLevel.new_value, __index = CurrentSensitivityLevel.base_type}) +return CurrentSensitivityLevel diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/DefaultSensitivityLevel.lua b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/DefaultSensitivityLevel.lua new file mode 100644 index 0000000000..dc343735dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/DefaultSensitivityLevel.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local DefaultSensitivityLevel = { + ID = 0x0002, + NAME = "DefaultSensitivityLevel", + base_type = require "st.matter.data_types.Uint8", +} + +function DefaultSensitivityLevel:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function DefaultSensitivityLevel:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function DefaultSensitivityLevel:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function DefaultSensitivityLevel:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function DefaultSensitivityLevel: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 DefaultSensitivityLevel:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(DefaultSensitivityLevel, {__call = DefaultSensitivityLevel.new_value, __index = DefaultSensitivityLevel.base_type}) +return DefaultSensitivityLevel diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SensorFault.lua b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SensorFault.lua new file mode 100644 index 0000000000..83db4bc404 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SensorFault.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SensorFault = { + ID = 0x0007, + NAME = "SensorFault", + base_type = require "BooleanStateConfiguration.types.SensorFaultBitmap", +} + +function SensorFault:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function SensorFault:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function SensorFault:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function SensorFault:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SensorFault:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function SensorFault:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SensorFault, {__call = SensorFault.new_value, __index = SensorFault.base_type}) +return SensorFault \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SupportedSensitivityLevels.lua b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SupportedSensitivityLevels.lua new file mode 100644 index 0000000000..c80177d043 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SupportedSensitivityLevels.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SupportedSensitivityLevels = { + ID = 0x0001, + NAME = "SupportedSensitivityLevels", + base_type = require "st.matter.data_types.Uint8", +} + +function SupportedSensitivityLevels:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function SupportedSensitivityLevels:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function SupportedSensitivityLevels:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function SupportedSensitivityLevels:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SupportedSensitivityLevels: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 SupportedSensitivityLevels:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(SupportedSensitivityLevels, {__call = SupportedSensitivityLevels.new_value, __index = SupportedSensitivityLevels.base_type}) +return SupportedSensitivityLevels diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/init.lua new file mode 100644 index 0000000000..47ef55963b --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/init.lua @@ -0,0 +1,25 @@ +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("BooleanStateConfiguration.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 BooleanStateConfigurationServerAttributes = {} + +function BooleanStateConfigurationServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(BooleanStateConfigurationServerAttributes, attr_mt) + +return BooleanStateConfigurationServerAttributes + + diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/Feature.lua new file mode 100644 index 0000000000..3a4cb77058 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/Feature.lua @@ -0,0 +1,119 @@ +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.VISUAL = 0x0001 +Feature.AUDIBLE = 0x0002 +Feature.ALARM_SUPPRESS = 0x0004 +Feature.SENSITIVITY_LEVEL = 0x0008 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + VISUAL = 0x0001, + AUDIBLE = 0x0002, + ALARM_SUPPRESS = 0x0004, + SENSITIVITY_LEVEL = 0x0008, +} + +Feature.is_visual_set = function(self) + return (self.value & self.VISUAL) ~= 0 +end + +Feature.set_visual = function(self) + if self.value ~= nil then + self.value = self.value | self.VISUAL + else + self.value = self.VISUAL + end +end + +Feature.unset_visual = function(self) + self.value = self.value & (~self.VISUAL & self.BASE_MASK) +end +Feature.is_audible_set = function(self) + return (self.value & self.AUDIBLE) ~= 0 +end + +Feature.set_audible = function(self) + if self.value ~= nil then + self.value = self.value | self.AUDIBLE + else + self.value = self.AUDIBLE + end +end + +Feature.unset_audible = function(self) + self.value = self.value & (~self.AUDIBLE & self.BASE_MASK) +end + +Feature.is_alarm_suppress_set = function(self) + return (self.value & self.ALARM_SUPPRESS) ~= 0 +end + +Feature.set_alarm_suppress = function(self) + if self.value ~= nil then + self.value = self.value | self.ALARM_SUPPRESS + else + self.value = self.ALARM_SUPPRESS + end +end + +Feature.unset_alarm_suppress = function(self) + self.value = self.value & (~self.ALARM_SUPPRESS & self.BASE_MASK) +end + +Feature.is_sensitivity_level_set = function(self) + return (self.value & self.SENSITIVITY_LEVEL) ~= 0 +end + +Feature.set_sensitivity_level = function(self) + if self.value ~= nil then + self.value = self.value | self.SENSITIVITY_LEVEL + else + self.value = self.SENSITIVITY_LEVEL + end +end + +Feature.unset_sensitivity_level = function(self) + self.value = self.value & (~self.SENSITIVITY_LEVEL & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.VISUAL | + Feature.AUDIBLE | + Feature.ALARM_SUPPRESS | + Feature.SENSITIVITY_LEVEL + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_visual_set = Feature.is_visual_set, + set_visual = Feature.set_visual, + unset_visual = Feature.unset_visual, + is_audible_set = Feature.is_audible_set, + set_audible = Feature.set_audible, + unset_audible = Feature.unset_audible, + is_alarm_suppress_set = Feature.is_alarm_suppress_set, + set_alarm_suppress = Feature.set_alarm_suppress, + unset_alarm_suppress = Feature.unset_alarm_suppress, + is_sensitivity_level_set = Feature.is_sensitivity_level_set, + set_sensitivity_level = Feature.set_sensitivity_level, + unset_sensitivity_level = Feature.unset_sensitivity_level, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/SensorFaultBitmap.lua b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/SensorFaultBitmap.lua new file mode 100644 index 0000000000..c9399e50f1 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/SensorFaultBitmap.lua @@ -0,0 +1,44 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local SensorFaultBitmap = {} +local new_mt = UintABC.new_mt({NAME = "SensorFaultBitmap", ID = data_types.name_to_id_map["Uint16"]}, 2) + +SensorFaultBitmap.BASE_MASK = 0xFFFF +SensorFaultBitmap.GENERAL_FAULT = 0x0001 + +SensorFaultBitmap.mask_fields = { + BASE_MASK = 0xFFFF, + GENERAL_FAULT = 0x0001, +} + +SensorFaultBitmap.is_general_fault_set = function(self) + return (self.value & self.GENERAL_FAULT) ~= 0 +end + +SensorFaultBitmap.set_general_fault = function(self) + if self.value ~= nil then + self.value = self.value | self.GENERAL_FAULT + else + self.value = self.GENERAL_FAULT + end +end + +SensorFaultBitmap.unset_general_fault = function(self) + self.value = self.value & (~self.GENERAL_FAULT & self.BASE_MASK) +end + + +SensorFaultBitmap.mask_methods = { + is_general_fault_set = SensorFaultBitmap.is_general_fault_set, + set_general_fault = SensorFaultBitmap.set_general_fault, + unset_general_fault = SensorFaultBitmap.unset_general_fault, +} + +SensorFaultBitmap.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(SensorFaultBitmap, new_mt) + +return SensorFaultBitmap diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/init.lua b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/init.lua new file mode 100644 index 0000000000..79aae9c6e9 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/init.lua @@ -0,0 +1,14 @@ +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("BooleanStateConfiguration.types." .. key) + end + return types_mt.__types_cache[key] +end + +local BooleanStateConfigurationTypes = {} + +setmetatable(BooleanStateConfigurationTypes, types_mt) + +return BooleanStateConfigurationTypes diff --git a/drivers/SmartThings/matter-sensor/src/embedded-cluster-utils.lua b/drivers/SmartThings/matter-sensor/src/embedded-cluster-utils.lua index 3f6fab859d..366453629e 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded-cluster-utils.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded-cluster-utils.lua @@ -18,6 +18,10 @@ if version.api < 10 then clusters.SmokeCoAlarm = require "SmokeCoAlarm" end +if version.api < 11 then + clusters.BooleanStateConfiguration = require "BooleanStateConfiguration" +end + local embedded_cluster_utils = {} local embedded_clusters = { diff --git a/drivers/SmartThings/matter-sensor/src/init.lua b/drivers/SmartThings/matter-sensor/src/init.lua index 67754c409f..bc4325e4db 100644 --- a/drivers/SmartThings/matter-sensor/src/init.lua +++ b/drivers/SmartThings/matter-sensor/src/init.lua @@ -17,6 +17,7 @@ local log = require "log" local clusters = require "st.matter.clusters" local MatterDriver = require "st.matter.driver" local utils = require "st.utils" +local embedded_cluster_utils = require "embedded-cluster-utils" -- This can be removed once LuaLibs supports the PressureMeasurement cluster if not pcall(function(cluster) return clusters[cluster] end, @@ -37,11 +38,15 @@ if version.api < 10 then clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" clusters.SmokeCoAlarm = require "SmokeCoAlarm" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" +end + +-- Include driver-side definitions when lua libs api version is < 11 +if version.api < 11 then + clusters.BooleanStateConfiguration = require "BooleanStateConfiguration" end -local BATTERY_CHECKED = "__battery_checked" local TEMP_BOUND_RECEIVED = "__temp_bound_received" local TEMP_MIN = "__temp_min" local TEMP_MAX = "__temp_max" @@ -56,6 +61,33 @@ local function set_field_for_endpoint(device, field, endpoint, value, additional device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) end +local BOOLEAN_DEVICE_TYPE_INFO = { + ["RAIN_SENSOR"] = { id = 0x0044, sensitivity_preference = "rainSensitivity", sensitivity_max = "rainMax" }, + ["WATER_FREEZE_DETECTOR"] = { id = 0x0041, sensitivity_preference = "freezeSensitivity", sensitivity_max = "freezeMax" }, + ["WATER_LEAK_DETECTOR"] = { id = 0x0043, sensitivity_preference = "leakSensitivity", sensitivity_max = "leakMax" }, + ["CONTACT_SENSOR"] = { id = 0x0015, sensitivity_preference = "N/A", sensitivity_max = "N/A" }, +} + +local ORDERED_DEVICE_TYPE_INFO = { + "RAIN_SENSOR", + "WATER_FREEZE_DETECTOR", + "WATER_LEAK_DETECTOR", + "CONTACT_SENSOR" +} + +local function set_boolean_device_type_per_endpoint(driver, device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + for dt_name, info in pairs(BOOLEAN_DEVICE_TYPE_INFO) do + if dt.device_type_id == info.id then + device:set_field(dt_name, ep.endpoint_id) + device:send(clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(device, ep.endpoint_id)) + end + end + end + end +end + local function supports_battery_percentage_remaining(device) local battery_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) @@ -66,7 +98,27 @@ local function supports_battery_percentage_remaining(device) return false end -local function check_for_battery(device) +local function supports_sensitivity_preferences(device) + local preference_names = "" + local sensitivity_eps = embedded_cluster_utils.get_endpoints(device, clusters.BooleanStateConfiguration.ID, + {feature_bitmap = clusters.BooleanStateConfiguration.types.Feature.SENSITIVITY_LEVEL}) + if sensitivity_eps and #sensitivity_eps > 0 then + for _, dt_name in ipairs(ORDERED_DEVICE_TYPE_INFO) do + for _, sensitivity_ep in pairs(sensitivity_eps) do + if device:get_field(dt_name) == sensitivity_ep and BOOLEAN_DEVICE_TYPE_INFO[dt_name].sensitivity_preference ~= "N/A" then + preference_names = preference_names .. "-" .. BOOLEAN_DEVICE_TYPE_INFO[dt_name].sensitivity_preference + end + end + end + end + return preference_names +end + +local function device_added(driver, device) + set_boolean_device_type_per_endpoint(driver, device) +end + +local function do_configure(driver, device) local profile_name = "" if device:supports_capability(capabilities.motionSensor) then @@ -93,28 +145,64 @@ local function check_for_battery(device) profile_name = profile_name .. "-pressure" end + if device:supports_capability(capabilities.rainSensor) then + profile_name = profile_name .. "-rain" + end + + if device:supports_capability(capabilities.temperatureAlarm) then + profile_name = profile_name .. "-freeze" + end + + if device:supports_capability(capabilities.waterSensor) then + profile_name = profile_name .. "-leak" + end + if supports_battery_percentage_remaining(device) then profile_name = profile_name .. "-battery" end + if device:supports_capability(capabilities.hardwareFault) then + profile_name = profile_name .. "-fault" + end + + local concatenated_preferences = supports_sensitivity_preferences(device) + profile_name = profile_name .. concatenated_preferences + -- remove leading "-" profile_name = string.sub(profile_name, 2) + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) device:try_update_metadata({profile = profile_name}) - device:set_field(BATTERY_CHECKED, 1, {persist = true}) end local function device_init(driver, device) log.info("device init") - if not device:get_field(BATTERY_CHECKED) then - check_for_battery(device) - end device:subscribe() end local function info_changed(driver, device, event, args) if device.profile.id ~= args.old_st_store.profile.id then device:subscribe() + set_boolean_device_type_per_endpoint(driver, device) + end + if not device.preferences then + return + end + for dt_name, info in pairs(BOOLEAN_DEVICE_TYPE_INFO) do + local dt_ep = device:get_field(dt_name) + if dt_ep and info.sensitivity_preference and (device.preferences[info.sensitivity_preference] ~= args.old_st_store.preferences[info.sensitivity_preference]) then + local sensitivity_preference = device.preferences[info.sensitivity_preference] + if sensitivity_preference == "2" then -- high + local max_sensitivity_level = device:get_field(info.sensitivity_max) - 1 + device:send(clusters.BooleanStateConfiguration.attributes.CurrentSensitivityLevel:write(device, dt_ep, max_sensitivity_level)) + elseif sensitivity_preference == "1" then -- medium + local medium_sensitivity_level = math.floor((device:get_field(info.sensitivity_max) + 1) / 2) + device:send(clusters.BooleanStateConfiguration.attributes.CurrentSensitivityLevel:write(device, dt_ep, medium_sensitivity_level)) + elseif sensitivity_preference == "0" then -- low + local min_sensitivity_level = 0 + device:send(clusters.BooleanStateConfiguration.attributes.CurrentSensitivityLevel:write(device, dt_ep, min_sensitivity_level)) + end + end end end @@ -166,11 +254,57 @@ local function humidity_attr_handler(driver, device, ib, response) end end +local BOOLEAN_CAP_EVENT_MAP = { + [true] = { + ["WATER_FREEZE_DETECTOR"] = capabilities.temperatureAlarm.temperatureAlarm.freeze(), + ["WATER_LEAK_DETECTOR"] = capabilities.waterSensor.water.wet(), + ["RAIN_SENSOR"] = capabilities.rainSensor.rain.detected(), + ["CONTACT_SENSOR"] = capabilities.contactSensor.contact.closed(), + }, + [false] = { + ["WATER_FREEZE_DETECTOR"] = capabilities.temperatureAlarm.temperatureAlarm.cleared(), + ["WATER_LEAK_DETECTOR"] = capabilities.waterSensor.water.dry(), + ["RAIN_SENSOR"] = capabilities.rainSensor.rain.undetected(), + ["CONTACT_SENSOR"] = capabilities.contactSensor.contact.open(), + } +} + local function boolean_attr_handler(driver, device, ib, response) - if ib.data.value then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.contactSensor.contact.closed()) + local name + for dt_name, _ in pairs(BOOLEAN_DEVICE_TYPE_INFO) do + local dt_ep_id = device:get_field(dt_name) + if ib.endpoint_id == dt_ep_id then + name = dt_name + break + end + end + if name then + device:emit_event_for_endpoint(ib.endpoint_id, BOOLEAN_CAP_EVENT_MAP[ib.data.value][name]) + elseif device:supports_capability(capabilities.contactSensor) then + -- The generic case where no device type has been specified but the profile uses this capability. + device:emit_event_for_endpoint(ib.endpoint_id, BOOLEAN_CAP_EVENT_MAP[ib.data.value]["CONTACT_SENSOR"]) else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.contactSensor.contact.open()) + log.error("No Boolean device type found on an endpoint, BooleanState handler aborted") + end +end + +local function supported_sensitivities_handler(driver, device, ib, response) + if not ib.data.value then + return + end + + for dt_name, info in pairs(BOOLEAN_DEVICE_TYPE_INFO) do + if device:get_field(dt_name) == ib.endpoint_id then + device:set_field(info.sensitivity_max, ib.data.value) + end + end +end + +local function sensor_fault_handler(driver, device, ib, response) + if ib.data.value > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.hardwareFault.hardwareFault.detected()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.hardwareFault.hardwareFault.clear()) end end @@ -196,7 +330,9 @@ end local matter_driver_template = { lifecycle_handlers = { init = device_init, - infoChanged = info_changed + infoChanged = info_changed, + doConfigure = do_configure, + added = device_added, }, matter_handlers = { attr = { @@ -223,6 +359,10 @@ local matter_driver_template = { [clusters.PressureMeasurement.ID] = { [clusters.PressureMeasurement.attributes.MeasuredValue.ID] = pressure_attr_handler, }, + [clusters.BooleanStateConfiguration.ID] = { + [clusters.BooleanStateConfiguration.attributes.SensorFault.ID] = sensor_fault_handler, + [clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels.ID] = supported_sensitivities_handler, + }, } }, -- TODO Once capabilities all have default handlers move this info there, and @@ -335,11 +475,21 @@ local matter_driver_template = { clusters.SmokeCoAlarm.attributes.TestInProgress, }, [capabilities.hardwareFault.ID] = { - clusters.SmokeCoAlarm.attributes.HardwareFaultAlert + clusters.SmokeCoAlarm.attributes.HardwareFaultAlert, + clusters.BooleanStateConfiguration.attributes.SensorFault, }, [capabilities.batteryLevel.ID] = { clusters.SmokeCoAlarm.attributes.BatteryAlert, }, + [capabilities.waterSensor.ID] = { + clusters.BooleanState.attributes.StateValue, + }, + [capabilities.temperatureAlarm.ID] = { + clusters.BooleanState.attributes.StateValue, + }, + [capabilities.rainSensor.ID] = { + clusters.BooleanState.attributes.StateValue, + }, }, capability_handlers = { }, @@ -351,6 +501,10 @@ local matter_driver_template = { capabilities.relativeHumidityMeasurement, capabilities.illuminanceMeasurement, capabilities.atmosphericPressureMeasurement, + capabilities.waterSensor, + capabilities.temperatureAlarm, + capabilities.rainSensor, + capabilities.hardwareFault }, sub_drivers = { require("air-quality-sensor"), diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_freeze_leak_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_freeze_leak_sensor.lua new file mode 100644 index 0000000000..49c846f4ec --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_freeze_leak_sensor.lua @@ -0,0 +1,252 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local clusters = require "st.matter.clusters" +clusters.BooleanStateConfiguration = require "BooleanStateConfiguration" + +local mock_device_freeze_leak = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("freeze-leak-fault-freezeSensitivity-leakSensitivity.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.BooleanState.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.BooleanStateConfiguration.ID, cluster_type = "SERVER", feature_map = 31}, + }, + device_types = { + {device_type_id = 0x0043, device_type_revision = 1} -- Water Leak Detector + } + }, + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.BooleanState.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.BooleanStateConfiguration.ID, cluster_type = "SERVER", feature_map = 31}, + }, + device_types = { + {device_type_id = 0x0041, device_type_revision = 1} -- Water Freeze Detector + } + } + } +}) + +local subscribed_attributes = { + clusters.BooleanState.attributes.StateValue, + clusters.BooleanStateConfiguration.attributes.SensorFault, +} + +local function test_init_freeze_leak() + local subscribe_request = subscribed_attributes[1]:subscribe(mock_device_freeze_leak) + for i, cluster in ipairs(subscribed_attributes) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_freeze_leak)) + end + end + test.socket.matter:__expect_send({mock_device_freeze_leak.id, subscribe_request}) + test.mock_device.add_test_device(mock_device_freeze_leak) + test.socket.device_lifecycle:__queue_receive({ mock_device_freeze_leak.id, "added" }) + test.socket.matter:__expect_send({mock_device_freeze_leak.id, clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(mock_device_freeze_leak, 1)}) + test.socket.matter:__expect_send({mock_device_freeze_leak.id, clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(mock_device_freeze_leak, 2)}) +end +test.set_test_init_function(test_init_freeze_leak) + +test.register_coroutine_test( + "Test profile change on init for Freeze and Leak combined device type", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_freeze_leak.id, "doConfigure" }) + mock_device_freeze_leak:expect_metadata_update({ profile = "freeze-leak-fault-freezeSensitivity-leakSensitivity" }) + mock_device_freeze_leak:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { test_init = test_init_freeze_leak } +) + +test.register_message_test( + "Boolean state freeze detection reports should generate correct messages", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device_freeze_leak.id, + clusters.BooleanState.server.attributes.StateValue:build_test_report_data(mock_device_freeze_leak, 2, false) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_freeze_leak:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.cleared()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device_freeze_leak.id, + clusters.BooleanState.server.attributes.StateValue:build_test_report_data(mock_device_freeze_leak, 2, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_freeze_leak:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.freeze()) + } + } +) + + +test.register_message_test( + "Boolean state leak detection reports should generate correct messages", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device_freeze_leak.id, + clusters.BooleanState.server.attributes.StateValue:build_test_report_data(mock_device_freeze_leak, 1, false) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_freeze_leak:generate_test_message("main", capabilities.waterSensor.water.dry()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device_freeze_leak.id, + clusters.BooleanState.server.attributes.StateValue:build_test_report_data(mock_device_freeze_leak, 1, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_freeze_leak:generate_test_message("main", capabilities.waterSensor.water.wet()) + } + } +) + +test.register_message_test( + "Test hardware fault alert handler", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device_freeze_leak.id, + clusters.BooleanStateConfiguration.attributes.SensorFault:build_test_report_data(mock_device_freeze_leak, 1, 0x1) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_freeze_leak:generate_test_message("main", capabilities.hardwareFault.hardwareFault.detected()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device_freeze_leak.id, + clusters.BooleanStateConfiguration.attributes.SensorFault:build_test_report_data(mock_device_freeze_leak, 1, 0x0) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_freeze_leak:generate_test_message("main", capabilities.hardwareFault.hardwareFault.clear()) + } + } +) + +test.register_coroutine_test( + "Check that preference updates to low as expected", function() + test.socket.matter:__queue_receive({ + mock_device_freeze_leak.id, + clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:build_test_report_data( + mock_device_freeze_leak, 2, 4 + ) + }) + test.wait_for_events() + + test.socket.device_lifecycle():__queue_receive(mock_device_freeze_leak:generate_info_changed({ preferences = { freezeSensitivity = "0" } })) + test.socket.matter:__expect_send({ + mock_device_freeze_leak.id, + clusters.BooleanStateConfiguration.attributes.CurrentSensitivityLevel:write(mock_device_freeze_leak, 2, 0) + }) + end +) + +test.register_coroutine_test( + "Check that preference updates to high as expected", function() + test.socket.matter:__queue_receive({ + mock_device_freeze_leak.id, + clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:build_test_report_data( + mock_device_freeze_leak, 2, 4 + ) + }) + test.wait_for_events() + test.socket.device_lifecycle():__queue_receive(mock_device_freeze_leak:generate_info_changed({ preferences = { freezeSensitivity = "2" } })) + test.socket.matter:__expect_send({ + mock_device_freeze_leak.id, + clusters.BooleanStateConfiguration.attributes.CurrentSensitivityLevel:write(mock_device_freeze_leak, 2, mock_device_freeze_leak:get_field("freezeMax") - 1) + }) + end +) + +test.register_coroutine_test( + "Check that preference updates to high after being set on-device as expected", function() + test.socket.matter:__queue_receive({ + mock_device_freeze_leak.id, + clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:build_test_report_data( + mock_device_freeze_leak, 2, 4 + ) + }) + test.wait_for_events() + test.socket.device_lifecycle():__queue_receive(mock_device_freeze_leak:generate_info_changed({ preferences = { freezeSensitivity = "2" } })) + test.socket.matter:__expect_send({ + mock_device_freeze_leak.id, + clusters.BooleanStateConfiguration.attributes.CurrentSensitivityLevel:write(mock_device_freeze_leak, 2, mock_device_freeze_leak:get_field("freezeMax") - 1) + }) + test.socket["matter"]:__queue_receive( + { + mock_device_freeze_leak.id, + clusters.BooleanStateConfiguration.attributes.CurrentSensitivityLevel:build_test_report_data( + mock_device_freeze_leak, 2, 2 -- put on level two + ) + } + ) + test.socket.device_lifecycle():__queue_receive(mock_device_freeze_leak:generate_info_changed({ preferences = { freezeSensitivity = "0" } })) + test.socket.matter:__expect_send({ + mock_device_freeze_leak.id, + clusters.BooleanStateConfiguration.attributes.CurrentSensitivityLevel:write(mock_device_freeze_leak, 2, 0) + }) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_pressure_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_pressure_sensor.lua index 9c439301b0..27d3b90842 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_pressure_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_pressure_sensor.lua @@ -14,7 +14,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" - local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" local PressureMeasurementCluster = require "PressureMeasurement" @@ -38,6 +37,9 @@ local matter_endpoints = { clusters = { {cluster_id = PressureMeasurementCluster.ID, cluster_type = "SERVER"}, {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0305, device_type_revision = 1} -- Pressure Sensor } } } diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_rain_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_rain_sensor.lua new file mode 100644 index 0000000000..3225a53e06 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_rain_sensor.lua @@ -0,0 +1,145 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" + +local clusters = require "st.matter.clusters" + +clusters.BooleanStateConfiguration = require "BooleanStateConfiguration" + +local mock_device_rain = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("rain-fault.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.BooleanState.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.BooleanStateConfiguration.ID, cluster_type = "SERVER", feature_map = 31}, + }, + device_types = { + {device_type_id = 0x0044, device_type_revision = 1} -- Rain Sensor + } + } + } +}) + +local subscribed_attributes = { + clusters.BooleanState.attributes.StateValue, + clusters.BooleanStateConfiguration.attributes.SensorFault, +} + +local function test_init_rain() + local subscribe_request = subscribed_attributes[1]:subscribe(mock_device_rain) + for i, cluster in ipairs(subscribed_attributes) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_rain)) + end + end + test.socket.matter:__expect_send({mock_device_rain.id, subscribe_request}) + test.mock_device.add_test_device(mock_device_rain) + test.socket.device_lifecycle:__queue_receive({ mock_device_rain.id, "added" }) + test.socket.matter:__expect_send({mock_device_rain.id, clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(mock_device_rain, 1)}) +end +test.set_test_init_function(test_init_rain) + +test.register_coroutine_test( + "Test profile change on init for Freeze and Leak combined device type", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_rain.id, "doConfigure" }) + mock_device_rain:expect_metadata_update({ profile = "rain-fault-rainSensitivity" }) + mock_device_rain:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { test_init = test_init_rain } +) + +test.register_message_test( + "Boolean state rain detection reports should generate correct messages", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device_rain.id, + clusters.BooleanState.server.attributes.StateValue:build_test_report_data(mock_device_rain, 1, false) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_rain:generate_test_message("main", capabilities.rainSensor.rain.undetected()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device_rain.id, + clusters.BooleanState.server.attributes.StateValue:build_test_report_data(mock_device_rain, 1, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_rain:generate_test_message("main", capabilities.rainSensor.rain.detected()) + } + } +) + +test.register_message_test( + "Test hardware fault alert handler", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device_rain.id, + clusters.BooleanStateConfiguration.attributes.SensorFault:build_test_report_data(mock_device_rain, 1, 0x1) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_rain:generate_test_message("main", capabilities.hardwareFault.hardwareFault.detected()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device_rain.id, + clusters.BooleanStateConfiguration.attributes.SensorFault:build_test_report_data(mock_device_rain, 1, 0x0) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_rain:generate_test_message("main", capabilities.hardwareFault.hardwareFault.clear()) + } + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_featuremap.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_featuremap.lua index 18fa4573e6..ec294d70fb 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_featuremap.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_featuremap.lua @@ -134,7 +134,11 @@ local function test_init_humidity_battery() test.socket.matter:__expect_send({mock_device_humidity_battery.id, subscribe_request_humidity_battery}) test.mock_device.add_test_device(mock_device_humidity_battery) + + test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "doConfigure" }) mock_device_humidity_battery:expect_metadata_update({ profile = "humidity-battery" }) + mock_device_humidity_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_humidity_no_battery() @@ -147,7 +151,11 @@ local function test_init_humidity_no_battery() test.socket.matter:__expect_send({mock_device_humidity_no_battery.id, subscribe_request_humidity_no_battery}) test.mock_device.add_test_device(mock_device_humidity_no_battery) + + test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_no_battery.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_no_battery.id, "doConfigure" }) mock_device_humidity_no_battery:expect_metadata_update({ profile = "humidity" }) + mock_device_humidity_no_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_temp_humidity() @@ -160,7 +168,11 @@ local function test_init_temp_humidity() test.socket.matter:__expect_send({mock_device_temp_humidity.id, subscribe_request_temp_humidity}) test.mock_device.add_test_device(mock_device_temp_humidity) + + test.socket.device_lifecycle:__queue_receive({ mock_device_temp_humidity.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_temp_humidity.id, "doConfigure" }) mock_device_temp_humidity:expect_metadata_update({ profile = "temperature-humidity" }) + mock_device_temp_humidity:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.register_coroutine_test( diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_rpc.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_rpc.lua index 9f7568fb2a..d8b38e392a 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_rpc.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_rpc.lua @@ -35,6 +35,9 @@ local matter_endpoints = { endpoint_id = 1, clusters = { {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "BOTH"}, + }, + device_types = { + device_type_id = 0x0301, device_type_revision = 1, -- Thermostat } } } diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 72760a5be9..e61e164a05 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -2374,6 +2374,17 @@ matterGeneric: deviceTypes: - id: 0x010B # Dimmable Plug-in Unit deviceProfileName: plug-level + - id: "matter/on-off/plug/electrical-sensor" + deviceLabel: Matter OnOff Plug + deviceTypes: + - id: 0x010A # On Off Plug-in Unit + - id: 0x0510 # Electrical Sensor + deviceProfileName: plug-power-energy-powerConsumption + - id: "matter/water-valve" + deviceLabel: Matter Water Valve + deviceTypes: + - id: 0x0042 # Water Valve + deviceProfileName: water-valve - id: "button" deviceLabel: Matter Button deviceTypes: diff --git a/drivers/SmartThings/matter-switch/profiles/plug-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/plug-energy-powerConsumption.yml new file mode 100644 index 0000000000..9c44a4d29f --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/plug-energy-powerConsumption.yml @@ -0,0 +1,16 @@ +name: plug-energy-powerConsumption +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartPlug diff --git a/drivers/SmartThings/matter-switch/profiles/plug-power-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/plug-power-energy-powerConsumption.yml new file mode 100644 index 0000000000..5d09912dd3 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/plug-power-energy-powerConsumption.yml @@ -0,0 +1,18 @@ +name: plug-power-energy-powerConsumption +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartPlug diff --git a/drivers/SmartThings/matter-switch/profiles/plug-power.yml b/drivers/SmartThings/matter-switch/profiles/plug-power.yml new file mode 100644 index 0000000000..018fba88ee --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/plug-power.yml @@ -0,0 +1,14 @@ +name: plug-power +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: powerMeter + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartPlug diff --git a/drivers/SmartThings/matter-switch/profiles/water-valve-level.yml b/drivers/SmartThings/matter-switch/profiles/water-valve-level.yml new file mode 100644 index 0000000000..48bfb78eef --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/water-valve-level.yml @@ -0,0 +1,18 @@ +name: water-valve-level +components: +- id: main + capabilities: + - id: valve + version: 1 + - id: level + version: 1 + config: + values: + - key: "level.value" + range: [0, 100] + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: WaterValve diff --git a/drivers/SmartThings/matter-switch/profiles/water-valve.yml b/drivers/SmartThings/matter-switch/profiles/water-valve.yml new file mode 100644 index 0000000000..7f2827b648 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/water-valve.yml @@ -0,0 +1,12 @@ +name: water-valve +components: +- id: main + capabilities: + - id: valve + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: WaterValve diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/init.lua b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/init.lua new file mode 100644 index 0000000000..83bc66aa1b --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/init.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local ElectricalEnergyMeasurementServerAttributes = require "ElectricalEnergyMeasurement.server.attributes" +local ElectricalEnergyMeasurementTypes = require "ElectricalEnergyMeasurement.types" +local ElectricalEnergyMeasurement = {} + +ElectricalEnergyMeasurement.ID = 0x0091 +ElectricalEnergyMeasurement.NAME = "ElectricalEnergyMeasurement" +ElectricalEnergyMeasurement.server = {} +ElectricalEnergyMeasurement.client = {} +ElectricalEnergyMeasurement.server.attributes = ElectricalEnergyMeasurementServerAttributes:set_parent_cluster(ElectricalEnergyMeasurement) +ElectricalEnergyMeasurement.types = ElectricalEnergyMeasurementTypes + +function ElectricalEnergyMeasurement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "Accuracy", + [0x0001] = "CumulativeEnergyImported", + [0x0002] = "CumulativeEnergyExported", + [0x0003] = "PeriodicEnergyImported", + [0x0004] = "PeriodicEnergyExported", + [0x0005] = "CumulativeEnergyReset", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +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 + +function ElectricalEnergyMeasurement.are_features_supported(feature, feature_map) + if (ElectricalEnergyMeasurement.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 = ElectricalEnergyMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ElectricalEnergyMeasurement.NAME)) + end + return ElectricalEnergyMeasurement[direction].attributes[key] +end +ElectricalEnergyMeasurement.attributes = {} +setmetatable(ElectricalEnergyMeasurement.attributes, attribute_helper_mt) + +setmetatable(ElectricalEnergyMeasurement, {__index = cluster_base}) + +return ElectricalEnergyMeasurement + diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyExported.lua b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyExported.lua new file mode 100644 index 0000000000..22befec642 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyExported.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local CumulativeEnergyExported = { + ID = 0x0002, + NAME = "CumulativeEnergyExported", + base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", +} + +function CumulativeEnergyExported:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function CumulativeEnergyExported:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CumulativeEnergyExported:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CumulativeEnergyExported:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CumulativeEnergyExported:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function CumulativeEnergyExported:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(CumulativeEnergyExported, {__call = CumulativeEnergyExported.new_value, __index = CumulativeEnergyExported.base_type}) +return CumulativeEnergyExported + diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyExported.lua b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyExported.lua new file mode 100644 index 0000000000..4c1ee29274 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyExported.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local PeriodicEnergyExported = { + ID = 0x0004, + NAME = "PeriodicEnergyExported", + base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", +} + +function PeriodicEnergyExported:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function PeriodicEnergyExported:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function PeriodicEnergyExported:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function PeriodicEnergyExported:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function PeriodicEnergyExported:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function PeriodicEnergyExported:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(PeriodicEnergyExported, {__call = PeriodicEnergyExported.new_value, __index = PeriodicEnergyExported.base_type}) +return PeriodicEnergyExported + diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..adfdf42bbf --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/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("ElectricalEnergyMeasurement.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 ElectricalEnergyMeasurementServerAttributes = {} + +function ElectricalEnergyMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ElectricalEnergyMeasurementServerAttributes, attr_mt) + +return ElectricalEnergyMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua new file mode 100644 index 0000000000..950b260227 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua @@ -0,0 +1,98 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +local EnergyMeasurementStruct = {} +local new_mt = StructureABC.new_mt({NAME = "EnergyMeasurementStruct", ID = data_types.name_to_id_map["Structure"]}) + +EnergyMeasurementStruct.field_defs = { + { + name = "energy", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Int64", + }, + { + name = "start_timestamp", + field_id = 1, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint32", + }, + { + name = "end_timestamp", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint32", + }, + { + name = "start_systime", + field_id = 3, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint64", + }, + { + name = "end_systime", + field_id = 4, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint64", + }, +} + +EnergyMeasurementStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for idx, field_def in ipairs(cls.field_defs) do + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + else + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +EnergyMeasurementStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = EnergyMeasurementStruct.init +new_mt.__index.serialize = EnergyMeasurementStruct.serialize + +EnergyMeasurementStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(EnergyMeasurementStruct, new_mt) + +return EnergyMeasurementStruct + diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/Feature.lua b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/Feature.lua new file mode 100644 index 0000000000..717ba6a2f3 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/Feature.lua @@ -0,0 +1,116 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.IMPORTED_ENERGY = 0x0001 +Feature.EXPORTED_ENERGY = 0x0002 +Feature.CUMULATIVE_ENERGY = 0x0004 +Feature.PERIODIC_ENERGY = 0x0008 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + IMPORTED_ENERGY = 0x0001, + EXPORTED_ENERGY = 0x0002, + CUMULATIVE_ENERGY = 0x0004, + PERIODIC_ENERGY = 0x0008, +} + +Feature.is_imported_energy_set = function(self) + return (self.value & self.IMPORTED_ENERGY) ~= 0 +end + +Feature.set_imported_energy = function(self) + if self.value ~= nil then + self.value = self.value | self.IMPORTED_ENERGY + else + self.value = self.IMPORTED_ENERGY + end +end + +Feature.unset_imported_energy = function(self) + self.value = self.value & (~self.IMPORTED_ENERGY & self.BASE_MASK) +end +Feature.is_exported_energy_set = function(self) + return (self.value & self.EXPORTED_ENERGY) ~= 0 +end + +Feature.set_exported_energy = function(self) + if self.value ~= nil then + self.value = self.value | self.EXPORTED_ENERGY + else + self.value = self.EXPORTED_ENERGY + end +end + +Feature.unset_exported_energy = function(self) + self.value = self.value & (~self.EXPORTED_ENERGY & self.BASE_MASK) +end +Feature.is_cumulative_energy_set = function(self) + return (self.value & self.CUMULATIVE_ENERGY) ~= 0 +end + +Feature.set_cumulative_energy = function(self) + if self.value ~= nil then + self.value = self.value | self.CUMULATIVE_ENERGY + else + self.value = self.CUMULATIVE_ENERGY + end +end + +Feature.unset_cumulative_energy = function(self) + self.value = self.value & (~self.CUMULATIVE_ENERGY & self.BASE_MASK) +end +Feature.is_periodic_energy_set = function(self) + return (self.value & self.PERIODIC_ENERGY) ~= 0 +end + +Feature.set_periodic_energy = function(self) + if self.value ~= nil then + self.value = self.value | self.PERIODIC_ENERGY + else + self.value = self.PERIODIC_ENERGY + end +end + +Feature.unset_periodic_energy = function(self) + self.value = self.value & (~self.PERIODIC_ENERGY & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.IMPORTED_ENERGY | + Feature.EXPORTED_ENERGY | + Feature.CUMULATIVE_ENERGY | + Feature.PERIODIC_ENERGY + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_imported_energy_set = Feature.is_imported_energy_set, + set_imported_energy = Feature.set_imported_energy, + unset_imported_energy = Feature.unset_imported_energy, + is_exported_energy_set = Feature.is_exported_energy_set, + set_exported_energy = Feature.set_exported_energy, + unset_exported_energy = Feature.unset_exported_energy, + is_cumulative_energy_set = Feature.is_cumulative_energy_set, + set_cumulative_energy = Feature.set_cumulative_energy, + unset_cumulative_energy = Feature.unset_cumulative_energy, + is_periodic_energy_set = Feature.is_periodic_energy_set, + set_periodic_energy = Feature.set_periodic_energy, + unset_periodic_energy = Feature.unset_periodic_energy, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/init.lua b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/init.lua new file mode 100644 index 0000000000..bb0c39fe0e --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/init.lua @@ -0,0 +1,15 @@ +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("ElectricalEnergyMeasurement.types." .. key) + end + return types_mt.__types_cache[key] +end + +local ElectricalEnergyMeasurementTypes = {} + +setmetatable(ElectricalEnergyMeasurementTypes, types_mt) + +return ElectricalEnergyMeasurementTypes + diff --git a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/init.lua b/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/init.lua new file mode 100644 index 0000000000..54785d16c6 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/init.lua @@ -0,0 +1,94 @@ +local cluster_base = require "st.matter.cluster_base" +local ElectricalPowerMeasurementServerAttributes = require "ElectricalPowerMeasurement.server.attributes" +local ElectricalPowerMeasurementTypes = require "ElectricalPowerMeasurement.types" + +local ElectricalPowerMeasurement = {} + +ElectricalPowerMeasurement.ID = 0x0090 +ElectricalPowerMeasurement.NAME = "ElectricalPowerMeasurement" +ElectricalPowerMeasurement.server = {} +ElectricalPowerMeasurement.client = {} +ElectricalPowerMeasurement.server.attributes = ElectricalPowerMeasurementServerAttributes:set_parent_cluster(ElectricalPowerMeasurement) +ElectricalPowerMeasurement.types = ElectricalPowerMeasurementTypes + +function ElectricalPowerMeasurement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "PowerMode", + [0x0001] = "NumberOfMeasurementTypes", + [0x0002] = "Accuracy", + [0x0003] = "Ranges", + [0x0004] = "Voltage", + [0x0005] = "ActiveCurrent", + [0x0006] = "ReactiveCurrent", + [0x0007] = "ApparentCurrent", + [0x0008] = "ActivePower", + [0x0009] = "ReactivePower", + [0x000A] = "ApparentPower", + [0x000B] = "RMSVoltage", + [0x000C] = "RMSCurrent", + [0x000D] = "RMSPower", + [0x000E] = "Frequency", + [0x000F] = "HarmonicCurrents", + [0x0010] = "HarmonicPhases", + [0x0011] = "PowerFactor", + [0x0012] = "NeutralCurrent", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +ElectricalPowerMeasurement.attribute_direction_map = { + ["PowerMode"] = "server", + ["NumberOfMeasurementTypes"] = "server", + ["Accuracy"] = "server", + ["Ranges"] = "server", + ["Voltage"] = "server", + ["ActiveCurrent"] = "server", + ["ReactiveCurrent"] = "server", + ["ApparentCurrent"] = "server", + ["ActivePower"] = "server", + ["ReactivePower"] = "server", + ["ApparentPower"] = "server", + ["RMSVoltage"] = "server", + ["RMSCurrent"] = "server", + ["RMSPower"] = "server", + ["Frequency"] = "server", + ["HarmonicCurrents"] = "server", + ["HarmonicPhases"] = "server", + ["PowerFactor"] = "server", + ["NeutralCurrent"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +ElectricalPowerMeasurement.FeatureMap = ElectricalPowerMeasurement.types.Feature + +function ElectricalPowerMeasurement.are_features_supported(feature, feature_map) + if (ElectricalPowerMeasurement.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ElectricalPowerMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ElectricalPowerMeasurement.NAME)) + end + return ElectricalPowerMeasurement[direction].attributes[key] +end +ElectricalPowerMeasurement.attributes = {} +setmetatable(ElectricalPowerMeasurement.attributes, attribute_helper_mt) + +setmetatable(ElectricalPowerMeasurement, {__index = cluster_base}) + +return ElectricalPowerMeasurement + diff --git a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua b/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua new file mode 100644 index 0000000000..6c34abd2f4 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ActivePower = { + ID = 0x0008, + NAME = "ActivePower", + base_type = require "st.matter.data_types.Int64", +} + +function ActivePower:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function ActivePower:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ActivePower:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ActivePower:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ActivePower: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 ActivePower:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(ActivePower, {__call = ActivePower.new_value, __index = ActivePower.base_type}) +return ActivePower + diff --git a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/server/attributes/init.lua new file mode 100644 index 0000000000..0c30fa8dd4 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/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("ElectricalPowerMeasurement.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 ElectricalPowerMeasurementServerAttributes = {} + +function ElectricalPowerMeasurementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ElectricalPowerMeasurementServerAttributes, attr_mt) + +return ElectricalPowerMeasurementServerAttributes + diff --git a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/types/Feature.lua b/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/types/Feature.lua new file mode 100644 index 0000000000..cbda4f3478 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/types/Feature.lua @@ -0,0 +1,138 @@ +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/ElectricalPowerMeasurement/types/init.lua b/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/types/init.lua new file mode 100644 index 0000000000..16d13a0688 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/types/init.lua @@ -0,0 +1,15 @@ +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("ElectricalPowerMeasurement.types." .. key) + end + return types_mt.__types_cache[key] +end + +local ElectricalPowerMeasurementTypes = {} + +setmetatable(ElectricalPowerMeasurementTypes, types_mt) + +return ElectricalPowerMeasurementTypes + diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/init.lua b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/init.lua new file mode 100644 index 0000000000..d8ab93412f --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/init.lua @@ -0,0 +1,123 @@ +local cluster_base = require "st.matter.cluster_base" +local ValveConfigurationAndControlServerAttributes = require "ValveConfigurationAndControl.server.attributes" +local ValveConfigurationAndControlServerCommands = require "ValveConfigurationAndControl.server.commands" +local ValveConfigurationAndControlTypes = require "ValveConfigurationAndControl.types" +local ValveConfigurationAndControl = {} + +ValveConfigurationAndControl.ID = 0x0081 +ValveConfigurationAndControl.NAME = "ValveConfigurationAndControl" +ValveConfigurationAndControl.server = {} +ValveConfigurationAndControl.client = {} +ValveConfigurationAndControl.server.attributes = ValveConfigurationAndControlServerAttributes:set_parent_cluster(ValveConfigurationAndControl) +ValveConfigurationAndControl.server.commands = ValveConfigurationAndControlServerCommands:set_parent_cluster(ValveConfigurationAndControl) +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 + return self.attributes[attr_name] + end + return nil +end + +function ValveConfigurationAndControl:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "Open", + [0x0001] = "Close", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + 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 = { + ["Open"] = "server", + ["Close"] = "server", +} + +ValveConfigurationAndControl.FeatureMap = ValveConfigurationAndControl.types.Feature + +function ValveConfigurationAndControl.are_features_supported(feature, feature_map) + if (ValveConfigurationAndControl.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 = ValveConfigurationAndControl.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ValveConfigurationAndControl.NAME)) + end + return ValveConfigurationAndControl[direction].attributes[key] +end +ValveConfigurationAndControl.attributes = {} +setmetatable(ValveConfigurationAndControl.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = ValveConfigurationAndControl.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, ValveConfigurationAndControl.NAME)) + end + return ValveConfigurationAndControl[direction].commands[key] +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/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua new file mode 100644 index 0000000000..807beba485 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local CurrentLevel = { + ID = 0x0006, + NAME = "CurrentLevel", + base_type = require "st.matter.data_types.Uint8", +} + +function CurrentLevel:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function CurrentLevel:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CurrentLevel:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CurrentLevel:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CurrentLevel: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 CurrentLevel:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(CurrentLevel, {__call = CurrentLevel.new_value, __index = CurrentLevel.base_type}) +return CurrentLevel diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/CurrentState.lua b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/CurrentState.lua new file mode 100644 index 0000000000..76f156d2a7 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/CurrentState.lua @@ -0,0 +1,67 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local CurrentState = { + ID = 0x0004, + NAME = "CurrentState", + base_type = require "ValveConfigurationAndControl.types.ValveStateEnum", +} + +function CurrentState:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function CurrentState:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CurrentState:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CurrentState:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CurrentState:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function CurrentState:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(CurrentState, {__call = CurrentState.new_value, __index = CurrentState.base_type}) +return CurrentState diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/init.lua new file mode 100644 index 0000000000..237818d98f --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/init.lua @@ -0,0 +1,23 @@ +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("ValveConfigurationAndControl.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 ValveConfigurationAndControlServerAttributes = {} + +function ValveConfigurationAndControlServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ValveConfigurationAndControlServerAttributes, attr_mt) + +return ValveConfigurationAndControlServerAttributes diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/Close.lua b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/Close.lua new file mode 100644 index 0000000000..baee7688ec --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/Close.lua @@ -0,0 +1,89 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local Close = {} + +Close.NAME = "Close" +Close.ID = 0x0001 +Close.field_defs = { +} + +function Close:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, + status + ) +end + +function Close:init(device, endpoint_id) + local out = {} + local args = {} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = Close, + __tostring = Close.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function Close:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function Close:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + base_type_obj.elements = elems +end + +function Close:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(Close, {__call = Close.init}) + +return Close diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/Open.lua b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/Open.lua new file mode 100644 index 0000000000..3f84040fc0 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/Open.lua @@ -0,0 +1,103 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" +local Open = {} + +Open.NAME = "Open" +Open.ID = 0x0000 +Open.field_defs = { + { + name = "open_duration", + field_id = 0, + is_nullable = true, + is_optional = true, + data_type = require "st.matter.data_types.Uint32", + }, + { + name = "target_level", + field_id = 1, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint8", + }, +} + +function Open:build_test_command_response(device, endpoint_id, status) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil, + status + ) +end + +function Open:init(device, endpoint_id, open_duration, target_level) + local out = {} + local args = {open_duration, target_level} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = Open, + __tostring = Open.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function Open:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function Open:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + base_type_obj.elements = elems +end + +function Open:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(Open, {__call = Open.init}) + +return Open diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/init.lua b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/init.lua new file mode 100644 index 0000000000..330e35bba3 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/init.lua @@ -0,0 +1,22 @@ +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("ValveConfigurationAndControl.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local ValveConfigurationAndControlServerCommands = {} + +function ValveConfigurationAndControlServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ValveConfigurationAndControlServerCommands, command_mt) + +return ValveConfigurationAndControlServerCommands diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/Feature.lua b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/Feature.lua new file mode 100644 index 0000000000..7433a99c9e --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/Feature.lua @@ -0,0 +1,74 @@ +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.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 | + Feature.LEVEL + if (feature <= max) and (feature >= 1) then + return true + else + return false + 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/ValveConfigurationAndControl/types/ValveStateEnum.lua b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/ValveStateEnum.lua new file mode 100644 index 0000000000..8b6da46ad8 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/ValveStateEnum.lua @@ -0,0 +1,29 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" +local ValveStateEnum = {} +local new_mt = UintABC.new_mt({NAME = "ValveStateEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.CLOSED] = "CLOSED", + [self.OPEN] = "OPEN", + [self.TRANSITIONING] = "TRANSITIONING", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.CLOSED = 0x00 +new_mt.__index.OPEN = 0x01 +new_mt.__index.TRANSITIONING = 0x02 + +ValveStateEnum.CLOSED = 0x00 +ValveStateEnum.OPEN = 0x01 +ValveStateEnum.TRANSITIONING = 0x02 + +ValveStateEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(ValveStateEnum, new_mt) + +return ValveStateEnum diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/init.lua b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/init.lua new file mode 100644 index 0000000000..835167f485 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/init.lua @@ -0,0 +1,14 @@ +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("ValveConfigurationAndControl.types." .. key) + end + return types_mt.__types_cache[key] +end + +local ValveConfigurationAndControlTypes = {} + +setmetatable(ValveConfigurationAndControlTypes, types_mt) + +return ValveConfigurationAndControlTypes diff --git a/drivers/SmartThings/matter-switch/src/embedded-cluster-utils.lua b/drivers/SmartThings/matter-switch/src/embedded-cluster-utils.lua new file mode 100644 index 0000000000..66db6097c7 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded-cluster-utils.lua @@ -0,0 +1,53 @@ +local clusters = require "st.matter.clusters" +local utils = require "st.utils" + +-- Include driver-side definitions when lua libs api version is < 11 +local version = require "version" +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" + clusters.ValveConfigurationAndControl = require "ValveConfigurationAndControl" +end + +local embedded_cluster_utils = {} + +local embedded_clusters = { + [clusters.ElectricalEnergyMeasurement.ID] = clusters.ElectricalEnergyMeasurement, + [clusters.ElectricalPowerMeasurement.ID] = clusters.ElectricalPowerMeasurement, + [clusters.ValveConfigurationAndControl.ID] = clusters.ValveConfigurationAndControl +} + +function embedded_cluster_utils.get_endpoints(device, cluster_id, opts) + -- If using older lua libs and need to check for an embedded cluster feature, + -- we must use the embedded cluster definitions here + if version.api < 11 and embedded_clusters[cluster_id] ~= nil then + local embedded_cluster = embedded_clusters[cluster_id] + local opts = opts or {} + if utils.table_size(opts) > 1 then + device.log.warn_with({hub_logs = true}, "Invalid options for get_endpoints") + return + end + local clus_has_features = function(clus, feature_bitmap) + if not feature_bitmap or not clus then return false end + return embedded_cluster.are_features_supported(feature_bitmap, clus.feature_map) + end + local eps = {} + for _, ep in ipairs(device.endpoints) do + for _, clus in ipairs(ep.clusters) do + if ((clus.cluster_id == cluster_id) + and (opts.feature_bitmap == nil or clus_has_features(clus, opts.feature_bitmap)) + and ((opts.cluster_type == nil and clus.cluster_type == "SERVER" or clus.cluster_type == "BOTH") + or (opts.cluster_type == clus.cluster_type)) + or (cluster_id == nil)) then + table.insert(eps, ep.endpoint_id) + if cluster_id == nil then break end + end + end + end + return eps + else + return device:get_endpoints(cluster_id, opts) + end + end + + return embedded_cluster_utils \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 9b90cfb1d7..6368b237da 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -149,6 +149,97 @@ local child_device_profile_overrides = { local detect_matter_thing +local CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" +local FIRST_EXPORT_REPORT_TIMESTAMP = "__first_export_report_timestamp" +local EXPORT_POLL_TIMER_SETTING_ATTEMPTED = "__export_poll_timer_setting_attempted" +local EXPORT_REPORT_TIMEOUT = "__export_report_timeout" +local TOTAL_EXPORTED_ENERGY = "__total_exported_energy" +local LAST_EXPORTED_REPORT_TIMESTAMP = "__last_exported_report_timestamp" +local RECURRING_EXPORT_REPORT_POLL_TIMER = "__recurring_export_report_poll_timer" +local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds +local SUBSCRIPTION_REPORT_OCCURRED = "__subscription_report_occurred" + +local embedded_cluster_utils = require "embedded-cluster-utils" + +-- Include driver-side definitions when lua libs api version is < 11 +local version = require "version" +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" + clusters.ValveConfigurationAndControl = require "ValveConfigurationAndControl" +end + +-- Return an ISO-8061 timestamp in UTC +local function iso8061Timestamp(time) + return os.date("!%Y-%m-%dT%H:%M:%SZ", time) +end + +local function delete_export_poll_schedule(device) + local export_poll_timer = device:get_field(RECURRING_EXPORT_REPORT_POLL_TIMER) + if export_poll_timer then + device.thread:cancel_timer(export_poll_timer) + device:set_field(RECURRING_EXPORT_REPORT_POLL_TIMER, nil) + device:set_field(EXPORT_POLL_TIMER_SETTING_ATTEMPTED, nil) + end +end + +local function send_export_poll_report(device, latest_total_exported_energy_wh) + local current_time = os.time() + local last_time = device:get_field(LAST_EXPORTED_REPORT_TIMESTAMP) or 0 + device:set_field(LAST_EXPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) + + -- Calculate the energy delta between reports + local energy_delta_wh = 0.0 + local previous_exported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, + capabilities.powerConsumptionReport.powerConsumption.NAME) + if previous_exported_report and previous_exported_report.energy then + energy_delta_wh = math.max(latest_total_exported_energy_wh - previous_exported_report.energy, 0.0) + end + + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' + device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ + start = iso8061Timestamp(last_time), + ["end"] = iso8061Timestamp(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_exported_energy_wh + })) +end + +local function create_poll_report_schedule(device) + local export_timer = device.thread:call_on_schedule( + device:get_field(EXPORT_REPORT_TIMEOUT), + send_export_poll_report(device, device:get_field(TOTAL_EXPORTED_ENERGY)), + "polling_export_report_schedule_timer" + ) + device:set_field(RECURRING_EXPORT_REPORT_POLL_TIMER, export_timer) +end + +local function set_poll_report_timer_and_schedule(device, is_cumulative_report) + local cumul_eps = embedded_cluster_utils.get_endpoints(device, + clusters.ElectricalEnergyMeasurement.ID, + {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY }) + if #cumul_eps == 0 then + device:set_field(CUMULATIVE_REPORTS_NOT_SUPPORTED, true) + end + if #cumul_eps > 0 and not is_cumulative_report then + return + elseif not device:get_field(SUBSCRIPTION_REPORT_OCCURRED) then + device:set_field(SUBSCRIPTION_REPORT_OCCURRED, true) + elseif not device:get_field(FIRST_EXPORT_REPORT_TIMESTAMP) then + device:set_field(FIRST_EXPORT_REPORT_TIMESTAMP, os.time()) + else + local first_timestamp = device:get_field(FIRST_EXPORT_REPORT_TIMESTAMP) + local second_timestamp = os.time() + local report_interval_secs = second_timestamp - first_timestamp + device:set_field(EXPORT_REPORT_TIMEOUT, math.max(report_interval_secs, MINIMUM_ST_ENERGY_REPORT_INTERVAL)) + -- the poll schedule is only needed for devices that support powerConsumption + if device:supports_capability(capabilities.powerConsumptionReport) then + create_poll_report_schedule(device) + end + device:set_field(EXPORT_POLL_TIMER_SETTING_ATTEMPTED, true) + end +end + local START_BUTTON_PRESS = "__start_button_press" local TIMEOUT_THRESHOLD = 10 --arbitrary timeout local HELD_THRESHOLD = 1 @@ -246,7 +337,7 @@ end --- device does not have endpoint ids in sequential order from 1 --- In this case the function returns the lowest endpoint value that isn't 0 --- and supports the OnOff or Switch cluster. This is done to bypass the ---- BRIDGED_NODE_DEVICE_TYPE on bridged devices +--- BRIDGED_NODE_DEVICE_TYPE on bridged devices. local function find_default_endpoint(device, component) local switch_eps = device:get_endpoints(clusters.OnOff.ID) local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH}) @@ -260,7 +351,7 @@ local function find_default_endpoint(device, component) end table.sort(all_eps) - for _,ep in ipairs(all_eps) do + for _, ep in ipairs(all_eps) do if ep ~= 0 then --0 is the matter RootNode endpoint return ep end @@ -299,6 +390,30 @@ local function assign_child_profile(device, child_ep) return profile or "switch-binary" end +local function do_configure(driver, device) + 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 + if #energy_eps > 0 and #power_eps > 0 then + profile_name = "plug-power-energy-powerConsumption" + elseif #energy_eps > 0 then + profile_name = "plug-energy-powerConsumption" + elseif #power_eps > 0 then + profile_name = "plug-power" + elseif #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 + profile_name = profile_name .. "-level" + end + end + + if profile_name then + device:try_update_metadata({ profile = profile_name }) + end +end + local function configure_buttons(device) if device.network_type ~= device_lib.NETWORK_TYPE_CHILD then local MS = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.Feature.MOMENTARY_SWITCH}) @@ -360,25 +475,25 @@ local function initialize_switch(driver, device) -- support for bindings. local num_switch_server_eps = 0 local main_endpoint = find_default_endpoint(device) - if #switch_eps > 0 then for _, ep in ipairs(switch_eps) do - -- Create child devices for non-main switch endpoints - 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 + if device:supports_server_cluster(clusters.OnOff.ID, ep) then + num_switch_server_eps = num_switch_server_eps + 1 local name = string.format("%s %d", device.label, num_switch_server_eps) - local child_profile = assign_child_profile(device, ep) - driver:try_create_device( - { - type = "EDGE_CHILD", - label = name, - profile = child_profile, - parent_device_id = device.id, - parent_assigned_child_key = string.format("%d", ep), - vendor_provided_label = name - } - ) - parent_child_device = true + if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint + local child_profile = assign_child_profile(device, ep) + driver:try_create_device( + { + type = "EDGE_CHILD", + label = name, + profile = child_profile, + parent_device_id = device.id, + parent_assigned_child_key = string.format("%d", ep), + vendor_provided_label = name + } + ) + parent_child_device = true + end end end elseif #button_eps > 0 then @@ -409,7 +524,7 @@ local function initialize_switch(driver, device) device:set_field(COMPONENT_TO_ENDPOINT_MAP_BUTTON, component_map, {persist = true}) end - if #switch_eps > 0 then + if num_switch_server_eps > 0 then -- The case where num_switch_server_eps > 0 is a workaround for devices that have a -- Light Switch device type but implement the On Off cluster as server (which is against the spec -- for this device type). By default, we do not support Light Switch device types because by spec these @@ -417,7 +532,7 @@ local function initialize_switch(driver, device) -- do not have a generic fingerprint and will join as a matter-thing. However, we have seen some devices -- claim to be Light Switch device types and still implement their clusters as server, so this is a -- workaround for those devices. - if num_switch_server_eps > 0 and detect_matter_thing(device) == true then + if detect_matter_thing(device) then local id = 0 for _, ep in ipairs(device.endpoints) do -- main_endpoint only supports server cluster by definition of get_endpoints() @@ -526,6 +641,7 @@ end local function device_removed(driver, device) log.info("device removed") + delete_export_poll_schedule(device) end local function handle_switch_on(driver, device, cmd) @@ -547,7 +663,8 @@ local function handle_switch_off(driver, device, cmd) device:send(req) end -local function handle_set_level(driver, device, cmd) + +local function handle_set_switch_level(driver, device, cmd) if type(device.register_native_capability_cmd_handler) == "function" then device:register_native_capability_cmd_handler(cmd.capability, cmd.command) end @@ -610,6 +727,31 @@ local function handle_set_color_temperature(driver, device, cmd) device:send(req) end +local function handle_valve_open(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = clusters.ValveConfigurationAndControl.server.commands.Open(device, endpoint_id) + device:send(req) +end + +local function handle_valve_close(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = clusters.ValveConfigurationAndControl.server.commands.Close(device, endpoint_id) + device:send(req) +end + +local function handle_set_level(driver, device, cmd) + local commands = clusters.ValveConfigurationAndControl.server.commands + local endpoint_id = device:component_to_endpoint(cmd.component) + local level = cmd.args.level + if not level then + return + elseif level == 0 then + device:send(commands.Close(device, endpoint_id)) + else + device:send(commands.Open(device, endpoint_id, nil, level)) + end +end + local function handle_refresh(driver, device, cmd) --Note: no endpoint specified indicates a wildcard endpoint local req = clusters.OnOff.attributes.OnOff:read(device) @@ -778,6 +920,31 @@ local function occupancy_attr_handler(driver, device, ib, response) device:emit_event(ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) end +local function cumul_energy_exported_handler(driver, device, ib, response) + device:set_field(TOTAL_EXPORTED_ENERGY, ib.data.elements.energy.value) + device:emit_event(capabilities.energyMeter.energy({ value = ib.data.elements.energy.value, unit = "Wh" })) +end + +local function per_energy_exported_handler(driver, device, ib, response) + local latest_energy_report = device:get_field(TOTAL_EXPORTED_ENERGY) or 0 + local summed_energy_report = latest_energy_report + ib.data.elements.energy.value + device:set_field(TOTAL_EXPORTED_ENERGY, summed_energy_report) + device:emit_event(capabilities.energyMeter.energy({ value = summed_energy_report, unit = "Wh" })) +end + +local function energy_report_handler_factory(is_cumulative_report) + return function(driver, device, ib, response) + if not device:get_field(EXPORT_POLL_TIMER_SETTING_ATTEMPTED) then + set_poll_report_timer_and_schedule(device, is_cumulative_report) + end + if is_cumulative_report then + cumul_energy_exported_handler(driver, device, ib, response) + elseif device:get_field(CUMULATIVE_REPORTS_NOT_SUPPORTED) then + per_energy_exported_handler(driver, device, ib, response) + end + end +end + local function initial_press_event_handler(driver, device, ib, response) if get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then -- Receipt of an InitialPress event means we do not want to ignore the next MultiPressComplete event @@ -811,6 +978,26 @@ local function short_release_event_handler(driver, device, ib, response) end end +local function active_power_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event(capabilities.powerMeter.power({ value = ib.data.value, unit = "W"})) + end +end + +local function valve_state_attr_handler(driver, device, ib, response) + if ib.data.value == 0 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.valve.valve.closed()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.valve.valve.open()) + end +end + +local function valve_level_attr_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.level.level(ib.data.value)) + end +end + local function multi_press_complete_event_handler(driver, device, ib, response) -- in the case of multiple button presses -- emit number of times, multiple presses have been completed @@ -871,6 +1058,14 @@ local function device_added(driver, device) handle_refresh(driver, device) end + -- Reset the values + if device:supports_capability(capabilities.powerMeter) then + device:emit_event(capabilities.powerMeter.power({ value = 0.0, unit = "W" })) + end + if device:supports_capability(capabilities.energyMeter) then + device:emit_event(capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) + end + -- call device init in case init is not called after added due to device caching device_init(driver, device) end @@ -880,7 +1075,8 @@ local matter_driver_template = { init = device_init, added = device_added, removed = device_removed, - infoChanged = info_changed + infoChanged = info_changed, + doConfigure = do_configure }, matter_handlers = { attr = { @@ -908,6 +1104,17 @@ local matter_driver_template = { [clusters.OccupancySensing.ID] = { [clusters.OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler, }, + [clusters.ElectricalPowerMeasurement.ID] = { + [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = active_power_handler, + }, + [clusters.ElectricalEnergyMeasurement.ID] = { + [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported.ID] = energy_report_handler_factory(true), + [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported.ID] = energy_report_handler_factory(false), + }, + [clusters.ValveConfigurationAndControl.ID] = { + [clusters.ValveConfigurationAndControl.attributes.CurrentState.ID] = valve_state_attr_handler, + [clusters.ValveConfigurationAndControl.attributes.CurrentLevel.ID] = valve_level_attr_handler + }, [clusters.PowerSource.ID] = { [clusters.PowerSource.attributes.BatPercentRemaining.ID] = battery_percent_remaining_attr_handler, }, @@ -951,9 +1158,22 @@ local matter_driver_template = { [capabilities.motionSensor.ID] = { clusters.OccupancySensing.attributes.Occupancy }, + [capabilities.valve.ID] = { + clusters.ValveConfigurationAndControl.attributes.CurrentState + }, + [capabilities.level.ID] = { + clusters.ValveConfigurationAndControl.attributes.CurrentLevel + }, [capabilities.battery.ID] = { clusters.PowerSource.attributes.BatPercentRemaining, }, + [capabilities.energyMeter.ID] = { + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported + }, + [capabilities.powerMeter.ID] = { + clusters.ElectricalPowerMeasurement.attributes.ActivePower + } }, subscribed_events = { [capabilities.button.ID] = { @@ -969,7 +1189,7 @@ local matter_driver_template = { [capabilities.switch.commands.off.NAME] = handle_switch_off, }, [capabilities.switchLevel.ID] = { - [capabilities.switchLevel.commands.setLevel.NAME] = handle_set_level + [capabilities.switchLevel.commands.setLevel.NAME] = handle_set_switch_level }, [capabilities.refresh.ID] = { [capabilities.refresh.commands.refresh.NAME] = handle_refresh, @@ -982,14 +1202,26 @@ local matter_driver_template = { [capabilities.colorTemperature.ID] = { [capabilities.colorTemperature.commands.setColorTemperature.NAME] = handle_set_color_temperature, }, + [capabilities.valve.ID] = { + [capabilities.valve.commands.open.NAME] = handle_valve_open, + [capabilities.valve.commands.close.NAME] = handle_valve_close + }, + [capabilities.level.ID] = { + [capabilities.level.commands.setLevel.NAME] = handle_set_level + } }, supported_capabilities = { capabilities.switch, capabilities.switchLevel, capabilities.colorControl, capabilities.colorTemperature, + capabilities.level, capabilities.motionSensor, capabilities.illuminanceMeasurement, + capabilities.powerMeter, + capabilities.energyMeter, + capabilities.powerConsumptionReport, + capabilities.valve, capabilities.button, capabilities.battery }, diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua new file mode 100644 index 0000000000..23125b002a --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -0,0 +1,711 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" + +local clusters = require "st.matter.clusters" + +clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" +clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("plug-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, }, + }, + 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, }, + }, + device_types = { + { device_type_id = 0x010A, device_type_revision = 1 } -- OnOff Plug + } + }, + }, +}) + + +local mock_device_periodic = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("plug-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 = 10, }, + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 } -- Electrical Sensor + } + }, + }, +}) + +local subscribed_attributes_periodic = { + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported, +} +local subscribed_attributes = { + clusters.OnOff.attributes.OnOff, + clusters.ElectricalPowerMeasurement.attributes.ActivePower, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported, +} + +local cumulative_report_val_19 = { + energy = 19, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, +} + +local cumulative_report_val_29 = { + energy = 29, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, +} + +local cumulative_report_val_39 = { + energy = 39, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, +} + +local periodic_report_val_23 = { + energy = 23, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, +} + +local function test_init() + 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.matter:__expect_send({ mock_device.id, subscribe_request }) + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +local function test_init_periodic() + local subscribe_request = subscribed_attributes_periodic[1]:subscribe(mock_device_periodic) + for i, cluster in ipairs(subscribed_attributes_periodic) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_periodic)) + end + end + test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) + test.mock_device.add_test_device(mock_device_periodic) +end + +test.register_coroutine_test( + "Check the power and energy meter when the device is added", function() + test.socket.matter:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + 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.matter:__expect_send({ mock_device.id, subscribe_request }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 0.0, unit = "W" })) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) + ) + + test.wait_for_events() + end +) + +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, 17) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerMeter.power({value = 17, unit="W"})) + }, + } +) + +test.register_message_test( + "Cumulative Energy measurement should generate correct messages", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyExported:build_test_report_data(mock_device, 1, cumulative_report_val_19) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 19, unit="Wh"})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyExported:build_test_report_data(mock_device, 1, cumulative_report_val_19) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 19, unit="Wh"})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyExported:build_test_report_data(mock_device, 1, cumulative_report_val_29) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1969-12-31T23:59:59Z", + deltaEnergy = 0.0, + energy = 19 + })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 29, unit="Wh"})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyExported:build_test_report_data(mock_device, 1, cumulative_report_val_39) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 39, unit="Wh"})) + }, + } +) + +test.register_message_test( + "Periodic Energy as subordinate to Cumulative Energy measurement should not generate any messages", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyExported:build_test_report_data(mock_device, 1, periodic_report_val_23) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyExported:build_test_report_data(mock_device, 1, periodic_report_val_23) + } + }, + } +) + +test.register_message_test( + "Periodic Energy measurement should generate correct messages", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyExported:build_test_report_data(mock_device_periodic, 1, periodic_report_val_23) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 23, unit="Wh"})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyExported:build_test_report_data(mock_device_periodic, 1, periodic_report_val_23) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 46, unit="Wh"})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyExported:build_test_report_data(mock_device_periodic, 1, periodic_report_val_23) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1969-12-31T23:59:59Z", + deltaEnergy = 0.0, + energy = 46 + })) + }, + { + channel = "capability", + direction = "send", + message = mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 69, unit="Wh"})) + }, + }, + { test_init = test_init_periodic } +) + +local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds + +test.register_coroutine_test( + "Generated poll timer (<15 minutes) gets correctly set", function() + + test.socket["matter"]:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported: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, unit = "Wh" })) + ) + test.socket["matter"]:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported: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, unit = "Wh" })) + ) + test.wait_for_events() + test.mock_time.advance_time(899) + test.socket["matter"]:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:build_test_report_data( + mock_device, 1, cumulative_report_val_29 + ) + } + ) + test.socket["capability"]:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:14:58Z", + deltaEnergy = 0.0, + energy = 19 + })) + ) + test.socket["capability"]:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29, unit = "Wh" })) + ) + test.wait_for_events() + local report_export_poll_timer = mock_device:get_field("__recurring_export_report_poll_timer") + local export_timer_length = mock_device:get_field("__export_report_timeout") + assert(report_export_poll_timer ~= nil, "report_export_poll_timer should exist") + assert(export_timer_length ~= nil, "export_timer_length should exist") + assert(export_timer_length == MINIMUM_ST_ENERGY_REPORT_INTERVAL, "export_timer should min_interval") + end +) + +test.register_coroutine_test( + "Generated poll timer (>15 minutes) gets correctly set", function() + + test.socket["matter"]:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported: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, unit = "Wh" })) + ) + test.socket["matter"]:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported: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, unit = "Wh" })) + ) + test.wait_for_events() + test.mock_time.advance_time(2000) + test.socket["matter"]:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:build_test_report_data( + mock_device, 1, cumulative_report_val_29 + ) + } + ) + test.socket["capability"]:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:33:19Z", + deltaEnergy = 0.0, + energy = 19 + })) + ) + test.socket["capability"]:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29, unit = "Wh" })) + ) + test.wait_for_events() + local report_export_poll_timer = mock_device:get_field("__recurring_export_report_poll_timer") + local export_timer_length = mock_device:get_field("__export_report_timeout") + assert(report_export_poll_timer ~= nil, "report_export_poll_timer should exist") + assert(export_timer_length ~= nil, "export_timer_length should exist") + assert(export_timer_length == 2000, "export_timer should min_interval") + end +) + +test.register_coroutine_test( + "Check when the device is removed", function() + + test.socket["matter"]:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported: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, unit = "Wh" })) + ) + test.socket["matter"]:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported: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, unit = "Wh" })) + ) + test.wait_for_events() + test.mock_time.advance_time(2000) + test.socket["matter"]:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:build_test_report_data( + mock_device, 1, cumulative_report_val_29 + ) + } + ) + test.socket["capability"]:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:33:19Z", + deltaEnergy = 0.0, + energy = 19 + })) + ) + test.socket["capability"]:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29, unit = "Wh" })) + ) + test.wait_for_events() + local report_export_poll_timer = mock_device:get_field("__recurring_export_report_poll_timer") + local export_timer_length = mock_device:get_field("__export_report_timeout") + assert(report_export_poll_timer ~= nil, "report_export_poll_timer should exist") + assert(export_timer_length ~= nil, "export_timer_length should exist") + assert(export_timer_length == 2000, "export_timer should min_interval") + + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "removed" }) + test.wait_for_events() + report_export_poll_timer = mock_device:get_field("__recurring_export_report_poll_timer") + export_timer_length = mock_device:get_field("__export_report_timeout") + assert(report_export_poll_timer == nil, "report_export_poll_timer should exist") + assert(export_timer_length == nil, "export_timer_length should exist") + end +) + +test.register_coroutine_test( + "Generated periodic export energy device poll timer (<15 minutes) gets correctly set", function() + test.socket["matter"]:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported:build_test_report_data( + mock_device_periodic, 1, periodic_report_val_23 + ) + } + ) + test.socket["capability"]:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 23, unit = "Wh" })) + ) + test.socket["matter"]:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported:build_test_report_data( + mock_device_periodic, 1, periodic_report_val_23 + ) + } + ) + test.socket["capability"]:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 46, unit = "Wh" })) + ) + test.wait_for_events() + test.mock_time.advance_time(899) + test.socket["matter"]:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported:build_test_report_data( + mock_device_periodic, 1, periodic_report_val_23 + ) + } + ) + test.socket["capability"]:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + deltaEnergy=0.0, + ["end"]="1970-01-01T00:14:58Z", + energy=46, + start="1970-01-01T00:00:00Z" + })) + ) + test.socket["capability"]:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 69, unit = "Wh" })) + ) + test.wait_for_events() + local report_export_poll_timer = mock_device_periodic:get_field("__recurring_export_report_poll_timer") + local export_timer_length = mock_device_periodic:get_field("__export_report_timeout") + assert(report_export_poll_timer ~= nil, "report_export_poll_timer should exist") + assert(export_timer_length ~= nil, "export_timer_length should exist") + assert(export_timer_length == MINIMUM_ST_ENERGY_REPORT_INTERVAL, "export_timer should min_interval") + end, + { test_init = test_init_periodic } +) + + +test.register_coroutine_test( + "Generated periodic export energy device poll timer (>15 minutes) gets correctly set", function() + test.socket["matter"]:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported:build_test_report_data( + mock_device_periodic, 1, periodic_report_val_23 + ) + } + ) + test.socket["capability"]:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 23, unit = "Wh" })) + ) + test.socket["matter"]:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported:build_test_report_data( + mock_device_periodic, 1, periodic_report_val_23 + ) + } + ) + test.socket["capability"]:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 46, unit = "Wh" })) + ) + test.wait_for_events() + test.mock_time.advance_time(2000) + test.socket["matter"]:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported:build_test_report_data( + mock_device_periodic, 1, periodic_report_val_23 + ) + } + ) + test.socket["capability"]:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + deltaEnergy=0.0, + ["end"] = "1970-01-01T00:33:19Z", + energy=46, + start="1970-01-01T00:00:00Z" + })) + ) + test.socket["capability"]:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 69, unit = "Wh" })) + ) + test.wait_for_events() + local report_export_poll_timer = mock_device_periodic:get_field("__recurring_export_report_poll_timer") + local export_timer_length = mock_device_periodic:get_field("__export_report_timeout") + assert(report_export_poll_timer ~= nil, "report_export_poll_timer should exist") + assert(export_timer_length ~= nil, "export_timer_length should exist") + assert(export_timer_length == 2000, "export_timer should min_interval") + end, + { test_init = test_init_periodic } +) + +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-power-energy-powerConsumption" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { test_init = test_init } +) + +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" }) + end, + { test_init = test_init_periodic } +) + +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 638931d03c..4329cbce65 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 @@ -119,6 +119,34 @@ local mock_device_color_dimmer = test.mock_device.build_test_matter_device({ } }) +local mock_device_water_valve = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("matter-thing.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.ValveConfigurationAndControl.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 2}, + }, + device_types = { + {device_type_id = 0x0042, device_type_revision = 1} -- Water Valve + } + } + } +}) + local mock_device_parent_client_child_server = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("matter-thing.yml"), manufacturer_info = { @@ -315,6 +343,11 @@ local function test_init_color_dimmer() mock_device_color_dimmer:expect_metadata_update({ profile = "switch-color-level" }) end +local function test_init_water_valve() + test.mock_device.add_test_device(mock_device_water_valve) + mock_device_water_valve:expect_metadata_update({ profile = "water-valve-level" }) +end + local function test_init_parent_child_different_types() local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, @@ -389,6 +422,13 @@ test.register_coroutine_test( { test_init = test_init_onoff_client } ) +test.register_coroutine_test( + "Test profile change on init for water valve parent cluster as server", + function() + end, + { test_init = test_init_water_valve } +) + test.register_coroutine_test( "Test profile change on init for onoff parent cluster as client and onoff child as server", function() 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 new file mode 100644 index 0000000000..d4ead24458 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua @@ -0,0 +1,235 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" + +local clusters = require "st.matter.clusters" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("water-valve-level.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = clusters.ValveConfigurationAndControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 2 + }, + }, + device_types = { + {device_type_id = 0x0042, device_type_revision = 1} -- Water Valve + } + } + } +}) + +local cluster_subscribe_list = { + clusters.ValveConfigurationAndControl.attributes.CurrentState, + clusters.ValveConfigurationAndControl.attributes.CurrentLevel +} + +local function test_init() + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +test.register_message_test( + "Open command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "valve", component = "main", command = "open", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ValveConfigurationAndControl.server.commands.Open(mock_device, 1) + } + } + } +) + +test.register_message_test( + "Close command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "valve", component = "main", command = "close", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ValveConfigurationAndControl.server.commands.Close(mock_device, 1) + } + } + } +) + +test.register_message_test( + "Set level command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "level", component = "main", command = "setLevel", args = { 25 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ValveConfigurationAndControl.server.commands.Open(mock_device, 1, nil, 25) + } + } + } +) + +test.register_message_test( + "Set level command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "level", component = "main", command = "setLevel", args = { 0 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ValveConfigurationAndControl.server.commands.Close(mock_device, 1) + } + } + } +) + +test.register_message_test( + "Current state reports should generate appropriate events", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ValveConfigurationAndControl.server.attributes.CurrentState:build_test_report_data(mock_device, 1, 0) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.valve.valve.closed()) + }, + } +) + +test.register_message_test( + "Current state reports should generate appropriate events", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ValveConfigurationAndControl.server.attributes.CurrentState:build_test_report_data(mock_device, 1, 1) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.valve.valve.open()) + }, + } +) + +test.register_message_test( + "Current state reports should generate appropriate events", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ValveConfigurationAndControl.server.attributes.CurrentState:build_test_report_data(mock_device, 1, 2) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.valve.valve.open()) + }, + } +) + +test.register_message_test( + "Current level reports should generate appropriate events", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ValveConfigurationAndControl.server.attributes.CurrentLevel:build_test_report_data(mock_device, 1, 50) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.level.level(50)) + }, + } +) + +test.run_registered_tests() diff --git a/tools/config.luacov b/tools/config.luacov index c72c977219..137c9c91a1 100644 --- a/tools/config.luacov +++ b/tools/config.luacov @@ -32,6 +32,16 @@ configuration = { "TotalVolatileOrganicCompoundsConcentrationMeasurement", "ActivatedCarbonFilterMonitoring", "HepaFilterMonitoring", + "MicrowaveOvenControl", + "MicrowaveOvenMode", + "OvenMode", + "DeviceEnergyManagementMode", + "ElectricalEnergyMeasurement", + "ElectricalPowerMeasurement", + "EnergyEvse", + "EnergyEvseMode", + "BooleanStateConfiguration", + "ValveConfigurationAndControl", }, cobertura = { filenameparser = function(filename)