Skip to content

Fix Tuya support for climate fan modes which use "windspeed" function #148646

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Jul 17, 2025

Conversation

timmo001
Copy link
Contributor

@timmo001 timmo001 commented Jul 12, 2025

Breaking change

Proposed change

Allows devices which use the "windspeed" function to see and control the fan speed (in my case a SereneLife SLPAC905WUK Air Conditioner).

Tested on my device. The labels still show as "1" or "2" for "LOW" and "High", but these labels should probably be from the provider.

Also added fixture and snapshot for my device as requested.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

  • This PR fixes or closes issue: fixes #
  • This PR is related to issue:
  • Link to documentation pull request:
  • Link to developer documentation pull request:
  • Link to frontend pull request:

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.

To help with the load of incoming pull requests:

@home-assistant
Copy link

Hey there @tuya, @zlinoliver, mind taking a look at this pull request as it has been labeled with an integration (tuya) you are listed as a code owner for? Thanks!

Code owner commands

Code owners of tuya can trigger bot actions by commenting:

  • @home-assistant close Closes the pull request.
  • @home-assistant rename Awesome new title Renames the pull request.
  • @home-assistant reopen Reopen the pull request.
  • @home-assistant unassign tuya Removes the current integration label and assignees on the pull request, add the integration domain after the command.
  • @home-assistant add-label needs-more-information Add a label (needs-more-information, problem in dependency, problem in custom component) to the pull request.
  • @home-assistant remove-label needs-more-information Remove a label (needs-more-information, problem in dependency, problem in custom component) on the pull request.

@timmo001 timmo001 marked this pull request as ready for review July 12, 2025 11:25
@Copilot Copilot AI review requested due to automatic review settings July 12, 2025 11:25
Copilot

This comment was marked as outdated.

@timmo001 timmo001 requested a review from Copilot July 12, 2025 11:29
Copilot

This comment was marked as outdated.

@timmo001 timmo001 requested a review from Copilot July 12, 2025 11:40
Copilot

This comment was marked as outdated.

@epenet
Copy link
Contributor

epenet commented Jul 12, 2025

Please add a real fixture file for your device, as extracted from diagnostic dump

@epenet epenet marked this pull request as draft July 12, 2025 21:43
@epenet
Copy link
Contributor

epenet commented Jul 15, 2025

Please add a real fixture file for your device, as extracted from diagnostic dump

See sample in #148797

@timmo001
Copy link
Contributor Author

Fixture and snapshot added

@timmo001 timmo001 force-pushed the fix/tuya-windspeed-fan-modes branch from 652980e to c7ad29b Compare July 16, 2025 09:30
@@ -97,6 +97,10 @@
Platform.SELECT,
Platform.SWITCH,
],
"kt_serenelife_slpac905wuk_air_conditioner": [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep in alphabetical order (after ks_tower_fan)

)


def test_fan_mode_windspeed() -> None:
Copy link
Contributor

@epenet epenet Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the tests to the bottom of the file, after the setup tests.

Also, please look at test_cover.py for running tests on a mocked device.
You should not be creating a TuyaClimateEntity manually, and you should be able to drop make_climate_entity / DummyFunction / DummyManager / DummyDevice

@pytest.mark.parametrize(
"mock_device_code",
["am43_corded_motor_zigbee_cover"],
)

{"windspeed": "2"},
)
assert entity.fan_mode == "2"
entity.set_fan_mode("1")
Copy link
Contributor

@epenet epenet Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set_fan_mode should be tested using service calls - not direct function calls on the entity
The best would be to make the service call, and then check that the manager received the correct instructions

@timmo001 timmo001 marked this pull request as ready for review July 16, 2025 11:32
@timmo001 timmo001 requested review from Copilot and epenet July 16, 2025 11:33
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes Tuya climate entity support for devices that use the "windspeed" function instead of "fan_speed_enum" for fan mode control, specifically targeting the SereneLife SLPAC905WUK Air Conditioner.

  • Updates climate entity initialization to detect and store the correct fan mode DPCode ("windspeed" or "fan_speed_enum")
  • Modifies fan mode getter and setter methods to use the dynamically determined DPCode
  • Adds comprehensive test coverage for the windspeed functionality and error handling

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
homeassistant/components/tuya/climate.py Core logic changes to support windspeed DPCode with dynamic fan mode handling
tests/components/tuya/test_climate.py New test cases for windspeed fan mode functionality and error scenarios
tests/components/tuya/snapshots/test_climate.ambr Snapshot data for the new SereneLife device test case
tests/components/tuya/fixtures/kt_serenelife_slpac905wuk_air_conditioner.json Device fixture data for the SereneLife air conditioner
tests/components/tuya/__init__.py Device mock configuration entry for the new test device

@@ -304,7 +306,10 @@ def set_preset_mode(self, preset_mode: str) -> None:

def set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
self._send_command([{"code": DPCode.FAN_SPEED_ENUM, "value": fan_mode}])
if not self._fan_dp_code:
raise RuntimeError("No valid fan DPCode set for this device.")
Copy link
Preview

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ServiceNotSupported exception instead of RuntimeError for unsupported operations. This follows Home Assistant's pattern for indicating when a service is not available for a device.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

