mirror of https://github.com/home-assistant/core
198 lines
6.4 KiB
Python
198 lines
6.4 KiB
Python
"""Config flow for the Bang & Olufsen integration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from ipaddress import AddressValueError, IPv4Address
|
|
from typing import Any, TypedDict
|
|
|
|
from aiohttp.client_exceptions import ClientConnectorError
|
|
from mozart_api.exceptions import ApiException
|
|
from mozart_api.mozart_client import MozartClient
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
|
from homeassistant.const import CONF_HOST, CONF_MODEL
|
|
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
|
from homeassistant.util.ssl import get_default_context
|
|
|
|
from .const import (
|
|
ATTR_FRIENDLY_NAME,
|
|
ATTR_ITEM_NUMBER,
|
|
ATTR_SERIAL_NUMBER,
|
|
ATTR_TYPE_NUMBER,
|
|
COMPATIBLE_MODELS,
|
|
CONF_SERIAL_NUMBER,
|
|
DEFAULT_MODEL,
|
|
DOMAIN,
|
|
)
|
|
from .util import get_serial_number_from_jid
|
|
|
|
|
|
class EntryData(TypedDict, total=False):
|
|
"""TypedDict for config_entry data."""
|
|
|
|
host: str
|
|
jid: str
|
|
model: str
|
|
name: str
|
|
|
|
|
|
# Map exception types to strings
|
|
_exception_map = {
|
|
ApiException: "api_exception",
|
|
ClientConnectorError: "client_connector_error",
|
|
TimeoutError: "timeout_error",
|
|
AddressValueError: "invalid_ip",
|
|
}
|
|
|
|
|
|
class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|
"""Handle a config flow."""
|
|
|
|
_beolink_jid = ""
|
|
_client: MozartClient
|
|
_host = ""
|
|
_model = ""
|
|
_name = ""
|
|
_serial_number = ""
|
|
|
|
def __init__(self) -> None:
|
|
"""Init the config flow."""
|
|
|
|
VERSION = 1
|
|
|
|
async def async_step_user(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Handle the initial step."""
|
|
data_schema = vol.Schema(
|
|
{
|
|
vol.Required(CONF_HOST): str,
|
|
vol.Required(CONF_MODEL, default=DEFAULT_MODEL): SelectSelector(
|
|
SelectSelectorConfig(options=COMPATIBLE_MODELS)
|
|
),
|
|
}
|
|
)
|
|
|
|
if user_input is not None:
|
|
self._host = user_input[CONF_HOST]
|
|
self._model = user_input[CONF_MODEL]
|
|
|
|
# Check if the IP address is a valid IPv4 address.
|
|
try:
|
|
IPv4Address(self._host)
|
|
except AddressValueError as error:
|
|
return self.async_show_form(
|
|
step_id="user",
|
|
data_schema=data_schema,
|
|
errors={"base": _exception_map[type(error)]},
|
|
)
|
|
|
|
self._client = MozartClient(
|
|
host=self._host, ssl_context=get_default_context()
|
|
)
|
|
|
|
# Try to get information from Beolink self method.
|
|
async with self._client:
|
|
try:
|
|
beolink_self = await self._client.get_beolink_self(
|
|
_request_timeout=3
|
|
)
|
|
except (
|
|
ApiException,
|
|
ClientConnectorError,
|
|
TimeoutError,
|
|
) as error:
|
|
return self.async_show_form(
|
|
step_id="user",
|
|
data_schema=data_schema,
|
|
errors={"base": _exception_map[type(error)]},
|
|
)
|
|
|
|
self._beolink_jid = beolink_self.jid
|
|
self._serial_number = get_serial_number_from_jid(beolink_self.jid)
|
|
|
|
await self.async_set_unique_id(self._serial_number)
|
|
self._abort_if_unique_id_configured()
|
|
|
|
return await self._create_entry()
|
|
|
|
return self.async_show_form(
|
|
step_id="user",
|
|
data_schema=data_schema,
|
|
)
|
|
|
|
async def async_step_zeroconf(
|
|
self, discovery_info: ZeroconfServiceInfo
|
|
) -> ConfigFlowResult:
|
|
"""Handle discovery using Zeroconf."""
|
|
|
|
# Check if the discovered device is a Mozart device
|
|
if ATTR_FRIENDLY_NAME not in discovery_info.properties:
|
|
return self.async_abort(reason="not_mozart_device")
|
|
|
|
# Ensure that an IPv4 address is received
|
|
self._host = discovery_info.host
|
|
try:
|
|
IPv4Address(self._host)
|
|
except AddressValueError:
|
|
return self.async_abort(reason="ipv6_address")
|
|
|
|
# Check connection to ensure valid address is received
|
|
self._client = MozartClient(self._host, ssl_context=get_default_context())
|
|
|
|
async with self._client:
|
|
try:
|
|
await self._client.get_beolink_self(_request_timeout=3)
|
|
except (ClientConnectorError, TimeoutError):
|
|
return self.async_abort(reason="invalid_address")
|
|
|
|
self._model = discovery_info.hostname[:-16].replace("-", " ")
|
|
self._serial_number = discovery_info.properties[ATTR_SERIAL_NUMBER]
|
|
self._beolink_jid = f"{discovery_info.properties[ATTR_TYPE_NUMBER]}.{discovery_info.properties[ATTR_ITEM_NUMBER]}.{self._serial_number}@products.bang-olufsen.com"
|
|
|
|
await self.async_set_unique_id(self._serial_number)
|
|
self._abort_if_unique_id_configured(updates={CONF_HOST: self._host})
|
|
|
|
# Set the discovered device title
|
|
self.context["title_placeholders"] = {
|
|
"name": discovery_info.properties[ATTR_FRIENDLY_NAME]
|
|
}
|
|
|
|
return await self.async_step_zeroconf_confirm()
|
|
|
|
async def _create_entry(self) -> ConfigFlowResult:
|
|
"""Create the config entry for a discovered or manually configured Bang & Olufsen device."""
|
|
# Ensure that created entities have a unique and easily identifiable id and not a "friendly name"
|
|
self._name = f"{self._model}-{self._serial_number}"
|
|
|
|
return self.async_create_entry(
|
|
title=self._name,
|
|
data=EntryData(
|
|
host=self._host,
|
|
jid=self._beolink_jid,
|
|
model=self._model,
|
|
name=self._name,
|
|
),
|
|
)
|
|
|
|
async def async_step_zeroconf_confirm(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Confirm the configuration of the device."""
|
|
if user_input is not None:
|
|
return await self._create_entry()
|
|
|
|
self._set_confirm_only()
|
|
|
|
return self.async_show_form(
|
|
step_id="zeroconf_confirm",
|
|
description_placeholders={
|
|
CONF_HOST: self._host,
|
|
CONF_MODEL: self._model,
|
|
CONF_SERIAL_NUMBER: self._serial_number,
|
|
},
|
|
last_step=True,
|
|
)
|