24_TPLinkHS110: Added patch for viegener for more reliable communication with newer models or firmwares

git-svn-id: https://svn.fhem.de/fhem/trunk@19532 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
vk 2019-06-02 06:38:05 +00:00
parent ead21543e1
commit 1e63f6700d

View File

@ -1,7 +1,7 @@
################################################################ ################################################################
# $Id$ # $Id$
# #
# Release 2018-11-01 SetExtension # Release 2019-05-31 SendCommand2
# #
# Copyright notice # Copyright notice
# #
@ -25,7 +25,7 @@
# In older distribution try "cpan IO::Socket::Timeout" # In older distribution try "cpan IO::Socket::Timeout"
# #
# Origin: # Origin:
# https://github.com/kettenbach-it/FHEM-TPLink-HS110 # https://gitlab.com/volkerkettenbach/FHEM-TPLink-Kasa
# #
################################################################ ################################################################
@ -41,8 +41,7 @@ use Data::Dumper;
##################################### #####################################
sub TPLinkHS110_Initialize($) sub TPLinkHS110_Initialize($) {
{
my ($hash) = @_; my ($hash) = @_;
$hash->{DefFn} = "TPLinkHS110_Define"; $hash->{DefFn} = "TPLinkHS110_Define";
@ -51,7 +50,7 @@ sub TPLinkHS110_Initialize($)
$hash->{UndefFn} = "TPLinkHS110_Undefine"; $hash->{UndefFn} = "TPLinkHS110_Undefine";
$hash->{DeleteFn} = "TPLinkHS110_Delete"; $hash->{DeleteFn} = "TPLinkHS110_Delete";
$hash->{AttrFn} = "TPLinkHS110_Attr"; $hash->{AttrFn} = "TPLinkHS110_Attr";
$hash->{AttrList} = "interval ". $hash->{AttrList} = "interval " .
"disable:0,1 " . "disable:0,1 " .
"nightmode:on,off " . "nightmode:on,off " .
"timeout " . "timeout " .
@ -59,64 +58,135 @@ sub TPLinkHS110_Initialize($)
} }
##################################### #####################################
sub TPLinkHS110_Define($$) sub TPLinkHS110_Define($$) {
{
my ($hash, $def) = @_; my ($hash, $def) = @_;
my $name= $hash->{NAME}; my $name = $hash->{NAME};
my @a = split( "[ \t][ \t]*", $def ); my @a = split("[ \t][ \t]*", $def);
return "Wrong syntax: use define <name> TPLinkHS110 <hostname/ip> " if (int(@a) != 3); return "Wrong syntax: use define <name> TPLinkHS110 <hostname/ip> " if (int(@a) != 3);
$hash->{INTERVAL}=300; $hash->{INTERVAL} = 300;
$hash->{TIMEOUT}=1; $hash->{TIMEOUT} = 1;
$hash->{HOST}=$a[2]; $hash->{HOST} = $a[2];
$attr{$name}{"disable"} = 0; $attr{$name}{"disable"} = 0;
# initial request after 2 secs, there timer is set to interval for further update # initial request after 2 secs, there timer is set to interval for further update
InternalTimer(gettimeofday()+2, "TPLinkHS110_Get", $hash, 0); InternalTimer(gettimeofday() + 2, "TPLinkHS110_Get", $hash, 0);
Log3 $hash, 3, "TPLinkHS110: $name defined."; Log3 $hash, 3, "TPLinkHS110: $name defined.";
return undef; return undef;
} }
##################################### #####################################
sub TPLinkHS110_Get($$) # sends given command and returns ($errmsg/undef,undef/$decrypteddata)
{ sub TPLinkHS110_SendCommand($$) {
my ($hash) = @_; my ($hash, $command) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my($success,$json,$realtimejson);
return "Device disabled in config" if ($attr{$name}{"disable"} eq "1");
RemoveInternalTimer($hash);
InternalTimer(gettimeofday()+$hash->{INTERVAL}, "TPLinkHS110_Get", $hash, 1);
$hash->{NEXTUPDATE}=localtime(gettimeofday()+$hash->{INTERVAL});
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
$mon++;
$year += 1900;
my $remote_host = $hash->{HOST}; my $remote_host = $hash->{HOST};
my $remote_port = 9999; my $remote_port = 9999;
my $command = '{"system":{"get_sysinfo":{}}}';
my $c = encrypt($command); my $c = encrypt($command);
my $socket = IO::Socket::INET->new(PeerAddr => $remote_host, my $socket = IO::Socket::INET->new(PeerAddr => $remote_host,
PeerPort => $remote_port, PeerPort => $remote_port,
Proto => 'tcp', Proto => 'tcp',
Type => SOCK_STREAM, Type => SOCK_STREAM,
Timeout => $hash->{TIMEOUT} ) Timeout => $hash->{TIMEOUT})
or return "Couldn't connect to $remote_host:$remote_port: $@\n"; or return("Couldn't connect to $remote_host:$remote_port: $@\n", undef);
$socket->write($c); $socket->write($c);
IO::Socket::Timeout->enable_timeouts_on($socket); IO::Socket::Timeout->enable_timeouts_on($socket);
$socket->read_timeout(.5); $socket->read_timeout(2.5);
my $dlen;
my $res;
my $errmsg;
my $data; my $data;
$data = <$socket>;
$res = sysread($socket, $dlen, 4);
$dlen = "" if (!defined($res));
if ($res != 4) {
$errmsg = "Could not read 4 length bytes";
}
my $datalen = 0;
if (!defined($errmsg)) {
for (my $i = 0; $i < 4; $i++) {
$datalen *= 256;
$datalen += ord(substr($dlen, $i, 1));
}
Log3 $hash, 4, "TPLinkHS110: $name Get length - " . $datalen; # JV
my $datapart;
$data = "";
my $partlen = 0;
my $remainlen = $datalen;
my $ctr = 0;
while (($remainlen > 0) && (!defined($errmsg))) {
$res = sysread($socket, $datapart, $remainlen);
if (!defined($res) || $res < 0) {
$errmsg = "Data reading failed - received errcode: " . $res;
}
elsif ($res == 0) {
$ctr++;
$errmsg = "Could not read correct length - expected: " . $datalen . " received: " . $partlen if ($ctr > 2);
}
else {
$ctr = 0;
$data .= $datapart;
$remainlen -= $res;
}
}
Log3 $hash, 4, "TPLinkHS110: $name Get read data length - " . length($data); # JV
}
$socket->close(); $socket->close();
readingsBeginUpdate($hash); if (!defined($errmsg)) {
$data = decrypt(substr($data,4)); $data = decrypt($data);
return(undef, $data);
}
else {
return($errmsg, undef);
}
($success,$json) = TPLinkHS110__evaljson($name,$data); }
if(!$success) {
#####################################
sub TPLinkHS110_Get($$) {
my ($hash) = @_;
my $name = $hash->{NAME};
my ($success, $json, $realtimejson);
return "Device disabled in config" if ($attr{$name}{"disable"} eq "1");
RemoveInternalTimer($hash);
InternalTimer(gettimeofday() + $hash->{INTERVAL}, "TPLinkHS110_Get", $hash, 1);
$hash->{NEXTUPDATE} = localtime(gettimeofday() + $hash->{INTERVAL});
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
$mon++;
$year += 1900;
my $errmsg;
my $data;
my $command = '{"system":{"get_sysinfo":{}}}';
($errmsg, $data) = TPLinkHS110_SendCommand($hash, $command);
if (defined($errmsg)) {
Log3 $hash, 1, "TPLinkHS110: $name Get failed - " . $errmsg; # JV
return;
}
readingsBeginUpdate($hash);
($success, $json) = TPLinkHS110__evaljson($name, $data);
if (!$success) {
Log3 $hash, 1, "TPLinkHS110: $name Get failed"; # JV
readingsEndUpdate($hash, 1); readingsEndUpdate($hash, 1);
return; return;
} }
@ -145,32 +215,30 @@ sub TPLinkHS110_Get($$)
readingsBulkUpdate($hash, "state", "on"); readingsBulkUpdate($hash, "state", "on");
} }
# If the device is a HS110, get realtime data: # If the device is a HS110, get realtime data:
# if ( 1 == 0 ) {
if ($json->{'system'}->{'get_sysinfo'}->{'model'} eq "HS110(EU)" or $json->{'system'}->{'get_sysinfo'}->{'model'} eq "HS110(UK)") { if ($json->{'system'}->{'get_sysinfo'}->{'model'} eq "HS110(EU)" or $json->{'system'}->{'get_sysinfo'}->{'model'} eq "HS110(UK)") {
my $realtimejcommand='{"emeter":{"get_realtime":{}}}'; my $realtimejcommand = '{"emeter":{"get_realtime":{}}}';
my $rc = encrypt($realtimejcommand);
my $socket = IO::Socket::INET->new(PeerAddr => $remote_host,
PeerPort => $remote_port,
Proto => 'tcp',
Type => SOCK_STREAM,
Timeout => $hash->{TIMEOUT} )
or return "Couldn't connect to $remote_host:$remote_port: $@\n";
$socket->write($rc);
IO::Socket::Timeout->enable_timeouts_on($socket);
$socket->read_timeout(.5);
my $rdata; my $rdata;
$rdata = <$socket>; ($errmsg, $rdata) = TPLinkHS110_SendCommand($hash, $realtimejcommand);
$rdata = decrypt(substr($rdata,4)); if (defined($errmsg)) {
Log3 $hash, 1, "TPLinkHS110: $name Get realtime data failed - " . $errmsg; # JV
if (length($rdata)==0) { readingsEndUpdate($hash, 1);
Log3 $hash, 1, "TPLinkHS110: $name: Received zero bytes of realtime data. Cannot process realtime data";
return; return;
} }
($success,$realtimejson) = TPLinkHS110__evaljson($name,$rdata); if (length($rdata) == 0) {
if(!$success) { Log3 $hash, 1, "TPLinkHS110: $name: Received zero bytes of realtime data. Cannot process realtime data";
readingsEndUpdate($hash, 1); readingsEndUpdate($hash, 1);
return; return;
} else { }
($success, $realtimejson) = TPLinkHS110__evaljson($name, $rdata);
if (!$success) {
Log3 $hash, 1, "TPLinkHS110: $name: Received zero bytes of realtime data. Cannot process realtime data";
readingsEndUpdate($hash, 1);
return;
}
else {
Log3 $hash, 2, "TPLinkHS110: $name Realtime data updated"; Log3 $hash, 2, "TPLinkHS110: $name Realtime data updated";
} }
@ -188,7 +256,8 @@ sub TPLinkHS110_Get($$)
$key2 = $hwMap{$hw_ver}{'emeter'}{'get_realtime'}{$key2}{'name'}; $key2 = $hwMap{$hw_ver}{'emeter'}{'get_realtime'}{$key2}{'name'};
readingsBulkUpdate($hash, $key2, $emeterValue); readingsBulkUpdate($hash, $key2, $emeterValue);
$emeterReadings{$key2} = $emeterValue; $emeterReadings{$key2} = $emeterValue;
} else { }
else {
return "Check supported hw_ver of device: $hw_ver\n"; return "Check supported hw_ver of device: $hw_ver\n";
} }
} }
@ -197,50 +266,45 @@ sub TPLinkHS110_Get($$)
# Get Daily Stats # Get Daily Stats
my $command = '{"emeter":{"get_daystat":{"month":'.$mon.',"year":'.$year.'}}}'; $command = '{"emeter":{"get_daystat":{"month":' . $mon . ',"year":' . $year . '}}}';
my $c = encrypt($command); ($errmsg, $data) = TPLinkHS110_SendCommand($hash, $command);
$socket = IO::Socket::INET->new(PeerAddr => $remote_host, if (defined($errmsg)) {
PeerPort => $remote_port, Log3 $hash, 1, "TPLinkHS110: $name Get daily stats failed - " . $errmsg; # JV
Proto => 'tcp', readingsEndUpdate($hash, 1);
Type => SOCK_STREAM, return;
Timeout => $hash->{TIMEOUT} ) }
or return "Couldn't connect to $remote_host:$remote_port: $@\n";
$socket->write($c);
IO::Socket::Timeout->enable_timeouts_on($socket);
$socket->read_timeout(.5);
my $data;
$data = <$socket>;
$socket->close();
$data = decrypt(substr($data,4));
Log3 $hash, 3, "TPLinkHS110: $name Updating daystat. Data: " . $data; Log3 $hash, 3, "TPLinkHS110: $name Updating daystat. Data: " . $data;
($success,$json) = TPLinkHS110__evaljson($name,$data); ($success, $json) = TPLinkHS110__evaljson($name, $data);
if($success && $json) { if ($success && $json) {
my $total=0;
my $total = 0;
foreach my $key (sort keys @{$json->{'emeter'}->{'get_daystat'}->{'day_list'}}) { foreach my $key (sort keys @{$json->{'emeter'}->{'get_daystat'}->{'day_list'}}) {
foreach my $key2 ($json->{'emeter'}->{'get_daystat'}->{'day_list'}[$key]) { foreach my $key2 ($json->{'emeter'}->{'get_daystat'}->{'day_list'}[$key]) {
if ($hw_ver eq "1.0") { if ($hw_ver eq "1.0") {
$total = $total+ $key2->{'energy'}; $total = $total + $key2->{'energy'};
if ($key2->{'day'} == $mday) { if ($key2->{'day'} == $mday) {
readingsBulkUpdate($hash, "daily_total", sprintf("%.3f", $key2->{'energy'})); readingsBulkUpdate($hash, "daily_total", sprintf("%.3f", $key2->{'energy'}));
} }
} else { }
$total = $total+ $key2->{'energy_wh'}; else {
$total = $total + $key2->{'energy_wh'};
if ($key2->{'day'} == $mday) { if ($key2->{'day'} == $mday) {
readingsBulkUpdate($hash, "daily_total", sprintf("%.3f", $key2->{'energy_wh'}*0.001)); readingsBulkUpdate($hash, "daily_total", sprintf("%.3f", $key2->{'energy_wh'} * 0.001));
} }
} }
} }
} }
my $count=1; my $count = 1;
$count = @{$json->{'emeter'}->{'get_daystat'}->{'day_list'}}; $count = @{$json->{'emeter'}->{'get_daystat'}->{'day_list'}};
if ($hw_ver eq "1.0") {readingsBulkUpdate($hash, "monthly_total", $total);} if ($hw_ver eq "1.0") {readingsBulkUpdate($hash, "monthly_total", $total);}
if ($hw_ver eq "2.0") {readingsBulkUpdate($hash, "monthly_total", $total*0.001);} if ($hw_ver eq "2.0") {readingsBulkUpdate($hash, "monthly_total", $total * 0.001);}
if ($count) { readingsBulkUpdate($hash, "daily_average", $total/$count)}; if ($count) {readingsBulkUpdate($hash, "daily_average", $total / $count)};
Log3 $hash, 2, "TPLinkHS110: $name Daystat updated"; Log3 $hash, 2, "TPLinkHS110: $name Daystat updated";
} else { }
else {
Log3 $hash, 1, "TPLinkHS110: $name Error updating daystat. Success: " . $success . ", json: " . $json; Log3 $hash, 1, "TPLinkHS110: $name Error updating daystat. Success: " . $success . ", json: " . $json;
Log3 $hash, 3, "TPLinkHS110: $name Updating readings"; Log3 $hash, 3, "TPLinkHS110: $name Updating readings";
readingsEndUpdate($hash, 1); readingsEndUpdate($hash, 1);
@ -255,22 +319,19 @@ sub TPLinkHS110_Get($$)
##################################### #####################################
sub TPLinkHS110_Set($$) sub TPLinkHS110_Set($$) {
{ my ($hash, $name, $cmd, @args) = @_;
my ( $hash, $name, $cmd, @args ) = @_;
my $cmdList = "on off"; my $cmdList = "on off";
my($success,$json,$realtimejson); my ($success, $json, $realtimejson);
return "\"set $name\" needs at least one argument" unless(defined($cmd)); return "\"set $name\" needs at least one argument" unless (defined($cmd));
return if ($attr{$name}{"disable"} eq "1"); return if ($attr{$name}{"disable"} eq "1");
Log3 $hash, 3, "TPLinkHS110: $name Set <". $cmd ."> called" if ($cmd !~ /\?/); Log3 $hash, 3, "TPLinkHS110: $name Set <" . $cmd . "> called" if ($cmd !~ /\?/);
my $command=""; my $command = "";
if($cmd eq "on") if ($cmd eq "on") {
{
$command = '{"system":{"set_relay_state":{"state":1}}}'; $command = '{"system":{"set_relay_state":{"state":1}}}';
} }
elsif($cmd eq "off") elsif ($cmd eq "off") {
{
$command = '{"system":{"set_relay_state":{"state":0}}}'; $command = '{"system":{"set_relay_state":{"state":0}}}';
} }
else # wenn der übergebene Befehl nicht durch X_Set() verarbeitet werden kann, Weitergabe an SetExtensions else # wenn der übergebene Befehl nicht durch X_Set() verarbeitet werden kann, Weitergabe an SetExtensions
@ -278,35 +339,31 @@ sub TPLinkHS110_Set($$)
return SetExtensions($hash, $cmdList, $name, $cmd, @args); return SetExtensions($hash, $cmdList, $name, $cmd, @args);
} }
my $remote_host = $hash->{HOST}; my $errmsg;
my $remote_port = 9999;
my $c = encrypt($command);
my $socket = IO::Socket::INET->new(PeerAddr => $remote_host,
PeerPort => $remote_port,
Proto => 'tcp',
Type => SOCK_STREAM,
Timeout => $hash->{TIMEOUT})
or return "Couldn't connect to $remote_host:$remote_port: $@\n";
$socket->write($c);
IO::Socket::Timeout->enable_timeouts_on($socket);
$socket->read_timeout(.5);
my $data; my $data;
$data = <$socket>;
$socket->close(); ($errmsg, $data) = TPLinkHS110_SendCommand($hash, $command);
if (defined($errmsg)) {
Log3 $hash, 1, "TPLinkHS110: $name Set failed - " . $errmsg;
return;
}
readingsBeginUpdate($hash); readingsBeginUpdate($hash);
$data = decrypt(substr($data,4));
($success,$json) = TPLinkHS110__evaljson($name,$data); ($success, $json) = TPLinkHS110__evaljson($name, $data);
if(!$success) { if (!$success) {
Log3 $hash, 1, "TPLinkHS110: $name Set failed - parsing";
readingsEndUpdate($hash, 1); readingsEndUpdate($hash, 1);
return; return;
} }
if ($json->{'system'}->{'set_relay_state'}->{'err_code'} eq "0") { if ($json->{'system'}->{'set_relay_state'}->{'err_code'} eq "0") {
TPLinkHS110_Get($hash,""); Log3 $hash, 3, "TPLinkHS110: $name Set OK - get status data";
TPLinkHS110_Get($hash, "");
} else { }
else {
Log3 $hash, 1, "TPLinkHS110: $name Set failed with error code";
return "Command failed!"; return "Command failed!";
} }
return undef; return undef;
@ -314,10 +371,9 @@ sub TPLinkHS110_Set($$)
##################################### #####################################
sub TPLinkHS110_Undefine($$) sub TPLinkHS110_Undefine($$) {
{
my ($hash, $arg) = @_; my ($hash, $arg) = @_;
my $name= $hash->{NAME}; my $name = $hash->{NAME};
RemoveInternalTimer($hash); RemoveInternalTimer($hash);
Log3 $hash, 3, "TPLinkHS110: $name undefined."; Log3 $hash, 3, "TPLinkHS110: $name undefined.";
return; return;
@ -327,7 +383,7 @@ sub TPLinkHS110_Undefine($$)
##################################### #####################################
sub TPLinkHS110_Delete { sub TPLinkHS110_Delete {
my ($hash, $arg) = @_; my ($hash, $arg) = @_;
my $name= $hash->{NAME}; my $name = $hash->{NAME};
Log3 $hash, 3, "TPLinkHS110: $name deleted."; Log3 $hash, 3, "TPLinkHS110: $name deleted.";
return undef; return undef;
} }
@ -335,13 +391,14 @@ sub TPLinkHS110_Delete {
##################################### #####################################
sub TPLinkHS110_Attr { sub TPLinkHS110_Attr {
my ($cmd,$name,$aName,$aVal) = @_; my ($cmd, $name, $aName, $aVal) = @_;
my $hash = $defs{$name}; my $hash = $defs{$name};
if ($aName eq "interval") { if ($aName eq "interval") {
if ($cmd eq "set") { if ($cmd eq "set") {
$hash->{INTERVAL} = $aVal; $hash->{INTERVAL} = $aVal;
} else { }
else {
$hash->{INTERVAL} = 300; $hash->{INTERVAL} = 300;
} }
Log3 $hash, 3, "TPLinkHS110: $name INTERVAL set to " . $hash->{INTERVAL}; Log3 $hash, 3, "TPLinkHS110: $name INTERVAL set to " . $hash->{INTERVAL};
@ -350,7 +407,8 @@ sub TPLinkHS110_Attr {
if ($aName eq "timeout") { if ($aName eq "timeout") {
if ($cmd eq "set") { if ($cmd eq "set") {
$hash->{TIMEOUT} = $aVal; $hash->{TIMEOUT} = $aVal;
} else { }
else {
$hash->{TIMEOUT} = 1; $hash->{TIMEOUT} = 1;
} }
Log3 $hash, 3, "TPLinkHS110: $name TIMEOUT set to " . $hash->{TIMEOUT}; Log3 $hash, 3, "TPLinkHS110: $name TIMEOUT set to " . $hash->{TIMEOUT};
@ -384,7 +442,7 @@ sub TPLinkHS110_Attr {
my $data; my $data;
$data = <$socket>; $data = <$socket>;
$socket->close(); $socket->close();
$data = decrypt(substr($data,4)); $data = decrypt(substr($data, 4));
my $json; my $json;
eval { eval {
$json = decode_json($data); $json = decode_json($data);
@ -401,8 +459,8 @@ sub TPLinkHS110_Attr {
# Based on https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/ # Based on https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/
sub encrypt { sub encrypt {
my $key = 171; my $key = 171;
my @string=split(//, $_[0]); my @string = split(//, $_[0]);
my $result = "\0\0\0".chr(@string); my $result = "\0\0\0" . chr(@string);
foreach (@string) { foreach (@string) {
my $a = $key ^ ord($_); my $a = $key ^ ord($_);
$key = $a; $key = $a;
@ -410,10 +468,11 @@ sub encrypt {
} }
return $result; return $result;
} }
sub decrypt { sub decrypt {
my $key = 171; my $key = 171;
my $result = ""; my $result = "";
my @string=split(//, $_[0]); my @string = split(//, $_[0]);
foreach (@string) { foreach (@string) {
my $a = $key ^ ord($_); my $a = $key ^ ord($_);
$key = ord($_); $key = ord($_);
@ -462,28 +521,29 @@ sub hwMapping {
############################################################################### ###############################################################################
# Test ob JSON-String empfangen wurde # Test ob JSON-String empfangen wurde
sub TPLinkHS110__evaljson($$) { sub TPLinkHS110__evaljson($$) {
my ($name,$data)= @_; my ($name, $data) = @_;
my $hash = $defs{$name}; my $hash = $defs{$name};
my $json; my $json;
my $success = 1; my $success = 1;
my $jerr = "ok"; my $jerr = "ok";
Log3 $name, 5, "$name - Data returned: ". Dumper $data; Log3 $name, 5, "$name - Data returned: " . Dumper $data;
eval {$json = decode_json($data);} or do eval {$json = decode_json($data);} or do
{ {
$success = 0; $success = 0;
}; };
if($@) { if ($@) {
$jerr = $@; $jerr = $@;
}; };
readingsBulkUpdate($hash, "decode_json", $jerr); readingsBulkUpdate($hash, "decode_json", $jerr);
if($success) { if ($success) {
return($success,$json); return($success, $json);
} else { }
return($success,undef); else {
return($success, undef);
} }
} }