@timmo001 timmo001 Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#148646 (comment)

It's actually not possible to get to this state as self._fan_dp_code sets fan_mode to None so a ServiceNotSupported exception is raised before this can occur. Would it be safe to remove this check?

Comment on lines 82 to 88
"fan_mode": 2,
},
)
await hass.async_block_till_done()

# Simulate the device reporting the new windspeed
mock_device.status["windspeed"] = 2
Copy link
Preview

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number 2 should be replaced with a named constant or variable for better readability and maintainability.

Suggested change
"fan_mode": 2,
},
)
await hass.async_block_till_done()
# Simulate the device reporting the new windspeed
mock_device.status["windspeed"] = 2
"fan_mode": FAN_MODE_HIGH,
},
)
await hass.async_block_till_done()
# Simulate the device reporting the new windspeed
mock_device.status["windspeed"] = FAN_MODE_HIGH

Copilot uses AI. Check for mistakes.

Comment on lines 82 to 88
"fan_mode": 2,
},
)
await hass.async_block_till_done()

# Simulate the device reporting the new windspeed
mock_device.status["windspeed"] = 2
Copy link
Preview

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number 2 should be replaced with a named constant or variable for better readability and maintainability.

Suggested change
"fan_mode": 2,
},
)
await hass.async_block_till_done()
# Simulate the device reporting the new windspeed
mock_device.status["windspeed"] = 2
"fan_mode": WINDSPEED_HIGH,
},
)
await hass.async_block_till_done()
# Simulate the device reporting the new windspeed
mock_device.status["windspeed"] = WINDSPEED_HIGH

Copilot uses AI. Check for mistakes.

assert state is not None, (
"climate.air_conditioner does not exist after service call"
)
assert state.attributes["fan_mode"] == 2
Copy link
Preview

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number 2 should be replaced with a named constant or variable for better readability and maintainability.

Suggested change
assert state.attributes["fan_mode"] == 2
assert state.attributes["fan_mode"] == FAN_MODE_HIGH

Copilot uses AI. Check for mistakes.

"set_fan_mode",
{
"entity_id": "climate.air_conditioner",
"fan_mode": 2,
Copy link
Preview

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number 2 should be replaced with a named constant or variable for better readability and maintainability.

Suggested change
"fan_mode": 2,
"fan_mode": FAN_MODE_HIGH,

Copilot uses AI. Check for mistakes.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@@ -250,13 +250,15 @@ def __init__(
)

# Determine fan modes
self._fan_dp_code: str | None = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename to _mode_dp_code or _fan_mode_dp_code ?

@@ -304,7 +306,10 @@ def set_preset_mode(self, preset_mode: str) -> None:

def set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
self._send_command([{"code": DPCode.FAN_SPEED_ENUM, "value": fan_mode}])
if not self._fan_dp_code:
raise RuntimeError("No valid fan DPCode set for this device.")
Copy link
Contributor

@epenet epenet Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this can never happen it might be possible to use "if TYPE_CHECKING: assert is not none" with a comment

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Comment on lines 93 to 106
# Simulate the device reporting the new windspeed
mock_device.status["windspeed"] = WINDSPEED_HIGH
await hass.services.async_call(
"homeassistant",
"update_entity",
{"entity_id": "climate.air_conditioner"},
)
await hass.async_block_till_done()

state = hass.states.get("climate.air_conditioner")
assert state is not None, (
"climate.air_conditioner does not exist after service call"
)
assert state.attributes["fan_mode"] == WINDSPEED_HIGH
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this part "Simulate the device reporting the new windspeed" is needed in the same test

I think it should be a separate test, using the device listener similar to https://github.com/home-assistant/core/pull/125098/files#diff-ba8c8bfcb09fab51857cdd9f286698a867da7a456abeee7cdfb04734b830f9f1

And it can be done in a follow-up PR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Simulate the device reporting the new windspeed
mock_device.status["windspeed"] = WINDSPEED_HIGH
await hass.services.async_call(
"homeassistant",
"update_entity",
{"entity_id": "climate.air_conditioner"},
)
await hass.async_block_till_done()
state = hass.states.get("climate.air_conditioner")
assert state is not None, (
"climate.air_conditioner does not exist after service call"
)
assert state.attributes["fan_mode"] == WINDSPEED_HIGH

timmo001 and others added 4 commits July 16, 2025 14:47
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
This reverts commit d602590.
@timmo001 timmo001 requested a review from epenet July 16, 2025 14:24
@epenet epenet added this to the 2025.8.0b0 milestone Jul 16, 2025
Copy link
Contributor

@epenet epenet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@epenet epenet merged commit 72d1c3c into home-assistant:dev Jul 17, 2025
34 checks passed
@timmo001 timmo001 deleted the fix/tuya-windspeed-fan-modes branch July 17, 2025 10:05
@github-actions github-actions bot locked and limited conversation to collaborators Jul 18, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy