mirror of https://github.com/home-assistant/core
155 lines
5.5 KiB
Python
155 lines
5.5 KiB
Python
"""Support for interfacing with Russound via RNET Protocol."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import math
|
|
|
|
from russound import russound
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.media_player import (
|
|
PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA,
|
|
MediaPlayerEntity,
|
|
MediaPlayerEntityFeature,
|
|
MediaPlayerState,
|
|
)
|
|
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
|
|
from homeassistant.core import HomeAssistant
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CONF_ZONES = "zones"
|
|
CONF_SOURCES = "sources"
|
|
|
|
|
|
ZONE_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string})
|
|
|
|
SOURCE_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string})
|
|
|
|
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
|
|
{
|
|
vol.Required(CONF_HOST): cv.string,
|
|
vol.Required(CONF_NAME): cv.string,
|
|
vol.Required(CONF_PORT): cv.port,
|
|
vol.Required(CONF_ZONES): vol.Schema({cv.positive_int: ZONE_SCHEMA}),
|
|
vol.Required(CONF_SOURCES): vol.All(cv.ensure_list, [SOURCE_SCHEMA]),
|
|
}
|
|
)
|
|
|
|
|
|
def setup_platform(
|
|
hass: HomeAssistant,
|
|
config: ConfigType,
|
|
add_entities: AddEntitiesCallback,
|
|
discovery_info: DiscoveryInfoType | None = None,
|
|
) -> None:
|
|
"""Set up the Russound RNET platform."""
|
|
host = config.get(CONF_HOST)
|
|
port = config.get(CONF_PORT)
|
|
|
|
if host is None or port is None:
|
|
_LOGGER.error("Invalid config. Expected %s and %s", CONF_HOST, CONF_PORT)
|
|
return
|
|
|
|
russ = russound.Russound(host, port)
|
|
russ.connect()
|
|
|
|
sources = [source["name"] for source in config[CONF_SOURCES]]
|
|
|
|
if russ.is_connected():
|
|
for zone_id, extra in config[CONF_ZONES].items():
|
|
add_entities(
|
|
[RussoundRNETDevice(hass, russ, sources, zone_id, extra)], True
|
|
)
|
|
else:
|
|
_LOGGER.error("Not connected to %s:%s", host, port)
|
|
|
|
|
|
class RussoundRNETDevice(MediaPlayerEntity):
|
|
"""Representation of a Russound RNET device."""
|
|
|
|
_attr_supported_features = (
|
|
MediaPlayerEntityFeature.VOLUME_MUTE
|
|
| MediaPlayerEntityFeature.VOLUME_SET
|
|
| MediaPlayerEntityFeature.TURN_ON
|
|
| MediaPlayerEntityFeature.TURN_OFF
|
|
| MediaPlayerEntityFeature.SELECT_SOURCE
|
|
)
|
|
|
|
def __init__(self, hass, russ, sources, zone_id, extra):
|
|
"""Initialise the Russound RNET device."""
|
|
self._attr_name = extra["name"]
|
|
self._russ = russ
|
|
self._attr_source_list = sources
|
|
# Each controller has a maximum of 6 zones, every increment of 6 zones
|
|
# maps to an additional controller for easier backward compatibility
|
|
self._controller_id = str(math.ceil(zone_id / 6))
|
|
# Each zone resets to 1-6 per controller
|
|
self._zone_id = (zone_id - 1) % 6 + 1
|
|
|
|
def update(self) -> None:
|
|
"""Retrieve latest state."""
|
|
# Updated this function to make a single call to get_zone_info, so that
|
|
# with a single call we can get On/Off, Volume and Source, reducing the
|
|
# amount of traffic and speeding up the update process.
|
|
try:
|
|
ret = self._russ.get_zone_info(self._controller_id, self._zone_id, 4)
|
|
except BrokenPipeError:
|
|
_LOGGER.error("Broken Pipe Error, trying to reconnect to Russound RNET")
|
|
self._russ.connect()
|
|
ret = self._russ.get_zone_info(self._controller_id, self._zone_id, 4)
|
|
|
|
_LOGGER.debug("ret= %s", ret)
|
|
if ret is not None:
|
|
_LOGGER.debug(
|
|
"Updating status for RNET zone %s on controller %s",
|
|
self._zone_id,
|
|
self._controller_id,
|
|
)
|
|
if ret[0] == 0:
|
|
self._attr_state = MediaPlayerState.OFF
|
|
else:
|
|
self._attr_state = MediaPlayerState.ON
|
|
self._attr_volume_level = ret[2] * 2 / 100.0
|
|
# Returns 0 based index for source.
|
|
index = ret[1]
|
|
# Possibility exists that user has defined list of all sources.
|
|
# If a source is set externally that is beyond the defined list then
|
|
# an exception will be thrown.
|
|
# In this case return and unknown source (None)
|
|
if self.source_list and 0 <= index < len(self.source_list):
|
|
self._attr_source = self.source_list[index]
|
|
else:
|
|
_LOGGER.error("Could not update status for zone %s", self._zone_id)
|
|
|
|
def set_volume_level(self, volume: float) -> None:
|
|
"""Set volume level. Volume has a range (0..1).
|
|
|
|
Translate this to a range of (0..100) as expected
|
|
by _russ.set_volume()
|
|
"""
|
|
self._russ.set_volume(self._controller_id, self._zone_id, volume * 100)
|
|
|
|
def turn_on(self) -> None:
|
|
"""Turn the media player on."""
|
|
self._russ.set_power(self._controller_id, self._zone_id, "1")
|
|
|
|
def turn_off(self) -> None:
|
|
"""Turn off media player."""
|
|
self._russ.set_power(self._controller_id, self._zone_id, "0")
|
|
|
|
def mute_volume(self, mute: bool) -> None:
|
|
"""Send mute command."""
|
|
self._russ.toggle_mute(self._controller_id, self._zone_id)
|
|
|
|
def select_source(self, source: str) -> None:
|
|
"""Set the input source."""
|
|
if self.source_list and source in self.source_list:
|
|
index = self.source_list.index(source)
|
|
# 0 based value for source
|
|
self._russ.set_source(self._controller_id, self._zone_id, index)
|