diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 202ca0d9e4b74..ca0ed583d86e9 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -17,6 +17,7 @@ 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.""" diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 1d87e8d89e833..b89b3cbc91de3 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -7,6 +7,7 @@ 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 @@ 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 @@ async 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 @@ async def _validate_user_input( 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 @@ async def _validate_user_input( str, Callable[[HomeAssistant, str, dict[str, Any]], TemplateEntity], ] = { + "binary_sensor": async_create_preview_binary_sensor, "sensor": async_create_preview_sensor, } diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 6ceb4b495eff1..482682d0ce1e7 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -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", diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index dbaa23fae116a..dd283ff921448 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -25,6 +25,16 @@ "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 @@ async def test_config_flow_preview( @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')) }}",