Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for WiZ Platform."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 from pywizlight import wizlight
9 from pywizlight.discovery import DiscoveredBulb
10 from pywizlight.exceptions import WizLightConnectionError, WizLightTimeOutError
11 import voluptuous as vol
12 
13 from homeassistant.components import dhcp, onboarding
14 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
15 from homeassistant.const import CONF_HOST
16 from homeassistant.data_entry_flow import AbortFlow
17 from homeassistant.util.network import is_ip_address
18 
19 from .const import DEFAULT_NAME, DISCOVER_SCAN_TIMEOUT, DOMAIN, WIZ_CONNECT_EXCEPTIONS
20 from .discovery import async_discover_devices
21 from .utils import _short_mac, name_from_bulb_type_and_mac
22 
23 _LOGGER = logging.getLogger(__name__)
24 
25 CONF_DEVICE = "device"
26 
27 
28 class WizConfigFlow(ConfigFlow, domain=DOMAIN):
29  """Handle a config flow for WiZ."""
30 
31  VERSION = 1
32 
33  _discovered_device: DiscoveredBulb
34  _name: str
35 
36  def __init__(self) -> None:
37  """Initialize the config flow."""
38  self._discovered_devices_discovered_devices: dict[str, DiscoveredBulb] = {}
39 
40  async def async_step_dhcp(
41  self, discovery_info: dhcp.DhcpServiceInfo
42  ) -> ConfigFlowResult:
43  """Handle discovery via dhcp."""
44  self._discovered_device_discovered_device = DiscoveredBulb(
45  discovery_info.ip, discovery_info.macaddress
46  )
47  return await self._async_handle_discovery_async_handle_discovery()
48 
50  self, discovery_info: dict[str, str]
51  ) -> ConfigFlowResult:
52  """Handle integration discovery."""
53  self._discovered_device_discovered_device = DiscoveredBulb(
54  discovery_info["ip_address"], discovery_info["mac_address"]
55  )
56  return await self._async_handle_discovery_async_handle_discovery()
57 
58  async def _async_handle_discovery(self) -> ConfigFlowResult:
59  """Handle any discovery."""
60  device = self._discovered_device_discovered_device
61  _LOGGER.debug("Discovered device: %s", device)
62  ip_address = device.ip_address
63  mac = device.mac_address
64  await self.async_set_unique_idasync_set_unique_id(mac)
65  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: ip_address})
66  await self._async_connect_discovered_or_abort_async_connect_discovered_or_abort()
67  return await self.async_step_discovery_confirmasync_step_discovery_confirm()
68 
69  async def _async_connect_discovered_or_abort(self) -> None:
70  """Connect to the device and verify its responding."""
71  device = self._discovered_device_discovered_device
72  bulb = wizlight(device.ip_address)
73  try:
74  bulbtype = await bulb.get_bulbtype()
75  except WIZ_CONNECT_EXCEPTIONS as ex:
76  _LOGGER.debug(
77  "Failed to connect to %s during discovery: %s",
78  device.ip_address,
79  ex,
80  exc_info=True,
81  )
82  raise AbortFlow("cannot_connect") from ex
83  self._name_name = name_from_bulb_type_and_mac(bulbtype, device.mac_address)
84 
86  self, user_input: dict[str, Any] | None = None
87  ) -> ConfigFlowResult:
88  """Confirm discovery."""
89  ip_address = self._discovered_device_discovered_device.ip_address
90  if user_input is not None or not onboarding.async_is_onboarded(self.hass):
91  # Make sure the device is still there and
92  # update the name if the firmware has auto
93  # updated since discovery
94  await self._async_connect_discovered_or_abort_async_connect_discovered_or_abort()
95  return self.async_create_entryasync_create_entryasync_create_entry(
96  title=self._name_name,
97  data={CONF_HOST: ip_address},
98  )
99 
100  self._set_confirm_only_set_confirm_only()
101  placeholders = {"name": self._name_name, "host": ip_address}
102  self.context["title_placeholders"] = placeholders
103  return self.async_show_formasync_show_formasync_show_form(
104  step_id="discovery_confirm",
105  description_placeholders=placeholders,
106  )
107 
109  self, user_input: dict[str, Any] | None = None
110  ) -> ConfigFlowResult:
111  """Handle the step to pick discovered device."""
112  if user_input is not None:
113  device = self._discovered_devices_discovered_devices[user_input[CONF_DEVICE]]
114  await self.async_set_unique_idasync_set_unique_id(device.mac_address, raise_on_progress=False)
115  bulb = wizlight(device.ip_address)
116  try:
117  bulbtype = await bulb.get_bulbtype()
118  except WIZ_CONNECT_EXCEPTIONS:
119  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
120 
121  return self.async_create_entryasync_create_entryasync_create_entry(
122  title=name_from_bulb_type_and_mac(bulbtype, device.mac_address),
123  data={CONF_HOST: device.ip_address},
124  )
125 
126  current_unique_ids = self._async_current_ids_async_current_ids()
127  current_hosts = {
128  entry.data[CONF_HOST]
129  for entry in self._async_current_entries_async_current_entries(include_ignore=False)
130  }
131  discovered_devices = await async_discover_devices(
132  self.hass, DISCOVER_SCAN_TIMEOUT
133  )
134  self._discovered_devices_discovered_devices = {
135  device.mac_address: device for device in discovered_devices
136  }
137  devices_name = {
138  mac: f"{DEFAULT_NAME} {_short_mac(mac)} ({device.ip_address})"
139  for mac, device in self._discovered_devices_discovered_devices.items()
140  if mac not in current_unique_ids and device.ip_address not in current_hosts
141  }
142  # Check if there is at least one device
143  if not devices_name:
144  return self.async_abortasync_abortasync_abort(reason="no_devices_found")
145  return self.async_show_formasync_show_formasync_show_form(
146  step_id="pick_device",
147  data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}),
148  )
149 
150  async def async_step_user(
151  self, user_input: dict[str, Any] | None = None
152  ) -> ConfigFlowResult:
153  """Handle a flow initialized by the user."""
154  errors = {}
155  if user_input is not None:
156  if not (host := user_input[CONF_HOST]):
157  return await self.async_step_pick_deviceasync_step_pick_device()
158  if not is_ip_address(user_input[CONF_HOST]):
159  errors["base"] = "no_ip"
160  else:
161  bulb = wizlight(host)
162  try:
163  bulbtype = await bulb.get_bulbtype()
164  mac = await bulb.getMac()
165  except WizLightTimeOutError:
166  errors["base"] = "bulb_time_out"
167  except ConnectionRefusedError:
168  errors["base"] = "cannot_connect"
169  except WizLightConnectionError:
170  errors["base"] = "no_wiz_light"
171  except Exception:
172  _LOGGER.exception("Unexpected exception")
173  errors["base"] = "unknown"
174  else:
175  await self.async_set_unique_idasync_set_unique_id(mac, raise_on_progress=False)
176  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
177  updates={CONF_HOST: user_input[CONF_HOST]}
178  )
179  name = name_from_bulb_type_and_mac(bulbtype, mac)
180  return self.async_create_entryasync_create_entryasync_create_entry(
181  title=name,
182  data=user_input,
183  )
184 
185  return self.async_show_formasync_show_formasync_show_form(
186  step_id="user",
187  data_schema=vol.Schema({vol.Optional(CONF_HOST, default=""): str}),
188  errors=errors,
189  )
ConfigFlowResult async_step_discovery_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:87
ConfigFlowResult async_step_dhcp(self, dhcp.DhcpServiceInfo discovery_info)
Definition: config_flow.py:42
ConfigFlowResult async_step_integration_discovery(self, dict[str, str] discovery_info)
Definition: config_flow.py:51
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:152
ConfigFlowResult async_step_pick_device(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:110
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)
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
str name_from_bulb_type_and_mac(BulbType bulb_type, str mac)
Definition: utils.py:16
bool is_ip_address(str address)
Definition: network.py:63