diff --git a/drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua b/drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua index 10c34d8049..34cd83fe6c 100644 --- a/drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua +++ b/drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua @@ -33,10 +33,6 @@ local mock_device = test.mock_device.build_test_matter_device({ cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, --u32 bitmap - attributes = nil, -- attribute id list - server_commands = nil, --server cmd id list - client_commands = nil, --client cmd id list - events = nil, --event id list }, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} } diff --git a/drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua b/drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua index 6f26e84cc5..6c5ffa7901 100644 --- a/drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua +++ b/drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua @@ -33,10 +33,6 @@ local mock_device = test.mock_device.build_test_matter_device({ cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, --u32 bitmap - attributes = nil, -- attribute id list - server_commands = nil, --server cmd id list - client_commands = nil, --client cmd id list - events = nil, --event id list }, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"}, {cluster_id = clusters.MediaPlayback.ID, cluster_type = "SERVER", feature_map = 0x0}, diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua index 988f10f575..4b7ef4d6f3 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -32,10 +32,6 @@ local mock_device = test.mock_device.build_test_matter_device({ cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, --u32 bitmap - attributes = nil, -- attribute id list - server_commands = nil, --server cmd id list - client_commands = nil, --client cmd id list - events = nil, --event id list }, {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 31}, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} diff --git a/drivers/SmartThings/matter-thermostat/src/AttributeList.lua b/drivers/SmartThings/matter-thermostat/src/AttributeList.lua new file mode 100644 index 0000000000..9b8e48f7cb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/AttributeList.lua @@ -0,0 +1,119 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + + +--Note this file is present in the driver package in case it is not available in the lua libs on +-- the hub. It will be required for any API version <= 2. It will be removed by SmartThings once +-- all hub platforms running the driver support an API version greater than 2 +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +--- @class st.matter.clusters.Thermostat.AttributeList +--- @alias AttributeList +--- +--- @field public ID number 0xFFFB the ID of this attribute +--- @field public NAME string "AttributeList" the name of this attribute +--- @field public data_type st.matter.data_types.Array the data type of this attribute + +local AttributeList = { + ID = 0xFFFB, + NAME = "AttributeList", + base_type = data_types.Array, +} +--- Create a Array object of this attribute with any additional features provided for the attribute +--- This is also usable with the AttributeList(...) syntax +--- +--- @vararg vararg the values needed to construct a Array +--- @return st.matter.data_types.Array +function AttributeList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +--- Constructs an st.matter.interaction_model.InteractionRequest to read +--- this attribute from a device +--- @param device st.matter.Device +--- @param endpoint_id number|nil +--- @return st.matter.interaction_model.InteractionRequest containing an Interaction Request +function AttributeList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + + +--- Reporting policy: AttributeList => true => mandatory + +--- Sets up a Subscribe Interaction +--- +--- @param device any +--- @param endpoint_id number|nil +--- @return any +function AttributeList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AttributeList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +--- Builds an AttributeList test attribute reponse for the driver integration testing framework +--- +--- @param device st.matter.Device the device to build this message for +--- @param endpoint_id number|nil +--- @param value any +--- @param status string Interaction status associated with the path +--- @return st.matter.interaction_model.InteractionResponse of type REPORT_DATA +function AttributeList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function AttributeList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AttributeList, {__call = AttributeList.new_value}) +return AttributeList + diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua index fe21598ec2..af9682af55 100644 --- a/drivers/SmartThings/matter-thermostat/src/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/init.lua @@ -17,9 +17,20 @@ local log = require "log" local clusters = require "st.matter.clusters" local im = require "st.matter.interaction_model" +local version = require "version" local MatterDriver = require "st.matter.driver" local utils = require "st.utils" +--TODO should we remove this or not? With or without it is blocked on hub FW 0.45 +local ThermostatClusterAttributeList +if version.api < 3 then + ThermostatClusterAttributeList = require "AttributeList" + ThermostatClusterAttributeList:set_parent_cluster(clusters.Thermostat) +else + ThermostatClusterAttributeList = clusters.Thermostat.attributes.AttributeList +end + + local THERMOSTAT_MODE_MAP = { [clusters.Thermostat.types.ThermostatSystemMode.OFF] = capabilities.thermostatMode.thermostatMode.off, [clusters.Thermostat.types.ThermostatSystemMode.AUTO] = capabilities.thermostatMode.thermostatMode.auto, @@ -55,6 +66,7 @@ local function do_configure(driver, device) local fan_eps = device:get_endpoints(clusters.FanControl.ID) local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) local profile_name = "thermostat" + --Note: we have not encountered thermostats with multiple endpoints that support the Thermostat cluster if #thermo_eps == 1 then if #humidity_eps > 0 and #fan_eps > 0 then @@ -74,10 +86,9 @@ local function do_configure(driver, device) profile_name = profile_name .. "-cooling-only" end - -- TODO remove this in favor of reading Thermostat clusters AttributeList attribute - -- to determine support for ThermostatRunningState profile_name = profile_name .. "-nostate" - + device:set_field("profile_name", profile_name) + device:send(ThermostatClusterAttributeList:read(device, thermo_eps[1])) log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) device:try_update_metadata({profile = profile_name}) else @@ -142,6 +153,18 @@ local function system_mode_handler(driver, device, ib, response) end end +local function attr_list_handler(driver, device, ib, response) + local utils = require "st.utils" + for _, attr_id in ipairs (ib.data.elements or {}) do + if attr_id.value == clusters.Thermostat.attributes.ThermostatRunningState.ID then + local new_profile = string.gsub(device:get_field("profile_name"), "-nostate", "") + device.log.info(string.format("Updating device profile to %s.", new_profile)) + device:try_update_metadata({ profile = new_profile }) + return + end + end +end + local function running_state_handler(driver, device, ib, response) for mode, operating_state in pairs(THERMOSTAT_OPERATING_MODE_MAP) do if ((ib.data.value >> mode) & 1) > 0 then @@ -346,6 +369,7 @@ local matter_driver_template = { [clusters.Thermostat.attributes.AbsMinCoolSetpointLimit.ID] = setpoint_limit_handler(setpoint_limit_device_field.MIN_COOL), [clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit.ID] = setpoint_limit_handler(setpoint_limit_device_field.MAX_COOL), [clusters.Thermostat.attributes.MinSetpointDeadBand.ID] = min_deadband_limit_handler, + [ThermostatClusterAttributeList.ID] = attr_list_handler }, [clusters.FanControl.ID] = { [clusters.FanControl.attributes.FanModeSequence.ID] = fan_mode_sequence_handler, diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_featuremap.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_featuremap.lua index ad69f6be82..10ffdc0f0a 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_featuremap.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_featuremap.lua @@ -16,7 +16,7 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local utils = require "st.utils" - +local Uint32 = require "st.matter.data_types".Uint32 local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -32,26 +32,9 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER"}, { cluster_id = clusters.Thermostat.ID, - attributes={ - 0, - 18, - 26, - 27, - 28, - 65528, - 65529, - 65531, - 65532, - 65533, - }, - client_commands={ - 0, - }, cluster_revision=5, cluster_type="SERVER", - events={}, feature_map=1, -- Heat feature only. - server_commands={}, }, {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"} @@ -72,26 +55,10 @@ local mock_device_simple = test.mock_device.build_test_matter_device({ clusters = { { cluster_id = clusters.Thermostat.ID, - attributes={ - 0, - 18, - 26, - 27, - 28, - 65528, - 65529, - 65531, - 65532, - 65533, - }, - client_commands={ - 0, - }, cluster_revision=5, cluster_type="SERVER", events={}, feature_map=2, -- Cool feature only. - server_commands={}, }, {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, } @@ -144,28 +111,63 @@ local function test_init() end test.set_test_init_function(test_init) +local function configure(device, is_heat) + test.socket.device_lifecycle:__queue_receive({ device.id, "doConfigure" }) + local read_limits + if is_heat then + read_limits = clusters.Thermostat.attributes.AbsMinHeatSetpointLimit:read() + read_limits:merge(clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit:read()) + else + read_limits = clusters.Thermostat.attributes.AbsMinCoolSetpointLimit:read() + read_limits:merge(clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit:read()) + end + test.socket.matter:__expect_send({device.id, read_limits}) + test.socket.matter:__expect_send({ + device.id, + clusters.Thermostat.attributes.AttributeList:read(device, 1) + }) +end + test.register_coroutine_test( - "Profile change on doConfigure lifecycle event due to cluster feature map", + "Profile change on doConfigure lifecycle event due to cluster heating feature map", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - local read_limits = clusters.Thermostat.attributes.AbsMinHeatSetpointLimit:read() - read_limits:merge(clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit:read()) - test.socket.matter:__expect_send({mock_device.id, read_limits}) - --TODO why does provisiong state get added in the do configure event handle, but not the refres? + configure(mock_device, true) mock_device:expect_metadata_update({ profile = "thermostat-humidity-fan-heating-only-nostate" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device_simple.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(mock_device_simple, 1, {Uint32(0x2)}), + }) end ) test.register_coroutine_test( - "Profile change on doConfigure lifecycle event due to cluster feature map", + "Profile change on doConfigure lifecycle event due to cluster cooling feature map", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_simple.id, "doConfigure" }) - local read_limits = clusters.Thermostat.attributes.AbsMinCoolSetpointLimit:read() - read_limits:merge(clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit:read()) - test.socket.matter:__expect_send({mock_device_simple.id, read_limits}) + configure(mock_device_simple, false) + mock_device_simple:expect_metadata_update({ profile = "thermostat-cooling-only-nostate" }) + mock_device_simple:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device_simple.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(mock_device_simple, 1, {Uint32(0x1)}), + }) +end +) + +test.register_coroutine_test( + "Profile change due to Thermostat attribute list", + function() + configure(mock_device_simple, false) mock_device_simple:expect_metadata_update({ profile = "thermostat-cooling-only-nostate" }) mock_device_simple:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device_simple.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(mock_device_simple, 1, {Uint32(0x29)}), + }) + mock_device_simple:expect_metadata_update({ profile = "thermostat-cooling-only" }) end ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits.lua index 23656a36a7..285579eda5 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits.lua @@ -65,13 +65,19 @@ local cached_heating_setpoint = capabilities.thermostatHeatingSetpoint.heatingSe local cached_cooling_setpoint = capabilities.thermostatCoolingSetpoint.coolingSetpoint({ value = 26.67, unit = "C" }) local function configure(device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.device_lifecycle:__queue_receive({ device.id, "doConfigure" }) local read_limits = clusters.Thermostat.attributes.AbsMinHeatSetpointLimit:read() read_limits:merge(clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit:read()) read_limits:merge(clusters.Thermostat.attributes.AbsMinCoolSetpointLimit:read()) read_limits:merge(clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit:read()) read_limits:merge(clusters.Thermostat.attributes.MinSetpointDeadBand:read()) test.socket.matter:__expect_send({device.id, read_limits}) + test.socket.matter:__expect_send({ + device.id, + clusters.Thermostat.attributes.AttributeList:read(device, 1) + }) + + --Note nostate profile updates only happen when we receive and process an attribute list report mock_device:expect_metadata_update({ profile = "thermostat-nostate" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.wait_for_events() diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua index 28d2feada1..3ff65e9fcb 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua @@ -13,6 +13,9 @@ -- limitations under the License. local test = require "integration_test" +--TODO remove this hack once "integration_test" has a mock for it +-- This is coming in Lua libs version 046x +function rpc_version() return 0 end local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local utils = require "st.utils" @@ -32,26 +35,9 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER"}, { cluster_id = clusters.Thermostat.ID, - attributes={ - 0, - 18, - 26, - 27, - 28, - 65528, - 65529, - 65531, - 65532, - 65533, - }, - client_commands={ - 0, - }, cluster_revision=5, cluster_type="SERVER", - events={}, feature_map=3, -- Heat and Cool features - server_commands={}, }, {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, diff --git a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua index 6320e1c21a..8d166f9dbe 100644 --- a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua +++ b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua @@ -33,10 +33,6 @@ local mock_device = test.mock_device.build_test_matter_device( cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, - attributes = nil, - server_commands = nil, - client_commands = nil, - event = nil, }, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"}, {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}