-
-
Notifications
You must be signed in to change notification settings - Fork 34.4k
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
Fix Tuya support for climate fan modes which use "windspeed" function #148646
Conversation
Hey there @tuya, @zlinoliver, mind taking a look at this pull request as it has been labeled with an integration ( Code owner commandsCode owners of
|
Please add a real fixture file for your device, as extracted from diagnostic dump |
See sample in #148797 |
Fixture and snapshot added |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
652980e
to
c7ad29b
Compare
tests/components/tuya/__init__.py
Outdated
@@ -97,6 +97,10 @@ | |||
Platform.SELECT, | |||
Platform.SWITCH, | |||
], | |||
"kt_serenelife_slpac905wuk_air_conditioner": [ |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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
core/tests/components/tuya/test_cover.py
Lines 60 to 63 in fe83847
@pytest.mark.parametrize( | |
"mock_device_code", | |
["am43_corded_motor_zigbee_cover"], | |
) |
{"windspeed": "2"}, | ||
) | ||
assert entity.fan_mode == "2" | ||
entity.set_fan_mode("1") |
There was a problem hiding this comment.
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
There was a problem hiding this 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.") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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?
"fan_mode": 2, | ||
}, | ||
) | ||
await hass.async_block_till_done() | ||
|
||
# Simulate the device reporting the new windspeed | ||
mock_device.status["windspeed"] = 2 |
There was a problem hiding this comment.
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.
"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.
"fan_mode": 2, | ||
}, | ||
) | ||
await hass.async_block_till_done() | ||
|
||
# Simulate the device reporting the new windspeed | ||
mock_device.status["windspeed"] = 2 |
There was a problem hiding this comment.
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.
"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 |
There was a problem hiding this comment.
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.
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, |
There was a problem hiding this comment.
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.
"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 |
There was a problem hiding this comment.
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.") |
There was a problem hiding this comment.
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>
# 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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍
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
Additional information
Checklist
ruff format homeassistant tests
)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest
.requirements_all.txt
.Updated by running
python3 -m script.gen_requirements_all
.To help with the load of incoming pull requests: