1
mirror of https://github.com/home-assistant/core synced 2024-08-31 05:57:13 +02:00
ha-core/homeassistant/components/gpmdp/media_player.py
2021-03-27 12:39:37 +01:00

388 lines
12 KiB
Python

"""Support for Google Play Music Desktop Player."""
import json
import logging
import socket
import time
import voluptuous as vol
from websocket import _exceptions, create_connection
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
from homeassistant.components.media_player.const import (
MEDIA_TYPE_MUSIC,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
SUPPORT_PREVIOUS_TRACK,
SUPPORT_SEEK,
SUPPORT_VOLUME_SET,
)
from homeassistant.const import (
CONF_HOST,
CONF_NAME,
CONF_PORT,
STATE_OFF,
STATE_PAUSED,
STATE_PLAYING,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json, save_json
_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
| SUPPORT_PLAY
)
PLAYBACK_DICT = {"0": STATE_PAUSED, "1": STATE_PAUSED, "2": STATE_PLAYING} # Stopped
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_entities_callback):
"""Request configuration steps from the user."""
configurator = hass.components.configurator
if "gpmdp" in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING["gpmdp"], "Failed to register, please try again."
)
return
websocket = create_connection((url), timeout=1)
websocket.send(
json.dumps(
{
"namespace": "connect",
"method": "connect",
"arguments": ["Home Assistant"],
}
)
)
def gpmdp_configuration_callback(callback_data):
"""Handle configuration changes."""
while True:
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_entities_callback)
save_json(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(
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_entities):
"""Set up gpmdp."""
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
url = f"ws://{host}:{port}"
if not code:
request_configuration(hass, config, url, add_entities)
return
if "gpmdp" in _CONFIGURING:
configurator = hass.components.configurator
configurator.request_done(_CONFIGURING.pop("gpmdp"))
add_entities([GPMDP(name, url, code)], True)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the GPMDP platform."""
codeconfig = load_json(hass.config.path(GPMDP_CONFIG_FILE))
if 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_entities)
class GPMDP(MediaPlayerEntity):
"""Representation of a GPMDP."""
def __init__(self, name, url, code):
"""Initialize the media player."""
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._available = True
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."""
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 and 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)
try:
self._available = True
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
except OSError:
self._available = False
@property
def available(self):
"""Return if media player is available."""
return self._available
@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_features(self):
"""Flag media player features 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.schedule_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.schedule_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.schedule_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.schedule_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.schedule_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.schedule_update_ha_state()