Allow disabling specific triggers/actions/conditions (#70082)
This commit is contained in:
parent
ae9315aa29
commit
e04fef3c2d
8 changed files with 137 additions and 3 deletions
|
@ -143,6 +143,7 @@ CONF_EFFECT: Final = "effect"
|
||||||
CONF_ELEVATION: Final = "elevation"
|
CONF_ELEVATION: Final = "elevation"
|
||||||
CONF_ELSE: Final = "else"
|
CONF_ELSE: Final = "else"
|
||||||
CONF_EMAIL: Final = "email"
|
CONF_EMAIL: Final = "email"
|
||||||
|
CONF_ENABLED: Final = "enabled"
|
||||||
CONF_ENTITIES: Final = "entities"
|
CONF_ENTITIES: Final = "entities"
|
||||||
CONF_ENTITY_CATEGORY: Final = "entity_category"
|
CONF_ENTITY_CATEGORY: Final = "entity_category"
|
||||||
CONF_ENTITY_ID: Final = "entity_id"
|
CONF_ENTITY_ID: Final = "entity_id"
|
||||||
|
|
|
@ -27,6 +27,7 @@ from homeassistant.const import (
|
||||||
CONF_BELOW,
|
CONF_BELOW,
|
||||||
CONF_CONDITION,
|
CONF_CONDITION,
|
||||||
CONF_DEVICE_ID,
|
CONF_DEVICE_ID,
|
||||||
|
CONF_ENABLED,
|
||||||
CONF_ENTITY_ID,
|
CONF_ENTITY_ID,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_MATCH,
|
CONF_MATCH,
|
||||||
|
@ -166,6 +167,18 @@ async def async_from_config(
|
||||||
if factory is None:
|
if factory is None:
|
||||||
raise HomeAssistantError(f'Invalid condition "{condition}" specified {config}')
|
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 for partials to properly determine if coroutine function
|
||||||
check_factory = factory
|
check_factory = factory
|
||||||
while isinstance(check_factory, ft.partial):
|
while isinstance(check_factory, ft.partial):
|
||||||
|
|
|
@ -44,6 +44,7 @@ from homeassistant.const import (
|
||||||
CONF_DEVICE_ID,
|
CONF_DEVICE_ID,
|
||||||
CONF_DOMAIN,
|
CONF_DOMAIN,
|
||||||
CONF_ELSE,
|
CONF_ELSE,
|
||||||
|
CONF_ENABLED,
|
||||||
CONF_ENTITY_ID,
|
CONF_ENTITY_ID,
|
||||||
CONF_ENTITY_NAMESPACE,
|
CONF_ENTITY_NAMESPACE,
|
||||||
CONF_ERROR,
|
CONF_ERROR,
|
||||||
|
@ -1060,6 +1061,7 @@ SCRIPT_SCHEMA = vol.All(ensure_list, [script_action])
|
||||||
SCRIPT_ACTION_BASE_SCHEMA = {
|
SCRIPT_ACTION_BASE_SCHEMA = {
|
||||||
vol.Optional(CONF_ALIAS): string,
|
vol.Optional(CONF_ALIAS): string,
|
||||||
vol.Optional(CONF_CONTINUE_ON_ERROR): boolean,
|
vol.Optional(CONF_CONTINUE_ON_ERROR): boolean,
|
||||||
|
vol.Optional(CONF_ENABLED): boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
EVENT_SCHEMA = vol.Schema(
|
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"]))
|
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(
|
NUMERIC_STATE_CONDITION_SCHEMA = vol.All(
|
||||||
vol.Schema(
|
vol.Schema(
|
||||||
|
@ -1337,6 +1342,7 @@ TRIGGER_BASE_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_PLATFORM): str,
|
vol.Required(CONF_PLATFORM): str,
|
||||||
vol.Optional(CONF_ID): str,
|
vol.Optional(CONF_ID): str,
|
||||||
vol.Optional(CONF_VARIABLES): SCRIPT_VARIABLES_SCHEMA,
|
vol.Optional(CONF_VARIABLES): SCRIPT_VARIABLES_SCHEMA,
|
||||||
|
vol.Optional(CONF_ENABLED): boolean,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ from homeassistant.const import (
|
||||||
CONF_DEVICE_ID,
|
CONF_DEVICE_ID,
|
||||||
CONF_DOMAIN,
|
CONF_DOMAIN,
|
||||||
CONF_ELSE,
|
CONF_ELSE,
|
||||||
|
CONF_ENABLED,
|
||||||
CONF_ERROR,
|
CONF_ERROR,
|
||||||
CONF_EVENT,
|
CONF_EVENT,
|
||||||
CONF_EVENT_DATA,
|
CONF_EVENT_DATA,
|
||||||
|
@ -411,8 +412,17 @@ class _ScriptRun:
|
||||||
async with trace_action(self._hass, self, self._stop, self._variables):
|
async with trace_action(self._hass, self, self._stop, self._variables):
|
||||||
if self._stop.is_set():
|
if self._stop.is_set():
|
||||||
return
|
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:
|
try:
|
||||||
handler = f"_async_{cv.determine_script_action(self._action)}_step"
|
handler = f"_async_{action}_step"
|
||||||
await getattr(self, handler)()
|
await getattr(self, handler)()
|
||||||
except Exception as ex: # pylint: disable=broad-except
|
except Exception as ex: # pylint: disable=broad-except
|
||||||
self._handle_exception(
|
self._handle_exception(
|
||||||
|
|
|
@ -9,7 +9,7 @@ from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
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.core import CALLBACK_TYPE, Context, HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.loader import IntegrationNotFound, async_get_integration
|
from homeassistant.loader import IntegrationNotFound, async_get_integration
|
||||||
|
@ -89,6 +89,10 @@ async def async_initialize_triggers(
|
||||||
|
|
||||||
triggers = []
|
triggers = []
|
||||||
for idx, conf in enumerate(trigger_config):
|
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)
|
platform = await _async_get_trigger_platform(hass, conf)
|
||||||
trigger_id = conf.get(CONF_ID, f"{idx}")
|
trigger_id = conf.get(CONF_ID, f"{idx}")
|
||||||
trigger_idx = f"{idx}"
|
trigger_idx = f"{idx}"
|
||||||
|
|
|
@ -3004,3 +3004,23 @@ async def test_platform_async_validate_condition_config(hass):
|
||||||
platform.async_validate_condition_config.return_value = config
|
platform.async_validate_condition_config.return_value = config
|
||||||
await condition.async_validate_condition_config(hass, config)
|
await condition.async_validate_condition_config(hass, config)
|
||||||
platform.async_validate_condition_config.assert_awaited()
|
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)
|
||||||
|
|
|
@ -4436,3 +4436,46 @@ async def test_continue_on_error_unknown_error(hass: HomeAssistant) -> None:
|
||||||
},
|
},
|
||||||
expected_script_execution="error",
|
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": {}}}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ from unittest.mock import MagicMock, call, patch
|
||||||
import pytest
|
import pytest
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
from homeassistant.helpers.trigger import (
|
from homeassistant.helpers.trigger import (
|
||||||
_async_get_trigger_platform,
|
_async_get_trigger_platform,
|
||||||
async_validate_trigger_config,
|
async_validate_trigger_config,
|
||||||
|
@ -66,3 +67,39 @@ async def test_if_fires_on_event(hass, calls):
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
assert calls[0].data["hello"] == "Paulus + test_event"
|
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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue