Skip to content

Fix thermostat fan modes to respect fanModeSequence#708

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

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

Conversation

@vmvarga
Copy link
Copy Markdown

@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).

Comment thread zha/application/platforms/climate/const.py Outdated
@puddly
Copy link
Copy Markdown
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
Copy Markdown

codecov Bot commented Mar 12, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.64%. Comparing base (8a0dac0) to head (4e3737c).
⚠️ Report is 4 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #708      +/-   ##
==========================================
+ Coverage   97.63%   97.64%   +0.01%     
==========================================
  Files          62       62              
  Lines       10814    10829      +15     
==========================================
+ Hits        10558    10574      +16     
+ Misses        256      255       -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
Copy Markdown
Author

vmvarga commented Mar 13, 2026

Should resolve #226

@vmvarga vmvarga requested a review from puddly March 14, 2026 14:49
@puddly
Copy link
Copy Markdown
Contributor

puddly commented Mar 18, 2026

I noticed that one of the test devices was my thermostat. I can confirm this shows up properly:

image

But the fan mode doesn't really do anything for my specific thermostat even when switching the fan mode from "auto" to "on", it doesn't actually support fan mode independent from heat and cool. This is probably just a device-specific bug.

@dmulcahey @TheJulianJES Do you have a device to test this change with?

@TheJulianJES
Copy link
Copy Markdown
Contributor

TheJulianJES commented Mar 18, 2026

I'll have a closer look later, but I think the issue is still present in Matter: https://github.com/home-assistant/core/blob/9ddefaaacd1df9da4f5a4f673eea2348b524d0ac/homeassistant/components/matter/climate.py#L121-L159 (assuming it's even the same issue here, which it might not be...)

@Arol450
Copy link
Copy Markdown

Arol450 commented Apr 15, 2026

Hi, I wanted to add a real-world data point in support of this PR.

I've been running a custom ESP32-C6 Zigbee bridge that exposes a Midea split AC unit to Home Assistant via ZHA using standard clusters (Thermostat 0x0201, Fan Control 0x0202, Temperature Measurement 0x0402). The device reports FanModeSequence.Low_Med_High_Auto (0x02) and supports SystemMode.Fan_only (0x07) and SystemMode.Dry (0x08).

With the current ZHA code:

  • Fan modes are hardcoded to [auto, on] — the actual sequence reported by the device is ignored
  • fan_only is not available as an HVAC mode despite the Fan Control cluster being present

Both of these issues are exactly what this PR addresses. I'd be happy to test a build with this change applied against my device if that would help move things forward.

Thanks for working on this.

@puddly
Copy link
Copy Markdown
Contributor

puddly commented Apr 15, 2026

The specific concern I brought above is that "fan only" support isn't reliably signaled by devices. My thermostat (and I imagine many of the others in our testing database) show up as supporting "fan only" but do not actually support it. There is, as far as I can tell, no way to tell from the ZCL alone that a device supports "fan only". We may need an opt-in mechanism via quirks, unfortunately.

@vmvarga
Copy link
Copy Markdown
Author

vmvarga commented Apr 22, 2026

Added opt-in via quirks

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.

4 participants