Skip to content

Commit

Permalink
Add websocket, refactor to async and fix zeroconf
Browse files Browse the repository at this point in the history
  • Loading branch information
fondberg authored Jan 19, 2020
1 parent ea97a72 commit 99f42b2
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 78 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
199 changes: 125 additions & 74 deletions custom_components/spotcast/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
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)
from homeassistant.components.cast.media_player import KNOWN_CHROMECAST_INFO_KEY
import random
import time

_VERSION = '2.5.1'
_VERSION = '2.6.0'
DOMAIN = 'spotcast'

_LOGGER = logging.getLogger(__name__)
Expand All @@ -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,
Expand All @@ -45,45 +55,31 @@
}, extra=vol.ALLOW_EXTRA)


def setup(hass, config):
async def async_setup(hass, config):
"""Setup the Spotcast service."""
conf = config[DOMAIN]

username = conf[CONF_USERNAME]
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

Expand Down Expand Up @@ -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)
Expand All @@ -145,66 +198,64 @@ 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:
raise HomeAssistantError('Failed to launch spotify controller due to timeout')
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
5 changes: 3 additions & 2 deletions custom_components/spotcast/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down

0 comments on commit 99f42b2

Please sign in to comment.