diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 7e0e50c3d61..52c1bc6117a 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -19,6 +19,7 @@ from homeassistant.components.light import ( COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_XY, + FLASH_SHORT, SUPPORT_FLASH, SUPPORT_TRANSITION, LightEntity, @@ -37,7 +38,6 @@ ALLOWED_ERRORS = [ 'device (groupedLight) is "soft off", command (on) may not have effect', "device (light) has communication issues, command (on) may not have effect", 'device (light) is "soft off", command (on) may not have effect', - "attribute (supportedAlertActions) cannot be written", ] @@ -155,6 +155,11 @@ class GroupedHueLight(HueBaseEntity, LightEntity): brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS)) flash = kwargs.get(ATTR_FLASH) + if flash is not None: + await self.async_set_flash(flash) + # flash can not be sent with other commands at the same time + return + # NOTE: a grouped_light can only handle turn on/off # To set other features, you'll have to control the attached lights if ( @@ -194,6 +199,12 @@ class GroupedHueLight(HueBaseEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) + flash = kwargs.get(ATTR_FLASH) + + if flash is not None: + await self.async_set_flash(flash) + # flash can not be sent with other commands at the same time + return # NOTE: a grouped_light can only handle turn on/off # To set other features, you'll have to control the attached lights @@ -220,6 +231,19 @@ class GroupedHueLight(HueBaseEntity, LightEntity): ] ) + async def async_set_flash(self, flash: str) -> None: + """Send flash command to light.""" + await asyncio.gather( + *[ + self.bridge.async_request_call( + self.api.lights.set_flash, + id=light.id, + short=flash == FLASH_SHORT, + ) + for light in self.controller.get_lights(self.resource.id) + ] + ) + @callback def on_update(self) -> None: """Call on update event.""" diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index d6578c8ef9a..91afbe53e45 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -6,7 +6,6 @@ from typing import Any from aiohue import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.lights import LightsController -from aiohue.v2.models.feature import AlertEffectType from aiohue.v2.models.light import Light from homeassistant.components.light import ( @@ -19,6 +18,7 @@ from homeassistant.components.light import ( COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_XY, + FLASH_SHORT, SUPPORT_FLASH, SUPPORT_TRANSITION, LightEntity, @@ -35,7 +35,6 @@ from .helpers import normalize_hue_brightness, normalize_hue_transition ALLOWED_ERRORS = [ "device (light) has communication issues, command (on) may not have effect", 'device (light) is "soft off", command (on) may not have effect', - "attribute (supportedAlertActions) cannot be written", ] @@ -73,7 +72,8 @@ class HueLight(HueBaseEntity, LightEntity): ) -> None: """Initialize the light.""" super().__init__(bridge, controller, resource) - self._attr_supported_features |= SUPPORT_FLASH + if self.resource.alert and self.resource.alert.action_values: + self._attr_supported_features |= SUPPORT_FLASH self.resource = resource self.controller = controller self._supported_color_modes = set() @@ -162,6 +162,14 @@ class HueLight(HueBaseEntity, LightEntity): brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS)) flash = kwargs.get(ATTR_FLASH) + if flash is not None: + await self.async_set_flash(flash) + # flash can not be sent with other commands at the same time or result will be flaky + # Hue's default behavior is that a light returns to its previous state for short + # flash (identify) and the light is kept turned on for long flash (breathe effect) + # Why is this flash alert/effect hidden in the turn_on/off commands ? + return + await self.bridge.async_request_call( self.controller.set_state, id=self.resource.id, @@ -170,7 +178,6 @@ class HueLight(HueBaseEntity, LightEntity): color_xy=xy_color, color_temp=color_temp, transition_time=transition, - alert=AlertEffectType.BREATHE if flash is not None else None, allowed_errors=ALLOWED_ERRORS, ) @@ -179,11 +186,25 @@ class HueLight(HueBaseEntity, LightEntity): transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) flash = kwargs.get(ATTR_FLASH) + if flash is not None: + await self.async_set_flash(flash) + # flash can not be sent with other commands at the same time or result will be flaky + # Hue's default behavior is that a light returns to its previous state for short + # flash (identify) and the light is kept turned on for long flash (breathe effect) + return + await self.bridge.async_request_call( self.controller.set_state, id=self.resource.id, on=False, transition_time=transition, - alert=AlertEffectType.BREATHE if flash is not None else None, allowed_errors=ALLOWED_ERRORS, ) + + async def async_set_flash(self, flash: str) -> None: + """Send flash command to light.""" + await self.bridge.async_request_call( + self.controller.set_flash, + id=self.resource.id, + short=flash == FLASH_SHORT, + ) diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index 936da661dd7..970497214c5 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -121,7 +121,7 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is True assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 200 - # test again with sending flash/alert + # test again with sending long flash await hass.services.async_call( "light", "turn_on", @@ -129,9 +129,18 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat blocking=True, ) assert len(mock_bridge_v2.mock_requests) == 3 - assert mock_bridge_v2.mock_requests[2]["json"]["on"]["on"] is True assert mock_bridge_v2.mock_requests[2]["json"]["alert"]["action"] == "breathe" + # test again with sending short flash + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "flash": "short"}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 4 + assert mock_bridge_v2.mock_requests[3]["json"]["identify"]["action"] == "identify" + async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data): """Test calling the turn off service on a light.""" @@ -177,6 +186,26 @@ async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_da assert mock_bridge_v2.mock_requests[1]["json"]["on"]["on"] is False assert mock_bridge_v2.mock_requests[1]["json"]["dynamics"]["duration"] == 200 + # test again with sending long flash + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": test_light_id, "flash": "long"}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 3 + assert mock_bridge_v2.mock_requests[2]["json"]["alert"]["action"] == "breathe" + + # test again with sending short flash + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": test_light_id, "flash": "short"}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 4 + assert mock_bridge_v2.mock_requests[3]["json"]["identify"]["action"] == "identify" + async def test_light_added(hass, mock_bridge_v2): """Test new light added to bridge.""" @@ -386,3 +415,65 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): assert ( mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 200 ) + + # Test sending short flash effect to a grouped light + mock_bridge_v2.mock_requests.clear() + test_light_id = "light.test_zone" + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": test_light_id, + "flash": "short", + }, + blocking=True, + ) + + # PUT request should have been sent to ALL group lights with correct params + assert len(mock_bridge_v2.mock_requests) == 3 + for index in range(0, 3): + assert ( + mock_bridge_v2.mock_requests[index]["json"]["identify"]["action"] + == "identify" + ) + + # Test sending long flash effect to a grouped light + mock_bridge_v2.mock_requests.clear() + test_light_id = "light.test_zone" + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": test_light_id, + "flash": "long", + }, + blocking=True, + ) + + # PUT request should have been sent to ALL group lights with correct params + assert len(mock_bridge_v2.mock_requests) == 3 + for index in range(0, 3): + assert ( + mock_bridge_v2.mock_requests[index]["json"]["alert"]["action"] == "breathe" + ) + + # Test sending flash effect in turn_off call + mock_bridge_v2.mock_requests.clear() + test_light_id = "light.test_zone" + await hass.services.async_call( + "light", + "turn_off", + { + "entity_id": test_light_id, + "flash": "short", + }, + blocking=True, + ) + + # PUT request should have been sent to ALL group lights with correct params + assert len(mock_bridge_v2.mock_requests) == 3 + for index in range(0, 3): + assert ( + mock_bridge_v2.mock_requests[index]["json"]["identify"]["action"] + == "identify" + )