Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for the Template integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Coroutine, Mapping
6 from functools import partial
7 from typing import Any, cast
8 
9 import voluptuous as vol
10 
11 from homeassistant.components import websocket_api
12 from homeassistant.components.binary_sensor import BinarySensorDeviceClass
13 from homeassistant.components.button import ButtonDeviceClass
15  CONF_STATE_CLASS,
16  DEVICE_CLASS_STATE_CLASSES,
17  DEVICE_CLASS_UNITS,
18  SensorDeviceClass,
19  SensorStateClass,
20 )
21 from homeassistant.const import (
22  CONF_DEVICE_CLASS,
23  CONF_DEVICE_ID,
24  CONF_NAME,
25  CONF_STATE,
26  CONF_UNIT_OF_MEASUREMENT,
27  CONF_URL,
28  CONF_VALUE_TEMPLATE,
29  CONF_VERIFY_SSL,
30  Platform,
31 )
32 from homeassistant.core import HomeAssistant, callback
33 from homeassistant.exceptions import HomeAssistantError
34 from homeassistant.helpers import entity_registry as er, selector
36  SchemaCommonFlowHandler,
37  SchemaConfigFlowHandler,
38  SchemaFlowFormStep,
39  SchemaFlowMenuStep,
40 )
41 
42 from .alarm_control_panel import (
43  CONF_ARM_AWAY_ACTION,
44  CONF_ARM_CUSTOM_BYPASS_ACTION,
45  CONF_ARM_HOME_ACTION,
46  CONF_ARM_NIGHT_ACTION,
47  CONF_ARM_VACATION_ACTION,
48  CONF_CODE_ARM_REQUIRED,
49  CONF_CODE_FORMAT,
50  CONF_DISARM_ACTION,
51  CONF_TRIGGER_ACTION,
52  TemplateCodeFormat,
53 )
54 from .binary_sensor import async_create_preview_binary_sensor
55 from .const import CONF_PRESS, CONF_TURN_OFF, CONF_TURN_ON, DOMAIN
56 from .number import (
57  CONF_MAX,
58  CONF_MIN,
59  CONF_SET_VALUE,
60  CONF_STEP,
61  DEFAULT_MAX_VALUE,
62  DEFAULT_MIN_VALUE,
63  DEFAULT_STEP,
64  async_create_preview_number,
65 )
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
70 
71 _SCHEMA_STATE: dict[vol.Marker, Any] = {
72  vol.Required(CONF_STATE): selector.TemplateSelector(),
73 }
74 
75 
76 def generate_schema(domain: str, flow_type: str) -> vol.Schema:
77  """Generate schema."""
78  schema: dict[vol.Marker, Any] = {}
79 
80  if flow_type == "config":
81  schema = {vol.Required(CONF_NAME): selector.TextSelector()}
82 
83  if domain == Platform.ALARM_CONTROL_PANEL:
84  schema |= {
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(),
93  vol.Optional(
94  CONF_CODE_ARM_REQUIRED, default=True
95  ): selector.BooleanSelector(),
96  vol.Optional(
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",
103  )
104  ),
105  }
106 
107  if domain == Platform.BINARY_SENSOR:
108  schema |= _SCHEMA_STATE
109  if flow_type == "config":
110  schema |= {
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",
116  sort=True,
117  ),
118  ),
119  }
120 
121  if domain == Platform.BUTTON:
122  schema |= {
123  vol.Optional(CONF_PRESS): selector.ActionSelector(),
124  }
125  if flow_type == "config":
126  schema |= {
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",
132  sort=True,
133  ),
134  )
135  }
136 
137  if domain == Platform.IMAGE:
138  schema |= {
139  vol.Required(CONF_URL): selector.TemplateSelector(),
140  vol.Optional(CONF_VERIFY_SSL, default=True): selector.BooleanSelector(),
141  }
142 
143  if domain == Platform.NUMBER:
144  schema |= {
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),
148  ),
149  vol.Required(CONF_MAX, default=DEFAULT_MAX_VALUE): selector.NumberSelector(
150  selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
151  ),
152  vol.Required(CONF_STEP, default=DEFAULT_STEP): selector.NumberSelector(
153  selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
154  ),
155  vol.Optional(CONF_UNIT_OF_MEASUREMENT): selector.TextSelector(
156  selector.TextSelectorConfig(
157  type=selector.TextSelectorType.TEXT, multiline=False
158  )
159  ),
160  vol.Required(CONF_SET_VALUE): selector.ActionSelector(),
161  }
162 
163  if domain == Platform.SELECT:
164  schema |= _SCHEMA_STATE | {
165  vol.Required(CONF_OPTIONS): selector.TemplateSelector(),
166  vol.Optional(CONF_SELECT_OPTION): selector.ActionSelector(),
167  }
168 
169  if domain == Platform.SENSOR:
170  schema |= _SCHEMA_STATE | {
171  vol.Optional(CONF_UNIT_OF_MEASUREMENT): selector.SelectSelector(
172  selector.SelectSelectorConfig(
173  options=list(
174  {
175  str(unit)
176  for units in DEVICE_CLASS_UNITS.values()
177  for unit in units
178  if unit is not None
179  }
180  ),
181  mode=selector.SelectSelectorMode.DROPDOWN,
182  translation_key="sensor_unit_of_measurement",
183  custom_value=True,
184  sort=True,
185  ),
186  ),
187  vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector(
188  selector.SelectSelectorConfig(
189  options=[
190  cls.value
191  for cls in SensorDeviceClass
192  if cls != SensorDeviceClass.ENUM
193  ],
194  mode=selector.SelectSelectorMode.DROPDOWN,
195  translation_key="sensor_device_class",
196  sort=True,
197  ),
198  ),
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",
204  sort=True,
205  ),
206  ),
207  }
208 
209  if domain == Platform.SWITCH:
210  schema |= {
211  vol.Optional(CONF_VALUE_TEMPLATE): selector.TemplateSelector(),
212  vol.Optional(CONF_TURN_ON): selector.ActionSelector(),
213  vol.Optional(CONF_TURN_OFF): selector.ActionSelector(),
214  }
215 
216  schema[vol.Optional(CONF_DEVICE_ID)] = selector.DeviceSelector()
217 
218  return vol.Schema(schema)
219 
220 
221 options_schema = partial(generate_schema, flow_type="options")
222 
223 config_schema = partial(generate_schema, flow_type="config")
224 
225 
226 async def choose_options_step(options: dict[str, Any]) -> str:
227  """Return next step_id for options flow according to template_type."""
228  return cast(str, options["template_type"])
229 
230 
231 def _validate_unit(options: dict[str, Any]) -> None:
232  """Validate unit of measurement."""
233  if (
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
237  ):
238  sorted_units = sorted(
239  [f"'{unit!s}'" if unit else "no unit of measurement" for unit in units],
240  key=str.casefold,
241  )
242  if len(sorted_units) == 1:
243  units_string = sorted_units[0]
244  else:
245  units_string = f"one of {', '.join(sorted_units)}"
246 
247  raise vol.Invalid(
248  f"'{unit}' is not a valid unit for device class '{device_class}'; "
249  f"expected {units_string}"
250  )
251 
252 
253 def _validate_state_class(options: dict[str, Any]) -> None:
254  """Validate state class."""
255  if (
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
260  ):
261  sorted_state_classes = sorted(
262  [f"'{state_class!s}'" for state_class in state_classes],
263  key=str.casefold,
264  )
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]
269  else:
270  state_classes_string = f"one of {', '.join(sorted_state_classes)}"
271 
272  raise vol.Invalid(
273  f"'{state_class}' is not a valid state class for device class "
274  f"'{device_class}'; expected {state_classes_string}"
275  )
276 
277 
279  template_type: str,
280 ) -> Callable[
281  [SchemaCommonFlowHandler, dict[str, Any]],
282  Coroutine[Any, Any, dict[str, Any]],
283 ]:
284  """Do post validation of user input.
285 
286  For sensors: Validate unit of measurement.
287  For all domaines: Set template type.
288  """
289 
290  async def _validate_user_input(
291  _: SchemaCommonFlowHandler,
292  user_input: dict[str, Any],
293  ) -> dict[str, Any]:
294  """Add template type to user input."""
295  if template_type == Platform.SENSOR:
296  _validate_unit(user_input)
297  _validate_state_class(user_input)
298  return {"template_type": template_type} | user_input
299 
300  return _validate_user_input
301 
302 
303 TEMPLATE_TYPES = [
304  "alarm_control_panel",
305  "binary_sensor",
306  "button",
307  "image",
308  "number",
309  "select",
310  "sensor",
311  "switch",
312 ]
313 
314 CONFIG_FLOW = {
315  "user": SchemaFlowMenuStep(TEMPLATE_TYPES),
316  Platform.ALARM_CONTROL_PANEL: SchemaFlowFormStep(
317  config_schema(Platform.ALARM_CONTROL_PANEL),
318  validate_user_input=validate_user_input(Platform.ALARM_CONTROL_PANEL),
319  ),
320  Platform.BINARY_SENSOR: SchemaFlowFormStep(
321  config_schema(Platform.BINARY_SENSOR),
322  preview="template",
323  validate_user_input=validate_user_input(Platform.BINARY_SENSOR),
324  ),
325  Platform.BUTTON: SchemaFlowFormStep(
326  config_schema(Platform.BUTTON),
327  validate_user_input=validate_user_input(Platform.BUTTON),
328  ),
329  Platform.IMAGE: SchemaFlowFormStep(
330  config_schema(Platform.IMAGE),
331  validate_user_input=validate_user_input(Platform.IMAGE),
332  ),
333  Platform.NUMBER: SchemaFlowFormStep(
334  config_schema(Platform.NUMBER),
335  preview="template",
336  validate_user_input=validate_user_input(Platform.NUMBER),
337  ),
338  Platform.SELECT: SchemaFlowFormStep(
339  config_schema(Platform.SELECT),
340  validate_user_input=validate_user_input(Platform.SELECT),
341  ),
342  Platform.SENSOR: SchemaFlowFormStep(
343  config_schema(Platform.SENSOR),
344  preview="template",
345  validate_user_input=validate_user_input(Platform.SENSOR),
346  ),
347  Platform.SWITCH: SchemaFlowFormStep(
348  config_schema(Platform.SWITCH),
349  preview="template",
350  validate_user_input=validate_user_input(Platform.SWITCH),
351  ),
352 }
353 
354 
355 OPTIONS_FLOW = {
356  "init": SchemaFlowFormStep(next_step=choose_options_step),
357  Platform.ALARM_CONTROL_PANEL: SchemaFlowFormStep(
358  options_schema(Platform.ALARM_CONTROL_PANEL),
359  validate_user_input=validate_user_input(Platform.ALARM_CONTROL_PANEL),
360  ),
361  Platform.BINARY_SENSOR: SchemaFlowFormStep(
362  options_schema(Platform.BINARY_SENSOR),
363  preview="template",
364  validate_user_input=validate_user_input(Platform.BINARY_SENSOR),
365  ),
366  Platform.BUTTON: SchemaFlowFormStep(
367  options_schema(Platform.BUTTON),
368  validate_user_input=validate_user_input(Platform.BUTTON),
369  ),
370  Platform.IMAGE: SchemaFlowFormStep(
371  options_schema(Platform.IMAGE),
372  validate_user_input=validate_user_input(Platform.IMAGE),
373  ),
374  Platform.NUMBER: SchemaFlowFormStep(
375  options_schema(Platform.NUMBER),
376  preview="template",
377  validate_user_input=validate_user_input(Platform.NUMBER),
378  ),
379  Platform.SELECT: SchemaFlowFormStep(
380  options_schema(Platform.SELECT),
381  validate_user_input=validate_user_input(Platform.SELECT),
382  ),
383  Platform.SENSOR: SchemaFlowFormStep(
384  options_schema(Platform.SENSOR),
385  preview="template",
386  validate_user_input=validate_user_input(Platform.SENSOR),
387  ),
388  Platform.SWITCH: SchemaFlowFormStep(
389  options_schema(Platform.SWITCH),
390  preview="template",
391  validate_user_input=validate_user_input(Platform.SWITCH),
392  ),
393 }
394 
395 CREATE_PREVIEW_ENTITY: dict[
396  str,
397  Callable[[HomeAssistant, str, dict[str, Any]], TemplateEntity],
398 ] = {
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,
403 }
404 
405 
407  """Handle config flow for template helper."""
408 
409  config_flow = CONFIG_FLOW
410  options_flow = OPTIONS_FLOW
411 
412  @callback
413  def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
414  """Return config entry title."""
415  return cast(str, options["name"])
416 
417  @staticmethod
418  async def async_setup_preview(hass: HomeAssistant) -> None:
419  """Set up preview WS API."""
420  websocket_api.async_register_command(hass, ws_start_preview)
421 
422 
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,
427  }
428 )
429 @callback
430 def ws_start_preview(
431  hass: HomeAssistant,
433  msg: dict[str, Any],
434 ) -> None:
435  """Generate a preview."""
436 
437  def _validate(schema: vol.Schema, domain: str, user_input: dict[str, Any]) -> Any:
438  errors = {}
439  key: vol.Marker
440  for key, validator in schema.schema.items():
441  if key.schema not in user_input:
442  continue
443  try:
444  validator(user_input[key.schema])
445  except vol.Invalid as ex:
446  errors[key.schema] = str(ex.msg)
447 
448  if domain == Platform.SENSOR:
449  try:
450  _validate_unit(user_input)
451  except vol.Invalid as ex:
452  errors[CONF_UNIT_OF_MEASUREMENT] = str(ex.msg)
453  try:
454  _validate_state_class(user_input)
455  except vol.Invalid as ex:
456  errors[CONF_STATE_CLASS] = str(ex.msg)
457 
458  return errors
459 
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"]
467  else:
468  flow_status = hass.config_entries.options.async_get(msg["flow_id"])
469  config_entry = hass.config_entries.async_get_entry(flow_status["handler"])
470  if not config_entry:
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"]
478  )
479  if entries:
480  entity_registry_entry = entries[0]
481 
482  errors = _validate(schema, template_type, msg["user_input"])
483 
484  @callback
485  def async_preview_updated(
486  state: str | None,
487  attributes: Mapping[str, Any] | None,
488  listeners: dict[str, bool | set[str]] | None,
489  error: str | None,
490  ) -> None:
491  """Forward config entry state events to websocket."""
492  if error is not None:
493  connection.send_message(
494  websocket_api.event_message(
495  msg["id"],
496  {"error": error},
497  )
498  )
499  return
500  connection.send_message(
501  websocket_api.event_message(
502  msg["id"],
503  {"attributes": attributes, "listeners": listeners, "state": state},
504  )
505  )
506 
507  if errors:
508  connection.send_message(
509  {
510  "id": msg["id"],
511  "type": websocket_api.TYPE_RESULT,
512  "success": False,
513  "error": {"code": "invalid_user_input", "message": errors},
514  }
515  )
516  return
517 
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
521 
522  connection.send_result(msg["id"])
523  connection.subscriptions[msg["id"]] = preview_entity.async_start_preview(
524  async_preview_updated
525  )
526 
None _validate_unit(dict[str, Any] options)
Definition: config_flow.py:231
str choose_options_step(dict[str, Any] options)
Definition: config_flow.py:226
None _validate_state_class(dict[str, Any] options)
Definition: config_flow.py:253
Callable[[SchemaCommonFlowHandler, dict[str, Any]], Coroutine[Any, Any, dict[str, Any]],] validate_user_input(str template_type)
Definition: config_flow.py:283
None ws_start_preview(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: config_flow.py:436
vol.Schema generate_schema(str domain, str flow_type)
Definition: config_flow.py:76