diff --git a/homeassistant/components/websocket_api/automation.py b/homeassistant/components/websocket_api/automation.py index 825e57222b7..7f68b203632 100644 --- a/homeassistant/components/websocket_api/automation.py +++ b/homeassistant/components/websocket_api/automation.py @@ -9,6 +9,9 @@ from typing import Any, Self from homeassistant.const import CONF_TARGET from homeassistant.core import HomeAssistant from homeassistant.helpers import target as target_helpers +from homeassistant.helpers.condition import ( + async_get_all_descriptions as async_get_all_condition_descriptions, +) from homeassistant.helpers.entity import ( entity_sources, get_device_class, @@ -199,6 +202,16 @@ async def async_get_triggers_for_target( ) +async def async_get_conditions_for_target( + hass: HomeAssistant, target_selector: ConfigType, expand_group: bool +) -> set[str]: + """Get conditions for a target.""" + descriptions = await async_get_all_condition_descriptions(hass) + return _async_get_automation_components_for_target( + hass, target_selector, expand_group, descriptions + ) + + async def async_get_services_for_target( hass: HomeAssistant, target_selector: ConfigType, expand_group: bool ) -> set[str]: diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 2040e18af76..040811bca43 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -87,7 +87,11 @@ from homeassistant.setup import ( from homeassistant.util.json import format_unserializable_data from . import const, decorators, messages -from .automation import async_get_services_for_target, async_get_triggers_for_target +from .automation import ( + async_get_conditions_for_target, + async_get_services_for_target, + async_get_triggers_for_target, +) from .connection import ActiveConnection from .messages import construct_event_message, construct_result_message @@ -109,6 +113,7 @@ def async_register_commands( async_reg(hass, handle_execute_script) async_reg(hass, handle_extract_from_target) async_reg(hass, handle_fire_event) + async_reg(hass, handle_get_conditions_for_target) async_reg(hass, handle_get_config) async_reg(hass, handle_get_services) async_reg(hass, handle_get_services_for_target) @@ -903,6 +908,29 @@ async def handle_get_triggers_for_target( connection.send_result(msg["id"], triggers) +@decorators.websocket_command( + { + vol.Required("type"): "get_conditions_for_target", + vol.Required("target"): cv.TARGET_FIELDS, + vol.Optional("expand_group", default=True): bool, + } +) +@decorators.async_response +async def handle_get_conditions_for_target( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle get conditions for target command. + + This command returns all conditions that can be used with any entities that are currently + part of a target. + """ + conditions = await async_get_conditions_for_target( + hass, msg["target"], msg["expand_group"] + ) + + connection.send_result(msg["id"], conditions) + + @decorators.websocket_command( { vol.Required("type"): "get_services_for_target", diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 93976235f37..732849bf557 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -3659,49 +3659,57 @@ async def test_extract_from_target_validation_error( @pytest.mark.usefixtures("enable_experimental_triggers_conditions", "target_entities") @patch("annotatedyaml.loader.load_yaml") -@patch.object(Integration, "has_triggers", return_value=True) +@pytest.mark.parametrize("automation_component", ["trigger", "condition"]) async def test_get_triggers_for_target( - mock_has_triggers: Mock, mock_load_yaml: Mock, hass: HomeAssistant, websocket_client: MockHAClientWebSocket, + automation_component: str, ) -> None: - """Test get_triggers_for_target command with mixed target types.""" + """Test get_triggers_for_target/get_conditions_for_target command with mixed target types.""" - async def async_get_triggers(hass: HomeAssistant) -> dict[str, type]: + async def async_get_triggers_conditions(hass: HomeAssistant) -> dict[str, type]: return { "turned_on": Mock, - "non_target_trigger": Mock, + "non_target": Mock, } - mock_platform(hass, "light.trigger", Mock(async_get_triggers=async_get_triggers)) - mock_platform(hass, "switch.trigger", Mock(async_get_triggers=async_get_triggers)) - mock_platform(hass, "sensor.trigger", Mock(async_get_triggers=async_get_triggers)) + common_mock = Mock( + **{f"async_get_{automation_component}s": async_get_triggers_conditions} + ) + + mock_platform(hass, f"light.{automation_component}", common_mock) + mock_platform(hass, f"switch.{automation_component}", common_mock) + mock_platform(hass, f"sensor.{automation_component}", common_mock) mock_platform( hass, - "component1.trigger", + f"component1.{automation_component}", Mock( - async_get_triggers=AsyncMock( - return_value={ - "_": Mock, - "light_message": Mock, - "light_flash": Mock, - "light_dance": Mock, - } - ) + **{ + f"async_get_{automation_component}s": AsyncMock( + return_value={ + "_": Mock, + "light_message": Mock, + "light_flash": Mock, + "light_dance": Mock, + } + ) + } ), ) mock_platform( hass, - "component2.trigger", + f"component2.{automation_component}", Mock( - async_get_triggers=AsyncMock( - return_value={"match_all": Mock, "other_integration_lights": Mock} - ) + **{ + f"async_get_{automation_component}s": AsyncMock( + return_value={"match_all": Mock, "other_integration_lights": Mock} + ) + } ), ) - def get_common_trigger_descriptions(domain: str): + def get_common_descriptions(domain: str): return f""" turned_on: target: @@ -3717,7 +3725,7 @@ async def test_get_triggers_for_target( - first - last - any - non_target_trigger: + non_target: fields: behavior: required: true @@ -3730,7 +3738,7 @@ async def test_get_triggers_for_target( - any """ - component1_trigger_descriptions = """ + component1_descriptions = """ _: target: entity: @@ -3761,7 +3769,7 @@ async def test_get_triggers_for_target( - light.LightEntityFeature.TRANSITION """ - component2_trigger_descriptions = """ + component2_descriptions = """ match_all: target: @@ -3776,145 +3784,148 @@ async def test_get_triggers_for_target( """ def _load_yaml(fname, secrets=None): - if fname.endswith("component1/triggers.yaml"): - trigger_descriptions = component1_trigger_descriptions - elif fname.endswith("component2/triggers.yaml"): - trigger_descriptions = component2_trigger_descriptions + if fname.endswith(f"component1/{automation_component}s.yaml"): + descriptions = component1_descriptions + elif fname.endswith(f"component2/{automation_component}s.yaml"): + descriptions = component2_descriptions else: - trigger_descriptions = get_common_trigger_descriptions(fname.split("/")[-2]) - with io.StringIO(trigger_descriptions) as file: + descriptions = get_common_descriptions(fname.split("/")[-2]) + with io.StringIO(descriptions) as file: return parse_yaml(file) mock_load_yaml.side_effect = _load_yaml - assert await async_setup_component(hass, "light", {}) - assert await async_setup_component(hass, "switch", {}) - assert await async_setup_component(hass, "sensor", {}) - assert await async_setup_component(hass, "component1", {}) - assert await async_setup_component(hass, "component2", {}) - await hass.async_block_till_done() + with patch.object(Integration, f"has_{automation_component}s", return_value=True): + assert await async_setup_component(hass, "light", {}) + assert await async_setup_component(hass, "switch", {}) + assert await async_setup_component(hass, "sensor", {}) + assert await async_setup_component(hass, "component1", {}) + assert await async_setup_component(hass, "component2", {}) + await hass.async_block_till_done() - async def assert_triggers(target: dict[str, list[str]], expected: list[str]) -> Any: - """Call the command and assert expected triggers.""" - await websocket_client.send_json_auto_id( - {"type": "get_triggers_for_target", "target": target} + async def assert_command( + target: dict[str, list[str]], expected: list[str] + ) -> Any: + """Call the command and assert expected triggers/conditions.""" + await websocket_client.send_json_auto_id( + {"type": f"get_{automation_component}s_for_target", "target": target} + ) + msg = await websocket_client.receive_json() + + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert sorted(msg["result"]) == sorted(expected) + + # Test entity target - unknown entity + await assert_command({"entity_id": ["light.unknown_entity"]}, []) + + # Test entity target - entity not in registry + await assert_command( + {"entity_id": ["light.not_registry"]}, + [ + "component2.match_all", + "component2.other_integration_lights", + "light.turned_on", + ], ) - msg = await websocket_client.receive_json() - assert msg["type"] == const.TYPE_RESULT - assert msg["success"] - assert sorted(msg["result"]) == sorted(expected) + # Test entity targets + await assert_command( + {"entity_id": ["light.component1_light", "switch.component1_switch"]}, + [ + "component1", + "component1.light_message", + "component2.match_all", + "light.turned_on", + "switch.turned_on", + ], + ) + await assert_command( + {"entity_id": ["light.component1_flash_light"]}, + [ + "component1", + "component1.light_flash", + "component1.light_message", + "component2.match_all", + "light.turned_on", + ], + ) + await assert_command( + {"entity_id": ["light.component1_effect_flash_light"]}, + [ + "component1", + "component1.light_flash", + "component1.light_message", + "component2.match_all", + "component2.other_integration_lights", + "light.turned_on", + ], + ) + await assert_command( + {"entity_id": ["light.component1_flash_transition_light"]}, + [ + "component1", + "component1.light_dance", + "component1.light_flash", + "component1.light_message", + "component2.match_all", + "light.turned_on", + ], + ) - # Test entity target - unknown entity - await assert_triggers({"entity_id": ["light.unknown_entity"]}, []) + # Test device target - multiple devices + await assert_command( + {"device_id": ["device1", "device2"]}, + [ + "component1", + "component1.light_message", + "component2.match_all", + "component2.other_integration_lights", + "light.turned_on", + "sensor.turned_on", + "switch.turned_on", + ], + ) - # Test entity target - entity not in registry - await assert_triggers( - {"entity_id": ["light.not_registry"]}, - [ - "component2.match_all", - "component2.other_integration_lights", - "light.turned_on", - ], - ) + # Test area target - multiple areas + await assert_command( + {"area_id": ["kitchen", "living_room"]}, + [ + "component2.match_all", + "component2.other_integration_lights", + "light.turned_on", + "switch.turned_on", + ], + ) - # Test entity targets - await assert_triggers( - {"entity_id": ["light.component1_light", "switch.component1_switch"]}, - [ - "component1", - "component1.light_message", - "component2.match_all", - "light.turned_on", - "switch.turned_on", - ], - ) - await assert_triggers( - {"entity_id": ["light.component1_flash_light"]}, - [ - "component1", - "component1.light_flash", - "component1.light_message", - "component2.match_all", - "light.turned_on", - ], - ) - await assert_triggers( - {"entity_id": ["light.component1_effect_flash_light"]}, - [ - "component1", - "component1.light_flash", - "component1.light_message", - "component2.match_all", - "component2.other_integration_lights", - "light.turned_on", - ], - ) - await assert_triggers( - {"entity_id": ["light.component1_flash_transition_light"]}, - [ - "component1", - "component1.light_dance", - "component1.light_flash", - "component1.light_message", - "component2.match_all", - "light.turned_on", - ], - ) - - # Test device target - multiple devices - await assert_triggers( - {"device_id": ["device1", "device2"]}, - [ - "component1", - "component1.light_message", - "component2.match_all", - "component2.other_integration_lights", - "light.turned_on", - "sensor.turned_on", - "switch.turned_on", - ], - ) - - # Test area target - multiple areas - await assert_triggers( - {"area_id": ["kitchen", "living_room"]}, - [ - "component2.match_all", - "component2.other_integration_lights", - "light.turned_on", - "switch.turned_on", - ], - ) - - # Test label target - multiple labels - await assert_triggers( - {"label_id": ["label_1", "label_2"]}, - [ - "light.turned_on", - "component1", - "component2.match_all", - "component2.other_integration_lights", - "switch.turned_on", - ], - ) - # Test mixed target types - await assert_triggers( - { - "entity_id": ["light.test1"], - "device_id": ["device2"], - "area_id": ["kitchen"], - "label_id": ["label_1"], - }, - [ - "component1", - "component1.light_message", - "component2.match_all", - "component2.other_integration_lights", - "light.turned_on", - "sensor.turned_on", - "switch.turned_on", - ], - ) + # Test label target - multiple labels + await assert_command( + {"label_id": ["label_1", "label_2"]}, + [ + "light.turned_on", + "component1", + "component2.match_all", + "component2.other_integration_lights", + "switch.turned_on", + ], + ) + # Test mixed target types + await assert_command( + { + "entity_id": ["light.test1"], + "device_id": ["device2"], + "area_id": ["kitchen"], + "label_id": ["label_1"], + }, + [ + "component1", + "component1.light_message", + "component2.match_all", + "component2.other_integration_lights", + "light.turned_on", + "sensor.turned_on", + "switch.turned_on", + ], + ) @pytest.mark.usefixtures("target_entities")