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, Platform,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import section
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er, selector from homeassistant.helpers import entity_registry as er, selector
from homeassistant.helpers.schema_config_entry_flow import ( from homeassistant.helpers.schema_config_entry_flow import (
@ -53,7 +54,14 @@ from .alarm_control_panel import (
async_create_preview_alarm_control_panel, async_create_preview_alarm_control_panel,
) )
from .binary_sensor import async_create_preview_binary_sensor 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 ( from .number import (
CONF_MAX, CONF_MAX,
CONF_MIN, CONF_MIN,
@ -214,7 +222,17 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema:
vol.Optional(CONF_TURN_OFF): selector.ActionSelector(), 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) return vol.Schema(schema)
@ -530,7 +548,11 @@ def ws_start_preview(
) )
return 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.hass = hass
preview_entity.registry_entry = entity_registry_entry 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 import config_validation as cv
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
CONF_ADVANCED_OPTIONS = "advanced_options"
CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" CONF_ATTRIBUTE_TEMPLATES = "attribute_templates"
CONF_ATTRIBUTES = "attributes" CONF_ATTRIBUTES = "attributes"
CONF_AVAILABILITY = "availability" CONF_AVAILABILITY = "availability"

View File

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

View File

@ -19,6 +19,14 @@
"data_description": { "data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" "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" "title": "Template alarm control panel"
}, },
"binary_sensor": { "binary_sensor": {
@ -31,6 +39,14 @@
"data_description": { "data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" "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" "title": "Template binary sensor"
}, },
"button": { "button": {
@ -43,6 +59,14 @@
"data_description": { "data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" "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" "title": "Template button"
}, },
"image": { "image": {
@ -55,6 +79,14 @@
"data_description": { "data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" "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" "title": "Template image"
}, },
"number": { "number": {
@ -71,6 +103,14 @@
"data_description": { "data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" "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" "title": "Template number"
}, },
"select": { "select": {
@ -84,6 +124,14 @@
"data_description": { "data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" "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" "title": "Template select"
}, },
"sensor": { "sensor": {
@ -98,6 +146,14 @@
"data_description": { "data_description": {
"device_id": "Select a device to link to this entity." "device_id": "Select a device to link to this entity."
}, },
"sections": {
"advanced_options": {
"name": "Advanced options",
"data": {
"availability": "Availability template"
}
}
},
"title": "Template sensor" "title": "Template sensor"
}, },
"user": { "user": {
@ -126,6 +182,14 @@
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]", "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." "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" "title": "Template switch"
} }
} }
@ -149,6 +213,14 @@
"data_description": { "data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" "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%]" "title": "[%key:component::template::config::step::alarm_control_panel::title%]"
}, },
"binary_sensor": { "binary_sensor": {
@ -159,6 +231,14 @@
"data_description": { "data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" "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%]" "title": "[%key:component::template::config::step::binary_sensor::title%]"
}, },
"button": { "button": {
@ -169,6 +249,14 @@
"data_description": { "data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" "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%]" "title": "[%key:component::template::config::step::button::title%]"
}, },
"image": { "image": {
@ -180,6 +268,14 @@
"data_description": { "data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" "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%]" "title": "[%key:component::template::config::step::image::title%]"
}, },
"number": { "number": {
@ -195,6 +291,14 @@
"data_description": { "data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" "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%]" "title": "[%key:component::template::config::step::number::title%]"
}, },
"select": { "select": {
@ -208,6 +312,14 @@
"data_description": { "data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" "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%]" "title": "[%key:component::template::config::step::select::title%]"
}, },
"sensor": { "sensor": {
@ -221,6 +333,14 @@
"data_description": { "data_description": {
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" "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%]" "title": "[%key:component::template::config::step::sensor::title%]"
}, },
"switch": { "switch": {
@ -235,6 +355,14 @@
"device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]", "device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]",
"value_template": "[%key:component::template::config::step::switch::data_description::value_template%]" "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%]" "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 import config_entries
from homeassistant.components.template import DOMAIN, async_setup_entry 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.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import device_registry as dr 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["type"] is FlowResultType.FORM
assert result["step_id"] == template_type assert result["step_id"] == template_type
availability = {"advanced_options": {"availability": "{{ True }}"}}
with patch( with patch(
"homeassistant.components.template.async_setup_entry", wraps=async_setup_entry "homeassistant.components.template.async_setup_entry", wraps=async_setup_entry
) as mock_setup_entry: ) as mock_setup_entry:
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {"name": "My template", **state_template, **extra_input, **availability},
"name": "My template",
**state_template,
**extra_input,
},
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -238,6 +236,7 @@ async def test_config_flow(
"template_type": template_type, "template_type": template_type,
**state_template, **state_template,
**extra_options, **extra_options,
**availability,
} }
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
@ -248,6 +247,7 @@ async def test_config_flow(
"template_type": template_type, "template_type": template_type,
**state_template, **state_template,
**extra_options, **extra_options,
**availability,
} }
state = hass.states.get(f"{template_type}.my_template") 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='') }}", "{{ float(states('sensor.one'), default='') + float(states('sensor.two'), default='') }}",
{}, {},
{"one": "30.0", "two": "20.0"}, {"one": "30.0", "two": "20.0"},
["", STATE_UNAVAILABLE, "50.0"], ["", STATE_UNKNOWN, "50.0"],
[{}, {}], [{}, {}],
[["one", "two"], ["one", "two"]], [["one", "two"], ["one", "two"]],
), ),
@ -695,6 +695,9 @@ async def test_config_flow_preview(
"""Test the config flow preview.""" """Test the config flow preview."""
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
hass.states.async_set("binary_sensor.available", "on")
await hass.async_block_till_done()
input_entities = ["one", "two"] input_entities = ["one", "two"]
result = await hass.config_entries.flow.async_init( 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["errors"] is None
assert result["preview"] == "template" assert result["preview"] == "template"
availability = {
"advanced_options": {
"availability": "{{ is_state('binary_sensor.available', 'on') }}"
}
}
await client.send_json_auto_id( await client.send_json_auto_id(
{ {
"type": "template/start_preview", "type": "template/start_preview",
"flow_id": result["flow_id"], "flow_id": result["flow_id"],
"flow_type": "config_flow", "flow_type": "config_flow",
"user_input": {"name": "My template", "state": state_template} "user_input": {
"name": "My template",
"state": state_template,
**availability,
}
| extra_user_input, | extra_user_input,
} }
) )
@ -725,13 +738,16 @@ async def test_config_flow_preview(
assert msg["success"] assert msg["success"]
assert msg["result"] is None 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() 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],
"listeners": { "listeners": {
"all": False, "all": False,
"domains": [], "domains": [],
"entities": unordered([f"{template_type}.{_id}" for _id in listeners[0]]), "entities": unordered(entities),
"time": False, "time": False,
}, },
"state": template_states[0], "state": template_states[0],
@ -743,6 +759,9 @@ async def test_config_flow_preview(
) )
await hass.async_block_till_done() 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:]: for template_state in template_states[1:]:
msg = await client.receive_json() msg = await client.receive_json()
assert msg["event"] == { assert msg["event"] == {
@ -752,14 +771,32 @@ async def test_config_flow_preview(
"listeners": { "listeners": {
"all": False, "all": False,
"domains": [], "domains": [],
"entities": unordered( "entities": unordered(entities),
[f"{template_type}.{_id}" for _id in listeners[1]]
),
"time": False, "time": False,
}, },
"state": template_state, "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')" EARLY_END_ERROR = "invalid template (TemplateSyntaxError: unexpected 'end of template')"