Add sound mode support to Onkyo (#133531)

This commit is contained in:
Artur Pragacz 2025-02-25 17:21:05 +01:00 committed by GitHub
parent 2bba185e4c
commit 38cc26485a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 447 additions and 134 deletions

View File

@ -1,6 +1,7 @@
"""The onkyo component.""" """The onkyo component."""
from dataclasses import dataclass from dataclasses import dataclass
import logging
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform from homeassistant.const import CONF_HOST, Platform
@ -9,10 +10,18 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, OPTION_INPUT_SOURCES, InputSource from .const import (
DOMAIN,
OPTION_INPUT_SOURCES,
OPTION_LISTENING_MODES,
InputSource,
ListeningMode,
)
from .receiver import Receiver, async_interview from .receiver import Receiver, async_interview
from .services import DATA_MP_ENTITIES, async_register_services from .services import DATA_MP_ENTITIES, async_register_services
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.MEDIA_PLAYER] PLATFORMS = [Platform.MEDIA_PLAYER]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@ -24,6 +33,7 @@ class OnkyoData:
receiver: Receiver receiver: Receiver
sources: dict[InputSource, str] sources: dict[InputSource, str]
sound_modes: dict[ListeningMode, str]
type OnkyoConfigEntry = ConfigEntry[OnkyoData] type OnkyoConfigEntry = ConfigEntry[OnkyoData]
@ -50,7 +60,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: OnkyoConfigEntry) -> boo
sources_store: dict[str, str] = entry.options[OPTION_INPUT_SOURCES] sources_store: dict[str, str] = entry.options[OPTION_INPUT_SOURCES]
sources = {InputSource(k): v for k, v in sources_store.items()} sources = {InputSource(k): v for k, v in sources_store.items()}
entry.runtime_data = OnkyoData(receiver, sources) sound_modes_store: dict[str, str] = entry.options.get(OPTION_LISTENING_MODES, {})
sound_modes = {ListeningMode(k): v for k, v in sound_modes_store.items()}
entry.runtime_data = OnkyoData(receiver, sources, sound_modes)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@ -1,5 +1,6 @@
"""Config flow for Onkyo.""" """Config flow for Onkyo."""
from collections.abc import Mapping
import logging import logging
from typing import Any from typing import Any
@ -33,12 +34,14 @@ from .const import (
CONF_SOURCES, CONF_SOURCES,
DOMAIN, DOMAIN,
OPTION_INPUT_SOURCES, OPTION_INPUT_SOURCES,
OPTION_LISTENING_MODES,
OPTION_MAX_VOLUME, OPTION_MAX_VOLUME,
OPTION_MAX_VOLUME_DEFAULT, OPTION_MAX_VOLUME_DEFAULT,
OPTION_VOLUME_RESOLUTION, OPTION_VOLUME_RESOLUTION,
OPTION_VOLUME_RESOLUTION_DEFAULT, OPTION_VOLUME_RESOLUTION_DEFAULT,
VOLUME_RESOLUTION_ALLOWED, VOLUME_RESOLUTION_ALLOWED,
InputSource, InputSource,
ListeningMode,
) )
from .receiver import ReceiverInfo, async_discover, async_interview from .receiver import ReceiverInfo, async_discover, async_interview
@ -46,9 +49,14 @@ _LOGGER = logging.getLogger(__name__)
CONF_DEVICE = "device" CONF_DEVICE = "device"
INPUT_SOURCES_ALL_MEANINGS = [ INPUT_SOURCES_DEFAULT: dict[str, str] = {}
input_source.value_meaning for input_source in InputSource LISTENING_MODES_DEFAULT: dict[str, str] = {}
] INPUT_SOURCES_ALL_MEANINGS = {
input_source.value_meaning: input_source for input_source in InputSource
}
LISTENING_MODES_ALL_MEANINGS = {
listening_mode.value_meaning: listening_mode for listening_mode in ListeningMode
}
STEP_MANUAL_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) STEP_MANUAL_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
STEP_RECONFIGURE_SCHEMA = vol.Schema( STEP_RECONFIGURE_SCHEMA = vol.Schema(
{ {
@ -59,7 +67,14 @@ STEP_CONFIGURE_SCHEMA = STEP_RECONFIGURE_SCHEMA.extend(
{ {
vol.Required(OPTION_INPUT_SOURCES): SelectSelector( vol.Required(OPTION_INPUT_SOURCES): SelectSelector(
SelectSelectorConfig( SelectSelectorConfig(
options=INPUT_SOURCES_ALL_MEANINGS, 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, multiple=True,
mode=SelectSelectorMode.DROPDOWN, mode=SelectSelectorMode.DROPDOWN,
) )
@ -238,9 +253,8 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_HOST: self._receiver_info.host, CONF_HOST: self._receiver_info.host,
}, },
options={ options={
**entry_options,
OPTION_VOLUME_RESOLUTION: volume_resolution, OPTION_VOLUME_RESOLUTION: volume_resolution,
OPTION_MAX_VOLUME: entry_options[OPTION_MAX_VOLUME],
OPTION_INPUT_SOURCES: entry_options[OPTION_INPUT_SOURCES],
}, },
) )
@ -250,12 +264,24 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
input_source_meanings: list[str] = user_input[OPTION_INPUT_SOURCES] input_source_meanings: list[str] = user_input[OPTION_INPUT_SOURCES]
if not input_source_meanings: if not input_source_meanings:
errors[OPTION_INPUT_SOURCES] = "empty_input_source_list" errors[OPTION_INPUT_SOURCES] = "empty_input_source_list"
else:
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] = {} input_sources_store: dict[str, str] = {}
for input_source_meaning in input_source_meanings: for input_source_meaning in input_source_meanings:
input_source = InputSource.from_meaning(input_source_meaning) input_source = INPUT_SOURCES_ALL_MEANINGS[input_source_meaning]
input_sources_store[input_source.value] = 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( result = self.async_create_entry(
title=self._receiver_info.model_name, title=self._receiver_info.model_name,
data={ data={
@ -265,6 +291,7 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
OPTION_VOLUME_RESOLUTION: volume_resolution, OPTION_VOLUME_RESOLUTION: volume_resolution,
OPTION_MAX_VOLUME: OPTION_MAX_VOLUME_DEFAULT, OPTION_MAX_VOLUME: OPTION_MAX_VOLUME_DEFAULT,
OPTION_INPUT_SOURCES: input_sources_store, OPTION_INPUT_SOURCES: input_sources_store,
OPTION_LISTENING_MODES: listening_modes_store,
}, },
) )
@ -278,16 +305,13 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
if reconfigure_entry is None: if reconfigure_entry is None:
suggested_values = { suggested_values = {
OPTION_VOLUME_RESOLUTION: OPTION_VOLUME_RESOLUTION_DEFAULT, OPTION_VOLUME_RESOLUTION: OPTION_VOLUME_RESOLUTION_DEFAULT,
OPTION_INPUT_SOURCES: [], OPTION_INPUT_SOURCES: INPUT_SOURCES_DEFAULT,
OPTION_LISTENING_MODES: LISTENING_MODES_DEFAULT,
} }
else: else:
entry_options = reconfigure_entry.options entry_options = reconfigure_entry.options
suggested_values = { suggested_values = {
OPTION_VOLUME_RESOLUTION: entry_options[OPTION_VOLUME_RESOLUTION], OPTION_VOLUME_RESOLUTION: entry_options[OPTION_VOLUME_RESOLUTION],
OPTION_INPUT_SOURCES: [
InputSource(input_source).value_meaning
for input_source in entry_options[OPTION_INPUT_SOURCES]
],
} }
return self.async_show_form( return self.async_show_form(
@ -356,6 +380,7 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
OPTION_VOLUME_RESOLUTION: volume_resolution, OPTION_VOLUME_RESOLUTION: volume_resolution,
OPTION_MAX_VOLUME: max_volume, OPTION_MAX_VOLUME: max_volume,
OPTION_INPUT_SOURCES: sources_store, OPTION_INPUT_SOURCES: sources_store,
OPTION_LISTENING_MODES: LISTENING_MODES_DEFAULT,
}, },
) )
@ -373,7 +398,14 @@ OPTIONS_STEP_INIT_SCHEMA = vol.Schema(
), ),
vol.Required(OPTION_INPUT_SOURCES): SelectSelector( vol.Required(OPTION_INPUT_SOURCES): SelectSelector(
SelectSelectorConfig( SelectSelectorConfig(
options=INPUT_SOURCES_ALL_MEANINGS, 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, multiple=True,
mode=SelectSelectorMode.DROPDOWN, mode=SelectSelectorMode.DROPDOWN,
) )
@ -387,6 +419,7 @@ class OnkyoOptionsFlowHandler(OptionsFlow):
_data: dict[str, Any] _data: dict[str, Any]
_input_sources: dict[InputSource, str] _input_sources: dict[InputSource, str]
_listening_modes: dict[ListeningMode, str]
async def async_step_init( async def async_step_init(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
@ -394,20 +427,40 @@ class OnkyoOptionsFlowHandler(OptionsFlow):
"""Manage the options.""" """Manage the options."""
errors = {} errors = {}
entry_options = self.config_entry.options entry_options: Mapping[str, Any] = self.config_entry.options
entry_options = {
OPTION_LISTENING_MODES: LISTENING_MODES_DEFAULT,
**entry_options,
}
if user_input is not None: if user_input is not None:
self._input_sources = {} input_source_meanings: list[str] = user_input[OPTION_INPUT_SOURCES]
for input_source_meaning in user_input[OPTION_INPUT_SOURCES]: if not input_source_meanings:
input_source = InputSource.from_meaning(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
if not self._input_sources:
errors[OPTION_INPUT_SOURCES] = "empty_input_source_list" errors[OPTION_INPUT_SOURCES] = "empty_input_source_list"
else:
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 = { self._data = {
OPTION_VOLUME_RESOLUTION: entry_options[OPTION_VOLUME_RESOLUTION], OPTION_VOLUME_RESOLUTION: entry_options[OPTION_VOLUME_RESOLUTION],
OPTION_MAX_VOLUME: user_input[OPTION_MAX_VOLUME], OPTION_MAX_VOLUME: user_input[OPTION_MAX_VOLUME],
@ -423,6 +476,10 @@ class OnkyoOptionsFlowHandler(OptionsFlow):
InputSource(input_source).value_meaning InputSource(input_source).value_meaning
for input_source in entry_options[OPTION_INPUT_SOURCES] for input_source in entry_options[OPTION_INPUT_SOURCES]
], ],
OPTION_LISTENING_MODES: [
ListeningMode(listening_mode).value_meaning
for listening_mode in entry_options[OPTION_LISTENING_MODES]
],
} }
return self.async_show_form( return self.async_show_form(
@ -440,28 +497,48 @@ class OnkyoOptionsFlowHandler(OptionsFlow):
if user_input is not None: if user_input is not None:
input_sources_store: dict[str, str] = {} input_sources_store: dict[str, str] = {}
for input_source_meaning, input_source_name in user_input[ for input_source_meaning, input_source_name in user_input[
"input_sources" OPTION_INPUT_SOURCES
].items(): ].items():
input_source = InputSource.from_meaning(input_source_meaning) input_source = INPUT_SOURCES_ALL_MEANINGS[input_source_meaning]
input_sources_store[input_source.value] = input_source_name 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( return self.async_create_entry(
data={ data={
**self._data, **self._data,
OPTION_INPUT_SOURCES: input_sources_store, OPTION_INPUT_SOURCES: input_sources_store,
OPTION_LISTENING_MODES: listening_modes_store,
} }
) )
schema_dict: dict[Any, Selector] = {} input_sources_schema_dict: dict[Any, Selector] = {}
for input_source, input_source_name in self._input_sources.items(): for input_source, input_source_name in self._input_sources.items():
schema_dict[ input_sources_schema_dict[
vol.Required(input_source.value_meaning, default=input_source_name) vol.Required(input_source.value_meaning, default=input_source_name)
] = TextSelector() ] = 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(listening_mode.value_meaning, default=listening_mode_name)
] = TextSelector()
return self.async_show_form( return self.async_show_form(
step_id="names", step_id="names",
data_schema=vol.Schema( data_schema=vol.Schema(
{vol.Required("input_sources"): section(vol.Schema(schema_dict))} {
vol.Required(OPTION_INPUT_SOURCES): section(
vol.Schema(input_sources_schema_dict)
),
vol.Required(OPTION_LISTENING_MODES): section(
vol.Schema(listening_modes_schema_dict)
),
}
), ),
) )

View File

@ -2,7 +2,7 @@
from enum import Enum from enum import Enum
import typing import typing
from typing import ClassVar, Literal, Self from typing import Literal, Self
import pyeiscp import pyeiscp
@ -24,7 +24,27 @@ VOLUME_RESOLUTION_ALLOWED: tuple[VolumeResolution, ...] = typing.get_args(
OPTION_MAX_VOLUME = "max_volume" OPTION_MAX_VOLUME = "max_volume"
OPTION_MAX_VOLUME_DEFAULT = 100.0 OPTION_MAX_VOLUME_DEFAULT = 100.0
class EnumWithMeaning(Enum):
"""Enum with meaning."""
value_meaning: str
def __new__(cls, value: str) -> Self:
"""Create enum."""
obj = object.__new__(cls)
obj._value_ = value
obj.value_meaning = cls._get_meanings()[value]
return obj
@staticmethod
def _get_meanings() -> dict[str, str]:
raise NotImplementedError
OPTION_INPUT_SOURCES = "input_sources" OPTION_INPUT_SOURCES = "input_sources"
OPTION_LISTENING_MODES = "listening_modes"
_INPUT_SOURCE_MEANINGS = { _INPUT_SOURCE_MEANINGS = {
"00": "VIDEO1 ··· VCR/DVR ··· STB/DVR", "00": "VIDEO1 ··· VCR/DVR ··· STB/DVR",
@ -71,7 +91,7 @@ _INPUT_SOURCE_MEANINGS = {
} }
class InputSource(Enum): class InputSource(EnumWithMeaning):
"""Receiver input source.""" """Receiver input source."""
DVR = "00" DVR = "00"
@ -116,24 +136,100 @@ class InputSource(Enum):
HDMI_7 = "57" HDMI_7 = "57"
MAIN_SOURCE = "80" MAIN_SOURCE = "80"
__meaning_mapping: ClassVar[dict[str, Self]] = {} # type: ignore[misc] @staticmethod
def _get_meanings() -> dict[str, str]:
return _INPUT_SOURCE_MEANINGS
value_meaning: str
def __new__(cls, value: str) -> Self: _LISTENING_MODE_MEANINGS = {
"""Create InputSource enum.""" "00": "STEREO",
obj = object.__new__(cls) "01": "DIRECT",
obj._value_ = value "02": "SURROUND",
obj.value_meaning = _INPUT_SOURCE_MEANINGS[value] "03": "FILM ··· GAME RPG ··· ADVANCED GAME",
"04": "THX",
"05": "ACTION ··· GAME ACTION",
"06": "MUSICAL ··· GAME ROCK ··· ROCK/POP",
"07": "MONO MOVIE",
"08": "ORCHESTRA ··· CLASSICAL",
"09": "UNPLUGGED",
"0A": "STUDIO MIX ··· ENTERTAINMENT SHOW",
"0B": "TV LOGIC ··· DRAMA",
"0C": "ALL CH STEREO ··· EXTENDED STEREO",
"0D": "THEATER DIMENSIONAL ··· FRONT STAGE SURROUND",
"0E": "ENHANCED 7/ENHANCE ··· GAME SPORTS ··· SPORTS",
"0F": "MONO",
"11": "PURE AUDIO ··· PURE DIRECT",
"12": "MULTIPLEX",
"13": "FULL MONO ··· MONO MUSIC",
"14": "DOLBY VIRTUAL/SURROUND ENHANCER",
"15": "DTS SURROUND SENSATION",
"16": "AUDYSSEY DSX",
"17": "DTS VIRTUAL:X",
"1F": "WHOLE HOUSE MODE ··· MULTI ZONE MUSIC",
"23": "STAGE (JAPAN GENRE CONTROL)",
"25": "ACTION (JAPAN GENRE CONTROL)",
"26": "MUSIC (JAPAN GENRE CONTROL)",
"2E": "SPORTS (JAPAN GENRE CONTROL)",
"40": "STRAIGHT DECODE ··· 5.1 CH SURROUND",
"41": "DOLBY EX/DTS ES",
"42": "THX CINEMA",
"43": "THX SURROUND EX",
"44": "THX MUSIC",
"45": "THX GAMES",
"50": "THX U(2)/S(2)/I/S CINEMA",
"51": "THX U(2)/S(2)/I/S MUSIC",
"52": "THX U(2)/S(2)/I/S GAMES",
"80": "DOLBY ATMOS/DOLBY SURROUND ··· PLII/PLIIx MOVIE",
"81": "PLII/PLIIx MUSIC",
"82": "DTS:X/NEURAL:X ··· NEO:6/NEO:X CINEMA",
"83": "NEO:6/NEO:X MUSIC",
"84": "DOLBY SURROUND THX CINEMA ··· PLII/PLIIx THX CINEMA",
"85": "DTS NEURAL:X THX CINEMA ··· NEO:6/NEO:X THX CINEMA",
"86": "PLII/PLIIx GAME",
"87": "NEURAL SURR",
"88": "NEURAL THX/NEURAL SURROUND",
"89": "DOLBY SURROUND THX GAMES ··· PLII/PLIIx THX GAMES",
"8A": "DTS NEURAL:X THX GAMES ··· NEO:6/NEO:X THX GAMES",
"8B": "DOLBY SURROUND THX MUSIC ··· PLII/PLIIx THX MUSIC",
"8C": "DTS NEURAL:X THX MUSIC ··· NEO:6/NEO:X THX MUSIC",
"8D": "NEURAL THX CINEMA",
"8E": "NEURAL THX MUSIC",
"8F": "NEURAL THX GAMES",
"90": "PLIIz HEIGHT",
"91": "NEO:6 CINEMA DTS SURROUND SENSATION",
"92": "NEO:6 MUSIC DTS SURROUND SENSATION",
"93": "NEURAL DIGITAL MUSIC",
"94": "PLIIz HEIGHT + THX CINEMA",
"95": "PLIIz HEIGHT + THX MUSIC",
"96": "PLIIz HEIGHT + THX GAMES",
"97": "PLIIz HEIGHT + THX U2/S2 CINEMA",
"98": "PLIIz HEIGHT + THX U2/S2 MUSIC",
"99": "PLIIz HEIGHT + THX U2/S2 GAMES",
"9A": "NEO:X GAME",
"A0": "PLIIx/PLII Movie + AUDYSSEY DSX",
"A1": "PLIIx/PLII MUSIC + AUDYSSEY DSX",
"A2": "PLIIx/PLII GAME + AUDYSSEY DSX",
"A3": "NEO:6 CINEMA + AUDYSSEY DSX",
"A4": "NEO:6 MUSIC + AUDYSSEY DSX",
"A5": "NEURAL SURROUND + AUDYSSEY DSX",
"A6": "NEURAL DIGITAL MUSIC + AUDYSSEY DSX",
"A7": "DOLBY EX + AUDYSSEY DSX",
"FF": "AUTO SURROUND",
}
cls.__meaning_mapping[obj.value_meaning] = obj
return obj class ListeningMode(EnumWithMeaning):
"""Receiver listening mode."""
@classmethod _ignore_ = "ListeningMode _k _v _meaning"
def from_meaning(cls, meaning: str) -> Self:
"""Get InputSource enum from its meaning.""" ListeningMode = vars()
return cls.__meaning_mapping[meaning] for _k in _LISTENING_MODE_MEANINGS:
ListeningMode["I" + _k] = _k
@staticmethod
def _get_meanings() -> dict[str, str]:
return _LISTENING_MODE_MEANINGS
ZONES = {"main": "Main", "zone2": "Zone 2", "zone3": "Zone 3", "zone4": "Zone 4"} ZONES = {"main": "Main", "zone2": "Zone 2", "zone3": "Zone 3", "zone4": "Zone 4"}

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from enum import Enum
from functools import cache from functools import cache
import logging import logging
from typing import Any, Literal from typing import Any, Literal
@ -39,6 +40,7 @@ from .const import (
PYEISCP_COMMANDS, PYEISCP_COMMANDS,
ZONES, ZONES,
InputSource, InputSource,
ListeningMode,
VolumeResolution, VolumeResolution,
) )
from .receiver import Receiver, async_discover from .receiver import Receiver, async_discover
@ -63,6 +65,8 @@ CONF_SOURCES_DEFAULT = {
"fm": "Radio", "fm": "Radio",
} }
ISSUE_URL_PLACEHOLDER = "/config/integrations/dashboard/add?domain=onkyo"
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
{ {
vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_HOST): cv.string,
@ -79,23 +83,23 @@ PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
} }
) )
SUPPORT_ONKYO_WO_VOLUME = (
SUPPORTED_FEATURES_BASE = (
MediaPlayerEntityFeature.TURN_ON MediaPlayerEntityFeature.TURN_ON
| MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.TURN_OFF
| MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.PLAY_MEDIA
) )
SUPPORT_ONKYO = ( SUPPORTED_FEATURES_VOLUME = (
SUPPORT_ONKYO_WO_VOLUME MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_STEP
) )
DEFAULT_PLAYABLE_SOURCES = ( PLAYABLE_SOURCES = (
InputSource.from_meaning("FM"), InputSource.FM,
InputSource.from_meaning("AM"), InputSource.AM,
InputSource.from_meaning("DAB"), InputSource.DAB,
) )
ATTR_PRESET = "preset" ATTR_PRESET = "preset"
@ -118,7 +122,6 @@ AUDIO_INFORMATION_MAPPING = [
"auto_phase_control_phase", "auto_phase_control_phase",
"upmix_mode", "upmix_mode",
] ]
VIDEO_INFORMATION_MAPPING = [ VIDEO_INFORMATION_MAPPING = [
"video_input_port", "video_input_port",
"input_resolution", "input_resolution",
@ -131,7 +134,6 @@ VIDEO_INFORMATION_MAPPING = [
"picture_mode", "picture_mode",
"input_hdr", "input_hdr",
] ]
ISSUE_URL_PLACEHOLDER = "/config/integrations/dashboard/add?domain=onkyo"
type LibValue = str | tuple[str, ...] type LibValue = str | tuple[str, ...]
@ -139,7 +141,19 @@ type LibValue = str | tuple[str, ...]
def _get_single_lib_value(value: LibValue) -> str: def _get_single_lib_value(value: LibValue) -> str:
if isinstance(value, str): if isinstance(value, str):
return value return value
return value[0] return value[-1]
def _get_lib_mapping[T: Enum](cmds: Any, cls: type[T]) -> dict[T, LibValue]:
result: dict[T, LibValue] = {}
for k, v in cmds["values"].items():
try:
key = cls(k)
except ValueError:
continue
result[key] = v["name"]
return result
@cache @cache
@ -154,15 +168,7 @@ def _input_source_lib_mappings(zone: str) -> dict[InputSource, LibValue]:
case "zone4": case "zone4":
cmds = PYEISCP_COMMANDS["zone4"]["SL4"] cmds = PYEISCP_COMMANDS["zone4"]["SL4"]
result: dict[InputSource, LibValue] = {} return _get_lib_mapping(cmds, InputSource)
for k, v in cmds["values"].items():
try:
source = InputSource(k)
except ValueError:
continue
result[source] = v["name"]
return result
@cache @cache
@ -170,6 +176,24 @@ def _rev_input_source_lib_mappings(zone: str) -> dict[LibValue, InputSource]:
return {value: key for key, value in _input_source_lib_mappings(zone).items()} return {value: key for key, value in _input_source_lib_mappings(zone).items()}
@cache
def _listening_mode_lib_mappings(zone: str) -> dict[ListeningMode, LibValue]:
match zone:
case "main":
cmds = PYEISCP_COMMANDS["main"]["LMD"]
case "zone2":
cmds = PYEISCP_COMMANDS["zone2"]["LMZ"]
case _:
return {}
return _get_lib_mapping(cmds, ListeningMode)
@cache
def _rev_listening_mode_lib_mappings(zone: str) -> dict[LibValue, ListeningMode]:
return {value: key for key, value in _listening_mode_lib_mappings(zone).items()}
async def async_setup_platform( async def async_setup_platform(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigType, config: ConfigType,
@ -303,6 +327,7 @@ async def async_setup_entry(
volume_resolution: VolumeResolution = entry.options[OPTION_VOLUME_RESOLUTION] volume_resolution: VolumeResolution = entry.options[OPTION_VOLUME_RESOLUTION]
max_volume: float = entry.options[OPTION_MAX_VOLUME] max_volume: float = entry.options[OPTION_MAX_VOLUME]
sources = data.sources sources = data.sources
sound_modes = data.sound_modes
def connect_callback(receiver: Receiver) -> None: def connect_callback(receiver: Receiver) -> None:
if not receiver.first_connect: if not receiver.first_connect:
@ -331,6 +356,7 @@ async def async_setup_entry(
volume_resolution=volume_resolution, volume_resolution=volume_resolution,
max_volume=max_volume, max_volume=max_volume,
sources=sources, sources=sources,
sound_modes=sound_modes,
) )
entities[zone] = zone_entity entities[zone] = zone_entity
async_add_entities([zone_entity]) async_add_entities([zone_entity])
@ -345,6 +371,7 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
_attr_should_poll = False _attr_should_poll = False
_supports_volume: bool = False _supports_volume: bool = False
_supports_sound_mode: bool = False
_supports_audio_info: bool = False _supports_audio_info: bool = False
_supports_video_info: bool = False _supports_video_info: bool = False
_query_timer: asyncio.TimerHandle | None = None _query_timer: asyncio.TimerHandle | None = None
@ -357,6 +384,7 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
volume_resolution: VolumeResolution, volume_resolution: VolumeResolution,
max_volume: float, max_volume: float,
sources: dict[InputSource, str], sources: dict[InputSource, str],
sound_modes: dict[ListeningMode, str],
) -> None: ) -> None:
"""Initialize the Onkyo Receiver.""" """Initialize the Onkyo Receiver."""
self._receiver = receiver self._receiver = receiver
@ -381,7 +409,27 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
value: key for key, value in self._source_mapping.items() value: key for key, value in self._source_mapping.items()
} }
self._sound_mode_lib_mapping = _listening_mode_lib_mappings(zone)
self._rev_sound_mode_lib_mapping = _rev_listening_mode_lib_mappings(zone)
self._sound_mode_mapping = {
key: value
for key, value in sound_modes.items()
if key in self._sound_mode_lib_mapping
}
self._rev_sound_mode_mapping = {
value: key for key, value in self._sound_mode_mapping.items()
}
self._attr_source_list = list(self._rev_source_mapping) self._attr_source_list = list(self._rev_source_mapping)
self._attr_sound_mode_list = list(self._rev_sound_mode_mapping)
self._attr_supported_features = SUPPORTED_FEATURES_BASE
if zone == "main":
self._attr_supported_features |= SUPPORTED_FEATURES_VOLUME
self._supports_volume = True
self._attr_supported_features |= MediaPlayerEntityFeature.SELECT_SOUND_MODE
self._supports_sound_mode = True
self._attr_extra_state_attributes = {} self._attr_extra_state_attributes = {}
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
@ -394,13 +442,6 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
self._query_timer.cancel() self._query_timer.cancel()
self._query_timer = None self._query_timer = None
@property
def supported_features(self) -> MediaPlayerEntityFeature:
"""Return media player features that are supported."""
if self._supports_volume:
return SUPPORT_ONKYO
return SUPPORT_ONKYO_WO_VOLUME
@callback @callback
def _update_receiver(self, propname: str, value: Any) -> None: def _update_receiver(self, propname: str, value: Any) -> None:
"""Update a property in the receiver.""" """Update a property in the receiver."""
@ -466,6 +507,24 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
"input-selector" if self._zone == "main" else "selector", source_lib_single "input-selector" if self._zone == "main" else "selector", source_lib_single
) )
async def async_select_sound_mode(self, sound_mode: str) -> None:
"""Select listening sound mode."""
if not self.sound_mode_list or sound_mode not in self.sound_mode_list:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_sound_mode",
translation_placeholders={
"invalid_sound_mode": sound_mode,
"entity_id": self.entity_id,
},
)
sound_mode_lib = self._sound_mode_lib_mapping[
self._rev_sound_mode_mapping[sound_mode]
]
sound_mode_lib_single = _get_single_lib_value(sound_mode_lib)
self._update_receiver("listening-mode", sound_mode_lib_single)
async def async_select_output(self, hdmi_output: str) -> None: async def async_select_output(self, hdmi_output: str) -> None:
"""Set hdmi-out.""" """Set hdmi-out."""
self._update_receiver("hdmi-output-selector", hdmi_output) self._update_receiver("hdmi-output-selector", hdmi_output)
@ -476,7 +535,7 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
"""Play radio station by preset number.""" """Play radio station by preset number."""
if self.source is not None: if self.source is not None:
source = self._rev_source_mapping[self.source] source = self._rev_source_mapping[self.source]
if media_type.lower() == "radio" and source in DEFAULT_PLAYABLE_SOURCES: if media_type.lower() == "radio" and source in PLAYABLE_SOURCES:
self._update_receiver("preset", media_id) self._update_receiver("preset", media_id)
@callback @callback
@ -517,7 +576,9 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
self._attr_extra_state_attributes.pop(ATTR_PRESET, None) self._attr_extra_state_attributes.pop(ATTR_PRESET, None)
self._attr_extra_state_attributes.pop(ATTR_VIDEO_OUT, None) self._attr_extra_state_attributes.pop(ATTR_VIDEO_OUT, None)
elif command in ["volume", "master-volume"] and value != "N/A": elif command in ["volume", "master-volume"] and value != "N/A":
self._supports_volume = True if not self._supports_volume:
self._attr_supported_features |= SUPPORTED_FEATURES_VOLUME
self._supports_volume = True
# AMP_VOL / (VOL_RESOLUTION * (MAX_VOL / 100)) # AMP_VOL / (VOL_RESOLUTION * (MAX_VOL / 100))
volume_level: float = value / ( volume_level: float = value / (
self._volume_resolution * self._max_volume / 100 self._volume_resolution * self._max_volume / 100
@ -535,6 +596,14 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
self._attr_extra_state_attributes[ATTR_PRESET] = value self._attr_extra_state_attributes[ATTR_PRESET] = value
elif ATTR_PRESET in self._attr_extra_state_attributes: elif ATTR_PRESET in self._attr_extra_state_attributes:
del self._attr_extra_state_attributes[ATTR_PRESET] del self._attr_extra_state_attributes[ATTR_PRESET]
elif command == "listening-mode" and value != "N/A":
if not self._supports_sound_mode:
self._attr_supported_features |= (
MediaPlayerEntityFeature.SELECT_SOUND_MODE
)
self._supports_sound_mode = True
self._parse_sound_mode(value)
self._query_av_info_delayed()
elif command == "audio-information": elif command == "audio-information":
self._supports_audio_info = True self._supports_audio_info = True
self._parse_audio_information(value) self._parse_audio_information(value)
@ -561,6 +630,21 @@ class OnkyoMediaPlayer(MediaPlayerEntity):
) )
self._attr_source = source_meaning self._attr_source = source_meaning
@callback
def _parse_sound_mode(self, mode_lib: LibValue) -> None:
sound_mode = self._rev_sound_mode_lib_mapping[mode_lib]
if sound_mode in self._sound_mode_mapping:
self._attr_sound_mode = self._sound_mode_mapping[sound_mode]
return
sound_mode_meaning = sound_mode.value_meaning
_LOGGER.error(
'Listening mode "%s" is invalid for entity: %s',
sound_mode_meaning,
self.entity_id,
)
self._attr_sound_mode = sound_mode_meaning
@callback @callback
def _parse_audio_information( def _parse_audio_information(
self, audio_information: tuple[str] | Literal["N/A"] self, audio_information: tuple[str] | Literal["N/A"]

View File

@ -27,17 +27,20 @@
"description": "Configure {name}", "description": "Configure {name}",
"data": { "data": {
"volume_resolution": "Volume resolution", "volume_resolution": "Volume resolution",
"input_sources": "[%key:component::onkyo::options::step::init::data::input_sources%]" "input_sources": "[%key:component::onkyo::options::step::init::data::input_sources%]",
"listening_modes": "[%key:component::onkyo::options::step::init::data::listening_modes%]"
}, },
"data_description": { "data_description": {
"volume_resolution": "Number of steps it takes for the receiver to go from the lowest to the highest possible volume.", "volume_resolution": "Number of steps it takes for the receiver to go from the lowest to the highest possible volume.",
"input_sources": "[%key:component::onkyo::options::step::init::data_description::input_sources%]" "input_sources": "[%key:component::onkyo::options::step::init::data_description::input_sources%]",
"listening_modes": "[%key:component::onkyo::options::step::init::data_description::listening_modes%]"
} }
} }
}, },
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"empty_input_source_list": "[%key:component::onkyo::options::error::empty_input_source_list%]", "empty_input_source_list": "[%key:component::onkyo::options::error::empty_input_source_list%]",
"empty_listening_mode_list": "[%key:component::onkyo::options::error::empty_listening_mode_list%]",
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
}, },
"abort": { "abort": {
@ -53,11 +56,13 @@
"init": { "init": {
"data": { "data": {
"max_volume": "Maximum volume limit (%)", "max_volume": "Maximum volume limit (%)",
"input_sources": "Input sources" "input_sources": "Input sources",
"listening_modes": "Listening modes"
}, },
"data_description": { "data_description": {
"max_volume": "Maximum volume limit as a percentage. This will associate Home Assistant's maximum volume to this value on the receiver, i.e., if you set this to 50%, then setting the volume to 100% in Home Assistant will cause the volume on the receiver to be set to 50% of its maximum value.", "max_volume": "Maximum volume limit as a percentage. This will associate Home Assistant's maximum volume to this value on the receiver, i.e., if you set this to 50%, then setting the volume to 100% in Home Assistant will cause the volume on the receiver to be set to 50% of its maximum value.",
"input_sources": "List of input sources supported by the receiver." "input_sources": "List of input sources supported by the receiver.",
"listening_modes": "List of listening modes supported by the receiver."
} }
}, },
"names": { "names": {
@ -65,12 +70,17 @@
"input_sources": { "input_sources": {
"name": "Input source names", "name": "Input source names",
"description": "Mappings of receiver's input sources to their names." "description": "Mappings of receiver's input sources to their names."
},
"listening_modes": {
"name": "Listening mode names",
"description": "Mappings of receiver's listening modes to their names."
} }
} }
} }
}, },
"error": { "error": {
"empty_input_source_list": "Input source list cannot be empty" "empty_input_source_list": "Input source list cannot be empty",
"empty_listening_mode_list": "Listening mode list cannot be empty"
} }
}, },
"issues": { "issues": {
@ -84,6 +94,9 @@
} }
}, },
"exceptions": { "exceptions": {
"invalid_sound_mode": {
"message": "Cannot select sound mode \"{invalid_sound_mode}\" for entity: {entity_id}."
},
"invalid_source": { "invalid_source": {
"message": "Cannot select input source \"{invalid_source}\" for entity: {entity_id}." "message": "Cannot select input source \"{invalid_source}\" for entity: {entity_id}."
} }

View File

@ -34,8 +34,9 @@ def create_config_entry_from_info(info: ReceiverInfo) -> MockConfigEntry:
data = {CONF_HOST: info.host} data = {CONF_HOST: info.host}
options = { options = {
"volume_resolution": 80, "volume_resolution": 80,
"input_sources": {"12": "tv"},
"max_volume": 100, "max_volume": 100,
"input_sources": {"12": "tv"},
"listening_modes": {"00": "stereo"},
} }
return MockConfigEntry( return MockConfigEntry(
@ -52,8 +53,9 @@ def create_empty_config_entry() -> MockConfigEntry:
data = {CONF_HOST: ""} data = {CONF_HOST: ""}
options = { options = {
"volume_resolution": 80, "volume_resolution": 80,
"input_sources": {"12": "tv"},
"max_volume": 100, "max_volume": 100,
"input_sources": {"12": "tv"},
"listening_modes": {"00": "stereo"},
} }
return MockConfigEntry( return MockConfigEntry(

View File

@ -11,7 +11,9 @@ from homeassistant.components.onkyo.config_flow import OnkyoConfigFlow
from homeassistant.components.onkyo.const import ( from homeassistant.components.onkyo.const import (
DOMAIN, DOMAIN,
OPTION_INPUT_SOURCES, OPTION_INPUT_SOURCES,
OPTION_LISTENING_MODES,
OPTION_MAX_VOLUME, OPTION_MAX_VOLUME,
OPTION_MAX_VOLUME_DEFAULT,
OPTION_VOLUME_RESOLUTION, OPTION_VOLUME_RESOLUTION,
) )
from homeassistant.config_entries import SOURCE_USER from homeassistant.config_entries import SOURCE_USER
@ -226,7 +228,11 @@ async def test_ssdp_discovery_success(
select_result = await hass.config_entries.flow.async_configure( select_result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
user_input={"volume_resolution": 200, "input_sources": ["TV"]}, user_input={
"volume_resolution": 200,
"input_sources": ["TV"],
"listening_modes": ["THX"],
},
) )
assert select_result["type"] is FlowResultType.CREATE_ENTRY assert select_result["type"] is FlowResultType.CREATE_ENTRY
@ -349,34 +355,6 @@ async def test_ssdp_discovery_no_host(
assert result["reason"] == "unknown" assert result["reason"] == "unknown"
async def test_configure_empty_source_list(
hass: HomeAssistant, default_mock_discovery
) -> None:
"""Test receiver configuration with no sources set."""
init_result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
form_result = await hass.config_entries.flow.async_configure(
init_result["flow_id"],
{"next_step_id": "manual"},
)
select_result = await hass.config_entries.flow.async_configure(
form_result["flow_id"],
user_input={CONF_HOST: "sample-host-name"},
)
configure_result = await hass.config_entries.flow.async_configure(
select_result["flow_id"],
user_input={"volume_resolution": 200, "input_sources": []},
)
assert configure_result["errors"] == {"input_sources": "empty_input_source_list"}
async def test_configure_no_resolution( async def test_configure_no_resolution(
hass: HomeAssistant, default_mock_discovery hass: HomeAssistant, default_mock_discovery
) -> None: ) -> None:
@ -404,33 +382,61 @@ async def test_configure_no_resolution(
) )
async def test_configure_resolution_set( async def test_configure(hass: HomeAssistant, default_mock_discovery) -> None:
hass: HomeAssistant, default_mock_discovery """Test receiver configure."""
) -> None:
"""Test receiver configure with specified resolution."""
init_result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": SOURCE_USER}, context={"source": SOURCE_USER},
) )
form_result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
init_result["flow_id"], result["flow_id"],
{"next_step_id": "manual"}, {"next_step_id": "manual"},
) )
select_result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
form_result["flow_id"], result["flow_id"],
user_input={CONF_HOST: "sample-host-name"}, user_input={CONF_HOST: "sample-host-name"},
) )
configure_result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
select_result["flow_id"], result["flow_id"],
user_input={"volume_resolution": 200, "input_sources": ["TV"]}, user_input={
OPTION_VOLUME_RESOLUTION: 200,
OPTION_INPUT_SOURCES: [],
OPTION_LISTENING_MODES: ["THX"],
},
) )
assert result["step_id"] == "configure_receiver"
assert result["errors"] == {OPTION_INPUT_SOURCES: "empty_input_source_list"}
assert configure_result["type"] is FlowResultType.CREATE_ENTRY result = await hass.config_entries.flow.async_configure(
assert configure_result["options"]["volume_resolution"] == 200 result["flow_id"],
user_input={
OPTION_VOLUME_RESOLUTION: 200,
OPTION_INPUT_SOURCES: ["TV"],
OPTION_LISTENING_MODES: [],
},
)
assert result["step_id"] == "configure_receiver"
assert result["errors"] == {OPTION_LISTENING_MODES: "empty_listening_mode_list"}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
OPTION_VOLUME_RESOLUTION: 200,
OPTION_INPUT_SOURCES: ["TV"],
OPTION_LISTENING_MODES: ["THX"],
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["options"] == {
OPTION_VOLUME_RESOLUTION: 200,
OPTION_MAX_VOLUME: OPTION_MAX_VOLUME_DEFAULT,
OPTION_INPUT_SOURCES: {"12": "TV"},
OPTION_LISTENING_MODES: {"04": "THX"},
}
async def test_configure_invalid_resolution_set( async def test_configure_invalid_resolution_set(
@ -601,21 +607,26 @@ async def test_import_success(
await hass.async_block_till_done() await hass.async_block_till_done()
assert import_result["type"] is FlowResultType.CREATE_ENTRY assert import_result["type"] is FlowResultType.CREATE_ENTRY
assert import_result["data"]["host"] == "host 1" assert import_result["data"] == {"host": "host 1"}
assert import_result["options"]["volume_resolution"] == 80 assert import_result["options"] == {
assert import_result["options"]["max_volume"] == 100 "volume_resolution": 80,
assert import_result["options"]["input_sources"] == { "max_volume": 100,
"00": "Auxiliary", "input_sources": {
"01": "Video", "00": "Auxiliary",
"01": "Video",
},
"listening_modes": {},
} }
@pytest.mark.parametrize( @pytest.mark.parametrize(
"ignore_translations", "ignore_translations",
[ [
[ # The schema is dynamically created from input sources [ # The schema is dynamically created from input sources and listening modes
"component.onkyo.options.step.names.sections.input_sources.data.TV", "component.onkyo.options.step.names.sections.input_sources.data.TV",
"component.onkyo.options.step.names.sections.input_sources.data_description.TV", "component.onkyo.options.step.names.sections.input_sources.data_description.TV",
"component.onkyo.options.step.names.sections.listening_modes.data.STEREO",
"component.onkyo.options.step.names.sections.listening_modes.data_description.STEREO",
] ]
], ],
) )
@ -635,6 +646,7 @@ async def test_options_flow(hass: HomeAssistant, config_entry: MockConfigEntry)
user_input={ user_input={
OPTION_MAX_VOLUME: 42, OPTION_MAX_VOLUME: 42,
OPTION_INPUT_SOURCES: [], OPTION_INPUT_SOURCES: [],
OPTION_LISTENING_MODES: ["STEREO"],
}, },
) )
@ -647,6 +659,20 @@ async def test_options_flow(hass: HomeAssistant, config_entry: MockConfigEntry)
user_input={ user_input={
OPTION_MAX_VOLUME: 42, OPTION_MAX_VOLUME: 42,
OPTION_INPUT_SOURCES: ["TV"], OPTION_INPUT_SOURCES: ["TV"],
OPTION_LISTENING_MODES: [],
},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "init"
assert result["errors"] == {OPTION_LISTENING_MODES: "empty_listening_mode_list"}
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
OPTION_MAX_VOLUME: 42,
OPTION_INPUT_SOURCES: ["TV"],
OPTION_LISTENING_MODES: ["STEREO"],
}, },
) )
@ -657,6 +683,7 @@ async def test_options_flow(hass: HomeAssistant, config_entry: MockConfigEntry)
result["flow_id"], result["flow_id"],
user_input={ user_input={
OPTION_INPUT_SOURCES: {"TV": "television"}, OPTION_INPUT_SOURCES: {"TV": "television"},
OPTION_LISTENING_MODES: {"STEREO": "Duophonia"},
}, },
) )
@ -665,4 +692,5 @@ async def test_options_flow(hass: HomeAssistant, config_entry: MockConfigEntry)
OPTION_VOLUME_RESOLUTION: old_volume_resolution, OPTION_VOLUME_RESOLUTION: old_volume_resolution,
OPTION_MAX_VOLUME: 42.0, OPTION_MAX_VOLUME: 42.0,
OPTION_INPUT_SOURCES: {"12": "television"}, OPTION_INPUT_SOURCES: {"12": "television"},
OPTION_LISTENING_MODES: {"00": "Duophonia"},
} }