Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions homeassistant/components/template/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import logging
from typing import Optional

from homeassistant.const import CONF_SENSORS, EVENT_HOMEASSISTANT_START
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.core import CoreState, callback
from homeassistant.helpers import (
discovery,
Expand Down Expand Up @@ -51,15 +52,16 @@ async def async_setup(self, hass_config):
EVENT_HOMEASSISTANT_START, self._attach_triggers
)

self.hass.async_create_task(
discovery.async_load_platform(
self.hass,
"sensor",
DOMAIN,
{"coordinator": self, "entities": self.config[CONF_SENSORS]},
hass_config,
for platform_domain in (SENSOR_DOMAIN,):
self.hass.async_create_task(
discovery.async_load_platform(
self.hass,
platform_domain,
DOMAIN,
{"coordinator": self, "entities": self.config[platform_domain]},
hass_config,
)
)
)

async def _attach_triggers(self, start_event=None) -> None:
"""Attach the triggers."""
Expand Down
91 changes: 84 additions & 7 deletions homeassistant/components/template/config.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,72 @@
"""Template config validator."""
import logging

import voluptuous as vol

from homeassistant.components.sensor import (
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
DOMAIN as SENSOR_DOMAIN,
)
from homeassistant.config import async_log_exception, config_without_domain
from homeassistant.const import CONF_SENSORS, CONF_UNIQUE_ID
from homeassistant.helpers import config_validation as cv
from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_ENTITY_PICTURE_TEMPLATE,
CONF_FRIENDLY_NAME,
CONF_FRIENDLY_NAME_TEMPLATE,
CONF_ICON,
CONF_ICON_TEMPLATE,
CONF_NAME,
CONF_SENSORS,
CONF_STATE,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
)
from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.trigger import async_validate_trigger_config

from .const import CONF_TRIGGER, DOMAIN
from .sensor import SENSOR_SCHEMA
from .const import (
CONF_ATTRIBUTE_TEMPLATES,
CONF_ATTRIBUTES,
CONF_AVAILABILITY,
CONF_AVAILABILITY_TEMPLATE,
CONF_PICTURE,
CONF_TRIGGER,
DOMAIN,
)
from .sensor import SENSOR_SCHEMA as PLATFORM_SENSOR_SCHEMA

CONVERSION_PLATFORM = {
CONF_ICON_TEMPLATE: CONF_ICON,
CONF_ENTITY_PICTURE_TEMPLATE: CONF_PICTURE,
CONF_AVAILABILITY_TEMPLATE: CONF_AVAILABILITY,
CONF_ATTRIBUTE_TEMPLATES: CONF_ATTRIBUTES,
CONF_FRIENDLY_NAME_TEMPLATE: CONF_NAME,
CONF_FRIENDLY_NAME: CONF_NAME,
CONF_VALUE_TEMPLATE: CONF_STATE,
}

CONF_STATE = "state"

SENSOR_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME): cv.template,
vol.Required(CONF_STATE): cv.template,
vol.Optional(CONF_ICON): cv.template,
vol.Optional(CONF_PICTURE): cv.template,
vol.Optional(CONF_AVAILABILITY): cv.template,
vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}),
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)

TRIGGER_ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA),
vol.Optional(SENSOR_DOMAIN): vol.All(cv.ensure_list, [SENSOR_SCHEMA]),
vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys(PLATFORM_SENSOR_SCHEMA),
}
)

Expand All @@ -37,9 +86,37 @@ async def async_validate_config(hass, config):
)
except vol.Invalid as err:
async_log_exception(err, DOMAIN, cfg, hass)
continue

else:
if CONF_SENSORS not in cfg:
trigger_entity_configs.append(cfg)
continue

logging.getLogger(__name__).warning(
"The entity definition format under template: differs from the platform configuration format. See https://www.home-assistant.io/integrations/template#configuration-for-trigger-based-template-sensors"
)
sensor = list(cfg[SENSOR_DOMAIN]) if SENSOR_DOMAIN in cfg else []

