diff --git a/fhem/CHANGED b/fhem/CHANGED index dd7303392..594b1fdc1 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # 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. + - new: 24_TPLinkHS110: Support for TPLink HS100/110 wifi power outlet - bugfix: 73_GasCalculator: get/set list corrected for CounterDevice - bugfix: 73_ElectricityCalculator: get/set list corrected for CounterDevice - bugfix: 93__DbRep: error in diffValue if no value was selected diff --git a/fhem/FHEM/24_TPLinkHS110.pm b/fhem/FHEM/24_TPLinkHS110.pm new file mode 100644 index 000000000..26b5676b5 --- /dev/null +++ b/fhem/FHEM/24_TPLinkHS110.pm @@ -0,0 +1,419 @@ +################################################################ +# $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; + $socket->recv($data,1024); + $socket->close(); + $data = decrypt(substr($data,4)); + my $json = decode_json($data); + + 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; + $socket->recv($rdata,1024); + $socket->close(); + $rdata = decrypt(substr($rdata,4)); + my $realtimejson = decode_json($rdata); + 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; + $socket->recv($data,1024); + $socket->close(); + $data = decrypt(substr($data,4)); + 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'}; + } + } + my $count=1; + $count = @{$json->{'emeter'}->{'get_daystat'}->{'day_list'}}; + readingsBulkUpdate($hash, "monthly_total", $total); + readingsBulkUpdate($hash, "daily_average", $total/$count); + } + 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"); + return "Unknown argument $a[1], choose one of on off " if($a[1] eq "?"); + Log3 $hash, 3, "TPLinkHS110: $name Set <". $a[1] ."> called"; + + 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; + $socket->recv($data,1024); + $socket->close(); + $data = decrypt(substr($data,4)); + my $json = decode_json($data); + 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; + $socket->recv($data,1024); + $socket->close(); + $data = decrypt(substr($data,4)); + my $json = decode_json($data); + } + + 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 diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index eeec491c5..192997892 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -119,6 +119,7 @@ FHEM/23_LUXTRONIK2.pm tupol http://forum.fhem.de Sonstiges FHEM/23_WEBIO.pm sachag http://forum.fhem.de Sonstiges FHEM/23_WEBIO_12DIGITAL.pm sachag http://forum.fhem.de Sonstiges FHEM/24_NetIO230B.pm rudolfkoenig/orphan http://forum.fhem.de Sonstiges +FHEM/24_TPLinkHS110.pm VolkerKettenbach http://forum.fhem.de Sonstige Systeme FHEM/26_tahoma.pm mike3436 http://forum.fhem.de Sonstige Systeme FHEM/30_DUOFERN telekatz http://forum.fhem.de Sonstige Systeme FHEM/30_HUEBridge.pm justme1968 http://forum.fhem.de Beleuchtung