diff --git a/fhem/FHEM/89_AndroidDB.pm b/fhem/FHEM/89_AndroidDB.pm new file mode 100644 index 000000000..bdcb2ffd8 --- /dev/null +++ b/fhem/FHEM/89_AndroidDB.pm @@ -0,0 +1,273 @@ + +# +# $Id$ +# +# 89_AndroidDB +# +# Version 0.1 +# +# FHEM Integration for Android Devices +# +# Dependencies: +# +# 89_AndroidDBHost +# +# Prerequisits: +# +# - Enable developer mode on Android device +# - Allow USB debugging on Android device +# + + +package main; + +use strict; +use warnings; + +sub AndroidDB_Initialize ($) +{ + my ($hash) = @_; + + $hash->{DefFn} = "AndroidDB::Define"; + $hash->{UndefFn} = "AndroidDB::Undef"; + $hash->{SetFn} = "AndroidDB::Set"; + $hash->{GetFn} = "AndroidDB::Get"; + $hash->{AttrFn} = "AndroidDB::Attr"; + $hash->{ShutdownFn} = "AndroidDB::Shutdown"; + + $hash->{parseParams} = 1; + $hash->{AttrList} = 'macros:textField-long preset:MagentaTVStick,SonyTV'; +} + +package AndroidDB; + +use strict; +use warnings; + +use Data::Dumper; + +use SetExtensions; + +use GPUtils qw(:all); + +BEGIN { + GP_Import(qw( + readingsSingleUpdate + readingsBulkUpdate + readingsBulkUpdateIfChanged + readingsBeginUpdate + readingsEndUpdate + Log3 + AttrVal + ReadingsVal + AssignIoPort + defs + )) +}; + +# Remote control presets +my %PRESET = ( + 'MagentaTVStick' => { + 'APPS' => 'KEYCODE_ALL_APPS', + 'BACK' => 'KEYCODE_BACK', + 'EPG' => 'KEYCODE_TV_INPUT_HDMI_2', + 'HOME' => 'KEYCODE_HOME', + 'INFO' => 'KEYCODE_INFO', + 'MEGATHEK' => 'KEYCODE_TV_INPUT_HDMI_3', + 'MUTE' => 'KEYCODE_MUTE', + 'OK' => 'KEYCODE_DPAD_CENTER', + 'POWER' => 'KEYCODE_POWER', + 'PROG+' => 'KEYCODE_CHANNEL_UP', + 'PROG-' => 'KEYCODE_CHANNEL_DOWN', + 'RECORD' => 'KEYCODE_MEDIA_RECORD', + 'SEARCH' => 'KEYCODE_TV_INPUT_HDMI_1', + 'TV' => 'KEYCODE_TV_INPUT_HDMI_4' + }, + 'SonyTV' => { + 'POWER' => 'KEYCODE_POWER' + } +); + +sub Define ($$$) +{ + my ($hash, $a, $h) = @_; + + my $usage = "define $hash->{NAME} AndroidDB {NameOrIP}"; + + return $usage if (scalar(@$a) < 3); + + # Set parameters + my ($devName, $devPort) = split (':', $$a[2]); + $hash->{ADBDevice} = $devName.':'.($devPort // '5555'); + + AssignIoPort ($hash); + + return undef; +} + +sub Undef ($$) +{ + my ($hash, $name) = @_; + + AndroidDBHost::Disconnect ($hash); + + return undef; +} + +sub Shutdown ($) +{ + my ($hash) = @_; + + AndroidDBHost::Disconnect ($hash); +} + +sub Set ($@) +{ + my ($hash, $a, $h) = @_; + + my $name = shift @$a; + my $opt = shift @$a // return 'No set command specified'; + + # Preprare list of available commands + my $options = 'reboot sendKey shell'; + my @macroList = (); + my $preset = AttrVal ($hash->{NAME}, 'preset', ''); + my $macros = AttrVal ($hash->{NAME}, 'macros', ''); + push @macroList, sort keys %{$PRESET{$preset}} if ($preset ne '' && exists($PRESET{$preset})); + push @macroList, sort keys %{$PRESET{_custom_}} if ($macros ne '' && exists($PRESET{_custom_})); + my %e; + $options .= ' remoteControl:'.join(',', sort grep { !$e{$_}++ } @macroList) if (scalar(@macroList) > 0); + $opt = lc($opt); + + if ($opt eq 'sendkey') { + my $key = shift @$a // return "Usage: set $name $opt KeyCode"; + my ($rc, $result, $error) = AndroidDBHost::Run ($hash, 'shell', '.*', 'input', 'keyevent', $key); + return $error if ($rc == 0); + } + elsif ($opt eq 'reboot') { + my ($rc, $result, $error) = AndroidDBHost::Run ($hash, $opt); + return $error if ($rc == 0); + } + elsif ($opt eq 'shell') { + return "Usage: set $name $opt ShellCommand" if (scalar(@$a) == 0); + my ($rc, $result, $error) = AndroidDBHost::Run ($hash, $opt, '.*', @$a); + return $result.$error, + } + elsif ($opt eq 'remotecontrol') { + my $macroName = shift @$a // return "Usage: set $name $opt MacroName"; + $preset = '_custom_' if (exists($PRESET{_custom_}) && exists($PRESET{_custom_}{$macroName})); + return "Preset and/or macro $macroName not defined" if ($preset eq '' || !exists($PRESET{$preset}{$macroName})); + my ($rc, $result, $error) = AndroidDBHost::Run ($hash, 'shell', '.*', 'input', 'keyevent', + split (',', $PRESET{$preset}{$macroName})); + return $error if ($rc == 0); + } + else { + return "Unknown argument $opt, choose one of $options"; + } +} + +sub Get ($@) +{ + my ($hash, $a, $h) = @_; + + my $name = shift @$a; + my $opt = shift @$a // return 'No get command specified'; + + my $options = 'presets'; + + $opt = lc($opt); + + if ($opt eq 'presets') { + return Dumper (\%PRESET); + } + else { + return "Unknown argument $opt, choose one of $options"; + } +} + +sub Attr ($@) +{ + my ($cmd, $name, $attrName, $attrVal) = @_; + my $hash = $defs{$name}; + + if ($cmd eq 'set') { + if ($attrName eq 'macros') { + foreach my $macroDef (split /\s+/, $attrVal) { + my ($macroName, $macroKeycodes) = split (':', $macroDef); + $PRESET{_custom_}{$macroName} = $macroKeycodes; + } + } + } + elsif ($cmd eq 'del') { + delete $PRESET{_custom_} if (exists($PRESET{_custom_})); + } + + return undef; +} + +1; + +=pod +=item device +=item summary Allows to control an Android device via ADB +=begin html + + +

