core/homeassistant/components/text/__init__.py

271 lines
8.3 KiB
Python

"""Component to allow setting text as platforms."""
from __future__ import annotations
from dataclasses import asdict, dataclass
from datetime import timedelta
import logging
import re
from typing import Any, final
import voluptuous as vol
from homeassistant.backports.enum import StrEnum
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import MAX_LENGTH_STATE_STATE
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
)
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
from homeassistant.helpers.typing import ConfigType
from .const import (
ATTR_MAX,
ATTR_MIN,
ATTR_MODE,
ATTR_PATTERN,
ATTR_VALUE,
DOMAIN,
SERVICE_SET_VALUE,
)
SCAN_INTERVAL = timedelta(seconds=30)
ENTITY_ID_FORMAT = DOMAIN + ".{}"
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
_LOGGER = logging.getLogger(__name__)
__all__ = ["DOMAIN", "TextEntity", "TextEntityDescription", "TextMode"]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Text entities."""
component = hass.data[DOMAIN] = EntityComponent[TextEntity](
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
)
await component.async_setup(config)
component.async_register_entity_service(
SERVICE_SET_VALUE,
{vol.Required(ATTR_VALUE): cv.string},
_async_set_value,
)
return True
async def _async_set_value(entity: TextEntity, service_call: ServiceCall) -> None:
"""Service call wrapper to set a new value."""
value = service_call.data[ATTR_VALUE]
if len(value) < entity.min:
raise ValueError(
f"Value {value} for {entity.name} is too short (minimum length"
f" {entity.min})"
)
if len(value) > entity.max:
raise ValueError(
f"Value {value} for {entity.name} is too long (maximum length {entity.max})"
)
if entity.pattern_cmp and not entity.pattern_cmp.match(value):
raise ValueError(
f"Value {value} for {entity.name} doesn't match pattern {entity.pattern}"
)
await entity.async_set_value(value)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
component: EntityComponent[TextEntity] = 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[TextEntity] = hass.data[DOMAIN]
return await component.async_unload_entry(entry)
class TextMode(StrEnum):
"""Modes for text entities."""
PASSWORD = "password"
TEXT = "text"
@dataclass
class TextEntityDescription(EntityDescription):
"""A class that describes text entities."""
native_min: int = 0
native_max: int = MAX_LENGTH_STATE_STATE
mode: TextMode = TextMode.TEXT
pattern: str | None = None
class TextEntity(Entity):
"""Representation of a Text entity."""
entity_description: TextEntityDescription
_attr_mode: TextMode
_attr_native_value: str | None
_attr_native_min: int
_attr_native_max: int
_attr_pattern: str | None
_attr_state: None = None
__pattern_cmp: re.Pattern | None = None
@property
def capability_attributes(self) -> dict[str, Any]:
"""Return capability attributes."""
return {
ATTR_MODE: self.mode,
ATTR_MIN: self.min,
ATTR_MAX: self.max,
ATTR_PATTERN: self.pattern,
}
@property
@final
def state(self) -> str | None:
"""Return the entity state."""
if self.native_value is None:
return None
if len(self.native_value) < self.min:
raise ValueError(
f"Entity {self.entity_id} provides state {self.native_value} which is "
f"too short (minimum length {self.min})"
)
if len(self.native_value) > self.max:
raise ValueError(
f"Entity {self.entity_id} provides state {self.native_value} which is "
f"too long (maximum length {self.max})"
)
if self.pattern_cmp and not self.pattern_cmp.match(self.native_value):
raise ValueError(
f"Entity {self.entity_id} provides state {self.native_value} which "
f"does not match expected pattern {self.pattern}"
)
return self.native_value
@property
def mode(self) -> TextMode:
"""Return the mode of the entity."""
if hasattr(self, "_attr_mode"):
return self._attr_mode
if hasattr(self, "entity_description"):
return self.entity_description.mode
return TextMode.TEXT
@property
def native_min(self) -> int:
"""Return the minimum length of the value."""
if hasattr(self, "_attr_native_min"):
return self._attr_native_min
if hasattr(self, "entity_description"):
return self.entity_description.native_min
return 0
@property
@final
def min(self) -> int:
"""Return the minimum length of the value."""
return max(self.native_min, 0)
@property
def native_max(self) -> int:
"""Return the maximum length of the value."""
if hasattr(self, "_attr_native_max"):
return self._attr_native_max
if hasattr(self, "entity_description"):
return self.entity_description.native_max
return MAX_LENGTH_STATE_STATE
@property
@final
def max(self) -> int:
"""Return the maximum length of the value."""
return min(self.native_max, MAX_LENGTH_STATE_STATE)
@property
@final
def pattern_cmp(self) -> re.Pattern | None:
"""Return a compiled pattern."""
if self.pattern is None:
self.__pattern_cmp = None
return None
if not self.__pattern_cmp or self.pattern != self.__pattern_cmp.pattern:
self.__pattern_cmp = re.compile(self.pattern)
return self.__pattern_cmp
@property
def pattern(self) -> str | None:
"""Return the regex pattern that the value must match."""
if hasattr(self, "_attr_pattern"):
return self._attr_pattern
if hasattr(self, "entity_description"):
return self.entity_description.pattern
return None
@property
def native_value(self) -> str | None:
"""Return the value reported by the text."""
return self._attr_native_value
def set_value(self, value: str) -> None:
"""Change the value."""
raise NotImplementedError()
async def async_set_value(self, value: str) -> None:
"""Change the value."""
await self.hass.async_add_executor_job(self.set_value, value)
@dataclass
class TextExtraStoredData(ExtraStoredData):
"""Object to hold extra stored data."""
native_value: str | None
native_min: int
native_max: int
def as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the text data."""
return asdict(self)
@classmethod
def from_dict(cls, restored: dict[str, Any]) -> TextExtraStoredData | None:
"""Initialize a stored text state from a dict."""
try:
return cls(
restored["native_value"],
restored["native_min"],
restored["native_max"],
)
except KeyError:
return None
class RestoreText(TextEntity, RestoreEntity):
"""Mixin class for restoring previous text state."""
@property
def extra_restore_state_data(self) -> TextExtraStoredData:
"""Return text specific state data to be restored."""
return TextExtraStoredData(
self.native_value,
self.native_min,
self.native_max,
)
async def async_get_last_text_data(self) -> TextExtraStoredData | None:
"""Restore attributes."""
if (restored_last_extra_data := await self.async_get_last_extra_data()) is None:
return None
return TextExtraStoredData.from_dict(restored_last_extra_data.as_dict())