Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions drivers/SmartThings/matter-thermostat/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ function ThermostatLifecycleHandlers.device_init(driver, device)
device:subscribe()
device:set_component_to_endpoint_fn(thermostat_utils.component_to_endpoint)
device:set_endpoint_to_component_fn(thermostat_utils.endpoint_to_component)
thermostat_utils.handle_thermostat_operating_state_info(device)
Copy link
Contributor

@nickolas-deboom nickolas-deboom Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this running in init could falsely cause an offline device to be marked online since it emits a capability. doConfigure might be a better spot for this call (infoChanged as well in case to update the capability if the profile changes)?
Edit: thinking about it again, I'm not sure if this is true for a driver-scoped lifecycle event.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did add it to info_changed. I also put it in init because I want already existing devices to be udpated. Finally, I believe the {visibility = {displayed = false}} option should fix the issue you're describing? Or am I misremembering what that does?

Copy link
Contributor

@nickolas-deboom nickolas-deboom Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit 2: I confirmed that this does indeed cause offline devices to be marked online

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I edited the conditional to only set this if it has not been set before. Effectively, that will only do the setting once, on startup. Else, it will do this once for existing devices (and may make offline devices appear online in-app, once)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect, thanks!

if not device:get_field(fields.setpoint_limit_device_field.MIN_SETPOINT_DEADBAND_CHECKED) then
local auto_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE})
--Query min setpoint deadband if needed
Expand All @@ -100,14 +101,11 @@ function ThermostatLifecycleHandlers.device_init(driver, device)

-- device energy reporting must be handled cumulatively, periodically, or by both simulatanously.
-- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported.
local electrical_energy_measurement_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID)
if #electrical_energy_measurement_eps > 0 then
local cumulative_energy_eps = embedded_cluster_utils.get_endpoints(
if #embedded_cluster_utils.get_endpoints(
device,
clusters.ElectricalEnergyMeasurement.ID,
{feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY}
)
if #cumulative_energy_eps == 0 then device:set_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = false}) end
) == 0 then device:set_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = false})
end
end

Expand All @@ -119,6 +117,7 @@ function ThermostatLifecycleHandlers.info_changed(driver, device, event, args)
end

if device.profile.id ~= args.old_st_store.profile.id then
thermostat_utils.handle_thermostat_operating_state_info(device)
device:subscribe()
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ local mock_device = test.mock_device.build_test_matter_device({
{cluster_id = clusters.Basic.ID, cluster_type = "SERVER"},
},
device_types = {
device_type_id = 0x0016, device_type_revision = 1, -- RootNode
{ device_type_id = 0x0016, device_type_revision = 1 } -- RootNode
}
},
{
Expand All @@ -32,6 +32,9 @@ local mock_device = test.mock_device.build_test_matter_device({
{cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 0},
{cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"},
{cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"},
},
device_types = {
{ device_type_id = 0x0072, device_type_revision = 1 } -- Room Air Conditioner
}
}
}
Expand Down Expand Up @@ -153,6 +156,9 @@ local function test_init()
end
end
end
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle"}, {visibility = {displayed = false}}))
)
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
test.mock_device.add_test_device(mock_device)
end
Expand Down Expand Up @@ -211,6 +217,9 @@ local function test_init_configure()
end
end
end
test.socket.capability:__expect_send(
mock_device_configure:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}}))
)
test.socket.matter:__expect_send({mock_device_configure.id, subscribe_request})

local read_setpoint_deadband = clusters.Thermostat.attributes.MinSetpointDeadBand:read()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ local function initialize_mock_device(generic_mock_device, generic_subscribed_at
end
end
end
test.socket.capability:__expect_send(
generic_mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}}))
)
test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request})
return subscribe_request
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
local test = require "integration_test"
local t_utils = require "integration_test.utils"
local clusters = require "st.matter.clusters"
local capabilities = require "st.capabilities"
local uint32 = require "st.matter.data_types.Uint32"

test.set_rpc_version(7)
Expand Down Expand Up @@ -69,6 +70,9 @@ local function test_init()
end

test.socket.matter:__expect_send({mock_device.id, subscribe_request})
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "cooling"}, {visibility = {displayed = false}}))
)
test.mock_device.add_test_device(mock_device)

test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ local clusters = require "st.matter.clusters"
test.set_rpc_version(7)

