"""Config flow for the SystemNexa2 integration.""" from dataclasses import dataclass import logging import socket from typing import Any, override import aiohttp from sn2.device import Device import voluptuous as vol from homeassistant.config_entries import ( SOURCE_RECONFIGURE, SOURCE_USER, ConfigFlow, ConfigFlowResult, ) from homeassistant.const import ( ATTR_MODEL, ATTR_SW_VERSION, CONF_DEVICE_ID, CONF_HOST, CONF_MODEL, CONF_NAME, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.util.network import is_ip_address from . import DOMAIN _LOGGER = logging.getLogger(__name__) _SCHEMA = vol.Schema( { vol.Required(CONF_HOST): str, } ) @dataclass(kw_only=True) class _DiscoveryInfo: name: str host: str model: str device_id: str device_version: str class SystemNexa2ConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for the devices.""" VERSION = 1 def __init__(self) -> None: """Initialize the config flow.""" self._discovered_device: _DiscoveryInfo @override async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle user-initiated configuration and reconfiguration.""" errors: dict[str, str] = {} if user_input is not None: host = user_input[CONF_HOST] if not await self._async_is_valid_host(host): errors["base"] = "invalid_host" else: try: temp_dev = await Device.initiate_device( host=host, session=async_get_clientsession(self.hass), ) info = await temp_dev.get_info() except TimeoutError, aiohttp.ClientError: errors["base"] = "cannot_connect" except Exception: _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: device_id = info.information.unique_id device_model = info.information.model device_version = info.information.sw_version supported, error = Device.is_device_supported( model=device_model, device_version=device_version, ) if device_id is None or device_version is None or not supported: _LOGGER.error("Unsupported model: %s", error) return self.async_abort( reason="unsupported_model", description_placeholders={ ATTR_MODEL: str(device_model), ATTR_SW_VERSION: str(device_version), }, ) await self.async_set_unique_id(info.information.unique_id) if self.source == SOURCE_USER: self._abort_if_unique_id_configured() if self.source == SOURCE_RECONFIGURE: self._abort_if_unique_id_mismatch(reason="wrong_device") return self.async_update_reload_and_abort( self._get_reconfigure_entry(), data_updates={CONF_HOST: host}, ) self._discovered_device = _DiscoveryInfo( name=info.information.name, host=host, device_id=device_id, model=device_model, device_version=device_version, ) return await self._async_create_device_entry() if self.source == SOURCE_RECONFIGURE: return self.async_show_form( step_id="reconfigure", data_schema=self.add_suggested_values_to_schema( _SCHEMA, user_input or self._get_reconfigure_entry().data, ), errors=errors, ) return self.async_show_form( step_id="user", data_schema=_SCHEMA, errors=errors, ) @override async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo ) -> ConfigFlowResult: """Handle zeroconf discovery.""" device_id = discovery_info.properties.get("id") device_model = discovery_info.properties.get("model") device_version = discovery_info.properties.get("version") supported, error = Device.is_device_supported( model=device_model, device_version=device_version, ) if ( device_id is None or device_model is None or device_version is None or not supported ): _LOGGER.error("Unsupported model: %s", error) return self.async_abort(reason="unsupported_model") self._discovered_device = _DiscoveryInfo( name=discovery_info.name.split(".")[0], host=discovery_info.host, device_id=device_id, model=device_model, device_version=device_version, ) await self._async_set_unique_id() return await self.async_step_discovery_confirm() async def _async_set_unique_id(self) -> None: await self.async_set_unique_id(self._discovered_device.device_id) self._abort_if_unique_id_configured( updates={CONF_HOST: self._discovered_device.host} ) self.context["title_placeholders"] = { "name": self._discovered_device.name, "model": self._discovered_device.model or "Unknown model", } async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Confirm discovery.""" if user_input is not None and self._discovered_device is not None: return await self._async_create_device_entry() self._set_confirm_only() return self.async_show_form( step_id="discovery_confirm", description_placeholders={"name": self._discovered_device.name}, ) async def _async_create_device_entry(self) -> ConfigFlowResult: device_name = self._discovered_device.name device_model = self._discovered_device.model return self.async_create_entry( title=f"{device_name} ({device_model})", data={ CONF_HOST: self._discovered_device.host, CONF_NAME: self._discovered_device.name, CONF_MODEL: self._discovered_device.model, CONF_DEVICE_ID: self._discovered_device.device_id, }, ) async def async_step_reconfigure( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle reconfiguration.""" return await self.async_step_user(user_input) async def _async_is_valid_host(self, ip_or_hostname: str) -> bool: if not ip_or_hostname: return False if is_ip_address(ip_or_hostname): return True try: await self.hass.async_add_executor_job(socket.gethostbyname, ip_or_hostname) except socket.gaierror: return False return True