Skip to content

Matter Thermostat mode discovery state and Auto feature detection are not endpoint-specific #3265

Open

Description

@ldeora

Summary

For devices with multiple Thermostat endpoints, SystemMode / ControlSequenceOfOperation discovery state is cached at device scope, and Auto support is detected by asking whether any thermostat endpoint on the device has the Auto feature. This can cause one endpoint's modes to affect another endpoint's supportedThermostatModes.

Validated against main commit 43f47501b217336d17ed2fc92b64d08971f305ce.

Affected code

attribute_handlers.lua lines 44-71:

if device:get_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN) == nil then
  ...
  device:set_field(fields.SAVED_SYSTEM_MODE_IB, ib)
  return
end
...
local disallowed_thermostat_modes = device:get_field(fields.DISALLOWED_THERMOSTAT_MODES) or {}
...
local optional_modes_seen = st_utils.deep_copy(device:get_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN)) or {}
...
device:set_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN, optional_modes_seen, {persist=true})

attribute_handlers.lua around the Auto check:

local auto = device:get_endpoints(
  clusters.Thermostat.ID,
  {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}
)
if #auto > 0 then
  table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.auto.NAME)
else
  table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.auto.NAME)
end

The endpoint that generated the ControlSequenceOfOperation report is not checked.

Failure mechanism

OPTIONAL_THERMOSTAT_MODES_SEEN, DISALLOWED_THERMOSTAT_MODES, and SAVED_SYSTEM_MODE_IB are properties of an individual thermostat server endpoint but are stored once per SmartThings device. In addition, Auto support is considered true for every thermostat endpoint when any endpoint has the feature bit.

Reproduction case

Use a composed Matter device with two Thermostat server endpoints:

EP 2: Auto feature set
EP 3: Auto feature not set

Send a ControlSequenceOfOperation report from EP 3. The current handler calls device:get_endpoints(...AUTOMODE) and sees EP 2, so it adds auto to EP 3's supportedThermostatModes even though EP 3 does not advertise the feature.

A second variant is to let one endpoint report an optional SystemMode such as Emergency Heat; the globally cached OPTIONAL_THERMOSTAT_MODES_SEEN can then influence later recomputation for the other endpoint.

Expected behavior

Each thermostat endpoint should maintain its own discovered/allowed modes, and Auto should be exposed only for an endpoint whose Thermostat FeatureMap contains the Auto feature.

Proposed fix

Cache OPTIONAL_THERMOSTAT_MODES_SEEN, DISALLOWED_THERMOSTAT_MODES, and SAVED_SYSTEM_MODE_IB per Thermostat endpoint, and test the Auto feature against the endpoint being processed.

For example, in system_mode_handler():

local optional_modes_seen = thermostat_utils.get_field_for_endpoint(
  device, fields.OPTIONAL_THERMOSTAT_MODES_SEEN, ib.endpoint_id
)

if optional_modes_seen == nil then
  device.log.info_with(
    {hub_logs = true},
    "In the SystemMode handler: ControlSequenceOfOperation has not run yet. Exiting early."
  )
  thermostat_utils.set_field_for_endpoint(
    device, fields.SAVED_SYSTEM_MODE_IB, ib.endpoint_id, ib
  )
  return
end

local disallowed_thermostat_modes = thermostat_utils.get_field_for_endpoint(
  device, fields.DISALLOWED_THERMOSTAT_MODES, ib.endpoint_id
) or {}

When an optional mode is learned:

thermostat_utils.set_field_for_endpoint(
  device,
  fields.OPTIONAL_THERMOSTAT_MODES_SEEN,
  ib.endpoint_id,
  optional_modes_seen,
  { persist = true }
)

In control_sequence_of_operation_handler(), determine Auto support for the reporting endpoint, not for the device as a whole:

local auto_eps = device:get_endpoints(
  clusters.Thermostat.ID,
  {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}
)
local auto_supported = thermostat_utils.tbl_contains(auto_eps, ib.endpoint_id)

if auto_supported then
  table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.auto.NAME)
else
  table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.auto.NAME)
end

Store the resulting disallowed list per endpoint and retrieve any saved SystemMode IB from that same endpoint:

thermostat_utils.set_field_for_endpoint(
  device,
  fields.DISALLOWED_THERMOSTAT_MODES,
  ib.endpoint_id,
  disallowed_mode_operations
)

local saved_system_mode_ib = thermostat_utils.get_field_for_endpoint(
  device, fields.SAVED_SYSTEM_MODE_IB, ib.endpoint_id
)

The same endpoint-specific Auto check is also required in thermostat_set_setpoint_factory(). The current code treats the device as Auto-capable if any Thermostat endpoint has the feature:

local auto_eps = device:get_endpoints(
  clusters.Thermostat.ID,
  {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}
)
local is_auto_capable = thermostat_utils.tbl_contains(auto_eps, endpoint_id)

Without this final change, an endpoint that does not support Auto can still be subjected to the Auto-mode deadband validation because another endpoint supports it.

Suggested regression test

Add a two-endpoint thermostat/heat-pump test where only one Thermostat endpoint has the Auto feature. Verify auto appears only in that component's supportedThermostatModes, and verify an optional mode observed on one endpoint does not appear on the other.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions