diff --git a/homeassistant/const.py b/homeassistant/const.py index 7661e21b2d1..75d0aa9b6bd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -143,6 +143,7 @@ CONF_EFFECT: Final = "effect" CONF_ELEVATION: Final = "elevation" CONF_ELSE: Final = "else" CONF_EMAIL: Final = "email" +CONF_ENABLED: Final = "enabled" CONF_ENTITIES: Final = "entities" CONF_ENTITY_CATEGORY: Final = "entity_category" CONF_ENTITY_ID: Final = "entity_id" diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index ddd521af802..8985b7b721c 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -27,6 +27,7 @@ from homeassistant.const import ( CONF_BELOW, CONF_CONDITION, CONF_DEVICE_ID, + CONF_ENABLED, CONF_ENTITY_ID, CONF_ID, CONF_MATCH, @@ -166,6 +167,18 @@ async def async_from_config( if factory is None: raise HomeAssistantError(f'Invalid condition "{condition}" specified {config}') + # Check if condition is not enabled + if not config.get(CONF_ENABLED, True): + + @trace_condition_function + def disabled_condition( + hass: HomeAssistant, variables: TemplateVarsType = None + ) -> bool: + """Condition not enabled, will always pass.""" + return True + + return disabled_condition + # Check for partials to properly determine if coroutine function check_factory = factory while isinstance(check_factory, ft.partial): diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index af4bf9bf599..c681b5a8284 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -44,6 +44,7 @@ from homeassistant.const import ( CONF_DEVICE_ID, CONF_DOMAIN, CONF_ELSE, + CONF_ENABLED, CONF_ENTITY_ID, CONF_ENTITY_NAMESPACE, CONF_ERROR, @@ -1060,6 +1061,7 @@ SCRIPT_SCHEMA = vol.All(ensure_list, [script_action]) SCRIPT_ACTION_BASE_SCHEMA = { vol.Optional(CONF_ALIAS): string, vol.Optional(CONF_CONTINUE_ON_ERROR): boolean, + vol.Optional(CONF_ENABLED): boolean, } EVENT_SCHEMA = vol.Schema( @@ -1098,7 +1100,10 @@ NUMERIC_STATE_THRESHOLD_SCHEMA = vol.Any( vol.Coerce(float), vol.All(str, entity_domain(["input_number", "number", "sensor"])) ) -CONDITION_BASE_SCHEMA = {vol.Optional(CONF_ALIAS): string} +CONDITION_BASE_SCHEMA = { + vol.Optional(CONF_ALIAS): string, + vol.Optional(CONF_ENABLED): boolean, +} NUMERIC_STATE_CONDITION_SCHEMA = vol.All( vol.Schema( @@ -1337,6 +1342,7 @@ TRIGGER_BASE_SCHEMA = vol.Schema( vol.Required(CONF_PLATFORM): str, vol.Optional(CONF_ID): str, vol.Optional(CONF_VARIABLES): SCRIPT_VARIABLES_SCHEMA, + vol.Optional(CONF_ENABLED): boolean, } ) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index c26a5c1b852..e314cdc5c9c 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -36,6 +36,7 @@ from homeassistant.const import ( CONF_DEVICE_ID, CONF_DOMAIN, CONF_ELSE, + CONF_ENABLED, CONF_ERROR, CONF_EVENT, CONF_EVENT_DATA, @@ -411,8 +412,17 @@ class _ScriptRun: async with trace_action(self._hass, self, self._stop, self._variables): if self._stop.is_set(): return + + action = cv.determine_script_action(self._action) + + if not self._action.get(CONF_ENABLED, True): + self._log( + "Skipped disabled step %s", self._action.get(CONF_ALIAS, action) + ) + return + try: - handler = f"_async_{cv.determine_script_action(self._action)}_step" + handler = f"_async_{action}_step" await getattr(self, handler)() except Exception as ex: # pylint: disable=broad-except self._handle_exception( diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 79ac9f33f24..a3cb2f9421d 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -9,7 +9,7 @@ from typing import Any import voluptuous as vol -from homeassistant.const import CONF_ID, CONF_PLATFORM, CONF_VARIABLES +from homeassistant.const import CONF_ENABLED, CONF_ID, CONF_PLATFORM, CONF_VARIABLES from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import IntegrationNotFound, async_get_integration @@ -89,6 +89,10 @@ async def async_initialize_triggers( triggers = [] for idx, conf in enumerate(trigger_config): + # Skip triggers that are not enabled + if not conf.get(CONF_ENABLED, True): + continue + platform = await _async_get_trigger_platform(hass, conf) trigger_id = conf.get(CONF_ID, f"{idx}") trigger_idx = f"{idx}" diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 6d0ecfbd053..449df4f1108 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -3004,3 +3004,23 @@ async def test_platform_async_validate_condition_config(hass): platform.async_validate_condition_config.return_value = config await condition.async_validate_condition_config(hass, config) platform.async_validate_condition_config.assert_awaited() + + +async def test_disabled_condition(hass: HomeAssistant) -> None: + """Test a disabled condition always passes.""" + config = { + "enabled": False, + "condition": "state", + "entity_id": "binary_sensor.test", + "state": "on", + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) + + hass.states.async_set("binary_sensor.test", "on") + assert test(hass) + + # Still passses, condition is not enabled + hass.states.async_set("binary_sensor.test", "off") + assert test(hass) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index b04ad4a4ddf..9a1f643bc06 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -4436,3 +4436,46 @@ async def test_continue_on_error_unknown_error(hass: HomeAssistant) -> None: }, expected_script_execution="error", ) + + +async def test_disabled_actions( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test disabled action steps.""" + events = async_capture_events(hass, "test_event") + + @callback + def broken_service(service: ServiceCall) -> None: + """Break this service with an error.""" + raise HomeAssistantError("This service should not be called") + + hass.services.async_register("broken", "service", broken_service) + + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": "test_event"}, + { + "alias": "Hello", + "enabled": False, + "service": "broken.service", + }, + {"alias": "World", "enabled": False, "event": "test_event"}, + {"event": "test_event"}, + ] + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + await script_obj.async_run(context=Context()) + + assert len(events) == 2 + assert "Test Name: Skipped disabled step Hello" in caplog.text + assert "Test Name: Skipped disabled step World" in caplog.text + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{}], + "2": [{}], + "3": [{"result": {"event": "test_event", "event_data": {}}}], + }, + ) diff --git a/tests/helpers/test_trigger.py b/tests/helpers/test_trigger.py index 598906b48c3..9c59b00c843 100644 --- a/tests/helpers/test_trigger.py +++ b/tests/helpers/test_trigger.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock, call, patch import pytest import voluptuous as vol +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers.trigger import ( _async_get_trigger_platform, async_validate_trigger_config, @@ -66,3 +67,39 @@ async def test_if_fires_on_event(hass, calls): await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["hello"] == "Paulus + test_event" + + +async def test_if_disabled_trigger_not_firing( + hass: HomeAssistant, calls: list[ServiceCall] +) -> None: + """Test disabled triggers don't fire.""" + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "trigger": [ + { + "platform": "event", + "event_type": "enabled_trigger_event", + }, + { + "enabled": False, + "platform": "event", + "event_type": "disabled_trigger_event", + }, + ], + "action": { + "service": "test.automation", + }, + } + }, + ) + + hass.bus.async_fire("disabled_trigger_event") + await hass.async_block_till_done() + assert not calls + + hass.bus.async_fire("enabled_trigger_event") + await hass.async_block_till_done() + assert len(calls) == 1