Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow to configure the Bluetooth integration."""
2 
3 from __future__ import annotations
4 
5 import platform
6 from typing import Any, cast
7 
8 from bluetooth_adapters import (
9  ADAPTER_ADDRESS,
10  ADAPTER_MANUFACTURER,
11  DEFAULT_ADDRESS,
12  AdapterDetails,
13  adapter_human_name,
14  adapter_model,
15  get_adapters,
16 )
17 from habluetooth import get_manager
18 import voluptuous as vol
19 
20 from homeassistant.components import onboarding
21 from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
22 from homeassistant.core import callback
24  SchemaFlowFormStep,
25  SchemaOptionsFlowHandler,
26 )
27 from homeassistant.helpers.typing import DiscoveryInfoType
28 
29 from .const import CONF_ADAPTER, CONF_DETAILS, CONF_PASSIVE, DOMAIN
30 from .util import adapter_title
31 
32 OPTIONS_SCHEMA = vol.Schema(
33  {
34  vol.Required(CONF_PASSIVE, default=False): bool,
35  }
36 )
37 OPTIONS_FLOW = {
38  "init": SchemaFlowFormStep(OPTIONS_SCHEMA),
39 }
40 
41 
42 def adapter_display_info(adapter: str, details: AdapterDetails) -> str:
43  """Return the adapter display info."""
44  name = adapter_human_name(adapter, details[ADAPTER_ADDRESS])
45  model = adapter_model(details)
46  manufacturer = details[ADAPTER_MANUFACTURER] or "Unknown"
47  return f"{name} {manufacturer} {model}"
48 
49 
50 class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
51  """Config flow for Bluetooth."""
52 
53  VERSION = 1
54 
55  def __init__(self) -> None:
56  """Initialize the config flow."""
57  self._adapter_adapter: str | None = None
58  self._details_details: AdapterDetails | None = None
59  self._adapters_adapters: dict[str, AdapterDetails] = {}
60  self._placeholders_placeholders: dict[str, str] = {}
61 
63  self, discovery_info: DiscoveryInfoType
64  ) -> ConfigFlowResult:
65  """Handle a flow initialized by discovery."""
66  self._adapter_adapter = cast(str, discovery_info[CONF_ADAPTER])
67  self._details_details = cast(AdapterDetails, discovery_info[CONF_DETAILS])
68  await self.async_set_unique_idasync_set_unique_id(self._details_details[ADAPTER_ADDRESS])
69  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
70  details = self._details_details
71  self._async_set_adapter_info_async_set_adapter_info(self._adapter_adapter, details)
72  return await self.async_step_single_adapterasync_step_single_adapter()
73 
74  @callback
75  def _async_set_adapter_info(self, adapter: str, details: AdapterDetails) -> None:
76  """Set the adapter info."""
77  name = adapter_human_name(adapter, details[ADAPTER_ADDRESS])
78  model = adapter_model(details)
79  manufacturer = details[ADAPTER_MANUFACTURER]
80  self._placeholders_placeholders = {
81  "name": name,
82  "model": model,
83  "manufacturer": manufacturer or "Unknown",
84  }
85  self.context["title_placeholders"] = self._placeholders_placeholders
86 
88  self, user_input: dict[str, Any] | None = None
89  ) -> ConfigFlowResult:
90  """Select an adapter."""
91  adapter = self._adapter_adapter
92  details = self._details_details
93  assert adapter is not None
94  assert details is not None
95  assert self._placeholders_placeholders is not None
96 
97  address = details[ADAPTER_ADDRESS]
98 
99  if user_input is not None or not onboarding.async_is_onboarded(self.hass):
100  await self.async_set_unique_idasync_set_unique_id(address, raise_on_progress=False)
101  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
102  return self.async_create_entryasync_create_entryasync_create_entry(
103  title=adapter_title(adapter, details), data={}
104  )
105 
106  return self.async_show_formasync_show_formasync_show_form(
107  step_id="single_adapter",
108  description_placeholders=self._placeholders_placeholders,
109  )
110 
112  self, user_input: dict[str, Any] | None = None
113  ) -> ConfigFlowResult:
114  """Handle a flow initialized by the user."""
115  if user_input is not None:
116  assert self._adapters_adapters is not None
117  adapter = user_input[CONF_ADAPTER]
118  details = self._adapters_adapters[adapter]
119  address = details[ADAPTER_ADDRESS]
120  await self.async_set_unique_idasync_set_unique_id(address, raise_on_progress=False)
121  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
122  return self.async_create_entryasync_create_entryasync_create_entry(
123  title=adapter_title(adapter, details), data={}
124  )
125 
126  configured_addresses = self._async_current_ids_async_current_ids()
127  bluetooth_adapters = get_adapters()
128  await bluetooth_adapters.refresh()
129  self._adapters_adapters = bluetooth_adapters.adapters
130  system = platform.system()
131  unconfigured_adapters = [
132  adapter
133  for adapter, details in self._adapters_adapters.items()
134  if details[ADAPTER_ADDRESS] not in configured_addresses
135  # DEFAULT_ADDRESS is perfectly valid on MacOS but on
136  # Linux it means the adapter is not yet configured
137  # or crashed
138  and not (system == "Linux" and details[ADAPTER_ADDRESS] == DEFAULT_ADDRESS)
139  ]
140  if not unconfigured_adapters:
141  ignored_adapters = len(
142  self._async_current_entries_async_current_entries(include_ignore=True)
143  ) - len(self._async_current_entries_async_current_entries(include_ignore=False))
144  return self.async_abortasync_abortasync_abort(
145  reason="no_adapters",
146  description_placeholders={"ignored_adapters": str(ignored_adapters)},
147  )
148  if len(unconfigured_adapters) == 1:
149  self._adapter_adapter = list(self._adapters_adapters)[0]
150  self._details_details = self._adapters_adapters[self._adapter_adapter]
151  self._async_set_adapter_info_async_set_adapter_info(self._adapter_adapter, self._details_details)
152  return await self.async_step_single_adapterasync_step_single_adapter()
153 
154  return self.async_show_formasync_show_formasync_show_form(
155  step_id="multiple_adapters",
156  data_schema=vol.Schema(
157  {
158  vol.Required(CONF_ADAPTER): vol.In(
159  {
160  adapter: adapter_display_info(
161  adapter, self._adapters_adapters[adapter]
162  )
163  for adapter in sorted(unconfigured_adapters)
164  }
165  ),
166  }
167  ),
168  )
169 
170  async def async_step_user(
171  self, user_input: dict[str, Any] | None = None
172  ) -> ConfigFlowResult:
173  """Handle a flow initialized by the user."""
174  return await self.async_step_multiple_adaptersasync_step_multiple_adapters()
175 
176  @staticmethod
177  @callback
179  config_entry: ConfigEntry,
180  ) -> SchemaOptionsFlowHandler:
181  """Get the options flow for this handler."""
182  return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
183 
184  @classmethod
185  @callback
186  def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool:
187  """Return options flow support for this handler."""
188  return bool((manager := get_manager()) and manager.supports_passive_scan)
bool async_supports_options_flow(cls, ConfigEntry config_entry)
Definition: config_flow.py:186
SchemaOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:180
ConfigFlowResult async_step_single_adapter(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:89
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:172
ConfigFlowResult async_step_multiple_adapters(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:113
None _async_set_adapter_info(self, str adapter, AdapterDetails details)
Definition: config_flow.py:75
ConfigFlowResult async_step_integration_discovery(self, DiscoveryInfoType discovery_info)
Definition: config_flow.py:64
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
set[str|None] _async_current_ids(self, bool include_ignore=True)
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_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)
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)
str adapter_display_info(str adapter, AdapterDetails details)
Definition: config_flow.py:42
str adapter_title(str adapter, AdapterDetails details)
Definition: util.py:82