Update trigger based template entity resolution order (#140660)

* Update trigger based template entity resolution order

* add test

* fix most comments

* Move resolution to base class

* add comment

* remove uncessary if statement

* add more tests

* update availability tests

* update logic stage 1

* phase 2 changes

* fix trigger template entity tests

* fix trigger template entities

* command line tests

* sql tests

* scrape test

* update doc string

* add rest tests

* update sql sensor _update signature

* fix scrape test constructor

* move state check to trigger_entity

* fix comments

* Update homeassistant/components/template/trigger_entity.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/helpers/trigger_template_entity.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/helpers/trigger_template_entity.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* update command_line and rest

* update scrape

* update sql

* add case to command_line sensor

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Petro31 2025-04-25 07:17:25 -04:00 committed by GitHub
parent dc8e1773f1
commit ff2c901930
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1892 additions and 185 deletions

View File

@ -56,7 +56,10 @@ from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.trigger_template_entity import CONF_AVAILABILITY from homeassistant.helpers.trigger_template_entity import (
CONF_AVAILABILITY,
ValueTemplate,
)
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
@ -91,7 +94,9 @@ BINARY_SENSOR_SCHEMA = vol.Schema(
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
cv.template, ValueTemplate.from_template
),
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional( vol.Optional(
@ -108,7 +113,9 @@ COVER_SCHEMA = vol.Schema(
vol.Optional(CONF_COMMAND_STOP, default="true"): cv.string, vol.Optional(CONF_COMMAND_STOP, default="true"): cv.string,
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_ICON): cv.template, vol.Optional(CONF_ICON): cv.template,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
cv.template, ValueTemplate.from_template
),
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
@ -134,7 +141,9 @@ SENSOR_SCHEMA = vol.Schema(
vol.Optional(CONF_NAME, default=SENSOR_DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=SENSOR_DEFAULT_NAME): cv.string,
vol.Optional(CONF_ICON): cv.template, vol.Optional(CONF_ICON): cv.template,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
cv.template, ValueTemplate.from_template
),
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA, vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
@ -150,7 +159,9 @@ SWITCH_SCHEMA = vol.Schema(
vol.Optional(CONF_COMMAND_ON, default="true"): cv.string, vol.Optional(CONF_COMMAND_ON, default="true"): cv.string,
vol.Optional(CONF_COMMAND_STATE): cv.string, vol.Optional(CONF_COMMAND_STATE): cv.string,
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
cv.template, ValueTemplate.from_template
),
vol.Optional(CONF_ICON): cv.template, vol.Optional(CONF_ICON): cv.template,
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,

View File

@ -18,7 +18,10 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity from homeassistant.helpers.trigger_template_entity import (
ManualTriggerEntity,
ValueTemplate,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -50,7 +53,7 @@ async def async_setup_platform(
scan_interval: timedelta = binary_sensor_config.get( scan_interval: timedelta = binary_sensor_config.get(
CONF_SCAN_INTERVAL, SCAN_INTERVAL CONF_SCAN_INTERVAL, SCAN_INTERVAL
) )
value_template: Template | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE) value_template: ValueTemplate | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE)
data = CommandSensorData(hass, command, command_timeout) data = CommandSensorData(hass, command, command_timeout)
@ -86,7 +89,7 @@ class CommandBinarySensor(ManualTriggerEntity, BinarySensorEntity):
config: ConfigType, config: ConfigType,
payload_on: str, payload_on: str,
payload_off: str, payload_off: str,
value_template: Template | None, value_template: ValueTemplate | None,
scan_interval: timedelta, scan_interval: timedelta,
) -> None: ) -> None:
"""Initialize the Command line binary sensor.""" """Initialize the Command line binary sensor."""
@ -133,9 +136,14 @@ class CommandBinarySensor(ManualTriggerEntity, BinarySensorEntity):
await self.data.async_update() await self.data.async_update()
value = self.data.value value = self.data.value
variables = self._template_variables_with_value(value)
if not self._render_availability_template(variables):
self.async_write_ha_state()
return
if self._value_template is not None: if self._value_template is not None:
value = self._value_template.async_render_with_possible_json_value( value = self._value_template.async_render_as_value_template(
value, None self.entity_id, variables, None
) )
self._attr_is_on = None self._attr_is_on = None
if value == self._payload_on: if value == self._payload_on:
@ -143,7 +151,7 @@ class CommandBinarySensor(ManualTriggerEntity, BinarySensorEntity):
elif value == self._payload_off: elif value == self._payload_off:
self._attr_is_on = False self._attr_is_on = False
self._process_manual_data(value) self._process_manual_data(variables)
self.async_write_ha_state() self.async_write_ha_state()
async def async_update(self) -> None: async def async_update(self) -> None:

View File

@ -20,7 +20,10 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity from homeassistant.helpers.trigger_template_entity import (
ManualTriggerEntity,
ValueTemplate,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util, slugify from homeassistant.util import dt as dt_util, slugify
@ -79,7 +82,7 @@ class CommandCover(ManualTriggerEntity, CoverEntity):
command_close: str, command_close: str,
command_stop: str, command_stop: str,
command_state: str | None, command_state: str | None,
value_template: Template | None, value_template: ValueTemplate | None,
timeout: int, timeout: int,
scan_interval: timedelta, scan_interval: timedelta,
) -> None: ) -> None:
@ -164,14 +167,20 @@ class CommandCover(ManualTriggerEntity, CoverEntity):
"""Update device state.""" """Update device state."""
if self._command_state: if self._command_state:
payload = str(await self._async_query_state()) payload = str(await self._async_query_state())
variables = self._template_variables_with_value(payload)
if not self._render_availability_template(variables):
self.async_write_ha_state()
return
if self._value_template: if self._value_template:
payload = self._value_template.async_render_with_possible_json_value( payload = self._value_template.async_render_as_value_template(
payload, None self.entity_id, variables, None
) )
self._state = None self._state = None
if payload: if payload:
self._state = int(payload) self._state = int(payload)
self._process_manual_data(payload) self._process_manual_data(variables)
self.async_write_ha_state() self.async_write_ha_state()
async def async_update(self) -> None: async def async_update(self) -> None:

View File

@ -23,7 +23,10 @@ from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from homeassistant.helpers.trigger_template_entity import ManualTriggerSensorEntity from homeassistant.helpers.trigger_template_entity import (
ManualTriggerSensorEntity,
ValueTemplate,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -57,7 +60,7 @@ async def async_setup_platform(
json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES) json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES)
json_attributes_path: str | None = sensor_config.get(CONF_JSON_ATTRIBUTES_PATH) json_attributes_path: str | None = sensor_config.get(CONF_JSON_ATTRIBUTES_PATH)
scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE) value_template: ValueTemplate | None = sensor_config.get(CONF_VALUE_TEMPLATE)
data = CommandSensorData(hass, command, command_timeout) data = CommandSensorData(hass, command, command_timeout)
trigger_entity_config = { trigger_entity_config = {
@ -88,7 +91,7 @@ class CommandSensor(ManualTriggerSensorEntity):
self, self,
data: CommandSensorData, data: CommandSensorData,
config: ConfigType, config: ConfigType,
value_template: Template | None, value_template: ValueTemplate | None,
json_attributes: list[str] | None, json_attributes: list[str] | None,
json_attributes_path: str | None, json_attributes_path: str | None,
scan_interval: timedelta, scan_interval: timedelta,
@ -144,6 +147,11 @@ class CommandSensor(ManualTriggerSensorEntity):
await self.data.async_update() await self.data.async_update()
value = self.data.value value = self.data.value
variables = self._template_variables_with_value(self.data.value)
if not self._render_availability_template(variables):
self.async_write_ha_state()
return
if self._json_attributes: if self._json_attributes:
self._attr_extra_state_attributes = {} self._attr_extra_state_attributes = {}
if value: if value:
@ -168,16 +176,17 @@ class CommandSensor(ManualTriggerSensorEntity):
LOGGER.warning("Unable to parse output as JSON: %s", value) LOGGER.warning("Unable to parse output as JSON: %s", value)
else: else:
LOGGER.warning("Empty reply found when expecting JSON data") LOGGER.warning("Empty reply found when expecting JSON data")
if self._value_template is None: if self._value_template is None:
self._attr_native_value = None self._attr_native_value = None
self._process_manual_data(value) self._process_manual_data(variables)
self.async_write_ha_state()
return return
self._attr_native_value = None self._attr_native_value = None
if self._value_template is not None and value is not None: if self._value_template is not None and value is not None:
value = self._value_template.async_render_with_possible_json_value( value = self._value_template.async_render_as_value_template(
value, self.entity_id, variables, None
None,
) )
if self.device_class not in { if self.device_class not in {
@ -190,7 +199,7 @@ class CommandSensor(ManualTriggerSensorEntity):
value, self.entity_id, self.device_class value, self.entity_id, self.device_class
) )
self._process_manual_data(value) self._process_manual_data(variables)
self.async_write_ha_state() self.async_write_ha_state()
async def async_update(self) -> None: async def async_update(self) -> None:

View File

@ -19,7 +19,10 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity from homeassistant.helpers.trigger_template_entity import (
ManualTriggerEntity,
ValueTemplate,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util, slugify from homeassistant.util import dt as dt_util, slugify
@ -78,7 +81,7 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
command_on: str, command_on: str,
command_off: str, command_off: str,
command_state: str | None, command_state: str | None,
value_template: Template | None, value_template: ValueTemplate | None,
timeout: int, timeout: int,
scan_interval: timedelta, scan_interval: timedelta,
) -> None: ) -> None:
@ -166,15 +169,21 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
"""Update device state.""" """Update device state."""
if self._command_state: if self._command_state:
payload = str(await self._async_query_state()) payload = str(await self._async_query_state())
variables = self._template_variables_with_value(payload)
if not self._render_availability_template(variables):
self.async_write_ha_state()
return
value = None value = None
if self._value_template: if self._value_template:
value = self._value_template.async_render_with_possible_json_value( value = self._value_template.async_render_as_value_template(
payload, None self.entity_id, variables, None
) )
self._attr_is_on = None self._attr_is_on = None
if payload or value: if payload or value:
self._attr_is_on = (value or payload).lower() == "true" self._attr_is_on = (value or payload).lower() == "true"
self._process_manual_data(payload) self._process_manual_data(variables)
self.async_write_ha_state() self.async_write_ha_state()
async def async_update(self) -> None: async def async_update(self) -> None:

View File

@ -32,6 +32,7 @@ from homeassistant.helpers.trigger_template_entity import (
CONF_AVAILABILITY, CONF_AVAILABILITY,
CONF_PICTURE, CONF_PICTURE,
ManualTriggerEntity, ManualTriggerEntity,
ValueTemplate,
) )
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@ -132,7 +133,7 @@ class RestBinarySensor(ManualTriggerEntity, RestEntity, BinarySensorEntity):
config[CONF_FORCE_UPDATE], config[CONF_FORCE_UPDATE],
) )
self._previous_data = None self._previous_data = None
self._value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) self._value_template: ValueTemplate | None = config.get(CONF_VALUE_TEMPLATE)
@property @property
def available(self) -> bool: def available(self) -> bool:
@ -156,11 +157,14 @@ class RestBinarySensor(ManualTriggerEntity, RestEntity, BinarySensorEntity):
) )
return return
raw_value = response variables = self._template_variables_with_value(response)
if not self._render_availability_template(variables):
self.async_write_ha_state()
return
if response is not None and self._value_template is not None: if response is not None and self._value_template is not None:
response = self._value_template.async_render_with_possible_json_value( response = self._value_template.async_render_as_value_template(
response, False self.entity_id, variables, False
) )
try: try:
@ -173,5 +177,5 @@ class RestBinarySensor(ManualTriggerEntity, RestEntity, BinarySensorEntity):
"yes": True, "yes": True,
}.get(str(response).lower(), False) }.get(str(response).lower(), False)
self._process_manual_data(raw_value) self._process_manual_data(variables)
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -31,6 +31,7 @@ from homeassistant.helpers.trigger_template_entity import (
CONF_AVAILABILITY, CONF_AVAILABILITY,
TEMPLATE_ENTITY_BASE_SCHEMA, TEMPLATE_ENTITY_BASE_SCHEMA,
TEMPLATE_SENSOR_BASE_SCHEMA, TEMPLATE_SENSOR_BASE_SCHEMA,
ValueTemplate,
) )
from homeassistant.util.ssl import SSLCipherList from homeassistant.util.ssl import SSLCipherList
@ -76,7 +77,9 @@ SENSOR_SCHEMA = {
**TEMPLATE_SENSOR_BASE_SCHEMA.schema, **TEMPLATE_SENSOR_BASE_SCHEMA.schema,
vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv,
vol.Optional(CONF_JSON_ATTRS_PATH): cv.string, vol.Optional(CONF_JSON_ATTRS_PATH): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
cv.template, ValueTemplate.from_template
),
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
vol.Optional(CONF_AVAILABILITY): cv.template, vol.Optional(CONF_AVAILABILITY): cv.template,
} }
@ -84,7 +87,9 @@ SENSOR_SCHEMA = {
BINARY_SENSOR_SCHEMA = { BINARY_SENSOR_SCHEMA = {
**TEMPLATE_ENTITY_BASE_SCHEMA.schema, **TEMPLATE_ENTITY_BASE_SCHEMA.schema,
vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
cv.template, ValueTemplate.from_template
),
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
vol.Optional(CONF_AVAILABILITY): cv.template, vol.Optional(CONF_AVAILABILITY): cv.template,
} }

