Skip to content

Legacy Matter Thermostat profile generator can select 11 profile names that do not exist #3271

Open

Description

@ldeora

Summary

The legacy thermostat profiling path generates a profile name from humidity support, fan support, heating/cooling support, ThermostatRunningState support, and battery reporting type. Exhaustively enumerating those dimensions produces 72 valid generic thermostat profile names, but only 61 of them exist in the driver package. Eleven reachable names are missing.

This affects the legacy path (api < 14 or rpc < 8); the newer modular profile path avoids this particular problem.

Validated against main commit 43f47501b217336d17ed2fc92b64d08971f305ce and the current profiles/ directory.

Affected code

legacy_device_configuration.lua lines 214-251:

elseif #thermostat_eps > 0 then
  profile_name = "thermostat"

  if #humidity_eps > 0 then
    profile_name = profile_name .. "-humidity"
  end

  local fan_name = LegacyConfigurationHelpers.create_fan_profile(device)
  if fan_name ~= "" then
    profile_name = profile_name .. "-fan"
  end

  local thermostat_modes = LegacyConfigurationHelpers.create_thermostat_modes_profile(device)
  ...
  profile_name = profile_name .. thermostat_modes

  if not running_state_supported then
    profile_name = profile_name .. "-nostate"
  end

  if battery_supported == fields.battery_support.BATTERY_LEVEL then
    profile_name = profile_name .. "-batteryLevel"
  elseif battery_supported == fields.battery_support.NO_BATTERY then
    profile_name = profile_name .. "-nobattery"
  end
end
...
device:try_update_metadata({profile = profile_name})

The legacy/modular branch is selected in device_configuration.lua lines 319-328:

if version.api >= 14 and version.rpc >= 8 and match_modular_device_type[primary_device_type] then
  match_modular_device_type[primary_device_type](device)
  return
else
  local legacy_device_cfg = require "thermostat_utils.legacy_device_configuration"
  legacy_device_cfg.match_profile(device)
end

Reproduction case

One concrete reachable combination is:

humidity:              yes
fan:                   yes
Thermostat heating:    no
Thermostat cooling:    yes
ThermostatRunningState yes
battery reporting:     BatChargeLevel

The generator constructs:

thermostat-humidity-fan-cooling-only-batteryLevel

but that profile is absent.

An exhaustive enumeration found these 11 missing names:

thermostat-fan-cooling-only-nobattery
thermostat-fan-heating-only-nobattery
thermostat-fan-nobattery
thermostat-humidity-cooling-only-nobattery
thermostat-humidity-fan-cooling-only-batteryLevel
thermostat-humidity-fan-cooling-only-nobattery
thermostat-humidity-fan-cooling-only-nostate-batteryLevel
thermostat-humidity-fan-heating-only-nobattery
thermostat-humidity-fan-nobattery
thermostat-humidity-heating-only-nobattery
thermostat-humidity-nobattery

Expected behavior

Every profile name reachable from the legacy generator should resolve to a packaged profile, or the generator should explicitly collapse unsupported combinations to an existing compatible profile.

Proposed fix

The least invasive compatibility fix is to add the 11 profile files the legacy generator can currently construct but which are absent from profiles/:

thermostat-fan-cooling-only-nobattery.yml
thermostat-fan-heating-only-nobattery.yml
thermostat-fan-nobattery.yml
thermostat-humidity-cooling-only-nobattery.yml
thermostat-humidity-fan-cooling-only-batteryLevel.yml
thermostat-humidity-fan-cooling-only-nobattery.yml
thermostat-humidity-fan-cooling-only-nostate-batteryLevel.yml
thermostat-humidity-fan-heating-only-nobattery.yml
thermostat-humidity-fan-nobattery.yml
thermostat-humidity-heating-only-nobattery.yml
thermostat-humidity-nobattery.yml

For example, thermostat-fan-heating-only-nobattery.yml should be the same capability composition implied by its name:

name: thermostat-fan-heating-only-nobattery
components:
- id: main
  capabilities:
  - id: temperatureMeasurement
    version: 1
  - id: thermostatMode
    version: 1
  - id: thermostatFanMode
    version: 1
  - id: thermostatHeatingSetpoint
    version: 1
  - id: thermostatOperatingState
    version: 1
    config:
      values:
      - key: thermostatOperatingState.value
        enabledValues:
        - idle
        - heating
  - id: firmwareUpdate
    version: 1
  - id: refresh
    version: 1
  categories:
  - name: Thermostat

The other ten files can be generated from the nearest existing profile by applying the dimensions already encoded in the filename (humidity, fan, heating/cooling mode, nostate, batteryLevel/nobattery).

A regression test should also enumerate every state reachable by LegacyConfigurationHelpers.match_profile() and assert that the resulting profile name exists. Pseudocode for the core assertion:

for _, profile_name in ipairs(all_reachable_legacy_thermostat_profile_names()) do
  assert(
    profile_exists(profile_name),
    string.format("legacy thermostat profile does not exist: %s", profile_name)
  )
end

That test is important because merely adding the current 11 files fixes today's gap but does not prevent another missing combination when a new profile dimension is introduced later.

Suggested regression test

Enumerate:

humidity      2 states
fan           2 states
heat/cool     3 states (both, heat-only, cool-only)
running state 2 states
battery       3 states (percentage/default, batteryLevel, no battery)

This gives 2 * 2 * 3 * 2 * 3 = 72 names. Assert all 72 are present in profiles/ for the legacy path.

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