diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index 31eceda6c41..9fe33e8e99e 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -12,7 +12,9 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_platform from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -23,6 +25,16 @@ from .utils import BondDevice _LOGGER = logging.getLogger(__name__) +SERVICE_START_INCREASING_BRIGHTNESS = "start_increasing_brightness" +SERVICE_START_DECREASING_BRIGHTNESS = "start_decreasing_brightness" +SERVICE_STOP = "stop" + +ENTITY_SERVICES = [ + SERVICE_START_INCREASING_BRIGHTNESS, + SERVICE_START_DECREASING_BRIGHTNESS, + SERVICE_STOP, +] + async def async_setup_entry( hass: HomeAssistant, @@ -34,6 +46,14 @@ async def async_setup_entry( hub: BondHub = data[HUB] bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] + platform = entity_platform.async_get_current_platform() + for service in ENTITY_SERVICES: + platform.async_register_entity_service( + service, + {}, + f"async_{service}", + ) + fan_lights: list[Entity] = [ BondLight(hub, device, bpup_subs) for device in hub.devices @@ -119,6 +139,31 @@ class BondLight(BondBaseLight, BondEntity, LightEntity): """Turn off the light.""" await self._hub.bond.action(self._device.device_id, Action.turn_light_off()) + @callback + def _async_has_action_or_raise(self, action: str) -> None: + """Raise HomeAssistantError if the device does not support an action.""" + if not self._device.has_action(action): + raise HomeAssistantError(f"{self.entity_id} does not support {action}") + + async def async_start_increasing_brightness(self) -> None: + """Start increasing the light brightness.""" + self._async_has_action_or_raise(Action.START_INCREASING_BRIGHTNESS) + await self._hub.bond.action( + self._device.device_id, Action(Action.START_INCREASING_BRIGHTNESS) + ) + + async def async_start_decreasing_brightness(self) -> None: + """Start decreasing the light brightness.""" + self._async_has_action_or_raise(Action.START_DECREASING_BRIGHTNESS) + await self._hub.bond.action( + self._device.device_id, Action(Action.START_DECREASING_BRIGHTNESS) + ) + + async def async_stop(self) -> None: + """Stop all actions and clear the queue.""" + self._async_has_action_or_raise(Action.STOP) + await self._hub.bond.action(self._device.device_id, Action(Action.STOP)) + class BondDownLight(BondBaseLight, BondEntity, LightEntity): """Representation of a Bond light.""" diff --git a/homeassistant/components/bond/services.yaml b/homeassistant/components/bond/services.yaml new file mode 100644 index 00000000000..1cb24c5ed71 --- /dev/null +++ b/homeassistant/components/bond/services.yaml @@ -0,0 +1,23 @@ +start_increasing_brightness: + name: Start increasing brightness + description: "Start increasing the brightness of the light." + target: + entity: + integration: bond + domain: light + +start_decreasing_brightness: + name: Start decreasing brightness + description: "Start decreasing the brightness of the light." + target: + entity: + integration: bond + domain: light + +stop: + name: Stop + description: "Stop any in-progress action and empty the queue." + target: + entity: + integration: bond + domain: light diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index 6ace83831fe..4f3de1bf1f0 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -25,7 +25,8 @@ class BondDevice: """Create a helper device from ID and attributes returned by API.""" self.device_id = device_id self.props = props - self._attrs = attrs + self._attrs = attrs or {} + self._supported_actions: set[str] = set(self._attrs.get("actions", [])) def __repr__(self) -> str: """Return readable representation of a bond device.""" @@ -65,13 +66,13 @@ class BondDevice: """Check if Trust State is turned on.""" return self.props.get("trust_state", False) + def has_action(self, action: str) -> bool: + """Check to see if the device supports an actions.""" + return action in self._supported_actions + def _has_any_action(self, actions: set[str]) -> bool: """Check to see if the device supports any of the actions.""" - supported_actions: list[str] = self._attrs["actions"] - for action in supported_actions: - if action in actions: - return True - return False + return bool(self._supported_actions.intersection(actions)) def supports_speed(self) -> bool: """Return True if this device supports any of the speed related commands.""" diff --git a/tests/components/bond/test_light.py b/tests/components/bond/test_light.py index e59efcd7bcf..545feee21a5 100644 --- a/tests/components/bond/test_light.py +++ b/tests/components/bond/test_light.py @@ -2,8 +2,15 @@ from datetime import timedelta from bond_api import Action, DeviceType +import pytest from homeassistant import core +from homeassistant.components.bond.const import DOMAIN +from homeassistant.components.bond.light import ( + SERVICE_START_DECREASING_BRIGHTNESS, + SERVICE_START_INCREASING_BRIGHTNESS, + SERVICE_STOP, +) from homeassistant.components.light import ( ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN, @@ -16,6 +23,7 @@ from homeassistant.const import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, ) +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.util import utcnow @@ -98,6 +106,21 @@ def fireplace_with_light(name: str): } +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, + ], + } + + async def test_fan_entity_registry(hass: core.HomeAssistant): """Tests that fan with light devices are registered in the entity registry.""" await setup_platform( @@ -231,6 +254,133 @@ async def test_no_trust_state(hass: core.HomeAssistant): assert device.attributes.get(ATTR_ASSUMED_STATE) is not True +async def test_light_start_increasing_brightness(hass: core.HomeAssistant): + """Tests a light that can only increase or decrease brightness delegates to API can start increasing brightness.""" + await setup_platform( + hass, + LIGHT_DOMAIN, + light_brightness_increase_decrease_only("name-1"), + bond_device_id="test-device-id", + ) + + with patch_bond_action() as mock_bond_action, patch_bond_device_state(): + await hass.services.async_call( + DOMAIN, + SERVICE_START_INCREASING_BRIGHTNESS, + {ATTR_ENTITY_ID: "light.name_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_bond_action.assert_called_once_with( + "test-device-id", Action(Action.START_INCREASING_BRIGHTNESS) + ) + + +async def test_light_start_increasing_brightness_missing_service( + hass: core.HomeAssistant, +): + """Tests a light does not have start increasing brightness throws.""" + await setup_platform( + hass, LIGHT_DOMAIN, light("name-1"), bond_device_id="test-device-id" + ) + + with pytest.raises(HomeAssistantError), patch_bond_device_state(): + await hass.services.async_call( + DOMAIN, + SERVICE_START_INCREASING_BRIGHTNESS, + {ATTR_ENTITY_ID: "light.name_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + +async def test_light_start_decreasing_brightness(hass: core.HomeAssistant): + """Tests a light that can only increase or decrease brightness delegates to API can start decreasing brightness.""" + await setup_platform( + hass, + LIGHT_DOMAIN, + light_brightness_increase_decrease_only("name-1"), + bond_device_id="test-device-id", + ) + + with patch_bond_action() as mock_bond_action, patch_bond_device_state(): + await hass.services.async_call( + DOMAIN, + SERVICE_START_DECREASING_BRIGHTNESS, + {ATTR_ENTITY_ID: "light.name_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_bond_action.assert_called_once_with( + "test-device-id", Action(Action.START_DECREASING_BRIGHTNESS) + ) + + +async def test_light_start_decreasing_brightness_missing_service( + hass: core.HomeAssistant, +): + """Tests a light does not have start decreasing brightness throws.""" + await setup_platform( + hass, + LIGHT_DOMAIN, + light("name-1"), + bond_device_id="test-device-id", + ) + + with pytest.raises(HomeAssistantError), patch_bond_device_state(): + await hass.services.async_call( + DOMAIN, + SERVICE_START_DECREASING_BRIGHTNESS, + {ATTR_ENTITY_ID: "light.name_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + +async def test_light_stop(hass: core.HomeAssistant): + """Tests a light that can only increase or decrease brightness delegates to API can stop.""" + await setup_platform( + hass, + LIGHT_DOMAIN, + light_brightness_increase_decrease_only("name-1"), + bond_device_id="test-device-id", + ) + + with patch_bond_action() as mock_bond_action, patch_bond_device_state(): + await hass.services.async_call( + DOMAIN, + SERVICE_STOP, + {ATTR_ENTITY_ID: "light.name_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_bond_action.assert_called_once_with("test-device-id", Action(Action.STOP)) + + +async def test_light_stop_missing_service( + hass: core.HomeAssistant, +): + """Tests a light does not have stop throws.""" + await setup_platform( + hass, + LIGHT_DOMAIN, + light("name-1"), + bond_device_id="test-device-id", + ) + + with pytest.raises(HomeAssistantError), patch_bond_device_state(): + await hass.services.async_call( + DOMAIN, + SERVICE_STOP, + {ATTR_ENTITY_ID: "light.name_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + async def test_turn_on_light(hass: core.HomeAssistant): """Tests that turn on command delegates to API.""" await setup_platform(