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, BinarySensorDeviceClass,
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME, 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): class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity):
"""A virtual binary sensor that triggers from another sensor.""" """A virtual binary sensor that triggers from another sensor."""

View file

@ -7,6 +7,7 @@ from typing import Any, cast
import voluptuous as vol import voluptuous as vol
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
CONF_STATE_CLASS, CONF_STATE_CLASS,
DEVICE_CLASS_STATE_CLASSES, DEVICE_CLASS_STATE_CLASSES,
@ -31,6 +32,7 @@ from homeassistant.helpers.schema_config_entry_flow import (
SchemaFlowMenuStep, SchemaFlowMenuStep,
) )
from .binary_sensor import async_create_preview_binary_sensor
from .const import DOMAIN from .const import DOMAIN
from .sensor import async_create_preview_sensor from .sensor import async_create_preview_sensor
from .template_entity import TemplateEntity from .template_entity import TemplateEntity
@ -42,6 +44,23 @@ def generate_schema(domain: str) -> dict[vol.Marker, Any]:
"""Generate schema.""" """Generate schema."""
schema: dict[vol.Marker, Any] = {} 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: if domain == Platform.SENSOR:
schema = { schema = {
vol.Optional( vol.Optional(
@ -197,11 +216,17 @@ def validate_user_input(
TEMPLATE_TYPES = [ TEMPLATE_TYPES = [
"binary_sensor",
"sensor", "sensor",
] ]
CONFIG_FLOW = { CONFIG_FLOW = {
"user": SchemaFlowMenuStep(TEMPLATE_TYPES), "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( Platform.SENSOR: SchemaFlowFormStep(
config_schema(Platform.SENSOR), config_schema(Platform.SENSOR),
preview="template", preview="template",
@ -212,6 +237,11 @@ CONFIG_FLOW = {
OPTIONS_FLOW = { OPTIONS_FLOW = {
"init": SchemaFlowFormStep(next_step=choose_options_step), "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( Platform.SENSOR: SchemaFlowFormStep(
options_schema(Platform.SENSOR), options_schema(Platform.SENSOR),
preview="template", preview="template",
@ -223,6 +253,7 @@ CREATE_PREVIEW_ENTITY: dict[
str, str,
Callable[[HomeAssistant, str, dict[str, Any]], TemplateEntity], Callable[[HomeAssistant, str, dict[str, Any]], TemplateEntity],
] = { ] = {
"binary_sensor": async_create_preview_binary_sensor,
"sensor": async_create_preview_sensor, "sensor": async_create_preview_sensor,
} }

View file

@ -1,6 +1,14 @@
{ {
"config": { "config": {
"step": { "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": { "sensor": {
"data": { "data": {
"device_class": "Device class", "device_class": "Device class",
@ -14,6 +22,7 @@
"user": { "user": {
"description": "This helper allow you to create helper entities that define their state using a template.", "description": "This helper allow you to create helper entities that define their state using a template.",
"menu_options": { "menu_options": {
"binary_sensor": "Template a binary sensor",
"sensor": "Template a sensor" "sensor": "Template a sensor"
}, },
"title": "Template helper" "title": "Template helper"
@ -22,6 +31,13 @@
}, },
"options": { "options": {
"step": { "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": { "sensor": {
"data": { "data": {
"device_class": "[%key:component::template::config::step::sensor::data::device_class%]", "device_class": "[%key:component::template::config::step::sensor::data::device_class%]",
@ -34,6 +50,38 @@
} }
}, },
"selector": { "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": { "sensor_device_class": {
"options": { "options": {
"none": "No device class", "none": "No device class",

View file

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