##############################################################################
#
# 89_FULLY.pm 0.8
#
# $Id$
#
# Control Fully browser on Android tablets from FHEM.
# Requires Fully Plus license!
#
##############################################################################
package main;
use strict;
use warnings;
use Blocking;
use SetExtensions;
# Declare functions
sub FULLY_Initialize ($);
sub FULLY_Define ($$);
sub FULLY_Undef ($$);
sub FULLY_Shutdown ($);
sub FULLY_Set ($@);
sub FULLY_Get ($@);
sub FULLY_Attr ($@);
sub FULLY_Detail ($@);
sub FULLY_UpdateDeviceInfo ($);
sub FULLY_Execute ($$$$);
sub FULLY_ScreenOff ($);
sub FULLY_GetDeviceInfo ($);
sub FULLY_ProcessDeviceInfo ($$);
sub FULLY_GotDeviceInfo ($);
sub FULLY_Abort ($);
sub FULLY_UpdateReadings ($$);
sub FULLY_Ping ($$);
my $FULLY_VERSION = "0.8";
my $FULLY_TIMEOUT = 4;
my $FULLY_POLL_INTERVAL = 3600;
my $FULLY_FHEM_COMMAND = qq(
function SendRequest(FHEM_Address, Devicename, Command) {
var Port = "8085"
var url = "http://" + FHEM_Address + ":" + Port + "/fhem?XHR=1&cmd." + Devicename + "=" + Command;
var req = new XMLHttpRequest();
req.open("GET", url);
req.send(null);
req = null;
}
);
##################################################
# Initialize module
##################################################
sub FULLY_Initialize ($)
{
my ($hash) = @_;
$hash->{DefFn} = "FULLY_Define";
$hash->{UndefFn} = "FULLY_Undef";
$hash->{SetFn} = "FULLY_Set";
$hash->{GetFn} = "FULLY_Get";
$hash->{AttrFn} = "FULLY_Attr";
$hash->{ShutdownFn} = "FULLY_Shutdown";
$hash->{FW_detailFn} = "FULLY_Detail";
$hash->{parseParams} = 1;
$hash->{AttrList} = "pingBeforeCmd:0,1,2 pollInterval requestTimeout repeatCommand:0,1,2 " .
"disable:0,1 " .
$readingFnAttributes;
}
##################################################
# Define device
##################################################
sub FULLY_Define ($$)
{
my ($hash, $a, $h) = @_;
my $name = $hash->{NAME};
my $rc = 0;
return "Usage: define devname FULLY IP_or_Hostname password [poll-interval]"
if (@$a < 4);
return "FULLY: polling interval must be in range 10 - 86400"
if (@$a == 5 && ($$a[4] !~ /^[1-9][0-9]+$/ || $$a[4] < 10 || $$a[4] > 86400));
$hash->{host} = $$a[2];
$hash->{version} = $FULLY_VERSION;
$hash->{onForTimer} = 'off';
# $hash->{remoteAdmin} = 'Remote Admin';
$hash->{fully}{password} = $$a[3];
$hash->{fully}{schedule} = 0;
Log3 $name, 1, "FULLY: Version $FULLY_VERSION Opening device ".$hash->{host};
my $result = FULLY_GetDeviceInfo ($name);
if (!FULLY_UpdateReadings ($hash, $result)) {
Log3 $name, 2, "FULLY: Update of device info failed";
}
if (@$a == 5) {
$attr{$name}{'pollInterval'} = $$a[4];
$hash->{nextUpdate} = strftime "%d.%m.%Y %H:%M:%S", localtime (time+$$a[4]);
InternalTimer (gettimeofday()+$$a[4], "FULLY_UpdateDeviceInfo", $hash, 0);
}
else {
$hash->{nextUpdate} = 'off';
}
return undef;
}
#####################################
# Set or delete attribute
#####################################
sub FULLY_Attr ($@)
{
my ($cmd, $name, $attrname, $attrval) = @_;
my $hash = $defs{$name};
if ($cmd eq 'set') {
if ($attrname eq 'pollInterval') {
if ($attrval >= 10 && $attrval <= 86400) {
my $curval = AttrVal ($name, 'pollInterval', $FULLY_POLL_INTERVAL);
if ($attrval != $curval) {
Log3 $name, 2, "FULLY: Polling interval set to $attrval";
RemoveInternalTimer ($hash);
$hash->{nextUpdate} = strftime "%d.%m.%Y %H:%M:%S", localtime (time+$attrval);
InternalTimer (gettimeofday()+$attrval, "FULLY_UpdateDeviceInfo", $hash, 0);
}
}
elsif ($attrval == 0) {
RemoveInternalTimer ($hash);
$hash->{nextUpdate} = 'off';
}
else {
return "FULLY: Polling interval must be in range 10-86400";
}
}
elsif ($attrname eq 'requestTimeout') {
return "FULLY: Timeout must be greater than 0" if ($attrval < 1);
}
}
elsif ($cmd eq 'del') {
if ($attrname eq 'pollInterval') {
RemoveInternalTimer ($hash);
$hash->{nextUpdate} = 'off';
}
}
return undef;
}
#####################################
# Delete device
#####################################
sub FULLY_Undef ($$)
{
my ($hash, $arg) = @_;
RemoveInternalTimer ($hash);
BlockingKill ($hash->{fully}{bc}) if (defined ($hash->{fully}{bc}));
return undef;
}
#####################################
# Shutdown FHEM
#####################################
sub FULLY_Shutdown ($)
{
my ($hash) = @_;
RemoveInternalTimer ($hash);
BlockingKill ($hash->{fully}{bc}) if (defined ($hash->{fully}{bc}));
return undef;
}
#####################################
# Enhance device detail view
#####################################
sub FULLY_Detail ($@)
{
my ($FW_wname, $name, $room, $pageHash) = @_;
my $hash = $defs{$name};
my $html = qq(
Device Administration
);
return $html;
}
#####################################
# Set commands
#####################################
sub FULLY_Set ($@)
{
my ($hash, $a, $h) = @_;
my $name = shift @$a;
my $opt = shift @$a;
my $options = "brightness clearCache:noArg exit:noArg lock:noArg motionDetection:on,off ".
"off:noArg on:noArg on-for-timer restart:noArg screenOffTimer screenSaver:start,stop ".
"screenSaverTimer screenSaverURL speak startURL unlock:noArg url";
my $response;
# Fully commands without argument
my %cmds = (
"clearCache" => "clearCache",
"exit" => "exitApp", "restart" => "restartApp",
"on" => "screenOn", "off" => "screenOff",
"lock" => "enabledLockedMode", "unlock" => "disableLockedMode"
);
my $disable = AttrVal ($name, 'disable', 0);
return undef if ($disable);
if (exists ($cmds{$opt})) {
$response = FULLY_Execute ($hash, $cmds{$opt}, undef, 1);
}
elsif ($opt eq 'on-for-timer') {
my $par = shift @$a;
$par = "forever" if (!defined ($par));
if ($par eq 'forever') {
$response = FULLY_Execute ($hash, "setBooleanSetting",
{ "key" => "keepScreenOn", "value" => "true" }, 1);
$response = FULLY_Execute ($hash, "screenOn", undef, 0)
if (defined ($response) && $response ne '');
}
elsif ($par eq 'off') {
$response = FULLY_Execute ($hash, "setBooleanSetting",
{ "key" => "keepScreenOn", "value" => "false" }, 1);
$response = FULLY_Execute ($hash, "setStringSetting",
{ "key" => "timeToScreenOffV2", "value" => "0" }, 0)
if (defined ($response) && $response ne '');
}
elsif ($par =~ /^[0-9]+$/) {
$response = FULLY_Execute ($hash, "setBooleanSetting",
{ "key" => "keepScreenOn", "value" => "true" }, 1);
$response = FULLY_Execute ($hash, "screenOn", undef, 0)
if (defined ($response) && $response ne '');
InternalTimer (gettimeofday()+$par, "FULLY_ScreenOff", $hash, 0);
}
else {
return "Usage: set $name on-for-timer [{ Seconds | forever | off }]";
}
RemoveInternalTimer ($hash, "FULLY_ScreenOff") if ($par eq 'off' || $par eq 'forever');
$hash->{onForTimer} = $par if (defined ($response) && $response ne '');
}
elsif ($opt eq 'screenOffTimer') {
my $value = shift @$a;
return "Usage: set $name $opt {seconds}" if (!defined ($value));
$response = FULLY_Execute ($hash, "setStringSetting",
{ "key" => "timeToScreenOffV2", "value" => "$value" }, 1);
}
elsif ($opt eq 'screenSaver') {
my $state = shift @$a;
return "Usage: set $name $opt { start | stop }" if (!defined ($state));
if ($state eq 'start') {
$response = FULLY_Execute ($hash, "startScreensaver", undef, 1);
}
elsif ($state eq 'stop') {
$response = FULLY_Execute ($hash, "stopScreensaver", undef, 1);
}
else {
return "Usage: set $name $opt { start | stop }";
}
}
elsif ($opt eq 'screenSaverTimer') {
my $value = shift @$a;
return "Usage: set $name $opt {seconds}" if (!defined ($value));
$response = FULLY_Execute ($hash, "setStringSetting",
{ "key" => "timeToScreensaverV2", "value" => "$value" }, 1);
}
elsif ($opt eq 'screenSaverURL') {
my $value = shift @$a;
return "Usage: set $name $opt {URL}" if (!defined ($value));
$response = FULLY_Execute ($hash, "setStringSetting",
{ "key" => "screensaverURL", "value" => "$value" }, 1);
}
elsif ($opt eq 'startURL') {
my $value = shift @$a;
return "Usage: set $name $opt {URL}" if (!defined ($value));
$response = FULLY_Execute ($hash, "setStringSetting",
{ "key" => "startURL", "value" => "$value" }, 1);
}
elsif ($opt eq 'brightness') {
my $value = shift @$a;
return "Usage: set $name brightness 0-255" if (!defined ($value));
$value = 255 if ($value > 255);
$response = FULLY_Execute ($hash, "setStringSetting",
{ "key" => "screenBrightness", "value" => "$value" }, 1);
}
elsif ($opt eq 'motionDetection') {
my $state = shift @$a;
return "Usage: set $name motionDetection { on | off }" if (!defined ($state));
my $value = $state eq 'on' ? 'true' : 'false';
$response = FULLY_Execute ($hash, "setBooleanSetting",
{ "key" => "motionDetection", "value" => "$value" }, 1);
}
elsif ($opt eq 'speak') {
my $text = shift @$a;
return 'Usage: set $name speak "{Text}"' if (!defined ($text));
while ($text =~ /\[(.+):(.+)\]/) {
my ($device, $reading) = ($1, $2);
my $value = ReadingsVal ($device, $reading, '');
$text =~ s/\[$device:$reading\]/$value/g;
}
my $enctext = urlEncode ($text);
$response = FULLY_Execute ($hash, "textToSpeech", { "text" => "$enctext" }, 1);
}
elsif ($opt eq 'url') {
my $url = shift @$a;
my $cmd = defined ($url) ? "loadURL" : "loadStartURL";
$response = FULLY_Execute ($hash, $cmd, { "url" => "$url" }, 1);
}
else {
return "FULLY: Unknown argument $opt, choose one of ".$options;
}
my $result = FULLY_ProcessDeviceInfo ($name, $response);
if (!FULLY_UpdateReadings ($hash, $result)) {
Log3 $name, 2, "FULLY: Command failed";
return "FULLY: Command failed";
}
return undef;
}
#####################################
# Get commands
#####################################
sub FULLY_Get ($@)
{
my ($hash, $a, $h) = @_;
my $name = shift @$a;
my $opt = shift @$a;
my $options = "info:noArg stats:noArg update:noArg";
my $response;
my $disable = AttrVal ($name, 'disable', 0);
return undef if ($disable);
if ($opt eq 'info') {
my $result = FULLY_Execute ($hash, 'deviceInfo', undef, 1);
if (!defined ($result) || $result eq '') {
Log3 $name, 2, "FULLY: Command failed";
return "FULLY: Command failed";
}
elsif ($response =~ /Wrong password/) {
Log3 $name, 2, "FULLY: Wrong password";
return "FULLY: Wrong password";
}
$response = '';
while ($result =~ /table-cell\">([^<]+)<\/td>([^<]+)\n";
}
return $response;
}
elsif ($opt eq 'stats') {
return "FULLY: Command not implemented";
}
elsif ($opt eq 'update') {
my $result = FULLY_GetDeviceInfo ($name);
if (!FULLY_UpdateReadings ($hash, $result)) {
Log3 $name, 2, "FULLY: Command failed";
return "FULLY: Command failed";
}
}
else {
return "FULLY: Unknown argument $opt, choose one of ".$options;
}
return undef;
}
#####################################
# Execute Fully command
#####################################
sub FULLY_Execute ($$$$)
{
my ($hash, $command, $param, $doping) = @_;
my $name = $hash->{NAME};
# Get attributes
my $timeout = AttrVal ($name, 'requestTimeout', $FULLY_TIMEOUT);
my $repeatCommand = min (AttrVal ($name, 'repeatCommand', 0), 2);
my $ping = min (AttrVal ($name, 'pingBeforeCmd', 0), 2);
my $response = '';
my $url = "http://".$hash->{host}.":2323/?cmd=$command";
if (defined ($param)) {
foreach my $parname (keys %$param) {
if (defined ($param->{$parname})) {
$url .= "&$parname=".$param->{$parname};
}
}
}
# Ping tablet device
FULLY_Ping ($hash, $ping) if ($doping && $ping > 0);
my $i = 0;
while ($i <= $repeatCommand && (!defined ($response) || $response eq '')) {
$response = GetFileFromURL ("$url&password=".$hash->{fully}{password}, $timeout);
Log3 $name, 4, "FULLY: HTTP response empty" if (defined ($response) && $response eq '');
$i++;
}
return $response;
}
#####################################
# Timer function: Turn screen off
#####################################
sub FULLY_ScreenOff ($)
{
my ($hash) = @_;
my $response = FULLY_Execute ($hash, "setBooleanSetting",
{ "key" => "keepScreenOn", "value" => "false" }, 1);
$response = FULLY_Execute ($hash, "screenOff", undef, 1)
if (defined ($response) && $response ne '');
$hash->{onForTimer} = 'off' if (defined ($response) && $response ne '');
}
#####################################
# Timer function: Read device info
#####################################
sub FULLY_UpdateDeviceInfo ($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
my $disable = AttrVal ($name, 'disable', 0);
if (!exists ($hash->{fully}{bc}) && $disable == 0) {
$hash->{fully}{bc} = BlockingCall ("FULLY_GetDeviceInfo", $name, "FULLY_GotDeviceInfo",
120, "FULLY_Abort", $hash);
}
}
#####################################
# Get tablet device information
#####################################
sub FULLY_GetDeviceInfo ($)
{
my ($name) = @_;
my $hash = $defs{$name};
my $result = FULLY_Execute ($hash, 'deviceInfo', undef, 1);
return FULLY_ProcessDeviceInfo ($name, $result);
}
#####################################
# Extract parameters from HTML code
#####################################
sub FULLY_ProcessDeviceInfo ($$)
{
my ($name, $result) = @_;
return "$name|0|state=failed" if (!defined ($result) || $result eq '');
return "$name|0|state=wrong password" if ($result =~ /Wrong password/);
my $parameters = "$name|1";
while ($result =~ /table-cell\">([^<]+)<\/td> | ([^<]+){fully}{bc} if (exists ($hash->{fully}{bc}));
my $rc = FULLY_UpdateReadings ($hash, $string);
if (!$rc) {
Log3 $name, 2, "FULLY: Request timed out";
if ($hash->{fully}{schedule} == 0) {
$hash->{fully}{schedule} += 1;
Log3 $name, 2, "FULLY: Rescheduling in $timeout seconds.";
$pollInterval = $timeout;
}
else {
$hash->{fully}{schedule} = 0;
}
}
$hash->{nextUpdate} = strftime "%d.%m.%Y %H:%M:%S", localtime (time+$pollInterval);
InternalTimer (gettimeofday()+$pollInterval, "FULLY_UpdateDeviceInfo", $hash, 0)
if ($pollInterval > 0);
}
#####################################
# Abort function for blocking call
#####################################
sub FULLY_Abort ($)
{
my ($hash) = @_;
my $name = $hash->{NAME};
my $pollInterval = AttrVal ($name, 'pollInterval', $FULLY_POLL_INTERVAL);
my $timeout = AttrVal ($name, 'requestTimeout', $FULLY_TIMEOUT);
delete $hash->{fully}{bc} if (exists ($hash->{fully}{bc}));
Log3 $name, 2, "FULLY: request timed out";
if ($hash->{fully}{schedule} == 0) {
$hash->{fully}{schedule} += 1;
Log3 $name, 2, "FULLY: Rescheduling in $timeout seconds.";
$pollInterval = $timeout;
}
else {
$hash->{fully}{schedule} = 0;
}
$hash->{nextUpdate} = strftime "%d.%m.%Y %H:%M:%S", localtime (time+$pollInterval);
InternalTimer (gettimeofday()+$pollInterval, "FULLY_UpdateDeviceInfo", $hash, 0)
if ($pollInterval > 0);
}
#####################################
# Update readings
#####################################
sub FULLY_UpdateReadings ($$)
{
my ($hash, $result) = @_;
my $name = $hash->{NAME};
my $rc = 1;
if (!defined ($result) || $result eq '') {
Log3 $name, 2, "FULLY: empty response";
return 0;
}
my @parameters = split ('\|', $result);
if (scalar (@parameters) == 0) {
Log3 $name, 2, "FULLY: empty response";
return 0;
}
if ($parameters[0] eq $name) {
my $n = shift @parameters;
$rc = shift @parameters;
}
readingsBeginUpdate ($hash);
foreach my $parval (@parameters) {
my ($rn, $rv) = split ('=', $parval);
readingsBulkUpdate ($hash, $rn, $rv);
}
readingsEndUpdate ($hash, 1);
return $rc;
}
#####################################
# Send ICMP request to tablet device
# Adapted from presence module.
# Thx Markus.
#####################################
sub FULLY_Ping ($$)
{
my ($hash, $count) = @_;
my $name = $hash->{NAME};
my $host = $hash->{host};
my $temp;
if ($^O =~ m/(Win|cygwin)/)
{
$temp = qx(ping -n $count -4 $host);
}
elsif ($^O =~ m/solaris/)
{
$temp = qx(ping $host $count 2>&1);
}
else
{
$temp = qx(ping -c $count $host 2>&1);
}
Log3 $name, 4, "FULLY: Ping response = $temp" if (defined ($temp));
sleep (1);
return $temp;
}
1;
=pod
=item device
=item summary FULLY Browser Integration
=begin html
FULLY
Module for controlling of Fully browser on Android tablets.
Define
define <name> FULLY <HostOrIP> <password> [<poll-interval>]
The parameter password is the password set in Fully browser.
Set
- set <name> brightness 0-255
Adjust screen brightness.
- set <name> clearCache
Clear browser cache.
- set <name> exit
Terminate Fully.
- set <name> motionDetection { on | off }
Turn motion detection by camera on or off.
- set <name> { lock | unlock }
Lock or unlock display.
- set <name> { on | off }
Turn tablet display on or off.
- set <name> on-for-timer [{ <Seconds> | forever | off }]
Set timer for display. Default is forever.
- set <name> restart
Restart Fully.
- set <name> screenOffTimer <seconds>
Turn screen off after some idle seconds, set to 0 to disable timer.
- set <name> screenSaver { start | stop }
Start or stop screen saver. Screen saver URL can be set with command set screenSaverURL.
- set <name> screenSaverTimer <seconds>
Show screen saver URL after some idle seconds, set to 0 to disable timer.
- set <name> screenSaverURL <URL>
Show this URL when screensaver starts, set daydream: for Android daydream or dim: for black.
- set <name> speak <text>
Audio output of text. If text contains blanks it must be enclosed
in double quotes. The text can contain device readings in format [device:reading].
- set <name> startURL <URL>
Show this URL when FULLY starts.
- set <name> url [<URL>]
Navigate to URL. If no URL is specified navigate to start URL.
Get
- get <name> info
Display Fully information.
- get <name> stats
Show Fully statistics.
- get <name> update
Update readings.
Attributes
- disable <0 | 1>
Disable device and automatic polling.
- pingBeforeCmd <Count>
Send Count ping request to tablet before executing commands. Valid values
for Count are 0,1,2. Default is 0 (do not send ping request).
- pollInterval <seconds>
Set polling interval for FULLY device information.
If seconds is 0 polling is turned off. Valid values are from 10 to
86400 seconds.
- requestTimeout <seconds>
Set timeout for http requests. Default is 4 seconds.
- repeatCommand <Count>
Repeat fully command on failure. Valid values for Count are 0,1,2. Default
is 0 (do not repeat commands).
=end html
=cut
|