""" Support for Google Play Music Desktop Player. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.gpmdp/ """ import logging import json import os import socket import time import voluptuous as vol from homeassistant.components.media_player import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_SEEK, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( STATE_PLAYING, STATE_PAUSED, STATE_OFF, CONF_HOST, CONF_PORT, CONF_NAME) from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['websocket-client==0.37.0'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) DEFAULT_HOST = 'localhost' DEFAULT_NAME = 'GPM Desktop Player' DEFAULT_PORT = 5672 GPMDP_CONFIG_FILE = 'gpmpd.conf' SUPPORT_GPMDP = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ SUPPORT_SEEK | SUPPORT_VOLUME_SET PLAYBACK_DICT = {'0': STATE_PAUSED, # Stopped '1': STATE_PAUSED, '2': STATE_PLAYING} 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, }) def request_configuration(hass, config, url, add_devices_callback): """Request configuration steps from the user.""" configurator = get_component('configurator') if 'gpmdp' in _CONFIGURING: configurator.notify_errors( _CONFIGURING['gpmdp'], "Failed to register, please try again.") return from websocket import create_connection websocket = create_connection((url), timeout=1) websocket.send(json.dumps({'namespace': 'connect', 'method': 'connect', 'arguments': ['Home Assistant']})) # pylint: disable=unused-argument def gpmdp_configuration_callback(callback_data): """The actions to do when our configuration callback is called.""" while True: from websocket import _exceptions try: msg = json.loads(websocket.recv()) except _exceptions.WebSocketConnectionClosedException: continue if msg['channel'] != 'connect': continue if msg['payload'] != "CODE_REQUIRED": continue pin = callback_data.get('pin') websocket.send(json.dumps({'namespace': 'connect', 'method': 'connect', 'arguments': ['Home Assistant', pin]})) tmpmsg = json.loads(websocket.recv()) if tmpmsg['channel'] == 'time': _LOGGER.error('Error setting up GPMDP. Please pause' ' the desktop player and try again.') break code = tmpmsg['payload'] if code == 'CODE_REQUIRED': continue setup_gpmdp(hass, config, code, add_devices_callback) _save_config(hass.config.path(GPMDP_CONFIG_FILE), {"CODE": code}) websocket.send(json.dumps({'namespace': 'connect', 'method': 'connect', 'arguments': ['Home Assistant', code]})) websocket.close() break _CONFIGURING['gpmdp'] = configurator.request_config( hass, DEFAULT_NAME, gpmdp_configuration_callback, description=( 'Enter the pin that is displayed in the ' 'Google Play Music Desktop Player.'), submit_caption="Submit", fields=[{'id': 'pin', 'name': 'Pin Code', 'type': 'number'}] ) def setup_gpmdp(hass, config, code, add_devices): """Setup gpmdp.""" name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) url = 'ws://{}:{}'.format(host, port) if not code: request_configuration(hass, config, url, add_devices) return if 'gpmdp' in _CONFIGURING: configurator = get_component('configurator') configurator.request_done(_CONFIGURING.pop('gpmdp')) add_devices([GPMDP(name, url, code)]) def _load_config(filename): """Load configuration.""" if not os.path.isfile(filename): return {} try: with open(filename, 'r') as fdesc: inp = fdesc.read() # In case empty file if not inp: return {} return json.loads(inp) except (IOError, ValueError) as error: _LOGGER.error("Reading config file %s failed: %s", filename, error) return None def _save_config(filename, config): """Save configuration.""" try: with open(filename, 'w') as fdesc: fdesc.write(json.dumps(config, indent=4, sort_keys=True)) except (IOError, TypeError) as error: _LOGGER.error("Saving configuration file failed: %s", error) return False return True def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Setup the GPMDP platform.""" codeconfig = _load_config(hass.config.path(GPMDP_CONFIG_FILE)) if len(codeconfig): code = codeconfig.get('CODE') elif discovery_info is not None: if 'gpmdp' in _CONFIGURING: return code = None else: code = None setup_gpmdp(hass, config, code, add_devices_callback) class GPMDP(MediaPlayerDevice): """Representation of a GPMDP.""" # pylint: disable=too-many-public-methods, abstract-method # pylint: disable=too-many-instance-attributes def __init__(self, name, url, code): """Initialize the media player.""" from websocket import create_connection self._connection = create_connection self._url = url self._authorization_code = code self._name = name self._status = STATE_OFF self._ws = None self._title = None self._artist = None self._albumart = None self._seek_position = None self._duration = None self._volume = None self._request_id = 0 self.update() def get_ws(self): """Check if the websocket is setup and connected.""" if self._ws is None: try: self._ws = self._connection((self._url), timeout=1) msg = json.dumps({'namespace': 'connect', 'method': 'connect', 'arguments': ['Home Assistant', self._authorization_code]}) self._ws.send(msg) except (socket.timeout, ConnectionRefusedError, ConnectionResetError): self._ws = None return self._ws def send_gpmdp_msg(self, namespace, method, with_id=True): """Send ws messages to GPMDP and verify request id in response.""" from websocket import _exceptions try: websocket = self.get_ws() if websocket is None: self._status = STATE_OFF return self._request_id += 1 websocket.send(json.dumps({'namespace': namespace, 'method': method, 'requestID': self._request_id})) if not with_id: return while True: msg = json.loads(websocket.recv()) if 'requestID' in msg: if msg['requestID'] == self._request_id: return msg except (ConnectionRefusedError, ConnectionResetError, _exceptions.WebSocketTimeoutException, _exceptions.WebSocketProtocolException, _exceptions.WebSocketPayloadException, _exceptions.WebSocketConnectionClosedException): self._ws = None def update(self): """Get the latest details from the player.""" time.sleep(1) playstate = self.send_gpmdp_msg('playback', 'getPlaybackState') if playstate is None: return self._status = PLAYBACK_DICT[str(playstate['value'])] time_data = self.send_gpmdp_msg('playback', 'getCurrentTime') if time_data is not None: self._seek_position = int(time_data['value'] / 1000) track_data = self.send_gpmdp_msg('playback', 'getCurrentTrack') if track_data is not None: self._title = track_data['value']['title'] self._artist = track_data['value']['artist'] self._albumart = track_data['value']['albumArt'] self._duration = int(track_data['value']['duration'] / 1000) volume_data = self.send_gpmdp_msg('volume', 'getVolume') if volume_data is not None: self._volume = volume_data['value'] / 100 @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.""" return self._status @property def media_title(self): """Title of current playing media.""" return self._title @property def media_artist(self): """Artist of current playing media (Music track only).""" return self._artist @property def media_image_url(self): """Image url of current playing media.""" return self._albumart @property def media_seek_position(self): """Time in seconds of current seek position.""" return self._seek_position @property def media_duration(self): """Time in seconds of current song duration.""" return self._duration @property def volume_level(self): """Volume level of the media player (0..1).""" return self._volume @property def name(self): """Return the name of the device.""" return self._name @property def supported_media_commands(self): """Flag of media commands that are supported.""" return SUPPORT_GPMDP def media_next_track(self): """Send media_next command to media player.""" self.send_gpmdp_msg('playback', 'forward', False) def media_previous_track(self): """Send media_previous command to media player.""" self.send_gpmdp_msg('playback', 'rewind', False) def media_play(self): """Send media_play command to media player.""" self.send_gpmdp_msg('playback', 'playPause', False) self._status = STATE_PLAYING self.update_ha_state() def media_pause(self): """Send media_pause command to media player.""" self.send_gpmdp_msg('playback', 'playPause', False) self._status = STATE_PAUSED self.update_ha_state() def media_seek(self, position): """Send media_seek command to media player.""" websocket = self.get_ws() if websocket is None: return websocket.send(json.dumps({'namespace': 'playback', 'method': 'setCurrentTime', 'arguments': [position*1000]})) self.update_ha_state() def volume_up(self): """Send volume_up command to media player.""" websocket = self.get_ws() if websocket is None: return websocket.send('{"namespace": "volume", "method": "increaseVolume"}') self.update_ha_state() def volume_down(self): """Send volume_down command to media player.""" websocket = self.get_ws() if websocket is None: return websocket.send('{"namespace": "volume", "method": "decreaseVolume"}') self.update_ha_state() def set_volume_level(self, volume): """Set volume on media player, range(0..1).""" websocket = self.get_ws() if websocket is None: return websocket.send(json.dumps({'namespace': 'volume', 'method': 'setVolume', 'arguments': [volume*100]})) self.update_ha_state()