70_BOTVAC.pm: Optimize error message handling; Renew accessToken if necessary; PBP issues

git-svn-id: https://svn.fhem.de/fhem/trunk@21819 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
vuffiraa
2020-04-30 17:29:42 +00:00
parent 8b36aa5318
commit b10f9901f0
2 changed files with 203 additions and 146 deletions

View File

@@ -1,5 +1,8 @@
# Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # 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. # Do not insert empty lines here, update check depends on it.
- change: 70_BOTVAC: Optimze error message handling
Renew accessToken if necessary
PBP issues
- change: 57_Calendar: reactivated random delay for calendar updates on start - change: 57_Calendar: reactivated random delay for calendar updates on start
- bugfix 98_todoist: Forum: #1048705 - bugfix 98_todoist: Forum: #1048705
- bugfix: 74_GardenaSmartDevice: setter from mower not visible - bugfix: 74_GardenaSmartDevice: setter from mower not visible

View File

@@ -24,32 +24,7 @@
# #
############################################################################## ##############################################################################
package main; package FHEM::BOTVAC;
use strict;
use warnings;
sub BOTVAC_Initialize($) {
my ($hash) = @_;
our $readingFnAttributes;
$hash->{DefFn} = "BOTVAC::Define";
$hash->{GetFn} = "BOTVAC::Get";
$hash->{SetFn} = "BOTVAC::Set";
$hash->{UndefFn} = "BOTVAC::Undefine";
$hash->{DeleteFn} = "BOTVAC::Delete";
$hash->{ReadFn} = "BOTVAC::wsRead";
$hash->{ReadyFn} = "BOTVAC::wsReady";
$hash->{AttrFn} = "BOTVAC::Attr";
$hash->{AttrList} = "disable:0,1 " .
"actionInterval " .
"boundaries:textField-long " .
"sslVerify:0,1 " .
$readingFnAttributes;
}
package BOTVAC;
use strict; use strict;
use warnings; use warnings;
@@ -58,40 +33,55 @@ use POSIX;
use GPUtils qw(:all); # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt use GPUtils qw(:all); # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt
use Time::HiRes qw(gettimeofday); use Time::HiRes qw(gettimeofday);
use Time::Local qw(timelocal);
use JSON qw(decode_json encode_json); use JSON qw(decode_json encode_json);
use Digest::SHA qw(hmac_sha256_hex sha1_hex); use Digest::SHA qw(hmac_sha256_hex sha1_hex);
use Encode qw(encode_utf8); use Encode qw(encode_utf8);
use MIME::Base64; use MIME::Base64;
require "DevIo.pm"; require DevIo;
require "HttpUtils.pm"; require HttpUtils;
## Import der FHEM Funktionen ## Import der FHEM Funktionen
BEGIN { BEGIN {
GP_Import(qw( GP_Import(qw(
AttrVal AttrVal
CommandAttr
createUniqueId createUniqueId
fhemTzOffset
FmtDateTime FmtDateTime
FmtDateTimeRFC1123 FmtDateTimeRFC1123
fhemTzOffset
getKeyValue getKeyValue
setKeyValue
getUniqueId getUniqueId
InternalTimer InternalTimer
InternalVal InternalVal
readingsSingleUpdate Log3
readingFnAttributes
readingsBeginUpdate
readingsBulkUpdate readingsBulkUpdate
readingsBulkUpdateIfChanged readingsBulkUpdateIfChanged
readingsBeginUpdate
readingsDelete readingsDelete
readingsEndUpdate readingsEndUpdate
ReadingsNum ReadingsNum
readingsSingleUpdate
ReadingsVal ReadingsVal
RemoveInternalTimer RemoveInternalTimer
Log3 setKeyValue
trim trim
)) ));
}; }
GP_Export(
qw(
Initialize
)
);
my $useDigestMD5 = 0;
if ( eval { require Digest::MD5; 1 } ) {
$useDigestMD5 = 1;
Digest::MD5->import();
}
my %opcode = ( # Opcode interpretation of the ws "Payload data my %opcode = ( # Opcode interpretation of the ws "Payload data
'continuation' => 0x00, 'continuation' => 0x00,
@@ -103,7 +93,28 @@ my %opcode = ( # Opcode interpretation of the ws "Payload data
); );
################################### ###################################
sub Define($$) { sub Initialize {
my ($hash) = @_;
$hash->{DefFn} = \&Define;
$hash->{GetFn} = \&Get;
$hash->{SetFn} = \&Set;
$hash->{UndefFn} = \&Undefine;
$hash->{DeleteFn} = \&Delete;
$hash->{ReadFn} = \&wsRead;
$hash->{ReadyFn} = \&wsReady;
$hash->{AttrFn} = \&Attr;
$hash->{AttrList} = "disable:0,1 " .
"actionInterval " .
"boundaries:textField-long " .
"sslVerify:0,1 " .
$readingFnAttributes;
return;
}
###################################
sub Define {
my ( $hash, $def ) = @_; my ( $hash, $def ) = @_;
my @a = split( "[ \t][ \t]*", $def ); my @a = split( "[ \t][ \t]*", $def );
my $name = $hash->{NAME}; my $name = $hash->{NAME};
@@ -147,22 +158,20 @@ sub Define($$) {
$hash->{VENDOR} = $vendor; $hash->{VENDOR} = $vendor;
$hash->{INTERVAL} = $interval; $hash->{INTERVAL} = $interval;
unless ( defined( AttrVal( $name, "webCmd", undef ) ) ) { CommandAttr($hash, "$name webCmd startCleaning:stop:sendToBase")
no warnings "once"; if (AttrVal( $name, 'webCmd', 'none' ) eq 'none');
$::attr{$name}{webCmd} = 'startCleaning:stop:sendToBase';
}
# start the status update timer # start the status update timer
RemoveInternalTimer($hash); RemoveInternalTimer($hash);
InternalTimer( gettimeofday() + 2, "BOTVAC::GetStatus", $hash, 1 ); InternalTimer( gettimeofday() + 2, \&GetStatus, $hash, 1 );
AddExtension($name, "BOTVAC::GetMap", "BOTVAC/$name/map"); AddExtension($name, \&GetMap, "BOTVAC/$name/map");
return; return;
} }
##################################### #####################################
sub GetStatus($;$) { sub GetStatus {
my ( $hash, $update ) = @_; my ( $hash, $update ) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $interval = $hash->{INTERVAL}; my $interval = $hash->{INTERVAL};
@@ -171,10 +180,10 @@ sub GetStatus($;$) {
Log3($name, 5, "BOTVAC $name: called function GetStatus()"); Log3($name, 5, "BOTVAC $name: called function GetStatus()");
# use actionInterval if state is busy or paused # use actionInterval if state is busy or paused
$interval = AttrVal($name, "actionInterval", $interval) if (ReadingsVal($name, "stateId", "0") =~ /2|3/); $interval = AttrVal($name, "actionInterval", $interval) if (ReadingsVal($name, "stateId", "0") =~ /2|3/x);
RemoveInternalTimer($hash); RemoveInternalTimer($hash);
InternalTimer( gettimeofday() + $interval, "BOTVAC::GetStatus", $hash, 0 ); InternalTimer( gettimeofday() + $interval, \&GetStatus, $hash, 0 );
return if ( AttrVal($name, "disable", 0) == 1 or ReadingsVal($name,"pollingMode",1) == 0); return if ( AttrVal($name, "disable", 0) == 1 or ReadingsVal($name,"pollingMode",1) == 0);
@@ -187,8 +196,8 @@ sub GetStatus($;$) {
push(@successor, ["dashboard", undef]) if ($secs <= $interval); push(@successor, ["dashboard", undef]) if ($secs <= $interval);
push(@successor, ["messages", "getSchedule"]); push(@successor, ["messages", "getSchedule"]);
push(@successor, ["messages", "getGeneralInfo"]) if (GetServiceVersion($hash, "generalInfo") =~ /.*-1/); push(@successor, ["messages", "getGeneralInfo"]) if (GetServiceVersion($hash, "generalInfo") =~ /.*-1/x);
push(@successor, ["messages", "getPreferences"]) if (GetServiceVersion($hash, "preferences") ne ""); push(@successor, ["messages", "getPreferences"]) if (GetServiceVersion($hash, "preferences") ne '');
SendCommand($hash, "messages", "getRobotState", undef, @successor); SendCommand($hash, "messages", "getRobotState", undef, @successor);
} }
@@ -201,7 +210,7 @@ sub GetStatus($;$) {
} }
################################### ###################################
sub Get($@) { sub Get {
my ( $hash, @a ) = @_; my ( $hash, @a ) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $what; my $what;
@@ -230,7 +239,7 @@ sub Get($@) {
} }
################################### ###################################
sub Set($@) { sub Set {
my ( $hash, @a ) = @_; my ( $hash, @a ) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
@@ -315,7 +324,7 @@ sub Set($@) {
my @names; my @names;
for (my $i = 0; $i < @Boundaries; $i++) { for (my $i = 0; $i < @Boundaries; $i++) {
my $name = $Boundaries[$i]->{name}; my $name = $Boundaries[$i]->{name};
push @names,$name if (!(grep { $_ eq $name } @names) and ($name ne "")); push @names,$name if (!(grep { $_ eq $name } @names) && ($name ne ""));
} }
my $BoundariesList = @names ? "multiple-strict,".join(",", @names) : "textField"; my $BoundariesList = @names ? "multiple-strict,".join(",", @names) : "textField";
$usage .= " setBoundariesOnFloorplan_0:".$BoundariesList if (ReadingsVal($name, "floorplan_0_id" ,"") ne ""); $usage .= " setBoundariesOnFloorplan_0:".$BoundariesList if (ReadingsVal($name, "floorplan_0_id" ,"") ne "");
@@ -574,7 +583,7 @@ sub Set($@) {
} }
################################### ###################################
sub Undefine($$) { sub Undefine {
my ( $hash, $arg ) = @_; my ( $hash, $arg ) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
@@ -589,7 +598,7 @@ sub Undefine($$) {
} }
################################### ###################################
sub Delete($$) { sub Delete {
my ( $hash, $arg ) = @_; my ( $hash, $arg ) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
@@ -602,7 +611,7 @@ sub Delete($$) {
} }
################################### ###################################
sub Attr(@) sub Attr
{ {
my ($cmd,$name,$attr_name,$attr_value) = @_; my ($cmd,$name,$attr_name,$attr_value) = @_;
my $hash = $::defs{$name}; my $hash = $::defs{$name};
@@ -637,7 +646,7 @@ sub Attr(@)
############################################################################################################ ############################################################################################################
######################### #########################
sub AddExtension($$$) { sub AddExtension {
my ( $name, $func, $link ) = @_; my ( $name, $func, $link ) = @_;
my $url = "/$link"; my $url = "/$link";
@@ -645,20 +654,24 @@ sub AddExtension($$$) {
$::data{FWEXT}{$url}{deviceName} = $name; $::data{FWEXT}{$url}{deviceName} = $name;
$::data{FWEXT}{$url}{FUNC} = $func; $::data{FWEXT}{$url}{FUNC} = $func;
$::data{FWEXT}{$url}{LINK} = $link; $::data{FWEXT}{$url}{LINK} = $link;
return;
} }
######################### #########################
sub RemoveExtension($) { sub RemoveExtension {
my ($link) = @_; my ($link) = @_;
my $url = "/$link"; my $url = "/$link";
my $name = $::data{FWEXT}{$url}{deviceName}; my $name = $::data{FWEXT}{$url}{deviceName};
Log3($name, 2, "Unregistering BOTVAC $name for URL $url..."); Log3($name, 2, "Unregistering BOTVAC $name for URL $url...");
delete $::data{FWEXT}{$url}; delete $::data{FWEXT}{$url};
return;
} }
################################### ###################################
sub SendCommand($$;$$@) { sub SendCommand {
my ( $hash, $service, $cmd, $option, @successor ) = @_; my ( $hash, $service, $cmd, $option, @successor ) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $email = $hash->{EMAIL}; my $email = $hash->{EMAIL};
@@ -694,11 +707,11 @@ sub SendCommand($$;$$@) {
my $sslVerify= AttrVal($name, "sslVerify", undef); my $sslVerify= AttrVal($name, "sslVerify", undef);
if(defined($sslVerify)) { if(defined($sslVerify)) {
eval "use IO::Socket::SSL"; eval {use IO::Socket::SSL};
if($@) { if($@) {
Log3($name, 2, $@); Log3($name, 2, $@);
} else { } else {
my $sslVerifyMode= eval("$sslVerify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE"); my $sslVerifyMode= eval {$sslVerify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE};
Log3($name, 5, "SSL verify mode set to $sslVerifyMode"); Log3($name, 5, "SSL verify mode set to $sslVerifyMode");
$sslArgs{SSL_verify_mode} = $sslVerifyMode; $sslArgs{SSL_verify_mode} = $sslVerifyMode;
} }
@@ -835,7 +848,7 @@ sub SendCommand($$;$$@) {
if ( defined($data) ); if ( defined($data) );
Log3($name, 5, "BOTVAC $name: GET $URL") Log3($name, 5, "BOTVAC $name: GET $URL")
if ( !defined($data) ); if ( !defined($data) );
Log3($name, 5, "BOTVAC $name: header ".join("\n", map(($_.': '.$header{$_}), keys %header))) Log3($name, 5, "BOTVAC $name: header ".join("\n", map {$_.': '.$header{$_}} keys %header))
if ( %header ); if ( %header );
my $params = { my $params = {
@@ -865,7 +878,7 @@ sub SendCommand($$;$$@) {
} }
################################### ###################################
sub ReceiveCommand($$$) { sub ReceiveCommand {
my ( $param, $err, $data ) = @_; my ( $param, $err, $data ) = @_;
my $hash = $param->{hash}; my $hash = $param->{hash};
my $name = $hash->{NAME}; my $name = $hash->{NAME};
@@ -905,6 +918,9 @@ sub ReceiveCommand($$$) {
# stop pulling for current interval # stop pulling for current interval
Log3($name, 4, "BOTVAC $name: drop successors"); Log3($name, 4, "BOTVAC $name: drop successors");
LogSuccessors($hash, @successor); LogSuccessors($hash, @successor);
readingsEndUpdate( $hash, 1 );
return; return;
} }
@@ -921,11 +937,30 @@ sub ReceiveCommand($$$) {
if ( $data ne "" ) { if ( $data ne "" ) {
if ( $service eq "loadmap" ) { if ( $service eq "loadmap" ) {
# use $data later # use $data later
} elsif ( $data =~ /^{"message":"Could not find robot_serial for specified vendor_name"}$/ ) { } elsif ( $data eq '{"message":"Could not find robot_serial for specified vendor_name"}' ) {
# currently no data available # currently no data available
readingsBulkUpdateIfChanged($hash, "state", "Couldn't find robot"); readingsBulkUpdateIfChanged($hash, "state", "Couldn't find robot");
readingsEndUpdate( $hash, 1 ); readingsEndUpdate( $hash, 1 );
return; return;
} elsif ( $data eq '{"message":"Bad credentials"}' ||
$data eq '{"message":"Not allowed"}' ) {
if ( !defined($cmd) || $cmd eq "" ) {
Log3($name, 3, "BOTVAC $name: RES $service - $data");
} else {
Log3($name, 3, "BOTVAC $name: RES $service/$cmd - $data");
}
# remove invalid access token
readingsDelete($hash, ".accessToken");
readingsEndUpdate( $hash, 1 );
if ( $service ne "sessions") {
# put last command back into queue
unshift(@successor, [$service, $cmd]);
# send registration
SendCommand($hash, "sessions", undef, undef, @successor);
}
return;
} elsif ( $data =~ /^{/ || $data =~ /^\[/ ) { } elsif ( $data =~ /^{/ || $data =~ /^\[/ ) {
if ( !defined($cmd) || $cmd eq "" ) { if ( !defined($cmd) || $cmd eq "" ) {
Log3($name, 4, "BOTVAC $name: RES $service - $data"); Log3($name, 4, "BOTVAC $name: RES $service - $data");
@@ -940,7 +975,8 @@ sub ReceiveCommand($$$) {
} else { } else {
Log3($name, 5, "BOTVAC $name: RES ERROR $service/$cmd\n$data"); Log3($name, 5, "BOTVAC $name: RES ERROR $service/$cmd\n$data");
} }
return undef; readingsEndUpdate( $hash, 1 );
return;
} }
} }
@@ -1294,7 +1330,7 @@ sub ReceiveCommand($$$) {
readingsEndUpdate( $hash, 1 ); readingsEndUpdate( $hash, 1 );
if (defined($hash->{helper}{".HTTP_CONNECTION"}) and if (defined($hash->{helper}{".HTTP_CONNECTION"}) and
(($keepalive and $closeConnection) or !@successor)) { (($keepalive && $closeConnection) || !@successor)) {
Log3($name, 4, "BOTVAC $name: Close connection"); Log3($name, 4, "BOTVAC $name: Close connection");
::HttpUtils_Close($hash->{helper}{".HTTP_CONNECTION"}); ::HttpUtils_Close($hash->{helper}{".HTTP_CONNECTION"});
undef($hash->{helper}{".HTTP_CONNECTION"}); undef($hash->{helper}{".HTTP_CONNECTION"});
@@ -1330,31 +1366,29 @@ sub ReceiveCommand($$$) {
return; return;
} }
sub GetTimeFromString($) { sub GetTimeFromString {
my ($timeStr) = @_; my ($timeStr) = @_;
eval { if(defined($timeStr) and $timeStr =~ m/^(\d{4})-(\d{2})-(\d{2})T([0-2]\d):([0-5]\d):([0-5]\d)Z$/x) {
use Time::Local;
if(defined($timeStr) and $timeStr =~ m/^(\d{4})-(\d{2})-(\d{2})T([0-2]\d):([0-5]\d):([0-5]\d)Z$/) {
my $time = timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900); my $time = timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900);
return FmtDateTime($time + fhemTzOffset($time)); return FmtDateTime($time + fhemTzOffset($time));
} }
}
return;
} }
sub GetSecondsFromString($) { sub GetSecondsFromString {
my ($timeStr) = @_; my ($timeStr) = @_;
eval { if(defined($timeStr) and $timeStr =~ m/^(\d{4})-(\d{2})-(\d{2})T([0-2]\d):([0-5]\d):([0-5]\d)Z$/x) {
use Time::Local;
if(defined($timeStr) and $timeStr =~ m/^(\d{4})-(\d{2})-(\d{2})T([0-2]\d):([0-5]\d):([0-5]\d)Z$/) {
my $time = timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900); my $time = timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900);
return $time; return $time;
} }
}
return;
} }
sub SetRobot($$) { sub SetRobot {
my ( $hash, $robot ) = @_; my ( $hash, $robot ) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
@@ -1370,9 +1404,11 @@ sub SetRobot($$) {
readingsBulkUpdateIfChanged($hash, "macAddr", $robots[$robot]->{macAddr}); readingsBulkUpdateIfChanged($hash, "macAddr", $robots[$robot]->{macAddr});
readingsBulkUpdateIfChanged($hash, "nucleoUrl", $robots[$robot]->{nucleoUrl}); readingsBulkUpdateIfChanged($hash, "nucleoUrl", $robots[$robot]->{nucleoUrl});
readingsBulkUpdateIfChanged($hash, "robot", $robot); readingsBulkUpdateIfChanged($hash, "robot", $robot);
return;
} }
sub GetCleaningParameter($$$) { sub GetCleaningParameter {
my ($hash, $param, $default) = @_; my ($hash, $param, $default) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
@@ -1380,7 +1416,7 @@ sub GetCleaningParameter($$$) {
return ReadingsVal($name, $nextReading, ReadingsVal($name, $param, $default)); return ReadingsVal($name, $nextReading, ReadingsVal($name, $param, $default));
} }
sub GetServiceVersion($$) { sub GetServiceVersion {
my ($hash, $service) = @_; my ($hash, $service) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
@@ -1396,16 +1432,18 @@ sub SetServices {
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $serviceList = join(", ", map { "$_:$services->{$_}" } keys %$services); my $serviceList = join(", ", map { "$_:$services->{$_}" } keys %$services);
$hash->{SERVICES} = $serviceList if (!defined($hash->{SERVICES}) or $hash->{SERVICES} ne $serviceList); $hash->{SERVICES} = $serviceList if (!defined($hash->{SERVICES}) || $hash->{SERVICES} ne $serviceList);
return;
} }
sub StorePassword($$) { sub StorePassword {
my ($hash, $password) = @_; my ($hash, $password) = @_;
my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd";
my $key = getUniqueId().$index; my $key = getUniqueId().$index;
my $enc_pwd = ""; my $enc_pwd = "";
if(eval "use Digest::MD5;1") { if($useDigestMD5) {
$key = Digest::MD5::md5_hex(unpack "H*", $key); $key = Digest::MD5::md5_hex(unpack "H*", $key);
$key .= Digest::MD5::md5_hex($key); $key .= Digest::MD5::md5_hex($key);
} }
@@ -1422,7 +1460,7 @@ sub StorePassword($$) {
return "password successfully saved"; return "password successfully saved";
} }
sub ReadPassword($) { sub ReadPassword {
my ($hash) = @_; my ($hash) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd";
@@ -1435,16 +1473,16 @@ sub ReadPassword($) {
if ( defined($err) ) { if ( defined($err) ) {
Log3($name, 3, "BOTVAC $name: unable to read password from file: $err"); Log3($name, 3, "BOTVAC $name: unable to read password from file: $err");
return undef; return;
} }
if ( defined($password) ) { if ( defined($password) ) {
if ( eval "use Digest::MD5;1" ) { if($useDigestMD5) {
$key = Digest::MD5::md5_hex(unpack "H*", $key); $key = Digest::MD5::md5_hex(unpack "H*", $key);
$key .= Digest::MD5::md5_hex($key); $key .= Digest::MD5::md5_hex($key);
} }
my $dec_pwd = ''; my $dec_pwd = '';
for my $char (map { pack('C', hex($_)) } ($password =~ /(..)/g)) { for my $char (map { pack('C', hex($_)) } ($password =~ /(..)/gx)) {
my $decode=chop($key); my $decode=chop($key);
$dec_pwd.=chr(ord($char)^ord($decode)); $dec_pwd.=chr(ord($char)^ord($decode));
$key=$decode.$key; $key=$decode.$key;
@@ -1452,11 +1490,11 @@ sub ReadPassword($) {
return $dec_pwd; return $dec_pwd;
} else { } else {
Log3($name, 3, "BOTVAC $name: No password in file"); Log3($name, 3, "BOTVAC $name: No password in file");
return undef; return;
} }
} }
sub CheckRegistration($$$$$) { sub CheckRegistration {
my ( $hash, $service, $cmd, $option, @successor ) = @_; my ( $hash, $service, $cmd, $option, @successor ) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
@@ -1482,7 +1520,7 @@ sub CheckRegistration($$$$$) {
return; return;
} }
sub GetBoolean($) { sub GetBoolean {
my ($value) = @_; my ($value) = @_;
my $booleans = { my $booleans = {
'0' => "0", '0' => "0",
@@ -1498,7 +1536,7 @@ sub GetBoolean($) {
} }
} }
sub SetBoolean($) { sub SetBoolean {
my ($value) = @_; my ($value) = @_;
my $booleans = { my $booleans = {
'0' => "false", '0' => "false",
@@ -1514,7 +1552,7 @@ sub SetBoolean($) {
} }
} }
sub BuildState($$$$) { sub BuildState {
my ($hash,$state,$action,$error) = @_; my ($hash,$state,$action,$error) = @_;
my $states = { my $states = {
'0' => "Invalid", '0' => "Invalid",
@@ -1539,7 +1577,7 @@ sub BuildState($$$$) {
} }
} }
sub GetActionText($) { sub GetActionText {
my ($action) = @_; my ($action) = @_;
my $actions = { my $actions = {
'0' => "Invalid", '0' => "Invalid",
@@ -1567,7 +1605,7 @@ sub GetActionText($) {
} }
} }
sub GetErrorText($) { sub GetErrorText {
my ($error) = @_; my ($error) = @_;
my $errors = { my $errors = {
'ui_alert_invalid' => 'Ok', 'ui_alert_invalid' => 'Ok',
@@ -1589,7 +1627,7 @@ sub GetErrorText($) {
} }
} }
sub GetDayText($) { sub GetDayText {
my ($day) = @_; my ($day) = @_;
my $days = { my $days = {
'0' => "Sunday", '0' => "Sunday",
@@ -1608,7 +1646,7 @@ sub GetDayText($) {
} }
} }
sub GetCategoryText($) { sub GetCategoryText {
my ($category) = @_; my ($category) = @_;
my $categories = { my $categories = {
'1' => 'manual', '1' => 'manual',
@@ -1624,7 +1662,7 @@ sub GetCategoryText($) {
} }
} }
sub GetModeText($) { sub GetModeText {
my ($mode) = @_; my ($mode) = @_;
my $modes = { my $modes = {
'1' => 'eco', '1' => 'eco',
@@ -1638,7 +1676,7 @@ sub GetModeText($) {
} }
} }
sub GetModifierText($) { sub GetModifierText {
my ($modifier) = @_; my ($modifier) = @_;
my $modifiers = { my $modifiers = {
'1' => 'normal', '1' => 'normal',
@@ -1652,7 +1690,7 @@ sub GetModifierText($) {
} }
} }
sub GetNavigationModeText($) { sub GetNavigationModeText {
my ($navMode) = @_; my ($navMode) = @_;
my $navModes = { my $navModes = {
'1' => 'normal', '1' => 'normal',
@@ -1667,7 +1705,7 @@ sub GetNavigationModeText($) {
} }
} }
sub GetAuthStatusText($) { sub GetAuthStatusText {
my ($authStatus) = @_; my ($authStatus) = @_;
my $authStatusHash = { my $authStatusHash = {
'0' => 'not supported', '0' => 'not supported',
@@ -1682,7 +1720,7 @@ sub GetAuthStatusText($) {
} }
} }
sub GetBeehiveHost($) { sub GetBeehiveHost {
my ($vendor) = @_; my ($vendor) = @_;
my $vendors = { my $vendors = {
'neato' => 'beehive.neatocloud.com', 'neato' => 'beehive.neatocloud.com',
@@ -1696,7 +1734,7 @@ sub GetBeehiveHost($) {
} }
} }
sub GetNucleoHost($) { sub GetNucleoHost {
my ($vendor) = @_; my ($vendor) = @_;
my $vendors = { my $vendors = {
'neato' => 'nucleo.neatocloud.com', 'neato' => 'nucleo.neatocloud.com',
@@ -1710,12 +1748,12 @@ sub GetNucleoHost($) {
} }
} }
sub GetValidityEnd($) { sub GetValidityEnd {
my ($validFor) = @_; my ($validFor) = @_;
return ($validFor =~ /\d+/ ? FmtDateTime(time() + $validFor) : $validFor); return ($validFor =~ /\d+/ ? FmtDateTime(time() + $validFor) : $validFor);
} }
sub LogSuccessors($@) { sub LogSuccessors {
my ($hash,@successor) = @_; my ($hash,@successor) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
@@ -1727,9 +1765,11 @@ sub LogSuccessors($@) {
$msg .= join(",", map { defined($_) ? $_ : '' } @succ_item); $msg .= join(",", map { defined($_) ? $_ : '' } @succ_item);
} }
Log3($name, 4, $msg) if (@successor > 0); Log3($name, 4, $msg) if (@successor > 0);
return;
} }
sub ShowMap($;$$) { sub ShowMap {
my ($name,$width,$height) = @_; my ($name,$width,$height) = @_;
my $img = '<img src="/fhem/BOTVAC/'.$name.'/map"'; my $img = '<img src="/fhem/BOTVAC/'.$name.'/map"';
@@ -1755,17 +1795,17 @@ sub GetMap() {
} }
sub ShowStatistics($) { sub ShowStatistics {
my ($name) = @_; my ($name) = @_;
my $hash = $::defs{$name}; my $hash = $::defs{$name};
return "maps for statistics are not available yet" return "maps for statistics are not available yet"
if (!defined($hash->{helper}{MAPS}) or @{$hash->{helper}{MAPS}} == 0); if (!defined($hash->{helper}{MAPS}) || @{$hash->{helper}{MAPS}} == 0);
return GetStatistics($hash); return GetStatistics($hash);
} }
sub GetStatistics($) { sub GetStatistics {
my($hash) = @_; my($hash) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $mapcount = @{$hash->{helper}{MAPS}}; my $mapcount = @{$hash->{helper}{MAPS}};
@@ -1866,7 +1906,7 @@ sub GetStatistics($) {
####################################### #######################################
# Websocket Functions # Websocket Functions
####################################### #######################################
sub wsOpen($$$) { sub wsOpen {
my ($hash,$ip_address,$port) = @_; my ($hash,$ip_address,$port) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
@@ -1875,16 +1915,18 @@ sub wsOpen($$$) {
::DevIo_CloseDev($hash) if(::DevIo_IsOpen($hash)); ::DevIo_CloseDev($hash) if(::DevIo_IsOpen($hash));
if (::DevIo_OpenDev($hash, 0, "BOTVAC::wsHandshake")) { if (::DevIo_OpenDev($hash, 0, \&wsHandshake)) {
Log3($name, 2, "BOTVAC(ws) $name: ERROR: Can't open websocket to $hash->{DeviceName}"); Log3($name, 2, "BOTVAC(ws) $name: ERROR: Can't open websocket to $hash->{DeviceName}");
readingsSingleUpdate($hash,'result','ws_connect_error',1); readingsSingleUpdate($hash,'result','ws_connect_error',1);
readingsSingleUpdate($hash,'result','ws_ko',1); readingsSingleUpdate($hash,'result','ws_ko',1);
} else { } else {
readingsSingleUpdate($hash,'result','ws_ok',1); readingsSingleUpdate($hash,'result','ws_ok',1);
} }
return;
} }
sub wsClose($) { sub wsClose {
my $hash = shift; my $hash = shift;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $normal_closure = pack("H*", "03e8"); #code 1000 my $normal_closure = pack("H*", "03e8"); #code 1000
@@ -1894,10 +1936,12 @@ sub wsClose($) {
wsEncode($hash, $normal_closure, "close"); wsEncode($hash, $normal_closure, "close");
delete $hash->{HELPER}{WEBSOCKETS}; delete $hash->{HELPER}{WEBSOCKETS};
delete $hash->{HELPER}{wsKey}; delete $hash->{HELPER}{wsKey};
readingsSingleUpdate($hash,'state','ws_closed',1) if (::DevIo_CloseDev($hash)) readingsSingleUpdate($hash,'state','ws_closed',1) if (::DevIo_CloseDev($hash));
return;
} }
sub wsHandshake($) { sub wsHandshake {
my $hash = shift; my $hash = shift;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $host = ReadingsVal($name, "wlanIpAddress", ""); my $host = ReadingsVal($name, "wlanIpAddress", "");
@@ -1926,10 +1970,10 @@ sub wsHandshake($) {
$hash->{HELPER}{wsKey} = $wsKey; $hash->{HELPER}{wsKey} = $wsKey;
return undef; return;
} }
sub wsCheckHandshake($$) { sub wsCheckHandshake {
my ($hash,$response) = @_; my ($hash,$response) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
@@ -1959,20 +2003,20 @@ sub wsCheckHandshake($$) {
readingsSingleUpdate($hash,'state','ws_handshake-error',1); readingsSingleUpdate($hash,'state','ws_handshake-error',1);
} }
} }
return undef; return;
} }
sub wsWrite($@) { sub wsWrite {
my ($hash,$string) = @_; my ($hash,$string) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
Log3($name, 4, "BOTVAC(ws) $name: WriteFn called:\n$string"); Log3($name, 4, "BOTVAC(ws) $name: WriteFn called:\n$string");
::DevIo_SimpleWrite($hash, $string, 0); ::DevIo_SimpleWrite($hash, $string, 0);
return undef; return;
} }
sub wsRead($) { sub wsRead {
my $hash = shift; my $hash = shift;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $buf; my $buf;
@@ -1987,13 +2031,15 @@ sub wsRead($) {
wsDecode($hash,$buf); wsDecode($hash,$buf);
} elsif( $buf =~ /HTTP\/1.1 101 Switching Protocols/ ) { } elsif( $buf =~ /HTTP\/1.1 101 Switching Protocols/ ) {
Log3($name, 4, "BOTVAC(ws) $name: received HTTP data string, start response processing:\n$buf"); Log3($name, 4, "BOTVAC(ws) $name: received HTTP data string, start response processing:\n$buf");
BOTVAC::wsCheckHandshake($hash,$buf); wsCheckHandshake($hash,$buf);
} else { } else {
Log3($name, 1, "BOTVAC(ws) $name: corrupted data found:\n$buf"); Log3($name, 1, "BOTVAC(ws) $name: corrupted data found:\n$buf");
} }
return;
} }
sub wsCallback(@) { sub wsCallback {
my ($param, $err, $data) = @_; my ($param, $err, $data) = @_;
my $hash = $param->{hash}; my $hash = $param->{hash};
my $name = $hash->{NAME}; my $name = $hash->{NAME};
@@ -2009,12 +2055,15 @@ sub wsCallback(@) {
} else { } else {
Log3($name, 2, "received callback without Data and Error String!!!"); Log3($name, 2, "received callback without Data and Error String!!!");
} }
return undef; return;
} }
sub wsReady($) { sub wsReady {
my ($hash) = @_; my ($hash) = @_;
return ::DevIo_OpenDev($hash, 1, "BOTVAC::wsHandshake") if ( $hash->{STATE} eq "disconnected" ); if ( $hash->{STATE} eq "disconnected" ) {
return ::DevIo_OpenDev($hash, 1, \&wsHandshake);
}
return;
} }
# 0 1 2 3 # 0 1 2 3
@@ -2036,7 +2085,7 @@ sub wsReady($) {
# | Payload Data continued ... | # | Payload Data continued ... |
# +---------------------------------------------------------------+ # +---------------------------------------------------------------+
# https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 # https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17
sub wsEncode($$;$$) { sub wsEncode {
my ($hash, $payload, $type, $masked) = @_; my ($hash, $payload, $type, $masked) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
$type //= "text"; $type //= "text";
@@ -2072,16 +2121,19 @@ sub wsEncode($$;$$) {
Log3($name, 3, "BOTVAC(ws) $name: String: " . unpack('H*',$wsString)); Log3($name, 3, "BOTVAC(ws) $name: String: " . unpack('H*',$wsString));
wsWrite($hash, $wsString); wsWrite($hash, $wsString);
return;
} }
sub wsPong($) { sub wsPong {
my $hash = shift; my $hash = shift;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
Log3($name, 3, "BOTVAC(ws) $name: wsPong"); Log3($name, 3, "BOTVAC(ws) $name: wsPong");
wsEncode($hash, undef, "pong"); wsEncode($hash, undef, "pong");
return;
} }
sub wsDecode($$) { sub wsDecode {
my ($hash,$wsString) = @_; my ($hash,$wsString) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
@@ -2122,9 +2174,11 @@ sub wsDecode($$) {
wsPong($hash) if ($OPCODE == $opcode{"ping"}); wsPong($hash) if ($OPCODE == $opcode{"ping"});
} }
} }
return;
} }
sub wsMasking($$) { sub wsMasking {
my ($payload, $mask) = @_; my ($payload, $mask) = @_;
$mask = $mask x (int(length($payload) / 4) + 1); $mask = $mask x (int(length($payload) / 4) + 1);
$mask = substr($mask, 0, length($payload)); $mask = substr($mask, 0, length($payload));
@@ -2346,7 +2400,7 @@ sub wsMasking($$) {
Even though an internet connection is necessary as the initialization is triggered by a remote call. Even though an internet connection is necessary as the initialization is triggered by a remote call.
<br> <br>
<em>Note:</em> If the robot does not receive any messages for 30 seconds it will exit Manual Cleaning, <em>Note:</em> If the robot does not receive any messages for 30 seconds it will exit Manual Cleaning,
but it will not close the websocket connection automaticaly. but it will not close the websocket connection automatically.
</li> </li>
<br> <br>
<li> <li>