"""Config flow for WiZ Platform.""" from __future__ import annotations import logging from typing import Any from pywizlight import wizlight from pywizlight.discovery import DiscoveredBulb from pywizlight.exceptions import WizLightConnectionError, WizLightTimeOutError import voluptuous as vol from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.util.network import is_ip_address from .const import DEFAULT_NAME, DISCOVER_SCAN_TIMEOUT, DOMAIN, WIZ_CONNECT_EXCEPTIONS from .discovery import async_discover_devices from .utils import _short_mac, name_from_bulb_type_and_mac _LOGGER = logging.getLogger(__name__) CONF_DEVICE = "device" class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for WiZ.""" VERSION = 1 def __init__(self) -> None: """Initialize the config flow.""" self._discovered_device: DiscoveredBulb | None = None self._discovered_devices: dict[str, DiscoveredBulb] = {} self._name: str | None = None async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle discovery via dhcp.""" self._discovered_device = DiscoveredBulb( discovery_info.ip, discovery_info.macaddress ) return await self._async_handle_discovery() async def async_step_integration_discovery( self, discovery_info: dict[str, str] ) -> FlowResult: """Handle integration discovery.""" self._discovered_device = DiscoveredBulb( discovery_info["ip_address"], discovery_info["mac_address"] ) return await self._async_handle_discovery() async def _async_handle_discovery(self) -> FlowResult: """Handle any discovery.""" device = self._discovered_device assert device is not None _LOGGER.debug("Discovered device: %s", device) ip_address = device.ip_address mac = device.mac_address await self.async_set_unique_id(mac) self._abort_if_unique_id_configured(updates={CONF_HOST: ip_address}) await self._async_connect_discovered_or_abort() return await self.async_step_discovery_confirm() async def _async_connect_discovered_or_abort(self) -> None: """Connect to the device and verify its responding.""" device = self._discovered_device assert device is not None bulb = wizlight(device.ip_address) try: bulbtype = await bulb.get_bulbtype() except WIZ_CONNECT_EXCEPTIONS as ex: _LOGGER.debug( "Failed to connect to %s during discovery: %s", device.ip_address, ex, exc_info=True, ) raise AbortFlow("cannot_connect") from ex self._name = name_from_bulb_type_and_mac(bulbtype, device.mac_address) async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm discovery.""" assert self._discovered_device is not None assert self._name is not None ip_address = self._discovered_device.ip_address if user_input is not None: # Make sure the device is still there and # update the name if the firmware has auto # updated since discovery await self._async_connect_discovered_or_abort() return self.async_create_entry( title=self._name, data={CONF_HOST: ip_address}, ) self._set_confirm_only() placeholders = {"name": self._name, "host": ip_address} self.context["title_placeholders"] = placeholders return self.async_show_form( step_id="discovery_confirm", description_placeholders=placeholders, ) async def async_step_pick_device( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the step to pick discovered device.""" if user_input is not None: device = self._discovered_devices[user_input[CONF_DEVICE]] await self.async_set_unique_id(device.mac_address, raise_on_progress=False) bulb = wizlight(device.ip_address) try: bulbtype = await bulb.get_bulbtype() except WIZ_CONNECT_EXCEPTIONS: return self.async_abort(reason="cannot_connect") else: return self.async_create_entry( title=name_from_bulb_type_and_mac(bulbtype, device.mac_address), data={CONF_HOST: device.ip_address}, ) current_unique_ids = self._async_current_ids() current_hosts = { entry.data[CONF_HOST] for entry in self._async_current_entries(include_ignore=False) } discovered_devices = await async_discover_devices( self.hass, DISCOVER_SCAN_TIMEOUT ) self._discovered_devices = { device.mac_address: device for device in discovered_devices } devices_name = { mac: f"{DEFAULT_NAME} {_short_mac(mac)} ({device.ip_address})" for mac, device in self._discovered_devices.items() if mac not in current_unique_ids and device.ip_address not in current_hosts } # Check if there is at least one device if not devices_name: return self.async_abort(reason="no_devices_found") return self.async_show_form( step_id="pick_device", data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}), ) async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: if not (host := user_input[CONF_HOST]): return await self.async_step_pick_device() if not is_ip_address(user_input[CONF_HOST]): errors["base"] = "no_ip" else: bulb = wizlight(host) try: bulbtype = await bulb.get_bulbtype() mac = await bulb.getMac() except WizLightTimeOutError: errors["base"] = "bulb_time_out" except ConnectionRefusedError: errors["base"] = "cannot_connect" except WizLightConnectionError: errors["base"] = "no_wiz_light" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: await self.async_set_unique_id(mac, raise_on_progress=False) self._abort_if_unique_id_configured( updates={CONF_HOST: user_input[CONF_HOST]} ) name = name_from_bulb_type_and_mac(bulbtype, mac) return self.async_create_entry( title=name, data=user_input, ) return self.async_show_form( step_id="user", data_schema=vol.Schema({vol.Optional(CONF_HOST, default=""): str}), errors=errors, )