################################################################ # $Id$ # # 93_DbLog.pm # written by Dr. Boris Neubert 2007-12-30 # e-mail: omega at online dot de # # modified and maintained by Tobias Faust since 2012-06-26 # e-mail: tobias dot faust at online dot de # # reduceLog() created by Claudiu Schuster (rapster) # ################################################################ package main; use strict; use warnings; use DBI; use Data::Dumper; my %columns = ("DEVICE" => 64, "TYPE" => 64, "EVENT" => 512, "READING" => 64, "VALUE" => 128, "UNIT" => 32 ); sub dbReadings($@); ################################################################ sub DbLog_Initialize($) { my ($hash) = @_; $hash->{DefFn} = "DbLog_Define"; $hash->{UndefFn} = "DbLog_Undef"; $hash->{NotifyFn} = "DbLog_Log"; $hash->{SetFn} = "DbLog_Set"; $hash->{GetFn} = "DbLog_Get"; $hash->{AttrFn} = "DbLog_Attr"; $hash->{SVG_regexpFn} = "DbLog_regexpFn"; $hash->{ShutdownFn} = "DbLog_Shutdown"; $hash->{AttrList} = "disable:0,1 ". "DbLogType:Current,History,Current/History ". "shutdownWait ". "suppressUndef:0,1 ". "DbLogSelectionMode:Exclude,Include,Exclude/Include"; # Das Attribut DbLogSelectionMode legt fest, wie die Device-Spezifischen Atrribute # DbLogExclude und DbLogInclude behandelt werden sollen. # - Exclude: Es wird nur das Device-spezifische Attribut Exclude beruecksichtigt, # d.h. generell wird alles geloggt, was nicht per DBLogExclude ausgeschlossen wird # - Include: Es wird nur das Device-spezifische Attribut Include beruecksichtigt, # d.h. generell wird nichts geloggt, ausßer dem was per DBLogInclude eingeschlossen wird # - Exclude/Include: Es wird zunaechst Exclude geprueft und bei Ausschluß wird ggf. noch zusaetzlich Include geprueft, # d.h. generell wird alles geloggt, es sei denn es wird per DBLogExclude ausgeschlossen. # Wird es von DBLogExclude ausgeschlossen, kann es trotzdem wieder per DBLogInclude # eingeschlossen werden. addToAttrList("DbLogInclude"); addToAttrList("DbLogExclude"); $hash->{FW_detailFn} = "DbLog_fhemwebFn"; $hash->{SVG_sampleDataFn} = "DbLog_sampleDataFn"; } ############################################################### sub DbLog_Define($@) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); return "wrong syntax: define DbLog configuration regexp" if(int(@a) != 4); my $regexp = $a[3]; eval { "Hallo" =~ m/^$regexp$/ }; return "Bad regexp: $@" if($@); $hash->{REGEXP} = $regexp; $hash->{CONFIGURATION}= $a[2]; #remember PID for plotfork $hash->{PID} = $$; # read configuration data my $ret = _DbLog_readCfg($hash); return $ret if ($ret); # return on error while reading configuration readingsSingleUpdate($hash, 'state', 'waiting for connection', 1); eval { DbLog_Connect($hash); }; return undef; } ################################################################ sub DbLog_Undef($$) { my ($hash, $name) = @_; my $dbh= $hash->{DBH}; $dbh->disconnect() if(defined($dbh)); return undef; } ################################################################ sub DbLog_Shutdown($) { my ($hash) = @_; my $name = $hash->{NAME}; my $shutdownWait = AttrVal($name,"shutdownWait",undef); if(defined($shutdownWait)) { Log3($name, 2, "DbLog $name waiting for shutdown"); sleep($shutdownWait); } return undef; } ################################################################ # # Wird bei jeder Aenderung eines Attributes dieser # DbLog-Instanz aufgerufen # ################################################################ sub DbLog_Attr(@) { my @a = @_; my $do = 0; if($a[0] eq "set" && $a[2] eq "disable") { $do = (!defined($a[3]) || $a[3]) ? 1 : 2; } $do = 2 if($a[0] eq "del" && (!$a[2] || $a[2] eq "disable")); return if(!$do); $defs{$a[1]}{STATE} = ($do == 1 ? "disabled" : "active"); return undef; } ################################################################ sub DbLog_Set($@) { my ($hash, @a) = @_; my $name = $hash->{NAME}; my $usage = "Unknown argument, choose one of reduceLog reopen:noArg rereadcfg:noArg count:noArg deleteOldDays userCommand"; return $usage if(int(@a) < 2); my $dbh = $hash->{DBH}; my $ret; if ($a[1] eq 'reduceLog') { if ( !$dbh || not $dbh->ping ) { Log3($name, 1, "DbLog $name: DBLog_Set - reduceLog - DB Session dead, try to reopen now !"); DbLog_Connect($hash); } else { if (defined $a[2] && $a[2] =~ /^\d+$/) { $ret = DbLog_reduceLog($hash,@a); } else { Log3($name, 1, "DbLog $name: reduceLog error, no given."); $ret = "reduceLog error, no given."; } } } elsif ($a[1] eq 'reopen') { Log3($name, 4, "DbLog $name: Reopen requested."); if ($dbh) { $dbh->commit() if(!$dbh->{AutoCommit}); $dbh->disconnect(); } DbLog_Connect($hash); $ret = "Reopen executed."; } elsif ($a[1] eq 'rereadcfg') { Log3($name, 4, "DbLog $name: Rereadcfg requested."); if ($dbh) { $dbh->commit() if(!$dbh->{AutoCommit}); $dbh->disconnect(); } $ret = _DbLog_readCfg($hash); return $ret if $ret; DbLog_Connect($hash); $ret = "Rereadcfg executed."; } elsif ($a[1] eq 'count') { if ( !$dbh || not $dbh->ping ) { Log3($name, 1, "DbLog $name: DBLog_Set - count - DB Session dead, try to reopen now !"); DbLog_Connect($hash); } else { Log3($name, 4, "DbLog $name: Records count requested."); my $c = $dbh->selectrow_array('SELECT count(*) FROM history'); readingsSingleUpdate($hash, 'countHistory', $c ,1); $c = $dbh->selectrow_array('SELECT count(*) FROM current'); readingsSingleUpdate($hash, 'countCurrent', $c ,1); } } elsif ($a[1] eq 'deleteOldDays') { Log3($name, 4, "DbLog $name: Deletion of old records requested."); my ($c, $cmd); if ( !$dbh || not $dbh->ping ) { Log3($name, 1, "DbLog $name: DBLog_Set - deleteOldDays - DB Session dead, try to reopen now !"); DbLog_Connect($hash); } else { $cmd = "delete from history where TIMESTAMP < "; if ($hash->{DBMODEL} eq 'SQLITE') { $cmd .= "datetime('now', '-$a[2] days')"; } elsif ($hash->{DBMODEL} eq 'MYSQL') { $cmd .= "DATE_SUB(CURDATE(),INTERVAL $a[2] DAY)"; } elsif ($hash->{DBMODEL} eq 'POSTGRESQL') { $cmd .= "NOW() - INTERVAL '$a[2]' DAY"; } else { $cmd = undef; $ret = 'Unknown database type. Maybe you can try userCommand anyway.'; } if(defined($cmd)) { $c = $dbh->do($cmd); readingsSingleUpdate($hash, 'lastRowsDeleted', $c ,1); } } } elsif ($a[1] eq 'userCommand') { if ( !$dbh || not $dbh->ping ) { Log3($name, 1, "DbLog $name: DBLog_Set - userCommand - DB Session dead, try to reopen now !"); DbLog_Connect($hash); } else { Log3($name, 4, "DbLog $name: userCommand execution requested."); my ($c, @cmd, $sql); @cmd = @a; shift(@cmd); shift(@cmd); $sql = join(" ",@cmd); readingsSingleUpdate($hash, 'userCommand', $sql, 1); $c = $dbh->selectrow_array($sql); readingsSingleUpdate($hash, 'userCommandResult', $c ,1); } } else { $ret = $usage; } return $ret; } ############################################################################################### # # Exrahieren des Filters aus der ColumnsSpec (gplot-Datei) # # Die grundlegend idee ist das jeder svg plot einen filter hat der angibt # welches device und reading dargestellt wird so das der plot sich neu # lädt wenn es ein entsprechendes event gibt. # # Parameter: Quell-Instanz-Name, und alle FileLog-Parameter, die diese Instanz betreffen. # Quelle: http://forum.fhem.de/index.php/topic,40176.msg325200.html#msg325200 ############################################################################################### sub DbLog_regexpFn($$) { my ($name, $filter) = @_; my $ret; my @a = split( ' ', $filter ); for(my $i = 0; $i < int(@a); $i++) { my @fld = split(":", $a[$i]); $ret .= '|' if( $ret ); $ret .= $fld[0] .'.'. $fld[1]; } return $ret; } ################################################################ # # Parsefunktion, abhaengig vom Devicetyp # ################################################################ sub DbLog_ParseEvent($$$) { my ($device, $type, $event)= @_; my @result; my $reading; my $value; my $unit; my $dtype = $defs{$device}{TYPE}; if($modules{$dtype}{DbLog_splitFn}) { # let the module do the job! Log3 $device, 5, "DbLog_ParseEvent calling external DbLog_splitFn for type: $dtype , device: $device"; no strict "refs"; ($reading,$value,$unit) = &{$modules{$dtype}{DbLog_splitFn}}($event, $device); use strict "refs"; @result= ($reading,$value,$unit); return @result; } # split the event into reading, value and unit # "day-temp: 22.0 (Celsius)" -> "day-temp", "22.0 (Celsius)" my @parts = split(/: /,$event); $reading = shift @parts; if(@parts == 2) { $value = $parts[0]; $unit = $parts[1]; } else { $value = join(": ", @parts); $unit = ""; } #default if(!defined($reading)) { $reading = ""; } if(!defined($value)) { $value = ""; } if( $value eq "" ) { $reading= "state"; $value= $event; } #globales Abfangen von # - temperature # - humidity if ($reading =~ m(^temperature)) { $unit= "°C"; } # wenn reading mit temperature beginnt elsif($reading =~ m(^humidity)) { $unit= "%"; } # the interpretation of the argument depends on the device type # EMEM, M232Counter, M232Voltage return plain numbers if(($type eq "M232Voltage") || ($type eq "M232Counter") || ($type eq "EMEM")) { } #OneWire elsif(($type eq "OWMULTI")) { if(int(@parts)>1) { $reading = "data"; $value = $event; } else { @parts = split(/\|/, AttrVal($device, $reading."VUnit", "")); $unit = $parts[1] if($parts[1]); if(lc($reading) =~ m/temp/) { $value=~ s/ \(Celsius\)//; $value=~ s/([-\.\d]+).*/$1/; $unit= "°C"; } elsif(lc($reading) =~ m/(humidity|vwc)/) { $value=~ s/ \(\%\)//; $unit= "%"; } } } # Onewire elsif(($type eq "OWAD") || ($type eq "OWSWITCH")) { if(int(@parts)>1) { $reading = "data"; $value = $event; } else { @parts = split(/\|/, AttrVal($device, $reading."Unit", "")); $unit = $parts[1] if($parts[1]); } } # FBDECT elsif (($type eq "FBDECT")) { if ( $value=~/([\.\d]+)\s([a-z])/i ) { $value = $1; $unit = $2; } } # MAX elsif(($type eq "MAX")) { $unit= "°C" if(lc($reading) =~ m/temp/); $unit= "%" if(lc($reading) eq "valveposition"); } # FS20 elsif(($type eq "FS20") || ($type eq "X10")) { if($reading =~ m/^dim(\d+).*/o) { $value = $1; $reading= "dim"; $unit= "%"; } elsif(!defined($value) || $value eq "") { $value= $reading; $reading= "data"; } } # FHT elsif($type eq "FHT") { if($reading =~ m(-from[12]\ ) || $reading =~ m(-to[12]\ )) { @parts= split(/ /,$event); $reading= $parts[0]; $value= $parts[1]; $unit= ""; } elsif($reading =~ m(-temp)) { $value=~ s/ \(Celsius\)//; $unit= "°C"; } elsif($reading =~ m(temp-offset)) { $value=~ s/ \(Celsius\)//; $unit= "°C"; } elsif($reading =~ m(^actuator[0-9]*)) { if($value eq "lime-protection") { $reading= "actuator-lime-protection"; undef $value; } elsif($value =~ m(^offset:)) { $reading= "actuator-offset"; @parts= split(/: /,$value); $value= $parts[1]; if(defined $value) { $value=~ s/%//; $value= $value*1.; $unit= "%"; } } elsif($value =~ m(^unknown_)) { @parts= split(/: /,$value); $reading= "actuator-" . $parts[0]; $value= $parts[1]; if(defined $value) { $value=~ s/%//; $value= $value*1.; $unit= "%"; } } elsif($value =~ m(^synctime)) { $reading= "actuator-synctime"; undef $value; } elsif($value eq "test") { $reading= "actuator-test"; undef $value; } elsif($value eq "pair") { $reading= "actuator-pair"; undef $value; } else { $value=~ s/%//; $value= $value*1.; $unit= "%"; } } } # KS300 elsif($type eq "KS300") { if($event =~ m(T:.*)) { $reading= "data"; $value= $event; } elsif($event =~ m(avg_day)) { $reading= "data"; $value= $event; } elsif($event =~ m(avg_month)) { $reading= "data"; $value= $event; } elsif($reading eq "temperature") { $value=~ s/ \(Celsius\)//; $unit= "°C"; } elsif($reading eq "wind") { $value=~ s/ \(km\/h\)//; $unit= "km/h"; } elsif($reading eq "rain") { $value=~ s/ \(l\/m2\)//; $unit= "l/m2"; } elsif($reading eq "rain_raw") { $value=~ s/ \(counter\)//; $unit= ""; } elsif($reading eq "humidity") { $value=~ s/ \(\%\)//; $unit= "%"; } elsif($reading eq "israining") { $value=~ s/ \(yes\/no\)//; $value=~ s/no/0/; $value=~ s/yes/1/; } } # HMS elsif($type eq "HMS" || $type eq "CUL_WS" || $type eq "OWTHERM") { if($event =~ m(T:.*)) { $reading= "data"; $value= $event; } elsif($reading eq "temperature") { $value=~ s/ \(Celsius\)//; $value=~ s/([-\.\d]+).*/$1/; #OWTHERM $unit= "°C"; } elsif($reading eq "humidity") { $value=~ s/ \(\%\)//; $unit= "%"; } elsif($reading eq "battery") { $value=~ s/ok/1/; $value=~ s/replaced/1/; $value=~ s/empty/0/; } } # CUL_HM elsif ($type eq "CUL_HM") { # remove trailing % $value=~ s/ \%$//; } # BS elsif($type eq "BS") { if($event =~ m(brightness:.*)) { @parts= split(/ /,$event); $reading= "lux"; $value= $parts[4]*1.; $unit= "lux"; } } # RFXTRX Lighting elsif($type eq "TRX_LIGHT") { if($reading =~ m/^level (\d+)/) { $value = $1; $reading= "level"; } } # RFXTRX Sensors elsif($type eq "TRX_WEATHER") { if($reading eq "energy_current") { $value=~ s/ W//; } elsif($reading eq "energy_total") { $value=~ s/ kWh//; } # elsif($reading eq "temperature") {TODO} # elsif($reading eq "temperature") {TODO elsif($reading eq "battery") { if ($value=~ m/(\d+)\%/) { $value= $1; } else { $value= ($value eq "ok"); } } } # Weather elsif($type eq "WEATHER") { if($event =~ m(^wind_condition)) { @parts= split(/ /,$event); # extract wind direction from event if(defined $parts[0]) { $reading = "wind_direction"; $value= $parts[2]; } } elsif($reading eq "wind_chill") { $unit= "°C"; } elsif($reading eq "wind_direction") { $unit= ""; } elsif($reading =~ m(^wind)) { $unit= "km/h"; } # wind, wind_speed elsif($reading =~ m(^temperature)) { $unit= "°C"; } # wenn reading mit temperature beginnt elsif($reading =~ m(^humidity)) { $unit= "%"; } elsif($reading =~ m(^pressure)) { $unit= "hPa"; } elsif($reading =~ m(^pressure_trend)) { $unit= ""; } } # FHT8V elsif($type eq "FHT8V") { if($reading =~ m(valve)) { @parts= split(/ /,$event); $reading= $parts[0]; $value= $parts[1]; $unit= "%"; } } # Dummy elsif($type eq "DUMMY") { if( $value eq "" ) { $reading= "data"; $value= $event; } $unit= ""; } @result= ($reading,$value,$unit); return @result; } ################################################################ # Schreibroutine Einfügen Werte in DB # # param1: hash # param2: DbLogType -> Current oder History oder Current/History # param4: Timestamp # param5: Device # param6: Type # param7: Event # param8: Reading # param9: Value # param10: Unit # ################################################################ sub DbLog_Push(@) { my ($hash, $DbLogType, $timestamp, $device, $type, $event, $reading, $value, $unit) = @_; my $dbh= $hash->{DBH}; my $name = $hash->{NAME}; $dbh->{RaiseError} = 1; $dbh->{PrintError} = 0; eval { if ( !$dbh || not $dbh->ping ) { #### DB Session dead, try to reopen now ! DbLog_Connect($hash); } }; if ($@) { Log3($name, 1, "DbLog $name: DBLog_Push - DB Session dead! - $@"); return $dbh->{RaiseError}; } if ($hash->{DBMODEL} ne 'SQLITE') { # Daten auf maximale laenge beschneiden $device = substr($device,0, $columns{DEVICE}); $type = substr($type,0, $columns{TYPE}); $event = substr($event,0, $columns{EVENT}); $reading = substr($reading,0, $columns{READING}); $value = substr($value,0, $columns{VALUE}); $unit = substr($unit,0, $columns{UNIT}); } $dbh->begin_work(); my $sth_ih = $dbh->prepare_cached("INSERT INTO history (TIMESTAMP, DEVICE, TYPE, EVENT, READING, VALUE, UNIT) VALUES (?,?,?,?,?,?,?)") if (lc($DbLogType) =~ m(history) ); my $sth_ic = $dbh->prepare_cached("INSERT INTO current (TIMESTAMP, DEVICE, TYPE, EVENT, READING, VALUE, UNIT) VALUES (?,?,?,?,?,?,?)") if (lc($DbLogType) =~ m(current) ); my $sth_uc = $dbh->prepare_cached("UPDATE current SET TIMESTAMP=?, TYPE=?, EVENT=?, VALUE=?, UNIT=? WHERE (DEVICE=?) AND (READING=?)") if (lc($DbLogType) =~ m(current) ); Log3 $hash->{NAME}, 5, "DbLog: logging of Device: $device , Type: $type , Event: $event , Reading: $reading , Value: $value , Unit: $unit"; eval { # insert into history if (lc($DbLogType) =~ m(history) ) { my $rv_ih = $sth_ih->execute(($timestamp, $device, $type, $event, $reading, $value, $unit)); } # update or insert current if (lc($DbLogType) =~ m(current) ) { my $rv_uc = $sth_uc->execute(($timestamp, $type, $event, $value, $unit, $device, $reading)); if ($rv_uc == 0) { my $rv_ic = $sth_ic->execute(($timestamp, $device, $type, $event, $reading, $value, $unit)); } } }; if ($@) { Log3 $hash->{NAME}, 2, "DbLog: Failed to insert new readings into database: $@"; $dbh->rollback(); # reconnect $dbh->disconnect(); DbLog_Connect($hash); } else { $dbh->commit(); $dbh->{RaiseError} = 0; $dbh->{PrintError} = 1; } return $dbh->{RaiseError}; } ################################################################ # # Hauptroutine zum Loggen. Wird bei jedem Eventchange # aufgerufen # ################################################################ sub DbLog_Log($$) { # Log is my entry, Dev is the entry of the changed device my ($hash, $dev) = @_; Log3 $hash->{NAME}, 5, "Notify from Device: ".$dev->{NAME}." recieved"; return undef if($hash->{STATE} eq "disabled"); # name and type required for parsing my $n= $dev->{NAME}; my $t= uc($dev->{TYPE}); # timestamp in SQL format YYYY-MM-DD hh:mm:ss #my ($sec,$min,$hr,$day,$mon,$yr,$wday,$yday,$isdst)= localtime(time); #my $ts= sprintf("%04d-%02d-%02d %02d:%02d:%02d", $yr+1900,$mon+1,$day,$hr,$min,$sec); my $re = $hash->{REGEXP}; my $max = int(@{$dev->{CHANGED}}); my $ts_0 = TimeNow(); my $now = gettimeofday(); # get timestamp in seconds since epoch my $DbLogExclude = AttrVal($dev->{NAME}, "DbLogExclude", undef); my $DbLogInclude = AttrVal($dev->{NAME}, "DbLogInclude",undef); my $DbLogSelectionMode=AttrVal($hash->{NAME},"DbLogSelectionMode","Exclude"); my $DbLogType = AttrVal($hash->{NAME}, "DbLogType", "Current/History"); #one Transaction eval { for (my $i = 0; $i < $max; $i++) { my $s = $dev->{CHANGED}[$i]; $s = "" if(!defined($s)); if($n =~ m/^$re$/ || "$n:$s" =~ m/^$re$/ || $DbLogSelectionMode eq 'Include') { my $ts = $ts_0; $ts = $dev->{CHANGETIME}[$i] if(defined($dev->{CHANGETIME}[$i])); # $ts is in SQL format YYYY-MM-DD hh:mm:ss my @r= DbLog_ParseEvent($n, $t, $s); my $reading= $r[0]; my $value= $r[1]; my $unit= $r[2]; if(!defined $reading) { $reading= ""; } if(!defined $value) { $value= ""; } if(!defined $unit || $unit eq "") { $unit = AttrVal("$n", "unit", ""); } #Je nach DBLogSelectionMode muss das vorgegebene Ergebnis der Include-, bzw. Exclude-Pruefung #entsprechend unterschiedlich vorbelegt sein. #keine Readings loggen die in DbLogExclude explizit ausgeschlossen sind my $DoIt = 0; $DoIt = 1 if($DbLogSelectionMode =~ m/Exclude/ ); if($DbLogExclude && $DbLogSelectionMode =~ m/Exclude/) { # Bsp: "(temperature|humidity):300 battery:3600" my @v1 = split(/,/, $DbLogExclude); for (my $i=0; $i{NAME}}{Helper}{DBLOG}{$reading}{$hash->{NAME}}{TIME}; my $lv = $defs{$dev->{NAME}}{Helper}{DBLOG}{$reading}{$hash->{NAME}}{VALUE}; $lt = 0 if(!$lt); $lv = "" if(!$lv); if(($now-$lt < $v2[1]) && ($lv eq $value)) { # innerhalb MinIntervall und LastValue=Value $DoIt = 0; } } } } #Hier ggf. zusaetlich noch dbLogInclude pruefen, falls bereits durch DbLogExclude ausgeschlossen #Im Endeffekt genau die gleiche Pruefung, wie fuer DBLogExclude, lediglich mit umgegkehrtem Ergebnis. if($DoIt == 0) { if($DbLogInclude && ($DbLogSelectionMode =~ m/Include/)) { my @v1 = split(/,/, $DbLogInclude); for (my $i=0; $i{NAME}}{Helper}{DBLOG}{$reading}{$hash->{NAME}}{TIME}; my $lv = $defs{$dev->{NAME}}{Helper}{DBLOG}{$reading}{$hash->{NAME}}{VALUE}; $lt = 0 if(!$lt); $lv = "" if(!$lv); if(($now-$lt < $v2[1]) && ($lv eq $value)) { # innerhalb MinIntervall und LastValue=Value $DoIt = 0; } } } } } next if($DoIt == 0); $defs{$dev->{NAME}}{Helper}{DBLOG}{$reading}{$hash->{NAME}}{TIME} = $now; $defs{$dev->{NAME}}{Helper}{DBLOG}{$reading}{$hash->{NAME}}{VALUE} = $value; DbLog_Push($hash, $DbLogType, $ts, $n, $t, $s, $reading, $value, $unit) } } }; return ""; } ################################################################ # # zerlegt uebergebenes FHEM-Datum in die einzelnen Bestandteile # und fuegt noch Defaultwerte ein # uebergebenes SQL-Format: YYYY-MM-DD HH24:MI:SS # ################################################################ sub DbLog_explode_datetime($%) { my ($t, %def) = @_; my %retv; my (@datetime, @date, @time); @datetime = split(" ", $t); #Datum und Zeit auftrennen @date = split("-", $datetime[0]); @time = split(":", $datetime[1]) if ($datetime[1]); if ($date[0]) {$retv{year} = $date[0];} else {$retv{year} = $def{year};} if ($date[1]) {$retv{month} = $date[1];} else {$retv{month} = $def{month};} if ($date[2]) {$retv{day} = $date[2];} else {$retv{day} = $def{day};} if ($time[0]) {$retv{hour} = $time[0];} else {$retv{hour} = $def{hour};} if ($time[1]) {$retv{minute}= $time[1];} else {$retv{minute}= $def{minute};} if ($time[2]) {$retv{second}= $time[2];} else {$retv{second}= $def{second};} $retv{datetime}=DbLog_implode_datetime($retv{year}, $retv{month}, $retv{day}, $retv{hour}, $retv{minute}, $retv{second}); #Log 1, Dumper(%retv); return %retv } sub DbLog_implode_datetime($$$$$$) { my ($year, $month, $day, $hour, $minute, $second) = @_; my $retv = $year."-".$month."-".$day." ".$hour.":".$minute.":".$second; return $retv; } ################################################################ # # Verbindung zur DB aufbauen # ################################################################ sub _DbLog_readCfg($){ my ($hash)= @_; my $name = $hash->{NAME}; my $configfilename= $hash->{CONFIGURATION}; my %dbconfig; my $ret; # use generic fileRead to get configuration data my ($err, @config) = FileRead($configfilename); return $err if($err); eval join("\n", @config); $hash->{dbconn} = $dbconfig{connection}; $hash->{dbuser} = $dbconfig{user}; $attr{"sec$name"}{secret} = $dbconfig{password}; #check the database model if($hash->{dbconn} =~ m/pg:/i) { $hash->{DBMODEL}="POSTGRESQL"; } elsif ($hash->{dbconn} =~ m/mysql:/i) { $hash->{DBMODEL}="MYSQL"; } elsif ($hash->{dbconn} =~ m/oracle:/i) { $hash->{DBMODEL}="ORACLE"; } elsif ($hash->{dbconn} =~ m/sqlite:/i) { $hash->{DBMODEL}="SQLITE"; } else { $hash->{DBMODEL}="unknown"; Log3 $hash->{NAME}, 3, "Unknown dbmodel type in configuration file $configfilename."; Log3 $hash->{NAME}, 3, "Only Mysql, Postgresql, Oracle, SQLite are fully supported."; Log3 $hash->{NAME}, 3, "It may cause SQL-Erros during generating plots."; } return; } sub DbLog_Connect($) { my ($hash)= @_; my $name = $hash->{NAME}; my $dbconn = $hash->{dbconn}; my $dbuser = $hash->{dbuser}; my $dbpassword = $attr{"sec$name"}{secret}; Log3 $hash->{NAME}, 3, "Connecting to database $dbconn with user $dbuser"; my $dbh = DBI->connect_cached("dbi:$dbconn", $dbuser, $dbpassword, { PrintError => 0 }); if(!$dbh) { RemoveInternalTimer($hash); Log3 $hash->{NAME}, 4, 'DbLog: Trying to connect to database'; readingsSingleUpdate($hash, 'state', 'disconnected', 1); InternalTimer(time+5, 'DbLog_Connect', $hash, 0); Log3 $hash->{NAME}, 4, 'Waiting for database connection'; return 0; } Log3 $hash->{NAME}, 3, "Connection to db $dbconn established for pid $$"; readingsSingleUpdate($hash, 'state', 'connected', 1); $hash->{DBH}= $dbh; if ($hash->{DBMODEL} eq "SQLITE") { $dbh->do("PRAGMA temp_store=MEMORY"); $dbh->do("PRAGMA synchronous=NORMAL"); $dbh->do("PRAGMA journal_mode=WAL"); $dbh->do("PRAGMA cache_size=4000"); $dbh->do("CREATE TEMP TABLE IF NOT EXISTS current (TIMESTAMP TIMESTAMP, DEVICE varchar(64), TYPE varchar(64), EVENT varchar(512), READING varchar(64), VALUE varchar(128), UNIT varchar(32))"); $dbh->do("CREATE TABLE IF NOT EXISTS history (TIMESTAMP TIMESTAMP, DEVICE varchar(64), TYPE varchar(64), EVENT varchar(512), READING varchar(64), VALUE varchar(128), UNIT varchar(32))"); $dbh->do("CREATE INDEX IF NOT EXISTS Search_Idx ON `history` (DEVICE, READING, TIMESTAMP)"); } # no webfrontend connection for plotfork return 1 if( $hash->{PID} != $$ ); # creating an own connection for the webfrontend, saved as DBHF in Hash # this makes sure that the connection doesnt get lost due to other modules my $dbhf = DBI->connect_cached("dbi:$dbconn", $dbuser, $dbpassword, { PrintError => 0 }); if(!$dbh) { RemoveInternalTimer($hash); Log3 $hash->{NAME}, 4, 'DbLog: Trying to connect to database'; InternalTimer(time+5, 'DbLog_Connect', $hash, 0); Log3 $hash->{NAME}, 4, 'Waiting for database connection'; return 0; } # if(!$dbhf) { # Log3 $hash->{NAME}, 2, "Can't connect to $dbconn: $DBI::errstr"; # return 0; # } Log3 $hash->{NAME}, 3, "Connection to db $dbconn established"; $hash->{DBHF}= $dbhf; return 1; } ################################################################ # # Prozeduren zum Ausfuehren des SQLs # # param1: hash # param2: pointer : DBFilehandle # param3: string : SQL ################################################################ sub DbLog_ExecSQL1($$$) { my ($hash,$dbh,$sql)= @_; my $sth = $dbh->do($sql); if(!$sth) { Log3 $hash->{NAME}, 2, "DBLog error: " . $DBI::errstr; return 0; } return $sth; } sub DbLog_ExecSQL($$) { my ($hash,$sql)= @_; Log3 $hash->{NAME}, 4, "Executing $sql"; my $dbh= $hash->{DBH}; my $sth = DbLog_ExecSQL1($hash,$dbh,$sql); if(!$sth) { #retry $dbh->disconnect(); if(!DbLog_Connect($hash)) { Log3 $hash->{NAME}, 2, "DBLog reconnect failed."; return 0; } $dbh= $hash->{DBH}; $sth = DbLog_ExecSQL1($hash,$dbh,$sql); if(!$sth) { Log3 $hash->{NAME}, 2, "DBLog retry failed."; return 0; } Log3 $hash->{NAME}, 2, "DBLog retry ok."; } return $sth; } ################################################################ # # GET Funktion # wird zb. zur Generierung der Plots implizit aufgerufen # infile : [-|current|history] # outfile: [-|ALL|INT|WEBCHART] # ################################################################ sub DbLog_Get($@) { my ($hash, @a) = @_; my $name = $hash->{NAME}; return dbReadings($hash,@a) if $a[1] =~ m/^Readings/; return "Usage: get $a[0] ...\n". " where column_spec is :::\n" . " see the #DbLog entries in the .gplot files\n" . " is not used, only for compatibility for FileLog, please use - \n" . " is a prefix, - means stdout\n" if(int(@a) < 5); shift @a; my $inf = lc(shift @a); my $outf = lc(shift @a); my $from = shift @a; my $to = shift @a; # Now @a contains the list of column_specs my ($internal, @fld); if($inf eq "-") { $inf = "history"; } if($outf eq "int" && $inf eq "current") { $inf = "history"; Log3 $hash->{NAME}, 3, "Defining DbLog SVG-Plots with :CURRENT is deprecated. Please define DbLog SVG-Plots with :HISTORY instead of :CURRENT. (define SVG ::HISTORY)"; } if($outf eq "int") { $outf = "-"; $internal = 1; } elsif($outf eq "array"){ } elsif(lc($outf) eq "webchart") { # redirect the get request to the chartQuery function return chartQuery($hash, @_); } my @readings = (); my (%sqlspec, %from_datetime, %to_datetime); #uebergebenen Timestamp anpassen #moegliche Formate: YYYY | YYYY-MM | YYYY-MM-DD | YYYY-MM-DD_HH24 $from =~ s/_/\ /g; $to =~ s/_/\ /g; %from_datetime = DbLog_explode_datetime($from, DbLog_explode_datetime("2000-01-01 00:00:00", ())); %to_datetime = DbLog_explode_datetime($to, DbLog_explode_datetime("2099-01-01 00:00:00", ())); $from = $from_datetime{datetime}; $to = $to_datetime{datetime}; my ($retval,$retvaldummy,$hour,$sql_timestamp, $sql_device, $sql_reading, $sql_value, $type, $event, $unit) = ""; my @ReturnArray; my $writeout = 0; my (@min, @max, @sum, @cnt, @lastv, @lastd, @mind, @maxd); my (%tstamp, %lasttstamp, $out_tstamp, $out_value, $minval, $maxval, $deltacalc); #fuer delta-h/d Berechnung #extract the Device:Reading arguments into @readings array for(my $i = 0; $i < int(@a); $i++) { @fld = split(":", $a[$i], 5); $readings[$i][0] = $fld[0]; # Device $readings[$i][1] = $fld[1]; # Reading $readings[$i][2] = $fld[2]; # Default $readings[$i][3] = $fld[3]; # function $readings[$i][4] = $fld[4]; # regexp $readings[$i][1] = "%" if(!$readings[$i][1] || length($readings[$i][1])==0); #falls Reading nicht gefuellt setze Joker } #create new connection for plotfork if( $hash->{PID} != $$ ) { $hash->{DBH}->disconnect(); return "Can't connect to database." if(!DbLog_Connect($hash)); } my $dbh = $hash->{DBH}; if ( !$dbh || not $dbh->ping ) { Log3($name, 1, "DbLog $name: DBLog_Get - DB Session dead, try to reopen now !"); DbLog_Connect($hash); return undef; } #vorbereiten der DB-Abfrage, DB-Modell-abhaengig if ($hash->{DBMODEL} eq "POSTGRESQL") { $sqlspec{get_timestamp} = "TO_CHAR(TIMESTAMP, 'YYYY-MM-DD HH24:MI:SS')"; $sqlspec{from_timestamp} = "TO_TIMESTAMP('$from', 'YYYY-MM-DD HH24:MI:SS')"; $sqlspec{to_timestamp} = "TO_TIMESTAMP('$to', 'YYYY-MM-DD HH24:MI:SS')"; #$sqlspec{reading_clause} = "(DEVICE || '|' || READING)"; $sqlspec{order_by_hour} = "TO_CHAR(TIMESTAMP, 'YYYY-MM-DD HH24')"; $sqlspec{max_value} = "MAX(VALUE)"; $sqlspec{day_before} = "($sqlspec{from_timestamp} - INTERVAL '1 DAY')"; } elsif ($hash->{DBMODEL} eq "ORACLE") { $sqlspec{get_timestamp} = "TO_CHAR(TIMESTAMP, 'YYYY-MM-DD HH24:MI:SS')"; $sqlspec{from_timestamp} = "TO_TIMESTAMP('$from', 'YYYY-MM-DD HH24:MI:SS')"; $sqlspec{to_timestamp} = "TO_TIMESTAMP('$to', 'YYYY-MM-DD HH24:MI:SS')"; $sqlspec{order_by_hour} = "TO_CHAR(TIMESTAMP, 'YYYY-MM-DD HH24')"; $sqlspec{max_value} = "MAX(VALUE)"; $sqlspec{day_before} = "DATE_SUB($sqlspec{from_timestamp},INTERVAL 1 DAY)"; } elsif ($hash->{DBMODEL} eq "MYSQL") { $sqlspec{get_timestamp} = "DATE_FORMAT(TIMESTAMP, '%Y-%m-%d %H:%i:%s')"; $sqlspec{from_timestamp} = "STR_TO_DATE('$from', '%Y-%m-%d %H:%i:%s')"; $sqlspec{to_timestamp} = "STR_TO_DATE('$to', '%Y-%m-%d %H:%i:%s')"; $sqlspec{order_by_hour} = "DATE_FORMAT(TIMESTAMP, '%Y-%m-%d %H')"; $sqlspec{max_value} = "MAX(CAST(VALUE AS DECIMAL(20,8)))"; $sqlspec{day_before} = "DATE_SUB($sqlspec{from_timestamp},INTERVAL 1 DAY)"; } elsif ($hash->{DBMODEL} eq "SQLITE") { $sqlspec{get_timestamp} = "TIMESTAMP"; $sqlspec{from_timestamp} = "'$from'"; $sqlspec{to_timestamp} = "'$to'"; $sqlspec{order_by_hour} = "strftime('%Y-%m-%d %H', TIMESTAMP)"; $sqlspec{max_value} = "MAX(VALUE)"; $sqlspec{day_before} = "date($sqlspec{from_timestamp},'-1 day')"; } else { $sqlspec{get_timestamp} = "TIMESTAMP"; $sqlspec{from_timestamp} = "'$from'"; $sqlspec{to_timestamp} = "'$to'"; $sqlspec{order_by_hour} = "strftime('%Y-%m-%d %H', TIMESTAMP)"; $sqlspec{max_value} = "MAX(VALUE)"; $sqlspec{day_before} = "date($sqlspec{from_timestamp},'-1 day')"; } if($outf =~ m/(all|array)/) { $sqlspec{all} = ",TYPE,EVENT,UNIT"; $sqlspec{all_max} = ",MAX(TYPE) AS TYPE,MAX(EVENT) AS EVENT,MAX(UNIT) AS UNIT"; } else { $sqlspec{all} = ""; $sqlspec{all_max} = ""; } for(my $i=0; $i> 1); $max[$i] = -(~0 >> 1); $sum[$i] = 0; $cnt[$i] = 0; $lastv[$i] = 0; $lastd[$i] = "undef"; $mind[$i] = "undef"; $maxd[$i] = "undef"; $minval = (~0 >> 1); $maxval = -(~0 >> 1); $deltacalc = 0; if($readings[$i]->[3] && ($readings[$i]->[3] eq "delta-h" || $readings[$i]->[3] eq "delta-d")) { $deltacalc = 1; } my $stm; my $stm2; my $stmdelta; $stm = "SELECT MAX($sqlspec{get_timestamp}) AS TIMESTAMP, MAX(DEVICE) AS DEVICE, MAX(READING) AS READING, $sqlspec{max_value} $sqlspec{all_max} "; $stm .= "FROM current " if($inf eq "current"); $stm .= "FROM history " if($inf eq "history"); $stm .= "WHERE 1=1 "; $stm .= "AND DEVICE = '".$readings[$i]->[0]."' " if ($readings[$i]->[0] !~ m(\%)); $stm .= "AND DEVICE LIKE '".$readings[$i]->[0]."' " if(($readings[$i]->[0] !~ m(^\%$)) && ($readings[$i]->[0] =~ m(\%))); $stm .= "AND READING = '".$readings[$i]->[1]."' " if ($readings[$i]->[1] !~ m(\%)); $stm .= "AND READING LIKE '".$readings[$i]->[1]."' " if(($readings[$i]->[1] !~ m(^%$)) && ($readings[$i]->[1] =~ m(\%))); $stmdelta = $stm; $stm .= "AND TIMESTAMP < $sqlspec{from_timestamp} "; $stm .= "AND TIMESTAMP > $sqlspec{day_before} "; $stm .= "UNION ALL "; $stm2 = "SELECT $sqlspec{get_timestamp}, DEVICE, READING, VALUE $sqlspec{all} "; $stm2 .= "FROM current " if($inf eq "current"); $stm2 .= "FROM history " if($inf eq "history"); $stm2 .= "WHERE 1=1 "; $stm2 .= "AND DEVICE = '".$readings[$i]->[0]."' " if ($readings[$i]->[0] !~ m(\%)); $stm2 .= "AND DEVICE LIKE '".$readings[$i]->[0]."' " if(($readings[$i]->[0] !~ m(^\%$)) && ($readings[$i]->[0] =~ m(\%))); $stm2 .= "AND READING = '".$readings[$i]->[1]."' " if ($readings[$i]->[1] !~ m(\%)); $stm2 .= "AND READING LIKE '".$readings[$i]->[1]."' " if(($readings[$i]->[1] !~ m(^%$)) && ($readings[$i]->[1] =~ m(\%))); $stm2 .= "AND TIMESTAMP >= $sqlspec{from_timestamp} "; $stm2 .= "AND TIMESTAMP < $sqlspec{to_timestamp} "; $stm2 .= "ORDER BY TIMESTAMP"; if($deltacalc) { $stmdelta .= "AND TIMESTAMP >= $sqlspec{from_timestamp} "; $stmdelta .= "AND TIMESTAMP < $sqlspec{to_timestamp} "; $stmdelta .= "GROUP BY $sqlspec{order_by_hour} " if($deltacalc); $stmdelta .= "ORDER BY TIMESTAMP"; $stm .= $stmdelta; } else { $stm = $stm2; } Log3 $hash->{NAME}, 4, "Processing Statement: $stm"; my $sth= $dbh->prepare($stm) || return "Cannot prepare statement $stm: $DBI::errstr"; my $rc= $sth->execute() || return "Cannot execute statement $stm: $DBI::errstr"; if($outf =~ m/(all|array)/) { $sth->bind_columns(undef, \$sql_timestamp, \$sql_device, \$sql_reading, \$sql_value, \$type, \$event, \$unit); } else { $sth->bind_columns(undef, \$sql_timestamp, \$sql_device, \$sql_reading, \$sql_value); } if ($outf =~ m/(all)/) { $retval .= "Timestamp: Device, Type, Event, Reading, Value, Unit\n"; $retval .= "=====================================================\n"; } while($sth->fetch()) { ############ Auswerten des 5. Parameters: Regexp ################### # die Regexep wird vor der Function ausgewertet und der Wert im Feld # Value angepasst. #################################################################### if($readings[$i]->[4]) { #evaluate my $val = $sql_value; my $ts = $sql_timestamp; eval("$readings[$i]->[4]"); $sql_value = $val; $sql_timestamp = $ts; if($@) {Log3 $hash->{NAME}, 3, "DbLog: Error in inline function: <".$readings[$i]->[4].">, Error: $@";} } if($sql_timestamp lt $from && $deltacalc) { if(Scalar::Util::looks_like_number($sql_value)){ #nur setzen wenn nummerisch $minval = $sql_value if($sql_value < $minval); $maxval = $sql_value if($sql_value > $maxval); $lastv[$i] = $sql_value; } } else { $writeout = 0; $out_value = ""; $out_tstamp = ""; $retvaldummy = ""; if($readings[$i]->[4]) { $out_tstamp = $sql_timestamp; $writeout=1 if(!$deltacalc); } ############ Auswerten des 4. Parameters: function ################### if($readings[$i]->[3] && $readings[$i]->[3] eq "int") { #nur den integerwert uebernehmen falls zb value=15°C $out_value = $1 if($sql_value =~ m/^(\d+).*/o); $out_tstamp = $sql_timestamp; $writeout=1; } elsif ($readings[$i]->[3] && $readings[$i]->[3] =~ m/^int(\d+).*/o) { #Uebernehme den Dezimalwert mit den angegebenen Stellen an Nachkommastellen $out_value = $1 if($sql_value =~ m/^([-\.\d]+).*/o); $out_tstamp = $sql_timestamp; $writeout=1; } elsif ($readings[$i]->[3] && $readings[$i]->[3] eq "delta-ts" && lc($sql_value) !~ m(ignore)) { #Berechung der vergangen Sekunden seit dem letten Logeintrag #zb. die Zeit zwischen on/off my @a = split("[- :]", $sql_timestamp); my $akt_ts = mktime($a[5],$a[4],$a[3],$a[2],$a[1]-1,$a[0]-1900,0,0,-1); if($lastd[$i] ne "undef") { @a = split("[- :]", $lastd[$i]); } my $last_ts = mktime($a[5],$a[4],$a[3],$a[2],$a[1]-1,$a[0]-1900,0,0,-1); $out_tstamp = $sql_timestamp; $out_value = sprintf("%02d", $akt_ts - $last_ts); if(lc($sql_value) =~ m(hide)){$writeout=0;} else {$writeout=1;} } elsif ($readings[$i]->[3] && $readings[$i]->[3] eq "delta-h") { #Berechnung eines Stundenwertes %tstamp = DbLog_explode_datetime($sql_timestamp, ()); if($lastd[$i] eq "undef") { %lasttstamp = DbLog_explode_datetime($sql_timestamp, ()); $lasttstamp{hour} = "00"; } else { %lasttstamp = DbLog_explode_datetime($lastd[$i], ()); } # 04 01 # 06 23 if("$tstamp{hour}" ne "$lasttstamp{hour}") { # Aenderung der stunde, Berechne Delta #wenn die Stundendifferenz größer 1 ist muss ein Dummyeintrag erstellt werden $retvaldummy = ""; if(($tstamp{hour}-$lasttstamp{hour}) > 1) { for (my $j=$lasttstamp{hour}+1; $j < $tstamp{hour}; $j++) { $out_value = "0"; $hour = $j; $hour = '0'.$j if $j<10; $cnt[$i]++; $out_tstamp = DbLog_implode_datetime($tstamp{year}, $tstamp{month}, $tstamp{day}, $hour, "30", "00"); if ($outf =~ m/(all)/) { # Timestamp: Device, Type, Event, Reading, Value, Unit $retvaldummy .= sprintf("%s: %s, %s, %s, %s, %s, %s\n", $out_tstamp, $sql_device, $type, $event, $sql_reading, $out_value, $unit); } elsif ($outf =~ m/(array)/) { push(@ReturnArray, {"tstamp" => $out_tstamp, "device" => $sql_device, "type" => $type, "event" => $event, "reading" => $sql_reading, "value" => $out_value, "unit" => $unit}); } else { $out_tstamp =~ s/\ /_/g; #needed by generating plots $retvaldummy .= "$out_tstamp $out_value\n"; } } } if(($tstamp{hour}-$lasttstamp{hour}) < 0) { for (my $j=0; $j < $tstamp{hour}; $j++) { $out_value = "0"; $hour = $j; $hour = '0'.$j if $j<10; $cnt[$i]++; $out_tstamp = DbLog_implode_datetime($tstamp{year}, $tstamp{month}, $tstamp{day}, $hour, "30", "00"); if ($outf =~ m/(all)/) { # Timestamp: Device, Type, Event, Reading, Value, Unit $retvaldummy .= sprintf("%s: %s, %s, %s, %s, %s, %s\n", $out_tstamp, $sql_device, $type, $event, $sql_reading, $out_value, $unit); } elsif ($outf =~ m/(array)/) { push(@ReturnArray, {"tstamp" => $out_tstamp, "device" => $sql_device, "type" => $type, "event" => $event, "reading" => $sql_reading, "value" => $out_value, "unit" => $unit}); } else { $out_tstamp =~ s/\ /_/g; #needed by generating plots $retvaldummy .= "$out_tstamp $out_value\n"; } } } $out_value = sprintf("%g", $maxval - $minval); $sum[$i] += $out_value; $cnt[$i]++; $out_tstamp = DbLog_implode_datetime($lasttstamp{year}, $lasttstamp{month}, $lasttstamp{day}, $lasttstamp{hour}, "30", "00"); #$minval = (~0 >> 1); $minval = $maxval; # $maxval = -(~0 >> 1); $writeout=1; } } elsif ($readings[$i]->[3] && $readings[$i]->[3] eq "delta-d") { #Berechnung eines Tageswertes %tstamp = DbLog_explode_datetime($sql_timestamp, ()); if($lastd[$i] eq "undef") { %lasttstamp = DbLog_explode_datetime($sql_timestamp, ()); } else { %lasttstamp = DbLog_explode_datetime($lastd[$i], ()); } if("$tstamp{day}" ne "$lasttstamp{day}") { # Aenderung des Tages, Berechne Delta $out_value = sprintf("%g", $maxval - $minval); $sum[$i] += $out_value; $cnt[$i]++; $out_tstamp = DbLog_implode_datetime($lasttstamp{year}, $lasttstamp{month}, $lasttstamp{day}, "12", "00", "00"); # $minval = (~0 >> 1); $minval = $maxval; # $maxval = -(~0 >> 1); $writeout=1; } } else { $out_value = $sql_value; $out_tstamp = $sql_timestamp; $writeout=1; } # Wenn Attr SuppressUndef gesetzt ist, dann ausfiltern aller undef-Werte $writeout = 0 if (!defined($sql_value) && AttrVal($hash->{NAME}, "suppressUndef", 0)); ###################### Ausgabe ########################### if($writeout) { if ($outf =~ m/(all)/) { # Timestamp: Device, Type, Event, Reading, Value, Unit $retval .= sprintf("%s: %s, %s, %s, %s, %s, %s\n", $out_tstamp, $sql_device, $type, $event, $sql_reading, $out_value, $unit); $retval .= $retvaldummy; } elsif ($outf =~ m/(array)/) { push(@ReturnArray, {"tstamp" => $out_tstamp, "device" => $sql_device, "type" => $type, "event" => $event, "reading" => $sql_reading, "value" => $out_value, "unit" => $unit}); } else { $out_tstamp =~ s/\ /_/g; #needed by generating plots $retval .= "$out_tstamp $out_value\n"; $retval .= $retvaldummy; } } if(Scalar::Util::looks_like_number($sql_value)){ #nur setzen wenn nummerisch if($deltacalc) { if(Scalar::Util::looks_like_number($out_value)){ if($out_value < $min[$i]) { $min[$i] = $out_value; $mind[$i] = $out_tstamp; } if($out_value > $max[$i]) { $max[$i] = $out_value; $maxd[$i] = $out_tstamp; } } $maxval = $sql_value; } else { if($sql_value < $min[$i]) { $min[$i] = $sql_value; $mind[$i] = $sql_timestamp; } if($sql_value > $max[$i]) { $max[$i] = $sql_value; $maxd[$i] = $sql_timestamp; } $sum[$i] += $sql_value; $minval = $sql_value if($sql_value < $minval); $maxval = $sql_value if($sql_value > $maxval); } } else { $min[$i] = 0; $max[$i] = 0; $sum[$i] = 0; $minval = 0; $maxval = 0; } if(!$deltacalc) { $cnt[$i]++; $lastv[$i] = $sql_value; } else { $lastv[$i] = $out_value if($out_value); } $lastd[$i] = $sql_timestamp; } } #while fetchrow ######## den letzten Abschlusssatz rausschreiben ########## if($readings[$i]->[3] && ($readings[$i]->[3] eq "delta-h" || $readings[$i]->[3] eq "delta-d")) { if($lastd[$i] eq "undef") { $out_value = "0"; $out_tstamp = DbLog_implode_datetime($from_datetime{year}, $from_datetime{month}, $from_datetime{day}, $from_datetime{hour}, "30", "00") if($readings[$i]->[3] eq "delta-h"); $out_tstamp = DbLog_implode_datetime($from_datetime{year}, $from_datetime{month}, $from_datetime{day}, "12", "00", "00") if($readings[$i]->[3] eq "delta-d"); } else { %lasttstamp = DbLog_explode_datetime($lastd[$i], ()); $out_value = sprintf("%g", $maxval - $minval); $out_tstamp = DbLog_implode_datetime($lasttstamp{year}, $lasttstamp{month}, $lasttstamp{day}, $lasttstamp{hour}, "30", "00") if($readings[$i]->[3] eq "delta-h"); $out_tstamp = DbLog_implode_datetime($lasttstamp{year}, $lasttstamp{month}, $lasttstamp{day}, "12", "00", "00") if($readings[$i]->[3] eq "delta-d"); } $sum[$i] += $out_value; $cnt[$i]++; if($outf =~ m/(all)/) { $retval .= sprintf("%s: %s %s %s %s %s %s\n", $out_tstamp, $sql_device, $type, $event, $sql_reading, $out_value, $unit); } elsif ($outf =~ m/(array)/) { push(@ReturnArray, {"tstamp" => $out_tstamp, "device" => $sql_device, "type" => $type, "event" => $event, "reading" => $sql_reading, "value" => $out_value, "unit" => $unit}); } else { $out_tstamp =~ s/\ /_/g; #needed by generating plots $retval .= "$out_tstamp $out_value\n"; } } # DatenTrenner setzen $retval .= "#$readings[$i]->[0]"; $retval .= ":"; $retval .= "$readings[$i]->[1]" if($readings[$i]->[1]); $retval .= ":"; $retval .= "$readings[$i]->[2]" if($readings[$i]->[2]); $retval .= ":"; $retval .= "$readings[$i]->[3]" if($readings[$i]->[3]); $retval .= ":"; $retval .= "$readings[$i]->[4]" if($readings[$i]->[4]); $retval .= "\n"; } #for @readings #Ueberfuehren der gesammelten Werte in die globale Variable %data for(my $j=0; $jdisconnect() if( $hash->{PID} != $$ ); if($internal) { $internal_data = \$retval; return undef; } elsif($outf =~ m/(array)/) { return @ReturnArray; } else { return $retval; } } ### DBLog - Historische Werte ausduennen > Forum #41089 sub DbLog_reduceLog($@) { my ($hash,@a) = @_; my ($ret,$cmd,$row,$filter,$exclude,$c,$day,$hour,$lastHour,$updDate,$updHour,$average,$processingDay,$lastUpdH,%hourlyKnown,%averageHash,@excludeRegex,@dayRows,@averageUpd,@averageUpdD); my ($dbh,$name,$startTime,$currentHour,$currentDay,$deletedCount,$updateCount,$sum,$rowCount,$excludeCount) = ($hash->{DBH},$hash->{NAME},time(),99,0,0,0,0,0,0); if ($a[-1] =~ /^EXCLUDE=(.+:.+)+/i) { ($filter) = $a[-1] =~ /^EXCLUDE=(.+)/i; @excludeRegex = split(',',$filter); } elsif ($a[-1] =~ /^INCLUDE=.+:.+$/i) { $filter = 1; } if (defined($a[3])) { $average = ($a[3] =~ /average=day/i) ? "AVERAGE=DAY" : ($a[3] =~ /average/i) ? "AVERAGE=HOUR" : 0; } Log3($name, 3, "DbLog $name: reduceLog requested with DAYS=$a[2]" .(($average || $filter) ? ', ' : '').(($average) ? "$average" : '') .(($average && $filter) ? ", " : '').(($filter) ? uc((split('=',$a[-1]))[0]).'='.(split('=',$a[-1]))[1] : '')); if ($hash->{DBMODEL} eq 'SQLITE') { $cmd = "datetime('now', '-$a[2] days')"; } elsif ($hash->{DBMODEL} eq 'MYSQL') { $cmd = "DATE_SUB(CURDATE(),INTERVAL $a[2] DAY)"; } elsif ($hash->{DBMODEL} eq 'POSTGRESQL') { $cmd = "NOW() - INTERVAL '$a[2]' DAY"; } else { $ret = 'Unknown database type.'; } if ($cmd) { my $sth_del = $dbh->prepare_cached("DELETE FROM history WHERE (DEVICE=?) AND (READING=?) AND (TIMESTAMP=?) AND (VALUE=?)"); my $sth_upd = $dbh->prepare_cached("UPDATE history SET TIMESTAMP=?, EVENT=?, VALUE=? WHERE (DEVICE=?) AND (READING=?) AND (TIMESTAMP=?) AND (VALUE=?)"); my $sth_delD = $dbh->prepare_cached("DELETE FROM history WHERE (DEVICE=?) AND (READING=?) AND (TIMESTAMP=?)"); my $sth_updD = $dbh->prepare_cached("UPDATE history SET TIMESTAMP=?, EVENT=?, VALUE=? WHERE (DEVICE=?) AND (READING=?) AND (TIMESTAMP=?)"); my $sth_get = $dbh->prepare("SELECT TIMESTAMP,DEVICE,'',READING,VALUE FROM history WHERE " .($a[-1] =~ /^INCLUDE=(.+):(.+)$/i ? "DEVICE like '$1' AND READING like '$2' AND " : '') ."TIMESTAMP < $cmd ORDER BY TIMESTAMP ASC"); # '' was EVENT, no longer in use $sth_get->execute(); do { $row = $sth_get->fetchrow_arrayref || ['0000-00-00 00:00:00','D','','R','V']; # || execute last-day dummy $ret = 1; ($day,$hour) = $row->[0] =~ /-(\d{2})\s(\d{2}):/; $rowCount++ if($day != 00); if ($day != $currentDay) { if ($currentDay) { # false on first executed day if (scalar @dayRows) { ($lastHour) = $dayRows[-1]->[0] =~ /(.*\d+\s\d{2}):/; $c = 0; for my $delRow (@dayRows) { $c++ if($day != 00 || $delRow->[0] !~ /$lastHour/); } if($c) { $deletedCount += $c; Log3($name, 3, "DbLog $name: reduceLog deleting $c records of day: $processingDay"); $dbh->{RaiseError} = 1; $dbh->{PrintError} = 0; $dbh->begin_work(); eval { for my $delRow (@dayRows) { if($day != 00 || $delRow->[0] !~ /$lastHour/) { Log3($name, 5, "DbLog $name: DELETE FROM history WHERE (DEVICE=$delRow->[1]) AND (READING=$delRow->[3]) AND (TIMESTAMP=$delRow->[0]) AND (VALUE=$delRow->[4])"); $sth_del->execute(($delRow->[1], $delRow->[3], $delRow->[0], $delRow->[4])); } } }; if ($@) { Log3($hash->{NAME}, 3, "DbLog $name: reduceLog ! FAILED ! for day $processingDay"); $dbh->rollback(); $ret = 0; } else { $dbh->commit(); } $dbh->{RaiseError} = 0; $dbh->{PrintError} = 1; } @dayRows = (); } if ($ret && defined($a[3]) && $a[3] =~ /average/i) { $dbh->{RaiseError} = 1; $dbh->{PrintError} = 0; $dbh->begin_work(); eval { push(@averageUpd, {%hourlyKnown}) if($day != 00); $c = 0; for my $hourHash (@averageUpd) { # Only count for logging... for my $hourKey (keys %$hourHash) { $c++ if ($hourHash->{$hourKey}->[0] && scalar(@{$hourHash->{$hourKey}->[4]}) > 1); } } $updateCount += $c; Log3($name, 3, "DbLog $name: reduceLog (hourly-average) updating $c records of day: $processingDay") if($c); # else only push to @averageUpdD for my $hourHash (@averageUpd) { for my $hourKey (keys %$hourHash) { if ($hourHash->{$hourKey}->[0]) { # true if reading is a number ($updDate,$updHour) = $hourHash->{$hourKey}->[0] =~ /(.*\d+)\s(\d{2}):/; if (scalar(@{$hourHash->{$hourKey}->[4]}) > 1) { # true if reading has multiple records this hour for (@{$hourHash->{$hourKey}->[4]}) { $sum += $_; } $average = sprintf('%.3f', $sum/scalar(@{$hourHash->{$hourKey}->[4]}) ); $sum = 0; Log3($name, 5, "DbLog $name: UPDATE history SET TIMESTAMP=$updDate $updHour:30:00, EVENT='rl_av_h', VALUE=$average WHERE DEVICE=$hourHash->{$hourKey}->[1] AND READING=$hourHash->{$hourKey}->[3] AND TIMESTAMP=$hourHash->{$hourKey}->[0] AND VALUE=$hourHash->{$hourKey}->[4]->[0]"); $sth_upd->execute(("$updDate $updHour:30:00", 'rl_av_h', $average, $hourHash->{$hourKey}->[1], $hourHash->{$hourKey}->[3], $hourHash->{$hourKey}->[0], $hourHash->{$hourKey}->[4]->[0])); push(@averageUpdD, ["$updDate $updHour:30:00", 'rl_av_h', $average, $hourHash->{$hourKey}->[1], $hourHash->{$hourKey}->[3], $updDate]) if (defined($a[3]) && $a[3] =~ /average=day/i); } else { push(@averageUpdD, [$hourHash->{$hourKey}->[0], $hourHash->{$hourKey}->[2], $hourHash->{$hourKey}->[4]->[0], $hourHash->{$hourKey}->[1], $hourHash->{$hourKey}->[3], $updDate]) if (defined($a[3]) && $a[3] =~ /average=day/i); } } } } }; if ($@) { Log3($hash->{NAME}, 3, "DbLog $name: reduceLog average=hour ! FAILED ! for day $processingDay"); $dbh->rollback(); @averageUpdD = (); } else { $dbh->commit(); } $dbh->{RaiseError} = 0; $dbh->{PrintError} = 1; @averageUpd = (); } if (defined($a[3]) && $a[3] =~ /average=day/i && scalar(@averageUpdD) && $day != 00) { $dbh->{RaiseError} = 1; $dbh->{PrintError} = 0; $dbh->begin_work(); eval { for (@averageUpdD) { push(@{$averageHash{$_->[3].$_->[4]}->{tedr}}, [$_->[0], $_->[1], $_->[3], $_->[4]]); $averageHash{$_->[3].$_->[4]}->{sum} += $_->[2]; $averageHash{$_->[3].$_->[4]}->{date} = $_->[5]; } $c = 0; for (keys %averageHash) { if(scalar @{$averageHash{$_}->{tedr}} == 1) { delete $averageHash{$_}; } else { $c += (scalar(@{$averageHash{$_}->{tedr}}) - 1); } } $deletedCount += $c; $updateCount += keys(%averageHash); Log3($name, 3, "DbLog $name: reduceLog (daily-average) updating ".(keys %averageHash).", deleting $c records of day: $processingDay") if(keys %averageHash); for my $reading (keys %averageHash) { $average = sprintf('%.3f', $averageHash{$reading}->{sum}/scalar(@{$averageHash{$reading}->{tedr}})); $lastUpdH = pop @{$averageHash{$reading}->{tedr}}; for (@{$averageHash{$reading}->{tedr}}) { Log3($name, 5, "DbLog $name: DELETE FROM history WHERE DEVICE='$_->[2]' AND READING='$_->[3]' AND TIMESTAMP='$_->[0]'"); $sth_delD->execute(($_->[2], $_->[3], $_->[0])); } Log3($name, 5, "DbLog $name: UPDATE history SET TIMESTAMP=$averageHash{$reading}->{date} 12:00:00, EVENT='rl_av_d', VALUE=$average WHERE (DEVICE=$lastUpdH->[2]) AND (READING=$lastUpdH->[3]) AND (TIMESTAMP=$lastUpdH->[0])"); $sth_updD->execute(($averageHash{$reading}->{date}." 12:00:00", 'rl_av_d', $average, $lastUpdH->[2], $lastUpdH->[3], $lastUpdH->[0])); } }; if ($@) { Log3($hash->{NAME}, 3, "DbLog $name: reduceLog average=day ! FAILED ! for day $processingDay"); $dbh->rollback(); } else { $dbh->commit(); } $dbh->{RaiseError} = 0; $dbh->{PrintError} = 1; } %averageHash = (); %hourlyKnown = (); @averageUpd = (); @averageUpdD = (); $currentHour = 99; } $currentDay = $day; } if ($hour != $currentHour) { # forget records from last hour, but remember these for average if (defined($a[3]) && $a[3] =~ /average/i && keys(%hourlyKnown)) { push(@averageUpd, {%hourlyKnown}); } %hourlyKnown = (); $currentHour = $hour; } if (defined $hourlyKnown{$row->[1].$row->[3]}) { # remember first readings for device per h, other can be deleted push(@dayRows, [@$row]); if (defined($a[3]) && $a[3] =~ /average/i && defined($row->[4]) && $row->[4] =~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/ && $hourlyKnown{$row->[1].$row->[3]}->[0]) { if ($hourlyKnown{$row->[1].$row->[3]}->[0]) { push(@{$hourlyKnown{$row->[1].$row->[3]}->[4]}, $row->[4]); } } } else { $exclude = 0; for (@excludeRegex) { $exclude = 1 if("$row->[1]:$row->[3]" =~ /^$_$/); } if ($exclude) { $excludeCount++ if($day != 00); } else { $hourlyKnown{$row->[1].$row->[3]} = (defined($row->[4]) && $row->[4] =~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/) ? [$row->[0],$row->[1],$row->[2],$row->[3],[$row->[4]]] : [0]; } } $processingDay = (split(' ',$row->[0]))[0]; } while( $day != 00 ); my $result = "Rows processed: $rowCount, deleted: $deletedCount" .((defined($a[3]) && $a[3] =~ /average/i)? ", updated: $updateCount" : '') .(($excludeCount)? ", excluded: $excludeCount" : '') .", time: ".sprintf('%.2f',time() - $startTime)."sec"; Log3($name, 3, "DbLog $name: reduceLog executed. $result"); readingsSingleUpdate($hash, 'lastReduceLogResult', $result ,1); $ret = "reduceLog executed. $result"; } return $ret; } ################################################################ # # Charting Specific functions start here # ################################################################ ################################################################ # # Error handling, returns a JSON String # ################################################################ sub jsonError($) { my $errormsg = $_[0]; my $json = '{"success": "false", "msg":"'.$errormsg.'"}'; return $json; } ################################################################ # # Prepare the SQL String # ################################################################ sub prepareSql(@) { my ($hash, @a) = @_; my $starttime = $_[5]; $starttime =~ s/_/ /; my $endtime = $_[6]; $endtime =~ s/_/ /; my $device = $_[7]; my $userquery = $_[8]; my $xaxis = $_[9]; my $yaxis = $_[10]; my $savename = $_[11]; my $jsonChartConfig = $_[12]; my $pagingstart = $_[13]; my $paginglimit = $_[14]; my $dbmodel = $hash->{DBMODEL}; my ($sql, $jsonstring, $countsql, $hourstats, $daystats, $weekstats, $monthstats, $yearstats); if ($dbmodel eq "POSTGRESQL") { ### POSTGRESQL Queries for Statistics ### ### hour: $hourstats = "SELECT to_char(timestamp, 'YYYY-MM-DD HH24:00:00') AS TIMESTAMP, SUM(VALUE::float) AS SUM, "; $hourstats .= "AVG(VALUE::float) AS AVG, MIN(VALUE::float) AS MIN, MAX(VALUE::float) AS MAX, "; $hourstats .= "COUNT(VALUE) AS COUNT FROM history WHERE READING = '$yaxis' AND DEVICE = '$device' "; $hourstats .= "AND TIMESTAMP Between '$starttime' AND '$endtime' GROUP BY 1 ORDER BY 1;"; ### day: $daystats = "SELECT to_char(timestamp, 'YYYY-MM-DD 00:00:00') AS TIMESTAMP, SUM(VALUE::float) AS SUM, "; $daystats .= "AVG(VALUE::float) AS AVG, MIN(VALUE::float) AS MIN, MAX(VALUE::float) AS MAX, "; $daystats .= "COUNT(VALUE) AS COUNT FROM history WHERE READING = '$yaxis' AND DEVICE = '$device' "; $daystats .= "AND TIMESTAMP Between '$starttime' AND '$endtime' GROUP BY 1 ORDER BY 1;"; ### week: $weekstats = "SELECT date_trunc('week',timestamp) AS TIMESTAMP, SUM(VALUE::float) AS SUM, "; $weekstats .= "AVG(VALUE::float) AS AVG, MIN(VALUE::float) AS MIN, MAX(VALUE::float) AS MAX, "; $weekstats .= "COUNT(VALUE) AS COUNT FROM history WHERE READING = '$yaxis' AND DEVICE = '$device' "; $weekstats .= "AND TIMESTAMP Between '$starttime' AND '$endtime' GROUP BY 1 ORDER BY 1;"; ### month: $monthstats = "SELECT to_char(timestamp, 'YYYY-MM-01 00:00:00') AS TIMESTAMP, SUM(VALUE::float) AS SUM, "; $monthstats .= "AVG(VALUE::float) AS AVG, MIN(VALUE::float) AS MIN, MAX(VALUE::float) AS MAX, "; $monthstats .= "COUNT(VALUE) AS COUNT FROM history WHERE READING = '$yaxis' AND DEVICE = '$device' "; $monthstats .= "AND TIMESTAMP Between '$starttime' AND '$endtime' GROUP BY 1 ORDER BY 1;"; ### year: $yearstats = "SELECT to_char(timestamp, 'YYYY-01-01 00:00:00') AS TIMESTAMP, SUM(VALUE::float) AS SUM, "; $yearstats .= "AVG(VALUE::float) AS AVG, MIN(VALUE::float) AS MIN, MAX(VALUE::float) AS MAX, "; $yearstats .= "COUNT(VALUE) AS COUNT FROM history WHERE READING = '$yaxis' AND DEVICE = '$device' "; $yearstats .= "AND TIMESTAMP Between '$starttime' AND '$endtime' GROUP BY 1 ORDER BY 1;"; } elsif ($dbmodel eq "MYSQL") { ### MYSQL Queries for Statistics ### ### hour: $hourstats = "SELECT date_format(timestamp, '%Y-%m-%d %H:00:00') AS TIMESTAMP, SUM(CAST(VALUE AS DECIMAL(12,4))) AS SUM, "; $hourstats .= "AVG(CAST(VALUE AS DECIMAL(12,4))) AS AVG, MIN(CAST(VALUE AS DECIMAL(12,4))) AS MIN, "; $hourstats .= "MAX(CAST(VALUE AS DECIMAL(12,4))) AS MAX, COUNT(VALUE) AS COUNT FROM history WHERE READING = '$yaxis' "; $hourstats .= "AND DEVICE = '$device' AND TIMESTAMP Between '$starttime' AND '$endtime' GROUP BY 1 ORDER BY 1;"; ### day: $daystats = "SELECT date_format(timestamp, '%Y-%m-%d 00:00:00') AS TIMESTAMP, SUM(CAST(VALUE AS DECIMAL(12,4))) AS SUM, "; $daystats .= "AVG(CAST(VALUE AS DECIMAL(12,4))) AS AVG, MIN(CAST(VALUE AS DECIMAL(12,4))) AS MIN, "; $daystats .= "MAX(CAST(VALUE AS DECIMAL(12,4))) AS MAX, COUNT(VALUE) AS COUNT FROM history WHERE READING = '$yaxis' "; $daystats .= "AND DEVICE = '$device' AND TIMESTAMP Between '$starttime' AND '$endtime' GROUP BY 1 ORDER BY 1;"; ### week: $weekstats = "SELECT date_format(timestamp, '%Y-%m-%d 00:00:00') AS TIMESTAMP, SUM(CAST(VALUE AS DECIMAL(12,4))) AS SUM, "; $weekstats .= "AVG(CAST(VALUE AS DECIMAL(12,4))) AS AVG, MIN(CAST(VALUE AS DECIMAL(12,4))) AS MIN, "; $weekstats .= "MAX(CAST(VALUE AS DECIMAL(12,4))) AS MAX, COUNT(VALUE) AS COUNT FROM history WHERE READING = '$yaxis' "; $weekstats .= "AND DEVICE = '$device' AND TIMESTAMP Between '$starttime' AND '$endtime' "; $weekstats .= "GROUP BY date_format(timestamp, '%Y-%u 00:00:00') ORDER BY 1;"; ### month: $monthstats = "SELECT date_format(timestamp, '%Y-%m-01 00:00:00') AS TIMESTAMP, SUM(CAST(VALUE AS DECIMAL(12,4))) AS SUM, "; $monthstats .= "AVG(CAST(VALUE AS DECIMAL(12,4))) AS AVG, MIN(CAST(VALUE AS DECIMAL(12,4))) AS MIN, "; $monthstats .= "MAX(CAST(VALUE AS DECIMAL(12,4))) AS MAX, COUNT(VALUE) AS COUNT FROM history WHERE READING = '$yaxis' "; $monthstats .= "AND DEVICE = '$device' AND TIMESTAMP Between '$starttime' AND '$endtime' GROUP BY 1 ORDER BY 1;"; ### year: $yearstats = "SELECT date_format(timestamp, '%Y-01-01 00:00:00') AS TIMESTAMP, SUM(CAST(VALUE AS DECIMAL(12,4))) AS SUM, "; $yearstats .= "AVG(CAST(VALUE AS DECIMAL(12,4))) AS AVG, MIN(CAST(VALUE AS DECIMAL(12,4))) AS MIN, "; $yearstats .= "MAX(CAST(VALUE AS DECIMAL(12,4))) AS MAX, COUNT(VALUE) AS COUNT FROM history WHERE READING = '$yaxis' "; $yearstats .= "AND DEVICE = '$device' AND TIMESTAMP Between '$starttime' AND '$endtime' GROUP BY 1 ORDER BY 1;"; } elsif ($hash->{DBMODEL} eq "SQLITE") { ### SQLITE Queries for Statistics ### ### hour: $hourstats = "SELECT TIMESTAMP, SUM(CAST(VALUE AS FLOAT)) AS SUM, AVG(CAST(VALUE AS FLOAT)) AS AVG, "; $hourstats .= "MIN(CAST(VALUE AS FLOAT)) AS MIN, MAX(CAST(VALUE AS FLOAT)) AS MAX, COUNT(VALUE) AS COUNT "; $hourstats .= "FROM history WHERE READING = '$yaxis' AND DEVICE = '$device' "; $hourstats .= "AND TIMESTAMP Between '$starttime' AND '$endtime' GROUP BY strftime('%Y-%m-%d %H:00:00', TIMESTAMP);"; ### day: $daystats = "SELECT TIMESTAMP, SUM(CAST(VALUE AS FLOAT)) AS SUM, AVG(CAST(VALUE AS FLOAT)) AS AVG, "; $daystats .= "MIN(CAST(VALUE AS FLOAT)) AS MIN, MAX(CAST(VALUE AS FLOAT)) AS MAX, COUNT(VALUE) AS COUNT "; $daystats .= "FROM history WHERE READING = '$yaxis' AND DEVICE = '$device' "; $daystats .= "AND TIMESTAMP Between '$starttime' AND '$endtime' GROUP BY strftime('%Y-%m-%d 00:00:00', TIMESTAMP);"; ### week: $weekstats = "SELECT TIMESTAMP, SUM(CAST(VALUE AS FLOAT)) AS SUM, AVG(CAST(VALUE AS FLOAT)) AS AVG, "; $weekstats .= "MIN(CAST(VALUE AS FLOAT)) AS MIN, MAX(CAST(VALUE AS FLOAT)) AS MAX, COUNT(VALUE) AS COUNT "; $weekstats .= "FROM history WHERE READING = '$yaxis' AND DEVICE = '$device' "; $weekstats .= "AND TIMESTAMP Between '$starttime' AND '$endtime' GROUP BY strftime('%Y-%W 00:00:00', TIMESTAMP);"; ### month: $monthstats = "SELECT TIMESTAMP, SUM(CAST(VALUE AS FLOAT)) AS SUM, AVG(CAST(VALUE AS FLOAT)) AS AVG, "; $monthstats .= "MIN(CAST(VALUE AS FLOAT)) AS MIN, MAX(CAST(VALUE AS FLOAT)) AS MAX, COUNT(VALUE) AS COUNT "; $monthstats .= "FROM history WHERE READING = '$yaxis' AND DEVICE = '$device' "; $monthstats .= "AND TIMESTAMP Between '$starttime' AND '$endtime' GROUP BY strftime('%Y-%m 00:00:00', TIMESTAMP);"; ### year: $yearstats = "SELECT TIMESTAMP, SUM(CAST(VALUE AS FLOAT)) AS SUM, AVG(CAST(VALUE AS FLOAT)) AS AVG, "; $yearstats .= "MIN(CAST(VALUE AS FLOAT)) AS MIN, MAX(CAST(VALUE AS FLOAT)) AS MAX, COUNT(VALUE) AS COUNT "; $yearstats .= "FROM history WHERE READING = '$yaxis' AND DEVICE = '$device' "; $yearstats .= "AND TIMESTAMP Between '$starttime' AND '$endtime' GROUP BY strftime('%Y 00:00:00', TIMESTAMP);"; } else { $sql = "errordb"; } if($userquery eq "getreadings") { $sql = "SELECT distinct(reading) FROM history WHERE device = '".$device."'"; } elsif($userquery eq "getdevices") { $sql = "SELECT distinct(device) FROM history"; } elsif($userquery eq "timerange") { $sql = "SELECT ".$xaxis.", VALUE FROM history WHERE READING = '$yaxis' AND DEVICE = '$device' AND TIMESTAMP Between '$starttime' AND '$endtime' ORDER BY TIMESTAMP;"; } elsif($userquery eq "hourstats") { $sql = $hourstats; } elsif($userquery eq "daystats") { $sql = $daystats; } elsif($userquery eq "weekstats") { $sql = $weekstats; } elsif($userquery eq "monthstats") { $sql = $monthstats; } elsif($userquery eq "yearstats") { $sql = $yearstats; } elsif($userquery eq "savechart") { $sql = "INSERT INTO frontend (TYPE, NAME, VALUE) VALUES ('savedchart', '$savename', '$jsonChartConfig')"; } elsif($userquery eq "renamechart") { $sql = "UPDATE frontend SET NAME = '$savename' WHERE ID = '$jsonChartConfig'"; } elsif($userquery eq "deletechart") { $sql = "DELETE FROM frontend WHERE TYPE = 'savedchart' AND ID = '".$savename."'"; } elsif($userquery eq "updatechart") { $sql = "UPDATE frontend SET VALUE = '$jsonChartConfig' WHERE ID = '".$savename."'"; } elsif($userquery eq "getcharts") { $sql = "SELECT * FROM frontend WHERE TYPE = 'savedchart'"; } elsif($userquery eq "getTableData") { if ($device ne '""' && $yaxis ne '""') { $sql = "SELECT * FROM history WHERE READING = '$yaxis' AND DEVICE = '$device' "; $sql .= "AND TIMESTAMP Between '$starttime' AND '$endtime'"; $sql .= " LIMIT '$paginglimit' OFFSET '$pagingstart'"; $countsql = "SELECT count(*) FROM history WHERE READING = '$yaxis' AND DEVICE = '$device' "; $countsql .= "AND TIMESTAMP Between '$starttime' AND '$endtime'"; } elsif($device ne '""' && $yaxis eq '""') { $sql = "SELECT * FROM history WHERE DEVICE = '$device' "; $sql .= "AND TIMESTAMP Between '$starttime' AND '$endtime'"; $sql .= " LIMIT '$paginglimit' OFFSET '$pagingstart'"; $countsql = "SELECT count(*) FROM history WHERE DEVICE = '$device' "; $countsql .= "AND TIMESTAMP Between '$starttime' AND '$endtime'"; } else { $sql = "SELECT * FROM history"; $sql .= " WHERE TIMESTAMP Between '$starttime' AND '$endtime'"; $sql .= " LIMIT '$paginglimit' OFFSET '$pagingstart'"; $countsql = "SELECT count(*) FROM history"; $countsql .= " WHERE TIMESTAMP Between '$starttime' AND '$endtime'"; } return ($sql, $countsql); } else { $sql = "error"; } return $sql; } ################################################################ # # Do the query # ################################################################ sub chartQuery($@) { my ($sql, $countsql) = prepareSql(@_); if ($sql eq "error") { return jsonError("Could not setup SQL String. Maybe the Database is busy, please try again!"); } elsif ($sql eq "errordb") { return jsonError("The Database Type is not supported!"); } my ($hash, @a) = @_; my $dbhf= $hash->{DBHF}; my $totalcount; if (defined $countsql && $countsql ne "") { my $query_handle = $dbhf->prepare($countsql) or return jsonError("Could not prepare statement: " . $dbhf->errstr . ", SQL was: " .$countsql); $query_handle->execute() or return jsonError("Could not execute statement: " . $query_handle->errstr); my @data = $query_handle->fetchrow_array(); $totalcount = join(", ", @data); } # prepare the query my $query_handle = $dbhf->prepare($sql) or return jsonError("Could not prepare statement: " . $dbhf->errstr . ", SQL was: " .$sql); # execute the query $query_handle->execute() or return jsonError("Could not execute statement: " . $query_handle->errstr); my $columns = $query_handle->{'NAME'}; my $columncnt; # When columns are empty but execution was successful, we have done a successful INSERT, UPDATE or DELETE if($columns) { $columncnt = scalar @$columns; } else { return '{"success": "true", "msg":"All ok"}'; } my $i = 0; my $jsonstring = '{"data":['; while ( my @data = $query_handle->fetchrow_array()) { if($i == 0) { $jsonstring .= '{'; } else { $jsonstring .= ',{'; } for ($i = 0; $i < $columncnt; $i++) { $jsonstring .= '"'; $jsonstring .= uc($query_handle->{NAME}->[$i]); $jsonstring .= '":'; if (defined $data[$i]) { my $fragment = substr($data[$i],0,1); if ($fragment eq "{") { $jsonstring .= $data[$i]; } else { $jsonstring .= '"'.$data[$i].'"'; } } else { $jsonstring .= '""' } if($i != ($columncnt -1)) { $jsonstring .= ','; } } $jsonstring .= '}'; } $jsonstring .= ']'; if (defined $totalcount && $totalcount ne "") { $jsonstring .= ',"totalCount": '.$totalcount.'}'; } else { $jsonstring .= '}'; } return $jsonstring; } ######################### sub DbLog_fhemwebFn($$$$) { my ($FW_wname, $d, $room, $pageHash) = @_; # pageHash is set for summaryFn. my $ret; my $newIdx=1; while($defs{"SVG_${d}_$newIdx"}) { $newIdx++; } my $name = "SVG_${d}_$newIdx"; $ret .= FW_pH("cmd=define $name SVG $d:templateDB:HISTORY;". "set $name copyGplotFile&detail=$name", "
Create SVG plot from DbLog
", 0, "dval", 1); return $ret; } sub DbLog_sampleDataFn($$$$$) { my ($dlName, $dlog, $max, $conf, $wName) = @_; my $desc = "Device:Reading"; my @htmlArr; my @example; my @colregs; my $counter; my $currentPresent = AttrVal($dlName,'DbLogType','Current'); if($currentPresent =~ m/Current/) { # Table Current present, use it for sample data my $dbhf = $defs{$dlName}{DBHF}; my $query = "select device,reading,value from current where device <> '' order by device,reading"; my $sth = $dbhf->prepare( $query ); $sth->execute(); while (my @line = $sth->fetchrow_array()) { $counter++; push (@example, join (" ",@line)) if($counter <= 8); # show max 8 examples push (@colregs, "$line[0]:$line[1]"); # push all eventTypes to selection list } my $cols = join(",", sort @colregs); $max = 8 if($max > 8); for(my $r=0; $r < $max; $r++) { my @f = split(":", ($dlog->[$r] ? $dlog->[$r] : ":::"), 4); my $ret = ""; $ret .= SVG_sel("par_${r}_0", $cols, "$f[0]:$f[1]"); # $ret .= SVG_txt("par_${r}_2", "", $f[2], 1); # Default not yet implemented # $ret .= SVG_txt("par_${r}_3", "", $f[3], 3); # Function # $ret .= SVG_txt("par_${r}_4", "", $f[4], 3); # RegExp push @htmlArr, $ret; } } else { # Table Current not present, so create an empty input field push @example, "No sample data due to missing table 'Current'"; $max = 8 if($max > 8); for(my $r=0; $r < $max; $r++) { my @f = split(":", ($dlog->[$r] ? $dlog->[$r] : ":::"), 4); my $ret = ""; $ret .= SVG_txt("par_${r}_0", "", "$f[0]:$f[1]:$f[2]:$f[3]", 20); # $ret .= SVG_txt("par_${r}_2", "", $f[2], 1); # Default not yet implemented # $ret .= SVG_txt("par_${r}_3", "", $f[3], 3); # Function # $ret .= SVG_txt("par_${r}_4", "", $f[4], 3); # RegExp push @htmlArr, $ret; } } return ($desc, \@htmlArr, join("
", @example)); } # # get ReadingsVal # get ReadingsTimestamp # sub dbReadings($@) { my($hash,@a) = @_; my $dbhf= $hash->{DBHF}; return 'Wrong Syntax for ReadingsVal!' unless defined($a[4]); my $DbLogType = AttrVal($a[0],'DbLogType','current'); my $query; if (lc($DbLogType) =~ m(current) ) { $query = "select VALUE,TIMESTAMP from current where DEVICE= '$a[2]' and READING= '$a[3]'"; } else { $query = "select VALUE,TIMESTAMP from history where DEVICE= '$a[2]' and READING= '$a[3]' order by TIMESTAMP desc limit 1"; } my ($reading,$timestamp) = $dbhf->selectrow_array($query); $reading = (defined($reading)) ? $reading : $a[4]; $timestamp = (defined($timestamp)) ? $timestamp : $a[4]; return $reading if $a[1] eq 'ReadingsVal'; return $timestamp if $a[1] eq 'ReadingsTimestamp'; return "Syntax error: $a[1]"; } 1; =pod =item helper =begin html

