Add availability template to template helper config flow (#147623)

Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Petro31 2025-07-21 11:41:56 -04:00 committed by GitHub
parent 3f42911af4
commit b85ec55abb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 208 additions and 16 deletions

View File

@ -30,6 +30,7 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import section
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er, selector
from homeassistant.helpers.schema_config_entry_flow import (
@ -53,7 +54,14 @@ from .alarm_control_panel import (
async_create_preview_alarm_control_panel,
)
from .binary_sensor import async_create_preview_binary_sensor
from .const import CONF_PRESS, CONF_TURN_OFF, CONF_TURN_ON, DOMAIN
from .const import (
CONF_ADVANCED_OPTIONS,
CONF_AVAILABILITY,
CONF_PRESS,
CONF_TURN_OFF,
CONF_TURN_ON,
DOMAIN,
)
from .number import (
CONF_MAX,
CONF_MIN,
@ -214,7 +222,17 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema:
vol.Optional(CONF_TURN_OFF): selector.ActionSelector(),
}
schema[vol.Optional(CONF_DEVICE_ID)] = selector.DeviceSelector()
schema |= {
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
vol.Optional(CONF_ADVANCED_OPTIONS): section(
vol.Schema(
{
vol.Optional(CONF_AVAILABILITY): selector.TemplateSelector(),
}
),
{"collapsed": True},
),
}
return vol.Schema(schema)
@ -530,7 +548,11 @@ def ws_start_preview(
)
return
preview_entity = CREATE_PREVIEW_ENTITY[template_type](hass, name, msg["user_input"])
config: dict = msg["user_input"]
advanced_options = config.pop(CONF_ADVANCED_OPTIONS, {})
preview_entity = CREATE_PREVIEW_ENTITY[template_type](
hass, name, {**config, **advanced_options}
)
preview_entity.hass = hass
preview_entity.registry_entry = entity_registry_entry

View File

@ -6,6 +6,7 @@ from homeassistant.const import CONF_ICON, CONF_NAME, CONF_UNIQUE_ID, Platform
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
CONF_ADVANCED_OPTIONS = "advanced_options"
CONF_ATTRIBUTE_TEMPLATES = "attribute_templates"
CONF_ATTRIBUTES = "attributes"
CONF_AVAILABILITY = "availability"

View File

@ -33,6 +33,7 @@ from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import (
CONF_ADVANCED_OPTIONS,
CONF_ATTRIBUTE_TEMPLATES,
CONF_ATTRIBUTES,
CONF_AVAILABILITY,
@ -248,6 +249,9 @@ async def async_setup_template_entry(
options = dict(config_entry.options)
options.pop("template_type")
if advanced_options := options.pop(CONF_ADVANCED_OPTIONS, None):
options = {**options, **advanced_options}
if replace_value_template and CONF_VALUE_TEMPLATE in options:
options[CONF_STATE] = options.pop(CONF_VALUE_TEMPLATE)

View File

@ -19,6 +19,14 @@
"data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::config::step::sensor::sections::advanced_options::name%]",
"data": {
"availability": "[%key:component::template::config::step::sensor::sections::advanced_options::data::availability%]"
}
}
},
"title": "Template alarm control panel"
},
"binary_sensor": {
@ -31,6 +39,14 @@
"data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::config::step::sensor::sections::advanced_options::name%]",
"data": {
"availability": "[%key:component::template::config::step::sensor::sections::advanced_options::data::availability%]"
}
}
},
"title": "Template binary sensor"
},
"button": {
@ -43,6 +59,14 @@
"data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::config::step::sensor::sections::advanced_options::name%]",
"data": {
"availability": "[%key:component::template::config::step::sensor::sections::advanced_options::data::availability%]"
}
}
},
"title": "Template button"
},
"image": {
@ -55,6 +79,14 @@
"data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::config::step::sensor::sections::advanced_options::name%]",
"data": {
"availability": "[%key:component::template::config::step::sensor::sections::advanced_options::data::availability%]"
}
}
},
"title": "Template image"
},
"number": {
@ -71,6 +103,14 @@
"data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::config::step::sensor::sections::advanced_options::name%]",
"data": {
"availability": "[%key:component::template::config::step::sensor::sections::advanced_options::data::availability%]"
}
}
},
"title": "Template number"
},
"select": {
@ -84,6 +124,14 @@
"data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::config::step::sensor::sections::advanced_options::name%]",
"data": {
"availability": "[%key:component::template::config::step::sensor::sections::advanced_options::data::availability%]"
}
}
},
"title": "Template select"
},
"sensor": {
@ -98,6 +146,14 @@
"data_description": {
"device_id": "Select a device to link to this entity."
},
"sections": {
"advanced_options": {
"name": "Advanced options",
"data": {
"availability": "Availability template"
}
}
},
"title": "Template sensor"
},
"user": {
@ -126,6 +182,14 @@
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]",
"value_template": "Defines a template to set the state of the switch. If not defined, the switch will optimistically assume all commands are successful."
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::config::step::sensor::sections::advanced_options::name%]",
"data": {
"availability": "[%key:component::template::config::step::sensor::sections::advanced_options::data::availability%]"
}
}
},
"title": "Template switch"
}
}
@ -149,6 +213,14 @@
"data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::config::step::sensor::sections::advanced_options::name%]",
"data": {
"availability": "[%key:component::template::config::step::sensor::sections::advanced_options::data::availability%]"
}
}
},
"title": "[%key:component::template::config::step::alarm_control_panel::title%]"
},
"binary_sensor": {
@ -159,6 +231,14 @@
"data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::config::step::sensor::sections::advanced_options::name%]",
"data": {
"availability": "[%key:component::template::config::step::sensor::sections::advanced_options::data::availability%]"
}
}
},
"title": "[%key:component::template::config::step::binary_sensor::title%]"
},
"button": {
@ -169,6 +249,14 @@
"data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::config::step::sensor::sections::advanced_options::name%]",
"data": {
"availability": "[%key:component::template::config::step::sensor::sections::advanced_options::data::availability%]"
}
}
},
"title": "[%key:component::template::config::step::button::title%]"
},
"image": {
@ -180,6 +268,14 @@
"data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::config::step::sensor::sections::advanced_options::name%]",
"data": {
"availability": "[%key:component::template::config::step::sensor::sections::advanced_options::data::availability%]"
}
}
},
"title": "[%key:component::template::config::step::image::title%]"
},
"number": {
@ -195,6 +291,14 @@
"data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::config::step::sensor::sections::advanced_options::name%]",
"data": {
"availability": "[%key:component::template::config::step::sensor::sections::advanced_options::data::availability%]"
}
}
},
"title": "[%key:component::template::config::step::number::title%]"
},
"select": {
@ -208,6 +312,14 @@
"data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::config::step::sensor::sections::advanced_options::name%]",
"data": {
"availability": "[%key:component::template::config::step::sensor::sections::advanced_options::data::availability%]"
}
}
},
"title": "[%key:component::template::config::step::select::title%]"
},
"sensor": {
@ -221,6 +333,14 @@
"data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::config::step::sensor::sections::advanced_options::name%]",
"data": {
"availability": "[%key:component::template::config::step::sensor::sections::advanced_options::data::availability%]"
}
}
},
"title": "[%key:component::template::config::step::sensor::title%]"
},
"switch": {
@ -235,6 +355,14 @@
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]",
"value_template": "[%key:component::template::config::step::switch::data_description::value_template%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::config::step::sensor::sections::advanced_options::name%]",
"data": {
"availability": "[%key:component::template::config::step::sensor::sections::advanced_options::data::availability%]"
}
}
},
"title": "[%key:component::template::config::step::switch::title%]"
}
}

