Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for HomeKit integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Iterable
6 from copy import deepcopy
7 from operator import itemgetter
8 import random
9 import re
10 import string
11 from typing import Any, Final, TypedDict
12 
13 import voluptuous as vol
14 
15 from homeassistant.components import device_automation
16 from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
17 from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
18 from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
19 from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN
20 from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN
21 from homeassistant.config_entries import (
22  SOURCE_IMPORT,
23  ConfigEntry,
24  ConfigFlow,
25  ConfigFlowResult,
26  OptionsFlow,
27 )
28 from homeassistant.const import (
29  ATTR_FRIENDLY_NAME,
30  CONF_DEVICES,
31  CONF_DOMAINS,
32  CONF_ENTITIES,
33  CONF_ENTITY_ID,
34  CONF_NAME,
35  CONF_PORT,
36 )
37 from homeassistant.core import HomeAssistant, callback, split_entity_id
38 from homeassistant.helpers import (
39  config_validation as cv,
40  device_registry as dr,
41  entity_registry as er,
42  selector,
43 )
44 from homeassistant.loader import async_get_integrations
45 
46 from .const import (
47  CONF_ENTITY_CONFIG,
48  CONF_EXCLUDE_ACCESSORY_MODE,
49  CONF_FILTER,
50  CONF_HOMEKIT_MODE,
51  CONF_SUPPORT_AUDIO,
52  CONF_VIDEO_CODEC,
53  DEFAULT_CONFIG_FLOW_PORT,
54  DEFAULT_HOMEKIT_MODE,
55  DOMAIN,
56  HOMEKIT_MODE_ACCESSORY,
57  HOMEKIT_MODE_BRIDGE,
58  HOMEKIT_MODES,
59  SHORT_BRIDGE_NAME,
60  VIDEO_CODEC_COPY,
61 )
62 from .util import async_find_next_available_port, state_needs_accessory_mode
63 
64 CONF_CAMERA_AUDIO = "camera_audio"
65 CONF_CAMERA_COPY = "camera_copy"
66 CONF_INCLUDE_EXCLUDE_MODE = "include_exclude_mode"
67 
68 MODE_INCLUDE = "include"
69 MODE_EXCLUDE = "exclude"
70 
71 INCLUDE_EXCLUDE_MODES = [MODE_EXCLUDE, MODE_INCLUDE]
72 
73 DOMAINS_NEED_ACCESSORY_MODE = {
74  CAMERA_DOMAIN,
75  LOCK_DOMAIN,
76  MEDIA_PLAYER_DOMAIN,
77  REMOTE_DOMAIN,
78 }
79 NEVER_BRIDGED_DOMAINS = {CAMERA_DOMAIN}
80 
81 CAMERA_ENTITY_PREFIX = f"{CAMERA_DOMAIN}."
82 
83 SUPPORTED_DOMAINS = [
84  "alarm_control_panel",
85  "automation",
86  "binary_sensor",
87  "button",
88  CAMERA_DOMAIN,
89  "climate",
90  "cover",
91  "demo",
92  "device_tracker",
93  "fan",
94  "humidifier",
95  "input_boolean",
96  "input_button",
97  "input_select",
98  "light",
99  "lock",
100  MEDIA_PLAYER_DOMAIN,
101  "person",
102  REMOTE_DOMAIN,
103  "scene",
104  "script",
105  "select",
106  "sensor",
107  "switch",
108  "vacuum",
109  "water_heater",
110  VALVE_DOMAIN,
111 ]
112 
113 DEFAULT_DOMAINS = [
114  "alarm_control_panel",
115  "climate",
116  CAMERA_DOMAIN,
117  "cover",
118  "humidifier",
119  "fan",
120  "light",
121  "lock",
122  MEDIA_PLAYER_DOMAIN,
123  REMOTE_DOMAIN,
124  "switch",
125  "vacuum",
126  "water_heater",
127 ]
128 
129 CONF_INCLUDE_DOMAINS: Final = "include_domains"
130 CONF_INCLUDE_ENTITIES: Final = "include_entities"
131 CONF_EXCLUDE_DOMAINS: Final = "exclude_domains"
132 CONF_EXCLUDE_ENTITIES: Final = "exclude_entities"
133 
134 
135 class EntityFilterDict(TypedDict, total=False):
136  """Entity filter dict."""
137 
138  include_domains: list[str]
139  include_entities: list[str]
140  exclude_domains: list[str]
141  exclude_entities: list[str]
142 
143 
145  include_domains: list[str] | None = None,
146  include_entities: list[str] | None = None,
147  exclude_domains: list[str] | None = None,
148  exclude_entities: list[str] | None = None,
149 ) -> EntityFilterDict:
150  """Create a filter dict."""
151  return EntityFilterDict(
152  include_domains=include_domains or [],
153  include_entities=include_entities or [],
154  exclude_domains=exclude_domains or [],
155  exclude_entities=exclude_entities or [],
156  )
157 
158 
159 async def _async_domain_names(hass: HomeAssistant, domains: list[str]) -> str:
160  """Build a list of integration names from domains."""
161  name_to_type_map = await _async_name_to_type_map(hass)
162  return ", ".join(
163  [name for domain, name in name_to_type_map.items() if domain in domains]
164  )
165 
166 
167 @callback
169  domains: list[str], entities: list[str]
170 ) -> EntityFilterDict:
171  """Build an entities filter from domains and entities."""
172  # Include all of the domain if there are no entities
173  # explicitly included as the user selected the domain
174  return _make_entity_filter(
175  include_domains=sorted(
176  set(domains).difference(_domains_set_from_entities(entities))
177  ),
178  include_entities=entities,
179  )
180 
181 
182 def _async_cameras_from_entities(entities: list[str]) -> list[str]:
183  return [
184  entity_id
185  for entity_id in entities
186  if entity_id.startswith(CAMERA_ENTITY_PREFIX)
187  ]
188 
189 
190 async def _async_name_to_type_map(hass: HomeAssistant) -> dict[str, str]:
191  """Create a mapping of types of devices/entities HomeKit can support."""
192  integrations = await async_get_integrations(hass, SUPPORTED_DOMAINS)
193  return {
194  domain: integration_or_exception.name
195  if (integration_or_exception := integrations[domain])
196  and not isinstance(integration_or_exception, Exception)
197  else domain
198  for domain in SUPPORTED_DOMAINS
199  }
200 
201 
202 class HomeKitConfigFlow(ConfigFlow, domain=DOMAIN):
203  """Handle a config flow for HomeKit."""
204 
205  VERSION = 1
206 
207  def __init__(self) -> None:
208  """Initialize config flow."""
209  self.hk_data: dict[str, Any] = {}
210 
211  async def async_step_user(
212  self, user_input: dict[str, Any] | None = None
213  ) -> ConfigFlowResult:
214  """Choose specific domains in bridge mode."""
215  if user_input is not None:
216  self.hk_data[CONF_FILTER] = _make_entity_filter(
217  include_domains=user_input[CONF_INCLUDE_DOMAINS]
218  )
219  return await self.async_step_pairingasync_step_pairing()
220 
221  self.hk_data[CONF_HOMEKIT_MODE] = HOMEKIT_MODE_BRIDGE
222  default_domains = (
223  [] if self._async_current_entries_async_current_entries(include_ignore=False) else DEFAULT_DOMAINS
224  )
225  name_to_type_map = await _async_name_to_type_map(self.hass)
226  return self.async_show_formasync_show_formasync_show_form(
227  step_id="user",
228  data_schema=vol.Schema(
229  {
230  vol.Required(
231  CONF_INCLUDE_DOMAINS, default=default_domains
232  ): cv.multi_select(name_to_type_map),
233  }
234  ),
235  )
236 
238  self, user_input: dict[str, Any] | None = None
239  ) -> ConfigFlowResult:
240  """Pairing instructions."""
241  hk_data = self.hk_data
242 
243  if user_input is not None:
244  port = async_find_next_available_port(self.hass, DEFAULT_CONFIG_FLOW_PORT)
245  await self._async_add_entries_for_accessory_mode_entities_async_add_entries_for_accessory_mode_entities(port)
246  hk_data[CONF_PORT] = port
247  conf_filter: EntityFilterDict = hk_data[CONF_FILTER]
248  conf_filter[CONF_INCLUDE_DOMAINS] = [
249  domain
250  for domain in conf_filter[CONF_INCLUDE_DOMAINS]
251  if domain not in NEVER_BRIDGED_DOMAINS
252  ]
253  return self.async_create_entryasync_create_entryasync_create_entry(
254  title=f"{hk_data[CONF_NAME]}:{hk_data[CONF_PORT]}",
255  data=hk_data,
256  )
257 
258  hk_data[CONF_NAME] = self._async_available_name_async_available_name(SHORT_BRIDGE_NAME)
259  hk_data[CONF_EXCLUDE_ACCESSORY_MODE] = True
260  return self.async_show_formasync_show_formasync_show_form(
261  step_id="pairing",
262  description_placeholders={CONF_NAME: hk_data[CONF_NAME]},
263  )
264 
266  self, last_assigned_port: int
267  ) -> None:
268  """Generate new flows for entities that need their own instances."""
269  accessory_mode_entity_ids = _async_get_entity_ids_for_accessory_mode(
270  self.hass, self.hk_data[CONF_FILTER][CONF_INCLUDE_DOMAINS]
271  )
272  exiting_entity_ids_accessory_mode = _async_entity_ids_with_accessory_mode(
273  self.hass
274  )
275  next_port_to_check = last_assigned_port + 1
276  for entity_id in accessory_mode_entity_ids:
277  if entity_id in exiting_entity_ids_accessory_mode:
278  continue
279  port = async_find_next_available_port(self.hass, next_port_to_check)
280  next_port_to_check = port + 1
281  self.hass.async_create_task(
282  self.hass.config_entries.flow.async_init(
283  DOMAIN,
284  context={"source": "accessory"},
285  data={CONF_ENTITY_ID: entity_id, CONF_PORT: port},
286  )
287  )
288 
290  self, accessory_input: dict[str, Any]
291  ) -> ConfigFlowResult:
292  """Handle creation a single accessory in accessory mode."""
293  entity_id = accessory_input[CONF_ENTITY_ID]
294  port = accessory_input[CONF_PORT]
295 
296  state = self.hass.states.get(entity_id)
297  assert state is not None
298  name = state.attributes.get(ATTR_FRIENDLY_NAME) or state.entity_id
299 
300  entry_data = {
301  CONF_PORT: port,
302  CONF_NAME: self._async_available_name_async_available_name(name),
303  CONF_HOMEKIT_MODE: HOMEKIT_MODE_ACCESSORY,
304  CONF_FILTER: _make_entity_filter(include_entities=[entity_id]),
305  }
306  if entity_id.startswith(CAMERA_ENTITY_PREFIX):
307  entry_data[CONF_ENTITY_CONFIG] = {
308  entity_id: {CONF_VIDEO_CODEC: VIDEO_CODEC_COPY}
309  }
310 
311  return self.async_create_entryasync_create_entryasync_create_entry(
312  title=f"{name}:{entry_data[CONF_PORT]}", data=entry_data
313  )
314 
315  async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
316  """Handle import from yaml."""
317  if not self._async_is_unique_name_port_async_is_unique_name_port(import_data):
318  return self.async_abortasync_abortasync_abort(reason="port_name_in_use")
319  return self.async_create_entryasync_create_entryasync_create_entry(
320  title=f"{import_data[CONF_NAME]}:{import_data[CONF_PORT]}", data=import_data
321  )
322 
323  @callback
324  def _async_current_names(self) -> set[str]:
325  """Return a set of bridge names."""
326  return {
327  entry.data[CONF_NAME]
328  for entry in self._async_current_entries_async_current_entries(include_ignore=False)
329  if CONF_NAME in entry.data
330  }
331 
332  @callback
333  def _async_available_name(self, requested_name: str) -> str:
334  """Return an available for the bridge."""
335  current_names = self._async_current_names_async_current_names()
336  valid_mdns_name = re.sub("[^A-Za-z0-9 ]+", " ", requested_name)
337 
338  if valid_mdns_name not in current_names:
339  return valid_mdns_name
340 
341  acceptable_mdns_chars = string.ascii_uppercase + string.digits
342  suggested_name: str | None = None
343  while not suggested_name or suggested_name in current_names:
344  trailer = "".join(random.choices(acceptable_mdns_chars, k=2))
345  suggested_name = f"{valid_mdns_name} {trailer}"
346 
347  return suggested_name
348 
349  @callback
350  def _async_is_unique_name_port(self, user_input: dict[str, Any]) -> bool:
351  """Determine is a name or port is already used."""
352  name = user_input[CONF_NAME]
353  port = user_input[CONF_PORT]
354  return not any(
355  entry.data[CONF_NAME] == name or entry.data[CONF_PORT] == port
356  for entry in self._async_current_entries_async_current_entries(include_ignore=False)
357  )
358 
359  @staticmethod
360  @callback
362  config_entry: ConfigEntry,
363  ) -> OptionsFlowHandler:
364  """Get the options flow for this handler."""
365  return OptionsFlowHandler()
366 
367 
369  """Handle a option flow for homekit."""
370 
371  def __init__(self) -> None:
372  """Initialize options flow."""
373  self.hk_optionshk_options: dict[str, Any] = {}
374  self.included_camerasincluded_cameras: list[str] = []
375 
376  async def async_step_yaml(
377  self, user_input: dict[str, Any] | None = None
378  ) -> ConfigFlowResult:
379  """No options for yaml managed entries."""
380  if user_input is not None:
381  # Apparently not possible to abort an options flow
382  # at the moment
383  return self.async_create_entryasync_create_entry(title="", data=self.config_entryconfig_entryconfig_entry.options)
384 
385  return self.async_show_formasync_show_form(step_id="yaml")
386 
388  self, user_input: dict[str, Any] | None = None
389  ) -> ConfigFlowResult:
390  """Choose advanced options."""
391  hk_options = self.hk_optionshk_options
392  show_advanced_options = self.show_advanced_optionsshow_advanced_options
393  bridge_mode = hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_BRIDGE
394 
395  if not show_advanced_options or user_input is not None or not bridge_mode:
396  if user_input:
397  hk_options.update(user_input)
398  if show_advanced_options and bridge_mode:
399  hk_options[CONF_DEVICES] = user_input[CONF_DEVICES]
400 
401  hk_options.pop(CONF_DOMAINS, None)
402  hk_options.pop(CONF_ENTITIES, None)
403  hk_options.pop(CONF_INCLUDE_EXCLUDE_MODE, None)
404  return self.async_create_entryasync_create_entry(title="", data=self.hk_optionshk_options)
405 
406  all_supported_devices = await _async_get_supported_devices(self.hass)
407  # Strip out devices that no longer exist to prevent error in the UI
408  devices = [
409  device_id
410  for device_id in self.hk_optionshk_options.get(CONF_DEVICES, [])
411  if device_id in all_supported_devices
412  ]
413  return self.async_show_formasync_show_form(
414  step_id="advanced",
415  data_schema=vol.Schema(
416  {
417  vol.Optional(CONF_DEVICES, default=devices): cv.multi_select(
418  all_supported_devices
419  )
420  }
421  ),
422  )
423 
425  self, user_input: dict[str, Any] | None = None
426  ) -> ConfigFlowResult:
427  """Choose camera config."""
428  hk_options = self.hk_optionshk_options
429  all_entity_config: dict[str, dict[str, Any]]
430 
431  if user_input is not None:
432  all_entity_config = hk_options[CONF_ENTITY_CONFIG]
433  for entity_id in self.included_camerasincluded_cameras:
434  entity_config = all_entity_config.setdefault(entity_id, {})
435 
436  if entity_id in user_input[CONF_CAMERA_COPY]:
437  entity_config[CONF_VIDEO_CODEC] = VIDEO_CODEC_COPY
438  elif CONF_VIDEO_CODEC in entity_config:
439  del entity_config[CONF_VIDEO_CODEC]
440 
441  if entity_id in user_input[CONF_CAMERA_AUDIO]:
442  entity_config[CONF_SUPPORT_AUDIO] = True
443  elif CONF_SUPPORT_AUDIO in entity_config:
444  del entity_config[CONF_SUPPORT_AUDIO]
445 
446  if not entity_config:
447  all_entity_config.pop(entity_id)
448 
449  return await self.async_step_advancedasync_step_advanced()
450 
451  cameras_with_audio = []
452  cameras_with_copy = []
453  all_entity_config = hk_options.setdefault(CONF_ENTITY_CONFIG, {})
454  for entity in self.included_camerasincluded_cameras:
455  entity_config = all_entity_config.get(entity, {})
456  if entity_config.get(CONF_VIDEO_CODEC) == VIDEO_CODEC_COPY:
457  cameras_with_copy.append(entity)
458  if entity_config.get(CONF_SUPPORT_AUDIO):
459  cameras_with_audio.append(entity)
460 
461  data_schema = vol.Schema(
462  {
463  vol.Optional(
464  CONF_CAMERA_COPY, default=cameras_with_copy
465  ): selector.EntitySelector(
466  selector.EntitySelectorConfig(
467  multiple=True,
468  include_entities=(self.included_camerasincluded_cameras),
469  )
470  ),
471  vol.Optional(
472  CONF_CAMERA_AUDIO, default=cameras_with_audio
473  ): selector.EntitySelector(
474  selector.EntitySelectorConfig(
475  multiple=True,
476  include_entities=(self.included_camerasincluded_cameras),
477  )
478  ),
479  }
480  )
481  return self.async_show_formasync_show_form(step_id="cameras", data_schema=data_schema)
482 
484  self, user_input: dict[str, Any] | None = None
485  ) -> ConfigFlowResult:
486  """Choose entity for the accessory."""
487  hk_options = self.hk_optionshk_options
488  domains = hk_options[CONF_DOMAINS]
489  entity_filter: EntityFilterDict
490 
491  if user_input is not None:
492  entities = cv.ensure_list(user_input[CONF_ENTITIES])
493  entity_filter = _async_build_entities_filter(domains, entities)
494  self.included_camerasincluded_cameras = _async_cameras_from_entities(entities)
495  hk_options[CONF_FILTER] = entity_filter
496  if self.included_camerasincluded_cameras:
497  return await self.async_step_camerasasync_step_cameras()
498  return await self.async_step_advancedasync_step_advanced()
499 
500  entity_filter = hk_options.get(CONF_FILTER, {})
501  entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
502  all_supported_entities = _async_get_matching_entities(
503  self.hass, domains, include_entity_category=True, include_hidden=True
504  )
505  # In accessory mode we can only have one
506  default_value = next(
507  iter(
508  entity_id
509  for entity_id in entities
510  if entity_id in all_supported_entities
511  ),
512  None,
513  )
514 
515  return self.async_show_formasync_show_form(
516  step_id="accessory",
517  data_schema=vol.Schema(
518  {
519  vol.Required(
520  CONF_ENTITIES, default=default_value
521  ): selector.EntitySelector(
522  selector.EntitySelectorConfig(
523  include_entities=all_supported_entities,
524  )
525  ),
526  }
527  ),
528  )
529 
531  self, user_input: dict[str, Any] | None = None
532  ) -> ConfigFlowResult:
533  """Choose entities to include from the domain on the bridge."""
534  hk_options = self.hk_optionshk_options
535  domains = hk_options[CONF_DOMAINS]
536  if user_input is not None:
537  entities = cv.ensure_list(user_input[CONF_ENTITIES])
538  self.included_camerasincluded_cameras = _async_cameras_from_entities(entities)
539  hk_options[CONF_FILTER] = _async_build_entities_filter(domains, entities)
540  if self.included_camerasincluded_cameras:
541  return await self.async_step_camerasasync_step_cameras()
542  return await self.async_step_advancedasync_step_advanced()
543 
544  entity_filter: EntityFilterDict = hk_options.get(CONF_FILTER, {})
545  entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
546  all_supported_entities = _async_get_matching_entities(
547  self.hass, domains, include_entity_category=True, include_hidden=True
548  )
549  # Strip out entities that no longer exist to prevent error in the UI
550  default_value = [
551  entity_id for entity_id in entities if entity_id in all_supported_entities
552  ]
553 
554  return self.async_show_formasync_show_form(
555  step_id="include",
556  description_placeholders={
557  "domains": await _async_domain_names(self.hass, domains)
558  },
559  data_schema=vol.Schema(
560  {
561  vol.Optional(
562  CONF_ENTITIES, default=default_value
563  ): selector.EntitySelector(
564  selector.EntitySelectorConfig(
565  multiple=True,
566  include_entities=all_supported_entities,
567  )
568  ),
569  }
570  ),
571  )
572 
574  self, user_input: dict[str, Any] | None = None
575  ) -> ConfigFlowResult:
576  """Choose entities to exclude from the domain on the bridge."""
577  hk_options = self.hk_optionshk_options
578  domains = hk_options[CONF_DOMAINS]
579 
580  if user_input is not None:
581  self.included_camerasincluded_cameras = []
582  entities = cv.ensure_list(user_input[CONF_ENTITIES])
583  if CAMERA_DOMAIN in domains:
584  camera_entities = _async_get_matching_entities(
585  self.hass, [CAMERA_DOMAIN]
586  )
587  self.included_camerasincluded_cameras = [
588  entity_id
589  for entity_id in camera_entities
590  if entity_id not in entities
591  ]
592  hk_options[CONF_FILTER] = _make_entity_filter(
593  include_domains=domains, exclude_entities=entities
594  )
595  if self.included_camerasincluded_cameras:
596  return await self.async_step_camerasasync_step_cameras()
597  return await self.async_step_advancedasync_step_advanced()
598 
599  entity_filter = self.hk_optionshk_options.get(CONF_FILTER, {})
600  entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
601 
602  all_supported_entities = _async_get_matching_entities(self.hass, domains)
603  if not entities:
604  entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, [])
605 
606  # Strip out entities that no longer exist to prevent error in the UI
607  default_value = [
608  entity_id for entity_id in entities if entity_id in all_supported_entities
609  ]
610 
611  return self.async_show_formasync_show_form(
612  step_id="exclude",
613  description_placeholders={
614  "domains": await _async_domain_names(self.hass, domains)
615  },
616  data_schema=vol.Schema(
617  {
618  vol.Optional(
619  CONF_ENTITIES, default=default_value
620  ): selector.EntitySelector(
621  selector.EntitySelectorConfig(
622  multiple=True,
623  include_entities=all_supported_entities,
624  )
625  ),
626  }
627  ),
628  )
629 
630  async def async_step_init(
631  self, user_input: dict[str, Any] | None = None
632  ) -> ConfigFlowResult:
633  """Handle options flow."""
634  if self.config_entryconfig_entryconfig_entry.source == SOURCE_IMPORT:
635  return await self.async_step_yamlasync_step_yaml(user_input)
636 
637  if user_input is not None:
638  self.hk_optionshk_options.update(user_input)
639  if self.hk_optionshk_options.get(CONF_HOMEKIT_MODE) == HOMEKIT_MODE_ACCESSORY:
640  return await self.async_step_accessoryasync_step_accessory()
641  if user_input[CONF_INCLUDE_EXCLUDE_MODE] == MODE_INCLUDE:
642  return await self.async_step_includeasync_step_include()
643  return await self.async_step_excludeasync_step_exclude()
644 
645  self.hk_optionshk_options = deepcopy(dict(self.config_entryconfig_entryconfig_entry.options))
646  homekit_mode = self.hk_optionshk_options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE)
647  entity_filter: EntityFilterDict = self.hk_optionshk_options.get(CONF_FILTER, {})
648  include_exclude_mode = MODE_INCLUDE
649  entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
650  if homekit_mode != HOMEKIT_MODE_ACCESSORY:
651  include_exclude_mode = MODE_INCLUDE if entities else MODE_EXCLUDE
652  domains = entity_filter.get(CONF_INCLUDE_DOMAINS, [])
653  if include_entities := entity_filter.get(CONF_INCLUDE_ENTITIES):
654  domains.extend(_domains_set_from_entities(include_entities))
655  name_to_type_map = await _async_name_to_type_map(self.hass)
656  return self.async_show_formasync_show_form(
657  step_id="init",
658  data_schema=vol.Schema(
659  {
660  vol.Required(CONF_HOMEKIT_MODE, default=homekit_mode): vol.In(
661  HOMEKIT_MODES
662  ),
663  vol.Required(
664  CONF_INCLUDE_EXCLUDE_MODE, default=include_exclude_mode
665  ): vol.In(INCLUDE_EXCLUDE_MODES),
666  vol.Required(
667  CONF_DOMAINS,
668  default=domains,
669  ): cv.multi_select(name_to_type_map),
670  }
671  ),
672  )
673 
674 
675 async def _async_get_supported_devices(hass: HomeAssistant) -> dict[str, str]:
676  """Return all supported devices."""
677  results = await device_automation.async_get_device_automations(
678  hass, device_automation.DeviceAutomationType.TRIGGER
679  )
680  dev_reg = dr.async_get(hass)
681  unsorted: dict[str, str] = {}
682  for device_id in results:
683  entry = dev_reg.async_get(device_id)
684  unsorted[device_id] = entry.name or device_id if entry else device_id
685  return dict(sorted(unsorted.items(), key=itemgetter(1)))
686 
687 
689  ent_reg: er.EntityRegistry,
690  entity_id: str,
691  include_entity_category: bool,
692  include_hidden: bool,
693 ) -> bool:
694  """Filter out hidden entities and ones with entity category (unless specified)."""
695  return bool(
696  (entry := ent_reg.async_get(entity_id))
697  and (
698  (not include_hidden and entry.hidden_by is not None)
699  or (not include_entity_category and entry.entity_category is not None)
700  )
701  )
702 
703 
705  hass: HomeAssistant,
706  domains: list[str] | None = None,
707  include_entity_category: bool = False,
708  include_hidden: bool = False,
709 ) -> list[str]:
710  """Fetch all entities or entities in the given domains."""
711  ent_reg = er.async_get(hass)
712  return [
713  state.entity_id
714  for state in sorted(
715  hass.states.async_all(domains and set(domains)),
716  key=lambda item: item.entity_id,
717  )
719  ent_reg, state.entity_id, include_entity_category, include_hidden
720  )
721  ]
722 
723 
724 def _domains_set_from_entities(entity_ids: Iterable[str]) -> set[str]:
725  """Build a set of domains for the given entity ids."""
726  return {split_entity_id(entity_id)[0] for entity_id in entity_ids}
727 
728 
729 @callback
731  hass: HomeAssistant, include_domains: Iterable[str]
732 ) -> list[str]:
733  """Build a list of entities that should be paired in accessory mode."""
734  accessory_mode_domains = {
735  domain for domain in include_domains if domain in DOMAINS_NEED_ACCESSORY_MODE
736  }
737 
738  if not accessory_mode_domains:
739  return []
740 
741  return [
742  state.entity_id
743  for state in hass.states.async_all(accessory_mode_domains)
744  if state_needs_accessory_mode(state)
745  ]
746 
747 
748 @callback
749 def _async_entity_ids_with_accessory_mode(hass: HomeAssistant) -> set[str]:
750  """Return a set of entity ids that have config entries in accessory mode."""
751 
752  entity_ids: set[str] = set()
753 
754  current_entries = hass.config_entries.async_entries(DOMAIN)
755  for entry in current_entries:
756  # We have to handle the case where the data has not yet
757  # been migrated to options because the data was just
758  # imported and the entry was never started
759  target = entry.options if CONF_HOMEKIT_MODE in entry.options else entry.data
760  if target.get(CONF_HOMEKIT_MODE) != HOMEKIT_MODE_ACCESSORY:
761  continue
762 
763  entity_ids.add(target[CONF_FILTER][CONF_INCLUDE_ENTITIES][0])
764 
765  return entity_ids
None _async_add_entries_for_accessory_mode_entities(self, int last_assigned_port)
Definition: config_flow.py:267
ConfigFlowResult async_step_import(self, dict[str, Any] import_data)
Definition: config_flow.py:315
ConfigFlowResult async_step_accessory(self, dict[str, Any] accessory_input)
Definition: config_flow.py:291
ConfigFlowResult async_step_pairing(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:239
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:213
bool _async_is_unique_name_port(self, dict[str, Any] user_input)
Definition: config_flow.py:350
OptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:363
ConfigFlowResult async_step_include(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:532
ConfigFlowResult async_step_cameras(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:426
ConfigFlowResult async_step_exclude(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:575
ConfigFlowResult async_step_accessory(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:485
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:632
ConfigFlowResult async_step_yaml(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:378
ConfigFlowResult async_step_advanced(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:389
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=None)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
ConfigFlowResult async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
None config_entry(self, ConfigEntry value)
bool show_advanced_options(self)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
list[str] _async_cameras_from_entities(list[str] entities)
Definition: config_flow.py:182
list[str] _async_get_matching_entities(HomeAssistant hass, list[str]|None domains=None, bool include_entity_category=False, bool include_hidden=False)
Definition: config_flow.py:709
set[str] _async_entity_ids_with_accessory_mode(HomeAssistant hass)
Definition: config_flow.py:749
bool _exclude_by_entity_registry(er.EntityRegistry ent_reg, str entity_id, bool include_entity_category, bool include_hidden)
Definition: config_flow.py:693
list[str] _async_get_entity_ids_for_accessory_mode(HomeAssistant hass, Iterable[str] include_domains)
Definition: config_flow.py:732
set[str] _domains_set_from_entities(Iterable[str] entity_ids)
Definition: config_flow.py:724
EntityFilterDict _async_build_entities_filter(list[str] domains, list[str] entities)
Definition: config_flow.py:170
dict[str, str] _async_get_supported_devices(HomeAssistant hass)
Definition: config_flow.py:675
EntityFilterDict _make_entity_filter(list[str]|None include_domains=None, list[str]|None include_entities=None, list[str]|None exclude_domains=None, list[str]|None exclude_entities=None)
Definition: config_flow.py:149
dict[str, str] _async_name_to_type_map(HomeAssistant hass)
Definition: config_flow.py:190
str _async_domain_names(HomeAssistant hass, list[str] domains)
Definition: config_flow.py:159
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
tuple[str, str] split_entity_id(str entity_id)
Definition: core.py:214
dict[str, Integration|Exception] async_get_integrations(HomeAssistant hass, Iterable[str] domains)
Definition: loader.py:1368