From 670bd97777537fe51df38a3bae52d7249ed87b63 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Mar 2024 16:28:37 +0100 Subject: [PATCH] Find referenced labels in automations & scripts (#113812) --- .../components/automation/__init__.py | 27 +++++ homeassistant/components/script/__init__.py | 27 +++++ homeassistant/helpers/script.py | 10 +- tests/components/automation/test_init.py | 25 +++++ tests/components/script/test_init.py | 25 +++++ tests/helpers/test_script.py | 104 ++++++++++++++++++ 6 files changed, 217 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index be20ee17181..7b1b142719a 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -245,6 +245,18 @@ def floors_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: return _x_in_automation(hass, entity_id, "referenced_floors") +@callback +def automations_with_label(hass: HomeAssistant, label_id: str) -> list[str]: + """Return all automations that reference the label.""" + return _automations_with_x(hass, label_id, "referenced_labels") + + +@callback +def labels_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all labels in an automation.""" + return _x_in_automation(hass, entity_id, "referenced_labels") + + @callback def automations_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str]: """Return all automations that reference the blueprint.""" @@ -353,6 +365,11 @@ class BaseAutomationEntity(ToggleEntity, ABC): return {CONF_ID: self.unique_id} return None + @cached_property + @abstractmethod + def referenced_labels(self) -> set[str]: + """Return a set of referenced labels.""" + @cached_property @abstractmethod def referenced_floors(self) -> set[str]: @@ -413,6 +430,11 @@ class UnavailableAutomationEntity(BaseAutomationEntity): """Return the name of the entity.""" return self._name + @cached_property + def referenced_labels(self) -> set[str]: + """Return a set of referenced labels.""" + return set() + @cached_property def referenced_floors(self) -> set[str]: """Return a set of referenced floors.""" @@ -505,6 +527,11 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): """Return True if entity is on.""" return self._async_detach_triggers is not None or self._is_enabled + @property + def referenced_labels(self) -> set[str]: + """Return a set of referenced labels.""" + return self.action_script.referenced_labels + @property def referenced_floors(self) -> set[str]: """Return a set of referenced floors.""" diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index c742c42309f..954100c3a71 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -170,6 +170,18 @@ def floors_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: return _x_in_script(hass, entity_id, "referenced_floors") +@callback +def scripts_with_label(hass: HomeAssistant, label_id: str) -> list[str]: + """Return all scripts that reference the label.""" + return _scripts_with_x(hass, label_id, "referenced_labels") + + +@callback +def labels_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all labels in a script.""" + return _x_in_script(hass, entity_id, "referenced_labels") + + @callback def scripts_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str]: """Return all scripts that reference the blueprint.""" @@ -401,6 +413,11 @@ class BaseScriptEntity(ToggleEntity, ABC): raw_config: ConfigType | None + @cached_property + @abstractmethod + def referenced_labels(self) -> set[str]: + """Return a set of referenced labels.""" + @cached_property @abstractmethod def referenced_floors(self) -> set[str]: @@ -451,6 +468,11 @@ class UnavailableScriptEntity(BaseScriptEntity): """Return the name of the entity.""" return self._name + @cached_property + def referenced_labels(self) -> set[str]: + """Return a set of referenced labels.""" + return set() + @cached_property def referenced_floors(self) -> set[str]: """Return a set of referenced floors.""" @@ -539,6 +561,11 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity): """Return true if script is on.""" return self.script.is_running + @cached_property + def referenced_labels(self) -> set[str]: + """Return a set of referenced labels.""" + return self.script.referenced_labels + @cached_property def referenced_floors(self) -> set[str]: """Return a set of referenced floors.""" diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index d5d3534f793..4f9c1d113ea 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -27,6 +27,7 @@ from homeassistant.const import ( ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_FLOOR_ID, + ATTR_LABEL_ID, CONF_ALIAS, CONF_CHOOSE, CONF_CONDITION, @@ -1381,6 +1382,13 @@ class Script: """Return true if the current mode support max.""" return self.script_mode in (SCRIPT_MODE_PARALLEL, SCRIPT_MODE_QUEUED) + @cached_property + def referenced_labels(self) -> set[str]: + """Return a set of referenced labels.""" + referenced_labels: set[str] = set() + Script._find_referenced_target(ATTR_LABEL_ID, referenced_labels, self.sequence) + return referenced_labels + @cached_property def referenced_floors(self) -> set[str]: """Return a set of referenced fooors.""" @@ -1397,7 +1405,7 @@ class Script: @staticmethod def _find_referenced_target( - target: Literal["area_id", "floor_id"], + target: Literal["area_id", "floor_id", "label_id"], referenced: set[str], sequence: Sequence[dict[str, Any]], ) -> None: diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 7ee829c7bc9..234fc912118 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1563,6 +1563,8 @@ async def test_extraction_functions_not_setup(hass: HomeAssistant) -> None: assert automation.entities_in_automation(hass, "automation.test") == [] assert automation.automations_with_floor(hass, "floor-in-both") == [] assert automation.floors_in_automation(hass, "automation.test") == [] + assert automation.automations_with_label(hass, "label-in-both") == [] + assert automation.labels_in_automation(hass, "automation.test") == [] async def test_extraction_functions_unknown_automation(hass: HomeAssistant) -> None: @@ -1573,6 +1575,7 @@ async def test_extraction_functions_unknown_automation(hass: HomeAssistant) -> N assert automation.devices_in_automation(hass, "automation.unknown") == [] assert automation.entities_in_automation(hass, "automation.unknown") == [] assert automation.floors_in_automation(hass, "automation.unknown") == [] + assert automation.labels_in_automation(hass, "automation.unknown") == [] async def test_extraction_functions_unavailable_automation(hass: HomeAssistant) -> None: @@ -1600,6 +1603,8 @@ async def test_extraction_functions_unavailable_automation(hass: HomeAssistant) assert automation.entities_in_automation(hass, entity_id) == [] assert automation.automations_with_floor(hass, "floor-in-both") == [] assert automation.floors_in_automation(hass, entity_id) == [] + assert automation.automations_with_label(hass, "label-in-both") == [] + assert automation.labels_in_automation(hass, entity_id) == [] async def test_extraction_functions( @@ -1703,6 +1708,10 @@ async def test_extraction_functions( "service": "test.test", "target": {"floor_id": "floor-in-both"}, }, + { + "service": "test.test", + "target": {"label_id": "label-in-both"}, + }, ], }, { @@ -1830,6 +1839,14 @@ async def test_extraction_functions( "service": "test.test", "target": {"floor_id": "floor-in-last"}, }, + { + "service": "test.test", + "target": {"label_id": "label-in-both"}, + }, + { + "service": "test.test", + "target": {"label_id": "label-in-last"}, + }, ], }, ] @@ -1880,6 +1897,14 @@ async def test_extraction_functions( "floor-in-both", "floor-in-last", } + assert set(automation.automations_with_label(hass, "label-in-both")) == { + "automation.test1", + "automation.test3", + } + assert set(automation.labels_in_automation(hass, "automation.test3")) == { + "label-in-both", + "label-in-last", + } assert automation.blueprint_in_automation(hass, "automation.test3") is None diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 95fc3338333..02587d7bc98 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -686,11 +686,14 @@ async def test_extraction_functions_not_setup(hass: HomeAssistant) -> None: assert script.entities_in_script(hass, "script.test") == [] assert script.scripts_with_floor(hass, "floor-in-both") == [] assert script.floors_in_script(hass, "script.test") == [] + assert script.scripts_with_label(hass, "label-in-both") == [] + assert script.labels_in_script(hass, "script.test") == [] async def test_extraction_functions_unknown_script(hass: HomeAssistant) -> None: """Test extraction functions for an unknown script.""" assert await async_setup_component(hass, DOMAIN, {}) + assert script.labels_in_script(hass, "script.unknown") == [] assert script.floors_in_script(hass, "script.unknown") == [] assert script.areas_in_script(hass, "script.unknown") == [] assert script.blueprint_in_script(hass, "script.unknown") is None @@ -717,6 +720,8 @@ async def test_extraction_functions_unavailable_script(hass: HomeAssistant) -> N assert script.entities_in_script(hass, entity_id) == [] assert script.scripts_with_floor(hass, "floor-in-both") == [] assert script.floors_in_script(hass, entity_id) == [] + assert script.scripts_with_label(hass, "label-in-both") == [] + assert script.labels_in_script(hass, entity_id) == [] async def test_extraction_functions( @@ -765,6 +770,10 @@ async def test_extraction_functions( "service": "test.test", "target": {"floor_id": "floor-in-both"}, }, + { + "service": "test.test", + "target": {"label_id": "label-in-both"}, + }, ] }, "test2": { @@ -821,6 +830,14 @@ async def test_extraction_functions( "service": "test.test", "target": {"floor_id": "floor-in-last"}, }, + { + "service": "test.test", + "target": {"label_id": "label-in-both"}, + }, + { + "service": "test.test", + "target": {"label_id": "label-in-last"}, + }, ], }, } @@ -860,6 +877,14 @@ async def test_extraction_functions( "floor-in-both", "floor-in-last", } + assert set(script.scripts_with_label(hass, "label-in-both")) == { + "script.test1", + "script.test3", + } + assert set(script.labels_in_script(hass, "script.test3")) == { + "label-in-both", + "label-in-last", + } assert script.blueprint_in_script(hass, "script.test3") is None diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 4d20add4f3e..0e658f35702 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -3695,6 +3695,110 @@ async def test_propagate_error_service_exception(hass: HomeAssistant) -> None: assert_action_trace(expected_trace, expected_script_execution="error") +async def test_referenced_labels(hass: HomeAssistant) -> None: + """Test referenced labels.""" + script_obj = script.Script( + hass, + cv.SCRIPT_SCHEMA( + [ + { + "service": "test.script", + "data": {"label_id": "label_service_not_list"}, + }, + { + "service": "test.script", + "data": { + "label_id": ["label_service_list_1", "label_service_list_2"] + }, + }, + { + "service": "test.script", + "data": {"label_id": "{{ 'label_service_template' }}"}, + }, + { + "service": "test.script", + "target": {"label_id": "label_in_target"}, + }, + { + "service": "test.script", + "data_template": {"label_id": "label_in_data_template"}, + }, + {"service": "test.script", "data": {"without": "label_id"}}, + { + "choose": [ + { + "conditions": "{{ true == false }}", + "sequence": [ + { + "service": "test.script", + "data": {"label_id": "label_choice_1_seq"}, + } + ], + }, + { + "conditions": "{{ true == false }}", + "sequence": [ + { + "service": "test.script", + "data": {"label_id": "label_choice_2_seq"}, + } + ], + }, + ], + "default": [ + { + "service": "test.script", + "data": {"label_id": "label_default_seq"}, + } + ], + }, + {"event": "test_event"}, + {"delay": "{{ delay_period }}"}, + { + "if": [], + "then": [ + { + "service": "test.script", + "data": {"label_id": "label_if_then"}, + } + ], + "else": [ + { + "service": "test.script", + "data": {"label_id": "label_if_else"}, + } + ], + }, + { + "parallel": [ + { + "service": "test.script", + "data": {"label_id": "label_parallel"}, + } + ], + }, + ] + ), + "Test Name", + "test_domain", + ) + assert script_obj.referenced_labels == { + "label_choice_1_seq", + "label_choice_2_seq", + "label_default_seq", + "label_in_data_template", + "label_in_target", + "label_service_list_1", + "label_service_list_2", + "label_service_not_list", + "label_if_then", + "label_if_else", + "label_parallel", + } + # Test we cache results. + assert script_obj.referenced_labels is script_obj.referenced_labels + + async def test_referenced_floors(hass: HomeAssistant) -> None: """Test referenced floors.""" script_obj = script.Script(