From 9778000e9ac023b6301cf8d68052415ae2289114 Mon Sep 17 00:00:00 2001 From: Jay Love Date: Fri, 17 Mar 2017 02:32:52 -0400 Subject: [PATCH] Add new media_player platform: Volumio Media Player (#6556) * Add new media_player platform: Volumio Media Player Volumio media player is a rpi music player, this platfor adds http based control of the player. * Modify mute command to accept boolean * Adjust mute call to reset volume after unmute Remove references to volimi"a" * Use yield from calls in mute and volume calls Trying to speed up the indication of mute and volume level changes in UI, but doesn't seem to do much. * Adjust async_add_devices call --- .coveragerc | 1 + .../components/media_player/volumio.py | 237 ++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100755 homeassistant/components/media_player/volumio.py diff --git a/.coveragerc b/.coveragerc index be48f4750ea..3825faf24df 100644 --- a/.coveragerc +++ b/.coveragerc @@ -271,6 +271,7 @@ omit = homeassistant/components/media_player/sonos.py homeassistant/components/media_player/squeezebox.py homeassistant/components/media_player/vlc.py + homeassistant/components/media_player/volumio.py homeassistant/components/media_player/yamaha.py homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_sns.py diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py new file mode 100755 index 00000000000..46345f9c7f3 --- /dev/null +++ b/homeassistant/components/media_player/volumio.py @@ -0,0 +1,237 @@ +""" +Volumio Platform. + +The volumio platform allows you to control a Volumio media player +from Home Assistant. + + +To add a Volumio player to your installation, add the following to +your configuration.yaml file. + +# Example configuration.yaml entry +media_player: + - platform: volumio + name: 'Volumio Home Audio' + host: homeaudio.local + port: 3000 +Configuration variables: + +- **name** (*Optional*): Name of the device +- **host** (*Required*): IP address or hostname of the device +- **port** (*Required*): Port number of Volumio service +""" +import logging +import asyncio +import aiohttp +import voluptuous as vol + +from homeassistant.components.media_player import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, + SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, SUPPORT_STOP, + SUPPORT_PLAY, MediaPlayerDevice, + PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC) +from homeassistant.const import ( + STATE_PLAYING, STATE_PAUSED, STATE_IDLE, CONF_HOST, CONF_PORT, CONF_NAME) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession + + +_CONFIGURING = {} +_LOGGER = logging.getLogger(__name__) + +DEFAULT_HOST = 'localhost' +DEFAULT_NAME = 'Volumio' +DEFAULT_PORT = 3000 +TIMEOUT = 10 + +SUPPORT_VOLUMIO = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ + SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup the Volumio platform.""" + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + name = config.get(CONF_NAME) + async_add_devices([Volumio(name, host, port, hass)]) + + +class Volumio(MediaPlayerDevice): + """Volumio Player Object.""" + + def __init__(self, name, host, port, hass): + """Initialize the media player.""" + self.host = host + self.port = port + self.hass = hass + self._url = host + ":" + str(port) + self._name = name + self._state = {} + self.async_update() + self._lastvol = self._state.get('volume', 0) + + @asyncio.coroutine + def send_volumio_msg(self, method, params=None): + """Send message.""" + url = "http://{}:{}/api/v1/{}/".format( + self.host, self.port, method) + response = None + + _LOGGER.debug("URL: %s params: %s", url, params) + + try: + websession = async_get_clientsession(self.hass) + response = yield from websession.get(url, params=params) + if response.status == 200: + data = yield from response.json() + else: + _LOGGER.error( + "Query failed, response code: %s Full message: %s", + response.status, response) + return False + + except (asyncio.TimeoutError, + aiohttp.errors.ClientError, + aiohttp.errors.ClientDisconnectedError) as error: + _LOGGER.error("Failed communicating with Volumio: %s", type(error)) + return False + finally: + if response is not None: + yield from response.release() + + try: + return data + except AttributeError: + _LOGGER.error("Received invalid response: %s", data) + return False + + @asyncio.coroutine + def async_update(self): + """Update state.""" + resp = yield from self.send_volumio_msg('getState') + if resp is False: + return + self._state = resp.copy() + + @property + def media_content_type(self): + """Content type of current playing media.""" + return MEDIA_TYPE_MUSIC + + @property + def state(self): + """Return the state of the device.""" + status = self._state.get('status', None) + if status == 'pause': + return STATE_PAUSED + elif status == 'play': + return STATE_PLAYING + else: + return STATE_IDLE + + @property + def media_title(self): + """Title of current playing media.""" + return self._state.get('title', None) + + @property + def media_artist(self): + """Artist of current playing media (Music track only).""" + return self._state.get('artist', None) + + @property + def media_album_name(self): + """Artist of current playing media (Music track only).""" + return self._state.get('album', None) + + @property + def media_image_url(self): + """Image url of current playing media.""" + url = self._state.get('albumart', None) + if url is None: + return + if str(url[0:2]).lower() == 'ht': + mediaurl = url + else: + mediaurl = "http://" + self.host + ":" + str(self.port) + url + return mediaurl + + @property + def media_seek_position(self): + """Time in seconds of current seek position.""" + return self._state.get('seek', None) + + @property + def media_duration(self): + """Time in seconds of current song duration.""" + return self._state.get('duration', None) + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + volume = self._state.get('volume', None) + if volume is not None: + volume = volume / 100 + return volume + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._state.get('mute', None) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def supported_features(self): + """Flag of media commands that are supported.""" + return SUPPORT_VOLUMIO + + def async_media_next_track(self): + """Send media_next command to media player.""" + return self.send_volumio_msg('commands', params={'cmd': 'next'}) + + def async_media_previous_track(self): + """Send media_previous command to media player.""" + return self.send_volumio_msg('commands', params={'cmd': 'prev'}) + + def async_media_play(self): + """Send media_play command to media player.""" + return self.send_volumio_msg('commands', params={'cmd': 'play'}) + + def async_media_pause(self): + """Send media_pause command to media player.""" + return self.send_volumio_msg('commands', params={'cmd': 'pause'}) + + def async_set_volume_level(self, volume): + """Send volume_up command to media player.""" + return self.send_volumio_msg('commands', + params={'cmd': 'volume', + 'volume': int(volume * 100)}) + + def async_mute_volume(self, mute): + """Send mute command to media player.""" + mutecmd = 'mute' if mute else 'unmute' + if mute: + # mute is implemenhted as 0 volume, do save last volume level + self._lastvol = self._state['volume'] + return self.send_volumio_msg('commands', + params={'cmd': 'volume', + 'volume': mutecmd}) + else: + return self.send_volumio_msg('commands', + params={'cmd': 'volume', + 'volume': self._lastvol})