Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Snooz component."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from dataclasses import dataclass
7 from typing import Any
8 
9 from pysnooz.advertisement import SnoozAdvertisementData
10 import voluptuous as vol
11 
13  BluetoothScanningMode,
14  BluetoothServiceInfo,
15  async_discovered_service_info,
16  async_process_advertisements,
17 )
18 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
19 from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TOKEN
20 
21 from .const import DOMAIN
22 
23 # number of seconds to wait for a device to be put in pairing mode
24 WAIT_FOR_PAIRING_TIMEOUT = 30
25 
26 
27 @dataclass
29  """Represents a discovered Snooz device."""
30 
31  info: BluetoothServiceInfo
32  device: SnoozAdvertisementData
33 
34 
35 class SnoozConfigFlow(ConfigFlow, domain=DOMAIN):
36  """Handle a config flow for Snooz."""
37 
38  VERSION = 1
39 
40  def __init__(self) -> None:
41  """Initialize the config flow."""
42  self._discovery_discovery: DiscoveredSnooz | None = None
43  self._discovered_devices: dict[str, DiscoveredSnooz] = {}
44  self._pairing_task_pairing_task: asyncio.Task | None = None
45 
47  self, discovery_info: BluetoothServiceInfo
48  ) -> ConfigFlowResult:
49  """Handle the bluetooth discovery step."""
50  await self.async_set_unique_idasync_set_unique_id(discovery_info.address)
51  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
52  device = SnoozAdvertisementData()
53  if not device.supported(discovery_info):
54  return self.async_abortasync_abortasync_abort(reason="not_supported")
55  self._discovery_discovery = DiscoveredSnooz(discovery_info, device)
56  return await self.async_step_bluetooth_confirmasync_step_bluetooth_confirm()
57 
59  self, user_input: dict[str, Any] | None = None
60  ) -> ConfigFlowResult:
61  """Confirm discovery."""
62  assert self._discovery_discovery is not None
63 
64  if user_input is not None:
65  if not self._discovery_discovery.device.is_pairing:
66  return await self.async_step_wait_for_pairing_modeasync_step_wait_for_pairing_mode()
67 
68  return self._create_snooz_entry_create_snooz_entry(self._discovery_discovery)
69 
70  self._set_confirm_only_set_confirm_only()
71  assert self._discovery_discovery.device.display_name
72  placeholders = {"name": self._discovery_discovery.device.display_name}
73  self.context["title_placeholders"] = placeholders
74  return self.async_show_formasync_show_formasync_show_form(
75  step_id="bluetooth_confirm", description_placeholders=placeholders
76  )
77 
78  async def async_step_user(
79  self, user_input: dict[str, Any] | None = None
80  ) -> ConfigFlowResult:
81  """Handle the user step to pick discovered device."""
82  if user_input is not None:
83  name = user_input[CONF_NAME]
84 
85  discovered = self._discovered_devices[name]
86 
87  assert discovered is not None
88 
89  self._discovery_discovery = discovered
90 
91  if not discovered.device.is_pairing:
92  return await self.async_step_wait_for_pairing_modeasync_step_wait_for_pairing_mode()
93 
94  address = discovered.info.address
95  await self.async_set_unique_idasync_set_unique_id(address, raise_on_progress=False)
96  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
97  return self._create_snooz_entry_create_snooz_entry(discovered)
98 
99  configured_addresses = self._async_current_ids_async_current_ids()
100 
101  for info in async_discovered_service_info(self.hass):
102  address = info.address
103  if address in configured_addresses:
104  continue
105  device = SnoozAdvertisementData()
106  if device.supported(info):
107  assert device.display_name
108  self._discovered_devices[device.display_name] = DiscoveredSnooz(
109  info, device
110  )
111 
112  if not self._discovered_devices:
113  return self.async_abortasync_abortasync_abort(reason="no_devices_found")
114 
115  return self.async_show_formasync_show_formasync_show_form(
116  step_id="user",
117  data_schema=vol.Schema(
118  {
119  vol.Required(CONF_NAME): vol.In(
120  [
121  d.device.display_name
122  for d in self._discovered_devices.values()
123  ]
124  )
125  }
126  ),
127  )
128 
130  self, user_input: dict[str, Any] | None = None
131  ) -> ConfigFlowResult:
132  """Wait for device to enter pairing mode."""
133  if not self._pairing_task_pairing_task:
134  self._pairing_task_pairing_task = self.hass.async_create_task(
135  self._async_wait_for_pairing_mode_async_wait_for_pairing_mode()
136  )
137 
138  if not self._pairing_task_pairing_task.done():
139  return self.async_show_progressasync_show_progress(
140  step_id="wait_for_pairing_mode",
141  progress_action="wait_for_pairing_mode",
142  progress_task=self._pairing_task_pairing_task,
143  )
144 
145  try:
146  await self._pairing_task_pairing_task
147  except TimeoutError:
148  return self.async_show_progress_doneasync_show_progress_done(next_step_id="pairing_timeout")
149  finally:
150  self._pairing_task_pairing_task = None
151 
152  return self.async_show_progress_doneasync_show_progress_done(next_step_id="pairing_complete")
153 
155  self, user_input: dict[str, Any] | None = None
156  ) -> ConfigFlowResult:
157  """Create a configuration entry for a device that entered pairing mode."""
158  assert self._discovery_discovery
159 
160  await self.async_set_unique_idasync_set_unique_id(
161  self._discovery_discovery.info.address, raise_on_progress=False
162  )
163  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
164 
165  return self._create_snooz_entry_create_snooz_entry(self._discovery_discovery)
166 
168  self, user_input: dict[str, Any] | None = None
169  ) -> ConfigFlowResult:
170  """Inform the user that the device never entered pairing mode."""
171  if user_input is not None:
172  return await self.async_step_wait_for_pairing_modeasync_step_wait_for_pairing_mode()
173 
174  self._set_confirm_only_set_confirm_only()
175  return self.async_show_formasync_show_formasync_show_form(step_id="pairing_timeout")
176 
177  def _create_snooz_entry(self, discovery: DiscoveredSnooz) -> ConfigFlowResult:
178  assert discovery.device.display_name
179  return self.async_create_entryasync_create_entryasync_create_entry(
180  title=discovery.device.display_name,
181  data={
182  CONF_ADDRESS: discovery.info.address,
183  CONF_TOKEN: discovery.device.pairing_token,
184  },
185  )
186 
187  async def _async_wait_for_pairing_mode(self) -> None:
188  """Process advertisements until pairing mode is detected."""
189  assert self._discovery_discovery
190  device = self._discovery_discovery.device
191 
192  def is_device_in_pairing_mode(
193  service_info: BluetoothServiceInfo,
194  ) -> bool:
195  return device.supported(service_info) and device.is_pairing
196 
198  self.hass,
199  is_device_in_pairing_mode,
200  {"address": self._discovery_discovery.info.address},
201  BluetoothScanningMode.ACTIVE,
202  WAIT_FOR_PAIRING_TIMEOUT,
203  )
ConfigFlowResult _create_snooz_entry(self, DiscoveredSnooz discovery)
Definition: config_flow.py:177
ConfigFlowResult async_step_pairing_timeout(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:169
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:80
ConfigFlowResult async_step_bluetooth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:60
ConfigFlowResult async_step_bluetooth(self, BluetoothServiceInfo discovery_info)
Definition: config_flow.py:48
ConfigFlowResult async_step_wait_for_pairing_mode(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:131
ConfigFlowResult async_step_pairing_complete(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:156
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)
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)
_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_show_progress(self, *str|None step_id=None, str progress_action, Mapping[str, str]|None description_placeholders=None, asyncio.Task[Any]|None progress_task=None)
_FlowResultT async_show_progress_done(self, *str next_step_id)
_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)
Iterable[BluetoothServiceInfoBleak] async_discovered_service_info(HomeAssistant hass, bool connectable=True)
Definition: api.py:72
BluetoothServiceInfoBleak async_process_advertisements(HomeAssistant hass, ProcessAdvertisementCallback callback, BluetoothCallbackMatcher match_dict, BluetoothScanningMode mode, int timeout)
Definition: api.py:134