diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index 531dc8c36f..34406cf01c 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -261,3 +261,8 @@ matterGeneric: deviceTypes: - id: 0x0202 # Window Covering deviceProfileName: window-covering + - id: "closure" + deviceLabel: Matter Closure + deviceTypes: + - id: 0x0230 # Closure + deviceProfileName: covering diff --git a/drivers/SmartThings/matter-window-covering/profiles/covering.yml b/drivers/SmartThings/matter-window-covering/profiles/covering.yml new file mode 100644 index 0000000000..11e2319a5a --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/profiles/covering.yml @@ -0,0 +1,56 @@ +name: covering +components: +- id: main + capabilities: + - id: windowShade + version: 1 + - id: windowShadeLevel + version: 1 + optional: true + - id: battery + version: 1 + optional: true + - id: batteryLevel + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Blind +- id: windowShade1 + optional: true + capabilities: + - id: windowShadeLevel + version: 1 + optional: true + categories: + - name: Blind +- id: windowShade2 + optional: true + capabilities: + - id: windowShadeLevel + version: 1 + optional: true + categories: + - name: Blind +- id: windowShade3 + optional: true + capabilities: + - id: windowShadeLevel + version: 1 + optional: true + categories: + - name: Blind +- id: windowShade4 + optional: true + capabilities: + - id: windowShadeLevel + version: 1 + optional: true + categories: + - name: Blind +preferences: + - preferenceId: reverse + explicit: true diff --git a/drivers/SmartThings/matter-window-covering/profiles/door.yml b/drivers/SmartThings/matter-window-covering/profiles/door.yml new file mode 100644 index 0000000000..bd19c2dc8f --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/profiles/door.yml @@ -0,0 +1,56 @@ +name: door +components: +- id: main + capabilities: + - id: doorControl + version: 1 + - id: level + version: 1 + optional: true + - id: battery + version: 1 + optional: true + - id: batteryLevel + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Door +- id: door1 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: Door +- id: door2 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: Door +- id: door3 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: Door +- id: door4 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: Door +preferences: + - preferenceId: reverse + explicit: true diff --git a/drivers/SmartThings/matter-window-covering/profiles/garage-door.yml b/drivers/SmartThings/matter-window-covering/profiles/garage-door.yml new file mode 100644 index 0000000000..a75ae63580 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/profiles/garage-door.yml @@ -0,0 +1,56 @@ +name: garage-door +components: +- id: main + capabilities: + - id: doorControl + version: 1 + - id: level + version: 1 + optional: true + - id: battery + version: 1 + optional: true + - id: batteryLevel + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: GarageDoor +- id: door1 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: GarageDoor +- id: door2 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: GarageDoor +- id: door3 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: GarageDoor +- id: door4 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: GarageDoor +preferences: + - preferenceId: reverse + explicit: true diff --git a/drivers/SmartThings/matter-window-covering/profiles/gate.yml b/drivers/SmartThings/matter-window-covering/profiles/gate.yml new file mode 100644 index 0000000000..8cc49b425f --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/profiles/gate.yml @@ -0,0 +1,56 @@ +name: gate +components: +- id: main + capabilities: + - id: doorControl + version: 1 + - id: level + version: 1 + optional: true + - id: battery + version: 1 + optional: true + - id: batteryLevel + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Door +- id: door1 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: Door +- id: door2 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: Door +- id: door3 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: Door +- id: door4 + optional: true + capabilities: + - id: level + version: 1 + optional: true + categories: + - name: Door +preferences: + - preferenceId: reverse + explicit: true diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/init.lua new file mode 100644 index 0000000000..76adc03375 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/init.lua @@ -0,0 +1,97 @@ +local cluster_base = require "st.matter.cluster_base" +local ClosureControlServerAttributes = require "embedded_clusters.ClosureControl.server.attributes" +local ClosureControlServerCommands = require "embedded_clusters.ClosureControl.server.commands" +local ClosureControlTypes = require "embedded_clusters.ClosureControl.types" + +local ClosureControl = {} + +ClosureControl.ID = 0x0104 +ClosureControl.NAME = "ClosureControl" +ClosureControl.server = {} +ClosureControl.client = {} +ClosureControl.server.attributes = ClosureControlServerAttributes:set_parent_cluster(ClosureControl) +ClosureControl.server.commands = ClosureControlServerCommands:set_parent_cluster(ClosureControl) +ClosureControl.types = ClosureControlTypes + +function ClosureControl:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "CountdownTime", + [0x0001] = "MainState", + [0x0002] = "CurrentErrorList", + [0x0003] = "OverallCurrentState", + [0x0004] = "OverallTargetState", + [0x0005] = "LatchControlModes", + [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 ClosureControl:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "Stop", + [0x0001] = "MoveTo", + [0x0002] = "Calibrate", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + + +ClosureControl.attribute_direction_map = { + ["CountdownTime"] = "server", + ["MainState"] = "server", + ["CurrentErrorList"] = "server", + ["OverallCurrentState"] = "server", + ["OverallTargetState"] = "server", + ["LatchControlModes"] = "server", + ["AcceptedCommandList"] = "server", + ["AttributeList"] = "server", +} + +ClosureControl.command_direction_map = { + ["Stop"] = "server", + ["MoveTo"] = "server", + ["Calibrate"] = "server", +} + +ClosureControl.FeatureMap = ClosureControl.types.Feature + +function ClosureControl.are_features_supported(feature, feature_map) + if (ClosureControl.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 = ClosureControl.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ClosureControl.NAME)) + end + return ClosureControl[direction].attributes[key] +end +ClosureControl.attributes = {} +setmetatable(ClosureControl.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = ClosureControl.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, ClosureControl.NAME)) + end + return ClosureControl[direction].commands[key] +end +ClosureControl.commands = {} +setmetatable(ClosureControl.commands, command_helper_mt) + +setmetatable(ClosureControl, {__index = cluster_base}) + +return ClosureControl diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/AcceptedCommandList.lua new file mode 100644 index 0000000000..f86ac60dbe --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/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 + ) +end + +function AcceptedCommandList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AcceptedCommandList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AcceptedCommandList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AcceptedCommandList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value, __index = AcceptedCommandList.base_type}) +return AcceptedCommandList diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/AttributeList.lua new file mode 100644 index 0000000000..7f6827b026 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/AttributeList.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 AttributeList = { + ID = 0xFFFB, + NAME = "AttributeList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +function AttributeList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, AttributeList.element_type) + end +end + +function AttributeList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function AttributeList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AttributeList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AttributeList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AttributeList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AttributeList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) +return AttributeList diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/CountdownTime.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/CountdownTime.lua new file mode 100644 index 0000000000..b6e4f861b6 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/CountdownTime.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 CountdownTime = { + ID = 0x0000, + NAME = "CountdownTime", + base_type = require "st.matter.data_types.Uint32", +} + +function CountdownTime:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function CountdownTime:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CountdownTime:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CountdownTime:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CountdownTime: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 CountdownTime:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(CountdownTime, {__call = CountdownTime.new_value, __index = CountdownTime.base_type}) +return CountdownTime diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/CurrentErrorList.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/CurrentErrorList.lua new file mode 100644 index 0000000000..21076e7e5d --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/CurrentErrorList.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 CurrentErrorList = { + ID = 0x0002, + NAME = "CurrentErrorList", + base_type = require "st.matter.data_types.Array", + element_type = require "embedded_clusters.ClosureControl.types.ClosureErrorEnum", +} + +function CurrentErrorList: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, CurrentErrorList.element_type) + end +end + +function CurrentErrorList:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function CurrentErrorList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CurrentErrorList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function CurrentErrorList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function CurrentErrorList: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 CurrentErrorList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(CurrentErrorList, {__call = CurrentErrorList.new_value, __index = CurrentErrorList.base_type}) +return CurrentErrorList diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/LatchControlModes.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/LatchControlModes.lua new file mode 100644 index 0000000000..d62831b4f3 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/LatchControlModes.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 LatchControlModes = { + ID = 0x0005, + NAME = "LatchControlModes", + base_type = require "embedded_clusters.ClosureControl.types.LatchControlModesBitmap", +} + +function LatchControlModes:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function LatchControlModes:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function LatchControlModes:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function LatchControlModes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LatchControlModes: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 LatchControlModes:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(LatchControlModes, {__call = LatchControlModes.new_value, __index = LatchControlModes.base_type}) +return LatchControlModes diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/MainState.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/MainState.lua new file mode 100644 index 0000000000..5f1bbc06e2 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/MainState.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 MainState = { + ID = 0x0001, + NAME = "MainState", + base_type = require "embedded_clusters.ClosureControl.types.MainStateEnum", +} + +function MainState:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function MainState:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function MainState:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function MainState:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MainState: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 MainState:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MainState, {__call = MainState.new_value, __index = MainState.base_type}) +return MainState diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/OverallCurrentState.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/OverallCurrentState.lua new file mode 100644 index 0000000000..f93477ec47 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/OverallCurrentState.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 OverallCurrentState = { + ID = 0x0003, + NAME = "OverallCurrentState", + base_type = require "embedded_clusters.ClosureControl.types.OverallCurrentStateStruct", +} + +function OverallCurrentState:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function OverallCurrentState:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function OverallCurrentState:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function OverallCurrentState:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function OverallCurrentState: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 OverallCurrentState:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(OverallCurrentState, {__call = OverallCurrentState.new_value, __index = OverallCurrentState.base_type}) +return OverallCurrentState diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/OverallTargetState.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/OverallTargetState.lua new file mode 100644 index 0000000000..6c8cbb9ee3 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/OverallTargetState.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 OverallTargetState = { + ID = 0x0004, + NAME = "OverallTargetState", + base_type = require "embedded_clusters.ClosureControl.types.OverallTargetStateStruct", +} + +function OverallTargetState:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function OverallTargetState:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function OverallTargetState:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function OverallTargetState:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function OverallTargetState: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 OverallTargetState:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(OverallTargetState, {__call = OverallTargetState.new_value, __index = OverallTargetState.base_type}) +return OverallTargetState diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/init.lua new file mode 100644 index 0000000000..0c71152760 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/attributes/init.lua @@ -0,0 +1,19 @@ +local attr_mt = {} +attr_mt.__index = function(self, key) + local req_loc = string.format("embedded_clusters.ClosureControl.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + return raw_def +end + +local ClosureControlServerAttributes = {} + +function ClosureControlServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ClosureControlServerAttributes, attr_mt) + +return ClosureControlServerAttributes diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/Calibrate.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/Calibrate.lua new file mode 100644 index 0000000000..1a2931c0ad --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/Calibrate.lua @@ -0,0 +1,91 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local Calibrate = {} + +Calibrate.NAME = "Calibrate" +Calibrate.ID = 0x0002 +Calibrate.field_defs = { +} + +function Calibrate: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 Calibrate: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 = Calibrate, + __tostring = Calibrate.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + true + ) +end + +function Calibrate:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function Calibrate: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 Calibrate:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(Calibrate, {__call = Calibrate.init}) + +return Calibrate diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/MoveTo.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/MoveTo.lua new file mode 100644 index 0000000000..80c1bca52d --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/MoveTo.lua @@ -0,0 +1,112 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local MoveTo = {} + +MoveTo.NAME = "MoveTo" +MoveTo.ID = 0x0001 +MoveTo.field_defs = { + { + name = "position", + field_id = 0, + is_nullable = false, + is_optional = true, + data_type = require "embedded_clusters.ClosureControl.types.TargetPositionEnum", + }, + { + name = "latch", + field_id = 1, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Boolean", + }, + { + name = "speed", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + }, +} + +function MoveTo: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 MoveTo:init(device, endpoint_id, position, latch, speed) + local out = {} + local args = {position, latch, speed} + 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 = MoveTo, + __tostring = MoveTo.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + true + ) +end + +function MoveTo:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MoveTo: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 MoveTo:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MoveTo, {__call = MoveTo.init}) + +return MoveTo diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/Stop.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/Stop.lua new file mode 100644 index 0000000000..9ac41ba122 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/Stop.lua @@ -0,0 +1,90 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local Stop = {} + +Stop.NAME = "Stop" +Stop.ID = 0x0000 +Stop.field_defs = { +} + +function Stop: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 Stop: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 = Stop, + __tostring = Stop.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function Stop:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function Stop: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 Stop:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(Stop, {__call = Stop.init}) + +return Stop diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/init.lua new file mode 100644 index 0000000000..1125ef4296 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/init.lua @@ -0,0 +1,19 @@ +local command_mt = {} +command_mt.__index = function(self, key) + local req_loc = string.format("embedded_clusters.ClosureControl.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + return raw_def +end + +local ClosureControlServerCommands = {} + +function ClosureControlServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ClosureControlServerCommands, command_mt) + +return ClosureControlServerCommands diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/ClosureErrorEnum.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/ClosureErrorEnum.lua new file mode 100644 index 0000000000..5e5a09da56 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/ClosureErrorEnum.lua @@ -0,0 +1,36 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local ClosureErrorEnum = {} +local new_mt = UintABC.new_mt({NAME = "ClosureErrorEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.PHYSICALLY_BLOCKED] = "PHYSICALLY_BLOCKED", + [self.BLOCKED_BY_SENSOR] = "BLOCKED_BY_SENSOR", + [self.TEMPERATURE_LIMITED] = "TEMPERATURE_LIMITED", + [self.MAINTENANCE_REQUIRED] = "MAINTENANCE_REQUIRED", + [self.INTERNAL_INTERFERENCE] = "INTERNAL_INTERFERENCE", + } + 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.PHYSICALLY_BLOCKED = 0x00 +new_mt.__index.BLOCKED_BY_SENSOR = 0x01 +new_mt.__index.TEMPERATURE_LIMITED = 0x02 +new_mt.__index.MAINTENANCE_REQUIRED = 0x03 +new_mt.__index.INTERNAL_INTERFERENCE = 0x04 + +ClosureErrorEnum.PHYSICALLY_BLOCKED = 0x00 +ClosureErrorEnum.BLOCKED_BY_SENSOR = 0x01 +ClosureErrorEnum.TEMPERATURE_LIMITED = 0x02 +ClosureErrorEnum.MAINTENANCE_REQUIRED = 0x03 +ClosureErrorEnum.INTERNAL_INTERFERENCE = 0x04 + +ClosureErrorEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(ClosureErrorEnum, new_mt) + +return ClosureErrorEnum diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/CurrentPositionEnum.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/CurrentPositionEnum.lua new file mode 100644 index 0000000000..a5fee86d89 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/CurrentPositionEnum.lua @@ -0,0 +1,39 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local CurrentPositionEnum = {} +local new_mt = UintABC.new_mt({NAME = "CurrentPositionEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.FULLY_CLOSED] = "FULLY_CLOSED", + [self.FULLY_OPENED] = "FULLY_OPENED", + [self.PARTIALLY_OPENED] = "PARTIALLY_OPENED", + [self.OPENED_FOR_PEDESTRIAN] = "OPENED_FOR_PEDESTRIAN", + [self.OPENED_FOR_VENTILATION] = "OPENED_FOR_VENTILATION", + [self.OPENED_AT_SIGNATURE] = "OPENED_AT_SIGNATURE", + } + 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.FULLY_CLOSED = 0x00 +new_mt.__index.FULLY_OPENED = 0x01 +new_mt.__index.PARTIALLY_OPENED = 0x02 +new_mt.__index.OPENED_FOR_PEDESTRIAN = 0x03 +new_mt.__index.OPENED_FOR_VENTILATION = 0x04 +new_mt.__index.OPENED_AT_SIGNATURE = 0x05 + +CurrentPositionEnum.FULLY_CLOSED = 0x00 +CurrentPositionEnum.FULLY_OPENED = 0x01 +CurrentPositionEnum.PARTIALLY_OPENED = 0x02 +CurrentPositionEnum.OPENED_FOR_PEDESTRIAN = 0x03 +CurrentPositionEnum.OPENED_FOR_VENTILATION = 0x04 +CurrentPositionEnum.OPENED_AT_SIGNATURE = 0x05 + +CurrentPositionEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(CurrentPositionEnum, new_mt) + +return CurrentPositionEnum diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/Feature.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/Feature.lua new file mode 100644 index 0000000000..e41485dbe7 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/Feature.lua @@ -0,0 +1,228 @@ +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.POSITIONING = 0x0001 +Feature.MOTION_LATCHING = 0x0002 +Feature.INSTANTANEOUS = 0x0004 +Feature.SPEED = 0x0008 +Feature.VENTILATION = 0x0010 +Feature.PEDESTRIAN = 0x0020 +Feature.CALIBRATION = 0x0040 +Feature.PROTECTION = 0x0080 +Feature.MANUALLY_OPERABLE = 0x0100 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + POSITIONING = 0x0001, + MOTION_LATCHING = 0x0002, + INSTANTANEOUS = 0x0004, + SPEED = 0x0008, + VENTILATION = 0x0010, + PEDESTRIAN = 0x0020, + CALIBRATION = 0x0040, + PROTECTION = 0x0080, + MANUALLY_OPERABLE = 0x0100, +} + +Feature.is_positioning_set = function(self) + return (self.value & self.POSITIONING) ~= 0 +end + +Feature.set_positioning = function(self) + if self.value ~= nil then + self.value = self.value | self.POSITIONING + else + self.value = self.POSITIONING + end +end + +Feature.unset_positioning = function(self) + self.value = self.value & (~self.POSITIONING & self.BASE_MASK) +end +Feature.is_motion_latching_set = function(self) + return (self.value & self.MOTION_LATCHING) ~= 0 +end + +Feature.set_motion_latching = function(self) + if self.value ~= nil then + self.value = self.value | self.MOTION_LATCHING + else + self.value = self.MOTION_LATCHING + end +end + +Feature.unset_motion_latching = function(self) + self.value = self.value & (~self.MOTION_LATCHING & self.BASE_MASK) +end + +Feature.is_instantaneous_set = function(self) + return (self.value & self.INSTANTANEOUS) ~= 0 +end + +Feature.set_instantaneous = function(self) + if self.value ~= nil then + self.value = self.value | self.INSTANTANEOUS + else + self.value = self.INSTANTANEOUS + end +end + +Feature.unset_instantaneous = function(self) + self.value = self.value & (~self.INSTANTANEOUS & self.BASE_MASK) +end + +Feature.is_speed_set = function(self) + return (self.value & self.SPEED) ~= 0 +end + +Feature.set_speed = function(self) + if self.value ~= nil then + self.value = self.value | self.SPEED + else + self.value = self.SPEED + end +end + +Feature.unset_speed = function(self) + self.value = self.value & (~self.SPEED & self.BASE_MASK) +end + +Feature.is_ventilation_set = function(self) + return (self.value & self.VENTILATION) ~= 0 +end + +Feature.set_ventilation = function(self) + if self.value ~= nil then + self.value = self.value | self.VENTILATION + else + self.value = self.VENTILATION + end +end + +Feature.unset_ventilation = function(self) + self.value = self.value & (~self.VENTILATION & self.BASE_MASK) +end + +Feature.is_pedestrian_set = function(self) + return (self.value & self.PEDESTRIAN) ~= 0 +end + +Feature.set_pedestrian = function(self) + if self.value ~= nil then + self.value = self.value | self.PEDESTRIAN + else + self.value = self.PEDESTRIAN + end +end + +Feature.unset_pedestrian = function(self) + self.value = self.value & (~self.PEDESTRIAN & self.BASE_MASK) +end + +Feature.is_calibration_set = function(self) + return (self.value & self.CALIBRATION) ~= 0 +end + +Feature.set_calibration = function(self) + if self.value ~= nil then + self.value = self.value | self.CALIBRATION + else + self.value = self.CALIBRATION + end +end + +Feature.unset_calibration = function(self) + self.value = self.value & (~self.CALIBRATION & self.BASE_MASK) +end + +Feature.is_protection_set = function(self) + return (self.value & self.PROTECTION) ~= 0 +end + +Feature.set_protection = function(self) + if self.value ~= nil then + self.value = self.value | self.PROTECTION + else + self.value = self.PROTECTION + end +end + +Feature.unset_protection = function(self) + self.value = self.value & (~self.PROTECTION & self.BASE_MASK) +end + +Feature.is_manually_operable_set = function(self) + return (self.value & self.MANUALLY_OPERABLE) ~= 0 +end + +Feature.set_manually_operable = function(self) + if self.value ~= nil then + self.value = self.value | self.MANUALLY_OPERABLE + else + self.value = self.MANUALLY_OPERABLE + end +end + +Feature.unset_manually_operable = function(self) + self.value = self.value & (~self.MANUALLY_OPERABLE & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.POSITIONING | + Feature.MOTION_LATCHING | + Feature.INSTANTANEOUS | + Feature.SPEED | + Feature.VENTILATION | + Feature.PEDESTRIAN | + Feature.CALIBRATION | + Feature.PROTECTION | + Feature.MANUALLY_OPERABLE + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_positioning_set = Feature.is_positioning_set, + set_positioning = Feature.set_positioning, + unset_positioning = Feature.unset_positioning, + is_motion_latching_set = Feature.is_motion_latching_set, + set_motion_latching = Feature.set_motion_latching, + unset_motion_latching = Feature.unset_motion_latching, + is_instantaneous_set = Feature.is_instantaneous_set, + set_instantaneous = Feature.set_instantaneous, + unset_instantaneous = Feature.unset_instantaneous, + is_speed_set = Feature.is_speed_set, + set_speed = Feature.set_speed, + unset_speed = Feature.unset_speed, + is_ventilation_set = Feature.is_ventilation_set, + set_ventilation = Feature.set_ventilation, + unset_ventilation = Feature.unset_ventilation, + is_pedestrian_set = Feature.is_pedestrian_set, + set_pedestrian = Feature.set_pedestrian, + unset_pedestrian = Feature.unset_pedestrian, + is_calibration_set = Feature.is_calibration_set, + set_calibration = Feature.set_calibration, + unset_calibration = Feature.unset_calibration, + is_protection_set = Feature.is_protection_set, + set_protection = Feature.set_protection, + unset_protection = Feature.unset_protection, + is_manually_operable_set = Feature.is_manually_operable_set, + set_manually_operable = Feature.set_manually_operable, + unset_manually_operable = Feature.unset_manually_operable, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/LatchControlModesBitmap.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/LatchControlModesBitmap.lua new file mode 100644 index 0000000000..ba6188e4a3 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/LatchControlModesBitmap.lua @@ -0,0 +1,64 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local LatchControlModesBitmap = {} +local new_mt = UintABC.new_mt({NAME = "LatchControlModesBitmap", ID = data_types.name_to_id_map["Uint8"]}, 1) + +LatchControlModesBitmap.BASE_MASK = 0xFFFF +LatchControlModesBitmap.REMOTE_LATCHING = 0x0001 +LatchControlModesBitmap.REMOTE_UNLATCHING = 0x0002 + +LatchControlModesBitmap.mask_fields = { + BASE_MASK = 0xFFFF, + REMOTE_LATCHING = 0x0001, + REMOTE_UNLATCHING = 0x0002, +} + +LatchControlModesBitmap.is_remote_latching_set = function(self) + return (self.value & self.REMOTE_LATCHING) ~= 0 +end + +LatchControlModesBitmap.set_remote_latching = function(self) + if self.value ~= nil then + self.value = self.value | self.REMOTE_LATCHING + else + self.value = self.REMOTE_LATCHING + end +end + +LatchControlModesBitmap.unset_remote_latching = function(self) + self.value = self.value & (~self.REMOTE_LATCHING & self.BASE_MASK) +end + +LatchControlModesBitmap.is_remote_unlatching_set = function(self) + return (self.value & self.REMOTE_UNLATCHING) ~= 0 +end + +LatchControlModesBitmap.set_remote_unlatching = function(self) + if self.value ~= nil then + self.value = self.value | self.REMOTE_UNLATCHING + else + self.value = self.REMOTE_UNLATCHING + end +end + +LatchControlModesBitmap.unset_remote_unlatching = function(self) + self.value = self.value & (~self.REMOTE_UNLATCHING & self.BASE_MASK) +end + +LatchControlModesBitmap.mask_methods = { + is_remote_latching_set = LatchControlModesBitmap.is_remote_latching_set, + set_remote_latching = LatchControlModesBitmap.set_remote_latching, + unset_remote_latching = LatchControlModesBitmap.unset_remote_latching, + is_remote_unlatching_set = LatchControlModesBitmap.is_remote_unlatching_set, + set_remote_unlatching = LatchControlModesBitmap.set_remote_unlatching, + unset_remote_unlatching = LatchControlModesBitmap.unset_remote_unlatching, +} + +LatchControlModesBitmap.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(LatchControlModesBitmap, new_mt) + +return LatchControlModesBitmap diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/MainStateEnum.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/MainStateEnum.lua new file mode 100644 index 0000000000..916e27dda0 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/MainStateEnum.lua @@ -0,0 +1,45 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local MainStateEnum = {} +local new_mt = UintABC.new_mt({NAME = "MainStateEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.STOPPED] = "STOPPED", + [self.MOVING] = "MOVING", + [self.WAITING_FOR_MOTION] = "WAITING_FOR_MOTION", + [self.ERROR] = "ERROR", + [self.CALIBRATING] = "CALIBRATING", + [self.PROTECTED] = "PROTECTED", + [self.DISENGAGED] = "DISENGAGED", + [self.SETUP_REQUIRED] = "SETUP_REQUIRED", + } + 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.STOPPED = 0x00 +new_mt.__index.MOVING = 0x01 +new_mt.__index.WAITING_FOR_MOTION = 0x02 +new_mt.__index.ERROR = 0x03 +new_mt.__index.CALIBRATING = 0x04 +new_mt.__index.PROTECTED = 0x05 +new_mt.__index.DISENGAGED = 0x06 +new_mt.__index.SETUP_REQUIRED = 0x07 + +MainStateEnum.STOPPED = 0x00 +MainStateEnum.MOVING = 0x01 +MainStateEnum.WAITING_FOR_MOTION = 0x02 +MainStateEnum.ERROR = 0x03 +MainStateEnum.CALIBRATING = 0x04 +MainStateEnum.PROTECTED = 0x05 +MainStateEnum.DISENGAGED = 0x06 +MainStateEnum.SETUP_REQUIRED = 0x07 + +MainStateEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(MainStateEnum, new_mt) + +return MainStateEnum diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallCurrentStateStruct.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallCurrentStateStruct.lua new file mode 100644 index 0000000000..41af9d66db --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallCurrentStateStruct.lua @@ -0,0 +1,91 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local OverallCurrentStateStruct = {} +local new_mt = StructureABC.new_mt({NAME = "OverallCurrentStateStruct", ID = data_types.name_to_id_map["Structure"]}) + +OverallCurrentStateStruct.field_defs = { + { + name = "position", + field_id = 0, + is_nullable = true, + is_optional = true, + data_type = require "embedded_clusters.ClosureControl.types.CurrentPositionEnum", + }, + { + name = "latch", + field_id = 1, + is_nullable = true, + is_optional = true, + data_type = require "st.matter.data_types.Boolean", + }, + { + name = "speed", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + }, + { + name = "secure_state", + field_id = 3, + is_nullable = true, + is_optional = false, + data_type = require "st.matter.data_types.Boolean", + }, +} + +OverallCurrentStateStruct.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 + +OverallCurrentStateStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = OverallCurrentStateStruct.init +new_mt.__index.serialize = OverallCurrentStateStruct.serialize + +OverallCurrentStateStruct.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(OverallCurrentStateStruct, new_mt) + +return OverallCurrentStateStruct diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallTargetStateStruct.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallTargetStateStruct.lua new file mode 100644 index 0000000000..eac6492815 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallTargetStateStruct.lua @@ -0,0 +1,84 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local OverallTargetStateStruct = {} +local new_mt = StructureABC.new_mt({NAME = "OverallTargetStateStruct", ID = data_types.name_to_id_map["Structure"]}) + +OverallTargetStateStruct.field_defs = { + { + name = "position", + field_id = 0, + is_nullable = true, + is_optional = true, + data_type = require "embedded_clusters.ClosureControl.types.TargetPositionEnum", + }, + { + name = "latch", + field_id = 1, + is_nullable = true, + is_optional = true, + data_type = require "st.matter.data_types.Boolean", + }, + { + name = "speed", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + }, +} + +OverallTargetStateStruct.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 + +OverallTargetStateStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = OverallTargetStateStruct.init +new_mt.__index.serialize = OverallTargetStateStruct.serialize + +OverallTargetStateStruct.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(OverallTargetStateStruct, new_mt) + +return OverallTargetStateStruct diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/TargetPositionEnum.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/TargetPositionEnum.lua new file mode 100644 index 0000000000..b7a6122863 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/TargetPositionEnum.lua @@ -0,0 +1,36 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local TargetPositionEnum = {} +local new_mt = UintABC.new_mt({NAME = "TargetPositionEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.MOVE_TO_FULLY_CLOSED] = "MOVE_TO_FULLY_CLOSED", + [self.MOVE_TO_FULLY_OPEN] = "MOVE_TO_FULLY_OPEN", + [self.MOVE_TO_PEDESTRIAN_POSITION] = "MOVE_TO_PEDESTRIAN_POSITION", + [self.MOVE_TO_VENTILATION_POSITION] = "MOVE_TO_VENTILATION_POSITION", + [self.MOVE_TO_SIGNATURE_POSITION] = "MOVE_TO_SIGNATURE_POSITION", + } + 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.MOVE_TO_FULLY_CLOSED = 0x00 +new_mt.__index.MOVE_TO_FULLY_OPEN = 0x01 +new_mt.__index.MOVE_TO_PEDESTRIAN_POSITION = 0x02 +new_mt.__index.MOVE_TO_VENTILATION_POSITION = 0x03 +new_mt.__index.MOVE_TO_SIGNATURE_POSITION = 0x04 + +TargetPositionEnum.MOVE_TO_FULLY_CLOSED = 0x00 +TargetPositionEnum.MOVE_TO_FULLY_OPEN = 0x01 +TargetPositionEnum.MOVE_TO_PEDESTRIAN_POSITION = 0x02 +TargetPositionEnum.MOVE_TO_VENTILATION_POSITION = 0x03 +TargetPositionEnum.MOVE_TO_SIGNATURE_POSITION = 0x04 + +TargetPositionEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(TargetPositionEnum, new_mt) + +return TargetPositionEnum diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/init.lua new file mode 100644 index 0000000000..6531f734a3 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/init.lua @@ -0,0 +1,10 @@ +local types_mt = {} +types_mt.__index = function(self, key) + return require("embedded_clusters.ClosureControl.types." .. key) +end + +local ClosureControlTypes = {} + +setmetatable(ClosureControlTypes, types_mt) + +return ClosureControlTypes diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/init.lua new file mode 100644 index 0000000000..eb2f4cdae2 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/init.lua @@ -0,0 +1,106 @@ +local cluster_base = require "st.matter.cluster_base" +local ClosureDimensionServerAttributes = require "embedded_clusters.ClosureDimension.server.attributes" +local ClosureDimensionServerCommands = require "embedded_clusters.ClosureDimension.server.commands" +local ClosureDimensionTypes = require "embedded_clusters.ClosureDimension.types" + +local ClosureDimension = {} + +ClosureDimension.ID = 0x0105 +ClosureDimension.NAME = "ClosureDimension" +ClosureDimension.server = {} +ClosureDimension.client = {} +ClosureDimension.server.attributes = ClosureDimensionServerAttributes:set_parent_cluster(ClosureDimension) +ClosureDimension.server.commands = ClosureDimensionServerCommands:set_parent_cluster(ClosureDimension) +ClosureDimension.types = ClosureDimensionTypes + +function ClosureDimension:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "CurrentState", + [0x0001] = "TargetState", + [0x0002] = "Resolution", + [0x0003] = "StepValue", + [0x0004] = "Unit", + [0x0005] = "UnitRange", + [0x0006] = "LimitRange", + [0x0007] = "TranslationDirection", + [0x0008] = "RotationAxis", + [0x0009] = "Overflow", + [0x000A] = "ModulationType", + [0x000B] = "LatchControlModes", + [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 ClosureDimension:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "SetTarget", + [0x0001] = "Step", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +ClosureDimension.attribute_direction_map = { + ["CurrentState"] = "server", + ["TargetState"] = "server", + ["Resolution"] = "server", + ["StepValue"] = "server", + ["Unit"] = "server", + ["UnitRange"] = "server", + ["LimitRange"] = "server", + ["TranslationDirection"] = "server", + ["RotationAxis"] = "server", + ["Overflow"] = "server", + ["ModulationType"] = "server", + ["LatchControlModes"] = "server", + ["AcceptedCommandList"] = "server", + ["AttributeList"] = "server", +} + +ClosureDimension.command_direction_map = { + ["SetTarget"] = "server", + ["Step"] = "server", +} + +ClosureDimension.FeatureMap = ClosureDimension.types.Feature + +function ClosureDimension.are_features_supported(feature, feature_map) + if (ClosureDimension.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 = ClosureDimension.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ClosureDimension.NAME)) + end + return ClosureDimension[direction].attributes[key] +end +ClosureDimension.attributes = {} +setmetatable(ClosureDimension.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = ClosureDimension.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, ClosureDimension.NAME)) + end + return ClosureDimension[direction].commands[key] +end +ClosureDimension.commands = {} +setmetatable(ClosureDimension.commands, command_helper_mt) + +setmetatable(ClosureDimension, {__index = cluster_base}) + +return ClosureDimension diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/CurrentState.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/CurrentState.lua new file mode 100644 index 0000000000..cd0566b774 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/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 = 0x0000, + NAME = "CurrentState", + base_type = require "embedded_clusters.ClosureDimension.types.DimensionStateStruct", +} + +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-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/LimitRange.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/LimitRange.lua new file mode 100644 index 0000000000..8f9b357bf8 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/LimitRange.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 LimitRange = { + ID = 0x0006, + NAME = "LimitRange", + base_type = require "embedded_clusters.ClosureDimension.types.RangePercent100thsStruct", +} + +function LimitRange:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function LimitRange:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function LimitRange:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function LimitRange:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LimitRange: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 LimitRange:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(LimitRange, {__call = LimitRange.new_value, __index = LimitRange.base_type}) +return LimitRange diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/StepValue.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/StepValue.lua new file mode 100644 index 0000000000..c6958a20bb --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/StepValue.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 StepValue = { + ID = 0x0003, + NAME = "StepValue", + base_type = require "st.matter.data_types.Uint16", +} + +function StepValue:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function StepValue:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function StepValue:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function StepValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function StepValue: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 StepValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(StepValue, {__call = StepValue.new_value, __index = StepValue.base_type}) +return StepValue diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/TargetState.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/TargetState.lua new file mode 100644 index 0000000000..fa242ca507 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/TargetState.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 TargetState = { + ID = 0x0001, + NAME = "TargetState", + base_type = require "embedded_clusters.ClosureDimension.types.DimensionStateStruct", +} + +function TargetState:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function TargetState:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function TargetState:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function TargetState:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function TargetState: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 TargetState:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(TargetState, {__call = TargetState.new_value, __index = TargetState.base_type}) +return TargetState diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/init.lua new file mode 100644 index 0000000000..8728e92157 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/attributes/init.lua @@ -0,0 +1,19 @@ +local attr_mt = {} +attr_mt.__index = function(self, key) + local req_loc = string.format("embedded_clusters.ClosureDimension.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + return raw_def +end + +local ClosureDimensionServerAttributes = {} + +function ClosureDimensionServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ClosureDimensionServerAttributes, attr_mt) + +return ClosureDimensionServerAttributes diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/SetTarget.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/SetTarget.lua new file mode 100644 index 0000000000..d2f4a51b78 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/SetTarget.lua @@ -0,0 +1,112 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local SetTarget = {} + +SetTarget.NAME = "SetTarget" +SetTarget.ID = 0x0000 +SetTarget.field_defs = { + { + name = "position", + field_id = 0, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "latch", + field_id = 1, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Boolean", + }, + { + name = "speed", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + }, +} + +function SetTarget: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 SetTarget:init(device, endpoint_id, position, latch, speed) + local out = {} + local args = {position, latch, speed} + 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 = SetTarget, + __tostring = SetTarget.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + true + ) +end + +function SetTarget:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SetTarget: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 SetTarget:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SetTarget, {__call = SetTarget.init}) + +return SetTarget diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/Step.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/Step.lua new file mode 100644 index 0000000000..ce70c7e680 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/Step.lua @@ -0,0 +1,112 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local Step = {} + +Step.NAME = "Step" +Step.ID = 0x0001 +Step.field_defs = { + { + name = "direction", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "embedded_clusters.ClosureDimension.types.StepDirectionEnum", + }, + { + name = "number_of_steps", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "speed", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + }, +} + +function Step: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 Step:init(device, endpoint_id, direction, number_of_steps, speed) + local out = {} + local args = {direction, number_of_steps, speed} + 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 = Step, + __tostring = Step.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + true + ) +end + +function Step:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function Step: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 Step:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(Step, {__call = Step.init}) + +return Step diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/init.lua new file mode 100644 index 0000000000..74003acc00 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/init.lua @@ -0,0 +1,19 @@ +local command_mt = {} +command_mt.__index = function(self, key) + local req_loc = string.format("embedded_clusters.ClosureDimension.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + return raw_def +end + +local ClosureDimensionServerCommands = {} + +function ClosureDimensionServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ClosureDimensionServerCommands, command_mt) + +return ClosureDimensionServerCommands diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/DimensionStateStruct.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/DimensionStateStruct.lua new file mode 100644 index 0000000000..56958b2020 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/DimensionStateStruct.lua @@ -0,0 +1,84 @@ +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local DimensionStateStruct = {} +local new_mt = StructureABC.new_mt({NAME = "DimensionStateStruct", ID = data_types.name_to_id_map["Structure"]}) + +DimensionStateStruct.field_defs = { + { + name = "position", + field_id = 0, + is_nullable = true, + is_optional = true, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "latch", + field_id = 1, + is_nullable = true, + is_optional = true, + data_type = require "st.matter.data_types.Boolean", + }, + { + name = "speed", + field_id = 2, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + }, +} + +DimensionStateStruct.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 + +DimensionStateStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = DimensionStateStruct.init +new_mt.__index.serialize = DimensionStateStruct.serialize + +DimensionStateStruct.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(DimensionStateStruct, new_mt) + +return DimensionStateStruct diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/Feature.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/Feature.lua new file mode 100644 index 0000000000..ecf029105c --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/Feature.lua @@ -0,0 +1,207 @@ +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.POSITIONING = 0x0001 +Feature.MOTION_LATCHING = 0x0002 +Feature.UNIT = 0x0004 +Feature.LIMITATION = 0x0008 +Feature.SPEED = 0x0010 +Feature.TRANSLATION = 0x0020 +Feature.ROTATION = 0x0040 +Feature.MODULATION = 0x0080 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + POSITIONING = 0x0001, + MOTION_LATCHING = 0x0002, + UNIT = 0x0004, + LIMITATION = 0x0008, + SPEED = 0x0010, + TRANSLATION = 0x0020, + ROTATION = 0x0040, + MODULATION = 0x0080, +} + +Feature.is_positioning_set = function(self) + return (self.value & self.POSITIONING) ~= 0 +end + +Feature.set_positioning = function(self) + if self.value ~= nil then + self.value = self.value | self.POSITIONING + else + self.value = self.POSITIONING + end +end + +Feature.unset_positioning = function(self) + self.value = self.value & (~self.POSITIONING & self.BASE_MASK) +end + +Feature.is_motion_latching_set = function(self) + return (self.value & self.MOTION_LATCHING) ~= 0 +end + +Feature.set_motion_latching = function(self) + if self.value ~= nil then + self.value = self.value | self.MOTION_LATCHING + else + self.value = self.MOTION_LATCHING + end +end + +Feature.unset_motion_latching = function(self) + self.value = self.value & (~self.MOTION_LATCHING & self.BASE_MASK) +end + +Feature.is_unit_set = function(self) + return (self.value & self.UNIT) ~= 0 +end + +Feature.set_unit = function(self) + if self.value ~= nil then + self.value = self.value | self.UNIT + else + self.value = self.UNIT + end +end + +Feature.unset_unit = function(self) + self.value = self.value & (~self.UNIT & self.BASE_MASK) +end + +Feature.is_limitation_set = function(self) + return (self.value & self.LIMITATION) ~= 0 +end + +Feature.set_limitation = function(self) + if self.value ~= nil then + self.value = self.value | self.LIMITATION + else + self.value = self.LIMITATION + end +end + +Feature.unset_limitation = function(self) + self.value = self.value & (~self.LIMITATION & self.BASE_MASK) +end + +Feature.is_speed_set = function(self) + return (self.value & self.SPEED) ~= 0 +end + +Feature.set_speed = function(self) + if self.value ~= nil then + self.value = self.value | self.SPEED + else + self.value = self.SPEED + end +end + +Feature.unset_speed = function(self) + self.value = self.value & (~self.SPEED & self.BASE_MASK) +end + +Feature.is_translation_set = function(self) + return (self.value & self.TRANSLATION) ~= 0 +end + +Feature.set_translation = function(self) + if self.value ~= nil then + self.value = self.value | self.TRANSLATION + else + self.value = self.TRANSLATION + end +end + +Feature.unset_translation = function(self) + self.value = self.value & (~self.TRANSLATION & self.BASE_MASK) +end + +Feature.is_rotation_set = function(self) + return (self.value & self.ROTATION) ~= 0 +end + +Feature.set_rotation = function(self) + if self.value ~= nil then + self.value = self.value | self.ROTATION + else + self.value = self.ROTATION + end +end + +Feature.unset_rotation = function(self) + self.value = self.value & (~self.ROTATION & self.BASE_MASK) +end + +Feature.is_modulation_set = function(self) + return (self.value & self.MODULATION) ~= 0 +end + +Feature.set_modulation = function(self) + if self.value ~= nil then + self.value = self.value | self.MODULATION + else + self.value = self.MODULATION + end +end + +Feature.unset_modulation = function(self) + self.value = self.value & (~self.MODULATION & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.POSITIONING | + Feature.MOTION_LATCHING | + Feature.UNIT | + Feature.LIMITATION | + Feature.SPEED | + Feature.TRANSLATION | + Feature.ROTATION | + Feature.MODULATION + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_positioning_set = Feature.is_positioning_set, + set_positioning = Feature.set_positioning, + unset_positioning = Feature.unset_positioning, + is_motion_latching_set = Feature.is_motion_latching_set, + set_motion_latching = Feature.set_motion_latching, + unset_motion_latching = Feature.unset_motion_latching, + is_unit_set = Feature.is_unit_set, + set_unit = Feature.set_unit, + unset_unit = Feature.unset_unit, + is_limitation_set = Feature.is_limitation_set, + set_limitation = Feature.set_limitation, + unset_limitation = Feature.unset_limitation, + is_speed_set = Feature.is_speed_set, + set_speed = Feature.set_speed, + unset_speed = Feature.unset_speed, + is_translation_set = Feature.is_translation_set, + set_translation = Feature.set_translation, + unset_translation = Feature.unset_translation, + is_rotation_set = Feature.is_rotation_set, + set_rotation = Feature.set_rotation, + unset_rotation = Feature.unset_rotation, + is_modulation_set = Feature.is_modulation_set, + set_modulation = Feature.set_modulation, + unset_modulation = Feature.unset_modulation, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/StepDirectionEnum.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/StepDirectionEnum.lua new file mode 100644 index 0000000000..c33e8a23ef --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/StepDirectionEnum.lua @@ -0,0 +1,27 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local StepDirectionEnum = {} +local new_mt = UintABC.new_mt({NAME = "StepDirectionEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.DECREASE] = "DECREASE", + [self.INCREASE] = "INCREASE", + } + 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.DECREASE = 0x00 +new_mt.__index.INCREASE = 0x01 + +StepDirectionEnum.DECREASE = 0x00 +StepDirectionEnum.INCREASE = 0x01 + +StepDirectionEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(StepDirectionEnum, new_mt) + +return StepDirectionEnum diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/TranslationDirectionEnum.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/TranslationDirectionEnum.lua new file mode 100644 index 0000000000..b84a353442 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/TranslationDirectionEnum.lua @@ -0,0 +1,57 @@ +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local TranslationDirectionEnum = {} +local new_mt = UintABC.new_mt({NAME = "TranslationDirectionEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.DOWNWARD] = "DOWNWARD", + [self.UPWARD] = "UPWARD", + [self.VERTICAL_MASK] = "VERTICAL_MASK", + [self.VERTICAL_SYMMETRY] = "VERTICAL_SYMMETRY", + [self.LEFTWARD] = "LEFTWARD", + [self.RIGHTWARD] = "RIGHTWARD", + [self.HORIZONTAL_MASK] = "HORIZONTAL_MASK", + [self.HORIZONTAL_SYMMETRY] = "HORIZONTAL_SYMMETRY", + [self.FORWARD] = "FORWARD", + [self.BACKWARD] = "BACKWARD", + [self.DEPTH_MASK] = "DEPTH_MASK", + [self.DEPTH_SYMMETRY] = "DEPTH_SYMMETRY", + } + 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.DOWNWARD = 0x00 +new_mt.__index.UPWARD = 0x01 +new_mt.__index.VERTICAL_MASK = 0x02 +new_mt.__index.VERTICAL_SYMMETRY = 0x03 +new_mt.__index.LEFTWARD = 0x04 +new_mt.__index.RIGHTWARD = 0x05 +new_mt.__index.HORIZONTAL_MASK = 0x06 +new_mt.__index.HORIZONTAL_SYMMETRY = 0x07 +new_mt.__index.FORWARD = 0x08 +new_mt.__index.BACKWARD = 0x09 +new_mt.__index.DEPTH_MASK = 0x0A +new_mt.__index.DEPTH_SYMMETRY = 0x0B + +TranslationDirectionEnum.DOWNWARD = 0x00 +TranslationDirectionEnum.UPWARD = 0x01 +TranslationDirectionEnum.VERTICAL_MASK = 0x02 +TranslationDirectionEnum.VERTICAL_SYMMETRY = 0x03 +TranslationDirectionEnum.LEFTWARD = 0x04 +TranslationDirectionEnum.RIGHTWARD = 0x05 +TranslationDirectionEnum.HORIZONTAL_MASK = 0x06 +TranslationDirectionEnum.HORIZONTAL_SYMMETRY = 0x07 +TranslationDirectionEnum.FORWARD = 0x08 +TranslationDirectionEnum.BACKWARD = 0x09 +TranslationDirectionEnum.DEPTH_MASK = 0x0A +TranslationDirectionEnum.DEPTH_SYMMETRY = 0x0B + +TranslationDirectionEnum.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(TranslationDirectionEnum, new_mt) + +return TranslationDirectionEnum diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/init.lua new file mode 100644 index 0000000000..8bc2fe2cb2 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/init.lua @@ -0,0 +1,10 @@ +local types_mt = {} +types_mt.__index = function(self, key) + return require("embedded_clusters.ClosureDimension.types." .. key) +end + +local ClosureDimensionTypes = {} + +setmetatable(ClosureDimensionTypes, types_mt) + +return ClosureDimensionTypes diff --git a/drivers/SmartThings/matter-window-covering/src/init.lua b/drivers/SmartThings/matter-window-covering/src/init.lua index a98f8a321d..a196742210 100644 --- a/drivers/SmartThings/matter-window-covering/src/init.lua +++ b/drivers/SmartThings/matter-window-covering/src/init.lua @@ -4,6 +4,7 @@ --Note: Currently only support for window shades with the PositionallyAware Feature --Note: No support for setting device into calibration mode, it must be done manually + local capabilities = require "st.capabilities" local im = require "st.matter.interaction_model" local log = require "log" @@ -325,7 +326,7 @@ local matter_driver_template = { }, subscribed_attributes = { [capabilities.windowShade.ID] = { - clusters.WindowCovering.attributes.OperationalStatus + clusters.WindowCovering.attributes.OperationalStatus, }, [capabilities.windowShadeLevel.ID] = { clusters.LevelControl.attributes.CurrentLevel, @@ -363,6 +364,8 @@ local matter_driver_template = { capabilities.windowShadeTiltLevel, capabilities.windowShade, capabilities.windowShadePreset, + capabilities.doorControl, + capabilities.level, capabilities.battery, capabilities.batteryLevel, }, diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers.lua index ff048340d0..ad3e53e4b0 100644 --- a/drivers/SmartThings/matter-window-covering/src/sub_drivers.lua +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers.lua @@ -3,6 +3,7 @@ local lazy_load_if_possible = require "lazy_load_subdriver" local sub_drivers = { - lazy_load_if_possible("matter-window-covering-position-updates-while-moving"), + lazy_load_if_possible("sub_drivers.closure"), + lazy_load_if_possible("sub_drivers.matter-window-covering-position-updates-while-moving"), } return sub_drivers diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/can_handle.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/can_handle.lua new file mode 100644 index 0000000000..80343aded8 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local CLOSURE_CONTROL_CLUSTER_ID = 0x0104 + +return function(opts, driver, device) + if #device:get_endpoints(CLOSURE_CONTROL_CLUSTER_ID) > 0 then + return true, require("sub_drivers.closure") + end + return false +end diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_handlers/attribute_handlers.lua new file mode 100644 index 0000000000..36a2e0d372 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_handlers/attribute_handlers.lua @@ -0,0 +1,110 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local version = require "version" + +if version.api < 20 then + clusters.ClosureControl = require "embedded_clusters.ClosureControl" + clusters.ClosureDimension = require "embedded_clusters.ClosureDimension" +end + +local fields = require "sub_drivers.closure.closure_utils.fields" +local closure_utils = require "sub_drivers.closure.closure_utils.utils" + +local ClosureAttrHandlers = {} + +function ClosureAttrHandlers.main_state_attr_handler(driver, device, ib, response) + if ib.data.value == nil then return end + closure_utils.set_closure_control_state(device, ib.endpoint_id, { main = ib.data.value }) + closure_utils.emit_closure_control_capability(device, ib.endpoint_id) +end + +function ClosureAttrHandlers.overall_current_state_attr_handler(driver, device, ib, response) + if not ib.data.elements then return end + clusters.ClosureControl.types.OverallCurrentStateStruct:augment_type(ib.data) + for _, v in pairs(ib.data.elements or {}) do + if v.field_id == 0 then + local current = v.value + closure_utils.set_closure_control_state(device, ib.endpoint_id, { current = current }) + closure_utils.emit_closure_control_capability(device, ib.endpoint_id) + break + end + end +end + +function ClosureAttrHandlers.overall_target_state_attr_handler(driver, device, ib, response) + if not ib.data.elements then return end + clusters.ClosureControl.types.OverallTargetStateStruct:augment_type(ib.data) + for _, v in pairs(ib.data.elements or {}) do + if v.field_id == 0 then + local target = v.value + closure_utils.set_closure_control_state(device, ib.endpoint_id, { target = target }) + closure_utils.emit_closure_control_capability(device, ib.endpoint_id) + break + end + end +end + +function ClosureAttrHandlers.closure_dimension_current_state_handler(driver, device, ib, response) + if not ib.data.elements then return end + clusters.ClosureDimension.types.DimensionStateStruct:augment_type(ib.data) + local pos_field = ib.data.elements.position + if not pos_field or pos_field.value == nil then return end + local level = math.floor(pos_field.value / 100) + if device:supports_capability_by_id(capabilities.doorControl.ID) then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.level.level(level)) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windowShadeLevel.shadeLevel(level)) + end +end + +function ClosureAttrHandlers.tag_list_handler(driver, device, ib, response) + if not ib.data.elements then return end + local tag_value + for _, v in ipairs(ib.data.elements) do + local tag = v.elements + if tag and tag.namespace_id and tag.namespace_id.value == 0x44 then + tag_value = tag.tag and tag.tag.value + break + end + end + + local closure_tag_map = { + [0] = fields.closure_tag_list.COVERING, + [1] = fields.closure_tag_list.WINDOW, + [2] = fields.closure_tag_list.BARRIER, + [3] = fields.closure_tag_list.CABINET, + [4] = fields.closure_tag_list.GATE, + [5] = fields.closure_tag_list.GARAGE_DOOR, + [6] = fields.closure_tag_list.DOOR, + } + + local closure_tag = fields.closure_tag_list.NA + if tag_value ~= nil and closure_tag_map[tag_value] ~= nil then + closure_tag = closure_tag_map[tag_value] + end + + device:set_field(fields.CLOSURE_TAG, closure_tag, {persist = true}) + closure_utils.match_profile(device) +end + +function ClosureAttrHandlers.power_source_attribute_list_handler(driver, device, ib, response) + for _, attr in ipairs(ib.data.elements) do + if attr.value == 0x0C then -- BatPercentRemaining + device:set_field(fields.CLOSURE_BATTERY_SUPPORT, fields.battery_support.BATTERY_PERCENTAGE, {persist = true}) + closure_utils.match_profile(device) + return + elseif attr.value == 0x0E then -- BatChargeLevel + device:set_field(fields.CLOSURE_BATTERY_SUPPORT, fields.battery_support.BATTERY_LEVEL, {persist = true}) + closure_utils.match_profile(device) + return + end + end + -- No battery attribute found + device:set_field(fields.CLOSURE_BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist = true}) + closure_utils.match_profile(device) +end + +return ClosureAttrHandlers diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_handlers/capability_handlers.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_handlers/capability_handlers.lua new file mode 100644 index 0000000000..e6d68c086d --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_handlers/capability_handlers.lua @@ -0,0 +1,71 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" +local version = require "version" + +if version.api < 20 then + clusters.ClosureControl = require "embedded_clusters.ClosureControl" + clusters.ClosureDimension = require "embedded_clusters.ClosureDimension" +end + +local fields = require "sub_drivers.closure.closure_utils.fields" +local closure_utils = require "sub_drivers.closure.closure_utils.utils" + +local ClosureCapabilityHandlers = {} + +-- close covering (or door/gate) +function ClosureCapabilityHandlers.handle_close(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local reverse = device:get_field(fields.REVERSE_POLARITY) + local req = reverse and + clusters.ClosureControl.server.commands.MoveTo( + device, endpoint_id, clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_OPEN + ) or + clusters.ClosureControl.server.commands.MoveTo( + device, endpoint_id, clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_CLOSED + ) + device:send(req) +end + +-- open covering (or door/gate) +function ClosureCapabilityHandlers.handle_open(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local reverse = device:get_field(fields.REVERSE_POLARITY) + local req = reverse and + clusters.ClosureControl.server.commands.MoveTo( + device, endpoint_id, clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_CLOSED + ) or + clusters.ClosureControl.server.commands.MoveTo( + device, endpoint_id, clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_OPEN + ) + device:send(req) +end + +-- pause / stop covering +function ClosureCapabilityHandlers.handle_pause(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.ClosureControl.server.commands.Stop(device, endpoint_id)) +end + +-- move to shade level 0-100 for covering Closure devices +function ClosureCapabilityHandlers.handle_shade_level(driver, device, cmd) + local dim_eps = closure_utils.get_closure_dimension_eps(device) + local endpoint_id = #dim_eps == 1 and dim_eps[1] or device:component_to_endpoint(cmd.component) + if endpoint_id then + device:send(clusters.ClosureDimension.server.commands.SetTarget( + device, endpoint_id, cmd.args.shadeLevel * 100 + )) + end +end + +-- move to level 0-100 for door/gate/garage-door Closure devices +function ClosureCapabilityHandlers.handle_level(driver, device, cmd) + local dim_eps = closure_utils.get_closure_dimension_eps(device) + local endpoint_id = #dim_eps == 1 and dim_eps[1] or device:component_to_endpoint(cmd.component) + device:send(clusters.ClosureDimension.server.commands.SetTarget( + device, endpoint_id, cmd.args.level * 100 + )) +end + +return ClosureCapabilityHandlers diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/fields.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/fields.lua new file mode 100644 index 0000000000..3471090518 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/fields.lua @@ -0,0 +1,33 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local fields = {} + +fields.REVERSE_POLARITY = "__reverse_polarity" +fields.PRESET_LEVEL_KEY = "__preset_level_key" +fields.DEFAULT_PRESET_LEVEL = 50 + +fields.battery_support = { + NO_BATTERY = "NO_BATTERY", + BATTERY_LEVEL = "BATTERY_LEVEL", + BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE", +} + +fields.CLOSURE_CONTROL_STATE_CACHE = "__closure_control_state_cache" +fields.CLOSURE_BATTERY_SUPPORT = "__closure_battery_support" +fields.CLOSURE_TAG = "__closure_tag" + +fields.closure_tag_list = { + NA = "N/A", + COVERING = "COVERING", + WINDOW = "WINDOW", + BARRIER = "BARRIER", + CABINET = "CABINET", + GATE = "GATE", + GARAGE_DOOR = "GARAGE_DOOR", + DOOR = "DOOR", +} + +fields.SUBSCRIBED_ATTRIBUTES_KEY = "__subscribed_attributes" + +return fields diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/utils.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/utils.lua new file mode 100644 index 0000000000..d57656b743 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/utils.lua @@ -0,0 +1,323 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local im = require "st.matter.interaction_model" +local log = require "log" +local version = require "version" + +if version.api < 20 then + clusters.ClosureControl = require "embedded_clusters.ClosureControl" + clusters.ClosureDimension = require "embedded_clusters.ClosureDimension" +end + +local fields = require "sub_drivers.closure.closure_utils.fields" + +local utils = {} + +function utils.find_default_endpoint(device, cluster) + local res = device.MATTER_DEFAULT_ENDPOINT + local eps = device:get_endpoints(cluster) + table.sort(eps) + for _, v in ipairs(eps) do + if v ~= 0 then -- 0 is the Matter RootNode endpoint + return v + end + end + device.log.warn(string.format( + "Did not find default endpoint, will use endpoint %d instead", + device.MATTER_DEFAULT_ENDPOINT + )) + return res +end + +function utils.get_closure_dimension_eps(device) + local eps = device:get_endpoints(clusters.ClosureDimension.ID) or {} + table.sort(eps) + local result = {} + for _, ep in ipairs(eps) do + if ep ~= 0 then + table.insert(result, ep) + if #result >= 4 then break end + end + end + return result +end + +--- Single-panel devices always map to "main"; +--- multi-panel devices map to "windowShade1"..."windowShade4" or "door1"..."door4". +function utils.endpoint_to_component(device, ep_id) + local dim_eps = utils.get_closure_dimension_eps(device) + if #dim_eps > 1 then + local is_door_type = device:supports_capability_by_id(capabilities.doorControl.ID) + local prefix = is_door_type and "door" or "windowShade" + for i, ep in ipairs(dim_eps) do + if ep == ep_id then + return prefix .. i + end + end + end + return "main" +end + +function utils.component_to_endpoint(device, component_name) + local dim_eps = utils.get_closure_dimension_eps(device) + if #dim_eps > 1 then + local comp_num = tonumber(component_name:match("(%d+)$")) + if comp_num and dim_eps[comp_num] then + return dim_eps[comp_num] + end + end + return utils.find_default_endpoint(device, clusters.ClosureControl.ID) +end + +function utils.match_profile(device) + if not device:get_field(fields.CLOSURE_TAG) or not device:get_field(fields.CLOSURE_BATTERY_SUPPORT) then + log.warn("Closure tag or battery support not set yet, cannot match profile") + return + end + + local tag = device:get_field(fields.CLOSURE_TAG) + local profile_name + local is_door_type = true + + if tag == fields.closure_tag_list.GATE then + profile_name = "gate" + elseif tag == fields.closure_tag_list.GARAGE_DOOR then + profile_name = "garage-door" + elseif tag == fields.closure_tag_list.DOOR then + profile_name = "door" + else + -- COVERING, WINDOW, BARRIER, CABINET, NA -> generic covering profile + profile_name = "covering" + is_door_type = false + end + + local optional_caps = {} + + local closure_battery = device:get_field(fields.CLOSURE_BATTERY_SUPPORT) + if closure_battery == fields.battery_support.BATTERY_PERCENTAGE then + table.insert(optional_caps, {"main", {capabilities.battery.ID}}) + elseif closure_battery == fields.battery_support.BATTERY_LEVEL then + table.insert(optional_caps, {"main", {capabilities.batteryLevel.ID}}) + end + + -- ClosureDimension capabilities: windowShadeLevel (covering) or level (door types) + local dim_eps = utils.get_closure_dimension_eps(device) + if #dim_eps > 0 then + local dim_cap = is_door_type and capabilities.level.ID or capabilities.windowShadeLevel.ID + if #dim_eps == 1 then + -- Single ClosureDimension: enable the capability on the main component. + local found_main = false + for _, entry in ipairs(optional_caps) do + if entry[1] == "main" then + table.insert(entry[2], dim_cap) + found_main = true + break + end + end + if not found_main then + table.insert(optional_caps, {"main", {dim_cap}}) + end + else + -- Multiple ClosureDimensions: one optional component+capability per panel. + local prefix = is_door_type and "door" or "windowShade" + for i = 1, math.min(#dim_eps, 4) do + table.insert(optional_caps, {prefix .. i, {dim_cap}}) + end + end + end + + device:try_update_metadata({ + profile = profile_name, + optional_component_capabilities = #optional_caps > 0 and optional_caps or nil, + }) +end + +--- Deeply compare two values. +--- Handles metatables. Can optionally ignore cycle checking and/or function differences. +--- +--- @param a any +--- @param b any +--- @param opts table|nil { ignore_functions = boolean, ignore_cycles = boolean } +--- @param seen table|nil +--- @return boolean +function utils.deep_equals(a, b, opts, seen) + if a == b then return true end + if type(a) ~= type(b) then return false end + if type(a) == "function" and opts and opts.ignore_functions then return true end + if type(a) ~= "table" then return false end + + if not (opts and opts.ignore_cycles) then + seen = seen or {} + seen[a] = seen[a] or {} + if seen[a][b] then + return seen[a][b] + end + seen[a][b] = true + end + + for k, v in pairs(a) do + if not utils.deep_equals(v, b[k], opts, seen) then + return false + end + end + + for k in pairs(b) do + if a[k] == nil then + return false + end + end + + local mt_a = getmetatable(a) + local mt_b = getmetatable(b) + return utils.deep_equals(mt_a, mt_b, opts, seen) +end + +function utils.set_closure_control_state(device, endpoint_id, field) + local cache = device:get_field(fields.CLOSURE_CONTROL_STATE_CACHE) or {} + if not cache[endpoint_id] then cache[endpoint_id] = {} end + for k, v in pairs(field) do + cache[endpoint_id][k] = v + end + device:set_field(fields.CLOSURE_CONTROL_STATE_CACHE, cache) +end + +--- Emits the appropriate windowShade / doorControl capability event from the +--- cached MainState, OverallCurrentState.position, and OverallTargetState.position. +function utils.emit_closure_control_capability(device, endpoint_id) + local cache = device:get_field(fields.CLOSURE_CONTROL_STATE_CACHE) + if not cache then return end + local closure_control_state = cache[endpoint_id] or {} + local reverse = device:get_field(fields.REVERSE_POLARITY) + + local main = closure_control_state.main + local current = closure_control_state.current + local target = closure_control_state.target + + local closure_capability = capabilities.windowShade.windowShade + if device:supports_capability_by_id(capabilities.doorControl.ID) then + closure_capability = capabilities.doorControl.door + end + + if main == clusters.ClosureControl.types.MainStateEnum.MOVING then + if target == clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_CLOSED then + device:emit_event_for_endpoint( + endpoint_id, reverse and closure_capability.opening() or closure_capability.closing() + ) + elseif target == clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_OPEN then + device:emit_event_for_endpoint( + endpoint_id, reverse and closure_capability.closing() or closure_capability.opening() + ) + end + elseif main == clusters.ClosureControl.types.MainStateEnum.STOPPED or main == nil then + if current == nil then return end + if current == clusters.ClosureControl.types.CurrentPositionEnum.FULLY_CLOSED then + device:emit_event_for_endpoint( + endpoint_id, reverse and closure_capability.open() or closure_capability.closed() + ) + elseif current == clusters.ClosureControl.types.CurrentPositionEnum.FULLY_OPENED or + device:supports_capability_by_id(capabilities.doorControl.ID) then + -- doorControl does not support partially_open; treat any non-fully-closed as open + device:emit_event_for_endpoint( + endpoint_id, reverse and closure_capability.closed() or closure_capability.open() + ) + else + device:emit_event_for_endpoint(endpoint_id, closure_capability.partially_open()) + end + end +end + +--- helper for the switch subscribe override, which adds to a subscribed request for a checked device +--- +--- @param checked_device any a Matter device object, either a parent or child device, so not necessarily the same as device +--- @param subscribe_request table a subscribe request that will be appended to as needed for the device +--- @param capabilities_seen table a list of capabilities that have already been checked by previously handled devices +--- @param attributes_seen table a list of attributes that have already been checked +--- @param subscribed_attributes table key-value pairs mapping capability ids to subscribed attributes +function utils.populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, subscribed_attributes) + for _, component in pairs(checked_device.st_store.profile.components) do + for _, capability in pairs(component.capabilities) do + if not capabilities_seen[capability.id] then + for _, attr in ipairs(subscribed_attributes[capability.id] or {}) do + local cluster_id = attr.cluster or attr._cluster.ID + local attr_id = attr.ID or attr.attribute + if not attributes_seen[cluster_id] or not attributes_seen[cluster_id][attr_id] then + local ib = im.InteractionInfoBlock(nil, cluster_id, attr_id) + subscribe_request:with_info_block(ib) + attributes_seen[cluster_id] = attributes_seen[cluster_id] or {} + attributes_seen[cluster_id][attr_id] = ib + end + end + capabilities_seen[capability.id] = true -- only loop through any capability once + end + end + end +end + +function utils.subscribe(device) + local closure_subscribed_attributes = { + [capabilities.windowShade.ID] = { + clusters.ClosureControl.attributes.MainState, + clusters.ClosureControl.attributes.OverallCurrentState, + clusters.ClosureControl.attributes.OverallTargetState, + }, + [capabilities.doorControl.ID] = { + clusters.ClosureControl.attributes.MainState, + clusters.ClosureControl.attributes.OverallCurrentState, + clusters.ClosureControl.attributes.OverallTargetState, + }, + [capabilities.windowShadeLevel.ID] = { + clusters.ClosureDimension.attributes.CurrentState, + }, + [capabilities.level.ID] = { + clusters.ClosureDimension.attributes.CurrentState, + }, + [capabilities.battery.ID] = { + clusters.PowerSource.attributes.BatPercentRemaining, + }, + [capabilities.batteryLevel.ID] = { + clusters.PowerSource.attributes.BatChargeLevel, + }, + } + + local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {}) + local capabilities_seen, attributes_seen = {}, {} + local additional_attributes = {} + + -- The refresh capability command handler in the lua libs uses this key to determine which attributes to read. + device:set_field(fields.SUBSCRIBED_ATTRIBUTES_KEY, attributes_seen) + + -- If the type of battery support has not yet been determined, add the PowerSource AttributeList to the list of + -- subscribed attributes in order to determine which if any battery capability should be used. + if device:get_field(fields.CLOSURE_BATTERY_SUPPORT) == nil then + local ib = im.InteractionInfoBlock(nil, clusters.PowerSource.ID, clusters.PowerSource.attributes.AttributeList.ID) + subscribe_request:with_info_block(ib) + end + + if device:get_field(fields.CLOSURE_TAG) == nil then + table.insert(additional_attributes, clusters.Descriptor.attributes.TagList) + end + + utils.populate_subscribe_request_for_device( + device, subscribe_request, capabilities_seen, attributes_seen, closure_subscribed_attributes + ) + + for _, attr in ipairs(additional_attributes) do + local cluster_id = attr.cluster or attr._cluster.ID + local attr_id = attr.ID or attr.attribute + if not attributes_seen[cluster_id] or not attributes_seen[cluster_id][attr_id] then + local ib = im.InteractionInfoBlock(nil, cluster_id, attr_id) + subscribe_request:with_info_block(ib) + attributes_seen[cluster_id] = attributes_seen[cluster_id] or {} + attributes_seen[cluster_id][attr_id] = ib + end + end + + if #subscribe_request.info_blocks > 0 then + device:send(subscribe_request) + end +end + +return utils diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/init.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/init.lua new file mode 100644 index 0000000000..45b25cc47a --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/init.lua @@ -0,0 +1,143 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local log = require "log" +local version = require "version" + +if version.api < 20 then + clusters.ClosureControl = require "embedded_clusters.ClosureControl" + clusters.ClosureDimension = require "embedded_clusters.ClosureDimension" +end + +local fields = require "sub_drivers.closure.closure_utils.fields" +local closure_utils = require "sub_drivers.closure.closure_utils.utils" +local attribute_handlers = require "sub_drivers.closure.closure_handlers.attribute_handlers" +local capability_handlers = require "sub_drivers.closure.closure_handlers.capability_handlers" + +-- --------------------------------------------------------------------------- +-- Lifecycle handlers +-- --------------------------------------------------------------------------- + +local ClosureLifecycleHandlers = {} + +function ClosureLifecycleHandlers.device_init(driver, device) + device:set_component_to_endpoint_fn(closure_utils.component_to_endpoint) + device:set_endpoint_to_component_fn(closure_utils.endpoint_to_component) + if device:supports_capability_by_id(capabilities.windowShadePreset.ID) and + device:get_latest_state("main", capabilities.windowShadePreset.ID, + capabilities.windowShadePreset.position.NAME) == nil then + device:emit_event(capabilities.windowShadePreset.supportedCommands( + {"presetPosition", "setPresetPosition"}, {visibility = {displayed = false}} + )) + local preset_position = device:get_field(fields.PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or + fields.DEFAULT_PRESET_LEVEL + device:emit_event(capabilities.windowShadePreset.position( + preset_position, {visibility = {displayed = false}} + )) + device:set_field(fields.PRESET_LEVEL_KEY, preset_position, {persist = true}) + end + device:extend_device("subscribe", closure_utils.subscribe) + device:subscribe() +end + +function ClosureLifecycleHandlers.device_added(driver, device) + if device:supports_capability_by_id(capabilities.windowShade.ID) then + device:emit_event( + capabilities.windowShade.supportedWindowShadeCommands( + {"open", "close", "pause"}, {visibility = {displayed = false}} + ) + ) + end + device:set_field(fields.REVERSE_POLARITY, false, {persist = true}) +end + +function ClosureLifecycleHandlers.do_configure(driver, device) + if #device:get_endpoints(clusters.Descriptor.ID) == 0 then + log.warn( + "Descriptor cluster not implemented on ClosureControl endpoint, " .. + "cannot read TagList to determine closure type" + ) + device:set_field(fields.CLOSURE_TAG, fields.closure_tag_list.NA, {persist = true}) + end + + local battery_feature_eps = device:get_endpoints( + clusters.PowerSource.ID, + {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY} + ) + if #battery_feature_eps == 0 then + device:set_field(fields.CLOSURE_BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist = true}) + closure_utils.match_profile(device) + end +end + +function ClosureLifecycleHandlers.info_changed(driver, device, event, args) + if not closure_utils.deep_equals( + device.profile, args.old_st_store.profile, {ignore_functions = true} + ) then + -- Profile has changed - resubscribe so new capability subscriptions take effect. + device:subscribe() + elseif args.old_st_store.preferences.reverse ~= device.preferences.reverse then + if device.preferences.reverse then + device:set_field(fields.REVERSE_POLARITY, true, {persist = true}) + else + device:set_field(fields.REVERSE_POLARITY, false, {persist = true}) + end + end +end + +-- --------------------------------------------------------------------------- +-- Subdriver template +-- --------------------------------------------------------------------------- + +local closure_handler = { + NAME = "Closure Handler", + + lifecycle_handlers = { + init = ClosureLifecycleHandlers.device_init, + added = ClosureLifecycleHandlers.device_added, + doConfigure = ClosureLifecycleHandlers.do_configure, + infoChanged = ClosureLifecycleHandlers.info_changed, + }, + matter_handlers = { + attr = { + [clusters.ClosureControl.ID] = { + [clusters.ClosureControl.attributes.MainState.ID] = attribute_handlers.main_state_attr_handler, + [clusters.ClosureControl.attributes.OverallCurrentState.ID] = attribute_handlers.overall_current_state_attr_handler, + [clusters.ClosureControl.attributes.OverallTargetState.ID] = attribute_handlers.overall_target_state_attr_handler, + }, + [clusters.ClosureDimension.ID] = { + [clusters.ClosureDimension.attributes.CurrentState.ID] = attribute_handlers.closure_dimension_current_state_handler, + }, + [clusters.Descriptor.ID] = { + [clusters.Descriptor.attributes.TagList.ID] = attribute_handlers.tag_list_handler, + }, + [clusters.PowerSource.ID] = { + [clusters.PowerSource.attributes.AttributeList.ID] = attribute_handlers.power_source_attribute_list_handler, + }, + }, + }, + capability_handlers = { + [capabilities.windowShade.ID] = { + [capabilities.windowShade.commands.close.NAME] = capability_handlers.handle_close, + [capabilities.windowShade.commands.open.NAME] = capability_handlers.handle_open, + [capabilities.windowShade.commands.pause.NAME] = capability_handlers.handle_pause, + }, + [capabilities.doorControl.ID] = { + [capabilities.doorControl.commands.open.NAME] = capability_handlers.handle_open, + [capabilities.doorControl.commands.close.NAME] = capability_handlers.handle_close, + }, + [capabilities.windowShadeLevel.ID] = { + [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = capability_handlers.handle_shade_level, + }, + [capabilities.level.ID] = { + [capabilities.level.commands.setLevel.NAME] = capability_handlers.handle_level, + }, + }, + + can_handle = require("sub_drivers.closure.can_handle"), +} + +return closure_handler diff --git a/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/can_handle.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/can_handle.lua similarity index 72% rename from drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/can_handle.lua rename to drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/can_handle.lua index ba9207f3d2..e3e12116e1 100644 --- a/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/can_handle.lua +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/can_handle.lua @@ -6,11 +6,11 @@ local function is_matter_window_covering_position_updates_while_moving(opts, dri if device.network_type ~= device_lib.NETWORK_TYPE_MATTER then return false end - local FINGERPRINTS = require("matter-window-covering-position-updates-while-moving.fingerprints") + local FINGERPRINTS = require("sub_drivers.matter-window-covering-position-updates-while-moving.fingerprints") for i, v in ipairs(FINGERPRINTS) do if device.manufacturer_info.vendor_id == v[1] and device.manufacturer_info.product_id == v[2] then - return true, require("matter-window-covering-position-updates-while-moving") + return true, require("sub_drivers.matter-window-covering-position-updates-while-moving") end end return false diff --git a/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/fingerprints.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/fingerprints.lua similarity index 100% rename from drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/fingerprints.lua rename to drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/fingerprints.lua diff --git a/drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/init.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/init.lua similarity index 100% rename from drivers/SmartThings/matter-window-covering/src/matter-window-covering-position-updates-while-moving/init.lua rename to drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/init.lua diff --git a/drivers/SmartThings/matter-window-covering/src/test/test_matter_closure.lua b/drivers/SmartThings/matter-window-covering/src/test/test_matter_closure.lua new file mode 100644 index 0000000000..b336ba0f4d --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/test/test_matter_closure.lua @@ -0,0 +1,470 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local uint32 = require "st.matter.data_types.Uint32" +local version = require "version" + +if version.api < 20 then + clusters.ClosureControl = require "embedded_clusters.ClosureControl" + clusters.ClosureDimension = require "embedded_clusters.ClosureDimension" +end + +local mock_device = test.mock_device.build_test_matter_device( + { + label = "Matter Closure", + profile = t_utils.get_profile_definition("covering.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } + }, + { + endpoint_id = 10, + clusters = { + { + cluster_id = clusters.ClosureControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 3, + }, + {cluster_id = clusters.Descriptor.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} + }, + device_types = { + {device_type_id = 0x0230, device_type_revision = 1} -- Closure + } + }, + { + endpoint_id = 11, + clusters = { + { + cluster_id = clusters.ClosureDimension.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0, + }, + }, + device_types = { + {device_type_id = 0x0231, device_type_revision = 1} -- ClosureDimension + } + }, + { + endpoint_id = 12, + clusters = { + { + cluster_id = clusters.ClosureDimension.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0, + }, + }, + device_types = { + {device_type_id = 0x0231, device_type_revision = 1} -- ClosureDimension + } + }, + }, + } +) + +local CLUSTER_SUBSCRIBE_LIST = { + clusters.ClosureControl.attributes.MainState, + clusters.ClosureControl.attributes.OverallCurrentState, + clusters.ClosureControl.attributes.OverallTargetState, +} + +-- additional clusters that will be subscribed to initially but not after the profile is matched. +local ADDITIONAL_SUBSCRIBE_LIST = { + clusters.PowerSource.attributes.AttributeList, + clusters.Descriptor.attributes.TagList, +} + +local function test_init() + test.disable_startup_messages() + 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.windowShade.supportedWindowShadeCommands({"open", "close", "pause"}, + {visibility = {displayed = false}}) + ) + ) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end + end + for _, clus in ipairs(ADDITIONAL_SUBSCRIBE_LIST) do + subscribe_request:merge(clus:subscribe(mock_device)) + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +test.set_test_init_function(test_init) + +local function update_profile() + test.socket.matter:__queue_receive({mock_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data( + mock_device, 10, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)} + )}) + test.socket.matter:__queue_receive({mock_device.id, clusters.Descriptor.attributes.TagList:build_test_report_data( + mock_device, 10, {clusters.Global.types.SemanticTagStruct({mfg_code = 0x00, namespace_id = 0x44, tag = 0x00, name = "Covering"}) } + )}) + mock_device:expect_metadata_update({ + profile = "covering", + optional_component_capabilities = { + {"main", {"battery"}}, + {"windowShade1", {"windowShadeLevel"}}, + {"windowShade2", {"windowShadeLevel"}}, + } + }) + test.wait_for_events() + local updated_device_profile = t_utils.get_profile_definition("covering.yml", { + enabled_optional_capabilities = { + {"main", {"battery"}}, + {"windowShade1", {"windowShadeLevel"}}, + {"windowShade2", {"windowShadeLevel"}}, + } + }) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = updated_device_profile })) + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end + end + subscribe_request:merge(clusters.PowerSource.server.attributes.BatPercentRemaining:subscribe(mock_device)) + subscribe_request:merge(clusters.ClosureDimension.attributes.CurrentState:subscribe(mock_device)) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) +end + +test.register_coroutine_test( + "windowShade closed following MainState and OverallTargetState update", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.MainState:build_test_report_data(mock_device, 10, clusters.ClosureControl.types.MainStateEnum.MOVING), + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.OverallTargetState:build_test_report_data(mock_device, 10, + clusters.ClosureControl.types.OverallTargetStateStruct({ + position = clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_CLOSED, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.MEDIUM + })) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closing()) + ) + end +) + +test.register_coroutine_test( + "windowShade opening following MainState and OverallTargetState update", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.MainState:build_test_report_data(mock_device, 10, clusters.ClosureControl.types.MainStateEnum.MOVING), + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.OverallTargetState:build_test_report_data(mock_device, 10, + clusters.ClosureControl.types.OverallTargetStateStruct({ + position = clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_OPEN, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.MEDIUM + })) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + end +) + +test.register_coroutine_test( + "windowShade closed following OverallCurrentState FULLY_CLOSED", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.OverallCurrentState:build_test_report_data(mock_device, 10, + clusters.ClosureControl.types.OverallCurrentStateStruct({ + position = clusters.ClosureControl.types.CurrentPositionEnum.FULLY_CLOSED, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO, + secure_state = false + })) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + ) + end +) + +test.register_coroutine_test( + "windowShade open following OverallCurrentState FULLY_OPENED", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.OverallCurrentState:build_test_report_data(mock_device, 10, + clusters.ClosureControl.types.OverallCurrentStateStruct({ + position = clusters.ClosureControl.types.CurrentPositionEnum.FULLY_OPENED, + latch = true, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO, + secure_state = false + })) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.open()) + ) + end +) + +test.register_coroutine_test( + "windowShade partially_open following OverallCurrentState PARTIALLY_OPENED", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.OverallCurrentState:build_test_report_data(mock_device, 10, + clusters.ClosureControl.types.OverallCurrentStateStruct({ + position = clusters.ClosureControl.types.CurrentPositionEnum.PARTIALLY_OPENED, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO, + secure_state = false + })) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + end +) + +test.register_coroutine_test( + "windowShade state transitions from closing to closed", function() + update_profile() + test.wait_for_events() + -- device starts moving toward closed + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.MainState:build_test_report_data(mock_device, 10, clusters.ClosureControl.types.MainStateEnum.MOVING), + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.OverallTargetState:build_test_report_data(mock_device, 10, + clusters.ClosureControl.types.OverallTargetStateStruct({ + position = clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_CLOSED, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.MEDIUM + })) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closing()) + ) + test.wait_for_events() + -- device stops and reports fully closed + -- MainState STOPPED with no current position cached yet. no capability event emitted + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.MainState:build_test_report_data(mock_device, 10, clusters.ClosureControl.types.MainStateEnum.STOPPED), + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureControl.attributes.OverallCurrentState:build_test_report_data(mock_device, 10, + clusters.ClosureControl.types.OverallCurrentStateStruct({ + position = clusters.ClosureControl.types.CurrentPositionEnum.FULLY_CLOSED, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO, + secure_state = false + })) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + ) + end +) + +test.register_coroutine_test( + "windowShade close command sends ClosureControl MoveTo FULLY_CLOSED", function() + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShade", component = "main", command = "close", args = {}}, + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ClosureControl.server.commands.MoveTo( + mock_device, 10, clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_CLOSED + ) + }) + end +) + +test.register_coroutine_test( + "windowShade open command sends ClosureControl MoveTo FULLY_OPEN", function() + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShade", component = "main", command = "open", args = {}}, + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ClosureControl.server.commands.MoveTo( + mock_device, 10, clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_OPEN + ) + }) + end +) + +test.register_coroutine_test( + "windowShade pause command sends ClosureControl Stop", function() + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShade", component = "main", command = "pause", args = {}}, + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ClosureControl.server.commands.Stop(mock_device, 10) + }) + end +) + +test.register_coroutine_test( + "Battery percentage reported correctly for closure device", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data(mock_device, 10, 150) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5))) + ) + end +) + +test.register_coroutine_test( + "setShadeLevel on windowShade1 sends SetTarget to endpoint 11", function() + update_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShadeLevel", component = "windowShade1", command = "setShadeLevel", args = {75}}, + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ClosureDimension.server.commands.SetTarget(mock_device, 11, 75 * 100) + }) + end +) + +test.register_coroutine_test( + "setShadeLevel on windowShade2 sends SetTarget to endpoint 12", function() + update_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShadeLevel", component = "windowShade2", command = "setShadeLevel", args = {40}}, + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ClosureDimension.server.commands.SetTarget(mock_device, 12, 40 * 100) + }) + end +) + +test.register_coroutine_test( + "ClosureDimension CurrentState on endpoint 11 emits shadeLevel on windowShade1", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureDimension.attributes.CurrentState:build_test_report_data(mock_device, 11, + clusters.ClosureDimension.types.DimensionStateStruct({ + position = 6000, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO + }) + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("windowShade1", capabilities.windowShadeLevel.shadeLevel(60)) + ) + end +) + +test.register_coroutine_test( + "ClosureDimension CurrentState on endpoint 12 emits shadeLevel on windowShade2", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureDimension.attributes.CurrentState:build_test_report_data(mock_device, 12, + clusters.ClosureDimension.types.DimensionStateStruct({ + position = 2500, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO + }) + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("windowShade2", capabilities.windowShadeLevel.shadeLevel(25)) + ) + end +) + +test.register_coroutine_test( + "ClosureDimension CurrentState with closed position emits shadeLevel 0", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureDimension.attributes.CurrentState:build_test_report_data(mock_device, 11, + clusters.ClosureDimension.types.DimensionStateStruct({ + position = 0, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO + }) + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("windowShade1", capabilities.windowShadeLevel.shadeLevel(0)) + ) + end +) + +test.register_coroutine_test( + "ClosureDimension CurrentState with full-open position emits shadeLevel 100", function() + update_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ClosureDimension.attributes.CurrentState:build_test_report_data(mock_device, 11, + clusters.ClosureDimension.types.DimensionStateStruct({ + position = 10000, + latch = false, + speed = clusters.Global.types.ThreeLevelAutoEnum.AUTO + }) + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("windowShade1", capabilities.windowShadeLevel.shadeLevel(100)) + ) + end +) + +test.run_registered_tests()