diff --git a/fhem/CHANGED b/fhem/CHANGED index 7f38a9d9f..07753f460 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - new: 98_GOOGLECAST: first release, read commandref for requirements - changed: 74_AMADautomagicFlowset_4.0.3.xml: workaround for better auto flowsetupdate - feature: 74_AMADDevice: 4.0.5 - Support für NFC and scanning NFC TagId diff --git a/fhem/FHEM/98_GOOGLECAST.pm b/fhem/FHEM/98_GOOGLECAST.pm new file mode 100755 index 000000000..b645f0705 --- /dev/null +++ b/fhem/FHEM/98_GOOGLECAST.pm @@ -0,0 +1,649 @@ +############################################################# +# +# GOOGLECAST.pm (c) by Dominik Karall, 2016-2017 +# dominik karall at gmail dot com +# $Id$ +# +# FHEM module to communicate with Google Cast devices +# e.g. Chromecast Video, Chromecast Audio, Google Home +# +# Version: 2.0.0 +# +############################################################# +# +# v2.0.0 - 20170812 +# - CHANGE: renamed to 98_GOOGLECAST.pm +# - CHANGE: removed favoriteName_X attribute, it was never used +# - BUGFIX: updated commandref with further required packages +# +# v1.0.7 - 20170804 +# - BUGFIX: fix reconnection in some cases +# +# v1.0.6 - 20170705 +# - BUGFIX: speed up youtube video URL extraction with youtube_dl +# - BUGFIX: fixed one more issue when chromecast offline +# - BUGFIX: improved performance by adding socket to FHEM main loop +# +# v1.0.5 - 20170704 +# - BUGFIX: hopefuly fixed the annoying hangs when chromecast offline +# - FEATURE: add presence reading (online/offline) +# +# v1.0.4 - 20170101 +# - FEATURE: support all services supported by youtube-dl +# https://github.com/rg3/youtube-dl/blob/master/docs/supportedsites.md +# playlists not yet supported! +# - BUGFIX: support non-blocking chromecast search +# +# v1.0.3 - 20161219 +# - FEATURE: support volume +# - FEATURE: add new readings and removed +# castStatus, mediaStatus reading +# - FEATURE: add attribute favoriteURL_[1-5] +# - FEATURE: add playFavorite [1-5] set function +# - FEATURE: retry init chromecast every 10s if not found on startup +# - BUGFIX: support special characters for device name +# +# v1.0.2 - 20161216 +# - FEATURE: support play of every mime type which is supported +# by Chromecast (see https://developers.google.com/cast/docs/media) +# including youtube URLs +# - CHANGE: change play* methods to play +# - FEATURE: support very simple .m3u which contain only URL +# - BUGFIX: non-blocking playYoutube +# - BUGFIX: fix play if media player is already running +# +# v1.0.1 - 20161211 +# - FEATURE: support playYoutube +# +# v1.0.0 - 20161015 +# - FEATURE: first public release +# +# TODO +# - check spotify integration +# - support youtube playlists +# +# NOTES +# def play_media(self, url, content_type, title=None, thumb=None, +# current_time=0, autoplay=True, +# stream_type=STREAM_TYPE_BUFFERED, +# metadata=None, subtitles=None, subtitles_lang='en-US', +# subtitles_mime='text/vtt', subtitle_id=1): +# """ +# Plays media on the Chromecast. Start default media receiver if not +# already started. +# Parameters: +# url: str - url of the media. +# content_type: str - mime type. Example: 'video/mp4'. +# title: str - title of the media. +# thumb: str - thumbnail image url. +# current_time: float - seconds from the beginning of the media +# to start playback. +# autoplay: bool - whether the media will automatically play. +# stream_type: str - describes the type of media artifact as one of the +# following: "NONE", "BUFFERED", "LIVE". +# subtitles: str - url of subtitle file to be shown on chromecast. +# subtitles_lang: str - language for subtitles. +# subtitles_mime: str - mimetype of subtitles. +# subtitle_id: int - id of subtitle to be loaded. +# metadata: dict - media metadata object, one of the following: +# GenericMediaMetadata, MovieMediaMetadata, TvShowMediaMetadata, +# MusicTrackMediaMetadata, PhotoMediaMetadata. +# Docs: +# https://developers.google.com/cast/docs/reference/messages#MediaData +# """ +# +############################################################# + +package main; + +use strict; +use warnings; + +use Blocking; +use Encode; +use SetExtensions; + +use LWP::UserAgent; + +sub GOOGLECAST_Initialize($) { + my ($hash) = @_; + + $hash->{DefFn} = 'GOOGLECAST_Define'; + $hash->{UndefFn} = 'GOOGLECAST_Undef'; + $hash->{GetFn} = 'GOOGLECAST_Get'; + $hash->{SetFn} = 'GOOGLECAST_Set'; + $hash->{ReadFn} = 'GOOGLECAST_Read'; + $hash->{AttrFn} = 'GOOGLECAST_Attribute'; + $hash->{AttrList} = "favoriteURL_1 favoriteURL_2 favoriteURL_3 favoriteURL_4 ". + "favoriteURL_5 ".$readingFnAttributes; + + Log3 $hash, 3, "GOOGLECAST: GoogleCast v2.0.0"; + + return undef; +} + +sub GOOGLECAST_Define($$) { + my ($hash, $def) = @_; + my @a = split("[ \t]+", $def); + my $name = $a[0]; + + $hash->{STATE} = "initialized"; + + if (int(@a) > 3) { + return 'GOOGLECAST: Wrong syntax, must be define GOOGLECAST '; + } elsif(int(@a) == 3) { + Log3 $hash, 3, "GOOGLECAST: $a[2] initializing..."; + $hash->{CCNAME} = $a[2]; + GOOGLECAST_updateReading($hash, "presence", "offline"); + GOOGLECAST_initDevice($hash); + } + + return undef; +} + +sub GOOGLECAST_findChromecasts { + my ($string) = @_; + my ($name) = split("\\|", $string); + my $result = "$name"; + + my @ccResult = GOOGLECAST_findChromecastsPython(); + foreach my $ref_cc (@ccResult) { + my @cc = @$ref_cc; + $result .= "|CCDEVICE|".$cc[0]."|".$cc[1]."|".$cc[2]."|".$cc[3]."|".$cc[4]; + } + Log3 $name, 4, "GOOGLECAST: search result: $result"; + + return $result; +} + +sub GOOGLECAST_initDevice { + my ($hash) = @_; + my $devName = $hash->{CCNAME}; + + BlockingCall("GOOGLECAST_findChromecasts", $hash->{NAME}, "GOOGLECAST_findChromecastsResult"); + + return undef; +} + +sub GOOGLECAST_findChromecastsResult { + my ($string) = @_; + my ($name, @ccResult) = split("\\|", $string); + my $hash = $main::defs{$name}; + my $devName = $hash->{CCNAME}; + $hash->{helper}{ccdevice} = ""; + + for my $i (0..$#ccResult) { + if($ccResult[$i] eq "CCDEVICE" and $ccResult[$i+5] eq $devName) { + Log3 $hash, 4, "GOOGLECAST ($hash->{NAME}): init cast device $devName"; + eval { + $hash->{helper}{ccdevice} = GOOGLECAST_createChromecastPython($ccResult[$i+1],$ccResult[$i+2],$ccResult[$i+3],$ccResult[$i+4],$ccResult[$i+5]); + }; + if($@) { + $hash->{helper}{ccdevice} = ""; + } + Log3 $hash, 4, "GOOGLECAST ($hash->{NAME}): device initialized"; + } + } + + if($hash->{helper}{ccdevice} eq "") { + Log3 $hash, 4, "GOOGLECAST: $devName not found, retry in 10s."; + InternalTimer(gettimeofday()+10, "GOOGLECAST_initDevice", $hash, 0); + return undef; + } + + Log3 $hash, 3, "GOOGLECAST: $devName initialized successfully"; + + GOOGLECAST_addSocketToMainloop($hash); + GOOGLECAST_checkConnection($hash); + + return undef; +} + +sub GOOGLECAST_Attribute($$$$) { + my ($mode, $devName, $attrName, $attrValue) = @_; + + if($mode eq "set") { + + } elsif($mode eq "del") { + + } + + return undef; +} + +sub GOOGLECAST_Set($@) { + my ($hash, $name, @params) = @_; + my $workType = shift(@params); + my $list = "stop:noArg pause:noArg quitApp:noArg play playFavorite:1,2,3,4,5 volume:slider,0,1,100"; + + # check parameters for set function + if($workType eq "?") { + return SetExtensions($hash, $list, $name, $workType, @params); + } + + if($workType eq "stop") { + GOOGLECAST_setStop($hash); + } elsif($workType eq "pause") { + GOOGLECAST_setPause($hash); + } elsif($workType eq "play") { + GOOGLECAST_setPlay($hash, $params[0]); + } elsif($workType eq "playFavorite") { + GOOGLECAST_setPlayFavorite($hash, $params[0]); + } elsif($workType eq "quitApp") { + GOOGLECAST_setQuitApp($hash); + } elsif($workType eq "volume") { + GOOGLECAST_setVolume($hash, $params[0]); + } else { + return SetExtensions($hash, $list, $name, $workType, @params); + } + + return undef; +} + +### volume ### +sub GOOGLECAST_setVolume { + my ($hash, $volume) = @_; + $volume = $volume/100; + + eval { + $hash->{helper}{ccdevice}->set_volume($volume); + }; +} + +### playType ### +sub GOOGLECAST_setPlayType { + my ($hash, $url, $mime) = @_; + + eval { + $hash->{helper}{ccdevice}->{media_controller}->play_media($url, $mime); + }; + + return undef; +} + +sub GOOGLECAST_setPlayType_String { + my ($string) = @_; + my ($name, $url, $mime) = split("\\|", $string); + my $hash = $main::defs{$name}; + + if($mime ne "" && $url ne "") { + GOOGLECAST_setPlayType($hash, $url, $mime); + } +} + +### playMedia ### +sub GOOGLECAST_setPlayMedia { + my ($hash, $url) = @_; + + BlockingCall("GOOGLECAST_setPlayMediaBlocking", $hash->{NAME}."|".$url, "GOOGLECAST_setPlayType_String"); + + return undef; +} + +sub GOOGLECAST_setPlayMedia_String { + my ($string) = @_; + my ($name, $videoUrl, $origUrl) = split("\\|", $string); + my $hash = $main::defs{$name}; + + if($videoUrl ne "") { + GOOGLECAST_setPlayMedia($hash, $videoUrl); + } else { + GOOGLECAST_setPlayMedia($hash, $origUrl); + } +} + +sub GOOGLECAST_setPlayMediaBlocking { + my ($string) = @_; + my ($name, $url) = split("\\|", $string); + + #$url = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" + #$url = "http://swr-mp3-m-swr3.akacast.akamaistream.net:80/7/720/137136/v1/gnl.akacast.akamaistream.net/swr-mp3-m-swr3"; + + my $ua = new LWP::UserAgent(agent => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.5) Gecko/20060719 Firefox/1.5.0.5'); + $ua->max_size(0); + my $resp = $ua->get($url); + my $mime = $resp->header('Content-Type'); + + if($mime eq "audio/x-mpegurl") { + $mime = "audio/mpeg"; + $url = $resp->decoded_content; + $url =~ s/\R//g; + } + + return $name."|".$url."|".$mime; +} + +### playYoutue ### +sub GOOGLECAST_setPlayYtDl { + my ($hash, $ytUrl) = @_; + + BlockingCall("GOOGLECAST_setPlayYtDlBlocking", $hash->{NAME}."|".$ytUrl, "GOOGLECAST_setPlayMedia_String"); + + return undef; +} + +sub GOOGLECAST_setPlayYtDlBlocking { + my ($string) = @_; + my ($name, $ytUrl) = split("\\|", $string); + my $videoUrl = ""; + + eval { + $videoUrl = GOOGLECAST_getYTVideoURLPython($ytUrl); + }; + + return $name."|".$videoUrl."|".$ytUrl; +} + +### stop ### +sub GOOGLECAST_setStop { + my ($hash) = @_; + + eval { + $hash->{helper}{ccdevice}->{media_controller}->stop(); + }; + + return undef; +} + +### playFavorite ### +sub GOOGLECAST_setPlayFavorite { + my ($hash, $favoriteNr) = @_; + GOOGLECAST_setPlay($hash, AttrVal($hash->{NAME}, "favoriteURL_".$favoriteNr, "")); + return undef; +} + +### play ### +sub GOOGLECAST_setPlay { + my ($hash, $url) = @_; + + if(defined($url)) { + #support streams are listed here + #https://github.com/rg3/youtube-dl/blob/master/docs/supportedsites.md + GOOGLECAST_setPlayYtDl($hash, $url); + } else { + eval { + $hash->{helper}{ccdevice}->{media_controller}->play(); + }; + } + + return undef; +} + +### pause ### +sub GOOGLECAST_setPause { + my ($hash) = @_; + + eval { + $hash->{helper}{ccdevice}->{media_controller}->pause(); + }; + + return undef; +} + +### quitApp ### +sub GOOGLECAST_setQuitApp { + my ($hash) = @_; + + eval { + $hash->{helper}{ccdevice}->quit_app(); + }; + + return undef; +} + +sub GOOGLECAST_Undef($) { + my ($hash) = @_; + + #remove internal timer + RemoveInternalTimer($hash); + + return undef; +} + +sub GOOGLECAST_Get($$) { + return undef; +} + +sub GOOGLECAST_updateReading { + my ($hash, $readingName, $value) = @_; + my $oldValue = ReadingsVal($hash->{NAME}, $readingName, ""); + + if(!defined($value)) { + $value = ""; + } + + if($oldValue ne $value) { + readingsSingleUpdate($hash, $readingName, $value, 1); + } +} + +sub GOOGLECAST_newChash { + my ($hash, $socket, $chash) = @_; + + $chash->{TYPE} = $hash->{TYPE}; + $chash->{UDN} = -1; + + $chash->{NR} = $devcount++; + + $chash->{phash} = $hash; + $chash->{PNAME} = $hash->{NAME}; + + $chash->{CD} = $socket; + $chash->{FD} = $socket->fileno(); + + #$chash->{PORT} = $socket->sockport if( $socket->sockport ); + + $chash->{TEMPORARY} = 1; + $attr{$chash->{NAME}}{room} = 'hidden'; + + $defs{$chash->{NAME}} = $chash; + $selectlist{$chash->{NAME}} = $chash; +} + +sub GOOGLECAST_addSocketToMainloop { + my ($hash) = @_; + my $sock; + + eval { + $sock = $hash->{helper}{ccdevice}->{socket_client}->get_socket(); + }; + + my $chash = GOOGLECAST_newChash($hash, $sock, {NAME => "GOOGLECAST-".$hash->{NAME}}); + return undef; +} + +sub GOOGLECAST_checkConnection { + my ($hash) = @_; + + eval { + Log3 $hash, 5, "GOOGLECAST ($hash->{NAME}): run_once"; + $hash->{helper}{ccdevice}->{socket_client}->run_once(); + }; + + if($@ || !defined($selectlist{"GOOGLECAST-".$hash->{NAME}})) { + Log3 $hash, 4, "GOOGLECAST ($hash->{NAME}): checkConnection, connection failure, reconnect..."; + GOOGLECAST_initDevice($hash); + GOOGLECAST_updateReading($hash, "presence", "offline"); + return undef; + } + + InternalTimer(gettimeofday()+10, "GOOGLECAST_checkConnection", $hash, 0); + return undef; +} + + +sub GOOGLECAST_Read { + my ($hash) = @_; + my $name = $hash->{NAME}; + $hash = $hash->{phash}; + + eval { + Log3 $hash, 5, "GOOGLECAST ($hash->{NAME}): run_once"; + $hash->{helper}{ccdevice}->{socket_client}->run_once(); + }; + + if($@) { + Log3 $hash, 4, "GOOGLECAST ($hash->{NAME}): connection failure, reconnect..."; + eval { + delete($selectlist{$name}); + }; + GOOGLECAST_initDevice($hash); + GOOGLECAST_updateReading($hash, "presence", "offline"); + return undef; + } + + GOOGLECAST_updateReading($hash, "presence", "online"); + GOOGLECAST_updateReading($hash, "name", $hash->{helper}{ccdevice}->{name}); + GOOGLECAST_updateReading($hash, "model", $hash->{helper}{ccdevice}->{model_name}); + GOOGLECAST_updateReading($hash, "uuid", $hash->{helper}{ccdevice}->{uuid}); + GOOGLECAST_updateReading($hash, "castType", $hash->{helper}{ccdevice}->{cast_type}); + GOOGLECAST_updateReading($hash, "model", $hash->{helper}{ccdevice}->{model_name}); + GOOGLECAST_updateReading($hash, "appId", $hash->{helper}{ccdevice}->{app_id}); + GOOGLECAST_updateReading($hash, "appName", $hash->{helper}{ccdevice}->{app_display_name}); + GOOGLECAST_updateReading($hash, "idle", $hash->{helper}{ccdevice}->{is_idle}); + + my $newStatus = $hash->{helper}{ccdevice}->{media_controller}->{status}; + if(defined($newStatus)) { + #GOOGLECAST_updateReading($hash, "mediaStatus", $newStatus); + GOOGLECAST_updateReading($hash, "mediaTitle", $newStatus->{title}); + GOOGLECAST_updateReading($hash, "mediaSeriesTitle", $newStatus->{series_title}); + GOOGLECAST_updateReading($hash, "mediaSeason", $newStatus->{season}); + GOOGLECAST_updateReading($hash, "mediaEpisode", $newStatus->{episode}); + GOOGLECAST_updateReading($hash, "mediaArtist", $newStatus->{artist}); + GOOGLECAST_updateReading($hash, "mediaAlbum", $newStatus->{album_name}); + GOOGLECAST_updateReading($hash, "mediaAlbumArtist", $newStatus->{album_artist}); + GOOGLECAST_updateReading($hash, "mediaTrack", $newStatus->{track}); + if(length($newStatus->{images}) > 0) { + GOOGLECAST_updateReading($hash, "mediaImage", $newStatus->{images}[0]->{url}); + } else { + GOOGLECAST_updateReading($hash, "mediaImage", ""); + } + } + + my $newCastStatus = $hash->{helper}{ccdevice}->{status}; + if(defined($newCastStatus)) { + #GOOGLECAST_updateReading($hash, "castStatus", $newCastStatus); + GOOGLECAST_updateReading($hash, "volume", $newCastStatus->{volume_level}*100); + } + + return undef; +} + +use Inline Python => <<'PYTHON_CODE_END'; + +from __future__ import unicode_literals +import pychromecast +import time +import logging +import youtube_dl + +def GOOGLECAST_findChromecastsPython(): + logging.basicConfig(level=logging.CRITICAL) + return pychromecast.discovery.discover_chromecasts() + +def GOOGLECAST_createChromecastPython(ip, port, uuid, model_name, friendly_name): + logging.basicConfig(level=logging.CRITICAL) + return pychromecast._get_chromecast_from_host((ip, int(port), uuid, model_name, friendly_name), blocking=False, timeout=0.1, tries=1, retry_wait=0.1) + +def GOOGLECAST_getYTVideoURLPython(yt_url): + ydl = youtube_dl.YoutubeDL({'quiet': '1', 'no_warnings': '1'}) + + with ydl: + result = ydl.extract_info( + yt_url, + download=False # We just want to extract the info + ) + + if 'entries' in result: + # Can be a playlist or a list of videos + video = result['entries'][0] + else: + # Just a video + video = result + + video_url = video['url'] + return video_url + +PYTHON_CODE_END + +1; + + +=pod +=item device +=item summary Easily control your Google Cast devices (Video, Audio, Google Home) +=item summary_DE Einfache Steuerung deiner Google Cast Geräte (Video, Audio, Google Home) +=begin html + + +

