mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 09:47:13 +00:00
Add sound mode support to Onkyo (#133531)
This commit is contained in:
parent
2bba185e4c
commit
38cc26485a
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
),
|
||||||
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -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"}
|
||||||
|
@ -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"]
|
||||||
|
@ -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}."
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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"},
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user