Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Apple TV integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections import deque
7 from collections.abc import Awaitable, Callable, Mapping
8 from ipaddress import ip_address
9 import logging
10 from random import randrange
11 from typing import Any, Self
12 
13 from pyatv import exceptions, pair, scan
14 from pyatv.const import DeviceModel, PairingRequirement, Protocol
15 from pyatv.convert import model_str, protocol_str
16 from pyatv.helpers import get_unique_id
17 from pyatv.interface import BaseConfig, PairingHandler
18 import voluptuous as vol
19 
20 from homeassistant.components import zeroconf
21 from homeassistant.config_entries import (
22  SOURCE_IGNORE,
23  SOURCE_ZEROCONF,
24  ConfigEntry,
25  ConfigFlow,
26  ConfigFlowResult,
27 )
28 from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN
29 from homeassistant.core import HomeAssistant, callback
30 from homeassistant.data_entry_flow import AbortFlow
31 from homeassistant.exceptions import HomeAssistantError
32 from homeassistant.helpers.aiohttp_client import async_get_clientsession
34  SchemaFlowFormStep,
35  SchemaOptionsFlowHandler,
36 )
37 
38 from .const import CONF_CREDENTIALS, CONF_IDENTIFIERS, CONF_START_OFF, DOMAIN
39 
40 _LOGGER = logging.getLogger(__name__)
41 
42 DEVICE_INPUT = "device_input"
43 
44 INPUT_PIN_SCHEMA = vol.Schema({vol.Required(CONF_PIN, default=None): int})
45 
46 DEFAULT_START_OFF = False
47 
48 DISCOVERY_AGGREGATION_TIME = 15 # seconds
49 
50 OPTIONS_SCHEMA = vol.Schema(
51  {
52  vol.Optional(CONF_START_OFF, default=DEFAULT_START_OFF): bool,
53  }
54 )
55 OPTIONS_FLOW = {
56  "init": SchemaFlowFormStep(OPTIONS_SCHEMA),
57 }
58 
59 
60 async def device_scan(
61  hass: HomeAssistant, identifier: str | None, loop: asyncio.AbstractEventLoop
62 ) -> tuple[BaseConfig | None, list[str] | None]:
63  """Scan for a specific device using identifier as filter."""
64 
65  def _filter_device(dev: BaseConfig) -> bool:
66  if identifier is None:
67  return True
68  if identifier == str(dev.address):
69  return True
70  if identifier == dev.name:
71  return True
72  return any(service.identifier == identifier for service in dev.services)
73 
74  def _host_filter() -> list[str] | None:
75  if identifier is None:
76  return None
77  try:
78  ip_address(identifier)
79  except ValueError:
80  return None
81  return [identifier]
82 
83  # If we have an address, only probe that address to avoid
84  # broadcast traffic on the network
85  aiozc = await zeroconf.async_get_async_instance(hass)
86  scan_result = await scan(loop, timeout=3, hosts=_host_filter(), aiozc=aiozc)
87  matches = [atv for atv in scan_result if _filter_device(atv)]
88 
89  if matches:
90  return matches[0], matches[0].all_identifiers
91 
92  return None, None
93 
94 
95 class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
96  """Handle a config flow for Apple TV."""
97 
98  VERSION = 1
99 
100  scan_filter: str | None = None
101  all_identifiers: set[str]
102  atv: BaseConfig | None = None
103  atv_identifiers: list[str] | None = None
104  _host: str # host in zeroconf discovery info, should not be accessed by other flows
105  host: str | None = None # set by _async_aggregate_discoveries, for other flows
106  protocol: Protocol | None = None
107  pairing: PairingHandler | None = None
108  protocols_to_pair: deque[Protocol] | None = None
109 
110  @staticmethod
111  @callback
113  config_entry: ConfigEntry,
114  ) -> SchemaOptionsFlowHandler:
115  """Get options flow for this handler."""
116  return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
117 
118  def __init__(self) -> None:
119  """Initialize a new AppleTVConfigFlow."""
120  self.credentials: dict[int, str | None] = {} # Protocol -> credentials
121 
122  @property
123  def device_identifier(self) -> str | None:
124  """Return a identifier for the config entry.
125 
126  A device has multiple unique identifiers, but Home Assistant only supports one
127  per config entry. Normally, a "main identifier" is determined by pyatv by
128  first collecting all identifiers and then picking one in a pre-determine order.
129  Under normal circumstances, this works fine but if a service is missing or
130  removed due to deprecation (which happened with MRP), then another identifier
131  will be calculated instead. To fix this, all identifiers belonging to a device
132  is stored with the config entry and one of them (could be random) is used as
133  unique_id for said entry. When a new (zeroconf) service or device is
134  discovered, the identifier is first used to look up if it belongs to an
135  existing config entry. If that's the case, the unique_id from that entry is
136  re-used, otherwise the newly discovered identifier is used instead.
137  """
138  assert self.atv
139  all_identifiers = set(self.atv.all_identifiers)
140  if unique_id := self._entry_unique_id_from_identifers_entry_unique_id_from_identifers(all_identifiers):
141  return unique_id
142  return self.atv.identifier
143 
144  @callback
145  def _entry_unique_id_from_identifers(self, all_identifiers: set[str]) -> str | None:
146  """Search existing entries for an identifier and return the unique id."""
147  for entry in self._async_current_entries_async_current_entries(include_ignore=True):
148  if not all_identifiers.isdisjoint(
149  entry.data.get(CONF_IDENTIFIERS, [entry.unique_id])
150  ):
151  return entry.unique_id
152  return None
153 
154  async def async_step_reauth(
155  self, entry_data: Mapping[str, Any]
156  ) -> ConfigFlowResult:
157  """Handle initial step when updating invalid credentials."""
158  self.context["title_placeholders"] = {
159  "name": entry_data[CONF_NAME],
160  "type": "Apple TV",
161  }
162  self.scan_filterscan_filter = self.unique_idunique_id
163  return await self.async_step_restore_deviceasync_step_restore_device()
164 
166  self, user_input: dict[str, str] | None = None
167  ) -> ConfigFlowResult:
168  """Inform user that reconfiguration is about to start."""
169  if user_input is not None:
170  return await self.async_find_device_wrapperasync_find_device_wrapper(
171  self.async_pair_next_protocolasync_pair_next_protocol, allow_exist=True
172  )
173 
174  return self.async_show_formasync_show_formasync_show_form(step_id="restore_device")
175 
176  async def async_step_user(
177  self, user_input: dict[str, str] | None = None
178  ) -> ConfigFlowResult:
179  """Handle the initial step."""
180  errors = {}
181  if user_input is not None:
182  self.scan_filterscan_filter = user_input[DEVICE_INPUT]
183  try:
184  await self.async_find_deviceasync_find_device()
185  except DeviceNotFound:
186  errors["base"] = "no_devices_found"
187  except DeviceAlreadyConfigured:
188  errors["base"] = "already_configured"
189  except Exception:
190  _LOGGER.exception("Unexpected exception")
191  errors["base"] = "unknown"
192  else:
193  await self.async_set_unique_idasync_set_unique_id(
194  self.device_identifierdevice_identifier, raise_on_progress=False
195  )
196  assert self.atv
197  self.all_identifiersall_identifiers = set(self.atv.all_identifiers)
198  return await self.async_step_confirmasync_step_confirm()
199 
200  return self.async_show_formasync_show_formasync_show_form(
201  step_id="user",
202  data_schema=vol.Schema({vol.Required(DEVICE_INPUT): str}),
203  errors=errors,
204  )
205 
207  self, discovery_info: zeroconf.ZeroconfServiceInfo
208  ) -> ConfigFlowResult:
209  """Handle device found via zeroconf."""
210  if discovery_info.ip_address.version == 6:
211  return self.async_abortasync_abortasync_abort(reason="ipv6_not_supported")
212  self._host_host = host = discovery_info.host
213  service_type = discovery_info.type[:-1] # Remove leading .
214  name = discovery_info.name.replace(f".{service_type}.", "")
215  properties = discovery_info.properties
216 
217  # Extract unique identifier from service
218  unique_id = get_unique_id(service_type, name, properties)
219  if unique_id is None:
220  return self.async_abortasync_abortasync_abort(reason="unknown")
221 
222  # The unique id for the zeroconf service may not be
223  # the same as the unique id for the device. If the
224  # device is already configured so if we don't
225  # find a match here, we will fallback to
226  # looking up the device by all its identifiers
227  # in the next block.
228  await self.async_set_unique_idasync_set_unique_id(unique_id)
229  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_ADDRESS: host})
230 
231  if existing_unique_id := self._entry_unique_id_from_identifers_entry_unique_id_from_identifers({unique_id}):
232  await self.async_set_unique_idasync_set_unique_id(existing_unique_id)
233  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_ADDRESS: host})
234 
235  self._async_abort_entries_match_async_abort_entries_match({CONF_ADDRESS: host})
236 
237  await self._async_aggregate_discoveries_async_aggregate_discoveries(host, unique_id)
238  # Scan for the device in order to extract _all_ unique identifiers assigned to
239  # it. Not doing it like this will yield multiple config flows for the same
240  # device, one per protocol, which is undesired.
241  self.scan_filterscan_filter = host
242  return await self.async_find_device_wrapperasync_find_device_wrapper(self.async_found_zeroconf_deviceasync_found_zeroconf_device)
243 
244  async def _async_aggregate_discoveries(self, host: str, unique_id: str) -> None:
245  """Wait for multiple zeroconf services to be discovered an aggregate them."""
246  #
247  # Suppose we have a device with three services: A, B and C. Let's assume
248  # service A is discovered by Zeroconf, triggering a device scan that also finds
249  # service B but *not* C. An identifier is picked from one of the services and
250  # used as unique_id. The select process is deterministic (let's say in order A,
251  # B and C) but in practice that doesn't matter. So, a flow is set up for the
252  # device with unique_id set to "A" for services A and B.
253  #
254  # Now, service C is found and the same thing happens again but only service B
255  # is found. In this case, unique_id will be set to "B" which is problematic
256  # since both flows really represent the same device. They will however end up
257  # as two separate flows.
258  #
259  # To solve this, all identifiers are stored as
260  # "all_identifiers" in the flow. When a new service is discovered, the
261  # code below will check these identifiers for all active flows and abort if a
262  # match is found. Before aborting, the original flow is updated with any
263  # potentially new identifiers. In the example above, when service C is
264  # discovered, the identifier of service C will be inserted into
265  # "all_identifiers" of the original flow (making the device complete).
266  #
267  # Wait DISCOVERY_AGGREGATION_TIME for multiple services to be
268  # discovered via zeroconf. Once the first service is discovered
269  # this allows other services to be discovered inside the time
270  # window before triggering a scan of the device. This prevents
271  # multiple scans of the device at the same time since each
272  # apple_tv device has multiple services that are discovered by
273  # zeroconf.
274  #
275  self._async_check_and_update_in_progress_async_check_and_update_in_progress(host, unique_id)
276  await asyncio.sleep(DISCOVERY_AGGREGATION_TIME)
277  # Check again after sleeping in case another flow
278  # has made progress while we yielded to the event loop
279  self._async_check_and_update_in_progress_async_check_and_update_in_progress(host, unique_id)
280  # Host must only be set AFTER checking and updating in progress
281  # flows or we will have a race condition where no flows move forward.
282  self.hosthost = host
283 
284  @callback
285  def _async_check_and_update_in_progress(self, host: str, unique_id: str) -> None:
286  """Check for in-progress flows and update them with identifiers if needed."""
287  if self.hass.config_entries.flow.async_has_matching_flow(self):
288  raise AbortFlow("already_in_progress")
289 
290  def is_matching(self, other_flow: Self) -> bool:
291  """Return True if other_flow is matching this flow."""
292  if (
293  other_flow.context.get("source") != SOURCE_ZEROCONF
294  or other_flow.host != self._host_host
295  ):
296  return False
297  if self.unique_idunique_id is not None:
298  # Add potentially new identifiers from this device to the existing flow
299  other_flow.all_identifiers.add(self.unique_idunique_id)
300  return True
301 
303  self, user_input: dict[str, str] | None = None
304  ) -> ConfigFlowResult:
305  """Handle device found after Zeroconf discovery."""
306  assert self.atv
307  self.all_identifiersall_identifiers = set(self.atv.all_identifiers)
308  # Also abort if an integration with this identifier already exists
309  await self.async_set_unique_idasync_set_unique_id(self.device_identifierdevice_identifier)
310  # but be sure to update the address if its changed so the scanner
311  # will probe the new address
312  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
313  updates={CONF_ADDRESS: str(self.atv.address)}
314  )
315  return await self.async_step_confirmasync_step_confirm()
316 
318  self,
319  next_func: Callable[[], Awaitable[ConfigFlowResult]],
320  allow_exist: bool = False,
321  ) -> ConfigFlowResult:
322  """Find a specific device and call another function when done.
323 
324  This function will do error handling and bail out when an error
325  occurs.
326  """
327  try:
328  await self.async_find_deviceasync_find_device(allow_exist)
329  except DeviceNotFound:
330  return self.async_abortasync_abortasync_abort(reason="no_devices_found")
331  except DeviceAlreadyConfigured:
332  return self.async_abortasync_abortasync_abort(reason="already_configured")
333  except Exception:
334  _LOGGER.exception("Unexpected exception")
335  return self.async_abortasync_abortasync_abort(reason="unknown")
336 
337  return await next_func()
338 
339  async def async_find_device(self, allow_exist: bool = False) -> None:
340  """Scan for the selected device to discover services."""
341  self.atv, self.atv_identifiersatv_identifiers = await device_scan(
342  self.hass, self.scan_filterscan_filter, self.hass.loop
343  )
344  if not self.atv:
345  raise DeviceNotFound
346 
347  # Protocols supported by the device are prospects for pairing
348  self.protocols_to_pairprotocols_to_pair = deque(
349  service.protocol for service in self.atv.services if service.enabled
350  )
351 
352  dev_info = self.atv.device_info
353  self.context["title_placeholders"] = {
354  "name": self.atv.name,
355  "type": (
356  dev_info.raw_model
357  if dev_info.model == DeviceModel.Unknown and dev_info.raw_model
358  else model_str(dev_info.model)
359  ),
360  }
361  all_identifiers = set(self.atv.all_identifiers)
362  discovered_ip_address = str(self.atv.address)
363  for entry in self._async_current_entries_async_current_entries():
364  existing_identifiers = set(
365  entry.data.get(CONF_IDENTIFIERS, [entry.unique_id])
366  )
367  if all_identifiers.isdisjoint(existing_identifiers):
368  continue
369  combined_identifiers = existing_identifiers | all_identifiers
370  if entry.data.get(
371  CONF_ADDRESS
372  ) != discovered_ip_address or combined_identifiers != set(
373  entry.data.get(CONF_IDENTIFIERS, [])
374  ):
375  self.hass.config_entries.async_update_entry(
376  entry,
377  data={
378  **entry.data,
379  CONF_ADDRESS: discovered_ip_address,
380  CONF_IDENTIFIERS: list(combined_identifiers),
381  },
382  )
383  if entry.source != SOURCE_IGNORE:
384  self.hass.config_entries.async_schedule_reload(entry.entry_id)
385  if not allow_exist:
386  raise DeviceAlreadyConfigured
387 
389  self, user_input: dict[str, str] | None = None
390  ) -> ConfigFlowResult:
391  """Handle user-confirmation of discovered node."""
392  assert self.atv
393  if user_input is not None:
394  expected_identifier_count = len(self.all_identifiersall_identifiers)
395  # If number of services found during device scan mismatch number of
396  # identifiers collected during Zeroconf discovery, then trigger a new scan
397  # with hopes of finding all services.
398  if len(self.atv.all_identifiers) != expected_identifier_count:
399  try:
400  await self.async_find_deviceasync_find_device()
401  except DeviceNotFound:
402  return self.async_abortasync_abortasync_abort(reason="device_not_found")
403 
404  # If all services still were not found, bail out with an error
405  if len(self.atv.all_identifiers) != expected_identifier_count:
406  return self.async_abortasync_abortasync_abort(reason="inconsistent_device")
407 
408  return await self.async_pair_next_protocolasync_pair_next_protocol()
409 
410  return self.async_show_formasync_show_formasync_show_form(
411  step_id="confirm",
412  description_placeholders={
413  "name": self.atv.name,
414  "type": model_str(self.atv.device_info.model),
415  },
416  )
417 
418  async def async_pair_next_protocol(self) -> ConfigFlowResult:
419  """Start pairing process for the next available protocol."""
420  await self._async_cleanup_async_cleanup()
421 
422  # Any more protocols to pair? Else bail out here
423  if not self.protocols_to_pairprotocols_to_pair:
424  return await self._async_get_entry_async_get_entry()
425 
426  self.protocolprotocol = self.protocols_to_pairprotocols_to_pair.popleft()
427  assert self.atv
428  service = self.atv.get_service(self.protocolprotocol)
429 
430  if service is None:
431  _LOGGER.debug(
432  "%s does not support pairing (cannot find a corresponding service)",
433  self.protocolprotocol,
434  )
435  return await self.async_pair_next_protocolasync_pair_next_protocol()
436 
437  # Service requires a password
438  if service.requires_password:
439  return await self.async_step_passwordasync_step_password()
440 
441  # Figure out, depending on protocol, what kind of pairing is needed
442  if service.pairing == PairingRequirement.Unsupported:
443  _LOGGER.debug("%s does not support pairing", self.protocolprotocol)
444  return await self.async_pair_next_protocolasync_pair_next_protocol()
445  if service.pairing == PairingRequirement.Disabled:
446  return await self.async_step_protocol_disabledasync_step_protocol_disabled()
447  if service.pairing == PairingRequirement.NotNeeded:
448  _LOGGER.debug("%s does not require pairing", self.protocolprotocol)
449  self.credentials[self.protocolprotocol.value] = None
450  return await self.async_pair_next_protocolasync_pair_next_protocol()
451 
452  _LOGGER.debug("%s requires pairing", self.protocolprotocol)
453 
454  # Protocol specific arguments
455  pair_args: dict[str, Any] = {}
456  if self.protocolprotocol in {Protocol.AirPlay, Protocol.Companion, Protocol.DMAP}:
457  pair_args["name"] = "Home Assistant"
458  if self.protocolprotocol == Protocol.DMAP:
459  pair_args["zeroconf"] = await zeroconf.async_get_instance(self.hass)
460 
461  # Initiate the pairing process
462  abort_reason = None
463  session = async_get_clientsession(self.hass)
464  self.pairingpairing = await pair(
465  self.atv, self.protocolprotocol, self.hass.loop, session=session, **pair_args
466  )
467  try:
468  await self.pairingpairing.begin()
469  except exceptions.ConnectionFailedError:
470  return await self.async_step_service_problemasync_step_service_problem()
471  except exceptions.BackOffError:
472  abort_reason = "backoff"
473  except exceptions.PairingError:
474  _LOGGER.exception("Authentication problem")
475  abort_reason = "invalid_auth"
476  except Exception:
477  _LOGGER.exception("Unexpected exception")
478  abort_reason = "unknown"
479 
480  if abort_reason:
481  await self._async_cleanup_async_cleanup()
482  return self.async_abortasync_abortasync_abort(reason=abort_reason)
483 
484  # Choose step depending on if PIN is required from user or not
485  if self.pairingpairing.device_provides_pin:
486  return await self.async_step_pair_with_pinasync_step_pair_with_pin()
487 
488  return await self.async_step_pair_no_pinasync_step_pair_no_pin()
489 
491  self, user_input: dict[str, str] | None = None
492  ) -> ConfigFlowResult:
493  """Inform user that a protocol is disabled and cannot be paired."""
494  assert self.protocolprotocol
495  if user_input is not None:
496  return await self.async_pair_next_protocolasync_pair_next_protocol()
497  return self.async_show_formasync_show_formasync_show_form(
498  step_id="protocol_disabled",
499  description_placeholders={"protocol": protocol_str(self.protocolprotocol)},
500  )
501 
503  self, user_input: dict[str, str] | None = None
504  ) -> ConfigFlowResult:
505  """Handle pairing step where a PIN is required from the user."""
506  errors = {}
507  assert self.pairingpairing
508  assert self.protocolprotocol
509  if user_input is not None:
510  try:
511  self.pairingpairing.pin(user_input[CONF_PIN])
512  await self.pairingpairing.finish()
513  self.credentials[self.protocolprotocol.value] = self.pairingpairing.service.credentials
514  return await self.async_pair_next_protocolasync_pair_next_protocol()
515  except exceptions.PairingError:
516  _LOGGER.exception("Authentication problem")
517  errors["base"] = "invalid_auth"
518  except Exception:
519  _LOGGER.exception("Unexpected exception")
520  errors["base"] = "unknown"
521 
522  return self.async_show_formasync_show_formasync_show_form(
523  step_id="pair_with_pin",
524  data_schema=INPUT_PIN_SCHEMA,
525  errors=errors,
526  description_placeholders={"protocol": protocol_str(self.protocolprotocol)},
527  )
528 
530  self, user_input: dict[str, str] | None = None
531  ) -> ConfigFlowResult:
532  """Handle step where user has to enter a PIN on the device."""
533  assert self.pairingpairing
534  assert self.protocolprotocol
535  if user_input is not None:
536  await self.pairingpairing.finish()
537  if self.pairingpairing.has_paired:
538  self.credentials[self.protocolprotocol.value] = self.pairingpairing.service.credentials
539  return await self.async_pair_next_protocolasync_pair_next_protocol()
540 
541  await self.pairingpairing.close()
542  return self.async_abortasync_abortasync_abort(reason="device_did_not_pair")
543 
544  pin = randrange(1000, stop=10000)
545  self.pairingpairing.pin(pin)
546  return self.async_show_formasync_show_formasync_show_form(
547  step_id="pair_no_pin",
548  description_placeholders={
549  "protocol": protocol_str(self.protocolprotocol),
550  "pin": str(pin),
551  },
552  )
553 
555  self, user_input: dict[str, str] | None = None
556  ) -> ConfigFlowResult:
557  """Inform user that a service will not be added."""
558  assert self.protocolprotocol
559  if user_input is not None:
560  return await self.async_pair_next_protocolasync_pair_next_protocol()
561 
562  return self.async_show_formasync_show_formasync_show_form(
563  step_id="service_problem",
564  description_placeholders={"protocol": protocol_str(self.protocolprotocol)},
565  )
566 
568  self, user_input: dict[str, str] | None = None
569  ) -> ConfigFlowResult:
570  """Inform user that password is not supported."""
571  assert self.protocolprotocol
572  if user_input is not None:
573  return await self.async_pair_next_protocolasync_pair_next_protocol()
574 
575  return self.async_show_formasync_show_formasync_show_form(
576  step_id="password",
577  description_placeholders={"protocol": protocol_str(self.protocolprotocol)},
578  )
579 
580  async def _async_cleanup(self) -> None:
581  """Clean up allocated resources."""
582  if self.pairingpairing is not None:
583  await self.pairingpairing.close()
584  self.pairingpairing = None
585 
586  async def _async_get_entry(self) -> ConfigFlowResult:
587  """Return config entry or update existing config entry."""
588  # Abort if no protocols were paired
589  if not self.credentials:
590  return self.async_abortasync_abortasync_abort(reason="setup_failed")
591 
592  assert self.atv
593 
594  data = {
595  CONF_NAME: self.atv.name,
596  CONF_CREDENTIALS: self.credentials,
597  CONF_ADDRESS: str(self.atv.address),
598  CONF_IDENTIFIERS: self.atv_identifiersatv_identifiers,
599  }
600 
601  existing_entry = await self.async_set_unique_idasync_set_unique_id(
602  self.device_identifierdevice_identifier, raise_on_progress=False
603  )
604 
605  # If an existing config entry is updated, then this was a re-auth
606  if existing_entry:
607  return self.async_update_reload_and_abortasync_update_reload_and_abort(
608  existing_entry, data=data, unique_id=self.unique_idunique_id
609  )
610 
611  return self.async_create_entryasync_create_entryasync_create_entry(title=self.atv.name, data=data)
612 
613 
615  """Error to indicate device could not be found."""
616 
617 
618 class DeviceAlreadyConfigured(HomeAssistantError):
619  """Error to indicate device is already configured."""
ConfigFlowResult async_step_pair_with_pin(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:504
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:208
ConfigFlowResult async_step_confirm(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:390
str|None _entry_unique_id_from_identifers(self, set[str] all_identifiers)
Definition: config_flow.py:145
SchemaOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:114
ConfigFlowResult async_step_service_problem(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:556
ConfigFlowResult async_step_protocol_disabled(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:492
ConfigFlowResult async_step_password(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:569
ConfigFlowResult async_step_pair_no_pin(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:531
None _async_aggregate_discoveries(self, str host, str unique_id)
Definition: config_flow.py:244
ConfigFlowResult async_found_zeroconf_device(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:304
None _async_check_and_update_in_progress(self, str host, str unique_id)
Definition: config_flow.py:285
ConfigFlowResult async_step_restore_device(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:167
ConfigFlowResult async_find_device_wrapper(self, Callable[[], Awaitable[ConfigFlowResult]] next_func, bool allow_exist=False)
Definition: config_flow.py:321
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:156
ConfigFlowResult async_step_user(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:178
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
ConfigEntry|None async_set_unique_id(self, str|None unique_id=None, *bool raise_on_progress=True)
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_update_reload_and_abort(self, ConfigEntry entry, *str|None|UndefinedType unique_id=UNDEFINED, str|UndefinedType title=UNDEFINED, Mapping[str, Any]|UndefinedType data=UNDEFINED, Mapping[str, Any]|UndefinedType data_updates=UNDEFINED, Mapping[str, Any]|UndefinedType options=UNDEFINED, str|UndefinedType reason=UNDEFINED, bool reload_even_if_entry_is_unchanged=True)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
None _async_abort_entries_match(self, dict[str, Any]|None match_dict=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)
str
_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)
tuple[BaseConfig|None, list[str]|None] device_scan(HomeAssistant hass, str|None identifier, asyncio.AbstractEventLoop loop)
Definition: config_flow.py:62
AppriseNotificationService|None get_service(HomeAssistant hass, ConfigType config, DiscoveryInfoType|None discovery_info=None)
Definition: notify.py:39
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)