Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use device registry for casts and spotify devices #178

Merged
merged 9 commits into from
Apr 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) [![spotcast](https://img.shields.io/github/release/fondberg/spotcast.svg?1)](https://github.com/fondberg/spotcast) ![Maintenance](https://img.shields.io/maintenance/yes/2020.svg)
[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) [![spotcast](https://img.shields.io/github/release/fondberg/spotcast.svg?1)](https://github.com/fondberg/spotcast) ![Maintenance](https://img.shields.io/maintenance/yes/2021.svg)

[![Buy me a coffee](https://img.shields.io/static/v1.svg?label=Buy%20me%20a%20coffee&message=🥨&color=black&logo=buy%20me%20a%20coffee&logoColor=white&labelColor=6f4e37)](https://www.buymeacoffee.com/fondberg)

Expand Down
221 changes: 161 additions & 60 deletions custom_components/spotcast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.components.cast.media_player import KNOWN_CHROMECAST_INFO_KEY
from homeassistant.components.cast.media_player import CastDevice
from homeassistant.components.cast.helpers import ChromeCastZeroconf
from homeassistant.components.spotify.media_player import SpotifyMediaPlayer
from homeassistant.helpers import entity_platform

__VERSION__ = "3.4.7"
__VERSION__ = "3.5.2"
fondberg marked this conversation as resolved.
Show resolved Hide resolved
DOMAIN = "spotcast"

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -48,22 +50,32 @@

WS_TYPE_SPOTCAST_DEVICES = "spotcast/devices"
SCHEMA_WS_DEVICES = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{vol.Required("type"): WS_TYPE_SPOTCAST_DEVICES, vol.Optional("account"): str,}
{
vol.Required("type"): WS_TYPE_SPOTCAST_DEVICES,
vol.Optional("account"): str,
}
)

WS_TYPE_SPOTCAST_PLAYER = "spotcast/player"
SCHEMA_WS_PLAYER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{vol.Required("type"): WS_TYPE_SPOTCAST_PLAYER, vol.Optional("account"): str,}
{
vol.Required("type"): WS_TYPE_SPOTCAST_PLAYER,
vol.Optional("account"): str,
}
)

WS_TYPE_SPOTCAST_ACCOUNTS = "spotcast/accounts"
SCHEMA_WS_ACCOUNTS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{vol.Required("type"): WS_TYPE_SPOTCAST_ACCOUNTS,}
{
vol.Required("type"): WS_TYPE_SPOTCAST_ACCOUNTS,
}
)

WS_TYPE_SPOTCAST_CASTDEVICES = "spotcast/castdevices"
SCHEMA_WS_CASTDEVICES = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
{vol.Required("type"): WS_TYPE_SPOTCAST_CASTDEVICES,}
{
vol.Required("type"): WS_TYPE_SPOTCAST_CASTDEVICES,
}
)

SERVICE_START_COMMAND_SCHEMA = vol.Schema(
Expand All @@ -83,7 +95,10 @@
)

ACCOUNTS_SCHEMA = vol.Schema(
{vol.Required(CONF_SP_DC): cv.string, vol.Required(CONF_SP_KEY): cv.string,}
{
vol.Required(CONF_SP_DC): cv.string,
vol.Required(CONF_SP_KEY): cv.string,
}
)

CONFIG_SCHEMA = vol.Schema(
Expand All @@ -99,6 +114,45 @@
extra=vol.ALLOW_EXTRA,
)


def get_spotify_devices(hass):
platforms = entity_platform.async_get_platforms(hass, "spotify")
spotify_media_player = None
for platform in platforms:
if platform.domain != "media_player":
continue

for entity in platform.entities.values():
if isinstance(entity, SpotifyMediaPlayer):
_LOGGER.debug(
f"get_spotify_devices: {entity.entity_id}: {entity.name} HH: %s",
entity._devices,
)
spotify_media_player = entity
break
if spotify_media_player:
# try later to see if it possible to retrieve the devices instead of relying on the caches one which
# might be 30 seconds old from spotify_media_player
# return spotify_media_player._spotify.devices()
return spotify_media_player._devices


def get_cast_devices(hass):
platforms = entity_platform.async_get_platforms(hass, "cast")
cast_infos = []
for platform in platforms:
if platform.domain != "media_player":
continue
for entity in platform.entities.values():
if isinstance(entity, CastDevice):
_LOGGER.debug(
f"get_cast_devices: {entity.entity_id}: {entity.name} cast info: %s",
entity._cast_info,
)
cast_infos.append(entity._cast_info)
return cast_infos


# Async wrap sync function
def async_wrap(func):
@wraps(func)
Expand Down Expand Up @@ -164,7 +218,11 @@ def get_playlist():
resp = resp.get("content")
elif playlistType == "featured":
resp = client.featured_playlists(
locale=locale, country=countryCode, timestamp=datetime.now().strftime('%Y-%m-%dT%H:%M:%S'), limit=limit, offset=0
locale=locale,
country=countryCode,
timestamp=datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
limit=limit,
offset=0,
)
resp = resp.get("playlists")
else:
Expand All @@ -178,11 +236,9 @@ def get_playlist():
def websocket_handle_devices(hass, connection, msg):
@async_wrap
def get_devices():
"""Handle to get devices"""
account = msg.get("account", None)
_LOGGER.debug("websocket_handle_devices msg: %s", msg)
client = spotipy.Spotify(auth=get_token_instance(account).access_token)
resp = client.devices()
"""Handle to get devices. Only for default account"""
devices = get_spotify_devices(hass)
resp = {"devices": devices}
connection.send_message(websocket_api.result_message(msg["id"], resp))

hass.async_add_job(get_devices())
Expand Down Expand Up @@ -212,30 +268,47 @@ def websocket_handle_accounts(hass, connection, msg):
def websocket_handle_castdevices(hass, connection, msg):
"""Handle to get cast devices for debug purposes"""
_LOGGER.debug("websocket_handle_castdevices msg: %s", msg)
known_devices = hass.data.get(KNOWN_CHROMECAST_INFO_KEY, [])

known_devices = get_cast_devices(hass)
_LOGGER.debug("%s", known_devices)
resp = [
{
"host": str(known_devices[k].host),
"port": known_devices[k].port,
"uuid": known_devices[k].uuid,
"model_name": known_devices[k].model_name,
"friendly_name": known_devices[k].friendly_name,
"uuid": cast_info.uuid,
"model_name": cast_info.model_name,
"friendly_name": cast_info.friendly_name,
}
for k in known_devices
for cast_info in known_devices
]

connection.send_message(websocket_api.result_message(msg["id"], resp))

def play(client, spotify_device_id, uri, random_song, repeat, shuffle, position):
_LOGGER.debug(
"Version: %s, playing URI: %s on device-id: %s", __VERSION__, uri, spotify_device_id
"Playing URI: %s on device-id: %s",
uri,
spotify_device_id,
)
if uri.find("track") > 0:
if uri.find("show") > 0:
show_episodes_info = client.show_episodes(uri)
if show_episodes_info and len(show_episodes_info["items"]) > 0:
episode_uri = show_episodes_info["items"][0]["external_urls"]["spotify"]
_LOGGER.debug(
"Playing episode using uris (latest podcast playlist)= for uri: %s",
episode_uri,
)
client.start_playback(device_id=spotify_device_id, uris=[episode_uri])
elif uri.find("episode") > 0:
_LOGGER.debug("Playing episode using uris= for uri: %s", uri)
client.start_playback(device_id=spotify_device_id, uris=[uri])

elif uri.find("track") > 0:
_LOGGER.debug("Playing track using uris= for uri: %s", uri)
client.start_playback(device_id=spotify_device_id, uris=[uri])
else:
if uri == "random":
_LOGGER.debug("Cool, you found the easter egg with playing a random playlist")
_LOGGER.debug(
"Cool, you found the easter egg with playing a random playlist"
)
playlists = client.user_playlists("me", 50)
no_playlists = len(playlists["items"])
uri = playlists["items"][random.randint(0, no_playlists - 1)]["uri"]
Expand Down Expand Up @@ -286,13 +359,21 @@ def start_casting(call):
# first, rely on spotify id given in config
if not spotify_device_id:
# if not present, check if there's a spotify connect device with that name
spotify_device_id = getSpotifyConnectDeviceId(client, call.data.get(CONF_DEVICE_NAME))
spotify_device_id = getSpotifyConnectDeviceId(
client, call.data.get(CONF_DEVICE_NAME)
)
if not spotify_device_id:
# if still no id available, check cast devices and launch the app on chromecast
devices = get_spotify_devices(hass)
devices_available = {"devices": devices}
spotify_cast_device = SpotifyCastDevice(
hass, call.data.get(CONF_DEVICE_NAME), call.data.get(CONF_ENTITY_ID)
hass,
call.data.get(CONF_DEVICE_NAME),
call.data.get(CONF_ENTITY_ID),
devices_available,
)
spotify_cast_device.startSpotifyController(access_token, expires)
time.sleep(1)
spotify_device_id = spotify_cast_device.getSpotifyDeviceId(client)

if uri is None or uri.strip() == "":
Expand All @@ -302,7 +383,9 @@ def start_casting(call):
_LOGGER.debug("Current_playback from spotify: %s", current_playback)
force_playback = True
_LOGGER.debug("Force playback: %s", force_playback)
client.transfer_playback(device_id=spotify_device_id, force_play=force_playback)
client.transfer_playback(
device_id=spotify_device_id, force_play=force_playback
)
else:
play(client, spotify_device_id, uri, random_song, repeat, shuffle, position)
if shuffle or repeat or start_volume <= 100:
Expand Down Expand Up @@ -335,10 +418,14 @@ def start_casting(call):
)

hass.components.websocket_api.async_register_command(
WS_TYPE_SPOTCAST_CASTDEVICES, websocket_handle_castdevices, SCHEMA_WS_CASTDEVICES
WS_TYPE_SPOTCAST_CASTDEVICES,
websocket_handle_castdevices,
SCHEMA_WS_CASTDEVICES,
)

hass.services.register(DOMAIN, "start", start_casting, schema=SERVICE_START_COMMAND_SCHEMA)
hass.services.register(
DOMAIN, "start", start_casting, schema=SERVICE_START_COMMAND_SCHEMA
)

return True

Expand Down Expand Up @@ -370,7 +457,9 @@ def get_spotify_token(self):
import spotify_token as st

try:
self._access_token, self._token_expires = st.start_session(self.sp_dc, self.sp_key)
self._access_token, self._token_expires = st.start_session(
self.sp_dc, self.sp_key
)
expires = self._token_expires - int(time.time())
return self._access_token, expires
except:
Expand All @@ -383,17 +472,21 @@ class SpotifyCastDevice:
hass = None
castDevice = None
spotifyController = None
devices_available = []

def __init__(self, hass, call_device_name, call_entity_id):
def __init__(self, hass, call_device_name, call_entity_id, devices_available):
"""Initialize a spotify cast device."""
self.hass = hass
self.devices_available = devices_available

# Get device name from either device_name or entity_id
device_name = None
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")
raise HomeAssistantError(
"Either entity_id or device_name must be specified"
)
entity_states = hass.states.get(entity_id)
if entity_states is None:
_LOGGER.error("Could not find entity_id: %s", entity_id)
Expand All @@ -414,41 +507,33 @@ def getChromecastDevice(self, device_name):
import pychromecast

# Get cast from discovered devices of cast platform
known_devices = self.hass.data.get(KNOWN_CHROMECAST_INFO_KEY, [])
known_devices = get_cast_devices(self.hass)

_LOGGER.debug("Chromecast devices: %s", known_devices)
try:
# HA below 0.113
cast_info = next((x for x in known_devices if x.friendly_name == device_name), None)
except:
cast_info = next(
(
known_devices[x]
for x in known_devices
if known_devices[x].friendly_name == device_name
),
None,
)

cast_info = next(
(
castinfo
for castinfo in known_devices
if castinfo.friendly_name == device_name
),
None,
)

_LOGGER.debug("cast info: %s", cast_info)

if cast_info:
return pychromecast.get_chromecast_from_service(
(
cast_info.services,
cast_info.uuid,
cast_info.model_name,
cast_info.friendly_name,
None,
None,
),
ChromeCastZeroconf.get_zeroconf())
return pychromecast.get_chromecast_from_cast_info(
cast_info, ChromeCastZeroconf.get_zeroconf()
)
_LOGGER.error(
"Could not find device %s from hass.data",
device_name,
)

raise HomeAssistantError("Could not find device with name {}".format(device_name))
raise HomeAssistantError(
"Could not find device with name {}".format(device_name)
)

def startSpotifyController(self, access_token, expires):
from pychromecast.controllers.spotify import SpotifyController
Expand All @@ -458,21 +543,37 @@ def startSpotifyController(self, access_token, expires):
sp.launch_app()

if not sp.is_launched and not sp.credential_error:
raise HomeAssistantError("Failed to launch spotify controller due to timeout")
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")
raise HomeAssistantError(
"Failed to launch spotify controller due to credentials error"
)

self.spotifyController = sp

def getSpotifyDeviceId(self, client):
# Look for device
devices_available = client.devices()
devices_available = self.devices_available

_LOGGER.info(
"devices_available: %s %s", devices_available, self.spotifyController.device
)

for device in devices_available["devices"]:
if device["id"] == self.spotifyController.device:
return device["id"]

_LOGGER.error(
'No device with id "{}" known by Spotify'.format(self.spotifyController.device)
'No device with id "{}" known by Spotify'.format(
self.spotifyController.device
)
)
_LOGGER.error("Known devices: {}".format(devices_available["devices"]))
raise HomeAssistantError("Failed to get device id from Spotify")

# Default to try to use the one we got
return self.spotifyController.device

# can't throw as as we are not in control over the retrieval of devices for cast
# raise HomeAssistantError("Failed to get device id from Spotify")
Loading