# $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 readingsDelete )) }; ################################### 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/120.0.0.0 Safari/537.36\r\n". "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7r\n". "Accept-Encoding: gzip, deflate, br\r\n". "Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7"; my $datauser = "UserName=$username&Password=$password&RememberMe=false"; if ($hash->{Klafs}->{LoginFailures} eq "0"){ HttpUtils_NonblockingGet({ url => "https://sauna-app-19.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; charset=utf-8\r\n". "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\n". "Accept: text/plain, */*; q=0.01r\n". "Accept-Encoding: gzip, deflate, br\r\n". "Accept-Language: de,en;q=0.7,en-US;q=0.3\r\n". "Cookie: $aspxauth"; my $datauser_gs = '{"saunaId":"'.$saunaid.'"}'; HttpUtils_NonblockingGet({ url => "https://sauna-app-19.klafs.com/SaunaApp/GetData?id=$saunaid", timeout => 5, hash => $hash, method => "GET", header => $header_gs, data => $datauser_gs, callback => \&klafs_getStatusResponse, }); #Name Vorname Mail Benutzername #GET Anfrage mit ASPXAUTH my $header_user = "Content-Type: application/json; charset=utf-8\r\n". "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\n". "Accept: text/plain, */*; q=0.01r\n". "Accept-Encoding: gzip, deflate, br\r\n". "Accept-Language: de,en;q=0.7,en-US;q=0.3\r\n". "Cookie: $aspxauth"; HttpUtils_NonblockingGet({ url => "https://sauna-app-19.klafs.com/Account/ChangeProfile", timeout => 5, hash => $hash, method => "GET", header => $header_user, callback => \&Klafs_GETProfile, }); my $header_set = "Content-Type: application/json; charset=utf-8\r\n". "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\n". "Accept: text/plain, */*; q=0.01r\n". "Accept-Encoding: gzip, deflate, br\r\n". "Accept-Language: de,en;q=0.7,en-US;q=0.3\r\n". "Cookie: $aspxauth"; HttpUtils_NonblockingGet({ url => "https://sauna-app-19.klafs.com/SaunaApp/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); } } ## unset ErrorMessageHeader. Dort steht "Fehler" drin. Wert wird mitgeliefert auch wenn ErrorMessage leer ist. Bei Fehler wird Reading last_errormsg gesetzt readingsDelete($hash, "ErrorMessageHeader"); 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 -> Starten mit zuletzt verwendeten Werten | | | 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] | | | Infrarot ist nicht supported, da keine Testumgebung verfuegbar. | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |off | Schaltet die Sauna|Sanarium 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) - GetSauna ID start."); my $header = "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\n". "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\n". "Accept-Encoding: gzip, deflate, br\r\n". "Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7\r\n". "Cookie: $aspxauth"; HttpUtils_NonblockingGet({ url => "https://sauna-app-19.klafs.com/SaunaApp/ChangeSettings", 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 $returnwert1; my $returnwert2; Log3 ($name, 5, "$name ($self) - GetSauna ID Ende."); 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 $returnwert1 = ""; $returnwert2 = ""; if($data=~//) { for my $output ($data =~ m /(.*?)<\/tr>/gis) { $output=~ m/