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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add select platform to template integration #54835

Merged
merged 10 commits into from
Aug 25, 2021
2 changes: 1 addition & 1 deletion homeassistant/components/template/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ def __init__(

for key in (CONF_DELAY_ON, CONF_DELAY_OFF, CONF_AUTO_OFF):
if isinstance(config.get(key), template.Template):
self._to_render.append(key)
self._to_render_simple.append(key)
bdraco marked this conversation as resolved.
Show resolved Hide resolved
self._parse_result.add(key)

self._delay_cancel = None
Expand Down
10 changes: 9 additions & 1 deletion homeassistant/components/template/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
import voluptuous as vol

from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.config import async_log_exception, config_without_domain
from homeassistant.const import CONF_BINARY_SENSORS, CONF_SENSORS, CONF_UNIQUE_ID
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.trigger import async_validate_trigger_config

from . import binary_sensor as binary_sensor_platform, sensor as sensor_platform
from . import (
binary_sensor as binary_sensor_platform,
select as select_platform,
sensor as sensor_platform,
)
from .const import CONF_TRIGGER, DOMAIN

PACKAGE_MERGE_HINT = "list"
Expand All @@ -31,6 +36,9 @@
vol.Optional(CONF_BINARY_SENSORS): cv.schema_with_slug_keys(
binary_sensor_platform.LEGACY_BINARY_SENSOR_SCHEMA
),
vol.Optional(SELECT_DOMAIN): vol.All(
cv.ensure_list, [select_platform.SELECT_SCHEMA]
),
}
)

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 @@ -15,6 +15,7 @@
"fan",
"light",
"lock",
"select",
"sensor",
"switch",
"vacuum",
Expand Down
197 changes: 197 additions & 0 deletions homeassistant/components/template/select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
"""Support for selects which integrates with other components."""
from __future__ import annotations

import contextlib
import logging
from typing import Any

import voluptuous as vol

from homeassistant.components.select import SelectEntity
from homeassistant.components.select.const import (
ATTR_OPTION,
ATTR_OPTIONS,
DOMAIN as SELECT_DOMAIN,
)
from homeassistant.components.template.trigger_entity import TriggerEntity
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, CONF_UNIQUE_ID
from homeassistant.core import Config, HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.script import Script
from homeassistant.helpers.template import Template, TemplateError

from . import TriggerUpdateCoordinator
from .const import CONF_AVAILABILITY
from .template_entity import TemplateEntity

CONF_SELECT_OPTION = "select_option"

DEFAULT_NAME = "Template Select"
DEFAULT_OPTIMISTIC = False

SELECT_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.template,
vol.Required(CONF_STATE): cv.template,
vol.Required(CONF_SELECT_OPTION): cv.SCRIPT_SCHEMA,
vol.Required(ATTR_OPTIONS): cv.template,
vol.Optional(CONF_AVAILABILITY): cv.template,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)


async def _async_create_entities(
hass: HomeAssistant, entities: list[dict[str, Any]], unique_id_prefix: str | None
) -> list[TemplateSelect]:
"""Create the Template select."""
for entity in entities:
unique_id = entity.get(CONF_UNIQUE_ID)

if unique_id and unique_id_prefix:
unique_id = f"{unique_id_prefix}-{unique_id}"

return [
TemplateSelect(
hass,
entity.get(CONF_NAME, DEFAULT_NAME),
entity[CONF_STATE],
entity.get(CONF_AVAILABILITY),
entity[CONF_SELECT_OPTION],
entity[ATTR_OPTIONS],
entity.get(CONF_OPTIMISTIC, DEFAULT_OPTIMISTIC),
unique_id,
)
]


async def async_setup_platform(
hass: HomeAssistant,
config: Config,
async_add_entities: AddEntitiesCallback,
discovery_info: dict[str, Any] | None = None,
) -> None:
"""Set up the template select."""
if "coordinator" in discovery_info:
raman325 marked this conversation as resolved.
Show resolved Hide resolved
async_add_entities(
TriggerSelectEntity(hass, discovery_info["coordinator"], config)
for config in discovery_info["entities"]
)
return

async_add_entities(
await _async_create_entities(
hass, discovery_info["entities"], discovery_info["unique_id"]
)
)


