2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-03-03 04:36:36 +00:00

AndroidDB: added new features

git-svn-id: https://svn.fhem.de/fhem/trunk@25737 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
zap 2022-02-24 12:10:53 +00:00
parent 3711b9deb9
commit e4fd8a1c80
3 changed files with 525 additions and 93 deletions

View File

@ -1,5 +1,7 @@
# Add changes at the top of the list. Keep it in ASCII, and 80-char wide.
# Do not insert empty lines here, update check depends on it.
- feature: 89_AndroidDB: Added new features
- bugfix: 89_AndroidDBHost: Bug fixes
- bugfix: 70_DENON_AVR: handled some uninitialized values
- new: 72_UBUS_CALL: module to send call requests via uBus client
- new: 72_UBUS_CLIENT: module to communicate with uBus devices

View File

@ -4,7 +4,7 @@
#
# 89_AndroidDB
#
# Version 0.4
# Version 0.6
#
# FHEM Integration for Android Devices
#
@ -36,16 +36,19 @@ sub AndroidDB_Initialize ($)
$hash->{ShutdownFn} = "AndroidDB::Shutdown";
$hash->{parseParams} = 1;
$hash->{AttrList} = 'macros:textField-long preset:MagentaTVStick,SonyTV presetFile';
$hash->{AttrList} = 'connect:0,1 macros:textField-long preset presetFile';
$data{RC_layout}{MagentaTVStick} = "AndroidDB::RCLayoutMagentaTVStick";
$data{RC_layout}{MagentaOne} = "AndroidDB::RCLayoutMagentaOne";
$data{RC_layout}{MagentaTVExtended} = "AndroidDB::RCLayoutMagentaTVExt";
}
package AndroidDB;
use strict;
use warnings;
use SetExtensions;
use Storable qw(dclone);
use GPUtils qw(:all);
BEGIN {
@ -55,42 +58,113 @@ BEGIN {
readingsBulkUpdateIfChanged
readingsBeginUpdate
readingsEndUpdate
setDevAttrList
CommandDefine
CommandSet
CommandAttr
Log3
AttrVal
ReadingsVal
AssignIoPort
InternalTimer
asyncOutput
gettimeofday
parseParams
defs
attr
modules
data
init_done
))
};
# Remote control presets
my %PRESET = (
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'
'ASSISTANT' => 'KEYCODE_ASSIST',
'APPS' => 'KEYCODE_ALL_APPS',
'BACK' => 'KEYCODE_BACK',
'EPG' => 'KEYCODE_TV_INPUT_HDMI_2',
'HOME' => 'KEYCODE_HOME',
'LEFT' => 'KEYCODE_DPAD_LEFT',
'RIGHT' => 'KEYCODE_DPAD_RIGHT',
'UP' => 'KEYCODE_DPAD_UP',
'DOWN' => 'KEYCODE_DPAD_DOWN',
'INFO' => 'KEYCODE_INFO',
'MEGATHEK' => 'KEYCODE_TV_INPUT_HDMI_3',
'MUTE' => 'KEYCODE_VOLUME_MUTE',
'OK' => 'KEYCODE_DPAD_CENTER',
'PLAYPAUSE' => 'KEYCODE_MEDIA_PLAY_PAUSE',
'POWER' => 'KEYCODE_POWER',
'PROG+' => 'KEYCODE_CHANNEL_UP',
'PROG-' => 'KEYCODE_CHANNEL_DOWN',
'RECORD' => 'KEYCODE_MEDIA_RECORD',
'SEARCH' => 'KEYCODE_TV_INPUT_HDMI_1',
'STREAMINFO' => '--longpress,KEYCODE_INFO',
'TV' => 'KEYCODE_TV_INPUT_HDMI_4',
'VOL+' => 'KEYCODE_VOLUME_UP',
'VOL-' => 'KEYCODE_VOLUME_DOWN'
},
'SonyTV' => {
'POWER' => 'KEYCODE_POWER'
'MagentaOne' => {
'ASSISTANT' => 'KEYCODE_ASSIST',
'APPS' => 'KEYCODE_ALL_APPS',
'BACK' => 'KEYCODE_BACK',
'EPG' => 'KEYCODE_TV_INPUT_HDMI_2',
'HOME' => 'KEYCODE_HOME',
'LEFT' => 'KEYCODE_DPAD_LEFT',
'RIGHT' => 'KEYCODE_DPAD_RIGHT',
'UP' => 'KEYCODE_DPAD_UP',
'DOWN' => 'KEYCODE_DPAD_DOWN',
'INFO' => 'KEYCODE_INFO',
'MEGATHEK' => 'KEYCODE_TV_INPUT_HDMI_3',
'MUTE' => 'KEYCODE_VOLUME_MUTE',
'OK' => 'KEYCODE_DPAD_CENTER',
'PLAYPAUSE' => 'KEYCODE_MEDIA_PLAY_PAUSE',
'POWER' => 'KEYCODE_POWER',
'PROG+' => 'KEYCODE_CHANNEL_UP',
'PROG-' => 'KEYCODE_CHANNEL_DOWN',
'RECORD' => 'KEYCODE_MEDIA_RECORD',
'SEARCH' => 'KEYCODE_TV_INPUT_HDMI_1',
'STREAMINFO' => '--longpress,KEYCODE_INFO',
'TV' => 'KEYCODE_TV_INPUT_HDMI_4',
'VOL+' => 'KEYCODE_VOLUME_UP',
'VOL-' => 'KEYCODE_VOLUME_DOWN'
},
'AndroidTV' => {
'APPS' => 'KEYCODE_ALL_APPS',
'BACK' => 'KEYCODE_BACK',
'EPG' => 'KEYCODE_GUIDE',
'HOME' => 'KEYCODE_HOME',
'LEFT' => 'KEYCODE_DPAD_LEFT',
'RIGHT' => 'KEYCODE_DPAD_RIGHT',
'UP' => 'KEYCODE_DPAD_UP',
'DOWN' => 'KEYCODE_DPAD_DOWN',
'INFO' => 'KEYCODE_INFO',
'MUTE' => 'KEYCODE_VOLUME_MUTE',
'OK' => 'KEYCODE_DPAD_CENTER',
'PLAYPAUSE' => 'KEYCODE_MEDIA_PLAY_PAUSE',
'POWER' => 'KEYCODE_POWER',
'PROG+' => 'KEYCODE_CHANNEL_UP',
'PROG-' => 'KEYCODE_CHANNEL_DOWN',
'RECORD' => 'KEYCODE_MEDIA_RECORD',
'SEARCH' => 'KEYCODE_SEARCH',
'VOL+' => 'KEYCODE_VOLUME_UP',
'VOL-' => 'KEYCODE_VOLUME_DOWN',
'RED' => 'KEYCODE_PROG_RED',
'GREEN' => 'KEYCODE_PROG_GREEN',
'BLUE' => 'KEYCODE_PROG_BLUE',
'YELLOW' => 'KEYCODE_PROG_YELLOW'
}
);
};
# Command presets
my $MACRO = { };
sub Define ($$$)
{
my ($hash, $a, $h) = @_;
my $usage = "define $hash->{NAME} AndroidDB {NameOrIP}";
my $usage = "define $hash->{NAME} AndroidDB {NameOrIP[:Port]}";
return $usage if (scalar(@$a) < 3);
@ -100,9 +174,31 @@ sub Define ($$$)
AssignIoPort ($hash);
$attr{$hash->{NAME}}{webCmd} = 'remoteControl';
# Clone predefined presets and macros
$hash->{adb}{preset} = dclone $PRESET;
$hash->{adb}{macro} = dclone $MACRO;
InitAfterStart ($hash) if ($init_done);
return undef;
}
sub InitAfterStart ($)
{
my ($hash) = @_;
my @presets = map { $_ eq '_custom_' ? () : $_ } keys %{$hash->{adb}{preset}};
if (scalar(@presets) > 0) {
my $attrPreset = 'preset:select,'.join(',',@presets);
my $attributes = $modules{AndroidDB}{AttrList};
$attributes =~ s/preset/$attrPreset/;
setDevAttrList ($hash->{NAME}, $attributes);
}
}
sub Undef ($$)
{
my ($hash, $name) = @_;
@ -126,39 +222,119 @@ sub Set ($@)
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_}));
#
# Standard commands
my $options = 'exportPresets reboot rollHorz rollVert sendKey sendNumKeys sendText shell tap';
# Add remote control key presets to command remoteControl
my @presetList = ();
my $preset = AttrVal ($name, 'preset', '');
push @presetList, sort keys %{$hash->{adb}{preset}{$preset}} if ($preset ne '' && exists($hash->{adb}{preset}{$preset}));
push @presetList, sort keys %{$hash->{adb}{preset}{_custom_}} if (exists($hash->{adb}{preset}{_custom_}));
my %e;
$options .= ' remoteControl:'.join(',', sort grep { !$e{$_}++ } @macroList) if (scalar(@macroList) > 0);
$opt = lc($opt);
$options .= ' remoteControl:'.join(',', sort grep { !$e{$_}++ } @presetList) if (scalar(@presetList) > 0);
# Add remote control layouts to command createRemote
my @layouts = keys %{$data{RC_layout}};
$options .= ' createRemote:'.join(',', sort @layouts) if (scalar(@layouts) > 0);
# Add command macros to command list
my @macroList = ();
push @macroList, sort keys %{$hash->{adb}{macro}{$preset}} if ($preset ne '' && exists($hash->{adb}{macro}{$preset}));
push @macroList, sort keys %{$hash->{adb}{macro}{_custom_}} if (exists($hash->{adb}{macro}{_custom_}));
my %e;
$options .= ' '.join(' ', sort grep { !$e{$_}++ } @macroList) if (scalar(@macroList) > 0);
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);
my $lcopt = lc($opt);
if ($lcopt eq 'sendkey') {
return "Usage: set $name $opt [--longpress] KeyCode [...]" if (scalar(@$a) == 0);
return "Only one KeyCode allowed when option '--longpress' is specified"
if ($$a[0] eq '--longpress' && scalar(@$a) > 2);
my ($rc, $result, $error) = AndroidDBHost::Run ($hash, 'shell', '.*', 'input', 'keyevent', @$a);
return $error if ($rc == 0);
}
elsif ($opt eq 'reboot') {
elsif ($lcopt eq 'sendnumkeys') {
my $number = shift @$a // return "Usage: set $name $opt Number";
return 'Parameter Number must be in range 1-9999' if ($number !~ /^[0-9]+$/ || $number < 1 || $number > 9999);
my ($rc, $result, $error) = AndroidDBHost::Run ($hash, 'shell', '.*', 'input', 'text', $number);
return $error if ($rc == 0);
}
elsif ($lcopt eq 'sendtext') {
return "Usage: set $name $opt Text" if (scalar(@$a) < 1);
my ($rc, $result, $error) = AndroidDBHost::Run ($hash, 'shell', '.*', 'input', 'text', join(' ',@$a));
return $error if ($rc == 0);
}
elsif ($lcopt eq 'reboot') {
my ($rc, $result, $error) = AndroidDBHost::Run ($hash, $opt);
return $error if ($rc == 0);
}
elsif ($opt eq 'shell') {
elsif ($lcopt 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') {
elsif ($lcopt 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}));
$preset = '_custom_' if (exists($hash->{adb}{preset}{_custom_}) && exists($hash->{adb}{preset}{_custom_}{$macroName}));
return "Preset and/or macro $macroName not defined in preset $preset"
if ($preset eq '' || !exists($hash->{adb}{preset}{$preset}{$macroName}));
my ($rc, $result, $error) = AndroidDBHost::Run ($hash, 'shell', '.*', 'input', 'keyevent',
split (',', $PRESET{$preset}{$macroName}));
split (',', $hash->{adb}{preset}{$preset}{$macroName}));
return $error if ($rc == 0);
}
elsif ($lcopt eq 'tap') {
my ($x, $y) = @$a;
return "Usage: set $name $opt tap X Y" if (!defined($y));
my ($rc, $result, $error) = AndroidDBHost::Run ($hash, 'shell', '.*', 'input', 'tap', $x, $y);
return $error if ($rc == 0);
}
elsif ($lcopt eq 'rollhorz' || $lcopt eq 'rollvert') {
my $delta = shift @$a // return "Usage: set $name $opt Delta";
my ($dx, $dy) = $opt eq 'rollhorz' ? ($delta, 0) : (0, $delta);
my ($rc, $result, $error) = AndroidDBHost::Run ($hash, 'shell', '.*', 'input', 'roll', $dx, $dy);
return $error if ($rc == 0);
}
elsif ($lcopt eq 'createremote') {
my $layout = shift @$a // return "Usage: set $name $opt LayoutName";
my $rcName = $name.'_RC';
return "$name: Can't create remotecontrol device $rcName"
if (CommandDefine (undef, "$rcName remotecontrol"));
Log3 $name, 2, "$name: Created remotecontrol device $rcName";
return "$name: Can't select layout $layout for remotecontrol device $rcName"
if (CommandSet (undef, "$rcName layout $layout"));
Log3 $name, 2, "Selected layout $layout for $rcName";
my $room = AttrVal ($name, 'room', '');
if ($room ne '') {
Log3 $name, 2, "$name: Assigning $rcName to room $room";
CommandAttr (undef, "$rcName room $room");
}
CommandSet (undef, "$rcName makenotify $name");
CommandAttr (undef, "$name group $name");
CommandAttr (undef, "$rcName group $name");
Log3 $name, 2, "Created notify device notify_$rcName";
}
elsif ($lcopt eq 'exportpresets') {
my $filename = shift @$a // return "Usage: set $name $opt Filename";
my $rc = ExportPresets ($hash, $filename);
return "Error while saving presets to file $filename" if ($rc == 0);
return "Presets saved to file $filename";
}
elsif (exists($hash->{adb}{macro}{_custom_}) && exists($hash->{adb}{macro}{_custom_}{$opt})) {
my ($args, $pars) = parseParams ($hash->{adb}{macro}{_custom_}{$opt});
my $cmd = shift @$args;
my ($rc, $result, $error) = AndroidDBHost::Run ($hash, $cmd, '.*', @$args);
return $rc == 0 ? $error : $result;
}
elsif ($preset ne '' && exists($hash->{adb}{macro}{$preset}) && exists($hash->{adb}{macro}{$preset}{$opt})) {
my ($args, $pars) = parseParams ($hash->{adb}{macro}{$preset}{$opt});
my $cmd = shift @$args;
my ($rc, $result, $error) = AndroidDBHost::Run ($hash, $cmd, '.*', @$args);
return $rc == 0 ? $error : $result;
}
else {
return "Unknown argument $opt, choose one of $options";
}
@ -171,12 +347,35 @@ sub Get ($@)
my $name = shift @$a;
my $opt = shift @$a // return 'No get command specified';
my $options = 'presets';
my $options = 'keyPreset';
my @presetList = sort keys %{$hash->{adb}{preset}};
$options .= ':'.join(',', @presetList) if (scalar(@presetList) > 0);
my @macroList = sort keys %{$hash->{adb}{macro}};
$options .= ' cmdPreset:'.join(',', @macroList) if (scalar(@macroList) > 0);
$opt = lc($opt);
my $attrPreset = AttrVal ($name, 'preset', '');
if ($opt eq 'presets') {
return "Command not implemented";
my $lcopt = lc($opt);
if ($lcopt eq 'keypreset') {
my $preset = shift @$a // $attrPreset;
return "Usage: get $name $opt PresetName" if ($preset eq '');
return "Key preset $preset not found" if (!exists($hash->{adb}{preset}{$preset}));
my $presetDef = "Definition of key preset $preset:<br/><br/>";
foreach my $macro (sort keys %{$hash->{adb}{preset}{$preset}}) {
$presetDef .= "$macro = $hash->{adb}{preset}{$preset}{$macro}<br/>";
}
return $presetDef;
}
elsif ($lcopt eq 'cmdpreset') {
my $preset = shift @$a // $attrPreset;
return "Usage: get $name $opt PresetName" if ($preset eq '');
return "Command preset $preset not found" if (!exists($hash->{adb}{macro}{$preset}));
my $macroDef = "Definition of command preset $preset:<br/><br/>";
foreach my $macro (sort keys %{$hash->{adb}{macro}{$preset}}) {
$macroDef .= "$macro = $hash->{adb}{macro}{$preset}{$macro}<br/>";
}
return $macroDef;
}
else {
return "Unknown argument $opt, choose one of $options";
@ -190,20 +389,31 @@ sub Attr ($@)
if ($cmd eq 'set') {
if ($attrName eq 'macros') {
foreach my $macroDef (split /\s+/, $attrVal) {
my ($macroName, $macroKeycodes) = split (':', $macroDef);
$PRESET{_custom_}{$macroName} = $macroKeycodes;
foreach my $macroDef (split /;/, $attrVal) {
my ($macroName, $macroPar) = split (':', $macroDef);
if (!defined($macroDef)) {
Log3 $name, 2, "Missing defintion for macro $macroName";
return "Missing definition for macro $macroName";
}
if ($macroPar =~ /^[0-9]+/ || $macroPar =~ /^KEYCODE_/) {
$hash->{adb}{preset}{_custom_}{$macroName} = $macroPar;
}
else {
$hash->{adb}{macro}{_custom_}{$macroName} = $macroPar;
}
}
}
elsif ($attrName eq 'presetFile') {
if (!LoadPreset ($hash, $attrVal)) {
Log3 $hash, 2, "Can't load preset from file $attrVal";
return "Cannot load preset from file $attrVal";
if (!LoadPresets ($hash, $attrVal)) {
return "Cannot load presets from file $attrVal";
}
}
elsif ($attrName eq 'connect') {
AndroidDBHost::Connect ($hash) if (!$init_done && $attrVal eq '1');
}
}
elsif ($cmd eq 'del') {
delete $PRESET{_custom_} if (exists($PRESET{_custom_}));
delete $hash->{adb}{preset}{_custom_} if (exists($hash->{adb}{preset}{_custom_}));
}
return undef;
@ -213,50 +423,194 @@ sub Attr ($@)
# Load macro definitions from file
# File format:
# - Lines starting with a # are treated as comments
# - Empty lines are ignored
# - Lines containing a single word are setting the preset name for the
# following lines
# - Lines in format Name:KeyList are defining a macro. KeyList is a comma
# separated list of keycodes.
# - Lines in format Name:Command:Parameters are defining a command macro.
##############################################################################
sub LoadPreset ($$)
sub LoadPresets ($$)
{
my ($hash, $fileName) = @_;
my @lines;
# Read file
my @lines;
if (open (PRESETFILE, "<$fileName")) {
@lines = <PRESETFILE>;
close (PRESETFILE);
}
else {
ShowMessage ($hash, 2, "Can't open file $fileName");
return 0;
}
# Delete old presets
my @presets = keys %{$hash->{adb}{preset}};
my @macros = keys %{$hash->{adb}{macro}};
foreach my $e (@presets) { delete $hash->{adb}{preset}{$e} if ($e ne '_custom_'); }
foreach my $e (@macros) { delete $hash->{adb}{macro}{$e} if ($e ne '_custom_'); }
chomp @lines;
my $presetName = '';
foreach my $l (@lines) {
next if ($l =~ /^#/); # Comments are allowed
my ($macroName, $keyList) = split (':', $l);
if (!defined($keyList)) {
my ($macroName, $macroPar) = split (':', $l);
if (!defined($macroPar)) {
next if (!defined($macroName) || $macroName eq '');
if ($macroName !~ /^[a-zA-Z0-9-_]+$/) {
ShowMessage ($hash, 2, "Invalid character in macro name $macroName in file $fileName");
return 0;
}
$presetName = $macroName;
next;
}
elsif (defined($keyList) && $presetName eq '') {
Log3 $hash, 2, "Preset name must be specified before first macro definition in file $fileName";
return 0;
else {
if ($presetName eq '') {
ShowMessage ($hash, 2, "No preset name set for macro name $macroName in file $fileName");
return 0;
}
if ($macroPar =~ /^[0-9]+/ || $macroPar =~ /^(KEYCODE_|--longpress)/) {
$hash->{adb}{preset}{$presetName}{$macroName} = $macroPar;
}
else {
$hash->{adb}{macro}{$presetName}{$macroName} = $macroPar;
}
}
$PRESET{$presetName}{$macroName} = $keyList;
}
# Init options of attribute 'preset'
InitAfterStart ($hash);
return 1;
}
sub ExportPresets ($$)
{
my ($hash, $filename) = @_;
if (open (PRESETFILE, ">$filename")) {
foreach my $preset (sort keys %{$hash->{adb}{preset}}) {
next if ($preset eq '_custom_');
print PRESETFILE "#\n# Preset $preset\n#\n$preset\n#\n";
foreach my $macro (sort keys %{$hash->{adb}{preset}{$preset}}) {
print PRESETFILE "$macro:$hash->{adb}{preset}{$preset}{$macro}\n";
}
if (exists($hash->{adb}{macro}{$preset})) {
foreach my $macro (sort keys %{$hash->{adb}{macro}{$preset}}) {
print PRESETFILE "$macro:$hash->{adb}{macro}{$preset}{$macro}\n";
}
}
}
foreach my $preset (sort keys %{$hash->{adb}{macro}}) {
next if ($preset eq '_custom_' || exists($hash->{adb}{preset}{$preset}));
print PRESETFILE "#\n# Preset $preset\n#\n$preset\n#\n";
foreach my $macro (sort keys %{$hash->{adb}{macro}{$preset}}) {
print PRESETFILE "$macro:$hash->{adb}{macro}{$preset}{$macro}\n";
}
}
close (PRESETFILE);
return 1;
}
return 0;
}
sub ShowMessage ($$$)
{
my ($hash, $level, $msg) = @_;
Log3 $hash->{NAME}, $level, $msg;
if ($init_done && exists($hash->{CL})) {
my $cl = $hash->{CL};
InternalTimer (gettimeofday()+1, sub { asyncOutput ($cl, $msg) }, undef, 1);
}
}
##############################################################################
# Remote control layout for Magenta TV Stick
##############################################################################
sub RCLayoutMagentaTVStick () {
my @row = (
'sendKey KEYCODE_POWER:POWEROFF,:blank,sendKey KEYCODE_VOLUME_MUTE:MUTE',
':blank,sendKey KEYCODE_TV_INPUT_HDMI_3:PS3Rectangle,:blank',
':blank,sendKey KEYCODE_ALL_APPS:TOOLS,:blank',
'sendKey KEYCODE_TV_INPUT_HDMI_4:TV,:blank,sendKey KEYCODE_TV_INPUT_HDMI_2:GUIDE',
':blank,sendKey KEYCODE_DPAD_UP:UP,:blank',
'sendKey KEYCODE_DPAD_LEFT:LEFT,sendKey KEYCODE_DPAD_CENTER:OK,sendKey KEYCODE_DPAD_RIGHT:RIGHT',
':blank,sendKey KEYCODE_DPAD_DOWN:DOWN,:blank',
'sendKey KEYCODE_BACK:BACKDroid,sendKey KEYCODE_HOME:HOMEDroid,sendKey KEYCODE_INFO:INFO',
'sendKey KEYCODE_MEDIA_RECORD:REC,sendKey KEYCODE_TV_INPUT_HDMI_1:SEARCH,sendKey KEYCODE_VOLUME_UP:VOLUP',
'sendKey KEYCODE_MEDIA_PLAY_PAUSE:PLAYPAUSE,sendKey KEYCODE_ASSIST:SOURCE,sendKey KEYCODE_VOLUME_DOWN:VOLDOWN',
'attr rc_iconpath icons/remotecontrol',
'attr rc_iconprefix black_btn_'
);
return @row;
}
##############################################################################
# Remote control layout for Magenta One
##############################################################################
sub RCLayoutMagentaOne () {
my @row = (
'sendKey KEYCODE_POWER:POWEROFF,:blank,sendKey KEYCODE_VOLUME_MUTE:MUTE',
':blank,:blank,:blank',
'sendKey KEYCODE_TV_INPUT_HDMI_1:SEARCH,sendKey KEYCODE_ASSIST:SOURCE,sendKey KEYCODE_GUIDE:GUIDE',
'sendKey KEYCODE_BACK:BACKDroid,sendKey KEYCODE_HOME:HOMEDroid,sendKey KEYCODE_ALL_APPS:MENUDroid',
':blank,sendKey KEYCODE_DPAD_UP:UP,:blank',
'sendKey KEYCODE_DPAD_LEFT:LEFT,sendKey KEYCODE_DPAD_CENTER:OK,sendKey KEYCODE_DPAD_RIGHT:RIGHT',
':blank,sendKey KEYCODE_DPAD_DOWN:DOWN,:blank',
'sendKey KEYCODE_VOLUME_UP:VOLUP,sendKey KEYCODE_MEDIA_PLAY_PAUSE:PLAYPAUSE,sendKey KEYCODE_CHANNEL_UP:CHUP',
'sendKey KEYCODE_VOLUME_DOWN:VOLDOWN,:blank,sendKey KEYCODE_CHANNEL_DOWN:CHDOWN',
'sendKey KEYCODE_1:1,sendKey KEYCODE_2:2,sendKey KEYCODE_3:3',
'sendKey KEYCODE_4:4,sendKey KEYCODE_5:5,sendKey KEYCODE_6:6',
'sendKey KEYCODE_7:7,sendKey KEYCODE_8:8,sendKey KEYCODE_9:9',
'sendKey KEYCODE_INFO:INFO,sendKey KEYCODE_0:0,sendKey KEYCODE_MEDIA_RECORD:REC',
'attr rc_iconpath icons/remotecontrol',
'attr rc_iconprefix black_btn_'
);
return @row;
}
##############################################################################
# Extended remote control layout for Magenta TV
##############################################################################
sub RCLayoutMagentaTVExt () {
my @row = (
'sendKey KEYCODE_POWER:POWEROFF,:blank,sendKey KEYCODE_VOLUME_MUTE:MUTE,:blank',
':blank,:blank,:blank,:blank',
'sendKey KEYCODE_TV_INPUT_HDMI_1:SEARCH,sendKey KEYCODE_ASSIST:SOURCE,sendKey KEYCODE_GUIDE:GUIDE,sendKey KEYCODE_TV_INPUT_HDMI_4:TV',
'sendKey KEYCODE_BACK:BACKDroid,sendKey KEYCODE_HOME:HOMEDroid,sendKey KEYCODE_ALL_APPS:MENUDroid,:blank',
':blank,sendKey KEYCODE_DPAD_UP:UP,:blank,:blank',
'sendKey KEYCODE_DPAD_LEFT:LEFT,sendKey KEYCODE_DPAD_CENTER:OK,sendKey KEYCODE_DPAD_RIGHT:RIGHT,:blank',
':blank,sendKey KEYCODE_DPAD_DOWN:DOWN,:blank,:blank',
'sendKey KEYCODE_VOLUME_UP:VOLUP,sendKey KEYCODE_MEDIA_PLAY_PAUSE:PLAYPAUSE,sendKey KEYCODE_CHANNEL_UP:CHUP,:blank',
'sendKey KEYCODE_VOLUME_DOWN:VOLDOWN,:blank,sendKey KEYCODE_CHANNEL_DOWN:CHDOWN,:blank',
'sendKey KEYCODE_1:1,sendKey KEYCODE_2:2,sendKey KEYCODE_3:3,:blank',
'sendKey KEYCODE_4:4,sendKey KEYCODE_5:5,sendKey KEYCODE_6:6,:blank',
'sendKey KEYCODE_7:7,sendKey KEYCODE_8:8,sendKey KEYCODE_9:9,:blank',
'sendKey KEYCODE_INFO:INFO,sendKey KEYCODE_0:0,sendKey KEYCODE_MEDIA_RECORD:REC,:blank',
'sendKey KEYCODE_PROG_RED:RED,sendKey KEYCODE_PROG_GREEN:GREEN,sendKey KEYCODE_PROG_YELLOW:YELLOW,sendKey KEYCODE_PROG_BLUE:BLUE',
'attr rc_iconpath icons/remotecontrol',
'attr rc_iconprefix black_btn_'
);
return @row;
}
1;
=pod
=item device
=item summary Allows to control an Android device via ADB
=item summary Allows to control an Android device via ADB (Android Debug Bridge)
=begin html
<a name="AndroidDB"></a>
@ -270,8 +624,10 @@ sub LoadPreset ($$)
<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.
<code>define &lt;name&gt; AndroidDB &lt;NameOrIP&gt;[&lt;Port&gt;]</code>
The parameter <i>NameOrIP</i> is the hostname or the IP address of the Android device.
The parameter <i>Port</i> specifies the TCP port to be used for the device connection.
The default port is 5555.
</ul>
<br/>
</ul>
@ -279,6 +635,14 @@ sub LoadPreset ($$)
<a name="AndroidDBset"></a>
<b>Set</b><br/><br/>
<ul>
<li><b>set &lt;Name&gt; createRemote &lt;Layout&gt;</b><br/>
Create a remote control device for the Android device. Create a notify device for
the remote control to send button events to the AndroidDB device.
</li>br/>
<li><b>set &lt;Name&gt; exportPresets &lt;Filename&gt;</b><br/>
Export the currently loaded presets to file. This file can be modified and assigned
again by using attribute 'preset'.
</li><br/>
<li><b>set &lt;name&gt; reboot</b><br/>
Reboot the device.
</li><br/>
@ -288,23 +652,61 @@ sub LoadPreset ($$)
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><b>set &lt;name&gt; rollHorz &lt;DeltaX&gt;</b><br/>
Scroll display horizontally. Not supported on all types of Android devices.
</li><br/>
<li><b>set &lt;name&gt; rollVert &lt;DeltaY&gt;</b><br/>
Scroll display vertically. Not supported on all types of Android devices.
</li><br/>
<li><b>set &lt;name&gt; sendKey [longpress] &lt;KeyCode&gt;</b> [...]<br/>
Send a key code to the Android device. If option 'longpress' is specified, only one
<i>KeyCode</i> is allowed.
</li><br/>
<li><b>set &lt;name&gt; sendNumKeys &lt;Number&gt;</b><br/>
Send digits of <i>Number</i> to the Android device. <i>Number</i> must be in range 0-9999.
</li><br/>
<li><b>set &lt;name&gt; sendText &lt;Text&gt;</b><br/>
Send <i>Text</i> 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/>
<li><b>set &lt;name&gt; tap &lt;X&gt; &lt;Y&gt;</b><br/>
Simulate a tap on a touchscreen (only available an devices having a touchscreen).
</li><br/>
</ul>
<a name="AndroidDBget"></a>
<b>Get</b><br/><br/>
<ul>
<li><b>set &lt;Name&gt; keyPreset &lt;PresetName&gt;</b><br/>
List key preset definition.
</li>br/>
<li><b>set &lt;Name&gt; cmdPreset &lt;PresetName&gt;</b><br/>
List command preset definition.
</li>br/>
</ul>
<a name="AndroidDBattr"></a>
<b>Attributes</b><br/><br/>
<ul>
<a name="connect"></a>
<li><b>connect 0|1</b><br/>
If set to 1, a connection to the Android device will be established during
FHEM start. Note: Set this attribute for one Android device only!
</li><br/>
<a name="macros"></a>
<li><b>macros &lt;MacroDef&gt; [...]</b><br/>
<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/>
command or define shortcuts for remote commands.<br/>
A 'MacroDef' is using the following syntax:<br/>
MacroName:KeyCode[,...]<br/>
Several macro definitions can be specified by seperating them using a blank character.
or<br/>
MacroName:Command<br/><br/>
Parameter <i>Command</i> is a adb command.<br/>
Example, define a command 'set listpackages':<br/>
<pre>attr myDev macros listpackages:shell pm list packages -f</pre><br/>
Several macro definitions can be specified by seperating them using a semicolon.
</li><br/>
<a name="preset"></a>
<li><b>preset &lt;PresetName&gt;</b><br/>
@ -320,12 +722,17 @@ sub LoadPreset ($$)
<pre>
# Comment
PresetName1
MacroName1:KeyCode[,...]
MacroName2:KeyCode[,...]
MacroDef1
MacroDef2
...
PresetName2
...
</pre>
A 'MacroDef' is using the following syntax:<br/>
MacroName:KeyCode[,...]<br/>
or<br/>
MacroName:Command:Parameters<br/><br/>
Usually <i>Command</i> is 'shell'.
</li><br/>
</ul>

View File

@ -4,7 +4,7 @@
#
# 89_AndroidDBHost
#
# Version 0.4
# Version 0.6
#
# FHEM Integration for Android Debug Bridge
#
@ -19,8 +19,6 @@
# Windows/MacOSX/Linux x86: https://developer.android.com/studio/releases/platform-tools
#
package main;
use strict;
@ -86,7 +84,7 @@ sub Define ($$$)
$hash->{adb}{port} = $port // 5037;
$hash->{adb}{cmd} = $h->{adb} // '/usr/bin/adb';
$hash->{Clients} = ':AndroidDB:';
$hash->{NOTIFYDEV} = 'global,TYPE=(AndroidDBHost|AndroidDB)';
$hash->{NOTIFYDEV} = 'global,TYPE=AndroidDBHost';
# 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}");
@ -132,7 +130,15 @@ sub Notify ($$)
return if (!$events);
if ($devhash->{NAME} eq 'global' && grep (/INITIALIZED/, @$events)) {
InternalTimer (gettimeofday()+60, 'AndroidDBHost::CheckADBServerTimer', $hash, 0);
foreach my $clName (keys %defs) {
my $clHash = $defs{$clName};
if ($clHash->{TYPE} eq 'AndroidDB') {
AndroidDB::InitAfterStart ($clHash);
}
}
# First refresh of client connection state after 10 seconds
InternalTimer (gettimeofday()+10, 'AndroidDBHost::CheckADBServerTimer', $hash, 0);
}
}
@ -194,7 +200,8 @@ sub UpdateClientStates ($)
foreach my $d (keys %defs) {
my $clHash = $defs{$d};
if ($clHash->{TYPE} eq 'AndroidDB') {
next if (!defined($clHash->{TYPE}) || !defined($clHash->{IODev}));
if ($clHash->{TYPE} eq 'AndroidDB' && $clHash->{IODev} == $hash) {
my $clState = $device->{$clHash->{ADBDevice}} // 'disconnected';
$clState =~ s/device/connected/;
readingsSingleUpdate ($clHash, 'state', $clState, 1);
@ -253,10 +260,9 @@ sub Set ($@)
my ($rc, $result, $error) = Execute ($hash, $command, '.*', @$a);
return $result.$error;
}
elsif ($opt eq 'disconnect') {
my ($rc, $result, $error) = Execute ($hash, 'disconnect', 'disconnected');
UpdateClientStates ($hash);
return "Disconnecting all devices failed $result $error" if ($rc == 0);
elsif ($opt eq 'disconnectall') {
my ($rc, $error) = DisconnectAll ($hash);
return "Disconnecting all devices failed: $error" if ($rc == 0);
}
else {
return "Unknown argument $opt, choose one of $options";
@ -311,8 +317,8 @@ sub Get ($@)
# Return value:
# (returncode, stdout, stderr)
# Return codes:
# 0 - error
# 1 - success
# 0 = error
# 1 = success
##############################################################################
sub Execute ($@)
@ -324,6 +330,8 @@ sub Execute ($@)
Log3 $ioHash->{NAME}, 2, 'Execute: ADB server not running';
return (0, '', 'ADB server not running');
}
Log3 $ioHash->{NAME}, 5, "Executing $ioHash->{adb}{cmd} $command ".join(' ',@args);
# Execute ADB command
local (*CHILDIN, *CHILDOUT, *CHILDERR);
@ -370,13 +378,13 @@ sub Execute ($@)
sub IsConnected ($)
{
my $clHash = shift // return 0;
my $clHash = shift // return -1;
my $ioHash = $clHash->{IODev} // return -1;
# Get active connections
my $device = GetDeviceList ($ioHash) // return -1;
my $devCount = keys %$device;
my $devCount = scalar(keys %$device);
if ($devCount == 1) {
return exists($device->{$clHash->{ADBDevice}}) ? 1 : 2;
@ -403,13 +411,13 @@ sub Connect ($)
my $ioHash = $clHash->{IODev} // return -1;
my $connect = IsConnected ($clHash);
# Log3 $clHash->{NAME}, 2, "Connection state is $connect";
if ($connect == 1) {
return 1;
}
elsif ($connect == 2) {
# Disconnect all devices
my ($rc, $result, $error) = Execute ($ioHash, 'disconnect', 'disconnected');
return -1 if ($rc == 0);
DisconnectAll ($ioHash);
}
elsif ($connect == -1) {
Log3 $clHash->{NAME}, 2, 'Cannot detect connection state';
@ -424,11 +432,11 @@ sub Connect ($)
}
##############################################################################
# Connect to Android device
# Disconnect Android device
#
# Return value:
# 0 = error
# 1 = connected
# -1, 0 = error
# 1 = success
##############################################################################
sub Disconnect ($)
@ -443,6 +451,21 @@ sub Disconnect ($)
return $rc;
}
##############################################################################
# Disconnect all Android devices
##############################################################################
sub DisconnectAll ($)
{
my ($hash) = @_;
my ($rc, $result, $error) = Execute ($hash, 'disconnect', 'disconnected');
UpdateClientStates ($hash);
return (0, $error) if ($rc == 0);
return (1, 'ok');
}
##############################################################################
# Get list of devices
##############################################################################
@ -482,12 +505,9 @@ sub Run ($@)
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);
}
@ -546,6 +566,9 @@ sub TCPConnect ($$$)
<li><b>set &lt;name&gt; command &lt;Command&gt; [&lt;Args&gt;]</b><br/>
Execute ADB command.
</li><br/>
<li><b>set &lt;name&gt; disconnectAll</b><br/>
Disconnect all devices.
</li><br/>
<li><b>set &lt;name&gt; start</b><br/>
Start ADB server.
</li><br/>