local mock_device = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("thermostat-humidity-fan.yml"),
profile = t_utils.get_profile_definition("thermostat-humidity-fan-nostate.yml"),
manufacturer_info = {
vendor_id = 0x0000,
product_id = 0x0000,
Expand Down Expand Up @@ -46,7 +46,7 @@ local mock_device = test.mock_device.build_test_matter_device({
})

local mock_device_simple = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("thermostat.yml"),
profile = t_utils.get_profile_definition("thermostat-nostate.yml"),
manufacturer_info = {
vendor_id = 0x0000,
product_id = 0x0000,
Expand Down Expand Up @@ -81,7 +81,7 @@ local mock_device_simple = test.mock_device.build_test_matter_device({
})

local mock_device_no_battery = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("thermostat.yml"),
profile = t_utils.get_profile_definition("thermostat-nostate.yml"),
manufacturer_info = {
vendor_id = 0x0000,
product_id = 0x0000,
Expand Down Expand Up @@ -123,7 +123,6 @@ local cluster_subscribe_list = {
clusters.Thermostat.attributes.AbsMinHeatSetpointLimit,
clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit,
clusters.Thermostat.attributes.SystemMode,
clusters.Thermostat.attributes.ThermostatRunningState,
clusters.Thermostat.attributes.ControlSequenceOfOperation,
clusters.TemperatureMeasurement.attributes.MeasuredValue,
clusters.TemperatureMeasurement.attributes.MinMeasuredValue,
Expand All @@ -142,7 +141,6 @@ local cluster_subscribe_list_simple = {
clusters.Thermostat.attributes.AbsMinHeatSetpointLimit,
clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit,
clusters.Thermostat.attributes.SystemMode,
clusters.Thermostat.attributes.ThermostatRunningState,
clusters.Thermostat.attributes.ControlSequenceOfOperation,
clusters.TemperatureMeasurement.attributes.MeasuredValue,
clusters.TemperatureMeasurement.attributes.MinMeasuredValue,
Expand All @@ -158,7 +156,6 @@ local cluster_subscribe_list_no_battery = {
clusters.Thermostat.attributes.AbsMinHeatSetpointLimit,
clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit,
clusters.Thermostat.attributes.SystemMode,
clusters.Thermostat.attributes.ThermostatRunningState,
clusters.Thermostat.attributes.ControlSequenceOfOperation,
clusters.TemperatureMeasurement.attributes.MeasuredValue,
clusters.TemperatureMeasurement.attributes.MinMeasuredValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ local function test_init()
test.socket.matter:__expect_send({mock_device.id, read_req})

test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" })
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}}))
)
test.socket.matter:__expect_send({mock_device.id, get_subscribe_request(mock_device, cluster_subscribe_list)})
end
test.set_test_init_function(test_init)
Expand All @@ -167,6 +170,9 @@ local function test_init_disorder_endpoints()
test.socket.matter:__expect_send({mock_device_disorder_endpoints.id, read_req})

test.socket.device_lifecycle:__queue_receive({ mock_device_disorder_endpoints.id, "init" })
test.socket.capability:__expect_send(
mock_device_disorder_endpoints:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}}))
)
test.socket.matter:__expect_send({mock_device_disorder_endpoints.id, get_subscribe_request(
mock_device_disorder_endpoints, cluster_subscribe_list)})
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ local function test_init()
subscribe_request:merge(cluster:subscribe(mock_device))
end
end
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}}))
)
test.socket.matter:__expect_send({mock_device.id, subscribe_request})

local read_setpoint_deadband = clusters.Thermostat.attributes.MinSetpointDeadBand:read()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
local test = require "integration_test"
local t_utils = require "integration_test.utils"

local capabilities = require "st.capabilities"
local clusters = require "st.matter.clusters"

local mock_device = test.mock_device.build_test_matter_device({
Expand Down Expand Up @@ -33,6 +34,9 @@ local mock_device = test.mock_device.build_test_matter_device({
},
{cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY},
{cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "BOTH"},
},
device_types = {
{ device_type_id = 0x0301, device_type_revision = 1 } -- Thermostat
}
}
}
Expand Down Expand Up @@ -62,6 +66,9 @@ local function test_init()
subscribe_request:merge(cluster:subscribe(mock_device))
end
end
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}}))
)
test.socket.matter:__expect_send({mock_device.id, subscribe_request})

local read_setpoint_deadband = clusters.Thermostat.attributes.MinSetpointDeadBand:read()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ local mock_device = test.mock_device.build_test_matter_device({
{cluster_id = clusters.Basic.ID, cluster_type = "SERVER"},
},
device_types = {
device_type_id = 0x0016, device_type_revision = 1, -- RootNode
{ device_type_id = 0x0016, device_type_revision = 1 } -- RootNode
}
},
{
Expand All @@ -36,6 +36,9 @@ local mock_device = test.mock_device.build_test_matter_device({
{cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"},
{cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"},
{cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"},
},
device_types = {
{ device_type_id = 0x0301, device_type_revision = 1 } -- Thermostat
}
}
}
Expand Down Expand Up @@ -70,6 +73,9 @@ local mock_device_auto = test.mock_device.build_test_matter_device({
{cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"},
{cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"},
{cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"},
},
device_types = {
{ device_type_id = 0x0301, device_type_revision = 1 } -- Thermostat
}
}
}
Expand Down Expand Up @@ -103,7 +109,10 @@ local function test_init()
end
end
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
test.mock_device.add_test_device(mock_device)
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}}))
)
test.mock_device.add_test_device(mock_device)
end
test.set_test_init_function(test_init)

