224 lines
6.8 KiB
Python
224 lines
6.8 KiB
Python
"""Support for Russound multizone controllers using RIO Protocol."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
import logging
|
|
from typing import TYPE_CHECKING
|
|
|
|
from aiorussound import Controller
|
|
from aiorussound.const import FeatureFlag
|
|
from aiorussound.models import PlayStatus, Source
|
|
from aiorussound.rio import ZoneControlSurface
|
|
from aiorussound.util import is_feature_supported
|
|
|
|
from homeassistant.components.media_player import (
|
|
MediaPlayerDeviceClass,
|
|
MediaPlayerEntity,
|
|
MediaPlayerEntityFeature,
|
|
MediaPlayerState,
|
|
MediaType,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
|
|
from . import RussoundConfigEntry
|
|
from .entity import RussoundBaseEntity, command
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
PARALLEL_UPDATES = 0
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: RussoundConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up the Russound RIO platform."""
|
|
client = entry.runtime_data
|
|
sources = client.sources
|
|
|
|
async_add_entities(
|
|
RussoundZoneDevice(controller, zone_id, sources)
|
|
for controller in client.controllers.values()
|
|
for zone_id in controller.zones
|
|
)
|
|
|
|
|
|
class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
|
|
"""Representation of a Russound Zone."""
|
|
|
|
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
|
_attr_media_content_type = MediaType.MUSIC
|
|
_attr_supported_features = (
|
|
MediaPlayerEntityFeature.VOLUME_SET
|
|
| MediaPlayerEntityFeature.VOLUME_STEP
|
|
| MediaPlayerEntityFeature.VOLUME_MUTE
|
|
| MediaPlayerEntityFeature.TURN_ON
|
|
| MediaPlayerEntityFeature.TURN_OFF
|
|
| MediaPlayerEntityFeature.SELECT_SOURCE
|
|
| MediaPlayerEntityFeature.SEEK
|
|
)
|
|
|
|
def __init__(
|
|
self, controller: Controller, zone_id: int, sources: dict[int, Source]
|
|
) -> None:
|
|
"""Initialize the zone device."""
|
|
super().__init__(controller)
|
|
self._zone_id = zone_id
|
|
_zone = self._zone
|
|
self._sources = sources
|
|
self._attr_name = _zone.name
|
|
self._attr_unique_id = f"{self._primary_mac_address}-{_zone.device_str}"
|
|
|
|
@property
|
|
def _zone(self) -> ZoneControlSurface:
|
|
return self._controller.zones[self._zone_id]
|
|
|
|
@property
|
|
def _source(self) -> Source:
|
|
return self._zone.fetch_current_source()
|
|
|
|
@property
|
|
def state(self) -> MediaPlayerState | None:
|
|
"""Return the state of the device."""
|
|
status = self._zone.status
|
|
play_status = self._source.play_status
|
|
if not status:
|
|
return MediaPlayerState.OFF
|
|
if play_status == PlayStatus.PLAYING:
|
|
return MediaPlayerState.PLAYING
|
|
if play_status == PlayStatus.PAUSED:
|
|
return MediaPlayerState.PAUSED
|
|
if play_status == PlayStatus.TRANSITIONING:
|
|
return MediaPlayerState.BUFFERING
|
|
if play_status == PlayStatus.STOPPED:
|
|
return MediaPlayerState.IDLE
|
|
return MediaPlayerState.ON
|
|
|
|
@property
|
|
def source(self) -> str:
|
|
"""Get the currently selected source."""
|
|
return self._source.name
|
|
|
|
@property
|
|
def source_list(self) -> list[str]:
|
|
"""Return a list of available input sources."""
|
|
if TYPE_CHECKING:
|
|
assert self._client.rio_version
|
|
available_sources = (
|
|
[
|
|
source
|
|
for source_id, source in self._sources.items()
|
|
if source_id in self._zone.enabled_sources
|
|
]
|
|
if is_feature_supported(
|
|
self._client.rio_version, FeatureFlag.SUPPORT_ZONE_SOURCE_EXCLUSION
|
|
)
|
|
else self._sources.values()
|
|
)
|
|
return [x.name for x in available_sources]
|
|
|
|
@property
|
|
def media_title(self) -> str | None:
|
|
"""Title of current playing media."""
|
|
return self._source.song_name
|
|
|
|
@property
|
|
def media_artist(self) -> str | None:
|
|
"""Artist of current playing media, music track only."""
|
|
return self._source.artist_name
|
|
|
|
@property
|
|
def media_album_name(self) -> str | None:
|
|
"""Album name of current playing media, music track only."""
|
|
return self._source.album_name
|
|
|
|
@property
|
|
def media_image_url(self) -> str | None:
|
|
"""Image url of current playing media."""
|
|
return self._source.cover_art_url
|
|
|
|
@property
|
|
def media_duration(self) -> int | None:
|
|
"""Duration of the current media."""
|
|
return self._source.track_time
|
|
|
|
@property
|
|
def media_position(self) -> int | None:
|
|
"""Position of the current media."""
|
|
return self._source.play_time
|
|
|
|
@property
|
|
def media_position_updated_at(self) -> dt.datetime:
|
|
"""Last time the media position was updated."""
|
|
return self._source.position_last_updated
|
|
|
|
@property
|
|
def volume_level(self) -> float:
|
|
"""Volume level of the media player (0..1).
|
|
|
|
Value is returned based on a range (0..50).
|
|
Therefore float divide by 50 to get to the required range.
|
|
"""
|
|
return self._zone.volume / 50.0
|
|
|
|
@property
|
|
def is_volume_muted(self) -> bool:
|
|
"""Return whether zone is muted."""
|
|
return self._zone.is_mute
|
|
|
|
@command
|
|
async def async_turn_off(self) -> None:
|
|
"""Turn off the zone."""
|
|
await self._zone.zone_off()
|
|
|
|
@command
|
|
async def async_turn_on(self) -> None:
|
|
"""Turn on the zone."""
|
|
await self._zone.zone_on()
|
|
|
|
@command
|
|
async def async_set_volume_level(self, volume: float) -> None:
|
|
"""Set the volume level."""
|
|
rvol = int(volume * 50.0)
|
|
await self._zone.set_volume(str(rvol))
|
|
|
|
@command
|
|
async def async_select_source(self, source: str) -> None:
|
|
"""Select the source input for this zone."""
|
|
for source_id, src in self._sources.items():
|
|
if src.name.lower() != source.lower():
|
|
continue
|
|
await self._zone.select_source(source_id)
|
|
break
|
|
|
|
@command
|
|
async def async_volume_up(self) -> None:
|
|
"""Step the volume up."""
|
|
await self._zone.volume_up()
|
|
|
|
@command
|
|
async def async_volume_down(self) -> None:
|
|
"""Step the volume down."""
|
|
await self._zone.volume_down()
|
|
|
|
@command
|
|
async def async_mute_volume(self, mute: bool) -> None:
|
|
"""Mute the media player."""
|
|
if FeatureFlag.COMMANDS_ZONE_MUTE_OFF_ON in self._client.supported_features:
|
|
if mute:
|
|
await self._zone.mute()
|
|
else:
|
|
await self._zone.unmute()
|
|
return
|
|
|
|
if mute != self.is_volume_muted:
|
|
await self._zone.toggle_mute()
|
|
|
|
@command
|
|
async def async_media_seek(self, position: float) -> None:
|
|
"""Seek to a position in the current media."""
|
|
await self._zone.set_seek_time(int(position))
|