DbLog


    Define
      define <name> DbLog <configfilename> <regexp>

      Log events to a database. The database connection is defined in <configfilename> (see sample configuration file contrib/dblog/db.conf). The configuration is stored in a separate file to avoid storing the password in the main configuration file and to have it visible in the output of the list command.

      The modules DBI and DBD::<dbtype> need to be installed (use cpan -i <module> if your distribution does not have it).

      <regexp> is the same as in FileLog.

      Sample code to create a MySQL/PostgreSQL database is in <DBType>_create.sql. The database contains two tables: current and history. The latter contains all events whereas the former only contains the last event for any given reading and device. The columns have the following meaning:
      1. TIMESTAMP: timestamp of event, e.g. 2007-12-30 21:45:22
      2. DEVICE: device name, e.g. Wetterstation
      3. TYPE: device type, e.g. KS300
      4. EVENT: event specification as full string, e.g. humidity: 71 (%)
      5. READING: name of reading extracted from event, e.g. humidity
      6. VALUE: actual reading extracted from event, e.g. 71
      7. UNIT: unit extracted from event, e.g. %
      The content of VALUE is optimized for automated post-processing, e.g. yes is translated to 1

      The current values can be retrieved by the following code like FileLog:
        get myDbLog - - 2012-11-10 2012-11-10 KS300:temperature::


      Examples:
        # log everything to database
        define myDbLog DbLog /etc/fhem/db.conf .*:.*


    Set
      set <name> reopen

        Perform a database disconnect and immediate reconnect to clear cache and flush journal file.

      set <name> rereadcfg

        Perform a database disconnect and immediate reconnect to clear cache and flush journal file.
        Probably same behavior als reopen, but rereadcfg will read the configuration data before reconnect.

      set <name> count

        Count records in tables current and history and write results into readings countCurrent and countHistory.

      set <name> deleteOldDays <n>

        Delete records from history older than <n> days. Number of deleted record will be written into reading lastRowsDeleted.

      set <name> reduceLog <n> [average[=day]] [exclude=deviceRegExp1:ReadingRegExp1,deviceRegExp2:ReadingRegExp2,...]

        Reduce records older than <n> days to one record each hour (the 1st) per device & reading.
        CAUTION: It is strongly recommended to check if the default INDEX 'Search_Idx' exists on the table 'history'!
        The execution of this command may take (without INDEX) extremely long, FHEM will be completely blocked after issuing the command to completion!
        With the optional argument 'average' not only the records will be reduced, but all numerical values of an hour will be reduced to a single average.
        With the optional argument 'average=day' not only the records will be reduced, but all numerical values of a day will be reduced to a single average. (implies 'average')
        You can optional set the last argument to "EXCLUDE=deviceRegExp1:ReadingRegExp1,deviceRegExp2:ReadingRegExp2,...." to exclude device/readings from reduceLog
        You can optional set the last argument to "INCLUDE=Database-deviceRegExp:Database-ReadingRegExp" to delimit the SELECT statement which is executet on the database. This reduce the system RAM load and increase the performance. (Wildcards are % and _)

      set <name> userCommand <validSqlStatement>

        DO NOT USE THIS COMMAND UNLESS YOU REALLY (REALLY!) KNOW WHAT YOU ARE DOING!!!

        Perform any (!!!) sql statement on connected database. Useercommand and result will be written into corresponding readings.


    Get
      get <name> ReadingsVal       <device> <reading> <default>
      get <name> ReadingsTimestamp <device> <reading> <default>

      Retrieve one single value, use and syntax are similar to ReadingsVal() and ReadingsTimestamp() functions.


      get <name> <infile> <outfile> <from> <to> <column_spec>

      Read data from the Database, used by frontends to plot data without direct access to the Database.
      • <in>
        A dummy parameter for FileLog compatibility. Sessing by defaultto -
        • current: reading actual readings from table "current"
        • history: reading history readings from table "history"
        • -: identical to "history"
      • <out>
        A dummy parameter for FileLog compatibility. Setting by default to - to check the output for plot-computing.
        Set it to the special keyword all to get all columns from Database.
        • ALL: get all colums from table, including a header
        • Array: get the columns as array of hashes
        • INT: internally used by generating plots
        • -: default
      • <from> / <to>
        Used to select the data. Please use the following timeformat or an initial substring of it:
          YYYY-MM-DD_HH24:MI:SS
      • <column_spec>
        For each column_spec return a set of data separated by a comment line on the current connection.
        Syntax: <device>:<reading>:<default>:<fn>:<regexp>
        • <device>
          The name of the device. Case sensitive. Using a the joker "%" is supported.
        • <reading>
          The reading of the given device to select. Case sensitive. Using a the joker "%" is supported.
        • <default>
          no implemented yet
        • <fn> One of the following:
          • int
            Extract the integer at the beginning of the string. Used e.g. for constructs like 10%
          • int<digit>
            Extract the decimal digits including negative character and decimal point at the beginning og the string. Used e.g. for constructs like 15.7°C
          • delta-h / delta-d
            Return the delta of the values for a given hour or a given day. Used if the column contains a counter, as is the case for the KS300 rain column.
          • delta-ts
            Replaced the original value with a measured value of seconds since the last and the actual logentry.
        • <regexp>
          The string is evaluated as a perl expression. The regexp is executed before <fn> parameter.
          Note: The string/perl expression cannot contain spaces, as the part after the space will be considered as the next column_spec.
          Keywords
        • $val is the current value returned from the Database.
        • $ts is the current timestamp returned from the Database.
        • This Logentry will not print out if $val contains th keyword "hide".
        • This Logentry will not print out and not used in the following processing if $val contains th keyword "ignore".


      Examples:
      • get myDbLog - - 2012-11-10 2012-11-20 KS300:temperature
      • get myDbLog current ALL - - %:temperature

      • you will get all actual readings "temperature" from all logged devices. Be carful by using "history" as inputfile because a long execution time will be expected!
      • get myDbLog - - 2012-11-10_10 2012-11-10_20 KS300:temperature::int1
        like from 10am until 08pm at 10.11.2012
      • get myDbLog - all 2012-11-10 2012-11-20 KS300:temperature
      • get myDbLog - - 2012-11-10 2012-11-20 KS300:temperature KS300:rain::delta-h KS300:rain::delta-d
      • get myDbLog - - 2012-11-10 2012-11-20 MyFS20:data:::$val=~s/(on|off).*/$1eq"on"?1:0/eg
        return 1 for all occurance of on* (on|on-for-timer etc) and 0 for all off*
      • get myDbLog - - 2012-11-10 2012-11-20 Bodenfeuchte:data:::$val=~s/.*B:\s([-\.\d]+).*/$1/eg
        Example of OWAD: value like this: "A: 49.527 % B: 66.647 % C: 9.797 % D: 0.097 V"
        and output for port B is like this: 2012-11-20_10:23:54 66.647
      • get DbLog - - 2013-05-26 2013-05-28 Pumpe:data::delta-ts:$val=~s/on/hide/
        Setting up a "Counter of Uptime". The function delta-ts gets the seconds between the last and the actual logentry. The keyword "hide" will hide the logentry of "on" because this time is a "counter of Downtime"


    Get when used for webcharts
      get <name> <infile> <outfile> <from> <to> <device> <querytype> <xaxis> <yaxis> <savename>

      Query the Database to retrieve JSON-Formatted Data, which is used by the charting frontend.
      • <name>
        The name of the defined DbLog, like it is given in fhem.cfg.
      • <in>
        A dummy parameter for FileLog compatibility. Always set to -
      • <out>
        A dummy parameter for FileLog compatibility. Set it to webchart to use the charting related get function.
      • <from> / <to>
        Used to select the data. Please use the following timeformat:
          YYYY-MM-DD_HH24:MI:SS
      • <device>
        A string which represents the device to query.
      • <querytype>
        A string which represents the method the query should use. Actually supported values are:
        getreadings to retrieve the possible readings for a given device
        getdevices to retrieve all available devices
        timerange to retrieve charting data, which requires a given xaxis, yaxis, device, to and from
        savechart to save a chart configuration in the database. Requires a given xaxis, yaxis, device, to and from, and a 'savename' used to save the chart
        deletechart to delete a saved chart. Requires a given id which was set on save of the chart
        getcharts to get a list of all saved charts.
        getTableData to get jsonformatted data from the database. Uses paging Parameters like start and limit.
        hourstats to get statistics for a given value (yaxis) for an hour.
        daystats to get statistics for a given value (yaxis) for a day.
        weekstats to get statistics for a given value (yaxis) for a week.
        monthstats to get statistics for a given value (yaxis) for a month.
        yearstats to get statistics for a given value (yaxis) for a year.
      • <xaxis>
        A string which represents the xaxis
      • <yaxis>
        A string which represents the yaxis
      • <savename>
        A string which represents the name a chart will be saved with
      • <chartconfig>
        A jsonstring which represents the chart to save
      • <pagingstart>
        An integer used to determine the start for the sql used for query 'getTableData'
      • <paginglimit>
        An integer used to set the limit for the sql used for query 'getTableData'


      Examples:
      • get logdb - webchart "" "" "" getcharts
        Retrieves all saved charts from the Database
      • get logdb - webchart "" "" "" getdevices
        Retrieves all available devices from the Database
      • get logdb - webchart "" "" ESA2000_LED_011e getreadings
        Retrieves all available Readings for a given device from the Database
      • get logdb - webchart 2013-02-11_00:00:00 2013-02-12_00:00:00 ESA2000_LED_011e timerange TIMESTAMP day_kwh
        Retrieves charting data, which requires a given xaxis, yaxis, device, to and from
        Will ouput a JSON like this: [{'TIMESTAMP':'2013-02-11 00:10:10','VALUE':'0.22431388090756'},{'TIMESTAMP'.....}]
      • get logdb - webchart 2013-02-11_00:00:00 2013-02-12_00:00:00 ESA2000_LED_011e savechart TIMESTAMP day_kwh tageskwh
        Will save a chart in the database with the given name and the chart configuration parameters
      • get logdb - webchart "" "" "" deletechart "" "" 7
        Will delete a chart from the database with the given id


    Attributes
      shutdownWait
        attr <device> shutdownWait
        causes fhem shutdown to wait n seconds for pending database commit

      DbLogSelectionMode
        attr <device> DbLogSelectionMode
        Thise DbLog-Device-Attribute specifies how the device specific Attributes DbLogExclude and DbLogInclude are handled. If this Attribute is missing it defaults to "Exclude".
        • Exclude: DbLog behaves just as usual. This means everything specified in the regex in DEF will be logged by default and anything excluded via the DbLogExclude attribute will not be logged
        • Include: Nothing will be logged, except the readings specified via regex in the DbLogInclude attribute
        • Exclude/Include: Just almost the same as Exclude, but if the reading matches the DbLogExclude attribute, then it will further be checked against the regex in DbLogInclude whicht may possibly re-include the already excluded reading.


    See also DbLogSelectionMode-Attribute of DbLog-Device which takes influence on on how DbLogExclude and DbLogInclude are handled
      DbLogInclude
        set <device> DbLogInclude regex:MinInterval [regex:MinInterval] ...
      A new Attribute DbLogInclude will be propagated to all Devices if DBLog is used. DbLogInclude works just like DbLogExclude but to include matching readings. See also DbLogSelectionMode-Attribute of DbLog-Device which takes influence on on how DbLogExclude and DbLogInclude are handled
      Example
        attr MyDevice1 DbLogInclude .* attr MyDevice2 DbLogInclude state,(floorplantext|MyUserReading):300,battery:3600

      DbLogExclude
        set <device> DbLogExclude regex:MinInterval [regex:MinInterval] ...
      A new Attribute DbLogExclude will be propagated to all Devices if DBLog is used. DbLogExclude will work as regexp to exclude defined readings to log. Each individual regexp-group are separated by comma. If a MinInterval is set, the logentry is dropped if the defined interval is not reached and value vs. lastvalue is eqal .
      Example
        attr MyDevice1 DbLogExclude .* attr MyDevice2 DbLogExclude state,(floorplantext|MyUserReading):300,battery:3600

      suppressUndef
        attr <device> ignoreUndef
        suppresses all undef values when returning data from the DB via get
      Example
        #DbLog eMeter:power:::$val=($val>1500)?undef:$val

