Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Adds config flow for Vulcan."""
2 
3 from collections.abc import Mapping
4 import logging
5 from typing import TYPE_CHECKING, Any
6 
7 from aiohttp import ClientConnectionError
8 import voluptuous as vol
9 from vulcan import (
10  Account,
11  ExpiredTokenException,
12  InvalidPINException,
13  InvalidSymbolException,
14  InvalidTokenException,
15  Keystore,
16  UnauthorizedCertificateException,
17  Vulcan,
18 )
19 from vulcan.model import Student
20 
21 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
22 from homeassistant.const import CONF_PIN, CONF_REGION, CONF_TOKEN
23 from homeassistant.helpers.aiohttp_client import async_get_clientsession
24 
25 from . import DOMAIN
26 from .register import register
27 
28 _LOGGER = logging.getLogger(__name__)
29 
30 LOGIN_SCHEMA = {
31  vol.Required(CONF_TOKEN): str,
32  vol.Required(CONF_REGION): str,
33  vol.Required(CONF_PIN): str,
34 }
35 
36 
37 class VulcanFlowHandler(ConfigFlow, domain=DOMAIN):
38  """Handle a Uonet+ Vulcan config flow."""
39 
40  VERSION = 1
41 
42  account: Account
43  keystore: Keystore
44 
45  def __init__(self) -> None:
46  """Initialize config flow."""
47  self.studentsstudents: list[Student] | None = None
48 
49  async def async_step_user(
50  self, user_input: dict[str, Any] | None = None
51  ) -> ConfigFlowResult:
52  """Handle config flow."""
53  if self._async_current_entries_async_current_entries():
54  return await self.async_step_add_next_config_entryasync_step_add_next_config_entry()
55 
56  return await self.async_step_authasync_step_auth()
57 
58  async def async_step_auth(
59  self,
60  user_input: dict[str, str] | None = None,
61  errors: dict[str, str] | None = None,
62  ) -> ConfigFlowResult:
63  """Authorize integration."""
64 
65  if user_input is not None:
66  try:
67  credentials = await register(
68  user_input[CONF_TOKEN],
69  user_input[CONF_REGION],
70  user_input[CONF_PIN],
71  )
72  except InvalidSymbolException:
73  errors = {"base": "invalid_symbol"}
74  except InvalidTokenException:
75  errors = {"base": "invalid_token"}
76  except InvalidPINException:
77  errors = {"base": "invalid_pin"}
78  except ExpiredTokenException:
79  errors = {"base": "expired_token"}
80  except ClientConnectionError as err:
81  errors = {"base": "cannot_connect"}
82  _LOGGER.error("Connection error: %s", err)
83  except Exception:
84  _LOGGER.exception("Unexpected exception")
85  errors = {"base": "unknown"}
86  if not errors:
87  account = credentials["account"]
88  keystore = credentials["keystore"]
89  client = Vulcan(keystore, account, async_get_clientsession(self.hass))
90  students = await client.get_students()
91 
92  if len(students) > 1:
93  self.accountaccount = account
94  self.keystorekeystore = keystore
95  self.studentsstudents = students
96  return await self.async_step_select_studentasync_step_select_student()
97  student = students[0]
98  await self.async_set_unique_idasync_set_unique_id(str(student.pupil.id))
99  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
100  return self.async_create_entryasync_create_entryasync_create_entry(
101  title=f"{student.pupil.first_name} {student.pupil.last_name}",
102  data={
103  "student_id": str(student.pupil.id),
104  "keystore": keystore.as_dict,
105  "account": account.as_dict,
106  },
107  )
108 
109  return self.async_show_formasync_show_formasync_show_form(
110  step_id="auth",
111  data_schema=vol.Schema(LOGIN_SCHEMA),
112  errors=errors,
113  )
114 
116  self, user_input: dict[str, str] | None = None
117  ) -> ConfigFlowResult:
118  """Allow user to select student."""
119  errors: dict[str, str] = {}
120  students: dict[str, str] = {}
121  if self.studentsstudents is not None:
122  for student in self.studentsstudents:
123  students[str(student.pupil.id)] = (
124  f"{student.pupil.first_name} {student.pupil.last_name}"
125  )
126  if user_input is not None:
127  if TYPE_CHECKING:
128  assert self.keystorekeystore is not None
129  student_id = user_input["student"]
130  await self.async_set_unique_idasync_set_unique_id(str(student_id))
131  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
132  return self.async_create_entryasync_create_entryasync_create_entry(
133  title=students[student_id],
134  data={
135  "student_id": str(student_id),
136  "keystore": self.keystorekeystore.as_dict,
137  "account": self.accountaccount.as_dict,
138  },
139  )
140 
141  return self.async_show_formasync_show_formasync_show_form(
142  step_id="select_student",
143  data_schema=vol.Schema({vol.Required("student"): vol.In(students)}),
144  errors=errors,
145  )
146 
148  self,
149  user_input: dict[str, str] | None = None,
150  errors: dict[str, str] | None = None,
151  ) -> ConfigFlowResult:
152  """Allow user to select saved credentials."""
153 
154  credentials: dict[str, Any] = {}
155  for entry in self.hass.config_entries.async_entries(DOMAIN):
156  credentials[entry.entry_id] = entry.data["account"]["UserName"]
157 
158  if user_input is not None:
159  existing_entry = self.hass.config_entries.async_get_entry(
160  user_input["credentials"]
161  )
162  if TYPE_CHECKING:
163  assert existing_entry is not None
164  keystore = Keystore.load(existing_entry.data["keystore"])
165  account = Account.load(existing_entry.data["account"])
166  client = Vulcan(keystore, account, async_get_clientsession(self.hass))
167  try:
168  students = await client.get_students()
169  except UnauthorizedCertificateException:
170  return await self.async_step_authasync_step_auth(
171  errors={"base": "expired_credentials"}
172  )
173  except ClientConnectionError as err:
174  _LOGGER.error("Connection error: %s", err)
175  return await self.async_step_select_saved_credentialsasync_step_select_saved_credentials(
176  errors={"base": "cannot_connect"}
177  )
178  except Exception:
179  _LOGGER.exception("Unexpected exception")
180  return await self.async_step_authasync_step_auth(errors={"base": "unknown"})
181  if len(students) == 1:
182  student = students[0]
183  await self.async_set_unique_idasync_set_unique_id(str(student.pupil.id))
184  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
185  return self.async_create_entryasync_create_entryasync_create_entry(
186  title=f"{student.pupil.first_name} {student.pupil.last_name}",
187  data={
188  "student_id": str(student.pupil.id),
189  "keystore": keystore.as_dict,
190  "account": account.as_dict,
191  },
192  )
193  self.accountaccount = account
194  self.keystorekeystore = keystore
195  self.studentsstudents = students
196  return await self.async_step_select_studentasync_step_select_student()
197 
198  data_schema = {
199  vol.Required(
200  "credentials",
201  ): vol.In(credentials),
202  }
203  return self.async_show_formasync_show_formasync_show_form(
204  step_id="select_saved_credentials",
205  data_schema=vol.Schema(data_schema),
206  errors=errors,
207  )
208 
210  self, user_input: dict[str, bool] | None = None
211  ) -> ConfigFlowResult:
212  """Flow initialized when user is adding next entry of that integration."""
213 
214  existing_entries = self.hass.config_entries.async_entries(DOMAIN)
215 
216  errors: dict[str, str] = {}
217 
218  if user_input is not None:
219  if not user_input["use_saved_credentials"]:
220  return await self.async_step_authasync_step_auth()
221  if len(existing_entries) > 1:
222  return await self.async_step_select_saved_credentialsasync_step_select_saved_credentials()
223  keystore = Keystore.load(existing_entries[0].data["keystore"])
224  account = Account.load(existing_entries[0].data["account"])
225  client = Vulcan(keystore, account, async_get_clientsession(self.hass))
226  students = await client.get_students()
227  existing_entry_ids = [
228  entry.data["student_id"] for entry in existing_entries
229  ]
230  new_students = [
231  student
232  for student in students
233  if str(student.pupil.id) not in existing_entry_ids
234  ]
235  if not new_students:
236  return self.async_abortasync_abortasync_abort(reason="all_student_already_configured")
237  if len(new_students) == 1:
238  await self.async_set_unique_idasync_set_unique_id(str(new_students[0].pupil.id))
239  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
240  return self.async_create_entryasync_create_entryasync_create_entry(
241  title=(
242  f"{new_students[0].pupil.first_name} {new_students[0].pupil.last_name}"
243  ),
244  data={
245  "student_id": str(new_students[0].pupil.id),
246  "keystore": keystore.as_dict,
247  "account": account.as_dict,
248  },
249  )
250  self.accountaccount = account
251  self.keystorekeystore = keystore
252  self.studentsstudents = new_students
253  return await self.async_step_select_studentasync_step_select_student()
254 
255  data_schema = {
256  vol.Required("use_saved_credentials", default=True): bool,
257  }
258  return self.async_show_formasync_show_formasync_show_form(
259  step_id="add_next_config_entry",
260  data_schema=vol.Schema(data_schema),
261  errors=errors,
262  )
263 
264  async def async_step_reauth(
265  self, entry_data: Mapping[str, Any]
266  ) -> ConfigFlowResult:
267  """Perform reauth upon an API authentication error."""
268  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
269 
271  self, user_input: dict[str, str] | None = None
272  ) -> ConfigFlowResult:
273  """Reauthorize integration."""
274  errors = {}
275  if user_input is not None:
276  try:
277  credentials = await register(
278  user_input[CONF_TOKEN],
279  user_input[CONF_REGION],
280  user_input[CONF_PIN],
281  )
282  except InvalidSymbolException:
283  errors = {"base": "invalid_symbol"}
284  except InvalidTokenException:
285  errors = {"base": "invalid_token"}
286  except InvalidPINException:
287  errors = {"base": "invalid_pin"}
288  except ExpiredTokenException:
289  errors = {"base": "expired_token"}
290  except ClientConnectionError as err:
291  errors["base"] = "cannot_connect"
292  _LOGGER.error("Connection error: %s", err)
293  except Exception:
294  _LOGGER.exception("Unexpected exception")
295  errors["base"] = "unknown"
296  if not errors:
297  account = credentials["account"]
298  keystore = credentials["keystore"]
299  client = Vulcan(keystore, account, async_get_clientsession(self.hass))
300  students = await client.get_students()
301  existing_entries = self.hass.config_entries.async_entries(DOMAIN)
302  matching_entries = False
303  for student in students:
304  for entry in existing_entries:
305  if str(student.pupil.id) == str(entry.data["student_id"]):
306  self.hass.config_entries.async_update_entry(
307  entry,
308  title=(
309  f"{student.pupil.first_name} {student.pupil.last_name}"
310  ),
311  data={
312  "student_id": str(student.pupil.id),
313  "keystore": keystore.as_dict,
314  "account": account.as_dict,
315  },
316  )
317  await self.hass.config_entries.async_reload(entry.entry_id)
318  matching_entries = True
319  if not matching_entries:
320  return self.async_abortasync_abortasync_abort(reason="no_matching_entries")
321  return self.async_abortasync_abortasync_abort(reason="reauth_successful")
322 
323  return self.async_show_formasync_show_formasync_show_form(
324  step_id="reauth_confirm",
325  data_schema=vol.Schema(LOGIN_SCHEMA),
326  errors=errors,
327  )
ConfigFlowResult async_step_select_saved_credentials(self, dict[str, str]|None user_input=None, dict[str, str]|None errors=None)
Definition: config_flow.py:151
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:266
ConfigFlowResult async_step_select_student(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:117
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:51
ConfigFlowResult async_step_reauth_confirm(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:272
ConfigFlowResult async_step_add_next_config_entry(self, dict[str, bool]|None user_input=None)
Definition: config_flow.py:211
ConfigFlowResult async_step_auth(self, dict[str, str]|None user_input=None, dict[str, str]|None errors=None)
Definition: config_flow.py:62
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_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)
def register(HomeAssistant hass, Heos controller)
Definition: services.py:29
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)