Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Internally work with modern config syntax for template binary sensor platform config #48981

Merged
merged 6 commits into from
Apr 13, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
173 changes: 135 additions & 38 deletions homeassistant/components/template/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Support for exposing a templated binary sensor."""
from __future__ import annotations

import voluptuous as vol

from homeassistant.components.binary_sensor import (
Expand All @@ -12,26 +14,64 @@
ATTR_FRIENDLY_NAME,
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.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.template import result_as_boolean

from .const import CONF_AVAILABILITY_TEMPLATE
from .const import (
CONF_ATTRIBUTES,
CONF_AVAILABILITY,
CONF_AVAILABILITY_TEMPLATE,
CONF_OBJECT_ID,
CONF_PICTURE,
)
from .template_entity import TemplateEntity

CONF_DELAY_ON = "delay_on"
CONF_DELAY_OFF = "delay_off"
CONF_ATTRIBUTE_TEMPLATES = "attribute_templates"

SENSOR_SCHEMA = vol.All(
LEGACY_FIELDS = {
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,
}

BINARY_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): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DELAY_ON): vol.Any(cv.positive_time_period, cv.template),
vol.Optional(CONF_DELAY_OFF): vol.Any(cv.positive_time_period, cv.template),
}
)

LEGACY_BINARY_SENSOR_SCHEMA = vol.All(
cv.deprecated(ATTR_ENTITY_ID),
vol.Schema(
{
Expand All @@ -52,70 +92,104 @@
),
)


def rewrite_legacy_to_modern_conf(cfg: dict[str, dict]) -> list[dict]:
"""Rewrite legacy binary sensor definitions to modern ones."""
sensors = []

for object_id, entity_cfg in cfg.items():
entity_cfg = {**entity_cfg, CONF_OBJECT_ID: object_id}

for from_key, to_key in LEGACY_FIELDS.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(object_id)

sensors.append(entity_cfg)

return sensors


PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA)}
{
vol.Optional(CONF_SENSORS): cv.schema_with_slug_keys(
balloob marked this conversation as resolved.
Show resolved Hide resolved
LEGACY_BINARY_SENSOR_SCHEMA
),
}
)


async def _async_create_entities(hass, config):
@callback
def _async_create_template_tracking_entities(async_add_entities, hass, definitions):
"""Create the template binary sensors."""
sensors = []

for device, device_config in config[CONF_SENSORS].items():
value_template = device_config[CONF_VALUE_TEMPLATE]
icon_template = device_config.get(CONF_ICON_TEMPLATE)
entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE)
availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE)
attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {})
for entity_conf in definitions:
# Still available on legacy
object_id = entity_conf.get(CONF_OBJECT_ID)

friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
device_class = device_config.get(CONF_DEVICE_CLASS)
delay_on_raw = device_config.get(CONF_DELAY_ON)
delay_off_raw = device_config.get(CONF_DELAY_OFF)
unique_id = device_config.get(CONF_UNIQUE_ID)
value = entity_conf[CONF_STATE]
icon = entity_conf.get(CONF_ICON)
entity_picture = entity_conf.get(CONF_PICTURE)
availability = entity_conf.get(CONF_AVAILABILITY)
attributes = entity_conf.get(CONF_ATTRIBUTES, {})
friendly_name = entity_conf.get(CONF_NAME)
device_class = entity_conf.get(CONF_DEVICE_CLASS)
delay_on_raw = entity_conf.get(CONF_DELAY_ON)
delay_off_raw = entity_conf.get(CONF_DELAY_OFF)
unique_id = entity_conf.get(CONF_UNIQUE_ID)

sensors.append(
BinarySensorTemplate(
hass,
device,
object_id,
friendly_name,
device_class,
value_template,
icon_template,
entity_picture_template,
availability_template,
value,
icon,
entity_picture,
availability,
delay_on_raw,
delay_off_raw,
attribute_templates,
attributes,
unique_id,
)
)

return sensors
async_add_entities(sensors)


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the template binary sensors."""
async_add_entities(await _async_create_entities(hass, config))
_async_create_template_tracking_entities(
async_add_entities, hass, rewrite_legacy_to_modern_conf(config[CONF_SENSORS])
)


class BinarySensorTemplate(TemplateEntity, BinarySensorEntity):
"""A virtual binary sensor that triggers from another sensor."""

def __init__(
self,
hass,
device,
friendly_name,
device_class,
value_template,
icon_template,
entity_picture_template,
availability_template,
hass: HomeAssistant,
object_id: str | None,
friendly_name: template.Template | None,
device_class: str,
value_template: template.Template,
icon_template: template.Template | None,
entity_picture_template: template.Template | None,
availability_template: template.Template | None,
delay_on_raw,
delay_off_raw,
attribute_templates,
unique_id,
attribute_templates: dict[str, template.Template],
unique_id: str | None,
):
"""Initialize the Template binary sensor."""
super().__init__(
Expand All @@ -124,8 +198,22 @@ def __init__(
icon_template=icon_template,
entity_picture_template=entity_picture_template,
)
self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device, hass=hass)
self._name = friendly_name
if object_id is not None:
self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, object_id, hass=hass
)

self._name: str | None = None
self._friendly_name_template: template.Template | None = friendly_name

# Try to render the name as it can influence the entity ID
if friendly_name:
friendly_name.hass = hass
try:
self._name = friendly_name.async_render(parse_result=False)
except template.TemplateError:
pass

self._device_class = device_class
self._template = value_template
self._state = None
Expand All @@ -139,6 +227,11 @@ def __init__(
async def async_added_to_hass(self):
"""Register callbacks."""
self.add_template_attribute("_state", self._template, None, self._update_state)
if (
self._friendly_name_template is not None
and not self._friendly_name_template.is_static
):
self.add_template_attribute("_name", self._friendly_name_template)

if self._delay_on_raw is not None:
try:
Expand Down Expand Up @@ -166,7 +259,11 @@ def _update_state(self, result):
self._delay_cancel()
self._delay_cancel = None

state = None if isinstance(result, TemplateError) else result_as_boolean(result)
state = (
None
if isinstance(result, TemplateError)
else template.result_as_boolean(result)
)

if state == self._state:
return
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/template/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@
CONF_AVAILABILITY = "availability"
CONF_ATTRIBUTES = "attributes"
CONF_PICTURE = "picture"
CONF_OBJECT_ID = "object_id"
16 changes: 15 additions & 1 deletion tests/components/template/test_binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,28 @@ async def test_setup(hass):
"sensors": {
"test": {
"friendly_name": "virtual thingy",
"value_template": "{{ foo }}",
"value_template": "{{ True }}",
"device_class": "motion",
}
},
}
}
assert await setup.async_setup_component(hass, binary_sensor.DOMAIN, config)
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test")
assert state is not None
assert state.name == "virtual thingy"
assert state.state == "on"
assert state.attributes["device_class"] == "motion"


async def test_setup_no_sensors(hass):
"""Test setup with no sensors."""
assert await setup.async_setup_component(
hass, binary_sensor.DOMAIN, {"binary_sensor": {"platform": "template"}}
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids()) == 0


async def test_setup_invalid_device(hass):
Expand All @@ -49,6 +57,8 @@ async def test_setup_invalid_device(hass):
binary_sensor.DOMAIN,
{"binary_sensor": {"platform": "template", "sensors": {"foo bar": {}}}},
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids()) == 0


async def test_setup_invalid_device_class(hass):
Expand All @@ -68,6 +78,8 @@ async def test_setup_invalid_device_class(hass):
}
},
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids()) == 0


async def test_setup_invalid_missing_template(hass):
Expand All @@ -82,6 +94,8 @@ async def test_setup_invalid_missing_template(hass):
}
},
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids()) == 0


async def test_icon_template(hass):
Expand Down