View File

@ -36,6 +36,7 @@ from homeassistant.helpers.trigger_template_entity import (
CONF_AVAILABILITY, CONF_AVAILABILITY,
CONF_PICTURE, CONF_PICTURE,
ManualTriggerSensorEntity, ManualTriggerSensorEntity,
ValueTemplate,
) )
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@ -138,7 +139,7 @@ class RestSensor(ManualTriggerSensorEntity, RestEntity):
config.get(CONF_RESOURCE_TEMPLATE), config.get(CONF_RESOURCE_TEMPLATE),
config[CONF_FORCE_UPDATE], config[CONF_FORCE_UPDATE],
) )
self._value_template = config.get(CONF_VALUE_TEMPLATE) self._value_template: ValueTemplate | None = config.get(CONF_VALUE_TEMPLATE)
self._json_attrs = config.get(CONF_JSON_ATTRS) self._json_attrs = config.get(CONF_JSON_ATTRS)
self._json_attrs_path = config.get(CONF_JSON_ATTRS_PATH) self._json_attrs_path = config.get(CONF_JSON_ATTRS_PATH)
self._attr_extra_state_attributes = {} self._attr_extra_state_attributes = {}
@ -165,16 +166,19 @@ class RestSensor(ManualTriggerSensorEntity, RestEntity):
) )
value = self.rest.data value = self.rest.data
variables = self._template_variables_with_value(value)
if not self._render_availability_template(variables):
self.async_write_ha_state()
return
if self._json_attrs: if self._json_attrs:
self._attr_extra_state_attributes = parse_json_attributes( self._attr_extra_state_attributes = parse_json_attributes(
value, self._json_attrs, self._json_attrs_path value, self._json_attrs, self._json_attrs_path
) )
raw_value = value
if value is not None and self._value_template is not None: if value is not None and self._value_template is not None:
value = self._value_template.async_render_with_possible_json_value( value = self._value_template.async_render_as_value_template(
value, None self.entity_id, variables, None
) )
if value is None or self.device_class not in ( if value is None or self.device_class not in (
@ -182,7 +186,7 @@ class RestSensor(ManualTriggerSensorEntity, RestEntity):
SensorDeviceClass.TIMESTAMP, SensorDeviceClass.TIMESTAMP,
): ):
self._attr_native_value = value self._attr_native_value = value
self._process_manual_data(raw_value) self._process_manual_data(variables)
self.async_write_ha_state() self.async_write_ha_state()
return return
@ -190,5 +194,5 @@ class RestSensor(ManualTriggerSensorEntity, RestEntity):
value, self.entity_id, self.device_class value, self.entity_id, self.device_class
) )
self._process_manual_data(raw_value) self._process_manual_data(variables)
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -38,6 +38,7 @@ from homeassistant.helpers.trigger_template_entity import (
CONF_PICTURE, CONF_PICTURE,
TEMPLATE_ENTITY_BASE_SCHEMA, TEMPLATE_ENTITY_BASE_SCHEMA,
ManualTriggerEntity, ManualTriggerEntity,
ValueTemplate,
) )
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@ -73,7 +74,9 @@ PLATFORM_SCHEMA = SWITCH_PLATFORM_SCHEMA.extend(
vol.Optional(CONF_PARAMS): {cv.string: cv.template}, vol.Optional(CONF_PARAMS): {cv.string: cv.template},
vol.Optional(CONF_BODY_OFF, default=DEFAULT_BODY_OFF): cv.template, vol.Optional(CONF_BODY_OFF, default=DEFAULT_BODY_OFF): cv.template,
vol.Optional(CONF_BODY_ON, default=DEFAULT_BODY_ON): cv.template, vol.Optional(CONF_BODY_ON, default=DEFAULT_BODY_ON): cv.template,
vol.Optional(CONF_IS_ON_TEMPLATE): cv.template, vol.Optional(CONF_IS_ON_TEMPLATE): vol.All(
cv.template, ValueTemplate.from_template
),
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.All( vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.All(
vol.Lower, vol.In(SUPPORT_REST_METHODS) vol.Lower, vol.In(SUPPORT_REST_METHODS)
), ),
@ -107,7 +110,7 @@ async def async_setup_platform(
try: try:
switch = RestSwitch(hass, config, trigger_entity_config) switch = RestSwitch(hass, config, trigger_entity_config)
req = await switch.get_device_state(hass) req = await switch.get_response(hass)
if req.status_code >= HTTPStatus.BAD_REQUEST: if req.status_code >= HTTPStatus.BAD_REQUEST:
_LOGGER.error("Got non-ok response from resource: %s", req.status_code) _LOGGER.error("Got non-ok response from resource: %s", req.status_code)
else: else:
@ -147,7 +150,7 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity):
self._auth = auth self._auth = auth
self._body_on: template.Template = config[CONF_BODY_ON] self._body_on: template.Template = config[CONF_BODY_ON]
self._body_off: template.Template = config[CONF_BODY_OFF] self._body_off: template.Template = config[CONF_BODY_OFF]
self._is_on_template: template.Template | None = config.get(CONF_IS_ON_TEMPLATE) self._is_on_template: ValueTemplate | None = config.get(CONF_IS_ON_TEMPLATE)
self._timeout: int = config[CONF_TIMEOUT] self._timeout: int = config[CONF_TIMEOUT]
self._verify_ssl: bool = config[CONF_VERIFY_SSL] self._verify_ssl: bool = config[CONF_VERIFY_SSL]
@ -208,35 +211,41 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity):
"""Get the current state, catching errors.""" """Get the current state, catching errors."""
req = None req = None
try: try:
req = await self.get_device_state(self.hass) req = await self.get_response(self.hass)
except (TimeoutError, httpx.TimeoutException): except (TimeoutError, httpx.TimeoutException):
_LOGGER.exception("Timed out while fetching data") _LOGGER.exception("Timed out while fetching data")
except httpx.RequestError: except httpx.RequestError:
_LOGGER.exception("Error while fetching data") _LOGGER.exception("Error while fetching data")
if req: if req:
self._process_manual_data(req.text) self._async_update(req.text)
self.async_write_ha_state()
async def get_device_state(self, hass: HomeAssistant) -> httpx.Response: async def get_response(self, hass: HomeAssistant) -> httpx.Response:
"""Get the latest data from REST API and update the state.""" """Get the latest data from REST API and update the state."""
websession = get_async_client(hass, self._verify_ssl) websession = get_async_client(hass, self._verify_ssl)
rendered_headers = template.render_complex(self._headers, parse_result=False) rendered_headers = template.render_complex(self._headers, parse_result=False)
rendered_params = template.render_complex(self._params) rendered_params = template.render_complex(self._params)
req = await websession.get( return await websession.get(
self._state_resource, self._state_resource,
auth=self._auth, auth=self._auth,
headers=rendered_headers, headers=rendered_headers,
params=rendered_params, params=rendered_params,
timeout=self._timeout, timeout=self._timeout,
) )
text = req.text
def _async_update(self, text: str) -> None:
"""Get the latest data from REST API and update the state."""
variables = self._template_variables_with_value(text)
if not self._render_availability_template(variables):
self.async_write_ha_state()
return
if self._is_on_template is not None: if self._is_on_template is not None:
text = self._is_on_template.async_render_with_possible_json_value( text = self._is_on_template.async_render_as_value_template(
text, "None" self.entity_id, variables, "None"
) )
text = text.lower() text = text.lower()
if text == "true": if text == "true":
@ -252,4 +261,5 @@ class RestSwitch(ManualTriggerEntity, SwitchEntity):
else: else:
self._attr_is_on = None self._attr_is_on = None
return req self._process_manual_data(variables)
self.async_write_ha_state()

View File

@ -28,6 +28,7 @@ from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.trigger_template_entity import ( from homeassistant.helpers.trigger_template_entity import (
CONF_AVAILABILITY, CONF_AVAILABILITY,
TEMPLATE_SENSOR_BASE_SCHEMA, TEMPLATE_SENSOR_BASE_SCHEMA,
ValueTemplate,
) )
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -43,7 +44,9 @@ SENSOR_SCHEMA = vol.Schema(
vol.Optional(CONF_ATTRIBUTE): cv.string, vol.Optional(CONF_ATTRIBUTE): cv.string,
vol.Optional(CONF_INDEX, default=0): cv.positive_int, vol.Optional(CONF_INDEX, default=0): cv.positive_int,
vol.Required(CONF_SELECT): cv.string, vol.Required(CONF_SELECT): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
cv.template, ValueTemplate.from_template
),
} }
) )

View File

