* Add bangolufsen integration
* add untested files to .coveragerc
* Simplify integration to media_player platform
* Remove missing files from .coveragerc
* Add beolink_set_relative_volume custom service
Tweaks
* Remove custom services
Remove grouping as it was dependent on custom services
* Update API to 3.2.1.150.0
Reduce and optimize code with feedback from joostlek
Tweaks
* Updated testing
* Remove unused options schema
* Fix bugfix setting wrong state
* Fix wrong initial state
* Bump API
* Fix Beosound Level not reconnecting properly
* Remove unused constant
* Fix wrong variable checked to determine source
* Update integration with feedback from emontnemery
* Update integration with feedback from emontnemery
* Remove unused code
* Move API client into dataclass
Fix not all config_flow exceptions caught
Tweaks
* Add Bang & Olufsen brand
* Revert "Add Bang & Olufsen brand"
This reverts commit 57b2722078
.
* Remove volume options from setup
Simplify device checks
rename integration to bang_olufsen
update tests to pass
Update API
* Remove _device from base
Add _device to websocket
* Move SW version device update to websocket
Sort websocket variables
* Add WebSocket connection test
* Remove unused constants
* Remove confirmation form
Make discovered devices get added to Home Assistant immediately
Fix device not being available on mdns discovery
Change config flow aborts to forms with error
* Update tests for new config_flow
Add missing api_exception test
* Restrict manual and discovered IP addresses to IPv4
* Re-add confirmation step for zeroconf discovery
Improve error messages
Move exception mapping dict to module level
* Enable remote control WebSocket listener
* Update tests
184 lines
5.9 KiB
Python
184 lines
5.9 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
|
|
from homeassistant.const import CONF_HOST, CONF_MODEL
|
|
from homeassistant.data_entry_flow import FlowResult
|
|
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
|
|
|
from .const import (
|
|
ATTR_FRIENDLY_NAME,
|
|
ATTR_ITEM_NUMBER,
|
|
ATTR_SERIAL_NUMBER,
|
|
ATTR_TYPE_NUMBER,
|
|
COMPATIBLE_MODELS,
|
|
CONF_SERIAL_NUMBER,
|
|
DEFAULT_MODEL,
|
|
DOMAIN,
|
|
)
|
|
|
|
|
|
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
|
|
) -> FlowResult:
|
|
"""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(self._host)
|
|
|
|
# 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 = beolink_self.jid.split(".")[2].split("@")[0]
|
|
|
|
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
|
|
) -> FlowResult:
|
|
"""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")
|
|
|
|
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) -> FlowResult:
|
|
"""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
|
|
) -> FlowResult:
|
|
"""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,
|
|
)
|