340 lines
11 KiB
Python
340 lines
11 KiB
Python
"""
|
|
homeassistant.components.chromecast
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Provides functionality to interact with Chromecasts.
|
|
"""
|
|
import logging
|
|
|
|
try:
|
|
import pychromecast
|
|
except ImportError:
|
|
# Ignore, we will raise appropriate error later
|
|
pass
|
|
|
|
from homeassistant.loader import get_component
|
|
import homeassistant.util as util
|
|
from homeassistant.helpers import extract_entity_ids
|
|
from homeassistant.const import (
|
|
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_VOLUME_UP,
|
|
SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY,
|
|
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK)
|
|
|
|
|
|
DOMAIN = 'chromecast'
|
|
DEPENDENCIES = []
|
|
|
|
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video'
|
|
|
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
|
STATE_NO_APP = 'idle'
|
|
|
|
ATTR_STATE = 'state'
|
|
ATTR_OPTIONS = 'options'
|
|
ATTR_MEDIA_STATE = 'media_state'
|
|
ATTR_MEDIA_CONTENT_ID = 'media_content_id'
|
|
ATTR_MEDIA_TITLE = 'media_title'
|
|
ATTR_MEDIA_ARTIST = 'media_artist'
|
|
ATTR_MEDIA_ALBUM = 'media_album'
|
|
ATTR_MEDIA_IMAGE_URL = 'media_image_url'
|
|
ATTR_MEDIA_VOLUME = 'media_volume'
|
|
ATTR_MEDIA_DURATION = 'media_duration'
|
|
|
|
MEDIA_STATE_UNKNOWN = 'unknown'
|
|
MEDIA_STATE_PLAYING = 'playing'
|
|
MEDIA_STATE_STOPPED = 'stopped'
|
|
|
|
|
|
def is_on(hass, entity_id=None):
|
|
""" Returns true if specified ChromeCast entity_id is on.
|
|
Will check all chromecasts if no entity_id specified. """
|
|
|
|
entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN)
|
|
|
|
return any(not hass.states.is_state(entity_id, STATE_NO_APP)
|
|
for entity_id in entity_ids)
|
|
|
|
|
|
def turn_off(hass, entity_id=None):
|
|
""" Will turn off specified Chromecast or all. """
|
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
|
|
|
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
|
|
|
|
|
def volume_up(hass, entity_id=None):
|
|
""" Send the chromecast the command for volume up. """
|
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
|
|
|
hass.services.call(DOMAIN, SERVICE_VOLUME_UP, data)
|
|
|
|
|
|
def volume_down(hass, entity_id=None):
|
|
""" Send the chromecast the command for volume down. """
|
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
|
|
|
hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data)
|
|
|
|
|
|
def media_play_pause(hass, entity_id=None):
|
|
""" Send the chromecast the command for play/pause. """
|
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
|
|
|
hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data)
|
|
|
|
|
|
def media_play(hass, entity_id=None):
|
|
""" Send the chromecast the command for play/pause. """
|
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
|
|
|
hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY, data)
|
|
|
|
|
|
def media_pause(hass, entity_id=None):
|
|
""" Send the chromecast the command for play/pause. """
|
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
|
|
|
hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data)
|
|
|
|
|
|
def media_next_track(hass, entity_id=None):
|
|
""" Send the chromecast the command for next track. """
|
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
|
|
|
hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data)
|
|
|
|
|
|
def media_prev_track(hass, entity_id=None):
|
|
""" Send the chromecast the command for prev track. """
|
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
|
|
|
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data)
|
|
|
|
|
|
def setup_chromecast(casts, host):
|
|
""" Tries to convert host to Chromecast object and set it up. """
|
|
|
|
# Check if already setup
|
|
if any(cast.host == host for cast in casts.values()):
|
|
return
|
|
|
|
try:
|
|
cast = pychromecast.PyChromecast(host)
|
|
|
|
entity_id = util.ensure_unique_string(
|
|
ENTITY_ID_FORMAT.format(
|
|
util.slugify(cast.device.friendly_name)),
|
|
casts.keys())
|
|
|
|
casts[entity_id] = cast
|
|
|
|
except pychromecast.ChromecastConnectionError:
|
|
pass
|
|
|
|
|
|
def setup(hass, config):
|
|
# pylint: disable=unused-argument,too-many-locals
|
|
""" Listen for chromecast events. """
|
|
logger = logging.getLogger(__name__)
|
|
discovery = get_component('discovery')
|
|
|
|
try:
|
|
# pylint: disable=redefined-outer-name
|
|
import pychromecast
|
|
except ImportError:
|
|
logger.exception(("Failed to import pychromecast. "
|
|
"Did you maybe not install the 'pychromecast' "
|
|
"dependency?"))
|
|
|
|
return False
|
|
|
|
casts = {}
|
|
|
|
# If discovery component not loaded, scan ourselves
|
|
if discovery.DOMAIN not in hass.components:
|
|
logger.info("Scanning for Chromecasts")
|
|
hosts = pychromecast.discover_chromecasts()
|
|
|
|
for host in hosts:
|
|
setup_chromecast(casts, host)
|
|
|
|
def chromecast_discovered(service, info):
|
|
""" Called when a Chromecast has been discovered. """
|
|
logger.info("New Chromecast discovered: %s", info[0])
|
|
setup_chromecast(casts, info[0])
|
|
|
|
discovery.listen(
|
|
hass, discovery.services.GOOGLE_CAST, chromecast_discovered)
|
|
|
|
def update_chromecast_state(entity_id, chromecast):
|
|
""" Retrieve state of Chromecast and update statemachine. """
|
|
chromecast.refresh()
|
|
|
|
status = chromecast.app
|
|
|
|
state_attr = {ATTR_FRIENDLY_NAME:
|
|
chromecast.device.friendly_name}
|
|
|
|
if status and status.app_id != pychromecast.APP_ID['HOME']:
|
|
state = status.app_id
|
|
|
|
ramp = chromecast.get_protocol(pychromecast.PROTOCOL_RAMP)
|
|
|
|
if ramp and ramp.state != pychromecast.RAMP_STATE_UNKNOWN:
|
|
|
|
if ramp.state == pychromecast.RAMP_STATE_PLAYING:
|
|
state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_PLAYING
|
|
else:
|
|
state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_STOPPED
|
|
|
|
if ramp.content_id:
|
|
state_attr[ATTR_MEDIA_CONTENT_ID] = ramp.content_id
|
|
|
|
if ramp.title:
|
|
state_attr[ATTR_MEDIA_TITLE] = ramp.title
|
|
|
|
if ramp.artist:
|
|
state_attr[ATTR_MEDIA_ARTIST] = ramp.artist
|
|
|
|
if ramp.album:
|
|
state_attr[ATTR_MEDIA_ALBUM] = ramp.album
|
|
|
|
if ramp.image_url:
|
|
state_attr[ATTR_MEDIA_IMAGE_URL] = ramp.image_url
|
|
|
|
if ramp.duration:
|
|
state_attr[ATTR_MEDIA_DURATION] = ramp.duration
|
|
|
|
state_attr[ATTR_MEDIA_VOLUME] = ramp.volume
|
|
else:
|
|
state = STATE_NO_APP
|
|
|
|
hass.states.set(entity_id, state, state_attr)
|
|
|
|
def update_chromecast_states(time):
|
|
""" Updates all chromecast states. """
|
|
if casts:
|
|
logger.info("Updating Chromecast status")
|
|
|
|
for entity_id, cast in casts.items():
|
|
update_chromecast_state(entity_id, cast)
|
|
|
|
def _service_to_entities(service):
|
|
""" Helper method to get entities from service. """
|
|
entity_ids = extract_entity_ids(hass, service)
|
|
|
|
if entity_ids:
|
|
for entity_id in entity_ids:
|
|
cast = casts.get(entity_id)
|
|
|
|
if cast:
|
|
yield entity_id, cast
|
|
|
|
else:
|
|
yield from casts.items()
|
|
|
|
def turn_off_service(service):
|
|
""" Service to exit any running app on the specified ChromeCast and
|
|
shows idle screen. Will quit all ChromeCasts if nothing specified.
|
|
"""
|
|
for entity_id, cast in _service_to_entities(service):
|
|
cast.quit_app()
|
|
update_chromecast_state(entity_id, cast)
|
|
|
|
def volume_up_service(service):
|
|
""" Service to send the chromecast the command for volume up. """
|
|
for _, cast in _service_to_entities(service):
|
|
ramp = cast.get_protocol(pychromecast.PROTOCOL_RAMP)
|
|
|
|
if ramp:
|
|
ramp.volume_up()
|
|
|
|
def volume_down_service(service):
|
|
""" Service to send the chromecast the command for volume down. """
|
|
for _, cast in _service_to_entities(service):
|
|
ramp = cast.get_protocol(pychromecast.PROTOCOL_RAMP)
|
|
|
|
if ramp:
|
|
ramp.volume_down()
|
|
|
|
def media_play_pause_service(service):
|
|
""" Service to send the chromecast the command for play/pause. """
|
|
for _, cast in _service_to_entities(service):
|
|
ramp = cast.get_protocol(pychromecast.PROTOCOL_RAMP)
|
|
|
|
if ramp:
|
|
ramp.playpause()
|
|
|
|
def media_play_service(service):
|
|
""" Service to send the chromecast the command for play/pause. """
|
|
for _, cast in _service_to_entities(service):
|
|
ramp = cast.get_protocol(pychromecast.PROTOCOL_RAMP)
|
|
|
|
if ramp and ramp.state == pychromecast.RAMP_STATE_STOPPED:
|
|
ramp.playpause()
|
|
|
|
def media_pause_service(service):
|
|
""" Service to send the chromecast the command for play/pause. """
|
|
for _, cast in _service_to_entities(service):
|
|
ramp = cast.get_protocol(pychromecast.PROTOCOL_RAMP)
|
|
|
|
if ramp and ramp.state == pychromecast.RAMP_STATE_PLAYING:
|
|
ramp.playpause()
|
|
|
|
def media_next_track_service(service):
|
|
""" Service to send the chromecast the command for next track. """
|
|
for entity_id, cast in _service_to_entities(service):
|
|
ramp = cast.get_protocol(pychromecast.PROTOCOL_RAMP)
|
|
|
|
if ramp:
|
|
next(ramp)
|
|
update_chromecast_state(entity_id, cast)
|
|
|
|
def play_youtube_video_service(service, video_id):
|
|
""" Plays specified video_id on the Chromecast's YouTube channel. """
|
|
if video_id: # if service.data.get('video') returned None
|
|
for entity_id, cast in _service_to_entities(service):
|
|
pychromecast.play_youtube_video(video_id, cast.host)
|
|
update_chromecast_state(entity_id, cast)
|
|
|
|
hass.track_time_change(update_chromecast_states, second=range(0, 60, 15))
|
|
|
|
hass.services.register(DOMAIN, SERVICE_TURN_OFF,
|
|
turn_off_service)
|
|
|
|
hass.services.register(DOMAIN, SERVICE_VOLUME_UP,
|
|
volume_up_service)
|
|
|
|
hass.services.register(DOMAIN, SERVICE_VOLUME_DOWN,
|
|
volume_down_service)
|
|
|
|
hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE,
|
|
media_play_pause_service)
|
|
|
|
hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY,
|
|
media_play_service)
|
|
|
|
hass.services.register(DOMAIN, SERVICE_MEDIA_PAUSE,
|
|
media_pause_service)
|
|
|
|
hass.services.register(DOMAIN, SERVICE_MEDIA_NEXT_TRACK,
|
|
media_next_track_service)
|
|
|
|
hass.services.register(DOMAIN, "start_fireplace",
|
|
lambda service:
|
|
play_youtube_video_service(service, "eyU3bRy2x44"))
|
|
|
|
hass.services.register(DOMAIN, "start_epic_sax",
|
|
lambda service:
|
|
play_youtube_video_service(service, "kxopViU98Xo"))
|
|
|
|
hass.services.register(DOMAIN, SERVICE_YOUTUBE_VIDEO,
|
|
lambda service:
|
|
play_youtube_video_service(service,
|
|
service.data.get(
|
|
'video')))
|
|
|
|
update_chromecast_states(None)
|
|
|
|
return True
|