Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow to configure the LaMetric integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 from ipaddress import ip_address
7 import logging
8 from typing import Any
9 
10 from demetriek import (
11  CloudDevice,
12  LaMetricCloud,
13  LaMetricConnectionError,
14  LaMetricDevice,
15  Model,
16  Notification,
17  NotificationIconType,
18  NotificationPriority,
19  NotificationSound,
20  Simple,
21  Sound,
22 )
23 import voluptuous as vol
24 from yarl import URL
25 
26 from homeassistant.components.dhcp import DhcpServiceInfo
28  ATTR_UPNP_FRIENDLY_NAME,
29  ATTR_UPNP_SERIAL,
30  SsdpServiceInfo,
31 )
32 from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
33 from homeassistant.const import CONF_API_KEY, CONF_DEVICE, CONF_HOST, CONF_MAC
34 from homeassistant.data_entry_flow import AbortFlow
35 from homeassistant.helpers.aiohttp_client import async_get_clientsession
36 from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
37 from homeassistant.helpers.device_registry import format_mac
39  SelectOptionDict,
40  SelectSelector,
41  SelectSelectorConfig,
42  SelectSelectorMode,
43  TextSelector,
44  TextSelectorConfig,
45  TextSelectorType,
46 )
47 from homeassistant.util.network import is_link_local
48 
49 from .const import DOMAIN, LOGGER
50 
51 
53  """Handle a LaMetric config flow."""
54 
55  DOMAIN = DOMAIN
56  VERSION = 1
57 
58  devices: dict[str, CloudDevice]
59  discovered_host: str
60  discovered_serial: str
61  discovered: bool = False
62 
63  @property
64  def logger(self) -> logging.Logger:
65  """Return logger."""
66  return LOGGER
67 
68  @property
69  def extra_authorize_data(self) -> dict[str, Any]:
70  """Extra data that needs to be appended to the authorize url."""
71  return {"scope": "basic devices_read"}
72 
73  async def async_step_user(
74  self, user_input: dict[str, Any] | None = None
75  ) -> ConfigFlowResult:
76  """Handle a flow initiated by the user."""
77  return await self.async_step_choice_enter_manual_or_fetch_cloudasync_step_choice_enter_manual_or_fetch_cloud()
78 
79  async def async_step_ssdp(
80  self, discovery_info: SsdpServiceInfo
81  ) -> ConfigFlowResult:
82  """Handle a flow initiated by SSDP discovery."""
83  url = URL(discovery_info.ssdp_location or "")
84  if url.host is None or not (
85  serial := discovery_info.upnp.get(ATTR_UPNP_SERIAL)
86  ):
87  return self.async_abortasync_abortasync_abort(reason="invalid_discovery_info")
88 
89  if is_link_local(ip_address(url.host)):
90  return self.async_abortasync_abortasync_abort(reason="link_local_address")
91 
92  await self.async_set_unique_idasync_set_unique_id(serial)
93  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: url.host})
94 
95  self.context.update(
96  {
97  "title_placeholders": {
98  "name": discovery_info.upnp.get(
99  ATTR_UPNP_FRIENDLY_NAME, "LaMetric TIME"
100  ),
101  },
102  "configuration_url": "https://developer.lametric.com",
103  }
104  )
105 
106  self.discovereddiscovered = True
107  self.discovered_hostdiscovered_host = str(url.host)
108  self.discovered_serialdiscovered_serial = serial
109  return await self.async_step_choice_enter_manual_or_fetch_cloudasync_step_choice_enter_manual_or_fetch_cloud()
110 
111  async def async_step_reauth(
112  self, entry_data: Mapping[str, Any]
113  ) -> ConfigFlowResult:
114  """Handle initiation of re-authentication with LaMetric."""
115  return await self.async_step_choice_enter_manual_or_fetch_cloudasync_step_choice_enter_manual_or_fetch_cloud()
116 
118  self, user_input: dict[str, Any] | None = None
119  ) -> ConfigFlowResult:
120  """Handle the user's choice.
121 
122  Either enter the manual credentials or fetch the cloud credentials.
123  """
124  return self.async_show_menuasync_show_menu(
125  step_id="choice_enter_manual_or_fetch_cloud",
126  menu_options=["pick_implementation", "manual_entry"],
127  )
128 
130  self, user_input: dict[str, Any] | None = None
131  ) -> ConfigFlowResult:
132  """Handle the user's choice of entering the device manually."""
133  errors: dict[str, str] = {}
134  if user_input is not None:
135  if self.discovereddiscovered:
136  host = self.discovered_hostdiscovered_host
137  elif self.sourcesourcesourcesource == SOURCE_REAUTH:
138  host = self._get_reauth_entry_get_reauth_entry().data[CONF_HOST]
139  else:
140  host = user_input[CONF_HOST]
141 
142  try:
143  return await self._async_step_create_entry_async_step_create_entry(
144  host, user_input[CONF_API_KEY]
145  )
146  except AbortFlow:
147  raise
148  except LaMetricConnectionError as ex:
149  LOGGER.error("Error connecting to LaMetric: %s", ex)
150  errors["base"] = "cannot_connect"
151  except Exception: # noqa: BLE001
152  LOGGER.exception("Unexpected error occurred")
153  errors["base"] = "unknown"
154 
155  # Don't ask for a host if it was discovered
156  schema = {
157  vol.Required(CONF_API_KEY): TextSelector(
158  TextSelectorConfig(type=TextSelectorType.PASSWORD)
159  )
160  }
161  if not self.discovereddiscovered and self.sourcesourcesourcesource != SOURCE_REAUTH:
162  schema = {vol.Required(CONF_HOST): TextSelector()} | schema
163 
164  return self.async_show_formasync_show_formasync_show_form(
165  step_id="manual_entry",
166  data_schema=vol.Schema(schema),
167  errors=errors,
168  )
169 
171  self, data: dict[str, Any]
172  ) -> ConfigFlowResult:
173  """Fetch information about devices from the cloud."""
174  lametric = LaMetricCloud(
175  token=data["token"]["access_token"],
176  session=async_get_clientsession(self.hass),
177  )
178  self.devicesdevices = {
179  device.serial_number: device
180  for device in sorted(await lametric.devices(), key=lambda d: d.name)
181  }
182 
183  if not self.devicesdevices:
184  return self.async_abortasync_abortasync_abort(reason="no_devices")
185 
186  return await self.async_step_cloud_select_deviceasync_step_cloud_select_device()
187 
189  self, user_input: dict[str, Any] | None = None
190  ) -> ConfigFlowResult:
191  """Handle device selection from devices offered by the cloud."""
192  if self.discovereddiscovered:
193  user_input = {CONF_DEVICE: self.discovered_serialdiscovered_serial}
194  elif self.sourcesourcesourcesource == SOURCE_REAUTH:
195  reauth_unique_id = self._get_reauth_entry_get_reauth_entry().unique_id
196  if reauth_unique_id not in self.devicesdevices:
197  return self.async_abortasync_abortasync_abort(reason="reauth_device_not_found")
198  user_input = {CONF_DEVICE: reauth_unique_id}
199  elif len(self.devicesdevices) == 1:
200  user_input = {CONF_DEVICE: list(self.devicesdevices.values())[0].serial_number}
201 
202  errors: dict[str, str] = {}
203  if user_input is not None:
204  device = self.devicesdevices[user_input[CONF_DEVICE]]
205  try:
206  return await self._async_step_create_entry_async_step_create_entry(
207  str(device.ip), device.api_key
208  )
209  except AbortFlow:
210  raise
211  except LaMetricConnectionError as ex:
212  LOGGER.error("Error connecting to LaMetric: %s", ex)
213  errors["base"] = "cannot_connect"
214  except Exception: # noqa: BLE001
215  LOGGER.exception("Unexpected error occurred")
216  errors["base"] = "unknown"
217 
218  return self.async_show_formasync_show_formasync_show_form(
219  step_id="cloud_select_device",
220  data_schema=vol.Schema(
221  {
222  vol.Required(CONF_DEVICE): SelectSelector(
224  mode=SelectSelectorMode.DROPDOWN,
225  options=[
227  value=device.serial_number,
228  label=device.name,
229  )
230  for device in self.devicesdevices.values()
231  ],
232  )
233  ),
234  }
235  ),
236  errors=errors,
237  )
238 
240  self, host: str, api_key: str
241  ) -> ConfigFlowResult:
242  """Create entry."""
243  lametric = LaMetricDevice(
244  host=host,
245  api_key=api_key,
246  session=async_get_clientsession(self.hass),
247  )
248 
249  device = await lametric.device()
250 
251  if self.sourcesourcesourcesource != SOURCE_REAUTH:
252  await self.async_set_unique_idasync_set_unique_id(device.serial_number)
253  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
254  updates={CONF_HOST: lametric.host, CONF_API_KEY: lametric.api_key}
255  )
256 
257  notify_sound: Sound | None = None
258  if device.model != "sa5":
259  notify_sound = Sound(sound=NotificationSound.WIN)
260 
261  await lametric.notify(
262  notification=Notification(
263  priority=NotificationPriority.CRITICAL,
264  icon_type=NotificationIconType.INFO,
265  model=Model(
266  cycles=2,
267  frames=[Simple(text="Connected to Home Assistant!", icon=7956)],
268  sound=notify_sound,
269  ),
270  )
271  )
272 
273  if self.sourcesourcesourcesource == SOURCE_REAUTH:
274  return self.async_update_reload_and_abortasync_update_reload_and_abort(
275  self._get_reauth_entry_get_reauth_entry(),
276  data_updates={
277  CONF_HOST: lametric.host,
278  CONF_API_KEY: lametric.api_key,
279  },
280  )
281 
282  return self.async_create_entryasync_create_entryasync_create_entry(
283  title=device.name,
284  data={
285  CONF_API_KEY: lametric.api_key,
286  CONF_HOST: lametric.host,
287  CONF_MAC: device.wifi.mac,
288  },
289  )
290 
291  async def async_step_dhcp(
292  self, discovery_info: DhcpServiceInfo
293  ) -> ConfigFlowResult:
294  """Handle dhcp discovery to update existing entries."""
295  mac = format_mac(discovery_info.macaddress)
296  for entry in self._async_current_entries_async_current_entries():
297  if format_mac(entry.data[CONF_MAC]) == mac:
298  self.hass.config_entries.async_update_entry(
299  entry,
300  data=entry.data | {CONF_HOST: discovery_info.ip},
301  )
302  self.hass.async_create_task(
303  self.hass.config_entries.async_reload(entry.entry_id)
304  )
305  return self.async_abortasync_abortasync_abort(reason="already_configured")
306 
307  return self.async_abortasync_abortasync_abort(reason="unknown")
308 
309  # Replace OAuth create entry with a fetch devices step
310  # LaMetric only use OAuth to get device information, but doesn't
311  # use it later on.
312  async_oauth_create_entry = async_step_cloud_fetch_devices
ConfigFlowResult async_step_choice_enter_manual_or_fetch_cloud(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:119
ConfigFlowResult async_step_cloud_select_device(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:190
ConfigFlowResult async_step_manual_entry(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:131
ConfigFlowResult async_step_ssdp(self, SsdpServiceInfo discovery_info)
Definition: config_flow.py:81
ConfigFlowResult async_step_dhcp(self, DhcpServiceInfo discovery_info)
Definition: config_flow.py:293
ConfigFlowResult async_step_cloud_fetch_devices(self, dict[str, Any] data)
Definition: config_flow.py:172
ConfigFlowResult _async_step_create_entry(self, str host, str api_key)
Definition: config_flow.py:241
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:75
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:113
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)
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_show_menu(self, *str|None step_id=None, Container[str] menu_options, Mapping[str, str]|None description_placeholders=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)
str|None source(self)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
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)
bool is_link_local(IPv4Address|IPv6Address address)
Definition: network.py:48