1 """Support for Onkyo Receivers."""
3 from __future__
import annotations
7 from typing
import Any, Literal
9 import voluptuous
as vol
12 PLATFORM_SCHEMA
as MEDIA_PLAYER_PLATFORM_SCHEMA,
14 MediaPlayerEntityFeature,
27 from .
import OnkyoConfigEntry
29 CONF_RECEIVER_MAX_VOLUME,
33 OPTION_VOLUME_RESOLUTION,
39 from .receiver
import Receiver, async_discover
40 from .services
import DATA_MP_ENTITIES
42 _LOGGER = logging.getLogger(__name__)
44 CONF_MAX_VOLUME_DEFAULT = 100
45 CONF_RECEIVER_MAX_VOLUME_DEFAULT = 80
46 CONF_SOURCES_DEFAULT = {
61 PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
63 vol.Optional(CONF_HOST): cv.string,
64 vol.Optional(CONF_NAME): cv.string,
65 vol.Optional(OPTION_MAX_VOLUME, default=CONF_MAX_VOLUME_DEFAULT): vol.All(
66 vol.Coerce(int), vol.Range(min=1, max=100)
69 CONF_RECEIVER_MAX_VOLUME, default=CONF_RECEIVER_MAX_VOLUME_DEFAULT
71 vol.Optional(CONF_SOURCES, default=CONF_SOURCES_DEFAULT): {
77 SUPPORT_ONKYO_WO_VOLUME = (
78 MediaPlayerEntityFeature.TURN_ON
79 | MediaPlayerEntityFeature.TURN_OFF
80 | MediaPlayerEntityFeature.SELECT_SOURCE
81 | MediaPlayerEntityFeature.PLAY_MEDIA
84 SUPPORT_ONKYO_WO_VOLUME
85 | MediaPlayerEntityFeature.VOLUME_SET
86 | MediaPlayerEntityFeature.VOLUME_MUTE
87 | MediaPlayerEntityFeature.VOLUME_STEP
90 DEFAULT_PLAYABLE_SOURCES = (
91 InputSource.from_meaning(
"FM"),
92 InputSource.from_meaning(
"AM"),
93 InputSource.from_meaning(
"TUNER"),
96 ATTR_PRESET =
"preset"
97 ATTR_AUDIO_INFORMATION =
"audio_information"
98 ATTR_VIDEO_INFORMATION =
"video_information"
99 ATTR_VIDEO_OUT =
"video_out"
101 AUDIO_VIDEO_INFORMATION_UPDATE_WAIT_TIME = 8
103 AUDIO_INFORMATION_MAPPING = [
105 "input_signal_format",
111 "precision_quartz_lock_system",
112 "auto_phase_control_delay",
113 "auto_phase_control_phase",
116 VIDEO_INFORMATION_MAPPING = [
119 "input_color_schema",
123 "output_color_schema",
124 "output_color_depth",
127 ISSUE_URL_PLACEHOLDER =
"/config/integrations/dashboard/add?domain=onkyo"
129 type InputLibValue = str | tuple[str, ...]
135 cmds = PYEISCP_COMMANDS[
"main"][
"SLI"]
137 cmds = PYEISCP_COMMANDS[
"zone2"][
"SLZ"]
139 cmds = PYEISCP_COMMANDS[
"zone3"][
"SL3"]
141 cmds = PYEISCP_COMMANDS[
"zone4"][
"SL4"]
143 result: dict[InputSource, InputLibValue] = {}
144 for k, v
in cmds[
"values"].items():
149 result[source] = v[
"name"]
157 async_add_entities: AddEntitiesCallback,
158 discovery_info: DiscoveryInfoType |
None =
None,
160 """Import config from yaml."""
161 host = config.get(CONF_HOST)
163 source_mapping: dict[str, InputSource] = {}
166 if isinstance(source_lib, str):
167 source_mapping.setdefault(source_lib, source)
169 for source_lib_single
in source_lib:
170 source_mapping.setdefault(source_lib_single, source)
172 sources: dict[InputSource, str] = {}
173 for source_lib_single, source_name
in config[CONF_SOURCES].items():
174 user_source = source_mapping.get(source_lib_single.lower())
175 if user_source
is not None:
176 sources[user_source] = source_name
178 config[CONF_SOURCES] = sources
182 _LOGGER.debug(
"Importing yaml single: %s", host)
183 result = await hass.config_entries.flow.async_init(
184 DOMAIN, context={
"source": SOURCE_IMPORT}, data=config
186 results.append((host, result))
192 registry = er.async_get(hass)
193 old_unique_id = f
"{info.model_name}_{info.identifier}"
194 new_unique_id = f
"{info.identifier}_main"
195 entity_id = registry.async_get_entity_id(
196 "media_player", DOMAIN, old_unique_id
198 if entity_id
is not None:
200 "Migrating unique_id from [%s] to [%s] for entity %s",
205 registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
207 _LOGGER.debug(
"Importing yaml discover: %s", info.host)
208 result = await hass.config_entries.flow.async_init(
210 context={
"source": SOURCE_IMPORT},
211 data=config | {CONF_HOST: info.host} | {
"info": info},
213 results.append((host, result))
215 _LOGGER.debug(
"Importing yaml results: %s", results)
220 "deprecated_yaml_import_issue_no_discover",
221 breaks_in_ha_version=
"2025.5.0",
224 severity=IssueSeverity.WARNING,
225 translation_key=
"deprecated_yaml_import_issue_no_discover",
226 translation_placeholders={
"url": ISSUE_URL_PLACEHOLDER},
229 all_successful =
True
230 for host, result
in results:
232 result.get(
"type") == FlowResultType.CREATE_ENTRY
233 or result.get(
"reason") ==
"already_configured"
236 if error := result.get(
"reason"):
237 all_successful =
False
241 f
"deprecated_yaml_import_issue_{host}_{error}",
242 breaks_in_ha_version=
"2025.5.0",
245 severity=IssueSeverity.WARNING,
246 translation_key=f
"deprecated_yaml_import_issue_{error}",
247 translation_placeholders={
249 "url": ISSUE_URL_PLACEHOLDER,
256 HOMEASSISTANT_DOMAIN,
257 f
"deprecated_yaml_{DOMAIN}",
260 breaks_in_ha_version=
"2025.5.0",
261 severity=IssueSeverity.WARNING,
262 translation_key=
"deprecated_yaml",
263 translation_placeholders={
265 "integration_title":
"onkyo",
272 entry: OnkyoConfigEntry,
273 async_add_entities: AddEntitiesCallback,
275 """Set up MediaPlayer for config entry."""
276 data = entry.runtime_data
278 receiver = data.receiver
279 all_entities = hass.data[DATA_MP_ENTITIES]
281 entities: dict[str, OnkyoMediaPlayer] = {}
282 all_entities[entry.entry_id] = entities
284 volume_resolution: VolumeResolution = entry.options[OPTION_VOLUME_RESOLUTION]
285 max_volume: float = entry.options[OPTION_MAX_VOLUME]
286 sources = data.sources
288 def connect_callback(receiver: Receiver) ->
None:
289 if not receiver.first_connect:
290 for entity
in entities.values():
292 entity.backfill_state()
294 def update_callback(receiver: Receiver, message: tuple[str, str, Any]) ->
None:
295 zone, _, value = message
296 entity = entities.get(zone)
297 if entity
is not None:
299 entity.process_update(message)
300 elif zone
in ZONES
and value !=
"N/A":
304 "Discovered %s on %s (%s)",
312 volume_resolution=volume_resolution,
313 max_volume=max_volume,
316 entities[zone] = zone_entity
319 receiver.callbacks.connect.append(connect_callback)
320 receiver.callbacks.update.append(update_callback)
324 """Representation of an Onkyo Receiver Media Player (one per each zone)."""
326 _attr_should_poll =
False
328 _supports_volume: bool =
False
329 _supports_audio_info: bool =
False
330 _supports_video_info: bool =
False
331 _query_timer: asyncio.TimerHandle |
None =
None
338 volume_resolution: VolumeResolution,
340 sources: dict[InputSource, str],
342 """Initialize the Onkyo Receiver."""
344 name = receiver.model_name
345 identifier = receiver.identifier
346 self.
_attr_name_attr_name = f
"{name}{' ' + ZONES[zone] if zone != 'main' else ''}"
358 value: key
for key, value
in self.
_lib_mapping_lib_mapping.items()
365 """Entity has been added to hass."""
369 """Cancel the query timer when the entity is removed."""
376 """Return media player features that are supported."""
379 return SUPPORT_ONKYO_WO_VOLUME
383 """Update a property in the receiver."""
384 self.
_receiver_receiver.conn.update_property(self.
_zone_zone, propname, value)
388 """Cause the receiver to send an update about a property."""
389 self.
_receiver_receiver.conn.query_property(self.
_zone_zone, propname)
392 """Turn the media player on."""
396 """Turn the media player off."""
400 """Set volume level, range 0..1.
402 However full volume on the amp is usually far too loud so allow the user to
403 specify the upper range with CONF_MAX_VOLUME. We change as per max_volume
404 set by user. This means that if max volume is 80 then full volume in HA
405 will give 80% volume on the receiver. Then we convert that to the correct
406 scale for the receiver.
414 """Increase volume by 1 step."""
418 """Decrease volume by 1 step."""
422 """Mute the volume."""
424 "audio-muting" if self.
_zone_zone ==
"main" else "muting",
425 "on" if mute
else "off",
429 """Select input source."""
432 if isinstance(source_lib, str):
433 source_lib_single = source_lib
435 source_lib_single = source_lib[0]
437 "input-selector" if self.
_zone_zone ==
"main" else "selector", source_lib_single
445 self, media_type: MediaType | str, media_id: str, **kwargs: Any
447 """Play radio station by preset number."""
448 if self.
sourcesource
is not None:
450 if media_type.lower() ==
"radio" and source
in DEFAULT_PLAYABLE_SOURCES:
455 """Get the receiver to send all the info we care about.
457 Usually run only on connect, as we can otherwise rely on the
458 receiver to keep us informed of changes.
463 if self.
_zone_zone ==
"main":
476 """Store relevant updates so they can be queried later."""
477 zone, command, value = update
478 if zone != self.
_zone_zone:
481 if command
in [
"system-power",
"power"]:
490 elif command
in [
"volume",
"master-volume"]
and value !=
"N/A":
493 volume_level: float = value / (
497 elif command
in [
"muting",
"audio-muting"]:
499 elif command
in [
"selector",
"input-selector"]:
502 elif command ==
"hdmi-output-selector":
504 elif command ==
"preset":
505 if self.
sourcesource
is not None and self.
sourcesource.lower() ==
"radio":
509 elif command ==
"audio-information":
512 elif command ==
"video-information":
515 elif command ==
"fl-display-information":
527 source_meaning = source.value_meaning
529 'Input source "%s" not in source list: %s', source_meaning, self.
entity_identity_id
535 self, audio_information: tuple[str] | Literal[
"N/A"]
539 if audio_information ==
"N/A":
545 for name, value
in zip(
546 AUDIO_INFORMATION_MAPPING, audio_information, strict=
False
553 self, video_information: tuple[str] | Literal[
"N/A"]
557 if video_information ==
"N/A":
563 for name, value
in zip(
564 VIDEO_INFORMATION_MAPPING, video_information, strict=
False
573 def _query_av_info() -> None:
581 AUDIO_VIDEO_INFORMATION_UPDATE_WAIT_TIME, _query_av_info
None async_write_ha_state(self)
None async_discover(DiscoveryInfo discovery_info)
None async_create_issue(HomeAssistant hass, str entry_id)