From 8ede441ab6cd040a2ca3e73870e26b9348fe16c4 Mon Sep 17 00:00:00 2001 From: justme-1968 Date: Sun, 18 May 2014 20:47:40 +0000 Subject: [PATCH] added 33_readingsHistory git-svn-id: https://svn.fhem.de/fhem/trunk@5889 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 1 + fhem/FHEM/33_readingsHistory.pm | 719 +++++++++++++++++++++++++++++ fhem/docs/commandref_frame.html | 1 + fhem/docs/commandref_frame_DE.html | 1 + 4 files changed, 722 insertions(+) create mode 100644 fhem/FHEM/33_readingsHistory.pm diff --git a/fhem/CHANGED b/fhem/CHANGED index 0da64f8e1..005f6dae1 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - feature: new module 33_readingsHistory.pm added (justme1968) - feature: new command copy (justme1968) - feature: enabled GIF, PNG and JPG as background image formats, enabled relative font size changed and perl specials for font size diff --git a/fhem/FHEM/33_readingsHistory.pm b/fhem/FHEM/33_readingsHistory.pm new file mode 100644 index 000000000..9ee6ccff8 --- /dev/null +++ b/fhem/FHEM/33_readingsHistory.pm @@ -0,0 +1,719 @@ +# $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 POSIX qw(strftime); + +use vars qw($FW_ME); +use vars qw($FW_wname); +use vars qw($FW_subdir); +use vars qw(%FW_hiddenroom); +use vars qw(%FW_visibleDeviceHash); +use vars qw(%FW_webArgs); # all arguments specified in the GET + +my $readingsHistory_hasJSON = 0; +my $readingsHistory_hasDataDumper = 1; + +sub readingsHistory_Initialize($) +{ + my ($hash) = @_; + + $hash->{DefFn} = "readingsHistory_Define"; + $hash->{NotifyFn} = "readingsHistory_Notify"; + $hash->{NotifyOrderPrefix}= "51-"; + $hash->{UndefFn} = "readingsHistory_Undefine"; + $hash->{SetFn} = "readingsHistory_Set"; + $hash->{GetFn} = "readingsHistory_Get"; + $hash->{AttrFn} = "readingsHistory_Attr"; + $hash->{AttrList} = "disable:1,2,3 mapping style timestampFormat valueFormat noheading:1 nolinks:1 notime:1 nostate:1 alwaysTrigger:1 rows"; + + $hash->{FW_detailFn} = "readingsHistory_detailFn"; + $hash->{FW_summaryFn} = "readingsHistory_detailFn"; + + $hash->{FW_atPageEnd} = 1; + + eval "use Data::Dumper"; + $readingsHistory_hasDataDumper = 0 if($@); + +} + +sub +readingsHistory_updateDevices($) +{ + my ($hash) = @_; + + my %list; + my @devices; + + my $def = $hash->{DEF}; + $def = "" if( !$def ); + my @params = split(" ", $def); + while (@params) { + my $param = shift(@params); + + my @device = split(":", $param); + + if($device[0] =~ m/(.*)=(.*)/) { + my ($lattr,$re) = ($1, $2); + foreach my $d (sort keys %defs) { + next if( IsIgnored($d) ); + next if( !defined($defs{$d}{$lattr}) ); + next if( $defs{$d}{$lattr} !~ m/^$re$/); + $list{$d} = 1; + push @devices, [$d,$device[1]]; + } + } elsif($device[0] =~ m/(.*)&(.*)/) { + my ($lattr,$re) = ($1, $2); + foreach my $d (sort keys %attr) { + next if( IsIgnored($d) ); + next if( !defined($attr{$d}{$lattr}) ); + next if( $attr{$d}{$lattr} !~ m/^$re$/); + $list{$d} = 1; + push @devices, [$d,$device[1]]; + } + } elsif( defined($defs{$device[0]}) ) { + $list{$device[0]} = 1; + push @devices, [@device]; + } else { + foreach my $d (sort keys %defs) { + next if( IsIgnored($d) ); + eval { $d =~ m/^$device[0]$/ }; + if( $@ ) { + Log3 $hash->{NAME}, 3, $hash->{NAME} .": ". $device[0] .": ". $@; + push @devices, ["<>"]; + last; + } + next if( $d !~ m/^$device[0]$/); + $list{$d} = 1; + push @devices, [$d,$device[1]]; + } + } + } + + $hash->{CONTENT} = \%list; + $hash->{DEVICES} = \@devices; + + delete( $hash->{NOTIFYDEV} ); + if( scalar keys %list == 0 ) { + $hash->{NOTIFYDEV} = "global"; + } + + $hash->{fhem}->{last_update} = gettimeofday(); +} + +sub +readingsHistory_Define($$) +{ + my ($hash, $def) = @_; + + my @args = split("[ \t]+", $def); + + return "Usage: define readingsHistory [[:]]+" if(@args < 2); + + my $name = shift(@args); + my $type = shift(@args); + + $hash->{HAS_DataDumper} = $readingsHistory_hasDataDumper; + + $hash->{fhem}{lines} = [] if( !defined($hash->{fhem}{lines}) ); + + readingsHistory_updateDevices($hash); + + $hash->{STATE} = 'Initialized'; + + return undef; +} + +sub +readingsHistory_lookup($$$$$$$$) +{ + my($mapping,$name,$alias,$reading,$value,$room,$group,$default) = @_; + + if( $mapping ) { + if( ref($mapping) eq 'HASH' ) { + $default = $mapping->{$name} if( defined($mapping->{$name}) ); + $default = $mapping->{$reading} if( defined($mapping->{$reading}) ); + $default = $mapping->{$name.".".$reading} if( defined($mapping->{$name.".".$reading}) ); + $default = $mapping->{$reading.".".$value} if( defined($mapping->{$reading.".".$value}) ); + #} elsif( $mapping =~ m/^{.*}$/) { + # my $DEVICE = $name; + # my $READING = $reading; + # my $VALUE = $value; + # $mapping = eval $mapping; + # $default = $mapping if( $mapping ); + } else { + $default = $mapping; + } + + return $default if( !defined($default) ); + + $default =~ s/\%ALIAS/$alias/g; + $default =~ s/\%DEVICE/$name/g; + $default =~ s/\%READING/$reading/g; + $default =~ s/\%VALUE/$value/g; + $default =~ s/\%ROOM/$room/g; + $default =~ s/\%GROUP/$group/g; + + $default =~ s/\$ALIAS/$alias/g; + $default =~ s/\$DEVICE/$name/g; + $default =~ s/\$READING/$reading/g; + $default =~ s/\$VALUE/$value/g; + $default =~ s/\$ROOM/$room/g; + $default =~ s/\$GROUP/$group/g; + } + + return $default; +} +sub +readingsHistory_lookup2($$$$) +{ + my($lookup,$name,$reading,$value) = @_; + + return $lookup if( !$lookup ); + + if( ref($lookup) eq 'HASH' ) { + my $vf = ""; + $vf = $lookup->{$reading} if( exists($lookup->{$reading}) ); + $vf = $lookup->{$name.".".$reading} if( exists($lookup->{$name.".".$reading}) ); + $vf = $lookup->{$reading.".".$value} if( defined($value) && exists($lookup->{$reading.".".$value}) ); + $lookup = $vf; + } + + if( !ref($lookup) && $lookup =~ m/^{.*}$/) { + my $DEVICE = $name; + my $READING = $reading; + my $VALUE = $value; + $lookup = eval $lookup; + $lookup = "" if( $@ ); + } + + return $lookup if( !defined($lookup) ); + + $lookup =~ s/\%DEVICE/$name/g; + $lookup =~ s/\%READING/$reading/g; + $lookup =~ s/\%VALUE/$value/g; + + $lookup =~ s/\$DEVICE/$name/g; + $lookup =~ s/\$READING/$reading/g; + $lookup =~ s/\$VALUE/$value/g; + + return $lookup; +} + + +sub readingsHistory_Undefine($$) +{ + my ($hash,$arg) = @_; + + return undef; +} + +sub +readingsHistory_2html($) +{ + my($hash) = @_; + $hash = $defs{$hash} if( ref($hash) ne 'HASH' ); + + return undef if( !$hash ); + + if( $hash->{DEF} =~ m/=/ ) { + if( !$hash->{fhem}->{last_update} + || gettimeofday() - $hash->{fhem}->{last_update} > 600 ) { + readingsHistory_updateDevices($hash); + } + } + + my $d = $hash->{NAME}; + + my $show_heading = !AttrVal( $d, "noheading", "0" ); + my $show_links = !AttrVal( $d, "nolinks", "0" ); + $show_links = 0 if($FW_hiddenroom{detail}); + + my $disable = AttrVal($d,"disable", 0); + if( AttrVal($d,"disable", 0) > 2 ) { + return undef; + } elsif( AttrVal($d,"disable", 0) > 1 ) { + my $ret; + $ret .= ""; + my $txt = AttrVal($d, "alias", $d); + $txt = "$txt" if( $show_links ); + $ret .= "" if( $show_heading ); + $ret .= ""; + $ret .= "
$txt
"; + #$ret .= ""; + $ret .= ""; + $ret .= "
disabled
"; + return $ret; + } + + my $show_time = !AttrVal( $d, "notime", "0" ); + + my $style = AttrVal( $d, "style", "" ); + + my $devices = $hash->{DEVICES}; + + my $row = 1; + my $ret; + $ret .= ""; + my $txt = AttrVal($d, "alias", $d); + $txt = "$txt" if( $show_links ); + $ret .= "" if( $show_heading ); + $ret .= ""; + $ret .= "
$txt
"; + $ret .= "" if( $disable > 0 ); + + my $rows = AttrVal($d,"rows", 5 ); + $rows = 1 if( $rows < 1 ); + + my $lines = ""; + for (my $i = 0; $i < $rows; $i++) { + $lines .= @{$hash->{fhem}{lines}}[$i] if( @{$hash->{fhem}{lines}}[$i] ); + $lines .= "
"; + } + + $ret .= sprintf("", ($row&1)?"odd":"even"); + $row++; + $ret .= ""; + + $ret .= sprintf("", ($row&1)?"odd":"even"); + $ret .= "" if( $disable > 0 ); + $ret .= "
updates disabled
$lines
updates disabled
"; + + return $ret; +} +sub +readingsHistory_detailFn() +{ + my ($FW_wname, $d, $room, $pageHash) = @_; # pageHash is set for summaryFn. + + my $hash = $defs{$d}; + + $hash->{mayBeVisible} = 1; + + return readingsHistory_2html($d); +} + +sub +readingsHistory_Notify($$) +{ + my ($hash,$dev) = @_; + my $name = $hash->{NAME}; + + if( grep(m/^INITIALIZED$/, @{$dev->{CHANGED}}) ) { + readingsHistory_updateDevices($hash); + readingsHistory_Load($hash); + return undef; + } elsif( grep(m/^REREADCFG$/, @{$dev->{CHANGED}}) ) { + readingsHistory_updateDevices($hash); + readingsHistory_Load($hash); + return undef; + } elsif( grep(m/^SAVE$/, @{$dev->{CHANGED}}) ) { + readingsHistory_Save(); + } + + return if( AttrVal($name,"disable", 0) > 0 ); + + return if($dev->{TYPE} eq $hash->{TYPE}); + #return if($dev->{NAME} eq $name); + + my $devices = $hash->{DEVICES}; + + my $max = int(@{$dev->{CHANGED}}); + for (my $i = 0; $i < $max; $i++) { + my $s = $dev->{CHANGED}[$i]; + $s = "" if(!defined($s)); + + if( $dev->{NAME} eq "global" && $s =~ m/^RENAMED ([^ ]*) ([^ ]*)$/) { + my ($old, $new) = ($1, $2); + if( defined($hash->{CONTENT}{$old}) ) { + + $hash->{DEF} =~ s/(\s*)$old((:\S+)?\s*)/$1$new$2/g; + } + readingsHistory_updateDevices($hash); + } elsif( $dev->{NAME} eq "global" && $s =~ m/^DELETED ([^ ]*)$/) { + my ($name) = ($1); + + if( defined($hash->{CONTENT}{$name}) ) { + + $hash->{DEF} =~ s/(\s*)$name((:\S+)?\s*)/ /g; + $hash->{DEF} =~ s/^ //; + $hash->{DEF} =~ s/ $//; + } + readingsHistory_updateDevices($hash); + } elsif( $dev->{NAME} eq "global" && $s =~ m/^DEFINED ([^ ]*)$/) { + readingsHistory_updateDevices($hash); + } else { + next if(AttrVal($name,"disable", undef)); + + next if (!$hash->{CONTENT}->{$dev->{NAME}}); + + if( $hash->{alwaysTrigger} ) { + $hash->{mayBeVisible} = 1; + } elsif( !defined($hash->{mayBeVisible}) ) { + Log3 $name, 5, "$name: not on any display, don't trigger"; + } else { + if( defined($FW_visibleDeviceHash{$name}) ) { + } else { + Log3 $name, 5, "$name: no longer visible, don't trigger"; + delete( $hash->{mayBeVisible} ); + } + } + + my @parts = split(/: /,$s); + my $reading = shift @parts; + my $value = join(": ", @parts); + + $reading = "" if( !defined($reading) ); + next if( $reading =~ m/^\./); + $value = "" if( !defined($value) ); + my $show_state = 1; + if( $value eq "" ) { + next if AttrVal( $name, "nostate", "0" ); + + $reading = "state"; + $value = $s; + } + + my $mapping = AttrVal( $name, "mapping", ""); + $mapping = eval $mapping if( $mapping =~ m/^{.*}$/ ); + #$mapping = undef if( ref($mapping) ne 'HASH' ); + + my $timestampFormat = AttrVal( $name, "timestampFormat", undef); + + my $value_format = AttrVal( $name, "valueFormat", "" ); + if( $value_format =~ m/^{.*}$/ ) { + my $vf = eval $value_format; + $value_format = $vf if( $vf ); + } + + foreach my $device (@{$devices}) { + my $item = 0; + my $h = $defs{@{$device}[0]}; + next if( !$h ); + next if( $dev->{NAME} ne $h->{NAME} ); + my $n = $h->{NAME}; + my $regex = @{$device}[1]; + my @list = (undef); + @list = split(",",$regex) if( $regex ); + foreach my $regex (@list) { + next if( $reading eq "state" && !$show_state && (!defined($regex) || $regex ne "state") ); + + next if( defined($regex) && $reading !~ m/^$regex$/); + + my $value_format = readingsHistory_lookup2($value_format,$n,$reading,$value); + next if( !defined($value_format) ); + if( $value_format =~ m/%/ ) { + $s = sprintf( $value_format, $value ); + } elsif( $value_format ) { + $s = $value_format; + } + + my @t = localtime; + my $tm; + if( $timestampFormat ) { + $tm = strftime( $timestampFormat, @t ); + } else { + $tm = sprintf("%02d:%02d:%02d", $t[2], $t[1], $t[0] ); + } + + my $show_links = !AttrVal( $name, "nolinks", "0" ); + $show_links = 0 if($FW_hiddenroom{detail}); + $show_links = 0 if(!defined($FW_ME)); + #$show_links = 0 if(!defined($FW_subdir)); + + my $a = AttrVal($n, "alias", $n); + my $m = $a; + if( $mapping ) { + my $room = AttrVal($n, "room", ""); + my $group = AttrVal($n, "group", ""); + $m = readingsHistory_lookup($mapping,$n,$a,$reading,$value,$room,$group,$m); + } + + my $line = "$tm  $m $s"; + $line = "$tm  $m $s" if( $show_links ); + + while( @{$hash->{fhem}{lines}} >= AttrVal($name,"rows", 5 ) ) { + pop @{$hash->{fhem}{lines}}; + } + unshift( @{$hash->{fhem}{lines}}, $line ); + + DoTrigger( "$name", "history: $line" ) if( $hash->{mayBeVisible} ); + } + } + } + } + + return undef; +} + +sub +readingsHistory_StatefileName() +{ + my $statefile = $attr{global}{statefile}; + $statefile = substr $statefile,0,rindex($statefile,'/')+1; + #return $statefile ."readingsHistorys.save" if( $readingsHistory_hasJSON ); + return $statefile ."readingsHistorys.dd.save" if( $readingsHistory_hasDataDumper ); +} +my $readingsHistory_LastSaveTime=""; +sub +readingsHistory_Save() +{ + my $time_now = TimeNow(); + return if( $time_now eq $readingsHistory_LastSaveTime); + $readingsHistory_LastSaveTime = $time_now; + + return "No statefile specified" if(!$attr{global}{statefile}); + my $statefile = readingsHistory_StatefileName(); + + my $hash; + for my $d (keys %defs) { + next if($defs{$d}{TYPE} ne "readingsHistory"); + next if( !defined($defs{$d}{fhem}{lines}) ); + + $hash->{$d} = $defs{$d}{fhem}{lines}; + } + + if(open(FH, ">$statefile")) { + my $t = localtime; + print FH "#$t\n"; + + if( $readingsHistory_hasJSON ) { + print FH encode_json($hash) if( defined($hash) ); + } elsif( $readingsHistory_hasDataDumper ) { + my $dumper = Data::Dumper->new([]); + $dumper->Terse(1); + + $dumper->Values([$hash]); + print FH $dumper->Dump; + } + + close(FH); + } else { + + my $msg = "readingsHistory_Save: Cannot open $statefile: $!"; + Log3 undef, 1, $msg; + } + + return undef; +} +sub +readingsHistory_Load($) +{ + my ($hash) = @_; + + return "No statefile specified" if(!$attr{global}{statefile}); + my $statefile = readingsHistory_StatefileName(); + + if(open(FH, "<$statefile")) { + my $encoded; + while (my $line = ) { + chomp $line; + next if($line =~ m/^#.*$/); + $encoded .= $line; + } + close(FH); + + return if( !defined($encoded) ); + + my $decoded; + if( $readingsHistory_hasJSON ) { + $decoded = decode_json( $encoded ); + } elsif( $readingsHistory_hasDataDumper ) { + $decoded = eval $encoded; + } + $hash->{fhem}{lines} = $decoded->{$hash->{NAME}} if( defined($decoded->{$hash->{NAME}}) ); + } else { + my $msg = "readingsHistory_Load: Cannot open $statefile: $!"; + Log3 undef, 1, $msg; + } + return undef; +} + +sub +readingsHistory_Set($@) +{ + my ($hash, $name, $cmd, $param, @a) = @_; + + my $list = "clear:noArgs add"; + + if( $cmd eq "clear" ) { + $hash->{fhem}{lines} = []; + + my @t = localtime; + my $tm = sprintf("%02d:%02d:%02d", $t[2], $t[1], $t[0] ); + my $line = "$tm  --clear--"; + + unshift( @{$hash->{fhem}{lines}}, $line ); + + DoTrigger( "$name", "clear: $line" ) if( $hash->{mayBeVisible} ); + + return undef; + } elsif ( $cmd eq "add" ) { + my @t = localtime; + my $tm = sprintf("%02d:%02d:%02d", $t[2], $t[1], $t[0] ); + my $line = "$tm  $param ". join( " ", @a ); + + while( @{$hash->{fhem}{lines}} >= AttrVal($name,"rows", 5 ) ) { + pop @{$hash->{fhem}{lines}}; + } + unshift( @{$hash->{fhem}{lines}}, $line ); + + DoTrigger( "$name", "history: $line" ) if( $hash->{mayBeVisible} ); + return undef; + } + + return "Unknown argument $cmd, choose one of $list"; +} + +sub +readingsHistory_Get($@) +{ + my ($hash, @a) = @_; + + my $name = $a[0]; + return "$name: get needs at least one parameter" if(@a < 2); + + my $cmd= $a[1]; + + my $ret = ""; + if( $cmd eq "html" ) { + return readingsHistory_2html($hash); + } + + return undef; + return "Unknown argument $cmd, choose one of html:noArg"; +} + +sub +readingsHistory_Attr($$$) +{ + my ($cmd, $name, $attrName, $attrVal) = @_; + my $orig = $attrVal; + + if( $attrName eq "alwaysTrigger" ) { + my $hash = $defs{$name}; + $attrVal = 1 if($attrVal); + + if( $cmd eq "set" ) { + $hash->{alwaysTrigger} = $attrVal; + delete( $hash->{helper}->{myDisplay} ) if( $hash->{alwaysTrigger} ); + } else { + delete $hash->{alwaysTrigger}; + } + } + + if( $cmd eq "set" ) { + if( $orig ne $attrVal ) { + $attr{$name}{$attrName} = $attrVal; + return $attrName ." set to ". $attrVal; + } + } + + return; +} + +1; + +=pod +=begin html + + +

