""" Support for foobar2000 Music Player as media player via pyfoobar2k https://gitlab.com/ed0zer-projects/pyfoobar2k And foobar2000 component foo_httpcontrol by oblikoamorale https://bitbucket.org/oblikoamorale/foo_httpcontrol """ import logging from datetime import timedelta import voluptuous as vol from homeassistant.helpers import script, config_validation as cv import homeassistant.util.dt as dt_util try: from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity except ImportError: from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice as MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_STEP, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_STOP, SUPPORT_SELECT_SOURCE, SUPPORT_SEEK, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_SHUFFLE_SET) from homeassistant.const import ( CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_TIMEOUT, STATE_UNKNOWN, STATE_IDLE) REQUIREMENTS = ['pyfoobar2k==0.2.8'] SCAN_INTERVAL = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Foobar2000' DEFAULT_PORT = '8888' DEFAULT_TIMEOUT = 3 DEFAULT_VOLUME_STEP = 5 CONF_VOLUME_STEP = 'volume_step' CONF_TURN_ON_ACTION = 'turn_on_action' CONF_TURN_OFF_ACTION = 'turn_off_action' SUPPORT_FOOBAR_PLAYER = \ SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PLAY | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_STOP | SUPPORT_SEEK | SUPPORT_SHUFFLE_SET | \ SUPPORT_VOLUME_STEP | SUPPORT_SELECT_SOURCE PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP): cv.positive_int, vol.Optional(CONF_TURN_ON_ACTION, default=None): cv.SCRIPT_SCHEMA, vol.Optional(CONF_TURN_OFF_ACTION, default=None): cv.SCRIPT_SCHEMA}) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Foobar Player platform.""" from pyfoobar2k import FoobarRemote name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) timeout = config.get(CONF_TIMEOUT) volume_step = config.get(CONF_VOLUME_STEP) turn_on_action = config.get(CONF_TURN_ON_ACTION) turn_off_action = config.get(CONF_TURN_OFF_ACTION) remote = FoobarRemote(host, port, username, password, timeout) add_devices([FoobarDevice(hass, remote, name, volume_step, turn_on_action, turn_off_action)]) class FoobarDevice(MediaPlayerEntity): def __init__(self, hass, remote, name, volume_step, turn_on_action=None, turn_off_action=None): self._name = name self._remote = remote self.hass = hass self._volume = 0.0 self._track_name = '' self._track_artist = '' self._track_album_name = '' self._track_duration = 0 self._track_position = 0 self._track_position_updated_at = None self._albumart = '' self._current_playlist = '' self._playlists = [] self._shuffle = 0 self._volume_step = volume_step self._selected_source = None self._state = STATE_UNKNOWN self._base_url = self._remote.url self._albumart_path = '' # Script creation for the turn on/off config options if turn_on_action is not None: turn_on_action = script.Script( self.hass, turn_on_action, "{} turn ON script".format(self.name), self.async_update_ha_state(True)) if turn_off_action is not None: turn_off_action = script.Script( self.hass, turn_off_action, "{} turn OFF script".format(self.name), self.async_update_ha_state(True)) self._turn_on_action = turn_on_action self._turn_off_action = turn_off_action def update(self): try: info = self._remote.state() if info: if info['isPlaying'] == '1': self._state = STATE_PLAYING elif info['isPaused'] == '1': self._state = STATE_PAUSED else: self._state = STATE_IDLE else: self._state = STATE_OFF self.schedule_update_ha_state() if self._state in [STATE_PLAYING, STATE_PAUSED]: self._track_name = info['title'] self._track_artist = info['artist'] self._track_album_name = info['album'] self._volume = int(info['volume']) / 100 self._shuffle = info['playbackorder'] self._track_duration = int(info['itemPlayingLen']) self._albumart_path = info['albumArt'] self._track_position = int(info['itemPlayingPos']) self._track_position_updated_at = dt_util.utcnow() if self._state in [STATE_PLAYING, STATE_PAUSED, STATE_IDLE]: sources_info = self._remote.playlist() if sources_info: current_playlist_position = int(sources_info['playlistActive']) playlists_raw = sources_info['playlists'] self._current_playlist = playlists_raw[current_playlist_position]['name'] self._playlists = [item["name"] for item in playlists_raw] else: _LOGGER.warning("Updating %s sources failed:", self._name) except Exception as e: _LOGGER.error("Updating %s state failed: %s", self._name, e) self._state = STATE_UNKNOWN @property def name(self): """Return the name of the device.""" return self._name @property def state(self): """Return the state of the device.""" return self._state @property def should_poll(self): """Return True if entity has to be polled for state.""" return True @property def volume_level(self): """Volume level of the media player (0 to 1).""" return float(self._volume) @property def is_volume_muted(self): """Return True if volume is muted.""" return float(self._volume) == 0 @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 track.""" return self._track_name @property def media_artist(self): """Artist of current playing track.""" return self._track_artist @property def media_album_name(self): """Album name of current playing track.""" return self._track_album_name @property def supported_features(self): """Flag media player features that are supported.""" supported_features = SUPPORT_FOOBAR_PLAYER if self._turn_on_action is not None: supported_features |= SUPPORT_TURN_ON if self._turn_off_action is not None: supported_features |= SUPPORT_TURN_OFF return supported_features def turn_on(self): """Execute turn_on_action to turn on media player.""" if self._turn_on_action is not None: self._turn_on_action.run(variables={"entity_id": self.entity_id}) else: _LOGGER.warning("turn_on requested but turn_on_action is none") def turn_off(self): """Execute turn_off_action to turn on media player.""" if self._turn_off_action is not None: self._turn_off_action.run(variables={"entity_id": self.entity_id}) else: _LOGGER.warning("turn_off requested but turn_off_action is none") def media_play_pause(self): """Send the media player the command for play/pause.""" self._remote.cmd('PlayOrPause') def media_pause(self): """Send the media player the command for play/pause if playing.""" if self._state == STATE_PLAYING: self._remote.cmd('PlayOrPause') def media_stop(self): """Send the media player the stop command.""" self._remote.cmd('Stop') def media_play(self): """Send the media player the command to play at the current playlist.""" self._remote.cmd('Start') def media_previous_track(self): """Send the media player the command for prev track.""" self._remote.cmd('StartPrevious') def media_next_track(self): """Send the media player the command for next track.""" self._remote.cmd('StartNext') def set_volume_level(self, volume): """Send the media player the command for setting the volume.""" self._remote.cmd('Volume', int(volume * 100)) def volume_up(self): """Send the media player the command for volume down.""" self._remote.cmd('VolumeDelta', self._volume_step) def volume_down(self): """Send the media player the command for volume down.""" self._remote.cmd('VolumeDelta', -self._volume_step) def mute_volume(self, mute): """Mute the volume.""" self._remote.cmd('VolumeMuteToggle') @property def media_position_updated_at(self): """When was the position of the current playing media valid. Returns value from homeassistant.util.dt.utcnow(). """ return self._track_position_updated_at @property def media_duration(self): """Duration of current playing media in seconds.""" return self._track_duration @property def media_position(self): """Position of current playing media in seconds.""" if self._state in [STATE_PLAYING, STATE_PAUSED]: return self._track_position def media_seek(self, position): """Send the media player the command to seek in current playing media.""" self._remote.cmd('SeekSecond', position) @property def media_image_url(self): """Image url of current playing media.""" if 'cover_not_available' not in self._albumart_path: self._albumart = '{}/{}'.format(self._base_url, self._albumart_path) return self._albumart @property def source(self): """Return current source name.""" return self._current_playlist @property def source_list(self): """List of available input sources.""" return self._playlists def select_source(self, source): """Select input source.""" playlists = self._remote.playlist()['playlists'] source_position = [index for index, item in enumerate(playlists) if item['name'] == source][0] """ignoring first playlist in playlists index""" if source_position == 0: return None if source_position is not None: self._remote.cmd('SwitchPlaylist', source_position) self._remote.cmd('Start', 0) self._current_playlist = source @property def media_playlist(self): """Title of Playlist currently playing.""" return self._current_playlist def set_shuffle(self, shuffle): """Send the media player the command to enable/disable shuffle mode.""" playback_order = 4 if shuffle else 0 self._remote.cmd('PlaybackOrder', playback_order) @property def shuffle(self): """Boolean if shuffle is enabled.""" return True if int(self._shuffle) == 4 else False