From 99f42b2e95d62298b457a0ff3bc352c56070e010 Mon Sep 17 00:00:00 2001 From: Niklas Fondberg Date: Sun, 19 Jan 2020 19:26:37 +0100 Subject: [PATCH] Add websocket, refactor to async and fix zeroconf --- README.md | 3 +- custom_components/spotcast/__init__.py | 199 ++++++++++++++--------- custom_components/spotcast/manifest.json | 5 +- 3 files changed, 129 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 70259f36..be872443 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,7 @@ Community post: https://community.home-assistant.io/t/spotcast-custom-component- _Now with support for speaker groups, transferring playback to and between chromecast devices and using HA entity id instead of device_name_. ***Important*** -Release 2.1 requires Home Assistant 0.99 due to update in pychromecast version. -As of release 2 the sensor is optional. To add it add it to the configs sensor section. +Release 2.6 has a dependency to zeroconf 0.24.3 to make HASS work until proper upstream fix exists. ## Installation diff --git a/custom_components/spotcast/__init__.py b/custom_components/spotcast/__init__.py index 44f4241e..685d1003 100644 --- a/custom_components/spotcast/__init__.py +++ b/custom_components/spotcast/__init__.py @@ -1,5 +1,8 @@ import logging +import asyncio import voluptuous as vol +from homeassistant.components import http, websocket_api +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME) @@ -7,7 +10,7 @@ import random import time -_VERSION = '2.5.1' +_VERSION = '2.6.0' DOMAIN = 'spotcast' _LOGGER = logging.getLogger(__name__) @@ -21,6 +24,13 @@ CONF_RANDOM = 'random_song' CONF_REPEAT = 'repeat' +WS_TYPE_SPOTCAST_PLAYLISTS = "spotcast/playlists" + +SCHEMA_PLAYLISTS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): WS_TYPE_SPOTCAST_PLAYLISTS} +) + + SERVICE_START_COMMAND_SCHEMA = vol.Schema({ vol.Optional(CONF_DEVICE_NAME): cv.string, vol.Optional(CONF_ENTITY_ID): cv.string, @@ -45,7 +55,7 @@ }, extra=vol.ALLOW_EXTRA) -def setup(hass, config): +async def async_setup(hass, config): """Setup the Spotcast service.""" conf = config[DOMAIN] @@ -53,37 +63,23 @@ def setup(hass, config): password = conf[CONF_PASSWORD] accounts = conf.get(CONF_ACCOUNTS) - # sensor - # hass.helpers.discovery.load_platform('sensor', DOMAIN, {}, config) - - # service - def get_chromecast_device(device_name): - import pychromecast - - # Get cast from discovered devices of cast platform - known_devices = hass.data.get(KNOWN_CHROMECAST_INFO_KEY, []) - cast_info = next((x for x in known_devices if x.friendly_name == device_name), None) - _LOGGER.debug('cast info: %s', cast_info) - if cast_info: - return pychromecast._get_chromecast_from_host(( - cast_info.host, cast_info.port, cast_info.uuid, - cast_info.model_name, cast_info.friendly_name - )) - _LOGGER.error('Could not find device %s from hass.data, falling back to pychromecast scan', device_name) - - # Discover devices manually - chromecasts = pychromecast.get_chromecasts() - for _cast in chromecasts: - if _cast.name == device_name: - _LOGGER.debug('Fallback, found cast device: %s', _cast) - return _cast - - raise HomeAssistantError('Could not find device with name {}'.format(device_name)) + @callback + def websocket_handle_playlists(hass, connection, msg): + """Handle get playlist""" + import spotipy + access_token, expires = get_spotify_token(username=username, password=password) + client = spotipy.Spotify(auth=access_token) + resp = client._get('views/made-for-x?content_limit=10&locale=en&platform=web&types=album%2Cplaylist%2Cartist%2Cshow%2Cstation', limit=10, + offset=0) + connection.send_message( + websocket_api.result_message(msg["id"], resp) + ) def get_spotify_token(username, password): import spotify_token as st data = st.start_session(username, password) access_token = data[0] + # token_expires = data[1] expires = data[1] - int(time.time()) return access_token, expires @@ -118,25 +114,82 @@ def play(client, spotify_device_id, uri, random_song, repeat): time.sleep(5) client.repeat(state=repeat, device_id=spotify_device_id) - def transfer_pb(client, spotify_device_id): - _LOGGER.debug('Transfering playback') - client.transfer_playback(device_id=spotify_device_id, force_play=True) + def get_account_credentials(call): + """ Get credentials for account """ + account = call.data.get(CONF_SPOTIFY_ACCOUNT) + user = username + pwd = password + if account is not None: + _LOGGER.debug('setting up with different account than default %s', account) + user = accounts.get(account).get(CONF_USERNAME) + pwd = accounts.get(account).get(CONF_PASSWORD) + return user, pwd - def start_casting(call): - """service called.""" + def shouldTransferPlayback(call, client): + """ Check if something is playing """ + uri = call.data.get(CONF_SPOTIFY_URI) + if uri is None or uri.strip() == '' or call.data.get(CONF_TRANSFER_PLAYBACK): + current_playback = client.current_playback() + if current_playback is not None: + _LOGGER.debug('current_playback from spotipy: %s', current_playback) + return True + return False - from pychromecast.controllers.spotify import SpotifyController + async def start_casting(call): + """service called.""" import spotipy - transfer_playback = False uri = call.data.get(CONF_SPOTIFY_URI) random_song = call.data.get(CONF_RANDOM, False) repeat = call.data.get(CONF_REPEAT) - # Get device name from tiehr device_name or entity_id + # Account + user, pwd = get_account_credentials(call) + + # login as real browser to get powerful token + access_token, expires = get_spotify_token(username=user, password=pwd) + + # get the spotify web api client + client = spotipy.Spotify(auth=access_token) + + # launch the app on chromecast + spotify_cast_device = SpotifyCastDevice(hass, call.data.get(CONF_DEVICE_NAME), call.data.get(CONF_ENTITY_ID)) + spotify_cast_device.startSpotifyController(access_token, expires) + spotify_device_id = spotify_cast_device.getSpotifyDeviceId(client) + + transfer_playback = shouldTransferPlayback(call, client) + if transfer_playback == True: + _LOGGER.debug('Transfering playback') + client.transfer_playback( + device_id=spotify_device_id, force_play=True) + else: + play(client, spotify_device_id, uri, random_song, repeat) + + # Register websocket and service + hass.components.websocket_api.async_register_command( + WS_TYPE_SPOTCAST_PLAYLISTS, websocket_handle_playlists, SCHEMA_PLAYLISTS + ) + + hass.services.async_register(DOMAIN, 'start', start_casting, + schema=SERVICE_START_COMMAND_SCHEMA) + + return True + + +class SpotifyCastDevice: + """Represents a spotify device.""" + hass = None + castDevice = None + spotifyController = None + + def __init__(self, hass, call_device_name, call_entity_id): + """Initialize a spotify cast device.""" + self.hass = hass + + # Get device name from either device_name or entity_id device_name = None - if call.data.get(CONF_DEVICE_NAME) is None: - entity_id = call.data.get(CONF_ENTITY_ID) + if call_device_name is None: + entity_id = call_entity_id if entity_id is None: raise HomeAssistantError('Either entity_id or device_name must be specified') entity_states = hass.states.get(entity_id) @@ -145,41 +198,44 @@ def start_casting(call): else: device_name = entity_states.attributes.get('friendly_name') else: - device_name = call.data.get(CONF_DEVICE_NAME) + device_name = call_device_name if device_name is None or device_name.strip() == '': raise HomeAssistantError('device_name is empty') # Find chromecast device - cast = get_chromecast_device(device_name) - _LOGGER.debug('Found cast device: %s', cast) - cast.wait() + self.castDevice = self.getChromecastDevice(device_name) + _LOGGER.debug('Found cast device: %s', self.castDevice) + self.castDevice.wait() - account = call.data.get(CONF_SPOTIFY_ACCOUNT) - user = username - pwd = password - if account is not None: - _LOGGER.debug('setting up with different account than default %s', account) - user = accounts.get(account).get(CONF_USERNAME) - pwd = accounts.get(account).get(CONF_PASSWORD) + def getChromecastDevice(self, device_name): + import pychromecast - # login as real browser to get powerful token - access_token, expires = get_spotify_token(username=user, password=pwd) + # Get cast from discovered devices of cast platform + known_devices = self.hass.data.get(KNOWN_CHROMECAST_INFO_KEY, []) + cast_info = next((x for x in known_devices if x.friendly_name == device_name), None) + _LOGGER.debug('cast info: %s', cast_info) + if cast_info: + return pychromecast._get_chromecast_from_host(( + cast_info.host, cast_info.port, cast_info.uuid, + cast_info.model_name, cast_info.friendly_name + )) + _LOGGER.error('Could not find device %s from hass.data, falling back to pychromecast scan', device_name) - # get the spotify web api client - client = spotipy.Spotify(auth=access_token) + # Discover devices manually + chromecasts = pychromecast.get_chromecasts() + for _cast in chromecasts: + if _cast.name == device_name: + _LOGGER.debug('Fallback, found cast device: %s', _cast) + return _cast - # Check if something is playing + raise HomeAssistantError('Could not find device with name {}'.format(device_name)) - if uri is None or uri.strip() == '' or call.data.get(CONF_TRANSFER_PLAYBACK): - current_playback = client.current_playback() - if current_playback is not None: - _LOGGER.debug('current_playback from spotipy: %s', current_playback) - transfer_playback = True + def startSpotifyController(self, access_token, expires): + from pychromecast.controllers.spotify import SpotifyController - # launch the app on chromecast sp = SpotifyController(access_token, expires) - cast.register_handler(sp) + self.castDevice.register_handler(sp) sp.launch_app() if not sp.is_launched and not sp.credential_error: @@ -187,24 +243,19 @@ def start_casting(call): if not sp.is_launched and sp.credential_error: raise HomeAssistantError('Failed to launch spotify controller due to credentials error') + self.spotifyController = sp + + def getSpotifyDeviceId(self, client): + # Look for device spotify_device_id = None devices_available = client.devices() for device in devices_available['devices']: - if device['id'] == sp.device: + if device['id'] == self.spotifyController.device: spotify_device_id = device['id'] break if not spotify_device_id: - _LOGGER.error('No device with id "{}" known by Spotify'.format(sp.device)) + _LOGGER.error('No device with id "{}" known by Spotify'.format(self.spotifyController.device)) _LOGGER.error('Known devices: {}'.format(devices_available['devices'])) - return - - if transfer_playback == True: - transfer_pb(client, spotify_device_id) - else: - play(client, spotify_device_id, uri, random_song, repeat) - - hass.services.register(DOMAIN, 'start', start_casting, - schema=SERVICE_START_COMMAND_SCHEMA) - - return True + raise HomeAssistantError('Failed to get device id from Spotify') + return spotify_device_id diff --git a/custom_components/spotcast/manifest.json b/custom_components/spotcast/manifest.json index 316438c0..7b3ac5ee 100644 --- a/custom_components/spotcast/manifest.json +++ b/custom_components/spotcast/manifest.json @@ -3,9 +3,10 @@ "name": "Start Spotify on chromecast", "documentation": "https://github.com/fondberg/spotcast", "requirements": [ - "fondberg-spotipy==2.4.5-dev2", + "spotipy==2.6.3", "pychromecast==4.0.1", - "spotify_token==0.1.2" + "spotify_token==0.1.2", + "zeroconf==0.24.3" ], "homeassistant": "0.99.0", "dependencies": [