diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 4a960a6c884..7005fce63cc 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -87,6 +87,7 @@ VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)) VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) VALID_BRIGHTNESS_STEP = vol.All(vol.Coerce(int), vol.Clamp(min=-255, max=255)) VALID_BRIGHTNESS_STEP_PCT = vol.All(vol.Coerce(float), vol.Clamp(min=-100, max=100)) +VALID_FLASH = vol.In([FLASH_SHORT, FLASH_LONG]) LIGHT_TURN_ON_SCHEMA = { vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string, @@ -116,7 +117,7 @@ LIGHT_TURN_ON_SCHEMA = { ), vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): vol.All(vol.Coerce(int), vol.Range(min=0)), ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), - ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), + ATTR_FLASH: VALID_FLASH, ATTR_EFFECT: cv.string, } @@ -252,10 +253,7 @@ async def async_setup(hass, config): component.async_register_entity_service( SERVICE_TURN_OFF, - { - ATTR_TRANSITION: VALID_TRANSITION, - ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), - }, + {ATTR_TRANSITION: VALID_TRANSITION, ATTR_FLASH: VALID_FLASH}, "async_turn_off", ) diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index 5c534cc4150..d499bc0c2a2 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -4,6 +4,13 @@ from typing import List import voluptuous as vol from homeassistant.components.device_automation import toggle_entity +from homeassistant.components.light import ( + ATTR_FLASH, + FLASH_SHORT, + SUPPORT_FLASH, + VALID_BRIGHTNESS_PCT, + VALID_FLASH, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -19,6 +26,7 @@ from . import ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS_STEP_PCT, DOMAIN, SUPPORT_BRI TYPE_BRIGHTNESS_INCREASE = "brightness_increase" TYPE_BRIGHTNESS_DECREASE = "brightness_decrease" +TYPE_FLASH = "flash" ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { @@ -26,11 +34,10 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( vol.Required(CONF_DOMAIN): DOMAIN, vol.Required(CONF_TYPE): vol.In( toggle_entity.DEVICE_ACTION_TYPES - + [TYPE_BRIGHTNESS_INCREASE, TYPE_BRIGHTNESS_DECREASE] - ), - vol.Optional(ATTR_BRIGHTNESS_PCT): vol.All( - vol.Coerce(int), vol.Range(min=0, max=100) + + [TYPE_BRIGHTNESS_INCREASE, TYPE_BRIGHTNESS_DECREASE, TYPE_FLASH] ), + vol.Optional(ATTR_BRIGHTNESS_PCT): VALID_BRIGHTNESS_PCT, + vol.Optional(ATTR_FLASH): VALID_FLASH, } ) @@ -60,6 +67,12 @@ async def async_call_action_from_config( elif ATTR_BRIGHTNESS_PCT in config: data[ATTR_BRIGHTNESS_PCT] = config[ATTR_BRIGHTNESS_PCT] + if config[CONF_TYPE] == TYPE_FLASH: + if ATTR_FLASH in config: + data[ATTR_FLASH] = config[ATTR_FLASH] + else: + data[ATTR_FLASH] = FLASH_SHORT + await hass.services.async_call( DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=context ) @@ -100,6 +113,18 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: ) ) + if supported_features & SUPPORT_FLASH: + actions.extend( + ( + { + CONF_TYPE: TYPE_FLASH, + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + }, + ) + ) + return actions @@ -119,15 +144,12 @@ async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> di elif entry: supported_features = entry.supported_features - if not supported_features & SUPPORT_BRIGHTNESS: - return {} + extra_fields = {} - return { - "extra_fields": vol.Schema( - { - vol.Optional(ATTR_BRIGHTNESS_PCT): vol.All( - vol.Coerce(int), vol.Range(min=0, max=100) - ) - } - ) - } + if supported_features & SUPPORT_BRIGHTNESS: + extra_fields[vol.Optional(ATTR_BRIGHTNESS_PCT)] = VALID_BRIGHTNESS_PCT + + if supported_features & SUPPORT_FLASH: + extra_fields[vol.Optional(ATTR_FLASH)] = VALID_FLASH + + return {"extra_fields": vol.Schema(extra_fields)} if extra_fields else {} diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 922a4957afd..fc089e64b36 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -5,7 +5,8 @@ "brightness_increase": "Increase {entity_name} brightness", "toggle": "Toggle {entity_name}", "turn_on": "Turn on {entity_name}", - "turn_off": "Turn off {entity_name}" + "turn_off": "Turn off {entity_name}", + "flash": "Flash {entity_name}" }, "condition_type": { "is_on": "{entity_name} is on", diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 6cddfc15744..116aff4ee78 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -2,7 +2,13 @@ import pytest import homeassistant.components.automation as automation -from homeassistant.components.light import DOMAIN, SUPPORT_BRIGHTNESS +from homeassistant.components.light import ( + DOMAIN, + FLASH_LONG, + FLASH_SHORT, + SUPPORT_BRIGHTNESS, + SUPPORT_FLASH, +) from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -48,7 +54,7 @@ async def test_get_actions(hass, device_reg, entity_reg): "test", "5678", device_id=device_entry.id, - supported_features=SUPPORT_BRIGHTNESS, + supported_features=SUPPORT_BRIGHTNESS | SUPPORT_FLASH, ) expected_actions = [ { @@ -81,6 +87,12 @@ async def test_get_actions(hass, device_reg, entity_reg): "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, + { + "domain": DOMAIN, + "type": "flash", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, ] actions = await async_get_device_automations(hass, "action", device_entry.id) assert actions == expected_actions @@ -128,7 +140,7 @@ async def test_get_action_capabilities_brightness(hass, device_reg, entity_reg): { "name": "brightness_pct", "optional": True, - "type": "integer", + "type": "float", "valueMax": 100, "valueMin": 0, } @@ -146,6 +158,45 @@ async def test_get_action_capabilities_brightness(hass, device_reg, entity_reg): assert capabilities == {"extra_fields": []} +async def test_get_action_capabilities_flash(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a light action.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + "5678", + device_id=device_entry.id, + supported_features=SUPPORT_FLASH, + ) + + expected_capabilities = { + "extra_fields": [ + { + "name": "flash", + "optional": True, + "type": "select", + "options": [("short", "short"), ("long", "long")], + } + ] + } + + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert len(actions) == 4 + for action in actions: + capabilities = await async_get_device_automation_capabilities( + hass, "action", action + ) + if action["type"] == "turn_on": + assert capabilities == expected_capabilities + else: + assert capabilities == {"extra_fields": []} + + async def test_action(hass, calls): """Test for turn_on and turn_off actions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -187,6 +238,25 @@ async def test_action(hass, calls): "type": "toggle", }, }, + { + "trigger": {"platform": "event", "event_type": "test_flash_short"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "flash", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_flash_long"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "flash", + "flash": "long", + }, + }, { "trigger": { "platform": "event", @@ -252,6 +322,22 @@ async def test_action(hass, calls): await hass.async_block_till_done() assert hass.states.get(ent1.entity_id).state == STATE_ON + hass.bus.async_fire("test_toggle") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_flash_short") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_toggle") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_flash_long") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") hass.bus.async_fire("test_brightness_increase") @@ -281,3 +367,17 @@ async def test_action(hass, calls): assert len(turn_on_calls) == 4 assert turn_on_calls[3].data["entity_id"] == ent1.entity_id assert "brightness_pct" not in turn_on_calls[3].data + + hass.bus.async_fire("test_flash_short") + await hass.async_block_till_done() + + assert len(turn_on_calls) == 5 + assert turn_on_calls[4].data["entity_id"] == ent1.entity_id + assert turn_on_calls[4].data["flash"] == FLASH_SHORT + + hass.bus.async_fire("test_flash_long") + await hass.async_block_till_done() + + assert len(turn_on_calls) == 6 + assert turn_on_calls[5].data["entity_id"] == ent1.entity_id + assert turn_on_calls[5].data["flash"] == FLASH_LONG