# $Id$ ############################################################################## # # This file is part of fhem. # # Fhem is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # Fhem is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # ############################################################################## package main; use strict; use warnings; use SetExtensions; sub logProxy_Initialize($) { my ($hash) = @_; $hash->{DefFn} = "logProxy_Define"; $hash->{UndefFn} = "logProxy_Undefine"; #$hash->{SetFn} = "logProxy_Set"; $hash->{GetFn} = "logProxy_Get"; #$hash->{AttrList} = "disable:1 "; $hash->{SVG_sampleDataFn} = "logProxy_sampleDataFn"; $hash->{SVG_regexpFn} = "logProxy_regexpFn"; } sub logProxy_Define($$) { my ($hash, $def) = @_; my @args = split("[ \t]+", $def); my $name = $args[0]; my $usage = "Usage: define logProxy"; return $usage if( int(@args) != 2 ); my $d = $modules{logProxy}{defptr}; return "logProxy device already defined as $d->{NAME}." if( defined($d) ); $modules{logProxy}{defptr} = $hash; $hash->{STATE} = 'Initialized'; return undef; } sub logProxy_Undefine($$) { my ($hash,$arg) = @_; my $name = $hash->{NAME}; delete $modules{logProxy}{defptr}; return undef; } my $logProxy_columns = "ConstX,ConstY,Func,Polar,FileLog,DbLog"; sub logProxy_sampleDataFn($$$$$) { my ($ldName, $flog, $max, $conf, $wName) = @_; my $desc = "Type,Spec"; my @htmlArr; $max = 16 if($max > 16); for(my $r=0; $r < $max; $r++) { my @f = split(":", ($flog->[$r] ? $flog->[$r] : ":"), 6); my $ret = ""; $ret .= SVG_sel("par_${r}_0", $logProxy_columns, $f[0]); $ret .= SVG_txt("par_${r}_1", "", join(":", @f[1..@f-1]), 30); push @htmlArr, $ret; } my @example; push @example, 'ConstY:0'; push @example, 'ConstY:$data{avg1}'; push @example, 'ConstY:$data{avg2}'; push @example, 'DbLog:myDB:myReading'; push @example, 'FileLog:myFileLog:4:myReading'; push @example, 'FileLog:FileLog_<SPEC1>:4:<SPEC1>.power'; push @example, 'FileLog:FileLog_<SPEC1>:4:<SPEC1>.consumption'; push @example, 'Func:logProxy_WeekProfile2Plot("HCB",$from,$to)'; push @example, 'Func:logProxy_WeekProfile2Plot("myHeatingControl",$from,$to,"(\\d*)\$")'; push @example, 'ConstX:logProxy_shiftTime($from,60*60*2),$data{min1},$data{max1}'; #Log 3, Dumper $desc; #Log 3, Dumper @htmlArr; #Log 3, Dumper $example; return ($desc, \@htmlArr, join("
", @example)); } sub logProxy_regexpFn($$) { my ($name, $filter) = @_; my $ret; my @a = split( ' ', $filter ); for(my $i = 0; $i < int(@a); $i++) { my @fld = split(":", $a[$i]); if( $a[$i] =~ m/^(FileLog|DbLog):([^:]*):(.*)/ ) { my @options = split( ',', $fld[1] ); my $log_dev = shift(@options); my $column_specs = $3; $ret .= '|' if( $ret ); $ret .= CallFn($log_dev, "SVG_regexpFn", $log_dev, $column_specs); } } return $ret; } sub logProxy_Set($@) { return undef; } #WeekProfile format: {$wday}{$time}{$value} with 0 = sunday sub logProxy_Heating_Controll2WeekProfile($) { my ($d) = @_; return undef if( !defined($defs{$d}) ); return undef if( !defined($defs{$d}->{helper}{SWITCHINGTIME}) ); return $defs{$d}->{helper}{SWITCHINGTIME}; } sub logProxy_HM2WeekProfile($;$) { my ($d,$list) = @_; return undef if( !defined($defs{$d}) ); # default to 1st list of tc-it $list = "P1" if ( !$list ); # if tc-it my @rl = sort( grep /^R_${list}_[0-7]_tempList...$/,keys %{$defs{$d}{READINGS}} ); # else cc-tc and rt @rl = sort( grep /^R_[0-7]_tempList...$/,keys %{$defs{$d}{READINGS}} ) if( !@rl ); return undef if( !@rl ); my %profile = (); for(my $i=0; $i<7; ++$i) { # correct wday my $reading = ReadingsVal($d,$rl[($i+1)%7],undef); # collect 'until' switching times my %tmp = (); my @parts = split( ' ', $reading ); while( @parts ) { my $time = shift @parts; $tmp{$time} = shift @parts; } # shift 'until' switching times into 'from' switching times # can not be done in one step if times are out of order my %st = (); my $time = "00:00"; foreach my $key (sort (keys %tmp)) { $st{$time} = $tmp{$key}; $time = $key; } $profile{$i} = \%st; } return undef if (scalar (keys %profile) != 7); return \%profile; } sub logProxy_MAX2WeekProfile($) { my ($d) = @_; return undef if( !defined($defs{$d}) ); my @rl = sort( grep /^weekprofile-.-...-(temp|time)$/,keys %{$defs{$d}{READINGS}} ); return undef if( !@rl ); my %profile = (); for(my $i=0; $i<7; ++$i) { # correct wday my $temps = ReadingsVal($d,$rl[(($i+1)%7)*2],undef); my $times = ReadingsVal($d,$rl[(($i+1)%7)*2+1],undef); my %st = (); my @temps = split( '/', $temps ); my @times = split( '/', $times ); while( @times ) { my $temp = shift @temps; $temp =~ s/\s*([\d\.]*).*/$1/; my $time = shift @times; $time =~ s/\s*(\d\d:\d\d).*/$1/; $st{$time} = $temp; } $profile{$i} = \%st; } return \%profile; } # sample implementaion to plot the week profile of a Heating_Control or HM Thermostat device. sub logProxy_WeekProfile2Plot($$$;$) { my ($profile, $from, $to, $regex) = @_; return undef if( !$profile ); if( $regex ) { eval { "test" =~ m/$regex/ }; if( $@ ) { Log3 undef, 3, "logProxy_WeekProfile2Plot: $regex: $@"; return undef; } } if( defined($defs{$profile}) ) { if( $defs{$profile}{TYPE} eq "Heating_Control" ) { $profile = logProxy_Heating_Controll2WeekProfile($profile); } elsif( $defs{$profile}{TYPE} eq "WeekdayTimer" ) { $profile = logProxy_Heating_Controll2WeekProfile($profile); } elsif( $defs{$profile}{TYPE} eq "CUL_HM" ) { my ($p,$l) = split( ',', $profile, 2 ); $profile = logProxy_HM2WeekProfile($p, $l); } elsif( $defs{$profile}{TYPE} eq "MAX" ) { $profile = logProxy_MAX2WeekProfile($profile); } else { Log3 undef, 2, "logProxy_WeekProfile2Plot: $profile is not a Heating_Control, WeekdayTimer, CUL_HM or MAX device"; return undef; } } #Log 3, Dumper $profile; if( ref($profile) ne "HASH" ) { Log3 undef, 2, "logProxy_WeekProfile2Plot: no profile hash given"; return undef; } my $fromsec = SVG_time_to_sec($from); my $tosec = SVG_time_to_sec($to); my (undef,undef,undef,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($fromsec); my $min = 999999; my $max = -999999; # go back one day to get the start value, TODO: go back multiple days $mday -= 1; $wday -= 1; $wday %= 7; my $ret = ""; my $value; my $prev_value; my $sec = $fromsec; # while not end of plot range reached while( $sec < $tosec ) { return undef if( !defined($profile->{$wday}) ); # for all switching times of current day foreach my $st (sort (keys %{ $profile->{$wday} })) { #remember previous value for start of plot range $prev_value = $value; my ($h, $m, $s) = split( ':', $st ); $s = 0 if( !$s ); $value = $profile->{$wday}{$st}; if( $regex ) { if( $value =~ m/$regex/ ) { Log3 undef, 4, "logProxy_WeekProfile2Plot: $value =~ m/$regex/ => $1"; $value = $1; } else { Log3 undef, 3, "logProxy_WeekProfile2Plot: $value =~ m/$regex/ => no match"; } } # map some specials to values and eco and comfort to temperatures $value = 0 if( $value eq "off" ); $value = 1 if( $value eq "on" ); $value = 1 if( $value eq "up" ); $value = 0 if( $value eq "down" ); $value = 18 if( $value eq "eco" ); $value = 22 if( $value eq "comfort" ); # 'dirty' hack that exploits the feature that $mday can be < 0 and > 31. # everything should better be based on a real second counter my $timestamp = sprintf("%04d-%02d-%02d_%02d:%02d:%02d", 1900+$year, 1+$mon, $mday, $h, $m, $s ); $sec = SVG_time_to_sec($timestamp); # skip all values before start of plot range next if( SVG_time_to_sec($timestamp) < $fromsec ); # add first value at start of plot range if( !$ret && defined($prev_value) ) { $min = $prev_value if( $prev_value < $min ); $max = $prev_value if( $prev_value > $max ); $ret .= "$from $prev_value\n"; } # done if after end of plot range last if( SVG_time_to_sec($timestamp) > $tosec ); $min = $value if( $value < $min ); $max = $value if( $value > $max ); # add actual controll point $ret .= "$timestamp $value\n"; } # next day $mday += 1; $wday += 1; $wday %= 7; } # add last value at end of plot range $ret .= "$to $prev_value\n"; return ($ret,$min,$max,$prev_value); } sub logProxy_hms2sec($){ my ($h,$m,$s) = split(":", shift); $m = 0 if(!$m); $s = 0 if(!$s); my $t = $s; $t += $m * 60; $t += $h * 60*60; return ($t) } sub logProxy_isDay($) { my ($sec) = @_; my $sr = logProxy_hms2sec(sunrise_abs_dat($sec)); my $ss = logProxy_hms2sec(sunset_abs_dat($sec)); my ($s,$m,$h) = localtime($sec); my $cur = logProxy_hms2sec( "$h:$m:$s" ); return ($cur > $sr && $cur < $ss)?1:0; } sub logProxy_hms2dec($){ my ($h,$m,$s) = split(":", shift); $m = 0 if(!$m); $s = 0 if(!$s); my $t = $m * 60; $t += $s; $t /= 3600; $t += $h; return ($t) } sub logProxy_dec2hms($){ my ($t) = @_; my $h = int($t); my $r = ($t - $h)*3600; my $m = int($r/60); my $s = $r - $m*60; return sprintf("%02d:%02d:%02d",$h,$m,$s); } sub logProxy_Range2Zoom($) { my( $range ) = @_; return "year" if( $range > 1+60*60*24*28*6); return "month" if( $range > 1+60*60*24*28); return "week" if( $range > 1+60*60*24); return "day" if( $range > 1+60*60*6); return "qday" if( $range > 1+60*60 ); return "hour"; } my %logProxy_stepDefault = ( year => 60*60*24, month => 60*60*24, week => 60*60*6, day => 60*60, qday => 60*15, hour => 60, ); # sample implementaion to plot an arbitrary function sub logProxy_Func2Plot($$$;$) { my ($from, $to, $func, $step) = @_; my $fromsec = SVG_time_to_sec($from); my $tosec = SVG_time_to_sec($to); my $secs = $tosec - $fromsec; $step = \%logProxy_stepDefault if( !$step ); $step = eval $step if( $step =~ m/^{.*}$/ ); $step = $step->{logProxy_Range2Zoom($secs)} if( ref($step) eq "HASH" ); $step = $logProxy_stepDefault{logProxy_Range2Zoom($secs)} if( !$step ); my $min = 999999; my $max = -999999; my $ret = ""; my $value; for(my $sec=$fromsec; $sec<$tosec; $sec+=$step) { my ($s,$m,$h,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($sec); $value = eval $func; if( $@ ) { Log3 undef, 1, "logProxy_Func2Plot: $func: $@"; next; } my $timestamp = sprintf("%04d-%02d-%02d_%02d:%02d:%02d", 1900+$year, 1+$mon, $mday, $h, $m, $s ); $min = $value if( $value < $min ); $max = $value if( $value > $max ); # add actual controll point $ret .= "$timestamp $value\n"; } return ($ret,$min,$max,$value); } # shift time by offset seconds (or months if offset ends with m) sub logProxy_shiftTime($$) { my ($time, $offset) = @_; $time =~ s/ /_/; if( $offset =~ m/((-)?\d+)m/ ) { my @t = split("[-_:]", $time); $time = mktime($t[5],$t[4],$t[3],$t[2],$t[1]-1+$1,$t[0]-1900,0,0,-1);; } else { $time = SVG_time_to_sec($time); $time += $offset; } my @t = localtime($time); $time = sprintf("%04d-%02d-%02d_%02d:%02d:%02d", $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]); return $time; } # shift plot data by offset sub logProxy_shiftData($$;$$) { my ($dp, $offset, $from, $to) = @_; my ($dpl,$dpoff,$l) = (length($$dp), 0, ""); while($dpoff < $dpl) { # using split instead is memory hog my $ndpoff = index($$dp, "\n", $dpoff); if($ndpoff == -1) { $l = substr($$dp, $dpoff); } else { $l = substr($$dp, $dpoff, $ndpoff-$dpoff); } if($l =~ m/^#/) { } else { my ($d, $v) = split(" ", $l); $d = logProxy_shiftTime($d, $offset); substr($$dp, $dpoff, 19, $d); } $dpoff = $ndpoff+1; last if($ndpoff == -1); } } sub logProxy_linearInterpolate($$$$$) { my ($t1, $v1, $t2, $v2, $t ) = @_; my $dt = $t2 - $t1; return $v1 if( !$dt ); my $dv = $v2 - $v1; my $v = $v1 + $dv * ( ($t-$t1) / $dt ); return $v; } # clip plot data to [$from,$to] range sub logProxy_clipData($$$$;$) { my ($dp, $from, $to, $interpolate, $predict) = @_; my $ret = ""; my $min = 999999; my $max = -999999; my $comment = ""; my ($dpl,$dpoff,$l) = (length($$dp), 0, ""); my $prev_value; my $prev_timestamp; my $next_value; my $next_timestamp; while($dpoff < $dpl) { # using split instead is memory hog my $ndpoff = index($$dp, "\n", $dpoff); if($ndpoff == -1) { $l = substr($$dp, $dpoff); } else { $l = substr($$dp, $dpoff, $ndpoff-$dpoff); } if($l =~ m/^#/) { $comment .= "$l\n"; } else { my ($d, $v) = split(" ", $l); my $sec = SVG_time_to_sec($d); if( $sec < $from ) { $prev_timestamp = $d; $prev_value = $v; } elsif( $sec > $to ) { if( !$next_value ) { $next_timestamp = $d; $next_value = $v; } } else { if( !$ret && $sec > $from && defined($prev_value) ) { my $value = $prev_value; $value = logProxy_linearInterpolate( SVG_time_to_sec($prev_timestamp), $prev_value, SVG_time_to_sec($d), $v, $from ) if( $interpolate ); $min = $value if( $value < $min ); $max = $value if( $value > $max ); my @t = localtime($from); my $timestamp = sprintf("%04d-%02d-%02d_%02d:%02d:%02d", $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]); $ret .= "$timestamp $value\n"; } $min = $v if( $v < $min ); $max = $v if( $v > $max ); $ret .= "$l\n"; $prev_timestamp = $d; $prev_value = $v; } } $dpoff = $ndpoff+1; last if($ndpoff == -1); } #if predict is set -> extend bejond last value if( defined($predict) && !defined($next_value) ) { $next_value = $prev_value; my $sec = SVG_time_to_sec($prev_timestamp); if( !$ret && $sec < $from && defined($prev_value) ) { my @t = localtime($from); my $timestamp = sprintf("%04d-%02d-%02d_%02d:%02d:%02d", $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]); $ret .= "$timestamp $prev_value\n"; } #if $predict = 0 -> predict to end of plot my $time = $to; #else predict by $predict $time = SVG_time_to_sec($prev_timestamp) + $predict if( $predict ); #but not later than now my ($now) = gettimeofday(); $to = minNum( $time, $now ); my @t = localtime($to); $next_timestamp = sprintf("%04d-%02d-%02d_%02d:%02d:%02d", $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]); } if( defined($next_value) ) { my $value = $prev_value; $value = logProxy_linearInterpolate( SVG_time_to_sec($prev_timestamp), $prev_value, SVG_time_to_sec($next_timestamp), $next_value, $to ) if( $interpolate ); $min = $value if( $value < $min ); $max = $value if( $value > $max ); my @t = localtime($to); my $timestamp = sprintf("%04d-%02d-%02d_%02d:%02d:%02d", $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]); $ret .= "$timestamp $value\n"; } $ret .= $comment; return (\$ret, $min, $max); } #parse plot data to array sub logProxy_data2Array($) { my ($dp) = @_; my @ret = (); my $comment; my ($dpl,$dpoff,$l) = (length($$dp), 0, ""); while($dpoff < $dpl) { # using split instead is memory hog my $ndpoff = index($$dp, "\n", $dpoff); if($ndpoff == -1) { $l = substr($$dp, $dpoff); } else { $l = substr($$dp, $dpoff, $ndpoff-$dpoff); } if($l =~ m/^#/) { $comment .= "$l\n"; } else { my ($d, $v) = split(" ", $l); my $sec = SVG_time_to_sec($d); push( @ret, [$sec, $v, $d] ); } $dpoff = $ndpoff+1; last if($ndpoff == -1); } return (\@ret,$comment); } #create plot data from array sub logProxy_array2Data($$) { my ($array,$comment) = @_; my $ret = ""; my $min = 999999; my $max = -999999; my $last; return ($ret,$min,$max,$last) if( !ref($array) eq "ARRAY" ); foreach my $point ( @{$array} ) { my @t = localtime($point->[0]); my $timestamp = sprintf("%04d-%02d-%02d_%02d:%02d:%02d", $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]); my $value = $point->[1]; $min = $value if( $value < $min ); $max = $value if( $value > $max ); $last = $value; $ret .= $timestamp . " " . $value ."\n"; } $ret .= $comment; return (\$ret,$min,$max,$last); } #create plot data from xy-array sub logProxy_xy2Plot($) { my ($array) = @_; my $ret = ";c 0\n"; my $min = 999999; my $max = -999999; my $last; my $xmin = 999999; my $xmax = -999999; return ($ret,$min,$max,$last) if( !ref($array) eq "ARRAY" ); foreach my $point ( @{$array} ) { my $x = $point->[0]; my $value = $point->[1]; $min = $value if( $value < $min ); $max = $value if( $value > $max ); $last = $value; $xmin = $x if( $x < $xmin ); $xmax = $x if( $x > $xmax ); $ret .= ";p $x $value\n"; } return ($ret,$min,$max,$last,$xmin,$xmax); } #create plot data from xy-file #assume colums separated by whitespace and x,y pairs separated by comma sub logProxy_xyFile2Plot($$$) { my ($filename, $column, $regex)= @_; my @array; $filename =~ s/%L/$attr{global}{logdir}/g if($filename =~ m/%L/ && $attr{global}{logdir}); if (open(F, "<$filename")) { while() { chomp; if(/$regex/) { my @a= split(/\s/,$_); my @pp= split(";", $a[$column-1]); map { my @p= split(",", $_); push @array, \@p; } @pp; } } close(F); } return logProxy_xy2Plot(\@array); } #create plot data from date-y-array sub logProxy_values2Plot($) { my ($array) = @_; my $ret = ";c 0\n"; my $min = 999999; my $max = -999999; my $last; return ($ret,$min,$max,$last) if( !ref($array) eq "ARRAY" ); foreach my $point ( @{$array} ) { $ret .= "$point->[0] $point->[1]\n"; } return ($ret,$min,$max,$last); } sub logProxy_Get($@) { my ($hash, $name, @a) = @_; #Log 3, "logProxy_Get"; #Log 3, Dumper @a; my $inf = shift @a; my $outf = shift @a; my $from = shift @a; my $to = shift @a; # Now @a contains the list of column_specs my $internal; if($outf && $outf eq "INT") { $outf = "-"; $internal = 1; } my $ret = ""; my %data; for(my $i = 0; $i < int(@a); $i++) { my $j = $i+1; $data{"min$j"} = undef; $data{"max$j"} = undef; $data{"avg$j"} = undef; $data{"sum$j"} = 0; $data{"cnt$j"} = undef; $data{"currval$j"} = 0; $data{"currdate$j"} = undef; $data{"mindate$j"} = undef; $data{"maxdate$j"} = undef; $data{"xmin$j"} = undef; $data{"xmax$j"} = undef; my @fld = split(":", $a[$i]); if( $a[$i] =~ m/^(FileLog|DbLog):([^:]*):(.*)/ ) { my @options = split( ',', $fld[1] ); my $log_dev = shift(@options); my $infile = $fld[0] eq "DbLog" ? "HISTORY" : "CURRENT"; my $column_specs = $3; my $extend; my $extend_scale; my $offset; my $offset_scale; my $interpolate; my $clip; my $predict; my $postFn; my $scale2reading; if( !defined($defs{$log_dev}) ) { Log3 $hash->{NAME}, 1, "$hash->{NAME}: $log_dev does not exist"; $ret .= "#$a[$i]\n"; next; } while (@options) { my $option = shift(@options); while ($option && $option =~ m/=\{/ && $option !~ m/>}/ ) { my $next = shift(@options); last if( !defined($next) ); $option .= ",". $next; } my ($name,$value) = split( '=', $option, 2 ); if( $value ) { $value = eval $value; if( $@ ) { Log3 $hash->{NAME}, 1, "$hash->{NAME}: $option: $@"; $ret .= "#$a[$i]\n"; next; } } if( $name eq "extend" ) { $value =~ m/(-?\d*)(m?)/; $extend = $1; $extend_scale = $2; $extend_scale = "" if( !$extend_scale ); $clip = 1; } elsif( $name eq "offset" ) { $value =~ m/(-?\d*)(m?)/; $offset = $1; $offset_scale = $2; $offset_scale = "" if( !$offset_scale ); } elsif( $name eq "interpolate" ) { $interpolate = 1; } elsif( $name eq "clip" ) { $clip = 1; } elsif( $name eq "predict" ) { $predict = 0; $predict = $value if( defined($value) ); } elsif( $name eq "postFn" ) { $postFn = $value; } elsif( $name eq "scale2reading" ) { $scale2reading = $value if( defined($value) ); } else { Log3 $hash->{NAME}, 2, "$hash->{NAME}: line $i: $fld[0]: unknown option >$option<"; } } my $fromsec = SVG_time_to_sec($from); my $tosec = SVG_time_to_sec($to); my $from = $from; my $to = $to; # shift $from and $to $from = logProxy_shiftTime($from,-$offset.$offset_scale) if( $offset ); $to = logProxy_shiftTime($to,-$offset.$offset_scale) if( $offset ); # extend query range $from = logProxy_shiftTime($from,-$extend.$extend_scale) if( $extend ); $to = logProxy_shiftTime($to,$extend.$extend_scale) if( $extend ); # zoom dependent reading if( $scale2reading ) { my @fld = split(':', $column_specs, 3); my $reading; my $zoom = logProxy_Range2Zoom($tosec-$fromsec); if( ref($scale2reading) eq "HASH" ) { $reading = $scale2reading->{$zoom}; $reading = $scale2reading->{"$fld[1].$zoom"} if( defined($scale2reading->{"$fld[1].$zoom"}) ); } elsif($scale2reading =~ m/^{.*}$/) { } else { no strict "refs"; $reading = eval {&{$scale2reading}($zoom,$fld[1])}; use strict "refs"; if( $@ ) { Log3 $hash->{NAME}, 1, "$hash->{NAME}:readingOfScale $a[$i]: $@"; } } if( $reading && $reading ne $fld[1] ) { Log3 $hash->{NAME}, 4, "$hash->{NAME}:scale $zoom: using $reading instead of $fld[1]"; $fld[1] = $reading; $column_specs = join( ':', @fld ); } else { Log3 $hash->{NAME}, 5, "$hash->{NAME}:scale $zoom: keeping $fld[1]"; } } $internal_data = ""; my $cmd = "get $log_dev $infile INT $from $to $column_specs"; Log3 $hash->{NAME}, 4, "$hash->{NAME}: calling $cmd"; FW_fC($cmd, 1); # shift data and specials back logProxy_shiftData($internal_data,$offset.$offset_scale) if( $offset ); $main::data{"currdate1"} = logProxy_shiftTime($main::data{"currdate1"},$offset.$offset_scale) if( $offset ); # clip extended query range to plot range if( $clip || defined($predict) ) { ($internal_data,$main::data{"min1"},$main::data{"max1"}) = logProxy_clipData($internal_data,$fromsec,$tosec,$interpolate,$predict); } #call postprocessing function if( $postFn ) { my($data,$comment) = logProxy_data2Array($internal_data); $main::data{"avg1"} = undef; $main::data{"sum1"} = undef; $main::data{"cnt1"} = int(@{$data}); $main::data{"currdate1"} = undef; $main::data{"mindate1"} = undef; $main::data{"maxdate1"} = undef; no strict "refs"; my $d = eval {&{$postFn}($a[$i],$data)}; use strict "refs"; if( $@ ) { Log3 $hash->{NAME}, 1, "$hash->{NAME}: postFn: $a[$i]: $@"; $ret .= "#$a[$i]\n"; next; } $data = $d; $comment = "#$a[$i]\n"; ($internal_data,$main::data{"min1"},$main::data{"max1"},$main::data{"currval1"}) = logProxy_array2Data($data,$comment); } if( ref($internal_data) eq "SCALAR" && $$internal_data ) { $ret .= $$internal_data; $data{"min$j"} = $main::data{"min1"}; $data{"max$j"} = $main::data{"max1"}; $data{"avg$j"} = $main::data{"avg1"}; $data{"sum$j"} = $main::data{"sum1"}; $data{"cnt$j"} = $main::data{"cnt1"}; $data{"currval$j"} = $main::data{"currval1"}; $data{"currdate$j"} = $main::data{"currdate1"}; $data{"mindate$j"} = $main::data{"mindate1"}; $data{"maxdate$j"} = $main::data{"maxdate1"}; } else { $ret .= "#$a[$i]\n"; } next; } elsif( $fld[0] eq "ConstX" && $fld[1] ) { $fld[1] = join( ':', @fld[1..@fld-1]); my ($t,$y,$y2) = eval $fld[1]; if( $@ ) { Log3 $hash->{NAME}, 1, "$hash->{NAME}: $fld[1]: $@"; $ret .= "#$a[$i]\n"; next; } if( !$t || !defined($y) || $y eq "undef" ) { $ret .= "#$a[$i]\n"; next; } $t =~ s/ /_/; my $from = $t; my $to = $t; $y2 = $y if( !defined($y2) ); $data{"min$j"} = $y > $y2 ? $y2 : $y; $data{"max$j"} = $y > $y2 ? $y : $y2; $data{"avg$j"} = ($y+$y2)/2; $data{"cnt$j"} = $y != $y2 ? 2 : 1; $data{"curdval$j"} = $y2; $data{"curddate$j"} = $to; $data{"maxdate$j"} = $to; $data{"mindate$j"} = $to; $ret .= "$from $y\n"; $ret .= "$to $y2\n"; $ret .= "#$a[$i]\n"; next; } elsif( $fld[0] eq "ConstY" && defined($fld[1]) ) { $fld[1] = join( ':', @fld[1..@fld-1]); my ($y,$f,$t) = eval $fld[1]; if( $@ ) { Log3 $hash->{NAME}, 1, "$hash->{NAME}: $fld[1]: $@"; $ret .= "#$a[$i]\n"; next; } if( !defined($y) || $y eq "undef" ) { $ret .= "#$a[$i]\n"; next; } $f =~ s/ /_/ if( $f ); $t =~ s/ /_/ if( $t ); my $from = $from; $from = $f if( $f ); my $to = $to; $to = $t if( $t ); $data{"min$j"} = $y; $data{"max$j"} = $y; $data{"avg$j"} = $y; $data{"cnt$j"} = 2; $data{"currval$j"} = $y; $data{"currdate$j"} = $to; $data{"maxdate$j"} = $to; $data{"mindate$j"} = $to; $ret .= "$from $y\n"; $ret .= "$to $y\n"; $ret .= "#$a[$i]\n"; next; } elsif( $fld[0] eq "Func" && $fld[1] ) { $fld[1] = join( ':', @fld[1..@fld-1]); #my $fromsec = SVG_time_to_sec($from); #my $tosec = SVG_time_to_sec($to); my ($r,$min,$max,$last,$xmin,$xmax) = eval $fld[1]; if( $@ ) { Log3 $hash->{NAME}, 1, "$hash->{NAME}: $fld[1]: $@"; next; } $data{"min$j"} = $min; $data{"max$j"} = $max; $data{"currval$j"} = $last; $data{"xmin$j"} = $xmin; $data{"xmax$j"} = $xmax; $ret .= $r; $ret .= "#$a[$i]\n"; next; } elsif( $fld[0] eq "Polar" ) { my $axis; my $noaxis; my $range; my $segments; my $isolines = "10|20|30"; my @options = split( ',', $fld[1] ); foreach my $option ( @options[0..@options-1] ) { my ($name,$value) = split( '=', $option, 2 ); if( $value ) { $value = eval $value; if( $@ ) { Log3 $hash->{NAME}, 1, "$hash->{NAME}: $option: $@"; $ret .= "#$a[$i]\n"; next; } } if( $name eq "axis" ) { $axis = 1; } elsif( $name eq "noaxis" ) { $noaxis = 1; } elsif( 0 && $name eq "isolines" && defined($value) ) { $isolines = $value; } elsif( $name eq "segments" && defined($value) ) { $segments = $value; } elsif( $name eq "range" && defined($value) ) { $range = $value; } else { Log3 $hash->{NAME}, 2, "$hash->{NAME}: line $i: $fld[0]: unknown option >$option<"; } } my $values; if( defined( $fld[2] ) ) { $fld[2] = join( ':', @fld[2..@fld-1]); $values = eval $fld[2]; if( $@ ) { Log3 $hash->{NAME}, 1, "$hash->{NAME}: $fld[2]: $@"; next; } } next if( !$values && !$segments ); next if( $values && ref($values) ne "ARRAY" ); $segments = scalar @{$values} if( !$segments ); next if( !$segments ); my $isText = $values && @{$values}[0] !~ m/^[.\d+-]*$/; $axis = 1 if( $isText ); $axis = 1 if( !defined($values) && $segments ); my $f = 3.14159265 / 180; if( $segments && defined( $values ) ) { my $segment = 0; my $first; $ret .= ";c 0\n"; for( my $a = 0; $a < 360; $a += (360/$segments) ) { my $value = @{$values}[$segment++]; next if( !defined($value) ); my $r; if( $isText ) { $r = 32; $r = 34 if( $a > 90 && $a < 270 ); } else { $r = $value; } my $x = sin( $a * $f ); my $y = cos( $a * $f ); $x *= $r; $y *= $r; if( $value =~ m/^[.\d+-]*$/ ) { $ret .= ";p $x $y\n"; $first .= ";p $x $y\n" if( !$first ); } else { my $align = "middle"; #$align = "start" if( $a > 30 && $a < 150 ); #$align = "end" if( $a > 210 && $a < 330 ); $align = "start" if( $a > 0 && $a < 180 ); $align = "end" if( $a > 180 && $a < 360 ); $ret .= ";t $x $y $align $value\n"; } } $ret .= $first if( $first ); } if( $axis && !$noaxis ) { my $axis; $ret .= ";\n" if( $ret ); $ret .= ";ls l7\n"; foreach my $r (split( '\|', $isolines)) { $ret .= ";\n"; #FIXME: this is one to many at the end... my $first; for( my $a = 0; $a < 360; $a += (360/$segments) ) { my $x = sin( $a * $f ); my $y = cos( $a * $f ); $x *= $r; $y *= $r; $ret .= ";p $x $y\n"; $ret .= ";t $x $y start $r\n" if( $a == 0 && ( $r == 10 || $r == 20 ) ) ; $first .= ";p $x $y\n" if( !$first ); if( $r == 30 ) { $axis .= ";\n" if( $axis ); $axis .= ";p 0 0\n"; $axis .= ";p $x $y\n"; } } $ret .= $first; } $ret .= ";\n"; $ret .= $axis; } $ret .= "#$a[0]\n"; } else { Log3 $name, 2, "$name: unknown keyword $fld[0] in column_spec, must be one of $logProxy_columns"; } } for(my $i = 0; $i < int(@a); $i++) { my $j = $i+1; $main::data{"min$j"} = $data{"min$j"}; $main::data{"max$j"} = $data{"max$j"}; $main::data{"avg$j"} = $data{"avg$j"}; $main::data{"sum$j"} = $data{"sum$j"}; $main::data{"cnt$j"} = $data{"cnt$j"}; $main::data{"currval$j"} = $data{"currval$j"}; $main::data{"currdate$j"} = $data{"currdate$j"}; $main::data{"mindate$j"} = $data{"mindate$j"}; $main::data{"maxdate$j"} = $data{"maxdate$j"}; $main::data{"xmin$j"} = $data{"xmin$j"}; $main::data{"xmax$j"} = $data{"xmax$j"}; } #Log 3, Dumper $ret; if( $internal ) { $internal_data = \$ret; return undef; } return $ret; } 1; =pod =item helper =item summary manipulate the date to be plotted in an SVG device =item summary_DE manipulation von mit SVG zu plottenden SVG Daten =begin html

