################################################################ # $Id$ # # Copyright notice # # (c) 2016 Copyright: Volker Kettenbach # e-mail: volker at kettenbach minus it dot de # # Description: # This is an FHEM-Module for the TP Link TPLinkHS110110/110 # wifi controlled power outlet. # It support switching on and of the outlet as well as switching # on and of the nightmode (green led off). # It supports reading several readings as well as the # realtime power readings of the HS110. # # Requirements # Perl Module: IO::Socket::INET # Perl Module: IO::Socket::Timeout # # In recent debian based distributions IO::Socket::Timeout can # be installed by "apt-get install libio-socket-timeout-perl" # In older distribution try "cpan IO::Socket::Timeout" # # Origin: # https://github.com/kettenbach-it/FHEM-TPLink-HS110 # ################################################################ package main; use strict; use warnings; use IO::Socket::INET; use IO::Socket::Timeout; use JSON; ##################################### sub TPLinkHS110_Initialize($) { my ($hash) = @_; $hash->{DefFn} = "TPLinkHS110_Define"; $hash->{ReadFn} = "TPLinkHS110_Get"; $hash->{SetFn} = "TPLinkHS110_Set"; $hash->{UndefFn} = "TPLinkHS110_Undefine"; $hash->{DeleteFn} = "TPLinkHS110_Delete"; $hash->{AttrFn} = "TPLinkHS110_Attr"; $hash->{AttrList} = "interval ". "disable:0,1 " . "nightmode:on,off " . "timeout " . "$readingFnAttributes"; } ##################################### sub TPLinkHS110_Define($$) { my ($hash, $def) = @_; my $name= $hash->{NAME}; my @a = split( "[ \t][ \t]*", $def ); return "Wrong syntax: use define TPLinkHS110 " if (int(@a) != 3); $hash->{INTERVAL}=300; $hash->{TIMEOUT}=1; $hash->{HOST}=$a[2]; $attr{$name}{"disable"} = 0; # initial request after 2 secs, there timer is set to interval for further update InternalTimer(gettimeofday()+2, "TPLinkHS110_Get", $hash, 0); Log3 $hash, 3, "TPLinkHS110: $name defined."; return undef; } ##################################### sub TPLinkHS110_Get($$) { my ($hash) = @_; my $name = $hash->{NAME}; 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_port = 9999; my $command = '{"system":{"get_sysinfo":{}}}'; 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->send($c); my $data; my $retval = $socket->recv($data,8192); $socket->close(); unless( defined $retval) { return undef; } $data = decrypt(substr($data,4)); my $json; eval { $json = decode_json($data); } or do { Log3 $hash, 2, "TPLinkHS110: $name json-decoding failed. Problem decoding getting statistical data"; return; }; Log3 $hash, 3, "TPLinkHS110: $name Get called. Relay state: $json->{'system'}->{'get_sysinfo'}->{'relay_state'}, RSSI: $json->{'system'}->{'get_sysinfo'}->{'rssi'}"; readingsBeginUpdate($hash); foreach my $key (sort keys %{$json->{'system'}->{'get_sysinfo'}}) { readingsBulkUpdate($hash, $key, $json->{'system'}->{'get_sysinfo'}->{$key}); } if ($json->{'system'}->{'get_sysinfo'}->{'relay_state'} == 0) { readingsBulkUpdate($hash, "state", "off"); } if ($json->{'system'}->{'get_sysinfo'}->{'relay_state'} == 1) { readingsBulkUpdate($hash, "state", "on"); } # If the device is a HS110, get realtime data: if ($json->{'system'}->{'get_sysinfo'}->{'model'} eq "HS110(EU)") { 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->send($rc); my $rdata; $retval = $socket->recv($rdata,8192); $socket->close(); unless( defined $retval) { return undef; } $rdata = decrypt(substr($rdata,4)); my $realtimejson; if (length($rdata)==0) { Log3 $hash, 1, "TPLinkHS110: $name: Received zero bytes of realtime data. Cannot process realtime data"; return; } eval { $realtimejson = decode_json($rdata); } or do { Log3 $hash, 2, "TPLinkHS110: $name json-decoding failed. Problem decoding getting statistical data"; return; }; foreach my $key2 (sort keys %{$realtimejson->{'emeter'}->{'get_realtime'}}) { readingsBulkUpdate($hash, $key2, $realtimejson->{'emeter'}->{'get_realtime'}->{$key2}); } Log3 $hash, 3, "TPLinkHS110: $name Device is an HS110. Got extra realtime data: $realtimejson->{'emeter'}->{'get_realtime'}->{'power'} Watt, $realtimejson->{'emeter'}->{'get_realtime'}->{'voltage'} Volt, $realtimejson->{'emeter'}->{'get_realtime'}->{'current'} Ampere"; # Get Daily Stats my $command = '{"emeter":{"get_daystat":{"month":'.$mon.',"year":'.$year.'}}}'; my $c = encrypt($command); $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->send($c); my $data; $retval = $socket->recv($data,8192); $socket->close(); unless( defined $retval) { return undef; } $data = decrypt(substr($data,4)); eval { my $json = decode_json($data); my $total=0; foreach my $key (sort keys @{$json->{'emeter'}->{'get_daystat'}->{'day_list'}}) { foreach my $key2 ($json->{'emeter'}->{'get_daystat'}->{'day_list'}[$key]) { $total = $total+ $key2->{'energy'}; if ($key2->{'day'} == $mday) { readingsBulkUpdate($hash, "daily_total", sprintf("%.3f", $key2->{'energy'})); } } } my $count=1; $count = @{$json->{'emeter'}->{'get_daystat'}->{'day_list'}}; readingsBulkUpdate($hash, "monthly_total", $total); if ($count) { readingsBulkUpdate($hash, "daily_average", $total/$count)}; 1; } or do { Log3 $hash, 2, "TPLinkHS110: $name json-decoding failed. Problem decoding getting statistical data"; return; }; } readingsEndUpdate($hash, 1); Log3 $hash, 3, "TPLinkHS110: $name Get end"; } ##################################### sub TPLinkHS110_Set($$) { my ( $hash, @a ) = @_; my $name= $hash->{NAME}; return "Device disabled in config" if ($attr{$name}{"disable"} eq "1"); Log3 $hash, 3, "TPLinkHS110: $name Set <". $a[1] ."> called"; return "Unknown argument $a[1], choose one of on off " if($a[1] ne "on" & $a[1] ne "off"); my $command; if($a[1] eq "on") { $command = '{"system":{"set_relay_state":{"state":1}}}'; } if($a[1] eq "off") { $command = '{"system":{"set_relay_state":{"state":0}}}'; } my $remote_host = $hash->{HOST}; 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->send($c); my $data; my $retval = $socket->recv($data,8192); $socket->close(); unless( defined $retval) { return undef; } $data = decrypt(substr($data,4)); my $json; eval { $json = decode_json($data); } or do { Log3 $hash, 2, "TPLinkHS110: $name json-decoding failed. Problem decoding getting statistical data"; return; }; if ($json->{'system'}->{'set_relay_state'}->{'err_code'} eq "0") { TPLinkHS110_Get($hash,""); } else { return "Command failed!"; } return undef; } ##################################### sub TPLinkHS110_Undefine($$) { my ($hash, $arg) = @_; my $name= $hash->{NAME}; RemoveInternalTimer($hash); Log3 $hash, 3, "TPLinkHS110: $name undefined."; return; } ##################################### sub TPLinkHS110_Delete { my ($hash, $arg) = @_; my $name= $hash->{NAME}; Log3 $hash, 3, "TPLinkHS110: $name deleted."; return undef; } ##################################### sub TPLinkHS110_Attr { my ($cmd,$name,$aName,$aVal) = @_; my $hash = $defs{$name}; if ($aName eq "interval") { if ($cmd eq "set") { $hash->{INTERVAL} = $aVal; } else { $hash->{INTERVAL} = 300; } Log3 $hash, 3, "TPLinkHS110: $name INTERVAL set to " . $hash->{INTERVAL}; } if ($aName eq "timeout") { if ($cmd eq "set") { $hash->{TIMEOUT} = $aVal; } else { $hash->{TIMEOUT} = 1; } Log3 $hash, 3, "TPLinkHS110: $name TIMEOUT set to " . $hash->{TIMEOUT}; } if ($aName eq "nightmode") { my $command; if ($cmd eq "set") { $hash->{NIGHTMODE} = $aVal; Log3 $hash, 3, "TPLinkHS110: $name Nightmode $aVal."; $command = '{"system":{"set_led_off":{"off":1}}}' if ($aVal eq "on"); $command = '{"system":{"set_led_off":{"off":0}}}' if ($aVal eq "off"); } if ($cmd eq "del") { Log3 $hash, 3, "TPLinkHS110: $name Nightmode attribute removed. Nightmode disabled."; $command = '{"system":{"set_led_off":{"off":0}}}'; $hash->{NIGHTMODE} = "off"; } my $remote_host = $hash->{HOST}; 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->send($c); my $data; my $retval = $socket->recv($data,8192); $socket->close(); unless( defined $retval) { return undef; } $data = decrypt(substr($data,4)); my $json; eval { $json = decode_json($data); } or do { Log3 $hash, 2, "TPLinkHS110: $name json-decoding failed. Problem decoding getting statistical data"; return; }; } return undef; } # Encryption and Decryption of TP-Link Smart Home Protocol # XOR Autokey Cipher with starting key = 171 # Based on https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/ sub encrypt { my $key = 171; my $result = "\0\0\0\0"; my @string=split(//, $_[0]); foreach (@string) { my $a = $key ^ ord($_); $key = $a; $result .= chr($a); } return $result; } sub decrypt { my $key = 171; my $result = ""; my @string=split(//, $_[0]); foreach (@string) { my $a = $key ^ ord($_); $key = ord($_); $result .= chr($a); } return $result; } ###################################################################################### 1; =pod =begin html

TPLinkHS110

=end html =begin html_DE

TPLinkHS110

=end html_DE =item summary Support for TPLink HS100/100 wifi controlled power outlet =item summary_DE Support für die TPLink HS100/110 WLAN Steckdosen