1 """Config flow for HomeKit integration."""
3 from __future__
import annotations
5 from collections.abc
import Iterable
6 from copy
import deepcopy
7 from operator
import itemgetter
11 from typing
import Any, Final, TypedDict
13 import voluptuous
as vol
39 config_validation
as cv,
40 device_registry
as dr,
41 entity_registry
as er,
48 CONF_EXCLUDE_ACCESSORY_MODE,
53 DEFAULT_CONFIG_FLOW_PORT,
56 HOMEKIT_MODE_ACCESSORY,
62 from .util
import async_find_next_available_port, state_needs_accessory_mode
64 CONF_CAMERA_AUDIO =
"camera_audio"
65 CONF_CAMERA_COPY =
"camera_copy"
66 CONF_INCLUDE_EXCLUDE_MODE =
"include_exclude_mode"
68 MODE_INCLUDE =
"include"
69 MODE_EXCLUDE =
"exclude"
71 INCLUDE_EXCLUDE_MODES = [MODE_EXCLUDE, MODE_INCLUDE]
73 DOMAINS_NEED_ACCESSORY_MODE = {
79 NEVER_BRIDGED_DOMAINS = {CAMERA_DOMAIN}
81 CAMERA_ENTITY_PREFIX = f
"{CAMERA_DOMAIN}."
84 "alarm_control_panel",
114 "alarm_control_panel",
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"
136 """Entity filter dict."""
138 include_domains: list[str]
139 include_entities: list[str]
140 exclude_domains: list[str]
141 exclude_entities: list[str]
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."""
152 include_domains=include_domains
or [],
153 include_entities=include_entities
or [],
154 exclude_domains=exclude_domains
or [],
155 exclude_entities=exclude_entities
or [],
160 """Build a list of integration names from domains."""
163 [name
for domain, name
in name_to_type_map.items()
if domain
in domains]
169 domains: list[str], entities: list[str]
170 ) -> EntityFilterDict:
171 """Build an entities filter from domains and entities."""
175 include_domains=sorted(
178 include_entities=entities,
185 for entity_id
in entities
186 if entity_id.startswith(CAMERA_ENTITY_PREFIX)
191 """Create a mapping of types of devices/entities HomeKit can support."""
194 domain: integration_or_exception.name
195 if (integration_or_exception := integrations[domain])
196 and not isinstance(integration_or_exception, Exception)
198 for domain
in SUPPORTED_DOMAINS
203 """Handle a config flow for HomeKit."""
208 """Initialize config flow."""
209 self.hk_data: dict[str, Any] = {}
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:
217 include_domains=user_input[CONF_INCLUDE_DOMAINS]
221 self.hk_data[CONF_HOMEKIT_MODE] = HOMEKIT_MODE_BRIDGE
228 data_schema=vol.Schema(
231 CONF_INCLUDE_DOMAINS, default=default_domains
232 ): cv.multi_select(name_to_type_map),
238 self, user_input: dict[str, Any] |
None =
None
239 ) -> ConfigFlowResult:
240 """Pairing instructions."""
241 hk_data = self.hk_data
243 if user_input
is not None:
244 port = async_find_next_available_port(self.hass, DEFAULT_CONFIG_FLOW_PORT)
246 hk_data[CONF_PORT] = port
247 conf_filter: EntityFilterDict = hk_data[CONF_FILTER]
248 conf_filter[CONF_INCLUDE_DOMAINS] = [
250 for domain
in conf_filter[CONF_INCLUDE_DOMAINS]
251 if domain
not in NEVER_BRIDGED_DOMAINS
254 title=f
"{hk_data[CONF_NAME]}:{hk_data[CONF_PORT]}",
259 hk_data[CONF_EXCLUDE_ACCESSORY_MODE] =
True
262 description_placeholders={CONF_NAME: hk_data[CONF_NAME]},
266 self, last_assigned_port: int
268 """Generate new flows for entities that need their own instances."""
270 self.hass, self.hk_data[CONF_FILTER][CONF_INCLUDE_DOMAINS]
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:
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(
284 context={
"source":
"accessory"},
285 data={CONF_ENTITY_ID: entity_id, CONF_PORT: port},
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]
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
303 CONF_HOMEKIT_MODE: HOMEKIT_MODE_ACCESSORY,
306 if entity_id.startswith(CAMERA_ENTITY_PREFIX):
307 entry_data[CONF_ENTITY_CONFIG] = {
308 entity_id: {CONF_VIDEO_CODEC: VIDEO_CODEC_COPY}
312 title=f
"{name}:{entry_data[CONF_PORT]}", data=entry_data
316 """Handle import from yaml."""
320 title=f
"{import_data[CONF_NAME]}:{import_data[CONF_PORT]}", data=import_data
325 """Return a set of bridge names."""
327 entry.data[CONF_NAME]
329 if CONF_NAME
in entry.data
334 """Return an available for the bridge."""
336 valid_mdns_name = re.sub(
"[^A-Za-z0-9 ]+",
" ", requested_name)
338 if valid_mdns_name
not in current_names:
339 return valid_mdns_name
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}"
347 return suggested_name
351 """Determine is a name or port is already used."""
352 name = user_input[CONF_NAME]
353 port = user_input[CONF_PORT]
355 entry.data[CONF_NAME] == name
or entry.data[CONF_PORT] == port
362 config_entry: ConfigEntry,
363 ) -> OptionsFlowHandler:
364 """Get the options flow for this handler."""
369 """Handle a option flow for homekit."""
372 """Initialize options flow."""
373 self.
hk_optionshk_options: dict[str, Any] = {}
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:
388 self, user_input: dict[str, Any] |
None =
None
389 ) -> ConfigFlowResult:
390 """Choose advanced options."""
393 bridge_mode = hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_BRIDGE
395 if not show_advanced_options
or user_input
is not None or not bridge_mode:
397 hk_options.update(user_input)
398 if show_advanced_options
and bridge_mode:
399 hk_options[CONF_DEVICES] = user_input[CONF_DEVICES]
401 hk_options.pop(CONF_DOMAINS,
None)
402 hk_options.pop(CONF_ENTITIES,
None)
403 hk_options.pop(CONF_INCLUDE_EXCLUDE_MODE,
None)
410 for device_id
in self.
hk_optionshk_options.
get(CONF_DEVICES, [])
411 if device_id
in all_supported_devices
415 data_schema=vol.Schema(
417 vol.Optional(CONF_DEVICES, default=devices): cv.multi_select(
418 all_supported_devices
425 self, user_input: dict[str, Any] |
None =
None
426 ) -> ConfigFlowResult:
427 """Choose camera config."""
429 all_entity_config: dict[str, dict[str, Any]]
431 if user_input
is not None:
432 all_entity_config = hk_options[CONF_ENTITY_CONFIG]
434 entity_config = all_entity_config.setdefault(entity_id, {})
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]
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]
446 if not entity_config:
447 all_entity_config.pop(entity_id)
451 cameras_with_audio = []
452 cameras_with_copy = []
453 all_entity_config = hk_options.setdefault(CONF_ENTITY_CONFIG, {})
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)
461 data_schema = vol.Schema(
464 CONF_CAMERA_COPY, default=cameras_with_copy
465 ): selector.EntitySelector(
466 selector.EntitySelectorConfig(
472 CONF_CAMERA_AUDIO, default=cameras_with_audio
473 ): selector.EntitySelector(
474 selector.EntitySelectorConfig(
481 return self.
async_show_formasync_show_form(step_id=
"cameras", data_schema=data_schema)
484 self, user_input: dict[str, Any] |
None =
None
485 ) -> ConfigFlowResult:
486 """Choose entity for the accessory."""
488 domains = hk_options[CONF_DOMAINS]
489 entity_filter: EntityFilterDict
491 if user_input
is not None:
492 entities = cv.ensure_list(user_input[CONF_ENTITIES])
495 hk_options[CONF_FILTER] = entity_filter
500 entity_filter = hk_options.get(CONF_FILTER, {})
501 entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
503 self.hass, domains, include_entity_category=
True, include_hidden=
True
506 default_value = next(
509 for entity_id
in entities
510 if entity_id
in all_supported_entities
517 data_schema=vol.Schema(
520 CONF_ENTITIES, default=default_value
521 ): selector.EntitySelector(
522 selector.EntitySelectorConfig(
523 include_entities=all_supported_entities,
531 self, user_input: dict[str, Any] |
None =
None
532 ) -> ConfigFlowResult:
533 """Choose entities to include from the domain on the bridge."""
535 domains = hk_options[CONF_DOMAINS]
536 if user_input
is not None:
537 entities = cv.ensure_list(user_input[CONF_ENTITIES])
544 entity_filter: EntityFilterDict = hk_options.get(CONF_FILTER, {})
545 entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
547 self.hass, domains, include_entity_category=
True, include_hidden=
True
551 entity_id
for entity_id
in entities
if entity_id
in all_supported_entities
556 description_placeholders={
559 data_schema=vol.Schema(
562 CONF_ENTITIES, default=default_value
563 ): selector.EntitySelector(
564 selector.EntitySelectorConfig(
566 include_entities=all_supported_entities,
574 self, user_input: dict[str, Any] |
None =
None
575 ) -> ConfigFlowResult:
576 """Choose entities to exclude from the domain on the bridge."""
578 domains = hk_options[CONF_DOMAINS]
580 if user_input
is not None:
582 entities = cv.ensure_list(user_input[CONF_ENTITIES])
583 if CAMERA_DOMAIN
in domains:
585 self.hass, [CAMERA_DOMAIN]
589 for entity_id
in camera_entities
590 if entity_id
not in entities
593 include_domains=domains, exclude_entities=entities
599 entity_filter = self.
hk_optionshk_options.
get(CONF_FILTER, {})
600 entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
604 entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, [])
608 entity_id
for entity_id
in entities
if entity_id
in all_supported_entities
613 description_placeholders={
616 data_schema=vol.Schema(
619 CONF_ENTITIES, default=default_value
620 ): selector.EntitySelector(
621 selector.EntitySelectorConfig(
623 include_entities=all_supported_entities,
631 self, user_input: dict[str, Any] |
None =
None
632 ) -> ConfigFlowResult:
633 """Handle options flow."""
637 if user_input
is not None:
639 if self.
hk_optionshk_options.
get(CONF_HOMEKIT_MODE) == HOMEKIT_MODE_ACCESSORY:
641 if user_input[CONF_INCLUDE_EXCLUDE_MODE] == MODE_INCLUDE:
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):
658 data_schema=vol.Schema(
660 vol.Required(CONF_HOMEKIT_MODE, default=homekit_mode): vol.In(
664 CONF_INCLUDE_EXCLUDE_MODE, default=include_exclude_mode
665 ): vol.In(INCLUDE_EXCLUDE_MODES),
669 ): cv.multi_select(name_to_type_map),
676 """Return all supported devices."""
677 results = await device_automation.async_get_device_automations(
678 hass, device_automation.DeviceAutomationType.TRIGGER
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)))
689 ent_reg: er.EntityRegistry,
691 include_entity_category: bool,
692 include_hidden: bool,
694 """Filter out hidden entities and ones with entity category (unless specified)."""
696 (entry := ent_reg.async_get(entity_id))
698 (
not include_hidden
and entry.hidden_by
is not None)
699 or (
not include_entity_category
and entry.entity_category
is not None)
706 domains: list[str] |
None =
None,
707 include_entity_category: bool =
False,
708 include_hidden: bool =
False,
710 """Fetch all entities or entities in the given domains."""
711 ent_reg = er.async_get(hass)
715 hass.states.async_all(domains
and set(domains)),
716 key=
lambda item: item.entity_id,
719 ent_reg, state.entity_id, include_entity_category, include_hidden
725 """Build a set of domains for the given entity ids."""
731 hass: HomeAssistant, include_domains: Iterable[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
738 if not accessory_mode_domains:
743 for state
in hass.states.async_all(accessory_mode_domains)
744 if state_needs_accessory_mode(state)
750 """Return a set of entity ids that have config entries in accessory mode."""
752 entity_ids: set[str] = set()
754 current_entries = hass.config_entries.async_entries(DOMAIN)
755 for entry
in current_entries:
759 target = entry.options
if CONF_HOMEKIT_MODE
in entry.options
else entry.data
760 if target.get(CONF_HOMEKIT_MODE) != HOMEKIT_MODE_ACCESSORY:
763 entity_ids.add(target[CONF_FILTER][CONF_INCLUDE_ENTITIES][0])
None _async_add_entries_for_accessory_mode_entities(self, int last_assigned_port)
ConfigFlowResult async_step_import(self, dict[str, Any] import_data)
ConfigFlowResult async_step_accessory(self, dict[str, Any] accessory_input)
ConfigFlowResult async_step_pairing(self, dict[str, Any]|None user_input=None)
set[str] _async_current_names(self)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
str _async_available_name(self, str requested_name)
bool _async_is_unique_name_port(self, dict[str, Any] user_input)
OptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
ConfigFlowResult async_step_include(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_cameras(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_exclude(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_accessory(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_yaml(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_advanced(self, dict[str, Any]|None user_input=None)
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)
ConfigEntry config_entry(self)
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)
list[str] _async_cameras_from_entities(list[str] entities)
list[str] _async_get_matching_entities(HomeAssistant hass, list[str]|None domains=None, bool include_entity_category=False, bool include_hidden=False)
set[str] _async_entity_ids_with_accessory_mode(HomeAssistant hass)
bool _exclude_by_entity_registry(er.EntityRegistry ent_reg, str entity_id, bool include_entity_category, bool include_hidden)
list[str] _async_get_entity_ids_for_accessory_mode(HomeAssistant hass, Iterable[str] include_domains)
set[str] _domains_set_from_entities(Iterable[str] entity_ids)
EntityFilterDict _async_build_entities_filter(list[str] domains, list[str] entities)
dict[str, str] _async_get_supported_devices(HomeAssistant hass)
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)
dict[str, str] _async_name_to_type_map(HomeAssistant hass)
str _async_domain_names(HomeAssistant hass, list[str] domains)
IssData update(pyiss.ISS iss)
tuple[str, str] split_entity_id(str entity_id)
dict[str, Integration|Exception] async_get_integrations(HomeAssistant hass, Iterable[str] domains)