diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua index a4015800a2..cf15b9c9a0 100644 --- a/drivers/SmartThings/matter-thermostat/src/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/init.lua @@ -120,6 +120,7 @@ local function do_configure(driver, device) local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) local battery_eps = device:get_endpoints(clusters.PowerSource.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 @@ -139,15 +140,15 @@ 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 - -- Add nobattery profiles if updated + -- Note that Thermostat.AttributeList report will correct this if the optional cluster + -- elements are present profile_name = profile_name .. "-nostate" - if #battery_eps == 0 then profile_name = profile_name .. "-nobattery" end + device:set_field("profile_name", profile_name) + device:send(clusters.Thermostat.attributes.AttributeList: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 @@ -212,6 +213,18 @@ local function system_mode_handler(driver, device, ib, response) end end +local function attr_list_handler(driver, device, ib, response) + 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:set_field("profile_name", new_profile) + 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 @@ -415,6 +428,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, + [clusters.Thermostat.attributes.AttributeList.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 3e408a16f4..6ce5286018 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 @@ -14,7 +14,7 @@ local test = require "integration_test" local t_utils = require "integration_test.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({ @@ -179,43 +179,78 @@ 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() - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({})) + 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 on doConfigure lifecycle event due to cluster feature map", + "Profile change due to Thermostat attribute list", 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(0x29)}), + }) + mock_device_simple:expect_metadata_update({ profile = "thermostat-cooling-only" }) +end +) + +test.register_coroutine_test( + "Profile change due to Thermostat attribute list no battery", + function() + configure(mock_device_no_battery, false) + mock_device_no_battery:expect_metadata_update({ profile = "thermostat-cooling-only-nostate-nobattery" }) + mock_device_no_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device_no_battery.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(mock_device_no_battery, 1, {Uint32(0x29)}), + }) + mock_device_no_battery:expect_metadata_update({ profile = "thermostat-cooling-only-nobattery" }) end ) @@ -224,6 +259,10 @@ test.register_coroutine_test( function() test.socket.device_lifecycle:__queue_receive({ mock_device_no_battery.id, "doConfigure" }) local read_limits = clusters.Thermostat.attributes.AbsMinCoolSetpointLimit:read() + test.socket.matter:__expect_send({ + mock_device_no_battery.id, + clusters.Thermostat.attributes.AttributeList:read(mock_device_no_battery, 1) + }) read_limits:merge(clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit:read()) test.socket.matter:__expect_send({mock_device_no_battery.id, read_limits}) mock_device_no_battery:expect_metadata_update({ profile = "thermostat-cooling-only-nostate-nobattery" }) 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 f5f04b574a..9e3533e707 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 @@ -75,13 +75,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() @@ -122,6 +128,7 @@ local function configure(device) test.socket.capability:__expect_send( device:generate_test_message("main", cached_cooling_setpoint) ) + test.wait_for_events() end 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 96f2a757ea..0d789f2b42 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,7 @@ -- limitations under the License. local test = require "integration_test" + local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local utils = require "st.utils"