From 1c1d18053b19dd4749569a6759c179bccf2008e1 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 25 Jun 2016 03:06:36 -0400 Subject: [PATCH] Add cmus media device (#2321) This commit adds support for the cmus console music player as a media device. --- .coveragerc | 1 + homeassistant/components/media_player/cmus.py | 214 ++++++++++++++++++ requirements_all.txt | 3 + tests/components/media_player/test_cmus.py | 31 +++ 4 files changed, 249 insertions(+) create mode 100644 homeassistant/components/media_player/cmus.py create mode 100644 tests/components/media_player/test_cmus.py diff --git a/.coveragerc b/.coveragerc index 6a9d1fc11fa..6526b2b1e3d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -127,6 +127,7 @@ omit = homeassistant/components/lirc.py homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/cast.py + homeassistant/components/media_player/cmus.py homeassistant/components/media_player/denon.py homeassistant/components/media_player/firetv.py homeassistant/components/media_player/gpmdp.py diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py new file mode 100644 index 00000000000..43ddee3ba02 --- /dev/null +++ b/homeassistant/components/media_player/cmus.py @@ -0,0 +1,214 @@ +""" +Support for interacting with and controlling the cmus music player. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.mpd/ +""" + +import logging + +from homeassistant.components.media_player import ( + MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, SUPPORT_SEEK, + MediaPlayerDevice) +from homeassistant.const import (STATE_OFF, STATE_PAUSED, STATE_PLAYING, + CONF_HOST, CONF_NAME, CONF_PASSWORD, + CONF_PORT) + +_LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['pycmus>=0.1.0'] + +SUPPORT_CMUS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \ + SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ + SUPPORT_PLAY_MEDIA | SUPPORT_SEEK + + +def setup_platform(hass, config, add_devices, discover_info=None): + """Setup the Cmus platform.""" + from pycmus import exceptions + + host = config.get(CONF_HOST, None) + password = config.get(CONF_PASSWORD, None) + port = config.get(CONF_PORT, None) + name = config.get(CONF_NAME, None) + if host and not password: + _LOGGER.error("A password must be set if using a remote cmus server") + return False + try: + cmus_remote = CmusDevice(host, password, port, name) + except exceptions.InvalidPassword: + _LOGGER.error("The provided password was rejected by cmus") + return False + add_devices([cmus_remote]) + + +class CmusDevice(MediaPlayerDevice): + """Representation of a running cmus.""" + + # pylint: disable=no-member, too-many-public-methods, abstract-method + def __init__(self, server, password, port, name): + """Initialize the CMUS device.""" + from pycmus import remote + + if server: + port = port or 3000 + self.cmus = remote.PyCmus(server=server, password=password, + port=port) + auto_name = "cmus-%s" % server + else: + self.cmus = remote.PyCmus() + auto_name = "cmus-local" + self._name = name or auto_name + self.status = {} + self.update() + + def update(self): + """Get the latest data and update the state.""" + status = self.cmus.get_status_dict() + if not status: + _LOGGER.warning("Recieved no status from cmus") + else: + self.status = status + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the media state.""" + if 'status' not in self.status: + self.update() + if self.status['status'] == 'playing': + return STATE_PLAYING + elif self.status['status'] == 'paused': + return STATE_PAUSED + else: + return STATE_OFF + + @property + def media_content_id(self): + """Content ID of current playing media.""" + return self.status.get('file') + + @property + def content_type(self): + """Content type of the current playing media.""" + return MEDIA_TYPE_MUSIC + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + return self.status.get('duration') + + @property + def media_title(self): + """Title of current playing media.""" + return self.status['tag'].get('title') + + @property + def media_artist(self): + """Artist of current playing media, music track only.""" + return self.status['tag'].get('artist') + + @property + def media_track(self): + """Track number of current playing media, music track only.""" + return self.status['tag'].get('tracknumber') + + @property + def media_album_name(self): + """Album name of current playing media, music track only.""" + return self.status['tag'].get('album') + + @property + def media_album_artist(self): + """Album artist of current playing media, music track only.""" + return self.status['tag'].get('albumartist') + + @property + def volume_level(self): + """Return the volume level.""" + left = self.status['set'].get('vol_left')[0] + right = self.status['set'].get('vol_right')[0] + if left != right: + volume = float(left + right) / 2 + else: + volume = left + return int(volume)/100 + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_CMUS + + def turn_off(self): + """Service to send the CMUS the command to stop playing.""" + self.cmus.player_stop() + + def turn_on(self): + """Service to send the CMUS the command to start playing.""" + self.cmus.player_play() + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self.cmus.set_volume(int(volume * 100)) + + def volume_up(self): + """Function to send CMUS the command for volume up.""" + left = self.status['set'].get('vol_left') + right = self.status['set'].get('vol_right') + if left != right: + current_volume = float(left + right) / 2 + else: + current_volume = left + + if current_volume <= 100: + self.cmus.set_volume(int(current_volume) + 5) + + def volume_down(self): + """Function to send CMUS the command for volume down.""" + left = self.status['set'].get('vol_left') + right = self.status['set'].get('vol_right') + if left != right: + current_volume = float(left + right) / 2 + else: + current_volume = left + + if current_volume <= 100: + self.cmus.set_volume(int(current_volume) - 5) + + def play_media(self, media_type, media_id, **kwargs): + """Send the play command.""" + if media_type in [MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST]: + self.cmus.player_play_file(media_id) + else: + _LOGGER.error( + "Invalid media type %s. Only %s and %s are supported", + media_type, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST) + + def media_pause(self): + """Send the pause command.""" + self.cmus.player_pause() + + def media_next_track(self): + """Send next track command.""" + self.cmus.player_next() + + def media_previous_track(self): + """Send next track command.""" + self.cmus.player_prev() + + def media_seek(self, position): + """Send seek command.""" + self.cmus.seek(position) + + def media_play(self): + """Send the play command.""" + self.cmus.player_play() + + def media_stop(self): + """Send the stop command.""" + self.cmus.stop() diff --git a/requirements_all.txt b/requirements_all.txt index a5ef32201c5..7360074d8ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,6 +247,9 @@ pyasn1==0.1.9 # homeassistant.components.media_player.cast pychromecast==0.7.2 +# homeassistant.components.media_player.cmus +pycmus>=0.1.0 + # homeassistant.components.envisalink # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/media_player/test_cmus.py b/tests/components/media_player/test_cmus.py new file mode 100644 index 00000000000..24322b5bce0 --- /dev/null +++ b/tests/components/media_player/test_cmus.py @@ -0,0 +1,31 @@ +"""The tests for the Demo Media player platform.""" +import unittest +from unittest import mock + +from homeassistant.components.media_player import cmus +from homeassistant import const + +from tests.common import get_test_home_assistant + +entity_id = 'media_player.cmus' + + +class TestCmusMediaPlayer(unittest.TestCase): + """Test the media_player module.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + @mock.patch('homeassistant.components.media_player.cmus.CmusDevice') + def test_password_required_with_host(self, cmus_mock): + """Test that a password is required when specifying a remote host.""" + fake_config = { + const.CONF_HOST: 'a_real_hostname', + } + self.assertFalse( + cmus.setup_platform(self.hass, fake_config, mock.MagicMock()))