Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Group 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.const import CONF_ENTITIES, CONF_TYPE
13 from homeassistant.core import HomeAssistant, callback
14 from homeassistant.exceptions import HomeAssistantError
15 from homeassistant.helpers import entity_registry as er, selector
17  SchemaCommonFlowHandler,
18  SchemaConfigFlowHandler,
19  SchemaFlowFormStep,
20  SchemaFlowMenuStep,
21  SchemaOptionsFlowHandler,
22  entity_selector_without_own_entities,
23 )
24 
25 from .binary_sensor import CONF_ALL, async_create_preview_binary_sensor
26 from .button import async_create_preview_button
27 from .const import CONF_HIDE_MEMBERS, CONF_IGNORE_NON_NUMERIC, DOMAIN
28 from .cover import async_create_preview_cover
29 from .entity import GroupEntity
30 from .event import async_create_preview_event
31 from .fan import async_create_preview_fan
32 from .light import async_create_preview_light
33 from .lock import async_create_preview_lock
34 from .media_player import MediaPlayerGroup, async_create_preview_media_player
35 from .notify import async_create_preview_notify
36 from .sensor import async_create_preview_sensor
37 from .switch import async_create_preview_switch
38 
39 _STATISTIC_MEASURES = [
40  "last",
41  "max",
42  "mean",
43  "median",
44  "min",
45  "product",
46  "range",
47  "stdev",
48  "sum",
49 ]
50 
51 
53  domain: str | list[str], handler: SchemaCommonFlowHandler | None
54 ) -> vol.Schema:
55  """Generate options schema."""
56  entity_selector: selector.Selector[Any] | vol.Schema
57  if handler is None:
58  entity_selector = selector.selector(
59  {"entity": {"domain": domain, "multiple": True}}
60  )
61  else:
62  entity_selector = entity_selector_without_own_entities(
63  cast(SchemaOptionsFlowHandler, handler.parent_handler),
64  selector.EntitySelectorConfig(domain=domain, multiple=True),
65  )
66 
67  return vol.Schema(
68  {
69  vol.Required(CONF_ENTITIES): entity_selector,
70  vol.Required(CONF_HIDE_MEMBERS, default=False): selector.BooleanSelector(),
71  }
72  )
73 
74 
75 def basic_group_config_schema(domain: str | list[str]) -> vol.Schema:
76  """Generate config schema."""
77  return vol.Schema(
78  {
79  vol.Required("name"): selector.TextSelector(),
80  vol.Required(CONF_ENTITIES): selector.EntitySelector(
81  selector.EntitySelectorConfig(domain=domain, multiple=True),
82  ),
83  vol.Required(CONF_HIDE_MEMBERS, default=False): selector.BooleanSelector(),
84  }
85  )
86 
87 
89  handler: SchemaCommonFlowHandler | None,
90 ) -> vol.Schema:
91  """Generate options schema."""
92  return (await basic_group_options_schema("binary_sensor", handler)).extend(
93  {
94  vol.Required(CONF_ALL, default=False): selector.BooleanSelector(),
95  }
96  )
97 
98 
99 BINARY_SENSOR_CONFIG_SCHEMA = basic_group_config_schema("binary_sensor").extend(
100  {
101  vol.Required(CONF_ALL, default=False): selector.BooleanSelector(),
102  }
103 )
104 
105 SENSOR_CONFIG_EXTENDS = {
106  vol.Required(CONF_TYPE): selector.SelectSelector(
107  selector.SelectSelectorConfig(
108  options=_STATISTIC_MEASURES, translation_key=CONF_TYPE
109  ),
110  ),
111 }
112 SENSOR_OPTIONS = {
113  vol.Optional(CONF_IGNORE_NON_NUMERIC, default=False): selector.BooleanSelector(),
114  vol.Required(CONF_TYPE): selector.SelectSelector(
115  selector.SelectSelectorConfig(
116  options=_STATISTIC_MEASURES, translation_key=CONF_TYPE
117  ),
118  ),
119 }
120 
121 
123  domain: str, handler: SchemaCommonFlowHandler | None
124 ) -> vol.Schema:
125  """Generate options schema."""
126  return (
127  await basic_group_options_schema(["sensor", "number", "input_number"], handler)
128  ).extend(SENSOR_OPTIONS)
129 
130 
131 SENSOR_CONFIG_SCHEMA = basic_group_config_schema(
132  ["sensor", "number", "input_number"]
133 ).extend(SENSOR_CONFIG_EXTENDS)
134 
135 
137  domain: str, handler: SchemaCommonFlowHandler | None
138 ) -> vol.Schema:
139  """Generate options schema."""
140  return (await basic_group_options_schema(domain, handler)).extend(
141  {
142  vol.Required(
143  CONF_ALL, default=False, description={"advanced": True}
144  ): selector.BooleanSelector(),
145  }
146  )
147 
148 
149 GROUP_TYPES = [
150  "binary_sensor",
151  "button",
152  "cover",
153  "event",
154  "fan",
155  "light",
156  "lock",
157  "media_player",
158  "notify",
159  "sensor",
160  "switch",
161 ]
162 
163 
164 async def choose_options_step(options: dict[str, Any]) -> str:
165  """Return next step_id for options flow according to group_type."""
166  return cast(str, options["group_type"])
167 
168 
170  group_type: str,
171 ) -> Callable[
172  [SchemaCommonFlowHandler, dict[str, Any]], Coroutine[Any, Any, dict[str, Any]]
173 ]:
174  """Set group type."""
175 
176  async def _set_group_type(
177  handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
178  ) -> dict[str, Any]:
179  """Add group type to user input."""
180  return {"group_type": group_type, **user_input}
181 
182  return _set_group_type
183 
184 
185 CONFIG_FLOW = {
186  "user": SchemaFlowMenuStep(GROUP_TYPES),
187  "binary_sensor": SchemaFlowFormStep(
188  BINARY_SENSOR_CONFIG_SCHEMA,
189  preview="group",
190  validate_user_input=set_group_type("binary_sensor"),
191  ),
192  "button": SchemaFlowFormStep(
193  basic_group_config_schema("button"),
194  preview="group",
195  validate_user_input=set_group_type("button"),
196  ),
197  "cover": SchemaFlowFormStep(
198  basic_group_config_schema("cover"),
199  preview="group",
200  validate_user_input=set_group_type("cover"),
201  ),
202  "event": SchemaFlowFormStep(
203  basic_group_config_schema("event"),
204  preview="group",
205  validate_user_input=set_group_type("event"),
206  ),
207  "fan": SchemaFlowFormStep(
209  preview="group",
210  validate_user_input=set_group_type("fan"),
211  ),
212  "light": SchemaFlowFormStep(
213  basic_group_config_schema("light"),
214  preview="group",
215  validate_user_input=set_group_type("light"),
216  ),
217  "lock": SchemaFlowFormStep(
219  preview="group",
220  validate_user_input=set_group_type("lock"),
221  ),
222  "media_player": SchemaFlowFormStep(
223  basic_group_config_schema("media_player"),
224  preview="group",
225  validate_user_input=set_group_type("media_player"),
226  ),
227  "notify": SchemaFlowFormStep(
228  basic_group_config_schema("notify"),
229  preview="group",
230  validate_user_input=set_group_type("notify"),
231  ),
232  "sensor": SchemaFlowFormStep(
233  SENSOR_CONFIG_SCHEMA,
234  preview="group",
235  validate_user_input=set_group_type("sensor"),
236  ),
237  "switch": SchemaFlowFormStep(
238  basic_group_config_schema("switch"),
239  preview="group",
240  validate_user_input=set_group_type("switch"),
241  ),
242 }
243 
244 
245 OPTIONS_FLOW = {
246  "init": SchemaFlowFormStep(next_step=choose_options_step),
247  "binary_sensor": SchemaFlowFormStep(
248  binary_sensor_options_schema,
249  preview="group",
250  ),
251  "button": SchemaFlowFormStep(
252  partial(basic_group_options_schema, "button"),
253  preview="group",
254  ),
255  "cover": SchemaFlowFormStep(
256  partial(basic_group_options_schema, "cover"),
257  preview="group",
258  ),
259  "event": SchemaFlowFormStep(
260  partial(basic_group_options_schema, "event"),
261  preview="group",
262  ),
263  "fan": SchemaFlowFormStep(
264  partial(basic_group_options_schema, "fan"),
265  preview="group",
266  ),
267  "light": SchemaFlowFormStep(
268  partial(light_switch_options_schema, "light"),
269  preview="group",
270  ),
271  "lock": SchemaFlowFormStep(
272  partial(basic_group_options_schema, "lock"),
273  preview="group",
274  ),
275  "media_player": SchemaFlowFormStep(
276  partial(basic_group_options_schema, "media_player"),
277  preview="group",
278  ),
279  "notify": SchemaFlowFormStep(
280  partial(basic_group_options_schema, "notify"),
281  preview="group",
282  ),
283  "sensor": SchemaFlowFormStep(
284  partial(sensor_options_schema, "sensor"),
285  preview="group",
286  ),
287  "switch": SchemaFlowFormStep(
288  partial(light_switch_options_schema, "switch"),
289  preview="group",
290  ),
291 }
292 
293 PREVIEW_OPTIONS_SCHEMA: dict[str, vol.Schema] = {}
294 
295 CREATE_PREVIEW_ENTITY: dict[
296  str,
297  Callable[[HomeAssistant, str, dict[str, Any]], GroupEntity | MediaPlayerGroup],
298 ] = {
299  "binary_sensor": async_create_preview_binary_sensor,
300  "button": async_create_preview_button,
301  "cover": async_create_preview_cover,
302  "event": async_create_preview_event,
303  "fan": async_create_preview_fan,
304  "light": async_create_preview_light,
305  "lock": async_create_preview_lock,
306  "media_player": async_create_preview_media_player,
307  "notify": async_create_preview_notify,
308  "sensor": async_create_preview_sensor,
309  "switch": async_create_preview_switch,
310 }
311 
312 
314  """Handle a config or options flow for groups."""
315 
316  config_flow = CONFIG_FLOW
317  options_flow = OPTIONS_FLOW
318 
319  @callback
320  def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
321  """Return config entry title.
322 
323  The options parameter contains config entry options, which is the union of user
324  input from the config flow steps.
325  """
326  return cast(str, options["name"]) if "name" in options else ""
327 
328  @callback
329  def async_config_flow_finished(self, options: Mapping[str, Any]) -> None:
330  """Hide the group members if requested."""
331  if options[CONF_HIDE_MEMBERS]:
333  self.hass, options[CONF_ENTITIES], er.RegistryEntryHider.INTEGRATION
334  )
335 
336  @callback
337  @staticmethod
339  hass: HomeAssistant, options: Mapping[str, Any]
340  ) -> None:
341  """Hide or unhide the group members as requested."""
342  hidden_by = (
343  er.RegistryEntryHider.INTEGRATION if options[CONF_HIDE_MEMBERS] else None
344  )
345  _async_hide_members(hass, options[CONF_ENTITIES], hidden_by)
346 
347  @staticmethod
348  async def async_setup_preview(hass: HomeAssistant) -> None:
349  """Set up preview WS API."""
350  for group_type, form_step in OPTIONS_FLOW.items():
351  if group_type not in GROUP_TYPES:
352  continue
353  schema = cast(
354  Callable[
355  [SchemaCommonFlowHandler | None], Coroutine[Any, Any, vol.Schema]
356  ],
357  form_step.schema,
358  )
359  PREVIEW_OPTIONS_SCHEMA[group_type] = await schema(None)
360  websocket_api.async_register_command(hass, ws_start_preview)
361 
362 
364  hass: HomeAssistant, members: list[str], hidden_by: er.RegistryEntryHider | None
365 ) -> None:
366  """Hide or unhide group members."""
367  registry = er.async_get(hass)
368  for member in members:
369  if not (entity_id := er.async_resolve_entity_id(registry, member)):
370  continue
371  if entity_id not in registry.entities:
372  continue
373  registry.async_update_entity(entity_id, hidden_by=hidden_by)
374 
375 
376 @websocket_api.websocket_command( { vol.Required("type"): "group/start_preview",
377  vol.Required("flow_id"): str,
378  vol.Required("flow_type"): vol.Any("config_flow", "options_flow"),
379  vol.Required("user_input"): dict,
380  }
381 )
382 @callback
383 def ws_start_preview(
384  hass: HomeAssistant,
386  msg: dict[str, Any],
387 ) -> None:
388  """Generate a preview."""
389  entity_registry_entry: er.RegistryEntry | None = None
390  if msg["flow_type"] == "config_flow":
391  flow_status = hass.config_entries.flow.async_get(msg["flow_id"])
392  group_type = flow_status["step_id"]
393  form_step = cast(SchemaFlowFormStep, CONFIG_FLOW[group_type])
394  schema = cast(vol.Schema, form_step.schema)
395  validated = schema(msg["user_input"])
396  name = validated["name"]
397  else:
398  flow_status = hass.config_entries.options.async_get(msg["flow_id"])
399  config_entry_id = flow_status["handler"]
400  config_entry = hass.config_entries.async_get_entry(config_entry_id)
401  if not config_entry:
402  raise HomeAssistantError
403  group_type = config_entry.options["group_type"]
404  name = config_entry.options["name"]
405  validated = PREVIEW_OPTIONS_SCHEMA[group_type](msg["user_input"])
406  entity_registry = er.async_get(hass)
407  entries = er.async_entries_for_config_entry(entity_registry, config_entry_id)
408  if entries:
409  entity_registry_entry = entries[0]
410 
411  @callback
412  def async_preview_updated(state: str, attributes: Mapping[str, Any]) -> None:
413  """Forward config entry state events to websocket."""
414  connection.send_message(
415  websocket_api.event_message(
416  msg["id"], {"attributes": attributes, "state": state}
417  )
418  )
419 
420  preview_entity: GroupEntity | MediaPlayerGroup = CREATE_PREVIEW_ENTITY[group_type](
421  hass, name, validated
422  )
423  preview_entity.hass = hass
424  preview_entity.registry_entry = entity_registry_entry
425 
426  connection.send_result(msg["id"])
427  connection.subscriptions[msg["id"]] = preview_entity.async_start_preview(
428  async_preview_updated
429  )
430 
None async_options_flow_finished(HomeAssistant hass, Mapping[str, Any] options)
Definition: config_flow.py:340
None async_config_flow_finished(self, Mapping[str, Any] options)
Definition: config_flow.py:329
str async_config_entry_title(self, Mapping[str, Any] options)
Definition: config_flow.py:320
None _async_hide_members(HomeAssistant hass, list[str] members, er.RegistryEntryHider|None hidden_by)
Definition: config_flow.py:365
vol.Schema basic_group_options_schema(str|list[str] domain, SchemaCommonFlowHandler|None handler)
Definition: config_flow.py:54
vol.Schema light_switch_options_schema(str domain, SchemaCommonFlowHandler|None handler)
Definition: config_flow.py:138
vol.Schema sensor_options_schema(str domain, SchemaCommonFlowHandler|None handler)
Definition: config_flow.py:124
Callable[[SchemaCommonFlowHandler, dict[str, Any]], Coroutine[Any, Any, dict[str, Any]]] set_group_type(str group_type)
Definition: config_flow.py:173
vol.Schema basic_group_config_schema(str|list[str] domain)
Definition: config_flow.py:75
str choose_options_step(dict[str, Any] options)
Definition: config_flow.py:164
None ws_start_preview(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: config_flow.py:389
vol.Schema binary_sensor_options_schema(SchemaCommonFlowHandler|None handler)
Definition: config_flow.py:90
selector.EntitySelector entity_selector_without_own_entities(SchemaOptionsFlowHandler handler, selector.EntitySelectorConfig entity_selector_config)