mirror of
https://github.com/home-assistant/core.git
synced 2025-12-20 02:48:57 +00:00
Add new template entity framework to template alarm control panel (#156614)
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator, Sequence
|
||||
from enum import Enum
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
@@ -29,8 +28,7 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
@@ -39,6 +37,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.script import Script
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import validators as tcv
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TriggerUpdateCoordinator
|
||||
from .entity import AbstractTemplateEntity
|
||||
@@ -56,19 +55,6 @@ from .template_entity import TemplateEntity
|
||||
from .trigger_entity import TriggerEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_VALID_STATES = [
|
||||
AlarmControlPanelState.ARMED_AWAY,
|
||||
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
|
||||
AlarmControlPanelState.ARMED_HOME,
|
||||
AlarmControlPanelState.ARMED_NIGHT,
|
||||
AlarmControlPanelState.ARMED_VACATION,
|
||||
AlarmControlPanelState.ARMING,
|
||||
AlarmControlPanelState.DISARMED,
|
||||
AlarmControlPanelState.DISARMING,
|
||||
AlarmControlPanelState.PENDING,
|
||||
AlarmControlPanelState.TRIGGERED,
|
||||
STATE_UNAVAILABLE,
|
||||
]
|
||||
|
||||
CONF_ALARM_CONTROL_PANELS = "panels"
|
||||
CONF_ARM_AWAY_ACTION = "arm_away"
|
||||
@@ -212,22 +198,22 @@ class AbstractTemplateAlarmControlPanel(
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
def __init__(self, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called
|
||||
"""Initialize the features."""
|
||||
self._attr_code_arm_required: bool = config[CONF_CODE_ARM_REQUIRED]
|
||||
self._attr_code_format = config[CONF_CODE_FORMAT].value
|
||||
# The super init is not called because TemplateEntity calls AbstractTemplateEntity.__init__.
|
||||
def __init__(self, name: str) -> None: # pylint: disable=super-init-not-called
|
||||
"""Setup the templates and scripts."""
|
||||
|
||||
self._attr_code_arm_required: bool = self._config[CONF_CODE_ARM_REQUIRED]
|
||||
self._attr_code_format = self._config[CONF_CODE_FORMAT].value
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_alarm_state",
|
||||
validator=tcv.strenum(self, CONF_STATE, AlarmControlPanelState),
|
||||
)
|
||||
|
||||
self._attr_supported_features: AlarmControlPanelEntityFeature = (
|
||||
AlarmControlPanelEntityFeature(0)
|
||||
)
|
||||
|
||||
def _iterate_scripts(
|
||||
self, config: dict[str, Any]
|
||||
) -> Generator[
|
||||
tuple[str, Sequence[dict[str, Any]], AlarmControlPanelEntityFeature | int]
|
||||
]:
|
||||
for action_id, supported_feature in (
|
||||
(CONF_DISARM_ACTION, 0),
|
||||
(CONF_ARM_AWAY_ACTION, AlarmControlPanelEntityFeature.ARM_AWAY),
|
||||
@@ -240,35 +226,21 @@ class AbstractTemplateAlarmControlPanel(
|
||||
),
|
||||
(CONF_TRIGGER_ACTION, AlarmControlPanelEntityFeature.TRIGGER),
|
||||
):
|
||||
if (action_config := config.get(action_id)) is not None:
|
||||
yield (action_id, action_config, supported_feature)
|
||||
if (action_config := self._config.get(action_id)) is not None:
|
||||
self.add_script(action_id, action_config, name, DOMAIN)
|
||||
self._attr_supported_features |= supported_feature
|
||||
|
||||
async def _async_handle_restored_state(self) -> None:
|
||||
if (
|
||||
(last_state := await self.async_get_last_state()) is not None
|
||||
and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||
and last_state.state in _VALID_STATES
|
||||
and last_state.state in AlarmControlPanelState
|
||||
# The trigger might have fired already while we waited for stored data,
|
||||
# then we should not restore state
|
||||
and self._attr_alarm_state is None
|
||||
):
|
||||
self._attr_alarm_state = AlarmControlPanelState(last_state.state)
|
||||
|
||||
def _handle_state(self, result: Any) -> None:
|
||||
# Validate state
|
||||
if result in _VALID_STATES:
|
||||
self._attr_alarm_state = result
|
||||
_LOGGER.debug("Valid state - %s", result)
|
||||
return
|
||||
|
||||
_LOGGER.error(
|
||||
"Received invalid alarm panel state: %s for entity %s. Expected: %s",
|
||||
result,
|
||||
self.entity_id,
|
||||
", ".join(_VALID_STATES),
|
||||
)
|
||||
self._attr_alarm_state = None
|
||||
|
||||
async def _async_alarm_arm(self, state: Any, script: Script | None, code: Any):
|
||||
"""Arm the panel to specified state with supplied script."""
|
||||
|
||||
@@ -351,39 +323,17 @@ class StateAlarmControlPanelEntity(TemplateEntity, AbstractTemplateAlarmControlP
|
||||
) -> None:
|
||||
"""Initialize the panel."""
|
||||
TemplateEntity.__init__(self, hass, config, unique_id)
|
||||
AbstractTemplateAlarmControlPanel.__init__(self, config)
|
||||
name = self._attr_name
|
||||
if TYPE_CHECKING:
|
||||
assert name is not None
|
||||
|
||||
for action_id, action_config, supported_feature in self._iterate_scripts(
|
||||
config
|
||||
):
|
||||
self.add_script(action_id, action_config, name, DOMAIN)
|
||||
self._attr_supported_features |= supported_feature
|
||||
AbstractTemplateAlarmControlPanel.__init__(self, name)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last state."""
|
||||
await super().async_added_to_hass()
|
||||
await self._async_handle_restored_state()
|
||||
|
||||
@callback
|
||||
def _update_state(self, result):
|
||||
if isinstance(result, TemplateError):
|
||||
self._attr_alarm_state = None
|
||||
return
|
||||
|
||||
self._handle_state(result)
|
||||
|
||||
@callback
|
||||
def _async_setup_templates(self) -> None:
|
||||
"""Set up templates."""
|
||||
if self._template:
|
||||
self.add_template_attribute(
|
||||
"_attr_alarm_state", self._template, None, self._update_state
|
||||
)
|
||||
super()._async_setup_templates()
|
||||
|
||||
|
||||
class TriggerAlarmControlPanelEntity(TriggerEntity, AbstractTemplateAlarmControlPanel):
|
||||
"""Alarm Control Panel entity based on trigger data."""
|
||||
@@ -398,19 +348,8 @@ class TriggerAlarmControlPanelEntity(TriggerEntity, AbstractTemplateAlarmControl
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
TriggerEntity.__init__(self, hass, coordinator, config)
|
||||
AbstractTemplateAlarmControlPanel.__init__(self, config)
|
||||
|
||||
self._attr_name = name = self._rendered.get(CONF_NAME, DEFAULT_NAME)
|
||||
|
||||
if isinstance(config.get(CONF_STATE), template.Template):
|
||||
self._to_render_simple.append(CONF_STATE)
|
||||
self._parse_result.add(CONF_STATE)
|
||||
|
||||
for action_id, action_config, supported_feature in self._iterate_scripts(
|
||||
config
|
||||
):
|
||||
self.add_script(action_id, action_config, name, DOMAIN)
|
||||
self._attr_supported_features |= supported_feature
|
||||
AbstractTemplateAlarmControlPanel.__init__(self, name)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last state."""
|
||||
@@ -426,7 +365,6 @@ class TriggerAlarmControlPanelEntity(TriggerEntity, AbstractTemplateAlarmControl
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if (rendered := self._rendered.get(CONF_STATE)) is not None:
|
||||
self._handle_state(rendered)
|
||||
if self.handle_rendered_result(CONF_STATE):
|
||||
self.async_set_context(self.coordinator.data["context"])
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Template entity base class."""
|
||||
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Sequence
|
||||
from collections.abc import Callable, Sequence
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -18,6 +19,17 @@ from .const import CONF_DEFAULT_ENTITY_ID
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityTemplate:
|
||||
"""Information class for properly handling template results."""
|
||||
|
||||
attribute: str
|
||||
template: Template
|
||||
validator: Callable[[Any], Any] | None
|
||||
on_update: Callable[[Any], None] | None
|
||||
none_on_template_error: bool
|
||||
|
||||
|
||||
class AbstractTemplateEntity(Entity):
|
||||
"""Actions linked to a template entity."""
|
||||
|
||||
@@ -34,6 +46,8 @@ class AbstractTemplateEntity(Entity):
|
||||
"""Initialize the entity."""
|
||||
|
||||
self.hass = hass
|
||||
self._config = config
|
||||
self._templates: dict[str, EntityTemplate] = {}
|
||||
self._action_scripts: dict[str, Script] = {}
|
||||
|
||||
if self._optimistic_entity:
|
||||
@@ -72,6 +86,35 @@ class AbstractTemplateEntity(Entity):
|
||||
def _render_script_variables(self) -> dict:
|
||||
"""Render configured variables."""
|
||||
|
||||
@abstractmethod
|
||||
def setup_state_template(
|
||||
self,
|
||||
option: str,
|
||||
attribute: str,
|
||||
validator: Callable[[Any], Any] | None = None,
|
||||
on_update: Callable[[Any], None] | None = None,
|
||||
) -> None:
|
||||
"""Set up a template that manages the main state of the entity."""
|
||||
|
||||
def add_template(
|
||||
self,
|
||||
option: str,
|
||||
attribute: str,
|
||||
validator: Callable[[Any], Any] | None = None,
|
||||
on_update: Callable[[Any], None] | None = None,
|
||||
none_on_template_error: bool = False,
|
||||
add_if_static: bool = True,
|
||||
) -> Template | None:
|
||||
"""Add a template."""
|
||||
if (template := self._config.get(option)) and isinstance(template, Template):
|
||||
if add_if_static or (not template.is_static):
|
||||
self._templates[option] = EntityTemplate(
|
||||
attribute, template, validator, on_update, none_on_template_error
|
||||
)
|
||||
return template
|
||||
|
||||
return None
|
||||
|
||||
def add_script(
|
||||
self,
|
||||
script_id: str,
|
||||
|
||||
@@ -181,9 +181,6 @@ class TemplateEntity(AbstractTemplateEntity):
|
||||
self._run_variables: ScriptVariables | dict
|
||||
self._attribute_templates = config.get(CONF_ATTRIBUTES)
|
||||
self._availability_template = config.get(CONF_AVAILABILITY)
|
||||
self._icon_template = config.get(CONF_ICON)
|
||||
self._entity_picture_template = config.get(CONF_PICTURE)
|
||||
self._friendly_name_template = config.get(CONF_NAME)
|
||||
self._run_variables = config.get(CONF_VARIABLES, {})
|
||||
self._blueprint_inputs = config.get("raw_blueprint_inputs")
|
||||
|
||||
@@ -208,27 +205,28 @@ class TemplateEntity(AbstractTemplateEntity):
|
||||
)
|
||||
variables = {"this": DummyState(), **variables}
|
||||
|
||||
# Try to render the name as it can influence the entity ID
|
||||
self.add_template(
|
||||
CONF_AVAILABILITY, "_attr_available", on_update=self._update_available
|
||||
)
|
||||
|
||||
# Render name, icon, and picture early. name is rendered early because it influences
|
||||
# the entity_id. icon and picture are rendered early to ensure they are populated even
|
||||
# if the entity renders unavailable.
|
||||
self._attr_name = None
|
||||
if self._friendly_name_template:
|
||||
with contextlib.suppress(TemplateError):
|
||||
self._attr_name = self._friendly_name_template.async_render(
|
||||
variables=variables, parse_result=False
|
||||
)
|
||||
|
||||
# Templates will not render while the entity is unavailable, try to render the
|
||||
# icon and picture templates.
|
||||
if self._entity_picture_template:
|
||||
with contextlib.suppress(TemplateError):
|
||||
self._attr_entity_picture = self._entity_picture_template.async_render(
|
||||
variables=variables, parse_result=False
|
||||
)
|
||||
|
||||
if self._icon_template:
|
||||
with contextlib.suppress(TemplateError):
|
||||
self._attr_icon = self._icon_template.async_render(
|
||||
variables=variables, parse_result=False
|
||||
)
|
||||
for option, attribute, validator in (
|
||||
(CONF_ICON, "_attr_icon", vol.Or(cv.whitespace, cv.icon)),
|
||||
(CONF_PICTURE, "_attr_entity_picture", cv.string),
|
||||
(CONF_NAME, "_attr_name", cv.string),
|
||||
):
|
||||
if template := self.add_template(
|
||||
option, attribute, validator, add_if_static=option != CONF_NAME
|
||||
):
|
||||
with contextlib.suppress(TemplateError):
|
||||
setattr(
|
||||
self,
|
||||
attribute,
|
||||
template.async_render(variables=variables, parse_result=False),
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_available(self, result: str | TemplateError) -> None:
|
||||
@@ -278,6 +276,33 @@ class TemplateEntity(AbstractTemplateEntity):
|
||||
},
|
||||
)
|
||||
|
||||
def setup_state_template(
|
||||
self,
|
||||
option: str,
|
||||
attribute: str,
|
||||
validator: Callable[[Any], Any] | None = None,
|
||||
on_update: Callable[[Any], None] | None = None,
|
||||
) -> None:
|
||||
"""Set up a template that manages the main state of the entity."""
|
||||
|
||||
@callback
|
||||
def _update_state(result: Any) -> None:
|
||||
if isinstance(result, TemplateError):
|
||||
setattr(self, attribute, None)
|
||||
if self._availability_template:
|
||||
return
|
||||
|
||||
self._attr_available = False
|
||||
return
|
||||
|
||||
state = validator(result) if validator else result
|
||||
if on_update:
|
||||
on_update(state)
|
||||
else:
|
||||
setattr(self, attribute, state)
|
||||
|
||||
self.add_template(option, attribute, on_update=_update_state)
|
||||
|
||||
def add_template_attribute(
|
||||
self,
|
||||
attribute: str,
|
||||
@@ -417,30 +442,20 @@ class TemplateEntity(AbstractTemplateEntity):
|
||||
@callback
|
||||
def _async_setup_templates(self) -> None:
|
||||
"""Set up templates."""
|
||||
if self._availability_template is not None:
|
||||
self.add_template_attribute(
|
||||
"_attr_available",
|
||||
self._availability_template,
|
||||
None,
|
||||
self._update_available,
|
||||
)
|
||||
|
||||
# Handle attributes as a dictionary.
|
||||
if self._attribute_templates is not None:
|
||||
for key, value in self._attribute_templates.items():
|
||||
self._add_attribute_template(key, value)
|
||||
if self._icon_template is not None:
|
||||
|
||||
# Iterate all dynamic templates and add listeners.
|
||||
for entity_template in self._templates.values():
|
||||
self.add_template_attribute(
|
||||
"_attr_icon", self._icon_template, vol.Or(cv.whitespace, cv.icon)
|
||||
)
|
||||
if self._entity_picture_template is not None:
|
||||
self.add_template_attribute(
|
||||
"_attr_entity_picture", self._entity_picture_template, cv.string
|
||||
)
|
||||
if (
|
||||
self._friendly_name_template is not None
|
||||
and not self._friendly_name_template.is_static
|
||||
):
|
||||
self.add_template_attribute(
|
||||
"_attr_name", self._friendly_name_template, cv.string
|
||||
entity_template.attribute,
|
||||
entity_template.template,
|
||||
entity_template.validator,
|
||||
entity_template.on_update,
|
||||
entity_template.none_on_template_error,
|
||||
)
|
||||
|
||||
@callback
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import CONF_STATE, CONF_VARIABLES
|
||||
@@ -50,6 +51,19 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
else:
|
||||
self._unique_id = unique_id
|
||||
|
||||
def setup_state_template(
|
||||
self,
|
||||
option: str,
|
||||
attribute: str,
|
||||
validator: Callable[[Any], Any] | None = None,
|
||||
on_update: Callable[[Any], None] | None = None,
|
||||
) -> None:
|
||||
"""Set up a template that manages the main state of the entity."""
|
||||
if self._config.get(option):
|
||||
self._to_render_simple.append(CONF_STATE)
|
||||
self._parse_result.add(CONF_STATE)
|
||||
self.add_template(option, attribute, validator, on_update)
|
||||
|
||||
@property
|
||||
def referenced_blueprint(self) -> str | None:
|
||||
"""Return referenced blueprint or None."""
|
||||
@@ -89,6 +103,23 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
self._render_attributes(rendered, variables)
|
||||
self._rendered = rendered
|
||||
|
||||
def handle_rendered_result(self, key: str) -> bool:
|
||||
"""Get a rendered result and return the value."""
|
||||
if (rendered := self._rendered.get(key)) is not None:
|
||||
if (entity_template := self._templates.get(key)) is not None:
|
||||
value = rendered
|
||||
if entity_template.validator:
|
||||
value = entity_template.validator(rendered)
|
||||
|
||||
if entity_template.on_update:
|
||||
entity_template.on_update(value)
|
||||
else:
|
||||
setattr(self, entity_template.attribute, value)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@callback
|
||||
def _process_data(self) -> None:
|
||||
"""Process new data."""
|
||||
|
||||
305
homeassistant/components/template/validators.py
Normal file
305
homeassistant/components/template/validators.py
Normal file
@@ -0,0 +1,305 @@
|
||||
"""Template config validation methods."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Valid on/off values for booleans. These tuples are pulled
|
||||
# from cv.boolean and are used to produce logger errors for the user.
|
||||
RESULT_ON = ("1", "true", "yes", "on", "enable")
|
||||
RESULT_OFF = ("0", "false", "no", "off", "disable")
|
||||
|
||||
|
||||
def _log_validation_result_error(
|
||||
entity: Entity,
|
||||
attribute: str,
|
||||
value: Any,
|
||||
expected: tuple[str, ...] | str,
|
||||
) -> None:
|
||||
"""Log a template result error."""
|
||||
|
||||
# in some cases, like `preview` entities, the entity_id does not exist.
|
||||
if entity.entity_id is None:
|
||||
message = f"Received invalid {attribute}: {value} for entity {entity.name}, %s"
|
||||
else:
|
||||
message = (
|
||||
f"Received invalid {entity.entity_id.split('.')[0]} {attribute}"
|
||||
f": {value} for entity {entity.entity_id}, %s"
|
||||
)
|
||||
|
||||
_LOGGER.error(
|
||||
message,
|
||||
expected
|
||||
if isinstance(expected, str)
|
||||
else "expected one of " + ", ".join(expected),
|
||||
)
|
||||
|
||||
|
||||
def _check_result_for_none(result: Any, **kwargs: Any) -> bool:
|
||||
"""Checks the result for none, unknown, unavailable."""
|
||||
if result is None:
|
||||
return True
|
||||
|
||||
if kwargs.get("none_on_unknown_unavailable") and isinstance(result, str):
|
||||
return result.lower() in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def strenum[T: StrEnum](
|
||||
entity: Entity,
|
||||
attribute: str,
|
||||
state_enum: type[T],
|
||||
state_on: T | None = None,
|
||||
state_off: T | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Callable[[Any], T | None]:
|
||||
"""Converts the template result to an StrEnum.
|
||||
|
||||
All strings will attempt to convert to the StrEnum
|
||||
If state_on or state_off are provided, boolean values will return the
|
||||
enum that represents each boolean value.
|
||||
Anything that cannot convert will result in None.
|
||||
|
||||
none_on_unknown_unavailable
|
||||
"""
|
||||
|
||||
def convert(result: Any) -> T | None:
|
||||
if _check_result_for_none(result, **kwargs):
|
||||
return None
|
||||
|
||||
if isinstance(result, str):
|
||||
value = result.lower().strip()
|
||||
try:
|
||||
return state_enum(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if state_on or state_off:
|
||||
try:
|
||||
bool_value = cv.boolean(result)
|
||||
if state_on and bool_value:
|
||||
return state_on
|
||||
|
||||
if state_off and not bool_value:
|
||||
return state_off
|
||||
|
||||
except vol.Invalid:
|
||||
pass
|
||||
|
||||
expected = tuple(s.value for s in state_enum)
|
||||
if state_on:
|
||||
expected += RESULT_ON
|
||||
if state_off:
|
||||
expected += RESULT_OFF
|
||||
|
||||
_log_validation_result_error(
|
||||
entity,
|
||||
attribute,
|
||||
result,
|
||||
expected,
|
||||
)
|
||||
return None
|
||||
|
||||
return convert
|
||||
|
||||
|
||||
def boolean(
|
||||
entity: Entity,
|
||||
attribute: str,
|
||||
as_true: tuple[str, ...] | None = None,
|
||||
as_false: tuple[str, ...] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Callable[[Any], bool | None]:
|
||||
"""Convert the result to a boolean.
|
||||
|
||||
True/not 0/'1'/'true'/'yes'/'on'/'enable' are considered truthy
|
||||
False/0/'0'/'false'/'no'/'off'/'disable' are considered falsy
|
||||
Additional values provided by as_true are considered truthy
|
||||
Additional values provided by as_false are considered truthy
|
||||
All other values are None
|
||||
"""
|
||||
|
||||
def convert(result: Any) -> bool | None:
|
||||
if _check_result_for_none(result, **kwargs):
|
||||
return None
|
||||
|
||||
if isinstance(result, bool):
|
||||
return result
|
||||
|
||||
if isinstance(result, str) and (as_true or as_false):
|
||||
value = result.lower().strip()
|
||||
if as_true and value in as_true:
|
||||
return True
|
||||
if as_false and value in as_false:
|
||||
return False
|
||||
|
||||
try:
|
||||
return cv.boolean(result)
|
||||
except vol.Invalid:
|
||||
pass
|
||||
|
||||
items: tuple[str, ...] = RESULT_ON + RESULT_OFF
|
||||
if as_true:
|
||||
items += as_true
|
||||
if as_false:
|
||||
items += as_false
|
||||
|
||||
_log_validation_result_error(entity, attribute, result, items)
|
||||
return None
|
||||
|
||||
return convert
|
||||
|
||||
|
||||
def number(
|
||||
entity: Entity,
|
||||
attribute: str,
|
||||
minimum: float | None = None,
|
||||
maximum: float | None = None,
|
||||
return_type: type[float] | type[int] = float,
|
||||
**kwargs: Any,
|
||||
) -> Callable[[Any], float | int | None]:
|
||||
"""Convert the result to a number (float or int).
|
||||
|
||||
Any value in the range is converted to a float or int
|
||||
All other values are None
|
||||
"""
|
||||
message = "expected a number"
|
||||
if minimum is not None and maximum is not None:
|
||||
message = f"{message} between {minimum:0.1f} and {maximum:0.1f}"
|
||||
elif minimum is not None and maximum is None:
|
||||
message = f"{message} greater than or equal to {minimum:0.1f}"
|
||||
elif minimum is None and maximum is not None:
|
||||
message = f"{message} less than or equal to {maximum:0.1f}"
|
||||
|
||||
def convert(result: Any) -> float | int | None:
|
||||
if _check_result_for_none(result, **kwargs):
|
||||
return None
|
||||
|
||||
if (result_type := type(result)) is bool:
|
||||
_log_validation_result_error(entity, attribute, result, message)
|
||||
return None
|
||||
|
||||
if isinstance(result, (float, int)):
|
||||
value = result
|
||||
if return_type is int and result_type is float:
|
||||
value = int(value)
|
||||
elif return_type is float and result_type is int:
|
||||
value = float(value)
|
||||
else:
|
||||
try:
|
||||
value = vol.Coerce(float)(result)
|
||||
if return_type is int:
|
||||
value = int(value)
|
||||
except vol.Invalid:
|
||||
_log_validation_result_error(entity, attribute, result, message)
|
||||
return None
|
||||
|
||||
if minimum is None and maximum is None:
|
||||
return value
|
||||
|
||||
if (
|
||||
(
|
||||
minimum is not None
|
||||
and maximum is not None
|
||||
and minimum <= value <= maximum
|
||||
)
|
||||
or (minimum is not None and maximum is None and value >= minimum)
|
||||
or (minimum is None and maximum is not None and value <= maximum)
|
||||
):
|
||||
return value
|
||||
|
||||
_log_validation_result_error(entity, attribute, result, message)
|
||||
return None
|
||||
|
||||
return convert
|
||||
|
||||
|
||||
def list_of_strings(
|
||||
entity: Entity,
|
||||
attribute: str,
|
||||
none_on_empty: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Callable[[Any], list[str] | None]:
|
||||
"""Convert the result to a list of strings.
|
||||
|
||||
This ensures the result is a list of strings.
|
||||
All other values that are not lists will result in None.
|
||||
|
||||
none_on_empty will cause the converter to return None when the list is empty.
|
||||
"""
|
||||
|
||||
def convert(result: Any) -> list[str] | None:
|
||||
if _check_result_for_none(result, **kwargs):
|
||||
return None
|
||||
|
||||
if not isinstance(result, list):
|
||||
_log_validation_result_error(
|
||||
entity,
|
||||
attribute,
|
||||
result,
|
||||
"expected a list of strings",
|
||||
)
|
||||
return None
|
||||
|
||||
if none_on_empty and len(result) == 0:
|
||||
return None
|
||||
|
||||
# Ensure the result are strings.
|
||||
return [str(v) for v in result]
|
||||
|
||||
return convert
|
||||
|
||||
|
||||
def item_in_list[T](
|
||||
entity: Entity,
|
||||
attribute: str,
|
||||
items: list[Any] | None,
|
||||
items_attribute: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Callable[[Any], Any | None]:
|
||||
"""Assert the result of the template is an item inside a list.
|
||||
|
||||
Returns the result if the result is inside the list.
|
||||
All results that are not inside the list will return None.
|
||||
"""
|
||||
|
||||
def convert(result: Any) -> Any | None:
|
||||
if _check_result_for_none(result, **kwargs):
|
||||
return None
|
||||
|
||||
# items may be mutable based on another template field. Always
|
||||
# perform this check when the items come from an configured
|
||||
# attribute.
|
||||
if items is None or (len(items) == 0):
|
||||
if items_attribute:
|
||||
_log_validation_result_error(
|
||||
entity,
|
||||
attribute,
|
||||
result,
|
||||
f"{items_attribute} is empty",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
if result not in items:
|
||||
_log_validation_result_error(
|
||||
entity,
|
||||
attribute,
|
||||
result,
|
||||
tuple(str(v) for v in items),
|
||||
)
|
||||
return None
|
||||
|
||||
return result
|
||||
|
||||
return convert
|
||||
@@ -1,7 +1,7 @@
|
||||
"""template conftest."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from enum import Enum, StrEnum
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -25,6 +25,15 @@ class ConfigurationStyle(Enum):
|
||||
TRIGGER = "Trigger"
|
||||
|
||||
|
||||
class Brewery(StrEnum):
|
||||
"""Test enum."""
|
||||
|
||||
MMMM = "mmmm"
|
||||
BEER = "beer"
|
||||
IS = "is"
|
||||
GOOD = "good"
|
||||
|
||||
|
||||
def make_test_trigger(*entities: str) -> dict:
|
||||
"""Make a test state trigger."""
|
||||
return {
|
||||
|
||||
@@ -364,23 +364,19 @@ async def test_template_state_text(hass: HomeAssistant) -> None:
|
||||
|
||||
@pytest.mark.parametrize("count", [1])
|
||||
@pytest.mark.parametrize(
|
||||
("state_template", "expected", "trigger_expected"),
|
||||
("state_template", "expected"),
|
||||
[
|
||||
("{{ 'disarmed' }}", AlarmControlPanelState.DISARMED, None),
|
||||
("{{ 'armed_home' }}", AlarmControlPanelState.ARMED_HOME, None),
|
||||
("{{ 'armed_away' }}", AlarmControlPanelState.ARMED_AWAY, None),
|
||||
("{{ 'armed_night' }}", AlarmControlPanelState.ARMED_NIGHT, None),
|
||||
("{{ 'armed_vacation' }}", AlarmControlPanelState.ARMED_VACATION, None),
|
||||
(
|
||||
"{{ 'armed_custom_bypass' }}",
|
||||
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
|
||||
None,
|
||||
),
|
||||
("{{ 'pending' }}", AlarmControlPanelState.PENDING, None),
|
||||
("{{ 'arming' }}", AlarmControlPanelState.ARMING, None),
|
||||
("{{ 'disarming' }}", AlarmControlPanelState.DISARMING, None),
|
||||
("{{ 'triggered' }}", AlarmControlPanelState.TRIGGERED, None),
|
||||
("{{ x - 1 }}", STATE_UNKNOWN, STATE_UNAVAILABLE),
|
||||
("{{ 'disarmed' }}", AlarmControlPanelState.DISARMED),
|
||||
("{{ 'armed_home' }}", AlarmControlPanelState.ARMED_HOME),
|
||||
("{{ 'armed_away' }}", AlarmControlPanelState.ARMED_AWAY),
|
||||
("{{ 'armed_night' }}", AlarmControlPanelState.ARMED_NIGHT),
|
||||
("{{ 'armed_vacation' }}", AlarmControlPanelState.ARMED_VACATION),
|
||||
("{{ 'armed_custom_bypass' }}", AlarmControlPanelState.ARMED_CUSTOM_BYPASS),
|
||||
("{{ 'pending' }}", AlarmControlPanelState.PENDING),
|
||||
("{{ 'arming' }}", AlarmControlPanelState.ARMING),
|
||||
("{{ 'disarming' }}", AlarmControlPanelState.DISARMING),
|
||||
("{{ 'triggered' }}", AlarmControlPanelState.TRIGGERED),
|
||||
("{{ x - 1 }}", STATE_UNAVAILABLE),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
@@ -388,9 +384,7 @@ async def test_template_state_text(hass: HomeAssistant) -> None:
|
||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||
)
|
||||
@pytest.mark.usefixtures("setup_state_panel")
|
||||
async def test_state_template_states(
|
||||
hass: HomeAssistant, expected: str, trigger_expected: str, style: ConfigurationStyle
|
||||
) -> None:
|
||||
async def test_state_template_states(hass: HomeAssistant, expected: str) -> None:
|
||||
"""Test the state template."""
|
||||
|
||||
# Force a trigger
|
||||
@@ -399,9 +393,6 @@ async def test_state_template_states(
|
||||
|
||||
state = hass.states.get(TEST_ENTITY_ID)
|
||||
|
||||
if trigger_expected and style == ConfigurationStyle.TRIGGER:
|
||||
expected = trigger_expected
|
||||
|
||||
assert state.state == expected
|
||||
|
||||
|
||||
|
||||
924
tests/components/template/test_validators.py
Normal file
924
tests/components/template/test_validators.py
Normal file
@@ -0,0 +1,924 @@
|
||||
"""Test template validators."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.template import validators as cv
|
||||
from homeassistant.components.template.template_entity import TemplateEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.template import Template
|
||||
|
||||
from .conftest import Brewery
|
||||
|
||||
|
||||
def expect_boolean_on(true_value: Any = True) -> list[tuple[Any, Any]]:
|
||||
"""Tuple of commonn boolean on value expected pairs."""
|
||||
return [
|
||||
("on", true_value),
|
||||
("On", true_value),
|
||||
("oN", true_value),
|
||||
("true", true_value),
|
||||
("yes", true_value),
|
||||
("enable", true_value),
|
||||
("1", true_value),
|
||||
(True, true_value),
|
||||
(1, true_value),
|
||||
(8.23432, true_value),
|
||||
(0.23432, true_value),
|
||||
]
|
||||
|
||||
|
||||
def expect_boolean_off(false_value: Any = False) -> list[tuple[Any, Any]]:
|
||||
"""Tuple of commonn boolean off value expected pairs."""
|
||||
return [
|
||||
("off", false_value),
|
||||
("false", false_value),
|
||||
("no", false_value),
|
||||
("disable", false_value),
|
||||
("0", false_value),
|
||||
(False, false_value),
|
||||
(0, false_value),
|
||||
]
|
||||
|
||||
|
||||
def expect_none(*args: Any) -> list[tuple[Any, None]]:
|
||||
"""Tuple of results that should return None."""
|
||||
return [(v, None) for v in args]
|
||||
|
||||
|
||||
def check_for_error(value: Any, expected: Any, caplog_text: str, error: str) -> None:
|
||||
"""Test the validator error."""
|
||||
if expected is None and value is not None:
|
||||
assert error in caplog_text
|
||||
else:
|
||||
assert error not in caplog_text
|
||||
|
||||
|
||||
def create_test_entity(hass: HomeAssistant, config: dict) -> TemplateEntity:
|
||||
"""Create a template result handler."""
|
||||
|
||||
class Test(TemplateEntity):
|
||||
_entity_id_format = "test.{}"
|
||||
|
||||
config = {
|
||||
"name": Template("Test", hass),
|
||||
**config,
|
||||
}
|
||||
|
||||
return Test(hass, config, "something_unique")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected one of mmmm, beer, is, good",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected one of mmmm, beer, is, good",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("mmmm", Brewery.MMMM),
|
||||
("MmMM", Brewery.MMMM),
|
||||
("mmMM", Brewery.MMMM),
|
||||
("beer", Brewery.BEER),
|
||||
("is", Brewery.IS),
|
||||
("good", Brewery.GOOD),
|
||||
*expect_none(
|
||||
None,
|
||||
"mm",
|
||||
"beeal;dfj",
|
||||
"unknown",
|
||||
"unavailable",
|
||||
"tru", # codespell:ignore tru
|
||||
"7",
|
||||
"-1",
|
||||
True,
|
||||
False,
|
||||
1,
|
||||
1.0,
|
||||
{},
|
||||
{"junk": "stuff"},
|
||||
{"junk"},
|
||||
[],
|
||||
["stuff"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_enum(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test enum validator."""
|
||||
entity = create_test_entity(hass, config)
|
||||
assert cv.strenum(entity, "state", Brewery)(value) == expected
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
[
|
||||
"unknown",
|
||||
"unavailable",
|
||||
"UknoWn", # codespell:ignore
|
||||
"UnavailablE", # codespell:ignore UnavailablE
|
||||
],
|
||||
)
|
||||
async def test_none_on_unknown_and_unavailable(
|
||||
hass: HomeAssistant,
|
||||
value: Any,
|
||||
) -> None:
|
||||
"""Test enum validator."""
|
||||
entity = create_test_entity(hass, {})
|
||||
assert (
|
||||
cv.strenum(entity, "state", Brewery, none_on_unknown_unavailable=True)(value)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected one of mmmm, beer, is, good, 1, true, yes, on, enable, 0, false, no, off, disable",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected one of mmmm, beer, is, good, 1, true, yes, on, enable, 0, false, no, off, disable",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("mmmm", Brewery.MMMM),
|
||||
("MmMM", Brewery.MMMM),
|
||||
("mmMM", Brewery.MMMM),
|
||||
("beer", Brewery.BEER),
|
||||
("is", Brewery.IS),
|
||||
("good", Brewery.GOOD),
|
||||
*expect_boolean_on(Brewery.MMMM),
|
||||
*expect_boolean_off(Brewery.BEER),
|
||||
*expect_none(
|
||||
None,
|
||||
"mm",
|
||||
"beeal;dfj",
|
||||
"unknown",
|
||||
"unavailable",
|
||||
"tru", # codespell:ignore tru
|
||||
"7",
|
||||
"-1",
|
||||
{},
|
||||
{"junk": "stuff"},
|
||||
{"junk"},
|
||||
[],
|
||||
["stuff"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_enum_with_on_off(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test enum validator."""
|
||||
entity = create_test_entity(hass, config)
|
||||
assert (
|
||||
cv.strenum(entity, "state", Brewery, Brewery.MMMM, Brewery.BEER)(value)
|
||||
== expected
|
||||
)
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected one of mmmm, beer, is, good, 1, true, yes, on, enable",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected one of mmmm, beer, is, good, 1, true, yes, on, enable",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("mmmm", Brewery.MMMM),
|
||||
("MmMM", Brewery.MMMM),
|
||||
("mmMM", Brewery.MMMM),
|
||||
("beer", Brewery.BEER),
|
||||
("is", Brewery.IS),
|
||||
("good", Brewery.GOOD),
|
||||
*expect_boolean_on(Brewery.MMMM),
|
||||
*expect_boolean_off(None),
|
||||
*expect_none(
|
||||
None,
|
||||
"mm",
|
||||
"beeal;dfj",
|
||||
"unknown",
|
||||
"unavailable",
|
||||
"tru", # codespell:ignore tru
|
||||
"7",
|
||||
"-1",
|
||||
{},
|
||||
{"junk": "stuff"},
|
||||
{"junk"},
|
||||
[],
|
||||
["stuff"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_enum_with_on(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test enum with state_on validator."""
|
||||
entity = create_test_entity(hass, config)
|
||||
assert (
|
||||
cv.strenum(entity, "state", Brewery, state_on=Brewery.MMMM)(value) == expected
|
||||
)
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected one of mmmm, beer, is, good, 0, false, no, off, disable",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected one of mmmm, beer, is, good, 0, false, no, off, disable",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("mmmm", Brewery.MMMM),
|
||||
("MmMM", Brewery.MMMM),
|
||||
("mmMM", Brewery.MMMM),
|
||||
("beer", Brewery.BEER),
|
||||
("is", Brewery.IS),
|
||||
("good", Brewery.GOOD),
|
||||
*expect_boolean_on(None),
|
||||
*expect_boolean_off(Brewery.BEER),
|
||||
*expect_none(
|
||||
None,
|
||||
"mm",
|
||||
"beeal;dfj",
|
||||
"unknown",
|
||||
"unavailable",
|
||||
"tru", # codespell:ignore tru
|
||||
"7",
|
||||
"-1",
|
||||
{},
|
||||
{"junk": "stuff"},
|
||||
{"junk"},
|
||||
[],
|
||||
["stuff"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_enum_with_off(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test enum with state_off validator."""
|
||||
entity = create_test_entity(hass, config)
|
||||
assert (
|
||||
cv.strenum(entity, "state", Brewery, state_off=Brewery.BEER)(value) == expected
|
||||
)
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected one of 1, true, yes, on, enable, 0, false, no, off, disable",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected one of 1, true, yes, on, enable, 0, false, no, off, disable",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
*expect_boolean_on(),
|
||||
*expect_boolean_off(),
|
||||
*expect_none(
|
||||
None,
|
||||
"al;dfj",
|
||||
"unknown",
|
||||
"unavailable",
|
||||
"tru", # codespell:ignore tru
|
||||
"7",
|
||||
"-1",
|
||||
{},
|
||||
{"junk": "stuff"},
|
||||
{"junk"},
|
||||
[],
|
||||
["stuff"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_boolean(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test boolean validator."""
|
||||
entity = create_test_entity(hass, config)
|
||||
assert cv.boolean(entity, "state")(value) == expected
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected one of 1, true, yes, on, enable, 0, false, no, off, disable",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected one of 1, true, yes, on, enable, 0, false, no, off, disable",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("something_unique", True),
|
||||
*expect_boolean_on(),
|
||||
*expect_boolean_off(),
|
||||
*expect_none(
|
||||
None,
|
||||
"al;dfj",
|
||||
"unknown",
|
||||
"unavailable",
|
||||
"tru", # codespell:ignore tru
|
||||
"7",
|
||||
"-1",
|
||||
{},
|
||||
{"junk": "stuff"},
|
||||
{"junk"},
|
||||
[],
|
||||
["stuff"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_boolean_as_true(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test boolean validator."""
|
||||
entity = create_test_entity(hass, config)
|
||||
assert cv.boolean(entity, "state", as_true=("something_unique",))(value) == expected
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected one of 1, true, yes, on, enable, 0, false, no, off, disable, something_unique",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected one of 1, true, yes, on, enable, 0, false, no, off, disable, something_unique",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("something_unique", False),
|
||||
*expect_boolean_on(),
|
||||
*expect_boolean_off(),
|
||||
*expect_none(
|
||||
None,
|
||||
"al;dfj",
|
||||
"unknown",
|
||||
"unavailable",
|
||||
"tru", # codespell:ignore tru
|
||||
"7",
|
||||
"-1",
|
||||
{},
|
||||
{"junk": "stuff"},
|
||||
{"junk"},
|
||||
[],
|
||||
["stuff"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_boolean_as_false(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test boolean validator."""
|
||||
entity = create_test_entity(hass, config)
|
||||
assert (
|
||||
cv.boolean(entity, "state", as_false=("something_unique",))(value) == expected
|
||||
)
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected a number",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected a number",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("7.5", 7.5),
|
||||
("0.0", 0.0),
|
||||
("-324.4564", -324.4564),
|
||||
("5e-4", 0.0005),
|
||||
("5e4", 50000.0),
|
||||
(7.5, 7.5),
|
||||
(0.0, 0.0),
|
||||
(-324.4564, -324.4564),
|
||||
(1, 1.0),
|
||||
*expect_none(
|
||||
None,
|
||||
"al;dfj",
|
||||
"unknown",
|
||||
"unavailable",
|
||||
"tru", # codespell:ignore tru
|
||||
True,
|
||||
False,
|
||||
{},
|
||||
{"junk": "stuff"},
|
||||
{"junk"},
|
||||
[],
|
||||
["stuff"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_number_as_float(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test number validator."""
|
||||
entity = create_test_entity(hass, config)
|
||||
value = cv.number(entity, "state")(value)
|
||||
assert value == expected
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected a number",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected a number",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("7.5", 7),
|
||||
("0.0", 0),
|
||||
("-324.4564", -324),
|
||||
("5e-4", 0),
|
||||
("5e4", 50000),
|
||||
(7.5, 7),
|
||||
(0.0, 0),
|
||||
(-324.4564, -324),
|
||||
(1, 1),
|
||||
*expect_none(
|
||||
None,
|
||||
"al;dfj",
|
||||
"unknown",
|
||||
"unavailable",
|
||||
"tru", # codespell:ignore tru
|
||||
True,
|
||||
False,
|
||||
{},
|
||||
{"junk": "stuff"},
|
||||
{"junk"},
|
||||
[],
|
||||
["stuff"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_number_as_int(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test number with return_type int validator."""
|
||||
entity = create_test_entity(hass, config)
|
||||
value = cv.number(entity, "state", return_type=int)(value)
|
||||
assert value == expected
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected a number greater than or equal to 0.0",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected a number greater than or equal to 0.0",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("7.5", 7.5),
|
||||
("0.0", 0),
|
||||
("5e-4", 0.0005),
|
||||
("5e4", 50000.0),
|
||||
(7.5, 7.5),
|
||||
(0.0, 0),
|
||||
(1, 1.0),
|
||||
*expect_none(
|
||||
None,
|
||||
"al;dfj",
|
||||
"unknown",
|
||||
"unavailable",
|
||||
"tru", # codespell:ignore tru
|
||||
"-324.4564",
|
||||
-324.4564,
|
||||
"-0.00001",
|
||||
-0.00001,
|
||||
True,
|
||||
False,
|
||||
{},
|
||||
{"junk": "stuff"},
|
||||
{"junk"},
|
||||
[],
|
||||
["stuff"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_number_with_minimum(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test number with minimum validator."""
|
||||
entity = create_test_entity(hass, config)
|
||||
value = cv.number(entity, "state", minimum=0.0)(value)
|
||||
assert value == expected
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected a number less than or equal to 0.0",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected a number less than or equal to 0.0",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("-7.5", -7.5),
|
||||
("0.0", 0),
|
||||
("-5e-4", -0.0005),
|
||||
("-5e4", -50000),
|
||||
(-7.5, -7.5),
|
||||
(0.0, 0.0),
|
||||
(-1, -1.0),
|
||||
*expect_none(
|
||||
None,
|
||||
"al;dfj",
|
||||
"unknown",
|
||||
"unavailable",
|
||||
"tru", # codespell:ignore tru
|
||||
"324.4564",
|
||||
"0.00001",
|
||||
True,
|
||||
False,
|
||||
{},
|
||||
{"junk": "stuff"},
|
||||
{"junk"},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_number_with_maximum(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test number with maximum validator."""
|
||||
entity = create_test_entity(hass, config)
|
||||
value = cv.number(entity, "state", maximum=0.0)(value)
|
||||
assert value == expected
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected a number between 0.0 and 100.0",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected a number between 0.0 and 100.0",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("7.5", 7.5),
|
||||
("0.0", 0),
|
||||
("0.0012", 0.0012),
|
||||
("99.0", 99.0),
|
||||
("100", 100),
|
||||
*expect_none(
|
||||
None,
|
||||
"al;dfj",
|
||||
"unknown",
|
||||
"unavailable",
|
||||
"tru", # codespell:ignore tru
|
||||
"324.4564",
|
||||
"-5e4101",
|
||||
True,
|
||||
False,
|
||||
{},
|
||||
{"junk": "stuff"},
|
||||
{"junk"},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_number_in_range(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test number within a range validator."""
|
||||
entity = create_test_entity(hass, config)
|
||||
value = cv.number(entity, "state", minimum=0.0, maximum=100.0)(value)
|
||||
assert value == expected
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected a list of strings",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected a list of strings",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
(["beer", "is", "good"], ["beer", "is", "good"]),
|
||||
(["beer", None, True], ["beer", "None", "True"]),
|
||||
([], []),
|
||||
(["99.0", 99.0, 99], ["99.0", "99.0", "99"]),
|
||||
*expect_none(
|
||||
None,
|
||||
"al;dfj",
|
||||
"unknown",
|
||||
"unavailable",
|
||||
"tru", # codespell:ignore tru
|
||||
83242.2342,
|
||||
True,
|
||||
False,
|
||||
{},
|
||||
{"junk": "stuff"},
|
||||
{"junk"},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_list_of_strings(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test result as a list of strings."""
|
||||
entity = create_test_entity(hass, config)
|
||||
value = cv.list_of_strings(entity, "state")(value)
|
||||
assert value == expected
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
|
||||
async def test_list_of_strings_none_on_empty(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test result as a list of strings with an empty list returning None."""
|
||||
entity = create_test_entity(hass, {"default_entity_id": "test.test"})
|
||||
value = cv.list_of_strings(entity, "state", none_on_empty=True)([])
|
||||
assert value is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected beer, is, GOOD",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected beer, is, GOOD",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("beer", "beer"),
|
||||
("is", "is"),
|
||||
("GOOD", "GOOD"),
|
||||
*expect_none(
|
||||
None,
|
||||
"BEER",
|
||||
"IS",
|
||||
"good",
|
||||
"al;dfj",
|
||||
"unknown",
|
||||
"unavailable",
|
||||
"tru", # codespell:ignore tru
|
||||
83242.2342,
|
||||
True,
|
||||
False,
|
||||
{},
|
||||
{"junk": "stuff"},
|
||||
{"junk"},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_item_in_list(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test result is in a list."""
|
||||
entity = create_test_entity(hass, config)
|
||||
value = cv.item_in_list(entity, "state", ["beer", "is", "GOOD"])(value)
|
||||
assert value == expected
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, expected one of beer, is, GOOD",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, expected one of beer, is, GOOD",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_item_in_list_changes(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test test an item is in a list after the list changes."""
|
||||
entity = create_test_entity(hass, config)
|
||||
items = ["beer", "is", "GOOD"]
|
||||
value = cv.item_in_list(entity, "state", items)("mmmm")
|
||||
assert value is None
|
||||
assert error.format("mmmm") in caplog.text
|
||||
|
||||
items.append("mmmm")
|
||||
|
||||
value = cv.item_in_list(entity, "state", items)("mmmm")
|
||||
assert value == "mmmm"
|
||||
assert error.format(value) + ", mmmm" not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error"),
|
||||
[
|
||||
(
|
||||
{"default_entity_id": "test.test"},
|
||||
"Received invalid test state: {} for entity test.test, bar is empty",
|
||||
),
|
||||
(
|
||||
{},
|
||||
"Received invalid state: {} for entity Test, bar is empty",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[("anything", None)],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("the_list"),
|
||||
[None, []],
|
||||
)
|
||||
async def test_empty_items_in_list(
|
||||
hass: HomeAssistant,
|
||||
config: dict,
|
||||
error: str,
|
||||
value: Any,
|
||||
expected: bool | None,
|
||||
the_list: list | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test result is in a list."""
|
||||
entity = create_test_entity(hass, config)
|
||||
value = cv.item_in_list(entity, "state", the_list, "bar")(value)
|
||||
assert value == expected
|
||||
check_for_error(value, expected, caplog.text, error.format(value))
|
||||
Reference in New Issue
Block a user