From 95e72b4c4bc757aa100c8281ed0b186945905fa5 Mon Sep 17 00:00:00 2001 From: Eugene Prystupa Date: Mon, 13 Jul 2020 19:00:05 -0400 Subject: [PATCH] Map bond fan speeds to standard HA speeds (#37808) --- homeassistant/components/bond/fan.py | 20 ++++++-- homeassistant/components/bond/utils.py | 9 +++- tests/components/bond/common.py | 13 ++++- tests/components/bond/test_fan.py | 66 +++++++++++++++++++++----- 4 files changed, 88 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index f6792e2c1bb..0d7013b4ccf 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -1,4 +1,5 @@ """Support for Bond fans.""" +import math from typing import Any, Callable, List, Optional from bond import DeviceTypes, Directions @@ -67,12 +68,15 @@ class BondFan(BondEntity, FanEntity): @property def speed(self) -> Optional[str]: """Return the current speed.""" - if self._power is None: - return None if self._power == 0: return SPEED_OFF + if not self._power or not self._speed: + return None - return self.speed_list[self._speed] if self._speed is not None else None + # map 1..max_speed Bond speed to 1..3 HA speed + max_speed = self._device.props.get("max_speed", 3) + ha_speed = math.ceil(self._speed * (len(self.speed_list) - 1) / max_speed) + return self.speed_list[ha_speed] @property def speed_list(self) -> list: @@ -99,8 +103,14 @@ class BondFan(BondEntity, FanEntity): def set_speed(self, speed: str) -> None: """Set the desired speed for the fan.""" - speed_index = self.speed_list.index(speed) - self._hub.bond.setSpeed(self._device.device_id, speed=speed_index) + max_speed = self._device.props.get("max_speed", 3) + if speed == SPEED_LOW: + bond_speed = 1 + elif speed == SPEED_HIGH: + bond_speed = max_speed + else: + bond_speed = math.ceil(max_speed / 2) + self._hub.bond.setSpeed(self._device.device_id, speed=bond_speed) def turn_on(self, speed: Optional[str] = None, **kwargs) -> None: """Turn on the fan.""" diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index d9fcd9b05d6..48fbcd80210 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -8,9 +8,10 @@ from bond import Actions, Bond class BondDevice: """Helper device class to hold ID and attributes together.""" - def __init__(self, device_id: str, attrs: dict): + def __init__(self, device_id: str, attrs: dict, props: dict): """Create a helper device from ID and attributes returned by API.""" self.device_id = device_id + self.props = props self._attrs = attrs @property @@ -67,7 +68,11 @@ class BondHub: """Fetch all available devices using Bond API.""" device_ids = self.bond.getDeviceIds() devices = [ - BondDevice(device_id, self.bond.getDevice(device_id)) + BondDevice( + device_id, + self.bond.getDevice(device_id), + self.bond.getProperties(device_id), + ) for device_id in device_ids ] return devices diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index 04cf3521a31..2a3f727f8bd 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -28,9 +28,16 @@ async def setup_bond_entity( async def setup_platform( - hass: core.HomeAssistant, platform: str, discovered_device: Dict[str, Any] + hass: core.HomeAssistant, + platform: str, + discovered_device: Dict[str, Any], + bond_device_id: str = "bond-device-id", + props: Dict[str, Any] = None, ): """Set up the specified Bond platform.""" + if not props: + props = {} + mock_entry = MockConfigEntry( domain=BOND_DOMAIN, data={CONF_HOST: "1.1.1.1", CONF_ACCESS_TOKEN: "test-token"}, @@ -41,11 +48,13 @@ async def setup_platform( "homeassistant.components.bond.Bond.getVersion", return_value=MOCK_HUB_VERSION ), patch( "homeassistant.components.bond.Bond.getDeviceIds", - return_value=["bond-device-id"], + return_value=[bond_device_id], ), patch( "homeassistant.components.bond.Bond.getDevice", return_value=discovered_device ), patch( "homeassistant.components.bond.Bond.getDeviceState", return_value={} + ), patch( + "homeassistant.components.bond.Bond.getProperties", return_value=props ): assert await async_setup_component(hass, BOND_DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/bond/test_fan.py b/tests/components/bond/test_fan.py index f762b4942d1..0c2df04e2a9 100644 --- a/tests/components/bond/test_fan.py +++ b/tests/components/bond/test_fan.py @@ -7,6 +7,7 @@ from homeassistant import core from homeassistant.components import fan from homeassistant.components.fan import ( ATTR_DIRECTION, + ATTR_SPEED_LIST, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN as FAN_DOMAIN, @@ -16,10 +17,10 @@ from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_O from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.util import utcnow -from ...common import async_fire_time_changed from .common import setup_platform from tests.async_mock import patch +from tests.common import async_fire_time_changed def ceiling_fan(name: str): @@ -31,6 +32,17 @@ def ceiling_fan(name: str): } +async def turn_fan_on(hass: core.HomeAssistant, fan_id: str, speed: str) -> None: + """Turn the fan on at the specified speed.""" + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_id, fan.ATTR_SPEED: speed}, + blocking=True, + ) + await hass.async_block_till_done() + + async def test_entity_registry(hass: core.HomeAssistant): """Tests that the devices are registered in the entity registry.""" await setup_platform(hass, FAN_DOMAIN, ceiling_fan("name-1")) @@ -39,6 +51,43 @@ async def test_entity_registry(hass: core.HomeAssistant): assert [key for key in registry.entities] == ["fan.name_1"] +async def test_entity_non_standard_speed_list(hass: core.HomeAssistant): + """Tests that the device is registered with custom speed list if number of supported speeds differs form 3.""" + await setup_platform( + hass, + FAN_DOMAIN, + ceiling_fan("name-1"), + bond_device_id="test-device-id", + props={"max_speed": 6}, + ) + + actual_speeds = hass.states.get("fan.name_1").attributes[ATTR_SPEED_LIST] + assert actual_speeds == [ + fan.SPEED_OFF, + fan.SPEED_LOW, + fan.SPEED_MEDIUM, + fan.SPEED_HIGH, + ] + + with patch("homeassistant.components.bond.Bond.turnOn"), patch( + "homeassistant.components.bond.Bond.setSpeed" + ) as mock_set_speed_low: + await turn_fan_on(hass, "fan.name_1", fan.SPEED_LOW) + mock_set_speed_low.assert_called_once_with("test-device-id", speed=1) + + with patch("homeassistant.components.bond.Bond.turnOn"), patch( + "homeassistant.components.bond.Bond.setSpeed" + ) as mock_set_speed_medium: + await turn_fan_on(hass, "fan.name_1", fan.SPEED_MEDIUM) + mock_set_speed_medium.assert_called_once_with("test-device-id", speed=3) + + with patch("homeassistant.components.bond.Bond.turnOn"), patch( + "homeassistant.components.bond.Bond.setSpeed" + ) as mock_set_speed_high: + await turn_fan_on(hass, "fan.name_1", fan.SPEED_HIGH) + mock_set_speed_high.assert_called_once_with("test-device-id", speed=6) + + async def test_turn_on_fan(hass: core.HomeAssistant): """Tests that turn on command delegates to API.""" await setup_platform(hass, FAN_DOMAIN, ceiling_fan("name-1")) @@ -46,16 +95,10 @@ async def test_turn_on_fan(hass: core.HomeAssistant): with patch("homeassistant.components.bond.Bond.turnOn") as mock_turn_on, patch( "homeassistant.components.bond.Bond.setSpeed" ) as mock_set_speed: - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.name_1", fan.ATTR_SPEED: fan.SPEED_LOW}, - blocking=True, - ) - await hass.async_block_till_done() + await turn_fan_on(hass, "fan.name_1", fan.SPEED_LOW) - mock_set_speed.assert_called_once() - mock_turn_on.assert_called_once() + mock_set_speed.assert_called_once() + mock_turn_on.assert_called_once() async def test_turn_off_fan(hass: core.HomeAssistant): @@ -67,7 +110,8 @@ async def test_turn_off_fan(hass: core.HomeAssistant): FAN_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "fan.name_1"}, blocking=True, ) await hass.async_block_till_done() - mock_turn_off.assert_called_once() + + mock_turn_off.assert_called_once() async def test_update_reports_fan_on(hass: core.HomeAssistant):