diff --git a/.coveragerc b/.coveragerc index c4051af5136..074e35a073d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -351,6 +351,7 @@ omit = homeassistant/components/media_player/pioneer.py homeassistant/components/media_player/plex.py homeassistant/components/media_player/roku.py + homeassistant/components/media_player/russound_rio.py homeassistant/components/media_player/russound_rnet.py homeassistant/components/media_player/samsungtv.py homeassistant/components/media_player/snapcast.py diff --git a/homeassistant/components/media_player/russound_rio.py b/homeassistant/components/media_player/russound_rio.py new file mode 100644 index 00000000000..743fc4e262d --- /dev/null +++ b/homeassistant/components/media_player/russound_rio.py @@ -0,0 +1,213 @@ +""" +Support for Russound multizone controllers using RIO Protocol. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.russound_rio/ +""" + +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.components.media_player import ( + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, + MEDIA_TYPE_MUSIC) +from homeassistant.const import ( + CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON, + CONF_NAME, EVENT_HOMEASSISTANT_STOP) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['russound_rio==0.1.3'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_RUSSOUND = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_PORT, default=9621): cv.port, + }) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the Russound RIO platform.""" + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + + from russound_rio import Russound + + russ = Russound(hass.loop, host, port) + + yield from russ.connect() + + # Discover sources + sources = yield from russ.enumerate_sources() + + # Discover zones + valid_zones = yield from russ.enumerate_zones() + + devices = [] + for zone_id, name in valid_zones: + yield from russ.watch_zone(zone_id) + dev = RussoundZoneDevice(russ, zone_id, name, sources) + devices.append(dev) + + @callback + def on_stop(event): + """Shutdown cleanly when hass stops.""" + hass.loop.create_task(russ.close()) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_stop) + + async_add_devices(devices) + + +class RussoundZoneDevice(MediaPlayerDevice): + """Representation of a Russound Zone.""" + + def __init__(self, russ, zone_id, name, sources): + """Initialize the zone device.""" + super().__init__() + self._name = name + self._russ = russ + self._zone_id = zone_id + self._sources = sources + + def _zone_var(self, name, default=None): + return self._russ.get_cached_zone_variable(self._zone_id, + name, + default) + + def _source_var(self, name, default=None): + current = int(self._zone_var('currentsource', 0)) + if current: + return self._russ.get_cached_source_variable( + current, name, default) + return default + + def _source_na_var(self, name): + """Will replace invalid values with None.""" + current = int(self._zone_var('currentsource', 0)) + if current: + value = self._russ.get_cached_source_variable( + current, name, None) + if value in (None, "", "------"): + return None + return value + else: + return None + + def _zone_callback_handler(self, zone_id, *args): + if zone_id == self._zone_id: + self.schedule_update_ha_state() + + def _source_callback_handler(self, source_id, *args): + current = int(self._zone_var('currentsource', 0)) + if source_id == current: + self.schedule_update_ha_state() + + @asyncio.coroutine + def async_added_to_hass(self): + """Register callback handlers.""" + self._russ.add_zone_callback(self._zone_callback_handler) + self._russ.add_source_callback(self._source_callback_handler) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the zone.""" + return self._zone_var('name', self._name) + + @property + def state(self): + """Return the state of the device.""" + status = self._zone_var('status', "OFF") + if status == 'ON': + return STATE_ON + elif status == 'OFF': + return STATE_OFF + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_RUSSOUND + + @property + def source(self): + """Get the currently selected source.""" + return self._source_na_var('name') + + @property + def source_list(self): + """Return a list of available input sources.""" + return [x[1] for x in self._sources] + + @property + def media_content_type(self): + """Content type of current playing media.""" + return MEDIA_TYPE_MUSIC + + @property + def media_title(self): + """Title of current playing media.""" + return self._source_na_var('songname') + + @property + def media_artist(self): + """Artist of current playing media, music track only.""" + return self._source_na_var('artistname') + + @property + def media_album_name(self): + """Album name of current playing media, music track only.""" + return self._source_na_var('albumname') + + @property + def media_image_url(self): + """Image url of current playing media.""" + return self._source_na_var('coverarturl') + + @property + def volume_level(self): + """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 float(self._zone_var('volume', 0)) / 50.0 + + def async_turn_off(self): + """Turn off the zone.""" + return self._russ.send_zone_event(self._zone_id, + "ZoneOff") + + def async_turn_on(self): + """Turn on the zone.""" + return self._russ.send_zone_event(self._zone_id, + "ZoneOn") + + def async_set_volume_level(self, volume): + """Set the volume level.""" + rvol = int(volume * 50.0) + return self._russ.send_zone_event(self._zone_id, + "KeyPress", + "Volume", + rvol) + + def async_select_source(self, source): + """Select the source input for this zone.""" + for source_id, name in self._sources: + if name.lower() != source.lower(): + continue + return self._russ.send_zone_event( + self._zone_id, "SelectSource", source_id) diff --git a/requirements_all.txt b/requirements_all.txt index 3c1f82ba2b8..e11739d0e87 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -830,6 +830,9 @@ ring_doorbell==0.1.4 # homeassistant.components.media_player.russound_rnet russound==0.1.7 +# homeassistant.components.media_player.russound_rio +russound_rio==0.1.3 + # homeassistant.components.media_player.yamaha rxv==0.4.0