226 lines
8.1 KiB
Python
Executable File
226 lines
8.1 KiB
Python
Executable File
"""
|
|
Run DashCast semi-persistently on a Chromecast while allowing other
|
|
Chromecast apps to work also by only launching when idle.
|
|
"""
|
|
|
|
from __future__ import print_function
|
|
import time
|
|
import os
|
|
import sys
|
|
import logging
|
|
import json
|
|
import paho.mqtt.client as mqtt
|
|
|
|
import pychromecast
|
|
import pychromecast.controllers.dashcast as dashcast
|
|
|
|
|
|
print('DashCast')
|
|
print('Searching for Chromecasts...')
|
|
|
|
DEFAULT_DASHBOARD_URL = os.getenv('DEFAULT_DASHBOARD_URL', 'https://home-assistant.io')
|
|
DEFAULT_DASHBOARD_URL_FORCE = os.getenv('DEFAULT_DASHBOARD_URL_FORCE') == 'True'
|
|
DISPLAY_NAME = os.getenv('DISPLAY_NAME')
|
|
IGNORE_CEC = os.getenv('IGNORE_CEC') == 'True'
|
|
|
|
SUBSCRIBE = 'chromecast/' + str(DISPLAY_NAME) + '/command/dashcast'
|
|
MQTT_SERVER = os.getenv('MQTT_SERVER', 'iot.eclipse.org')
|
|
MQTT_USERNAME = os.getenv('MQTT_USERNAME')
|
|
MQTT_PASSWORD = os.getenv('MQTT_PASSWORD')
|
|
|
|
if IGNORE_CEC:
|
|
print('Ignoring CEC for Chromecast', DISPLAY_NAME)
|
|
pychromecast.IGNORE_CEC.append(DISPLAY_NAME)
|
|
|
|
|
|
if '--show-debug' in sys.argv:
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
|
|
|
|
class DashboardLauncher():
|
|
|
|
def __init__(self, device, dashboard_url='https://home-assistant.io',
|
|
dashboard_url_force=False, dashboard_app_name='DashCast'):
|
|
self.device = device
|
|
print('DashboardLauncher', self.device.name)
|
|
|
|
# register dashcast controller
|
|
self.controller = dashcast.DashCastController()
|
|
self.device.register_handler(self.controller)
|
|
|
|
# Breaking Change pychromecast 3.0
|
|
# Do not automatically start worker thread and connect in Chromecast constructor
|
|
# https://github.com/balloob/pychromecast/pull/271
|
|
self.device.wait()
|
|
|
|
# register status listener
|
|
receiver_controller = device.socket_client.receiver_controller
|
|
receiver_controller.register_status_listener(self)
|
|
|
|
# register connection listener
|
|
self.device.socket_client.media_controller.register_status_listener(self)
|
|
self.device.register_connection_listener(self)
|
|
|
|
# set default url and dashboard app name
|
|
self.dashboard_url = dashboard_url
|
|
self.dashboard_url_force = dashboard_url_force
|
|
self.dashboard_app_name = dashboard_app_name
|
|
self.takeover = False
|
|
|
|
# Check status on init.
|
|
self.new_cast_status(self.device.status)
|
|
# Launch dashboard on init.
|
|
self.launch_dashboard(self.dashboard_url, self.dashboard_url_force)
|
|
|
|
# While dashboard is launching, start MQTT connection
|
|
print("Starting MQTT")
|
|
client = mqtt.Client("client_" + str(DISPLAY_NAME)) # create new instance
|
|
client.on_message = self.on_message # attach function to callback
|
|
client.on_connect = self.on_connect
|
|
print("Connecting to Broker: " + MQTT_SERVER)
|
|
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
|
|
client.connect(MQTT_SERVER) # connect to broker
|
|
|
|
# wait for dashboard to finish loading, then start MQTT client loop
|
|
time.sleep(10)
|
|
client.loop_start()
|
|
|
|
# keep_looping is the control variable.
|
|
# If it is False, loop stops, program cleans up and exits
|
|
self.keep_looping = True
|
|
|
|
# if launch_it is set to true, the dashboard is relaunched
|
|
# otherwise sleep a second and check again
|
|
self.launch_it = False
|
|
while self.keep_looping:
|
|
if self.launch_it:
|
|
print('launch_it', self.launch_it)
|
|
self.launch_dashboard(self.dashboard_url, self.dashboard_url_force)
|
|
self.launch_it = False
|
|
time.sleep(10)
|
|
time.sleep(1)
|
|
|
|
client.loop_stop()
|
|
|
|
# define MQTT client callbacks
|
|
|
|
def on_connect(self, client, userdata, flags, rc):
|
|
print("Connected with result code "+str(rc))
|
|
# Subscribing in on_connect() means that if we lose the connection and
|
|
# reconnect then subscriptions will be renewed.
|
|
print("Subscribing to topic ", SUBSCRIBE)
|
|
client.subscribe(SUBSCRIBE)
|
|
|
|
def on_message(self, client, userdata, message):
|
|
print("Message received: ", str(message.payload.decode("utf-8")))
|
|
print("-Topic: ", message.topic)
|
|
print("-QOS: ", message.qos)
|
|
print("-Retain: ", message.retain)
|
|
|
|
# try decoding received message
|
|
try:
|
|
json_decode = str(message.payload.decode("utf-8", "ignore"))
|
|
print("Decoding Json")
|
|
parsed_json = json.loads(json_decode)
|
|
except json.decoder.JSONDecodeError:
|
|
print("Error passing JSON")
|
|
return
|
|
|
|
# set dashboard_url to received data and
|
|
# set to relaunch/update dashboard
|
|
print("Chromecast: " + DISPLAY_NAME)
|
|
if 'url' in parsed_json:
|
|
print("Url: " + parsed_json["url"])
|
|
self.dashboard_url = parsed_json["url"]
|
|
|
|
if 'force' in parsed_json:
|
|
print("Force: " + str(parsed_json["force"]))
|
|
self.dashboard_url_force = parsed_json["force"]
|
|
else:
|
|
self.dashboard_url_force = False
|
|
|
|
if 'takeover' in parsed_json:
|
|
print("Takeover: " + str(parsed_json["takeover"]))
|
|
self.takeover = parsed_json["takeover"]
|
|
else:
|
|
self.takeover = False
|
|
|
|
self.dashboard_url = parsed_json["url"]
|
|
|
|
if self.is_dashboard_active() or self.takeover:
|
|
self.launch_it = True
|
|
|
|
# Define pychromecast Callback Receivers
|
|
def new_cast_status(self, cast_status):
|
|
""" Called when a new cast status has been received. """
|
|
print('new_cast_status', self.device.name, cast_status)
|
|
|
|
def should_launch():
|
|
""" If the device is active, the dashboard is not already active, and no other app is active. """
|
|
print('should launch', self.is_device_active(),
|
|
not self.is_dashboard_active(), not self.is_other_app_active())
|
|
return (self.is_device_active()
|
|
and not self.is_dashboard_active()
|
|
and not self.is_other_app_active())
|
|
|
|
if should_launch():
|
|
self.launch_it = True
|
|
|
|
def new_media_status(self, media_status):
|
|
"""Called when a new MediaStatus is received."""
|
|
print('new_media_status', self.device.name, media_status)
|
|
|
|
def new_connection_status(self, connection_status):
|
|
"""Called when a new ConnectionStatus is received."""
|
|
print('new_connection_status', self.device.name, connection_status)
|
|
|
|
def is_device_active(self):
|
|
""" Returns if there is currently an app running and (maybe) visible. """
|
|
|
|
return (self.device.status is not None
|
|
#and self.device.app_id is not None
|
|
and (self.device.status.is_active_input or self.device.ignore_cec)
|
|
#and (not self.device.status.is_stand_by and not self.device.ignore_cec)
|
|
)
|
|
|
|
def is_dashboard_active(self):
|
|
""" Returns if the dashboard is (probably) visible. """
|
|
return self.device.app_display_name == self.dashboard_app_name
|
|
|
|
def is_other_app_active(self):
|
|
""" Returns if an app other than the dashboard or the Backdrop is (probably) visible. """
|
|
return self.device.app_display_name not in ('Backdrop', self.dashboard_app_name)
|
|
|
|
def launch_dashboard(self, url_to_load, force):
|
|
print('Launching dashboard on Chromecast', self.device.name)
|
|
|
|
def callback(response):
|
|
print('callback called', response)
|
|
|
|
try:
|
|
self.controller.load_url(url_to_load, force, callback_function=callback)
|
|
except Exception as e:
|
|
print(e)
|
|
pass
|
|
|
|
|
|
casts = pychromecast.get_chromecasts()
|
|
if len(casts) == 0:
|
|
print('No Devices Found')
|
|
exit()
|
|
|
|
for cc in casts:
|
|
print('enumerate-casts', cc.device.friendly_name)
|
|
|
|
cast = next(cc for cc in casts if DISPLAY_NAME in (None, '') or cc.device.friendly_name == DISPLAY_NAME)
|
|
|
|
if not cast:
|
|
print('Chromecast with name', DISPLAY_NAME, 'not found')
|
|
exit()
|
|
|
|
DashboardLauncher(cast, dashboard_url=DEFAULT_DASHBOARD_URL, dashboard_url_force=DEFAULT_DASHBOARD_URL_FORCE)
|
|
|
|
# Keep running
|
|
while True:
|
|
time.sleep(1)
|