for device_id, entity_cfg in cfg[CONF_SENSORS].items():
entity_cfg = {**entity_cfg}

for from_key, to_key in CONVERSION_PLATFORM.items():
if from_key not in entity_cfg or to_key in entity_cfg:
continue

val = entity_cfg.pop(from_key)
if isinstance(val, str):
val = template.Template(val)
entity_cfg[to_key] = val

if CONF_NAME not in entity_cfg:
entity_cfg[CONF_NAME] = template.Template(device_id)

sensor.append(entity_cfg)

cfg = {**cfg, "sensor": sensor}

trigger_entity_configs.append(cfg)

# Create a copy of the configuration with all config for current
# component removed and add validated config back in.
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/template/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@
"vacuum",
"weather",
]

CONF_AVAILABILITY = "availability"
CONF_ATTRIBUTES = "attributes"
CONF_PICTURE = "picture"
11 changes: 6 additions & 5 deletions homeassistant/components/template/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
CONF_FRIENDLY_NAME_TEMPLATE,
CONF_ICON_TEMPLATE,
CONF_SENSORS,
CONF_STATE,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
Expand Down Expand Up @@ -89,7 +90,7 @@ def _async_create_template_tracking_entities(hass, config):
friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE)
unit_of_measurement = device_config.get(CONF_UNIT_OF_MEASUREMENT)
device_class = device_config.get(CONF_DEVICE_CLASS)
attribute_templates = device_config[CONF_ATTRIBUTE_TEMPLATES]
attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {})
unique_id = device_config.get(CONF_UNIQUE_ID)

sensors.append(
Expand Down Expand Up @@ -118,8 +119,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
async_add_entities(_async_create_template_tracking_entities(hass, config))
else:
async_add_entities(
TriggerSensorEntity(hass, discovery_info["coordinator"], device_id, config)
for device_id, config in discovery_info["entities"].items()
TriggerSensorEntity(hass, discovery_info["coordinator"], config)
for config in discovery_info["entities"]
)


Expand Down Expand Up @@ -203,9 +204,9 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity):
"""Sensor entity based on trigger data."""

domain = SENSOR_DOMAIN
extra_template_keys = (CONF_VALUE_TEMPLATE,)
extra_template_keys = (CONF_STATE,)

@property
def state(self) -> str | None:
"""Return state of the sensor."""
return self._rendered.get(CONF_VALUE_TEMPLATE)
return self._rendered.get(CONF_STATE)
79 changes: 32 additions & 47 deletions homeassistant/components/template/trigger_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,16 @@

