46_Aqicn.pm 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  1. ###############################################################################
  2. #
  3. # Developed with Kate
  4. #
  5. # (c) 2017 Copyright: Marko Oldenburg (leongaultier at gmail dot com)
  6. # All rights reserved
  7. #
  8. # Special thanks goes to comitters:
  9. # - maddhin(FHEM Forum) Thanks for Readings and Commandref
  10. #
  11. #
  12. # This script is free software; you can redistribute it and/or modify
  13. # it under the terms of the GNU General Public License as published by
  14. # the Free Software Foundation; either version 2 of the License, or
  15. # any later version.
  16. #
  17. # The GNU General Public License can be found at
  18. # http://www.gnu.org/copyleft/gpl.html.
  19. # A copy is found in the textfile GPL.txt and important notices to the license
  20. # from the author is found in LICENSE.txt distributed with these scripts.
  21. #
  22. # This script is distributed in the hope that it will be useful,
  23. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  24. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  25. # GNU General Public License for more details.
  26. #
  27. #
  28. # $Id: 46_Aqicn.pm 17701 2018-11-07 12:45:10Z CoolTux $
  29. #
  30. ###############################################################################
  31. ##
  32. ##
  33. ## Das JSON Modul immer in einem eval aufrufen
  34. # $data = eval{decode_json($data)};
  35. #
  36. # if($@){
  37. # Log3($SELF, 2, "$TYPE ($SELF) - error while request: $@");
  38. #
  39. # readingsSingleUpdate($hash, "state", "error", 1);
  40. #
  41. # return;
  42. # }
  43. #
  44. #######
  45. #######
  46. #
  47. ##
  48. ##
  49. package main;
  50. use strict;
  51. use warnings;
  52. my $version = "0.4.2";
  53. sub Aqicn_Initialize($) {
  54. my ($hash) = @_;
  55. # Consumer
  56. $hash->{GetFn} = "Aqicn::Get";
  57. $hash->{DefFn} = "Aqicn::Define";
  58. $hash->{UndefFn} = "Aqicn::Undef";
  59. $hash->{NotifyFn} = "Aqicn::Notify";
  60. $hash->{AttrFn} = "Aqicn::Attr";
  61. $hash->{AttrList} =
  62. "interval " . "disable:1 " . "language:de,en " . $readingFnAttributes;
  63. foreach my $d ( sort keys %{ $modules{Aqicn}{defptr} } ) {
  64. my $hash = $modules{Aqicn}{defptr}{$d};
  65. $hash->{VERSION} = $version;
  66. }
  67. }
  68. package Aqicn;
  69. use strict;
  70. use warnings;
  71. use HttpUtils;
  72. use GPUtils qw(GP_Import)
  73. ; # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt
  74. my $missingModul = "";
  75. eval "use Encode qw(encode encode_utf8 decode_utf8);1"
  76. or $missingModul .= "Encode ";
  77. eval "use JSON;1" or $missingModul .= "JSON ";
  78. ## Import der FHEM Funktionen
  79. BEGIN {
  80. GP_Import(
  81. qw(readingsSingleUpdate
  82. readingsBulkUpdate
  83. readingsBulkUpdateIfChanged
  84. readingsBeginUpdate
  85. readingsEndUpdate
  86. defs
  87. modules
  88. Log3
  89. CommandAttr
  90. AttrVal
  91. IsDisabled
  92. deviceEvents
  93. init_done
  94. gettimeofday
  95. InternalTimer
  96. RemoveInternalTimer
  97. urlEncode
  98. HttpUtils_NonblockingGet
  99. makeDeviceName
  100. asyncOutput)
  101. );
  102. }
  103. ### Air Quality Index scale
  104. my %AQIS = (
  105. 1 => {
  106. 'i18nde' => 'Gut',
  107. 'i18nen' => 'Good',
  108. 'bgcolor' => '#009966',
  109. 'font color' => '#FFFFFF'
  110. },
  111. 2 => {
  112. 'i18nde' => 'Moderat',
  113. 'i18nen' => 'Moderate',
  114. 'bgcolor' => '#ffde33',
  115. 'font color' => '#000000'
  116. },
  117. 3 => {
  118. 'i18nde' => 'Ungesund für empfindliche Personengruppen',
  119. 'i18nen' => 'Unhealthy for Sensitive Groups',
  120. 'bgcolor' => '#ff9933',
  121. 'font color' => '#000000'
  122. },
  123. 4 => {
  124. 'i18nde' => 'Ungesund',
  125. 'i18nen' => 'Unhealthy',
  126. 'bgcolor' => '#cc0033',
  127. 'font color' => '#FFFFFF'
  128. },
  129. 5 => {
  130. 'i18nde' => 'Sehr ungesund',
  131. 'i18nen' => 'Very Unhealthy',
  132. 'bgcolor' => '#660099',
  133. 'font color' => '#FFFFFF'
  134. },
  135. 6 => {
  136. 'i18nde' => 'Gefährlich',
  137. 'i18nen' => 'Hazardous',
  138. 'bgcolor' => '#7e0023',
  139. 'font color' => '#FFFFFF'
  140. },
  141. );
  142. my %paths = (
  143. 'statussoe' => 'system_status/soe',
  144. 'aggregates' => 'meters/aggregates',
  145. 'siteinfo' => 'site_info',
  146. 'sitemaster' => 'sitemaster',
  147. 'powerwalls' => 'powerwalls',
  148. 'registration' => 'customer/registration',
  149. 'status' => 'status'
  150. );
  151. sub Define($$) {
  152. my ( $hash, $def ) = @_;
  153. my @a = split( "[ \t][ \t]*", $def );
  154. if ( $a[2] =~ /^token=/ ) {
  155. $a[2] =~ m/token=([^\s]*)/;
  156. $hash->{TOKEN} = $1;
  157. }
  158. else {
  159. $hash->{UID} = $a[2];
  160. }
  161. return "Cannot define a Aqicn device. Perl modul $missingModul is missing."
  162. if ($missingModul);
  163. return "too few parameters: define <name> Aqicn <OPTION-PARAMETER>"
  164. if ( @a != 3 );
  165. return "too few parameters: define <name> Aqicn token=<TOKEN-KEY>"
  166. if ( not defined( $hash->{TOKEN} )
  167. and not defined( $modules{Aqicn}{defptr}{TOKEN} ) );
  168. return "too few parameters: define <name> Aqicn <STATION-UID>"
  169. if ( not defined( $hash->{UID} )
  170. and defined( $modules{Aqicn}{defptr}{TOKEN} ) );
  171. my $name = $a[0];
  172. $hash->{VERSION} = $version;
  173. $hash->{NOTIFYDEV} = "global";
  174. if ( defined( $hash->{TOKEN} ) ) {
  175. return
  176. "there is already a Aqicn Head Device, did you want to define a Aqicn station use: define <name> Aqicn <STATION-UID>"
  177. if ( $modules{Aqicn}{defptr}{TOKEN} );
  178. $hash->{HOST} = 'api.waqi.info';
  179. CommandAttr( undef, $name . ' room AQICN' )
  180. if ( AttrVal( $name, 'room', 'none' ) eq 'none' );
  181. readingsSingleUpdate( $hash, "state", "ready for search", 1 );
  182. Log3 $name, 3,
  183. "Aqicn ($name) - defined Aqicn Head Device with API-Key $hash->{TOKEN}";
  184. $modules{Aqicn}{defptr}{TOKEN} = $hash;
  185. }
  186. elsif ( defined( $hash->{UID} ) ) {
  187. CommandAttr( undef, $name . ' room AQICN' )
  188. if ( AttrVal( $name, 'room', 'none' ) eq 'none' );
  189. $hash->{INTERVAL} = 3600;
  190. $hash->{HEADDEVICE} = $modules{Aqicn}{defptr}{TOKEN}->{NAME};
  191. readingsSingleUpdate( $hash, "state", "initialized", 1 );
  192. Log3 $name, 3,
  193. "Aqicn ($name) - defined Aqicn Station Device with Station UID $hash->{UID}";
  194. $modules{Aqicn}{defptr}{UID} = $hash;
  195. }
  196. return undef;
  197. }
  198. sub Undef($$) {
  199. my ( $hash, $arg ) = @_;
  200. my $name = $hash->{NAME};
  201. if ( defined( $modules{Aqicn}{defptr}{TOKEN} ) and $hash->{TOKEN} ) {
  202. return
  203. "there is a Aqicn Station Device present, please delete all Station Device first"
  204. unless ( not defined( $modules{Aqicn}{defptr}{UID} ) );
  205. delete $modules{Aqicn}{defptr}{TOKEN};
  206. }
  207. elsif ( defined( $modules{Aqicn}{defptr}{UID} ) and $hash->{UID} ) {
  208. delete $modules{Aqicn}{defptr}{UID};
  209. }
  210. RemoveInternalTimer($hash);
  211. Log3 $name, 3, "Aqicn ($name) - Device $name deleted";
  212. return undef;
  213. }
  214. sub Attr(@) {
  215. my ( $cmd, $name, $attrName, $attrVal ) = @_;
  216. my $hash = $defs{$name};
  217. if ( $attrName eq "disable" ) {
  218. if ( $cmd eq "set" and $attrVal eq "1" ) {
  219. RemoveInternalTimer($hash);
  220. readingsSingleUpdate( $hash, "state", "disabled", 1 );
  221. Log3 $name, 3, "Aqicn ($name) - disabled";
  222. }
  223. elsif ( $cmd eq "del" ) {
  224. Log3 $name, 3, "Aqicn ($name) - enabled";
  225. }
  226. }
  227. if ( $attrName eq "disabledForIntervals" ) {
  228. if ( $cmd eq "set" ) {
  229. return
  230. "check disabledForIntervals Syntax HH:MM-HH:MM or 'HH:MM-HH:MM HH:MM-HH:MM ...'"
  231. unless ( $attrVal =~ /^((\d{2}:\d{2})-(\d{2}:\d{2})\s?)+$/ );
  232. Log3 $name, 3, "Aqicn ($name) - disabledForIntervals";
  233. readingsSingleUpdate( $hash, "state", "disabled", 1 );
  234. }
  235. elsif ( $cmd eq "del" ) {
  236. Log3 $name, 3, "Aqicn ($name) - enabled";
  237. readingsSingleUpdate( $hash, "state", "active", 1 );
  238. }
  239. }
  240. if ( $attrName eq "interval" ) {
  241. if ( $cmd eq "set" ) {
  242. if ( $attrVal < 30 ) {
  243. Log3 $name, 3,
  244. "Aqicn ($name) - interval too small, please use something >= 30 (sec), default is 300 (sec)";
  245. return
  246. "interval too small, please use something >= 30 (sec), default is 300 (sec)";
  247. }
  248. else {
  249. RemoveInternalTimer($hash);
  250. $hash->{INTERVAL} = $attrVal;
  251. Log3 $name, 3, "Aqicn ($name) - set interval to $attrVal";
  252. Timer_GetData($hash);
  253. }
  254. }
  255. elsif ( $cmd eq "del" ) {
  256. RemoveInternalTimer($hash);
  257. $hash->{INTERVAL} = 300;
  258. Log3 $name, 3, "Aqicn ($name) - set interval to default";
  259. Timer_GetData($hash);
  260. }
  261. }
  262. return undef;
  263. }
  264. sub Notify($$) {
  265. my ( $hash, $dev ) = @_;
  266. my $name = $hash->{NAME};
  267. return if ( IsDisabled($name) );
  268. my $devname = $dev->{NAME};
  269. my $devtype = $dev->{TYPE};
  270. my $events = deviceEvents( $dev, 1 );
  271. return if ( !$events );
  272. Timer_GetData($hash)
  273. if (
  274. (
  275. grep /^INITIALIZED$/,
  276. @{$events}
  277. or grep /^REREADCFG$/,
  278. @{$events}
  279. or grep /^MODIFIED.$name$/,
  280. @{$events}
  281. or ( grep /^DEFINED.$name$/, @{$events} and $init_done )
  282. )
  283. and defined( $hash->{UID} )
  284. );
  285. return;
  286. }
  287. sub Get($$@) {
  288. my ( $hash, $name, @aa ) = @_;
  289. my ( $cmd, @args ) = @aa;
  290. if ( $cmd eq 'update' ) {
  291. GetData($hash);
  292. return undef;
  293. }
  294. elsif ( $cmd eq 'stationSearchByCity' ) {
  295. return "usage: $cmd" if ( @args == 0 );
  296. my $city = join( " ", @args );
  297. my $ret;
  298. $ret = GetData( $hash, $city );
  299. return $ret;
  300. }
  301. else {
  302. my $list = '';
  303. $list .= 'update:noArg' if ( defined( $hash->{UID} ) );
  304. $list .= 'stationSearchByCity' if ( defined( $hash->{TOKEN} ) );
  305. return "Unknown argument $cmd, choose one of $list";
  306. }
  307. }
  308. sub Timer_GetData($) {
  309. my $hash = shift;
  310. my $name = $hash->{NAME};
  311. if ( not IsDisabled($name) ) {
  312. GetData($hash);
  313. }
  314. else {
  315. readingsSingleUpdate( $hash, 'state', 'disabled', 1 );
  316. }
  317. InternalTimer( gettimeofday() + $hash->{INTERVAL},
  318. 'Aqicn::Timer_GetData', $hash );
  319. Log3 $name, 4, "Aqicn ($name) - Call InternalTimer Timer_GetData";
  320. }
  321. sub GetData($;$) {
  322. my ( $hash, $cityName ) = @_;
  323. my $name = $hash->{NAME};
  324. my $host = $modules{Aqicn}{defptr}{TOKEN}->{HOST};
  325. my $token = $modules{Aqicn}{defptr}{TOKEN}->{TOKEN};
  326. my $uri;
  327. if ( $hash->{UID} ) {
  328. my $uid = $hash->{UID};
  329. $uri = $host . '/feed/@' . $hash->{UID} . '/?token=' . $token;
  330. readingsSingleUpdate( $hash, 'state', 'fetch data', 1 );
  331. }
  332. else {
  333. $uri =
  334. $host
  335. . '/search/?token='
  336. . $token
  337. . '&keyword='
  338. . urlEncode($cityName);
  339. }
  340. my $param = {
  341. url => "https://" . $uri,
  342. timeout => 5,
  343. method => 'GET',
  344. hash => $hash,
  345. doTrigger => 1,
  346. callback => \&ErrorHandling,
  347. };
  348. $param->{cl} = $hash->{CL}
  349. if ( $hash->{TOKEN} and ref( $hash->{CL} ) eq 'HASH' );
  350. HttpUtils_NonblockingGet($param);
  351. Log3 $name, 4, "Aqicn ($name) - Send with URI: https://$uri";
  352. }
  353. sub ErrorHandling($$$) {
  354. my ( $param, $err, $data ) = @_;
  355. my $hash = $param->{hash};
  356. my $name = $hash->{NAME};
  357. Log3 $name, 4, "Aqicn ($name) - Recieve JSON data: $data";
  358. #Log3 $name, 3, "Aqicn ($name) - Recieve HTTP Code: $param->{code}";
  359. #Log3 $name, 3, "Aqicn ($name) - Recieve Error: $err";
  360. ### Begin Error Handling
  361. if ( defined($err) ) {
  362. if ( $err ne "" ) {
  363. if ( $param->{cl} && $param->{cl}{canAsyncOutput} ) {
  364. asyncOutput( $param->{cl}, "Request Error: $err\r\n" );
  365. }
  366. readingsBeginUpdate($hash);
  367. readingsBulkUpdate( $hash, 'state', $err, 1 );
  368. readingsBulkUpdate( $hash, 'lastRequestError', $err, 1 );
  369. readingsEndUpdate( $hash, 1 );
  370. Log3 $name, 3, "Aqicn ($name) - RequestERROR: $err";
  371. return;
  372. }
  373. }
  374. if ( $data eq "" and exists( $param->{code} ) && $param->{code} ne 200 ) {
  375. #if( $param->{cl} && $param->{cl}{canAsyncOutput} ) {
  376. # asyncOutput( $param->{cl}, "Request Error: $param->{code}\r\n" );
  377. #}
  378. readingsBeginUpdate($hash);
  379. readingsBulkUpdate( $hash, 'state', $param->{code}, 1 );
  380. readingsBulkUpdate( $hash, 'lastRequestError', $param->{code}, 1 );
  381. Log3 $name, 3, "Aqicn ($name) - RequestERROR: " . $param->{code};
  382. readingsEndUpdate( $hash, 1 );
  383. Log3 $name, 5,
  384. "Aqicn ($name) - RequestERROR: received http code "
  385. . $param->{code}
  386. . " without any data after requesting";
  387. return;
  388. }
  389. if ( ( $data =~ /Error/i ) and exists( $param->{code} ) ) {
  390. #if( $param->{cl} && $param->{cl}{canAsyncOutput} ) {
  391. # asyncOutput( $param->{cl}, "Request Error: $param->{code}\r\n" );
  392. #}
  393. readingsBeginUpdate($hash);
  394. readingsBulkUpdate( $hash, 'state', $param->{code}, 1 );
  395. readingsBulkUpdate( $hash, "lastRequestError", $param->{code}, 1 );
  396. readingsEndUpdate( $hash, 1 );
  397. Log3 $name, 3,
  398. "Aqicn ($name) - statusRequestERROR: http error " . $param->{code};
  399. return;
  400. ### End Error Handling
  401. }
  402. Log3 $name, 4, "Aqicn ($name) - Recieve JSON data: $data";
  403. ResponseProcessing( $hash, $data, $param );
  404. }
  405. sub ResponseProcessing($$$) {
  406. my ( $hash, $json, $param ) = @_;
  407. my $name = $hash->{NAME};
  408. my $decode_json;
  409. my $readings;
  410. $decode_json = eval { decode_json($json) };
  411. if ($@) {
  412. Log3 $name, 4, "Aqicn ($name) - error while request: $@";
  413. readingsBeginUpdate($hash);
  414. readingsBulkUpdate( $hash, 'JSON_Error', $@ );
  415. readingsBulkUpdate( $hash, 'httpCode', $param->{code} );
  416. readingsBulkUpdate( $hash, 'state', 'JSON error' );
  417. readingsEndUpdate( $hash, 1 );
  418. return;
  419. }
  420. #### Verarbeitung der Readings zum passenden
  421. if ( $hash->{TOKEN} ) {
  422. ReadingsProcessing_SearchStationResponse( $decode_json, $param );
  423. readingsSingleUpdate( $hash, 'state', 'search finished', 1 );
  424. return;
  425. }
  426. elsif ( $hash->{UID} ) {
  427. $readings = ReadingsProcessing_AqiResponse($decode_json);
  428. }
  429. WriteReadings( $hash, $readings );
  430. }
  431. sub WriteReadings($$) {
  432. my ( $hash, $readings ) = @_;
  433. my $name = $hash->{NAME};
  434. Log3 $name, 4, "Aqicn ($name) - Write Readings";
  435. readingsBeginUpdate($hash);
  436. while ( my ( $r, $v ) = each %{$readings} ) {
  437. readingsBulkUpdate( $hash, $r, $v );
  438. }
  439. if ( defined( $readings->{'PM2.5-AQI'} ) ) {
  440. readingsBulkUpdateIfChanged(
  441. $hash,
  442. 'htmlStyle',
  443. '<div style="background-color: '
  444. . $AQIS{ AirPollutionLevel( $readings->{'PM2.5-AQI'} ) }
  445. {'bgcolor'}
  446. . ';"><font color="'
  447. . $AQIS{ AirPollutionLevel( $readings->{'PM2.5-AQI'} ) }
  448. {'font color'} . '">'
  449. . (
  450. (
  451. (
  452. AttrVal( 'global', 'language', 'none' ) eq 'DE'
  453. or AttrVal( $name, 'language', 'none' ) eq 'de'
  454. )
  455. and AttrVal( $name, 'language', 'none' ) ne 'en'
  456. )
  457. ? "$AQIS{AirPollutionLevel($readings->{'PM2.5-AQI'})}{'i18nde'}: $readings->{'PM2.5-AQI'} "
  458. : " $AQIS{AirPollutionLevel($readings->{'PM2.5-AQI'})}{'i18nen'}: $readings->{'PM2.5-AQI'}"
  459. )
  460. . '</div>'
  461. );
  462. readingsBulkUpdateIfChanged(
  463. $hash, 'state',
  464. (
  465. (
  466. (
  467. AttrVal( 'global', 'language', 'none' ) eq 'DE'
  468. or AttrVal( $name, 'language', 'none' ) eq 'de'
  469. )
  470. and AttrVal( $name, 'language', 'none' ) ne 'en'
  471. )
  472. ? "$AQIS{AirPollutionLevel($readings->{'PM2.5-AQI'})}{'i18nde'}: $readings->{'PM2.5-AQI'}"
  473. : "$AQIS{AirPollutionLevel($readings->{'PM2.5-AQI'})}{'i18nen'}: $readings->{'PM2.5-AQI'}"
  474. )
  475. );
  476. readingsBulkUpdateIfChanged(
  477. $hash, 'APL',
  478. (
  479. (
  480. (
  481. AttrVal( 'global', 'language', 'none' ) eq 'DE'
  482. or AttrVal( $name, 'language', 'none' ) eq 'de'
  483. )
  484. and AttrVal( $name, 'language', 'none' ) ne 'en'
  485. )
  486. ? "$AQIS{AirPollutionLevel($readings->{'PM2.5-AQI'})}{'i18nde'}"
  487. : "$AQIS{AirPollutionLevel($readings->{'PM2.5-AQI'})}{'i18nen'}"
  488. )
  489. );
  490. readingsBulkUpdateIfChanged(
  491. $hash,
  492. 'healthImplications',
  493. HealthImplications(
  494. $hash, AirPollutionLevel( $readings->{'PM2.5-AQI'} )
  495. )
  496. );
  497. }
  498. else {
  499. readingsBulkUpdateIfChanged(
  500. $hash,
  501. 'htmlStyle',
  502. '<div style="background-color: '
  503. . $AQIS{ AirPollutionLevel( $readings->{'AQI'} ) }{'bgcolor'}
  504. . ';"><font color="'
  505. . $AQIS{ AirPollutionLevel( $readings->{'PM2.5-AQI'} ) }
  506. {'font color'} . '">'
  507. . (
  508. (
  509. (
  510. AttrVal( 'global', 'language', 'none' ) eq 'DE'
  511. or AttrVal( $name, 'language', 'none' ) eq 'de'
  512. )
  513. and AttrVal( $name, 'language', 'none' ) ne 'en'
  514. )
  515. ? "$AQIS{AirPollutionLevel($readings->{'AQI'})}{'i18nde'}: $readings->{'AQI'} "
  516. : " $AQIS{AirPollutionLevel($readings->{'AQI'})}{'i18nen'}: $readings->{'AQI'}"
  517. )
  518. . '</div>'
  519. );
  520. readingsBulkUpdateIfChanged(
  521. $hash, 'state',
  522. (
  523. (
  524. (
  525. AttrVal( 'global', 'language', 'none' ) eq 'DE'
  526. or AttrVal( $name, 'language', 'none' ) eq 'de'
  527. )
  528. and AttrVal( $name, 'language', 'none' ) ne 'en'
  529. )
  530. ? "$AQIS{AirPollutionLevel($readings->{'AQI'})}{'i18nde'}: $readings->{'AQI'}"
  531. : "$AQIS{AirPollutionLevel($readings->{'AQI'})}{'i18nen'}: $readings->{'AQI'}"
  532. )
  533. );
  534. readingsBulkUpdateIfChanged(
  535. $hash, 'APL',
  536. (
  537. (
  538. (
  539. AttrVal( 'global', 'language', 'none' ) eq 'DE'
  540. or AttrVal( $name, 'language', 'none' ) eq 'de'
  541. )
  542. and AttrVal( $name, 'language', 'none' ) ne 'en'
  543. )
  544. ? "$AQIS{AirPollutionLevel($readings->{'AQI'})}{'i18nde'}"
  545. : "$AQIS{AirPollutionLevel($readings->{'AQI'})}{'i18nen'}"
  546. )
  547. );
  548. readingsBulkUpdateIfChanged(
  549. $hash,
  550. 'healthImplications',
  551. HealthImplications(
  552. $hash, AirPollutionLevel( $readings->{'AQI'} )
  553. )
  554. );
  555. }
  556. readingsEndUpdate( $hash, 1 );
  557. }
  558. #####
  559. #####
  560. ## my little Helper
  561. sub ReadingsProcessing_SearchStationResponse($$) {
  562. my ( $decode_json, $param ) = @_;
  563. if ( $param->{cl} and $param->{cl}->{TYPE} eq 'FHEMWEB' ) {
  564. my $ret = '<html><table><tr><td>';
  565. $ret .= '<table class="block wide">';
  566. $ret .= '<tr class="even">';
  567. $ret .= "<td><b>City</b></td>";
  568. $ret .= "<td><b>Last Update Time</b></td>";
  569. $ret .= "<td><b>Latitude</b></td>";
  570. $ret .= "<td><b>Longitude</b></td>";
  571. $ret .= "<td></td>";
  572. $ret .= '</tr>';
  573. if ( ref( $decode_json->{data} ) eq "ARRAY"
  574. and scalar( @{ $decode_json->{data} } ) > 0 )
  575. {
  576. my $linecount = 1;
  577. foreach my $dataset ( @{ $decode_json->{data} } ) {
  578. if ( $linecount % 2 == 0 ) {
  579. $ret .= '<tr class="even">';
  580. }
  581. else {
  582. $ret .= '<tr class="odd">';
  583. }
  584. $dataset->{station}{name} =~ s/'//g;
  585. $ret .=
  586. "<td>" . encode_utf8( $dataset->{station}{name} ) . "</td>";
  587. $ret .= "<td>$dataset->{'time'}{stime}</td>";
  588. $ret .= "<td>$dataset->{station}{geo}[0]</td>";
  589. $ret .= "<td>$dataset->{station}{geo}[1]</td>";
  590. ###### create Links
  591. my $aHref;
  592. # create Google Map Link
  593. $aHref =
  594. "<a target=\"_blank\" href=\"https://www.google.de/maps/search/"
  595. . $dataset->{station}{geo}[0] . "+"
  596. . $dataset->{station}{geo}[1]
  597. . "\">Station on Google Maps</a>";
  598. $ret .= "<td>" . $aHref . "</td>";
  599. # create define Link
  600. $aHref =
  601. "<a href=\""
  602. . $::FW_httpheader->{host}
  603. . "/fhem?cmd=define+"
  604. . makeDeviceName( $dataset->{station}{name} )
  605. . "+Aqicn+"
  606. . $dataset->{uid}
  607. . $::FW_CSRF
  608. . "\">Create Station Device</a>";
  609. $ret .= "<td>" . $aHref . "</td>";
  610. $ret .= '</tr>';
  611. $linecount++;
  612. }
  613. }
  614. $ret .= '</table></td></tr>';
  615. $ret .= '</table></html>';
  616. asyncOutput( $param->{cl}, $ret )
  617. if ( $param->{cl} and $param->{cl}{canAsyncOutput} );
  618. return;
  619. }
  620. elsif ( $param->{cl} and $param->{cl}->{TYPE} eq 'telnet' ) {
  621. my $ret = '';
  622. foreach my $dataset ( @{ $decode_json->{data} } ) {
  623. $ret .=
  624. encode_utf8( $dataset->{station}{name} )
  625. . "| $dataset->{'time'}{stime} | $dataset->{station}{geo}[0] | $dataset->{station}{geo}[1] | define "
  626. . makeDeviceName( $dataset->{station}{name} )
  627. . " Aqicn $dataset->{uid}\r\n";
  628. }
  629. asyncOutput( $param->{cl}, $ret )
  630. if ( $param->{cl} && $param->{cl}{canAsyncOutput} );
  631. return;
  632. }
  633. }
  634. sub ReadingsProcessing_AqiResponse($) {
  635. my ($decode_json) = @_;
  636. my %readings;
  637. if ( ref( $decode_json->{data} ) eq "HASH" ) {
  638. $readings{'CO-AQI'} = $decode_json->{data}{iaqi}{co}{v};
  639. $readings{'NO2-AQI'} = $decode_json->{data}{iaqi}{no2}{v};
  640. $readings{'PM10-AQI'} = $decode_json->{data}{iaqi}{pm10}{v};
  641. $readings{'PM2.5-AQI'} = $decode_json->{data}{iaqi}{pm25}{v};
  642. $readings{'AQI'} = $decode_json->{data}{aqi};
  643. $readings{'O3-AQI'} = $decode_json->{data}{iaqi}{o3}{v};
  644. $readings{'SO2-AQI'} = $decode_json->{data}{iaqi}{so2}{v};
  645. $readings{'temperature'} = $decode_json->{data}{iaqi}{t}{v};
  646. $readings{'pressure'} = $decode_json->{data}{iaqi}{p}{v};
  647. $readings{'humidity'} = $decode_json->{data}{iaqi}{h}{v};
  648. $readings{'status'} = $decode_json->{status};
  649. $readings{'pubDate'} = $decode_json->{data}{time}{s};
  650. $readings{'pubUnixTime'} = $decode_json->{data}{time}{v};
  651. $readings{'pubTimezone'} = $decode_json->{data}{time}{tz};
  652. $readings{'windSpeed'} = $decode_json->{data}{iaqi}{w}{v};
  653. $readings{'windDirection'} = $decode_json->{data}{iaqi}{wd}{v};
  654. $readings{'dewpoint'} = $decode_json->{data}{iaqi}{d}{v};
  655. $readings{'dominatPoll'} = $decode_json->{data}{dominentpol};
  656. }
  657. else {
  658. $readings{'status'} = 'no hash reference found';
  659. }
  660. return \%readings;
  661. }
  662. sub AirPollutionLevel($) {
  663. my $aqi = shift;
  664. return 1 unless ( defined($aqi) );
  665. my $apl;
  666. if ( $aqi < 51 ) { $apl = 1 }
  667. elsif ( $aqi < 101 ) { $apl = 2 }
  668. elsif ( $aqi < 151 ) { $apl = 3 }
  669. elsif ( $aqi < 201 ) { $apl = 4 }
  670. elsif ( $aqi < 301 ) { $apl = 5 }
  671. else { $apl = 6 }
  672. return $apl;
  673. }
  674. sub HealthImplications($$) {
  675. my ( $hash, $apl ) = @_;
  676. my $name = $hash->{NAME};
  677. my %HIen = (
  678. 1 =>
  679. 'Air quality is acceptable; however, for some pollutants there may be a moderate health concern for a very small number of people who are unusually sensitive to air pollution.',
  680. 2 =>
  681. 'Air quality is acceptable; however, for some pollutants there may be a moderate health concern for a very small number of people who are unusually sensitive to air pollution.',
  682. 3 =>
  683. 'Members of sensitive groups may experience health effects. The general public is not likely to be affected.',
  684. 4 =>
  685. 'Everyone may begin to experience health effects; members of sensitive groups may experience more serious health effects',
  686. 5 =>
  687. 'Health warnings of emergency conditions. The entire population is more likely to be affected.',
  688. 6 => 'Health alert: everyone may experience more serious health effects'
  689. );
  690. my %HIde = (
  691. 1 =>
  692. 'Die Qualität der Luft gilt als zufriedenstellend und die Luftverschmutzung stellt ein geringes oder kein Risiko dar',
  693. 2 =>
  694. 'Die Luftqualität ist insgesamt akzeptabel. Bei manchen Schadstoffe besteht jedoch eventuell eine geringe Gesundheitsgefahr für einen sehr kleinen Personenkreis, der sehr empfindlich auf Luftverschmutzung ist.',
  695. 3 =>
  696. 'Bei Mitgliedern von empfindlichen Personengruppen können gesundheitliche Auswirkungen auftreten. Die allgemeine Öffentlichkeit ist wahrscheinlich nicht betroffen.',
  697. 4 =>
  698. 'Erste gesundheitliche Auswirkungen können sich bei allen Personen einstellen. Bei empfindlichen Personengruppen können ernstere gesundheitliche Auswirkungen auftreten.',
  699. 5 =>
  700. 'Gesundheitswarnung aufgrund einer Notfallsituation. Die gesamte Bevölkerung ist voraussichtlich betroffen.',
  701. 6 =>
  702. 'Gesundheitsalarm: Jeder muss mit dem Auftreten ernsterer Gesundheitsschäden rechnen'
  703. );
  704. return (
  705. (
  706. AttrVal( 'global', 'language', 'none' ) eq 'DE'
  707. or AttrVal( $name, 'language', 'none' ) eq 'de'
  708. )
  709. and AttrVal( $name, 'language', 'none' ) ne 'en'
  710. ? $HIde{$apl}
  711. : $HIen{$apl}
  712. );
  713. }
  714. 1;
  715. =pod
  716. =item device
  717. =item summary Air Quality Index proving a transparent Air Quality information
  718. =item summary_DE Air Quality Index Nachweis einer transparenten Luftqualitätsinformation
  719. =begin html
  720. <a name="Aqicn"></a>
  721. <h3>Air Quality Index</h3>
  722. <ul>
  723. This modul fetch Air Quality data from http://aqicn.org.
  724. <br><br>
  725. <a name="Aqicndefine"></a>
  726. <b>Define</b>
  727. <ul><br>
  728. <code>define &lt;name&gt; Aqicn token=&ltTOKEN-KEY&gt</code>
  729. <br><br>
  730. Example:
  731. <ul><br>
  732. <code>define aqicnMaster Aqicn token=12345678</code><br>
  733. </ul>
  734. <br>
  735. This statement creates the Aqicn Master Device.<br>
  736. After the device has been created, you can search Aqicn Station by city name and create automatically the station device.
  737. </ul>
  738. <br><br>
  739. <a name="Aqicnreadings"></a>
  740. <b>Readings</b>
  741. <ul>
  742. <li>APL - Air Pollution Level</li>
  743. <li>AQI - Air Quality Index (AQI) of the dominant pollutant in city. Values are converted from µg/m³ to AQI level using US EPA standards. For more detailed information: https://en.wikipedia.org/wiki/Air_quality_index and https://www.airnow.gov/index.cfm?action=aqi_brochure.index. </li>
  744. <li>CO-AQI - AQI of CO (carbon monoxide). An AQI of 100 for carbon monoxide corresponds to a level of 9 parts per million (averaged over 8 hours).</li>
  745. <li>NO2-AQI - AQI of NO2 (nitrogen dioxide). See also https://www.airnow.gov/index.cfm?action=pubs.aqiguidenox</li>
  746. <li>PM10-AQI - AQI of PM10 (respirable particulate matter). For particles up to 10 micrometers in diameter: An AQI of 100 corresponds to 150 micrograms per cubic meter (averaged over 24 hours).</li>
  747. <li>PM2.5-AQI - AQI of PM2.5 (fine particulate matter). For particles up to 2.5 micrometers in diameter: An AQI of 100 corresponds to 35 micrograms per cubic meter (averaged over 24 hours).</li>
  748. <li>O3-AQI - AQI of O3 (ozone). An AQI of 100 for ozone corresponds to an ozone level of 0.075 parts per million (averaged over 8 hours). See also https://www.airnow.gov/index.cfm?action=pubs.aqiguideozone</li>
  749. <li>SO2-AQI - AQI of SO2 (sulfur dioxide). An AQI of 100 for sulfur dioxide corresponds to a level of 75 parts per billion (averaged over one hour).</li>
  750. <li>temperature - Temperature in degrees Celsius</li>
  751. <li>pressure - Atmospheric pressure in hectopascals (hPa)</li>
  752. <li>humidity - Relative humidity in percent</li>
  753. <li>state- Current AQI and air pollution level</li>
  754. <li>status - condition of the data</li>
  755. <li>pubDate- Local time of publishing the data</li>
  756. <li>pubUnixTime - Unix time stamp of local time but converted wrongly, if local time is e.g. 1300 GMT+1, the time stamp shows 1300 UTC.</li>
  757. <li>pubTimezone - Time zone of the city (UTC)</li>
  758. <li>windspeed - Wind speed in kilometer per hour</li>
  759. <li>windDirection - Wind direction</li>
  760. <li>dominatPoll - Dominant pollutant in city</li>
  761. <li>dewpoint - Dew in degrees Celsius</li>
  762. <li>healthImplications - Information about Health Implications</li>
  763. <li>htmlStyle - can be used to format the STATE and FHEMWEB (Example: stateFormate htmlStyle</li>
  764. </ul>
  765. <br>
  766. <a name="Aqicnget"></a>
  767. <b>get</b>
  768. <ul>
  769. <li>stationSearchByCity - search station by city name and open the result in seperate popup window</li>
  770. <li>update - fetch new data every x times</li>
  771. </ul>
  772. <br>
  773. <a name="Aqicnattribute"></a>
  774. <b>Attribute</b>
  775. <ul>
  776. <li>interval - interval in seconds for automatically fetch data (default 3600)</li>
  777. </ul>
  778. </ul>
  779. =end html
  780. =begin html_DE
  781. <a name="Aqicn"></a>
  782. <h3>Air Quality Index</h3>
  783. =end html_DE
  784. =cut