mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Add track_template_result method to events (#38802)
* Merge original changes from #23590 * guard * adjust * adjust * adjust * Update async_render_to_info for recent codebase changes * no more protected access * do not fire right away per review comments * update test to not fire right away * closer * rework tests for non firing first * augment coverage * remove cruft * test for complex listen add/remove * update docs to match review feedback to not fire right away * preserve existing behavior * fix test * Ensure listeners are cleaned up * de-dupe and comment * de-dupe and comment * coverage * test to login again if we go from exception to ok to exception * Update homeassistant/core.py Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> * Update homeassistant/helpers/event.py Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> * rename _boolean_coerce to result_as_boolean and move it out of event * additional coverage * Add more tests (may still be able to trim this down) Co-authored-by: Swamp-Ig <github@ninjateaparty.com> Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
0f1e70ca79
commit
7d0e356560
@ -905,7 +905,9 @@ class StateMachine:
|
|||||||
return future.result()
|
return future.result()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_entity_ids(self, domain_filter: Optional[str] = None) -> List[str]:
|
def async_entity_ids(
|
||||||
|
self, domain_filter: Optional[Union[str, Iterable]] = None
|
||||||
|
) -> List[str]:
|
||||||
"""List of entity ids that are being tracked.
|
"""List of entity ids that are being tracked.
|
||||||
|
|
||||||
This method must be run in the event loop.
|
This method must be run in the event loop.
|
||||||
@ -913,12 +915,13 @@ class StateMachine:
|
|||||||
if domain_filter is None:
|
if domain_filter is None:
|
||||||
return list(self._states.keys())
|
return list(self._states.keys())
|
||||||
|
|
||||||
domain_filter = domain_filter.lower()
|
if isinstance(domain_filter, str):
|
||||||
|
domain_filter = (domain_filter.lower(),)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
state.entity_id
|
state.entity_id
|
||||||
for state in self._states.values()
|
for state in self._states.values()
|
||||||
if state.domain == domain_filter
|
if state.domain in domain_filter
|
||||||
]
|
]
|
||||||
|
|
||||||
def all(self) -> List[State]:
|
def all(self) -> List[State]:
|
||||||
|
@ -4,7 +4,7 @@ from datetime import datetime, timedelta
|
|||||||
import functools as ft
|
import functools as ft
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from typing import Any, Awaitable, Callable, Dict, Iterable, Optional, Union
|
from typing import Any, Awaitable, Callable, Iterable, Optional, Union
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
@ -25,9 +25,11 @@ from homeassistant.core import (
|
|||||||
callback,
|
callback,
|
||||||
split_entity_id,
|
split_entity_id,
|
||||||
)
|
)
|
||||||
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
|
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
|
||||||
from homeassistant.helpers.sun import get_astral_event_next
|
from homeassistant.helpers.sun import get_astral_event_next
|
||||||
from homeassistant.helpers.template import Template
|
from homeassistant.helpers.template import Template, result_as_boolean
|
||||||
|
from homeassistant.helpers.typing import TemplateVarsType
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.util.async_ import run_callback_threadsafe
|
from homeassistant.util.async_ import run_callback_threadsafe
|
||||||
@ -354,36 +356,315 @@ def async_track_state_added_domain(
|
|||||||
def async_track_template(
|
def async_track_template(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
template: Template,
|
template: Template,
|
||||||
action: Callable[[str, State, State], None],
|
action: Callable[[str, Optional[State], Optional[State]], None],
|
||||||
variables: Optional[Dict[str, Any]] = None,
|
variables: Optional[TemplateVarsType] = None,
|
||||||
) -> CALLBACK_TYPE:
|
) -> Callable[[], None]:
|
||||||
"""Add a listener that track state changes with template condition."""
|
"""Add a listener that fires when a a template evaluates to 'true'.
|
||||||
from . import condition # pylint: disable=import-outside-toplevel
|
|
||||||
|
|
||||||
# Local variable to keep track of if the action has already been triggered
|
Listen for the result of the template becoming true, or a true-like
|
||||||
already_triggered = False
|
string result, such as 'On', 'Open', or 'Yes'. If the template results
|
||||||
|
in an error state when the value changes, this will be logged and not
|
||||||
|
passed through.
|
||||||
|
|
||||||
|
If the initial check of the template is invalid and results in an
|
||||||
|
exception, the listener will still be registered but will only
|
||||||
|
fire if the template result becomes true without an exception.
|
||||||
|
|
||||||
|
Action arguments
|
||||||
|
----------------
|
||||||
|
entity_id
|
||||||
|
ID of the entity that triggered the state change.
|
||||||
|
old_state
|
||||||
|
The old state of the entity that changed.
|
||||||
|
new_state
|
||||||
|
New state of the entity that changed.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
hass
|
||||||
|
Home assistant object.
|
||||||
|
template
|
||||||
|
The template to calculate.
|
||||||
|
action
|
||||||
|
Callable to call with results. See above for arguments.
|
||||||
|
variables
|
||||||
|
Variables to pass to the template.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Callable to unregister the listener.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def template_condition_listener(entity_id: str, from_s: State, to_s: State) -> None:
|
def state_changed_listener(
|
||||||
|
event: Event,
|
||||||
|
template: Template,
|
||||||
|
last_result: Optional[str],
|
||||||
|
result: Union[str, TemplateError],
|
||||||
|
) -> None:
|
||||||
"""Check if condition is correct and run action."""
|
"""Check if condition is correct and run action."""
|
||||||
nonlocal already_triggered
|
if isinstance(result, TemplateError):
|
||||||
template_result = condition.async_template(hass, template, variables)
|
_LOGGER.exception(result)
|
||||||
|
return
|
||||||
|
|
||||||
# Check to see if template returns true
|
if result_as_boolean(last_result) or not result_as_boolean(result):
|
||||||
if template_result and not already_triggered:
|
return
|
||||||
already_triggered = True
|
|
||||||
hass.async_run_job(action, entity_id, from_s, to_s)
|
|
||||||
elif not template_result:
|
|
||||||
already_triggered = False
|
|
||||||
|
|
||||||
return async_track_state_change(
|
hass.async_run_job(
|
||||||
hass, template.extract_entities(variables), template_condition_listener
|
action,
|
||||||
|
event.data.get("entity_id"),
|
||||||
|
event.data.get("old_state"),
|
||||||
|
event.data.get("new_state"),
|
||||||
|
)
|
||||||
|
|
||||||
|
info = async_track_template_result(
|
||||||
|
hass, template, state_changed_listener, variables
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return info.async_remove
|
||||||
|
|
||||||
|
|
||||||
track_template = threaded_listener_factory(async_track_template)
|
track_template = threaded_listener_factory(async_track_template)
|
||||||
|
|
||||||
|
|
||||||
|
_UNCHANGED = object()
|
||||||
|
|
||||||
|
|
||||||
|
class TrackTemplateResultInfo:
|
||||||
|
"""Handle removal / refresh of tracker."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
template: Template,
|
||||||
|
action: Callable,
|
||||||
|
variables: Optional[TemplateVarsType],
|
||||||
|
):
|
||||||
|
"""Handle removal / refresh of tracker init."""
|
||||||
|
self.hass = hass
|
||||||
|
self._template = template
|
||||||
|
self._action = action
|
||||||
|
self._variables = variables
|
||||||
|
self._last_result: Optional[str] = None
|
||||||
|
self._last_exception = False
|
||||||
|
self._all_listener: Optional[Callable] = None
|
||||||
|
self._domains_listener: Optional[Callable] = None
|
||||||
|
self._entities_listener: Optional[Callable] = None
|
||||||
|
self._info = template.async_render_to_info(variables)
|
||||||
|
if self._info.exception:
|
||||||
|
self._last_exception = True
|
||||||
|
_LOGGER.exception(self._info.exception)
|
||||||
|
self._create_listeners()
|
||||||
|
self._last_info = self._info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _needs_all_listener(self) -> bool:
|
||||||
|
# Tracking all states
|
||||||
|
if self._info.all_states:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Previous call had an exception
|
||||||
|
# so we do not know which states
|
||||||
|
# to track
|
||||||
|
if self._info.exception:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# There are no entities in the template
|
||||||
|
# to track so this template will
|
||||||
|
# re-render on EVERY state change
|
||||||
|
if not self._info.domains and not self._info.entities:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _create_listeners(self) -> None:
|
||||||
|
if self._info.is_static:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._needs_all_listener:
|
||||||
|
self._setup_all_listener()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._info.domains:
|
||||||
|
self._setup_domains_listener()
|
||||||
|
|
||||||
|
if self._info.entities or self._info.domains:
|
||||||
|
self._setup_entities_listener()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _cancel_domains_listener(self) -> None:
|
||||||
|
if self._domains_listener is None:
|
||||||
|
return
|
||||||
|
self._domains_listener()
|
||||||
|
self._domains_listener = None
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _cancel_entities_listener(self) -> None:
|
||||||
|
if self._entities_listener is None:
|
||||||
|
return
|
||||||
|
self._entities_listener()
|
||||||
|
self._entities_listener = None
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _cancel_all_listener(self) -> None:
|
||||||
|
if self._all_listener is None:
|
||||||
|
return
|
||||||
|
self._all_listener()
|
||||||
|
self._all_listener = None
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _update_listeners(self) -> None:
|
||||||
|
if self._needs_all_listener:
|
||||||
|
if self._all_listener:
|
||||||
|
return
|
||||||
|
self._cancel_domains_listener()
|
||||||
|
self._cancel_entities_listener()
|
||||||
|
self._setup_all_listener()
|
||||||
|
return
|
||||||
|
|
||||||
|
had_all_listener = self._all_listener is not None
|
||||||
|
if had_all_listener:
|
||||||
|
self._cancel_all_listener()
|
||||||
|
|
||||||
|
domains_changed = self._info.domains != self._last_info.domains
|
||||||
|
if had_all_listener or domains_changed:
|
||||||
|
domains_changed = True
|
||||||
|
self._cancel_domains_listener()
|
||||||
|
self._setup_domains_listener()
|
||||||
|
|
||||||
|
if (
|
||||||
|
had_all_listener
|
||||||
|
or domains_changed
|
||||||
|
or self._info.entities != self._last_info.entities
|
||||||
|
):
|
||||||
|
self._cancel_entities_listener()
|
||||||
|
self._setup_entities_listener()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _setup_entities_listener(self) -> None:
|
||||||
|
entities = set(self._info.entities)
|
||||||
|
for entity_id in self.hass.states.async_entity_ids(self._info.domains):
|
||||||
|
entities.add(entity_id)
|
||||||
|
self._entities_listener = async_track_state_change_event(
|
||||||
|
self.hass, entities, self._refresh
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _setup_domains_listener(self) -> None:
|
||||||
|
self._domains_listener = async_track_state_added_domain(
|
||||||
|
self.hass, self._info.domains, self._refresh
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _setup_all_listener(self) -> None:
|
||||||
|
self._all_listener = self.hass.bus.async_listen(
|
||||||
|
EVENT_STATE_CHANGED, self._refresh
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_remove(self) -> None:
|
||||||
|
"""Cancel the listener."""
|
||||||
|
self._cancel_all_listener()
|
||||||
|
self._cancel_domains_listener()
|
||||||
|
self._cancel_entities_listener()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_refresh(self, variables: Any = _UNCHANGED) -> None:
|
||||||
|
"""Force recalculate the template."""
|
||||||
|
if variables is not _UNCHANGED:
|
||||||
|
self._variables = variables
|
||||||
|
self._refresh(None)
|
||||||
|
|
||||||
|
def _refresh(self, event: Optional[Event]) -> None:
|
||||||
|
self._info = self._template.async_render_to_info(self._variables)
|
||||||
|
self._update_listeners()
|
||||||
|
self._last_info = self._info
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self._info.result
|
||||||
|
except TemplateError as ex:
|
||||||
|
if not self._last_exception:
|
||||||
|
self.hass.async_run_job(
|
||||||
|
self._action, event, self._template, self._last_result, ex
|
||||||
|
)
|
||||||
|
self._last_exception = True
|
||||||
|
return
|
||||||
|
self._last_exception = False
|
||||||
|
|
||||||
|
# Check to see if the result has changed
|
||||||
|
if result == self._last_result:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.hass.async_run_job(
|
||||||
|
self._action, event, self._template, self._last_result, result
|
||||||
|
)
|
||||||
|
self._last_result = result
|
||||||
|
|
||||||
|
|
||||||
|
TrackTemplateResultListener = Callable[
|
||||||
|
[Event, Template, Optional[str], Union[str, TemplateError]], None
|
||||||
|
]
|
||||||
|
"""Type for the listener for template results.
|
||||||
|
|
||||||
|
Action arguments
|
||||||
|
----------------
|
||||||
|
event
|
||||||
|
Event that caused the template to change output. None if not
|
||||||
|
triggered by an event.
|
||||||
|
template
|
||||||
|
The template that has changed.
|
||||||
|
last_result
|
||||||
|
The output from the template on the last successful run, or None
|
||||||
|
if no previous successful run.
|
||||||
|
result
|
||||||
|
Result from the template run. This will be a string or an
|
||||||
|
TemplateError if the template resulted in an error.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@bind_hass
|
||||||
|
def async_track_template_result(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
template: Template,
|
||||||
|
action: TrackTemplateResultListener,
|
||||||
|
variables: Optional[TemplateVarsType] = None,
|
||||||
|
) -> TrackTemplateResultInfo:
|
||||||
|
"""Add a listener that fires when a the result of a template changes.
|
||||||
|
|
||||||
|
The action will fire with the initial result from the template, and
|
||||||
|
then whenever the output from the template changes. The template will
|
||||||
|
be reevaluated if any states referenced in the last run of the
|
||||||
|
template change, or if manually triggered. If the result of the
|
||||||
|
evaluation is different from the previous run, the listener is passed
|
||||||
|
the result.
|
||||||
|
|
||||||
|
If the template results in an TemplateError, this will be returned to
|
||||||
|
the listener the first time this happens but not for subsequent errors.
|
||||||
|
Once the template returns to a non-error condition the result is sent
|
||||||
|
to the action as usual.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
hass
|
||||||
|
Home assistant object.
|
||||||
|
template
|
||||||
|
The template to calculate.
|
||||||
|
action
|
||||||
|
Callable to call with results.
|
||||||
|
variables
|
||||||
|
Variables to pass to the template.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Info object used to unregister the listener, and refresh the template.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return TrackTemplateResultInfo(hass, template, action, variables)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@bind_hass
|
@bind_hass
|
||||||
def async_track_same_state(
|
def async_track_same_state(
|
||||||
|
@ -8,7 +8,7 @@ import logging
|
|||||||
import math
|
import math
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict, Iterable, List, Optional, Union
|
from typing import Any, Iterable, List, Optional, Union
|
||||||
from urllib.parse import urlencode as urllib_urlencode
|
from urllib.parse import urlencode as urllib_urlencode
|
||||||
import weakref
|
import weakref
|
||||||
|
|
||||||
@ -16,6 +16,7 @@ import jinja2
|
|||||||
from jinja2 import contextfilter, contextfunction
|
from jinja2 import contextfilter, contextfunction
|
||||||
from jinja2.sandbox import ImmutableSandboxedEnvironment
|
from jinja2.sandbox import ImmutableSandboxedEnvironment
|
||||||
from jinja2.utils import Namespace # type: ignore
|
from jinja2.utils import Namespace # type: ignore
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
@ -28,7 +29,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import State, callback, split_entity_id, valid_entity_id
|
from homeassistant.core import State, callback, split_entity_id, valid_entity_id
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers import location as loc_helper
|
from homeassistant.helpers import config_validation as cv, location as loc_helper
|
||||||
from homeassistant.helpers.typing import HomeAssistantType, TemplateVarsType
|
from homeassistant.helpers.typing import HomeAssistantType, TemplateVarsType
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
from homeassistant.util import convert, dt as dt_util, location as loc_util
|
from homeassistant.util import convert, dt as dt_util, location as loc_util
|
||||||
@ -49,8 +50,13 @@ _RE_GET_ENTITIES = re.compile(
|
|||||||
r"(?:(?:(?:states\.|(?P<func>is_state|is_state_attr|state_attr|states|expand)\((?:[\ \'\"]?))(?P<entity_id>[\w]+\.[\w]+)|states\.(?P<domain_outer>[a-z]+)|states\[(?:[\'\"]?)(?P<domain_inner>[\w]+))|(?P<variable>[\w]+))",
|
r"(?:(?:(?:states\.|(?P<func>is_state|is_state_attr|state_attr|states|expand)\((?:[\ \'\"]?))(?P<entity_id>[\w]+\.[\w]+)|states\.(?P<domain_outer>[a-z]+)|states\[(?:[\'\"]?)(?P<domain_inner>[\w]+))|(?P<variable>[\w]+))",
|
||||||
re.I | re.M,
|
re.I | re.M,
|
||||||
)
|
)
|
||||||
|
|
||||||
_RE_JINJA_DELIMITERS = re.compile(r"\{%|\{\{")
|
_RE_JINJA_DELIMITERS = re.compile(r"\{%|\{\{")
|
||||||
|
|
||||||
|
_RESERVED_NAMES = {"contextfunction", "evalcontextfunction", "environmentfunction"}
|
||||||
|
|
||||||
|
_GROUP_DOMAIN_PREFIX = "group."
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
def attach(hass: HomeAssistantType, obj: Any) -> None:
|
def attach(hass: HomeAssistantType, obj: Any) -> None:
|
||||||
@ -79,7 +85,7 @@ def render_complex(value: Any, variables: TemplateVarsType = None) -> Any:
|
|||||||
def extract_entities(
|
def extract_entities(
|
||||||
hass: HomeAssistantType,
|
hass: HomeAssistantType,
|
||||||
template: Optional[str],
|
template: Optional[str],
|
||||||
variables: Optional[Dict[str, Any]] = None,
|
variables: TemplateVarsType = None,
|
||||||
) -> Union[str, List[str]]:
|
) -> Union[str, List[str]]:
|
||||||
"""Extract all entities for state_changed listener from template string."""
|
"""Extract all entities for state_changed listener from template string."""
|
||||||
if template is None or _RE_JINJA_DELIMITERS.search(template) is None:
|
if template is None or _RE_JINJA_DELIMITERS.search(template) is None:
|
||||||
@ -137,39 +143,45 @@ class RenderInfo:
|
|||||||
# Will be set sensibly once frozen.
|
# Will be set sensibly once frozen.
|
||||||
self.filter_lifecycle = _true
|
self.filter_lifecycle = _true
|
||||||
self._result = None
|
self._result = None
|
||||||
self._exception = None
|
self.is_static = False
|
||||||
self._all_states = False
|
self.exception = None
|
||||||
self._domains = []
|
self.all_states = False
|
||||||
self._entities = []
|
self.domains = set()
|
||||||
|
self.entities = set()
|
||||||
|
|
||||||
def filter(self, entity_id: str) -> bool:
|
def filter(self, entity_id: str) -> bool:
|
||||||
"""Template should re-render if the state changes."""
|
"""Template should re-render if the state changes."""
|
||||||
return entity_id in self._entities
|
return entity_id in self.entities
|
||||||
|
|
||||||
def _filter_lifecycle(self, entity_id: str) -> bool:
|
def _filter_lifecycle(self, entity_id: str) -> bool:
|
||||||
"""Template should re-render if the state changes."""
|
"""Template should re-render if the state changes."""
|
||||||
return (
|
return (
|
||||||
split_entity_id(entity_id)[0] in self._domains
|
split_entity_id(entity_id)[0] in self.domains or entity_id in self.entities
|
||||||
or entity_id in self._entities
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def result(self) -> str:
|
def result(self) -> str:
|
||||||
"""Results of the template computation."""
|
"""Results of the template computation."""
|
||||||
if self._exception is not None:
|
if self.exception is not None:
|
||||||
raise self._exception
|
raise self.exception
|
||||||
return self._result
|
return self._result
|
||||||
|
|
||||||
|
def _freeze_static(self) -> None:
|
||||||
|
self.is_static = True
|
||||||
|
self.entities = frozenset(self.entities)
|
||||||
|
self.domains = frozenset(self.domains)
|
||||||
|
self.all_states = False
|
||||||
|
|
||||||
def _freeze(self) -> None:
|
def _freeze(self) -> None:
|
||||||
self._entities = frozenset(self._entities)
|
self.entities = frozenset(self.entities)
|
||||||
if self._all_states:
|
self.domains = frozenset(self.domains)
|
||||||
# Leave lifecycle_filter as True
|
|
||||||
del self._domains
|
if self.all_states:
|
||||||
elif not self._domains:
|
return
|
||||||
del self._domains
|
|
||||||
|
if not self.domains:
|
||||||
self.filter_lifecycle = self.filter
|
self.filter_lifecycle = self.filter
|
||||||
else:
|
else:
|
||||||
self._domains = frozenset(self._domains)
|
|
||||||
self.filter_lifecycle = self._filter_lifecycle
|
self.filter_lifecycle = self._filter_lifecycle
|
||||||
|
|
||||||
|
|
||||||
@ -206,7 +218,7 @@ class Template:
|
|||||||
raise TemplateError(err)
|
raise TemplateError(err)
|
||||||
|
|
||||||
def extract_entities(
|
def extract_entities(
|
||||||
self, variables: Optional[Dict[str, Any]] = None
|
self, variables: TemplateVarsType = None
|
||||||
) -> Union[str, List[str]]:
|
) -> Union[str, List[str]]:
|
||||||
"""Extract all entities for state_changed listener."""
|
"""Extract all entities for state_changed listener."""
|
||||||
return extract_entities(self.hass, self.template, variables)
|
return extract_entities(self.hass, self.template, variables)
|
||||||
@ -247,10 +259,13 @@ class Template:
|
|||||||
try:
|
try:
|
||||||
render_info._result = self.async_render(variables, **kwargs)
|
render_info._result = self.async_render(variables, **kwargs)
|
||||||
except TemplateError as ex:
|
except TemplateError as ex:
|
||||||
render_info._exception = ex
|
render_info.exception = ex
|
||||||
finally:
|
finally:
|
||||||
del self.hass.data[_RENDER_INFO]
|
del self.hass.data[_RENDER_INFO]
|
||||||
render_info._freeze()
|
if _RE_JINJA_DELIMITERS.search(self.template) is None:
|
||||||
|
render_info._freeze_static()
|
||||||
|
else:
|
||||||
|
render_info._freeze()
|
||||||
return render_info
|
return render_info
|
||||||
|
|
||||||
def render_with_possible_json_value(self, value, error_value=_SENTINEL):
|
def render_with_possible_json_value(self, value, error_value=_SENTINEL):
|
||||||
@ -342,15 +357,19 @@ class AllStates:
|
|||||||
if not valid_entity_id(name):
|
if not valid_entity_id(name):
|
||||||
raise TemplateError(f"Invalid entity ID '{name}'")
|
raise TemplateError(f"Invalid entity ID '{name}'")
|
||||||
return _get_state(self._hass, name)
|
return _get_state(self._hass, name)
|
||||||
|
|
||||||
|
if name in _RESERVED_NAMES:
|
||||||
|
return None
|
||||||
|
|
||||||
if not valid_entity_id(f"{name}.entity"):
|
if not valid_entity_id(f"{name}.entity"):
|
||||||
raise TemplateError(f"Invalid domain name '{name}'")
|
raise TemplateError(f"Invalid domain name '{name}'")
|
||||||
|
|
||||||
return DomainStates(self._hass, name)
|
return DomainStates(self._hass, name)
|
||||||
|
|
||||||
def _collect_all(self) -> None:
|
def _collect_all(self) -> None:
|
||||||
render_info = self._hass.data.get(_RENDER_INFO)
|
render_info = self._hass.data.get(_RENDER_INFO)
|
||||||
if render_info is not None:
|
if render_info is not None:
|
||||||
# pylint: disable=protected-access
|
render_info.all_states = True
|
||||||
render_info._all_states = True
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"""Return all states."""
|
"""Return all states."""
|
||||||
@ -395,8 +414,7 @@ class DomainStates:
|
|||||||
def _collect_domain(self) -> None:
|
def _collect_domain(self) -> None:
|
||||||
entity_collect = self._hass.data.get(_RENDER_INFO)
|
entity_collect = self._hass.data.get(_RENDER_INFO)
|
||||||
if entity_collect is not None:
|
if entity_collect is not None:
|
||||||
# pylint: disable=protected-access
|
entity_collect.domains.add(self._domain)
|
||||||
entity_collect._domains.append(self._domain)
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"""Return the iteration over all the states."""
|
"""Return the iteration over all the states."""
|
||||||
@ -435,7 +453,6 @@ class TemplateState(State):
|
|||||||
def _access_state(self):
|
def _access_state(self):
|
||||||
state = object.__getattribute__(self, "_state")
|
state = object.__getattribute__(self, "_state")
|
||||||
hass = object.__getattribute__(self, "_hass")
|
hass = object.__getattribute__(self, "_hass")
|
||||||
|
|
||||||
_collect_state(hass, state.entity_id)
|
_collect_state(hass, state.entity_id)
|
||||||
return state
|
return state
|
||||||
|
|
||||||
@ -448,6 +465,13 @@ class TemplateState(State):
|
|||||||
return state.state
|
return state.state
|
||||||
return f"{state.state} {unit}"
|
return f"{state.state} {unit}"
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
"""Ensure we collect on equality check."""
|
||||||
|
state = object.__getattribute__(self, "_state")
|
||||||
|
hass = object.__getattribute__(self, "_hass")
|
||||||
|
_collect_state(hass, state.entity_id)
|
||||||
|
return super().__eq__(other)
|
||||||
|
|
||||||
def __getattribute__(self, name):
|
def __getattribute__(self, name):
|
||||||
"""Return an attribute of the state."""
|
"""Return an attribute of the state."""
|
||||||
# This one doesn't count as an access of the state
|
# This one doesn't count as an access of the state
|
||||||
@ -471,8 +495,7 @@ class TemplateState(State):
|
|||||||
def _collect_state(hass: HomeAssistantType, entity_id: str) -> None:
|
def _collect_state(hass: HomeAssistantType, entity_id: str) -> None:
|
||||||
entity_collect = hass.data.get(_RENDER_INFO)
|
entity_collect = hass.data.get(_RENDER_INFO)
|
||||||
if entity_collect is not None:
|
if entity_collect is not None:
|
||||||
# pylint: disable=protected-access
|
entity_collect.entities.add(entity_id)
|
||||||
entity_collect._entities.append(entity_id)
|
|
||||||
|
|
||||||
|
|
||||||
def _wrap_state(
|
def _wrap_state(
|
||||||
@ -503,6 +526,19 @@ def _resolve_state(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def result_as_boolean(template_result: Optional[str]) -> bool:
|
||||||
|
"""Convert the template result to a boolean.
|
||||||
|
|
||||||
|
True/not 0/'1'/'true'/'yes'/'on'/'enable' are considered truthy
|
||||||
|
False/0/None/'0'/'false'/'no'/'off'/'disable' are considered falsy
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return cv.boolean(template_result)
|
||||||
|
except vol.Invalid:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def expand(hass: HomeAssistantType, *args: Any) -> Iterable[State]:
|
def expand(hass: HomeAssistantType, *args: Any) -> Iterable[State]:
|
||||||
"""Expand out any groups into entity states."""
|
"""Expand out any groups into entity states."""
|
||||||
search = list(args)
|
search = list(args)
|
||||||
@ -523,16 +559,15 @@ def expand(hass: HomeAssistantType, *args: Any) -> Iterable[State]:
|
|||||||
# ignore other types
|
# ignore other types
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# pylint: disable=import-outside-toplevel
|
if entity_id.startswith(_GROUP_DOMAIN_PREFIX):
|
||||||
from homeassistant.components import group
|
|
||||||
|
|
||||||
if split_entity_id(entity_id)[0] == group.DOMAIN:
|
|
||||||
# Collect state will be called in here since it's wrapped
|
# Collect state will be called in here since it's wrapped
|
||||||
group_entities = entity.attributes.get(ATTR_ENTITY_ID)
|
group_entities = entity.attributes.get(ATTR_ENTITY_ID)
|
||||||
if group_entities:
|
if group_entities:
|
||||||
search += group_entities
|
search += group_entities
|
||||||
else:
|
else:
|
||||||
|
_collect_state(hass, entity_id)
|
||||||
found[entity_id] = entity
|
found[entity_id] = entity
|
||||||
|
|
||||||
return sorted(found.values(), key=lambda a: a.entity_id)
|
return sorted(found.values(), key=lambda a: a.entity_id)
|
||||||
|
|
||||||
|
|
||||||
@ -618,7 +653,10 @@ def distance(hass, *args):
|
|||||||
|
|
||||||
while to_process:
|
while to_process:
|
||||||
value = to_process.pop(0)
|
value = to_process.pop(0)
|
||||||
point_state = _resolve_state(hass, value)
|
if isinstance(value, str) and not valid_entity_id(value):
|
||||||
|
point_state = None
|
||||||
|
else:
|
||||||
|
point_state = _resolve_state(hass, value)
|
||||||
|
|
||||||
if point_state is None:
|
if point_state is None:
|
||||||
# We expect this and next value to be lat&lng
|
# We expect this and next value to be lat&lng
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
"""Test the condition helper."""
|
"""Test the condition helper."""
|
||||||
|
from logging import ERROR
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
@ -576,3 +578,18 @@ async def test_extract_devices():
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
) == {"abcd", "qwer", "abcd_not", "qwer_not", "abcd_or", "qwer_or"}
|
) == {"abcd", "qwer", "abcd_not", "qwer_not", "abcd_or", "qwer_or"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_condition_template_error(hass, caplog):
|
||||||
|
"""Test invalid template."""
|
||||||
|
caplog.set_level(ERROR)
|
||||||
|
|
||||||
|
test = await condition.async_from_config(
|
||||||
|
hass, {"condition": "template", "value_template": "{{ undefined.state }}"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not test(hass)
|
||||||
|
assert len(caplog.records) == 1
|
||||||
|
assert caplog.records[0].message.startswith(
|
||||||
|
"Error during template condition: UndefinedError:"
|
||||||
|
)
|
||||||
|
@ -10,6 +10,7 @@ from homeassistant.components import sun
|
|||||||
from homeassistant.const import MATCH_ALL
|
from homeassistant.const import MATCH_ALL
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
|
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
|
||||||
from homeassistant.helpers.event import (
|
from homeassistant.helpers.event import (
|
||||||
async_call_later,
|
async_call_later,
|
||||||
@ -22,6 +23,7 @@ from homeassistant.helpers.event import (
|
|||||||
async_track_sunrise,
|
async_track_sunrise,
|
||||||
async_track_sunset,
|
async_track_sunset,
|
||||||
async_track_template,
|
async_track_template,
|
||||||
|
async_track_template_result,
|
||||||
async_track_time_change,
|
async_track_time_change,
|
||||||
async_track_time_interval,
|
async_track_time_interval,
|
||||||
async_track_utc_time_change,
|
async_track_utc_time_change,
|
||||||
@ -490,61 +492,481 @@ async def test_track_template(hass):
|
|||||||
assert len(wildcard_runs) == 2
|
assert len(wildcard_runs) == 2
|
||||||
assert len(wildercard_runs) == 2
|
assert len(wildercard_runs) == 2
|
||||||
|
|
||||||
|
template_iterate = Template("{{ (states.switch | length) > 0 }}", hass)
|
||||||
async def test_track_same_state_simple_trigger(hass):
|
iterate_calls = []
|
||||||
"""Test track_same_change with trigger simple."""
|
|
||||||
thread_runs = []
|
|
||||||
callback_runs = []
|
|
||||||
coroutine_runs = []
|
|
||||||
period = timedelta(minutes=1)
|
|
||||||
|
|
||||||
def thread_run_callback():
|
|
||||||
thread_runs.append(1)
|
|
||||||
|
|
||||||
async_track_same_state(
|
|
||||||
hass,
|
|
||||||
period,
|
|
||||||
thread_run_callback,
|
|
||||||
lambda _, _2, to_s: to_s.state == "on",
|
|
||||||
entity_ids="light.Bowl",
|
|
||||||
)
|
|
||||||
|
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def callback_run_callback():
|
def iterate_callback(entity_id, old_state, new_state):
|
||||||
callback_runs.append(1)
|
iterate_calls.append((entity_id, old_state, new_state))
|
||||||
|
|
||||||
async_track_same_state(
|
async_track_template(hass, template_iterate, iterate_callback)
|
||||||
hass,
|
await hass.async_block_till_done()
|
||||||
period,
|
|
||||||
callback_run_callback,
|
hass.states.async_set("switch.new", "on")
|
||||||
callback(lambda _, _2, to_s: to_s.state == "on"),
|
await hass.async_block_till_done()
|
||||||
entity_ids="light.Bowl",
|
|
||||||
|
assert len(iterate_calls) == 1
|
||||||
|
assert iterate_calls[0][0] == "switch.new"
|
||||||
|
assert iterate_calls[0][1] is None
|
||||||
|
assert iterate_calls[0][2].state == "on"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_track_template_error(hass, caplog):
|
||||||
|
"""Test tracking template with error."""
|
||||||
|
template_error = Template("{{ (states.switch | lunch) > 0 }}", hass)
|
||||||
|
error_calls = []
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
|
def error_callback(entity_id, old_state, new_state):
|
||||||
|
error_calls.append((entity_id, old_state, new_state))
|
||||||
|
|
||||||
|
async_track_template(hass, template_error, error_callback)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
hass.states.async_set("switch.new", "on")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert not error_calls
|
||||||
|
assert "lunch" in caplog.text
|
||||||
|
assert "TemplateAssertionError" in caplog.text
|
||||||
|
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
with patch.object(Template, "async_render") as render:
|
||||||
|
render.return_value = "ok"
|
||||||
|
|
||||||
|
hass.states.async_set("switch.not_exist", "off")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert "lunch" not in caplog.text
|
||||||
|
assert "TemplateAssertionError" not in caplog.text
|
||||||
|
|
||||||
|
hass.states.async_set("switch.not_exist", "on")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert "lunch" in caplog.text
|
||||||
|
assert "TemplateAssertionError" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_track_template_result(hass):
|
||||||
|
"""Test tracking template."""
|
||||||
|
specific_runs = []
|
||||||
|
wildcard_runs = []
|
||||||
|
wildercard_runs = []
|
||||||
|
|
||||||
|
template_condition = Template("{{states.sensor.test.state}}", hass)
|
||||||
|
template_condition_var = Template(
|
||||||
|
"{{(states.sensor.test.state|int) + test }}", hass
|
||||||
)
|
)
|
||||||
|
|
||||||
async def coroutine_run_callback():
|
def specific_run_callback(event, template, old_result, new_result):
|
||||||
coroutine_runs.append(1)
|
specific_runs.append(int(new_result))
|
||||||
|
|
||||||
async_track_same_state(
|
async_track_template_result(hass, template_condition, specific_run_callback)
|
||||||
hass,
|
|
||||||
period,
|
@ha.callback
|
||||||
coroutine_run_callback,
|
def wildcard_run_callback(event, template, old_result, new_result):
|
||||||
callback(lambda _, _2, to_s: to_s.state == "on"),
|
wildcard_runs.append((int(old_result or 0), int(new_result)))
|
||||||
|
|
||||||
|
async_track_template_result(hass, template_condition, wildcard_run_callback)
|
||||||
|
|
||||||
|
async def wildercard_run_callback(event, template, old_result, new_result):
|
||||||
|
wildercard_runs.append((int(old_result or 0), int(new_result)))
|
||||||
|
|
||||||
|
async_track_template_result(
|
||||||
|
hass, template_condition_var, wildercard_run_callback, {"test": 5}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Adding state to state machine
|
|
||||||
hass.states.async_set("light.Bowl", "on")
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(thread_runs) == 0
|
|
||||||
assert len(callback_runs) == 0
|
|
||||||
assert len(coroutine_runs) == 0
|
|
||||||
|
|
||||||
# change time to track and see if they trigger
|
hass.states.async_set("sensor.test", 5)
|
||||||
future = dt_util.utcnow() + period
|
|
||||||
async_fire_time_changed(hass, future)
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(thread_runs) == 1
|
|
||||||
assert len(callback_runs) == 1
|
assert specific_runs == [5]
|
||||||
assert len(coroutine_runs) == 1
|
assert wildcard_runs == [(0, 5)]
|
||||||
|
assert wildercard_runs == [(0, 10)]
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.test", 30)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert specific_runs == [5, 30]
|
||||||
|
assert wildcard_runs == [(0, 5), (5, 30)]
|
||||||
|
assert wildercard_runs == [(0, 10), (10, 35)]
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.test", 30)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(specific_runs) == 2
|
||||||
|
assert len(wildcard_runs) == 2
|
||||||
|
assert len(wildercard_runs) == 2
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.test", 5)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(specific_runs) == 3
|
||||||
|
assert len(wildcard_runs) == 3
|
||||||
|
assert len(wildercard_runs) == 3
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.test", 5)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(specific_runs) == 3
|
||||||
|
assert len(wildcard_runs) == 3
|
||||||
|
assert len(wildercard_runs) == 3
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.test", 20)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(specific_runs) == 4
|
||||||
|
assert len(wildcard_runs) == 4
|
||||||
|
assert len(wildercard_runs) == 4
|
||||||
|
|
||||||
|
|
||||||
|
async def test_track_template_result_complex(hass):
|
||||||
|
"""Test tracking template."""
|
||||||
|
specific_runs = []
|
||||||
|
template_complex_str = """
|
||||||
|
|
||||||
|
{% if states("sensor.domain") == "light" %}
|
||||||
|
{{ states.light | map(attribute='entity_id') | list }}
|
||||||
|
{% elif states("sensor.domain") == "lock" %}
|
||||||
|
{{ states.lock | map(attribute='entity_id') | list }}
|
||||||
|
{% elif states("sensor.domain") == "single_binary_sensor" %}
|
||||||
|
{{ states("binary_sensor.single") }}
|
||||||
|
{% else %}
|
||||||
|
{{ states | map(attribute='entity_id') | list }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
"""
|
||||||
|
template_complex = Template(template_complex_str, hass)
|
||||||
|
|
||||||
|
def specific_run_callback(event, template, old_result, new_result):
|
||||||
|
specific_runs.append(new_result)
|
||||||
|
|
||||||
|
hass.states.async_set("light.one", "on")
|
||||||
|
hass.states.async_set("lock.one", "locked")
|
||||||
|
|
||||||
|
async_track_template_result(hass, template_complex, specific_run_callback)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.domain", "light")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(specific_runs) == 1
|
||||||
|
assert specific_runs[0].strip() == "['light.one']"
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.domain", "lock")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(specific_runs) == 2
|
||||||
|
assert specific_runs[1].strip() == "['lock.one']"
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.domain", "all")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(specific_runs) == 3
|
||||||
|
assert "light.one" in specific_runs[2]
|
||||||
|
assert "lock.one" in specific_runs[2]
|
||||||
|
assert "sensor.domain" in specific_runs[2]
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.domain", "light")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(specific_runs) == 4
|
||||||
|
assert specific_runs[3].strip() == "['light.one']"
|
||||||
|
|
||||||
|
hass.states.async_set("light.two", "on")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(specific_runs) == 5
|
||||||
|
assert "light.one" in specific_runs[4]
|
||||||
|
assert "light.two" in specific_runs[4]
|
||||||
|
assert "sensor.domain" not in specific_runs[4]
|
||||||
|
|
||||||
|
hass.states.async_set("light.three", "on")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(specific_runs) == 6
|
||||||
|
assert "light.one" in specific_runs[5]
|
||||||
|
assert "light.two" in specific_runs[5]
|
||||||
|
assert "light.three" in specific_runs[5]
|
||||||
|
assert "sensor.domain" not in specific_runs[5]
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.domain", "lock")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(specific_runs) == 7
|
||||||
|
assert specific_runs[6].strip() == "['lock.one']"
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.domain", "single_binary_sensor")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(specific_runs) == 8
|
||||||
|
assert specific_runs[7].strip() == "unknown"
|
||||||
|
|
||||||
|
hass.states.async_set("binary_sensor.single", "binary_sensor_on")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(specific_runs) == 9
|
||||||
|
assert specific_runs[8].strip() == "binary_sensor_on"
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.domain", "lock")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(specific_runs) == 10
|
||||||
|
assert specific_runs[9].strip() == "['lock.one']"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_track_template_result_with_wildcard(hass):
|
||||||
|
"""Test tracking template with a wildcard."""
|
||||||
|
specific_runs = []
|
||||||
|
template_complex_str = r"""
|
||||||
|
|
||||||
|
{% for state in states %}
|
||||||
|
{% if state.entity_id | regex_match('.*\.office_') %}
|
||||||
|
{{ state.entity_id }}={{ state.state }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
"""
|
||||||
|
template_complex = Template(template_complex_str, hass)
|
||||||
|
|
||||||
|
def specific_run_callback(event, template, old_result, new_result):
|
||||||
|
specific_runs.append(new_result)
|
||||||
|
|
||||||
|
hass.states.async_set("cover.office_drapes", "closed")
|
||||||
|
hass.states.async_set("cover.office_window", "closed")
|
||||||
|
hass.states.async_set("cover.office_skylight", "open")
|
||||||
|
|
||||||
|
async_track_template_result(hass, template_complex, specific_run_callback)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
hass.states.async_set("cover.office_window", "open")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(specific_runs) == 1
|
||||||
|
|
||||||
|
assert "cover.office_drapes=closed" in specific_runs[0]
|
||||||
|
assert "cover.office_window=open" in specific_runs[0]
|
||||||
|
assert "cover.office_skylight=open" in specific_runs[0]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_track_template_result_with_group(hass):
|
||||||
|
"""Test tracking template with a group."""
|
||||||
|
hass.states.async_set("sensor.power_1", 0)
|
||||||
|
hass.states.async_set("sensor.power_2", 200.2)
|
||||||
|
hass.states.async_set("sensor.power_3", 400.4)
|
||||||
|
hass.states.async_set("sensor.power_4", 800.8)
|
||||||
|
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"group",
|
||||||
|
{"group": {"power_sensors": "sensor.power_1,sensor.power_2,sensor.power_3"}},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get("group.power_sensors")
|
||||||
|
assert hass.states.get("group.power_sensors").state
|
||||||
|
|
||||||
|
specific_runs = []
|
||||||
|
template_complex_str = r"""
|
||||||
|
|
||||||
|
{{ states.group.power_sensors.attributes.entity_id | expand | map(attribute='state')|map('float')|sum }}
|
||||||
|
|
||||||
|
"""
|
||||||
|
template_complex = Template(template_complex_str, hass)
|
||||||
|
|
||||||
|
def specific_run_callback(event, template, old_result, new_result):
|
||||||
|
specific_runs.append(new_result)
|
||||||
|
|
||||||
|
async_track_template_result(hass, template_complex, specific_run_callback)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.power_1", 100.1)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(specific_runs) == 1
|
||||||
|
|
||||||
|
assert specific_runs[0] == str(100.1 + 200.2 + 400.4)
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.power_3", 0)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(specific_runs) == 2
|
||||||
|
|
||||||
|
assert specific_runs[1] == str(100.1 + 200.2 + 0)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.config.load_yaml_config_file",
|
||||||
|
return_value={
|
||||||
|
"group": {
|
||||||
|
"power_sensors": "sensor.power_1,sensor.power_2,sensor.power_3,sensor.power_4",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
):
|
||||||
|
await hass.services.async_call("group", "reload")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert specific_runs[-1] == str(100.1 + 200.2 + 0 + 800.8)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_track_template_result_iterator(hass):
|
||||||
|
"""Test tracking template."""
|
||||||
|
iterator_runs = []
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
|
def iterator_callback(event, template, old_result, new_result):
|
||||||
|
iterator_runs.append(new_result)
|
||||||
|
|
||||||
|
async_track_template_result(
|
||||||
|
hass,
|
||||||
|
Template(
|
||||||
|
"""
|
||||||
|
{% for state in states.sensor %}
|
||||||
|
{% if state.state == 'on' %}
|
||||||
|
{{ state.entity_id }},
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
""",
|
||||||
|
hass,
|
||||||
|
),
|
||||||
|
iterator_callback,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.test", 5)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert iterator_runs == [""]
|
||||||
|
|
||||||
|
filter_runs = []
|
||||||
|
|
||||||
|
@ha.callback
|
||||||
|
def filter_callback(event, template, old_result, new_result):
|
||||||
|
filter_runs.append(new_result)
|
||||||
|
|
||||||
|
async_track_template_result(
|
||||||
|
hass,
|
||||||
|
Template(
|
||||||
|
"""{{ states.sensor|selectattr("state","equalto","on")
|
||||||
|
|join(",", attribute="entity_id") }}""",
|
||||||
|
hass,
|
||||||
|
),
|
||||||
|
filter_callback,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.test", 6)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert filter_runs == [""]
|
||||||
|
assert iterator_runs == [""]
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.new", "on")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert iterator_runs == ["", "sensor.new,"]
|
||||||
|
assert filter_runs == ["", "sensor.new"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_track_template_result_errors(hass, caplog):
|
||||||
|
"""Test tracking template with errors in the template."""
|
||||||
|
template_syntax_error = Template("{{states.switch", hass)
|
||||||
|
|
||||||
|
template_not_exist = Template("{{states.switch.not_exist.state }}", hass)
|
||||||
|
|
||||||
|
syntax_error_runs = []
|
||||||
|
not_exist_runs = []
|
||||||
|
|
||||||
|
def syntax_error_listener(event, template, last_result, result):
|
||||||
|
syntax_error_runs.append((event, template, last_result, result))
|
||||||
|
|
||||||
|
async_track_template_result(hass, template_syntax_error, syntax_error_listener)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(syntax_error_runs) == 0
|
||||||
|
assert "TemplateSyntaxError" in caplog.text
|
||||||
|
|
||||||
|
async_track_template_result(
|
||||||
|
hass,
|
||||||
|
template_not_exist,
|
||||||
|
lambda event, template, last_result, result: (
|
||||||
|
not_exist_runs.append((event, template, last_result, result))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(syntax_error_runs) == 0
|
||||||
|
assert len(not_exist_runs) == 0
|
||||||
|
|
||||||
|
hass.states.async_set("switch.not_exist", "off")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(not_exist_runs) == 1
|
||||||
|
assert not_exist_runs[0][0].data.get("entity_id") == "switch.not_exist"
|
||||||
|
assert not_exist_runs[0][1] == template_not_exist
|
||||||
|
assert not_exist_runs[0][2] is None
|
||||||
|
assert not_exist_runs[0][3] == "off"
|
||||||
|
|
||||||
|
hass.states.async_set("switch.not_exist", "on")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(syntax_error_runs) == 0
|
||||||
|
assert len(not_exist_runs) == 2
|
||||||
|
assert not_exist_runs[1][0].data.get("entity_id") == "switch.not_exist"
|
||||||
|
assert not_exist_runs[1][1] == template_not_exist
|
||||||
|
assert not_exist_runs[1][2] == "off"
|
||||||
|
assert not_exist_runs[1][3] == "on"
|
||||||
|
|
||||||
|
with patch.object(Template, "async_render") as render:
|
||||||
|
render.side_effect = TemplateError("Test")
|
||||||
|
|
||||||
|
hass.states.async_set("switch.not_exist", "off")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(not_exist_runs) == 3
|
||||||
|
assert not_exist_runs[2][0].data.get("entity_id") == "switch.not_exist"
|
||||||
|
assert not_exist_runs[2][1] == template_not_exist
|
||||||
|
assert not_exist_runs[2][2] == "on"
|
||||||
|
assert isinstance(not_exist_runs[2][3], TemplateError)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_track_template_result_refresh_cancel(hass):
|
||||||
|
"""Test cancelling and refreshing result."""
|
||||||
|
template_refresh = Template("{{states.switch.test.state == 'on' and now() }}", hass)
|
||||||
|
|
||||||
|
refresh_runs = []
|
||||||
|
|
||||||
|
def refresh_listener(event, template, last_result, result):
|
||||||
|
refresh_runs.append(result)
|
||||||
|
|
||||||
|
info = async_track_template_result(hass, template_refresh, refresh_listener)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
hass.states.async_set("switch.test", "off")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert refresh_runs == ["False"]
|
||||||
|
|
||||||
|
assert len(refresh_runs) == 1
|
||||||
|
|
||||||
|
info.async_refresh()
|
||||||
|
hass.states.async_set("switch.test", "on")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(refresh_runs) == 2
|
||||||
|
assert refresh_runs[0] != refresh_runs[1]
|
||||||
|
|
||||||
|
info.async_remove()
|
||||||
|
hass.states.async_set("switch.test", "off")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(refresh_runs) == 2
|
||||||
|
|
||||||
|
template_refresh = Template("{{ value }}", hass)
|
||||||
|
refresh_runs = []
|
||||||
|
|
||||||
|
info = async_track_template_result(
|
||||||
|
hass, template_refresh, refresh_listener, {"value": "duck"}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
info.async_refresh()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert refresh_runs == ["duck"]
|
||||||
|
|
||||||
|
info.async_refresh()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert refresh_runs == ["duck"]
|
||||||
|
|
||||||
|
info.async_refresh({"value": "dog"})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert refresh_runs == ["duck", "dog"]
|
||||||
|
|
||||||
|
|
||||||
async def test_track_same_state_simple_no_trigger(hass):
|
async def test_track_same_state_simple_no_trigger(hass):
|
||||||
|
@ -39,25 +39,22 @@ def render_to_info(hass, template_str, variables=None):
|
|||||||
def extract_entities(hass, template_str, variables=None):
|
def extract_entities(hass, template_str, variables=None):
|
||||||
"""Extract entities from a template."""
|
"""Extract entities from a template."""
|
||||||
info = render_to_info(hass, template_str, variables)
|
info = render_to_info(hass, template_str, variables)
|
||||||
# pylint: disable=protected-access
|
return info.entities
|
||||||
assert not hasattr(info, "_domains")
|
|
||||||
return info._entities
|
|
||||||
|
|
||||||
|
|
||||||
def assert_result_info(info, result, entities=None, domains=None, all_states=False):
|
def assert_result_info(info, result, entities=None, domains=None, all_states=False):
|
||||||
"""Check result info."""
|
"""Check result info."""
|
||||||
assert info.result == result
|
assert info.result == result
|
||||||
# pylint: disable=protected-access
|
assert info.all_states == all_states
|
||||||
assert info._all_states == all_states
|
|
||||||
assert info.filter_lifecycle("invalid_entity_name.somewhere") == all_states
|
assert info.filter_lifecycle("invalid_entity_name.somewhere") == all_states
|
||||||
if entities is not None:
|
if entities is not None:
|
||||||
assert info._entities == frozenset(entities)
|
assert info.entities == frozenset(entities)
|
||||||
assert all([info.filter(entity) for entity in entities])
|
assert all([info.filter(entity) for entity in entities])
|
||||||
assert not info.filter("invalid_entity_name.somewhere")
|
assert not info.filter("invalid_entity_name.somewhere")
|
||||||
else:
|
else:
|
||||||
assert not info._entities
|
assert not info.entities
|
||||||
if domains is not None:
|
if domains is not None:
|
||||||
assert info._domains == frozenset(domains)
|
assert info.domains == frozenset(domains)
|
||||||
assert all([info.filter_lifecycle(domain + ".entity") for domain in domains])
|
assert all([info.filter_lifecycle(domain + ".entity") for domain in domains])
|
||||||
else:
|
else:
|
||||||
assert not hasattr(info, "_domains")
|
assert not hasattr(info, "_domains")
|
||||||
@ -1256,7 +1253,7 @@ async def test_closest_function_home_vs_group_entity_id(hass):
|
|||||||
|
|
||||||
info = render_to_info(hass, '{{ closest("group.location_group").entity_id }}')
|
info = render_to_info(hass, '{{ closest("group.location_group").entity_id }}')
|
||||||
assert_result_info(
|
assert_result_info(
|
||||||
info, "test_domain.object", ["test_domain.object", "group.location_group"]
|
info, "test_domain.object", {"group.location_group", "test_domain.object"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1281,12 +1278,12 @@ async def test_closest_function_home_vs_group_state(hass):
|
|||||||
|
|
||||||
info = render_to_info(hass, '{{ closest("group.location_group").entity_id }}')
|
info = render_to_info(hass, '{{ closest("group.location_group").entity_id }}')
|
||||||
assert_result_info(
|
assert_result_info(
|
||||||
info, "test_domain.object", ["test_domain.object", "group.location_group"]
|
info, "test_domain.object", {"group.location_group", "test_domain.object"}
|
||||||
)
|
)
|
||||||
|
|
||||||
info = render_to_info(hass, "{{ closest(states.group.location_group).entity_id }}")
|
info = render_to_info(hass, "{{ closest(states.group.location_group).entity_id }}")
|
||||||
assert_result_info(
|
assert_result_info(
|
||||||
info, "test_domain.object", ["test_domain.object", "group.location_group"]
|
info, "test_domain.object", {"test_domain.object", "group.location_group"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1303,7 +1300,7 @@ async def test_expand(hass):
|
|||||||
info = render_to_info(
|
info = render_to_info(
|
||||||
hass, "{{ expand('test.object') | map(attribute='entity_id') | join(', ') }}"
|
hass, "{{ expand('test.object') | map(attribute='entity_id') | join(', ') }}"
|
||||||
)
|
)
|
||||||
assert_result_info(info, "test.object", [])
|
assert_result_info(info, "test.object", ["test.object"])
|
||||||
|
|
||||||
info = render_to_info(
|
info = render_to_info(
|
||||||
hass,
|
hass,
|
||||||
@ -1322,26 +1319,45 @@ async def test_expand(hass):
|
|||||||
hass,
|
hass,
|
||||||
"{{ expand('group.new_group') | map(attribute='entity_id') | join(', ') }}",
|
"{{ expand('group.new_group') | map(attribute='entity_id') | join(', ') }}",
|
||||||
)
|
)
|
||||||
assert_result_info(info, "test.object", ["group.new_group"])
|
assert_result_info(info, "test.object", {"group.new_group", "test.object"})
|
||||||
|
|
||||||
info = render_to_info(
|
info = render_to_info(
|
||||||
hass, "{{ expand(states.group) | map(attribute='entity_id') | join(', ') }}"
|
hass, "{{ expand(states.group) | map(attribute='entity_id') | join(', ') }}"
|
||||||
)
|
)
|
||||||
assert_result_info(info, "test.object", ["group.new_group"], ["group"])
|
assert_result_info(
|
||||||
|
info, "test.object", {"test.object", "group.new_group"}, ["group"]
|
||||||
|
)
|
||||||
|
|
||||||
info = render_to_info(
|
info = render_to_info(
|
||||||
hass,
|
hass,
|
||||||
"{{ expand('group.new_group', 'test.object')"
|
"{{ expand('group.new_group', 'test.object')"
|
||||||
" | map(attribute='entity_id') | join(', ') }}",
|
" | map(attribute='entity_id') | join(', ') }}",
|
||||||
)
|
)
|
||||||
assert_result_info(info, "test.object", ["group.new_group"])
|
assert_result_info(info, "test.object", {"test.object", "group.new_group"})
|
||||||
|
|
||||||
info = render_to_info(
|
info = render_to_info(
|
||||||
hass,
|
hass,
|
||||||
"{{ ['group.new_group', 'test.object'] | expand"
|
"{{ ['group.new_group', 'test.object'] | expand"
|
||||||
" | map(attribute='entity_id') | join(', ') }}",
|
" | map(attribute='entity_id') | join(', ') }}",
|
||||||
)
|
)
|
||||||
assert_result_info(info, "test.object", ["group.new_group"])
|
assert_result_info(info, "test.object", {"test.object", "group.new_group"})
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.power_1", 0)
|
||||||
|
hass.states.async_set("sensor.power_2", 200.2)
|
||||||
|
hass.states.async_set("sensor.power_3", 400.4)
|
||||||
|
await group.Group.async_create_group(
|
||||||
|
hass, "power sensors", ["sensor.power_1", "sensor.power_2", "sensor.power_3"]
|
||||||
|
)
|
||||||
|
|
||||||
|
info = render_to_info(
|
||||||
|
hass,
|
||||||
|
"{{ states.group.power_sensors.attributes.entity_id | expand | map(attribute='state')|map('float')|sum }}",
|
||||||
|
)
|
||||||
|
assert_result_info(
|
||||||
|
info,
|
||||||
|
str(200.2 + 400.4),
|
||||||
|
{"group.power_sensors", "sensor.power_1", "sensor.power_2", "sensor.power_3"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_closest_function_to_coord(hass):
|
def test_closest_function_to_coord(hass):
|
||||||
@ -1390,6 +1406,198 @@ def test_closest_function_to_coord(hass):
|
|||||||
assert tpl.async_render() == "test_domain.closest_zone"
|
assert tpl.async_render() == "test_domain.closest_zone"
|
||||||
|
|
||||||
|
|
||||||
|
def test_async_render_to_info_with_branching(hass):
|
||||||
|
"""Test async_render_to_info function by domain."""
|
||||||
|
hass.states.async_set("light.a", "off")
|
||||||
|
hass.states.async_set("light.b", "on")
|
||||||
|
hass.states.async_set("light.c", "off")
|
||||||
|
|
||||||
|
info = render_to_info(
|
||||||
|
hass,
|
||||||
|
"""
|
||||||
|
{% if states.light.a == "on" %}
|
||||||
|
{{ states.light.b.state }}
|
||||||
|
{% else %}
|
||||||
|
{{ states.light.c.state }}
|
||||||
|
{% endif %}
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
assert_result_info(info, "off", {"light.a", "light.c"})
|
||||||
|
|
||||||
|
info = render_to_info(
|
||||||
|
hass,
|
||||||
|
"""
|
||||||
|
{% if states.light.a.state == "off" %}
|
||||||
|
{% set domain = "light" %}
|
||||||
|
{{ states[domain].b.state }}
|
||||||
|
{% endif %}
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
assert_result_info(info, "on", {"light.a", "light.b"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_async_render_to_info_with_complex_branching(hass):
|
||||||
|
"""Test async_render_to_info function by domain."""
|
||||||
|
hass.states.async_set("light.a", "off")
|
||||||
|
hass.states.async_set("light.b", "on")
|
||||||
|
hass.states.async_set("light.c", "off")
|
||||||
|
hass.states.async_set("vacuum.a", "off")
|
||||||
|
hass.states.async_set("device_tracker.a", "off")
|
||||||
|
hass.states.async_set("device_tracker.b", "off")
|
||||||
|
hass.states.async_set("lock.a", "off")
|
||||||
|
hass.states.async_set("sensor.a", "off")
|
||||||
|
hass.states.async_set("binary_sensor.a", "off")
|
||||||
|
|
||||||
|
info = render_to_info(
|
||||||
|
hass,
|
||||||
|
"""
|
||||||
|
{% set domain = "vacuum" %}
|
||||||
|
{% if states.light.a == "on" %}
|
||||||
|
{{ states.light.b.state }}
|
||||||
|
{% elif states.light.a == "on" %}
|
||||||
|
{{ states.device_tracker }}
|
||||||
|
{% elif states.light.a == "on" %}
|
||||||
|
{{ states[domain] | list }}
|
||||||
|
{% elif states('light.b') == "on" %}
|
||||||
|
{{ states[otherdomain] | map(attribute='entity_id') | list }}
|
||||||
|
{% elif states.light.a == "on" %}
|
||||||
|
{{ states["nonexist"] | list }}
|
||||||
|
{% else %}
|
||||||
|
else
|
||||||
|
{% endif %}
|
||||||
|
""",
|
||||||
|
{"otherdomain": "sensor"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_result_info(info, "['sensor.a']", {"light.a", "light.b"}, {"sensor"})
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_render_to_info_with_wildcard_matching_entity_id(hass):
|
||||||
|
"""Test tracking template with a wildcard."""
|
||||||
|
template_complex_str = r"""
|
||||||
|
|
||||||
|
{% for state in states %}
|
||||||
|
{% if state.entity_id | regex_match('.*\.office_') %}
|
||||||
|
{{ state.entity_id }}={{ state.state }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
"""
|
||||||
|
hass.states.async_set("cover.office_drapes", "closed")
|
||||||
|
hass.states.async_set("cover.office_window", "closed")
|
||||||
|
hass.states.async_set("cover.office_skylight", "open")
|
||||||
|
info = render_to_info(hass, template_complex_str)
|
||||||
|
|
||||||
|
assert not info.domains
|
||||||
|
assert info.entities == {
|
||||||
|
"cover.office_drapes",
|
||||||
|
"cover.office_window",
|
||||||
|
"cover.office_skylight",
|
||||||
|
}
|
||||||
|
assert info.all_states is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_render_to_info_with_wildcard_matching_state(hass):
|
||||||
|
"""Test tracking template with a wildcard."""
|
||||||
|
template_complex_str = """
|
||||||
|
|
||||||
|
{% for state in states %}
|
||||||
|
{% if state.state | regex_match('ope.*') %}
|
||||||
|
{{ state.entity_id }}={{ state.state }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
"""
|
||||||
|
hass.states.async_set("cover.office_drapes", "closed")
|
||||||
|
hass.states.async_set("cover.office_window", "closed")
|
||||||
|
hass.states.async_set("cover.office_skylight", "open")
|
||||||
|
hass.states.async_set("cover.x_skylight", "open")
|
||||||
|
hass.states.async_set("binary_sensor.door", "open")
|
||||||
|
|
||||||
|
info = render_to_info(hass, template_complex_str)
|
||||||
|
|
||||||
|
assert not info.domains
|
||||||
|
assert info.entities == {
|
||||||
|
"cover.x_skylight",
|
||||||
|
"binary_sensor.door",
|
||||||
|
"cover.office_drapes",
|
||||||
|
"cover.office_window",
|
||||||
|
"cover.office_skylight",
|
||||||
|
}
|
||||||
|
assert info.all_states is True
|
||||||
|
|
||||||
|
hass.states.async_set("binary_sensor.door", "closed")
|
||||||
|
info = render_to_info(hass, template_complex_str)
|
||||||
|
|
||||||
|
assert not info.domains
|
||||||
|
assert info.entities == {
|
||||||
|
"cover.x_skylight",
|
||||||
|
"binary_sensor.door",
|
||||||
|
"cover.office_drapes",
|
||||||
|
"cover.office_window",
|
||||||
|
"cover.office_skylight",
|
||||||
|
}
|
||||||
|
assert info.all_states is True
|
||||||
|
|
||||||
|
template_cover_str = """
|
||||||
|
|
||||||
|
{% for state in states.cover %}
|
||||||
|
{% if state.state | regex_match('ope.*') %}
|
||||||
|
{{ state.entity_id }}={{ state.state }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
"""
|
||||||
|
hass.states.async_set("cover.x_skylight", "closed")
|
||||||
|
info = render_to_info(hass, template_cover_str)
|
||||||
|
|
||||||
|
assert info.domains == {"cover"}
|
||||||
|
assert info.entities == {
|
||||||
|
"cover.x_skylight",
|
||||||
|
"cover.office_drapes",
|
||||||
|
"cover.office_window",
|
||||||
|
"cover.office_skylight",
|
||||||
|
}
|
||||||
|
assert info.all_states is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_nested_async_render_to_info_case(hass):
|
||||||
|
"""Test a deeply nested state with async_render_to_info."""
|
||||||
|
|
||||||
|
hass.states.async_set("input_select.picker", "vacuum.a")
|
||||||
|
hass.states.async_set("vacuum.a", "off")
|
||||||
|
|
||||||
|
info = render_to_info(
|
||||||
|
hass, "{{ states[states['input_select.picker'].state].state }}", {}
|
||||||
|
)
|
||||||
|
assert_result_info(info, "off", {"input_select.picker", "vacuum.a"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_as_boolean(hass):
|
||||||
|
"""Test converting a template result to a boolean."""
|
||||||
|
|
||||||
|
template.result_as_boolean(True) is True
|
||||||
|
template.result_as_boolean(" 1 ") is True
|
||||||
|
template.result_as_boolean(" true ") is True
|
||||||
|
template.result_as_boolean(" TrUE ") is True
|
||||||
|
template.result_as_boolean(" YeS ") is True
|
||||||
|
template.result_as_boolean(" On ") is True
|
||||||
|
template.result_as_boolean(" Enable ") is True
|
||||||
|
template.result_as_boolean(1) is True
|
||||||
|
template.result_as_boolean(-1) is True
|
||||||
|
template.result_as_boolean(500) is True
|
||||||
|
|
||||||
|
template.result_as_boolean(False) is False
|
||||||
|
template.result_as_boolean(" 0 ") is False
|
||||||
|
template.result_as_boolean(" false ") is False
|
||||||
|
template.result_as_boolean(" FaLsE ") is False
|
||||||
|
template.result_as_boolean(" no ") is False
|
||||||
|
template.result_as_boolean(" off ") is False
|
||||||
|
template.result_as_boolean(" disable ") is False
|
||||||
|
template.result_as_boolean(0) is False
|
||||||
|
template.result_as_boolean(None) is False
|
||||||
|
|
||||||
|
|
||||||
def test_closest_function_to_entity_id(hass):
|
def test_closest_function_to_entity_id(hass):
|
||||||
"""Test closest function to entity id."""
|
"""Test closest function to entity id."""
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
@ -1558,13 +1766,16 @@ def test_extract_entities_none_exclude_stuff(hass):
|
|||||||
|
|
||||||
assert (
|
assert (
|
||||||
template.extract_entities(
|
template.extract_entities(
|
||||||
hass, "{{ closest(states.zone.far_away, states.test_domain).entity_id }}"
|
hass,
|
||||||
|
"{{ closest(states.zone.far_away, states.test_domain.xxx).entity_id }}",
|
||||||
)
|
)
|
||||||
== MATCH_ALL
|
== MATCH_ALL
|
||||||
)
|
)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
template.extract_entities(hass, '{{ distance("123", states.test_object_2) }}')
|
template.extract_entities(
|
||||||
|
hass, '{{ distance("123", states.test_object_2.user) }}'
|
||||||
|
)
|
||||||
== MATCH_ALL
|
== MATCH_ALL
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1673,6 +1884,42 @@ def test_generate_select(hass):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_render_to_info_in_conditional(hass):
|
||||||
|
"""Test extract entities function with none entities stuff."""
|
||||||
|
template_str = """
|
||||||
|
{{ states("sensor.xyz") == "dog" }}
|
||||||
|
"""
|
||||||
|
|
||||||
|
tmp = template.Template(template_str, hass)
|
||||||
|
info = tmp.async_render_to_info()
|
||||||
|
assert_result_info(info, "False", ["sensor.xyz"], [])
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.xyz", "dog")
|
||||||
|
hass.states.async_set("sensor.cow", "True")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
template_str = """
|
||||||
|
{% if states("sensor.xyz") == "dog" %}
|
||||||
|
{{ states("sensor.cow") }}
|
||||||
|
{% else %}
|
||||||
|
{{ states("sensor.pig") }}
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
tmp = template.Template(template_str, hass)
|
||||||
|
info = tmp.async_render_to_info()
|
||||||
|
assert_result_info(info, "True", ["sensor.xyz", "sensor.cow"], [])
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.xyz", "sheep")
|
||||||
|
hass.states.async_set("sensor.pig", "oink")
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
tmp = template.Template(template_str, hass)
|
||||||
|
info = tmp.async_render_to_info()
|
||||||
|
assert_result_info(info, "oink", ["sensor.xyz", "sensor.pig"], [])
|
||||||
|
|
||||||
|
|
||||||
async def test_extract_entities_match_entities(hass):
|
async def test_extract_entities_match_entities(hass):
|
||||||
"""Test extract entities function with entities stuff."""
|
"""Test extract entities function with entities stuff."""
|
||||||
assert (
|
assert (
|
||||||
@ -1739,8 +1986,8 @@ Hercules you power goes done!.
|
|||||||
hass,
|
hass,
|
||||||
"""
|
"""
|
||||||
{{
|
{{
|
||||||
states.sensor.pick_temperature.state ~ „°C (“ ~
|
states.sensor.pick_temperature.state ~ "°C (" ~
|
||||||
states.sensor.pick_humidity.state ~ „ %“
|
states.sensor.pick_humidity.state ~ " %"
|
||||||
}}
|
}}
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
@ -1771,11 +2018,15 @@ states.sensor.pick_humidity.state ~ „ %“
|
|||||||
hass, "{{ expand('group.expand_group') | list | length }}"
|
hass, "{{ expand('group.expand_group') | list | length }}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assert ["test_domain.entity"] == template.Template(
|
assert ["test_domain.entity"] == template.Template(
|
||||||
'{{ is_state("test_domain.entity", "on") }}', hass
|
'{{ is_state("test_domain.entity", "on") }}', hass
|
||||||
).extract_entities()
|
).extract_entities()
|
||||||
|
|
||||||
|
# No expand, extract finds the group
|
||||||
|
assert template.extract_entities(hass, "{{ states('group.empty_group') }}") == [
|
||||||
|
"group.empty_group"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_extract_entities_with_variables(hass):
|
def test_extract_entities_with_variables(hass):
|
||||||
"""Test extract entities function with variables and entities stuff."""
|
"""Test extract entities function with variables and entities stuff."""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user