Add config flow for template binary sensor (#99339)

This commit is contained in:
Erik Montnemery 2023-08-30 17:28:56 +02:00 committed by GitHub
parent a5dcc25aab
commit 501d5db375
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 151 additions and 7 deletions

View file

@ -17,6 +17,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
@ -194,6 +195,29 @@ async def async_setup_platform(
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Initialize config entry."""
_options = dict(config_entry.options)
_options.pop("template_type")
validated_config = BINARY_SENSOR_SCHEMA(_options)
async_add_entities(
[BinarySensorTemplate(hass, validated_config, config_entry.entry_id)]
)
@callback
def async_create_preview_binary_sensor(
hass: HomeAssistant, name: str, config: dict[str, Any]
) -> BinarySensorTemplate:
"""Create a preview sensor."""
validated_config = BINARY_SENSOR_SCHEMA(config | {CONF_NAME: name})
return BinarySensorTemplate(hass, validated_config, None)
class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity):
"""A virtual binary sensor that triggers from another sensor."""

View file

@ -7,6 +7,7 @@ from typing import Any, cast
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
DEVICE_CLASS_STATE_CLASSES,
@ -31,6 +32,7 @@ from homeassistant.helpers.schema_config_entry_flow import (
SchemaFlowMenuStep,
)
from .binary_sensor import async_create_preview_binary_sensor
from .const import DOMAIN
from .sensor import async_create_preview_sensor
from .template_entity import TemplateEntity
@ -42,6 +44,23 @@ def generate_schema(domain: str) -> dict[vol.Marker, Any]:
"""Generate schema."""
schema: dict[vol.Marker, Any] = {}
if domain == Platform.BINARY_SENSOR:
schema = {
vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[
NONE_SENTINEL,
*sorted(
[cls.value for cls in BinarySensorDeviceClass],
key=str.casefold,
),
],
mode=selector.SelectSelectorMode.DROPDOWN,
translation_key="binary_sensor_device_class",
),
)
}
if domain == Platform.SENSOR:
schema = {
vol.Optional(
@ -197,11 +216,17 @@ def validate_user_input(
TEMPLATE_TYPES = [
"binary_sensor",
"sensor",
]
CONFIG_FLOW = {
"user": SchemaFlowMenuStep(TEMPLATE_TYPES),
Platform.BINARY_SENSOR: SchemaFlowFormStep(
config_schema(Platform.BINARY_SENSOR),
preview="template",
validate_user_input=validate_user_input(Platform.BINARY_SENSOR),
),
Platform.SENSOR: SchemaFlowFormStep(
config_schema(Platform.SENSOR),
preview="template",
@ -212,6 +237,11 @@ CONFIG_FLOW = {
OPTIONS_FLOW = {
"init": SchemaFlowFormStep(next_step=choose_options_step),
Platform.BINARY_SENSOR: SchemaFlowFormStep(
options_schema(Platform.BINARY_SENSOR),
preview="template",
validate_user_input=validate_user_input(Platform.BINARY_SENSOR),
),
Platform.SENSOR: SchemaFlowFormStep(
options_schema(Platform.SENSOR),
preview="template",
@ -223,6 +253,7 @@ CREATE_PREVIEW_ENTITY: dict[
str,
Callable[[HomeAssistant, str, dict[str, Any]], TemplateEntity],
] = {
"binary_sensor": async_create_preview_binary_sensor,
"sensor": async_create_preview_sensor,
}

View file

@ -1,6 +1,14 @@
{
"config": {
"step": {
"binary_sensor": {
"data": {
"device_class": "[%key:component::template::config::step::sensor::data::device_class%]",
"name": "[%key:common::config_flow::data::name%]",
"state_template": "[%key:component::template::config::step::sensor::data::state_template%]"
},
"title": "Template binary sensor"
},
"sensor": {
"data": {
"device_class": "Device class",
@ -14,6 +22,7 @@
"user": {
"description": "This helper allow you to create helper entities that define their state using a template.",
"menu_options": {
"binary_sensor": "Template a binary sensor",
"sensor": "Template a sensor"
},
"title": "Template helper"
@ -22,6 +31,13 @@
},
"options": {
"step": {
"binary_sensor": {
"data": {
"device_class": "[%key:component::template::config::step::sensor::data::device_class%]",
"state_template": "[%key:component::template::config::step::sensor::data::state_template%]"
},
"title": "[%key:component::template::config::step::binary_sensor::title%]"
},
"sensor": {
"data": {
"device_class": "[%key:component::template::config::step::sensor::data::device_class%]",
@ -34,6 +50,38 @@
}
},
"selector": {
"binary_sensor_device_class": {
"options": {
"none": "[%key:component::template::selector::sensor_device_class::options::none%]",
"battery": "[%key:component::binary_sensor::entity_component::battery::name%]",
"battery_charging": "[%key:component::binary_sensor::entity_component::battery_charging::name%]",
"carbon_monoxide": "[%key:component::binary_sensor::entity_component::carbon_monoxide::name%]",
"cold": "[%key:component::binary_sensor::entity_component::cold::name%]",
"connectivity": "[%key:component::binary_sensor::entity_component::connectivity::name%]",
"door": "[%key:component::binary_sensor::entity_component::door::name%]",
"garage_door": "[%key:component::binary_sensor::entity_component::garage_door::name%]",
"gas": "[%key:component::binary_sensor::entity_component::gas::name%]",
"heat": "[%key:component::binary_sensor::entity_component::heat::name%]",
"light": "[%key:component::binary_sensor::entity_component::light::name%]",
"lock": "[%key:component::binary_sensor::entity_component::lock::name%]",
"moisture": "[%key:component::binary_sensor::entity_component::moisture::name%]",
"motion": "[%key:component::binary_sensor::entity_component::motion::name%]",
"moving": "[%key:component::binary_sensor::entity_component::moving::name%]",
"occupancy": "[%key:component::binary_sensor::entity_component::occupancy::name%]",
"opening": "[%key:component::binary_sensor::entity_component::opening::name%]",
"plug": "[%key:component::binary_sensor::entity_component::plug::name%]",
"power": "[%key:component::binary_sensor::entity_component::power::name%]",
"presence": "[%key:component::binary_sensor::entity_component::presence::name%]",
"problem": "[%key:component::binary_sensor::entity_component::problem::name%]",
"running": "[%key:component::binary_sensor::entity_component::running::name%]",
"safety": "[%key:component::binary_sensor::entity_component::safety::name%]",
"smoke": "[%key:component::binary_sensor::entity_component::smoke::name%]",
"sound": "[%key:component::binary_sensor::entity_component::sound::name%]",
"update": "[%key:component::binary_sensor::entity_component::update::name%]",
"vibration": "[%key:component::binary_sensor::entity_component::vibration::name%]",
"window": "[%key:component::binary_sensor::entity_component::window::name%]"
}
},
"sensor_device_class": {
"options": {
"none": "No device class",

View file

@ -25,6 +25,16 @@ from tests.typing import WebSocketGenerator
"extra_attrs",
),
(
(
"binary_sensor",
"{{ states('binary_sensor.one') == 'on' or states('binary_sensor.two') == 'on' }}",
"on",
{"one": "on", "two": "off"},
{},
{},
{},
{},
),
(
"sensor",
"{{ float(states('sensor.one')) + float(states('sensor.two')) }}",
@ -125,15 +135,26 @@ def get_suggested(schema, key):
"template_type",
"old_state_template",
"new_state_template",
"template_state",
"input_states",
"extra_options",
"options_options",
),
(
(
"binary_sensor",
"{{ states('binary_sensor.one') == 'on' or states('binary_sensor.two') == 'on' }}",
"{{ states('binary_sensor.one') == 'on' and states('binary_sensor.two') == 'on' }}",
["on", "off"],
{"one": "on", "two": "off"},
{},
{},
),
(
"sensor",
"{{ float(states('sensor.one')) + float(states('sensor.two')) }}",
"{{ float(states('sensor.one')) - float(states('sensor.two')) }}",
["50.0", "10.0"],
{"one": "30.0", "two": "20.0"},
{},
{},
@ -145,6 +166,7 @@ async def test_options(
template_type,
old_state_template,
new_state_template,
template_state,
input_states,
extra_options,
options_options,
@ -174,7 +196,7 @@ async def test_options(
await hass.async_block_till_done()
state = hass.states.get(f"{template_type}.my_template")
assert state.state == "50.0"
assert state.state == template_state[0]
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
@ -207,7 +229,7 @@ async def test_options(
# Check config entry is reloaded with new options
await hass.async_block_till_done()
state = hass.states.get(f"{template_type}.my_template")
assert state.state == "10.0"
assert state.state == template_state[1]
# Check we don't get suggestions from another entry
result = await hass.config_entries.flow.async_init(
@ -233,16 +255,24 @@ async def test_options(
"state_template",
"extra_user_input",
"input_states",
"template_state",
"template_states",
"extra_attributes",
),
(
(
"binary_sensor",
"{{ states.binary_sensor.one.state == 'on' or states.binary_sensor.two.state == 'on' }}",
{},
{"one": "on", "two": "off"},
["off", "on"],
[{}, {}],
),
(
"sensor",
"{{ float(states('sensor.one')) + float(states('sensor.two')) }}",
{},
{"one": "30.0", "two": "20.0"},
"50.0",
["unavailable", "50.0"],
[{}, {}],
),
),
@ -254,7 +284,7 @@ async def test_config_flow_preview(
state_template: str,
extra_user_input: dict[str, Any],
input_states: list[str],
template_state: str,
template_states: str,
extra_attributes: list[dict[str, Any]],
) -> None:
"""Test the config flow preview."""
@ -293,7 +323,7 @@ async def test_config_flow_preview(
msg = await client.receive_json()
assert msg["event"] == {
"attributes": {"friendly_name": "My template"} | extra_attributes[0],
"state": "unavailable",
"state": template_states[0],
}
for input_entity in input_entities:
@ -306,7 +336,7 @@ async def test_config_flow_preview(
"attributes": {"friendly_name": "My template"}
| extra_attributes[0]
| extra_attributes[1],
"state": template_state,
"state": template_states[1],
}
assert len(hass.states.async_all()) == 2
@ -317,6 +347,7 @@ EARLY_END_ERROR = "invalid template (TemplateSyntaxError: unexpected 'end of tem
@pytest.mark.parametrize(
("template_type", "state_template", "extra_user_input", "error"),
[
("binary_sensor", "{{", {}, {"state": EARLY_END_ERROR}),
("sensor", "{{", {}, {"state": EARLY_END_ERROR}),
(
"sensor",
@ -453,6 +484,16 @@ async def test_config_flow_preview_bad_state(
"extra_attributes",
),
[
(
"binary_sensor",
"{{ states('binary_sensor.one') == 'on' or states('binary_sensor.two') == 'on' }}",
"{{ states('binary_sensor.one') == 'on' and states('binary_sensor.two') == 'on' }}",
{},
{},
{"one": "on", "two": "off"},
"off",
{},
),
(
"sensor",
"{{ float(states('sensor.one')) + float(states('sensor.two')) }}",