1 """Config flow for the Template integration."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Coroutine, Mapping
6 from functools
import partial
7 from typing
import Any, cast
9 import voluptuous
as vol
16 DEVICE_CLASS_STATE_CLASSES,
26 CONF_UNIT_OF_MEASUREMENT,
36 SchemaCommonFlowHandler,
37 SchemaConfigFlowHandler,
42 from .alarm_control_panel
import (
44 CONF_ARM_CUSTOM_BYPASS_ACTION,
46 CONF_ARM_NIGHT_ACTION,
47 CONF_ARM_VACATION_ACTION,
48 CONF_CODE_ARM_REQUIRED,
54 from .binary_sensor
import async_create_preview_binary_sensor
55 from .const
import CONF_PRESS, CONF_TURN_OFF, CONF_TURN_ON, DOMAIN
64 async_create_preview_number,
66 from .select
import CONF_OPTIONS, CONF_SELECT_OPTION
67 from .sensor
import async_create_preview_sensor
68 from .switch
import async_create_preview_switch
69 from .template_entity
import TemplateEntity
71 _SCHEMA_STATE: dict[vol.Marker, Any] = {
72 vol.Required(CONF_STATE): selector.TemplateSelector(),
77 """Generate schema."""
78 schema: dict[vol.Marker, Any] = {}
80 if flow_type ==
"config":
81 schema = {vol.Required(CONF_NAME): selector.TextSelector()}
83 if domain == Platform.ALARM_CONTROL_PANEL:
85 vol.Optional(CONF_VALUE_TEMPLATE): selector.TemplateSelector(),
86 vol.Optional(CONF_DISARM_ACTION): selector.ActionSelector(),
87 vol.Optional(CONF_ARM_AWAY_ACTION): selector.ActionSelector(),
88 vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): selector.ActionSelector(),
89 vol.Optional(CONF_ARM_HOME_ACTION): selector.ActionSelector(),
90 vol.Optional(CONF_ARM_NIGHT_ACTION): selector.ActionSelector(),
91 vol.Optional(CONF_ARM_VACATION_ACTION): selector.ActionSelector(),
92 vol.Optional(CONF_TRIGGER_ACTION): selector.ActionSelector(),
94 CONF_CODE_ARM_REQUIRED, default=
True
95 ): selector.BooleanSelector(),
97 CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name
98 ): selector.SelectSelector(
99 selector.SelectSelectorConfig(
100 options=[e.name
for e
in TemplateCodeFormat],
101 mode=selector.SelectSelectorMode.DROPDOWN,
102 translation_key=
"alarm_control_panel_code_format",
107 if domain == Platform.BINARY_SENSOR:
108 schema |= _SCHEMA_STATE
109 if flow_type ==
"config":
111 vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector(
112 selector.SelectSelectorConfig(
113 options=[cls.value
for cls
in BinarySensorDeviceClass],
114 mode=selector.SelectSelectorMode.DROPDOWN,
115 translation_key=
"binary_sensor_device_class",
121 if domain == Platform.BUTTON:
123 vol.Optional(CONF_PRESS): selector.ActionSelector(),
125 if flow_type ==
"config":
127 vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector(
128 selector.SelectSelectorConfig(
129 options=[cls.value
for cls
in ButtonDeviceClass],
130 mode=selector.SelectSelectorMode.DROPDOWN,
131 translation_key=
"button_device_class",
137 if domain == Platform.IMAGE:
139 vol.Required(CONF_URL): selector.TemplateSelector(),
140 vol.Optional(CONF_VERIFY_SSL, default=
True): selector.BooleanSelector(),
143 if domain == Platform.NUMBER:
145 vol.Required(CONF_STATE): selector.TemplateSelector(),
146 vol.Required(CONF_MIN, default=DEFAULT_MIN_VALUE): selector.NumberSelector(
147 selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
149 vol.Required(CONF_MAX, default=DEFAULT_MAX_VALUE): selector.NumberSelector(
150 selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
152 vol.Required(CONF_STEP, default=DEFAULT_STEP): selector.NumberSelector(
153 selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
155 vol.Optional(CONF_UNIT_OF_MEASUREMENT): selector.TextSelector(
156 selector.TextSelectorConfig(
157 type=selector.TextSelectorType.TEXT, multiline=
False
160 vol.Required(CONF_SET_VALUE): selector.ActionSelector(),
163 if domain == Platform.SELECT:
164 schema |= _SCHEMA_STATE | {
165 vol.Required(CONF_OPTIONS): selector.TemplateSelector(),
166 vol.Optional(CONF_SELECT_OPTION): selector.ActionSelector(),
169 if domain == Platform.SENSOR:
170 schema |= _SCHEMA_STATE | {
171 vol.Optional(CONF_UNIT_OF_MEASUREMENT): selector.SelectSelector(
172 selector.SelectSelectorConfig(
176 for units
in DEVICE_CLASS_UNITS.values()
181 mode=selector.SelectSelectorMode.DROPDOWN,
182 translation_key=
"sensor_unit_of_measurement",
187 vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector(
188 selector.SelectSelectorConfig(
191 for cls
in SensorDeviceClass
192 if cls != SensorDeviceClass.ENUM
194 mode=selector.SelectSelectorMode.DROPDOWN,
195 translation_key=
"sensor_device_class",
199 vol.Optional(CONF_STATE_CLASS): selector.SelectSelector(
200 selector.SelectSelectorConfig(
201 options=[cls.value
for cls
in SensorStateClass],
202 mode=selector.SelectSelectorMode.DROPDOWN,
203 translation_key=
"sensor_state_class",
209 if domain == Platform.SWITCH:
211 vol.Optional(CONF_VALUE_TEMPLATE): selector.TemplateSelector(),
212 vol.Optional(CONF_TURN_ON): selector.ActionSelector(),
213 vol.Optional(CONF_TURN_OFF): selector.ActionSelector(),
216 schema[vol.Optional(CONF_DEVICE_ID)] = selector.DeviceSelector()
218 return vol.Schema(schema)
221 options_schema = partial(generate_schema, flow_type=
"options")
223 config_schema = partial(generate_schema, flow_type=
"config")
227 """Return next step_id for options flow according to template_type."""
228 return cast(str, options[
"template_type"])
232 """Validate unit of measurement."""
234 (device_class := options.get(CONF_DEVICE_CLASS))
235 and (units := DEVICE_CLASS_UNITS.get(device_class))
is not None
236 and (unit := options.get(CONF_UNIT_OF_MEASUREMENT))
not in units
238 sorted_units = sorted(
239 [f
"'{unit!s}'" if unit
else "no unit of measurement" for unit
in units],
242 if len(sorted_units) == 1:
243 units_string = sorted_units[0]
245 units_string = f
"one of {', '.join(sorted_units)}"
248 f
"'{unit}' is not a valid unit for device class '{device_class}'; "
249 f
"expected {units_string}"
254 """Validate state class."""
256 (state_class := options.get(CONF_STATE_CLASS))
257 and (device_class := options.get(CONF_DEVICE_CLASS))
258 and (state_classes := DEVICE_CLASS_STATE_CLASSES.get(device_class))
is not None
259 and state_class
not in state_classes
261 sorted_state_classes = sorted(
262 [f
"'{state_class!s}'" for state_class
in state_classes],
265 if len(sorted_state_classes) == 0:
266 state_classes_string =
"no state class"
267 elif len(sorted_state_classes) == 1:
268 state_classes_string = sorted_state_classes[0]
270 state_classes_string = f
"one of {', '.join(sorted_state_classes)}"
273 f
"'{state_class}' is not a valid state class for device class "
274 f
"'{device_class}'; expected {state_classes_string}"
281 [SchemaCommonFlowHandler, dict[str, Any]],
282 Coroutine[Any, Any, dict[str, Any]],
284 """Do post validation of user input.
286 For sensors: Validate unit of measurement.
287 For all domaines: Set template type.
290 async
def _validate_user_input(
291 _: SchemaCommonFlowHandler,
292 user_input: dict[str, Any],
294 """Add template type to user input."""
295 if template_type == Platform.SENSOR:
298 return {
"template_type": template_type} | user_input
300 return _validate_user_input
304 "alarm_control_panel",
395 CREATE_PREVIEW_ENTITY: dict[
397 Callable[[HomeAssistant, str, dict[str, Any]], TemplateEntity],
399 "binary_sensor": async_create_preview_binary_sensor,
400 "number": async_create_preview_number,
401 "sensor": async_create_preview_sensor,
402 "switch": async_create_preview_switch,
407 """Handle config flow for template helper."""
409 config_flow = CONFIG_FLOW
410 options_flow = OPTIONS_FLOW
414 """Return config entry title."""
415 return cast(str, options[
"name"])
419 """Set up preview WS API."""
420 websocket_api.async_register_command(hass, ws_start_preview)
423 @websocket_api.websocket_command(
{
vol.Required("type"):
"template/start_preview",
424 vol.Required(
"flow_id"): str,
425 vol.Required(
"flow_type"): vol.Any(
"config_flow",
"options_flow"),
426 vol.Required(
"user_input"): dict,
435 """Generate a preview."""
437 def _validate(schema: vol.Schema, domain: str, user_input: dict[str, Any]) -> Any:
440 for key, validator
in schema.schema.items():
441 if key.schema
not in user_input:
444 validator(user_input[key.schema])
445 except vol.Invalid
as ex:
446 errors[key.schema] =
str(ex.msg)
448 if domain == Platform.SENSOR:
451 except vol.Invalid
as ex:
452 errors[CONF_UNIT_OF_MEASUREMENT] =
str(ex.msg)
455 except vol.Invalid
as ex:
456 errors[CONF_STATE_CLASS] =
str(ex.msg)
460 entity_registry_entry: er.RegistryEntry |
None =
None
461 if msg[
"flow_type"] ==
"config_flow":
462 flow_status = hass.config_entries.flow.async_get(msg[
"flow_id"])
463 template_type = flow_status[
"step_id"]
464 form_step = cast(SchemaFlowFormStep, CONFIG_FLOW[template_type])
465 schema = cast(vol.Schema, form_step.schema)
466 name = msg[
"user_input"][
"name"]
468 flow_status = hass.config_entries.options.async_get(msg[
"flow_id"])
469 config_entry = hass.config_entries.async_get_entry(flow_status[
"handler"])
471 raise HomeAssistantError
472 template_type = config_entry.options[
"template_type"]
473 name = config_entry.options[
"name"]
474 schema = cast(vol.Schema, OPTIONS_FLOW[template_type].schema)
475 entity_registry = er.async_get(hass)
476 entries = er.async_entries_for_config_entry(
477 entity_registry, flow_status[
"handler"]
480 entity_registry_entry = entries[0]
482 errors =
_validate(schema, template_type, msg[
"user_input"])
485 def async_preview_updated(
487 attributes: Mapping[str, Any] |
None,
488 listeners: dict[str, bool | set[str]] |
None,
491 """Forward config entry state events to websocket."""
492 if error
is not None:
493 connection.send_message(
494 websocket_api.event_message(
500 connection.send_message(
501 websocket_api.event_message(
503 {
"attributes": attributes,
"listeners": listeners,
"state": state},
508 connection.send_message(
511 "type": websocket_api.TYPE_RESULT,
513 "error": {
"code":
"invalid_user_input",
"message": errors},
518 preview_entity = CREATE_PREVIEW_ENTITY[template_type](hass, name, msg[
"user_input"])
519 preview_entity.hass = hass
520 preview_entity.registry_entry = entity_registry_entry
522 connection.send_result(msg[
"id"])
523 connection.subscriptions[msg[
"id"]] = preview_entity.async_start_preview(
524 async_preview_updated
526
str async_config_entry_title(self, Mapping[str, Any] options)
None async_setup_preview(HomeAssistant hass)
None _validate_unit(dict[str, Any] options)
str choose_options_step(dict[str, Any] options)
None _validate_state_class(dict[str, Any] options)
Callable[[SchemaCommonFlowHandler, dict[str, Any]], Coroutine[Any, Any, dict[str, Any]],] validate_user_input(str template_type)
None ws_start_preview(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
vol.Schema generate_schema(str domain, str flow_type)
None _validate(Wallbox wallbox)