From 5d2eb8d3ff6aa4cc1bfd2adb725688c88ac6a871 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Nov 2021 22:31:58 -0500 Subject: [PATCH] Add tilt support to bond covers (#59505) --- homeassistant/components/bond/cover.py | 38 ++++++- homeassistant/components/bond/manifest.json | 2 +- homeassistant/components/bond/utils.py | 20 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bond/test_cover.py | 114 +++++++++++++++++++- 6 files changed, 172 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py index 3a2777b09e8..2c5f0fe66c3 100644 --- a/homeassistant/components/bond/cover.py +++ b/homeassistant/components/bond/cover.py @@ -5,7 +5,16 @@ from typing import Any from bond_api import Action, BPUPSubscriptions, DeviceType -from homeassistant.components.cover import DEVICE_CLASS_SHADE, CoverEntity +from homeassistant.components.cover import ( + DEVICE_CLASS_SHADE, + SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, + SUPPORT_OPEN_TILT, + SUPPORT_STOP, + SUPPORT_STOP_TILT, + CoverEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity @@ -45,6 +54,21 @@ class BondCover(BondEntity, CoverEntity): ) -> None: """Create HA entity representing Bond cover.""" super().__init__(hub, device, bpup_subs) + supported_features = 0 + if self._device.supports_open(): + supported_features |= SUPPORT_OPEN + if self._device.supports_close(): + supported_features |= SUPPORT_CLOSE + if self._device.supports_tilt_open(): + supported_features |= SUPPORT_OPEN_TILT + if self._device.supports_tilt_close(): + supported_features |= SUPPORT_CLOSE_TILT + if self._device.supports_hold(): + if self._device.supports_open() or self._device.supports_close(): + supported_features |= SUPPORT_STOP + if self._device.supports_tilt_open() or self._device.supports_tilt_close(): + supported_features |= SUPPORT_STOP_TILT + self._attr_supported_features = supported_features def _apply_state(self, state: dict) -> None: cover_open = state.get("open") @@ -63,3 +87,15 @@ class BondCover(BondEntity, CoverEntity): async def async_stop_cover(self, **kwargs: Any) -> None: """Hold cover.""" await self._hub.bond.action(self._device.device_id, Action.hold()) + + async def async_open_cover_tilt(self, **kwargs: Any) -> None: + """Open the cover tilt.""" + await self._hub.bond.action(self._device.device_id, Action.tilt_open()) + + async def async_close_cover_tilt(self, **kwargs: Any) -> None: + """Close the cover tilt.""" + await self._hub.bond.action(self._device.device_id, Action.tilt_close()) + + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: + """Stop the cover.""" + await self._hub.bond.action(self._device.device_id, Action.hold()) diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index a8395b68d60..cf5255e84a4 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -3,7 +3,7 @@ "name": "Bond", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bond", - "requirements": ["bond-api==0.1.14"], + "requirements": ["bond-api==0.1.15"], "zeroconf": ["_bond._tcp.local."], "codeowners": ["@bdraco", "@prystupa", "@joshs85"], "quality_scale": "platinum", diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index 4f3de1bf1f0..abe3cc98002 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -82,6 +82,26 @@ class BondDevice: """Return True if this device supports any of the direction related commands.""" return self._has_any_action({Action.SET_DIRECTION}) + def supports_open(self) -> bool: + """Return True if this device supports opening.""" + return self._has_any_action({Action.OPEN}) + + def supports_close(self) -> bool: + """Return True if this device supports closing.""" + return self._has_any_action({Action.CLOSE}) + + def supports_tilt_open(self) -> bool: + """Return True if this device supports tilt opening.""" + return self._has_any_action({Action.TILT_OPEN}) + + def supports_tilt_close(self) -> bool: + """Return True if this device supports tilt closing.""" + return self._has_any_action({Action.TILT_CLOSE}) + + def supports_hold(self) -> bool: + """Return True if this device supports hold aka stop.""" + return self._has_any_action({Action.HOLD}) + def supports_light(self) -> bool: """Return True if this device supports any of the light related commands.""" return self._has_any_action({Action.TURN_LIGHT_ON, Action.TURN_LIGHT_OFF}) diff --git a/requirements_all.txt b/requirements_all.txt index de03be393e0..84adf1ec6cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -418,7 +418,7 @@ blockchain==1.4.4 # bme680==1.0.5 # homeassistant.components.bond -bond-api==0.1.14 +bond-api==0.1.15 # homeassistant.components.bosch_shc boschshcpy==0.2.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 797d667699e..f9902320929 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -263,7 +263,7 @@ blebox_uniapi==1.3.3 blinkpy==0.17.0 # homeassistant.components.bond -bond-api==0.1.14 +bond-api==0.1.15 # homeassistant.components.bosch_shc boschshcpy==0.2.19 diff --git a/tests/components/bond/test_cover.py b/tests/components/bond/test_cover.py index f516d84d50a..7a27617d607 100644 --- a/tests/components/bond/test_cover.py +++ b/tests/components/bond/test_cover.py @@ -4,12 +4,16 @@ from datetime import timedelta from bond_api import Action, DeviceType from homeassistant import core -from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.cover import 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_STOP_COVER, + SERVICE_STOP_COVER_TILT, + STATE_UNKNOWN, ) from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import EntityRegistry @@ -27,7 +31,29 @@ from tests.common import async_fire_time_changed def shades(name: str): """Create motorized shades with given name.""" - return {"name": name, "type": DeviceType.MOTORIZED_SHADES} + return { + "name": name, + "type": DeviceType.MOTORIZED_SHADES, + "actions": ["Open", "Close", "Hold"], + } + + +def tilt_only_shades(name: str): + """Create motorized shades that only tilt.""" + return { + "name": name, + "type": DeviceType.MOTORIZED_SHADES, + "actions": ["TiltOpen", "TiltClose", "Hold"], + } + + +def tilt_shades(name: str): + """Create motorized shades with given name that can also tilt.""" + return { + "name": name, + "type": DeviceType.MOTORIZED_SHADES, + "actions": ["Open", "Close", "Hold", "TiltOpen", "TiltClose", "Hold"], + } async def test_entity_registry(hass: core.HomeAssistant): @@ -99,6 +125,90 @@ async def test_stop_cover(hass: core.HomeAssistant): mock_hold.assert_called_once_with("test-device-id", Action.hold()) +async def test_tilt_open_cover(hass: core.HomeAssistant): + """Tests that tilt open cover command delegates to API.""" + await setup_platform( + hass, COVER_DOMAIN, tilt_only_shades("name-1"), bond_device_id="test-device-id" + ) + + with patch_bond_action() as mock_open, patch_bond_device_state(): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: "cover.name_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_open.assert_called_once_with("test-device-id", Action.tilt_open()) + assert hass.states.get("cover.name_1").state == STATE_UNKNOWN + + +async def test_tilt_close_cover(hass: core.HomeAssistant): + """Tests that tilt close cover command delegates to API.""" + await setup_platform( + hass, COVER_DOMAIN, tilt_only_shades("name-1"), bond_device_id="test-device-id" + ) + + with patch_bond_action() as mock_close, patch_bond_device_state(): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: "cover.name_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_close.assert_called_once_with("test-device-id", Action.tilt_close()) + assert hass.states.get("cover.name_1").state == STATE_UNKNOWN + + +async def test_tilt_stop_cover(hass: core.HomeAssistant): + """Tests that tilt stop cover command delegates to API.""" + await setup_platform( + hass, + COVER_DOMAIN, + tilt_only_shades("name-1"), + bond_device_id="test-device-id", + state={"counter1": 123}, + ) + + with patch_bond_action() as mock_hold, patch_bond_device_state(): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER_TILT, + {ATTR_ENTITY_ID: "cover.name_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_hold.assert_called_once_with("test-device-id", Action.hold()) + assert hass.states.get("cover.name_1").state == STATE_UNKNOWN + + +async def test_tilt_and_open(hass: core.HomeAssistant): + """Tests that supports both tilt and open.""" + await setup_platform( + hass, + COVER_DOMAIN, + tilt_shades("name-1"), + bond_device_id="test-device-id", + state={"open": False}, + ) + + with patch_bond_action() as mock_open, patch_bond_device_state(): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: "cover.name_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_open.assert_called_once_with("test-device-id", Action.tilt_open()) + assert hass.states.get("cover.name_1").state == STATE_CLOSED + + async def test_update_reports_open_cover(hass: core.HomeAssistant): """Tests that update command sets correct state when Bond API reports cover is open.""" await setup_platform(hass, COVER_DOMAIN, shades("name-1"))