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
Expand Up @@ -242,7 +242,7 @@ test.register_coroutine_test(
)

test.register_coroutine_test(
"Heat setpoint inside deadband",
"Heat setpoint inside deadband raises the cooling setpoint",
function()
configure(mock_device)
test.socket.matter:__queue_receive({
Expand All @@ -254,6 +254,92 @@ test.register_coroutine_test(
mock_device.id,
{ capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 26 } }
})
test.socket.matter:__expect_send({mock_device.id, clusters.Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device, 1, 2760)})
test.socket.matter:__expect_send({mock_device.id, clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1, 2600)})
end,
{
min_api_version = 15
}
)

test.register_coroutine_test(
"Cool setpoint inside deadband lowers the heating setpoint",
function()
configure(mock_device)
test.socket.matter:__queue_receive({
mock_device.id,
clusters.Thermostat.attributes.MinSetpointDeadBand:build_test_report_data(mock_device, 1, 16) --1.6 celcius
})
test.wait_for_events()
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = "thermostatCoolingSetpoint", component = "main", command = "setCoolingSetpoint", args = { 25 } }
})
test.socket.matter:__expect_send({mock_device.id, clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1, 2340)})
test.socket.matter:__expect_send({mock_device.id, clusters.Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device, 1, 2500)})
end,
{
min_api_version = 15
}
)

test.register_coroutine_test(
"Heat setpoint inside deadband raises the cooling setpoint exactly to its max",
function()
configure(mock_device)
test.socket.matter:__queue_receive({
mock_device.id,
clusters.Thermostat.attributes.MinSetpointDeadBand:build_test_report_data(mock_device, 1, 16) --1.6 celcius
})
test.socket.matter:__queue_receive({
mock_device.id,
clusters.Thermostat.attributes.AbsMinCoolSetpointLimit:build_test_report_data(mock_device, 1, 1000)
})
test.socket.matter:__queue_receive({
mock_device.id,
clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit:build_test_report_data(mock_device, 1, 2720)
})
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { minimum = 10.00, maximum = 27.20, step = 0.1 }, unit = "C" }))
)
test.wait_for_events()
-- 25.6 + 1.6 is 27.200000000000003 in floating point, still the max of 27.20
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 25.6 } }
})
test.socket.matter:__expect_send({mock_device.id, clusters.Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device, 1, 2720)})
test.socket.matter:__expect_send({mock_device.id, clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1, 2560)})
end,
{
min_api_version = 15
}
)

test.register_coroutine_test(
"Heat setpoint inside deadband is rejected when the cooling setpoint cannot be raised",
function()
configure(mock_device)
test.socket.matter:__queue_receive({
mock_device.id,
clusters.Thermostat.attributes.MinSetpointDeadBand:build_test_report_data(mock_device, 1, 16) --1.6 celcius
})
test.socket.matter:__queue_receive({
mock_device.id,
clusters.Thermostat.attributes.AbsMinCoolSetpointLimit:build_test_report_data(mock_device, 1, 1000)
})
test.socket.matter:__queue_receive({
mock_device.id,
clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit:build_test_report_data(mock_device, 1, 2700)
})
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { minimum = 10.00, maximum = 27.00, step = 0.1 }, unit = "C" }))
)
test.wait_for_events()
test.socket.capability:__queue_receive({
mock_device.id,
{ capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 26 } }
})
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", cached_heating_setpoint)
)
Expand All @@ -264,13 +350,24 @@ test.register_coroutine_test(
)

test.register_coroutine_test(
"Cool setpoint inside deadband",
"Cool setpoint inside deadband is rejected when the heating setpoint cannot be lowered",
function()
configure(mock_device)
test.socket.matter:__queue_receive({
mock_device.id,
clusters.Thermostat.attributes.MinSetpointDeadBand:build_test_report_data(mock_device, 1, 16) --1.6 celcius
})
test.socket.matter:__queue_receive({
mock_device.id,
clusters.Thermostat.attributes.AbsMinHeatSetpointLimit:build_test_report_data(mock_device, 1, 2400)
})
test.socket.matter:__queue_receive({
mock_device.id,
clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit:build_test_report_data(mock_device, 1, 3000)
})
test.socket.capability:__expect_send(
mock_device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = 24.00, maximum = 30.00, step = 0.1 }, unit = "C" }))
)
test.wait_for_events()
test.socket.capability:__queue_receive({
mock_device.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,49 @@ test.register_message_test(
}
)

test.register_message_test(
"Without the Auto feature a heating setpoint above the cooling setpoint is written alone",
{
{
channel = "matter",
direction = "receive",
message = {
mock_device.id,
clusters.Thermostat.server.attributes.OccupiedCoolingSetpoint:build_test_report_data(mock_device, 1, 20*100)
}
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" }))
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpoint({ value = 20.0, unit = "C" }))
},
{
channel = "capability",
direction = "receive",
message = {
mock_device.id,
{ capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 25 } }
}
},
{
channel = "matter",
direction = "send",
message = {
mock_device.id,
clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1, 25*100)
}
}
},
{
min_api_version = 15
}
)