@ -25,13 +25,14 @@ from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback, AddConfigEntryEntitiesCallback,
AddEntitiesCallback, AddEntitiesCallback,
) )
from homeassistant.helpers.template import Template from homeassistant.helpers.template import _SENTINEL, Template
from homeassistant.helpers.trigger_template_entity import ( from homeassistant.helpers.trigger_template_entity import (
CONF_AVAILABILITY, CONF_AVAILABILITY,
CONF_PICTURE, CONF_PICTURE,
TEMPLATE_SENSOR_BASE_SCHEMA, TEMPLATE_SENSOR_BASE_SCHEMA,
ManualTriggerEntity, ManualTriggerEntity,
ManualTriggerSensorEntity, ManualTriggerSensorEntity,
ValueTemplate,
) )
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -110,8 +111,8 @@ async def async_setup_entry(
name: str = sensor_config[CONF_NAME] name: str = sensor_config[CONF_NAME]
value_string: str | None = sensor_config.get(CONF_VALUE_TEMPLATE) value_string: str | None = sensor_config.get(CONF_VALUE_TEMPLATE)
value_template: Template | None = ( value_template: ValueTemplate | None = (
Template(value_string, hass) if value_string is not None else None ValueTemplate(value_string, hass) if value_string is not None else None
) )
trigger_entity_config: dict[str, str | Template | None] = {CONF_NAME: name} trigger_entity_config: dict[str, str | Template | None] = {CONF_NAME: name}
@ -150,7 +151,7 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], ManualTriggerSensorEnti
select: str, select: str,
attr: str | None, attr: str | None,
index: int, index: int,
value_template: Template | None, value_template: ValueTemplate | None,
yaml: bool, yaml: bool,
) -> None: ) -> None:
"""Initialize a web scrape sensor.""" """Initialize a web scrape sensor."""
@ -161,7 +162,6 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], ManualTriggerSensorEnti
self._index = index self._index = index
self._value_template = value_template self._value_template = value_template
self._attr_native_value = None self._attr_native_value = None
self._available = True
if not yaml and (unique_id := trigger_entity_config.get(CONF_UNIQUE_ID)): if not yaml and (unique_id := trigger_entity_config.get(CONF_UNIQUE_ID)):
self._attr_name = None self._attr_name = None
self._attr_has_entity_name = True self._attr_has_entity_name = True
@ -176,7 +176,6 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], ManualTriggerSensorEnti
"""Parse the html extraction in the executor.""" """Parse the html extraction in the executor."""
raw_data = self.coordinator.data raw_data = self.coordinator.data
value: str | list[str] | None value: str | list[str] | None
self._available = True
try: try:
if self._attr is not None: if self._attr is not None:
value = raw_data.select(self._select)[self._index][self._attr] value = raw_data.select(self._select)[self._index][self._attr]
@ -188,14 +187,12 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], ManualTriggerSensorEnti
value = tag.text value = tag.text
except IndexError: except IndexError:
_LOGGER.warning("Index '%s' not found in %s", self._index, self.entity_id) _LOGGER.warning("Index '%s' not found in %s", self._index, self.entity_id)
value = None return _SENTINEL
self._available = False
except KeyError: except KeyError:
_LOGGER.warning( _LOGGER.warning(
"Attribute '%s' not found in %s", self._attr, self.entity_id "Attribute '%s' not found in %s", self._attr, self.entity_id
) )
value = None return _SENTINEL
self._available = False
_LOGGER.debug("Parsed value: %s", value) _LOGGER.debug("Parsed value: %s", value)
return value return value
@ -207,26 +204,32 @@ class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], ManualTriggerSensorEnti
def _async_update_from_rest_data(self) -> None: def _async_update_from_rest_data(self) -> None:
"""Update state from the rest data.""" """Update state from the rest data."""
value = self._extract_value() self._attr_available = True
raw_value = value if (value := self._extract_value()) is _SENTINEL:
self._attr_available = False
return
variables = self._template_variables_with_value(value)
if not self._render_availability_template(variables):
return
if (template := self._value_template) is not None: if (template := self._value_template) is not None:
value = template.async_render_with_possible_json_value(value, None) value = template.async_render_as_value_template(
self.entity_id, variables, None
)
if self.device_class not in { if self.device_class not in {
SensorDeviceClass.DATE, SensorDeviceClass.DATE,
SensorDeviceClass.TIMESTAMP, SensorDeviceClass.TIMESTAMP,
}: }:
self._attr_native_value = value self._attr_native_value = value
self._attr_available = self._available self._process_manual_data(variables)
self._process_manual_data(raw_value)
return return
self._attr_native_value = async_parse_date_datetime( self._attr_native_value = async_parse_date_datetime(
value, self.entity_id, self.device_class value, self.entity_id, self.device_class
) )
self._attr_available = self._available self._process_manual_data(variables)
self._process_manual_data(raw_value)
@property @property
def available(self) -> bool: def available(self) -> bool:

View File

@ -45,6 +45,7 @@ from homeassistant.helpers.trigger_template_entity import (
CONF_PICTURE, CONF_PICTURE,
TEMPLATE_SENSOR_BASE_SCHEMA, TEMPLATE_SENSOR_BASE_SCHEMA,
ManualTriggerSensorEntity, ManualTriggerSensorEntity,
ValueTemplate,
) )
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@ -94,7 +95,9 @@ PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
vol.Optional(CONF_DEFAULT_VALUE): cv.string, vol.Optional(CONF_DEFAULT_VALUE): cv.string,
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
cv.template, ValueTemplate.from_template
),
vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.In(SNMP_VERSIONS), vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.In(SNMP_VERSIONS),
vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_AUTH_KEY): cv.string, vol.Optional(CONF_AUTH_KEY): cv.string,
@ -173,7 +176,7 @@ async def async_setup_platform(
continue continue
trigger_entity_config[key] = config[key] trigger_entity_config[key] = config[key]
value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) value_template: ValueTemplate | None = config.get(CONF_VALUE_TEMPLATE)
data = SnmpData(request_args, baseoid, accept_errors, default_value) data = SnmpData(request_args, baseoid, accept_errors, default_value)
async_add_entities([SnmpSensor(hass, data, trigger_entity_config, value_template)]) async_add_entities([SnmpSensor(hass, data, trigger_entity_config, value_template)])
@ -189,7 +192,7 @@ class SnmpSensor(ManualTriggerSensorEntity):
hass: HomeAssistant, hass: HomeAssistant,
data: SnmpData, data: SnmpData,
config: ConfigType, config: ConfigType,
value_template: Template | None, value_template: ValueTemplate | None,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(hass, config) super().__init__(hass, config)
@ -206,17 +209,16 @@ class SnmpSensor(ManualTriggerSensorEntity):
"""Get the latest data and updates the states.""" """Get the latest data and updates the states."""
await self.data.async_update() await self.data.async_update()
raw_value = self.data.value variables = self._template_variables_with_value(self.data.value)
if (value := self.data.value) is None: if (value := self.data.value) is None:
value = STATE_UNKNOWN value = STATE_UNKNOWN
elif self._value_template is not None: elif self._value_template is not None:
value = self._value_template.async_render_with_possible_json_value( value = self._value_template.async_render_as_value_template(
value, STATE_UNKNOWN self.entity_id, variables, STATE_UNKNOWN
) )
self._attr_native_value = value self._attr_native_value = value
self._process_manual_data(raw_value) self._process_manual_data(variables)
class SnmpData: class SnmpData:

View File

@ -28,6 +28,7 @@ from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.trigger_template_entity import ( from homeassistant.helpers.trigger_template_entity import (
CONF_AVAILABILITY, CONF_AVAILABILITY,
CONF_PICTURE, CONF_PICTURE,
ValueTemplate,
) )
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -55,7 +56,9 @@ QUERY_SCHEMA = vol.Schema(
vol.Required(CONF_NAME): cv.template, vol.Required(CONF_NAME): cv.template,
vol.Required(CONF_QUERY): vol.All(cv.string, validate_sql_select), vol.Required(CONF_QUERY): vol.All(cv.string, validate_sql_select),
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
cv.template, ValueTemplate.from_template
),
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DB_URL): cv.string, vol.Optional(CONF_DB_URL): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,

View File

