33_readingsHistory.pm 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808
  1. # $Id: 33_readingsHistory.pm 16299 2018-03-01 08:06:55Z justme1968 $
  2. ##############################################################################
  3. #
  4. # This file is part of fhem.
  5. #
  6. # Fhem is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 2 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # Fhem is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with fhem. If not, see <http://www.gnu.org/licenses/>.
  18. #
  19. ##############################################################################
  20. package main;
  21. use strict;
  22. use warnings;
  23. use POSIX qw(strftime);
  24. use vars qw(%defs);
  25. use vars qw(%attr);
  26. sub Log($$);
  27. sub Log3($$$);
  28. use vars qw($FW_ME);
  29. use vars qw($FW_wname);
  30. use vars qw($FW_subdir);
  31. use vars qw(%FW_hiddenroom);
  32. use vars qw(%FW_visibleDeviceHash);
  33. use vars qw(%FW_webArgs); # all arguments specified in the GET
  34. my $readingsHistory_hasJSON = 0;
  35. my $readingsHistory_hasDataDumper = 1;
  36. sub readingsHistory_Initialize($)
  37. {
  38. my ($hash) = @_;
  39. $hash->{DefFn} = "readingsHistory_Define";
  40. $hash->{NotifyFn} = "readingsHistory_Notify";
  41. $hash->{NotifyOrderPrefix}= "51-";
  42. $hash->{UndefFn} = "readingsHistory_Undefine";
  43. $hash->{SetFn} = "readingsHistory_Set";
  44. $hash->{GetFn} = "readingsHistory_Get";
  45. $hash->{AttrFn} = "readingsHistory_Attr";
  46. $hash->{AttrList} = "disable:1,2,3 mapping style timestampFormat valueFormat noheading:1 nohtml:1 nolinks:1 notime:1 nostate:1 alwaysTrigger:1 rows";
  47. $hash->{FW_detailFn} = "readingsHistory_detailFn";
  48. $hash->{FW_summaryFn} = "readingsHistory_detailFn";
  49. $data{FWEXT}{"readingsHistory"}{SCRIPT} = "fhemweb_readingsHistory.js";
  50. $hash->{FW_atPageEnd} = 1;
  51. eval "use Data::Dumper";
  52. $readingsHistory_hasDataDumper = 0 if($@);
  53. }
  54. sub
  55. readingsHistory_updateDevices($)
  56. {
  57. my ($hash) = @_;
  58. my %list;
  59. my @devices;
  60. my $def = $hash->{DEF};
  61. $def = "" if( !$def );
  62. my @params = split(" ", $def);
  63. while (@params) {
  64. my $param = shift(@params);
  65. my @device = split(":", $param);
  66. if($device[0] =~ m/(.*)=(.*)/) {
  67. my ($lattr,$re) = ($1, $2);
  68. foreach my $d (sort keys %defs) {
  69. next if( IsIgnored($d) );
  70. next if( !defined($defs{$d}{$lattr}) );
  71. next if( $defs{$d}{$lattr} !~ m/^$re$/);
  72. $list{$d} = 1;
  73. push @devices, [$d,$device[1]];
  74. }
  75. } elsif($device[0] =~ m/(.*)&(.*)/) {
  76. my ($lattr,$re) = ($1, $2);
  77. foreach my $d (sort keys %attr) {
  78. next if( IsIgnored($d) );
  79. next if( !defined($attr{$d}{$lattr}) );
  80. next if( $attr{$d}{$lattr} !~ m/^$re$/);
  81. $list{$d} = 1;
  82. push @devices, [$d,$device[1]];
  83. }
  84. } elsif( defined($defs{$device[0]}) ) {
  85. $list{$device[0]} = 1;
  86. push @devices, [@device];
  87. } else {
  88. foreach my $d (sort keys %defs) {
  89. next if( IsIgnored($d) );
  90. eval { $d =~ m/^$device[0]$/ };
  91. if( $@ ) {
  92. Log3 $hash->{NAME}, 3, $hash->{NAME} .": ". $device[0] .": ". $@;
  93. push @devices, ["<<ERROR>>"];
  94. last;
  95. }
  96. next if( $d !~ m/^$device[0]$/);
  97. $list{$d} = 1;
  98. push @devices, [$d,$device[1]];
  99. }
  100. }
  101. }
  102. $hash->{CONTENT} = \%list;
  103. $hash->{DEVICES} = \@devices;
  104. delete( $hash->{NOTIFYDEV} );
  105. if( scalar keys %list == 0 ) {
  106. $hash->{NOTIFYDEV} = "global";
  107. }
  108. $hash->{fhem}->{last_update} = gettimeofday();
  109. }
  110. sub
  111. readingsHistory_Define($$)
  112. {
  113. my ($hash, $def) = @_;
  114. my @args = split("[ \t]+", $def);
  115. return "Usage: define <name> readingsHistory [<device>[:<readings>]]+" if(@args < 2);
  116. my $name = shift(@args);
  117. my $type = shift(@args);
  118. $hash->{HAS_DataDumper} = $readingsHistory_hasDataDumper;
  119. $hash->{fhem}{history} = [] if( !defined($hash->{fhem}{history}) );
  120. readingsHistory_updateDevices($hash);
  121. $hash->{STATE} = 'Initialized';
  122. return undef;
  123. }
  124. sub
  125. readingsHistory_lookup($$$$$$$$)
  126. {
  127. my($mapping,$name,$alias,$reading,$value,$room,$group,$default) = @_;
  128. if( $mapping ) {
  129. if( !ref($mapping) && $mapping =~ m/^{.*}$/) {
  130. my $DEVICE = $name;
  131. my $READING = $reading;
  132. my $VALUE = $value;
  133. my $m = eval $mapping;
  134. if( $@ ) {
  135. Log 2, $@ if( $@ );
  136. } else {
  137. $mapping = $m;
  138. }
  139. }
  140. if( ref($mapping) eq 'HASH' ) {
  141. $default = $mapping->{$name} if( defined($mapping->{$name}) );
  142. $default = $mapping->{$reading} if( defined($mapping->{$reading}) );
  143. $default = $mapping->{$name.".".$reading} if( defined($mapping->{$name.".".$reading}) );
  144. $default = $mapping->{$reading.".".$value} if( defined($mapping->{$reading.".".$value}) );
  145. } else {
  146. $default = $mapping;
  147. }
  148. if( !ref($default) && $default =~ m/^{.*}$/) {
  149. my $DEVICE = $name;
  150. my $READING = $reading;
  151. my $VALUE = $value;
  152. $default = eval $default;
  153. $default = "" if( $@ );
  154. Log 2, $@ if( $@ );
  155. }
  156. return $default if( !defined($default) );
  157. $default =~ s/\%ALIAS/$alias/g;
  158. $default =~ s/\%DEVICE/$name/g;
  159. $default =~ s/\%READING/$reading/g;
  160. $default =~ s/\%VALUE/$value/g;
  161. $default =~ s/\%ROOM/$room/g;
  162. $default =~ s/\%GROUP/$group/g;
  163. $default =~ s/\$ALIAS/$alias/g;
  164. $default =~ s/\$DEVICE/$name/g;
  165. $default =~ s/\$READING/$reading/g;
  166. $default =~ s/\$VALUE/$value/g;
  167. $default =~ s/\$ROOM/$room/g;
  168. $default =~ s/\$GROUP/$group/g;
  169. }
  170. return $default;
  171. }
  172. sub
  173. readingsHistory_lookup2($$$$)
  174. {
  175. my($lookup,$name,$reading,$value) = @_;
  176. return $lookup if( !$lookup );
  177. if( !ref($lookup) && $lookup =~ m/^{.*}$/) {
  178. my $DEVICE = $name;
  179. my $READING = $reading;
  180. my $VALUE = $value;
  181. my $l = eval $lookup;
  182. if( $@ ) {
  183. Log 2, $@ if( $@ );
  184. } else {
  185. $lookup = $l;
  186. }
  187. }
  188. if( ref($lookup) eq 'HASH' ) {
  189. my $vf = "";
  190. $vf = $lookup->{$reading} if( defined($reading) && exists($lookup->{$reading}) );
  191. $vf = $lookup->{$name.".".$reading} if( defined($reading) && exists($lookup->{$name.".".$reading}) );
  192. $vf = $lookup->{$reading.".".$value} if( defined($value) && exists($lookup->{$reading.".".$value}) );
  193. $lookup = $vf;
  194. }
  195. return $lookup if( !defined($lookup) );
  196. if( !ref($lookup) && $lookup =~ m/^{.*}$/) {
  197. my $DEVICE = $name;
  198. my $READING = $reading;
  199. my $VALUE = $value;
  200. $lookup = eval $lookup;
  201. $lookup = "" if( $@ );
  202. Log 2, $@ if( $@ );
  203. }
  204. return $lookup if( !defined($lookup) );
  205. $lookup =~ s/\%DEVICE/$name/g;
  206. $lookup =~ s/\%READING/$reading/g;
  207. $lookup =~ s/\%VALUE/$value/g;
  208. $lookup =~ s/\$DEVICE/$name/g;
  209. $lookup =~ s/\$READING/$reading/g;
  210. $lookup =~ s/\$VALUE/$value/g;
  211. return $lookup;
  212. }
  213. sub readingsHistory_Undefine($$)
  214. {
  215. my ($hash,$arg) = @_;
  216. return undef;
  217. }
  218. sub
  219. readingsHistory_2html($)
  220. {
  221. my($hash) = @_;
  222. $hash = $defs{$hash} if( ref($hash) ne 'HASH' );
  223. return undef if( !$hash );
  224. if( $hash->{DEF} && $hash->{DEF} =~ m/=/ ) {
  225. if( !$hash->{fhem}->{last_update}
  226. || gettimeofday() - $hash->{fhem}->{last_update} > 600 ) {
  227. readingsHistory_updateDevices($hash);
  228. }
  229. }
  230. my $d = $hash->{NAME};
  231. my $show_heading = !AttrVal( $d, "noheading", "0" );
  232. my $show_links = !AttrVal( $d, "nolinks", "0" );
  233. $show_links = 0 if($FW_hiddenroom{detail});
  234. my $disable = AttrVal($d,"disable", 0);
  235. if( AttrVal($d,"disable", 0) > 2 ) {
  236. return undef;
  237. } elsif( AttrVal($d,"disable", 0) > 1 ) {
  238. my $ret;
  239. $ret .= "<table>";
  240. my $txt = AttrVal($d, "alias", $d);
  241. $txt = "<a href=\"$FW_ME$FW_subdir?detail=$d\">$txt</a>" if( $show_links );
  242. $ret .= "<tr><td><div class=\"devType\">$txt</a></div></td></tr>" if( $show_heading );
  243. $ret .= "<tr><td><table class=\"block wide\">";
  244. #$ret .= "<div class=\"devType\"><a style=\"color:#ff8888\" href=\"$FW_ME$FW_subdir?detail=$d\">readingsHistory $txt is disabled.</a></div>";
  245. $ret .= "<td><div style=\"color:#ff8888;text-align:center\">disabled</div></td>";
  246. $ret .= "</table></td></tr>";
  247. $ret .= "</table>";
  248. return $ret;
  249. }
  250. my $show_time = !AttrVal( $d, "notime", "0" );
  251. my $style = AttrVal( $d, "style", "" );
  252. my $devices = $hash->{DEVICES};
  253. my $row = 1;
  254. my $ret;
  255. $ret .= "<table>";
  256. my $txt = AttrVal($d, "alias", $d);
  257. $txt = "<a href=\"$FW_ME$FW_subdir?detail=$d\">$txt</a>" if( $show_links );
  258. $ret .= "<tr><td><div class=\"devType\">$txt</a></div></td></tr>" if( $show_heading );
  259. $ret .= "<tr><td><table $style class=\"block wide\">";
  260. $ret .= "<tr><td colspan=\"99\"><div style=\"color:#ff8888;text-align:center\">updates disabled</div></tr>" if( $disable > 0 );
  261. my $rows = AttrVal($d,"rows", 5 );
  262. $rows = 1 if( $rows < 1 );
  263. my $lines = "";
  264. for (my $i = 0; $i < $rows; $i++) {
  265. my $line = $hash->{fhem}{history}[$i];
  266. if( ref($line) eq 'ARRAY' ) {
  267. $lines .= $line->[3] if( $line );
  268. } else {
  269. $lines .= $line if( $line );
  270. }
  271. $lines .= "<br>";
  272. }
  273. $ret .= sprintf("<tr class=\"%s\">", ($row&1)?"odd":"even");
  274. $row++;
  275. $ret .= "<td><div id=\"$d-history\" rows=\"$rows\">$lines</div></td>";
  276. $ret .= sprintf("<tr class=\"%s\">", ($row&1)?"odd":"even");
  277. $ret .= "<td colspan=\"99\"><div style=\"color:#ff8888;text-align:center\">updates disabled</div></td></tr>" if( $disable > 0 );
  278. $ret .= "</table></td></tr>";
  279. $ret .= "</table>";
  280. return $ret;
  281. }
  282. sub
  283. readingsHistory_detailFn()
  284. {
  285. my ($FW_wname, $d, $room, $pageHash) = @_; # pageHash is set for summaryFn.
  286. my $hash = $defs{$d};
  287. $hash->{mayBeVisible} = 1;
  288. return readingsHistory_2html($d);
  289. }
  290. sub
  291. readingsHistory_makeTimestamp($$) {
  292. my ($t, $timestampFormat) = @_;
  293. my @lt = localtime($t);
  294. return strftime( $timestampFormat, @lt ) if( $timestampFormat );
  295. return sprintf("%02d:%02d:%02d", $lt[2], $lt[1], $lt[0] );
  296. }
  297. sub
  298. readingsHistory_Notify($$)
  299. {
  300. my ($hash,$dev) = @_;
  301. my $name = $hash->{NAME};
  302. if( grep(m/^INITIALIZED$/, @{$dev->{CHANGED}}) ) {
  303. readingsHistory_updateDevices($hash);
  304. readingsHistory_Load($hash);
  305. return undef;
  306. } elsif( grep(m/^REREADCFG$/, @{$dev->{CHANGED}}) ) {
  307. readingsHistory_updateDevices($hash);
  308. readingsHistory_Load($hash);
  309. return undef;
  310. } elsif( grep(m/^SAVE$/, @{$dev->{CHANGED}}) ) {
  311. readingsHistory_Save();
  312. }
  313. return if( AttrVal($name,"disable", 0) > 0 );
  314. return if($dev->{TYPE} eq $hash->{TYPE});
  315. #return if($dev->{NAME} eq $name);
  316. my $devices = $hash->{DEVICES};
  317. my $max = int(@{$dev->{CHANGED}});
  318. for (my $i = 0; $i < $max; $i++) {
  319. my $s = $dev->{CHANGED}[$i];
  320. $s = "" if(!defined($s));
  321. if( $dev->{NAME} eq "global" && $s =~ m/^RENAMED ([^ ]*) ([^ ]*)$/) {
  322. my ($old, $new) = ($1, $2);
  323. if( defined($hash->{CONTENT}{$old}) ) {
  324. $hash->{DEF} =~ s/(^|\s+)$old((:\S+)?\s*)/$1$new$2/g;
  325. }
  326. readingsHistory_updateDevices($hash);
  327. } elsif( $dev->{NAME} eq "global" && $s =~ m/^DELETED ([^ ]*)$/) {
  328. my ($name) = ($1);
  329. if( defined($hash->{CONTENT}{$name}) ) {
  330. $hash->{DEF} =~ s/(^|\s+)$name((:\S+)?\s*)/ /g;
  331. $hash->{DEF} =~ s/^ //;
  332. $hash->{DEF} =~ s/ $//;
  333. }
  334. readingsHistory_updateDevices($hash);
  335. } elsif( $dev->{NAME} eq "global" && $s =~ m/^DEFINED ([^ ]*)$/) {
  336. readingsHistory_updateDevices($hash);
  337. } else {
  338. next if(AttrVal($name,"disable", undef));
  339. next if (!$hash->{CONTENT}->{$dev->{NAME}});
  340. if( $hash->{alwaysTrigger} ) {
  341. $hash->{mayBeVisible} = 1;
  342. } elsif( !defined($hash->{mayBeVisible}) ) {
  343. Log3 $name, 5, "$name: not on any display, don't trigger";
  344. } else {
  345. if( defined($FW_visibleDeviceHash{$name}) ) {
  346. } else {
  347. Log3 $name, 5, "$name: no longer visible, don't trigger";
  348. delete( $hash->{mayBeVisible} );
  349. }
  350. }
  351. my ($reading,$value) = split(": ", $s, 2);
  352. $reading = "" if( !defined($reading) );
  353. next if( $reading =~ m/^\./);
  354. $value = "" if( !defined($value) );
  355. my $show_state = 1;
  356. if( $value eq "" ) {
  357. next if AttrVal( $name, "nostate", "0" );
  358. $reading = "state";
  359. $value = $s;
  360. }
  361. my $no_html = AttrVal( $name, "nohtml", "0" );
  362. my $spacing = "&nbsp;&nbsp;";
  363. $spacing = " " if $no_html;
  364. my $mapping = AttrVal( $name, "mapping", "");
  365. $mapping = eval $mapping if( $mapping =~ m/^{.*}$/ );
  366. #$mapping = undef if( ref($mapping) ne 'HASH' );
  367. my $timestampFormat = AttrVal( $name, "timestampFormat", undef);
  368. my $value_format = AttrVal( $name, "valueFormat", "" );
  369. if( $value_format =~ m/^{.*}$/ ) {
  370. my $vf = eval $value_format;
  371. $value_format = $vf if( $vf );
  372. }
  373. foreach my $device (@{$devices}) {
  374. my $item = 0;
  375. my $h = $defs{@{$device}[0]};
  376. next if( !$h );
  377. next if( $dev->{NAME} ne $h->{NAME} );
  378. my $n = $h->{NAME};
  379. my $regex = @{$device}[1];
  380. my @list = (undef);
  381. @list = split(",",$regex) if( $regex );
  382. foreach my $regex (@list) {
  383. next if( $reading eq "state" && !$show_state && (!defined($regex) || $regex ne "state") );
  384. next if( defined($regex) && $reading !~ m/^$regex$/);
  385. my $msg = $s;
  386. my $value_format = readingsHistory_lookup2($value_format,$n,$reading,$value);
  387. next if( !defined($value_format) );
  388. if( $value_format =~ m/%/ ) {
  389. $msg = sprintf( $value_format, $value );
  390. } elsif( $value_format ) {
  391. $msg = $value_format;
  392. }
  393. my $t = time;
  394. my $tm = readingsHistory_makeTimestamp( $t, $timestampFormat );
  395. my $show_links = !AttrVal( $name, "nolinks", "0" );
  396. $show_links = 0 if($FW_hiddenroom{detail});
  397. $show_links = 0 if(!defined($FW_ME));
  398. $show_links = 0 if($no_html);
  399. my $a = AttrVal($n, "alias", $n);
  400. my $m = $a;
  401. if( $mapping ) {
  402. my $room = AttrVal($n, "room", "");
  403. my $group = AttrVal($n, "group", "");
  404. $m = readingsHistory_lookup($mapping,$n,$a,$reading,$value,$room,$group,$m);
  405. }
  406. if( $show_links ) {
  407. $msg = "$tm$spacing<a href=\"$FW_ME$FW_subdir?detail=$n\">$m</a> $msg";
  408. } else {
  409. $msg = "$tm$spacing$m $msg";
  410. }
  411. my $entry = [$t, $n, $s,$msg];
  412. #my $entry = [$t, $n, "$reading: $value", $msg];
  413. while( @{$hash->{fhem}{history}} >= AttrVal($name,"rows", 5 ) ) {
  414. pop @{$hash->{fhem}{history}};
  415. }
  416. unshift( @{$hash->{fhem}{history}}, $entry );
  417. DoTrigger( "$name", "history: <html>$msg</html>" ) if( $hash->{mayBeVisible} );
  418. }
  419. }
  420. }
  421. }
  422. return undef;
  423. }
  424. sub
  425. readingsHistory_StatefileName()
  426. {
  427. my $statefile = $attr{global}{statefile};
  428. $statefile = substr $statefile,0,rindex($statefile,'/')+1;
  429. #return $statefile ."readingsHistorys.save" if( $readingsHistory_hasJSON );
  430. return $statefile ."readingsHistorys.dd.save" if( $readingsHistory_hasDataDumper );
  431. }
  432. my $readingsHistory_LastSaveTime="";
  433. sub
  434. readingsHistory_Save()
  435. {
  436. my $time_now = TimeNow();
  437. return if( $time_now eq $readingsHistory_LastSaveTime);
  438. $readingsHistory_LastSaveTime = $time_now;
  439. return "No statefile specified" if(!$attr{global}{statefile});
  440. my $statefile = readingsHistory_StatefileName();
  441. my $hash;
  442. for my $d (keys %defs) {
  443. next if($defs{$d}{TYPE} ne "readingsHistory");
  444. next if( !defined($defs{$d}{fhem}{history}) );
  445. $hash->{$d} = $defs{$d}{fhem}{history};
  446. }
  447. if(open(FH, ">$statefile")) {
  448. my $t = localtime;
  449. print FH "#$t\n";
  450. if( $readingsHistory_hasJSON ) {
  451. print FH encode_json($hash) if( defined($hash) );
  452. } elsif( $readingsHistory_hasDataDumper ) {
  453. my $dumper = Data::Dumper->new([]);
  454. $dumper->Terse(1);
  455. $dumper->Values([$hash]);
  456. print FH $dumper->Dump;
  457. }
  458. close(FH);
  459. } else {
  460. my $msg = "readingsHistory_Save: Cannot open $statefile: $!";
  461. Log3 undef, 1, $msg;
  462. }
  463. return undef;
  464. }
  465. sub
  466. readingsHistory_Load($)
  467. {
  468. my ($hash) = @_;
  469. return "No statefile specified" if(!$attr{global}{statefile});
  470. my $statefile = readingsHistory_StatefileName();
  471. if(open(FH, "<$statefile")) {
  472. my $encoded;
  473. while (my $line = <FH>) {
  474. chomp $line;
  475. next if($line =~ m/^#.*$/);
  476. $encoded .= $line;
  477. }
  478. close(FH);
  479. return if( !defined($encoded) );
  480. my $decoded;
  481. if( $readingsHistory_hasJSON ) {
  482. $decoded = decode_json( $encoded );
  483. } elsif( $readingsHistory_hasDataDumper ) {
  484. $decoded = eval $encoded;
  485. }
  486. $hash->{fhem}{history} = $decoded->{$hash->{NAME}} if( defined($decoded->{$hash->{NAME}}) );
  487. } else {
  488. my $msg = "readingsHistory_Load: Cannot open $statefile: $!";
  489. Log3 undef, 1, $msg;
  490. }
  491. return undef;
  492. }
  493. sub
  494. readingsHistory_Set($@)
  495. {
  496. my ($hash, $name, $cmd, $param, @a) = @_;
  497. my $list = "add clear:noArg";
  498. my $no_html = AttrVal( $name, "nohtml", "0" );
  499. my $spacing = "&nbsp;&nbsp;";
  500. $spacing = " " if $no_html;
  501. if( $cmd eq "clear" ) {
  502. $hash->{fhem}{history} = [];
  503. my $t = time;
  504. my $tm = readingsHistory_makeTimestamp( time, AttrVal( $name, "timestampFormat", undef) );
  505. my $msg = "$tm$spacing--clear--";
  506. my $entry = [$t,"", $cmd, $msg];
  507. unshift( @{$hash->{fhem}{history}}, $entry );
  508. DoTrigger( "$name", "clear: $msg" ) if( $hash->{mayBeVisible} );
  509. return undef;
  510. } elsif ( $cmd eq "add" ) {
  511. my $t = time;
  512. my $tm = readingsHistory_makeTimestamp( time, AttrVal( $name, "timestampFormat", undef) );
  513. my $msg = "$tm$spacing$param ". join( " ", @a );
  514. my $entry = [$t,"", "$param ". join( " ", @a ),$msg];
  515. while( @{$hash->{fhem}{history}} >= AttrVal($name,"rows", 5 ) ) {
  516. pop @{$hash->{fhem}{history}};
  517. }
  518. unshift( @{$hash->{fhem}{history}}, $entry );
  519. DoTrigger( "$name", "history: <html>$msg</html>" ) if( $hash->{mayBeVisible} );
  520. return undef;
  521. }
  522. return "Unknown argument $cmd, choose one of $list";
  523. }
  524. sub
  525. readingsHistory_Get($@)
  526. {
  527. my ($hash, @a) = @_;
  528. my $list = "history:noArg html:noArg";
  529. my $name = $a[0];
  530. return "$name: get needs at least one parameter" if(@a < 2);
  531. my $cmd= $a[1];
  532. my $ret = "";
  533. if( $cmd eq "html" ) {
  534. return readingsHistory_2html($hash);
  535. } elsif( $cmd eq "history" ) {
  536. my $rows = AttrVal($name,"rows", 5 );
  537. $rows = 1 if( $rows < 1 );
  538. my $timestampFormat = AttrVal( $name, "timestampFormat", undef);
  539. for (my $i = 0; $i < $rows; $i++) {
  540. my $line = $hash->{fhem}{history}[$i];
  541. if( ref($line) eq 'ARRAY' ) {
  542. my $tm = readingsHistory_makeTimestamp( $line->[0], $timestampFormat );
  543. $ret .= "$tm\t$line->[1]\t$line->[2]\t$line->[3]" if( $line );
  544. } else {
  545. $ret .= $line if( $line );
  546. }
  547. $ret .= "\n";
  548. }
  549. return $ret;
  550. }
  551. return "Unknown argument $cmd, choose one of $list";
  552. }
  553. sub
  554. readingsHistory_Attr($$$)
  555. {
  556. my ($cmd, $name, $attrName, $attrVal) = @_;
  557. my $orig = $attrVal;
  558. if( $attrName eq "alwaysTrigger" ) {
  559. my $hash = $defs{$name};
  560. $attrVal = 1 if($attrVal);
  561. if( $cmd eq "set" ) {
  562. $hash->{alwaysTrigger} = $attrVal;
  563. delete( $hash->{helper}->{myDisplay} ) if( $hash->{alwaysTrigger} );
  564. } else {
  565. delete $hash->{alwaysTrigger};
  566. }
  567. }
  568. if( $cmd eq "set" ) {
  569. if( $orig ne $attrVal ) {
  570. $attr{$name}{$attrName} = $attrVal;
  571. return $attrName ." set to ". $attrVal;
  572. }
  573. }
  574. return;
  575. }
  576. 1;
  577. =pod
  578. =item helper
  579. =item summary display a history of readings from on or more devices
  580. =item summary_DE Anzeige der Historie von Readings eines oder mehrerer Ger&auml;te
  581. =begin html
  582. <a name="readingsHistory"></a>
  583. <h3>readingsHistory</h3>
  584. <ul>
  585. Displays a history of readings from on or more devices.
  586. <br><br>
  587. <a name="readingsHistory_Define"></a>
  588. <b>Define</b>
  589. <ul>
  590. <code>define &lt;name&gt; readingsHistory [&lt;device&gt;[:regex] [&lt;device-2&gt;[:regex-2] ... [&lt;device-n&gt;[:regex-n]]]]</code><br>
  591. <br>
  592. Notes:
  593. <ul>
  594. <li>&lt;device&gt; can be of the form INTERNAL=VALUE where INTERNAL is the name of an internal value and VALUE is a regex.</li>
  595. <li>If regex is a comma separatet list it will be used as an enumeration of allowed readings.</li>
  596. <li>if no device/reading argument is given only lines with 'set &lt;device&gt; add ...' are displayed.</li>
  597. </ul><br>
  598. Examples:
  599. <ul>
  600. </ul>
  601. </ul><br>
  602. <a name="readingsHistory_Set"></a>
  603. <b>Set</b>
  604. <ul>
  605. <li>add ...<br>
  606. directly add text as new line to history.</li>
  607. <li>clear<br>
  608. clear the history.</li>
  609. </ul><br>
  610. <a name="readingsHistory_Get"></a>
  611. <b>Get</b>
  612. <ul>
  613. <li>history<br>
  614. list history</li>
  615. </ul><br>
  616. <a name="readingsHistory_Attr"></a>
  617. <b>Attributes</b>
  618. <ul>
  619. <li>alwaysTrigger<br>
  620. 1 -> alwaysTrigger update events. even if not visible.</li>
  621. <li>disable<br>
  622. 1 -> disable notify processing and longpoll updates. Notice: this also disables rename and delete handling.<br>
  623. 2 -> also disable html table creation<br>
  624. 3 -> also disable html creation completely</li>
  625. <li>noheading<br>
  626. If set to 1 the readings table will have no heading.</li>
  627. <li>nolinks<br>
  628. Disables the html links from the heading and the reading names.</li>
  629. <li>notime<br>
  630. If set to 1 the reading timestamp is not displayed.</li>
  631. <li>mapping<br>
  632. Can be a simple string or a perl expression enclosed in {} that returns a hash that maps device names
  633. to the displayed name. The keys can be either the name of the reading or &lt;device&gt;.&lt;reading&gt;.
  634. %DEVICE, %ALIAS, %ROOM and %GROUP are replaced by the device name, device alias, room attribute and
  635. group attribute respectively. You can also prefix these keywords with $ instead of %.
  636. </li>
  637. <li>style<br>
  638. Specify an HTML style for the readings table, e.g.:<br>
  639. <code>attr history style style="font-size:20px"</code></li>
  640. <li>timestampFormat<br>
  641. POSIX strftime compatible string for used as the timestamp for each line.
  642. </li>
  643. <li>valueFormat<br>
  644. Specify an sprintf style format string used to display the reading values. If the format string is undef
  645. this reading will be skipped. Can be given as a string, a perl expression returning a hash or a perl
  646. expression returning a string, e.g.:<br>
  647. <code>attr history valueFormat %.1f &deg;C</code><br>
  648. <code>attr history valueFormat { temperature => "%.1f &deg;C", humidity => "%.1f %" }</code><br>
  649. <code>attr history valueFormat { ($READING eq 'temperature')?"%.1f &deg;C":undef }</code></li>
  650. <li>rows<br>
  651. Number of history rows to show.</li>
  652. </ul>
  653. </ul>
  654. =end html
  655. =cut