test.register_message_test(
"Setting the cooling setpoint should send the appropriate commands",
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ function CapabilityHandlers.thermostat_set_setpoint_factory(setpoint)
--Check setpoint limits for the device
local setpoint_type = string.match(setpoint.NAME, "Heat") or "Cool"
local deadband = device:get_field(fields.setpoint_limit_device_field.MIN_DEADBAND) or 2.5 --spec default
-- With the AutoMode feature a setpoint that breaks the deadband moves the other setpoint along, as the
-- Thermostat cluster specification describes; it is rejected only if the other setpoint would leave its limits.
local companion_setpoint, companion_centi
if setpoint_type == "Heat" then
local min = device:get_field(fields.setpoint_limit_device_field.MIN_HEAT) or MIN_TEMP_IN_C
local max = device:get_field(fields.setpoint_limit_device_field.MAX_HEAT) or MAX_TEMP_IN_C
Expand All @@ -222,12 +225,19 @@ function CapabilityHandlers.thermostat_set_setpoint_factory(setpoint)
return
end
if is_auto_capable and value > (cached_cooling_val - deadband) then
log.warn(string.format(
"Invalid setpoint (%s) is greater than the cooling setpoint (%s) with the deadband (%s)",
value, cooling_setpoint, deadband
))
device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpoint(heating_setpoint, {state_change = true}))
return
local cool_min = device:get_field(fields.setpoint_limit_device_field.MIN_COOL) or MIN_TEMP_IN_C
local cool_max = device:get_field(fields.setpoint_limit_device_field.MAX_COOL) or MAX_TEMP_IN_C
companion_centi = st_utils.round((value + deadband) * 100.0)
if companion_centi < st_utils.round(cool_min * 100.0) or companion_centi > st_utils.round(cool_max * 100.0) then
log.warn(string.format(
"Invalid setpoint (%s) is greater than the cooling setpoint (%s) with the deadband (%s), and the cooling setpoint cannot be raised to %s (max %s)",
value, cached_cooling_val, deadband, companion_centi / 100.0, cool_max
))
device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpoint(heating_setpoint, {state_change = true}))
return
end
log.info(string.format("Raising the cooling setpoint to %s to keep the deadband (%s)", companion_centi / 100.0, deadband))
companion_setpoint = clusters.Thermostat.attributes.OccupiedCoolingSetpoint
end
else
local min = device:get_field(fields.setpoint_limit_device_field.MIN_COOL) or MIN_TEMP_IN_C
Expand All @@ -241,15 +251,28 @@ function CapabilityHandlers.thermostat_set_setpoint_factory(setpoint)
return
end
if is_auto_capable and value < (cached_heating_val + deadband) then
log.warn(string.format(
"Invalid setpoint (%s) is less than the heating setpoint (%s) with the deadband (%s)",
value, heating_setpoint, deadband
))
device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpoint(cooling_setpoint, {state_change = true}))
return
local heat_min = device:get_field(fields.setpoint_limit_device_field.MIN_HEAT) or MIN_TEMP_IN_C
local heat_max = device:get_field(fields.setpoint_limit_device_field.MAX_HEAT) or MAX_TEMP_IN_C
companion_centi = st_utils.round((value - deadband) * 100.0)
if companion_centi < st_utils.round(heat_min * 100.0) or companion_centi > st_utils.round(heat_max * 100.0) then
log.warn(string.format(
"Invalid setpoint (%s) is less than the heating setpoint (%s) with the deadband (%s), and the heating setpoint cannot be lowered to %s (min %s)",
value, cached_heating_val, deadband, companion_centi / 100.0, heat_min
))
device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpoint(cooling_setpoint, {state_change = true}))
return
end
log.info(string.format("Lowering the heating setpoint to %s to keep the deadband (%s)", companion_centi / 100.0, deadband))
companion_setpoint = clusters.Thermostat.attributes.OccupiedHeatingSetpoint
end
end
device:send(setpoint:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.Thermostat.ID), st_utils.round(value * 100.0)))
if companion_setpoint then
-- The other setpoint goes first, so a server that rejects deadband violations accepts both writes,
-- and one that adjusts itself has nothing to adjust. Separate requests: the hub rejects a write
-- request with more than one attribute.
device:send(companion_setpoint:write(device, endpoint_id, companion_centi))
end
device:send(setpoint:write(device, endpoint_id, st_utils.round(value * 100.0)))
end
end

Expand Down
Loading