readingsHistory

+
    + Displays a history of readings from on or more devices. + +

    + + Define +
      + define <name> readingsHistory [<device>[:regex] [<device-2>[:regex-2] ... [<device-n>[:regex-n]]]]
      +
      + + Notes: +
        +
      • <device> can be of the form INTERNAL=VALUE where INTERNAL is the name of an internal value and VALUE is a regex.
      • +
      • If regex is a comma separatet list it will be used as an enumeration of allowed readings.
      • +
      • if no device/reading argument is given only lines with 'set add ...' are displayed.
      • +

      + + Examples: +
        +
      +

    + + + Set +
      +
    • add ...
      + directly add text as new line to history.
      +
    • clear
      + clear the history.
      +

    + + + Get +
      +

    + + + Attributes +
      +
    • alwaysTrigger
      + 1 -> alwaysTrigger update events. even if not visible.
    • +
    • disable
      + 1 -> disable notify processing and longpoll updates. Notice: this also disables rename and delete handling.
      + 2 -> also disable html table creation
      + 3 -> also disable html creation completely
    • +
    • noheading
      + If set to 1 the readings table will have no heading.
    • +
    • nolinks
      + Disables the html links from the heading and the reading names.
    • +
    • notime
      + If set to 1 the reading timestamp is not displayed.
    • +
    • mapping
      + Can be a simple string or a perl expression enclosed in {} that returns a hash that maps device names + to the displayed name. The keys can be either the name of the reading or <device>.<reading>. + %DEVICE, %ALIAS, %ROOM and %GROUP are replaced by the device name, device alias, room attribute and + group attribute respectively. You can also prefix these keywords with $ instead of %. +
    • +
    • style
      + Specify an HTML style for the readings table, e.g.:
      + attr history style style="font-size:20px"
    • +
    • timestampFormat
      + POSIX strftime compatible string for used as the timestamp for each line. +
    • +
    • valueFormat
      + Specify an sprintf style format string used to display the reading values. If the format string is undef + this reading will be skipped. Can be given as a string, a perl expression returning a hash or a perl + expression returning a string, e.g.:
      + attr history valueFormat %.1f °C
      + attr history valueFormat { temperature => "%.1f °C", humidity => "%.1f %" }
      + attr history valueFormat { ($READING eq 'temperature')?"%.1f °C":undef }
    • +
    • rows
      + Number of history rows to show.
    • +
    +
+ +=end html +=cut diff --git a/fhem/docs/commandref_frame.html b/fhem/docs/commandref_frame.html index dd13187be..9ae5d2ba2 100644 --- a/fhem/docs/commandref_frame.html +++ b/fhem/docs/commandref_frame.html @@ -116,6 +116,7 @@ RandomTimer   rain   readingsGroup   + readingsHistory   readingsProxy   remotecontrol   SUNRISE_EL   diff --git a/fhem/docs/commandref_frame_DE.html b/fhem/docs/commandref_frame_DE.html index 7e7617d9b..012b4ec11 100644 --- a/fhem/docs/commandref_frame_DE.html +++ b/fhem/docs/commandref_frame_DE.html @@ -115,6 +115,7 @@ RandomTimer   rain   readingsGroup   + readingsHistory   readingsProxy   remotecontrol   SUNRISE_EL