core/homeassistant/components/lifx/config_flow.py

243 lines
9.1 KiB
Python

"""Config flow flow LIFX."""
from __future__ import annotations
import asyncio
import socket
from typing import Any
from aiolifx.aiolifx import Light
from aiolifx.connection import LIFXConnection
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.components.dhcp import DhcpServiceInfo
from homeassistant.const import CONF_DEVICE, CONF_HOST
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.typing import DiscoveryInfoType
from .const import _LOGGER, CONF_SERIAL, DOMAIN, TARGET_ANY
from .discovery import async_discover_devices
from .util import (
async_entry_is_legacy,
async_execute_lifx,
async_get_legacy_entry,
formatted_serial,
lifx_features,
mac_matches_serial_number,
)
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for tplink."""
VERSION = 1
def __init__(self) -> None:
"""Initialize the config flow."""
self._discovered_devices: dict[str, Light] = {}
self._discovered_device: Light | None = None
async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult:
"""Handle discovery via dhcp."""
mac = discovery_info.macaddress
host = discovery_info.ip
hass = self.hass
for entry in self._async_current_entries():
if (
entry.unique_id
and not async_entry_is_legacy(entry)
and mac_matches_serial_number(mac, entry.unique_id)
):
if entry.data[CONF_HOST] != host:
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_HOST: host}
)
hass.async_create_task(
hass.config_entries.async_reload(entry.entry_id)
)
return self.async_abort(reason="already_configured")
return await self._async_handle_discovery(host)
async def async_step_homekit(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> FlowResult:
"""Handle HomeKit discovery."""
return await self._async_handle_discovery(host=discovery_info.host)
async def async_step_integration_discovery(
self, discovery_info: DiscoveryInfoType
) -> FlowResult:
"""Handle discovery."""
_LOGGER.debug("async_step_integration_discovery %s", discovery_info)
serial = discovery_info[CONF_SERIAL]
host = discovery_info[CONF_HOST]
await self.async_set_unique_id(formatted_serial(serial))
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
return await self._async_handle_discovery(host, serial)
async def _async_handle_discovery(
self, host: str, serial: str | None = None
) -> FlowResult:
"""Handle any discovery."""
_LOGGER.debug("Discovery %s %s", host, serial)
self._async_abort_entries_match({CONF_HOST: host})
self.context[CONF_HOST] = host
if any(
progress.get("context", {}).get(CONF_HOST) == host
for progress in self._async_in_progress()
):
return self.async_abort(reason="already_in_progress")
if not (
device := await self._async_try_connect(
host, serial=serial, raise_on_progress=True
)
):
return self.async_abort(reason="cannot_connect")
self._discovered_device = device
return await self.async_step_discovery_confirm()
@callback
def _async_discovered_pending_migration(self) -> bool:
"""Check if a discovered device is pending migration."""
assert self.unique_id is not None
if not (legacy_entry := async_get_legacy_entry(self.hass)):
return False
device_registry = dr.async_get(self.hass)
existing_device = device_registry.async_get_device(
identifiers={(DOMAIN, self.unique_id)}
)
return bool(
existing_device is not None
and legacy_entry.entry_id in existing_device.config_entries
)
async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm discovery."""
assert self._discovered_device is not None
discovered = self._discovered_device
_LOGGER.debug(
"Confirming discovery: %s with serial %s",
discovered.label,
self.unique_id,
)
if user_input is not None or self._async_discovered_pending_migration():
return self._async_create_entry_from_device(discovered)
self._abort_if_unique_id_configured(updates={CONF_HOST: discovered.ip_addr})
self._set_confirm_only()
placeholders = {
"label": discovered.label,
"host": discovered.ip_addr,
"serial": self.unique_id,
}
self.context["title_placeholders"] = placeholders
return self.async_show_form(
step_id="discovery_confirm", description_placeholders=placeholders
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors = {}
if user_input is not None:
host = user_input[CONF_HOST]
if not host:
return await self.async_step_pick_device()
if (
device := await self._async_try_connect(host, raise_on_progress=False)
) is None:
errors["base"] = "cannot_connect"
else:
return self._async_create_entry_from_device(device)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Optional(CONF_HOST, default=""): str}),
errors=errors,
)
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:
serial = user_input[CONF_DEVICE]
await self.async_set_unique_id(serial, raise_on_progress=False)
device_without_label = self._discovered_devices[serial]
device = await self._async_try_connect(
device_without_label.ip_addr, raise_on_progress=False
)
if not device:
return self.async_abort(reason="cannot_connect")
return self._async_create_entry_from_device(device)
configured_serials: set[str] = set()
configured_hosts: set[str] = set()
for entry in self._async_current_entries():
if entry.unique_id and not async_entry_is_legacy(entry):
configured_serials.add(entry.unique_id)
configured_hosts.add(entry.data[CONF_HOST])
self._discovered_devices = {
# device.mac_addr is not the mac_address, its the serial number
device.mac_addr: device
for device in await async_discover_devices(self.hass)
}
devices_name = {
serial: f"{serial} ({device.ip_addr})"
for serial, device in self._discovered_devices.items()
if serial not in configured_serials
and device.ip_addr not in configured_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)}),
)
@callback
def _async_create_entry_from_device(self, device: Light) -> FlowResult:
"""Create a config entry from a smart device."""
self._abort_if_unique_id_configured(updates={CONF_HOST: device.ip_addr})
return self.async_create_entry(
title=device.label,
data={CONF_HOST: device.ip_addr},
)
async def _async_try_connect(
self, host: str, serial: str | None = None, raise_on_progress: bool = True
) -> Light | None:
"""Try to connect."""
self._async_abort_entries_match({CONF_HOST: host})
connection = LIFXConnection(host, TARGET_ANY)
try:
await connection.async_setup()
except socket.gaierror:
return None
device: Light = connection.device
device.get_hostfirmware()
try:
message = await async_execute_lifx(device.get_color)
except asyncio.TimeoutError:
return None
finally:
connection.async_stop()
if (
lifx_features(device)["relays"] is True
or device.host_firmware_version is None
):
return None # relays not supported
# device.mac_addr is not the mac_address, its the serial number
device.mac_addr = serial or message.target_addr
await self.async_set_unique_id(
formatted_serial(device.mac_addr), raise_on_progress=raise_on_progress
)
return device