AndroidDB

+ + + +Set

+ + + +Attributes

+ + +=end html +=cut + + diff --git a/fhem/FHEM/89_AndroidDBHost.pm b/fhem/FHEM/89_AndroidDBHost.pm new file mode 100644 index 000000000..92fbbc0b8 --- /dev/null +++ b/fhem/FHEM/89_AndroidDBHost.pm @@ -0,0 +1,486 @@ + +# +# $Id$ +# +# 89_AndroidDBHost +# +# Version 0.1 +# +# FHEM Integration for Android Debug Bridge +# +# Dependencies: +# +# - Perl Packages: IPC::Open3 +# - Android Platform Tools +# +# Install Android Platform Tools: +# +# Raspbian/Debian: apt-get install android-sdk-platform-tools +# Windows/MacOSX/Linux x86: https://developer.android.com/studio/releases/platform-tools +# + + + +package main; + +use strict; +use warnings; + +sub AndroidDBHost_Initialize ($) +{ + my ($hash) = @_; + + $hash->{DefFn} = "AndroidDBHost::Define"; + $hash->{UndefFn} = "AndroidDBHost::Undef"; + $hash->{SetFn} = "AndroidDBHost::Set"; + $hash->{GetFn} = "AndroidDBHost::Get"; + $hash->{NotifyFn} = "AndroidDBHost::Notify"; + $hash->{ShutdownFn} = "AndroidDBHost::Shutdown"; + + $hash->{parseParams} = 1; +} + +package AndroidDBHost; + +use strict; +use warnings; + +use IPC::Open3; + +use SetExtensions; +# use POSIX; + +use GPUtils qw(:all); + +BEGIN { + GP_Import(qw( + readingsSingleUpdate + readingsBulkUpdate + readingsBulkUpdateIfChanged + readingsBeginUpdate + readingsEndUpdate + Log3 + AttrVal + ReadingsVal + InternalTimer + RemoveInternalTimer + init_done + deviceEvents + gettimeofday + )) +}; + +sub Define ($$$) +{ + my ($hash, $a, $h) = @_; + + my $name = $hash->{NAME}; + my $usage = "define $name AndroidDB [server={host}[:{port}]] [adb={path}]"; + + # Set parameters + my ($host, $port) = split (':', $h->{ADB} // 'localhost:5037'); + $hash->{adb}{host} = $host; + $hash->{adb}{port} = $port // 5037; + $hash->{adb}{cmd} = $h->{adb} // '/usr/bin/adb'; + $hash->{Clients} = ':AndroidDB:'; + $hash->{NOTIFYDEV} = 'global,TYPE=(AndroidDBHost|AndroidDB)'; + + # Check path and rights of platform tools + return "ADB command not found or is not executable in $hash->{adb}{pt}" if (! -x "$hash->{adb}{cmd}"); + + # Check ADB settings, start adb server + CheckADBServer ($hash); + + return "ADB server not running or cannot be started on host $hash->{adb}{host}" if ($hash->{STATE} eq 'stopped'); + + return undef; +} + +sub Undef ($$) +{ + my ($hash, $name) = @_; + + Log3 $name, 2, "Stopping ADB server ..."; + RemoveInternalTimer ($hash); + Execute ($hash, 'kill-server') if (IsADBServerRunning ($hash)); + + return undef; +} + +sub Shutdown ($) +{ + my $hash = shift; + + RemoveInternalTimer ($hash); + Execute ($hash, 'kill-server') if (IsADBServerRunning ($hash)); +} + +############################################################################## +# Initialize ADB server checking timer after FHEM is initialized +############################################################################## + +sub Notify ($$) +{ + my ($hash, $devhash) = @_; + + return if (AttrVal ($hash->{NAME}, 'disable', 0) == 1); + + my $events = deviceEvents ($devhash, 1); + return if (!$events); + + if ($devhash->{NAME} eq 'global' && grep (/INITIALIZED/, @$events)) { + InternalTimer (gettimeofday()+60, 'AndroidDBHost::CheckADBServerTimer', $hash, 0); + } +} + +############################################################################## +# Timer function to check periodically, if ADB server is running +############################################################################## + +sub CheckADBServerTimer ($) +{ + my $hash = shift; + + CheckADBServer ($hash); + + InternalTimer (gettimeofday()+60, 'AndroidDBHost::CheckADBServerTimer', $hash, 0); +} + +############################################################################## +# Start ADB server if it's not running +############################################################################## + +sub CheckADBServer ($) +{ + my $hash = shift; + + my $newState = 'stopped'; + for (my $i=0; $i<3; $i++) { + Log3 $hash->{NAME}, 4, 'Check if ADB server is running. '.($i+1).'. attempt'; + if (IsADBServerRunning ($hash)) { + $newState = 'running'; + last; + } + + if ($hash->{adb}{host} eq 'localhost') { + # Start ADB server + Log3 $hash->{NAME}, 2, "Periodical check found no running ADB server. Starting ADB server ..."; + Execute ($hash, 'start-server'); + } + + sleep (1); + } + + readingsSingleUpdate ($hash, 'state', $newState, 1); + + return $newState eq 'running' ? 1 : 0; +} + +############################################################################## +# Check if ADB server is running by connecting to port +############################################################################## + +sub IsADBServerRunning ($) +{ + my $hash = shift; + + return TCPConnect ($hash->{adb}{host}, $hash->{adb}{port}, 1); +} + +############################################################################## +# Set commands +############################################################################## + +sub Set ($@) +{ + my ($hash, $a, $h) = @_; + + my $name = shift @$a; + my $opt = shift @$a // return 'No set command specified'; + + # Preprare list of available commands + my $options = 'start:noArg stop:noArg'; + + $opt = lc($opt); + + if ($opt eq 'start') { + RemoveInternalTimer ($hash, 'AndroidDBHost::CheckADBServerTimer'); + CheckADBServer ($hash); + return "Cannot start server" if ($hash->{STATE} eq 'stopped'); + } + elsif ($opt eq 'stop') { + my ($rc, $result, $error) = Execute ($hash, 'kill-server'); + return $error if ($rc == 0); + sleep (2); + if (!IsADBServerRunning ($hash)) { + RemoveInternalTimer ($hash, 'AndroidDBHost::CheckADBServerTimer'); + readingsSingleUpdate ($hash, 'state', 'stopped', 1); + } + else { + return "ADB server still running. Please try again."; + } + } + else { + return "Unknown argument $opt, choose one of $options"; + } +} + +############################################################################## +# Get commands +############################################################################## + +sub Get ($@) +{ + my ($hash, $a, $h) = @_; + + my $name = shift @$a; + my $opt = shift @$a // return 'No get command specified'; + + # Prepare list of available commands + my $options = 'status:noArg'; + + $opt = lc($opt); + + if ($opt eq 'status') { + my $status = IsADBServerRunning ($hash) ? 'running' : 'stopped'; + readingsSingleUpdate ($hash, 'state', $status, 1); + return "ADB server $status"; + } + else { + return "Unknown argument $opt, choose one of $options"; + } +} + +############################################################################## +# Execute adb commmand and return status code and command output +# +# Return value: +# (returncode, stdout, stderr) +# Return codes: +# 0 - error +# 1 - success +############################################################################## + +sub Execute ($@) +{ + my ($ioHash, $command, $succExp, @args) = @_; + $succExp //= '.*'; + + if ($command ne 'start-server' && !IsADBServerRunning ($ioHash)) { + Log3 $ioHash->{NAME}, 2, 'Execute: ADB server not running'; + return (0, '', 'ADB server not running'); + } + + # Execute ADB command + local (*CHILDIN, *CHILDOUT, *CHILDERR); + my $pid = open3 (*CHILDIN, *CHILDOUT, *CHILDERR, $ioHash->{adb}{cmd}, $command, @args); + close (CHILDIN); + + # Read output + my $result = ''; + while (my $line = ) { $result .= $line; } + my $error = ''; + while (my $line = ) { $error .= $line; } + + close (CHILDOUT); + close (CHILDERR); + waitpid ($pid, 0); + + Log3 $ioHash->{NAME}, 5, "stdout=$result"; + Log3 $ioHash->{NAME}, 5, "stderr=$error"; + + my $rc = 0; + if ($error eq '') { + if ($result !~ /$succExp/i) { + $error = "Response doesn't match $succExp for command $command"; + $rc = 0; + } + else { + $rc = 1; + $ioHash->{ADBPID} = $pid if ($command eq 'start-server'); + } + } + + return ($rc, $result, $error); +} + +############################################################################## +# Check Android device connection(s) +# +# Return value: +# -1 = Error +# 0 = No active connections +# 1 = Current device connected +# 2 = Multiple devices connected (need to disconnect) +############################################################################## + +sub IsConnected ($) +{ + my $clHash = shift // return 0; + + my $ioHash = $clHash->{IODev} // return -1; + + # Get active connections + my ($rc, $result, $error) = Execute ($ioHash, 'devices', 'list'); + return -1 if ($rc == 0); + + my @devices = $result =~ /device$/g; + if (scalar(@devices) == 1 && $result =~ /$clHash->{ADBDevice}/) { + return 1; + } + elsif (scalar(@devices) > 1) { + return 2; + } + + return 0; +} + +############################################################################## +# Connect to Android device +# +# Return value: +# 0 = error +# 1 = connected +############################################################################## + +sub Connect ($) +{ + my $clHash = shift // return 0; + + my $ioHash = $clHash->{IODev} // return -1; + + my $connect = IsConnected ($clHash); + if ($connect == 1) { + return 1; + } + elsif ($connect == 2) { + # Disconnect all devices + my ($rc, $result, $error) = Execute ($ioHash, 'disconnect', 'disconnected'); + return -1 if ($rc == 0); + } + elsif ($connect == -1) { + Log3 $clHash->{NAME}, 2, 'Cannot detect connection state'; + return 0; + } + + # Connect + my ($rc, $state, $error) = Execute ($ioHash, 'connect', 'connected', $clHash->{ADBDevice}); + readingsSingleUpdate ($clHash, 'state', 'connected', 1) if ($rc == 1); + + return $rc; +} + +############################################################################## +# Connect to Android device +# +# Return value: +# 0 = error +# 1 = connected +############################################################################## + +sub Disconnect ($) +{ + my $clHash = shift // return 0; + + my $ioHash = $clHash->{IODev} // return (-1, '', 'Cannot detect IO device'); + + my ($rc, $result, $error) = Execute ($ioHash, 'disconnect', 'disconnected', $clHash->{ADBDevice}); + readingsSingleUpdate ($clHash, 'state', 'disconnected', 1) if ($rc == 1); + + return $rc; +} + +############################################################################## +# Execute commmand and return status code and command output +# +# Return value: +# (returncode, stdout, stderr) +# Return codes: +# 0 - error +# 1 - success +############################################################################## + +sub Run ($@) +{ + my ($clHash, $command, $succExp, @args) = @_; + $succExp //= '.*'; + + my $ioHash = $clHash->{IODev} // return (0, '', 'Cannot detect IO device'); + + if (!Connect ($clHash)) { + readingsSingleUpdate ($clHash, 'state', 'connected', 1); + return (0, '', 'Cannot connect to device'); + } + + readingsSingleUpdate ($clHash, 'state', 'connected', 1); + + return Execute ($ioHash, $command, $succExp, @args); +} + +###################################################################### +# Check if TCP connection to specified host and port is possible +###################################################################### + +sub TCPConnect ($$$) +{ + my ($addr, $port, $timeout) = @_; + + my $socket = IO::Socket::INET->new (PeerAddr => $addr, PeerPort => $port, Timeout => $timeout); + if ($socket) { + close ($socket); + return 1; + } + + return 0; +} + + +1; + +=pod +=item device +=item summary Provides I/O device for AndroidDB devices +=begin html + + +

AndroidDBHost

+ + + +Set

+ + + +Get

+ + +=end html +=cut