From 15532c38d719a219029be93f9e258501d728bfc5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Jan 2022 18:52:00 -1000 Subject: [PATCH] Add button platform to bond to replace custom services (#64725) --- homeassistant/components/bond/__init__.py | 1 + homeassistant/components/bond/button.py | 238 ++++++++++++++++++++++ homeassistant/components/bond/entity.py | 10 +- tests/components/bond/test_button.py | 114 +++++++++++ 4 files changed, 361 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/bond/button.py create mode 100644 tests/components/bond/test_button.py diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index bcc8e5d5be1..062c1d844c4 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -24,6 +24,7 @@ from .const import BPUP_SUBS, BRIDGE_MAKE, DOMAIN, HUB from .utils import BondHub PLATFORMS = [ + Platform.BUTTON, Platform.COVER, Platform.FAN, Platform.LIGHT, diff --git a/homeassistant/components/bond/button.py b/homeassistant/components/bond/button.py new file mode 100644 index 00000000000..96aa051b2fa --- /dev/null +++ b/homeassistant/components/bond/button.py @@ -0,0 +1,238 @@ +"""Support for bond buttons.""" +from __future__ import annotations + +from dataclasses import dataclass +import logging +from typing import Any + +from bond_api import Action, BPUPSubscriptions + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import BPUP_SUBS, DOMAIN, HUB +from .entity import BondEntity +from .utils import BondDevice, BondHub + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class BondButtonEntityDescriptionMixin: + """Mixin to describe a Bond Button entity.""" + + mutually_exclusive: Action | None + + +@dataclass +class BondButtonEntityDescription( + ButtonEntityDescription, BondButtonEntityDescriptionMixin +): + """Class to describe a Bond Button entity.""" + + +BUTTONS: tuple[BondButtonEntityDescription, ...] = ( + BondButtonEntityDescription( + key=Action.STOP, + name="Stop Actions", + icon="mdi:stop-circle-outline", + mutually_exclusive=None, + ), + BondButtonEntityDescription( + key=Action.TOGGLE_POWER, + name="Toggle Power", + icon="mdi:power-cycle", + mutually_exclusive=Action.TURN_ON, + ), + BondButtonEntityDescription( + key=Action.TOGGLE_LIGHT, + name="Toggle Light", + icon="mdi:lightbulb", + mutually_exclusive=Action.TURN_LIGHT_ON, + ), + BondButtonEntityDescription( + key=Action.INCREASE_BRIGHTNESS, + name="Increase Brightness", + icon="mdi:brightness-7", + mutually_exclusive=Action.SET_BRIGHTNESS, + ), + BondButtonEntityDescription( + key=Action.DECREASE_BRIGHTNESS, + name="Decrease Brightness", + icon="mdi:brightness-1", + mutually_exclusive=Action.SET_BRIGHTNESS, + ), + BondButtonEntityDescription( + key=Action.TOGGLE_UP_LIGHT, + name="Toggle Up Light", + icon="mdi:lightbulb", + mutually_exclusive=Action.TURN_UP_LIGHT_ON, + ), + BondButtonEntityDescription( + key=Action.TOGGLE_DOWN_LIGHT, + name="Toggle Down Light", + icon="mdi:lightbulb", + mutually_exclusive=Action.TURN_DOWN_LIGHT_ON, + ), + BondButtonEntityDescription( + key=Action.START_UP_LIGHT_DIMMER, + name="Start Up Light Dimmer", + icon="mdi:brightness-percent", + mutually_exclusive=Action.SET_UP_LIGHT_BRIGHTNESS, + ), + BondButtonEntityDescription( + key=Action.START_DOWN_LIGHT_DIMMER, + name="Start Down Light Dimmer", + icon="mdi:brightness-percent", + mutually_exclusive=Action.SET_DOWN_LIGHT_BRIGHTNESS, + ), + BondButtonEntityDescription( + key=Action.START_INCREASING_BRIGHTNESS, + name="Start Increasing Brightness", + icon="mdi:brightness-percent", + mutually_exclusive=Action.SET_BRIGHTNESS, + ), + BondButtonEntityDescription( + key=Action.START_DECREASING_BRIGHTNESS, + name="Start Decreasing Brightness", + icon="mdi:brightness-percent", + mutually_exclusive=Action.SET_BRIGHTNESS, + ), + BondButtonEntityDescription( + key=Action.INCREASE_UP_LIGHT_BRIGHTNESS, + name="Increase Up Light Brightness", + icon="mdi:brightness-percent", + mutually_exclusive=Action.SET_UP_LIGHT_BRIGHTNESS, + ), + BondButtonEntityDescription( + key=Action.DECREASE_UP_LIGHT_BRIGHTNESS, + name="Decrease Up Light Brightness", + icon="mdi:brightness-percent", + mutually_exclusive=Action.SET_UP_LIGHT_BRIGHTNESS, + ), + BondButtonEntityDescription( + key=Action.INCREASE_DOWN_LIGHT_BRIGHTNESS, + name="Increase Down Light Brightness", + icon="mdi:brightness-percent", + mutually_exclusive=Action.SET_DOWN_LIGHT_BRIGHTNESS, + ), + BondButtonEntityDescription( + key=Action.DECREASE_DOWN_LIGHT_BRIGHTNESS, + name="Decrease Down Light Brightness", + icon="mdi:brightness-percent", + mutually_exclusive=Action.SET_DOWN_LIGHT_BRIGHTNESS, + ), + BondButtonEntityDescription( + key=Action.CYCLE_UP_LIGHT_BRIGHTNESS, + name="Cycle Up Light Brightness", + icon="mdi:brightness-percent", + mutually_exclusive=Action.SET_UP_LIGHT_BRIGHTNESS, + ), + BondButtonEntityDescription( + key=Action.CYCLE_DOWN_LIGHT_BRIGHTNESS, + name="Cycle Down Light Brightness", + icon="mdi:brightness-percent", + mutually_exclusive=Action.SET_DOWN_LIGHT_BRIGHTNESS, + ), + BondButtonEntityDescription( + key=Action.CYCLE_BRIGHTNESS, + name="Cycle Brightness", + icon="mdi:brightness-percent", + mutually_exclusive=Action.SET_BRIGHTNESS, + ), + BondButtonEntityDescription( + key=Action.INCREASE_SPEED, + name="Increase Speed", + icon="mdi:skew-more", + mutually_exclusive=Action.SET_SPEED, + ), + BondButtonEntityDescription( + key=Action.DECREASE_SPEED, + name="Decrease Speed", + icon="mdi:skew-less", + mutually_exclusive=Action.SET_SPEED, + ), + BondButtonEntityDescription( + key=Action.TOGGLE_DIRECTION, + name="Toggle Direction", + icon="mdi:directions-fork", + mutually_exclusive=Action.SET_DIRECTION, + ), + BondButtonEntityDescription( + key=Action.INCREASE_TEMPERATURE, + name="Increase Temperature", + icon="mdi:thermometer-plus", + mutually_exclusive=None, + ), + BondButtonEntityDescription( + key=Action.DECREASE_TEMPERATURE, + name="Decrease Temperature", + icon="mdi:thermometer-minus", + mutually_exclusive=None, + ), + BondButtonEntityDescription( + key=Action.INCREASE_FLAME, + name="Increase Flame", + icon="mdi:fire", + mutually_exclusive=None, + ), + BondButtonEntityDescription( + key=Action.DECREASE_FLAME, + name="Decrease Flame", + icon="mdi:fire-off", + mutually_exclusive=None, + ), + BondButtonEntityDescription( + key=Action.TOGGLE_OPEN, name="Toggle Open", mutually_exclusive=Action.OPEN + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Bond button devices.""" + data = hass.data[DOMAIN][entry.entry_id] + hub: BondHub = data[HUB] + bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] + + async_add_entities( + BondButtonEntity(hub, device, bpup_subs, description) + for device in hub.devices + for description in BUTTONS + if device.has_action(description.key) + and ( + description.mutually_exclusive is None + or not device.has_action(description.mutually_exclusive) + ) + ) + + +class BondButtonEntity(BondEntity, ButtonEntity): + """Bond Button Device.""" + + def __init__( + self, + hub: BondHub, + device: BondDevice, + bpup_subs: BPUPSubscriptions, + description: ButtonEntityDescription, + ) -> None: + """Init Bond button.""" + super().__init__( + hub, device, bpup_subs, description.name, description.key.lower() + ) + self.entity_description = description + + async def async_press(self, **kwargs: Any) -> None: + """Press the button.""" + await self._hub.bond.action( + self._device.device_id, Action(self.entity_description.key) + ) + + def _apply_state(self, state: dict) -> None: + """Apply the state.""" diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index 1beca4895e2..583f1cd96f7 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -41,6 +41,7 @@ class BondEntity(Entity): device: BondDevice, bpup_subs: BPUPSubscriptions, sub_device: str | None = None, + sub_device_id: str | None = None, ) -> None: """Initialize entity with API and device info.""" self._hub = hub @@ -51,7 +52,12 @@ class BondEntity(Entity): self._bpup_subs = bpup_subs self._update_lock: Lock | None = None self._initialized = False - sub_device_id: str = f"_{sub_device}" if sub_device else "" + if sub_device_id: + sub_device_id = f"_{sub_device_id}" + elif sub_device: + sub_device_id = f"_{sub_device}" + else: + sub_device_id = "" self._attr_unique_id = f"{hub.bond_id}_{device.device_id}{sub_device_id}" if sub_device: sub_device_name = sub_device.replace("_", " ").title() @@ -69,7 +75,7 @@ class BondEntity(Entity): configuration_url=f"http://{self._hub.host}", ) if self.name is not None: - device_info[ATTR_NAME] = self.name + device_info[ATTR_NAME] = self._device.name if self._hub.bond_id is not None: device_info[ATTR_VIA_DEVICE] = (DOMAIN, self._hub.bond_id) if self._device.location is not None: diff --git a/tests/components/bond/test_button.py b/tests/components/bond/test_button.py new file mode 100644 index 00000000000..5cae149f34a --- /dev/null +++ b/tests/components/bond/test_button.py @@ -0,0 +1,114 @@ +"""Tests for the Bond button device.""" + +from bond_api import Action, DeviceType + +from homeassistant import core +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_registry import EntityRegistry + +from .common import patch_bond_action, patch_bond_device_state, setup_platform + + +def light_brightness_increase_decrease_only(name: str): + """Create a light that can only increase or decrease brightness.""" + return { + "name": name, + "type": DeviceType.LIGHT, + "actions": [ + Action.TURN_LIGHT_ON, + Action.TURN_LIGHT_OFF, + Action.START_INCREASING_BRIGHTNESS, + Action.START_DECREASING_BRIGHTNESS, + Action.STOP, + ], + } + + +def fireplace_increase_decrease_only(name: str): + """Create a fireplace that can only increase or decrease flame.""" + return { + "name": name, + "type": DeviceType.LIGHT, + "actions": [ + Action.INCREASE_FLAME, + Action.DECREASE_FLAME, + ], + } + + +def light(name: str): + """Create a light with a given name.""" + return { + "name": name, + "type": DeviceType.LIGHT, + "actions": [Action.TURN_LIGHT_ON, Action.TURN_LIGHT_OFF, Action.SET_BRIGHTNESS], + } + + +async def test_entity_registry(hass: core.HomeAssistant): + """Tests that the devices are registered in the entity registry.""" + await setup_platform( + hass, + BUTTON_DOMAIN, + light_brightness_increase_decrease_only("name-1"), + bond_version={"bondid": "test-hub-id"}, + bond_device_id="test-device-id", + ) + + registry: EntityRegistry = er.async_get(hass) + entity = registry.entities["button.name_1_stop_actions"] + assert entity.unique_id == "test-hub-id_test-device-id_stop" + entity = registry.entities["button.name_1_start_increasing_brightness"] + assert entity.unique_id == "test-hub-id_test-device-id_startincreasingbrightness" + entity = registry.entities["button.name_1_start_decreasing_brightness"] + assert entity.unique_id == "test-hub-id_test-device-id_startdecreasingbrightness" + + +async def test_mutually_exclusive_actions(hass: core.HomeAssistant): + """Tests we do not create the button when there is a mutually exclusive action.""" + await setup_platform( + hass, + BUTTON_DOMAIN, + light("name-1"), + bond_device_id="test-device-id", + ) + + assert not hass.states.async_all("button") + + +async def test_press_button(hass: core.HomeAssistant): + """Tests we can press a button.""" + await setup_platform( + hass, + BUTTON_DOMAIN, + fireplace_increase_decrease_only("name-1"), + bond_device_id="test-device-id", + ) + + assert hass.states.get("button.name_1_increase_flame") + assert hass.states.get("button.name_1_decrease_flame") + + with patch_bond_action() as mock_action, patch_bond_device_state(): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.name_1_increase_flame"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_action.assert_called_once_with("test-device-id", Action(Action.INCREASE_FLAME)) + + with patch_bond_action() as mock_action, patch_bond_device_state(): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.name_1_decrease_flame"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_action.assert_called_once_with("test-device-id", Action(Action.DECREASE_FLAME))