Allow disabling specific triggers/actions/conditions (#70082)

This commit is contained in:
Franck Nijhof 2022-04-15 18:33:09 +02:00 committed by GitHub
parent ae9315aa29
commit e04fef3c2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 137 additions and 3 deletions

View File

@ -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"

View File

@ -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):

View File

@ -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,
}
)

View File

@ -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(

View File

@ -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}"

View File

@ -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)

View File

@ -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": {}}}],
},
)

View File

@ -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