98_FReplacer.pm 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. ##############################################
  2. # $Id: 98_FReplacer.pm 10630 2016-01-25 18:49:37Z ststrobel $
  3. #
  4. # Basiert auf der Idee Fhem Daten auf einem Kindle anzuzeigen
  5. # wie im Forum beschrieben
  6. #
  7. ##############################################################################
  8. # Changelog:
  9. #
  10. # 2014-07-12 initial version
  11. # 2014-10-02 fixed some minor issues and added documentation
  12. # 2014-10-19 fixed a typo and some minor issues
  13. # 2014-11-04 renamed some attributes and added PostCommand to make the module more flexible
  14. # 2014-11-08 added the attributes Reading.*, MaxAge.*, MinValue.*, MaxValue.* and Format.*
  15. # 2014-11-15 fixed bugs related to RepReading and InternalTimer
  16. # 2014-12-05 definierte Attribute werden der userattr list der Instanz hinzugefügt
  17. # 2014-12-25 little bug fixes, kleineres Datum ergänzt
  18. # 2015-03-22 flexibleres Datumsformat für das Reading LastUpdate per strftime ergänzt
  19. # 2015-04-25 allow {expr} as replacement for MaxAge, MinValue and MaxValue
  20. #
  21. package main;
  22. use strict;
  23. use warnings;
  24. use Time::HiRes qw( time );
  25. use POSIX qw(strftime);
  26. use Encode qw(decode encode);
  27. sub FReplacer_Initialize($);
  28. sub FReplacer_Define($$);
  29. sub FReplacer_Undef($$);
  30. sub FReplacer_Update($);
  31. sub FReplacer_Attr(@);
  32. require "$attr{global}{modpath}/FHEM/99_Utils.pm";
  33. #####################################
  34. sub FReplacer_Initialize($)
  35. {
  36. my ($hash) = @_;
  37. $hash->{DefFn} = "FReplacer_Define";
  38. $hash->{UndefFn} = "FReplacer_Undef";
  39. $hash->{AttrFn} = "FReplacer_Attr";
  40. $hash->{SetFn} = "FReplacer_Set";
  41. $hash->{AttrList}= "Rep[0-9]+Regex " . # Match für Ersetzungen
  42. "Rep[0-9]+Reading " . # Reading for Replacement
  43. "Rep[0-9]+MaxAge " . # optional Max age of Reading
  44. "Rep[0-9]+MinValue " . # optional Min Value of Reading
  45. "Rep[0-9]+MaxValue " . # optional Max Value of Reading
  46. "Rep[0-9]+Format " . # optional Format string for Replacement
  47. "Rep[0-9]+Expr " . # optional Expression to be evaluated before using the replacement
  48. "Rep[0-9]+Comment " . # optional comment or this replacement
  49. "ReplacementEncode " . # Ergebnis einer Ersetzung z.B. in UTF-8 Encoden
  50. "PostCommand " . # Systembefehl, der nach der Ersetzung ausgeführt wird
  51. "LUTimeFormat " . # time format for strftime for LastUpdate
  52. $readingFnAttributes;
  53. }
  54. #####################################
  55. sub FReplacer_Define($$)
  56. {
  57. my ($hash, $def) = @_;
  58. my @a = split("[ \t]+", $def);
  59. my ($name, $FReplacer, $template, $output, $interval) = @a;
  60. return "wrong syntax: define <name> FReplacer [Template] [Output] [interval]"
  61. if(@a < 4);
  62. $hash->{TEMPLATE} = $template;
  63. $hash->{OUTPUT} = $output;
  64. if (!defined($interval)) {
  65. $hash->{INTERVAL} = 60;
  66. } else {
  67. $hash->{INTERVAL} = $interval;
  68. }
  69. RemoveInternalTimer ($hash);
  70. InternalTimer(gettimeofday()+1, "FReplacer_Update", $hash, 0);
  71. return undef;
  72. }
  73. #####################################
  74. sub
  75. FReplacer_Undef($$)
  76. {
  77. my ($hash, $arg) = @_;
  78. #my $name = $hash->{NAME};
  79. RemoveInternalTimer ($hash);
  80. return undef;
  81. }
  82. #
  83. # Attr command
  84. ##############################################################
  85. sub
  86. FReplacer_Attr(@)
  87. {
  88. my ($cmd,$name,$aName,$aVal) = @_;
  89. # $cmd can be "del" or "set"
  90. # $name is device name
  91. # aName and aVal are Attribute name and value
  92. # Attributes are Regexp.*, Expr.*
  93. # Regex.* and Expr.* need validation
  94. if ($cmd eq "set") {
  95. if ($aName =~ "Regex") {
  96. eval { qr/$aVal/ };
  97. if ($@) {
  98. Log3 $name, 3, "$name: Invalid regex in attr $name $aName $aVal: $@";
  99. return "Invalid Regex $aVal in $aName";
  100. }
  101. } elsif ($aName =~ "Expr") {
  102. my $replacement = "";
  103. eval $aVal;
  104. if ($@) {
  105. Log3 $name, 3, "$name: Invalid Expression in attr $name $aName $aVal: $@";
  106. return "Invalid Expression $aVal in $aName";
  107. }
  108. } elsif ($aName =~ "MaxAge") {
  109. if ($aVal !~ '([0-9]+):(.+)') {
  110. Log3 $name, 3, "$name: wrong format in attr $name $aName $aVal";
  111. return "Invalid Format $aVal in $aName";
  112. }
  113. } elsif ($aName =~ "MinValue") {
  114. if ($aVal !~ '(^-?\d+\.?\d*):(.+)') {
  115. Log3 $name, 3, "$name: wrong format in attr $name $aName $aVal";
  116. return "Invalid Format $aVal in $aName";
  117. }
  118. } elsif ($aName =~ "MaxValue") {
  119. if ($aVal !~ '(^-?\d+\.?\d*):(.+)') {
  120. Log3 $name, 3, "$name: wrong format in attr $name $aName $aVal";
  121. return "Invalid Format $aVal in $aName";
  122. }
  123. } elsif ($aName =~ "Rep[0-9]+Format") {
  124. my $useless = eval { sprintf ($aVal, 1) };
  125. if ($@) {
  126. Log3 $name, 3, "$name: Invalid Format in attr $name $aName $aVal: $@";
  127. return "Invalid Format $aVal";
  128. }
  129. }
  130. addToDevAttrList($name, $aName)
  131. }
  132. return undef;
  133. }
  134. # SET command
  135. #########################################################################
  136. sub FReplacer_Set($@)
  137. {
  138. my ( $hash, @a ) = @_;
  139. return "\"set $a[0]\" needs at least an argument" if ( @a < 2 );
  140. # @a is an array with DeviceName, SetName, Rest of Set Line
  141. my ($name, $setName, $setVal) = @a;
  142. if($setName eq "ReplaceNow") {
  143. Log3 $name, 5, "$name: Set ReplaceNow is calling FReplacer_Update";
  144. RemoveInternalTimer ($hash);
  145. FReplacer_Update($hash);
  146. } else {
  147. return "Unknown argument $setName, choose one of ReplaceNow";
  148. }
  149. }
  150. #####################################
  151. sub
  152. FReplacer_Update($) {
  153. my ($hash) = @_;
  154. my $name = $hash->{NAME};
  155. InternalTimer(gettimeofday()+$hash->{INTERVAL}, "FReplacer_Update", $hash, 0);
  156. Log3 $name, 5, "$name: Update: Internal timer set for hash $hash to call update again in $hash->{INTERVAL} seconds";
  157. my ($tmpl, $out);
  158. if (!open($tmpl, "<", $hash->{TEMPLATE})) {
  159. Log3 $name, 3, "$name: Cannot open template file $hash->{TEMPLATE}";
  160. return;
  161. };
  162. if (!open($out, ">", $hash->{OUTPUT})) {
  163. Log3 $name, 3, "$name: Cannot create output file $hash->{OUTPUT}";
  164. return;
  165. };
  166. my $content = "";
  167. while (<$tmpl>) {
  168. $content .= $_;
  169. }
  170. my $timeFormat = AttrVal($name, "LUTimeFormat", "%d.%m.%Y %T");
  171. my $time = strftime($timeFormat, localtime);
  172. readingsSingleUpdate($hash, "LastUpdate", $time, 1 );
  173. my $time2 = strftime("%d.%m %R", localtime);
  174. readingsSingleUpdate($hash, "LastUpdateSmall", $time2, 1 );
  175. foreach my $key (keys %{$attr{$name}}) {
  176. if ($key =~ /Rep([0-9]+)Regex/) {
  177. my $index = $1;
  178. my $regex = $attr{$name}{"Rep${index}Regex"};
  179. my $replacement = "";
  180. my $skip = 0;
  181. if ($attr{$name}{"Rep${index}Reading"}) {
  182. if ($attr{$name}{"Rep${index}Reading"} !~ '([^\:]+):([^\:]+):?(.*)') {
  183. Log3 $name, 3, "$name: wrong format in attr Rep${index}Reading";
  184. next;
  185. }
  186. my $device = $1;
  187. my $rname = $2;
  188. my $default = ($3 ? $3 : 0);
  189. my $timestamp = ReadingsTimestamp ($device, $rname, 0);
  190. $replacement = ReadingsVal($device, $rname, $default);
  191. Log3 $name, 5, "$name: got reading $rname of device $device with default $default as $replacement with timestamp $timestamp";
  192. if ($attr{$name}{"Rep${index}MaxAge"}) {
  193. if ($attr{$name}{"Rep${index}MaxAge"} !~ '([0-9]+):(.+)') {
  194. Log3 $name, 3, "$name: wrong format in attr Rep${index}MaxAge";
  195. next;
  196. }
  197. my $max = $1;
  198. my $rep = $2;
  199. Log3 $name, 5, "$name: check max age $max";
  200. if (gettimeofday() - time_str2num($timestamp) > $max) {
  201. if ($rep =~ "{(.*)}") {
  202. Log3 $name, 5, "$name: reading too old - using Perl expression as MaxAge replacement: $1";
  203. $rep = eval($1);
  204. Log3 $name, 5, "$name: result is $rep";
  205. } else {
  206. Log3 $name, 5, "$name: reading too old - using $rep instead and skipping optional Expr and Format attributes";
  207. }
  208. $replacement = $rep;
  209. $skip = 1;
  210. }
  211. }
  212. if ($attr{$name}{"Rep${index}MinValue"} && !$skip) {
  213. if ($attr{$name}{"Rep${index}MinValue"} !~ '(^-?\d+\.?\d*):(.+)') {
  214. Log3 $name, 3, "$name: wrong format in attr Rep${index}MinValue";
  215. next;
  216. }
  217. my $lim = $1;
  218. my $rep = $2;
  219. Log3 $name, 5, "$name: check min value $lim";
  220. if ($replacement < $lim) {
  221. if ($rep =~ "{(.*)}") {
  222. Log3 $name, 5, "$name: using Perl expression as replacement: $1";
  223. $rep = eval($1);
  224. Log3 $name, 5, "$name: result is $rep";
  225. }
  226. Log3 $name, 5, "$name: reading too small - using $rep instead and skipping optional Expr and Format attributes";
  227. $replacement = $rep;
  228. $skip = 1;
  229. }
  230. }
  231. if ($attr{$name}{"Rep${index}MaxValue"} && !$skip) {
  232. if ($attr{$name}{"Rep${index}MaxValue"} !~ '(^-?\d+\.?\d*):(.+)') {
  233. Log3 $name, 3, "$name: wrong format in attr Rep${index}MaxValue";
  234. next;
  235. }
  236. my $lim = $1;
  237. my $rep = $2;
  238. Log3 $name, 5, "$name: check max value $lim";
  239. if ($replacement > $lim) {
  240. if ($rep =~ "{(.*)}") {
  241. Log3 $name, 5, "$name: using Perl expression as replacement: $1";
  242. $rep = eval($1);
  243. Log3 $name, 5, "$name: result is $rep";
  244. }
  245. Log3 $name, 5, "$name: reading too big - using $rep instead and skipping optional Expr and Format attributes";
  246. $replacement = $rep;
  247. $skip = 1;
  248. }
  249. }
  250. }
  251. if ($attr{$name}{"Rep${index}Expr"} && !$skip) {
  252. Log3 $name, 5, "$name: Evaluating Expr" . $attr{$name}{"Rep${index}Expr"} .
  253. "\$replacement = $replacement";
  254. $replacement = eval($attr{$name}{"Rep${index}Expr"});
  255. Log3 $name, 5, "$name: result is $replacement";
  256. if ($@) {
  257. Log3 $name, 3, "$name: error evaluating attribute Rep${index}Expr: $@";
  258. next;
  259. }
  260. }
  261. if ($attr{$name}{"Rep${index}Format"} && !$skip) {
  262. Log3 $name, 5, "$name: doing sprintf with format " . $attr{$name}{"Rep${index}Format"} .
  263. " value is $replacement";
  264. $replacement = sprintf($attr{$name}{"Rep${index}Format"}, $replacement);
  265. Log3 $name, 5, "$name: result is $replacement";
  266. }
  267. Log3 $name, 5, "$name: Replacing $regex with $replacement";
  268. $replacement = encode(AttrVal($name, "ReplacementEncode", undef), $replacement)
  269. if (AttrVal($name, "ReplacementEncode", undef));
  270. Log3 $name, 5, "$name: Replacement encoded as $replacement";
  271. $content =~ s/$regex/$replacement/g;
  272. }
  273. }
  274. print $out $content;
  275. if (AttrVal($name, "PostCommand", undef)) {
  276. my $convCmd = (AttrVal($name, "PostCommand", undef));
  277. Log3 $name, 5, "$name: Start conversion as $convCmd";
  278. system ($convCmd);
  279. Log3 $name, 5, "$name: Conversion started";
  280. }
  281. }
  282. 1;
  283. =pod
  284. =begin html
  285. <a name="FReplacer"></a>
  286. <h3>FReplacer</h3>
  287. <ul>
  288. This module provides a generic way to modify the contents of a file with Readings of other devices or the result of Perl expressions.<br>
  289. The typical use case is a custom designed SVG graphics template file which contains place holders that will be replaced with actual values.<br>
  290. The resulting SVG file can then optionally be converted to a PNG file which can be used as an online screensaver for Kindle devices for example.
  291. <br><br>
  292. <a name="FReplacerdefine"></a>
  293. <b>Define</b>
  294. <ul>
  295. <br>
  296. <code>define &lt;name&gt; FReplacer &lt;InputFile&gt; &lt;OutputFile&gt; &lt;Interval&gt;</code>
  297. <br><br>
  298. The module reads the given InputFile every Interval seconds, replaces strings with the results of expressions as defined in attributes and writes the result to the OutputFile<br>
  299. <br>
  300. Example:<br>
  301. <ul><code>define fr FReplacer /opt/fhem/www/images/template.svg /opt/fhem/www/images/status.svg 60</code></ul>
  302. </ul>
  303. <br>
  304. <a name="FReplacerconfiguration"></a>
  305. <b>Configuration of FReplacer Devices</b><br><br>
  306. <ul>
  307. Specify pairs of <code>attr FxxRegex</code> and <code>attr FxxReading</code> or <code>attr FxxExpr</code> to define which strings / placeholders in the InputFile should be replaced with which redings / expressions
  308. <br><br>
  309. Example:<br>
  310. <ul><code>
  311. define fr FReplacer /opt/fhem/www/images/template.svg /opt/fhem/www/images/status.svg 60 <br>
  312. attr fr Rep01Regex HeizungStat<br>
  313. attr fr Rep01Reading WP:Status<br>
  314. attr fr Rep01MaxAge 600:Heizung Aus<br>
  315. attr fr Rep02Regex AbluftTemp<br>
  316. attr fr Rep02Reading Lueftung:Temp_Abluft<br>
  317. attr fr Rep02Format "%.1f"<br>
  318. attr fr Rep03Regex AussenTemp<br>
  319. attr fr Rep03Expr sprintf("%.1f", ReadingsVal("Lueftung", "Temp_Aussen", 0))<br>
  320. </code></ul>
  321. <br>
  322. If you want to convert a resulting SVG file to a PNG e.g. for use as online screen saver on a Kindle device, <br>
  323. you have to specify the external conversion command with the attribute PostCommand, for Example:<br>
  324. <ul><code>
  325. attr fr PostCommand convert /opt/fhem/www/images/status.svg -type GrayScale -depth 8 /opt/fhem/www/images/status.png 2>/dev/null &
  326. </code></ul>
  327. <br>
  328. If you want to convert the replacement text from Readings to UTF8, e.g. to make special characters / umlauts display correctly, specify
  329. <ul><code>
  330. attr fr ReplacementEncode UTF-8
  331. </code></ul>
  332. <br>
  333. </ul>
  334. <a name="FReplacerset"></a>
  335. <b>Set-Commands</b><br>
  336. <ul>
  337. <li><b>ReplaceNow</b></li>
  338. starts a replace without waiting for the interval
  339. </ul>
  340. <br>
  341. <a name="FReplacerget"></a>
  342. <b>Get-Commands</b><br>
  343. <ul>
  344. none
  345. </ul>
  346. <br>
  347. <a name="FReplacerReadings"></a>
  348. <b>Readings</b><br>
  349. <ul>
  350. <li><b>LastUpdate</b></li>
  351. Date / Time of the last update of the output file / image. This reading is formatted with strftime and the default format string is "%d.%m.%Y %T".<br> This can be changed with the attribute LUTimeFormat.
  352. </ul>
  353. <br>
  354. <a name="FReplacerattr"></a>
  355. <b>Attributes</b><br><br>
  356. <ul>
  357. <li><b>Rep[0-9]+Regex</b></li>
  358. defines the regex to be used for finding the right string to be replaced with the corresponding Reading / Expr result
  359. <li><b>Rep[0-9]+Reading</b></li>
  360. defines a device and reading to be used as replacement value. It is specified as devicename:readingname:default_value.<br>
  361. The default_value is optional and defaults to 0. If the reading doesn't exist, default_value is used.
  362. <li><b>Rep[0-9]+MaxAge</b></li>
  363. this can optionally be used together with RepReading to define a maximum age of the reading. It is specified as seconds:replacement. If the corresponding reading has not been updated for the specified number of seconds, then the replacement is used instead of the reading to do the replacement and further RepExpr or RepFormat attributes will be ignored for this value<br>
  364. If you specify the replacement as {expr} then it is evaluated as a perl expression instead of a string.<br>
  365. <li><b>Rep[0-9]+MinValue</b></li>
  366. this can optionally be used together with RepReading to define a minimum value of the reading. It is specified as min:replacement. If the corresponding reading is too small, then the replacement string is used instead of the reading to do the replacement and further RepExpr or RepFormat attributes will be ignored for this value<br>
  367. If you specify the replacement as {expr} then it is evaluated as a perl expression instead of a string.<br>
  368. <li><b>Rep[0-9]+MaxValue</b></li>
  369. this can optionally be used together with RepReading to define a maximum value of the reading. It is specified as max:replacement. If the corresponding reading is too big, then the replacement string is used instead of the reading to do the replacement and further RepExpr or RepFormat attributes will be ignored for this value<br>
  370. If you specify the replacement as {expr} then it is evaluated as a perl expression instead of a string.<br>
  371. <li><b>Rep[0-9]+Expr</b></li>
  372. defines an optional expression that can be used to compute the replacement value. If RepExpr is used together with RepReading then the expression is evaluated after getting the reading and the value of the reading can be used in the expression as $replacement. <br>
  373. If only RepExpr is specified then readings can be retrieved with the perl function ReadingsVal() inside the expression. <br>
  374. If neither RepExpr nor RepReading is specified then the match for the correspondig regex will be replaced with an empty string.
  375. <li><b>Rep[0-9]+Format</b></li>
  376. defines an optional format string to be used in a sprintf statement to format the replacement before it is applied.<br>
  377. Can be used with RepReading or RepExpr or both.
  378. <li><b>LUTimeFormat</b></li>
  379. defines a time format string (see Posix strftime format) to be used when creating the reading LastUpdate.
  380. <li><b>PostCommand</b></li>
  381. Execute an external command after writing the output file, e.g. to convert a resulting SVG file to a PNG file.
  382. For an eInk Kindle you need a PNG in 8 bit greyscale format. A simple example to call the convert utility from ImageMagick would be <br>
  383. <code> attr fr PostCommand convert /opt/fhem/www/images/status.svg
  384. -type GrayScale -depth 8 /opt/fhem/www/images/status.png 2>/dev/null & </code><br>
  385. a more advanced example that starts inkscape before Imagemagick to make sure that embedded Icons in a SVG file are converted
  386. correctly could be <br>
  387. <code> attr fr PostCommand bash -c 'inkscape /opt/fhem/www/images/status.svg -e=tmp.png;; convert tmp.png -type GrayScale -depth 8 /opt/fhem/www/images/status.png' >/dev/null 2>&1 & </code><br>
  388. or even <br>
  389. <code> attr fr PostCommand bash -c 'inkscape /opt/fhem/www/images/status.svg -e=tmp.png -b=rgb\(255,255,255\) --export-height=1024 --export-width=758;; convert tmp.png -type GrayScale -depth 8 /opt/fhem/www/images/status.png' >/dev/null 2>&1 & </code><br>
  390. Inkscape might be needed because ImageMagick seems to have problems convertig SVG files with embedded icons. However a PNG file created by Inkscape is not in 8 bit greyscale so Imagemagick can be run after Inkscape to convert to 8 bit greyscale
  391. <li><b>ReplacementEncode</b></li>
  392. defines an encoding to apply to the replacement string, e.g. UTF-8
  393. </ul>
  394. </ul>
  395. =end html
  396. =cut