logProxy

    Allows the manipulation of data to be plotted in an SVG device:
    • addition of horizontal lines at fixed values
    • addition of horizontal lines at dynamic values eg: min, max or average values of another plot
    • addition of vertical lines at fixed or dynamic times between two fixed or dynamic y values
    • addition of calculated data like week profiles of HeatingControll devices or heating thermostats
    • merge plot data from different sources. eg. different FileLog devices
    • horizontaly shifting a (merged) plot to align average or statistic data to the correct day,week and month

    Define
      define <name> logProxy

      Only one logProxy device can be defined and is needed.

      Example:
        define myProxy logProxy

    Set

    Get
    Attributes


    #logProxy <column_spec>
    where <column_spec> can be one or more of the following:
    • FileLog:<log device>[,<options>]:<column_spec>
    • DbLog:<log device>[,<options>]:<column_spec>

    • options is a comma separated list of zero or more of:
      • clip
        clip the plot data to the plot window
      • extend=<value>
        extend the query range to the log device by <value> seconds (or <value> months if <value> ends in m). also activates cliping.
      • interpolate
        perform a linear interpolation to the values in the extended range to get the values at the plot boundary. only usefull if plotfunction is lines.
      • offset=<value>
        shift plot by <value> seconds (or <value> months if <value> ends in m). allows alignment of values calculated by average or statsitics module to the correct day, week or month.
      • predict[=<value>]
        no value -> extend the last plot value to now.
        value -> extend the last plot value by <value> but maximal to now.
      • postFn='<myPostFn>'
        myPostFn is the name of a postprocessing function that is called after all processing of the data by logProxy has been done. it is called with two arguments: the devspec line from the gplot file and a reference to a data array containing the points of the plot. each point is an array with three components: the point in time in seconds, the value at this point and the point in time in string form. the return value must return a reference to an array of the same format. the third component of each point can be omittet and is not evaluated.
      • scale2reading=<scaleHashRef>
        Use zoom step dependent reading names.
        The reading name to be used is the result of a lookup with the current zoom step into scaleHashRef. The keys can be from the following list: year, month, week, day, qday, hour
        Example:
          #logProxy DbLog:dbLog,scale2reading={year=>'temperature_avg_day',month=>'temperature_avg_day'}:s300ht_1:temperature::


    • ConstX:<time>,<y>[,<y2>]
      Will draw a vertical line (or point) at <time> between <y> to <y2>.
      Everything after the : is evaluated as a perl expression that hast to return one time string and one or two y values.
      Examples:
        #logProxy ConstX:$data{currdate1},$data{currval1}
        #logProxy ConstX:$data{mindate1},$data{min1},$data{avg1}
        #logProxy ConstX:$data{maxdate1},$data{max1},$data{avg1}
        #logProxy ConstX:logProxy_shiftTime($from,60*60*2),$data{min1},$data{max1}

    • ConstY:<value>[,<from>[,<to>]]
      Will draw a horizontal line at <value>, optional only between the from and to times.
      Everything after the : is evaluated as a perl expression that hast to return one value and optionaly one or two time strings.
      Examples:
        #logProxy ConstY:0
        #logProxy ConstY:1234+15+myFunc(123)
        #logProxy ConstY:$data{avg1}
        #logProxy ConstY:$data{avg2},$from,$to
        #logProxy ConstY:$data{avg2},logProxy_shiftTime($from,60*60*12),logProxy_shiftTime($from,-60*60*12)

    • Polar:[<options>]:<values>
      Will draw a polar/spiderweb diagram with the given values. <values> has to evaluate to a perl array.
      If <values> contains numbers these values are plottet and the last value will be connected to the first.
      If <values> contains strings these strings are used as labels for the segments.
      The axis are drawn automaticaly if the values are strings or if no values are given but the segments option is set.
      The corrosponding SVG device should have the plotsize attribute set (eg: attr <mySvg> plotsize 340,300) and the used gplot file has to contain xrange and yrange entries and the x- and y-axis labes should be switched off with xtics, ytics and y2tics entries.
      The following example will plot the temperature and desiredTemperature values of all devices named MAX.*:
        set xtics ()
        set ytics ()
        set y2tics ()
        set xrange [-40:40]
        set yrange [-40:40]

        #logProxy Polar::{[map{ReadingsVal($_,"temperature",0)}devspec2array("MAX.*")]}
        #logProxy Polar::{[map{ReadingsVal($_,"desiredTemperature",0)}devspec2array("MAX.*")]}
        #logProxy Polar::{[map{ReadingsVal($_,"temperature",0)}devspec2array("MAX.*")]}
        #logProxy Polar::{[devspec2array("MAX.*")]}

        plot "<IN>" using 1:2 axes x1y1 title 'Ist' ls l0 lw 1 with lines,\
        plot "<IN>" using 1:2 axes x1y1 title 'Soll' ls l1fill lw 1 with lines,\
        plot "<IN>" using 1:2 axes x1y1 notitle ls l0 lw 1 with points,\
        plot "<IN>" using 1:2 axes x1y1 notitle ls l2 lw 1 with lines,\

      options is a comma separated list of zero or more of:
      • axis
        force to draw the axis
      • noaxis
        disable to draw the axis
      • range=<value>
        the range to use for the radial axis
      • segments=<value>
        the number of circle/spiderweb segments to use for the plot
      • isolines=<value>
        a | separated list of values for which an isoline shoud be drawn. defaults to 10|20|30.

    • Func:<perl expression>
      Specifies a perl expression that returns the data to be plotted and its min, max and last value. It can not contain space or : characters. The data has to be one string of newline separated entries of the form: yyyy-mm-dd_hh:mm:ss value
      Example:
        #logProxy Func:logProxy_WeekProfile2Plot("HCB",$from,$to)
        #logProxy Func:logProxy_WeekProfile2Plot("myHeatingControll",$from,$to,"(\\d)*\$")
        #logProxy Func:logProxy_Func2Plot($from,$to,'{logProxy_hms2dec(sunrise_abs_dat($sec))}')
        #logProxy Func:logProxy_Func2Plot($from,$to,'{logProxy_hms2dec(sunset_abs_dat($sec))}')

      Notes:
      • logProxy_WeekProfile2Plot is a sample implementation of a function that will plot the week profile of a Heating_Control, WeekdyTimer, HomeMatic or MAX Thermostat device can be found in the 98_logProxy.pm module file.
      • logProxy_Func2Plot($from,$to,$func) is a sample implementation of a function that will evaluate the given function (3rd parameter) for a zoom factor dependent number of times. the current time is given in $sec. the step width can be given in an optional 4th parameter. either as a number or as an hash with the keys from the following list: hour,qday,day,week,month,year and the values representing the step with for the zoom level.
      • logProxy_xy2Plot(\@xyArray) is a sample implementation of a function that will accept a ref to an array of xy-cordinate pairs as the data to be plotted.
      • logProxy_xyFile2Plot($filename,$column,$regex) is a sample implementation of a function that will accept a filename, a column number and a regular expression. The requested column in all lines in the file that match the regular expression needs to be in the format x,y to indicate the xy-cordinate pairs as the data to be plotted.
      • logProxy_values2Plot(\@xyArray) is a sample implementation of a function that will accept a ref to an array of date-y-cordinate pairs as the data to be plotted.
      • The perl expressions have access to $from and $to for the begining and end of the plot range and also to the SVG specials min, max, avg, cnt, sum, currval (last value) and currdate (last date) values of the individual curves already plotted are available as $data{<special-n>}.
      • logProxy_Range2Zoom($seconds) can be used to get the approximate zoom step for a plot range of $seconds.
      • SVG_time_to_sec($timestamp) can be used to convert the timestamp strings to epoch times for calculation.


    Please see also the column_spec paragraphs of FileLog, DbLog and SVG.

    NOTE: spaces are not allowed inside the colums_specs.

    To use any of the logProxy features with an existing plot the associated SVG file hast to be changed to use the logProxy device and the .gplot file has to be changed in the following way:
    All existing #FileLog and #Dblog lines have to be changed to #logProxy lines and
    the column_spec of these line has to be prepended by FileLog:<log device>: or DbLog:<log device>: respectively.
    Examples:
      #DbLog <myDevice>:<myReading>
      #FileLog 4:<SPEC1>:power\x3a::
      #FileLog 4:<SPEC1>:consumption\x3a::

      will become:

      #logProxy DbLog:<myDb>:<myDevice>:<myReading>
      #logProxy FileLog:FileLog_<SPEC1>:4:<SPEC1>.power\x3a::
      #logProxy FileLog:FileLog_<SPEC1>:4:<SPEC1>.consumption\x3a::
=end html =cut