60_allergy.pm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. ##############################################
  2. # $Id: 60_allergy.pm 13588 2017-03-03 15:45:05Z moises $$$
  3. #
  4. # 60_allergy.pm
  5. #
  6. # 2017 Markus Moises < vorname at nachname . de >
  7. #
  8. # This module provides allergy forecast data
  9. #
  10. # http://forum.fhem.de/index.php/topic,37194.0.html
  11. #
  12. #
  13. ##############################################################################
  14. #
  15. # define <name> allergy <zipcode>
  16. #
  17. ##############################################################################
  18. package main;
  19. use strict;
  20. use warnings;
  21. use Time::Local;
  22. use Encode;
  23. use XML::Simple;
  24. use LWP::UserAgent;
  25. use HTTP::Request;
  26. use utf8;
  27. ##############################################################################
  28. sub allergy_Initialize($) {
  29. my ($hash) = @_;
  30. my $name = $hash->{NAME};
  31. $hash->{DefFn} = "allergy_Define";
  32. $hash->{UndefFn} = "allergy_Undefine";
  33. $hash->{GetFn} = "allergy_Get";
  34. $hash->{AttrList} = "disable:0,1 ".
  35. "ignoreList ".
  36. "updateIgnored:1 ".
  37. "updateEmpty:1 ".
  38. "levelsFormat ".
  39. "weekdaysFormat ".
  40. $readingFnAttributes;
  41. }
  42. sub allergy_Define($$$) {
  43. my ($hash, $def) = @_;
  44. my @a = split("[ \t][ \t]*", $def);
  45. my ($found, $dummy);
  46. return "syntax: define <name> allergy <zipcode>" if(int(@a) != 3 );
  47. my $name = $hash->{NAME};
  48. $hash->{helper}{ZIPCODE} = $a[2];
  49. $hash->{helper}{INTERVAL} = 10800;
  50. $hash->{ERROR} = 0;
  51. my $req = eval
  52. {
  53. require XML::Simple;
  54. XML::Simple->import();
  55. 1;
  56. };
  57. if($req)
  58. {
  59. InternalTimer( gettimeofday() + 60, "allergy_GetUpdate", $hash, 0);
  60. if (!defined($attr{$name}{stateFormat}))
  61. {
  62. $attr{$name}{stateFormat} = 'fc1_maximum';
  63. }
  64. }
  65. else
  66. {
  67. $hash->{STATE} = "XML::Simple is required!";
  68. $attr{$name}{disable} = "1";
  69. return undef;
  70. }
  71. $hash->{STATE} = "Initialized";
  72. return undef;
  73. }
  74. sub allergy_Undefine($$) {
  75. my ($hash, $arg) = @_;
  76. my $name = $hash->{NAME};
  77. RemoveInternalTimer($hash);
  78. fhem("deletereading $name fc.*", 1);
  79. return undef;
  80. }
  81. sub allergy_Get($@) {
  82. my ($hash, @a) = @_;
  83. my $command = $a[1];
  84. my $parameter = $a[2] if(defined($a[2]));
  85. my $name = $hash->{NAME};
  86. my $usage = "Unknown argument $command, choose one of data:noArg ";
  87. return $usage if $command eq '?';
  88. RemoveInternalTimer($hash);
  89. if(AttrVal($name, "disable", 0) eq 1) {
  90. $hash->{STATE} = "disabled";
  91. return "allergy $name is disabled. Aborting...";
  92. }
  93. allergy_GetUpdate($hash);
  94. return undef;
  95. }
  96. sub allergy_GetUpdate($) {
  97. my ($hash) = @_;
  98. my $name = $hash->{NAME};
  99. if(AttrVal($name, "disable", 0) eq 1) {
  100. $hash->{STATE} = "disabled";
  101. Log3 ($name, 2, "allergy $name is disabled, data update cancelled.");
  102. return undef;
  103. }
  104. my $url="http://www.allergie.hexal.de/pollenflug/xml-interface-neu/pollen_de_7tage.php?plz=".$hash->{helper}{ZIPCODE};
  105. Log3 ($name, 4, "Getting URL $url");
  106. HttpUtils_NonblockingGet({
  107. url => $url,
  108. noshutdown => 1,
  109. hash => $hash,
  110. type => 'allergydata',
  111. callback => \&allergy_Parse,
  112. });
  113. return undef;
  114. }
  115. sub allergy_Parse($$$)
  116. {
  117. my ($param, $err, $data) = @_;
  118. my $hash = $param->{hash};
  119. my $name = $hash->{NAME};
  120. if( $err )
  121. {
  122. Log3 $name, 1, "$name: URL error: ".$err;
  123. my $nextupdate = gettimeofday()+( (900*$hash->{ERROR}) + 90 );
  124. InternalTimer($nextupdate, "allergy_GetUpdate", $hash, 1);
  125. $hash->{STATE} = "error" if($hash->{ERROR} > 1);
  126. $hash->{ERROR} = $hash->{ERROR}+1;
  127. return undef;
  128. }
  129. $hash->{ERROR} = 0;
  130. Log3 $name, 5, "Received XML data ".$data;
  131. my $xml = new XML::Simple();
  132. my $xmldata = $xml->XMLin($data,forcearray => [qw( pollenbelastungen pollen )],keyattr => {pollen => 'name'});
  133. my @wdays = split(',',AttrVal($hash->{NAME}, "weekdaysFormat", "Sun,Mon,Tue,Wed,Thu,Fri,Sat" ));
  134. my @levels = split(',',AttrVal($hash->{NAME}, "levelsFormat", "-,low,moderate,high,extreme" ));
  135. readingsBeginUpdate($hash); # Start update readings
  136. my $city = $xmldata->{'pollendaten'}->{'ort'};
  137. readingsBulkUpdate($hash, "city", allergy_utf8clean($city));
  138. Log3 $name, 4, "Received data for postcode ".$xmldata->{'pollendaten'}->{'plz'};
  139. foreach my $day (@{$xmldata->{'pollendaten'}{'pollenbelastungen'}})
  140. {
  141. my $daycode = $day->{'tag'}+1;
  142. my @daydata = $day->{'pollen'};
  143. my $daymax = 0;
  144. my $pollenkey='';
  145. my $pollenvalue='';
  146. my $pollendata=0;
  147. my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time+($day->{'tag'}*86400));
  148. readingsBulkUpdate($hash, "fc".$daycode."_day_of_week", $wdays[$wday]);
  149. foreach my $pollenhash (@daydata)
  150. {
  151. while(($pollenkey, $pollenvalue) = each(%$pollenhash))
  152. {
  153. $pollenkey = allergy_utf8clean($pollenkey);
  154. $pollendata = $pollenvalue->{'belastung'};
  155. if (( AttrVal($hash->{NAME}, "updateEmpty", 0 ) gt 0 or $pollendata gt 0) and ( AttrVal($hash->{NAME}, "updateIgnored", 0 ) gt 0 or ( index(AttrVal($hash->{NAME}, "ignoreList", ""), $pollenkey ) == -1 )))
  156. {
  157. readingsBulkUpdate($hash, "fc".$daycode."_".$pollenkey, $levels[$pollendata]);
  158. $daymax = $pollendata if($pollendata gt $daymax);
  159. Log3 $name, 4, "Received pollen level for ".$pollenkey.": day".$daycode." level ".$pollendata;
  160. }
  161. else
  162. {
  163. fhem( "deletereading $name fc".$daycode."_".$pollenkey, 1 );
  164. Log3 $name, 5, "Received pollen level for ".$pollenkey.": day".$daycode." level ".$pollendata." (ignored)";
  165. }
  166. }
  167. }
  168. readingsBulkUpdate($hash, "fc".$daycode."_maximum", $levels[$daymax]);
  169. }
  170. readingsEndUpdate($hash, 1);
  171. $hash->{UPDATED} = FmtDateTime(time());
  172. my $nextupdate = gettimeofday()+$hash->{helper}{INTERVAL};
  173. InternalTimer($nextupdate, "allergy_GetUpdate", $hash, 1);
  174. return undef;
  175. }
  176. sub allergy_utf8clean($) {
  177. my ($string) = @_;
  178. my $log = "";
  179. if($string !~ m/^[A-Za-z\d_\.-]+$/)
  180. {
  181. $log .= $string."(standard) ";
  182. $string =~ s/Ä/Ae/g;
  183. $string =~ s/Ö/Oe/g;
  184. $string =~ s/Ü/Ue/g;
  185. $string =~ s/ä/ae/g;
  186. $string =~ s/ö/oe/g;
  187. $string =~ s/ü/ue/g;
  188. $string =~ s/ß/ss/g;
  189. }
  190. if($string !~ m/^[A-Za-z\d_\.-]+$/)
  191. {
  192. $log .= $string."(single) ";
  193. $string =~ s/Ä/Ae/g;
  194. $string =~ s/Ö/Oe/g;
  195. $string =~ s/Ü/Ue/g;
  196. $string =~ s/ä/ae/g;
  197. $string =~ s/ö/oe/g;
  198. $string =~ s/ü/ue/g;
  199. $string =~ s/ß/ss/g;
  200. }
  201. if($string !~ m/^[A-Za-z\d_\.-]+$/)
  202. {
  203. $log .= $string."(double) ";
  204. $string =~ s/Ä/Ae/g;
  205. $string =~ s/Ö/Oe/g;
  206. $string =~ s/Ü/Ue/g;
  207. $string =~ s/ä/ae/g;
  208. $string =~ s/ö/oe/g;
  209. $string =~ s/ü/ue/g;
  210. $string =~ s/ß/ss/g;
  211. }
  212. if($string !~ m/^[A-Za-z\d_\.-]+$/)
  213. {
  214. $log .= $string."(unknown)";
  215. #$string =~ s/[^!-~\s]//g;
  216. $string =~ s/[^A-Za-z\d_\.-]//g;
  217. }
  218. Log3 "utf8clean", 5, "Cleaned $string // $log" if($log ne "");
  219. return $string;
  220. }
  221. ##########################
  222. 1;
  223. =pod
  224. =item device
  225. =item summary Allergy forecast data for Germany
  226. =begin html
  227. <a name="allergy"></a>
  228. <h3>allergy</h3>
  229. <ul>
  230. This modul provides allergy forecast data for Germany.<br/>
  231. It requires the Perl module XML::Simple to be installed
  232. <br/><br/>
  233. <b>Define</b>
  234. <ul>
  235. <code>define &lt;name&gt; allergy &lt;zipcode&gt;</code>
  236. <br>
  237. Example: <code>define allergydata allergy 12345</code>
  238. <br>&nbsp;
  239. <li><code>zipcode</code>
  240. <br>
  241. German zipcode
  242. </li><br>
  243. </ul>
  244. <br>
  245. <b>Get</b>
  246. <ul>
  247. <li><code>data</code>
  248. <br>
  249. Manually trigger data update
  250. </li><br>
  251. </ul>
  252. <br>
  253. <b>Readings</b>
  254. <ul>
  255. <li><code>city</code>
  256. <br>
  257. Name of the city the forecast is read for
  258. </li><br>
  259. <li><code>fc<i>n</i>_total</code>
  260. <br>
  261. Daily maximum levels for all allergens that are not being ignored due to <i>ignoreList</i><br/>
  262. </li><br>
  263. <li><code>fc<i>n</i>_day_of_week</code>
  264. <br>
  265. Weekday, can be localized through <i>weekdaysFormat</i><br/>
  266. </li><br>
  267. <li><code>fc<i>n</i>_<i>allergen</i></code>
  268. <br>
  269. Daily levels for all allergens that are not being ignored due to <i>ignoreList</i>
  270. </li><br>
  271. </ul>
  272. <br>
  273. <b>Attributes</b>
  274. <ul>
  275. <li><code>ignoreList</code>
  276. <br>
  277. Comma-separated list of allergen names that are to be ignored during updates and for cumulated day levels calculation
  278. </li><br>
  279. <li><code>updateEmpty</code>
  280. <br>
  281. Also update (and keep) level readings for inactive allergens that are otherwise removed
  282. </li><br>
  283. <li><code>updateIgnored</code>
  284. <br>
  285. Also update (and keep) level readings for ignored allergens that are otherwise removed
  286. </li><br>
  287. <li><code>levelsFormat</code>
  288. <br>
  289. Localize levels by adding them comma separated (default: -,low,moderate,high,extreme)
  290. </li><br>
  291. <li><code>weekdaysFormat</code>
  292. <br>
  293. Localize Weekdays by adding them comma separated (default: Sun,Mon,Tue,Wed,Thu,Fr,Sat)
  294. </li><br>
  295. </ul>
  296. </ul>
  297. =end html
  298. =begin html_DE
  299. <a name="allergy"></a>
  300. <h3>allergy</h3>
  301. <ul>
  302. <br>Dieses Modul prognostiziert Allergie Daten für Deutschland.</br>
  303. Es erfordert dass das Perlmodul XML:: Simple installiert ist.
  304. <br/><br/>
  305. <b>Define</b>
  306. <ul>
  307. <code>define &lt;name&gt; allergy &lt;Postleitzahl&gt;</code>
  308. <br>
  309. Beispiel: <code>define allergydata allergy 12345</code>
  310. <br><br>
  311. <li><code>Postleitzahl</code>
  312. <br>
  313. Deutsche Postleitzahl
  314. </li><br>
  315. </ul>
  316. <br>
  317. <b>Get</b>
  318. <ul>
  319. <li><code>data</code>
  320. <br>
  321. Manuelles Datenupdate
  322. </li><br>
  323. </ul>
  324. <br>
  325. <b>Readings</b>
  326. <ul>
  327. <li><code>city</code>
  328. <br>
  329. Name der Stadt, für die Prognosen gelesen werden.
  330. </li><br>
  331. <li><code>fc<i>n</i>_total</code>
  332. <br>
  333. Täglicher Höchstwerte für alle Allergene, die nicht aufgrund der Ignoreliste <i>(attr ignoreList)</i> ignoriert werden<br/>
  334. </li><br>
  335. <li><code>fc<i>n</i>_day_of_week</code>
  336. <br>
  337. Wochentag, kann durch <i>weekdaysFormat</i> lokalisiert werden.<br/>
  338. </li><br>
  339. <li><code>fc<i>n</i>_<i>allergen</i></code>
  340. <br>
  341. Tägliche Werte für alle Allergene, die nicht aufgrund der Ignoreliste <i>(attr ignoreList)</i> ignoriert werden.
  342. </li><br>
  343. </ul>
  344. <br>
  345. <b>Attribute</b>
  346. <ul>
  347. <li><code>ignoreList</code>
  348. <br>
  349. Kommagetrennte Liste von Allergen-Namen, die bei der Aktualisierung ignoriert werden sollen.
  350. <br>
  351. </li><br>
  352. <li><code>updateEmpty (Standard: 0|1)</code>
  353. <br>
  354. Aktualisierung von Allergenen.
  355. <code> <br>
  356. 0 = nur Allergene mit Belastung.
  357. <br>
  358. 1 = auch Allergene die keine Belastung haben.
  359. </code>
  360. </li><br>
  361. <li><code>updateIgnored (1)</code>
  362. <br>
  363. Aktualisierung von Allergenen, die sonst durch die ignoreList entfernt werden.
  364. </li><br>
  365. <li><code>levelsFormat (Standard: -,low,moderate,high,extreme)</code>
  366. <br>
  367. Lokalisierte Levels, durch Kommas getrennt.
  368. </li><br>
  369. <li><code>weekdaysFormat (Standard: Sun,Mon,Tue,Wed,Thu,Fri,Sat)</code>
  370. <br>
  371. Lokalisierte Wochentage, durch Kommas getrennt.
  372. </li><br>
  373. </ul>
  374. </ul>
  375. =end html_DE
  376. =cut