Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Flow handler for Crownstone."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from typing import Any
7 
8 from crownstone_cloud import CrownstoneCloud
9 from crownstone_cloud.exceptions import (
10  CrownstoneAuthenticationError,
11  CrownstoneUnknownError,
12 )
13 import serial.tools.list_ports
14 from serial.tools.list_ports_common import ListPortInfo
15 import voluptuous as vol
16 
17 from homeassistant.components import usb
18 from homeassistant.config_entries import (
19  ConfigEntry,
20  ConfigEntryBaseFlow,
21  ConfigFlow,
22  ConfigFlowResult,
23  OptionsFlow,
24 )
25 from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
26 from homeassistant.core import callback
27 from homeassistant.helpers import aiohttp_client
28 
29 from .const import (
30  CONF_USB_MANUAL_PATH,
31  CONF_USB_PATH,
32  CONF_USB_SPHERE,
33  CONF_USB_SPHERE_OPTION,
34  CONF_USE_USB_OPTION,
35  DOMAIN,
36  DONT_USE_USB,
37  MANUAL_PATH,
38  REFRESH_LIST,
39 )
40 from .helpers import list_ports_as_str
41 
42 CONFIG_FLOW = "config_flow"
43 OPTIONS_FLOW = "options_flow"
44 
45 
47  """Represent the base flow for Crownstone."""
48 
49  cloud: CrownstoneCloud
50 
51  def __init__(
52  self, flow_type: str, create_entry_cb: Callable[..., ConfigFlowResult]
53  ) -> None:
54  """Set up flow instance."""
55  self.flow_typeflow_type = flow_type
56  self.create_entry_callbackcreate_entry_callback = create_entry_cb
57  self.usb_pathusb_path: str | None = None
58  self.usb_sphere_idusb_sphere_id: str | None = None
59 
61  self, user_input: dict[str, Any] | None = None
62  ) -> ConfigFlowResult:
63  """Set up a Crownstone USB dongle."""
64  list_of_ports = await self.hass.async_add_executor_job(
65  serial.tools.list_ports.comports
66  )
67  if self.flow_typeflow_type == CONFIG_FLOW:
68  ports_as_string = list_ports_as_str(list_of_ports)
69  else:
70  ports_as_string = list_ports_as_str(list_of_ports, False)
71 
72  if user_input is not None:
73  selection = user_input[CONF_USB_PATH]
74 
75  if selection == DONT_USE_USB:
76  return self.create_entry_callbackcreate_entry_callback()
77  if selection == MANUAL_PATH:
78  return await self.async_step_usb_manual_configasync_step_usb_manual_config()
79  if selection != REFRESH_LIST:
80  if self.flow_typeflow_type == OPTIONS_FLOW:
81  index = ports_as_string.index(selection)
82  else:
83  index = ports_as_string.index(selection) - 1
84 
85  selected_port: ListPortInfo = list_of_ports[index]
86  self.usb_pathusb_path = await self.hass.async_add_executor_job(
87  usb.get_serial_by_id, selected_port.device
88  )
89  return await self.async_step_usb_sphere_configasync_step_usb_sphere_config()
90 
91  return self.async_show_formasync_show_form(
92  step_id="usb_config",
93  data_schema=vol.Schema(
94  {vol.Required(CONF_USB_PATH): vol.In(ports_as_string)}
95  ),
96  )
97 
99  self, user_input: dict[str, Any] | None = None
100  ) -> ConfigFlowResult:
101  """Manually enter Crownstone USB dongle path."""
102  if user_input is None:
103  return self.async_show_formasync_show_form(
104  step_id="usb_manual_config",
105  data_schema=vol.Schema({vol.Required(CONF_USB_MANUAL_PATH): str}),
106  )
107 
108  self.usb_pathusb_path = user_input[CONF_USB_MANUAL_PATH]
109  return await self.async_step_usb_sphere_configasync_step_usb_sphere_config()
110 
112  self, user_input: dict[str, Any] | None = None
113  ) -> ConfigFlowResult:
114  """Select a Crownstone sphere that the USB operates in."""
115  spheres = {sphere.name: sphere.cloud_id for sphere in self.cloud.cloud_data}
116  # no need to select if there's only 1 option
117  sphere_id: str | None = None
118  if len(spheres) == 1:
119  sphere_id = next(iter(spheres.values()))
120 
121  if user_input is None and sphere_id is None:
122  return self.async_show_formasync_show_form(
123  step_id="usb_sphere_config",
124  data_schema=vol.Schema({CONF_USB_SPHERE: vol.In(spheres.keys())}),
125  )
126 
127  if sphere_id:
128  self.usb_sphere_idusb_sphere_id = sphere_id
129  elif user_input:
130  self.usb_sphere_idusb_sphere_id = spheres[user_input[CONF_USB_SPHERE]]
131 
132  return self.create_entry_callbackcreate_entry_callback()
133 
134 
136  """Handle a config flow for Crownstone."""
137 
138  VERSION = 1
139 
140  @staticmethod
141  @callback
143  config_entry: ConfigEntry,
144  ) -> CrownstoneOptionsFlowHandler:
145  """Return the Crownstone options."""
146  return CrownstoneOptionsFlowHandler(config_entry)
147 
148  def __init__(self) -> None:
149  """Initialize the flow."""
150  super().__init__(CONFIG_FLOW, self.async_create_new_entryasync_create_new_entry)
151  self.login_infologin_info: dict[str, Any] = {}
152 
153  async def async_step_user(
154  self, user_input: dict[str, Any] | None = None
155  ) -> ConfigFlowResult:
156  """Handle the initial step."""
157  errors: dict[str, str] = {}
158  if user_input is None:
159  return self.async_show_formasync_show_formasync_show_form(
160  step_id="user",
161  data_schema=vol.Schema(
162  {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str}
163  ),
164  )
165 
166  self.cloudcloud = CrownstoneCloud(
167  email=user_input[CONF_EMAIL],
168  password=user_input[CONF_PASSWORD],
169  clientsession=aiohttp_client.async_get_clientsession(self.hass),
170  )
171  # Login & sync all user data
172  try:
173  await self.cloudcloud.async_initialize()
174  except CrownstoneAuthenticationError as auth_error:
175  if auth_error.type == "LOGIN_FAILED":
176  errors["base"] = "invalid_auth"
177  elif auth_error.type == "LOGIN_FAILED_EMAIL_NOT_VERIFIED":
178  errors["base"] = "account_not_verified"
179  except CrownstoneUnknownError:
180  errors["base"] = "unknown"
181 
182  # show form again, with the errors
183  if errors:
184  return self.async_show_formasync_show_formasync_show_form(
185  step_id="user",
186  data_schema=vol.Schema(
187  {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str}
188  ),
189  errors=errors,
190  )
191 
192  await self.async_set_unique_idasync_set_unique_id(self.cloudcloud.cloud_data.user_id)
193  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
194 
195  self.login_infologin_info = user_input
196  return await self.async_step_usb_configasync_step_usb_config()
197 
198  def async_create_new_entry(self) -> ConfigFlowResult:
199  """Create a new entry."""
200  return super().async_create_entry(
201  title=f"Account: {self.login_info[CONF_EMAIL]}",
202  data={
203  CONF_EMAIL: self.login_infologin_info[CONF_EMAIL],
204  CONF_PASSWORD: self.login_infologin_info[CONF_PASSWORD],
205  },
206  options={CONF_USB_PATH: self.usb_pathusb_path, CONF_USB_SPHERE: self.usb_sphere_idusb_sphere_id},
207  )
208 
209 
211  """Handle Crownstone options."""
212 
213  def __init__(self, config_entry: ConfigEntry) -> None:
214  """Initialize Crownstone options."""
215  super().__init__(OPTIONS_FLOW, self.async_create_new_entryasync_create_new_entry)
216  self.optionsoptions = config_entry.options.copy()
217 
218  async def async_step_init(
219  self, user_input: dict[str, Any] | None = None
220  ) -> ConfigFlowResult:
221  """Manage Crownstone options."""
222  self.cloud: CrownstoneCloud = self.hass.data[DOMAIN][
223  self.config_entryconfig_entryconfig_entry.entry_id
224  ].cloud
225 
226  spheres = {sphere.name: sphere.cloud_id for sphere in self.cloud.cloud_data}
227  usb_path = self.config_entryconfig_entryconfig_entry.options.get(CONF_USB_PATH)
228  usb_sphere = self.config_entryconfig_entryconfig_entry.options.get(CONF_USB_SPHERE)
229 
230  options_schema = vol.Schema(
231  {vol.Optional(CONF_USE_USB_OPTION, default=usb_path is not None): bool}
232  )
233  if usb_path is not None and len(spheres) > 1:
234  options_schema = options_schema.extend(
235  {
236  vol.Optional(
237  CONF_USB_SPHERE_OPTION,
238  default=self.cloud.cloud_data.data[usb_sphere].name,
239  ): vol.In(spheres.keys())
240  }
241  )
242 
243  if user_input is not None:
244  if user_input[CONF_USE_USB_OPTION] and usb_path is None:
245  return await self.async_step_usb_configasync_step_usb_config()
246  if not user_input[CONF_USE_USB_OPTION] and usb_path is not None:
247  self.optionsoptions[CONF_USB_PATH] = None
248  self.optionsoptions[CONF_USB_SPHERE] = None
249  elif (
250  CONF_USB_SPHERE_OPTION in user_input
251  and spheres[user_input[CONF_USB_SPHERE_OPTION]] != usb_sphere
252  ):
253  sphere_id = spheres[user_input[CONF_USB_SPHERE_OPTION]]
254  self.optionsoptions[CONF_USB_SPHERE] = sphere_id
255 
256  return self.async_create_new_entryasync_create_new_entry()
257 
258  return self.async_show_formasync_show_form(step_id="init", data_schema=options_schema)
259 
260  def async_create_new_entry(self) -> ConfigFlowResult:
261  """Create a new entry."""
262  # these attributes will only change when a usb was configured
263  if self.usb_pathusb_path is not None and self.usb_sphere_idusb_sphere_id is not None:
264  self.optionsoptions[CONF_USB_PATH] = self.usb_pathusb_path
265  self.optionsoptions[CONF_USB_SPHERE] = self.usb_sphere_idusb_sphere_id
266 
267  return super().async_create_entry(title="", data=self.optionsoptions)
ConfigFlowResult async_step_usb_sphere_config(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:113
ConfigFlowResult async_step_usb_manual_config(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:100
None __init__(self, str flow_type, Callable[..., ConfigFlowResult] create_entry_cb)
Definition: config_flow.py:53
ConfigFlowResult async_step_usb_config(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:62
CrownstoneOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:144
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:155
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:220
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)
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)
_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)
list[str] list_ports_as_str(list[ListPortInfo] serial_ports, bool no_usb_option=True)
Definition: helpers.py:16