diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index 99f5b6b12bc..5ee2785a700 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -15,7 +15,7 @@ from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from . import ATTR_BRIGHTNESS_STEP_PCT, DOMAIN, SUPPORT_BRIGHTNESS +from . import ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_STEP_PCT, DOMAIN, SUPPORT_BRIGHTNESS TYPE_BRIGHTNESS_INCREASE = "brightness_increase" TYPE_BRIGHTNESS_DECREASE = "brightness_decrease" @@ -28,6 +28,9 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( toggle_entity.DEVICE_ACTION_TYPES + [TYPE_BRIGHTNESS_INCREASE, TYPE_BRIGHTNESS_DECREASE] ), + vol.Optional(ATTR_BRIGHTNESS): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), } ) @@ -39,7 +42,10 @@ async def async_call_action_from_config( context: Context, ) -> None: """Change state based on configuration.""" - if config[CONF_TYPE] in toggle_entity.DEVICE_ACTION_TYPES: + if ( + config[CONF_TYPE] in toggle_entity.DEVICE_ACTION_TYPES + and config[CONF_TYPE] != toggle_entity.CONF_TURN_ON + ): await toggle_entity.async_call_action_from_config( hass, config, variables, context, DOMAIN ) @@ -49,8 +55,10 @@ async def async_call_action_from_config( if config[CONF_TYPE] == TYPE_BRIGHTNESS_INCREASE: data[ATTR_BRIGHTNESS_STEP_PCT] = 10 - else: + elif config[CONF_TYPE] == TYPE_BRIGHTNESS_DECREASE: data[ATTR_BRIGHTNESS_STEP_PCT] = -10 + elif ATTR_BRIGHTNESS in config: + data[ATTR_BRIGHTNESS] = config[ATTR_BRIGHTNESS] await hass.services.async_call( DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=context @@ -93,3 +101,33 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: ) return actions + + +async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List action capabilities.""" + if config[CONF_TYPE] != toggle_entity.CONF_TURN_ON: + return {} + + registry = await entity_registry.async_get_registry(hass) + entry = registry.async_get(config[ATTR_ENTITY_ID]) + state = hass.states.get(config[ATTR_ENTITY_ID]) + + supported_features = 0 + + if state: + supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + elif entry: + supported_features = entry.supported_features + + if not supported_features & SUPPORT_BRIGHTNESS: + return {} + + return { + "extra_fields": vol.Schema( + { + vol.Optional(ATTR_BRIGHTNESS): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ) + } + ) + } diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 3ac8171ce7d..610f61dea52 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -9,6 +9,7 @@ from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, + async_get_device_automation_capabilities, async_get_device_automations, async_mock_service, mock_device_registry, @@ -85,6 +86,66 @@ async def test_get_actions(hass, device_reg, entity_reg): assert actions == expected_actions +async def test_get_action_capabilities(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, + ) + + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert len(actions) == 3 + for action in actions: + capabilities = await async_get_device_automation_capabilities( + hass, "action", action + ) + assert capabilities == {"extra_fields": []} + + +async def test_get_action_capabilities_brightness(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_BRIGHTNESS, + ) + + expected_capabilities = { + "extra_fields": [ + { + "name": "brightness", + "optional": True, + "type": "integer", + "valueMax": 100, + "valueMin": 0, + } + ] + } + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert len(actions) == 5 + 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}") @@ -100,7 +161,7 @@ async def test_action(hass, calls): { automation.DOMAIN: [ { - "trigger": {"platform": "event", "event_type": "test_event1"}, + "trigger": {"platform": "event", "event_type": "test_off"}, "action": { "domain": DOMAIN, "device_id": "", @@ -109,7 +170,7 @@ async def test_action(hass, calls): }, }, { - "trigger": {"platform": "event", "event_type": "test_event2"}, + "trigger": {"platform": "event", "event_type": "test_on"}, "action": { "domain": DOMAIN, "device_id": "", @@ -118,7 +179,7 @@ async def test_action(hass, calls): }, }, { - "trigger": {"platform": "event", "event_type": "test_event3"}, + "trigger": {"platform": "event", "event_type": "test_toggle"}, "action": { "domain": DOMAIN, "device_id": "", @@ -150,6 +211,16 @@ async def test_action(hass, calls): "type": "brightness_decrease", }, }, + { + "trigger": {"platform": "event", "event_type": "test_brightness"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turn_on", + "brightness": 75, + }, + }, ] }, ) @@ -157,27 +228,27 @@ async def test_action(hass, calls): assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 - hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_off") await hass.async_block_till_done() assert hass.states.get(ent1.entity_id).state == STATE_OFF - hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_off") await hass.async_block_till_done() assert hass.states.get(ent1.entity_id).state == STATE_OFF - hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_on") await hass.async_block_till_done() assert hass.states.get(ent1.entity_id).state == STATE_ON - hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_on") await hass.async_block_till_done() assert hass.states.get(ent1.entity_id).state == STATE_ON - hass.bus.async_fire("test_event3") + 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_event3") + hass.bus.async_fire("test_toggle") await hass.async_block_till_done() assert hass.states.get(ent1.entity_id).state == STATE_ON @@ -196,3 +267,17 @@ async def test_action(hass, calls): assert len(turn_on_calls) == 2 assert turn_on_calls[1].data["entity_id"] == ent1.entity_id assert turn_on_calls[1].data["brightness_step_pct"] == -10 + + hass.bus.async_fire("test_brightness") + await hass.async_block_till_done() + + assert len(turn_on_calls) == 3 + assert turn_on_calls[2].data["entity_id"] == ent1.entity_id + assert turn_on_calls[2].data["brightness"] == 75 + + hass.bus.async_fire("test_on") + await hass.async_block_till_done() + + assert len(turn_on_calls) == 4 + assert turn_on_calls[3].data["entity_id"] == ent1.entity_id + assert "brightness" not in turn_on_calls[3].data