Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
-- Copyright © 2026 SmartThings, Inc.
-- 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 mock_lennox_device = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("thermostat-humidity-fan.yml"),
manufacturer_info = {
vendor_id = 0x1356,
product_id = 0x0001,
},
endpoints = {
{
endpoint_id = 0,
clusters = {
{ cluster_id = clusters.Basic.ID, cluster_type = "SERVER" },
},
device_types = {
{ device_type_id = 0x0016, device_type_revision = 1 } -- RootNode
}
},
{
endpoint_id = 1,
clusters = {
{
cluster_id = clusters.Thermostat.ID,
cluster_revision = 5,
cluster_type = "SERVER",
feature_map = 35, -- Heat, Cool, and Auto features.
},
{ 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
}
}
}
})

local function test_init()
local cluster_subscribe_list = {
clusters.Thermostat.attributes.LocalTemperature,
clusters.Thermostat.attributes.OccupiedCoolingSetpoint,
clusters.Thermostat.attributes.OccupiedHeatingSetpoint,
clusters.Thermostat.attributes.AbsMinCoolSetpointLimit,
clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit,
clusters.Thermostat.attributes.AbsMinHeatSetpointLimit,
clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit,
clusters.Thermostat.attributes.SystemMode,
clusters.Thermostat.attributes.ThermostatRunningState,
clusters.Thermostat.attributes.ControlSequenceOfOperation,
clusters.PowerSource.attributes.BatPercentRemaining,
clusters.TemperatureMeasurement.attributes.MeasuredValue,
clusters.TemperatureMeasurement.attributes.MinMeasuredValue,
clusters.TemperatureMeasurement.attributes.MaxMeasuredValue,
}
local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_lennox_device)
for i, cluster in ipairs(cluster_subscribe_list) do
if i > 1 then
subscribe_request:merge(cluster:subscribe(mock_lennox_device))
end
end
test.socket.capability:__expect_send(
mock_lennox_device:generate_test_message("main",
capabilities.thermostatOperatingState.supportedThermostatOperatingStates({ "idle", "heating", "cooling" },
{ visibility = { displayed = false } }))
)
test.socket.matter:__expect_send({ mock_lennox_device.id, subscribe_request })

local read_setpoint_deadband = clusters.Thermostat.attributes.MinSetpointDeadBand:read()
test.socket.matter:__expect_send({ mock_lennox_device.id, read_setpoint_deadband })

test.mock_device.add_test_device(mock_lennox_device)

test.socket.device_lifecycle:__queue_receive({ mock_lennox_device.id, "added" })
local read_req = clusters.Thermostat.attributes.ControlSequenceOfOperation:read()
read_req:merge(clusters.FanControl.attributes.FanModeSequence:read())
read_req:merge(clusters.FanControl.attributes.WindSupport:read())
read_req:merge(clusters.FanControl.attributes.RockSupport:read())
read_req:merge(clusters.FanControl.attributes.RockSupport:read())
read_req:merge(clusters.PowerSource.attributes.AttributeList:read())
read_req:merge(clusters.Thermostat.attributes.AttributeList:read())
test.socket.matter:__expect_send({ mock_lennox_device.id, read_req })

test.set_rpc_version(6)
end

test.set_test_init_function(test_init)

test.register_message_test(
"Lennox thermostat uses 0.5 setpoint range step",
{
{
channel = "matter",
direction = "receive",
message = {
mock_lennox_device.id,
clusters.Thermostat.attributes.AbsMinHeatSetpointLimit:build_test_report_data(mock_lennox_device, 1, 1000)
}
},
{
channel = "matter",
direction = "receive",
message = {
mock_lennox_device.id,
clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit:build_test_report_data(mock_lennox_device, 1, 3222)
}
},
{
channel = "capability",
direction = "send",
message = mock_lennox_device:generate_test_message("main",
capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = 10.00, maximum = 32.22, step = 0.5 }, unit =
"C" }))
}
},
{
min_api_version = 17
}
)

test.run_registered_tests()
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ function AttributeHandlers.abs_heat_setpoint_limit_factory(minOrMax)
-- Only emit the capability for RPC version >= 5 (unit conversion for
-- heating setpoint range capability is only supported for RPC >= 5)
if version.rpc >= 5 then
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = min, maximum = max, step = 0.1 }, unit = "C" }))
local setpoint_step = thermostat_utils.get_product_override_field(device, "setpoint_step") or 0.1
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = min, maximum = max, step = setpoint_step }, unit = "C" }))
end
else
device.log.warn_with({hub_logs = true}, string.format("Device reported a min heating setpoint %d that is not lower than the reported max %d", min, max))
Expand All @@ -201,7 +202,8 @@ function AttributeHandlers.abs_cool_setpoint_limit_factory(minOrMax)
-- Only emit the capability for RPC version >= 5 (unit conversion for
-- cooling setpoint range capability is only supported for RPC >= 5)
if version.rpc >= 5 then
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { minimum = min, maximum = max, step = 0.1 }, unit = "C" }))
local setpoint_step = thermostat_utils.get_product_override_field(device, "setpoint_step") or 0.1
device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { minimum = min, maximum = max, step = setpoint_step }, unit = "C" }))
end
else
device.log.warn_with({hub_logs = true}, string.format("Device reported a min cooling setpoint %d that is not lower than the reported max %d", min, max))
Expand All @@ -228,7 +230,7 @@ function AttributeHandlers.temperature_handler_factory(attribute)
local range = {
minimum = device:get_field(fields.setpoint_limit_device_field.MIN_COOL) or fields.THERMOSTAT_MIN_TEMP_IN_C,
maximum = device:get_field(fields.setpoint_limit_device_field.MAX_COOL) or fields.THERMOSTAT_MAX_TEMP_IN_C,
step = 0.1
step = thermostat_utils.get_product_override_field(device, "setpoint_step") or 0.1
}
event = capabilities.thermostatCoolingSetpoint.coolingSetpointRange({value = range, unit = unit})
device:emit_event_for_endpoint(ib.endpoint_id, event)
Expand All @@ -244,7 +246,7 @@ function AttributeHandlers.temperature_handler_factory(attribute)
local range = {
minimum = device:get_field(fields.setpoint_limit_device_field.MIN_HEAT) or MIN_TEMP_IN_C,
maximum = device:get_field(fields.setpoint_limit_device_field.MAX_HEAT) or MAX_TEMP_IN_C,
step = 0.1
step = thermostat_utils.get_product_override_field(device, "setpoint_step") or 0.1
}
event = capabilities.thermostatHeatingSetpoint.heatingSetpointRange({value = range, unit = unit})
device:emit_event_for_endpoint(ib.endpoint_id, event)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,12 @@ ThermostatFields.conversion_tables = {
},
}

return ThermostatFields
ThermostatFields.vendor_overrides = {
[0x1356] = { -- LENNOX_MANUFACTURER_ID
[0x0001] = { setpoint_step = 0.5 }, -- This device requires a setpoint step of 0.5
[0x0002] = { setpoint_step = 0.5 }, -- This device requires a setpoint step of 0.5
[0x0003] = { setpoint_step = 0.5 }, -- This device requires a setpoint step of 0.5
}
}

return ThermostatFields
Original file line number Diff line number Diff line change
Expand Up @@ -244,4 +244,13 @@ function ThermostatUtils.report_power_consumption_to_st_energy(device, latest_to
}))
end

function ThermostatUtils.get_product_override_field(device, override_key)
if device.manufacturer_info
and fields.vendor_overrides[device.manufacturer_info.vendor_id]
and fields.vendor_overrides[device.manufacturer_info.vendor_id][device.manufacturer_info.product_id]
then
return fields.vendor_overrides[device.manufacturer_info.vendor_id][device.manufacturer_info.product_id][override_key]
end
end

return ThermostatUtils
Loading