Fix backwards compatiblity with fan update to new model (#45951)

* Fix backwards compatiblity with fans update to new model

There were some non-speeds and devices that report a none
speed. These problems were discovered when updating zha
tasmota and vesync to the new model in #45407

* Update coverage

* fix check
This commit is contained in:
J. Nick Koston 2021-02-06 01:48:18 -10:00 committed by GitHub
parent 369616a6c3
commit a74ae3585a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 114 additions and 9 deletions

View File

@ -15,6 +15,8 @@ from homeassistant.components.fan import (
PRESET_MODE_AUTO = "auto" PRESET_MODE_AUTO = "auto"
PRESET_MODE_SMART = "smart" PRESET_MODE_SMART = "smart"
PRESET_MODE_SLEEP = "sleep"
PRESET_MODE_ON = "on"
FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION
LIMITED_SUPPORT = SUPPORT_SET_SPEED LIMITED_SUPPORT = SUPPORT_SET_SPEED
@ -38,6 +40,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
SPEED_HIGH, SPEED_HIGH,
PRESET_MODE_AUTO, PRESET_MODE_AUTO,
PRESET_MODE_SMART, PRESET_MODE_SMART,
PRESET_MODE_SLEEP,
PRESET_MODE_ON,
], ],
), ),
DemoFan( DemoFan(
@ -54,7 +58,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"fan3", "fan3",
"Percentage Full Fan", "Percentage Full Fan",
FULL_SUPPORT, FULL_SUPPORT,
[PRESET_MODE_AUTO, PRESET_MODE_SMART], [
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
PRESET_MODE_SLEEP,
PRESET_MODE_ON,
],
None, None,
), ),
DemoPercentageFan( DemoPercentageFan(
@ -62,7 +71,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"fan4", "fan4",
"Percentage Limited Fan", "Percentage Limited Fan",
LIMITED_SUPPORT, LIMITED_SUPPORT,
[PRESET_MODE_AUTO, PRESET_MODE_SMART], [
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
PRESET_MODE_SLEEP,
PRESET_MODE_ON,
],
None, None,
), ),
AsyncDemoPercentageFan( AsyncDemoPercentageFan(
@ -70,7 +84,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"fan5", "fan5",
"Preset Only Limited Fan", "Preset Only Limited Fan",
SUPPORT_PRESET_MODE, SUPPORT_PRESET_MODE,
[PRESET_MODE_AUTO, PRESET_MODE_SMART], [
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
PRESET_MODE_SLEEP,
PRESET_MODE_ON,
],
[], [],
), ),
] ]
@ -99,7 +118,7 @@ class BaseDemoFan(FanEntity):
self._unique_id = unique_id self._unique_id = unique_id
self._supported_features = supported_features self._supported_features = supported_features
self._speed = SPEED_OFF self._speed = SPEED_OFF
self._percentage = 0 self._percentage = None
self._speed_list = speed_list self._speed_list = speed_list
self._preset_modes = preset_modes self._preset_modes = preset_modes
self._preset_mode = None self._preset_mode = None

View File

@ -64,18 +64,22 @@ ATTR_PRESET_MODES = "preset_modes"
# into core integrations at some point so we are temporarily # into core integrations at some point so we are temporarily
# accommodating them in the transition to percentages. # accommodating them in the transition to percentages.
_NOT_SPEED_OFF = "off" _NOT_SPEED_OFF = "off"
_NOT_SPEED_ON = "on"
_NOT_SPEED_AUTO = "auto" _NOT_SPEED_AUTO = "auto"
_NOT_SPEED_SMART = "smart" _NOT_SPEED_SMART = "smart"
_NOT_SPEED_INTERVAL = "interval" _NOT_SPEED_INTERVAL = "interval"
_NOT_SPEED_IDLE = "idle" _NOT_SPEED_IDLE = "idle"
_NOT_SPEED_FAVORITE = "favorite" _NOT_SPEED_FAVORITE = "favorite"
_NOT_SPEED_SLEEP = "sleep"
_NOT_SPEEDS_FILTER = { _NOT_SPEEDS_FILTER = {
_NOT_SPEED_OFF, _NOT_SPEED_OFF,
_NOT_SPEED_ON,
_NOT_SPEED_AUTO, _NOT_SPEED_AUTO,
_NOT_SPEED_SMART, _NOT_SPEED_SMART,
_NOT_SPEED_INTERVAL, _NOT_SPEED_INTERVAL,
_NOT_SPEED_IDLE, _NOT_SPEED_IDLE,
_NOT_SPEED_SLEEP,
_NOT_SPEED_FAVORITE, _NOT_SPEED_FAVORITE,
} }
@ -83,6 +87,8 @@ _FAN_NATIVE = "_fan_native"
OFF_SPEED_VALUES = [SPEED_OFF, None] OFF_SPEED_VALUES = [SPEED_OFF, None]
LEGACY_SPEED_LIST = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
class NoValidSpeedsError(ValueError): class NoValidSpeedsError(ValueError):
"""Exception class when there are no valid speeds.""" """Exception class when there are no valid speeds."""
@ -386,7 +392,10 @@ class FanEntity(ToggleEntity):
if preset_mode: if preset_mode:
return preset_mode return preset_mode
if self._implemented_percentage: if self._implemented_percentage:
return self.percentage_to_speed(self.percentage) percentage = self.percentage
if percentage is None:
return None
return self.percentage_to_speed(percentage)
return None return None
@property @property
@ -404,7 +413,7 @@ class FanEntity(ToggleEntity):
"""Get the list of available speeds.""" """Get the list of available speeds."""
speeds = [] speeds = []
if self._implemented_percentage: if self._implemented_percentage:
speeds += [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] speeds += [SPEED_OFF, *LEGACY_SPEED_LIST]
if self._implemented_preset_mode: if self._implemented_preset_mode:
speeds += self.preset_modes speeds += self.preset_modes
return speeds return speeds
@ -434,6 +443,17 @@ class FanEntity(ToggleEntity):
return attrs return attrs
@property
def _speed_list_without_preset_modes(self) -> list:
"""Return the speed list without preset modes.
This property provides forward and backwards
compatibility for conversion to percentage speeds.
"""
if not self._implemented_speed:
return LEGACY_SPEED_LIST
return speed_list_without_preset_modes(self.speed_list)
def speed_to_percentage(self, speed: str) -> int: def speed_to_percentage(self, speed: str) -> int:
""" """
Map a speed to a percentage. Map a speed to a percentage.
@ -453,7 +473,7 @@ class FanEntity(ToggleEntity):
if speed in OFF_SPEED_VALUES: if speed in OFF_SPEED_VALUES:
return 0 return 0
speed_list = speed_list_without_preset_modes(self.speed_list) speed_list = self._speed_list_without_preset_modes
if speed_list and speed not in speed_list: if speed_list and speed not in speed_list:
raise NotValidSpeedError(f"The speed {speed} is not a valid speed.") raise NotValidSpeedError(f"The speed {speed} is not a valid speed.")
@ -487,7 +507,7 @@ class FanEntity(ToggleEntity):
if percentage == 0: if percentage == 0:
return SPEED_OFF return SPEED_OFF
speed_list = speed_list_without_preset_modes(self.speed_list) speed_list = self._speed_list_without_preset_modes
try: try:
return percentage_to_ordered_list_item(speed_list, percentage) return percentage_to_ordered_list_item(speed_list, percentage)

View File

@ -2,7 +2,12 @@
import pytest import pytest
from homeassistant.components import fan from homeassistant.components import fan
from homeassistant.components.demo.fan import PRESET_MODE_AUTO, PRESET_MODE_SMART from homeassistant.components.demo.fan import (
PRESET_MODE_AUTO,
PRESET_MODE_ON,
PRESET_MODE_SLEEP,
PRESET_MODE_SMART,
)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ENTITY_MATCH_ALL, ENTITY_MATCH_ALL,
@ -60,6 +65,28 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id):
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH
assert state.attributes[fan.ATTR_PERCENTAGE] == 100 assert state.attributes[fan.ATTR_PERCENTAGE] == 100
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_MEDIUM},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_ON
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM
assert state.attributes[fan.ATTR_PERCENTAGE] == 66
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_LOW},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_ON
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW
assert state.attributes[fan.ATTR_PERCENTAGE] == 33
await hass.services.async_call( await hass.services.async_call(
fan.DOMAIN, fan.DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,
@ -71,6 +98,39 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id):
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH
assert state.attributes[fan.ATTR_PERCENTAGE] == 100 assert state.attributes[fan.ATTR_PERCENTAGE] == 100
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 66},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_ON
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM
assert state.attributes[fan.ATTR_PERCENTAGE] == 66
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 33},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_ON
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW
assert state.attributes[fan.ATTR_PERCENTAGE] == 33
await hass.services.async_call(
fan.DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 0},
blocking=True,
)
state = hass.states.get(fan_entity_id)
assert state.state == STATE_OFF
assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF
assert state.attributes[fan.ATTR_PERCENTAGE] == 0
@pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODE_ONLY) @pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODE_ONLY)
async def test_turn_on_with_preset_mode_only(hass, fan_entity_id): async def test_turn_on_with_preset_mode_only(hass, fan_entity_id):
@ -89,6 +149,8 @@ async def test_turn_on_with_preset_mode_only(hass, fan_entity_id):
assert state.attributes[fan.ATTR_PRESET_MODES] == [ assert state.attributes[fan.ATTR_PRESET_MODES] == [
PRESET_MODE_AUTO, PRESET_MODE_AUTO,
PRESET_MODE_SMART, PRESET_MODE_SMART,
PRESET_MODE_SLEEP,
PRESET_MODE_ON,
] ]
await hass.services.async_call( await hass.services.async_call(
@ -145,10 +207,14 @@ async def test_turn_on_with_preset_mode_and_speed(hass, fan_entity_id):
fan.SPEED_HIGH, fan.SPEED_HIGH,
PRESET_MODE_AUTO, PRESET_MODE_AUTO,
PRESET_MODE_SMART, PRESET_MODE_SMART,
PRESET_MODE_SLEEP,
PRESET_MODE_ON,
] ]
assert state.attributes[fan.ATTR_PRESET_MODES] == [ assert state.attributes[fan.ATTR_PRESET_MODES] == [
PRESET_MODE_AUTO, PRESET_MODE_AUTO,
PRESET_MODE_SMART, PRESET_MODE_SMART,
PRESET_MODE_SLEEP,
PRESET_MODE_ON,
] ]
await hass.services.async_call( await hass.services.async_call(