ha-core/homeassistant/components/event/__init__.py

216 lines
6.8 KiB
Python

"""Component for handling incoming events as a platform."""
from __future__ import annotations
from dataclasses import asdict, dataclass
from datetime import datetime, timedelta
from enum import StrEnum
from functools import cached_property
import logging
from typing import Any, Self, final
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.config_validation import (
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
)
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from .const import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES, DOMAIN
SCAN_INTERVAL = timedelta(seconds=30)
ENTITY_ID_FORMAT = DOMAIN + ".{}"
_LOGGER = logging.getLogger(__name__)
class EventDeviceClass(StrEnum):
"""Device class for events."""
DOORBELL = "doorbell"
BUTTON = "button"
MOTION = "motion"
__all__ = [
"ATTR_EVENT_TYPE",
"ATTR_EVENT_TYPES",
"DOMAIN",
"PLATFORM_SCHEMA_BASE",
"PLATFORM_SCHEMA",
"EventDeviceClass",
"EventEntity",
"EventEntityDescription",
]
# mypy: disallow-any-generics
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Event entities."""
component = hass.data[DOMAIN] = EntityComponent[EventEntity](
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
)
await component.async_setup(config)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
component: EntityComponent[EventEntity] = hass.data[DOMAIN]
return await component.async_setup_entry(entry)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
component: EntityComponent[EventEntity] = hass.data[DOMAIN]
return await component.async_unload_entry(entry)
class EventEntityDescription(EntityDescription, frozen_or_thawed=True):
"""A class that describes event entities."""
device_class: EventDeviceClass | None = None
event_types: list[str] | None = None
@dataclass
class EventExtraStoredData(ExtraStoredData):
"""Object to hold extra stored data."""
last_event_type: str | None
last_event_attributes: dict[str, Any] | None
def as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the event data."""
return asdict(self)
@classmethod
def from_dict(cls, restored: dict[str, Any]) -> Self | None:
"""Initialize a stored event state from a dict."""
try:
return cls(
restored["last_event_type"],
restored["last_event_attributes"],
)
except KeyError:
return None
CACHED_PROPERTIES_WITH_ATTR_ = {
"device_class",
"event_types",
}
class EventEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Representation of an Event entity."""
_entity_component_unrecorded_attributes = frozenset({ATTR_EVENT_TYPES})
entity_description: EventEntityDescription
_attr_device_class: EventDeviceClass | None
_attr_event_types: list[str]
_attr_state: None
__last_event_triggered: datetime | None = None
__last_event_type: str | None = None
__last_event_attributes: dict[str, Any] | None = None
@cached_property
def device_class(self) -> EventDeviceClass | None:
"""Return the class of this entity."""
if hasattr(self, "_attr_device_class"):
return self._attr_device_class
if hasattr(self, "entity_description"):
return self.entity_description.device_class
return None
@cached_property
def event_types(self) -> list[str]:
"""Return a list of possible events."""
if hasattr(self, "_attr_event_types"):
return self._attr_event_types
if (
hasattr(self, "entity_description")
and self.entity_description.event_types is not None
):
return self.entity_description.event_types
raise AttributeError
@final
def _trigger_event(
self, event_type: str, event_attributes: dict[str, Any] | None = None
) -> None:
"""Process a new event."""
if event_type not in self.event_types:
raise ValueError(f"Invalid event type {event_type} for {self.entity_id}")
self.__last_event_triggered = dt_util.utcnow()
self.__last_event_type = event_type
self.__last_event_attributes = event_attributes
def _default_to_device_class_name(self) -> bool:
"""Return True if an unnamed entity should be named by its device class.
For events this is True if the entity has a device class.
"""
return self.device_class is not None
@property
@final
def capability_attributes(self) -> dict[str, list[str]]:
"""Return capability attributes."""
return {
ATTR_EVENT_TYPES: self.event_types,
}
@property
@final
def state(self) -> str | None:
"""Return the entity state."""
if (last_event := self.__last_event_triggered) is None:
return None
return last_event.isoformat(timespec="milliseconds")
@final
@property
def state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
attributes = {ATTR_EVENT_TYPE: self.__last_event_type}
if last_event_attributes := self.__last_event_attributes:
attributes |= last_event_attributes
return attributes
@final
async def async_internal_added_to_hass(self) -> None:
"""Call when the event entity is added to hass."""
await super().async_internal_added_to_hass()
if (
(state := await self.async_get_last_state())
and state.state is not None
and (event_data := await self.async_get_last_event_data())
):
self.__last_event_triggered = dt_util.parse_datetime(state.state)
self.__last_event_type = event_data.last_event_type
self.__last_event_attributes = event_data.last_event_attributes
@property
def extra_restore_state_data(self) -> EventExtraStoredData:
"""Return event specific state data to be restored."""
return EventExtraStoredData(
self.__last_event_type,
self.__last_event_attributes,
)
async def async_get_last_event_data(self) -> EventExtraStoredData | None:
"""Restore event specific state date."""
if (restored_last_extra_data := await self.async_get_last_extra_data()) is None:
return None
return EventExtraStoredData.from_dict(restored_last_extra_data.as_dict())