View File

@ -8,7 +8,7 @@ from pytest_unordered import unordered
from homeassistant import config_entries
from homeassistant.components.template import DOMAIN, async_setup_entry
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import device_registry as dr
@ -217,16 +217,14 @@ async def test_config_flow(
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == template_type
availability = {"advanced_options": {"availability": "{{ True }}"}}
with patch(
"homeassistant.components.template.async_setup_entry", wraps=async_setup_entry
) as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"name": "My template",
**state_template,
**extra_input,
},
{"name": "My template", **state_template, **extra_input, **availability},
)
await hass.async_block_till_done()
@ -238,6 +236,7 @@ async def test_config_flow(
"template_type": template_type,
**state_template,
**extra_options,
**availability,
}
assert len(mock_setup_entry.mock_calls) == 1
@ -248,6 +247,7 @@ async def test_config_flow(
"template_type": template_type,
**state_template,
**extra_options,
**availability,
}
state = hass.states.get(f"{template_type}.my_template")
@ -675,7 +675,7 @@ async def test_options(
"{{ float(states('sensor.one'), default='') + float(states('sensor.two'), default='') }}",
{},
{"one": "30.0", "two": "20.0"},
["", STATE_UNAVAILABLE, "50.0"],
["", STATE_UNKNOWN, "50.0"],
[{}, {}],
[["one", "two"], ["one", "two"]],
),
@ -695,6 +695,9 @@ async def test_config_flow_preview(
"""Test the config flow preview."""
client = await hass_ws_client(hass)
hass.states.async_set("binary_sensor.available", "on")
await hass.async_block_till_done()
input_entities = ["one", "two"]
result = await hass.config_entries.flow.async_init(
@ -712,12 +715,22 @@ async def test_config_flow_preview(
assert result["errors"] is None
assert result["preview"] == "template"
availability = {
"advanced_options": {
"availability": "{{ is_state('binary_sensor.available', 'on') }}"
}
}
await client.send_json_auto_id(
{
"type": "template/start_preview",
"flow_id": result["flow_id"],
"flow_type": "config_flow",
"user_input": {"name": "My template", "state": state_template}
"user_input": {
"name": "My template",
"state": state_template,
**availability,
}
| extra_user_input,
}
)
@ -725,13 +738,16 @@ async def test_config_flow_preview(
assert msg["success"]
assert msg["result"] is None
entities = [f"{template_type}.{_id}" for _id in listeners[0]]
entities.append("binary_sensor.available")
msg = await client.receive_json()
assert msg["event"] == {
"attributes": {"friendly_name": "My template"} | extra_attributes[0],
"listeners": {
"all": False,
"domains": [],
"entities": unordered([f"{template_type}.{_id}" for _id in listeners[0]]),
"entities": unordered(entities),
"time": False,
},
"state": template_states[0],
@ -743,6 +759,9 @@ async def test_config_flow_preview(
)
await hass.async_block_till_done()
entities = [f"{template_type}.{_id}" for _id in listeners[1]]
entities.append("binary_sensor.available")
for template_state in template_states[1:]:
msg = await client.receive_json()
assert msg["event"] == {
@ -752,14 +771,32 @@ async def test_config_flow_preview(
"listeners": {
"all": False,
"domains": [],
"entities": unordered(
[f"{template_type}.{_id}" for _id in listeners[1]]
),
"entities": unordered(entities),
"time": False,
},
"state": template_state,
}
assert len(hass.states.async_all()) == 2
assert len(hass.states.async_all()) == 3
# Test preview availability.
hass.states.async_set("binary_sensor.available", "off")
await hass.async_block_till_done()
msg = await client.receive_json()
assert msg["event"] == {
"attributes": {"friendly_name": "My template"}
| extra_attributes[0]
| extra_attributes[1],
"listeners": {
"all": False,
"domains": [],
"entities": unordered(entities),
"time": False,
},
"state": STATE_UNAVAILABLE,
}
assert len(hass.states.async_all()) == 3
EARLY_END_ERROR = "invalid template (TemplateSyntaxError: unexpected 'end of template')"