class TemplateSelect(TemplateEntity, SelectEntity):
"""Representation of a template select."""

def __init__(
self,
hass: HomeAssistant,
name_template: Template | None,
value_template: Template,
availability_template: Template | None,
command_select_option: dict[str, Any],
options_template: Template,
optimistic: bool,
unique_id: str | None,
) -> None:
"""Initialize the select."""
super().__init__(availability_template=availability_template)
self._attr_name = DEFAULT_NAME
name_template.hass = hass
with contextlib.suppress(TemplateError):
self._attr_name = name_template.async_render(parse_result=False)
self._name_template = name_template
self._value_template = value_template
domain = __name__.split(".")[-2]
self._command_select_option = Script(
hass, command_select_option, self._attr_name, domain
)
self._options_template = options_template
self._attr_assumed_state = self._optimistic = optimistic
self._attr_unique_id = unique_id
self._attr_options = None
self._attr_current_option = None

async def async_added_to_hass(self) -> None:
"""Register callbacks."""
self.add_template_attribute(
"_attr_current_option",
self._value_template,
validator=cv.string,
none_on_template_error=True,
)
self.add_template_attribute(
"_attr_options",
self._options_template,
validator=vol.All(cv.ensure_list, [cv.string]),
none_on_template_error=True,
)
if self._name_template and not self._name_template.is_static:
self.add_template_attribute("_attr_name", self._name_template, cv.string)
await super().async_added_to_hass()

async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
if self._optimistic:
self._attr_current_option = option
self.async_write_ha_state()
await self._command_select_option.async_run(
{ATTR_OPTION: option}, context=self._context
)


class TriggerSelectEntity(TriggerEntity, SelectEntity):
"""Select entity based on trigger data."""

domain = SELECT_DOMAIN
extra_template_keys = (CONF_STATE,)
extra_template_keys_complex = (ATTR_OPTIONS,)

def __init__(
self,
hass: HomeAssistant,
coordinator: TriggerUpdateCoordinator,
config: dict,
) -> None:
"""Initialize the entity."""
super().__init__(hass, coordinator, config)
domain = __name__.split(".")[-2]
self._command_select_option = Script(
hass,
config[CONF_SELECT_OPTION],
self._rendered.get(CONF_NAME, DEFAULT_NAME),
domain,
)

@property
def current_option(self) -> str | None:
"""Return the currently selected option."""
return self._rendered.get(CONF_STATE)

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

raman325 marked this conversation as resolved.
Show resolved Hide resolved
@property
def options(self) -> list[str]:
"""Return the list of available options."""
logging.getLogger(__name__).error(self._rendered)
return self._rendered.get(ATTR_OPTIONS, [])

async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
if self._config[CONF_OPTIMISTIC]:
self._attr_current_option = option
self.async_write_ha_state()
await self._command_select_option.async_run(
{ATTR_OPTION: option}, context=self._context
)
19 changes: 15 additions & 4 deletions homeassistant/components/template/trigger_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class TriggerEntity(update_coordinator.CoordinatorEntity):

domain = ""
extra_template_keys: tuple | None = None
extra_template_keys_complex: tuple | None = None

def __init__(
self,
Expand All @@ -43,7 +44,8 @@ def __init__(
self._config = config

self._static_rendered = {}
self._to_render = []
self._to_render_simple = []
self._to_render_complex = []

for itm in (
CONF_NAME,
Expand All @@ -57,10 +59,13 @@ def __init__(
if config[itm].is_static:
self._static_rendered[itm] = config[itm].template
else:
self._to_render.append(itm)
self._to_render_simple.append(itm)

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

if self.extra_template_keys_complex is not None:
self._to_render_complex.extend(self.extra_template_keys_complex)

# We make a copy so our initial render is 'unknown' and not 'unavailable'
self._rendered = dict(self._static_rendered)
Expand Down Expand Up @@ -124,12 +129,18 @@ def _process_data(self) -> None:
try:
rendered = dict(self._static_rendered)

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

for key in self._to_render_complex:
rendered[key] = template.render_complex(
self._config[key],
self.coordinator.data["run_variables"],
)

if CONF_ATTRIBUTES in self._config:
rendered[CONF_ATTRIBUTES] = template.render_complex(
self._config[CONF_ATTRIBUTES],
Expand Down
Loading