# $Id: 70_Klafs.pm 26433 2022-09-20 15:32:58Z xasher $ ############################################################################## # # 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 # ############################################################################## package FHEM::Klafs; use strict; use warnings; sub ::Klafs_Initialize { goto &Initialize } use Carp qw(carp); use Scalar::Util qw(looks_like_number); use Time::HiRes qw(gettimeofday); use JSON qw(decode_json encode_json); use Time::Piece; use Time::Local; use HttpUtils; use GPUtils qw(:all); use FHEM::Core::Authentication::Passwords qw(:ALL); my %sets = ( off => 'noArg', password => '', on => '', ResetLoginFailures => '', update => 'noArg', ); my %gets = ( help => 'noArg', SaunaID => 'noArg', ); BEGIN { GP_Import(qw( readingsBeginUpdate readingsBulkUpdate readingsEndUpdate readingsSingleUpdate Log3 defs init_done InternalTimer strftime RemoveInternalTimer readingFnAttributes AttrVal notifyRegexpChanged ReadingsVal HttpUtils_NonblockingGet HttpUtils_BlockingGet )) }; ################################### sub Initialize { my $hash = shift; Log3 ($hash, 5, 'Klafs_Initialize: Entering'); $hash->{DefFn} = \&Define; $hash->{UndefFn} = \&Undef; $hash->{SetFn} = \&Set; $hash->{AttrFn} = \&Attr; $hash->{GetFn} = \&Get; $hash->{RenameFn} = \&Rename; $hash->{AttrList} = 'username saunaid pin interval disable:1,0 ' . $main::readingFnAttributes; return; } sub 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 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 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 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 ne ReadingsVal($hash->{NAME},'state',''); } else { readingsBulkUpdate($hash,'state',$set) if $set ne 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}; 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} ; } my $power = $entries->{isPoweredOn} eq q{true} ? 'on' : $entries->{isPoweredOn} eq q{false} ? 'off' : 0; $entries->{power} = $power; $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 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 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; # doppelte Leerzeichen bei einstelligen Datumsangaben entfernen $next =~ tr/ //s; my @Zeit = split(/ /,$next); my @Uhrzeit = split(/:/,$Zeit[3]); my $std = $Uhrzeit[0]; my $min = $Uhrzeit[1]; # print "Decoded Zeit:\n".Dumper(@Zeit); #Decoded Zeit: #$VAR1 = 'Mon'; #$VAR2 = 'Jun'; #$VAR3 = '20'; #$VAR4 = '15:15:00'; #$VAR5 = '2022'; 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 Klafs_DoUpdate() called."); RemoveInternalTimer($hash); if (Klafs_CONNECTED($hash) eq 'disabled') { Log3 ($name, 3, "$name - Device is disabled."); return; } InternalTimer(time() + $hash->{Klafs}->{interval}, \&Klafs_DoUpdate, $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 - Klafs_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 Klafs_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
              ResetLoginFailures If 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.
              off Turns 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.
              Update Refreshes the readings and performs a login if necessary.

            Get
              SaunaID Reads out the available SaunaIDs.
              help Displays the help for the SET commands.

            Readings

              Mode Sauna, Sanarium or Infrared
              LoginFailures Failed login attempts to the app. If the value is set to 1, no login attempts are made by the module. See set <name> ResetLoginFailures
              Restzeit Remaining 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
              bathingMinutes Minute of remaining bath time
              cookieExpire Logincookie runtime. 2 days
              currentHumidity In sanarium mode. Percentage humidity
              currentHumidityStatus undefined reading
              currentTemperature Temperature in the sauna. 0 When the sauna is off
              currentTemperatureStatus undefined reading
              firstname Defined first name in the app
              irSelected true/false - Currently set operating mode Infrared
              isConnected true/false - Sauna connected to the app
              isPoweredOn true/false - Sauna is on/off
              langcloud Language set in the app
              last_errormsg Last error message. Often that the safety check door contact was not performed.
              Safety check must be performed with the reed contact on the door
              lastname Defined last name in the app
              mail Defined mail address in the app
              sanariumSelected true/false - Currently set operating mode Sanarium
              saunaId SaunaID defined as an attribute
              saunaSelected true/false - Currently set operating mode Sauna
              selectedHour Defined switch-on time. Here hour
              selectedHumLevel Defined humidity levels in sanarium operation
              selectedIrLevel Defined intensity in infrared mode
              selectedIrTemperature Defined infrotemperature
              selectedMinute Defined switch-on time. Here minute
              selectedSanariumTemperature Defined sanarium temperature
              selectedSaunaTemperature Defined sauna temperature
              showBathingHour true/false - not further defined. true, if sauna is on.
              standbytime Defined standby time in the app.
              power on/off
              statusCode undefined reading
              statusMessage undefined reading
              username Username 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
              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 zurückgesetzt haben. Erfolgreicher Login resetet die Anzahl der Fehlversuche in der Klafs-Cloud.
              off Schaltet 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.
              Update Refresht die Readings und führt ggf. ein Login durch.

            Get
              SaunaID Liest die verfügbaren SaunaIDs aus.
              help Zeigt die Hilfe für die SET Befehle an.

            Readings

              Mode Sauna, Sanarium oder Infrared
              LoginFailures Fehlerhafte Loginversuche an der App. Steht der Wert auf 1, werden vom Modul keine Loginversuche unternommen. Siehe set <name> ResetLoginFailures
              Restzeit Restliche Badezeit. Wert aus bathingHours und bathingMinutes
              antiforgery_date Datum des Antiforgery Cookies. Dieses wird beim Einschalten erzeugt.
              bathingHours Stunde der Restbadezeit
              bathingMinutes Minute der Restbadezeit
              cookieExpire Laufzeit des Logincookies. 2 Tage
              currentHumidity Im Sanariumbetrieb. Prozentuale Luftfeuchtigkeit
              currentHumidityStatus nicht definiertes Reading
              currentTemperature Temperatur in der Sauna. 0 wenn die Sauna aus ist
              currentTemperatureStatus nicht definiertes Reading
              firstname Definierter Vorname in der App
              irSelected true/false - Aktuell eingestellter Betriebsmodus Infrarot
              isConnected true/false - Sauna mit der App verbunden
              isPoweredOn true/false - Sauna ist an/aus
              langcloud Eingestellte Sprache in der App
              last_errormsg Letzte 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.
              lastname Definierter Nachname in der App
              mail Definierte Mailadresse in der App
              sanariumSelected true/false - Aktuell eingestellter Betriebsmodus Sanarium
              saunaId SaunaID, die als Attribut definiert wurde
              saunaSelected true/false - Aktuell eingestellter Betriebsmodus Sauna
              selectedHour Definierte Einschaltzeit. Hier Stunde
              selectedHumLevel Definierte Luftfeuchtigkeitslevel im Sanariumbetrieb
              selectedIrLevel Definierte Intensivität im Infrarotbetrieb
              selectedIrTemperature Definierte Infrottemperatur
              selectedMinute Definierte Einschaltzeit. Hier Minute
              selectedSanariumTemperature Definierte Sanariumtemperatur
              selectedSaunaTemperature Definierte Saunatemperatur
              showBathingHour true/false - nicht näher definiert. true, wenn Sauna an ist.
              standbytime Definierte Standbyzeit in der App.
              power on/off
              statusCode nicht definiertes Reading
              statusMessage nicht definiertes Reading
              username Benutzername, der als Attribut definiert wurde

          =end html_DE =cut