Files
core/homeassistant/helpers/trigger/models.py
T

183 lines
5.6 KiB
Python

"""Common trigger classes and constants."""
import abc
import asyncio
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any, Protocol
import probatio
from homeassistant.const import CONF_OPTIONS, CONF_TARGET
from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
TRIGGERS: HassKey[dict[str, str]] = HassKey("triggers")
_TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{
probatio.Optional(CONF_OPTIONS): object,
probatio.Optional(CONF_TARGET): cv.TARGET_FIELDS,
}
)
@dataclass(slots=True, frozen=True)
class TriggerConfig:
"""Trigger config."""
key: str # The key used to identify the trigger, e.g. "zwave.event"
target: dict[str, Any] | None = None
options: dict[str, Any] | None = None
class TriggerActionRunner(Protocol):
"""Protocol type for the trigger action runner helper callback."""
@callback
def __call__(
self,
extra_trigger_payload: dict[str, Any],
description: str,
context: Context | None = None,
) -> asyncio.Task[Any]:
"""Define trigger action runner type.
Returns:
A Task that allows awaiting for the action to finish.
"""
@dataclass(slots=True, frozen=True)
class NotTriggeredInfo:
"""Diagnostics describing why a trigger evaluated a change but did not fire.
Passed by a trigger to its ``did_not_trigger`` reporter, the sibling of the
action runner that is called - in certain interesting cases - when the
trigger does not fire. ``reason`` is a stable, machine-readable code; the
optional ``data`` carries the evaluated context for the trace.
"""
reason: str
data: Mapping[str, Any] | None = None
def as_dict(self) -> dict[str, Any]:
"""Return a JSON-serializable dict for storing in a trace."""
result: dict[str, Any] = {"reason": self.reason}
if self.data is not None:
result["data"] = dict(self.data)
return result
class TriggerNotTriggeredReporter(Protocol):
"""Protocol type for the did_not_trigger reporter passed to a trigger runner.
A trigger calls this to report that it evaluated a relevant change but
decided not to fire, supplying diagnostics for tracing.
"""
@callback
def __call__(
self,
info: NotTriggeredInfo,
context: Context | None = None,
) -> None:
"""Report that the trigger did not fire."""
class TriggerActionPayloadBuilder(Protocol):
"""Protocol type for the trigger action payload builder."""
def __call__(
self, extra_trigger_payload: dict[str, Any], description: str
) -> dict[str, Any]:
"""Define trigger action payload builder type."""
class TriggerAction(Protocol):
"""Protocol type for trigger action callback."""
async def __call__(
self, run_variables: dict[str, Any], context: Context | None = None
) -> Any:
"""Define action callback type."""
class Trigger(abc.ABC):
"""Trigger class."""
_hass: HomeAssistant
@classmethod
async def async_validate_complete_config(
cls, hass: HomeAssistant, complete_config: ConfigType
) -> ConfigType:
"""Validate complete config.
The complete config includes fields that are generic to all triggers,
such as the alias or the ID.
This method should be overridden by triggers that need to migrate
from the old-style config.
"""
complete_config = _TRIGGER_SCHEMA(complete_config)
specific_config: ConfigType = {}
for key in (CONF_OPTIONS, CONF_TARGET):
if key in complete_config:
specific_config[key] = complete_config.pop(key)
specific_config = await cls.async_validate_config(hass, specific_config)
for key in (CONF_OPTIONS, CONF_TARGET):
if key in specific_config:
complete_config[key] = specific_config[key]
return complete_config
@classmethod
@abc.abstractmethod
async def async_validate_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize trigger."""
self._hass = hass
async def async_attach_action(
self,
action: TriggerAction,
action_payload_builder: TriggerActionPayloadBuilder,
*,
did_not_trigger: TriggerNotTriggeredReporter | None = None,
) -> CALLBACK_TYPE:
"""Attach the trigger to an action.
The optional ``did_not_trigger`` reporter is the sibling of the action
runner: triggers may call it - in certain interesting cases - when they
evaluate a relevant change but decide not to fire.
"""
@callback
def run_action(
extra_trigger_payload: dict[str, Any],
description: str,
context: Context | None = None,
) -> asyncio.Task[Any]:
"""Run action with trigger variables."""
payload = action_payload_builder(extra_trigger_payload, description)
return self._hass.async_create_task(action(payload, context))
return await self.async_attach_runner(run_action, did_not_trigger)
@abc.abstractmethod
async def async_attach_runner(
self,
run_action: TriggerActionRunner,
did_not_trigger: TriggerNotTriggeredReporter | None = None,
) -> CALLBACK_TYPE:
"""Attach the trigger to an action runner."""