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_SMART = "smart"
PRESET_MODE_SLEEP = "sleep"
PRESET_MODE_ON = "on"
FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION
LIMITED_SUPPORT = SUPPORT_SET_SPEED
@ -38,6 +40,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
SPEED_HIGH,
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
PRESET_MODE_SLEEP,
PRESET_MODE_ON,
],
),
DemoFan(
@ -54,7 +58,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"fan3",
"Percentage Full Fan",
FULL_SUPPORT,
[PRESET_MODE_AUTO, PRESET_MODE_SMART],
[
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
PRESET_MODE_SLEEP,
PRESET_MODE_ON,
],
None,
),
DemoPercentageFan(
@ -62,7 +71,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"fan4",
"Percentage Limited Fan",
LIMITED_SUPPORT,
[PRESET_MODE_AUTO, PRESET_MODE_SMART],
[
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
PRESET_MODE_SLEEP,
PRESET_MODE_ON,
],
None,
),
AsyncDemoPercentageFan(
@ -70,7 +84,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
"fan5",
"Preset Only Limited Fan",
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._supported_features = supported_features
self._speed = SPEED_OFF
self._percentage = 0
self._percentage = None
self._speed_list = speed_list
self._preset_modes = preset_modes
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
# accommodating them in the transition to percentages.
_NOT_SPEED_OFF = "off"
_NOT_SPEED_ON = "on"
_NOT_SPEED_AUTO = "auto"
_NOT_SPEED_SMART = "smart"
_NOT_SPEED_INTERVAL = "interval"
_NOT_SPEED_IDLE = "idle"
_NOT_SPEED_FAVORITE = "favorite"
_NOT_SPEED_SLEEP = "sleep"
_NOT_SPEEDS_FILTER = {
_NOT_SPEED_OFF,
_NOT_SPEED_ON,
_NOT_SPEED_AUTO,
_NOT_SPEED_SMART,
_NOT_SPEED_INTERVAL,
_NOT_SPEED_IDLE,
_NOT_SPEED_SLEEP,
_NOT_SPEED_FAVORITE,
}
@ -83,6 +87,8 @@ _FAN_NATIVE = "_fan_native"
OFF_SPEED_VALUES = [SPEED_OFF, None]
LEGACY_SPEED_LIST = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
class NoValidSpeedsError(ValueError):
"""Exception class when there are no valid speeds."""
@ -386,7 +392,10 @@ class FanEntity(ToggleEntity):
if preset_mode:
return preset_mode
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
@property
@ -404,7 +413,7 @@ class FanEntity(ToggleEntity):
"""Get the list of available speeds."""
speeds = []
if self._implemented_percentage:
speeds += [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
speeds += [SPEED_OFF, *LEGACY_SPEED_LIST]
if self._implemented_preset_mode:
speeds += self.preset_modes
return speeds
@ -434,6 +443,17 @@ class FanEntity(ToggleEntity):
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:
"""
Map a speed to a percentage.
@ -453,7 +473,7 @@ class FanEntity(ToggleEntity):
if speed in OFF_SPEED_VALUES:
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:
raise NotValidSpeedError(f"The speed {speed} is not a valid speed.")
@ -487,7 +507,7 @@ class FanEntity(ToggleEntity):
if percentage == 0:
return SPEED_OFF
speed_list = speed_list_without_preset_modes(self.speed_list)
speed_list = self._speed_list_without_preset_modes
try:
return percentage_to_ordered_list_item(speed_list, percentage)

View file

@ -2,7 +2,12 @@
import pytest
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 (
ATTR_ENTITY_ID,
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_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(
fan.DOMAIN,
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_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)
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] == [
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
PRESET_MODE_SLEEP,
PRESET_MODE_ON,
]
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,
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
PRESET_MODE_SLEEP,
PRESET_MODE_ON,
]
assert state.attributes[fan.ATTR_PRESET_MODES] == [
PRESET_MODE_AUTO,
PRESET_MODE_SMART,
PRESET_MODE_SLEEP,
PRESET_MODE_ON,
]
await hass.services.async_call(