Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Flux LED/MagicLight."""
2 
3 from __future__ import annotations
4 
5 import contextlib
6 from typing import Any, Self, cast
7 
8 from flux_led.const import (
9  ATTR_ID,
10  ATTR_IPADDR,
11  ATTR_MODEL,
12  ATTR_MODEL_DESCRIPTION,
13  ATTR_MODEL_INFO,
14  ATTR_VERSION_NUM,
15 )
16 from flux_led.scanner import FluxLEDDiscovery
17 import voluptuous as vol
18 
19 from homeassistant.components import dhcp
20 from homeassistant.config_entries import (
21  SOURCE_IGNORE,
22  ConfigEntry,
23  ConfigEntryState,
24  ConfigFlow,
25  ConfigFlowResult,
26  OptionsFlow,
27 )
28 from homeassistant.const import CONF_DEVICE, CONF_HOST
29 from homeassistant.core import callback
30 from homeassistant.data_entry_flow import AbortFlow
31 from homeassistant.helpers import device_registry as dr
32 from homeassistant.helpers.dispatcher import async_dispatcher_send
33 from homeassistant.helpers.typing import DiscoveryInfoType
34 
35 from . import async_wifi_bulb_for_host
36 from .const import (
37  CONF_CUSTOM_EFFECT_COLORS,
38  CONF_CUSTOM_EFFECT_SPEED_PCT,
39  CONF_CUSTOM_EFFECT_TRANSITION,
40  DEFAULT_EFFECT_SPEED,
41  DISCOVER_SCAN_TIMEOUT,
42  DOMAIN,
43  FLUX_LED_DISCOVERY_SIGNAL,
44  FLUX_LED_EXCEPTIONS,
45  TRANSITION_GRADUAL,
46  TRANSITION_JUMP,
47  TRANSITION_STROBE,
48 )
49 from .discovery import (
50  async_discover_device,
51  async_discover_devices,
52  async_name_from_discovery,
53  async_populate_data_from_discovery,
54  async_update_entry_from_discovery,
55 )
56 from .util import format_as_flux_mac, mac_matches_by_one
57 
58 
59 class FluxLedConfigFlow(ConfigFlow, domain=DOMAIN):
60  """Handle a config flow for Magic Home Integration."""
61 
62  VERSION = 1
63 
64  host: str | None = None
65 
66  def __init__(self) -> None:
67  """Initialize the config flow."""
68  self._discovered_devices_discovered_devices: dict[str, FluxLEDDiscovery] = {}
69  self._discovered_device_discovered_device: FluxLEDDiscovery | None = None
70  self._allow_update_mac_allow_update_mac = False
71 
72  @staticmethod
73  @callback
75  config_entry: ConfigEntry,
76  ) -> FluxLedOptionsFlow:
77  """Get the options flow for the Flux LED component."""
78  return FluxLedOptionsFlow()
79 
80  async def async_step_dhcp(
81  self, discovery_info: dhcp.DhcpServiceInfo
82  ) -> ConfigFlowResult:
83  """Handle discovery via dhcp."""
84  self._discovered_device_discovered_device = FluxLEDDiscovery(
85  ipaddr=discovery_info.ip,
86  model=None,
87  id=format_as_flux_mac(discovery_info.macaddress),
88  model_num=None,
89  version_num=None,
90  firmware_date=None,
91  model_info=None,
92  model_description=None,
93  remote_access_enabled=None,
94  remote_access_host=None,
95  remote_access_port=None,
96  )
97  return await self._async_handle_discovery_async_handle_discovery()
98 
100  self, discovery_info: DiscoveryInfoType
101  ) -> ConfigFlowResult:
102  """Handle integration discovery."""
103  self._allow_update_mac_allow_update_mac = True
104  self._discovered_device_discovered_device = cast(FluxLEDDiscovery, discovery_info)
105  return await self._async_handle_discovery_async_handle_discovery()
106 
108  self, device: FluxLEDDiscovery, allow_update_mac: bool
109  ) -> None:
110  """Set the discovered mac.
111 
112  We only allow it to be updated if it comes from udp
113  discovery since the dhcp mac can be one digit off from
114  the udp discovery mac for devices with multiple network interfaces
115  """
116  mac_address = device[ATTR_ID]
117  assert mac_address is not None
118  mac = dr.format_mac(mac_address)
119  await self.async_set_unique_idasync_set_unique_id(mac)
120  for entry in self._async_current_entries_async_current_entries(include_ignore=True):
121  if not (
122  entry.data.get(CONF_HOST) == device[ATTR_IPADDR]
123  or (
124  entry.unique_id
125  and ":" in entry.unique_id
126  and mac_matches_by_one(entry.unique_id, mac)
127  )
128  ):
129  continue
130  if entry.source == SOURCE_IGNORE:
131  raise AbortFlow("already_configured")
132  if (
134  self.hass, entry, device, None, allow_update_mac
135  )
136  and entry.state
137  not in (
138  ConfigEntryState.SETUP_IN_PROGRESS,
139  ConfigEntryState.NOT_LOADED,
140  )
141  ) or entry.state == ConfigEntryState.SETUP_RETRY:
142  self.hass.config_entries.async_schedule_reload(entry.entry_id)
143  else:
145  self.hass,
146  FLUX_LED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id),
147  )
148  raise AbortFlow("already_configured")
149 
150  async def _async_handle_discovery(self) -> ConfigFlowResult:
151  """Handle any discovery."""
152  device = self._discovered_device_discovered_device
153  assert device is not None
154  await self._async_set_discovered_mac_async_set_discovered_mac(device, self._allow_update_mac_allow_update_mac)
155  host = device[ATTR_IPADDR]
156  self.hosthost = host
157  if self.hass.config_entries.flow.async_has_matching_flow(self):
158  return self.async_abortasync_abortasync_abort(reason="already_in_progress")
159  if not device[ATTR_MODEL_DESCRIPTION]:
160  mac_address = device[ATTR_ID]
161  assert mac_address is not None
162  mac = dr.format_mac(mac_address)
163  try:
164  device = await self._async_try_connect_async_try_connect(host, device)
165  except FLUX_LED_EXCEPTIONS:
166  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
167 
168  discovered_mac = device[ATTR_ID]
169  if device[ATTR_MODEL_DESCRIPTION] or (
170  discovered_mac is not None
171  and (formatted_discovered_mac := dr.format_mac(discovered_mac))
172  and formatted_discovered_mac != mac
173  and mac_matches_by_one(discovered_mac, mac)
174  ):
175  self._discovered_device_discovered_device = device
176  await self._async_set_discovered_mac_async_set_discovered_mac(device, True)
177  return await self.async_step_discovery_confirmasync_step_discovery_confirm()
178 
179  def is_matching(self, other_flow: Self) -> bool:
180  """Return True if other_flow is matching this flow."""
181  return other_flow.host == self.hosthost
182 
184  self, user_input: dict[str, Any] | None = None
185  ) -> ConfigFlowResult:
186  """Confirm discovery."""
187  assert self._discovered_device_discovered_device is not None
188  device = self._discovered_device_discovered_device
189  mac_address = device[ATTR_ID]
190  assert mac_address is not None
191  if user_input is not None:
192  return self._async_create_entry_from_device_async_create_entry_from_device(self._discovered_device_discovered_device)
193 
194  self._set_confirm_only_set_confirm_only()
195  placeholders = {
196  "model": device[ATTR_MODEL_DESCRIPTION]
197  or device[ATTR_MODEL]
198  or "Magic Home",
199  "id": mac_address[-6:],
200  "ipaddr": device[ATTR_IPADDR],
201  }
202  self.context["title_placeholders"] = placeholders
203  return self.async_show_formasync_show_formasync_show_form(
204  step_id="discovery_confirm", description_placeholders=placeholders
205  )
206 
207  @callback
209  self, device: FluxLEDDiscovery
210  ) -> ConfigFlowResult:
211  """Create a config entry from a device."""
212  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: device[ATTR_IPADDR]})
213  name = async_name_from_discovery(device)
214  data: dict[str, Any] = {CONF_HOST: device[ATTR_IPADDR]}
215  async_populate_data_from_discovery(data, data, device)
216  return self.async_create_entryasync_create_entryasync_create_entry(
217  title=name,
218  data=data,
219  )
220 
221  async def async_step_user(
222  self, user_input: dict[str, Any] | None = None
223  ) -> ConfigFlowResult:
224  """Handle the initial step."""
225  errors = {}
226  if user_input is not None:
227  if not (host := user_input[CONF_HOST]):
228  return await self.async_step_pick_deviceasync_step_pick_device()
229  try:
230  device = await self._async_try_connect_async_try_connect(host, None)
231  except FLUX_LED_EXCEPTIONS:
232  errors["base"] = "cannot_connect"
233  else:
234  if (mac_address := device[ATTR_ID]) is not None:
235  await self.async_set_unique_idasync_set_unique_id(
236  dr.format_mac(mac_address), raise_on_progress=False
237  )
238  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: host})
239  return self._async_create_entry_from_device_async_create_entry_from_device(device)
240 
241  return self.async_show_formasync_show_formasync_show_form(
242  step_id="user",
243  data_schema=vol.Schema({vol.Optional(CONF_HOST, default=""): str}),
244  errors=errors,
245  )
246 
248  self, user_input: dict[str, Any] | None = None
249  ) -> ConfigFlowResult:
250  """Handle the step to pick discovered device."""
251  if user_input is not None:
252  mac = user_input[CONF_DEVICE]
253  await self.async_set_unique_idasync_set_unique_id(mac, raise_on_progress=False)
254  device = self._discovered_devices_discovered_devices[mac]
255  if not device.get(ATTR_MODEL_DESCRIPTION):
256  with contextlib.suppress(*FLUX_LED_EXCEPTIONS):
257  device = await self._async_try_connect_async_try_connect(device[ATTR_IPADDR], device)
258  return self._async_create_entry_from_device_async_create_entry_from_device(device)
259 
260  current_unique_ids = self._async_current_ids_async_current_ids()
261  current_hosts = {
262  entry.data[CONF_HOST]
263  for entry in self._async_current_entries_async_current_entries(include_ignore=False)
264  }
265  discovered_devices = await async_discover_devices(
266  self.hass, DISCOVER_SCAN_TIMEOUT
267  )
268  self._discovered_devices_discovered_devices = {}
269  for device in discovered_devices:
270  mac_address = device[ATTR_ID]
271  assert mac_address is not None
272  self._discovered_devices_discovered_devices[dr.format_mac(mac_address)] = device
273  devices_name = {
274  mac: f"{async_name_from_discovery(device)} ({device[ATTR_IPADDR]})"
275  for mac, device in self._discovered_devices_discovered_devices.items()
276  if mac not in current_unique_ids
277  and device[ATTR_IPADDR] not in current_hosts
278  }
279  # Check if there is at least one device
280  if not devices_name:
281  return self.async_abortasync_abortasync_abort(reason="no_devices_found")
282  return self.async_show_formasync_show_formasync_show_form(
283  step_id="pick_device",
284  data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}),
285  )
286 
288  self, host: str, discovery: FluxLEDDiscovery | None
289  ) -> FluxLEDDiscovery:
290  """Try to connect."""
291  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: host})
292  if (device := await async_discover_device(self.hass, host)) and device[
293  ATTR_MODEL_DESCRIPTION
294  ]:
295  # Older models do not return enough information
296  # to build the model description via UDP so we have
297  # to fallback to making a tcp connection to avoid
298  # identifying the device as the chip model number
299  # AKA `HF-LPB100-ZJ200`
300  return device
301  bulb = async_wifi_bulb_for_host(host, discovery=device)
302  bulb.discovery = discovery
303  try:
304  await bulb.async_setup(lambda: None)
305  finally:
306  await bulb.async_stop()
307  return FluxLEDDiscovery(
308  ipaddr=host,
309  model=discovery[ATTR_MODEL] if discovery else None,
310  id=discovery[ATTR_ID] if discovery else None,
311  model_num=bulb.model_num,
312  version_num=discovery[ATTR_VERSION_NUM] if discovery else None,
313  firmware_date=None,
314  model_info=discovery[ATTR_MODEL_INFO] if discovery else None,
315  model_description=bulb.model_data.description,
316  remote_access_enabled=None,
317  remote_access_host=None,
318  remote_access_port=None,
319  )
320 
321 
323  """Handle flux_led options."""
324 
325  async def async_step_init(
326  self, user_input: dict[str, Any] | None = None
327  ) -> ConfigFlowResult:
328  """Configure the options."""
329  errors: dict[str, str] = {}
330  if user_input is not None:
331  return self.async_create_entryasync_create_entry(title="", data=user_input)
332 
333  options = self.config_entryconfig_entryconfig_entry.options
334  options_schema = vol.Schema(
335  {
336  vol.Optional(
337  CONF_CUSTOM_EFFECT_COLORS,
338  default=options.get(CONF_CUSTOM_EFFECT_COLORS, ""),
339  ): str,
340  vol.Optional(
341  CONF_CUSTOM_EFFECT_SPEED_PCT,
342  default=options.get(
343  CONF_CUSTOM_EFFECT_SPEED_PCT, DEFAULT_EFFECT_SPEED
344  ),
345  ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)),
346  vol.Optional(
347  CONF_CUSTOM_EFFECT_TRANSITION,
348  default=options.get(
349  CONF_CUSTOM_EFFECT_TRANSITION, TRANSITION_GRADUAL
350  ),
351  ): vol.In([TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE]),
352  }
353  )
354 
355  return self.async_show_formasync_show_form(
356  step_id="init", data_schema=options_schema, errors=errors
357  )
ConfigFlowResult _async_create_entry_from_device(self, FluxLEDDiscovery device)
Definition: config_flow.py:210
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:223
FluxLEDDiscovery _async_try_connect(self, str host, FluxLEDDiscovery|None discovery)
Definition: config_flow.py:289
ConfigFlowResult async_step_discovery_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:185
FluxLedOptionsFlow async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:76
ConfigFlowResult async_step_pick_device(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:249
None _async_set_discovered_mac(self, FluxLEDDiscovery device, bool allow_update_mac)
Definition: config_flow.py:109
ConfigFlowResult async_step_dhcp(self, dhcp.DhcpServiceInfo discovery_info)
Definition: config_flow.py:82
ConfigFlowResult async_step_integration_discovery(self, DiscoveryInfoType discovery_info)
Definition: config_flow.py:101
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:327
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)
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)
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)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
ElkSystem|None async_discover_device(HomeAssistant hass, str host)
Definition: discovery.py:78
list[ElkSystem] async_discover_devices(HomeAssistant hass, int timeout, str|None address=None)
Definition: discovery.py:43
bool async_update_entry_from_discovery(HomeAssistant hass, config_entries.ConfigEntry entry, ElkSystem device)
Definition: discovery.py:30
str async_name_from_discovery(FluxLEDDiscovery device, int|None model_num=None)
Definition: discovery.py:87
None async_populate_data_from_discovery(Mapping[str, Any] current_data, dict[str, Any] data_updates, FluxLEDDiscovery device)
Definition: discovery.py:104
bool mac_matches_by_one(str formatted_mac_1, str formatted_mac_2)
Definition: util.py:30
str|None format_as_flux_mac(str|None mac)
Definition: util.py:21
AIOWifiLedBulb async_wifi_bulb_for_host(str host, FluxLEDDiscovery|None discovery)
Definition: __init__.py:84
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193