1 """Support for Denon AVR receivers using their HTTP interface."""
3 from __future__
import annotations
5 from collections.abc
import Awaitable, Callable, Coroutine
6 from datetime
import timedelta
7 from functools
import wraps
9 from typing
import Any, Concatenate
11 from denonavr
import DenonAVR
12 from denonavr.const
import (
21 from denonavr.exceptions
import (
29 import voluptuous
as vol
32 MediaPlayerDeviceClass,
34 MediaPlayerEntityFeature,
45 from .
import CONF_RECEIVER
46 from .config_flow
import (
51 DEFAULT_UPDATE_AUDYSSEY,
55 _LOGGER = logging.getLogger(__name__)
57 ATTR_SOUND_MODE_RAW =
"sound_mode_raw"
58 ATTR_DYNAMIC_EQ =
"dynamic_eq"
61 MediaPlayerEntityFeature.VOLUME_STEP
62 | MediaPlayerEntityFeature.VOLUME_MUTE
63 | MediaPlayerEntityFeature.TURN_ON
64 | MediaPlayerEntityFeature.TURN_OFF
65 | MediaPlayerEntityFeature.SELECT_SOURCE
66 | MediaPlayerEntityFeature.VOLUME_SET
69 SUPPORT_MEDIA_MODES = (
70 MediaPlayerEntityFeature.PLAY_MEDIA
71 | MediaPlayerEntityFeature.PAUSE
72 | MediaPlayerEntityFeature.PREVIOUS_TRACK
73 | MediaPlayerEntityFeature.NEXT_TRACK
74 | MediaPlayerEntityFeature.VOLUME_SET
75 | MediaPlayerEntityFeature.PLAY
82 SERVICE_GET_COMMAND =
"get_command"
83 SERVICE_SET_DYNAMIC_EQ =
"set_dynamic_eq"
84 SERVICE_UPDATE_AUDYSSEY =
"update_audyssey"
103 DENON_STATE_MAPPING = {
104 STATE_ON: MediaPlayerState.ON,
105 STATE_OFF: MediaPlayerState.OFF,
106 STATE_PLAYING: MediaPlayerState.PLAYING,
107 STATE_PAUSED: MediaPlayerState.PAUSED,
113 config_entry: ConfigEntry,
114 async_add_entities: AddEntitiesCallback,
116 """Set up the DenonAVR receiver from a config entry."""
118 data = hass.data[DOMAIN][config_entry.entry_id]
119 receiver = data[CONF_RECEIVER]
120 update_audyssey = config_entry.options.get(
121 CONF_UPDATE_AUDYSSEY, DEFAULT_UPDATE_AUDYSSEY
123 for receiver_zone
in receiver.zones.values():
124 if config_entry.data[CONF_SERIAL_NUMBER]
is not None:
125 unique_id = f
"{config_entry.unique_id}-{receiver_zone.zone}"
127 unique_id = f
"{config_entry.entry_id}-{receiver_zone.zone}"
137 "%s receiver at host %s initialized", receiver.manufacturer, receiver.host
141 platform = entity_platform.async_get_current_platform()
142 platform.async_register_entity_service(
144 {vol.Required(ATTR_COMMAND): cv.string},
145 f
"async_{SERVICE_GET_COMMAND}",
147 platform.async_register_entity_service(
148 SERVICE_SET_DYNAMIC_EQ,
149 {vol.Required(ATTR_DYNAMIC_EQ): cv.boolean},
150 f
"async_{SERVICE_SET_DYNAMIC_EQ}",
152 platform.async_register_entity_service(
153 SERVICE_UPDATE_AUDYSSEY,
155 f
"async_{SERVICE_UPDATE_AUDYSSEY}",
161 def async_log_errors[_DenonDeviceT: DenonDevice, **_P, _R](
162 func: Callable[Concatenate[_DenonDeviceT, _P], Awaitable[_R]],
163 ) -> Callable[Concatenate[_DenonDeviceT, _P], Coroutine[Any, Any, _R |
None]]:
164 """Log errors occurred when calling a Denon AVR receiver.
166 Decorates methods of DenonDevice class.
167 Declaration of staticmethod for this method is at the end of this class.
172 self: _DenonDeviceT, *args: _P.args, **kwargs: _P.kwargs
176 return await func(self, *args, **kwargs)
177 except AvrTimoutError:
182 "Timeout connecting to Denon AVR receiver at host %s. "
183 "Device is unavailable"
187 self._attr_available =
False
188 except AvrNetworkError:
193 "Network error connecting to Denon AVR receiver at host %s. "
194 "Device is unavailable"
198 self._attr_available =
False
199 except AvrProcessingError:
204 "Update of Denon AVR receiver at host %s not complete. "
205 "Device is still available"
209 except AvrForbiddenError:
214 "Denon AVR receiver at host %s responded with HTTP 403 error. "
215 "Device is unavailable. Please consider power cycling your "
220 self._attr_available =
False
221 except AvrCommandError
as err:
224 "Command %s failed with error: %s",
228 except DenonAvrError:
231 "Error occurred in method %s for Denon AVR receiver", func.__name__
234 if available
and not self.available:
236 "Denon AVR receiver at host %s is available again",
239 self._attr_available =
True
246 """Representation of a Denon Media Player Device."""
248 _attr_has_entity_name =
True
250 _attr_device_class = MediaPlayerDeviceClass.RECEIVER
256 config_entry: ConfigEntry,
257 update_audyssey: bool,
259 """Initialize the device."""
261 assert config_entry.unique_id
263 configuration_url=f
"http://{config_entry.data[CONF_HOST]}/",
264 hw_version=config_entry.data[CONF_TYPE],
265 identifiers={(DOMAIN, config_entry.unique_id)},
266 manufacturer=config_entry.data[CONF_MANUFACTURER],
267 model=config_entry.data[CONF_MODEL],
276 self.
_receiver_receiver.support_sound_mode
277 and MediaPlayerEntityFeature.SELECT_SOUND_MODE
281 """Process a telnet command callback."""
283 if zone
not in (self.
_receiver_receiver.zone, ALL_ZONES):
285 if event
not in TELNET_EVENTS:
289 if event ==
"NSE" and not parameter.startswith(
"4"):
291 if event ==
"TA" and not parameter.startswith(
"ANNAME"):
293 if event ==
"HD" and not parameter.startswith(
"ALBUM"):
298 """Register for telnet events."""
302 """Clean up the entity."""
303 if self.
_receiver_receiver.telnet_connected:
304 await self.
_receiver_receiver.async_telnet_disconnect()
309 """Get the latest status information from device."""
314 if receiver.telnet_connected
and receiver.telnet_healthy:
317 await receiver.async_update()
320 await receiver.async_update_audyssey()
323 def state(self) -> MediaPlayerState | None:
324 """Return the state of the device."""
325 return DENON_STATE_MAPPING.get(self.
_receiver_receiver.state)
329 """Return a list of available input sources."""
330 return self.
_receiver_receiver.input_func_list
334 """Return boolean if volume is currently muted."""
339 """Volume level of the media player (0..1)."""
342 if self.
_receiver_receiver.volume
is None:
348 """Return the current input source."""
349 return self.
_receiver_receiver.input_func
353 """Return the current matched sound mode."""
354 return self.
_receiver_receiver.sound_mode
358 """Flag media player features that are supported."""
365 """Content type of current playing media."""
366 if self.
_receiver_receiver.state
in {MediaPlayerState.PLAYING, MediaPlayerState.PAUSED}:
367 return MediaType.MUSIC
368 return MediaType.CHANNEL
372 """Image url of current playing media."""
379 """Title of current playing media."""
380 if self.
_receiver_receiver.input_func
not in self.
_receiver_receiver.playing_func_list:
381 return self.
_receiver_receiver.input_func
382 if self.
_receiver_receiver.title
is not None:
388 """Artist of current playing media, music track only."""
389 if self.
_receiver_receiver.artist
is not None:
395 """Album name of current playing media, music track only."""
396 if self.
_receiver_receiver.album
is not None:
402 """Return device specific state attributes."""
404 if receiver.power != POWER_ON:
406 state_attributes: dict[str, Any] = {}
408 sound_mode_raw := receiver.sound_mode_raw
409 )
is not None and receiver.support_sound_mode:
410 state_attributes[ATTR_SOUND_MODE_RAW] = sound_mode_raw
411 if (dynamic_eq := receiver.dynamic_eq)
is not None:
412 state_attributes[ATTR_DYNAMIC_EQ] = dynamic_eq
413 return state_attributes
417 """Status of DynamicEQ."""
418 return self.
_receiver_receiver.dynamic_eq
422 """Play or pause the media player."""
423 await self.
_receiver_receiver.async_toggle_play_pause()
427 """Send play command."""
428 await self.
_receiver_receiver.async_play()
432 """Send pause command."""
433 await self.
_receiver_receiver.async_pause()
437 """Send previous track command."""
438 await self.
_receiver_receiver.async_previous_track()
442 """Send next track command."""
443 await self.
_receiver_receiver.async_next_track()
447 """Select input source."""
448 await self.
_receiver_receiver.async_set_input_func(source)
452 """Select sound mode."""
453 await self.
_receiver_receiver.async_set_sound_mode(sound_mode)
457 """Turn on media player."""
458 await self.
_receiver_receiver.async_power_on()
462 """Turn off media player."""
463 await self.
_receiver_receiver.async_power_off()
467 """Volume up the media player."""
472 """Volume down media player."""
477 """Set volume level, range 0..1."""
480 volume_denon =
float((volume * 100) - 80)
481 if volume_denon > 18:
482 volume_denon =
float(18)
483 await self.
_receiver_receiver.async_set_volume(volume_denon)
487 """Send mute command."""
488 await self.
_receiver_receiver.async_mute(mute)
492 """Send generic command."""
497 """Get the latest audyssey information from device."""
502 """Turn DynamicEQ on or off."""
504 await self.
_receiver_receiver.async_dynamic_eq_on()
506 await self.
_receiver_receiver.async_dynamic_eq_off()
None async_write_ha_state(self)