notifications to chromecast

Alexa, Google Home and Siri
jeroentje
Posts: 8
Joined: Thursday 01 June 2017 16:22
Target OS: Raspberry Pi
Domoticz version:
Contact:

Re: notifications to chromecast

Post by jeroentje » Wednesday 12 September 2018 17:14

hi, another question.

After sending a text or sound, the volume on the google mini stays the volume set on the stream2chromecast script.

Anyone an idea to ask on google home what the current volume is in the script, so the script can put the initial volume back after sending the text/sound?

thanks,

User avatar
thecosmicgate
Posts: 260
Joined: Monday 06 April 2015 14:37
Target OS: Linux
Domoticz version: newest
Location: The Netherlands / Hoeven
Contact:

Re: notifications to chromecast

Post by thecosmicgate » Saturday 15 September 2018 15:46

jeroentje wrote:Hi,

same issue here. new google mini.
stream2chromecast stays "sending data" and takes 10 mins to stop.
The file is played well. I also use the french script with google TTS.

I've tried a lot (different versions of the .py files about messages and .py script and all the common solutions found in google for stream2chromecast), but still no solution.

Anyone ? " Sending data" hangs, you have to CNTL-C to stop:

Notification : testje
Volume : 0.2
Chromecast online
ip_addr: 192.168.0.200 device name:

-----------------------------------------

Stream2Chromecast version:0.6.3

Copyright (C) 2014-2016 Pat Carter
GNU General Public License v3.0
https://www.gnu.org/licenses/gpl-3.0.html

-----------------------------------------

ip_addr: 192.168.0.200 device name:
source is file: /tmp/message.mp3
local ip address: 192.168.0.202
OS identifies the mimetype as : audio/mpeg
URL & content-type: http://192.168.0.202:41791?/tmp/message.mp3 audio/mpeg
loading media...
192.168.0.200 - - [09/Sep/2018 16:18:03] "GET /?/tmp/message.mp3 HTTP/1.1" 200 -
sending data

and then it hangs :-(

Any help is appriciated !
So what's the working script you're using ?

Sent from my ONEPLUS A6003 using Tapatalk

It's nice to be important, but it's more important to be nice
Scooter ;)

glsf91
Posts: 38
Joined: Tuesday 14 November 2017 22:56
Target OS: Linux
Domoticz version:
Contact:

Re: notifications to chromecast

Post by glsf91 » Sunday 16 September 2018 17:43

jeroentje wrote:
Sunday 09 September 2018 16:21
Hi,

same issue here. new google mini.
stream2chromecast stays "sending data" and takes 10 mins to stop.
The file is played well. I also use the french script with google TTS.

I've tried a lot (different versions of the .py files about messages and .py script and all the common solutions found in google for stream2chromecast), but still no solution.

Anyone ? " Sending data" hangs, you have to CNTL-C to stop:

I think this is related to a firmware upgrade in june on the chromecast. I have the same problem I see.
But after changing the code a little bit it is working again. I'am not sure it is the right solution but it is working again for me.
I did not tested a lot.

Change this in cc_media_controller.py:

Code: Select all

def is_idle(self):
        """ return the IDLE state of the player """
        
        status = self.get_status()
        
        if status['media_status']  is None:
            if status['receiver_status'] is None:
                return True
            else:    
                return status['receiver_status'].get("statusText", "") == u"Ready To Cast"

        else:    
            return status['media_status'].get("playerState", "") == u"IDLE"
to this:

Code: Select all

def is_idle(self):
        """ return the IDLE state of the player """
        
        status = self.get_status()
        
        if status['media_status']  is None:
                return True
        else:    
            return status['media_status'].get("playerState", "") == u"IDLE"
Also working on my google mini.

User avatar
sincze
Posts: 988
Joined: Monday 02 June 2014 22:46
Target OS: Raspberry Pi
Domoticz version: 4.9700
Location: Netherlands
Contact:

Re: notifications to chromecast

Post by sincze » Sunday 16 September 2018 18:09

glsf91 wrote:
Sunday 16 September 2018 17:43
Spoiler: show

Code: Select all

def is_idle(self):
        """ return the IDLE state of the player """
        
        status = self.get_status()
        
        if status['media_status']  is None:
                return True
        else:    
            return status['media_status'].get("playerState", "") == u"IDLE"
Thanks for the update modified my code and indeed running smoothly without errors.
Using Pass2php since 2016-12
LAN: RFLink, P1-Port, OTGW, MySensors
USB: RFXCom, ZWave
WIFI: Mi-light Wifi-Bridge, Sonoff, ESP8266, Xiaomi Gateway
Solar: Omnik Inverter, PVOutput
Video: Kodi clients with Harmony HUB
Sensors: You name it I probably got 1.

mark.sellwood
Posts: 55
Joined: Tuesday 04 March 2014 11:33
Target OS: Raspberry Pi
Domoticz version:
Location: Surrey, UK
Contact:

Re: notifications to chromecast

Post by mark.sellwood » Wednesday 19 September 2018 22:13

Hi have made the change to cc_media_controller.py but still it hangs after the message sending data. I do hear the message.

Any suggestions?
3 x Pi, 1 Master, 2 Slaves, 1x Aeotec Z-Stick S2, 4xSP103 PIR, 5xPowerNode 1, 1xSmart Energy Switch Gen5, 4xFGSS101 Smoke Sensor, 2xFGD212, 9xFGS212 , 7xFGS221/2, 1xAD142 , 1xTKB TZ68E , 2xAeotec Multi Sensor, 3 x NodOn CRC-3-1-00.

glsf91
Posts: 38
Joined: Tuesday 14 November 2017 22:56
Target OS: Linux
Domoticz version:
Contact:

Re: notifications to chromecast

Post by glsf91 » Saturday 22 September 2018 14:42

mark.sellwood wrote:
Wednesday 19 September 2018 22:13
Hi have made the change to cc_media_controller.py but still it hangs after the message sending data. I do hear the message.