=end html =begin html_DE

DbLog


    Define
      define <name> DbLog <configfilename> <regexp>

      Speichert Events in eine Datenbank. Die Datenbankverbindungsparameter werden definiert in <configfilename>. (Vergleiche Beipspielkonfigurationsdatei in contrib/dblog/db.conf).
      Die Konfiguration ist in einer sparaten Datei abgelegt um das Datenbankpasswort nicht in Klartext in der FHEM-Haupt-Konfigurationsdatei speichern zu mössen. Ansonsten wäre es mittels des list Befehls einfach auslesbar.

      Die Perl-Module DBI and DBD::<dbtype> mössen installiert werden (use cpan -i <module> falls die eigene Distribution diese nicht schon mitbringt).

      <regexp> ist identisch wie FileLog.

      Ein Beispielcode zum Erstellen einer MySQL/PostGreSQL Datenbak ist in contrib/dblog/<DBType>_create.sql zu finden. Die Datenbank beinhaltet 2 Tabellen: current und history. Die Tabelle current enthält den letzten Stand pro Device und Reading. In der Tabelle history sind alle Events historisch gespeichert. Die Tabellenspalten haben folgende Bedeutung:
      1. TIMESTAMP: Zeitpunkt des Events, z.B. 2007-12-30 21:45:22
      2. DEVICE: name des Devices, z.B. Wetterstation
      3. TYPE: Type des Devices, z.B. KS300
      4. EVENT: das auftretende Event als volle Zeichenkette z.B. humidity: 71 (%)
      5. READING: Name des Readings, ermittelt aus dem Event, z.B. humidity
      6. VALUE: aktueller Wert des Readings, ermittelt aus dem Event, z.B. 71
      7. UNIT: Einheit, ermittelt aus dem Event, z.B. %
      Der Wert des Readings ist optimiert för eine automatisierte Nachverarbeitung z.B. yes ist transformiert nach 1

      Die gespeicherten Werte können mittels GET Funktion angezeigt werden:
        get myDbLog - - 2012-11-10 2012-11-10 KS300:temperature


      Beispiel:
        Speichert alles in der Datenbank
        define myDbLog DbLog /etc/fhem/db.conf .*:.*
    Set
      set <name> reopen

        Schließt die Datenbank und öffnet sie danach sofort wieder. Dabei wird die Journaldatei geleert und neu angelegt.
        Verbessert den Datendurchsatz und vermeidet Speicherplatzprobleme.

      set <name> rereadcfg

        Schließt die Datenbank und öffnet sie danach sofort wieder. Dabei wird die Journaldatei geleert und neu angelegt.
        Verbessert den Datendurchsatz und vermeidet Speicherplatzprobleme.
        Zwischen dem Schließen der Verbindung und dem Neuverbinden werden die Konfigurationsdaten neu gelesen

      set <name> count

        Zählt die Datensätze in den Tabellen current und history und schreibt die Ergebnisse in die Readings countCurrent und countHistory.

      set <name> deleteOldDays <n>

        Löscht Datensätze, die älter sind als <n> Tage. Die Anzahl der gelöschten Datensätze wird in das Reading lastRowsDeleted geschrieben.

      set <name> reduceLog <n> [average[=day]] [exclude=deviceRegExp1:ReadingRegExp1,deviceRegExp2:ReadingRegExp2,...]

        Reduziert historische Datensaetze, die aelter sind als <n> Tage auf einen Eintrag pro Stunde (den ersten) je device & reading.
        ACHTUNG: Es wird dringend empfohlen zu überprüfen ob der standard INDEX 'Search_Idx' in der Tabelle 'history' existiert!
        Die Abarbeitung dieses Befehls dauert unter umständen (ohne INDEX) extrem lange, FHEM wird nach absetzen des Befehls bis zur Fertigstellung komplett blockiert!
        Durch die optionale Angabe von 'average' wird nicht nur die Datenbank bereinigt, sondern alle numerischen Werte einer Stunde werden auf einen einzigen Mittelwert reduziert.
        Durch die optionale Angabe von 'average=day' wird nicht nur die Datenbank bereinigt, sondern alle numerischen Werte eines Tages auf einen einzigen Mittelwert reduziert. (impliziert 'average')
        Optional kann als letzer Parameter "EXCLUDE=deviceRegExp1:ReadingRegExp1,deviceRegExp2:ReadingRegExp2,...." angegeben werden um device/reading Kombinationen von reduceLog auszuschließen.
        Optional kann als letzer Parameter "INCLUDE=Database-deviceRegExp:Database-ReadingRegExp" angegeben werden um die auf die Datenbank ausgeführte SELECT abfrage einzugrenzen, was die RAM-Belastung verringer und die Performance erhöht. (Wildcards sind % und _)

      set <name> userCommand <validSqlStatement>

        BENUTZE DIESE FUNKTION NUR, WENN DU WIRKLICH (WIRKLICH!) WEISST, WAS DU TUST!!!

        Führt einen beliebigen (!!!) sql Befehl in der Datenbank aus. Der Befehl und ein zurückgeliefertes Ergebnis werden in entsprechende Readings geschrieben.


    Get
      get <name> ReadingsVal       <device> <reading> <default>
      get <name> ReadingsTimestamp <device> <reading> <default>

      Liest einen einzelnen Wert aus der Datenbank, Benutzung und Syntax sind weitgehend identisch zu ReadingsVal() und ReadingsTimestamp().


      get <name> <infile> <outfile> <from> <to> <column_spec>

      Liesst Daten aus der Datenbank. Wird durch die Frontends benutzt um Plots zu generieren ohne selbst auf die Datenank zugreifen zu mössen.
      • <in>
        Ein Parameter um eine Kompatibilität zum Filelog herzustellen. Dieser Parameter ist per default immer auf - zu setzen.
        Folgende Ausprägungen sind zugelassen:
        • current: die aktuellen Werte aus der Tabelle "current" werden gelesen.
        • history: die historischen Werte aus der Tabelle "history" werden gelesen.
        • -: identisch wie "history"
      • <out>
        Ein Parameter um eine Kompatibilität zum Filelog herzustellen. Dieser Parameter ist per default immer auf - zu setzen um die Ermittlung der Daten aus der Datenbank för die Plotgenerierung zu pröfen.
        Folgende Ausprägungen sind zugelassen:
        • ALL: Es werden alle Spalten der Datenbank ausgegeben. Inclusive einer Überschrift.
        • Array: Es werden alle Spalten der Datenbank als Hash ausgegeben. Alle Datensätze als Array zusammengefasst.
        • INT: intern zur Plotgenerierung verwendet
        • -: default
      • <from> / <to>
        Wird benutzt um den Zeitraum der Daten einzugrenzen. Es ist das folgende Zeitformat oder ein Teilstring davon zu benutzen:
          YYYY-MM-DD_HH24:MI:SS
      • <column_spec>
        För jede column_spec Gruppe wird ein Datenset zuröckgegeben welches durch einen Kommentar getrennt wird. Dieser Kommentar repräsentiert die column_spec.
        Syntax: <device>:<reading>:<default>:<fn>:<regexp>
        • <device>
          Der Name des Devices. Achtung: Gross/Kleinschreibung beachten!
          Es kann ein % als Jokerzeichen angegeben werden.
        • <reading>
          Das Reading des angegebenen Devices zur Datenselektion.
          Es kann ein % als Jokerzeichen angegeben werden.
          Achtung: Gross/Kleinschreibung beachten!
        • <default>
          Zur Zeit noch nicht implementiert.
        • <fn> Angabe einer speziellen Funktion:
          • int
            Ermittelt den Zahlenwert ab dem Anfang der Zeichenkette aus der Spalte "VALUE". Benutzt z.B. för Ausprägungen wie 10%.
          • int<digit>
            Ermittelt den Zahlenwert ab dem Anfang der Zeichenkette aus der Spalte "VALUE", inclusive negativen Vorzeichen und Dezimaltrenner. Benutzt z.B. för Ausprägungen wie -5.7°C.
          • delta-h / delta-d
            Ermittelt die relative Veränderung eines Zahlenwertes pro Stunde oder pro Tag. Wird benutzt z.B. för Spalten die einen hochlaufenden Zähler enthalten wie im Falle för ein KS300 Regenzähler oder dem 1-wire Modul OWCOUNT.
          • delta-ts
            Ermittelt die vergangene Zeit zwischen dem letzten und dem aktuellen Logeintrag in Sekunden und ersetzt damit den originalen Wert.
        • <regexp>
          Diese Zeichenkette wird als Perl Befehl ausgewertet. Die regexp wird vor dem angegebenen <fn> Parameter ausgeföhrt.
          Bitte zur Beachtung: Diese Zeichenkette darf keine Leerzeichen enthalten da diese sonst als <column_spec> Trennung interpretiert werden und alles nach dem Leerzeichen als neue <column_spec> gesehen wird.
          Schlösselwörter
        • $val ist der aktuelle Wert die die Datenbank för ein Device/Reading ausgibt.
        • $ts ist der aktuelle Timestamp des Logeintrages.
        • Wird als $val das Schlösselwort "hide" zuröckgegeben, so wird dieser Logeintrag nicht ausgegeben, trotzdem aber för die Zeitraumberechnung verwendet.
        • Wird als $val das Schlösselwort "ignore" zuröckgegeben, so wird dieser Logeintrag nicht för eine Folgeberechnung verwendet.


      Beispiele:
      • get myDbLog - - 2012-11-10 2012-11-20 KS300:temperature
      • get myDbLog current ALL - - %:temperature

      • Damit erh?lt man alle aktuellen Readings "temperature" von allen in der DB geloggten Devices. Achtung: bei Nutzung von Jokerzeichen auf die historyTabelle kann man sein FHEM aufgrund langer Laufzeit lahmlegen!
      • get myDbLog - - 2012-11-10_10 2012-11-10_20 KS300:temperature::int1
        gibt Daten aus von 10Uhr bis 20Uhr am 10.11.2012
      • get myDbLog - all 2012-11-10 2012-11-20 KS300:temperature
      • get myDbLog - - 2012-11-10 2012-11-20 KS300:temperature KS300:rain::delta-h KS300:rain::delta-d
      • get myDbLog - - 2012-11-10 2012-11-20 MyFS20:data:::$val=~s/(on|off).*/$1eq"on"?1:0/eg
        gibt 1 zuröck för alle Ausprägungen von on* (on|on-for-timer etc) und 0 för alle off*
      • get myDbLog - - 2012-11-10 2012-11-20 Bodenfeuchte:data:::$val=~s/.*B:\s([-\.\d]+).*/$1/eg
        Beispiel von OWAD: Ein Wert wie z.B.: "A: 49.527 % B: 66.647 % C: 9.797 % D: 0.097 V"
        und die Ausgabe ist för das Reading B folgende: 2012-11-20_10:23:54 66.647
      • get DbLog - - 2013-05-26 2013-05-28 Pumpe:data::delta-ts:$val=~s/on/hide/
        Realisierung eines Betriebsstundenzählers.Durch delta-ts wird die Zeit in Sek zwischen den Log- einträgen ermittelt. Die Zeiten werden bei den on-Meldungen nicht ausgegeben welche einer Abschaltzeit entsprechen wörden.


    Get för die Nutzung von webcharts
      get <name> <infile> <outfile> <from> <to> <device> <querytype> <xaxis> <yaxis> <savename>

      Liest Daten aus der Datenbank aus und gibt diese in JSON formatiert aus. Wird för das Charting Frontend genutzt
      • <name>
        Der Name des definierten DbLogs, so wie er in der fhem.cfg angegeben wurde.
      • <in>
        Ein Dummy Parameter um eine Kompatibilität zum Filelog herzustellen. Dieser Parameter ist immer auf - zu setzen.
      • <out>
        Ein Dummy Parameter um eine Kompatibilität zum Filelog herzustellen. Dieser Parameter ist auf webchart zu setzen um die Charting Get Funktion zu nutzen.
      • <from> / <to>
        Wird benutzt um den Zeitraum der Daten einzugrenzen. Es ist das folgende Zeitformat zu benutzen:
          YYYY-MM-DD_HH24:MI:SS
      • <device>
        Ein String, der das abzufragende Device darstellt.
      • <querytype>
        Ein String, der die zu verwendende Abfragemethode darstellt. Zur Zeit unterstötzte Werte sind:
        getreadings um för ein bestimmtes device alle Readings zu erhalten
        getdevices um alle verfögbaren devices zu erhalten
        timerange um Chart-Daten abzufragen. Es werden die Parameter 'xaxis', 'yaxis', 'device', 'to' und 'from' benötigt
        savechart um einen Chart unter Angabe eines 'savename' und seiner zugehörigen Konfiguration abzuspeichern
        deletechart um einen zuvor gespeicherten Chart unter Angabe einer id zu löschen
        getcharts um eine Liste aller gespeicherten Charts zu bekommen.
        getTableData um Daten aus der Datenbank abzufragen und in einer Tabelle darzustellen. Benötigt paging Parameter wie start und limit.
        hourstats um Statistiken för einen Wert (yaxis) för eine Stunde abzufragen.
        daystats um Statistiken för einen Wert (yaxis) för einen Tag abzufragen.
        weekstats um Statistiken för einen Wert (yaxis) för eine Woche abzufragen.
        monthstats um Statistiken för einen Wert (yaxis) för einen Monat abzufragen.
        yearstats um Statistiken för einen Wert (yaxis) för ein Jahr abzufragen.
      • <xaxis>
        Ein String, der die X-Achse repräsentiert
      • <yaxis>
        Ein String, der die Y-Achse repräsentiert
      • <savename>
        Ein String, unter dem ein Chart in der Datenbank gespeichert werden soll
      • <chartconfig>
        Ein jsonstring der den zu speichernden Chart repräsentiert
      • <pagingstart>
        Ein Integer um den Startwert för die Abfrage 'getTableData' festzulegen
      • <paginglimit>
        Ein Integer um den Limitwert för die Abfrage 'getTableData' festzulegen


      Beispiele:
      • get logdb - webchart "" "" "" getcharts
        Liefert alle gespeicherten Charts aus der Datenbank
      • get logdb - webchart "" "" "" getdevices
        Liefert alle verfögbaren Devices aus der Datenbank
      • get logdb - webchart "" "" ESA2000_LED_011e getreadings
        Liefert alle verfögbaren Readings aus der Datenbank unter Angabe eines Gerätes
      • get logdb - webchart 2013-02-11_00:00:00 2013-02-12_00:00:00 ESA2000_LED_011e timerange TIMESTAMP day_kwh
        Liefert Chart-Daten, die auf folgenden Parametern basieren: 'xaxis', 'yaxis', 'device', 'to' und 'from'
        Die Ausgabe erfolgt als JSON, z.B.: [{'TIMESTAMP':'2013-02-11 00:10:10','VALUE':'0.22431388090756'},{'TIMESTAMP'.....}]
      • get logdb - webchart 2013-02-11_00:00:00 2013-02-12_00:00:00 ESA2000_LED_011e savechart TIMESTAMP day_kwh tageskwh
        Speichert einen Chart unter Angabe eines 'savename' und seiner zugehörigen Konfiguration
      • get logdb - webchart "" "" "" deletechart "" "" 7
        Löscht einen zuvor gespeicherten Chart unter Angabe einer id


    Attribute
      shutdownWait
        attr <device> shutdownWait
        fhem wartet waehrend des shutdowns fuer n Sekunden, um die Datenbank korrekt zu beenden

      DbLogSelectionMode
        attr <device> DbLogSelectionMode
        Dieses, fuer DbLog-Devices spezifische Attribut beeinflußt, wie die Device-spezifischen Attributes DbLogExclude und DbLogInclude (s.u.) ausgewertet werden.
        Fehlt dieses Attribut wird dafuer "Exclude" als Default angenommen.
        • Exclude: DbLog verhaelt sich wie bisher auch, alles was ueber die RegExp im DEF angegeben ist, wird geloggt, bis auf das, was ueber die RegExp in DbLogExclude ausgeschlossen wird
          Das Attribut DbLogInclude wird in diesem Fall nicht beruecksichtigt
        • Include: Es wird nur das geloggt was ueber die RegExp in DbLogInclude eingeschlossen wird.
          Das Attribut DbLogExclude wird in diesem Fall nicht beruecksichtigt, ebenso wenig, wie die regex im DEF.
        • Exclude/Include: Funktioniert im Wesentlichen, wie "Exclude", nur das sowohl DbLogExclude, als auch DbLogInclude geprueft werden: Readings, die durch DbLogExclude zwar ausgeschlossen wurden, mit DbLogInclude aber wiederum eingeschlossen wwrden, werden somit dennoch geloggt.


    Siehe dazu auch das DbLog-Device-Spezifische Attribut DbLogSelectionMode, das beeinflußt wie DbLogExclude und DbLogInclude ausgewertet werden.
      DbLogInclude
        set <device> DbLogInclude regex:MinInterval [regex:MinInterval] ...

      Wenn DbLog genutzt wird, wird in alle Devices das Attribut DbLogInclude propagiert. DbLogInclude funktioniert im Endeffekt genau wie DbLogExclude, ausser dass eben readings mit diesen RegExp in das Logging eingeschlossen werden koennen, statt ausgeschlossen. Siehe dazu auch das DbLog-Device-Spezifische Attribut DbLogSelectionMode, das beeinflußt wie DbLogExclude und DbLogInclude ausgewertet werden.
      Beispiele
        attr MyDevice1 DbLogInclude .* attr MyDevice2 DbLogInclude state,(floorplantext|MyUserReading):300,battery:3600

      DbLogExclude
        set <device> DbLogExclude regex:MinInterval [regex:MinInterval] ...

      Wenn DbLog genutzt wird, wird in alle Devices das Attribut DbLogExclude propagiert. Der Wert des Attributes wird als Regexp ausgewertet und schliesst die damit matchenden Readings von einem Logging aus. Einzelne Regexp werden durch Kommata getrennt. Ist MinIntervall angegeben, so wird der Logeintrag nur dann nicht geloggt, wenn das Intervall noch nicht erreicht und der Wert des Readings sich nicht verändert hat.
      Beispiele
        attr MyDevice1 DbLogExclude .* attr MyDevice2 DbLogExclude state,(floorplantext|MyUserReading):300,battery:3600

      suppressUndef
        attr <device> ignoreUndef
        Unterdrueckt alle undef Werte die durch eine Get-Anfrage zb. Plot aus der Datenbank selektiert werden
      Beispiel
        #DbLog eMeter:power:::$val=($val>1500)?undef:$val

=end html_DE =cut