1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-20 02:48:57 +00:00

Modernize calendar trigger (#159395)

Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
This commit is contained in:
Abílio Costa
2025-12-19 16:41:30 +00:00
committed by GitHub
parent cfe542acb9
commit 293fbebef2
2 changed files with 92 additions and 52 deletions

View File

@@ -27,6 +27,7 @@ from homeassistant.const import (
CONF_EVENT_DATA, CONF_EVENT_DATA,
CONF_ID, CONF_ID,
CONF_MODE, CONF_MODE,
CONF_OPTIONS,
CONF_PATH, CONF_PATH,
CONF_PLATFORM, CONF_PLATFORM,
CONF_TRIGGERS, CONF_TRIGGERS,
@@ -1215,7 +1216,7 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]:
return trigger_conf[CONF_ENTITY_ID] # type: ignore[no-any-return] return trigger_conf[CONF_ENTITY_ID] # type: ignore[no-any-return]
if trigger_conf[CONF_PLATFORM] == "calendar": if trigger_conf[CONF_PLATFORM] == "calendar":
return [trigger_conf[CONF_ENTITY_ID]] return [trigger_conf[CONF_OPTIONS][CONF_ENTITY_ID]]
if trigger_conf[CONF_PLATFORM] == "zone": if trigger_conf[CONF_PLATFORM] == "zone":
return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]] # type: ignore[no-any-return] return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]] # type: ignore[no-any-return]

View File

@@ -2,29 +2,30 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine from collections.abc import Awaitable, Callable
from dataclasses import dataclass from dataclasses import dataclass
import datetime import datetime
import logging import logging
from typing import Any from typing import TYPE_CHECKING, Any, cast
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_ENTITY_ID, CONF_EVENT, CONF_OFFSET, CONF_PLATFORM from homeassistant.const import CONF_ENTITY_ID, CONF_EVENT, CONF_OFFSET, CONF_OPTIONS
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import move_top_level_schema_fields_to_options
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_point_in_time, async_track_point_in_time,
async_track_time_interval, async_track_time_interval,
) )
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import CalendarEntity, CalendarEvent from . import CalendarEntity, CalendarEvent
from .const import DATA_COMPONENT, DOMAIN from .const import DATA_COMPONENT
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -32,13 +33,17 @@ EVENT_START = "start"
EVENT_END = "end" EVENT_END = "end"
UPDATE_INTERVAL = datetime.timedelta(minutes=15) UPDATE_INTERVAL = datetime.timedelta(minutes=15)
TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
{ _OPTIONS_SCHEMA_DICT = {
vol.Required(CONF_PLATFORM): DOMAIN,
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_EVENT, default=EVENT_START): vol.In({EVENT_START, EVENT_END}), vol.Optional(CONF_EVENT, default=EVENT_START): vol.In({EVENT_START, EVENT_END}),
vol.Optional(CONF_OFFSET, default=datetime.timedelta(0)): cv.time_period, vol.Optional(CONF_OFFSET, default=datetime.timedelta(0)): cv.time_period,
} }
_CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_OPTIONS): _OPTIONS_SCHEMA_DICT,
},
) )
# mypy: disallow-any-generics # mypy: disallow-any-generics
@@ -169,14 +174,14 @@ class CalendarEventListener:
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
job: HassJob[..., Coroutine[Any, Any, None] | Any], action_runner: TriggerActionRunner,
trigger_data: dict[str, Any], trigger_payload: dict[str, Any],
fetcher: QueuedEventFetcher, fetcher: QueuedEventFetcher,
) -> None: ) -> None:
"""Initialize CalendarEventListener.""" """Initialize CalendarEventListener."""
self._hass = hass self._hass = hass
self._job = job self._action_runner = action_runner
self._trigger_data = trigger_data self._trigger_payload = trigger_payload
self._unsub_event: CALLBACK_TYPE | None = None self._unsub_event: CALLBACK_TYPE | None = None
self._unsub_refresh: CALLBACK_TYPE | None = None self._unsub_refresh: CALLBACK_TYPE | None = None
self._fetcher = fetcher self._fetcher = fetcher
@@ -233,15 +238,11 @@ class CalendarEventListener:
while self._events and self._events[0].trigger_time <= now: while self._events and self._events[0].trigger_time <= now:
queued_event = self._events.pop(0) queued_event = self._events.pop(0)
_LOGGER.debug("Dispatching event: %s", queued_event.event) _LOGGER.debug("Dispatching event: %s", queued_event.event)
self._hass.async_run_hass_job( payload = {
self._job, **self._trigger_payload,
{
"trigger": {
**self._trigger_data,
"calendar_event": queued_event.event.as_dict(), "calendar_event": queued_event.event.as_dict(),
} }
}, self._action_runner(payload, "calendar event state change")
)
async def _handle_refresh(self, now_utc: datetime.datetime) -> None: async def _handle_refresh(self, now_utc: datetime.datetime) -> None:
"""Handle core config update.""" """Handle core config update."""
@@ -259,31 +260,69 @@ class CalendarEventListener:
self._listen_next_calendar_event() self._listen_next_calendar_event()
async def async_attach_trigger( class EventTrigger(Trigger):
hass: HomeAssistant, """Calendar event trigger."""
config: ConfigType,
action: TriggerActionType, _options: dict[str, Any]
trigger_info: TriggerInfo,
) -> CALLBACK_TYPE: @classmethod
"""Attach trigger for the specified calendar.""" async def async_validate_complete_config(
entity_id = config[CONF_ENTITY_ID] cls, hass: HomeAssistant, complete_config: ConfigType
event_type = config[CONF_EVENT] ) -> ConfigType:
offset = config[CONF_OFFSET] """Validate complete config."""
complete_config = move_top_level_schema_fields_to_options(
complete_config, _OPTIONS_SCHEMA_DICT
)
return await super().async_validate_complete_config(hass, complete_config)
@classmethod
async def async_validate_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
return cast(ConfigType, _CONFIG_SCHEMA(config))
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize trigger."""
super().__init__(hass, config)
if TYPE_CHECKING:
assert config.options is not None
self._options = config.options
async def async_attach_runner(
self, run_action: TriggerActionRunner
) -> CALLBACK_TYPE:
"""Attach a trigger."""
entity_id = self._options[CONF_ENTITY_ID]
event_type = self._options[CONF_EVENT]
offset = self._options[CONF_OFFSET]
# Validate the entity id is valid # Validate the entity id is valid
get_entity(hass, entity_id) get_entity(self._hass, entity_id)
trigger_data = { trigger_data = {
**trigger_info["trigger_data"],
"platform": DOMAIN,
"event": event_type, "event": event_type,
"offset": offset, "offset": offset,
} }
listener = CalendarEventListener( listener = CalendarEventListener(
hass, self._hass,
HassJob(action), run_action,
trigger_data, trigger_data,
queued_event_fetcher(event_fetcher(hass, entity_id), event_type, offset), queued_event_fetcher(
event_fetcher(self._hass, entity_id), event_type, offset
),
) )
await listener.async_attach() await listener.async_attach()
return listener.async_detach return listener.async_detach
TRIGGERS: dict[str, type[Trigger]] = {
"_": EventTrigger,
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for calendars."""
return TRIGGERS