Expand Down Expand Up @@ -135,6 +144,9 @@ local function test_init_auto()
end
end
test.socket.matter:__expect_send({mock_device_auto.id, subscribe_request})
test.socket.capability:__expect_send(
mock_device_auto:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}}))
)
test.socket.matter:__expect_send({mock_device_auto.id, clusters.Thermostat.attributes.MinSetpointDeadBand:read(mock_device_auto)})
test.mock_device.add_test_device(mock_device_auto)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ local mock_device = test.mock_device.build_test_matter_device({
{ cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER" },
{ cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER" },
{ cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER" },
},
device_types = {
{ device_type_id = 0x0301, device_type_revision = 1 } -- Thermostat
}
}
}
Expand Down Expand Up @@ -67,6 +70,9 @@ local function test_init()
subscribe_request:merge(cluster:subscribe(mock_device))
end
end
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}}))
)
test.socket.matter:__expect_send({ mock_device.id, subscribe_request })
test.mock_device.add_test_device(mock_device)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
-- Licensed under the Apache License, Version 2.0

local test = require "integration_test"
local capabilities = require "st.capabilities"
local t_utils = require "integration_test.utils"
local clusters = require "st.matter.clusters"
local im = require "st.matter.interaction_model"
Expand Down Expand Up @@ -95,6 +96,9 @@ local function test_init()
test.socket.matter:__expect_send({ mock_device_basic.id, read_request })

test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "init" })
test.socket.capability:__expect_send(
mock_device_basic:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}}))
)
subscribe_request_basic = initialize_mock_device(mock_device_basic, subscribed_attributes)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
-- Licensed under the Apache License, Version 2.0

local clusters = require "st.matter.clusters"
local capabilities = require "st.capabilities"
local test = require "integration_test"
local t_utils = require "integration_test.utils"
local utils = require "st.utils"
Expand Down Expand Up @@ -39,6 +40,9 @@ local mock_device = test.mock_device.build_test_matter_device({
{cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"},
{cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"},
{cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"},
},
device_types = {
{ device_type_id = 0x0301, device_type_revision = 1 } -- Thermostat
}
}
}
Expand Down Expand Up @@ -70,6 +74,9 @@ local function test_init()
subscribe_request:merge(cluster:subscribe(mock_device))
end
end
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}}))
)
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
test.mock_device.add_test_device(mock_device)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@

local log = require "log"
local capabilities = require "st.capabilities"
local embedded_cluster_utils = require "thermostat_utils.embedded_cluster_utils"
local clusters = require "st.matter.clusters"
local fields = require "thermostat_utils.fields"
local embedded_cluster_utils = require "thermostat_utils.embedded_cluster_utils"

local ThermostatUtils = {}

Expand Down Expand Up @@ -85,6 +86,25 @@ function ThermostatUtils.get_endpoints_by_device_type(device, device_type)
return endpoints
end

-- set the supportedThermostatOperatingStates attribute if the thermostatOperatingState capability is supported and it has not been set before
function ThermostatUtils.handle_thermostat_operating_state_info(device)
local thermostat_operating_state_supported = device:supports_capability(capabilities.thermostatOperatingState)
local latest_supported_operating_states = thermostat_operating_state_supported and device:get_latest_state(
"main", capabilities.thermostatOperatingState.ID, capabilities.thermostatOperatingState.supportedThermostatOperatingStates.NAME
)
if thermostat_operating_state_supported and latest_supported_operating_states == nil then
local supported_operating_modes = { "idle" }
if #device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) > 0 then
table.insert(supported_operating_modes, "heating")
end
if #device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) > 0 then
table.insert(supported_operating_modes, "cooling")
end
local thermostat_ep_id = device:get_endpoints(clusters.Thermostat.ID)[1]
device:emit_event_for_endpoint(thermostat_ep_id, capabilities.thermostatOperatingState.supportedThermostatOperatingStates(supported_operating_modes, {visibility = {displayed = false}}))
end
end

function ThermostatUtils.get_device_type(device)
-- For cases where a device has multiple device types, this list indicates which
-- device type will be the "main" device type for purposes of selecting a profile
Expand Down
Loading