Files
core/homeassistant/components/onkyo/config_flow.py
2025-09-28 15:26:40 +02:00

498 lines
18 KiB
Python

"""Config flow for Onkyo."""
from collections.abc import Mapping
import logging
from typing import Any
from aioonkyo import ReceiverInfo
import voluptuous as vol
from yarl import URL
from homeassistant.config_entries import (
SOURCE_RECONFIGURE,
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_HOST
from homeassistant.core import callback
from homeassistant.data_entry_flow import section
from homeassistant.helpers.selector import (
NumberSelector,
NumberSelectorConfig,
NumberSelectorMode,
Selector,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
TextSelector,
)
from homeassistant.helpers.service_info.ssdp import SsdpServiceInfo
from . import OnkyoConfigEntry
from .const import (
DOMAIN,
OPTION_INPUT_SOURCES,
OPTION_LISTENING_MODES,
OPTION_MAX_VOLUME,
OPTION_MAX_VOLUME_DEFAULT,
OPTION_VOLUME_RESOLUTION,
OPTION_VOLUME_RESOLUTION_DEFAULT,
VOLUME_RESOLUTION_ALLOWED,
InputSource,
ListeningMode,
)
from .receiver import async_discover, async_interview
from .util import get_meaning
_LOGGER = logging.getLogger(__name__)
CONF_DEVICE = "device"
INPUT_SOURCES_DEFAULT: list[InputSource] = []
LISTENING_MODES_DEFAULT: list[ListeningMode] = []
INPUT_SOURCES_ALL_MEANINGS = {
get_meaning(input_source): input_source for input_source in InputSource
}
LISTENING_MODES_ALL_MEANINGS = {
get_meaning(listening_mode): listening_mode for listening_mode in ListeningMode
}
STEP_MANUAL_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
STEP_RECONFIGURE_SCHEMA = vol.Schema(
{
vol.Required(OPTION_VOLUME_RESOLUTION): vol.In(VOLUME_RESOLUTION_ALLOWED),
}
)
STEP_CONFIGURE_SCHEMA = STEP_RECONFIGURE_SCHEMA.extend(
{
vol.Required(OPTION_INPUT_SOURCES): SelectSelector(
SelectSelectorConfig(
options=list(INPUT_SOURCES_ALL_MEANINGS),
multiple=True,
mode=SelectSelectorMode.DROPDOWN,
)
),
vol.Required(OPTION_LISTENING_MODES): SelectSelector(
SelectSelectorConfig(
options=list(LISTENING_MODES_ALL_MEANINGS),
multiple=True,
mode=SelectSelectorMode.DROPDOWN,
)
),
}
)
class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
"""Onkyo config flow."""
_receiver_info: ReceiverInfo
_discovered_infos: dict[str, ReceiverInfo]
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
_LOGGER.debug("Config flow start user")
return self.async_show_menu(
step_id="user", menu_options=["manual", "eiscp_discovery"]
)
async def async_step_manual(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle manual device entry."""
errors = {}
if user_input is not None:
host = user_input[CONF_HOST]
_LOGGER.debug("Config flow manual: %s", host)
try:
info = await async_interview(host)
except TimeoutError:
_LOGGER.warning("Timed out interviewing: %s", host)
errors["base"] = "cannot_connect"
except OSError:
_LOGGER.exception("Unexpected exception interviewing: %s", host)
errors["base"] = "unknown"
else:
self._receiver_info = info
await self.async_set_unique_id(info.identifier, raise_on_progress=False)
if self.source == SOURCE_RECONFIGURE:
self._abort_if_unique_id_mismatch()
else:
self._abort_if_unique_id_configured()
return await self.async_step_configure_receiver()
suggested_values = user_input
if suggested_values is None and self.source == SOURCE_RECONFIGURE:
suggested_values = {
CONF_HOST: self._get_reconfigure_entry().data[CONF_HOST]
}
return self.async_show_form(
step_id="manual",
data_schema=self.add_suggested_values_to_schema(
STEP_MANUAL_SCHEMA, suggested_values
),
errors=errors,
)
async def async_step_eiscp_discovery(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Start eiscp discovery and handle user device selection."""
if user_input is not None:
self._receiver_info = self._discovered_infos[user_input[CONF_DEVICE]]
await self.async_set_unique_id(
self._receiver_info.identifier, raise_on_progress=False
)
self._abort_if_unique_id_configured(
updates={CONF_HOST: self._receiver_info.host}
)
return await self.async_step_configure_receiver()
_LOGGER.debug("Config flow start eiscp discovery")
try:
infos = list(await async_discover(self.hass))
except OSError:
_LOGGER.exception("Unexpected exception")
return self.async_abort(reason="unknown")
_LOGGER.debug("Discovered devices: %s", infos)
self._discovered_infos = {}
discovered_names = {}
current_unique_ids = self._async_current_ids(include_ignore=False)
for info in infos:
if info.identifier in current_unique_ids:
continue
self._discovered_infos[info.identifier] = info
device_name = f"{info.model_name} ({info.host})"
discovered_names[info.identifier] = device_name
_LOGGER.debug("Discovered new devices: %s", self._discovered_infos)
if not discovered_names:
return self.async_abort(reason="no_devices_found")
return self.async_show_form(
step_id="eiscp_discovery",
data_schema=vol.Schema(
{vol.Required(CONF_DEVICE): vol.In(discovered_names)}
),
)
async def async_step_ssdp(
self, discovery_info: SsdpServiceInfo
) -> ConfigFlowResult:
"""Handle flow initialized by SSDP discovery."""
_LOGGER.debug("Config flow start ssdp: %s", discovery_info)
if udn := discovery_info.ssdp_udn:
udn_parts = udn.split(":")
if len(udn_parts) == 2:
uuid = udn_parts[1]
last_uuid_section = uuid.split("-")[-1].upper()
await self.async_set_unique_id(last_uuid_section)
self._abort_if_unique_id_configured()
if discovery_info.ssdp_location is None:
_LOGGER.error("SSDP location is None")
return self.async_abort(reason="unknown")
host = URL(discovery_info.ssdp_location).host
if host is None:
_LOGGER.error("SSDP host is None")
return self.async_abort(reason="unknown")
try:
info = await async_interview(host)
except TimeoutError:
_LOGGER.warning("Timed out interviewing: %s", host)
return self.async_abort(reason="cannot_connect")
except OSError:
_LOGGER.exception("Unexpected exception interviewing: %s", host)
return self.async_abort(reason="unknown")
await self.async_set_unique_id(info.identifier)
self._abort_if_unique_id_configured(updates={CONF_HOST: info.host})
self._receiver_info = info
title_string = f"{info.model_name} ({info.host})"
self.context["title_placeholders"] = {"name": title_string}
return await self.async_step_configure_receiver()
async def async_step_configure_receiver(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the configuration of a single receiver."""
errors = {}
reconfigure_entry = None
schema = STEP_CONFIGURE_SCHEMA
if self.source == SOURCE_RECONFIGURE:
schema = STEP_RECONFIGURE_SCHEMA
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
volume_resolution = user_input[OPTION_VOLUME_RESOLUTION]
if reconfigure_entry is not None:
entry_options = reconfigure_entry.options
result = self.async_update_reload_and_abort(
reconfigure_entry,
data={
CONF_HOST: self._receiver_info.host,
},
options={
**entry_options,
OPTION_VOLUME_RESOLUTION: volume_resolution,
},
)
_LOGGER.debug("Reconfigured receiver, result: %s", result)
return result
input_source_meanings: list[str] = user_input[OPTION_INPUT_SOURCES]
if not input_source_meanings:
errors[OPTION_INPUT_SOURCES] = "empty_input_source_list"
listening_modes: list[str] = user_input[OPTION_LISTENING_MODES]
if not listening_modes:
errors[OPTION_LISTENING_MODES] = "empty_listening_mode_list"
if not errors:
input_sources_store: dict[str, str] = {}
for input_source_meaning in input_source_meanings:
input_source = INPUT_SOURCES_ALL_MEANINGS[input_source_meaning]
input_sources_store[input_source.value] = input_source_meaning
listening_modes_store: dict[str, str] = {}
for listening_mode_meaning in listening_modes:
listening_mode = LISTENING_MODES_ALL_MEANINGS[
listening_mode_meaning
]
listening_modes_store[listening_mode.value] = listening_mode_meaning
result = self.async_create_entry(
title=self._receiver_info.model_name,
data={
CONF_HOST: self._receiver_info.host,
},
options={
OPTION_VOLUME_RESOLUTION: volume_resolution,
OPTION_MAX_VOLUME: OPTION_MAX_VOLUME_DEFAULT,
OPTION_INPUT_SOURCES: input_sources_store,
OPTION_LISTENING_MODES: listening_modes_store,
},
)
_LOGGER.debug("Configured receiver, result: %s", result)
return result
_LOGGER.debug("Configuring receiver, info: %s", self._receiver_info)
suggested_values = user_input
if suggested_values is None:
if reconfigure_entry is None:
suggested_values = {
OPTION_VOLUME_RESOLUTION: OPTION_VOLUME_RESOLUTION_DEFAULT,
OPTION_INPUT_SOURCES: [
get_meaning(input_source)
for input_source in INPUT_SOURCES_DEFAULT
],
OPTION_LISTENING_MODES: [
get_meaning(listening_mode)
for listening_mode in LISTENING_MODES_DEFAULT
],
}
else:
entry_options = reconfigure_entry.options
suggested_values = {
OPTION_VOLUME_RESOLUTION: entry_options[OPTION_VOLUME_RESOLUTION],
}
return self.async_show_form(
step_id="configure_receiver",
data_schema=self.add_suggested_values_to_schema(schema, suggested_values),
errors=errors,
description_placeholders={
"name": f"{self._receiver_info.model_name} ({self._receiver_info.host})"
},
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of the receiver."""
_LOGGER.debug("Config flow start reconfigure")
return await self.async_step_manual()
@staticmethod
@callback
def async_get_options_flow(config_entry: OnkyoConfigEntry) -> OptionsFlowWithReload:
"""Return the options flow."""
return OnkyoOptionsFlowHandler()
OPTIONS_STEP_INIT_SCHEMA = vol.Schema(
{
vol.Required(OPTION_MAX_VOLUME): NumberSelector(
NumberSelectorConfig(min=1, max=100, mode=NumberSelectorMode.BOX)
),
vol.Required(OPTION_INPUT_SOURCES): SelectSelector(
SelectSelectorConfig(
options=list(INPUT_SOURCES_ALL_MEANINGS),
multiple=True,
mode=SelectSelectorMode.DROPDOWN,
)
),
vol.Required(OPTION_LISTENING_MODES): SelectSelector(
SelectSelectorConfig(
options=list(LISTENING_MODES_ALL_MEANINGS),
multiple=True,
mode=SelectSelectorMode.DROPDOWN,
)
),
}
)
class OnkyoOptionsFlowHandler(OptionsFlowWithReload):
"""Handle an options flow for Onkyo."""
_data: dict[str, Any]
_input_sources: dict[InputSource, str]
_listening_modes: dict[ListeningMode, str]
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the options."""
errors = {}
entry_options: Mapping[str, Any] = self.config_entry.options
entry_options = {
OPTION_LISTENING_MODES: {
listening_mode.value: get_meaning(listening_mode)
for listening_mode in LISTENING_MODES_DEFAULT
},
**entry_options,
}
if user_input is not None:
input_source_meanings: list[str] = user_input[OPTION_INPUT_SOURCES]
if not input_source_meanings:
errors[OPTION_INPUT_SOURCES] = "empty_input_source_list"
listening_mode_meanings: list[str] = user_input[OPTION_LISTENING_MODES]
if not listening_mode_meanings:
errors[OPTION_LISTENING_MODES] = "empty_listening_mode_list"
if not errors:
self._input_sources = {}
for input_source_meaning in input_source_meanings:
input_source = INPUT_SOURCES_ALL_MEANINGS[input_source_meaning]
input_source_name = entry_options[OPTION_INPUT_SOURCES].get(
input_source.value, input_source_meaning
)
self._input_sources[input_source] = input_source_name
self._listening_modes = {}
for listening_mode_meaning in listening_mode_meanings:
listening_mode = LISTENING_MODES_ALL_MEANINGS[
listening_mode_meaning
]
listening_mode_name = entry_options[OPTION_LISTENING_MODES].get(
listening_mode.value, listening_mode_meaning
)
self._listening_modes[listening_mode] = listening_mode_name
self._data = {
OPTION_VOLUME_RESOLUTION: entry_options[OPTION_VOLUME_RESOLUTION],
OPTION_MAX_VOLUME: user_input[OPTION_MAX_VOLUME],
}
return await self.async_step_names()
suggested_values = user_input
if suggested_values is None:
suggested_values = {
OPTION_MAX_VOLUME: entry_options[OPTION_MAX_VOLUME],
OPTION_INPUT_SOURCES: [
get_meaning(InputSource(input_source))
for input_source in entry_options[OPTION_INPUT_SOURCES]
],
OPTION_LISTENING_MODES: [
get_meaning(ListeningMode(listening_mode))
for listening_mode in entry_options[OPTION_LISTENING_MODES]
],
}
return self.async_show_form(
step_id="init",
data_schema=self.add_suggested_values_to_schema(
OPTIONS_STEP_INIT_SCHEMA, suggested_values
),
errors=errors,
)
async def async_step_names(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Configure names."""
if user_input is not None:
input_sources_store: dict[str, str] = {}
for input_source_meaning, input_source_name in user_input[
OPTION_INPUT_SOURCES
].items():
input_source = INPUT_SOURCES_ALL_MEANINGS[input_source_meaning]
input_sources_store[input_source.value] = input_source_name
listening_modes_store: dict[str, str] = {}
for listening_mode_meaning, listening_mode_name in user_input[
OPTION_LISTENING_MODES
].items():
listening_mode = LISTENING_MODES_ALL_MEANINGS[listening_mode_meaning]
listening_modes_store[listening_mode.value] = listening_mode_name
return self.async_create_entry(
data={
**self._data,
OPTION_INPUT_SOURCES: input_sources_store,
OPTION_LISTENING_MODES: listening_modes_store,
}
)
input_sources_schema_dict: dict[Any, Selector] = {}
for input_source, input_source_name in self._input_sources.items():
input_sources_schema_dict[
vol.Required(get_meaning(input_source), default=input_source_name)
] = TextSelector()
listening_modes_schema_dict: dict[Any, Selector] = {}
for listening_mode, listening_mode_name in self._listening_modes.items():
listening_modes_schema_dict[
vol.Required(get_meaning(listening_mode), default=listening_mode_name)
] = TextSelector()
return self.async_show_form(
step_id="names",
data_schema=vol.Schema(
{
vol.Required(OPTION_INPUT_SOURCES): section(
vol.Schema(input_sources_schema_dict)
),
vol.Required(OPTION_LISTENING_MODES): section(
vol.Schema(listening_modes_schema_dict)
),
}
),
)