Any suggestions?
I also have sometime a hanging script. So I switched to pychromecast.
Warning: the programming below is maybe awfull but it is working (I'am not a programmer).

I use this with viewtopic.php?f=69&t=22610#p175077.

Added a script play_chromecast.py:

Code: Select all

from __future__ import print_function
import time
import pychromecast
import threading
from time import sleep
import sys


args = sys.argv[1:]

if len(args) < 2:
    sys.exit("Usage: notification_google_home <text> <volume 0..1>")

messageUrl=args[0]
volume=float(args[1])

print("MessageUrl: ", messageUrl)
print("Volume: ", volume)


# Your Chromecast device Friendly Name
device_friendly_name = "Family room speaker"

chromecasts = pychromecast.get_chromecasts()

# select Chromecast device
cast = next(cc for cc in chromecasts if cc.device.friendly_name == device_friendly_name)

# wait for the device
cast.wait()
#print(cast.device)
#print(cast.status)

cast.set_volume(volume)
sleep(0.3)
#print(cast.status)

# get media controller
mc = cast.media_controller

start_playing = False

completion = threading.Event()

class StatusMediaListener:
    def __init__(self, name, cast):
        self.name = name
        self.cast= cast

    def new_media_status(self, status):
#        print('[',time.ctime(),' - ', self.name,'] status media change:')
#        print(status)
        if (status.player_is_idle and start_playing):
             completion.set()

listenerMedia = StatusMediaListener(cast.name, cast)
cast.media_controller.register_status_listener(listenerMedia)


# set online video url
mc.play_media(messageUrl, 'audio/mp3')

# blocks device
mc.block_until_active()
#print(mc.status)

# plays the video
mc.play()

start_playing = True

# poll so signal handlers still work
while not completion.wait(0.5):
        pass

Change device_friendly_name to yours.

and changed notification_chromecast.sh to:

Code: Select all

#!/bin/bash
# notification_google_home.sh Script de notification de message vocal sur la Google Home
# by JS Martin - 11/02/2018 - version 0.1

message=$1 # text message
volume=$2  # 0=auto 0.1=10% 1=100%
jingle=$3  # jingle track number (0=no track 1=default track)

echo "Started with: " $1 $2 $3

# ------ parameters ---------

# Autoset volume if  volume=0
Start_day="0700"
Start_night="2200"
Night_vol="0.4"
Day_vol="0.7"

# number of arg correction
case "$#" in
"1")
    volume="0"
    jingle="1"
    ;;
"2")
    jingle="1"
    ;;
*)
    echo "Usage: notification_google_home <text> [volume 0..1]"
    exit 1
    ;;
esac


# IP Google Home
IPGH="192.168.1.173"


echo "Notification : "$message

if [ $volume != "0" ]; then
   echo "Volume : "$volume
else
   CUR_TIME=`date +%H%M`
   if [ $CUR_TIME -ge $Start_day -a $CUR_TIME -le $Start_night ]; then
      echo "Day volume"
      volume=$Day_vol
   else
      echo "Night volume"
      volume=$Night_vol
   fi
   echo "Volume = automatique - set to "$volume
fi

# Exit if chromecast not online
ping -c1 -W 2 $IPGH >/dev/null
if [ $? -eq 0 ]
then echo "Chromecast online"
else echo "Chromecast offline"
exit 0
fi

if [ -f /tmp/message.mp3 ]
then
  rm /tmp/message.mp3
fi

#Text to MP3; afgekeken uit izSynth
#curl -s -G "http://api.naturalreaders.com/v4/tts/macspeak" --data "apikey=b98x9xlfs54ws4k0wc0o8g4gwc0w8ss&src=pw&r=22&s=1" --data-urlencode "t=$message" -o /tmp/message.mp3

# google gebruiken
#if [ ${#message} -lt 150 ]
#then
#  curl -s -G "http://translate.google.com/translate_tts" --data "ie=UTF-8&total=1&idx=0&client=tw-ob&&tl=nl-NL" --data-urlencode "q=$message" -A "Mozilla" --compressed -o /tmp/message.mp3
#else
#  /home/john/.local/bin/gtts-cli "$message"  -l 'nl' -o /tmp/message.mp3
#fi

#Microsoft Bing Speech API. Via Azure account john.brattinga@live.nl; free 5000 per maand max 5 per sec.
python /home/john/bingSpeech/BingTTSGen.py --cache=/home/john/bingSpeech/cache --dest=/tmp/message.mp3 --lang=nl-NL  --voice="HannaRUS"  --fileformat=audio-16khz-32kbitrate-mono-mp3  --apikey=c28d318e36b2422cb88f216bc63b0a4b  --text=''"$message"''


if [ -f /tmp/message.mp3 ]
then

# Problemen met stream2chromecast vanwege update chromecast juli 2018
  #Set Google Home volume
#  sudo python /home/john/stream2chromecast/stream2chromecast/stream2chromecast.py -devicename $IPGH -setvol $volume

  #MP3 to Google Home
#  sudo python /home/john/stream2chromecast/stream2chromecast/stream2chromecast.py -devicename $IPGH /tmp/message.mp3

   cp /tmp/message.mp3 /home/john/domoticz/www/message.mp3
   python3 /home/john/pychromecast/play_chromecast.py http://192.168.1.159:8080/message.mp3 $volume

fi

if [ -f /tmp/message.mp3 ]
then
  rm /tmp/message.mp3
fi
Of course adjust the path of the scripts and domoticz url in the last script to yours.
I think this works better but I did not test for a long time.

You need python 3.4+ (check with command: python3)
Install pychromecast with:
apt-get install python3-pip (if you don't have python3 pip)
python3 -m pip install pychromecast --user

User avatar
sincze
Posts: 988
Joined: Monday 02 June 2014 22:46
Target OS: Raspberry Pi
Domoticz version: 4.9700
Location: Netherlands
Contact:

Re: notifications to chromecast

Post by sincze » Sunday 23 September 2018 19:59

I thought... I had a good idea to move all my IOT stuff to a seperate VLAN.
My Google Chromecasts, Google Home , LG Smart TV's you name it.

After some modifications it all works.
Domoticz can reach the devices and control them, read the status.
... However... what is not working.. Stream2chromecast.

Code: Select all

-----------------------------------------

Stream2Chromecast version:0.6.3

Copyright (C) 2014-2016 Pat Carter
GNU General Public License v3.0
https://www.gnu.org/licenses/gpl-3.0.html

-----------------------------------------

ip_addr: 192.168.yy.yy device name:
source is file: /tmp/google/2f55628619d7d0366ece864b2cdf67d8.mp3
local ip address: 192.168.xx.xx
OS identifies the mimetype as : audio/mpeg
URL & content-type:  http://192.168.xx.xx:39121?/tmp/google/2f55628619d7d0366ece864b2cdf67d8.mp3 audio/mpeg
loading media...
I opened op the router VLAN Firewall (TCP & UDP) so the google home can reach the Domoticz machine where stream2chromecast is running from. However it seems it is unable to collect the file. It does adjust the playback volume to the correct level.
Where 192.168.yy.yy is the Google Home and 192.168.xx.xx is the Domoticz machine.

Code: Select all

user@raspberry-user:~ $ sudo iptables --list
iptables v1.6.0: can't initialize iptables table `filter': Table does not exist (do you need to insmod?)
Perhaps iptables or your kernel needs to be upgraded.
What did I miss :lol:
Using Pass2php since 2016-12
LAN: RFLink, P1-Port, OTGW, MySensors
USB: RFXCom, ZWave
WIFI: Mi-light Wifi-Bridge, Sonoff, ESP8266, Xiaomi Gateway
Solar: Omnik Inverter, PVOutput
Video: Kodi clients with Harmony HUB
Sensors: You name it I probably got 1.

User avatar
thecosmicgate
Posts: 260
Joined: Monday 06 April 2015 14:37
Target OS: Linux
Domoticz version: newest
Location: The Netherlands / Hoeven
Contact:

Re: notifications to chromecast

Post by thecosmicgate » Friday 19 October 2018 21:29

So what's the working script for now ?
Can't get this working . Still hangs .
Could anybody share his script or way how it's working

Verstuurd vanaf mijn ONEPLUS A6003 met Tapatalk

It's nice to be important, but it's more important to be nice
Scooter ;)

User avatar
sincze
Posts: 988
Joined: Monday 02 June 2014 22:46
Target OS: Raspberry Pi
Domoticz version: 4.9700
Location: Netherlands
Contact:

Re: notifications to chromecast

Post by sincze » Saturday 20 October 2018 18:17

thecosmicgate wrote:
Friday 19 October 2018 21:29
So what's the working script for now ?
Can't get this working . Still hangs .
Could anybody share his script or way how it's working

Verstuurd vanaf mijn ONEPLUS A6003 met Tapatalk
This stream2chromecast is the script I am using without any problems.(after modifying the cc_media_controller.py as well.
Spoiler: show

Code: Select all

#!/usr/bin/env python
"""
stream2chromecast.py: Chromecast media streamer for Linux

author: Pat Carter - https://github.com/Pat-Carter/stream2chromecast

version: 0.6.3

"""


# Copyright (C) 2014-2016 Pat Carter
#
# This file is part of Stream2chromecast.
#
# Stream2chromecast is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Stream2chromecast is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Stream2chromecast.  If not, see <http://www.gnu.org/licenses/>.


VERSION = "0.6.3"


import sys, os, errno
import signal

from cc_media_controller import CCMediaController
import cc_device_finder
import time

import BaseHTTPServer
import urllib
import mimetypes
from threading import Thread

import subprocess

import httplib
import urlparse

import socket

import tempfile



script_name = (sys.argv[0].split(os.sep))[-1]

USAGETEXT = """
Usage

Play a file:-
    %s <file>
    

Pause the current file:-
    %s -pause


Continue (un-pause) the current file:-
    %s -continue

        
Stop the current file playing:-
    %s -stop


Set the volume to a value between 0 & 1.0  (e.g. 0.5 = half volume):-
    %s -setvol <volume>


Adjust the volume up or down by 0.1:-
    %s -volup
    %s -voldown
    

Mute the volume:-
    %s -mute
    
           
Play an unsupported media type (e.g. an mpg file) using ffmpeg or avconv as a realtime transcoder (requires ffmpeg or avconv to be installed):-
    %s -transcode <file> 


Play remote file using a URL (e.g. a web video):
    %s -playurl <URL>

    
Display Chromecast status:-
    %s -status    
    
    
Search for all Chromecast devices on the network:-
    %s -devicelist
    
    
Additional option to specify an Chromecast device by name (or ip address) explicitly:
    e.g. to play a file on a specific device
    %s -devicename <chromecast device name> <file>
    
    
Additional option to specify the preferred transcoder tool when both ffmpeg & avconv are available
    e.g. to play and transcode a file using avconv
    %s -transcoder avconv -transcode <file>
    
    
Additional option to specify the port from which the media is streamed. This can be useful in a firewalled environment.
    e.g. to serve the media on port 8765
    %s -port 8765 <file>


Additional option to specify subtitles. Only WebVTT format is supported.
    e.g. to cast the subtitles on /path/to/subtitles.vtt
    %s -subtitles /path/to/subtitles.vtt <file>


Additional option to specify the port from which the subtitles is streamed. This can be useful in a firewalled environment.
    e.g. to serve the subtitles on port 8765
    %s -subtitles_port 8765 <file>


Additional option to specify the subtitles language. The language format is defined by RFC 5646.
    e.g. to serve the subtitles french subtitles
    %s -subtitles_language fr <file>

    
Additional option to supply custom parameters to the transcoder (ffmpeg or avconv) output
    e.g. to transcode the media with an output video bitrate of 1000k
    %s -transcode -transcodeopts '-b:v 1000k' <file>

    
Additional option to supply custom parameters to the transcoder input
    e.g. to transcode the media and seek to a position 15 minutes from the start of playback
    %s -transcode -transcodeinputopts '-ss 00:15:00' <file>
    
    
Additional option to specify the buffer size of the data returned from the transcoder. Increasing this can help when on a slow network.
    e.g. to specify a buffer size of 5 megabytes
    %s -transcode -transcodebufsize 5242880 <file>
    
""" % ((script_name,) * 21)


"""" 29-04-2018 Modified PID file location so www-data is able to write to the directory """
GOOGLE = os.path.join(tempfile.gettempdir(),"google/")
"""PIDFILE = os.path.join(tempfile.gettempdir(),"stream2chromecast_%s.pid")  """
PIDFILE = os.path.join(GOOGLE,"stream2chromecast_%s.pid") 

FFMPEG = 'ffmpeg %s -i "%s" -preset ultrafast -f mp4 -frag_duration 3000 -b:v 2000k -loglevel error %s -'
AVCONV = 'avconv %s -i "%s" -preset ultrafast -f mp4 -frag_duration 3000 -b:v 2000k -loglevel error %s -'



class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    content_type = "video/mp4"
    
    """ Handle HTTP requests for files which do not need transcoding """
    
    def do_GET(self):
        
        query = self.path.split("?",1)[-1]
        filepath = urllib.unquote_plus(query)
        
        self.suppress_socket_error_report = None
        
        self.send_headers(filepath)       
        
        print "sending data"      
        try: 
            self.write_response(filepath)
        except socket.error, e:     
            if isinstance(e.args, tuple):
                if e[0] in (errno.EPIPE, errno.ECONNRESET):
                   print "disconnected"
                   self.suppress_socket_error_report = True
                   return
            
            raise


    def handle_one_request(self):
        try:
            return BaseHTTPServer.BaseHTTPRequestHandler.handle_one_request(self)
        except socket.error:
            if not self.suppress_socket_error_report:
                raise


    def finish(self):
        try:
            return BaseHTTPServer.BaseHTTPRequestHandler.finish(self)
        except socket.error:
            if not self.suppress_socket_error_report:
                raise


    def send_headers(self, filepath):
        self.protocol_version = "HTTP/1.1"
        self.send_response(200)
        self.send_header("Content-type", self.content_type)
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header("Transfer-Encoding", "chunked")
        self.end_headers()    


    def write_response(self, filepath):
        with open(filepath, "rb") as f:           
            while True:
                line = f.read(1024)
                if len(line) == 0:
                    break
            
                chunk_size = "%0.2X" % len(line)
                self.wfile.write(chunk_size)
                self.wfile.write("\r\n")
                self.wfile.write(line) 
                self.wfile.write("\r\n")  
                
        self.wfile.write("0")
        self.wfile.write("\r\n\r\n")                             



class TranscodingRequestHandler(RequestHandler):
    """ Handle HTTP requests for files which require realtime transcoding with ffmpeg """
    transcoder_command = FFMPEG
    transcode_options = ""
    transcode_input_options = ""    
    bufsize = 0
                    
    def write_response(self, filepath):
        if self.bufsize != 0:
            print "transcode buffer size:", self.bufsize
        
        ffmpeg_command = self.transcoder_command % (self.transcode_input_options, filepath, self.transcode_options) 
        
        ffmpeg_process = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE, shell=True, bufsize=self.bufsize)       

        for line in ffmpeg_process.stdout:
            chunk_size = "%0.2X" % len(line)
            self.wfile.write(chunk_size)
            self.wfile.write("\r\n")
            self.wfile.write(line) 
            self.wfile.write("\r\n")            
            
        self.wfile.write("0")
        self.wfile.write("\r\n\r\n")



class SubRequestHandler(RequestHandler):
    """ Handle HTTP requests for subtitles files """
    content_type = "text/vtt;charset=utf-8"



            
def get_transcoder_cmds(preferred_transcoder=None):
    """ establish which transcoder utility to use depending on what is installed """
    probe_cmd = None
    transcoder_cmd = None
    
    ffmpeg_installed = is_transcoder_installed("ffmpeg")
    avconv_installed = is_transcoder_installed("avconv")  
    
    # if anything other than avconv is preferred, try to use ffmpeg otherwise use avconv    
    if preferred_transcoder != "avconv":
        if ffmpeg_installed:
            transcoder_cmd = "ffmpeg"
            probe_cmd = "ffprobe"
        elif avconv_installed:
            print "unable to find ffmpeg - using avconv"
            transcoder_cmd = "avconv"
            probe_cmd = "avprobe"
    
    # otherwise, avconv is preferred, so try to use avconv, followed by ffmpeg  
    else:
        if avconv_installed:
            transcoder_cmd = "avconv"
            probe_cmd = "avprobe"
        elif ffmpeg_installed:
            print "unable to find avconv - using ffmpeg"
            transcoder_cmd = "ffmpeg"
            probe_cmd = "ffprobe"
            
    return transcoder_cmd, probe_cmd
    
    
                

def is_transcoder_installed(transcoder_application):
    """ check for an installation of either ffmpeg or avconv """
    try:
        subprocess.check_output([transcoder_application, "-version"])
        return True
    except OSError:
        return False
       



def kill_old_pid(device_ip):
    """ attempts to kill a previously running instance of this application casting to the specified device. """
    pid_file = PIDFILE % device_ip
    try:
        with open(pid_file, "r") as pidfile:
            pid = int(pidfile.read())
            os.killpg(pid, signal.SIGTERM)    
    except:
        pass
               


def save_pid(device_ip):
    """ saves the process id of this application casting to the specified device in a pid file. """
    pid_file = PIDFILE % device_ip
    with open(pid_file, "w") as pidfile:
        pidfile.write("%d" %  os.getpid())




def get_mimetype(filename, ffprobe_cmd=None):
    """ find the container format of the file """
    # default value
    mimetype = "video/mp4"
    
    
    # guess based on filename extension
    guess = mimetypes.guess_type(filename)[0]
    if guess is not None:
        if guess.lower().startswith("video/") or guess.lower().startswith("audio/"):
            mimetype = guess
      
        
    # use the OS file command...
    try:
        file_cmd = 'file --mime-type -b "%s"' % filename
        file_mimetype = subprocess.check_output(file_cmd, shell=True).strip().lower()
        
        if file_mimetype.startswith("video/") or file_mimetype.startswith("audio/"):
            mimetype = file_mimetype
            
            print "OS identifies the mimetype as :", mimetype
            return mimetype
    except:
        pass
    
    
    # use ffmpeg/avconv if installed
    if ffprobe_cmd is None:
        return mimetype
    
    # ffmpeg/avconv is installed
    has_video = False
    has_audio = False
    format_name = None
    
    ffprobe_cmd = '%s -show_streams -show_format "%s"' % (ffprobe_cmd, filename)
    ffmpeg_process = subprocess.Popen(ffprobe_cmd, stdout=subprocess.PIPE, shell=True)

    for line in ffmpeg_process.stdout:
        if line.startswith("codec_type=audio"):
            has_audio = True
        elif line.startswith("codec_type=video"):
            has_video = True    
        elif line.startswith("format_name="):
            name, value = line.split("=")
            format_name = value.strip().lower().split(",")


    # use the default if it isn't possible to identify the format type
    if format_name is None:
        return mimetype
    
    
    if has_video:
        mimetype = "video/"
    else:
        mimetype = "audio/"
        
    if "mp4" in format_name:
        mimetype += "mp4"            
    elif "webm" in format_name:
        mimetype += "webm"
    elif "ogg" in format_name:
        mimetype += "ogg"        
    elif "mp3" in format_name:
        mimetype = "audio/mpeg"
    elif "wav" in format_name:
        mimetype = "audio/wav" 
    else:   
        mimetype += "mp4"     
        
    return mimetype
    
            
            
def play(filename, transcode=False, transcoder=None, transcode_options=None, transcode_input_options=None,
         transcode_bufsize=0, device_name=None, server_ip=None, server_port=None, server_external_port=None,
         subtitles=None, subtitles_port=None, subtitles_language=None):
    """ play a local file or transcode from a file or URL and stream to the chromecast """
    
    print_ident()
    
    
    cast = CCMediaController(device_name=device_name)
    
    kill_old_pid(cast.host)
    save_pid(cast.host)    


    if os.path.isfile(filename):
        filename = os.path.abspath(filename)
        print "source is file: %s" % filename
    else:
        if transcode and (filename.lower().startswith("http://") or filename.lower().startswith("https://") or filename.lower().startswith("rtsp://")):
            print "source is URL: %s" % filename
        else: 
            sys.exit("media file %s not found" % filename)
        

    
    transcoder_cmd, probe_cmd = get_transcoder_cmds(preferred_transcoder=transcoder)
    

    status = cast.get_status()
    webserver_ip = status['client'][0]
    
    if server_ip is None:
	server_ip = webserver_ip
    
    print "local ip address:", webserver_ip
        
    
    req_handler = RequestHandler
    
    if transcode:
        if transcoder_cmd in ("ffmpeg", "avconv"):
            req_handler = TranscodingRequestHandler
            
            if transcoder_cmd == "ffmpeg":  
                req_handler.transcoder_command = FFMPEG
            else:
                req_handler.transcoder_command = AVCONV
                
            if transcode_options is not None:    
                req_handler.transcode_options = transcode_options
                
            if transcode_input_options is not None:    
                req_handler.transcode_input_options = transcode_input_options                
                
            req_handler.bufsize = transcode_bufsize
        else:
            print "No transcoder is installed. Attempting standard playback"
   
    
    
    if req_handler == RequestHandler:
        req_handler.content_type = get_mimetype(filename, probe_cmd)
        
    
    # create a webserver to handle a single request for the media file on either a free port or on a specific port if passed in the port parameter   
    port = 0    
    
    if server_port is not None:
        port = int(server_port)
        
    server = BaseHTTPServer.HTTPServer((webserver_ip, port), req_handler)
    
    if server_external_port is None:
    	server_external_port = server.server_port

    thread = Thread(target=server.handle_request)
    thread.start()


    url = "http://%s:%s?%s" % (server_ip, str(server_external_port), urllib.quote_plus(filename, "/"))

    print "URL & content-type: ", url, req_handler.content_type


    # create another webserver to handle a request for the subtitles file, if specified in the subtitles parameter
    sub = None

    if subtitles:
        if os.path.isfile(subtitles):
            sub_port = 0

            if subtitles_port is not None:
                sub_port = int(subtitles_port)

            sub_server = BaseHTTPServer.HTTPServer((webserver_ip, sub_port), SubRequestHandler)
            thread2 = Thread(target=sub_server.handle_request)
            thread2.start()

            sub = "http://%s:%s?%s" % (webserver_ip, str(sub_server.server_port), urllib.quote_plus(subtitles, "/"))
            print "sub URL: ", sub
        else:
            print "Subtitles file %s not found" % subtitles


    load(cast, url, req_handler.content_type, sub, subtitles_language)

    
    

def load(cast, url, mimetype, sub=None, sub_language=None):
    """ load a chromecast instance with a url and wait for idle state """
    try:
        print "loading media..."
        
        cast.load(url, mimetype, sub, sub_language)
        
        # wait for playback to complete before exiting
        print "waiting for player to finish - press ctrl-c to stop..."    
        
        idle = False
        while not idle:
            time.sleep(1)
            idle = cast.is_idle()
   
    except KeyboardInterrupt:
        print
        print "stopping..."
        cast.stop()
        
    finally:
        print "done"
    
    
    
def playurl(url, device_name=None):
    """ play a remote HTTP resource on the chromecast """
    
    print_ident()

    def get_resp(url):
        url_parsed = urlparse.urlparse(url)
    
        scheme = url_parsed.scheme
        host = url_parsed.netloc
        path = url.split(host, 1)[-1]
        
        conn = None
        if scheme == "https":
            conn = httplib.HTTPSConnection(host)
        else:
            conn = httplib.HTTPConnection(host)
        
        conn.request("HEAD", path)
    
        resp = conn.getresponse()
        return resp


    def get_full_url(url, location):
        url_parsed = urlparse.urlparse(url)

        scheme = url_parsed.scheme
        host = url_parsed.netloc

        if location.startswith("/") is False:
            path = url.split(host, 1)[-1] 
            if path.endswith("/"):
                path = path.rsplit("/", 2)[0]
            else:
                path = path.rsplit("/", 1)[0] + "/"
            location = path + location

        full_url = scheme + "://" + host + location

        return full_url


    resp = get_resp(url)

    if resp.status != 200:
        redirect_codes = [ 301, 302, 303, 307, 308 ]
        if resp.status in redirect_codes:
            redirects = 0
            while resp.status in redirect_codes:
                redirects += 1
                if redirects > 9:
                    sys.exit("HTTP Error: Too many redirects")
                headers = resp.getheaders()
                for header in headers:
                    if len(header) > 1:
                        if header[0].lower() == "location":
                            redirect_location = header[1]
                if redirect_location.startswith("http") is False:
                    redirect_location = get_full_url(url, redirect_location)
                print "Redirecting to " + redirect_location
                resp = get_resp(redirect_location)
            if resp.status != 200:
                sys.exit("HTTP error:" + str(resp.status) + " - " + resp.reason)
        else:
            sys.exit("HTTP error:" + str(resp.status) + " - " + resp.reason)
        
    print "Found HTTP resource"
    
    headers = resp.getheaders()
    
    mimetype = None
    
    for header in headers:
        if len(header) > 1:
            if header[0].lower() == "content-type":
                mimetype = header[1]
    
    if mimetype != None:            
        print "content-type:", mimetype
    else:
        mimetype = "video/mp4"
        print "resource does not specify mimetype - using default:", mimetype
    
    cast = CCMediaController(device_name=device_name)
    load(cast, url, mimetype)    
    

            
    
def pause(device_name=None):
    """ pause playback """
    CCMediaController(device_name=device_name).pause()


def unpause(device_name=None):
    """ continue playback """
    CCMediaController(device_name=device_name).play()    

        
def stop(device_name=None):
    """ stop playback and quit the media player app on the chromecast """
    CCMediaController(device_name=device_name).stop()

def get_volume(device_name=None):
    """ print the status of the chromecast device """
    print CCMediaController(device_name=device_name).get_volume()

def get_status(device_name=None):
    """ print the status of the chromecast device """
    print CCMediaController(device_name=device_name).get_status()

def volume_up(device_name=None):
    """ raise the volume by 0.1 """
    CCMediaController(device_name=device_name).set_volume_up()


def volume_down(device_name=None):
    """ lower the volume by 0.1 """
    CCMediaController(device_name=device_name).set_volume_down()


def set_volume(v, device_name=None):
    """ set the volume to level between 0 and 1 """
    CCMediaController(device_name=device_name).set_volume(v)
    
    
def list_devices():
    print "Searching for devices, please wait..."
    device_ips = cc_device_finder.search_network(device_limit=None, time_limit=10)
    
    print "%d devices found" % len(device_ips)
    
    for device_ip in device_ips:
        print device_ip, ":", cc_device_finder.get_device_name(device_ip)
        

def print_ident():
    """ display initial messages """
    print
    print "-----------------------------------------"   
    print     
    print "Stream2Chromecast version:%s" % VERSION        
    print 
    print "Copyright (C) 2014-2016 Pat Carter"
    print "GNU General Public License v3.0" 
    print "https://www.gnu.org/licenses/gpl-3.0.html"
    print    
    print "-----------------------------------------"
    print 
    

def validate_args(args):
    """ validate that there are the correct number of arguments """
    if len(args) < 1:
        sys.exit(USAGETEXT)
        
    if args[0] == "-setvol" and len(args) < 2:
        sys.exit(USAGETEXT) 
    


def get_named_arg_value(arg_name, args, integer=False):
    """ get a argument value by name """
    arg_val = None
    if arg_name in args:

        arg_pos = args.index(arg_name)
        arg_name = args.pop(arg_pos)
        
        if len(args) > (arg_pos + 1):
            arg_val = args.pop(arg_pos)
    
    if integer:
        int_arg_val = 0
        if arg_val is not None:
            try:
                int_arg_val = int(arg_val)
            except ValueError:
                print "Invalid integer parameter, defaulting to zero. Parameter name:", arg_name
                
        arg_val = int_arg_val
                
    return arg_val
    
        

def run():
    """ main execution """
    args = sys.argv[1:]
    
    
    # optional device name parm. if not specified, device_name = None (the first device found will be used).
    device_name = get_named_arg_value("-devicename", args)
    
    # optional transcoder parm. if not specified, ffmpeg will be used, if installed, otherwise avconv.
    transcoder = get_named_arg_value("-transcoder", args)

    server_ip = get_named_arg_value("-server_ip", args) 
    
    # optional server port parm. if not specified, a random available port will be used
    server_port = get_named_arg_value("-port", args)

    server_external_port = get_named_arg_value("-external_port", args)     
    
    # optional transcode options parm. if specified, these options will be passed to the transcoder to be applied to the output
    transcode_options = get_named_arg_value("-transcodeopts", args)     
    
    # optional transcode options parm. if specified, these options will be passed to the transcoder to be applied to the input data
    transcode_input_options = get_named_arg_value("-transcodeinputopts", args)      
    
    # optional transcode bufsize parm. if specified, the transcoder will buffer approximately this many bytes of output
    transcode_bufsize = get_named_arg_value("-transcodebufsize", args, integer=True)

    # optional subtitle parm. if specified, the specified subtitles will be played.
    subtitles = get_named_arg_value("-subtitles", args)

    # optional subtitle_port parm. if not specified, a random available port will be used.
    subtitles_port = get_named_arg_value("-subtitles_port", args)

    # optional subtitle_language parm. if not specified en-US will be used.
    subtitles_language = get_named_arg_value("-subtitles_language", args)


        
    validate_args(args)
    
    if args[0] == "-stop":
        stop(device_name=device_name)
        
    elif args[0] == "-pause":
        pause(device_name=device_name)        
    
    elif args[0] == "-continue":
        unpause(device_name=device_name)           
    
    elif args[0] == "-status":
        get_status(device_name=device_name)

    elif args[0] == "-getvol":
        get_volume(device_name=device_name)

    elif args[0] == "-setvol":
        set_volume(float(args[1]), device_name=device_name)

    elif args[0] == "-volup":
        volume_up(device_name=device_name)

    elif args[0] == "-voldown":
        volume_down(device_name=device_name)

    elif args[0] == "-mute":
        set_volume(0, device_name=device_name)

    elif args[0] == "-transcode":    
        arg2 = args[1]  
        play(arg2, transcode=True, transcoder=transcoder, transcode_options=transcode_options, transcode_input_options=transcode_input_options, transcode_bufsize=transcode_bufsize,
             device_name=device_name, server_ip=server_ip, server_port=server_port, server_external_port=server_external_port, subtitles=subtitles, subtitles_port=subtitles_port,
             subtitles_language=subtitles_language)
        
    elif args[0] == "-playurl":    
        arg2 = args[1]  
        playurl(arg2, device_name=device_name)                          
        
    elif args[0] == "-devicelist":
        list_devices()
            
    else:
        play(args[0], device_name=device_name, server_ip=server_ip, server_port=server_port, server_external_port=server_external_port, subtitles=subtitles,
             subtitles_port=subtitles_port, subtitles_language=subtitles_language)
        
            
if __name__ == "__main__":
    run()

cc_media_controller.py
Spoiler: show

Code: Select all

"""
Provides a control interface to the Chromecast Media Player app

version 0.2.1

"""


# Copyright (C) 2014-2016 Pat Carter
#
# This file is part of Stream2chromecast.
#
# Stream2chromecast is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Stream2chromecast is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Stream2chromecast.  If not, see <http://www.gnu.org/licenses/>.



import socket, ssl, select
import json
import sys
import time
import re

import cc_device_finder
import cc_message


MEDIAPLAYER_APPID = "CC1AD845"
 

class CCMediaController():
    def __init__(self, device_name=None):
        """ initialise """
        
        self.host = self.get_device(device_name)

        self.sock = None
        
        self.request_id = 1
        self.source_id = "sender-0"

        self.receiver_app_status = None
        self.media_status = None
        self.volume_status = None
        self.current_applications = None
        
    
    
    def get_device(self, device_name):
        """ get the device ip address """
        
        host = None
        dummy = None

        is_ip_addr = device_name is not None and re.match( "[0-9]+.[0-9]+.[0-9]+.[0-9]+$", device_name) is not None
        
        if is_ip_addr:
            host = device_name
            try:
                #print "ip_addr:", host.encode('utf-8'), "device name:", cc_device_finder.get_device_name(host).encode('utf-8')
                print >> sys.stderr, "ip_addr:", host.encode('utf-8'), "device name:", cc_device_finder.get_device_name(host).encode('utf-8')

            except socket.error:
                sys.exit("No Chromecast found on ip:" + host)
        else:
            host, name = cc_device_finder.find_device(name=device_name)
            if host is None:
                sys.exit("No Chromecast found on the network")
                
            #print "device name:", name    
            print >> sys.stderr, "device name:", name 
        return host
        
        
        
    def open_socket(self):
        """ open a socket if there is not currently one open """
        
        if self.sock is None:
            self.sock = socket.socket()
            self.sock = ssl.wrap_socket(self.sock)

            self.sock.connect((self.host,8009))

                
    def close_socket(self):
        """ close the socket if there is one open """
        
        if self.sock is not None:
            self.sock.close()
            
        self.sock = None



    def send_data(self, namespace, data_dict):
        """ send data to the device in binary format"""
        
        data = json.dumps(data_dict)
        
        #print "Sending: ", namespace, data
        
        msg = cc_message.format_message(self.source_id, self.destination_id, namespace, data)
        
        self.sock.write(msg)

        
        
    def read_message(self):
        """ read a complete message from the device """

        data = ""
        while len(data) < 4:
            data += self.sock.recv(4)
        
        msg_length, data = cc_message.extract_length_header(data) 
        while len(data) < msg_length:
            data += self.sock.recv(2048)
            
       
        message_dict = cc_message.extract_message(data)
        
        message = {}
        
        try:
            message = json.loads(message_dict['data'])
        except:
            pass
        
        #print message_dict['namespace']
        #print json.dumps(message, indent=4, separators=(',', ': '))
        
        return message   
        
         
    
    def get_response(self, request_id):
        """ get the response matching the original request id """
        
        resp = {}
        
        count = 0
        while len(resp) == 0:
            msg = self.read_message()
            
            msg_type = msg.get("type", msg.get("responseType", ""))
            
            if msg_type == "PING":
                data = {"type":"PONG"}
                namespace = "urn:x-cast:com.google.cast.tp.heartbeat"
                self.send_data(namespace, data) 
                
                # if 30 ping/pong messages are received without a response to the request_id, 
                # assume no response is coming
                count += 1
                if count == 30:
                    return resp
                
            elif msg_type == "RECEIVER_STATUS":
                self.update_receiver_status_data(msg)
                
            elif msg_type == "MEDIA_STATUS":
                self.update_media_status_data(msg)
            
            if "requestId" in msg.keys() and msg['requestId'] == request_id:
                resp = msg
                
        return resp



    def send_msg_with_response(self, namespace, data):
        """ send a request to the device and wait for a response matching the request id """
        
        self.request_id += 1
        data['requestId'] = self.request_id
        
        self.send_data(namespace, data)
        
        return self.get_response(self.request_id)

            
        
    def update_receiver_status_data(self, msg):
        """ update the status for the Media Player app if it is running """
        
        self.receiver_app_status = None
        
        if msg.has_key('status'):
            status = msg['status']
            if status.has_key('applications'):
                self.current_applications = status['applications']
                for application in self.current_applications:
                    if application.get("appId") == MEDIAPLAYER_APPID:
                        self.receiver_app_status = application
                        
                        
            if status.has_key('volume'):
                self.volume_status = status['volume']
                        
                        
                        
    def update_media_status_data(self, msg): 
        """ update the media status if there is any media loaded """
        
        self.media_status = None
        
        status = msg.get("status", [])
        if len(status) > 0:  
            self.media_status = status[0] # status is an array - selecting the first result..?                 


         
        
    def connect(self, destination_id):  
        """ connect to to the receiver or the media transport """
        
        if self.sock is None:
            self.open_socket()
                     
        self.destination_id = destination_id
        
        data = {"type":"CONNECT","origin":{}}
        namespace = "urn:x-cast:com.google.cast.tp.connection"
        self.send_data(namespace, data)
        
        
    
    def get_receiver_status(self):
        """ send a status request to the receiver """
        
        data = {"type":"GET_STATUS"}
        namespace = "urn:x-cast:com.google.cast.receiver"
        self.send_msg_with_response(namespace, data)
                
    
    
    def get_media_status(self):
        """ send a status request to the media player """
        
        data = {"type":"GET_STATUS"}
        namespace = "urn:x-cast:com.google.cast.media"
        self.send_msg_with_response(namespace, data)   
            
            
                    
    def load(self, content_url, content_type, sub, sub_language):
        """ Launch the player app, load & play a URL """
        
        self.connect("receiver-0")

        self.get_receiver_status()
        
        # we only set the receiver status for MEDIAPLAYER - so if it is set, the app is currenty running
        if self.receiver_app_status is None:
            data = {"type":"LAUNCH","appId":MEDIAPLAYER_APPID}
            namespace = "urn:x-cast:com.google.cast.receiver"
            self.send_msg_with_response(namespace, data)
            
            # if there is still no receiver app status the launch failed.
            if self.receiver_app_status is None:
                self.close_socket()
                sys.exit("Cannot launch the Media Player app")
                
        
        session_id = str(self.receiver_app_status['sessionId'])
        transport_id = str(self.receiver_app_status['transportId'])

        self.connect(transport_id)

        data = {"type":"LOAD",
                "sessionId":session_id,
                "media":{
                    "contentId":content_url,
                    "streamType":"buffered",
                    "contentType":content_type,
                    },
                "autoplay":True,
                "currentTime":0,
                "customData":{
                    "payload":{
                        "title:":""
                        }
                    }
                }


        if sub:        
            if sub_language is None:
                sub_language = "en-US"
                
            data["media"].update({
                                "textTrackStyle":{
                                    'backgroundColor':'#FFFFFF00'
                                },
                                "tracks": [{"trackId": 1,
                                            "trackContentId": sub,
                                            "type": "TEXT",
                                            "language": sub_language,
                                            "subtype": "SUBTITLES",
                                            "name": "Englishx",
                                            "trackContentType": "text/vtt",
                                           }],
                                })
            data["activeTrackIds"] = [1]

        
        namespace = "urn:x-cast:com.google.cast.media"
        resp = self.send_msg_with_response(namespace, data)


        # wait for the player to return "BUFFERING", "PLAYING" or "IDLE"
        if resp.get("type", "") == "MEDIA_STATUS":            
            player_state = ""
            while player_state != "PLAYING" and player_state != "IDLE" and player_state != "BUFFERING":
                time.sleep(2)        
                
                self.get_media_status()
                
                if self.media_status != None:
                    player_state = self.media_status.get("playerState", "")

                
        self.close_socket()       


            
    def control(self, command, parameters={}):      
        """ send a control command to the player """
          
        self.connect("receiver-0")

        self.get_receiver_status()
        
        if self.receiver_app_status is None:
            print >> sys.stderr, "No media player app running"
            self.close_socket()
            return      
        
        transport_id = str(self.receiver_app_status['transportId'])       
        
        self.connect(transport_id)
        
        self.get_media_status()
        
        media_session_id = 1
        if self.media_status is not None:
            media_session_id = self.media_status['mediaSessionId']
                                                                     
        data = {"type":command, "mediaSessionId":media_session_id}
        data.update(parameters)  # for additional parameters
        
        namespace = "urn:x-cast:com.google.cast.media"
        self.send_msg_with_response(namespace, data)
        
        self.close_socket()
                       
    
    
    def get_status(self):
        """ get the receiver and media status """
        
        self.connect("receiver-0")

        self.get_receiver_status()
        
        if self.receiver_app_status is not None:   
        	transport_id = str(self.receiver_app_status['transportId']) 
        	self.connect(transport_id)
        	self.get_media_status()
        
        application_list = []
        if self.current_applications is not None:
            for application in self.current_applications:
                application_list.append({
                    'appId':application.get('appId', ""), 
                    'displayName':application.get('displayName', ""),  
                    'statusText':application.get('statusText', "")})
        
        status = {'receiver_status':self.receiver_app_status, 
                  'media_status':self.media_status, 
                  'host':self.host, 
                  'client':self.sock.getsockname(),
                  'applications':application_list}
                
        self.close_socket()
        
        return status
        
        
        
#    def is_idle(self):
#        """ return the IDLE state of the player """
        
#        status = self.get_status()
        
#        if status['media_status']  is None:
#            if status['receiver_status'] is None:
#                return True
#            else:    
#                return status['receiver_status'].get("statusText", "") == u"Ready To Cast"

#        else:    
#            return status['media_status'].get("playerState", "") == u"IDLE"

# https://www.domoticz.com/forum/viewtopic.php?f=69&t=22610&view=unread&sid=ad3e2da7f9d4eee231d26e7e63558869#unread
# Added 16-09-2918

    def is_idle(self):
        """ return the IDLE state of the player """
        
        status = self.get_status()
        
        if status['media_status']  is None:
                return True
        else:    
            return status['media_status'].get("playerState", "") == u"IDLE"

       
       

    def pause(self):
        """ pause """
        self.control("PAUSE") 
            
    def play(self):
        """ unpause """
        self.control("PLAY")   
        
    def stop(self):
        """ stop """
        self.control("STOP")
        self.connect("receiver-0")

        data = {"type":"STOP"}
        namespace = "urn:x-cast:com.google.cast.receiver"
        self.send_msg_with_response(namespace, data)  
        
        self.close_socket() 
         
        
        
        
    def set_volume(self, level):
        """ set the receiver volume - a float value in level for absolute level or "+" / "-" indicates up or down"""
        
        self.connect("receiver-0")

        if level in ("+", "-"):
            self.get_receiver_status()
        
            if self.volume_status is not None:
                curr_level = self.volume_status['level']
                if level == "+":
                    level = 0.1 + curr_level
                elif level == "-":
                    level = curr_level - 0.1
            
        
        data = {"type":"SET_VOLUME", "volume":{"muted":False, "level":level} }
        namespace = "urn:x-cast:com.google.cast.receiver"
        self.send_msg_with_response(namespace, data)  
        
        self.close_socket() 
        
        
    def get_volume_ext(self):
        """ get the current volume level """

        self.get_status()
        
        vol = None
        
        if self.volume_status is not None:
            vol = self.volume_status.get('level', None)
                
        return vol
    
    def get_volume(self):
        """ get the current volume level """
        self.get_status()
        
        vol = None
        
        if self.volume_status is not None:
            vol = self.volume_status.get('level', None)
                
        return vol
                         
                              
                                
    def set_volume_up(self):
        """ increase volume by one step """
        self.set_volume("+")
            

            
    def set_volume_down(self):
        """ decrease volume by one step """
        self.set_volume("-")    
            
Using Pass2php since 2016-12
LAN: RFLink, P1-Port, OTGW, MySensors
USB: RFXCom, ZWave
WIFI: Mi-light Wifi-Bridge, Sonoff, ESP8266, Xiaomi Gateway
Solar: Omnik Inverter, PVOutput
Video: Kodi clients with Harmony HUB
Sensors: You name it I probably got 1.

Post Reply

Who is online

Users browsing this forum: No registered users and 0 guests