GOOGLECAST

+
    + GOOGLECAST is used to control your Google Cast device

    + Note
    Following packages are required: +
      +
    • sudo apt-get install libwww-perl python-enum34 python-dev libextutils-makemaker-cpanfile-perl python-pip cpanminus
    • +
    • sudo pip install netifaces
    • +
    • sudo pip install enum34
    • +
    • sudo pip install pychromecast
    • +
    • sudo pip install youtube-dl
    • +
    • sudo cpanm Inline::Python
    • +
    + +
    +
    + + Define +
      + define <name> GOOGLECAST <name>
      +
      + Example: +
        + define livingroom.chromecast GOOGLECAST livingroom

        + Wait a few seconds till presence switches to online...

        + set livingroom.chromecast play https://www.youtube.com/watch?v=YE7VzlLtp-4
        +
      +
      + Following media types are supported:
      + Supported media formats
      + Play with youtube-dl works for following URLs:
      + Supported youtube-dl sites
      +
      +
    + +
    + + + Set +
      + set <name> <command> [<parameter>]
      + The following commands are defined:

      +
        +
      • play URL   -   play from URL
      • +
      • play   -   play, like resume if paused previsously
      • +
      • playFavorite   -   plays URL from favoriteURL_[1-5]
      • +
      • stop   -   stop, stops current playback
      • +
      • pause   -   pause
      • +
      • quitApp   -   quit current application, like YouTube
      • +
      +
      +
    + + + Attributes +
      +
    • favoriteURL_[1-5]   -   save URL to play afterwards with playFavorite [1-5]
    • +
    +
    + + + Get +
      + n/a +
    +
    + +
+ +=end html +=cut + diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index 49cfa2591..c032eedd5 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -428,6 +428,7 @@ FHEM/98_expandJSON.pm dev0 http://forum.fhem.de Unterstue FHEM/98_fheminfo.pm betateilchen http://forum.fhem.de Sonstiges FHEM/98_fhemdebug.pm rudolfkoenig http://forum.fhem.de Sonstiges FHEM/98_GoogleAuth.pm betateilchen http://forum.fhem.de Unterstuetzende Dienste +FHEM/98_GOOGLECAST.pm dominikkarall http://forum.fhem.de Multimedia FHEM/98_help.pm betateilchen http://forum.fhem.de Sonstiges FHEM/98_HourCounter.pm john http://forum.fhem.de MAX FHEM/98_logProxy.pm justme1968 http://forum.fhem.de Frontends/SVG Plots logProxy