diff --git a/98_FireTV.pm b/98_FireTV.pm deleted file mode 100644 index b56d703..0000000 --- a/98_FireTV.pm +++ /dev/null @@ -1,1657 +0,0 @@ -######################################################################## -# 98_FireTV.pm -# -# Control a FireTV-Device from FHEM -# -# Prerequisites: -# 1.) enable adb debugging in your fire tv -# 2.) get adb and copy the binary to /usr/bin/ -# some sources for raspbian binaries: -# apt-get install android-tools-adb -# https://github.com/DeepSilence/adb-arm -# https://forum.xda-developers.com/showthread.php?t=1924492 -# http://forum.xda-developers.com/attachment.php?attachmentid=1392336&d=1349930509 -# -# uses 73_PRESENCE.pm by Markus Bloch -# uses File::MimeInfo by Michiel Beijen -# -# 2018 by Thomas Nesges -######################################################################## - -package main; - -use strict; -use warnings; -use Time::HiRes; -use File::Temp qw(tempdir tempfile); - -sub FireTV_Initialize($); -sub FireTV_Define($$); -sub FireTV_Undef($$); -sub FireTV_Set($@); -sub FireTV_Get($@); -sub FireTV_Attr(@); -sub FireTV_Notify($$); -sub FireTV_SetTimer($;$); -sub FireTV_FetchStatus($); - -sub FireTV_Initialize($) { - my ($hash) = @_; - - $hash->{DefFn} = 'FireTV_Define'; - $hash->{UndefFn} = 'FireTV_Undef'; - $hash->{SetFn} = 'FireTV_Set'; - $hash->{GetFn} = 'FireTV_Get'; - $hash->{AttrFn} = 'FireTV_Attr'; - $hash->{AttrList} = "holdconnection:yes,no screenshotpath upviewdeleteafter uploaddeleteafter remotehtml interval ".$readingFnAttributes; - - if(LoadModule("PRESENCE") eq "PRESENCE") { - # PRESENCE - $hash->{ReadFn} = "PRESENCE_Read"; - $hash->{ReadyFn} = "PRESENCE_Ready"; - $hash->{NotifyFn} = "FireTV_Notify"; - $hash->{AttrList} .= " ping_count:1,2,3,4,5,6,7,8,9,10" - ." absenceThreshold:1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20" - ." presenceThreshold:1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20" - ." absenceTimeout presenceTimeout " - ." do_not_notify:0,1 disable:0,1 disabledForIntervals "; # disabledForIntervals seems to be broken - TODO - } - - # PORT was introduced later, set a default here to avoid users having to redefine their devices - if(! defined($hash->{PORT})) { - $hash->{PORT} = '5555'; - } -} - -sub FireTV_Define($$) { - my ($hash, $def) = @_; - my @param = split('[ \t]+', $def); - - my $name = $param[0]; - - if(LoadModule("PRESENCE") eq "PRESENCE") { - $hash->{helper}{$name}{'PRESENCE_loaded'} = 1; - } else { - Log3 $name, 3, "[$name] FireTV_Define WARNING: couldn't load module PRESENCE"; - $hash->{helper}{$name}{'PRESENCE_loaded'} = 0; - } - - if(int(@param) < 3) { - if($hash->{helper}{$name}{'PRESENCE_loaded'}) { - return "too few parameters: define FireTV [sudo] [] [] [] [] []"; - } else { - return "too few parameters: define FireTV [sudo] []"; - } - } - - if(defined($param[2]) && $param[2]!~/^[a-z0-9-.]+(:\d{1,5})?$/i) { - return "IP '".$param[2]."' is no valid ip address or hostname"; - } - if(defined($param[3]) && $param[3] eq "sudo") { - splice @param, 3, 1; - $hash->{ADB} = 'sudo '; - } else { - $hash->{ADB} = ''; - } - if(defined($param[3]) && ! -x $param[3]) { - return "ADB_PATH '".$param[3]."' is not executable"; - } - if(defined($param[4]) && $param[4]!~/^\d+$/) { - return "PRESENCE_TIMEOUT_ABSENT '".$param[3]."' is no valid integer number"; - } - if(defined($param[5]) && $param[5]!~/^\d+$/) { - return "PRESENCE_TIMEOUT_PRESENT '".$param[3]."' is no valid integer number"; - } - if(defined($param[6]) && $param[6]!~/^(lan-ping|lan-bluetooth|local-bluetooth|fritzbox|shellscript|function|event)$/) { - return "PRESENCE_MODE '".$param[3]."' must be one of lan-ping, lan-bluetooth, local-bluetooth, fritzbox, shellscript, function or event"; - } - - $hash->{NAME} = $name; - $hash->{IP} = $param[2]; - if($hash->{IP} =~ m/(.*?):(.*)/) { - $hash->{IP} = $1; - $hash->{PORT} = $2; - } else { - $hash->{PORT} = '5555'; - } - $hash->{ADB} .= $param[3] || '/usr/bin/adb'; - $hash->{STATE} = 'defined'; - $hash->{VERSION} = '0.6.1'; - FireTV_ReadDeviceInfo($hash); - - - if($hash->{helper}{$name}{'PRESENCE_loaded'}) { - # PRESENCE - $hash->{NOTIFYDEV} = "global,$name"; - $hash->{TIMEOUT_NORMAL} = $param[4] || 30; - $hash->{TIMEOUT_PRESENT} = $param[5] || $hash->{TIMEOUT_NORMAL}; - $hash->{MODE} = $param[6] || 'lan-ping'; - $hash->{ADDRESS} = $param[7] || $hash->{IP}; - - if(! IsDisabled($name)) { - PRESENCE_StartLocalScan($hash, 1); - } - } - - Log3 $name, 4, "[$name] FireTV_Define: getting packagelist"; - FireTV_Get($hash, $name, 'packages'); - - Log3 $name, 4, "[$name] FireTV_Define: starting FireTV_SetTimer"; - FireTV_SetTimer($hash); - return undef; -} - -sub FireTV_ReadDeviceInfo($) { - my $hash = shift; - if(ref $hash ne 'HASH' ) { - $hash = $defs{$hash}; - } - my $name = $hash->{NAME}; - - if(! IsDisabled($name)) { - $hash->{ADBVERSION} = `$hash->{ADB} version 2>&1` || $!; - - if(FireTV_connect($hash)) { - if(FireTV_adb($hash, 'shell cat /proc/version')) { - my $OSVERSION = $hash->{helper}{$name}{'lastadbresponse'}; - $hash->{OSVERSION} = $OSVERSION if $OSVERSION !~ /error:/; - } else { - Log3 $name, 4, "[$name] FireTV_ReadDeviceInfo: error reading OSVERSION"; - } - - if(FireTV_adb($hash, 'shell getprop ro.build.version.name')) { - my $OSNAME = $hash->{helper}{$name}{'lastadbresponse'}; - $hash->{OSVERSION} = $OSNAME if $OSNAME !~ /error:/; - } else { - Log3 $name, 4, "[$name] FireTV_ReadDeviceInfo: error reading OSNAME"; - } - - if(FireTV_adb($hash, 'shell getprop ro.serialno')) { - my $SERIAL = $hash->{helper}{$name}{'lastadbresponse'}; - $hash->{SERIAL} = $SERIAL if $SERIAL !~ /error:/; - } else { - Log3 $name, 4, "[$name] FireTV_ReadDeviceInfo: error reading SERIAL"; - } - } - } -} - -sub FireTV_Undef($$) { - my ($hash, $arg) = @_; - - my $name = $hash->{NAME}; - - RemoveInternalTimer($hash); - # PRESENCE - if(defined($hash->{helper}{RUNNING_PID})) { - BlockingKill($hash->{helper}{RUNNING_PID}); - } - # own - if(defined($hash->{helper}{$name}{'blockingcall'})) { - foreach my $blockingcall ( keys(@{$hash->{helper}{$name}{'blockingcall'}}) ) { - BlockingKill($blockingcall->{RUNNING_PID}); - } - } - DevIo_CloseDev($hash); - - return undef; -} - -sub FireTV_Get($@) { - my ($hash, @param) = @_; - - return '"get FireTV" needs at least one argument' if (int(@param) < 2); - - my $name = shift @param; - my $opt = shift @param; - my $value = join(" ", @param); - - if(! FireTV_connect($hash)) { - return "error: ".$hash->{helper}{$name}{'lastadbresponse'}; - } - - my $screen_state; - if($opt) { - # update screen_state reading - $screen_state = FireTV_screen_state($hash); - } - - if($opt eq 'packages') { - if(FireTV_adb($hash, 'shell pm list packages -f -3')) { - my @response = split(/[\n\r]+/, $hash->{helper}{$name}{'lastadbresponse'}); - my @apk; - foreach my $line (@response) { - my ($package, $apk) = split('=', $line); - push @apk, $apk; - } - if(@apk > 0) { - @apk = sort(@apk); - $hash->{helper}{$name}{'packages'} = join(',', @apk); - return "Found the following installed packages: \n\n".join("\n", @apk); - } else { - Log3 $name, 4, "[$name] FireTV_Get: no userpackages found"; - } - return "no userpackages found"; - } else { - return "error: ".$hash->{helper}{$name}{'lastadbresponse'}; - } - - } elsif($opt eq 'isapprunning') { - return FireTV_is_app_running($hash, $value); - - } elsif($opt eq 'adb') { - if(FireTV_adb($hash, $value)) { - my @response = split(/[\n\r]+/, $hash->{helper}{$name}{'lastadbresponse'}); - return join("\n", @response); - } else { - return "error: ".$hash->{helper}{$name}{'lastadbresponse'}; - } - - } elsif($opt eq 'getprop') { - if(FireTV_adb($hash, 'shell getprop')) { - my @response = split(/[\n\r]+/, $hash->{helper}{$name}{'lastadbresponse'}); - return join("\n", @response); - } else { - return "error: ".$hash->{helper}{$name}{'lastadbresponse'}; - } - - } elsif($opt eq 'screen_state') { - # $screen_state is updated on every $opt (see above) - if($screen_state) { - return $screen_state; - } else { - return "error: ".$hash->{helper}{$name}{'lastadbresponse'}; - } - - } elsif($opt eq 'currentapp') { - my $currentapp = FireTV_currentFocus($hash); - if($currentapp) { - return $currentapp ; - } else { - return "error: ".$hash->{helper}{$name}{'lastadbresponse'}; - } - - } else { - return "Unknown argument $opt, choose one of packages:noArg isapprunning:".$hash->{helper}{$name}{'packages'}." adb getprop:noArg screen_state:noArg currentapp:noArg"; - } - return undef; -} - -sub FireTV_Set($@) { - my ($hash, @param) = @_; - - return '"set FireTV" needs at least one argument' if (int(@param) < 2); - - my $name = shift @param; - my $opt = shift @param; - my $value = join(" ", @param); - my $response = undef; - - if($opt =~ /^(appstart|appstop|apptoggle|button|screen|window|search|text|upload|uploadandview|view|deletefile|install|adb|screenshot)$/) { - # $opt that need an adb-connection - if(FireTV_connect($hash)) { - # update screen_state reading - FireTV_screen_state($hash); - - if($opt eq 'appstart') { - $response = FireTV_app($hash, $value, 'start'); - } elsif($opt eq 'appstop') { - $response = FireTV_app($hash, $value, 'stop'); - } elsif($opt eq 'apptoggle') { - if(FireTV_is_app_running($hash, $value)) { - $response = FireTV_app($hash, $value, 'stop'); - } else { - $response = FireTV_app($hash, $value, 'start'); - } - - } elsif($opt eq 'button') { - if($value eq 'up') { - $response = FireTV_up($hash); - } elsif($value eq 'down') { - $response = FireTV_down($hash); - } elsif($value eq 'left') { - $response = FireTV_left($hash); - } elsif($value eq 'right') { - $response = FireTV_right($hash); - } elsif($value eq 'enter') { - $response = FireTV_enter($hash); - } elsif($value eq 'back') { - $response = FireTV_back($hash); - } elsif($value eq 'home') { - $response = FireTV_home($hash); - } elsif($value eq 'menu') { - $response = FireTV_menu($hash); - } elsif($value eq 'prev') { - $response = FireTV_prev($hash); - } elsif($value eq 'playpause') { - $response = FireTV_playpause($hash); - } elsif($value eq 'next') { - $response = FireTV_next($hash); - } - - } elsif($opt eq 'screen') { - if($value eq 'wakeup') { - $response = FireTV_wakeup($hash); - } elsif($value eq 'toggle') { - $response = FireTV_power($hash); - } elsif($value eq 'sleep') { - $response = FireTV_sleep($hash); - } - - } elsif($opt eq 'screenshot') { - # check if an internal timer is already running - my $pid=0; - if(exists($hash->{helper}{$name}{'blockingcall'}{'screenshot'}{RUNNING_PID})) { - $pid = $hash->{helper}{$name}{'blockingcall'}{'screenshot'}{RUNNING_PID}; - if($pid && !kill 0, $pid) { - Log3 $name, 4, "[$name] FireTV_Set screenshot: killing blockingcall $pid"; - delete($hash->{helper}{$name}{'blockingcall'}{'screenshot'}{RUNNING_PID}); - $pid=0; - } - } - if(!$pid) { - $hash->{helper}{$name}{'blockingcall'}{'screenshot'}{RUNNING_PID} = BlockingCall('FireTV_screenshot', $name, 'FireTV_screenshot_ok', 300, 'FireTV_screenshot_error', $name); - return undef; - } else { - return "screenshot already running ($pid)"; - } - - } elsif($opt eq 'window') { - if($value eq 'settings') { - $response = FireTV_settings($hash); - } elsif($value eq 'appsettings') { - $response = FireTV_appsettings($hash); - } elsif($value eq 'fotos') { - $response = FireTV_app($hash, 'com.amazon.bueller.photos', 'start'); - } elsif($value eq 'music') { - $response = FireTV_app($hash, 'com.amazon.bueller.music', 'start'); - } - - } elsif($opt eq 'search') { - $response = FireTV_search($hash, $value); - } elsif($opt eq 'searchonly') { - $response = FireTV_search_only($hash, $value); - } elsif($opt eq 'text') { - $response = FireTV_text($hash, $value); - - - } elsif($opt eq 'upload') { - my ($remotefile,$contenttype) = split(":", FireTV_uploadfile($hash, $value)); - if($remotefile) { - $response = "$remotefile:$contenttype"; - # internal timer to delete the uploaded file - if(defined($attr{$name}{uploaddeleteafter}) && $attr{$name}{uploaddeleteafter} >= 0) { - # check if an internal timer is already running - my $pid=0; - if(exists($hash->{helper}{$name}{'blockingcall'}{'deletefile_'.$remotefile}{RUNNING_PID})) { - $pid = $hash->{helper}{$name}{'blockingcall'}{'deletefile_'.$remotefile}{RUNNING_PID}; - if($pid && !kill 0, $pid) { - Log3 $name, 4, "[$name] FireTV_Set upload: killing blockingcall $pid"; - delete($hash->{helper}{$name}{'blockingcall'}{'deletefile_'.$remotefile}{RUNNING_PID}); - $pid=0; - } - } - if(!$pid) { - my $param = "$name|$remotefile|".$attr{$name}{uploaddeleteafter}; - Log3 $name, 4, "[$name] FireTV_Set upload: starting blockingcall to delete remotefile $remotefile in ".$attr{$name}{uploaddeleteafter}." seconds"; - $hash->{helper}{$name}{'blockingcall'}{'deletefile_'.$remotefile}{RUNNING_PID} = BlockingCall('FireTV_deletefile_blocking', $param, 'FireTV_deletefile_blocking_ok', $attr{$name}{uploaddeleteafter}+30, 'FireTV_deletefile_blocking_error', $param); - } else { - Log3 $name, 4, "[$name] FireTV_Set upload: blockingcall to delete remotefile $remotefile already running ($pid)"; - } - } - } else { - return "error while uploading localfile $value"; - } - - } elsif($opt eq 'view') { - my ($remotefile,$contenttype) = split(/\ |:/, $value); - if(! $contenttype) { - return "please specifiy the files contenttype, e.g: $remotefile image/png" - } - if(FireTV_wakeup($hash)) { - $response = FireTV_view($hash, $remotefile, $contenttype); - } - - } elsif($opt eq 'uploadandview') { - my ($remotefile,$contenttype) = split(":", FireTV_uploadfile($hash, $value)); - if($remotefile) { - if(FireTV_wakeup($hash)) { - $response = FireTV_view($hash, $remotefile, $contenttype); - } - - # internal timer to delete the uploaded file - if(defined($attr{$name}{upviewdeleteafter}) && $attr{$name}{upviewdeleteafter} >= 0) { - my $pid=0; - if(exists($hash->{helper}{$name}{'blockingcall'}{'deletefile_'.$remotefile}{RUNNING_PID})) { - $pid = $hash->{helper}{$name}{'blockingcall'}{'deletefile_'.$remotefile}{RUNNING_PID}; - if($pid && !kill 0, $pid) { - Log3 $name, 4, "[$name] FireTV_Set uploadandview: killing blockingcall $pid"; - delete($hash->{helper}{$name}{'blockingcall'}{'deletefile_'.$remotefile}{RUNNING_PID}); - $pid=0; - } - } - if(!$pid) { - my $param = "$name|$remotefile|".$attr{$name}{upviewdeleteafter}; - Log3 $name, 4, "[$name] FireTV_Set uploadandview: starting blockingcall to delete remotefile $remotefile in ".$attr{$name}{upviewdeleteafter}." seconds"; - $hash->{helper}{$name}{'blockingcall'}{'deletefile_'.$remotefile}{RUNNING_PID} = BlockingCall('FireTV_deletefile_blocking', $param, 'FireTV_deletefile_blocking_ok', $attr{$name}{upviewdeleteafter}+30, 'FireTV_deletefile_blocking_error', $param); - } else { - Log3 $name, 4, "[$name] FireTV_Set uploadandview: blockingcall to delete remotefile $remotefile already running ($pid)"; - } - } - } else { - return "error while uploading localfile $value"; - } - - } elsif($opt eq 'deletefile') { - return FireTV_deletefile($hash, $value); - - } elsif($opt eq 'install') { - # implemented as blocking call - # there should be no need to implement this nonblocking, since apk installation is usually something you oversee - $response = FireTV_adb($hash, "install -r $value"); - - } elsif($opt eq 'adb') { - $response = FireTV_adb($hash, $value); - } - } else { - $response = "error: Couldn't connect to FireTV ".$hash->{NAME}." (".$hash->{IP}.")"; - } - } elsif($opt =~ /^(connect|disconnect|statusRequest)$/) { - if($opt eq 'connect') { - $response = FireTV_connect($hash); - } elsif($opt eq 'disconnect') { - $response = FireTV_connect($hash, 'disconnect'); - - # PRESENCE - } elsif($opt eq 'statusRequest' && $hash->{helper}{$name}{'PRESENCE_loaded'}) { - if($hash->{MODE} ne "lan-bluetooth") { - Log3 $name, 4, "[$name] FireTV_Attr: starting local scan"; - return PRESENCE_StartLocalScan($hash, 1); - } else { - if(exists($hash->{FD})) { - DevIo_SimpleWrite($hash, "now\n", 2); - } else { - return "FireTV_Attr Definition '$name' is not connected to ".$hash->{DeviceName}; - } - } - } - - } else { - my $packages = $hash->{helper}{$name}{'packages'} || ''; - - my @buttons = sort(qw(up down left right enter back home menu prev playpause next)); - my @keys = sort(qw(KEYCODE_DPAD_UP KEYCODE_DPAD_DOWN KEYCODE_DPAD_LEFT KEYCODE_DPAD_CENTER - KEYCODE_DPAD_RIGHT KEYCODE_BACK KEYCODE_HOME KEYCODE_MENU KEYCODE_MEDIA_PREVIOUS - KEYCODE_MEDIA_PLAY_PAUSE KEYCODE_MEDIA_FAST_FORWARD KEYCODE_WAKEUP KEYCODE_POWER)); - my @windows = sort(qw(appsettings settings fotos music)); - - my @presence; - if($hash->{helper}{$name}{'PRESENCE_loaded'}) { - push @presence, 'statusRequest:noArg'; - } - - return "Unknown argument $opt choose one of " - ."appstart:".$packages." appstop:".$packages." apptoggle:".$packages." " - ."connect:noArg disconnect:noArg screen:sleep,toggle,wakeup screenshot:noArg " - ."search searchonly text upload uploadandview view deletefile install adb " - ."key:".join(',', @keys)." " - ."button:".join(',', @buttons)." " - ."window:".join(',', @windows)." " - .join(',', @presence)." "; - } - - if(!$response) { - $response = "error: ".$hash->{helper}{$name}{'lastadbresponse'}; - } - return $response eq "1"?undef:$response; -} - -sub FireTV_Attr(@) { - my ($cmd,$name,$attr_name,$attr_value) = @_; - - my $hash = $defs{$name}; - - if($cmd eq "set") { - my $err; - - if($attr_name eq "holdconnection") { - if($attr_value !~ /^yes|no$/) { - $err = "Invalid argument $attr_value to $attr_name. Must be yes or no."; - } - - } elsif($attr_name =~ /^upviewdeleteafter|uploaddeleteafter$/) { - if($attr_value !~ /^\d*$/) { - $err = "Invalid argument $attr_value to $attr_name. Must be a valid integer number."; - } - - } elsif($attr_name eq "screenshotpath") { - my $basename = $attr_value; - if(-d $basename ) { - if($basename !~ /\/$/) { - $basename .= '/'; - } - } else { - $basename =~ s|(.*/).*|$1|; - } - if(! -w $basename) { - $err = "$basename is not writeable"; - } - - } elsif($attr_name =~ "/^(absenceThreshold|presenceThreshold|ping_count)$/") { - if($attr_value !~ /^\d+$/) { - $err = "$attr_name must be a valid integer number"; - } - if($hash->{MODE} eq "event") { - $err = "$attr_name is not applicable for mode 'event'"; - } - - } elsif($attr_name =~ "/^(absenceTimeout|presenceTimeout)$/") { - if($attr_value !~ /^\d?\d(?::\d\d){0,2}$/) { - $err = "$attr_value is not a valid time frame value. See commandref on PRESENCE for the correct syntax" ; - } - if($hash->{MODE} ne "event") { - $err = "$attr_name is only applicable for mode 'event'"; - } - - } elsif($attr_name eq "disable") { - if($attr_value) { - $hash->{STATE} = 'disabled'; - RemoveInternalTimer($hash); - } else { - $hash->{STATE} = 'defined'; - FireTV_ReadDeviceInfo($hash); - PRESENCE_StartLocalScan($hash, 1); - } - readingsSingleUpdate($hash, "state", $hash->{STATE}, 1); - } - - if($err) { - Log3 $name, 3, "[$name] FireTV_Attr ERROR: $err"; - return $err; - } - - } elsif($cmd eq "del") { - if($attr_name eq "disable") { - $hash->{STATE} = 'defined'; - FireTV_ReadDeviceInfo($hash); - PRESENCE_StartLocalScan($hash, 1); - readingsSingleUpdate($hash, "state", $hash->{STATE}, 1); - } - } - return undef; -} - -sub FireTV_Notify($$) { - my ($hash,$dev) = @_; - my $name = $hash->{NAME}; - my $dev_name = $dev->{NAME}; - - return undef if(!defined($hash) or !defined($dev)); - return undef if(!defined($dev_name) or !defined($name)); - - my $events = deviceEvents($dev,0); - - if($hash->{helper}{$name}{'PRESENCE_loaded'}) { - # reread packages on state change from absent to present - if($dev_name eq $name) { - foreach my $event (@{$events}) { - if($event eq 'present' && OldValue($name) eq 'absent') { - Log3 $name, 4, "[$name] FireTV_Notify: changed state from absent to present; reread packages"; - FireTV_Get($hash, $name, 'packages'); - Log3 $name, 4, "[$name] FireTV_Notify: changed state from absent to present; reread device info"; - FireTV_ReadDeviceInfo($hash); - } - } - } else { - return PRESENCE_Notify($hash, $dev); - } - } - return; -} - - -# wrapper for the adb command -sub FireTV_adb($$) { - my $hash = shift; - my $cmd = shift; - - if(ref $hash ne 'HASH' ) { - $hash = $defs{$hash}; - } - my $name = $hash->{NAME}; - my $ip = $hash->{IP}; - - # connect if not connected - # don't rely on that! - # always call FireTV_connect before issuing commands, to make sure that - # an old/broken connection is reset first - if($cmd !~ /^(?:dis)?connect/) { - if(!$hash->{adbconnected}) { - FireTV_connect($hash); - } - } - - if($hash->{adbconnected} || $cmd =~ /^connect/ ) { - my $deviceid = "-s ".$hash->{IP}.":".$hash->{PORT}; - if($cmd =~ /^connect/) { - $deviceid=''; - } - $hash->{helper}{$name}{lastadbcmd} = $hash->{ADB}." $deviceid $cmd"; - Log3 $name, 4, "[$name] FireTV_adb command: ".$hash->{helper}{$name}{lastadbcmd}; - - # execute command - $hash->{helper}{$name}{lastadbresponse} = `$hash->{helper}{$name}{lastadbcmd} 2>&1` || ''; - - # check if adb server needs a restart - if($hash->{helper}{$name}{lastadbresponse} =~ /cannot bind '.*?:5037'/) { - Log3 $name, 4, "[$name] FireTV_adb response: ".$hash->{helper}{$name}{lastadbresponse}; - Log3 $name, 4, "[$name] FireTV_adb: restarting adb server and repeating last command"; - system($hash->{ADB}." kill-server"); - system($hash->{ADB}." start-server"); - system($hash->{ADB}." connect ".$hash->{IP}.":".$hash->{PORT}); - $hash->{helper}{$name}{lastadbresponse} = `$hash->{helper}{$name}{lastadbcmd} 2>&1` || ''; - } - - $hash->{helper}{$name}{lastadbresponse} =~ s/^\s*//sg; - $hash->{helper}{$name}{lastadbresponse} =~ s/\s*$//sg; - - Log3 $name, 4, "[$name] FireTV_adb response: ".$hash->{helper}{$name}{lastadbresponse} if $hash->{helper}{$name}{lastadbresponse}; - return 1; - } else { - Log3 $name, 4, "[$name] FireTV_adb not connected: ".$hash->{helper}{$name}{lastadbresponse}; - } - return undef; -} - -# connect/disconnect adb to your device -sub FireTV_connect($;$) { - my $hash = shift; - my $action = shift || 'connect'; - - if(ref $hash ne 'HASH' ) { - $hash = $defs{$hash}; - } - my $name = $hash->{NAME}; - my $ip = $hash->{IP}; - - # if marked as connected, disconnect first - if($action eq 'connect' && $hash->{adbconnected}) { - if(!defined($attr{$name}{holdconnection}) || $attr{$name}{holdconnection} ne 'yes') { - system($hash->{ADB}." disconnect ".$hash->{IP}.":".$hash->{PORT}); - # FireTV_adb($hash, "disconnect $ip"); - } else { - Log3 $name, 4, "[$name] FireTV_connect: no disconnect because of holdconnection yes"; - } - } - - # connect/disconnect - if(FireTV_adb($hash, "$action $ip")) { - if($action eq 'disconnect') { - Log3 $name, 4, "[$name] FireTV_connect (disconnect): ".$hash->{helper}{$name}{lastadbresponse}; - $hash->{adbconnected} = 0; - return 1; - } elsif($action eq 'connect' && $hash->{helper}{$name}{lastadbresponse} =~ 'unable to connect') { - Log3 $name, 4, "[$name] FireTV_connect (connect): ".$hash->{helper}{$name}{lastadbresponse}; - $hash->{adbconnected} = 0; - } else { - Log3 $name, 4, "[$name] FireTV_connect (connect): ".$hash->{helper}{$name}{lastadbresponse}; - $hash->{adbconnected} = 1; - } - return $hash->{adbconnected}; - } - return undef; -} - -# send a single keyevent -sub FireTV_key($$) { - my $hash = shift; - my $key = shift; - - return FireTV_adb($hash, "shell input keyevent $key"); -} - -# send a text -sub FireTV_text($;$) { - my $hash = shift; - my $text = shift; - $text =~ s/ /%s/g; - - return FireTV_adb($hash, "shell input text $text"); -} - - -# fire remote buttons -sub FireTV_up($) { - return FireTV_key(shift, "KEYCODE_DPAD_UP"); -} -sub FireTV_down($) { - return FireTV_key(shift, "KEYCODE_DPAD_DOWN"); -} -sub FireTV_left($) { - return FireTV_key(shift, "KEYCODE_DPAD_LEFT"); -} -sub FireTV_enter($) { - return FireTV_key(shift, "KEYCODE_DPAD_CENTER"); -} -sub FireTV_right($) { - return FireTV_key(shift, "KEYCODE_DPAD_RIGHT"); -} -sub FireTV_back($) { - return FireTV_key(shift, "KEYCODE_BACK"); -} -sub FireTV_home($) { - return FireTV_key(shift, "KEYCODE_HOME"); -} -sub FireTV_menu($) { - return FireTV_key(shift, "KEYCODE_MENU"); -} -sub FireTV_prev($) { - return FireTV_key(shift, "KEYCODE_MEDIA_PREVIOUS"); -} -sub FireTV_playpause($) { - return FireTV_key(shift, "KEYCODE_MEDIA_PLAY_PAUSE"); -} -sub FireTV_next($) { - return FireTV_key(shift, "KEYCODE_MEDIA_FAST_FORWARD"); -} - -sub FireTV_wakeup($) { - # wakeup from daydream - return FireTV_key(shift, "KEYCODE_WAKEUP"); -} -sub FireTV_power($) { - # press power button -> go to sleep or wakeup - return FireTV_key(shift, "KEYCODE_POWER"); -} -sub FireTV_sleep($) { - my $hash = shift; - if(FireTV_key($hash, "KEYCODE_WAKEUP")) { - usleep(10000); - return FireTV_key($hash, "KEYCODE_POWER"); - } - return undef; -} - -# inspired by https://github.com/happyleavesaoc/python-firetv -sub FireTV_dumpsys($$) { - my $hash = shift; - my $service = shift; - - if(ref $hash ne 'HASH' ) { - $hash = $defs{$hash}; - } - my $name = $hash->{NAME}; - - if(FireTV_adb($hash, "shell dumpsys $service")) { - return $hash->{helper}{$name}{lastadbresponse}; - } - return; -} - -sub FireTV_dumpsys_has($$$) { - my $hash = shift; - my $service = shift; - my $regex = shift; - - my $dump = FireTV_dumpsys($hash, $service); - - return $dump =~ $regex; -} - -sub FireTV_currentFocus($) { - my $hash = shift; - - my $dump = FireTV_dumpsys($hash, "window windows"); - if($dump =~ /mCurrentFocus=Window\{.*?\s.*?\s(.*?)\}/) { - return $1; - } - return; -} - -sub FireTV_screen_state($) { - my $hash = shift; - - my $screen_state; - if(! FireTV_dumpsys_has($hash, 'power', 'Display Power: state=ON')) { - $screen_state = 'off'; - } elsif(! FireTV_dumpsys_has($hash, 'power', 'mWakefulness=Awake')) { - if(FireTV_currentFocus($hash) =~ /^dream$/) { - $screen_state = 'daydream'; - } else { - $screen_state = 'idle'; - } - } elsif(FireTV_currentFocus($hash) =~ /^com.amazon.tv.launcher/) { - $screen_state = 'standby' - } elsif(! FireTV_dumpsys_has($hash, 'power', 'Locks: size=0')) { - $screen_state = 'playing' - } else { - $screen_state = 'paused' - } - - readingsSingleUpdate($hash, "screen_state", $screen_state, 1); - return $screen_state; -} - -# complex actions - -# navigate to global search, enter some text and navigate to the first result -sub FireTV_search($$) { - my $hash = shift; - my $text = shift; - - if($text) { - if(FireTV_search_only($hash,$text) && FireTV_down($hash) && FireTV_enter($hash)) { - return 1; - } - } - return undef; -} - -sub FireTV_search_only($$) { - my $hash = shift; - my $text = shift; - - if(ref $hash ne 'HASH' ) { - $hash = $defs{$hash}; - } - - my $osversion = 0; - if($hash->{OSVERSION} =~ /\((\d+)\)/ ) { - $osversion = $1; - } - - if(FireTV_wakeup($hash)) { - usleep(5000); - FireTV_home($hash); - usleep(5000); - FireTV_home($hash); - # we're on the homescreen now - if($osversion && $osversion < 573210520) { - # search is 'up' on older ui versions - FireTV_up($hash); - usleep(200); - } else { - # ...and 'left' on newer builds - FireTV_left($hash); - usleep(200); - } - if(FireTV_enter($hash)) { - usleep(200); - if($text) { - # input searchtext - if(FireTV_text($hash,$text)) { - return FireTV_next($hash); - } - } - } - } - return undef; -} - -# navigate to system settings -sub FireTV_settings($) { - my $hash = shift; - if(FireTV_wakeup($hash)) { - usleep(10000); - return FireTV_adb($hash, "shell am start -n com.amazon.tv.launcher/.ui.SettingsActivity"); - } - return undef; -} - -# navigate to system settings -> installed apps -sub FireTV_appsettings($) { - my $hash = shift; - if(FireTV_wakeup($hash)) { - usleep(10000); - return FireTV_adb($hash, "shell am start -n com.amazon.tv.settings/.tv.AllApplicationsSettingsActivity"); - } - return undef; -} - -sub FireTV_app($$;$) { - my $hash = shift; - my $app = shift; - my $action = shift || 'start'; - my $name = $hash->{NAME}; - - if(FireTV_wakeup($hash)) { - if($action eq 'start') { - my $response; - # try LEANBACK_LAUNCHER intent - Log3 $name, 4, "[$name] FireTV_app: trying LEANBACK_LAUNCHER for $app"; - if(FireTV_adb($hash, "shell monkey -p $app -c android.intent.category.LEANBACK_LAUNCHER 1")) { - $response = $hash->{helper}{$name}{lastadbresponse}; - if($response !~ /No activities found to run, monkey aborted/i) { - return $app.' started'; - } - } - - # try LAUNCHER intent - Log3 $name, 4, "[$name] FireTV_app: trying LAUNCHER for $app"; - if(FireTV_adb($hash, "shell monkey -p $app -c android.intent.category.LAUNCHER 1")) { - $response = $hash->{helper}{$name}{lastadbresponse}; - if($response !~ /No activities found to run, monkey aborted/i) { - return $app.' started'; - } - } - - Log3 $name, 4, "[$name] FireTV_app: couldn't start $app"; - return "error: ".$response; - } elsif($action eq 'stop') { - if(FireTV_adb($hash, "shell am force-stop $app")) { - return $app.' stopped'; - } - } - } - return undef; -} - -sub FireTV_is_app_running($$) { - my $hash = shift; - my $app = shift; - - my $name = $hash->{NAME}; - - if(FireTV_adb($hash, 'shell ps|grep '.$app)) { - my $adb = $hash->{helper}{$name}{lastadbresponse}; - if($adb =~ /(.+?)\s+(\d+)\s+.*$app/) { - my $pid = $2; - return $pid; - } - return 0; - } - return undef; -} - -sub FireTV_rndnam($) { - return shift - .chr(97+rand(24)) - .chr(97+rand(24)) - .chr(97+rand(24)) - .chr(97+rand(24)) - .chr(97+rand(24)) - .chr(97+rand(24)) - .int(rand(8999)+1000); -} - -sub FireTV_tempfile($;$$$) { - my $hash = shift; - my $prefix = shift || '/sdcard/'; - my $suffix = shift || ''; - my $maxtries = shift || 5000; - - my $name = $hash->{NAME}; - - my $c=0; - my $tempfile = FireTV_rndnam($prefix).$suffix; - until(FireTV_adb($hash, "shell ls $tempfile") && $hash->{helper}{$name}{lastadbresponse} =~ /no such file or directory/i) { - $tempfile = FireTV_rndnam($prefix).$suffix; - return undef if($hash->{helper}{$name}{lastadbresponse} =~ /device is offline/i); - return undef if(++$c>$maxtries); - } - if(! FireTV_adb($hash, "shell touch $tempfile")) { - Log3 $name, 3, "[$name] FireTV_tempfile: couldn't touch tempfile ".$tempfile; - } - Log3 $name, 4, "[$name] FireTV_tempfile tempfile: ".$tempfile; - return $tempfile; -} - -sub FireTV_localtempfile($;$$$) { - my $hash = shift; - my $prefix = shift || tempdir( CLEANUP=>1 ); - my $suffix = shift || ''; - my $maxtries = shift || 5000; - - my $name = $hash->{NAME}; - - if($prefix !~ /\/$/) { - $prefix .= '/'; - } - - my $c=0; - my $tempfile = FireTV_rndnam($prefix).$suffix; - until(! -e $tempfile) { - $tempfile = FireTV_rndnam($prefix).$suffix; - return undef if(++$c>$maxtries); - } - if(open my $fh, ">>", $tempfile) { - Log3 $name, 4, "[$name] FireTV_localtempfile tempfile: ".$tempfile; - close $fh; - return $tempfile; - } - return undef; -} - -sub FireTV_screenshot($) { - my $hash = shift; - if(ref $hash ne 'HASH' ) { - $hash = $defs{$hash}; - } - my $name = $hash->{NAME}; - - if(FireTV_connect($hash)) { - if(FireTV_wakeup($hash)) { - my $remote_tempfile = FireTV_tempfile($hash, '/sdcard/screenshot'); - if(FireTV_adb($hash, "shell screencap -p $remote_tempfile")) { - my $localfile; - if(!defined($attr{$name}{screenshotpath})) { - my ($fh, $localfile) = tempfile(); - } elsif(-d $attr{$name}{screenshotpath}) { - $localfile = FireTV_localtempfile($hash, $attr{$name}{screenshotpath}, '.png') - } else { - $localfile = $attr{$name}{screenshotpath}; - } - - if($localfile) { - FireTV_adb($hash, "pull $remote_tempfile $localfile"); - if(! -e $localfile) { - Log3 $name, 3, "[$name] FireTV_screenshot: couldn't pull to localfile $localfile (".$hash->{helper}{$name}{lastadbresponse}.")"; - } - } else { - Log3 $name, 3, "[$name] FireTV_screenshot: couldn't create localfile (".$hash->{helper}{$name}{lastadbresponse}.")"; - } - if(! FireTV_adb($hash, "shell rm $remote_tempfile")) { - Log3 $name, 3, "[$name] FireTV_screenshot: couldn't delete remote tempfile $remote_tempfile"; - } - - return "$name|$localfile"; - } - } - } - return undef; -} - -sub FireTV_deletefile($$) { - my $hash = shift; - my $remotefile = shift; - if(ref $hash ne 'HASH' ) { - $hash = $defs{$hash}; - } - my $name = $hash->{NAME}; - - # only allow to delete files that this device has uploaded - # uploaded files are memorized in internalval uploadedfiles - $remotefile =~ s/:.*//; - my @uploadedfiles = split(/,\ */, $hash->{uploadedfiles}); - if(grep {$_ =~ /^$remotefile:/ } @uploadedfiles) { - @uploadedfiles = grep { $_ !~ /^$remotefile:/ } @uploadedfiles; - $hash->{uploadedfiles} = join ', ', @uploadedfiles; - my $response = FireTV_adb($hash, "shell rm $remotefile"); - Log3 $name, 4, "[$name] FireTV_deletefile: deletefile $remotefile: $response"; - return $response; - } else { - if($remotefile eq '--all') { - Log3 $name, 4, "[$name] FireTV_deletefile: deletefile --all"; - if(@uploadedfiles > 0) { - my @deleted; - foreach my $ufile (@uploadedfiles) { - my ($file, $type) = split(/:/, $ufile); - if(FireTV_adb($hash, "shell rm $file")) { - Log3 $name, 4, "[$name] FireTV_deletefile: deleted file $file"; - my @u = split(/,\ */, $hash->{uploadedfiles}); - @u = grep { $_ !~ /^$file:/ } @u; - $hash->{uploadedfiles} = join ', ', @u; - push @deleted, $file; - } - } - return "deleted all uploaded files: ".join("\n", @deleted); - } else { - Log3 $name, 4, "[$name] FireTV_deletefile: no uploaded files to delete"; - return "no uploaded files to delete"; - } - } - return "$remotefile wasn't uploaded by this device, so I reject deleting it"; - } -} - -sub FireTV_deletefile_blocking($) { - my @param = split(/\|/, shift); - my $name = $param[0]; - my $remotefile = $param[1]; - my $delay = $param[2] || 0; - my $hash = $defs{$name}; - - if($remotefile ne '--all') { - sleep($delay); - return "$name|$remotefile|".FireTV_deletefile($hash, $remotefile); - } else { - Log3 $name, 3, "[$name] FireTV_deletefile_blocking: --all is not allowed here"; - } -} - -sub FireTV_deletefile_blocking_ok($) { - my @param = split(/\|/, shift); - my $name = $param[0]; - my $remotefile = $param[1]; - my $response = $param[2]; - my $hash = $defs{$name}; - - Log3 $name, 4, "[$name] FireTV_deletefile_blocking_ok: $response"; - - # delete the file from uploadedfiles - # FireTV_deletefile does this, but it's lost in the fork - my @uploadedfiles = split(/,\ */, $hash->{uploadedfiles}); - @uploadedfiles = grep { $_ !~ /^$remotefile:/ } @uploadedfiles; - $hash->{uploadedfiles} = join ', ', @uploadedfiles; - - delete($hash->{helper}{$name}{'blockingcall'}{'deletefile_'.$remotefile}{RUNNING_PID}); -} - -sub FireTV_deletefile_blocking_error($) { - my @param = split(/\|/, shift); - my $name = $param[0]; - my $remotefile = $param[1]; - my $delay = $param[2] || 0; - my $hash = $defs{$name}; - - Log3 $name, 3, "[$name] FireTV_deletefile_blocking_error: $name"; - delete($hash->{helper}{$name}{'blockingcall'}{'deletefile_'.$remotefile}{RUNNING_PID}); -} - - -sub FireTV_screenshot_ok($) { - my @param = split(/\|/, shift); - my $name = $param[0]; - my $localfile = $param[1]; - - my $hash = $defs{$name}; - - if(-r $localfile && -s $localfile) { - Log3 $name, 4, "[$name] FireTV_screenshot_ok: $localfile"; - readingsSingleUpdate($hash, "screenshot", $localfile, 1); - } else { - readingsSingleUpdate($hash, "screenshot", '', 1); - my $details = ''; - if(!-e $localfile) { - $details = 'file does not exist'; - } elsif(!-r $localfile) { - $details = 'file ist not readable'; - } elsif(!-s $localfile) { - $details = 'file ist zero sized'; - } - Log3 $name, 3, "[$name] FireTV_screenshot_ok: something went wrong when saving $localfile for $name ($details)"; - } - - delete($hash->{helper}{$name}{'blockingcall'}{'screenshot'}{RUNNING_PID}); -} - -sub FireTV_screenshot_error($) { - my $name = shift; - my $hash = $defs{$name}; - - Log3 $name, 3, "[$name] FireTV_screenshot_error: $name"; - readingsSingleUpdate($hash, "screenshot", '', 1); - delete($hash->{helper}{$name}{'blockingcall'}{'screenshot'}{RUNNING_PID}); -} - -sub FireTV_uploadfile($$;$$) { - my $hash = shift; - if(ref $hash ne 'HASH' ) { - $hash = $defs{$hash}; - } - my $name = $hash->{NAME}; - - my $localfile = shift; - my $remotefile = shift || FireTV_tempfile($hash); - my $contenttype = shift; - - if(! -r $localfile) { - Log3 $name, 3, "[$name] FireTV_uploadfile: can't read localfile $localfile"; - return; - } - - if(FireTV_adb($hash, "push $localfile $remotefile")) { - # logic to guess the content-type needs File::MimeInfo - # content-type is needed for FireTV_view - if(! $contenttype) { - eval 'use File::MimeInfo "mimetype";1'; - if($@) { - Log3 $name, 3, "[$name] FireTV_uploadfile: please install File::MimeInfo to automatically guess the content-type of uploaded files"; - } else { - $contenttype = mimetype($localfile); - } - } - - # memorize uploaded files in internalval uploadedfiles - my @uploadedfiles = split(/,\ */, $hash->{uploadedfiles}); - push @uploadedfiles, "$remotefile:$contenttype"; - $hash->{uploadedfiles} = join ', ', @uploadedfiles; - - return "$remotefile:$contenttype"; - } else { - Log3 $name, 3, "[$name] FireTV_uploadfile: couldn't upload localfile $localfile to remotefile $remotefile (".$hash->{helper}{$name}{lastadbresponse}.")"; - } - return undef; -} - -sub FireTV_view($$$) { - my $hash = shift; - if(ref $hash ne 'HASH' ) { - $hash = $defs{$hash}; - } - my $name = $hash->{NAME}; - my $remotefile = shift; - my $contenttype = shift; - - if($contenttype eq 'load') { - # download the file to guess it's contenttype - ugly! - my $localfile = FireTV_localtempfile($hash); - if(FireTV_adb($hash, "pull $remotefile $localfile")) { - # logic to guess the content-type needs File::MimeInfo - eval 'use File::MimeInfo "mimetype";1'; - if($@) { - Log3 $name, 3, "[$name] FireTV_view: please install File::MimeInfo to automatically guess the files content-type"; - } else { - $contenttype = mimetype($localfile); - } - unlink $localfile; - } else { - Log3 $name, 3, "[$name] FireTV_view: couldn't download remotefile $remotefile to localfile $localfile (".$hash->{helper}{$name}{lastadbresponse}.")"; - } - } - - if(! $contenttype) { - Log3 $name, 3, "[$name] FireTV_view: please specify the files content-type"; - return; - } - - return FireTV_adb($hash, "shell am start -a android.intent.action.VIEW -d file://$remotefile -t $contenttype"); -} - -# define FIRETV_REMOTE weblink htmlCode { FireTV_Remote('FIRETV', 'it_remote', 1) } -sub FireTV_Remote($;$$$) { - my $hash = shift; - my $remoteicon = shift || 'it_remote'; - my $collapsible = shift || 0; - my $devicelink = shift; - - if(ref $hash ne 'HASH' ) { - $hash = $defs{$hash}; - } - my $name = $hash->{NAME}; - if($hash->{TYPE} ne 'FireTV') { - return "$name is not of type FireTV"; - } - - if(defined($devicelink)){ - if($devicelink eq "0") { - $devicelink=""; - } else { - $devicelink="$devicelink"; - } - } else { - $devicelink="Remote for ".AttrVal($name, 'alias', $name).""; - } - - my $btncmd = "FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd.$name=set $name button "; - - my $remotehtml = AttrVal($name, 'remotehtml', undef); - if($remotehtml && $remotehtml =~ m/^\s*{.*}\s*$/) { - $remotehtml = eval $remotehtml; - } - - # example for remotehtml - # replace FIRETV (sub name and $name) with your devices name - # { - # my $name = 'FIRETV'; - # my $cmd = "FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd.$name=set $name "; - # - # return "
- # - # ".FW_makeImage("kodi", "Kodi", "rc-button")." - # ".FW_makeImage("spotify", "Spotify", "rc-button")." - # ".FW_makeImage("twitch", "twitch", "rc-button")." - # - # - # ".FW_makeImage("rc_TV\@red", "sleep", "rc-button")." - # ".FW_makeImage("rc_TV", "wakeup", "rc-button")." - # ".FW_makeImage("rc_SETUP", "settings", "rc-button")." - # - # - # ".FW_makeImage("upload", "upload", "rc-button")." - # ".FW_makeImage("upload\@red", "upload", "rc-button")." - # ".FW_makeImage("trash", "delete all", "rc-button")." - # - # - # ".FW_makeImage("image", "screenshot", "rc-button")." - # ".FW_makeImage("music", "music", "rc-button")." - # ".FW_makeImage("robot2", "hasta la vista", "rc-button")." - # "; - # } - - my $icon=''; - my $style=''; - if($collapsible) { - $icon = "" - .FW_makeImage($remoteicon, "expand/collapse", "rc-button").""; - $style = "style='display:none'"; - } else { - $icon = FW_makeImage($remoteicon, "Remote", "rc-button"); - } - - my $html = ""; - - return $html; -} - -sub FireTV_SetTimer($;$) { - my ($hash, $start) = @_; - my $nextTrigger; - my $name = $hash->{NAME}; - my $now = gettimeofday(); - $start = 0 if (!$start); - - my $interval = AttrVal($name, 'interval', 0); - if($interval) { - if ($hash->{TimeAlign}) { - my $count = int(($now - $hash->{TimeAlign} + $start) / $interval); - my $curCycle = $hash->{TimeAlign} + $count * $interval; - $nextTrigger = $curCycle + $interval; - } else { - $nextTrigger = $now + ($start ? $start : $interval); - } - - $hash->{TRIGGERTIME} = $nextTrigger; - $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger); - RemoveInternalTimer("update:$name"); - Log3 $name, 4, "[$name] FireTV_SetTimer: set InternalTimer"; - InternalTimer($nextTrigger, "FireTV_FetchStatus", "$name", 0); - } else { - $hash->{TRIGGERTIME} = 0; - $hash->{TRIGGERTIME_FMT} = ""; - } -} - -sub FireTV_FetchStatus($) { - my $hash = shift; - if(ref $hash ne 'HASH' ) { - $hash = $defs{$hash}; - } - my $name = $hash->{NAME}; - - if($hash->{STATE} eq 'absent' ) { - Log3 $name, 4, "[$name] FireTV_FetchStatus: Device is absent. Skipping"; - } elsif(!IsDisabled($name) && (!defined($attr{$name}{disabled}) || $attr{$name}{disabled} ne 'yes')) { - $hash->{BUSY} = 1; - $hash->{LASTSEND} = gettimeofday(); - - Log3 $name, 4, "[$name] FireTV_FetchStatus: starting FireTV_screen_state"; - FireTV_screen_state($hash); - $hash->{BUSY} = 0; - } else { - $hash->{STATE} = 'disabled'; - Log3 $name, 4, "[$name] FireTV_FetchStatus: Device is disabled. Skipping"; - } - FireTV_SetTimer($hash); -} - -1; - -=pod -=begin html - - -

