Skip to content

Fix thermostat fan modes to respect fanModeSequence#708

Open
vmvarga wants to merge 6 commits intozigpy:devfrom
vmvarga:dev
Open

Fix thermostat fan modes to respect fanModeSequence#708
vmvarga wants to merge 6 commits intozigpy:devfrom
vmvarga:dev

Conversation

@vmvarga
Copy link

@vmvarga vmvarga commented Mar 12, 2026

Summary

  • Fix the Thermostat entity to read the fan_mode_sequence attribute from the ZCL Fan Control cluster (0x0202) instead of hardcoding fan modes to [auto, on]. Devices now correctly expose Low, Medium, High, and Auto fan speeds based on the ZCL spec (Table 6-20).
  • Fix the fan_mode getter to read the actual fan_mode attribute from the Fan Control cluster handler instead of guessing from the thermostat's running_state bitmap.
  • Fix async_set_fan_mode to map all supported fan mode strings (low, medium, high, on, auto) to their corresponding FanMode ZCL enum values.
  • Expose HVACMode.FAN_ONLY as an available HVAC mode when a Fan Control cluster is present on the endpoint, since SEQ_OF_OPERATION (derived from controlSequenceOfOperation) never includes it per ZCL spec.

Problem

The Thermostat entity hardcodes fan modes to [auto, on] and completely ignores the fan_mode_sequence attribute (attribute 0x0001) from the Fan Control cluster. Per ZCL 8 (Table 6-20), FanModeSequenceType defines which fan modes a device supports:

Value Available Modes
0x00 Low, Medium, High
0x01 Low, High
0x02 Low, Medium, High, Auto
0x03 Low, High, Auto
0x04 On, Auto

Devices reporting e.g. fan_mode_sequence = 0x02 (Low/Medium/High/Auto) were stuck with only Auto/On in the UI.

Additionally, the thermostat never exposes HVACMode.FAN_ONLY even when the device supports SystemMode.Fan_only (0x07). The mappings HVAC_MODE_2_SYSTEM and SYSTEM_MODE_2_HVAC already handle FAN_ONLY <-> Fan_only, but hvac_modes derives its list solely from SEQ_OF_OPERATION which never includes it, since controlSequenceOfOperation only covers cooling/heating per the ZCL spec.

Changes

zha/application/platforms/climate/const.py

  • Import FanMode from zigpy
  • Add SEQ_FAN_MODES dict mapping fan_mode_sequence values (0x00–0x04) to fan mode string lists
  • Add FAN_MODE_TO_ZCL dict mapping fan mode strings to FanMode enum values
  • Add ZCL_TO_FAN_MODE reverse mapping dict

zha/application/platforms/climate/__init__.py

  • fan_modes: Read fan_mode_sequence from the fan cluster handler, look up modes via SEQ_FAN_MODES, fall back to [on, auto] for unknown sequences
  • fan_mode: Read actual fan_mode attribute from the fan cluster handler, map back via ZCL_TO_FAN_MODE, fall back to running_state heuristic when unavailable
  • async_set_fan_mode: Use FAN_MODE_TO_ZCL mapping instead of hardcoded On/Auto branch
  • hvac_modes: Append HVACMode.FAN_ONLY when a fan cluster handler is present

Backwards compatibility

  • Devices with fan_mode_sequence = 0x04 still get [on, auto] (unchanged)
  • Devices without a Fan Control cluster are completely unaffected
  • Unknown fan_mode_sequence values fall back to [on, auto]
  • No changes to HVAC_MODE_2_SYSTEM, SYSTEM_MODE_2_HVAC, or async_set_hvac_mode

Test plan

  • Device with fan_mode_sequence = 0x02 exposes: Low, Medium, High, Auto
  • Device with fan_mode_sequence = 0x04 (or unknown) exposes: On, Auto (backwards compatible)
  • Setting fan mode to "low" sends FanMode.Low (0x01) to the device
  • Setting fan mode to "high" sends FanMode.High (0x03) to the device
  • Current fan mode reflects the actual fan_mode attribute from the Fan Control cluster
  • Thermostat with a Fan Control cluster exposes "Fan only" as an HVAC mode
  • Selecting "Fan only" writes SystemMode.Fan_only (0x07) to the thermostat
  • Thermostat without a Fan Control cluster is unaffected

Affected devices

Any thermostat with a Fan Control cluster (0x0202) that reports fan_mode_sequence != 0x04, such as Tuya HVAC thermostats with cooling/heating/fan modes (e.g. _TZE204_mpbki2zm).

@puddly
Copy link
Contributor

puddly commented Mar 12, 2026

To regenerate device diagnostics files (to see what this change would affect in real devices), run the following:

python -m tools.regenerate_diagnostics

It looks like four devices (in the testing DB) change their exposed fan modes.

@codecov
Copy link

codecov bot commented Mar 12, 2026

Codecov Report

❌ Patch coverage is 89.47368% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.50%. Comparing base (c51efa5) to head (75510aa).
⚠️ Report is 2 commits behind head on dev.

Files with missing lines Patch % Lines
zha/application/platforms/climate/__init__.py 86.66% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #708      +/-   ##
==========================================
- Coverage   97.51%   97.50%   -0.01%     
==========================================
  Files          62       62              
  Lines       10946    10960      +14     
==========================================
+ Hits        10674    10687      +13     
- Misses        272      273       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@vmvarga
Copy link
Author

vmvarga commented Mar 13, 2026

Should resolve #226

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants