mirror of
https://github.com/home-assistant/core.git
synced 2026-09-14 20:48:32 +01:00
236 lines
7.9 KiB
Python
236 lines
7.9 KiB
Python
"""Config flow for the Bitvis Power Hub integration."""
|
|
|
|
import asyncio
|
|
import logging
|
|
from typing import Any, Self, override
|
|
|
|
from bitvis_protobuf.listener import FilterIp
|
|
from bitvis_protobuf.parse import PayloadDiagnostic, PayloadSample
|
|
from bitvis_protobuf.utils import (
|
|
InvalidMacAddressError,
|
|
async_resolve_host,
|
|
async_verify_udp_port_bindable,
|
|
normalize_host,
|
|
)
|
|
import probatio
|
|
|
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.data_entry_flow import AbortFlow
|
|
from homeassistant.helpers import config_validation as cv
|
|
from homeassistant.helpers.device_registry import format_mac
|
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
|
|
|
from .const import DEFAULT_NAME, DEFAULT_PORT, DISCOVERY_TIMEOUT, DOMAIN
|
|
from .coordinator import async_get_listener_registry
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
STEP_USER_DATA_SCHEMA = probatio.Schema(
|
|
{
|
|
probatio.Required(CONF_HOST): cv.string,
|
|
}
|
|
)
|
|
|
|
|
|
def _get_friendly_name(name: str | None) -> str:
|
|
"""Return a user-friendly name derived from the zeroconf name."""
|
|
if not name:
|
|
return DEFAULT_NAME
|
|
instance = name.split(".", 1)[0]
|
|
return instance or DEFAULT_NAME
|
|
|
|
|
|
async def _async_test_port(hass: HomeAssistant, port: int) -> None:
|
|
"""Verify the UDP port can be bound."""
|
|
|
|
if async_get_listener_registry(hass).has_listener(port):
|
|
return
|
|
|
|
await async_verify_udp_port_bindable(port)
|
|
|
|
|
|
async def _async_discover_mac_address(hass: HomeAssistant, host: str, port: int) -> str:
|
|
"""Wait for a UDP message from the device and return its MAC address."""
|
|
resolved_ips = await async_resolve_host(host)
|
|
listener_registry = async_get_listener_registry(hass)
|
|
listener = await listener_registry.async_get_or_create(port)
|
|
|
|
loop = asyncio.get_running_loop()
|
|
future: asyncio.Future[str] = loop.create_future()
|
|
|
|
@callback
|
|
def _on_payload(
|
|
payload: PayloadSample | PayloadDiagnostic, _addr: tuple[str, int]
|
|
) -> None:
|
|
if not future.done():
|
|
future.set_result(payload.mac_address)
|
|
|
|
@callback
|
|
def _on_error(err: Exception, addr: tuple[str, int]) -> None:
|
|
if (
|
|
addr[0] in resolved_ips
|
|
and not future.done()
|
|
and isinstance(err, InvalidMacAddressError)
|
|
):
|
|
future.set_exception(err)
|
|
|
|
filters: list[FilterIp] = []
|
|
listener.register_error_callback(_on_error)
|
|
try:
|
|
for ip in resolved_ips:
|
|
filt = FilterIp(ip)
|
|
try:
|
|
listener.register(filt, _on_payload)
|
|
except RuntimeError as err:
|
|
raise AbortFlow("already_in_progress") from err
|
|
filters.append(filt)
|
|
|
|
return await asyncio.wait_for(future, timeout=DISCOVERY_TIMEOUT)
|
|
finally:
|
|
listener.unregister_error_callback(_on_error)
|
|
for filt in filters:
|
|
listener.unregister(filt)
|
|
await listener_registry.async_remove_if_unused(port)
|
|
|
|
|
|
class BitvisConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
"""Handle a config flow for Bitvis Power Hub."""
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialize the config flow."""
|
|
self._discovery_info: ZeroconfServiceInfo | None = None
|
|
self._validated_host: str | None = None
|
|
self._host: str | None = None
|
|
|
|
@override
|
|
def is_matching(self, other_flow: Self) -> bool:
|
|
"""Return True if other_flow is matching this flow."""
|
|
return self._host is not None and self._host == other_flow._host
|
|
|
|
async def _async_validate_host(self, host: str) -> str:
|
|
"""Verify port availability and discover the device MAC address."""
|
|
await _async_test_port(self.hass, DEFAULT_PORT)
|
|
return await _async_discover_mac_address(self.hass, host, DEFAULT_PORT)
|
|
|
|
async def _async_create_entry_from_host(
|
|
self, host: str, title: str
|
|
) -> ConfigFlowResult:
|
|
"""Validate connectivity, discover MAC address, and create the entry."""
|
|
self._host = host
|
|
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
|
return self.async_abort(reason="already_in_progress")
|
|
|
|
try:
|
|
mac_address = await self._async_validate_host(host)
|
|
except TimeoutError:
|
|
return self.async_show_form(
|
|
step_id="user",
|
|
data_schema=self.add_suggested_values_to_schema(
|
|
STEP_USER_DATA_SCHEMA, {CONF_HOST: host}
|
|
),
|
|
errors={"base": "timeout_connect"},
|
|
)
|
|
except InvalidMacAddressError:
|
|
return self.async_show_form(
|
|
step_id="user",
|
|
data_schema=self.add_suggested_values_to_schema(
|
|
STEP_USER_DATA_SCHEMA, {CONF_HOST: host}
|
|
),
|
|
errors={"base": "invalid_mac"},
|
|
)
|
|
except OSError:
|
|
return self.async_show_form(
|
|
step_id="user",
|
|
data_schema=self.add_suggested_values_to_schema(
|
|
STEP_USER_DATA_SCHEMA, {CONF_HOST: host}
|
|
),
|
|
errors={"base": "cannot_connect"},
|
|
)
|
|
|
|
await self.async_set_unique_id(format_mac(mac_address))
|
|
self._abort_if_unique_id_configured()
|
|
|
|
return self.async_create_entry(
|
|
title=title,
|
|
data={
|
|
CONF_HOST: host,
|
|
CONF_PORT: DEFAULT_PORT,
|
|
},
|
|
)
|
|
|
|
@override
|
|
async def async_step_user(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Handle the initial step."""
|
|
if user_input is not None:
|
|
host = normalize_host(user_input[CONF_HOST])
|
|
return await self._async_create_entry_from_host(host, DEFAULT_NAME)
|
|
|
|
return self.async_show_form(
|
|
step_id="user",
|
|
data_schema=STEP_USER_DATA_SCHEMA,
|
|
)
|
|
|
|
@override
|
|
async def async_step_zeroconf(
|
|
self, discovery_info: ZeroconfServiceInfo
|
|
) -> ConfigFlowResult:
|
|
"""Handle zeroconf discovery."""
|
|
_LOGGER.debug("Discovered Bitvis Power Hub via Zeroconf: %s", discovery_info)
|
|
|
|
host = discovery_info.host
|
|
self._host = host
|
|
|
|
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
|
return self.async_abort(reason="already_in_progress")
|
|
|
|
try:
|
|
mac_address = await self._async_validate_host(host)
|
|
except TimeoutError:
|
|
return self.async_abort(reason="timeout_connect")
|
|
except InvalidMacAddressError:
|
|
return self.async_abort(reason="invalid_mac")
|
|
except OSError:
|
|
return self.async_abort(reason="cannot_connect")
|
|
|
|
await self.async_set_unique_id(format_mac(mac_address))
|
|
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
|
|
|
self._discovery_info = discovery_info
|
|
self._validated_host = host
|
|
|
|
self.context["title_placeholders"] = {
|
|
"name": _get_friendly_name(discovery_info.name),
|
|
"host": host,
|
|
}
|
|
|
|
return await self.async_step_zeroconf_confirm()
|
|
|
|
async def async_step_zeroconf_confirm(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Confirm discovery."""
|
|
assert self._discovery_info is not None
|
|
|
|
if user_input is not None:
|
|
assert self._validated_host is not None
|
|
|
|
return self.async_create_entry(
|
|
title=_get_friendly_name(self._discovery_info.name),
|
|
data={
|
|
CONF_HOST: self._validated_host,
|
|
CONF_PORT: DEFAULT_PORT,
|
|
},
|
|
)
|
|
|
|
return self.async_show_form(
|
|
step_id="zeroconf_confirm",
|
|
description_placeholders={
|
|
"name": _get_friendly_name(self._discovery_info.name),
|
|
"host": self._discovery_info.host,
|
|
},
|
|
)
|