Add config flow for template binary sensor (#99339)
This commit is contained in:
parent
a5dcc25aab
commit
501d5db375
4 changed files with 151 additions and 7 deletions
|
@ -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."""
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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')) }}",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue