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 __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Generator, Sequence
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
@@ -29,8 +28,7 @@ from homeassistant.const import (
|
|||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers import config_validation as cv, template
|
|
||||||
from homeassistant.helpers.entity_platform import (
|
from homeassistant.helpers.entity_platform import (
|
||||||
AddConfigEntryEntitiesCallback,
|
AddConfigEntryEntitiesCallback,
|
||||||
AddEntitiesCallback,
|
AddEntitiesCallback,
|
||||||
@@ -39,6 +37,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
|||||||
from homeassistant.helpers.script import Script
|
from homeassistant.helpers.script import Script
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
|
from . import validators as tcv
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import TriggerUpdateCoordinator
|
from .coordinator import TriggerUpdateCoordinator
|
||||||
from .entity import AbstractTemplateEntity
|
from .entity import AbstractTemplateEntity
|
||||||
@@ -56,19 +55,6 @@ from .template_entity import TemplateEntity
|
|||||||
from .trigger_entity import TriggerEntity
|
from .trigger_entity import TriggerEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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_ALARM_CONTROL_PANELS = "panels"
|
||||||
CONF_ARM_AWAY_ACTION = "arm_away"
|
CONF_ARM_AWAY_ACTION = "arm_away"
|
||||||
@@ -212,22 +198,22 @@ class AbstractTemplateAlarmControlPanel(
|
|||||||
_entity_id_format = ENTITY_ID_FORMAT
|
_entity_id_format = ENTITY_ID_FORMAT
|
||||||
_optimistic_entity = True
|
_optimistic_entity = True
|
||||||
|
|
||||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
# The super init is not called because TemplateEntity calls AbstractTemplateEntity.__init__.
|
||||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
def __init__(self, name: str) -> None: # pylint: disable=super-init-not-called
|
||||||
def __init__(self, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called
|
"""Setup the templates and scripts."""
|
||||||
"""Initialize the features."""
|
|
||||||
self._attr_code_arm_required: bool = config[CONF_CODE_ARM_REQUIRED]
|
self._attr_code_arm_required: bool = self._config[CONF_CODE_ARM_REQUIRED]
|
||||||
self._attr_code_format = config[CONF_CODE_FORMAT].value
|
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 = (
|
self._attr_supported_features: AlarmControlPanelEntityFeature = (
|
||||||
AlarmControlPanelEntityFeature(0)
|
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 (
|
for action_id, supported_feature in (
|
||||||
(CONF_DISARM_ACTION, 0),
|
(CONF_DISARM_ACTION, 0),
|
||||||
(CONF_ARM_AWAY_ACTION, AlarmControlPanelEntityFeature.ARM_AWAY),
|
(CONF_ARM_AWAY_ACTION, AlarmControlPanelEntityFeature.ARM_AWAY),
|
||||||
@@ -240,35 +226,21 @@ class AbstractTemplateAlarmControlPanel(
|
|||||||
),
|
),
|
||||||
(CONF_TRIGGER_ACTION, AlarmControlPanelEntityFeature.TRIGGER),
|
(CONF_TRIGGER_ACTION, AlarmControlPanelEntityFeature.TRIGGER),
|
||||||
):
|
):
|
||||||
if (action_config := config.get(action_id)) is not None:
|
if (action_config := self._config.get(action_id)) is not None:
|
||||||
yield (action_id, action_config, supported_feature)
|
self.add_script(action_id, action_config, name, DOMAIN)
|
||||||
|
self._attr_supported_features |= supported_feature
|
||||||
|
|
||||||
async def _async_handle_restored_state(self) -> None:
|
async def _async_handle_restored_state(self) -> None:
|
||||||
if (
|
if (
|
||||||
(last_state := await self.async_get_last_state()) is not None
|
(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 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,
|
# The trigger might have fired already while we waited for stored data,
|
||||||
# then we should not restore state
|
# then we should not restore state
|
||||||
and self._attr_alarm_state is None
|
and self._attr_alarm_state is None
|
||||||
):
|
):
|
||||||
self._attr_alarm_state = AlarmControlPanelState(last_state.state)
|
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):
|
async def _async_alarm_arm(self, state: Any, script: Script | None, code: Any):
|
||||||
"""Arm the panel to specified state with supplied script."""
|
"""Arm the panel to specified state with supplied script."""
|
||||||
|
|
||||||
@@ -351,39 +323,17 @@ class StateAlarmControlPanelEntity(TemplateEntity, AbstractTemplateAlarmControlP
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the panel."""
|
"""Initialize the panel."""
|
||||||
TemplateEntity.__init__(self, hass, config, unique_id)
|
TemplateEntity.__init__(self, hass, config, unique_id)
|
||||||
AbstractTemplateAlarmControlPanel.__init__(self, config)
|
|
||||||
name = self._attr_name
|
name = self._attr_name
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
assert name is not None
|
assert name is not None
|
||||||
|
|
||||||
for action_id, action_config, supported_feature in self._iterate_scripts(
|
AbstractTemplateAlarmControlPanel.__init__(self, name)
|
||||||
config
|
|
||||||
):
|
|
||||||
self.add_script(action_id, action_config, name, DOMAIN)
|
|
||||||
self._attr_supported_features |= supported_feature
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Restore last state."""
|
"""Restore last state."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
await self._async_handle_restored_state()
|
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):
|
class TriggerAlarmControlPanelEntity(TriggerEntity, AbstractTemplateAlarmControlPanel):
|
||||||
"""Alarm Control Panel entity based on trigger data."""
|
"""Alarm Control Panel entity based on trigger data."""
|
||||||
@@ -398,19 +348,8 @@ class TriggerAlarmControlPanelEntity(TriggerEntity, AbstractTemplateAlarmControl
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
TriggerEntity.__init__(self, hass, coordinator, config)
|
TriggerEntity.__init__(self, hass, coordinator, config)
|
||||||
AbstractTemplateAlarmControlPanel.__init__(self, config)
|
|
||||||
|
|
||||||
self._attr_name = name = self._rendered.get(CONF_NAME, DEFAULT_NAME)
|
self._attr_name = name = self._rendered.get(CONF_NAME, DEFAULT_NAME)
|
||||||
|
AbstractTemplateAlarmControlPanel.__init__(self, 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
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Restore last state."""
|
"""Restore last state."""
|
||||||
@@ -426,7 +365,6 @@ class TriggerAlarmControlPanelEntity(TriggerEntity, AbstractTemplateAlarmControl
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
return
|
return
|
||||||
|
|
||||||
if (rendered := self._rendered.get(CONF_STATE)) is not None:
|
if self.handle_rendered_result(CONF_STATE):
|
||||||
self._handle_state(rendered)
|
|
||||||
self.async_set_context(self.coordinator.data["context"])
|
self.async_set_context(self.coordinator.data["context"])
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"""Template entity base class."""
|
"""Template entity base class."""
|
||||||
|
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from collections.abc import Sequence
|
from collections.abc import Callable, Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -18,6 +19,17 @@ from .const import CONF_DEFAULT_ENTITY_ID
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_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):
|
class AbstractTemplateEntity(Entity):
|
||||||
"""Actions linked to a template entity."""
|
"""Actions linked to a template entity."""
|
||||||
|
|
||||||
@@ -34,6 +46,8 @@ class AbstractTemplateEntity(Entity):
|
|||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
|
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
self._config = config
|
||||||
|
self._templates: dict[str, EntityTemplate] = {}
|
||||||
self._action_scripts: dict[str, Script] = {}
|
self._action_scripts: dict[str, Script] = {}
|
||||||
|
|
||||||
if self._optimistic_entity:
|
if self._optimistic_entity:
|
||||||
@@ -72,6 +86,35 @@ class AbstractTemplateEntity(Entity):
|
|||||||
def _render_script_variables(self) -> dict:
|
def _render_script_variables(self) -> dict:
|
||||||
"""Render configured variables."""
|
"""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(
|
def add_script(
|
||||||
self,
|
self,
|
||||||
script_id: str,
|
script_id: str,
|
||||||
|
|||||||
@@ -181,9 +181,6 @@ class TemplateEntity(AbstractTemplateEntity):
|
|||||||
self._run_variables: ScriptVariables | dict
|
self._run_variables: ScriptVariables | dict
|
||||||
self._attribute_templates = config.get(CONF_ATTRIBUTES)
|
self._attribute_templates = config.get(CONF_ATTRIBUTES)
|
||||||
self._availability_template = config.get(CONF_AVAILABILITY)
|
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._run_variables = config.get(CONF_VARIABLES, {})
|
||||||
self._blueprint_inputs = config.get("raw_blueprint_inputs")
|
self._blueprint_inputs = config.get("raw_blueprint_inputs")
|
||||||
|
|
||||||
@@ -208,26 +205,27 @@ class TemplateEntity(AbstractTemplateEntity):
|
|||||||
)
|
)
|
||||||
variables = {"this": DummyState(), **variables}
|
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
|
self._attr_name = None
|
||||||
if self._friendly_name_template:
|
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):
|
with contextlib.suppress(TemplateError):
|
||||||
self._attr_name = self._friendly_name_template.async_render(
|
setattr(
|
||||||
variables=variables, parse_result=False
|
self,
|
||||||
)
|
attribute,
|
||||||
|
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@@ -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(
|
def add_template_attribute(
|
||||||
self,
|
self,
|
||||||
attribute: str,
|
attribute: str,
|
||||||
@@ -417,30 +442,20 @@ class TemplateEntity(AbstractTemplateEntity):
|
|||||||
@callback
|
@callback
|
||||||
def _async_setup_templates(self) -> None:
|
def _async_setup_templates(self) -> None:
|
||||||
"""Set up templates."""
|
"""Set up templates."""
|
||||||
if self._availability_template is not None:
|
|
||||||
self.add_template_attribute(
|
# Handle attributes as a dictionary.
|
||||||
"_attr_available",
|
|
||||||
self._availability_template,
|
|
||||||
None,
|
|
||||||
self._update_available,
|
|
||||||
)
|
|
||||||
if self._attribute_templates is not None:
|
if self._attribute_templates is not None:
|
||||||
for key, value in self._attribute_templates.items():
|
for key, value in self._attribute_templates.items():
|
||||||
self._add_attribute_template(key, value)
|
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(
|
self.add_template_attribute(
|
||||||
"_attr_icon", self._icon_template, vol.Or(cv.whitespace, cv.icon)
|
entity_template.attribute,
|
||||||
)
|
entity_template.template,
|
||||||
if self._entity_picture_template is not None:
|
entity_template.validator,
|
||||||
self.add_template_attribute(
|
entity_template.on_update,
|
||||||
"_attr_entity_picture", self._entity_picture_template, cv.string
|
entity_template.none_on_template_error,
|
||||||
)
|
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.const import CONF_STATE, CONF_VARIABLES
|
from homeassistant.const import CONF_STATE, CONF_VARIABLES
|
||||||
@@ -50,6 +51,19 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
|||||||
else:
|
else:
|
||||||
self._unique_id = unique_id
|
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
|
@property
|
||||||
def referenced_blueprint(self) -> str | None:
|
def referenced_blueprint(self) -> str | None:
|
||||||
"""Return referenced blueprint or None."""
|
"""Return referenced blueprint or None."""
|
||||||
@@ -89,6 +103,23 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
|||||||
self._render_attributes(rendered, variables)
|
self._render_attributes(rendered, variables)
|
||||||
self._rendered = rendered
|
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
|
@callback
|
||||||
def _process_data(self) -> None:
|
def _process_data(self) -> None:
|
||||||
"""Process new data."""
|
"""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."""
|
"""template conftest."""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum, StrEnum
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -25,6 +25,15 @@ class ConfigurationStyle(Enum):
|
|||||||
TRIGGER = "Trigger"
|
TRIGGER = "Trigger"
|
||||||
|
|
||||||
|
|
||||||
|
class Brewery(StrEnum):
|
||||||
|
"""Test enum."""
|
||||||
|
|
||||||
|
MMMM = "mmmm"
|
||||||
|
BEER = "beer"
|
||||||
|
IS = "is"
|
||||||
|
GOOD = "good"
|
||||||
|
|
||||||
|
|
||||||
def make_test_trigger(*entities: str) -> dict:
|
def make_test_trigger(*entities: str) -> dict:
|
||||||
"""Make a test state trigger."""
|
"""Make a test state trigger."""
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -364,23 +364,19 @@ async def test_template_state_text(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
@pytest.mark.parametrize("count", [1])
|
@pytest.mark.parametrize("count", [1])
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("state_template", "expected", "trigger_expected"),
|
("state_template", "expected"),
|
||||||
[
|
[
|
||||||
("{{ 'disarmed' }}", AlarmControlPanelState.DISARMED, None),
|
("{{ 'disarmed' }}", AlarmControlPanelState.DISARMED),
|
||||||
("{{ 'armed_home' }}", AlarmControlPanelState.ARMED_HOME, None),
|
("{{ 'armed_home' }}", AlarmControlPanelState.ARMED_HOME),
|
||||||
("{{ 'armed_away' }}", AlarmControlPanelState.ARMED_AWAY, None),
|
("{{ 'armed_away' }}", AlarmControlPanelState.ARMED_AWAY),
|
||||||
("{{ 'armed_night' }}", AlarmControlPanelState.ARMED_NIGHT, None),
|
("{{ 'armed_night' }}", AlarmControlPanelState.ARMED_NIGHT),
|
||||||
("{{ 'armed_vacation' }}", AlarmControlPanelState.ARMED_VACATION, None),
|
("{{ 'armed_vacation' }}", AlarmControlPanelState.ARMED_VACATION),
|
||||||
(
|
("{{ 'armed_custom_bypass' }}", AlarmControlPanelState.ARMED_CUSTOM_BYPASS),
|
||||||
"{{ 'armed_custom_bypass' }}",
|
("{{ 'pending' }}", AlarmControlPanelState.PENDING),
|
||||||
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
|
("{{ 'arming' }}", AlarmControlPanelState.ARMING),
|
||||||
None,
|
("{{ 'disarming' }}", AlarmControlPanelState.DISARMING),
|
||||||
),
|
("{{ 'triggered' }}", AlarmControlPanelState.TRIGGERED),
|
||||||
("{{ 'pending' }}", AlarmControlPanelState.PENDING, None),
|
("{{ x - 1 }}", STATE_UNAVAILABLE),
|
||||||
("{{ 'arming' }}", AlarmControlPanelState.ARMING, None),
|
|
||||||
("{{ 'disarming' }}", AlarmControlPanelState.DISARMING, None),
|
|
||||||
("{{ 'triggered' }}", AlarmControlPanelState.TRIGGERED, None),
|
|
||||||
("{{ x - 1 }}", STATE_UNKNOWN, STATE_UNAVAILABLE),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -388,9 +384,7 @@ async def test_template_state_text(hass: HomeAssistant) -> None:
|
|||||||
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
[ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER],
|
||||||
)
|
)
|
||||||
@pytest.mark.usefixtures("setup_state_panel")
|
@pytest.mark.usefixtures("setup_state_panel")
|
||||||
async def test_state_template_states(
|
async def test_state_template_states(hass: HomeAssistant, expected: str) -> None:
|
||||||
hass: HomeAssistant, expected: str, trigger_expected: str, style: ConfigurationStyle
|
|
||||||
) -> None:
|
|
||||||
"""Test the state template."""
|
"""Test the state template."""
|
||||||
|
|
||||||
# Force a trigger
|
# Force a trigger
|
||||||
@@ -399,9 +393,6 @@ async def test_state_template_states(
|
|||||||
|
|
||||||
state = hass.states.get(TEST_ENTITY_ID)
|
state = hass.states.get(TEST_ENTITY_ID)
|
||||||
|
|
||||||
if trigger_expected and style == ConfigurationStyle.TRIGGER:
|
|
||||||
expected = trigger_expected
|
|
||||||
|
|
||||||
assert state.state == 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