Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow flow LIFX."""
2 
3 from __future__ import annotations
4 
5 import socket
6 from typing import Any, Self
7 
8 from aiolifx.aiolifx import Light
9 from aiolifx.connection import LIFXConnection
10 import voluptuous as vol
11 
12 from homeassistant.components import zeroconf
13 from homeassistant.components.dhcp import DhcpServiceInfo
14 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
15 from homeassistant.const import CONF_DEVICE, CONF_HOST
16 from homeassistant.core import callback
17 from homeassistant.helpers import device_registry as dr
18 from homeassistant.helpers.typing import DiscoveryInfoType
19 
20 from .const import (
21  _LOGGER,
22  CONF_SERIAL,
23  DEFAULT_ATTEMPTS,
24  DOMAIN,
25  OVERALL_TIMEOUT,
26  TARGET_ANY,
27 )
28 from .discovery import async_discover_devices
29 from .util import (
30  async_entry_is_legacy,
31  async_get_legacy_entry,
32  async_multi_execute_lifx_with_retries,
33  formatted_serial,
34  lifx_features,
35  mac_matches_serial_number,
36 )
37 
38 
39 class LifXConfigFlow(ConfigFlow, domain=DOMAIN):
40  """Handle a config flow for LIFX."""
41 
42  VERSION = 1
43 
44  host: str | None = None
45 
46  def __init__(self) -> None:
47  """Initialize the config flow."""
48  self._discovered_devices_discovered_devices: dict[str, Light] = {}
49  self._discovered_device_discovered_device: Light | None = None
50 
51  async def async_step_dhcp(
52  self, discovery_info: DhcpServiceInfo
53  ) -> ConfigFlowResult:
54  """Handle discovery via DHCP."""
55  mac = discovery_info.macaddress
56  host = discovery_info.ip
57  hass = self.hass
58  for entry in self._async_current_entries_async_current_entries():
59  if (
60  entry.unique_id
61  and not async_entry_is_legacy(entry)
62  and mac_matches_serial_number(mac, entry.unique_id)
63  ):
64  if entry.data[CONF_HOST] != host:
65  hass.config_entries.async_update_entry(
66  entry, data={**entry.data, CONF_HOST: host}
67  )
68  hass.async_create_task(
69  hass.config_entries.async_reload(entry.entry_id)
70  )
71  return self.async_abortasync_abortasync_abort(reason="already_configured")
72  return await self._async_handle_discovery_async_handle_discovery(host)
73 
74  async def async_step_homekit(
75  self, discovery_info: zeroconf.ZeroconfServiceInfo
76  ) -> ConfigFlowResult:
77  """Handle HomeKit discovery."""
78  return await self._async_handle_discovery_async_handle_discovery(host=discovery_info.host)
79 
81  self, discovery_info: DiscoveryInfoType
82  ) -> ConfigFlowResult:
83  """Handle LIFX UDP broadcast discovery."""
84  serial = discovery_info[CONF_SERIAL]
85  host = discovery_info[CONF_HOST]
86  await self.async_set_unique_idasync_set_unique_id(formatted_serial(serial))
87  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: host})
88  return await self._async_handle_discovery_async_handle_discovery(host, serial)
89 
91  self, host: str, serial: str | None = None
92  ) -> ConfigFlowResult:
93  """Handle any discovery."""
94  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: host})
95  self.hosthost = host
96  if self.hass.config_entries.flow.async_has_matching_flow(self):
97  return self.async_abortasync_abortasync_abort(reason="already_in_progress")
98  if not (
99  device := await self._async_try_connect_async_try_connect(
100  host, serial=serial, raise_on_progress=True
101  )
102  ):
103  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
104  self._discovered_device_discovered_device = device
105  return await self.async_step_discovery_confirmasync_step_discovery_confirm()
106 
107  def is_matching(self, other_flow: Self) -> bool:
108  """Return True if other_flow is matching this flow."""
109  return other_flow.host == self.hosthost
110 
111  @callback
113  """Check if a discovered device is pending migration."""
114  assert self.unique_idunique_id is not None
115  if not (legacy_entry := async_get_legacy_entry(self.hass)):
116  return False
117  device_registry = dr.async_get(self.hass)
118  existing_device = device_registry.async_get_device(
119  identifiers={(DOMAIN, self.unique_idunique_id)}
120  )
121  return bool(
122  existing_device is not None
123  and legacy_entry.entry_id in existing_device.config_entries
124  )
125 
127  self, user_input: dict[str, Any] | None = None
128  ) -> ConfigFlowResult:
129  """Confirm discovery."""
130  assert self._discovered_device_discovered_device is not None
131  discovered = self._discovered_device_discovered_device
132  _LOGGER.debug(
133  "Confirming discovery of %s (%s) [%s]",
134  discovered.label,
135  discovered.group,
136  discovered.mac_addr,
137  )
138  if user_input is not None or self._async_discovered_pending_migration_async_discovered_pending_migration():
139  return self._async_create_entry_from_device_async_create_entry_from_device(discovered)
140 
141  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: discovered.ip_addr})
142  self._set_confirm_only_set_confirm_only()
143  placeholders = {
144  "label": discovered.label,
145  "group": discovered.group,
146  }
147  self.context["title_placeholders"] = placeholders
148  return self.async_show_formasync_show_formasync_show_form(
149  step_id="discovery_confirm", description_placeholders=placeholders
150  )
151 
152  async def async_step_user(
153  self, user_input: dict[str, Any] | None = None
154  ) -> ConfigFlowResult:
155  """Handle the initial step."""
156  errors = {}
157  if user_input is not None:
158  host = user_input[CONF_HOST]
159  if not host:
160  return await self.async_step_pick_deviceasync_step_pick_device()
161  if (
162  device := await self._async_try_connect_async_try_connect(host, raise_on_progress=False)
163  ) is None:
164  errors["base"] = "cannot_connect"
165  else:
166  return self._async_create_entry_from_device_async_create_entry_from_device(device)
167 
168  return self.async_show_formasync_show_formasync_show_form(
169  step_id="user",
170  data_schema=vol.Schema({vol.Optional(CONF_HOST, default=""): str}),
171  errors=errors,
172  )
173 
175  self, user_input: dict[str, Any] | None = None
176  ) -> ConfigFlowResult:
177  """Handle the step to pick discovered device."""
178  if user_input is not None:
179  serial = user_input[CONF_DEVICE]
180  await self.async_set_unique_idasync_set_unique_id(serial, raise_on_progress=False)
181  device_without_label = self._discovered_devices_discovered_devices[serial]
182  device = await self._async_try_connect_async_try_connect(
183  device_without_label.ip_addr, raise_on_progress=False
184  )
185  if not device:
186  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
187  return self._async_create_entry_from_device_async_create_entry_from_device(device)
188 
189  configured_serials: set[str] = set()
190  configured_hosts: set[str] = set()
191  for entry in self._async_current_entries_async_current_entries():
192  if entry.unique_id and not async_entry_is_legacy(entry):
193  configured_serials.add(entry.unique_id)
194  configured_hosts.add(entry.data[CONF_HOST])
195  self._discovered_devices_discovered_devices = {
196  # device.mac_addr is not the mac_address, its the serial number
197  device.mac_addr: device
198  for device in await async_discover_devices(self.hass)
199  }
200  devices_name = {
201  serial: f"{serial} ({device.ip_addr})"
202  for serial, device in self._discovered_devices_discovered_devices.items()
203  if serial not in configured_serials
204  and device.ip_addr not in configured_hosts
205  }
206  # Check if there is at least one device
207  if not devices_name:
208  return self.async_abortasync_abortasync_abort(reason="no_devices_found")
209  return self.async_show_formasync_show_formasync_show_form(
210  step_id="pick_device",
211  data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}),
212  )
213 
214  @callback
215  def _async_create_entry_from_device(self, device: Light) -> ConfigFlowResult:
216  """Create a config entry from a smart device."""
217  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: device.ip_addr})
218  return self.async_create_entryasync_create_entryasync_create_entry(
219  title=device.label,
220  data={CONF_HOST: device.ip_addr},
221  )
222 
224  self, host: str, serial: str | None = None, raise_on_progress: bool = True
225  ) -> Light | None:
226  """Try to connect."""
227  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: host})
228  connection = LIFXConnection(host, TARGET_ANY)
229  try:
230  await connection.async_setup()
231  except socket.gaierror:
232  return None
233  device: Light = connection.device
234  try:
235  # get_hostfirmware required for MAC address offset
236  # get_version required for lifx_features()
237  # get_label required to log the name of the device
238  # get_group required to populate suggested areas
239  messages = await async_multi_execute_lifx_with_retries(
240  [
241  device.get_hostfirmware,
242  device.get_version,
243  device.get_label,
244  device.get_group,
245  ],
246  DEFAULT_ATTEMPTS,
247  OVERALL_TIMEOUT,
248  )
249  except TimeoutError:
250  return None
251  finally:
252  connection.async_stop()
253  if (
254  messages is None
255  or len(messages) != 4
256  or lifx_features(device)["relays"] is True
257  or device.host_firmware_version is None
258  ):
259  return None # relays not supported
260  # device.mac_addr is not the mac_address, its the serial number
261  device.mac_addr = serial or messages[0].target_addr
262  await self.async_set_unique_idasync_set_unique_id(
263  formatted_serial(device.mac_addr), raise_on_progress=raise_on_progress
264  )
265  return device
ConfigFlowResult _async_create_entry_from_device(self, Light device)
Definition: config_flow.py:215
ConfigFlowResult async_step_dhcp(self, DhcpServiceInfo discovery_info)
Definition: config_flow.py:53
ConfigFlowResult async_step_homekit(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:76
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:154
ConfigFlowResult _async_handle_discovery(self, str host, str|None serial=None)
Definition: config_flow.py:92
Light|None _async_try_connect(self, str host, str|None serial=None, bool raise_on_progress=True)
Definition: config_flow.py:225
ConfigFlowResult async_step_pick_device(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:176
ConfigFlowResult async_step_integration_discovery(self, DiscoveryInfoType discovery_info)
Definition: config_flow.py:82
ConfigFlowResult async_step_discovery_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:128
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)
None _async_abort_entries_match(self, dict[str, Any]|None match_dict=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_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)
list[ElkSystem] async_discover_devices(HomeAssistant hass, int timeout, str|None address=None)
Definition: discovery.py:43
ConfigEntry|None async_get_legacy_entry(HomeAssistant hass)
Definition: util.py:49
list[Message] async_multi_execute_lifx_with_retries(list[Callable] methods, int attempts, int overall_timeout)
Definition: util.py:196
bool async_entry_is_legacy(ConfigEntry entry)
Definition: util.py:43
bool mac_matches_serial_number(str mac_addr, str serial_number)
Definition: util.py:176
str formatted_serial(str serial_number)
Definition: util.py:171
dict[str, Any] lifx_features(Light bulb)
Definition: util.py:78