FireTV

-
    - FireTV is used to remote control a Amazon FireTV device. It is not able - to read the currently playing music/movie or other status information, - but sending commands to the device. A working copy of adb is needed. -

    - Prerequisites -
      -
    • Activate adb debugging in your fire tv (see here)
    • -
    • Get adb for your fhem-server. Depending on your system, you have several options: - -
    • -
    • uses the perl module File::MimeInfo for some tasks. Installs via apt-get install libfile-mimeinfo-perl on some systems
    • -
    • uses 73_PRESENCE.pm by Markus Bloch for presence-detection (included in Fhem by default)
    • -
    -

    - - Define -
      - define <name> FireTV <HOST[:PORT]> [sudo] [<ADB_PATH>] [<PRESENCE_TIMEOUT_ABSENT>] [<PRESENCE_TIMEOUT_PRESENT>] [<PRESENCE_MODE>] [<PRESENCE_ADDRESS>]
      -
      - or, if 73_PRESENCE.pm is not available:
      -
      - define <name> FireTV <IP> [<ADB_PATH>]
      -
      - Example: define FIRETV FireTV 192.168.178.66 /usr/local/bin/adb -

      - HOST is the ip-address or hostname of your FireTV-device
      - PORT is the port where adb listens on the FireTV-device. It shouldn't be necessary to ever set this parameter. Default: 5555
      - sudo the keyword sudo ensures that adb is called using sudo. You need to add an entry for your fhem-user to call adb without password in /etc/sudoers - ADB_PATH is the full path to your adb-binary. Default: /usr/bin/adb
      - PRESENCE_TIMEOUT_ABSENT timeout (in seconds) to the next presence check if the device is absent. Default: 30
      - PRESENCE_TIMEOUT_PRESENT timeout (in seconds) to the next presence check if the device is present. Default: <PRESENCE_TIMEOUT_ABSENT>
      - PRESENCE_MODE mode for the presence check, see PRESENCE. Default: lan-ping
      - PRESENCE_ADRESS address for the presence check, see PRESENCE. Default: <IP>
      -
    -
    - - - Get
    -
      -
        -
      • adb <COMMAND>
        - Execute an adb command on your firetv and return it's response. Try adb help.
      • -
      • currentapp
        - Returns the package name off the currently active app
      • -
      • isapprunning <PACKAGE>
        - Returns the PID of a running app, or 0 if not running
      • -
      • getprop
        - Returns all of fires system properties
      • -
      • packages
        - Reads the list of installed packages on your firetv and stores it internally. get packages is called automatically when the device changes state from absent to present to populate the select-boxes for some other commands (e.g. appstart) in FHEMWEB
      • -
      • screen_state
        - Returns the current screen status. May be one off: off, idle, daydream, standby, playing, paused
      • -
      -
    - - - Set
    -
      -
        -
      • adb <COMMAND>
        - Execute an adb command on your firetv. If you need to see the devices response use the get version of this command instead.
      • -
      • appstart <PACKAGE>
        - Start an app on your firetv. You may read names of installed packages via get packages (see above)
      • -
      • appstop <PACKAGE>
        - Stop an app on your firetv
      • -
      • apptoggle <PACKAGE>
        - Start/stop an app on your firetv. Start if not running, stop otherwise
      • -
      • button <BUTTON>
        - Send a button-press to your firetv. Possible buttons (in order of appearance on a standard fire remote): - up, left, ok, right, down, back, home, menu, prev, playpause, next
      • -
      • connect
        - Connect adb to your firetv. This is done automatically by all defined set/get-commands -
      • -
      • deletefile <PATH>
        - Delete a file on your firetv. This command is restricted to files, that where uploaded vie upload/uploadandview (see below)
      • -
      • disconnect
        - Disconnect adb from your firetv
      • -
      • install <APK>
        - Install ("sideload") an apk-file on your firetv. APK is a local path on your - fhem-server. install is implemented as a blocking call, don't use it - in scripts
      • -
      • key <KEYCODE>
        - Send a standard android keycode to your firetv. You can send any keycode, - but firetv may not understand them all (known to work: KEYCODE_DPAD_UP, - KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_CENTER, KEYCODE_DPAD_RIGHT, - KEYCODE_BACK, KEYCODE_HOME, KEYCODE_MENU, KEYCODE_MEDIA_PREVIOUS, - KEYCODE_MEDIA_PLAY_PAUSE, KEYCODE_MEDIA_FAST_FORWARD, KEYCODE_WAKEUP, KEYCODE_POWER)
      • -
      • screen <wakeup|toggle|sleep>
        - Set screen to wake up or sleep or toggle between these states
      • -
      • screenshot
        - Take a screenshot and download it to a local tempfile on your fhem-server. - Since it may take some seconds to produce a screenshot, this function is - implemented nonblocking (iow: no direct feedback). The path to the local - tempfile is saved in a reading "screenshot" and may be set by the attribute - "screenshotpath". Screenshots taken while playing a movie/tv-show/etc from - amazons library are in general just black. On error the reading "screenshot" - is emptied
      • -
      • search <TEXT>
        - Navigate to the search-menu on your firetv, enter text and navigate to the first result
      • -
      • searchonly <TEXT>
        - Navigate to the search-menu on your firetv and enter text
      • -
      • statusRequest
        - Schedules an immediate presence-check
      • -
      • text <TEXT>
        - Send text to your firetv
      • -
      • uploadandview <PATH:CONTENTTYPE>
        - Upload a file to your firetv and view it on screen. The view action is - dependend on an arbitrary installed app that handles CONTENTTYPE and is - not limited to images. - You may omit CONTENTTYPE if you have the perl module File::MimeInfo - installed on your system. Such files may be automatically deleted when - the attribute upviewdeleteafter is set (see below)
      • -
      • upload <PATH>
        - Upload a file to your firetv. Such files may be automatically deleted - when the attribute uploaddeleteafter is set (see below)
      • -
      • view <PATH:CONTENTTYPE>
        - View a file on your firetv. The view action is dependend on an arbitrary - installed app that handles CONTENTTYPE and is - not limited to images. If you have the perl module File::MimeInfo installed - on your system, you may replace CONTENTTYPE whith the keyword load: - The file will be downloaded to your fhem-server prior viewing it on screen, then - (which may take some time and is generally speaking inefficient).
      • -
      • window <appsetting|fotos|music|settings>
        - Activate a named window of the firetv menu.
      • -
      -
    -
    - - - Attributes -
      -
        -
      • interval <SECONDS>
        - Setting interval to a number greater than 0 activates a cyclic refresh of screen_state every interval seconds
      • -
      • holdconnection yes|no
        - "yes" to keep the adb connection open or "no" to close it after every command. Default: no
      • -
      • remotehtml <HTML>
        - HTML to add to the output of FireTV_Remote() (see below)
      • -
      • screenshotpath <PATH>
        - If screenshotpath is set to a filename, every new screenshot (see set screenshot) will overwrite that file. - If set to a directory, a random file will be created in that directory. - If not set, a random file is created in your systems tempdirectory (POSIX tmpnam). Default: not set
      • -
      • uploaddeleteafter <SECONDS>
        - Files uploaded via set upload are deleted after SECONDS when set to a positve integer number. Default: not set
      • -
      • upviewdeleteafter <SECONDS>
        - Files uploaded via set uploadandview are deleted after SECONDS when set to a positve integer number. Default: not set
      • -
      - Inherited from PRESENCE: -
        -
      • absenceThreshold
      • -
      • absenceTimeout
      • -
      • disable
      • -
      • ping_count
      • -
      • presenceThreshold
      • -
      • presenceTimeout
      • -
      - Other: - -
    -
    - - - Values of STATE -
      -
        -
      • active
        - devicestatus is unknown, but a check is running (checked via 73_PRESENCE)
      • -
      • absent
        - device is absent (checked via 73_PRESENCE)
      • -
      • defined
        - device is defined
      • -
      • disabled
        - presence-check is disabled, all other functions may still work
      • -
      • present
        - device is present (checked via 73_PRESENCE)
      • -
      -
    - -

    - - FireTV_Remote() -
      - The module provides an additional function FireTV_Remote() which returns - html code for a graphic remote control usable in FHEMWEB. Just define a weblink - device like: -

      - define FIRETV_REMOTE weblink htmlCode { FireTV_Remote('FIRETV') }
      - define FIRETV_REMOTE weblink htmlCode { FireTV_Remote(DEVICE, ICON, COLLAPSIBLE, DEVICELINK) } -

      - Parameters: -
        -
      • DEVICE: Devicename of your FireTV-Device
      • -
      • ICON: Icon to display in the upper left corner. Default: it_remote
      • -
      • COLLAPSIBLE: If set to a positive value (1), the remote is displayed collapsed and will expand after clicking it's icon. Default: not set
      • -
      • DEVICELINK: If not set, the Remote has a clickable title which links to the controlled device. If set to "0" it has no title. If set to any other value, that value will be used as clickable title. Default: not set
      • -
      -
      - The content of the attribute remotehtml is added at the end of FireTV_Remote() - output. If the content is wrapped in curly brackets it is interpreted as perl-code. - The output is structured in a three-column table-layout. Here is an example of how to - add some additional buttons to the remote: -
      -
          {
      -        my $device = 'FIRETV';
      -        my $cmd = "FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd.$device=set $device ";
      -    
      -        return "<tr><td colspan='3'><hr></td></tr>
      -                <tr>
      -                    <td><a onClick=\"$cmd appstart org.xbmc.kodi')\">".FW_makeImage("kodi", "Kodi", "rc-button")."</a></td>
      -                    <td><a onClick=\"$cmd appstart com.spotify.tv.android')\">".FW_makeImage("spotify", "Spotify", "rc-button")."</a></td>
      -                    <td><a onClick=\"$cmd appstart tv.twitch.android.viewer')\">".FW_makeImage("twitch", "twitch", "rc-button")."</a></td>
      -                </tr>
      -                <tr>
      -                    <td><a onClick=\"$cmd screen sleep')\">".FW_makeImage("rc_TV\@red", "sleep", "rc-button")."</a></td>
      -                    <td><a onClick=\"$cmd screen wakeup')\">".FW_makeImage("rc_TV", "wakeup", "rc-button")."</a></td>
      -                    <td><a onClick=\"$cmd window settings')\">".FW_makeImage("rc_SETUP", "settings", "rc-button")."</a></td>
      -                </tr>";
      -    }
      -
    -
