2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-03-03 23:06:37 +00:00

AndroidDB: Initial upload

git-svn-id: https://svn.fhem.de/fhem/trunk@24426 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
zap 2021-05-12 11:36:22 +00:00
parent 7eee3b1916
commit c34af8ca7e
2 changed files with 759 additions and 0 deletions

273
fhem/FHEM/89_AndroidDB.pm Normal file
View File

@ -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
<a name="AndroidDB"></a>
<h3>AndroidDB</h3>
<ul>
The module allows to control an Android device by using the Android Debug Bridge (ADB).
Before one can define an Android device, an AndroidDBHost I/O device must exist.
<br/><br/>
Dependencies: 89_AndroidDBHost
<br/><br/>
<a name="AndroidDBdefine"></a>
<b>Define</b><br/><br/>
<ul>
<code>define &lt;name&gt; AndroidDB {&lt;NameOrIP&gt;}</code>
The parameter 'NameOrIP' is the hostname or the IP address of the Android device.
</ul>
<br/>
</ul>
<a name="AndroidDBset"></a>
<b>Set</b><br/><br/>
<ul>
<li><b>set &lt;name&gt; reboot</b><br/>
Reboot the device.
</li><br/>
<li><b>set &lt;name&gt; remoteControl &lt;MacroName&gt;</b><br/>
Send key codes associated with 'MacroName' to the Android device. Either attribute
'macros' or 'preset' must be set to make this command available. Macro names defined
in attribute 'macros' are overwriting macros with the same name in a preset selected
by attribute 'preset'.
</li><br/>
<li><b>set &lt;name&gt; sendKey &lt;KeyCode&gt;</b><br/>
Send a key code to the Android device.
</li><br/>
<li><b>set &lt;name&gt; shell &lt;Command&gt; [&lt;Arguments&gt;]</b><br/>
Execute shell command on Android device.
</li><br/>
</ul>
<a name="AndroidDBattr"></a>
<b>Attributes</b><br/><br/>
<ul>
<a name="macros"></a>
<li><b>macros &lt;MacroDef&gt; [...]</b><br/>
Define a list of keycode macros to be sent to an Android device with 'remoteControl'
command. A 'MacroDef' is using the following syntax:<br/>
MacroName:KeyCode[,...]<br/>
Several macro definitions can be specified by seperating them using a blank character.
</li><br/>
<a name="preset"></a>
<li><b>preset &lt;Preset&gt;</b><br/>
Select a preset of keycode macros. If the same macro name is defined in the selected
preset and in attribute 'macros', the definition in the 'macros' attribute overwrites
the definition in the preset.
</li><br/>
</ul>
=end html
=cut

View File

@ -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 = <CHILDOUT>) { $result .= $line; }
my $error = '';
while (my $line = <CHILDERR>) { $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
<a name="AndroidDBHost"></a>
<h3>AndroidDBHost</h3>
<ul>
Provides I/O device for AndroidDB devices.
<br/><br/>
Dependencies: Perl module IPC::Open3, Android Platform Tools
<br/><br/>
Android DB Platform Tools installation:<br/>
Debian/Raspbian: apt-get install android-sdk-platform-tools<br/>
Windows/MacOSX/Linux x86: <a href="https://developer.android.com/studio/releases/platform-tools">Android Developer Portal</a>
<br/><br/>
<a name="AndroidDBHostdefine"></a>
<b>Define</b><br/><br/>
<ul>
<code>define &lt;name&gt; AndroidDBHost [server=&lt;host&gt;}[:&lt;port&gt;]] [adb=&lt;path&gt;]</code><br/><br/>
The parameter 'host' is the hostname of the system, where the ADB server is running. Default is 'localhost'.
Parameter 'adb' can be used to specify the path to the adb command (must include 'adb' or 'adb.exe').
</ul>
<br/>
</ul>
<a name="AndroidDBHostset"></a>
<b>Set</b><br/><br/>
<ul>
<li><b>set &lt;name&gt; start</b><br/>
Start ADB server.
</li><br/>
<li><b>set &lt;name&gt; stop</b><br/>
Stop ADB server.
</li><br/>
</ul>
<a name="AndroidDBHostget"></a>
<b>Get</b><br/><br/>
<ul>
<li><b>get &lt;name&gt; status</b><br/>
Get status of ADB server.
</li><br/>
</ul>
=end html
=cut