Files
core/homeassistant/components/smlight/config_flow.py
T

389 lines
14 KiB
Python

"""Config flow for SMLIGHT Zigbee integration."""
from collections.abc import Mapping
from typing import Any, override
from pysmlight import Api2
from pysmlight.const import Devices
from pysmlight.exceptions import SmlightAuthError, SmlightConnectionError
import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_RECONFIGURE,
SOURCE_USER,
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .bluetooth import get_ble_scanner_mode
from .const import CONF_BLE_SCANNER_MODE, DOMAIN, BLEScannerMode
from .coordinator import SmConfigEntry
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
}
)
STEP_AUTH_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
BLE_SCANNER_OPTIONS = [
BLEScannerMode.DISABLED,
BLEScannerMode.AUTO,
BLEScannerMode.ACTIVE,
BLEScannerMode.PASSIVE,
]
BLE_SCANNER_SCHEMA = vol.Schema(
{
vol.Required(CONF_BLE_SCANNER_MODE): SelectSelector(
SelectSelectorConfig(
options=BLE_SCANNER_OPTIONS,
translation_key=CONF_BLE_SCANNER_MODE,
mode=SelectSelectorMode.DROPDOWN,
)
)
}
)
class SmlightConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for SMLIGHT Zigbee."""
_host: str
_device_name: str
client: Api2
@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
self._host = user_input[CONF_HOST]
self.client = Api2(self._host, session=async_get_clientsession(self.hass))
try:
if not await self._async_check_auth_required(user_input):
info = await self.client.get_info()
self._device_name = str(info.hostname)
if info.model not in Devices:
return self.async_abort(reason="unsupported_device")
return await self._async_complete_entry(user_input)
except SmlightConnectionError:
errors["base"] = "cannot_connect"
except SmlightAuthError:
return await self.async_step_auth()
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_auth(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle authentication to SLZB-06 device."""
errors: dict[str, str] = {}
if user_input is not None:
try:
if not await self._async_check_auth_required(user_input):
info = await self.client.get_info()
self._device_name = str(info.hostname)
if info.model not in Devices:
return self.async_abort(reason="unsupported_device")
if self.source == SOURCE_RECONFIGURE:
await self.async_set_unique_id(format_mac(info.MAC))
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
data_updates={
CONF_HOST: self._host,
**user_input,
},
)
return await self._async_complete_entry(user_input)
except SmlightConnectionError:
return self.async_abort(reason="cannot_connect")
except SmlightAuthError:
errors["base"] = "invalid_auth"
return self.async_show_form(
step_id="auth", data_schema=STEP_AUTH_DATA_SCHEMA, errors=errors
)
@override
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle a discovered Lan coordinator."""
mac: str | None = discovery_info.properties.get("mac")
self._device_name = discovery_info.hostname.removesuffix(".local.")
self._host = discovery_info.host
self.context["title_placeholders"] = {CONF_NAME: self._device_name}
self.client = Api2(self._host, session=async_get_clientsession(self.hass))
# fallback for legacy firmware older than v2.3.x
if mac is None:
try:
info = await self.client.get_info()
except SmlightConnectionError:
# User is likely running unsupported ESPHome firmware
return self.async_abort(reason="cannot_connect")
mac = info.MAC
await self.async_set_unique_id(format_mac(mac))
self._abort_if_unique_id_configured(updates={CONF_HOST: self._host})
return await self.async_step_confirm_discovery()
async def async_step_confirm_discovery(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle discovery confirm."""
errors: dict[str, str] = {}
if user_input is not None:
try:
if not await self._async_check_auth_required(user_input):
info = await self.client.get_info()
if info.model not in Devices:
return self.async_abort(reason="unsupported_device")
return await self._async_complete_entry(user_input)
except SmlightConnectionError:
return self.async_abort(reason="cannot_connect")
except SmlightAuthError:
return await self.async_step_auth()
self._set_confirm_only()
return self.async_show_form(
step_id="confirm_discovery",
description_placeholders={"host": self._device_name},
errors=errors,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauth when API Authentication failed."""
self._host = entry_data[CONF_HOST]
self.client = Api2(self._host, session=async_get_clientsession(self.hass))
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle re-authentication of an existing config entry."""
errors = {}
if user_input is not None:
try:
await self.client.authenticate(
user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
)
except SmlightAuthError:
errors["base"] = "invalid_auth"
except SmlightConnectionError:
return self.async_abort(reason="cannot_connect")
else:
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data_updates=user_input
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=STEP_AUTH_DATA_SCHEMA,
description_placeholders=self.context["title_placeholders"],
errors=errors,
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of SMLIGHT device."""
errors: dict[str, str] = {}
entry = self._get_reconfigure_entry()
if user_input is not None:
self._host = user_input[CONF_HOST]
self.client = Api2(self._host, session=async_get_clientsession(self.hass))
check_input = {**entry.data, **user_input}
try:
await self._async_check_auth_required(check_input)
info = await self.client.get_info()
except SmlightConnectionError:
errors["base"] = "cannot_connect"
except SmlightAuthError:
return await self.async_step_auth()
else:
if info.model not in Devices:
return self.async_abort(reason="unsupported_device")
await self.async_set_unique_id(format_mac(info.MAC))
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
entry,
data_updates=user_input,
)
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA, user_input or entry.data
),
errors=errors,
)
@override
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle DHCP discovery."""
await self.async_set_unique_id(format_mac(discovery_info.macaddress))
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})
# This should never happen since we only listen to DHCP requests
# for configured devices.
return self.async_abort(reason="already_configured")
async def _async_check_auth_required(self, user_input: dict[str, Any]) -> bool:
"""Check if auth required and attempt to authenticate."""
if await self.client.check_auth_needed():
if user_input.get(CONF_USERNAME) and user_input.get(CONF_PASSWORD):
return not await self.client.authenticate(
user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
)
raise SmlightAuthError
return False
async def _async_complete_entry(
self, user_input: dict[str, Any]
) -> ConfigFlowResult:
info = await self.client.get_info()
await self.async_set_unique_id(
format_mac(info.MAC), raise_on_progress=self.source != SOURCE_USER
)
self._abort_if_unique_id_configured(updates={CONF_HOST: self._host})
user_input[CONF_HOST] = self._host
assert info.model is not None
title = self._device_name or info.model
return self.async_create_entry(title=title, data=user_input)
@staticmethod
@callback
@override
def async_get_options_flow(
config_entry: SmConfigEntry,
) -> OptionsFlowHandler:
"""Get the options flow for this handler."""
return OptionsFlowHandler()
class OptionsFlowHandler(OptionsFlowWithReload):
"""Handle options flow for SMLIGHT."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle options flow."""
errors: dict[str, str] = {}
if not hasattr(self.config_entry, "runtime_data"):
errors["base"] = "cannot_connect"
return self.async_show_form(
step_id="init",
data_schema=self.add_suggested_values_to_schema(
BLE_SCANNER_SCHEMA,
user_input or {CONF_BLE_SCANNER_MODE: BLEScannerMode.DISABLED},
),
errors=errors,
)
coordinator = self.config_entry.runtime_data.data
info = coordinator.data.info
if info.ble is None:
return await self.async_step_no_settings()
if user_input is not None:
scanner_mode = BLEScannerMode(user_input[CONF_BLE_SCANNER_MODE])
user_input[CONF_BLE_SCANNER_MODE] = scanner_mode
current_mode = get_ble_scanner_mode(self.config_entry, info)
if (scanner_mode == BLEScannerMode.DISABLED) != (
current_mode == BLEScannerMode.DISABLED
):
try:
await coordinator.client.set_ble_proxy(
scanner_mode != BLEScannerMode.DISABLED
)
except SmlightConnectionError:
errors["base"] = "cannot_connect"
except SmlightAuthError:
errors["base"] = "invalid_auth"
self.config_entry.async_start_reauth(self.hass)
if not errors:
return self.async_create_entry(title="", data=user_input)
suggested_values = {
CONF_BLE_SCANNER_MODE: get_ble_scanner_mode(self.config_entry, info)
}
return self.async_show_form(
step_id="init",
data_schema=self.add_suggested_values_to_schema(
BLE_SCANNER_SCHEMA, user_input or suggested_values
),
errors=errors,
)
async def async_step_no_settings(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle options for devices without settings."""
if user_input is not None:
return self.async_create_entry(title="", data={})
coordinator = self.config_entry.runtime_data.data
return self.async_show_form(
step_id="no_settings",
data_schema=vol.Schema({}),
description_placeholders={"model": coordinator.data.info.model},
)