-=end html - -=cut diff --git a/FHEM/98_FireTV.pm b/FHEM/98_FireTV.pm index 3bdbb49..1057985 100644 --- a/FHEM/98_FireTV.pm +++ b/FHEM/98_FireTV.pm @@ -1,8 +1,8 @@ -######################################################################## +######################################################################## # 98_FireTV.pm # # Control a FireTV-Device from FHEM -# +# # Prerequisites: # 1.) enable adb debugging in your fire tv # 2.) get adb and copy the binary to /usr/bin/ @@ -44,10 +44,10 @@ sub FireTV_Initialize($) { $hash->{GetFn} = 'FireTV_Get'; $hash->{AttrFn} = 'FireTV_Attr'; $hash->{AttrList} = "holdconnection:yes,no screenshotpath upviewdeleteafter uploaddeleteafter remotehtml interval ".$readingFnAttributes; - - if(LoadModule("PRESENCE") eq "PRESENCE") { - # PRESENCE - $hash->{ReadFn} = "PRESENCE_Read"; + + if(LoadModule("PRESENCE") eq "PRESENCE") { + # PRESENCE + $hash->{ReadFn} = "PRESENCE_Read"; $hash->{ReadyFn} = "PRESENCE_Ready"; $hash->{NotifyFn} = "FireTV_Notify"; $hash->{AttrList} .= " ping_count:1,2,3,4,5,6,7,8,9,10" @@ -56,7 +56,7 @@ sub FireTV_Initialize($) { ." absenceTimeout presenceTimeout " ." do_not_notify:0,1 disable:0,1 disabledForIntervals "; # disabledForIntervals seems to be broken - TODO } - + # PORT was introduced later, set a default here to avoid users having to redefine their devices if(! defined($hash->{PORT})) { $hash->{PORT} = '5555'; @@ -66,16 +66,16 @@ sub FireTV_Initialize($) { sub FireTV_Define($$) { my ($hash, $def) = @_; my @param = split('[ \t]+', $def); - + my $name = $param[0]; - + if(LoadModule("PRESENCE") eq "PRESENCE") { $hash->{helper}{$name}{'PRESENCE_loaded'} = 1; } else { Log3 $name, 3, "[$name] FireTV_Define WARNING: couldn't load module PRESENCE"; - $hash->{helper}{$name}{'PRESENCE_loaded'} = 0; + $hash->{helper}{$name}{'PRESENCE_loaded'} = 0; } - + if(int(@param) < 3) { if($hash->{helper}{$name}{'PRESENCE_loaded'}) { return "too few parameters: define FireTV [sudo] [] [] [] [] []"; @@ -83,7 +83,7 @@ sub FireTV_Define($$) { return "too few parameters: define FireTV [sudo] []"; } } - + if(defined($param[2]) && $param[2]!~/^[a-z0-9-.]+(:\d{1,5})?$/i) { return "IP '".$param[2]."' is no valid ip address or hostname"; } @@ -105,7 +105,7 @@ sub FireTV_Define($$) { if(defined($param[6]) && $param[6]!~/^(lan-ping|lan-bluetooth|local-bluetooth|fritzbox|shellscript|function|event)$/) { return "PRESENCE_MODE '".$param[3]."' must be one of lan-ping, lan-bluetooth, local-bluetooth, fritzbox, shellscript, function or event"; } - + $hash->{NAME} = $name; $hash->{IP} = $param[2]; if($hash->{IP} =~ m/(.*?):(.*)/) { @@ -115,19 +115,19 @@ sub FireTV_Define($$) { $hash->{PORT} = '5555'; } $hash->{ADB} .= $param[3] || '/usr/bin/adb'; - $hash->{STATE} = 'defined'; + #readingsSingleUpdate($hash, "state", "defined", 1); $hash->{VERSION} = '0.6.1'; FireTV_ReadDeviceInfo($hash); - - + + if($hash->{helper}{$name}{'PRESENCE_loaded'}) { # PRESENCE $hash->{NOTIFYDEV} = "global,$name"; - $hash->{TIMEOUT_NORMAL} = $param[4] || 30; - $hash->{TIMEOUT_PRESENT} = $param[5] || $hash->{TIMEOUT_NORMAL}; + $hash->{INTERVAL_NORMAL} = $param[4] || 30; + $hash->{INTERVAL_PRESENT} = $param[5] || $hash->{INTERVAL_NORMAL}; $hash->{MODE} = $param[6] || 'lan-ping'; $hash->{ADDRESS} = $param[7] || $hash->{IP}; - + if(! IsDisabled($name)) { PRESENCE_StartLocalScan($hash, 1); } @@ -135,14 +135,14 @@ sub FireTV_Define($$) { Log3 $name, 4, "[$name] FireTV_Define: getting packagelist"; FireTV_Get($hash, $name, 'packages'); - + Log3 $name, 4, "[$name] FireTV_Define: starting FireTV_SetTimer"; FireTV_SetTimer($hash); return undef; } sub FireTV_ReadDeviceInfo($) { - my $hash = shift; + my $hash = shift; if(ref $hash ne 'HASH' ) { $hash = $defs{$hash}; } @@ -150,7 +150,7 @@ sub FireTV_ReadDeviceInfo($) { if(! IsDisabled($name)) { $hash->{ADBVERSION} = `$hash->{ADB} version 2>&1` || $!; - + if(FireTV_connect($hash)) { if(FireTV_adb($hash, 'shell cat /proc/version')) { my $OSVERSION = $hash->{helper}{$name}{'lastadbresponse'}; @@ -158,14 +158,14 @@ sub FireTV_ReadDeviceInfo($) { } else { Log3 $name, 4, "[$name] FireTV_ReadDeviceInfo: error reading OSVERSION"; } - + if(FireTV_adb($hash, 'shell getprop ro.build.version.name')) { my $OSNAME = $hash->{helper}{$name}{'lastadbresponse'}; $hash->{OSVERSION} = $OSNAME if $OSNAME !~ /error:/; } else { Log3 $name, 4, "[$name] FireTV_ReadDeviceInfo: error reading OSNAME"; } - + if(FireTV_adb($hash, 'shell getprop ro.serialno')) { my $SERIAL = $hash->{helper}{$name}{'lastadbresponse'}; $hash->{SERIAL} = $SERIAL if $SERIAL !~ /error:/; @@ -177,10 +177,10 @@ sub FireTV_ReadDeviceInfo($) { } sub FireTV_Undef($$) { - my ($hash, $arg) = @_; - + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; - + RemoveInternalTimer($hash); # PRESENCE if(defined($hash->{helper}{RUNNING_PID})) { @@ -192,30 +192,68 @@ sub FireTV_Undef($$) { BlockingKill($blockingcall->{RUNNING_PID}); } } - DevIo_CloseDev($hash); + DevIo_CloseDev($hash); + + return undef; +} + + +sub FireTV_UpdateState($) { + my ($hash) = @_; + + my $name = $hash->{NAME}; + + RemoveInternalTimer($hash); + + if(FireTV_connect($hash)) { + my $screen_state = FireTV_screen_state($hash); + my $currentapp = FireTV_currentFocus($hash); + + + readingsBeginUpdate( $hash ); + readingsBulkUpdate( $hash, "screenState", $screen_state ) if($screen_state); + readingsBulkUpdate( $hash, "currentApp", $currentapp ) if($currentapp); + readingsBulkUpdate( $hash, "state", "connected" ); + readingsEndUpdate( $hash, 1 ); + InternalTimer( gettimeofday() + AttrVal($name, 'interval', 900), "FireTV_UpdateState", $hash, 0 ); + } else { + readingsBeginUpdate( $hash ); + readingsBulkUpdate( $hash, "screenState", "off" ); + readingsBulkUpdate( $hash, "currentApp", "none" ); + readingsBulkUpdate( $hash, "state", "disconnected" ); + readingsEndUpdate( $hash, 1 ); + InternalTimer( gettimeofday() + AttrVal($name, 'interval', 900)*4, "FireTV_UpdateState", $hash, 0 ); + } return undef; } + sub FireTV_Get($@) { my ($hash, @param) = @_; - + return '"get FireTV" needs at least one argument' if (int(@param) < 2); - + my $name = shift @param; my $opt = shift @param; my $value = join(" ", @param); - + if(! FireTV_connect($hash)) { return "error: ".$hash->{helper}{$name}{'lastadbresponse'}; } - + my $screen_state; if($opt) { # update screen_state reading $screen_state = FireTV_screen_state($hash); + my $currentapp = FireTV_currentFocus($hash); + readingsBeginUpdate( $hash ); + readingsBulkUpdate( $hash, "screenState", $screen_state ) if($screen_state); + readingsBulkUpdate( $hash, "currentApp", $currentapp ) if($currentapp); + readingsBulkUpdate( $hash, "state", "connected"); + readingsEndUpdate( $hash, 1 ); } - + if($opt eq 'packages') { if(FireTV_adb($hash, 'shell pm list packages -f -3')) { my @response = split(/[\n\r]+/, $hash->{helper}{$name}{'lastadbresponse'}); @@ -224,7 +262,7 @@ sub FireTV_Get($@) { my ($package, $apk) = split('=', $line); push @apk, $apk; } - if(@apk > 0) { + if(defined($apk[0]) && @apk > 0) { @apk = sort(@apk); $hash->{helper}{$name}{'packages'} = join(',', @apk); return "Found the following installed packages: \n\n".join("\n", @apk); @@ -236,7 +274,7 @@ sub FireTV_Get($@) { return "error: ".$hash->{helper}{$name}{'lastadbresponse'}; } - } elsif($opt eq 'isapprunning') { + } elsif($opt eq 'isapprunning') { return FireTV_is_app_running($hash, $value); } elsif($opt eq 'adb') { @@ -246,7 +284,7 @@ sub FireTV_Get($@) { } else { return "error: ".$hash->{helper}{$name}{'lastadbresponse'}; } - + } elsif($opt eq 'getprop') { if(FireTV_adb($hash, 'shell getprop')) { my @response = split(/[\n\r]+/, $hash->{helper}{$name}{'lastadbresponse'}); @@ -272,26 +310,27 @@ sub FireTV_Get($@) { } } else { - return "Unknown argument $opt, choose one of packages:noArg isapprunning:".$hash->{helper}{$name}{'packages'}." adb getprop:noArg screen_state:noArg currentapp:noArg"; + my $packages = $hash->{helper}{$name}{'packages'} || ''; + return "Unknown argument $opt, choose one of packages:noArg isapprunning:".$packages." adb getprop:noArg screen_state:noArg currentapp:noArg"; } return undef; } sub FireTV_Set($@) { my ($hash, @param) = @_; - + return '"set FireTV" needs at least one argument' if (int(@param) < 2); - + my $name = shift @param; my $opt = shift @param; my $value = join(" ", @param); my $response = undef; - + if($opt =~ /^(appstart|appstop|apptoggle|button|screen|window|search|text|upload|uploadandview|view|deletefile|install|adb|screenshot)$/) { # $opt that need an adb-connection if(FireTV_connect($hash)) { # update screen_state reading - FireTV_screen_state($hash); + FireTV_UpdateState($hash); #FireTV_screen_state($hash); if($opt eq 'appstart') { $response = FireTV_app($hash, $value, 'start'); @@ -303,7 +342,7 @@ sub FireTV_Set($@) { } else { $response = FireTV_app($hash, $value, 'start'); } - + } elsif($opt eq 'button') { if($value eq 'up') { $response = FireTV_up($hash); @@ -322,12 +361,12 @@ sub FireTV_Set($@) { } elsif($value eq 'menu') { $response = FireTV_menu($hash); } elsif($value eq 'prev') { - $response = FireTV_playpause($hash); + $response = FireTV_prev($hash); } elsif($value eq 'playpause') { - $response = FireTV_enter($hash); + $response = FireTV_playpause($hash); } elsif($value eq 'next') { $response = FireTV_next($hash); - } + } } elsif($opt eq 'screen') { if($value eq 'wakeup') { @@ -337,7 +376,7 @@ sub FireTV_Set($@) { } elsif($value eq 'sleep') { $response = FireTV_sleep($hash); } - + } elsif($opt eq 'screenshot') { # check if an internal timer is already running my $pid=0; @@ -355,7 +394,7 @@ sub FireTV_Set($@) { } else { return "screenshot already running ($pid)"; } - + } elsif($opt eq 'window') { if($value eq 'settings') { $response = FireTV_settings($hash); @@ -366,15 +405,15 @@ sub FireTV_Set($@) { } elsif($value eq 'music') { $response = FireTV_app($hash, 'com.amazon.bueller.music', 'start'); } - + } elsif($opt eq 'search') { $response = FireTV_search($hash, $value); } elsif($opt eq 'searchonly') { $response = FireTV_search_only($hash, $value); } elsif($opt eq 'text') { $response = FireTV_text($hash, $value); - - + + } elsif($opt eq 'upload') { my ($remotefile,$contenttype) = split(":", FireTV_uploadfile($hash, $value)); if($remotefile) { @@ -418,8 +457,8 @@ sub FireTV_Set($@) { if(FireTV_wakeup($hash)) { $response = FireTV_view($hash, $remotefile, $contenttype); } - - # internal timer to delete the uploaded file + + # internal timer to delete the uploaded file if(defined($attr{$name}{upviewdeleteafter}) && $attr{$name}{upviewdeleteafter} >= 0) { my $pid=0; if(exists($hash->{helper}{$name}{'blockingcall'}{'deletefile_'.$remotefile}{RUNNING_PID})) { @@ -441,15 +480,15 @@ sub FireTV_Set($@) { } else { return "error while uploading localfile $value"; } - + } elsif($opt eq 'deletefile') { return FireTV_deletefile($hash, $value); - + } elsif($opt eq 'install') { # implemented as blocking call # there should be no need to implement this nonblocking, since apk installation is usually something you oversee $response = FireTV_adb($hash, "install -r $value"); - + } elsif($opt eq 'adb') { $response = FireTV_adb($hash, $value); } @@ -461,7 +500,7 @@ sub FireTV_Set($@) { $response = FireTV_connect($hash); } elsif($opt eq 'disconnect') { $response = FireTV_connect($hash, 'disconnect'); - + # PRESENCE } elsif($opt eq 'statusRequest' && $hash->{helper}{$name}{'PRESENCE_loaded'}) { if($hash->{MODE} ne "lan-bluetooth") { @@ -471,25 +510,25 @@ sub FireTV_Set($@) { if(exists($hash->{FD})) { DevIo_SimpleWrite($hash, "now\n", 2); } else { - return "FireTV_Attr Definition '$name' is not connected to ".$hash->{DeviceName}; + return "FireTV_Attr Definition '$name' is not connected to ".$hash->{DeviceName}; } - } - } - + } + } + } else { my $packages = $hash->{helper}{$name}{'packages'} || ''; - + my @buttons = sort(qw(up down left right enter back home menu prev playpause next)); - my @keys = sort(qw(KEYCODE_DPAD_UP KEYCODE_DPAD_DOWN KEYCODE_DPAD_LEFT KEYCODE_DPAD_CENTER - KEYCODE_DPAD_RIGHT KEYCODE_BACK KEYCODE_HOME KEYCODE_MENU KEYCODE_MEDIA_PREVIOUS + my @keys = sort(qw(KEYCODE_DPAD_UP KEYCODE_DPAD_DOWN KEYCODE_DPAD_LEFT KEYCODE_DPAD_CENTER + KEYCODE_DPAD_RIGHT KEYCODE_BACK KEYCODE_HOME KEYCODE_MENU KEYCODE_MEDIA_PREVIOUS KEYCODE_MEDIA_PLAY_PAUSE KEYCODE_MEDIA_FAST_FORWARD KEYCODE_WAKEUP KEYCODE_POWER)); my @windows = sort(qw(appsettings settings fotos music)); - + my @presence; if($hash->{helper}{$name}{'PRESENCE_loaded'}) { push @presence, 'statusRequest:noArg'; } - + return "Unknown argument $opt choose one of " ."appstart:".$packages." appstop:".$packages." apptoggle:".$packages." " ."connect:noArg disconnect:noArg screen:sleep,toggle,wakeup screenshot:noArg " @@ -508,9 +547,9 @@ sub FireTV_Set($@) { sub FireTV_Attr(@) { my ($cmd,$name,$attr_name,$attr_value) = @_; - + my $hash = $defs{$name}; - + if($cmd eq "set") { my $err; @@ -518,12 +557,12 @@ sub FireTV_Attr(@) { if($attr_value !~ /^yes|no$/) { $err = "Invalid argument $attr_value to $attr_name. Must be yes or no."; } - + } elsif($attr_name =~ /^upviewdeleteafter|uploaddeleteafter$/) { if($attr_value !~ /^\d*$/) { $err = "Invalid argument $attr_value to $attr_name. Must be a valid integer number."; } - + } elsif($attr_name eq "screenshotpath") { my $basename = $attr_value; if(-d $basename ) { @@ -531,12 +570,12 @@ sub FireTV_Attr(@) { $basename .= '/'; } } else { - $basename =~ s|(.*/).*|$1|; + $basename =~ s|(.*/).*|$1|; } if(! -w $basename) { $err = "$basename is not writeable"; } - + } elsif($attr_name =~ "/^(absenceThreshold|presenceThreshold|ping_count)$/") { if($attr_value !~ /^\d+$/) { $err = "$attr_name must be a valid integer number"; @@ -544,7 +583,7 @@ sub FireTV_Attr(@) { if($hash->{MODE} eq "event") { $err = "$attr_name is not applicable for mode 'event'"; } - + } elsif($attr_name =~ "/^(absenceTimeout|presenceTimeout)$/") { if($attr_value !~ /^\d?\d(?::\d\d){0,2}$/) { $err = "$attr_value is not a valid time frame value. See commandref on PRESENCE for the correct syntax" ; @@ -552,45 +591,43 @@ sub FireTV_Attr(@) { if($hash->{MODE} ne "event") { $err = "$attr_name is only applicable for mode 'event'"; } - + } elsif($attr_name eq "disable") { if($attr_value) { - $hash->{STATE} = 'disabled'; + readingsSingleUpdate($hash, "state", "disabled", 1); RemoveInternalTimer($hash); } else { - $hash->{STATE} = 'defined'; + #readingsSingleUpdate($hash, "state", "defined", 1); FireTV_ReadDeviceInfo($hash); PRESENCE_StartLocalScan($hash, 1); } - readingsSingleUpdate($hash, "state", $hash->{STATE}, 1); } if($err) { Log3 $name, 3, "[$name] FireTV_Attr ERROR: $err"; return $err; } - + } elsif($cmd eq "del") { if($attr_name eq "disable") { - $hash->{STATE} = 'defined'; - FireTV_ReadDeviceInfo($hash); - PRESENCE_StartLocalScan($hash, 1); - readingsSingleUpdate($hash, "state", $hash->{STATE}, 1); - } + #readingsSingleUpdate($hash, "state", "defined", 1); + FireTV_ReadDeviceInfo($hash); + PRESENCE_StartLocalScan($hash, 1); + } } - return undef; + return undef; } sub FireTV_Notify($$) { my ($hash,$dev) = @_; my $name = $hash->{NAME}; my $dev_name = $dev->{NAME}; - + return undef if(!defined($hash) or !defined($dev)); return undef if(!defined($dev_name) or !defined($name)); - + my $events = deviceEvents($dev,0); - + if($hash->{helper}{$name}{'PRESENCE_loaded'}) { # reread packages on state change from absent to present if($dev_name eq $name) { @@ -614,23 +651,23 @@ sub FireTV_Notify($$) { sub FireTV_adb($$) { my $hash = shift; my $cmd = shift; - + if(ref $hash ne 'HASH' ) { $hash = $defs{$hash}; } my $name = $hash->{NAME}; my $ip = $hash->{IP}; - + # connect if not connected - # don't rely on that! - # always call FireTV_connect before issuing commands, to make sure that + # don't rely on that! + # always call FireTV_connect before issuing commands, to make sure that # an old/broken connection is reset first if($cmd !~ /^(?:dis)?connect/) { if(!$hash->{adbconnected}) { FireTV_connect($hash); } } - + if($hash->{adbconnected} || $cmd =~ /^connect/ ) { my $deviceid = "-s ".$hash->{IP}.":".$hash->{PORT}; if($cmd =~ /^connect/) { @@ -641,7 +678,7 @@ sub FireTV_adb($$) { # execute command $hash->{helper}{$name}{lastadbresponse} = `$hash->{helper}{$name}{lastadbcmd} 2>&1` || ''; - + # check if adb server needs a restart if($hash->{helper}{$name}{lastadbresponse} =~ /cannot bind '.*?:5037'/) { Log3 $name, 4, "[$name] FireTV_adb response: ".$hash->{helper}{$name}{lastadbresponse}; @@ -651,7 +688,7 @@ sub FireTV_adb($$) { system($hash->{ADB}." connect ".$hash->{IP}.":".$hash->{PORT}); $hash->{helper}{$name}{lastadbresponse} = `$hash->{helper}{$name}{lastadbcmd} 2>&1` || ''; } - + $hash->{helper}{$name}{lastadbresponse} =~ s/^\s*//sg; $hash->{helper}{$name}{lastadbresponse} =~ s/\s*$//sg; @@ -683,7 +720,12 @@ sub FireTV_connect($;$) { Log3 $name, 4, "[$name] FireTV_connect: no disconnect because of holdconnection yes"; } } - + + if($action eq 'disconnect'){ + RemoveInternalTimer($hash); + return undef if($hash->{adbconnected} == 0); + } + # connect/disconnect if(FireTV_adb($hash, "$action $ip")) { if($action eq 'disconnect') { @@ -706,7 +748,7 @@ sub FireTV_connect($;$) { sub FireTV_key($$) { my $hash = shift; my $key = shift; - + return FireTV_adb($hash, "shell input keyevent $key"); } @@ -715,7 +757,7 @@ sub FireTV_text($;$) { my $hash = shift; my $text = shift; $text =~ s/ /%s/g; - + return FireTV_adb($hash, "shell input text $text"); } @@ -776,12 +818,12 @@ sub FireTV_sleep($) { sub FireTV_dumpsys($$) { my $hash = shift; my $service = shift; - + if(ref $hash ne 'HASH' ) { $hash = $defs{$hash}; } my $name = $hash->{NAME}; - + if(FireTV_adb($hash, "shell dumpsys $service")) { return $hash->{helper}{$name}{lastadbresponse}; } @@ -792,15 +834,15 @@ sub FireTV_dumpsys_has($$$) { my $hash = shift; my $service = shift; my $regex = shift; - + my $dump = FireTV_dumpsys($hash, $service); - + return $dump =~ $regex; } sub FireTV_currentFocus($) { my $hash = shift; - + my $dump = FireTV_dumpsys($hash, "window windows"); if($dump =~ /mCurrentFocus=Window\{.*?\s.*?\s(.*?)\}/) { return $1; @@ -809,8 +851,9 @@ sub FireTV_currentFocus($) { } sub FireTV_screen_state($) { - my $hash = shift; - + my $hash = shift; + my $name = $hash->{NAME}; + my $screen_state; if(! FireTV_dumpsys_has($hash, 'power', 'Display Power: state=ON')) { $screen_state = 'off'; @@ -827,8 +870,8 @@ sub FireTV_screen_state($) { } else { $screen_state = 'paused' } - - readingsSingleUpdate($hash, "screen_state", $screen_state, 1); + + #readingsSingleUpdate($hash, "screen_state", $screen_state, 1); return $screen_state; } @@ -838,7 +881,7 @@ sub FireTV_screen_state($) { sub FireTV_search($$) { my $hash = shift; my $text = shift; - + if($text) { if(FireTV_search_only($hash,$text) && FireTV_down($hash) && FireTV_enter($hash)) { return 1; @@ -850,16 +893,16 @@ sub FireTV_search($$) { sub FireTV_search_only($$) { my $hash = shift; my $text = shift; - + if(ref $hash ne 'HASH' ) { $hash = $defs{$hash}; } - + my $osversion = 0; if($hash->{OSVERSION} =~ /\((\d+)\)/ ) { $osversion = $1; } - + if(FireTV_wakeup($hash)) { usleep(5000); FireTV_home($hash); @@ -913,7 +956,7 @@ sub FireTV_app($$;$) { my $app = shift; my $action = shift || 'start'; my $name = $hash->{NAME}; - + if(FireTV_wakeup($hash)) { if($action eq 'start') { my $response; @@ -925,7 +968,7 @@ sub FireTV_app($$;$) { return $app.' started'; } } - + # try LAUNCHER intent Log3 $name, 4, "[$name] FireTV_app: trying LAUNCHER for $app"; if(FireTV_adb($hash, "shell monkey -p $app -c android.intent.category.LAUNCHER 1")) { @@ -949,9 +992,9 @@ sub FireTV_app($$;$) { sub FireTV_is_app_running($$) { my $hash = shift; my $app = shift; - + my $name = $hash->{NAME}; - + if(FireTV_adb($hash, 'shell ps|grep '.$app)) { my $adb = $hash->{helper}{$name}{lastadbresponse}; if($adb =~ /(.+?)\s+(\d+)\s+.*$app/) { @@ -1028,7 +1071,7 @@ sub FireTV_screenshot($) { $hash = $defs{$hash}; } my $name = $hash->{NAME}; - + if(FireTV_connect($hash)) { if(FireTV_wakeup($hash)) { my $remote_tempfile = FireTV_tempfile($hash, '/sdcard/screenshot'); @@ -1041,7 +1084,7 @@ sub FireTV_screenshot($) { } else { $localfile = $attr{$name}{screenshotpath}; } - + if($localfile) { FireTV_adb($hash, "pull $remote_tempfile $localfile"); if(! -e $localfile) { @@ -1053,7 +1096,7 @@ sub FireTV_screenshot($) { if(! FireTV_adb($hash, "shell rm $remote_tempfile")) { Log3 $name, 3, "[$name] FireTV_screenshot: couldn't delete remote tempfile $remote_tempfile"; } - + return "$name|$localfile"; } } @@ -1068,7 +1111,7 @@ sub FireTV_deletefile($$) { $hash = $defs{$hash}; } my $name = $hash->{NAME}; - + # only allow to delete files that this device has uploaded # uploaded files are memorized in internalval uploadedfiles $remotefile =~ s/:.*//; @@ -1110,7 +1153,7 @@ sub FireTV_deletefile_blocking($) { my $remotefile = $param[1]; my $delay = $param[2] || 0; my $hash = $defs{$name}; - + if($remotefile ne '--all') { sleep($delay); return "$name|$remotefile|".FireTV_deletefile($hash, $remotefile); @@ -1125,7 +1168,7 @@ sub FireTV_deletefile_blocking_ok($) { my $remotefile = $param[1]; my $response = $param[2]; my $hash = $defs{$name}; - + Log3 $name, 4, "[$name] FireTV_deletefile_blocking_ok: $response"; # delete the file from uploadedfiles @@ -1143,7 +1186,7 @@ sub FireTV_deletefile_blocking_error($) { my $remotefile = $param[1]; my $delay = $param[2] || 0; my $hash = $defs{$name}; - + Log3 $name, 3, "[$name] FireTV_deletefile_blocking_error: $name"; delete($hash->{helper}{$name}{'blockingcall'}{'deletefile_'.$remotefile}{RUNNING_PID}); } @@ -1153,9 +1196,9 @@ sub FireTV_screenshot_ok($) { my @param = split(/\|/, shift); my $name = $param[0]; my $localfile = $param[1]; - + my $hash = $defs{$name}; - + if(-r $localfile && -s $localfile) { Log3 $name, 4, "[$name] FireTV_screenshot_ok: $localfile"; readingsSingleUpdate($hash, "screenshot", $localfile, 1); @@ -1171,14 +1214,14 @@ sub FireTV_screenshot_ok($) { } Log3 $name, 3, "[$name] FireTV_screenshot_ok: something went wrong when saving $localfile for $name ($details)"; } - + delete($hash->{helper}{$name}{'blockingcall'}{'screenshot'}{RUNNING_PID}); } sub FireTV_screenshot_error($) { my $name = shift; my $hash = $defs{$name}; - + Log3 $name, 3, "[$name] FireTV_screenshot_error: $name"; readingsSingleUpdate($hash, "screenshot", '', 1); delete($hash->{helper}{$name}{'blockingcall'}{'screenshot'}{RUNNING_PID}); @@ -1194,12 +1237,12 @@ sub FireTV_uploadfile($$;$$) { my $localfile = shift; my $remotefile = shift || FireTV_tempfile($hash); my $contenttype = shift; - + if(! -r $localfile) { Log3 $name, 3, "[$name] FireTV_uploadfile: can't read localfile $localfile"; return; - } - + } + if(FireTV_adb($hash, "push $localfile $remotefile")) { # logic to guess the content-type needs File::MimeInfo # content-type is needed for FireTV_view @@ -1211,12 +1254,12 @@ sub FireTV_uploadfile($$;$$) { $contenttype = mimetype($localfile); } } - + # memorize uploaded files in internalval uploadedfiles my @uploadedfiles = split(/,\ */, $hash->{uploadedfiles}); push @uploadedfiles, "$remotefile:$contenttype"; $hash->{uploadedfiles} = join ', ', @uploadedfiles; - + return "$remotefile:$contenttype"; } else { Log3 $name, 3, "[$name] FireTV_uploadfile: couldn't upload localfile $localfile to remotefile $remotefile (".$hash->{helper}{$name}{lastadbresponse}.")"; @@ -1249,7 +1292,7 @@ sub FireTV_view($$$) { Log3 $name, 3, "[$name] FireTV_view: couldn't download remotefile $remotefile to localfile $localfile (".$hash->{helper}{$name}{lastadbresponse}.")"; } } - + if(! $contenttype) { Log3 $name, 3, "[$name] FireTV_view: please specify the files content-type"; return; @@ -1264,7 +1307,7 @@ sub FireTV_Remote($;$$$) { my $remoteicon = shift || 'it_remote'; my $collapsible = shift || 0; my $devicelink = shift; - + if(ref $hash ne 'HASH' ) { $hash = $defs{$hash}; } @@ -1272,7 +1315,7 @@ sub FireTV_Remote($;$$$) { if($hash->{TYPE} ne 'FireTV') { return "$name is not of type FireTV"; } - + if(defined($devicelink)){ if($devicelink eq "0") { $devicelink=""; @@ -1282,20 +1325,20 @@ sub FireTV_Remote($;$$$) { } else { $devicelink="Remote for ".AttrVal($name, 'alias', $name).""; } - + my $btncmd = "FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd.$name=set $name button "; - + my $remotehtml = AttrVal($name, 'remotehtml', undef); if($remotehtml && $remotehtml =~ m/^\s*{.*}\s*$/) { $remotehtml = eval $remotehtml; } - + # example for remotehtml # replace FIRETV (sub name and $name) with your devices name # { # my $name = 'FIRETV'; # my $cmd = "FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd.$name=set $name "; - # + # # return "
# # ".FW_makeImage("kodi", "Kodi", "rc-button")." @@ -1318,7 +1361,7 @@ sub FireTV_Remote($;$$$) { # ".FW_makeImage("robot2", "hasta la vista", "rc-button")." # "; # } - + my $icon=''; my $style=''; if($collapsible) { @@ -1328,7 +1371,7 @@ sub FireTV_Remote($;$$$) { } else { $icon = FW_makeImage($remoteicon, "Remote", "rc-button"); } - + my $html = "
$icon $devicelink @@ -1383,7 +1426,7 @@ sub FireTV_SetTimer($;$) { } else { $nextTrigger = $now + ($start ? $start : $interval); } - + $hash->{TRIGGERTIME} = $nextTrigger; $hash->{TRIGGERTIME_FMT} = FmtDateTime($nextTrigger); RemoveInternalTimer("update:$name"); @@ -1401,19 +1444,19 @@ sub FireTV_FetchStatus($) { $hash = $defs{$hash}; } my $name = $hash->{NAME}; - - if($hash->{STATE} eq 'absent' ) { + + if(ReadingsVal( $name, "state", "-") eq 'absent' ) { Log3 $name, 4, "[$name] FireTV_FetchStatus: Device is absent. Skipping"; } elsif(!IsDisabled($name) && (!defined($attr{$name}{disabled}) || $attr{$name}{disabled} ne 'yes')) { $hash->{BUSY} = 1; $hash->{LASTSEND} = gettimeofday(); Log3 $name, 4, "[$name] FireTV_FetchStatus: starting FireTV_screen_state"; - FireTV_screen_state($hash); + FireTV_UpdateState($hash); #FireTV_screen_state($hash); $hash->{BUSY} = 0; } else { - $hash->{STATE} = 'disabled'; - Log3 $name, 4, "[$name] FireTV_FetchStatus: Device is disabled. Skipping"; + readingsSingleUpdate($hash, "state", "disabled", 1); + Log3 $name, 4, "[$name] FireTV_FetchStatus: Device is disabled. Skipping"; } FireTV_SetTimer($hash); } @@ -1426,8 +1469,8 @@ sub FireTV_FetchStatus($) {

FireTV

    - FireTV is used to remote control a Amazon FireTV device. It is not able - to read the currently playing music/movie or other status information, + FireTV is used to remote control a Amazon FireTV device. It is not able + to read the currently playing music/movie or other status information, but sending commands to the device. A working copy of adb is needed.

    Prerequisites @@ -1455,7 +1498,7 @@ sub FireTV_FetchStatus($) { Example: define FIRETV FireTV 192.168.178.66 /usr/local/bin/adb

    HOST is the ip-address or hostname of your FireTV-device
    - PORT is the port where adb listens on the FireTV-device. It shouldn't be necessary to ever set this parameter. Default: 5555
    + PORT is the port where adb listens on the FireTV-device. It shouldn't be necessary to ever set this parameter. Default: 5555
    sudo the keyword sudo ensures that adb is called using sudo. You need to add an entry for your fhem-user to call adb without password in /etc/sudoers ADB_PATH is the full path to your adb-binary. Default: /usr/bin/adb
    PRESENCE_TIMEOUT_ABSENT timeout (in seconds) to the next presence check if the device is absent. Default: 30
    @@ -1464,7 +1507,7 @@ sub FireTV_FetchStatus($) { PRESENCE_ADRESS address for the presence check, see PRESENCE. Default: <IP>

- + Get
    @@ -1483,7 +1526,7 @@ sub FireTV_FetchStatus($) { Returns the current screen status. May be one off: off, idle, daydream, standby, playing, paused
- + Set
    @@ -1507,23 +1550,23 @@ sub FireTV_FetchStatus($) {
  • disconnect
    Disconnect adb from your firetv
  • install <APK>
    - Install ("sideload") an apk-file on your firetv. APK is a local path on your - fhem-server. install is implemented as a blocking call, don't use it + Install ("sideload") an apk-file on your firetv. APK is a local path on your + fhem-server. install is implemented as a blocking call, don't use it in scripts
  • key <KEYCODE>
    - Send a standard android keycode to your firetv. You can send any keycode, - but firetv may not understand them all (known to work: KEYCODE_DPAD_UP, - KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_CENTER, KEYCODE_DPAD_RIGHT, - KEYCODE_BACK, KEYCODE_HOME, KEYCODE_MENU, KEYCODE_MEDIA_PREVIOUS, + Send a standard android keycode to your firetv. You can send any keycode, + but firetv may not understand them all (known to work: KEYCODE_DPAD_UP, + KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_CENTER, KEYCODE_DPAD_RIGHT, + KEYCODE_BACK, KEYCODE_HOME, KEYCODE_MENU, KEYCODE_MEDIA_PREVIOUS, KEYCODE_MEDIA_PLAY_PAUSE, KEYCODE_MEDIA_FAST_FORWARD, KEYCODE_WAKEUP, KEYCODE_POWER)
  • screen <wakeup|toggle|sleep>
    Set screen to wake up or sleep or toggle between these states
  • screenshot
    - Take a screenshot and download it to a local tempfile on your fhem-server. + Take a screenshot and download it to a local tempfile on your fhem-server. Since it may take some seconds to produce a screenshot, this function is - implemented nonblocking (iow: no direct feedback). The path to the local - tempfile is saved in a reading "screenshot" and may be set by the attribute - "screenshotpath". Screenshots taken while playing a movie/tv-show/etc from + implemented nonblocking (iow: no direct feedback). The path to the local + tempfile is saved in a reading "screenshot" and may be set by the attribute + "screenshotpath". Screenshots taken while playing a movie/tv-show/etc from amazons library are in general just black. On error the reading "screenshot" is emptied
  • search <TEXT>
    @@ -1535,20 +1578,20 @@ sub FireTV_FetchStatus($) {
  • text <TEXT>
    Send text to your firetv
  • uploadandview <PATH:CONTENTTYPE>
    - Upload a file to your firetv and view it on screen. The view action is + Upload a file to your firetv and view it on screen. The view action is dependend on an arbitrary installed app that handles CONTENTTYPE and is - not limited to images. - You may omit CONTENTTYPE if you have the perl module File::MimeInfo + not limited to images. + You may omit CONTENTTYPE if you have the perl module File::MimeInfo installed on your system. Such files may be automatically deleted when the attribute upviewdeleteafter is set (see below)
  • upload <PATH>
    - Upload a file to your firetv. Such files may be automatically deleted + Upload a file to your firetv. Such files may be automatically deleted when the attribute uploaddeleteafter is set (see below)
  • view <PATH:CONTENTTYPE>
    - View a file on your firetv. The view action is dependend on an arbitrary + View a file on your firetv. The view action is dependend on an arbitrary installed app that handles CONTENTTYPE and is - not limited to images. If you have the perl module File::MimeInfo installed - on your system, you may replace CONTENTTYPE whith the keyword load: + not limited to images. If you have the perl module File::MimeInfo installed + on your system, you may replace CONTENTTYPE whith the keyword load: The file will be downloaded to your fhem-server prior viewing it on screen, then (which may take some time and is generally speaking inefficient).
  • window <appsetting|fotos|music|settings>
    @@ -1568,8 +1611,8 @@ sub FireTV_FetchStatus($) {
  • remotehtml <HTML>
    HTML to add to the output of FireTV_Remote() (see below)
  • screenshotpath <PATH>
    - If screenshotpath is set to a filename, every new screenshot (see set screenshot) will overwrite that file. - If set to a directory, a random file will be created in that directory. + If screenshotpath is set to a filename, every new screenshot (see set screenshot) will overwrite that file. + If set to a directory, a random file will be created in that directory. If not set, a random file is created in your systems tempdirectory. Default: not set
  • uploaddeleteafter <SECONDS>
    Files uploaded via set upload are deleted after SECONDS when set to a positve integer number. Default: not set
  • @@ -1592,7 +1635,7 @@ sub FireTV_FetchStatus($) {

- + Values of STATE
    @@ -1615,7 +1658,7 @@ sub FireTV_FetchStatus($) { FireTV_Remote()
      The module provides an additional function FireTV_Remote() which returns - html code for a graphic remote control usable in FHEMWEB. Just define a weblink + html code for a graphic remote control usable in FHEMWEB. Just define a weblink device like:

      define FIRETV_REMOTE weblink htmlCode { FireTV_Remote('FIRETV') }
      @@ -1629,15 +1672,15 @@ sub FireTV_FetchStatus($) {
    • DEVICELINK: If not set, the Remote has a clickable title which links to the controlled device. If set to "0" it has no title. If set to any other value, that value will be used as clickable title. Default: not set

    - The content of the attribute remotehtml is added at the end of FireTV_Remote() - output. If the content is wrapped in curly brackets it is interpreted as perl-code. + The content of the attribute remotehtml is added at the end of FireTV_Remote() + output. If the content is wrapped in curly brackets it is interpreted as perl-code. The output is structured in a three-column table-layout. Here is an example of how to add some additional buttons to the remote:
        {
             my $device = 'FIRETV';
             my $cmd = "FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd.$device=set $device ";
    -    
    +
             return "<tr><td colspan='3'><hr></td></tr>
                     <tr>
                         <td><a onClick=\"$cmd appstart org.xbmc.kodi')\">".FW_makeImage("kodi", "Kodi", "rc-button")."</a></td>
    diff --git a/controls_nesges-fhem-modules.txt b/controls_nesges-fhem-modules.txt
    index 1f534ba..7c94d10 100644
    --- a/controls_nesges-fhem-modules.txt
    +++ b/controls_nesges-fhem-modules.txt
    @@ -1,4 +1,4 @@
    -UPD 2017-03-08_14:14:18 5233 FHEM/98_Hello.pm
    -UPD 2017-03-08_14:14:07 31014 FHEM/98_ApacheStatus.pm
    -UPD 2018-09-20_12:08:38 66587 FHEM/98_FireTV.pm
    -UPD 2017-03-08_14:14:26 2432 contrib/99_Utils_FireTV.pm
    +UPD 2019-04-14_22:07:10 67692 FHEM/98_FireTV.pm
    +UPD 2019-04-13_15:18:16 5233 FHEM/98_Hello.pm
    +UPD 2019-04-13_15:18:16 31014 FHEM/98_ApacheStatus.pm
    +UPD 2019-04-13_15:18:16 2432 contrib/99_Utils_FireTV.pm