diff --git a/FHEM/70_Klafs.pm b/FHEM/70_Klafs.pm new file mode 100644 index 000000000..896416794 --- /dev/null +++ b/FHEM/70_Klafs.pm @@ -0,0 +1,1717 @@ +# $Id$ +############################################################################## +# +# 70_Klafs.pm +# A FHEM Perl module to control a Klafs sauna. +# +# This file is part of fhem. +# +# Fhem is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# Fhem is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with fhem. If not, see . +# Forum: https://forum.fhem.de/index.php?topic=127701 +# +############################################################################## +# ToDo +# get SaunaID +############################################################################## +package main; + +use strict; +use warnings; +use Carp qw(carp); +use Scalar::Util qw(looks_like_number); +use Time::HiRes qw(gettimeofday); +use JSON qw(decode_json encode_json); +#use Encode qw(encode_utf8 decode_utf8); +use Time::Piece; +use Time::Local; +#use Data::Dumper; +use HttpUtils; +use FHEM::Core::Authentication::Passwords qw(:ALL); + +my %sets = ( + off => 'noArg', + password => '', + on => '', + ResetLoginFailures => '', + update => 'noArg', +); + +my %gets = ( + help => 'noArg', + SaunaID => 'noArg', + ); + +################################### +sub KLAFS_Initialize { + my $hash = shift; + + Log3 ($hash, 5, 'KLAFS_Initialize: Entering'); + $hash->{DefFn} = \&Klafs_Define; + $hash->{UndefFn} = \&Klafs_Undef; + $hash->{SetFn} = \&Klafs_Set; + $hash->{AttrFn} = \&Klafs_Attr; + $hash->{GetFn} = \&Klafs_Get; + $hash->{RenameFn} = \&Klafs_Rename; + $hash->{AttrList} = 'username saunaid pin interval disable:1,0 ' . $main::readingFnAttributes; + return; +} + +sub Klafs_Attr +{ + my ( $cmd, $name, $attrName, $attrVal ) = @_; + my $hash = $defs{$name}; + + if( $attrName eq 'disable' ) { + RemoveInternalTimer($hash) if $cmd ne 'del'; + InternalTimer(gettimeofday(), \&Klafs_DoUpdate, $hash, 0) if $cmd eq 'del' || !$attrVal && $init_done; + }elsif( $attrName eq 'username' ) { + if( $cmd eq 'set' ) { + $hash->{Klafs}->{username} = $attrVal; + Log3 ($name, 3, "$name - username set to " . $hash->{Klafs}->{username}); + } + }elsif( $attrName eq 'saunaid' ) { + if( $cmd eq 'set' ) { + $hash->{Klafs}->{saunaid} = $attrVal; + Log3 ($name, 3, "$name - saunaid set to " . $hash->{Klafs}->{saunaid}); + } + }elsif( $attrName eq 'pin' ) { + if( $cmd eq 'set' ) { + return 'Pin is not a number!' if !looks_like_number($attrVal); + $hash->{Klafs}->{pin} = $attrVal; + Log3 ($name, 3, "$name - pin set to " . $hash->{Klafs}->{pin}); + } + }elsif( $attrName eq 'interval' ) { + if( $cmd eq 'set' ) { + return 'Interval must be greater than 0' if !$attrVal; + $hash->{Klafs}->{interval} = $attrVal; + InternalTimer( time() + $hash->{Klafs}->{interval}, \&Klafs_DoUpdate, $hash, 0 ); + Log3 ($name, 3, "$name - set interval: $attrVal"); + }elsif( $cmd eq 'del' ) { + $hash->{Klafs}->{interval} = 60; + InternalTimer( time() + $hash->{Klafs}->{interval}, \&Klafs_DoUpdate, $hash, 0 ); + Log3 ($name, 3, "$name - deleted interval and set to default: 60"); + } + } + return; +} + +################################### +sub Klafs_Define { + my $hash = shift; + my $def = shift; + + return $@ if !FHEM::Meta::SetInternals($hash); + my @args = split m{\s+}, $def; + my $usage = qq (syntax: define KLAFS); + return $usage if ( @args != 2 ); + my ( $name, $type ) = @args; + + Log3 ($name, 5, "KLAFS $name: called function KLAFS_Define()"); + + $hash->{NAME} = $name; + $hash->{helper}->{passObj} = FHEM::Core::Authentication::Passwords->new($hash->{TYPE}); + + readingsSingleUpdate( $hash, "last_errormsg", "0", 0 ); + Klafs_CONNECTED($hash,'initialized',1); + $hash->{Klafs}->{interval} = 60; + InternalTimer( time() + $hash->{Klafs}->{interval}, \&Klafs_DoUpdate, $hash, 0 ); + $hash->{Klafs}->{reconnect} = 0; + $hash->{Klafs}->{expire} = time(); + + InternalTimer(gettimeofday() + AttrVal($name,'interval',$hash->{Klafs}->{interval}), 'Klafs_DoUpdate', $hash, 0) if !$init_done; + notifyRegexpChanged($hash, 'global',1); + Klafs_DoUpdate($hash) if $init_done && !AttrVal($name,'disable',0); + + return; +} + +################################### +sub Klafs_Undef { + my $hash = shift // return; + my $name = $hash->{NAME}; + Log3 ($name, 5, "KLAFS $name: called function KLAFS_Undefine()"); + + # De-Authenticate + Klafs_CONNECTED( $hash, 'deauthenticate',1 ); + + # Stop the internal GetStatus-Loop and exit + RemoveInternalTimer($hash); + + return; +} + +sub Klafs_Rename +{ + my $name_new = shift // return; + my $name_old = shift // return; + + my $passObj = $main::defs{$name_new}->{helper}->{passObj}; + + my $password = $passObj->getReadPassword($name_old) // return; + + $passObj->setStorePassword($name_new, $password); + $passObj->setDeletePassword($name_old); + + return; +} + +sub Klafs_CONNECTED { + my $hash = shift // return; + my $set = shift; + my $notUseBulk = shift; + + if ($set) { + $hash->{Klafs}->{CONNECTED} = $set; + + if ( $notUseBulk ) { + readingsSingleUpdate($hash,'state',$set,1) if $set eq ReadingsVal($hash->{NAME},'state',''); + } else { + readingsBulkUpdate($hash,'state',$set) if $set eq ReadingsVal($hash->{NAME},'state',''); + } + return; + } + return 'disabled' if $hash->{Klafs}->{CONNECTED} eq 'disabled'; + return 1 if $hash->{Klafs}->{CONNECTED} eq 'connected'; + return 0; +} + +############################################################## +# +# API AUTHENTICATION +# +############################################################## +sub Klafs_Auth{ + my ($hash) = @_; + my $name = $hash->{NAME}; + # $hash->{Klafs}->{reconnect}: Sperre bei Reconnect. Zwischen Connects müssen 300 Sekunden liegen. + # $hash->{Klafs}->{LoginFailures}: Anzahl fehlerhafte Logins. Muss 0 sein, sonst kein connect. Bei drei Fehlversuchen sperrt Klafs den Benutzer + + $hash->{Klafs}->{reconnect} = 0 if(!defined $hash->{Klafs}->{reconnect}); + my $LoginFailures = ReadingsVal( $name, "LoginFailures", "0" ); + + $hash->{Klafs}->{LoginFailures} //= ''; + if($hash->{Klafs}->{LoginFailures} eq ""){ + $hash->{Klafs}->{LoginFailures} = 0; + } + + if (time() >= $hash->{Klafs}->{reconnect}){ + Log3 ($name, 4, "Reconnect"); + + + my $username = $hash->{Klafs}->{username} // carp q[No username found!] && return; + my $password = $hash->{helper}->{passObj}->getReadPassword($name) // q{} && carp q[No password found!] && return;; + + + #Reading auslesen und definieren um das Reading unten zu schreiben. Intern wird $hash->{Klafs}->{LoginFailures}, weil Readings ggf. nicht schnell genug zur Verfuegung stehen. + my $LoginFailures = ReadingsVal( $name, "LoginFailures", "0" ); + + return if $hash->{Klafs}->{LoginFailures} > 0; + Log3 ($name, 4, "Anzahl Loginfailures: $hash->{Klafs}->{LoginFailures}"); + + if ( $hash->{Klafs}->{username} eq "") { + my $msg = "Missing attribute: attr $name username "; + Log3 ($name, 4, $msg); + return $msg; + }elsif ( $password eq "") { + my $msg = "Missing password: set $name password "; + Log3 ($name, 4, $msg); + return $msg; + }else{ + # Reconnects nicht unter 300 Sekunden durchführen + my $reconnect = time() + 300; + $hash->{Klafs}->{reconnect} = $reconnect; + my $header = "Content-Type: application/x-www-form-urlencoded\r\n". + "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.71 Safari/537.36"; + my $datauser = "UserName=$username&Password=$password"; + + if ($hash->{Klafs}->{LoginFailures} eq "0"){ + + HttpUtils_NonblockingGet({ + url => "https://sauna-app.klafs.com/Account/Login", + ignoreredirects => 1, + timeout => 5, + hash => $hash, + method => "POST", + header => $header, + data => $datauser, + callback => \&Klafs_AuthResponse, + }); + } + } + } + return; +} + +# Antwortheader aus dem Login auslesen fuer das Cookie +sub Klafs_AuthResponse { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $header = $param->{httpheader}; + Log3 ($name, 5, "header: $header"); + Log3 ($name, 5, "Data: $data"); + Log3 ($name, 5, "Error: $err"); + readingsBeginUpdate($hash); + if($data=~/
  • /) { + for my $err ($data =~ m /
    • ?(.*)<\/li>/) { + my %umlaute = ("ä" => "ae", "ü" => "ue", "Ä" => "Ae", "Ö" => "Oe", "ö" => "oe", "Ü" => "Ue", "ß" => "ss"); + my $umlautkeys = join ("|", keys(%umlaute)); + $err=~ s/($umlautkeys)/$umlaute{$1}/g; + Log3 ($name, 1, "KLAFS $name: $err"); + $hash->{Klafs}->{LoginFailures} = $hash->{Klafs}->{LoginFailures}+1; + readingsBulkUpdate( $hash, 'last_errormsg', $err ); + readingsBulkUpdate( $hash, 'LoginFailures', $hash->{Klafs}->{LoginFailures}); + } + Klafs_CONNECTED($hash,'error'); + }else{ + readingsBulkUpdate( $hash, 'LoginFailures', 0, 0); + $hash->{Klafs}->{LoginFailures} =0; + for my $cookie ($header =~ m/set-cookie: ?(.*)/gi) { + $cookie =~ /([^,; ]+)=([^,;\s\v]+)[;,\s\v]*([^\v]*)/; + my $aspxauth = $1 . "=" .$2 .";"; + $hash->{Klafs}->{cookie} = $aspxauth; + Log3 ($name, 4, "$name: GetCookies parsed Cookie: $aspxauth"); + + # Cookie soll nach 2 Tagen neu erzeugt werden + my $expire = time() + 172800; + $hash->{Klafs}->{expire} = $expire; + my $expire_date = strftime("%Y-%m-%d %H:%M:%S", localtime($expire)); + readingsBulkUpdate( $hash, 'cookieExpire', $expire_date, 0 ); + + Klafs_CONNECTED($hash,'authenticated'); + } + } + readingsEndUpdate($hash,1); + return; +} +############################################################## +# +# Cookie pruefen und Readings erneuern +# +############################################################## + +sub klafs_getStatus{ + my ($hash, $def) = @_; + my $name = $hash->{NAME}; + + my $LoginFailures = ReadingsVal( $name, "LoginFailures", "0" ); + if(!defined $hash->{Klafs}->{LoginFailures}){ + $hash->{Klafs}->{LoginFailures} = $LoginFailures; + } + + # SaunaIDs für GET zur Verfügung stellen + Klafs_GetSaunaIDs_Send($hash); + + + if ( $hash->{Klafs}->{saunaid} eq "") { + my $msg = "Missing attribute: attr $name saunaid -> Use to receive your SaunaID"; + Log3 ($name, 1, $msg); + return $msg; + } + + my $aspxauth = $hash->{Klafs}->{cookie}; + my $saunaid = $hash->{Klafs}->{saunaid}; + + my $header_gs = "Content-Type: application/json\r\n". + "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.71 Safari/537.36\r\n". + "Cookie: $aspxauth"; + my $datauser_gs = '{"saunaId":"'.$saunaid.'"}'; + + HttpUtils_NonblockingGet({ + url => "https://sauna-app.klafs.com/Control/GetSaunaStatus", + timeout => 5, + hash => $hash, + method => "POST", + header => $header_gs, + data => $datauser_gs, + callback => \&klafs_getStatusResponse, + }); + + #Name Vorname Mail Benutzername + #GET Anfrage mit ASPXAUTH + my $header_user = "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.71 Safari/537.36\r\n". + "Cookie: $aspxauth"; + + HttpUtils_NonblockingGet({ + url => "https://sauna-app.klafs.com/Account/ChangeProfile", + timeout => 5, + hash => $hash, + method => "GET", + header => $header_user, + callback => \&Klafs_GETProfile, + }); + + my $header_set = "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.71 Safari/537.36\r\n". + "Cookie: $aspxauth"; + + HttpUtils_NonblockingGet({ + url => "https://sauna-app.klafs.com/Control/ChangeSettings", + timeout => 5, + hash => $hash, + method => "GET", + header => $header_set, + callback => \&Klafs_GETSettings, + }); + return; +} + +sub klafs_getStatusResponse { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $header = $param->{httpheader}; + my $power = ReadingsVal( $name, "power", "off" ); + + Log3 ($name, 5, "Status header: $header"); + Log3 ($name, 5, "Status Data: $data"); + Log3 ($name, 5, "Status Error: $err"); + + if($data !~/Account\/Login/) { + # Wenn in $data eine Anmeldung verlangt wird und kein json kommt, darf es nicht weitergehen. + # Connect darf es hier nicht geben. Das darf nur an einer Stelle kommen. Sonst macht perl mehrere connects gleichzeitig- bei 3 Fehlversuchen wäre der Account gesperrt + + #my $return = decode_json( "$data" ); + my $entries; + if ( !eval { $entries = decode_json($data) ; 1 } ) { + #sonstige Fehlerbehandlungsroutinen hierher, dann ; + return Log3($name, 1, "JSON decoding error: $@"); + } + + # boolsche Werte in true/false uebernehmen + for my $key (qw( saunaSelected sanariumSelected irSelected isConnected isPoweredOn isReadyForUse showBathingHour)) { + $entries->{$key} = $entries->{$key} ? q{true} : q{false} ; + } + $entries->{statusMessage} //= ''; + $entries->{currentTemperature} = '0' if $entries->{currentTemperature} eq '141'; + $entries->{RemainTime} = sprintf("%2.2d:%2.2d" , $entries->{bathingHours}, $entries->{bathingMinutes}); + my $modus = $entries->{saunaSelected} eq q{true} ? 'Sauna' + : $entries->{sanariumSelected} eq q{true} ? 'Sanarium' + : $entries->{irSelected} eq q{true} ? 'Infrared' + : 0; + $entries->{Mode} = $modus; + + # Loop ueber $entries und ggf. reading schreiben + my $old; + readingsBeginUpdate ($hash); + for my $record ($entries) { + for my $key (keys(%$record)) { + my $new = $record->{$key}; + # Alter Wert Readings auslesen + $old = ReadingsVal( $name, $key, "" ); + next if $old eq $new; + # Readings schreiben, wenn es einen anderen Wert hat + readingsBulkUpdate($hash, $key, $new); + } + } + + Klafs_CONNECTED($hash,'connected'); + readingsEndUpdate($hash, 1); + }else{ + # Wenn Account/Login zurück kommt, dann benötigt es einen reconnect + Klafs_CONNECTED($hash,'disconnected', 1); + } + return; +} + +sub Klafs_GETProfile { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $header = $param->{httpheader}; + Log3 ($name, 5, "Profile header: $header"); + Log3 ($name, 5, "Profile Data: $data"); + Log3 ($name, 5, "Profile Error: $err"); + + if($data !~/Account\/Login/) { + # Wenn in $data eine Anmeldung verlangt wird und kein json kommt, darf es nicht weitergehen. + # Connect darf es hier nicht geben. Das darf nur an einer Stelle kommen. Sonst macht perl mehrere connects gleichzeitig- bei 3 Fehlversuchen wäre der Account gesperrt + readingsBeginUpdate ($hash); + if($data=~/{hash}; + my $name = $hash->{NAME}; + my $header = $param->{httpheader}; + Log3 ($name, 5, "Settings header: $header"); + Log3 ($name, 5, "Settings Data: $data"); + Log3 ($name, 5, "Settings Error: $err"); + + if($data !~/Account\/Login/) { + # Wenn in $data eine Anmeldung verlangt wird und kein json kommt, darf es nicht weitergehen. + # Connect darf es hier nicht geben. Das darf nur an einer Stelle kommen. Sonst macht perl mehrere connects gleichzeitig- bei 3 Fehlversuchen wäre der Account gesperrt + if($data=~/StandByTime: parseInt\(\'/) { + readingsBeginUpdate ($hash); + for my $output ($data =~ m /StandByTime: parseInt\(\'?(.*)'/) { + my $sbtime = $1 eq q{24} ? '1 Tag' + : $1 eq q{72} ? '3 Tage' + : $1 eq q{168} ? '1 Woche' + : $1 eq q{672} ? '4 Wochen' + : $1 eq q{1344} ? '8 Wochen' + : 'Internal error'; + my $sbcloud = ReadingsVal( $name, 'standbytime', '' ); + if($sbcloud eq '' || $sbcloud ne $sbtime){ + readingsBulkUpdate( $hash, 'standbytime', $sbtime, 1 ); + } + } + readingsEndUpdate($hash, 1); + } + + if($data=~/Language: \'/) { + readingsBeginUpdate ($hash); + for my $output ($data =~ m /Language: \'?(.*)'/) { + my $language = $1 eq q{de} ? 'Deutsch' + : $1 eq q{en} ? 'Englisch' + : $1 eq q{fr} ? 'Franzoesisch' + : $1 eq q{es} ? 'Spanisch' + : $1 eq q{ru} ? 'Russisch' + : $1 eq q{pl} ? 'Polnisch' + : 'Internal error'; + my $langcloud = ReadingsVal( $name, 'langcloud', '' ); + if($langcloud eq '' || $langcloud ne $language){ + readingsBulkUpdate( $hash, 'langcloud', $language, 1 ); + } + } + readingsEndUpdate($hash, 1); + } + }else{ + # Wenn Account/Login zurück kommt, dann benötigt es einen reconnect + Klafs_CONNECTED($hash,'disconnected', 1); + } + return; +} + +################################### +sub Klafs_Get { + my ( $hash, @a ) = @_; + + my $name = $hash->{NAME}; + my $what; + Log3 ($name, 5, "KLAFS $name: called function KLAFS_Get()"); + + return "argument is missing" if ( @a < 2 ); + + $what = $a[1]; + + + return _KLAFS_help($hash) if ( $what =~ /^(help)$/ ); + return _KLAFS_saunaid($hash) if ( $what =~ /^(SaunaID)$/ ); + return "$name get with unknown argument $what, choose one of " . join(" ", sort keys %gets); +} + +sub _KLAFS_help { + return << 'EOT'; +------------------------------------------------------------------------------------------------------------------------------------------------------------ +| Set Parameter | +------------------------------------------------------------------------------------------------------------------------------------------------------------ +|on | ohne Parameter -> Default Sauna 90 Grad | +| | set "name" on Sauna 90 - 3 Parameter: Sauna mit Temperatur [10-100]; Optional Uhrzeit [19:30] | +| | set "name" on Saunarium 65 5 - 4 Parameter: Sanarium mit Temperatur [40-75]; Optional HumidtyLevel [0-10] und Uhrzeit [19:30] | +| | set "name" on Infrared 30 5 - 4 Parameter: Infrarot mit Temperatur [20-40] und IR Level [0-10]; Optional Uhrzeit [19:30] | +| | Infrarot ist nicht supported, da keine Testumgebung verfuegbar. | +------------------------------------------------------------------------------------------------------------------------------------------------------------ +|off | Schaltet die Sauna|Sanarium|Infrarot aus - ohne Parameter. | +------------------------------------------------------------------------------------------------------------------------------------------------------------ +|ResetLoginFailures | Bei fehlerhaftem Login wird das Reading LoginFailures auf 1 gesetzt. Damit ist der automatische Login vom diesem Modul gesperrt. | +| | Klafs sperrt den Account nach 3 Fehlversuchen. Damit nicht automatisch 3 falsche Logins hintereinander gemacht werden. | +| | ResetLoginFailures setzt das Reading wieder auf 0. Davor sollte man sich erfolgreich an der App bzw. unter sauna-app.klafs.com | +| | angemeldet bzw. das Passwort zurueckgesetzt haben. Erfolgreicher Login resetet die Anzahl der Fehlversuche in der Klafs-Cloud. | +------------------------------------------------------------------------------------------------------------------------------------------------------------ +|update | Refresht die Readings und fuehrt ggf. ein Login durch. | +------------------------------------------------------------------------------------------------------------------------------------------------------------ +| Get Parameter | +------------------------------------------------------------------------------------------------------------------------------------------------------------ +|SaunaID | Liest die verfuegbaren SaunaIDs aus. | +------------------------------------------------------------------------------------------------------------------------------------------------------------ +|help | Diese Hilfe | +------------------------------------------------------------------------------------------------------------------------------------------------------------ +EOT +} + +sub Klafs_GetSaunaIDs_Send{ + my ($hash) = @_; + my ($name,$self) = ($hash->{NAME},Klafs_Whoami()); + my $aspxauth = $hash->{Klafs}->{cookie}; + return if $hash->{Klafs}->{LoginFailures} > 0; + Log3 ($name, 5, "$name ($self) - executed."); + + my $header = "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.71 Safari/537.36\r\n". + "Cookie: $aspxauth"; + HttpUtils_NonblockingGet({ + url => "http://sauna-app.klafs.com/Control", + timeout => 5, + hash => $hash, + method => "GET", + header => $header, + callback => \&Klafs_GetSaunaIDs_Receive, + }); + return; +} + +sub Klafs_GetSaunaIDs_Receive { + my ($param, $err, $data) = @_; + my ($name,$self,$hash) = ($param->{hash}->{NAME},Klafs_Whoami(),$param->{hash}); + my $returnwerte; + + Log3 ($name, 5, "$name ($self) - executed."); + + if ($err ne "") { + Log3 ($name, 4, "$name ($self) - error."); + } + elsif ($data ne "") { + if ($param->{code} == 200 || $param->{code} == 400 || $param->{code} == 401) { + if($data !~/Account\/Login/) { + # Wenn in $data eine Anmeldung verlangt wird und keine Daten, darf es nicht weitergehen. + # Connect darf es hier nicht geben. Das darf nur an einer Stelle kommen. Sonst macht perl mehrere connects gleichzeitig - bei 3 Fehlversuchen wäre der Account gesperrt + $returnwerte = ""; + if($data=~//) { + for my $output ($data =~ m /(.*?)<\/tr>/gis) { + $output=~ m/(.*?)<\/span>/g; + $returnwerte .= $1.": "; + $output=~ m/
      {Klafs}->{GetSaunaIDs} = $returnwerte; + } + } + } + } + return; +} + +sub _KLAFS_saunaid { + my ( $hash, @a ) = @_; + my $name = $hash->{NAME}; + + return "======================================== FOUND SAUNA-IDs ========================================\n" + . $hash->{Klafs}->{GetSaunaIDs} . + "================================================================================================="; + +} + + +################################### +sub Klafs_Set { + my ( $hash, $name, $cmd, @args ) = @_; + return if $hash->{Klafs}->{LoginFailures} > 0 and !$cmd; + + + if (Klafs_CONNECTED($hash) eq 'disabled' && $cmd !~ /clear/) { + Log3 ($name, 3, "$name: set called with $cmd but device is disabled!") if ($cmd ne "?"); + return "Unknown argument $cmd, choose one of clear:all,readings"; + } + + my $temperature; + my $level; + my $power = ReadingsVal( $name, "power", "off" ); + + + # Klafs rundet bei der Startzeit immer auf volle 10 Minuten auf. Das ist der Zeitpunkt, wann die Sauna fertig aufgeheizt sein soll. Naechste 10 Minuten heisst also sofort aufheizen + my $FIFTEEN_MINS = (15 * 60); + my $now = time; + if (my $diff = $now % $FIFTEEN_MINS) { + $now += $FIFTEEN_MINS - $diff; + } + my $next = scalar localtime $now; + my @Zeit = split(/ /,$next); + my @Uhrzeit = split(/:/,$Zeit[3]); + my $std = $Uhrzeit[0]; + my $min = $Uhrzeit[1]; + + if($std < 10){ + if(substr($std,0,1) eq "0"){ + $std = substr($std,1,1); + } + } + if($min < 10){ + if(substr($min,0,1) eq "0"){ + $min = substr($min,1,1); + } + } + + + # on () + if ( $cmd eq "on" ) { + Log3 ($name, 2, "KLAFS set $name " . $cmd); + + klafs_getStatus($hash); + my $mode = shift @args; + my $aspxauth = $hash->{Klafs}->{cookie}; + + my $pin = $hash->{Klafs}->{pin}; + my $saunaid = $hash->{Klafs}->{saunaid}; + my $selectedSaunaTemperature = ReadingsVal( $name, "selectedSaunaTemperature", "90" ); + my $selectedSanariumTemperature = ReadingsVal( $name, "selectedSanariumTemperature", "65" ); + my $selectedIrTemperature = ReadingsVal( $name, "selectedIrTemperature", "0" ); + my $selectedHumLevel = ReadingsVal( $name, "selectedHumLevel", "5" ); + my $selectedIrLevel = ReadingsVal( $name, "selectedIrLevel", "0" ); + my $isConnected = ReadingsVal( $name, "isConnected", "true" ); + my $isPoweredOn = ReadingsVal( $name, "isPoweredOn", "false" ); + my $isReadyForUse = ReadingsVal( $name, "isReadyForUse", "false" ); + my $currentTemperature = ReadingsVal( $name, "currentTemperature", "141" ); + if($currentTemperature eq "0"){ + $currentTemperature = "141"; + } + my $currentHumidity = ReadingsVal( $name, "currentHumidity", "0" ); + my $statusCode = ReadingsVal( $name, "statusCode", "0" ); + my $statusMessage = ReadingsVal( $name, "statusMessage", "" ); + if($statusMessage eq ""){ + $statusMessage = 'null'; + } + my $showBathingHour = ReadingsVal( $name, "showBathingHour", "false" ); + my $bathingHours = ReadingsVal( $name, "bathingHours", "0" ); + my $bathingMinutes = ReadingsVal( $name, "bathingMinutes", "0" ); + my $currentHumidityStatus = ReadingsVal( $name, "currentHumidityStatus", "0" ); + my $currentTemperatureStatus = ReadingsVal( $name, "currentTemperatureStatus", "0" ); + + if ( $pin eq "") { + my $msg = "Missing attribute: attr $name pin "; + Log3 ($name, 1, $msg); + return $msg; + }elsif ( $saunaid eq "") { + my $msg = "Missing attribute: attr $name $saunaid "; + Log3 ($name, 1, $msg); + return $msg; + }else{ + my $datauser_cv = ""; + if ( $mode eq "Sauna"){ + # Sauna hat 1 Parameter: Temperatur + #return "Zu wenig Argumente: Temperatur fehlt" if ( @args < 1 ); + my $temperature = shift @args; + if(!looks_like_number($temperature)){ + return "Geben Sie einen nummerischen Wert fuer ein"; + } + if ($temperature >= 10 && $temperature <=100 && $temperature ne ""){ + # Wenn Temperatur zwischen 10 und 100 Grad angegeben wurde: Werte aus der App entnommen + $temperature = $temperature; + }else{ + # Keine Temperatur oder ausser Range, letzter Wert auslesen ggf. auf 90 Grad setzen + $temperature = ReadingsVal( $name, "selectedSaunaTemperature", "" ); + if ($temperature eq "" || $temperature eq 0){ + $temperature = 90; + } + } + my $Time; + $Time = shift @args; + + if(!defined($Time)){ + $Time ="$Uhrzeit[0]:$Uhrzeit[1]"; + } + + if($Time =~ /:/){ + my @Timer = split(/:/,$Time); + $std = $Timer[0]; + $min = $Timer[1]; + if($std < 10){ + if(substr($std,0,1) eq "0"){ + $std = substr($std,1,1); + } + } + if($min < 10){ + + if(substr($min,0,1) eq "0"){ + $min = substr($min,1,1); + } + } + } + if ($std <0 || $std >23 || $min <0 || $min >59){ + return "Checken Sie das Zeitformat $std:$min\n"; + } + $datauser_cv = '{"changedData":{"saunaId":"'.$saunaid.'","saunaSelected":true,"sanariumSelected":false,"irSelected":false,"selectedSaunaTemperature":'.$temperature.',"selectedSanariumTemperature":'.$selectedSanariumTemperature.',"selectedIrTemperature":'.$selectedIrTemperature.',"selectedHumLevel":'.$selectedHumLevel.',"selectedIrLevel":'.$selectedIrLevel.',"selectedHour":'.$std.',"selectedMinute":'.$min.',"isConnected":'.$isConnected.',"isPoweredOn":'.$isPoweredOn.',"isReadyForUse":'.$isReadyForUse.',"currentTemperature":'.$currentTemperature.',"currentHumidity":'.$currentHumidity.',"statusCode":'.$statusCode.',"statusMessage":'.$statusMessage.',"showBathingHour":'.$showBathingHour.',"bathingHours":'.$bathingHours.',"bathingMinutes":'.$bathingMinutes.',"currentHumidityStatus":'.$currentHumidityStatus.',"currentTemperatureStatus":'.$currentTemperatureStatus.'}}'; + }elsif ( $mode eq "Sanarium" ) { + my $temperature = shift @args; + + + if(!looks_like_number($temperature)){ + return "Geben Sie einen nummerischen Wert fuer ein"; + } + if ($temperature >= 40 && $temperature <=75 && $temperature ne ""){ + $temperature = $temperature; + }else{ + # Letzer Wert oder Standardtemperatur + $temperature = ReadingsVal( $name, "selectedSanariumTemperature", "" ); + if ($temperature eq "" || $temperature eq 0){ + $temperature = 65; + } + + } + my $Time; + my $level; + $level = shift @args; + $Time = shift @args; + + if(!defined($Time)){ + $Time ="$Uhrzeit[0]:$Uhrzeit[1]"; + } + + # Parameter level ist optional. Wird in der ersten Variable eine anstelle des Levels eine Uhrzeit gefunden, dann level auf "" setzen und $std,$min setzen + if($level =~ /:/ || $Time =~ /:/){ + if($level =~ /:/){ + my @Timer = split(/:/,$level); + $std = $Timer[0]; + $min = $Timer[1]; + if($std < 10){ + if(substr($std,0,1) eq "0"){ + $std = substr($std,1,1); + } + } + if($min < 10){ + if(substr($min,0,1) eq "0"){ + $min = substr($min,1,1); + } + } + $level = ""; + }else{ + my @Timer = split(/:/,$Time); + $std = $Timer[0]; + $min = $Timer[1]; + if($std < 10){ + if(substr($std,0,1) eq "0"){ + $std = substr($std,1,1); + } + } + if($min < 10){ + if(substr($min,0,1) eq "0"){ + $min = substr($min,1,1); + } + } + } + } + if ($std <0 || $std >23 || $min <0 || $min >59){ + return "Checken Sie das Zeitformat $std:$min\n"; + } + + # Auf volle 10 Minuten runden + #if( substr($min,-1,1) > 0){ + # my $min1 = substr($min,0,1)+1; + # $min = $min1."0"; + # if($min eq 60){ + # $min = "00"; + # $std = $std+1; + # if($std eq 24){ + # $std = "00"; + # } + # } + #} + + if ($level >= 0 && $level <=10 && $level ne ""){ + $level = $level; + }else{ + # Letzer Wert oder Standardlevel + $level = ReadingsVal( $name, "selectedHumLevel", "" ); + if ($level eq ""){ + $level = 5; + } + + } + $datauser_cv = '{"changedData":{"saunaId":"'.$saunaid.'","saunaSelected":false,"sanariumSelected":true,"irSelected":false,"selectedSaunaTemperature":'.$selectedSaunaTemperature.',"selectedSanariumTemperature":'.$temperature.',"selectedIrTemperature":'.$selectedIrTemperature.',"selectedHumLevel":'.$level.',"selectedIrLevel":'.$selectedIrLevel.',"selectedHour":'.$std.',"selectedMinute":'.$min.',"isConnected":'.$isConnected.',"isPoweredOn":'.$isPoweredOn.',"isReadyForUse":'.$isReadyForUse.',"currentTemperature":'.$currentTemperature.',"currentHumidity":'.$currentHumidity.',"statusCode":'.$statusCode.',"statusMessage":'.$statusMessage.',"showBathingHour":'.$showBathingHour.',"bathingHours":'.$bathingHours.',"bathingMinutes":'.$bathingMinutes.',"currentHumidityStatus":'.$currentHumidityStatus.',"currentTemperatureStatus":'.$currentTemperatureStatus.'}}'; + }elsif ( $mode eq "Infrared" ) { + my $temperature = shift @args; + if(!looks_like_number($temperature)){ + return "Geben Sie einen nummerischen Wert fuer ein"; + } + if ($temperature >= 20 && $temperature <=40 && $temperature ne ""){ + $temperature = $temperature; + }else{ + # Letzer Wert oder Standardtemperatur + $temperature = ReadingsVal( $name, "selectedIrTemperature", "" ); + if ($temperature eq "" || $temperature eq 0){ + $temperature = 35; + } + } + my $Time; + my $level; + $level = shift @args; + $Time = shift @args; + + if(!defined($Time)){ + $Time ="$Uhrzeit[0]:$Uhrzeit[1]"; + } + + # Parameter level ist optional. Wird in der ersten Variable eine anstelle des Levels eine Uhrzeit gefunden, dann level auf "" setzen und $std,$min setzen + if($level =~ /:/ || $Time =~ /:/){ + if($level =~ /:/){ + my @Timer = split(/:/,$level); + $std = $Timer[0]; + $min = $Timer[1]; + if($std < 10){ + if(substr($std,0,1) eq "0"){ + $std = substr($std,1,1); + } + } + if($min < 10){ + if(substr($min,0,1) eq "0"){ + $min = substr($min,1,1); + } + } + $level = ""; + }else{ + my @Timer = split(/:/,$Time); + $std = $Timer[0]; + $min = $Timer[1]; + if($std < 10){ + if(substr($std,0,1) eq "0"){ + $std = substr($std,1,1); + } + } + if($min < 10){ + if(substr($min,0,1) eq "0"){ + $min = substr($min,1,1); + } + } + } + } + if ($std <0 || $std >23 || $min <0 || $min >59){ + return "Checken Sie das Zeitformat $std:$min\n"; + } + + if ($level >= 0 && $level <=10 && $level ne "" ){ + $level = $level; + }else{ + # Letzer Wert oder Standardlevel + $level = ReadingsVal( $name, "selectedIrLevel", "" ); + if ($level eq ""){ + $level = 5; + } + } + $datauser_cv = '{"changedData":{"saunaId":"'.$saunaid.'","saunaSelected":false,"sanariumSelected":false,"irSelected":true,"selectedSaunaTemperature":'.$selectedSaunaTemperature.',"selectedSanariumTemperature":'.$selectedSanariumTemperature.',"selectedIrTemperature":'.$temperature.',"selectedHumLevel":'.$selectedHumLevel.',"selectedIrLevel":'.$level.',"selectedHour":'.$std.',"selectedMinute":'.$min.',"isConnected":'.$isConnected.',"isPoweredOn":'.$isPoweredOn.',"isReadyForUse":'.$isReadyForUse.',"currentTemperature":'.$currentTemperature.',"currentHumidity":'.$currentHumidity.',"statusCode":'.$statusCode.',"statusMessage":'.$statusMessage.',"showBathingHour":'.$showBathingHour.',"bathingHours":'.$bathingHours.',"bathingMinutes":'.$bathingMinutes.',"currentHumidityStatus":'.$currentHumidityStatus.',"currentTemperatureStatus":'.$currentTemperatureStatus.'}}'; + + }else{ + $datauser_cv = '{"changedData":{"saunaId":"'.$saunaid.'","saunaSelected":true,"sanariumSelected":false,"irSelected":false,"selectedSaunaTemperature":90,"selectedSanariumTemperature":'.$selectedSanariumTemperature.',"selectedIrTemperature":'.$selectedIrTemperature.',"selectedHumLevel":'.$selectedHumLevel.',"selectedIrLevel":'.$selectedIrLevel.',"selectedHour":'.$std.',"selectedMinute":'.$min.',"isConnected":'.$isConnected.',"isPoweredOn":'.$isPoweredOn.',"isReadyForUse":'.$isReadyForUse.',"currentTemperature":'.$currentTemperature.',"currentHumidity":'.$currentHumidity.',"statusCode":'.$statusCode.',"statusMessage":'.$statusMessage.',"showBathingHour":'.$showBathingHour.',"bathingHours":'.$bathingHours.',"bathingMinutes":'.$bathingMinutes.',"currentHumidityStatus":'.$currentHumidityStatus.',"currentTemperatureStatus":'.$currentTemperatureStatus.'}}'; + } + + Log3 ($name, 4, "$name - JSON ON: $datauser_cv"); + # 1) Werte aendern + #print "Mode: ". $mode . " Temperature: ". $temperature . " Level: " .$level ."\n$datauser_cv\n\n"; + my $header_cv = "Content-Type: application/json\r\n". + "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.71 Safari/537.36\r\n". + "Cookie: $aspxauth"; + HttpUtils_BlockingGet({ + url => "https://sauna-app.klafs.com//Control/PostConfigChange", + timeout => 5, + hash => $hash, + method => "POST", + header => $header_cv, + data => $datauser_cv, + }); + + + my $state_onoff = ReadingsVal( $name, "isPoweredOn", "false" ); + + # Einschalten, wenn Sauna aus ist. + if($state_onoff eq "false"){ + my $header_af = "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.71 Safari/537.36\r\n". + "Cookie: $aspxauth"; + my $datauser_af = "s=$saunaid"; + # 2 Steps: 2) Antiforgery erzeugen; 3) Einschalten + HttpUtils_NonblockingGet({ + url => "https://sauna-app.klafs.com/Control/EnterPin", + timeout => 5, + hash => $hash, + method => "POST", + header => $header_af, + data => $datauser_af, + callback=>sub($$$){ + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $header = $param->{httpheader}; + Log3 ($name, 5, "header: $header"); + Log3 ($name, 5, "Data: $data"); + Log3 ($name, 5, "Error: $err"); + readingsBeginUpdate ($hash); + for my $cookie ($header =~ m/set-cookie: ?(.*)/gi) { + $cookie =~ /([^,; ]+)=([^,;\s\v]+)[;,\s\v]*([^\v]*)/; + my $antiforgery = $1 . "=" .$2 .";"; + my $antiforgery_date = strftime("%Y-%m-%d %H:%M:%S", localtime(time())); + readingsBulkUpdate( $hash, "antiforgery_date", "$antiforgery_date", 1 ); + Log3 ($name, 5, "$name: Antiforgery found: $antiforgery"); + $hash->{Klafs}->{antiforgery} = $antiforgery; + } + readingsEndUpdate($hash, 1); + + # 2) Einschalten + my $headeron = "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.71 Safari/537.36\r\n". + "Cookie: $aspxauth"; + my $antiforgery = $hash->{Klafs}->{antiforgery}; + my $datauseron = "$antiforgery&Pin=$pin&saunaId=$saunaid"; + HttpUtils_NonblockingGet({ + url => "https://sauna-app.klafs.com/Control/EnterPin", + timeout => 5, + hash => $hash, + method => "POST", + header => $headeron, + data => $datauseron, + callback => sub($$$){ + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + Log3 ($name, 5, "header: $header"); + Log3 ($name, 5, "Data: $data"); + Log3 ($name, 5, "Error: $err"); + if($data=~/
      • /) { + readingsBeginUpdate ($hash); + for my $err ($data =~ m /
        • ?(.*)<\/li>/) { + my %umlaute = ("ä" => "ae", "ü" => "ue", "Ä" => "Ae", "Ö" => "Oe", "ö" => "oe", "Ü" => "Ue", "ß" => "ss"); + my $umlautkeys = join ("|", keys(%umlaute)); + $err=~ s/($umlautkeys)/$umlaute{$1}/g; + Log3 ($name, 1, "KLAFS $name: $err"); + readingsBulkUpdate( $hash, "last_errormsg", "$err", 1 ); + } + readingsEndUpdate($hash, 1); + }else{ + $power = "on"; + Log3 ($name, 3, "Sauna on"); + readingsBeginUpdate ($hash); + readingsBulkUpdate( $hash, "power", $power, 1 ); + readingsBulkUpdate( $hash, "last_errormsg", "0", 1 ); + readingsEndUpdate($hash, 1); + klafs_getStatus($hash); + } + } + }); + } + }); + } + } + + # sauna off + }elsif ( $cmd eq "off" ) { + Log3 ($name, 2, "KLAFS set $name " . $cmd); + klafs_getStatus($hash); + + my $aspxauth = $hash->{Klafs}->{cookie}; + + my $saunaid = $hash->{Klafs}->{saunaid}; + my $saunaSelected = ReadingsVal( $name, "saunaSelected", "true" ); + my $sanariumSelected = ReadingsVal( $name, "sanariumSelected", "false" ); + my $irSelected = ReadingsVal( $name, "irSelected", "false" ); + + my $selectedSaunaTemperature = ReadingsVal( $name, "selectedSaunaTemperature", "90" ); + my $selectedSanariumTemperature = ReadingsVal( $name, "selectedSanariumTemperature", "65" ); + my $selectedIrTemperature = ReadingsVal( $name, "selectedIrTemperature", "0" ); + my $selectedHumLevel = ReadingsVal( $name, "selectedHumLevel", "5" ); + my $selectedIrLevel = ReadingsVal( $name, "selectedIrLevel", "0" ); + my $selectedHour = ReadingsVal( $name, "selectedHour", "0" ); + my $selectedMinute = ReadingsVal( $name, "selectedMinute", "0" ); + + my $isConnected = ReadingsVal( $name, "isConnected", "true" ); + my $isPoweredOn = ReadingsVal( $name, "isPoweredOn", "false" ); + my $isReadyForUse = ReadingsVal( $name, "isReadyForUse", "false" ); + my $currentTemperature = ReadingsVal( $name, "currentTemperature", "141" ); + if($currentTemperature eq "0"){ + $currentTemperature = "141"; + } + my $currentHumidity = ReadingsVal( $name, "currentHumidity", "0" ); + my $statusCode = ReadingsVal( $name, "statusCode", "0" ); + my $statusMessage = ReadingsVal( $name, "statusMessage", "" ); + if($statusMessage eq ""){ + $statusMessage = 'null'; + } + my $showBathingHour = ReadingsVal( $name, "showBathingHour", "false" ); + my $bathingHours = ReadingsVal( $name, "bathingHours", "0" ); + my $bathingMinutes = ReadingsVal( $name, "bathingMinutes", "0" ); + my $currentHumidityStatus = ReadingsVal( $name, "currentHumidityStatus", "0" ); + my $currentTemperatureStatus = ReadingsVal( $name, "currentTemperatureStatus", "0" ); + + if ($saunaid eq ""){ + my $msg = "Missing attribute: attr $name saunaid "; + Log3 ($name, 1, $msg); + return $msg; + }else{ + + my $header = "Content-Type: application/json\r\n". + "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.71 Safari/537.36\r\n". + "Cookie: $aspxauth"; + + my $datauser_end = '{"changedData":{"saunaId":"'.$saunaid.'","saunaSelected":'.$saunaSelected.',"sanariumSelected":'.$sanariumSelected.',"irSelected":'.$irSelected.',"selectedSaunaTemperature":'.$selectedSaunaTemperature.',"selectedSanariumTemperature":'.$selectedSanariumTemperature.',"selectedIrTemperature":'.$selectedIrTemperature.',"selectedHumLevel":'.$selectedHumLevel.',"selectedIrLevel":'.$selectedIrLevel.',"selectedHour":'.$selectedHour.',"selectedMinute":'.$selectedMinute.',"isConnected":'.$isConnected.',"isPoweredOn":'.$isPoweredOn.',"isReadyForUse":'.$isReadyForUse.',"currentTemperature":'.$currentTemperature.',"currentHumidity":'.$currentHumidity.',"statusCode":'.$statusCode.',"statusMessage":'.$statusMessage.',"showBathingHour":'.$showBathingHour.',"bathingHours":'.$bathingHours.',"bathingMinutes":'.$bathingMinutes.',"currentHumidityStatus":'.$currentHumidityStatus.',"currentTemperatureStatus":'.$currentTemperatureStatus.'}}'; + Log3 ($name, 4, "$name - JSON_OFF: $datauser_end"); + + HttpUtils_BlockingGet({ + url => "https://sauna-app.klafs.com/Control/PostPowerOff", + timeout => 5, + hash => $hash, + method => "POST", + header => $header, + data => $datauser_end, + }); + + HttpUtils_BlockingGet({ + url => "https://sauna-app.klafs.com//Control/PostConfigChange", + timeout => 5, + hash => $hash, + method => "POST", + header => $header, + data => $datauser_end, + }); + $power = "off"; + readingsBeginUpdate ($hash); + readingsBulkUpdate( $hash, "power", $power, 1 ); + readingsEndUpdate($hash, 1); + Log3 ($name, 3, "Sauna off"); + } + }elsif ( $cmd eq "update" ) { + Klafs_DoUpdate($hash); + }elsif ( $cmd eq "ResetLoginFailures" ) { + readingsBeginUpdate ($hash); + readingsBulkUpdate( $hash, "LoginFailures", "0", 1 ); + readingsEndUpdate($hash, 1); + $hash->{Klafs}->{LoginFailures} =0; + }elsif($cmd eq 'password'){ + + my $password = shift @args; + print "$name - Passwort1: ".$password."\n"; + my ($res, $error) = defined $password ? $hash->{helper}->{passObj}->setStorePassword($name, $password) : $hash->{helper}->{passObj}->setDeletePassword($name); + + if(defined $error && !defined $res) + { + Log3($name, 1, "$name - could not update password"); + return "Error while updating the password - $error"; + }else{ + Log3($name, 1, "$name - password successfully saved"); + } + return; + }else{ + return "Unknown argument $cmd, choose one of " + . join( " ", + map { "$_" . ( $sets{$_} ? ":$sets{$_}" : "" ) } keys %sets ); + } + return; +} + +############################################################## +# +# UPDATE FUNCTIONS +# +############################################################## + +sub Klafs_Whoami() { return (split('::',(caller(1))[3]))[1] || ''; } +sub Klafs_Whowasi() { return (split('::',(caller(2))[3]))[1] || ''; } + +sub Klafs_DoUpdate { + my ($hash) = @_; + my ($name,$self) = ($hash->{NAME},Klafs_Whoami()); + Log3 ($name, 5, "$name doUpdate() called."); + + RemoveInternalTimer($hash); + if (Klafs_CONNECTED($hash) eq 'disabled') { + Log3 ($name, 3, "$name - Device is disabled."); + return; + } + + + + InternalTimer( time() + $hash->{Klafs}->{interval}, $self, $hash, 0 ); + if (time() >= $hash->{Klafs}->{expire} && $hash->{Klafs}->{CONNECTED} ne "disconnected" && $hash->{Klafs}->{CONNECTED} ne "initialized") { + Log3 ($name, 2, "$name - LOGIN TOKEN MISSING OR EXPIRED - DoUpdate"); + Klafs_CONNECTED($hash,'disconnected',1); + + } elsif ($hash->{Klafs}->{CONNECTED} eq 'connected') { + Log3 ($name, 4, "$name - Update with device: " . $hash->{Klafs}->{saunaid}); + klafs_getStatus($hash); + } elsif ($hash->{Klafs}->{CONNECTED} eq 'disconnected' || $hash->{Klafs}->{CONNECTED} eq "initialized") { + # Das übernimmt eigentlich das notify unten. Hier wird es gebraucht, wenn innerhalb 5 Minuten nach den letzten Reconnect die Verbindung abbricht, dann muss der Login das DoUpdate übernehmen + # Login wird 5 Minuten nach den letzten Login verhindert vom Modul. + Log3 ($name, 4, "$name - Reconnect within 5 Minutes"); + Klafs_Auth($hash); + } elsif ($hash->{Klafs}->{CONNECTED} eq 'authenticated') { + Log3 ($name, 4, "$name - Update with device: " . $hash->{Klafs}->{saunaid}); + klafs_getStatus($hash); + } +return; +} + +1; + +__END__ + +=pod + +=encoding utf8 +=item device +=item summary Klafs Sauna control +=item summary_DE Klafs Saunasteuerung +=begin html + + +

          Klafs Sauna control

          +
            + The module receives data and sends commands to the Klafs app.
            + In the current version, the sauna can be turned on and off, and the parameters can be set. +
            +
            + Requirements +
              +
              + The SaunaID must be known. This can be found in the URL directly after logging in to the app (http://sauna-app.klafs.com).
              + The ID is there with the parameter ?s=xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxxxxxx.
              + In addition, the user name and password must be known, as well as the PIN that was defined on the sauna module. +
            +
            + + Definition and use +
              +
              + The module is defined without mandatory parameters.
              + User name, password, refresh interval, saunaID and pin defined on the sauna module are set as attributes.
              +
            +
              + Definition of the module +
              +
            +
              +
              + define <name> KLAFS <Intervall>
              + attr <name> <saunaid> <xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx>
              + attr <name> <username> <xxxxxx>
              + attr <name> <pin> <1234>
              + attr <name> <interval> <60>
              +
              + set <name> <password> <secret>
              +
            +
          +
            + Example of a module definition:
            +
              +
              + define mySauna KLAFS
              + attr mySauna saunaid ab0c123d-ef4g-5h67-8ij9-k0l12mn34op5
              + attr mySauna username user01
              + attr mySauna pin 1234
              + attr mySauna interval 60
              +
              + set mySauna password secret
              +
            + + Set +
            +
              + + + + + + + + + + + + + + + + + + + + + +
              ResetLoginFailuresIf the login fails, the Reading LoginFailures is set to 1. This locks the automatic login from this module.
              + Klafs locks the account after 3 failed attempts. So that not automatically 3 wrong logins are made in a row.
              + ResetLoginFailures resets the reading to 0. Before this, you should have successfully logged in to the app or sauna-app.klafs.com
              + or reset the password. Successful login resets the number of failed attempts in the Klafs cloud. +
              offTurns off the sauna|sanarium|infrared - without parameters.
              on + set <name> on without parameters - default sauna 90 degrees
              + set <name> on Sauna 90 - 3 parameters possible: "Sauna" with temperature [10-100]; Optional time [19:30].
              + set <name> on Saunarium 65 5 - 4 parameters possible: "Sanarium" with temperature [40-75]; Optional HumidtyLevel [0-10] and time [19:30].
              + set <name> on Infrared 30 5 - 4 parameters possible: "Infrared" with temperature [20-40] and IR Level [0-10]; Optional time [19:30].
              + Infrared works, but is not supported because no test environment is available. +
              UpdateRefreshes the readings and performs a login if necessary.
              +
            +
            + Get +
            +
              + + + + + + + + + + + + + + +
              SaunaIDReads out the available SaunaIDs.
              helpDisplays the help for the SET commands.
              +
            +
            + + Readings +
              +
              + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
              Mode Sauna, Sanarium or Infrared
              LoginFailuresFailed login attempts to the app. If the value is set to 1, no login attempts are made by the module. See set <name> ResetLoginFailures
              RestzeitRemaining bathing time. Value from bathingHours and bathingMinutes
              antiforgery_date Date of the antiforgery cookie. This is generated when the program is switched on.
              bathingHours Hour of remaining bath time
              bathingMinutesMinute of remaining bath time
              cookieExpireLogincookie runtime. 2 days
              currentHumidityIn sanarium mode. Percentage humidity
              currentHumidityStatusundefined reading
              currentTemperatureTemperature in the sauna. 0 When the sauna is off
              currentTemperatureStatusundefined reading
              firstnameDefined first name in the app
              irSelectedtrue/false - Currently set operating mode Infrared
              isConnectedtrue/false - Sauna connected to the app
              isPoweredOntrue/false - Sauna is on/off
              langcloudLanguage set in the app
              last_errormsgLast error message. Often that the safety check door contact was not performed.
              + Safety check must be performed with the reed contact on the door +
              lastnameDefined last name in the app
              mailDefined mail address in the app
              sanariumSelectedtrue/false - Currently set operating mode Sanarium
              saunaIdSaunaID defined as an attribute
              saunaSelectedtrue/false - Currently set operating mode Sauna
              selectedHourDefined switch-on time. Here hour
              selectedHumLevelDefined humidity levels in sanarium operation
              selectedIrLevelDefined intensity in infrared mode
              selectedIrTemperatureDefined infrotemperature
              selectedMinuteDefined switch-on time. Here minute
              selectedSanariumTemperatureDefined sanarium temperature
              selectedSaunaTemperatureDefined sauna temperature
              showBathingHourtrue/false - not further defined. true, if sauna is on.
              standbytimeDefined standby time in the app.
              poweron/off
              statusCodeundefined reading
              statusMessageundefined reading
              usernameUsername defined as an attribute
              +
              +
            +
          +=end html + +=begin html_DE + + +

          Klafs Saunasteuerung

          +
            + Das Modul empfängt Daten und sendet Befehle an die Klafs App.
            + In der aktuellen Version kann die Sauna an- bzw. ausgeschaltet werden und dabei die Parameter mitgegeben werden. +
            +
            + Voraussetzungen +
              +
              + Die SaunaID muss bekannt sein. Diese findet sich in der URL direkt nach dem Login an der App (http://sauna-app.klafs.com).
              + Dort steht die ID mit dem Parameter ?s=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
              + Darüberhinaus müssen Benutzername und Passwort bekannt sein sowie die PIN, die am Saunamodul definiert wurde. +
            +
            + + Definition und Verwendung +
              +
              + Das Modul wird ohne Pflichtparameter definiert.
              + Benutzername, Passwort, Refresh-Intervall, SaunaID, und am Saunamodul definierte Pin werden als Attribute gesetzt.
              +
            +
              + Definition des Moduls +
              +
            +
              +
              + define <name> KLAFS <Intervall>
              + attr <name> <saunaid> <xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx>
              + attr <name> <username> <xxxxxx>
              + attr <name> <pin> <1234>
              + attr <name> <interval> <60>
              +
              + set <name> <password> <xxxxxx>
              +
            +
          +
            + Beispiel für eine Moduldefinition:
            +
              +
              + define mySauna KLAFS
              + attr mySauna saunaid ab0c123d-ef4g-5h67-8ij9-k0l12mn34op5
              + attr mySauna username user01
              + attr mySauna pin 1234
              + attr mySauna interval 60
              +
              + set mySauna password geheim
              +
            + + Set +
            +
              + + + + + + + + + + + + + + + + + + + + + +
              ResetLoginFailuresBei fehlerhaftem Login wird das Reading LoginFailures auf 1 gesetzt. Damit ist der automatische Login vom diesem Modul gesperrt.
              + Klafs sperrt den Account nach 3 Fehlversuchen. Damit nicht automatisch 3 falsche Logins hintereinander gemacht werden.
              + ResetLoginFailures setzt das Reading wieder auf 0. Davor sollte man sich erfolgreich an der App bzw. unter sauna-app.klafs.com
              + angemeldet bzw. das Passwort zurückgesetzt haben. Erfolgreicher Login resetet die Anzahl der Fehlversuche in der Klafs-Cloud. +
              offSchaltet die Sauna|Sanarium|Infrared aus - ohne Parameter.
              on + set <name> on ohne Parameter - Default Sauna 90 Grad
              + set <name> on Sauna 90 - 3 Parameter möglich: "Sauna" mit Temperatur [10-100]; Optional Uhrzeit [19:30]
              + set <name> on Saunarium 65 5 - 4 Parameter möglich: "Sanarium" mit Temperatur [40-75]; Optional HumidtyLevel [0-10] und Uhrzeit [19:30]
              + set <name> on Infrared 30 5 - 4 Parameter möglich: "Infrarot" mit Temperatur [20-40] und IR Level [0-10]; Optional Uhrzeit [19:30]
              + Infrarot funktioniert, ist aber nicht supported, da keine Testumgebung verfügbar. +
              UpdateRefresht die Readings und führt ggf. ein Login durch.
              +
            +
            + Get +
            +
              + + + + + + + + + + + + + + +
              SaunaIDLiest die verfügbaren SaunaIDs aus.
              helpZeigt die Hilfe für die SET Befehle an.
              +
            +
            + + Readings +
              +
              + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
              Mode Sauna, Sanarium oder Infrared
              LoginFailuresFehlerhafte Loginversuche an der App. Steht der Wert auf 1, werden vom Modul keine Loginversuche unternommen. Siehe set <name> ResetLoginFailures
              RestzeitRestliche Badezeit. Wert aus bathingHours und bathingMinutes
              antiforgery_date Datum des Antiforgery Cookies. Dieses wird beim Einschalten erzeugt.
              bathingHours Stunde der Restbadezeit
              bathingMinutesMinute der Restbadezeit
              cookieExpireLaufzeit des Logincookies. 2 Tage
              currentHumidityIm Sanariumbetrieb. Prozentuale Luftfeuchtigkeit
              currentHumidityStatusnicht definiertes Reading
              currentTemperatureTemperatur in der Sauna. 0 wenn die Sauna aus ist
              currentTemperatureStatusnicht definiertes Reading
              firstnameDefinierter Vorname in der App
              irSelectedtrue/false - Aktuell eingestellter Betriebsmodus Infrarot
              isConnectedtrue/false - Sauna mit der App verbunden
              isPoweredOntrue/false - Sauna ist an/aus
              langcloudEingestellte Sprache in der App
              last_errormsgLetzte Fehlermeldung. Häufig, dass die Sicherheitsüberprüfung Türkontakt nicht durchgeführt wurde.
              + Sicherheitsüberprüfung muss durchgeführt werden mit dem Reedkontakt an der Tür. +
              lastnameDefinierter Nachname in der App
              mailDefinierte Mailadresse in der App
              sanariumSelectedtrue/false - Aktuell eingestellter Betriebsmodus Sanarium
              saunaIdSaunaID, die als Attribut definiert wurde
              saunaSelectedtrue/false - Aktuell eingestellter Betriebsmodus Sauna
              selectedHourDefinierte Einschaltzeit. Hier Stunde
              selectedHumLevelDefinierte Luftfeuchtigkeitslevel im Sanariumbetrieb
              selectedIrLevelDefinierte Intensivität im Infrarotbetrieb
              selectedIrTemperatureDefinierte Infrottemperatur
              selectedMinuteDefinierte Einschaltzeit. Hier Minute
              selectedSanariumTemperatureDefinierte Sanariumtemperatur
              selectedSaunaTemperatureDefinierte Saunatemperatur
              showBathingHourtrue/false - nicht näher definiert. true, wenn Sauna an ist.
              standbytimeDefinierte Standbyzeit in der App.
              poweron/off
              statusCodenicht definiertes Reading
              statusMessagenicht definiertes Reading
              usernameBenutzername, der als Attribut definiert wurde
              +
              +
            +
          +=end html_DE +=cut