From 7cc13278145effec5fd9f0c5bc8444d8723d6346 Mon Sep 17 00:00:00 2001 From: Bogdan Vlaicu Date: Fri, 3 Apr 2020 17:15:16 -0400 Subject: [PATCH] Dashcast add-on --- dashcast/Dockerfile | 26 +++++ dashcast/README.md | 2 + dashcast/app.sh | 4 + dashcast/app_mqtt.py | 225 +++++++++++++++++++++++++++++++++++++++++++ dashcast/config.json | 3 +- 5 files changed, 258 insertions(+), 2 deletions(-) create mode 100755 dashcast/Dockerfile create mode 100755 dashcast/app.sh create mode 100755 dashcast/app_mqtt.py diff --git a/dashcast/Dockerfile b/dashcast/Dockerfile new file mode 100755 index 0000000..1410c6e --- /dev/null +++ b/dashcast/Dockerfile @@ -0,0 +1,26 @@ +# This Dockerfile will only work with host networking enabled. + +# Use an official Python runtime as a parent image +FROM python:3 + +# Set the working directory to /app +WORKDIR /app + +# Copy the current directory contents into the container at /app +ADD . /app + +# Install any needed packages specified in requirements.txt +RUN pip install -r requirements.txt + +# Define environment variables +ENV DEFAULT_DASHBOARD_URL "https://darksky.net" +ENV DEFAULT_DASHBOARD_URL_FORCE "False" +ENV DISPLAY_NAME "" +ENV IGNORE_CEC "True" + +ENV MQTT_SERVER "iot.eclipse.org" +ENV MQTT_USERNAME "" +ENV MQTT_PASSWORD "" + +# Run app.sh when the container launches +CMD ["./app.sh"] diff --git a/dashcast/README.md b/dashcast/README.md index de67c74..db8cc62 100644 --- a/dashcast/README.md +++ b/dashcast/README.md @@ -5,6 +5,8 @@ This is a Dashcast add-on for Home Assistant. Dascast is an app that displays dashboard web pages on your Chromecast device. +Credit to [ragingcomputer/dashcast-docker-MQTT](https://github.com/ragingcomputer/dashcast-docker-MQTT) for the docker code. + ## Installation The installation of this add-on is pretty straightforward and not different in diff --git a/dashcast/app.sh b/dashcast/app.sh new file mode 100755 index 0000000..f699f7d --- /dev/null +++ b/dashcast/app.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Keep the python app alive. +while true; do python -u app_mqtt.py --show-debug; done diff --git a/dashcast/app_mqtt.py b/dashcast/app_mqtt.py new file mode 100755 index 0000000..e824d91 --- /dev/null +++ b/dashcast/app_mqtt.py @@ -0,0 +1,225 @@ +""" +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) diff --git a/dashcast/config.json b/dashcast/config.json index 30b300e..8d99b3a 100644 --- a/dashcast/config.json +++ b/dashcast/config.json @@ -1,6 +1,6 @@ { "name": "Dashcast", - "version": "latest", + "version": "0.0.1", "slug": "dashcast", "description": "Display dashboard web pages on your Chromecast device", "url": "https://github.com/ragingcomputer/dashcast-docker-MQTT", @@ -23,7 +23,6 @@ "MQTT_USERNAME": "mqtt", "MQTT_PASSWORD": "testpassword" }, - "image": "ragingcomputer/dashcast-docker-mqtt", "options": { "log_level": "info", "chromecast_name": "Livingroom Chromecast",