From e9c861f2b2536690c567a2f7d76a5d1c0a37dbe2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 May 2022 09:49:26 -0500 Subject: [PATCH] Add support for cover positions in bond (#72180) --- homeassistant/components/bond/button.py | 14 +++++ homeassistant/components/bond/cover.py | 24 +++++++- homeassistant/components/bond/utils.py | 4 ++ tests/components/bond/test_cover.py | 79 ++++++++++++++++++++++++- 4 files changed, 118 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bond/button.py b/homeassistant/components/bond/button.py index 700c6b5f407..0152bedde23 100644 --- a/homeassistant/components/bond/button.py +++ b/homeassistant/components/bond/button.py @@ -223,6 +223,20 @@ BUTTONS: tuple[BondButtonEntityDescription, ...] = ( mutually_exclusive=Action.OPEN, argument=None, ), + BondButtonEntityDescription( + key=Action.INCREASE_POSITION, + name="Increase Position", + icon="mdi:plus-box", + mutually_exclusive=Action.SET_POSITION, + argument=STEP_SIZE, + ), + BondButtonEntityDescription( + key=Action.DECREASE_POSITION, + name="Decrease Position", + icon="mdi:minus-box", + mutually_exclusive=Action.SET_POSITION, + argument=STEP_SIZE, + ), ) diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py index 664431a3145..a50f7b93bbb 100644 --- a/homeassistant/components/bond/cover.py +++ b/homeassistant/components/bond/cover.py @@ -6,6 +6,7 @@ from typing import Any from bond_api import Action, BPUPSubscriptions, DeviceType from homeassistant.components.cover import ( + ATTR_POSITION, CoverDeviceClass, CoverEntity, CoverEntityFeature, @@ -20,6 +21,16 @@ from .entity import BondEntity from .utils import BondDevice, BondHub +def _bond_to_hass_position(bond_position: int) -> int: + """Convert bond 0-open 100-closed to hass 0-closed 100-open.""" + return abs(bond_position - 100) + + +def _hass_to_bond_position(hass_position: int) -> int: + """Convert hass 0-closed 100-open to bond 0-open 100-closed.""" + return 100 - hass_position + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -50,6 +61,8 @@ class BondCover(BondEntity, CoverEntity): """Create HA entity representing Bond cover.""" super().__init__(hub, device, bpup_subs) supported_features = 0 + if self._device.supports_set_position(): + supported_features |= CoverEntityFeature.SET_POSITION if self._device.supports_open(): supported_features |= CoverEntityFeature.OPEN if self._device.supports_close(): @@ -67,8 +80,15 @@ class BondCover(BondEntity, CoverEntity): def _apply_state(self, state: dict) -> None: cover_open = state.get("open") - self._attr_is_closed = ( - True if cover_open == 0 else False if cover_open == 1 else None + self._attr_is_closed = None if cover_open is None else cover_open == 0 + if (bond_position := state.get("position")) is not None: + self._attr_current_cover_position = _bond_to_hass_position(bond_position) + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Set the cover position.""" + await self._hub.bond.action( + self._device.device_id, + Action.set_position(_hass_to_bond_position(kwargs[ATTR_POSITION])), ) async def async_open_cover(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index 785d7dfbd00..fc78c5758c1 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -82,6 +82,10 @@ class BondDevice: """Return True if this device supports any of the direction related commands.""" return self._has_any_action({Action.SET_DIRECTION}) + def supports_set_position(self) -> bool: + """Return True if this device supports setting the position.""" + return self._has_any_action({Action.SET_POSITION}) + def supports_open(self) -> bool: """Return True if this device supports opening.""" return self._has_any_action({Action.OPEN}) diff --git a/tests/components/bond/test_cover.py b/tests/components/bond/test_cover.py index 7a27617d607..ca467d4a38d 100644 --- a/tests/components/bond/test_cover.py +++ b/tests/components/bond/test_cover.py @@ -4,15 +4,22 @@ from datetime import timedelta from bond_api import Action, DeviceType from homeassistant import core -from homeassistant.components.cover import DOMAIN as COVER_DOMAIN, STATE_CLOSED +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DOMAIN as COVER_DOMAIN, + STATE_CLOSED, +) from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, SERVICE_OPEN_COVER, SERVICE_OPEN_COVER_TILT, + SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER, SERVICE_STOP_COVER_TILT, + STATE_OPEN, STATE_UNKNOWN, ) from homeassistant.helpers import entity_registry as er @@ -38,6 +45,15 @@ def shades(name: str): } +def shades_with_position(name: str): + """Create motorized shades that supports set position.""" + return { + "name": name, + "type": DeviceType.MOTORIZED_SHADES, + "actions": [Action.OPEN, Action.CLOSE, Action.HOLD, Action.SET_POSITION], + } + + def tilt_only_shades(name: str): """Create motorized shades that only tilt.""" return { @@ -236,3 +252,64 @@ async def test_cover_available(hass: core.HomeAssistant): await help_test_entity_available( hass, COVER_DOMAIN, shades("name-1"), "cover.name_1" ) + + +async def test_set_position_cover(hass: core.HomeAssistant): + """Tests that set position cover command delegates to API.""" + await setup_platform( + hass, + COVER_DOMAIN, + shades_with_position("name-1"), + bond_device_id="test-device-id", + ) + + with patch_bond_action() as mock_hold, patch_bond_device_state( + return_value={"position": 0, "open": 1} + ): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.name_1", ATTR_POSITION: 100}, + blocking=True, + ) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + mock_hold.assert_called_once_with("test-device-id", Action.set_position(0)) + entity_state = hass.states.get("cover.name_1") + assert entity_state.state == STATE_OPEN + assert entity_state.attributes[ATTR_CURRENT_POSITION] == 100 + + with patch_bond_action() as mock_hold, patch_bond_device_state( + return_value={"position": 100, "open": 0} + ): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.name_1", ATTR_POSITION: 0}, + blocking=True, + ) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + mock_hold.assert_called_once_with("test-device-id", Action.set_position(100)) + entity_state = hass.states.get("cover.name_1") + assert entity_state.state == STATE_CLOSED + assert entity_state.attributes[ATTR_CURRENT_POSITION] == 0 + + with patch_bond_action() as mock_hold, patch_bond_device_state( + return_value={"position": 40, "open": 1} + ): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.name_1", ATTR_POSITION: 60}, + blocking=True, + ) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + mock_hold.assert_called_once_with("test-device-id", Action.set_position(40)) + entity_state = hass.states.get("cover.name_1") + assert entity_state.state == STATE_OPEN + assert entity_state.attributes[ATTR_CURRENT_POSITION] == 60