from homeassistant.const import (
CONF_DEVICE_CLASS,
CONF_ENTITY_PICTURE_TEMPLATE,
CONF_FRIENDLY_NAME,
CONF_FRIENDLY_NAME_TEMPLATE,
CONF_ICON_TEMPLATE,
CONF_ICON,
CONF_NAME,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import template, update_coordinator
from homeassistant.helpers.entity import async_generate_entity_id

from . import TriggerUpdateCoordinator
from .const import CONF_ATTRIBUTE_TEMPLATES, CONF_AVAILABILITY_TEMPLATE
from .const import CONF_ATTRIBUTES, CONF_AVAILABILITY, CONF_PICTURE


class TriggerEntity(update_coordinator.CoordinatorEntity):
Expand All @@ -32,56 +28,47 @@ def __init__(
self,
hass: HomeAssistant,
coordinator: TriggerUpdateCoordinator,
device_id: str,
config: dict,
):
"""Initialize the entity."""
super().__init__(coordinator)

self.entity_id = async_generate_entity_id(
self.domain + ".{}", device_id, hass=hass
)

self._name = config.get(CONF_FRIENDLY_NAME, device_id)

entity_unique_id = config.get(CONF_UNIQUE_ID)

if entity_unique_id is None and coordinator.unique_id:
entity_unique_id = device_id

if entity_unique_id and coordinator.unique_id:
self._unique_id = f"{coordinator.unique_id}-{entity_unique_id}"
else:
self._unique_id = entity_unique_id

self._config = config

self._to_render = [
itm
for itm in (
CONF_VALUE_TEMPLATE,
CONF_ICON_TEMPLATE,
CONF_ENTITY_PICTURE_TEMPLATE,
CONF_FRIENDLY_NAME_TEMPLATE,
CONF_AVAILABILITY_TEMPLATE,
)
if itm in config
]
self._static_rendered = {}
self._to_render = []

for itm in (
CONF_NAME,
CONF_ICON,
CONF_PICTURE,
CONF_AVAILABILITY,
):
if itm not in config:
continue

if config[itm].is_static:
self._static_rendered[itm] = config[itm].template
else:
self._to_render.append(itm)

if self.extra_template_keys is not None:
self._to_render.extend(self.extra_template_keys)

self._rendered = {}
# We make a copy so our initial render is 'unknown' and not 'unavailable'
self._rendered = dict(self._static_rendered)

@property
def name(self):
"""Name of the entity."""
if (
self._rendered is not None
and (name := self._rendered.get(CONF_FRIENDLY_NAME_TEMPLATE)) is not None
):
return name
return self._name
return self._rendered.get(CONF_NAME)

@property
def unique_id(self):
Expand All @@ -101,29 +88,27 @@ def unit_of_measurement(self) -> str | None:
@property
def icon(self) -> str | None:
"""Return icon."""
return self._rendered is not None and self._rendered.get(CONF_ICON_TEMPLATE)
return self._rendered.get(CONF_ICON)

@property
def entity_picture(self) -> str | None:
"""Return entity picture."""
return self._rendered is not None and self._rendered.get(
CONF_ENTITY_PICTURE_TEMPLATE
)
return self._rendered.get(CONF_PICTURE)

@property
def available(self):
"""Return availability of the entity."""
return (
self._rendered is not None
self._rendered is not self._static_rendered
and
# Check against False so `None` is ok
self._rendered.get(CONF_AVAILABILITY_TEMPLATE) is not False
self._rendered.get(CONF_AVAILABILITY) is not False
)

@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return extra attributes."""
return self._rendered.get(CONF_ATTRIBUTE_TEMPLATES)
return self._rendered.get(CONF_ATTRIBUTES)

async def async_added_to_hass(self) -> None:
"""Handle being added to Home Assistant."""
Expand All @@ -136,16 +121,16 @@ async def async_added_to_hass(self) -> None:
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
try:
rendered = {}
rendered = dict(self._static_rendered)

for key in self._to_render:
rendered[key] = self._config[key].async_render(
self.coordinator.data["run_variables"], parse_result=False
)

if CONF_ATTRIBUTE_TEMPLATES in self._config:
rendered[CONF_ATTRIBUTE_TEMPLATES] = template.render_complex(
self._config[CONF_ATTRIBUTE_TEMPLATES],
if CONF_ATTRIBUTES in self._config:
rendered[CONF_ATTRIBUTES] = template.render_complex(
self._config[CONF_ATTRIBUTES],
self.coordinator.data["run_variables"],
)

Expand All @@ -154,7 +139,7 @@ def _handle_coordinator_update(self) -> None:
logging.getLogger(f"{__package__}.{self.entity_id.split('.')[0]}").error(
"Error rendering %s template for %s: %s", key, self.entity_id, err
)
self._rendered = None
self._rendered = self._static_rendered

self.async_set_context(self.coordinator.data["context"])
self.async_write_ha_state()
4 changes: 2 additions & 2 deletions homeassistant/helpers/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ def render(
If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine.
"""
if self.is_static:
if self.hass.config.legacy_templates or not parse_result:
if not parse_result or self.hass.config.legacy_templates:
return self.template
return self._parse_result(self.template)

Expand All @@ -360,7 +360,7 @@ def async_render(
If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine.
"""
if self.is_static:
if self.hass.config.legacy_templates or not parse_result:
if not parse_result or self.hass.config.legacy_templates:
return self.template
return self._parse_result(self.template)

Expand Down
Loading