@ -45,6 +45,7 @@ from homeassistant.helpers.trigger_template_entity import (
CONF_AVAILABILITY, CONF_AVAILABILITY,
CONF_PICTURE, CONF_PICTURE,
ManualTriggerSensorEntity, ManualTriggerSensorEntity,
ValueTemplate,
) )
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@ -79,7 +80,7 @@ async def async_setup_platform(
name: Template = conf[CONF_NAME] name: Template = conf[CONF_NAME]
query_str: str = conf[CONF_QUERY] query_str: str = conf[CONF_QUERY]
value_template: Template | None = conf.get(CONF_VALUE_TEMPLATE) value_template: ValueTemplate | None = conf.get(CONF_VALUE_TEMPLATE)
column_name: str = conf[CONF_COLUMN_NAME] column_name: str = conf[CONF_COLUMN_NAME]
unique_id: str | None = conf.get(CONF_UNIQUE_ID) unique_id: str | None = conf.get(CONF_UNIQUE_ID)
db_url: str = resolve_db_url(hass, conf.get(CONF_DB_URL)) db_url: str = resolve_db_url(hass, conf.get(CONF_DB_URL))
@ -116,10 +117,10 @@ async def async_setup_entry(
template: str | None = entry.options.get(CONF_VALUE_TEMPLATE) template: str | None = entry.options.get(CONF_VALUE_TEMPLATE)
column_name: str = entry.options[CONF_COLUMN_NAME] column_name: str = entry.options[CONF_COLUMN_NAME]
value_template: Template | None = None value_template: ValueTemplate | None = None
if template is not None: if template is not None:
try: try:
value_template = Template(template, hass) value_template = ValueTemplate(template, hass)
value_template.ensure_valid() value_template.ensure_valid()
except TemplateError: except TemplateError:
value_template = None value_template = None
@ -179,7 +180,7 @@ async def async_setup_sensor(
trigger_entity_config: ConfigType, trigger_entity_config: ConfigType,
query_str: str, query_str: str,
column_name: str, column_name: str,
value_template: Template | None, value_template: ValueTemplate | None,
unique_id: str | None, unique_id: str | None,
db_url: str, db_url: str,
yaml: bool, yaml: bool,
@ -316,7 +317,7 @@ class SQLSensor(ManualTriggerSensorEntity):
sessmaker: scoped_session, sessmaker: scoped_session,
query: str, query: str,
column: str, column: str,
value_template: Template | None, value_template: ValueTemplate | None,
yaml: bool, yaml: bool,
use_database_executor: bool, use_database_executor: bool,
) -> None: ) -> None:
@ -359,14 +360,14 @@ class SQLSensor(ManualTriggerSensorEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Retrieve sensor data from the query using the right executor.""" """Retrieve sensor data from the query using the right executor."""
if self._use_database_executor: if self._use_database_executor:
data = await get_instance(self.hass).async_add_executor_job(self._update) await get_instance(self.hass).async_add_executor_job(self._update)
else: else:
data = await self.hass.async_add_executor_job(self._update) await self.hass.async_add_executor_job(self._update)
self._process_manual_data(data)
def _update(self) -> Any: def _update(self) -> None:
"""Retrieve sensor data from the query.""" """Retrieve sensor data from the query."""
data = None data = None
extra_state_attributes = {}
self._attr_extra_state_attributes = {} self._attr_extra_state_attributes = {}
sess: scoped_session = self.sessionmaker() sess: scoped_session = self.sessionmaker()
try: try:
@ -379,7 +380,7 @@ class SQLSensor(ManualTriggerSensorEntity):
) )
sess.rollback() sess.rollback()
sess.close() sess.close()
return None return
for res in result.mappings(): for res in result.mappings():
_LOGGER.debug("Query %s result in %s", self._query, res.items()) _LOGGER.debug("Query %s result in %s", self._query, res.items())
@ -391,15 +392,19 @@ class SQLSensor(ManualTriggerSensorEntity):
value = value.isoformat() value = value.isoformat()
elif isinstance(value, (bytes, bytearray)): elif isinstance(value, (bytes, bytearray)):
value = f"0x{value.hex()}" value = f"0x{value.hex()}"
extra_state_attributes[key] = value
self._attr_extra_state_attributes[key] = value self._attr_extra_state_attributes[key] = value
if data is not None and isinstance(data, (bytes, bytearray)): if data is not None and isinstance(data, (bytes, bytearray)):
data = f"0x{data.hex()}" data = f"0x{data.hex()}"
if data is not None and self._template is not None: if data is not None and self._template is not None:
self._attr_native_value = ( variables = self._template_variables_with_value(data)
self._template.async_render_with_possible_json_value(data, None) if self._render_availability_template(variables):
) self._attr_native_value = self._template.async_render_as_value_template(
self.entity_id, variables, None
)
self._process_manual_data(variables)
else: else:
self._attr_native_value = data self._attr_native_value = data
@ -407,4 +412,3 @@ class SQLSensor(ManualTriggerSensorEntity):
_LOGGER.warning("%s returned no results", self._query) _LOGGER.warning("%s returned no results", self._query)
sess.close() sess.close()
return data

View File

@ -2,8 +2,11 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
from homeassistant.const import CONF_STATE
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.template import TemplateStateFromEntityId from homeassistant.helpers.template import _SENTINEL
from homeassistant.helpers.trigger_template_entity import TriggerBaseEntity from homeassistant.helpers.trigger_template_entity import TriggerBaseEntity
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -29,6 +32,8 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
TriggerBaseEntity.__init__(self, hass, config) TriggerBaseEntity.__init__(self, hass, config)
AbstractTemplateEntity.__init__(self, hass) AbstractTemplateEntity.__init__(self, hass)
self._state_render_error = False
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Handle being added to Home Assistant.""" """Handle being added to Home Assistant."""
await super().async_added_to_hass() await super().async_added_to_hass()
@ -47,22 +52,47 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
"""Return referenced blueprint or None.""" """Return referenced blueprint or None."""
return self.coordinator.referenced_blueprint return self.coordinator.referenced_blueprint
@property
def available(self) -> bool:
"""Return availability of the entity."""
if self._state_render_error:
return False
return super().available
@callback @callback
def _render_script_variables(self) -> dict: def _render_script_variables(self) -> dict:
"""Render configured variables.""" """Render configured variables."""
return self.coordinator.data["run_variables"] return self.coordinator.data["run_variables"]
def _render_templates(self, variables: dict[str, Any]) -> None:
"""Render templates."""
self._state_render_error = False
rendered = dict(self._static_rendered)
# If state fails to render, the entity should go unavailable. Render the
# state as a simple template because the result should always be a string or None.
if CONF_STATE in self._to_render_simple:
if (
result := self._render_single_template(CONF_STATE, variables)
) is _SENTINEL:
self._rendered = self._static_rendered
self._state_render_error = True
return
rendered[CONF_STATE] = result
self._render_single_templates(rendered, variables, [CONF_STATE])
self._render_attributes(rendered, variables)
self._rendered = rendered
@callback @callback
def _process_data(self) -> None: def _process_data(self) -> None:
"""Process new data.""" """Process new data."""
run_variables = self.coordinator.data["run_variables"] variables = self._template_variables(self.coordinator.data["run_variables"])
variables = { if self._render_availability_template(variables):
"this": TemplateStateFromEntityId(self.hass, self.entity_id), self._render_templates(variables)
**(run_variables or {}),
}
self._render_templates(variables)
self.async_set_context(self.coordinator.data["context"]) self.async_set_context(self.coordinator.data["context"])

View File

@ -2,10 +2,11 @@
from __future__ import annotations from __future__ import annotations
import contextlib import itertools
import logging import logging
from typing import Any from typing import Any
import jinja2
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@ -30,7 +31,14 @@ from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
from . import config_validation as cv from . import config_validation as cv
from .entity import Entity from .entity import Entity
from .template import TemplateStateFromEntityId, render_complex from .template import (
_SENTINEL,
Template,
TemplateStateFromEntityId,
_render_with_context,
render_complex,
result_as_boolean,
)
from .typing import ConfigType from .typing import ConfigType
CONF_AVAILABILITY = "availability" CONF_AVAILABILITY = "availability"
@ -65,6 +73,27 @@ def make_template_entity_base_schema(default_name: str) -> vol.Schema:
) )
def log_triggered_template_error(
entity_id: str,
err: TemplateError,
key: str | None = None,
attribute: str | None = None,
) -> None:
"""Log a trigger entity template error."""
target = ""
if key:
target = f" {key}"
elif attribute:
target = f" {CONF_ATTRIBUTES}.{attribute}"
logging.getLogger(f"{__package__}.{entity_id.split('.')[0]}").error(
"Error rendering%s template for %s: %s",
target,
entity_id,
err,
)
TEMPLATE_SENSOR_BASE_SCHEMA = vol.Schema( TEMPLATE_SENSOR_BASE_SCHEMA = vol.Schema(
{ {
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
@ -74,6 +103,44 @@ TEMPLATE_SENSOR_BASE_SCHEMA = vol.Schema(
).extend(TEMPLATE_ENTITY_BASE_SCHEMA.schema) ).extend(TEMPLATE_ENTITY_BASE_SCHEMA.schema)
class ValueTemplate(Template):
"""Class to hold a value_template and manage caching and rendering it with 'value' in variables."""
@classmethod
def from_template(cls, template: Template) -> ValueTemplate:
"""Create a ValueTemplate object from a Template object."""
return cls(template.template, template.hass)
@callback
def async_render_as_value_template(
self, entity_id: str, variables: dict[str, Any], error_value: Any
) -> Any:
"""Render template that requires 'value' and optionally 'value_json'.
Template errors will be suppressed when an error_value is supplied.
This method must be run in the event loop.
"""
self._renders += 1
if self.is_static:
return self.template
compiled = self._compiled or self._ensure_compiled()
try:
render_result = _render_with_context(
self.template, compiled, **variables
).strip()
except jinja2.TemplateError as ex:
message = f"Error parsing value for {entity_id}: {ex} (value: {variables['value']}, template: {self.template})"
logger = logging.getLogger(f"{__package__}.{entity_id.split('.')[0]}")
logger.debug(message)
return error_value
return render_result
class TriggerBaseEntity(Entity): class TriggerBaseEntity(Entity):
"""Template Base entity based on trigger data.""" """Template Base entity based on trigger data."""
@ -122,6 +189,9 @@ class TriggerBaseEntity(Entity):
self._parse_result = {CONF_AVAILABILITY} self._parse_result = {CONF_AVAILABILITY}
self._attr_device_class = config.get(CONF_DEVICE_CLASS) self._attr_device_class = config.get(CONF_DEVICE_CLASS)
self._availability_template = config.get(CONF_AVAILABILITY)
self._available = True
@property @property
def name(self) -> str | None: def name(self) -> str | None:
"""Name of the entity.""" """Name of the entity."""
@ -145,12 +215,10 @@ class TriggerBaseEntity(Entity):
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return availability of the entity.""" """Return availability of the entity."""
return ( if self._availability_template is None:
self._rendered is not self._static_rendered return True
and
# Check against False so `None` is ok return self._available
self._rendered.get(CONF_AVAILABILITY) is not False
)
@property @property
def extra_state_attributes(self) -> dict[str, Any] | None: def extra_state_attributes(self) -> dict[str, Any] | None:
@ -176,35 +244,93 @@ class TriggerBaseEntity(Entity):
extra_state_attributes[attr] = last_state.attributes[attr] extra_state_attributes[attr] = last_state.attributes[attr]
self._rendered[CONF_ATTRIBUTES] = extra_state_attributes self._rendered[CONF_ATTRIBUTES] = extra_state_attributes
def _template_variables(self, run_variables: dict[str, Any] | None = None) -> dict:
"""Render template variables."""
return {
"this": TemplateStateFromEntityId(self.hass, self.entity_id),
**(run_variables or {}),
}
def _render_single_template(
self,
key: str,
variables: dict[str, Any],
strict: bool = False,
) -> Any:
"""Render a single template."""
try:
if key in self._to_render_complex:
return render_complex(self._config[key], variables)
return self._config[key].async_render(
variables, parse_result=key in self._parse_result, strict=strict
)
except TemplateError as err:
log_triggered_template_error(self.entity_id, err, key=key)
return _SENTINEL
def _render_availability_template(self, variables: dict[str, Any]) -> bool:
"""Render availability template."""
if not self._availability_template:
return True
try:
if (
available := self._availability_template.async_render(
variables, parse_result=True, strict=True
)
) is False:
self._rendered = dict(self._static_rendered)
self._available = result_as_boolean(available)
except TemplateError as err:
# The entity will be available when an error is rendered. This
# ensures functionality is consistent between template and trigger template
# entities.
self._available = True
log_triggered_template_error(self.entity_id, err, key=CONF_AVAILABILITY)
return self._available
def _render_attributes(self, rendered: dict, variables: dict[str, Any]) -> None:
"""Render template attributes."""
if CONF_ATTRIBUTES in self._config:
attributes = {}
for attribute, attribute_template in self._config[CONF_ATTRIBUTES].items():
try:
value = render_complex(attribute_template, variables)
attributes[attribute] = value
variables.update({attribute: value})
except TemplateError as err:
log_triggered_template_error(
self.entity_id, err, attribute=attribute
)
rendered[CONF_ATTRIBUTES] = attributes
def _render_single_templates(
self,
rendered: dict,
variables: dict[str, Any],
filtered: list[str] | None = None,
) -> None:
"""Render all single templates."""
for key in itertools.chain(self._to_render_simple, self._to_render_complex):
if filtered and key in filtered:
continue
if (
result := self._render_single_template(key, variables)
) is not _SENTINEL:
rendered[key] = result
def _render_templates(self, variables: dict[str, Any]) -> None: def _render_templates(self, variables: dict[str, Any]) -> None:
"""Render templates.""" """Render templates."""
try: rendered = dict(self._static_rendered)
rendered = dict(self._static_rendered) self._render_single_templates(rendered, variables)
self._render_attributes(rendered, variables)
for key in self._to_render_simple: self._rendered = rendered
rendered[key] = self._config[key].async_render(
variables,
parse_result=key in self._parse_result,
)
for key in self._to_render_complex:
rendered[key] = render_complex(
self._config[key],
variables,
)
if CONF_ATTRIBUTES in self._config:
rendered[CONF_ATTRIBUTES] = render_complex(
self._config[CONF_ATTRIBUTES],
variables,
)
self._rendered = rendered
except TemplateError as err:
logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error(
"Error rendering %s template for %s: %s", key, self.entity_id, err
)
self._rendered = self._static_rendered
class ManualTriggerEntity(TriggerBaseEntity): class ManualTriggerEntity(TriggerBaseEntity):
@ -223,23 +349,31 @@ class ManualTriggerEntity(TriggerBaseEntity):
parse_result=CONF_NAME in self._parse_result, parse_result=CONF_NAME in self._parse_result,
) )
def _template_variables_with_value(
self, value: str | None = None
) -> dict[str, Any]:
"""Render template variables.
Implementing class should call this first in update method to render variables for templates.
Ex: variables = self._render_template_variables_with_value(payload)
"""
run_variables: dict[str, Any] = {"value": value}
# Silently try if variable is a json and store result in `value_json` if it is.
try: # noqa: SIM105 - suppress is much slower
run_variables["value_json"] = json_loads(value) # type: ignore[arg-type]
except JSON_DECODE_EXCEPTIONS:
pass
return self._template_variables(run_variables)
@callback @callback
def _process_manual_data(self, value: Any | None = None) -> None: def _process_manual_data(self, variables: dict[str, Any]) -> None:
"""Process new data manually. """Process new data manually.
Implementing class should call this last in update method to render templates. Implementing class should call this last in update method to render templates.
Ex: self._process_manual_data(payload) Ex: self._process_manual_data(variables)
""" """
run_variables: dict[str, Any] = {"value": value}
# Silently try if variable is a json and store result in `value_json` if it is.
with contextlib.suppress(*JSON_DECODE_EXCEPTIONS):
run_variables["value_json"] = json_loads(run_variables["value"])
variables = {
"this": TemplateStateFromEntityId(self.hass, self.entity_id),
**(run_variables or {}),
}
self._render_templates(variables) self._render_templates(variables)

View File

@ -331,9 +331,10 @@ async def test_updating_manually(
"name": "Test", "name": "Test",
"command": "echo 10", "command": "echo 10",
"payload_on": "1.0", "payload_on": "1.0",
"payload_off": "0", "payload_off": "0.0",
"value_template": "{{ value | multiply(0.1) }}", "value_template": "{{ value | multiply(0.1) }}",
"availability": '{{ states("sensor.input1")=="on" }}', "availability": '{{ "sensor.input1" | has_value }}',
"icon": 'mdi:{{ states("sensor.input1") }}',
} }
} }
] ]
@ -346,8 +347,7 @@ async def test_availability(
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Test availability.""" """Test availability."""
hass.states.async_set("sensor.input1", STATE_ON)
hass.states.async_set("sensor.input1", "on")
freezer.tick(timedelta(minutes=1)) freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) await hass.async_block_till_done(wait_background_tasks=True)
@ -355,8 +355,9 @@ async def test_availability(
entity_state = hass.states.get("binary_sensor.test") entity_state = hass.states.get("binary_sensor.test")
assert entity_state assert entity_state
assert entity_state.state == STATE_ON assert entity_state.state == STATE_ON
assert entity_state.attributes["icon"] == "mdi:on"
hass.states.async_set("sensor.input1", "off") hass.states.async_set("sensor.input1", STATE_UNAVAILABLE)
await hass.async_block_till_done() await hass.async_block_till_done()
with mock_asyncio_subprocess_run(b"0"): with mock_asyncio_subprocess_run(b"0"):
freezer.tick(timedelta(minutes=1)) freezer.tick(timedelta(minutes=1))
@ -366,3 +367,64 @@ async def test_availability(
entity_state = hass.states.get("binary_sensor.test") entity_state = hass.states.get("binary_sensor.test")
assert entity_state assert entity_state
assert entity_state.state == STATE_UNAVAILABLE assert entity_state.state == STATE_UNAVAILABLE
assert "icon" not in entity_state.attributes
hass.states.async_set("sensor.input1", STATE_OFF)
await hass.async_block_till_done()
with mock_asyncio_subprocess_run(b"0"):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
entity_state = hass.states.get("binary_sensor.test")
assert entity_state
assert entity_state.state == STATE_OFF
assert entity_state.attributes["icon"] == "mdi:off"
@pytest.mark.parametrize(
"get_config",
[
{
"command_line": [
{
"binary_sensor": {
"name": "Test",
"command": "echo 10",
"payload_on": "1.0",
"payload_off": "0.0",
"value_template": "{{ x - 1 }}",
"availability": "{{ value == '50' }}",
}
}
]
}
],
)
async def test_availability_blocks_value_template(
hass: HomeAssistant,
load_yaml_integration: None,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test availability blocks value_template from rendering."""
error = "Error parsing value for binary_sensor.test: 'x' is undefined"
await hass.async_block_till_done()
with mock_asyncio_subprocess_run(b"51\n"):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert error not in caplog.text
entity_state = hass.states.get("binary_sensor.test")
assert entity_state
assert entity_state.state == STATE_UNAVAILABLE
await hass.async_block_till_done()
with mock_asyncio_subprocess_run(b"50\n"):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert error in caplog.text

View File

@ -371,7 +371,9 @@ async def test_updating_manually(
"cover": { "cover": {
"command_state": "echo 10", "command_state": "echo 10",
"name": "Test", "name": "Test",
"availability": '{{ states("sensor.input1")=="on" }}', "value_template": "{{ value }}",
"availability": '{{ "sensor.input1" | has_value }}',
"icon": 'mdi:{{ states("sensor.input1") }}',
}, },
} }
] ]
@ -393,8 +395,9 @@ async def test_availability(
entity_state = hass.states.get("cover.test") entity_state = hass.states.get("cover.test")
assert entity_state assert entity_state
assert entity_state.state == CoverState.OPEN assert entity_state.state == CoverState.OPEN
assert entity_state.attributes["icon"] == "mdi:on"
hass.states.async_set("sensor.input1", "off") hass.states.async_set("sensor.input1", STATE_UNAVAILABLE)
await hass.async_block_till_done() await hass.async_block_till_done()
with mock_asyncio_subprocess_run(b"50\n"): with mock_asyncio_subprocess_run(b"50\n"):
freezer.tick(timedelta(minutes=1)) freezer.tick(timedelta(minutes=1))
@ -404,6 +407,19 @@ async def test_availability(
entity_state = hass.states.get("cover.test") entity_state = hass.states.get("cover.test")
assert entity_state assert entity_state
assert entity_state.state == STATE_UNAVAILABLE assert entity_state.state == STATE_UNAVAILABLE
assert "icon" not in entity_state.attributes
hass.states.async_set("sensor.input1", "off")
await hass.async_block_till_done()
with mock_asyncio_subprocess_run(b"25\n"):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
entity_state = hass.states.get("cover.test")
assert entity_state
assert entity_state.state == CoverState.OPEN
assert entity_state.attributes["icon"] == "mdi:off"
async def test_icon_template(hass: HomeAssistant) -> None: async def test_icon_template(hass: HomeAssistant) -> None:
@ -455,3 +471,49 @@ async def test_icon_template(hass: HomeAssistant) -> None:
entity_state = hass.states.get("cover.test") entity_state = hass.states.get("cover.test")
assert entity_state assert entity_state
assert entity_state.attributes.get("icon") == "mdi:icon2" assert entity_state.attributes.get("icon") == "mdi:icon2"
@pytest.mark.parametrize(
"get_config",
[
{
"command_line": [
{
"cover": {
"command_state": "echo 10",
"name": "Test",
"value_template": "{{ x - 1 }}",
"availability": "{{ value == '50' }}",
},
}
]
}
],
)
async def test_availability_blocks_value_template(
hass: HomeAssistant,
load_yaml_integration: None,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test availability blocks value_template from rendering."""
error = "Error parsing value for cover.test: 'x' is undefined"
await hass.async_block_till_done()
with mock_asyncio_subprocess_run(b"51\n"):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert error not in caplog.text
entity_state = hass.states.get("cover.test")
assert entity_state
assert entity_state.state == STATE_UNAVAILABLE
await hass.async_block_till_done()
with mock_asyncio_subprocess_run(b"50\n"):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert error in caplog.text

View File

@ -772,17 +772,92 @@ async def test_template_not_error_when_data_is_none(
{ {
"sensor": { "sensor": {
"name": "Test", "name": "Test",
"command": "echo January 17, 2022", "command": 'echo { \\"key\\": \\"value\\" }',
"device_class": "date", "availability": '{{ "sensor.input1" | has_value }}',
"value_template": "{{ strptime(value, '%B %d, %Y').strftime('%Y-%m-%d') }}", "icon": 'mdi:{{ states("sensor.input1") }}',
"availability": '{{ states("sensor.input1")=="on" }}', "json_attributes": ["key"],
} }
} }
] ]
} }
], ],
) )
async def test_availability( async def test_availability_json_attributes_without_value_template(
hass: HomeAssistant,
load_yaml_integration: None,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test availability."""
hass.states.async_set("sensor.input1", "on")
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
entity_state = hass.states.get("sensor.test")
assert entity_state
assert entity_state.state == "unknown"
assert entity_state.attributes["key"] == "value"
assert entity_state.attributes["icon"] == "mdi:on"
hass.states.async_set("sensor.input1", STATE_UNAVAILABLE)
await hass.async_block_till_done()
with mock_asyncio_subprocess_run(b"Not A Number"):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert "Unable to parse output as JSON" not in caplog.text
entity_state = hass.states.get("sensor.test")
assert entity_state
assert entity_state.state == STATE_UNAVAILABLE
assert "key" not in entity_state.attributes
assert "icon" not in entity_state.attributes
hass.states.async_set("sensor.input1", "on")
await hass.async_block_till_done()
with mock_asyncio_subprocess_run(b"Not A Number"):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert "Unable to parse output as JSON" in caplog.text
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
with mock_asyncio_subprocess_run(b'{ "key": "value" }'):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
entity_state = hass.states.get("sensor.test")
assert entity_state
assert entity_state.state == "unknown"
assert entity_state.attributes["key"] == "value"
assert entity_state.attributes["icon"] == "mdi:on"
@pytest.mark.parametrize(
"get_config",
[
{
"command_line": [
{
"sensor": {
"name": "Test",
"command": "echo January 17, 2022",
"device_class": "date",
"value_template": "{{ strptime(value, '%B %d, %Y').strftime('%Y-%m-%d') }}",
"availability": '{{ states("sensor.input1")=="on" }}',
"icon": "mdi:o{{ 'n' if states('sensor.input1')=='on' else 'ff' }}",
}
}
]
}
],
)
async def test_availability_with_value_template(
hass: HomeAssistant, hass: HomeAssistant,
load_yaml_integration: None, load_yaml_integration: None,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
@ -797,6 +872,7 @@ async def test_availability(
entity_state = hass.states.get("sensor.test") entity_state = hass.states.get("sensor.test")
assert entity_state assert entity_state
assert entity_state.state == "2022-01-17" assert entity_state.state == "2022-01-17"
assert entity_state.attributes["icon"] == "mdi:on"
hass.states.async_set("sensor.input1", "off") hass.states.async_set("sensor.input1", "off")
await hass.async_block_till_done() await hass.async_block_till_done()
@ -808,3 +884,141 @@ async def test_availability(
entity_state = hass.states.get("sensor.test") entity_state = hass.states.get("sensor.test")
assert entity_state assert entity_state
assert entity_state.state == STATE_UNAVAILABLE assert entity_state.state == STATE_UNAVAILABLE
assert "icon" not in entity_state.attributes
async def test_template_render_with_availability_syntax_error(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test availability template render with syntax errors."""
assert await setup.async_setup_component(
hass,
"command_line",
{
"command_line": [
{
"sensor": {
"name": "Test",
"command": "echo {{ states.sensor.input_sensor.state }}",
"availability": "{{ what_the_heck == 2 }}",
}
}
]
},
)
await hass.async_block_till_done()
hass.states.async_set("sensor.input_sensor", "1")
await hass.async_block_till_done()
# Give time for template to load
async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(minutes=1),
)
await hass.async_block_till_done(wait_background_tasks=True)
# Sensors are unknown if never triggered
state = hass.states.get("sensor.test")
assert state is not None
assert state.state == "1"
assert (
"Error rendering availability template for sensor.test: UndefinedError: 'what_the_heck' is undefined"
in caplog.text
)
@pytest.mark.parametrize(
"get_config",
[
{
"command_line": [
{
"sensor": {
"name": "Test",
"command": "echo {{ states.sensor.input_sensor.state }}",
"availability": "{{ value|is_number}}",
"unit_of_measurement": " ",
"state_class": "measurement",
}
}
]
}
],
)
async def test_command_template_render_with_availability(
hass: HomeAssistant, load_yaml_integration: None
) -> None:
"""Test command template is rendered properly with availability."""
hass.states.async_set("sensor.input_sensor", "sensor_value")
# Give time for template to load
async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(minutes=1),
)
await hass.async_block_till_done(wait_background_tasks=True)
entity_state = hass.states.get("sensor.test")
assert entity_state
assert entity_state.state == STATE_UNAVAILABLE
hass.states.async_set("sensor.input_sensor", "1")
# Give time for template to load
async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(minutes=1),
)
await hass.async_block_till_done(wait_background_tasks=True)
entity_state = hass.states.get("sensor.test")
assert entity_state
assert entity_state.state == "1"
@pytest.mark.parametrize(
"get_config",
[
{
"command_line": [
{
"sensor": {
"name": "Test",
"command": "echo 0",
"value_template": "{{ x - 1 }}",
"availability": "{{ value == '50' }}",
},
}
]
}
],
)
async def test_availability_blocks_value_template(
hass: HomeAssistant,
load_yaml_integration: None,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test availability blocks value_template from rendering."""
error = "Error parsing value for sensor.test: 'x' is undefined"
await hass.async_block_till_done()
with mock_asyncio_subprocess_run(b"51\n"):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert error not in caplog.text
entity_state = hass.states.get("sensor.test")
assert entity_state
assert entity_state.state == STATE_UNAVAILABLE
await hass.async_block_till_done()
with mock_asyncio_subprocess_run(b"50\n"):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert error in caplog.text

View File

@ -735,7 +735,9 @@ async def test_updating_manually(
"command_on": "echo 2", "command_on": "echo 2",
"command_off": "echo 3", "command_off": "echo 3",
"name": "Test", "name": "Test",
"availability": '{{ states("sensor.input1")=="on" }}', "value_template": "{{ value_json == 0 }}",
"availability": '{{ "sensor.input1" | has_value }}',
"icon": 'mdi:{{ states("sensor.input1") }}',
}, },
} }
] ]
@ -749,16 +751,17 @@ async def test_availability(
) -> None: ) -> None:
"""Test availability.""" """Test availability."""
hass.states.async_set("sensor.input1", "on") hass.states.async_set("sensor.input1", STATE_OFF)
freezer.tick(timedelta(minutes=1)) freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) await hass.async_block_till_done(wait_background_tasks=True)
entity_state = hass.states.get("switch.test") entity_state = hass.states.get("switch.test")
assert entity_state assert entity_state
assert entity_state.state == STATE_ON assert entity_state.state == STATE_OFF
assert entity_state.attributes["icon"] == "mdi:off"
hass.states.async_set("sensor.input1", "off") hass.states.async_set("sensor.input1", STATE_UNAVAILABLE)
await hass.async_block_till_done() await hass.async_block_till_done()
with mock_asyncio_subprocess_run(b"50\n"): with mock_asyncio_subprocess_run(b"50\n"):
freezer.tick(timedelta(minutes=1)) freezer.tick(timedelta(minutes=1))
@ -768,3 +771,64 @@ async def test_availability(
entity_state = hass.states.get("switch.test") entity_state = hass.states.get("switch.test")
assert entity_state assert entity_state
assert entity_state.state == STATE_UNAVAILABLE assert entity_state.state == STATE_UNAVAILABLE
assert "icon" not in entity_state.attributes
hass.states.async_set("sensor.input1", STATE_ON)
await hass.async_block_till_done()
with mock_asyncio_subprocess_run(b"0\n"):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
entity_state = hass.states.get("switch.test")
assert entity_state
assert entity_state.state == STATE_ON
assert entity_state.attributes["icon"] == "mdi:on"
@pytest.mark.parametrize(
"get_config",
[
{
"command_line": [
{
"switch": {
"command_state": "echo 1",
"command_on": "echo 2",
"command_off": "echo 3",
"name": "Test",
"value_template": "{{ x - 1 }}",
"availability": "{{ value == '50' }}",
},
}
]
}
],
)
async def test_availability_blocks_value_template(
hass: HomeAssistant,
load_yaml_integration: None,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test availability blocks value_template from rendering."""
error = "Error parsing value for switch.test: 'x' is undefined"
await hass.async_block_till_done()
with mock_asyncio_subprocess_run(b"51\n"):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert error not in caplog.text
entity_state = hass.states.get("switch.test")
assert entity_state
assert entity_state.state == STATE_UNAVAILABLE
await hass.async_block_till_done()
with mock_asyncio_subprocess_run(b"50\n"):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert error in caplog.text

View File

@ -595,3 +595,53 @@ async def test_availability_in_config(hass: HomeAssistant) -> None:
state = hass.states.get("binary_sensor.rest_binary_sensor") state = hass.states.get("binary_sensor.rest_binary_sensor")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
@respx.mock
async def test_availability_blocks_value_template(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test availability blocks value_template from rendering."""
error = "Error parsing value for binary_sensor.block_template: 'x' is undefined"
respx.get("http://localhost").respond(status_code=HTTPStatus.OK, content="51")
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: [
{
"resource": "http://localhost",
"binary_sensor": [
{
"unique_id": "block_template",
"name": "block_template",
"value_template": "{{ x - 1 }}",
"availability": "{{ value == '50' }}",
}
],
}
]
},
)
await hass.async_block_till_done()
await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
assert error not in caplog.text
state = hass.states.get("binary_sensor.block_template")
assert state
assert state.state == STATE_UNAVAILABLE
respx.clear()
respx.get("http://localhost").respond(status_code=HTTPStatus.OK, content="50")
await hass.services.async_call(
"homeassistant",
"update_entity",
{ATTR_ENTITY_ID: ["binary_sensor.block_template"]},
blocking=True,
)
await hass.async_block_till_done()
assert error in caplog.text

View File

@ -1035,22 +1035,211 @@ async def test_entity_config(
@respx.mock @respx.mock
async def test_availability_in_config(hass: HomeAssistant) -> None: async def test_availability_in_config(hass: HomeAssistant) -> None:
"""Test entity configuration.""" """Test entity configuration."""
respx.get("http://localhost").respond(
config = { status_code=HTTPStatus.OK,
SENSOR_DOMAIN: { json={
# REST configuration "state": "okay",
"platform": DOMAIN, "available": True,
"method": "GET", "name": "rest_sensor",
"resource": "http://localhost", "icon": "mdi:foo",
# Entity configuration "picture": "foo.jpg",
"availability": "{{value==1}}",
"name": "{{'REST' + ' ' + 'Sensor'}}",
}, },
} )
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: [
{
"resource": "http://localhost",
"sensor": [
{
"unique_id": "somethingunique",
"availability": "{{ value_json.available }}",
"value_template": "{{ value_json.state }}",
"name": "{{ value_json.name if value_json is defined else 'rest_sensor' }}",
"icon": "{{ value_json.icon }}",
"picture": "{{ value_json.picture }}",
}
],
}
]
},
)
await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
respx.get("http://localhost").respond(status_code=HTTPStatus.OK, text="123") state = hass.states.get("sensor.rest_sensor")
assert await async_setup_component(hass, SENSOR_DOMAIN, config) assert state.state == "okay"
assert state.attributes["friendly_name"] == "rest_sensor"
assert state.attributes["icon"] == "mdi:foo"
assert state.attributes["entity_picture"] == "foo.jpg"
respx.get("http://localhost").respond(
status_code=HTTPStatus.OK,
json={
"state": "okay",
"available": False,
"name": "unavailable",
"icon": "mdi:unavailable",
"picture": "unavailable.jpg",
},
)
await hass.services.async_call(
"homeassistant",
"update_entity",
{ATTR_ENTITY_ID: ["sensor.rest_sensor"]},
blocking=True,
)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("sensor.rest_sensor") state = hass.states.get("sensor.rest_sensor")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
assert "friendly_name" not in state.attributes
assert "icon" not in state.attributes
assert "entity_picture" not in state.attributes
@respx.mock
async def test_json_response_with_availability_syntax_error(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test availability with syntax error."""
respx.get("http://localhost").respond(
status_code=HTTPStatus.OK,
json={"heartbeatList": {"1": [{"status": 1, "ping": 21.4}]}},
)
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: [
{
"resource": "http://localhost",
"sensor": [
{
"unique_id": "complex_json",
"name": "complex_json",
"value_template": '{% set v = value_json.heartbeatList["1"][-1] %}{{ v.ping }}',
"availability": "{{ what_the_heck == 2 }}",
}
],
}
]
},
)
await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1
state = hass.states.get("sensor.complex_json")
assert state.state == "21.4"
assert (
"Error rendering availability template for sensor.complex_json: UndefinedError: 'what_the_heck' is undefined"
in caplog.text
)
@respx.mock
async def test_json_response_with_availability(hass: HomeAssistant) -> None:
"""Test availability with complex json."""
respx.get("http://localhost").respond(
status_code=HTTPStatus.OK,
json={"heartbeatList": {"1": [{"status": 1, "ping": 21.4}]}},
)
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: [
{
"resource": "http://localhost",
"sensor": [
{
"unique_id": "complex_json",
"name": "complex_json",
"value_template": '{% set v = value_json.heartbeatList["1"][-1] %}{{ v.ping }}',
"availability": '{% set v = value_json.heartbeatList["1"][-1] %}{{ v.status == 1 and is_number(v.ping) }}',
"unit_of_measurement": "ms",
"state_class": "measurement",
}
],
}
]
},
)
await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
assert len(hass.states.async_all(SENSOR_DOMAIN)) == 1
state = hass.states.get("sensor.complex_json")
assert state.state == "21.4"
respx.get("http://localhost").respond(
status_code=HTTPStatus.OK,
json={"heartbeatList": {"1": [{"status": 0, "ping": None}]}},
)
await hass.services.async_call(
"homeassistant",
"update_entity",
{ATTR_ENTITY_ID: ["sensor.complex_json"]},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("sensor.complex_json")
assert state.state == STATE_UNAVAILABLE
@respx.mock
async def test_availability_blocks_value_template(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test availability blocks value_template from rendering."""
error = "Error parsing value for sensor.block_template: 'x' is undefined"
respx.get("http://localhost").respond(status_code=HTTPStatus.OK, content="51")
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: [
{
"resource": "http://localhost",
"sensor": [
{
"unique_id": "block_template",
"name": "block_template",
"value_template": "{{ x - 1 }}",
"availability": "{{ value == '50' }}",
"unit_of_measurement": "ms",
"state_class": "measurement",
}
],
}
]
},
)
await hass.async_block_till_done()
await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
assert error not in caplog.text
state = hass.states.get("sensor.block_template")
assert state
assert state.state == STATE_UNAVAILABLE
respx.clear()
respx.get("http://localhost").respond(status_code=HTTPStatus.OK, content="50")
await hass.services.async_call(
"homeassistant",
"update_entity",
{ATTR_ENTITY_ID: ["sensor.block_template"]},
blocking=True,
)
await hass.async_block_till_done()
assert error in caplog.text

View File

@ -37,6 +37,7 @@ from homeassistant.const import (
SERVICE_TURN_ON, SERVICE_TURN_ON,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -482,3 +483,122 @@ async def test_entity_config(
ATTR_FRIENDLY_NAME: "REST Switch", ATTR_FRIENDLY_NAME: "REST Switch",
ATTR_ICON: "mdi:one_two_three", ATTR_ICON: "mdi:one_two_three",
} }
@respx.mock
async def test_availability(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test entity configuration."""
respx.get("http://localhost").respond(
status_code=HTTPStatus.OK,
json={"beer": 1},
)
assert await async_setup_component(
hass,
SWITCH_DOMAIN,
{
SWITCH_DOMAIN: {
# REST configuration
CONF_PLATFORM: DOMAIN,
CONF_METHOD: "POST",
CONF_RESOURCE: "http://localhost",
# Entity configuration
CONF_NAME: "{{'REST' + ' ' + 'Switch'}}",
"is_on_template": "{{ value_json.beer == 1 }}",
"availability": "{{ value_json.beer is defined }}",
CONF_ICON: "mdi:{{ value_json.beer }}",
CONF_PICTURE: "{{ value_json.beer }}.png",
},
},
)
await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
state = hass.states.get("switch.rest_switch")
assert state
assert state.state == STATE_ON
assert state.attributes["icon"] == "mdi:1"
assert state.attributes["entity_picture"] == "1.png"
respx.get("http://localhost").respond(
status_code=HTTPStatus.OK,
json={"x": 1},
)
await hass.services.async_call(
"homeassistant",
"update_entity",
{ATTR_ENTITY_ID: ["switch.rest_switch"]},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("switch.rest_switch")
assert state
assert state.state == STATE_UNAVAILABLE
assert "icon" not in state.attributes
assert "entity_picture" not in state.attributes
respx.get("http://localhost").respond(
status_code=HTTPStatus.OK,
json={"beer": 0},
)
await hass.services.async_call(
"homeassistant",
"update_entity",
{ATTR_ENTITY_ID: ["switch.rest_switch"]},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("switch.rest_switch")
assert state
assert state.state == STATE_OFF
assert state.attributes["icon"] == "mdi:0"
assert state.attributes["entity_picture"] == "0.png"
@respx.mock
async def test_availability_blocks_is_on_template(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test availability blocks is_on_template from rendering."""
error = "Error parsing value for switch.block_template: 'x' is undefined"
respx.get(RESOURCE).respond(status_code=HTTPStatus.OK, content="51")
config = {
SWITCH_DOMAIN: {
# REST configuration
CONF_PLATFORM: DOMAIN,
CONF_METHOD: "POST",
CONF_RESOURCE: "http://localhost",
# Entity configuration
CONF_NAME: "block_template",
"is_on_template": "{{ x - 1 }}",
"availability": "{{ value == '50' }}",
},
}
assert await async_setup_component(hass, SWITCH_DOMAIN, config)
await hass.async_block_till_done()
await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
assert error not in caplog.text
state = hass.states.get("switch.block_template")
assert state
assert state.state == STATE_UNAVAILABLE
respx.clear()
respx.get("http://localhost").respond(status_code=HTTPStatus.OK, content="50")
await hass.services.async_call(
"homeassistant",
"update_entity",
{ATTR_ENTITY_ID: ["switch.block_template"]},
blocking=True,
)
await hass.async_block_till_done()
assert error in caplog.text

View File

@ -594,6 +594,8 @@ async def test_templates_with_yaml(hass: HomeAssistant) -> None:
CONF_INDEX: 0, CONF_INDEX: 0,
CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002", CONF_UNIQUE_ID: "3699ef88-69e6-11ed-a1eb-0242ac120002",
CONF_AVAILABILITY: '{{ states("sensor.input1")=="on" }}', CONF_AVAILABILITY: '{{ states("sensor.input1")=="on" }}',
CONF_ICON: 'mdi:o{{ "n" if states("sensor.input1")=="on" else "ff" }}',
CONF_PICTURE: 'o{{ "n" if states("sensor.input1")=="on" else "ff" }}.jpg',
} }
], ],
} }
@ -613,6 +615,8 @@ async def test_availability(
state = hass.states.get("sensor.current_version") state = hass.states.get("sensor.current_version")
assert state.state == "2021.12.10" assert state.state == "2021.12.10"
assert state.attributes["icon"] == "mdi:on"
assert state.attributes["entity_picture"] == "on.jpg"
hass.states.async_set("sensor.input1", "off") hass.states.async_set("sensor.input1", "off")
await hass.async_block_till_done() await hass.async_block_till_done()
@ -623,3 +627,93 @@ async def test_availability(
state = hass.states.get("sensor.current_version") state = hass.states.get("sensor.current_version")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
assert "icon" not in state.attributes
assert "entity_picture" not in state.attributes
async def test_template_render_with_availability_syntax_error(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test availability template render with syntax errors."""
config = {
DOMAIN: [
return_integration_config(
sensors=[
{
"select": ".current-version h1",
"name": "Current version",
"unique_id": "ha_version_unique_id",
CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}",
CONF_AVAILABILITY: "{{ what_the_heck == 2 }}",
}
]
)
]
}
mocker = MockRestData("test_scrape_sensor")
with patch(
"homeassistant.components.rest.RestData",
return_value=mocker,
):
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
state = hass.states.get("sensor.current_version")
assert state.state == "2021.12.10"
assert (
"Error rendering availability template for sensor.current_version: UndefinedError: 'what_the_heck' is undefined"
in caplog.text
)
async def test_availability_blocks_value_template(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test availability blocks value_template from rendering."""
error = "Error parsing value for sensor.current_version: 'x' is undefined"
config = {
DOMAIN: [
return_integration_config(
sensors=[
{
"select": ".current-version h1",
"name": "Current version",
"unique_id": "ha_version_unique_id",
CONF_VALUE_TEMPLATE: "{{ x - 1 }}",
CONF_AVAILABILITY: '{{ states("sensor.input1")=="on" }}',
}
]
)
]
}
hass.states.async_set("sensor.input1", "off")
await hass.async_block_till_done()
mocker = MockRestData("test_scrape_sensor")
with patch(
"homeassistant.components.rest.RestData",
return_value=mocker,
):
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
assert error not in caplog.text
state = hass.states.get("sensor.current_version")
assert state
assert state.state == STATE_UNAVAILABLE
hass.states.async_set("sensor.input1", "on")
await hass.async_block_till_done()
async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(minutes=10),
)
await hass.async_block_till_done(wait_background_tasks=True)
assert error in caplog.text

View File

@ -317,6 +317,8 @@ async def test_templates_with_yaml(
state = hass.states.get("sensor.get_values_with_template") state = hass.states.get("sensor.get_values_with_template")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
assert CONF_ICON not in state.attributes
assert "entity_picture" not in state.attributes
hass.states.async_set("sensor.input1", "on") hass.states.async_set("sensor.input1", "on")
hass.states.async_set("sensor.input2", "on") hass.states.async_set("sensor.input2", "on")
@ -660,3 +662,37 @@ async def test_setup_without_recorder(hass: HomeAssistant) -> None:
state = hass.states.get("sensor.get_value") state = hass.states.get("sensor.get_value")
assert state.state == "5" assert state.state == "5"
async def test_availability_blocks_value_template(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test availability blocks value_template from rendering."""
error = "Error parsing value for sensor.get_value: 'x' is undefined"
config = YAML_CONFIG
config["sql"]["value_template"] = "{{ x - 0 }}"
config["sql"]["availability"] = '{{ states("sensor.input1")=="on" }}'
hass.states.async_set("sensor.input1", "off")
await hass.async_block_till_done()
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
assert error not in caplog.text
state = hass.states.get("sensor.get_value")
assert state
assert state.state == STATE_UNAVAILABLE
hass.states.async_set("sensor.input1", "on")
await hass.async_block_till_done()
async_fire_time_changed(
hass,
dt_util.utcnow() + timedelta(minutes=1),
)
await hass.async_block_till_done(wait_background_tasks=True)
assert error in caplog.text

View File

@ -1527,6 +1527,217 @@ async def test_trigger_entity_available(hass: HomeAssistant) -> None:
assert state.state == "unavailable" assert state.state == "unavailable"
async def test_trigger_entity_available_skips_state(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test trigger entity availability works."""
assert await async_setup_component(
hass,
"template",
{
"template": [
{
"trigger": {"platform": "event", "event_type": "test_event"},
"sensor": [
{
"name": "Never Available",
"availability": "{{ trigger and trigger.event.data.beer == 2 }}",
"state": "{{ noexist - 1 }}",
},
],
},
],
},
)
await hass.async_block_till_done()
# Sensors are unknown if never triggered
state = hass.states.get("sensor.never_available")
assert state is not None
assert state.state == STATE_UNKNOWN
hass.bus.async_fire("test_event", {"beer": 1})
await hass.async_block_till_done()
state = hass.states.get("sensor.never_available")
assert state.state == "unavailable"
assert "'noexist' is undefined" not in caplog.text
hass.bus.async_fire("test_event", {"beer": 2})
await hass.async_block_till_done()
state = hass.states.get("sensor.never_available")
assert state.state == "unavailable"
assert "'noexist' is undefined" in caplog.text
async def test_trigger_state_with_availability_syntax_error(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test trigger entity is available when attributes have syntax errors."""
assert await async_setup_component(
hass,
"template",
{
"template": [
{
"trigger": {"platform": "event", "event_type": "test_event"},
"sensor": [
{
"name": "Test Sensor",
"availability": "{{ what_the_heck == 2 }}",
"state": "{{ trigger.event.data.beer }}",
},
],
},
],
},
)
await hass.async_block_till_done()
# Sensors are unknown if never triggered
state = hass.states.get("sensor.test_sensor")
assert state is not None
assert state.state == STATE_UNKNOWN
hass.bus.async_fire("test_event", {"beer": 2})
await hass.async_block_till_done()
state = hass.states.get("sensor.test_sensor")
assert state.state == "2"
assert (
"Error rendering availability template for sensor.test_sensor: UndefinedError: 'what_the_heck' is undefined"
in caplog.text
)
async def test_trigger_available_with_attribute_syntax_error(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test trigger entity is available when attributes have syntax errors."""
assert await async_setup_component(
hass,
"template",
{
"template": [
{
"trigger": {"platform": "event", "event_type": "test_event"},
"sensor": [
{
"name": "Test Sensor",
"availability": "{{ trigger and trigger.event.data.beer == 2 }}",
"state": "{{ trigger.event.data.beer }}",
"attributes": {
"beer": "{{ trigger.event.data.beer }}",
"no_beer": "{{ sad - 1 }}",
"more_beer": "{{ beer + 1 }}",
},
},
],
},
],
},
)
await hass.async_block_till_done()
# Sensors are unknown if never triggered
state = hass.states.get("sensor.test_sensor")
assert state is not None
assert state.state == STATE_UNKNOWN
hass.bus.async_fire("test_event", {"beer": 2})
await hass.async_block_till_done()
state = hass.states.get("sensor.test_sensor")
assert state.state == "2"
assert state.attributes["beer"] == 2
assert "no_beer" not in state.attributes
assert (
"Error rendering attributes.no_beer template for sensor.test_sensor: UndefinedError: 'sad' is undefined"
in caplog.text
)
assert state.attributes["more_beer"] == 3
async def test_trigger_attribute_order(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test trigger entity attributes order."""
assert await async_setup_component(
hass,
"template",
{
"template": [
{
"trigger": {"platform": "event", "event_type": "test_event"},
"sensor": [
{
"name": "Test Sensor",
"availability": "{{ trigger and trigger.event.data.beer == 2 }}",
"state": "{{ trigger.event.data.beer }}",
"attributes": {
"beer": "{{ trigger.event.data.beer }}",
"no_beer": "{{ sad - 1 }}",
"more_beer": "{{ beer + 1 }}",
"all_the_beer": "{{ this.state | int + more_beer }}",
},
},
],
},
],
},
)
await hass.async_block_till_done()
# Sensors are unknown if never triggered
state = hass.states.get("sensor.test_sensor")
assert state is not None
assert state.state == STATE_UNKNOWN
hass.bus.async_fire("test_event", {"beer": 2})
await hass.async_block_till_done()
state = hass.states.get("sensor.test_sensor")
assert state.state == "2"
assert state.attributes["beer"] == 2
assert "no_beer" not in state.attributes
assert (
"Error rendering attributes.no_beer template for sensor.test_sensor: UndefinedError: 'sad' is undefined"
in caplog.text
)
assert state.attributes["more_beer"] == 3
assert (
"Error rendering attributes.all_the_beer template for sensor.test_sensor: ValueError: Template error: int got invalid input 'unknown' when rendering template '{{ this.state | int + more_beer }}' but no default was specified"
in caplog.text
)
hass.bus.async_fire("test_event", {"beer": 2})
await hass.async_block_till_done()
state = hass.states.get("sensor.test_sensor")
assert state.state == "2"
assert state.attributes["beer"] == 2
assert state.attributes["more_beer"] == 3
assert state.attributes["all_the_beer"] == 5
assert (
caplog.text.count(
"Error rendering attributes.no_beer template for sensor.test_sensor: UndefinedError: 'sad' is undefined"
)
== 2
)
async def test_trigger_entity_device_class_parsing_works(hass: HomeAssistant) -> None: async def test_trigger_entity_device_class_parsing_works(hass: HomeAssistant) -> None:
"""Test trigger entity device class parsing works.""" """Test trigger entity device class parsing works."""
assert await async_setup_component( assert await async_setup_component(

View File

@ -1,8 +1,28 @@
"""Test trigger template entity.""" """Test trigger template entity."""
import pytest
from homeassistant.components.template import trigger_entity from homeassistant.components.template import trigger_entity
from homeassistant.components.template.coordinator import TriggerUpdateCoordinator from homeassistant.components.template.coordinator import TriggerUpdateCoordinator
from homeassistant.const import CONF_ICON, CONF_NAME, CONF_STATE, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import template
from homeassistant.helpers.trigger_template_entity import CONF_PICTURE
_ICON_TEMPLATE = 'mdi:o{{ "n" if value=="on" else "ff" }}'
_PICTURE_TEMPLATE = '/local/picture_o{{ "n" if value=="on" else "ff" }}'
class TestEntity(trigger_entity.TriggerEntity):
"""Test entity class."""
__test__ = False
extra_template_keys = (CONF_STATE,)
@property
def state(self) -> bool | None:
"""Return extra attributes."""
return self._rendered.get(CONF_STATE)
async def test_reference_blueprints_is_none(hass: HomeAssistant) -> None: async def test_reference_blueprints_is_none(hass: HomeAssistant) -> None:
@ -11,3 +31,90 @@ async def test_reference_blueprints_is_none(hass: HomeAssistant) -> None:
entity = trigger_entity.TriggerEntity(hass, coordinator, {}) entity = trigger_entity.TriggerEntity(hass, coordinator, {})
assert entity.referenced_blueprint is None assert entity.referenced_blueprint is None
async def test_template_state(hass: HomeAssistant) -> None:
"""Test manual trigger template entity with a state."""
config = {
CONF_NAME: template.Template("test_entity", hass),
CONF_ICON: template.Template(_ICON_TEMPLATE, hass),
CONF_PICTURE: template.Template(_PICTURE_TEMPLATE, hass),
CONF_STATE: template.Template("{{ value == 'on' }}", hass),
}
coordinator = TriggerUpdateCoordinator(hass, {})
entity = TestEntity(hass, coordinator, config)
entity.entity_id = "test.entity"
coordinator._execute_update({"value": STATE_ON})
entity._handle_coordinator_update()
await hass.async_block_till_done()
assert entity.state == "True"
assert entity.icon == "mdi:on"
assert entity.entity_picture == "/local/picture_on"
coordinator._execute_update({"value": STATE_OFF})
entity._handle_coordinator_update()
await hass.async_block_till_done()
assert entity.state == "False"
assert entity.icon == "mdi:off"
assert entity.entity_picture == "/local/picture_off"
async def test_bad_template_state(hass: HomeAssistant) -> None:
"""Test manual trigger template entity with a state."""
config = {
CONF_NAME: template.Template("test_entity", hass),
CONF_ICON: template.Template(_ICON_TEMPLATE, hass),
CONF_PICTURE: template.Template(_PICTURE_TEMPLATE, hass),
CONF_STATE: template.Template("{{ x - 1 }}", hass),
}
coordinator = TriggerUpdateCoordinator(hass, {})
entity = TestEntity(hass, coordinator, config)
entity.entity_id = "test.entity"
coordinator._execute_update({"x": 1})
entity._handle_coordinator_update()
await hass.async_block_till_done()
assert entity.available is True
assert entity.state == "0"
assert entity.icon == "mdi:off"
assert entity.entity_picture == "/local/picture_off"
coordinator._execute_update({"value": STATE_OFF})
entity._handle_coordinator_update()
await hass.async_block_till_done()
assert entity.available is False
assert entity.state is None
assert entity.icon is None
assert entity.entity_picture is None
async def test_template_state_syntax_error(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test manual trigger template entity when state render fails."""
config = {
CONF_NAME: template.Template("test_entity", hass),
CONF_ICON: template.Template(_ICON_TEMPLATE, hass),
CONF_PICTURE: template.Template(_PICTURE_TEMPLATE, hass),
CONF_STATE: template.Template("{{ incorrect ", hass),
}
coordinator = TriggerUpdateCoordinator(hass, {})
entity = TestEntity(hass, coordinator, config)
entity.entity_id = "test.entity"
coordinator._execute_update({"value": STATE_ON})
entity._handle_coordinator_update()
await hass.async_block_till_done()
assert f"Error rendering {CONF_STATE} template for test.entity" in caplog.text
assert entity.state is None
assert entity.icon is None
assert entity.entity_picture is None

View File

@ -1,8 +1,82 @@
"""Test template trigger entity.""" """Test template trigger entity."""
from typing import Any
import pytest
from homeassistant.const import (
CONF_ICON,
CONF_NAME,
CONF_STATE,
CONF_UNIQUE_ID,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import template from homeassistant.helpers import template
from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity from homeassistant.helpers.trigger_template_entity import (
CONF_ATTRIBUTES,
CONF_AVAILABILITY,
CONF_PICTURE,
ManualTriggerEntity,
ValueTemplate,
)
_ICON_TEMPLATE = 'mdi:o{{ "n" if value=="on" else "ff" }}'
_PICTURE_TEMPLATE = '/local/picture_o{{ "n" if value=="on" else "ff" }}'
@pytest.mark.parametrize(
("value", "test_template", "error_value", "expected", "error"),
[
(1, "{{ value == 1 }}", None, "True", None),
(1, "1", None, "1", None),
(
1,
"{{ x - 4 }}",
None,
None,
"",
),
(
1,
"{{ x - 4 }}",
template._SENTINEL,
template._SENTINEL,
"Error parsing value for test.entity: 'x' is undefined (value: 1, template: {{ x - 4 }})",
),
],
)
async def test_value_template_object(
hass: HomeAssistant,
value: Any,
test_template: str,
error_value: Any,
expected: Any,
error: str | None,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test ValueTemplate object."""
entity = ManualTriggerEntity(
hass,
{
CONF_NAME: template.Template("test_entity", hass),
},
)
entity.entity_id = "test.entity"
value_template = ValueTemplate.from_template(template.Template(test_template, hass))
variables = entity._template_variables_with_value(value)
result = value_template.async_render_as_value_template(
entity.entity_id, variables, error_value
)
assert result == expected
if error is not None:
assert error in caplog.text
async def test_template_entity_requires_hass_set(hass: HomeAssistant) -> None: async def test_template_entity_requires_hass_set(hass: HomeAssistant) -> None:
@ -20,21 +94,197 @@ async def test_template_entity_requires_hass_set(hass: HomeAssistant) -> None:
entity = ManualTriggerEntity(hass, config) entity = ManualTriggerEntity(hass, config)
entity.entity_id = "test.entity" entity.entity_id = "test.entity"
hass.states.async_set("test.entity", "on") hass.states.async_set("test.entity", STATE_ON)
await entity.async_added_to_hass() await entity.async_added_to_hass()
entity._process_manual_data("on") variables = entity._template_variables_with_value(STATE_ON)
entity._process_manual_data(variables)
await hass.async_block_till_done() await hass.async_block_till_done()
assert entity.name == "test_entity" assert entity.name == "test_entity"
assert entity.icon == "mdi:on" assert entity.icon == "mdi:on"
assert entity.entity_picture == "/local/picture_on" assert entity.entity_picture == "/local/picture_on"
hass.states.async_set("test.entity", "off") hass.states.async_set("test.entity", STATE_OFF)
await entity.async_added_to_hass() await entity.async_added_to_hass()
entity._process_manual_data("off")
variables = entity._template_variables_with_value(STATE_OFF)
entity._process_manual_data(variables)
await hass.async_block_till_done() await hass.async_block_till_done()
assert entity.name == "test_entity" assert entity.name == "test_entity"
assert entity.icon == "mdi:off" assert entity.icon == "mdi:off"
assert entity.entity_picture == "/local/picture_off" assert entity.entity_picture == "/local/picture_off"
@pytest.mark.parametrize(
("test_template", "test_entity_state", "expected"),
[
('{{ has_value("test.entity") }}', STATE_ON, True),
('{{ has_value("test.entity") }}', STATE_OFF, True),
('{{ has_value("test.entity") }}', STATE_UNKNOWN, False),
('{{ "a" if has_value("test.entity") else "b" }}', STATE_ON, False),
('{{ "something_not_boolean" }}', STATE_OFF, False),
("{{ 1 }}", STATE_OFF, True),
("{{ 0 }}", STATE_OFF, False),
],
)
async def test_trigger_template_availability(
hass: HomeAssistant,
test_template: str,
test_entity_state: str,
expected: bool,
) -> None:
"""Test manual trigger template entity availability template."""
config = {
CONF_NAME: template.Template("test_entity", hass),
CONF_AVAILABILITY: template.Template(test_template, hass),
CONF_UNIQUE_ID: "9961786c-f8c8-4ea0-ab1d-b9e922c39088",
}
entity = ManualTriggerEntity(hass, config)
entity.entity_id = "test.entity"
hass.states.async_set("test.entity", test_entity_state)
await entity.async_added_to_hass()
variables = entity._template_variables()
assert entity._render_availability_template(variables) is expected
await hass.async_block_till_done()
assert entity.unique_id == "9961786c-f8c8-4ea0-ab1d-b9e922c39088"
assert entity.available is expected
async def test_trigger_no_availability_template(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test manual trigger template entity when availability template isn't used."""
config = {
CONF_NAME: template.Template("test_entity", hass),
CONF_ICON: template.Template(_ICON_TEMPLATE, hass),
CONF_PICTURE: template.Template(_PICTURE_TEMPLATE, hass),
CONF_STATE: template.Template("{{ value == 'on' }}", hass),
}
class TestEntity(ManualTriggerEntity):
"""Test entity class."""
extra_template_keys = (CONF_STATE,)
@property
def state(self) -> bool | None:
"""Return extra attributes."""
return self._rendered.get(CONF_STATE)
entity = TestEntity(hass, config)
entity.entity_id = "test.entity"
variables = entity._template_variables_with_value(STATE_ON)
assert entity._render_availability_template(variables) is True
assert entity.available is True
entity._process_manual_data(variables)
await hass.async_block_till_done()
assert entity.state == "True"
assert entity.icon == "mdi:on"
assert entity.entity_picture == "/local/picture_on"
variables = entity._template_variables_with_value(STATE_OFF)
assert entity._render_availability_template(variables) is True
assert entity.available is True
entity._process_manual_data(variables)
await hass.async_block_till_done()
assert entity.state == "False"
assert entity.icon == "mdi:off"
assert entity.entity_picture == "/local/picture_off"
async def test_trigger_template_availability_with_syntax_error(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test manual trigger template entity when availability render fails."""
config = {
CONF_NAME: template.Template("test_entity", hass),
CONF_AVAILABILITY: template.Template("{{ incorrect ", hass),
}
entity = ManualTriggerEntity(hass, config)
entity.entity_id = "test.entity"
variables = entity._template_variables()
entity._render_availability_template(variables)
assert entity.available is True
assert "Error rendering availability template for test.entity" in caplog.text
async def test_attribute_order(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test manual trigger template entity when availability render fails."""
config = {
CONF_NAME: template.Template("test_entity", hass),
CONF_ATTRIBUTES: {
"beer": template.Template("{{ value }}", hass),
"no_beer": template.Template("{{ sad - 1 }}", hass),
"more_beer": template.Template("{{ beer + 1 }}", hass),
},
}
entity = ManualTriggerEntity(hass, config)
entity.entity_id = "test.entity"
hass.states.async_set("test.entity", STATE_ON)
await entity.async_added_to_hass()
variables = entity._template_variables_with_value(1)
entity._process_manual_data(variables)
await hass.async_block_till_done()
assert entity.extra_state_attributes == {"beer": 1, "more_beer": 2}
assert (
"Error rendering attributes.no_beer template for test.entity: UndefinedError: 'sad' is undefined"
in caplog.text
)
async def test_trigger_template_complex(hass: HomeAssistant) -> None:
"""Test manual trigger template entity complex template."""
complex_template = """
{% set d = {'test_key':'test_data'} %}
{{ dict(d) }}
"""
config = {
CONF_NAME: template.Template("test_entity", hass),
CONF_ICON: template.Template(
'{% if value=="on" %} mdi:on {% else %} mdi:off {% endif %}', hass
),
CONF_PICTURE: template.Template(
'{% if value=="on" %} /local/picture_on {% else %} /local/picture_off {% endif %}',
hass,
),
CONF_AVAILABILITY: template.Template('{{ has_value("test.entity") }}', hass),
"other_key": template.Template(complex_template, hass),
}
class TestEntity(ManualTriggerEntity):
"""Test entity class."""
extra_template_keys_complex = ("other_key",)
@property
def some_other_key(self) -> dict[str, Any] | None:
"""Return extra attributes."""
return self._rendered.get("other_key")
entity = TestEntity(hass, config)
entity.entity_id = "test.entity"
hass.states.async_set("test.entity", STATE_ON)
await entity.async_added_to_hass()
variables = entity._template_variables_with_value(STATE_ON)
entity._process_manual_data(variables)
await hass.async_block_till_done()
assert entity.some_other_key == {"test_key": "test_data"}