59_Wunderground.pm 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071
  1. # $Id: 59_Wunderground.pm 13339 2017-02-05 16:55:54Z loredo $
  2. ##############################################################################
  3. #
  4. # 59_Wunderground.pm
  5. #
  6. # Copyright by Julian Pawlowski
  7. # e-mail: julian.pawlowski at gmail.com
  8. #
  9. # This file is part of fhem.
  10. #
  11. # Fhem is free software: you can redistribute it and/or modify
  12. # it under the terms of the GNU General Public License as published by
  13. # the Free Software Foundation, either version 2 of the License, or
  14. # (at your option) any later version.
  15. #
  16. # Fhem is distributed in the hope that it will be useful,
  17. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. # GNU General Public License for more details.
  20. #
  21. # You should have received a copy of the GNU General Public License
  22. # along with fhem. If not, see <http://www.gnu.org/licenses/>.
  23. #
  24. ##############################################################################
  25. # http://api.wunderground.com/weather/api
  26. package main;
  27. use strict;
  28. use warnings;
  29. use vars qw(%data);
  30. use HttpUtils;
  31. use utf8;
  32. use Encode qw(encode_utf8 decode_utf8);
  33. use Unit;
  34. use Data::Dumper;
  35. sub Wunderground_Hash2Readings($$;$);
  36. ###################################
  37. sub Wunderground_Initialize($) {
  38. my ($hash) = @_;
  39. Log3 $hash, 5, "Wunderground_Initialize: Entering";
  40. my $webhookFWinstance =
  41. join( ",", devspec2array('TYPE=FHEMWEB:FILTER=TEMPORARY!=1') );
  42. $hash->{SetFn} = "Wunderground_Set";
  43. $hash->{DefFn} = "Wunderground_Define";
  44. $hash->{AttrFn} = "Wunderground_Attr";
  45. $hash->{UndefFn} = "Wunderground_Undefine";
  46. $hash->{DbLog_splitFn} = "Unit_DbLog_split";
  47. $hash->{parseParams} = 1;
  48. $hash->{AttrList} =
  49. "disable:0,1 timeout:1,2,3,4,5 pollInterval:300,450,600,750,900 wu_lang:en,de,at,ch,nl,fr,pl wu_pws:1,0 wu_bestfct:1,0 stateReadings stateReadingsFormat:0,1 "
  50. . "wu_features:multiple-strict,alerts,almanac,astronomy,conditions,currenthurricane,forecast,forecast10day,hourly,hourly10day "
  51. . $readingFnAttributes;
  52. $hash->{readingsDesc} = {
  53. 'UV' => { rtype => 'uvi' },
  54. 'dewpoint' => { rtype => 'c', formula_symbol => 'Td', },
  55. 'dewpoint_f' => { rtype => 'f', formula_symbol => 'Td', },
  56. 'fc0_high_c' =>
  57. { rtype => 'c', format => '%i', formula_symbol => 'Th', },
  58. 'fc0_high_f' =>
  59. { rtype => 'f', format => '%i', formula_symbol => 'Th', },
  60. 'fc0_humidity' => { rtype => 'pct', formula_symbol => 'H' },
  61. 'fc0_humidity_max' => { rtype => 'pct', formula_symbol => 'H' },
  62. 'fc0_humidity_min' => { rtype => 'pct', formula_symbol => 'H' },
  63. 'fc0_icon_url' => { rtype => 'url_http' },
  64. 'fc0_icon_url_night' => { rtype => 'url_http' },
  65. 'fc0_low_c' =>
  66. { rtype => 'c', format => '%i', formula_symbol => 'Tl', },
  67. 'fc0_low_f' =>
  68. { rtype => 'f', format => '%i', formula_symbol => 'Tl', },
  69. 'fc0_rain_day' => { rtype => 'mm' },
  70. 'fc0_rain_day_in' => { rtype => 'in' },
  71. 'fc0_rain_night' => { rtype => 'mm' },
  72. 'fc0_rain_night_in' => { rtype => 'in' },
  73. 'fc0_snow_day' => { rtype => 'cm' },
  74. 'fc0_snow_day_in' => { rtype => 'in' },
  75. 'fc0_snow_night' => { rtype => 'cm' },
  76. 'fc0_snow_night_in' => { rtype => 'in' },
  77. 'fc0_title' => { rtype => 'weekday', showLong => 1 },
  78. 'fc0_title_night' => { rtype => 'weekday_night', showLong => 1 },
  79. 'fc0_wind_direction' => { rtype => 'compasspoint' },
  80. 'fc0_wind_direction_max' => { rtype => 'compasspoint' },
  81. 'fc0_wind_speed' => { rtype => 'kmph', formula_symbol => 'Ws' },
  82. 'fc0_wind_speed_max' => { rtype => 'kmph', formula_symbol => 'Ws' },
  83. 'fc0_wind_speed_max_mph' => { rtype => 'mph', formula_symbol => 'Ws' },
  84. 'fc0_wind_speed_mph' => { rtype => 'mph', formula_symbol => 'Ws' },
  85. 'fc1_high_c' =>
  86. { rtype => 'c', format => '%i', formula_symbol => 'Th', },
  87. 'fc1_high_f' =>
  88. { rtype => 'f', format => '%i', formula_symbol => 'Th', },
  89. 'fc1_humidity' => { rtype => 'pct', formula_symbol => 'H' },
  90. 'fc1_humidity_max' => { rtype => 'pct', formula_symbol => 'H' },
  91. 'fc1_humidity_min' => { rtype => 'pct', formula_symbol => 'H' },
  92. 'fc1_icon_url' => { rtype => 'url_http' },
  93. 'fc1_icon_url_night' => { rtype => 'url_http' },
  94. 'fc1_low_c' =>
  95. { rtype => 'c', format => '%i', formula_symbol => 'Tl', },
  96. 'fc1_low_f' =>
  97. { rtype => 'f', format => '%i', formula_symbol => 'Tl', },
  98. 'fc1_rain_day' => { rtype => 'mm' },
  99. 'fc1_rain_day_in' => { rtype => 'in' },
  100. 'fc1_rain_night' => { rtype => 'mm' },
  101. 'fc1_rain_night_in' => { rtype => 'in' },
  102. 'fc1_snow_day' => { rtype => 'cm' },
  103. 'fc1_snow_day_in' => { rtype => 'in' },
  104. 'fc1_snow_night' => { rtype => 'cm' },
  105. 'fc1_snow_night_in' => { rtype => 'in' },
  106. 'fc1_title' => { rtype => 'weekday', showLong => 1 },
  107. 'fc1_title_night' => { rtype => 'weekday_night', showLong => 1 },
  108. 'fc1_wind_direction' => { rtype => 'compasspoint' },
  109. 'fc1_wind_direction_max' => { rtype => 'compasspoint' },
  110. 'fc1_wind_speed' => { rtype => 'kmph', formula_symbol => 'Ws' },
  111. 'fc1_wind_speed_max' => { rtype => 'kmph', formula_symbol => 'Ws' },
  112. 'fc1_wind_speed_max_mph' => { rtype => 'mph', formula_symbol => 'Ws' },
  113. 'fc1_wind_speed_mph' => { rtype => 'mph', formula_symbol => 'Ws' },
  114. 'fc2_high_c' =>
  115. { rtype => 'c', format => '%i', formula_symbol => 'Th', },
  116. 'fc2_high_f' =>
  117. { rtype => 'f', format => '%i', formula_symbol => 'Th', },
  118. 'fc2_humidity' => { rtype => 'pct', formula_symbol => 'H' },
  119. 'fc2_humidity_max' => { rtype => 'pct', formula_symbol => 'H' },
  120. 'fc2_humidity_min' => { rtype => 'pct', formula_symbol => 'H' },
  121. 'fc2_icon_url' => { rtype => 'url_http' },
  122. 'fc2_icon_url_night' => { rtype => 'url_http' },
  123. 'fc2_low_c' =>
  124. { rtype => 'c', format => '%i', formula_symbol => 'Tl', },
  125. 'fc2_low_f' =>
  126. { rtype => 'f', format => '%i', formula_symbol => 'Tl', },
  127. 'fc2_rain_day' => { rtype => 'mm' },
  128. 'fc2_rain_day_in' => { rtype => 'in' },
  129. 'fc2_rain_night' => { rtype => 'mm' },
  130. 'fc2_rain_night_in' => { rtype => 'in' },
  131. 'fc2_snow_day' => { rtype => 'cm' },
  132. 'fc2_snow_day_in' => { rtype => 'in' },
  133. 'fc2_snow_night' => { rtype => 'cm' },
  134. 'fc2_snow_night_in' => { rtype => 'in' },
  135. 'fc2_title' => { rtype => 'weekday', showLong => 1 },
  136. 'fc2_title_night' => { rtype => 'weekday_night', showLong => 1, },
  137. 'fc2_wind_direction' => { rtype => 'compasspoint' },
  138. 'fc2_wind_direction_max' => { rtype => 'compasspoint' },
  139. 'fc2_wind_speed' => { rtype => 'kmph', formula_symbol => 'Ws' },
  140. 'fc2_wind_speed_max' => { rtype => 'kmph', formula_symbol => 'Ws' },
  141. 'fc2_wind_speed_max_mph' => { rtype => 'mph', formula_symbol => 'Ws' },
  142. 'fc2_wind_speed_mph' => { rtype => 'mph', formula_symbol => 'Ws' },
  143. 'fc3_high_c' =>
  144. { rtype => 'c', format => '%i', formula_symbol => 'Th', },
  145. 'fc3_high_f' =>
  146. { rtype => 'f', format => '%i', formula_symbol => 'Th', },
  147. 'fc3_humidity' => { rtype => 'pct', formula_symbol => 'H' },
  148. 'fc3_humidity_max' => { rtype => 'pct', formula_symbol => 'H' },
  149. 'fc3_humidity_min' => { rtype => 'pct', formula_symbol => 'H' },
  150. 'fc3_icon_url' => { rtype => 'url_http' },
  151. 'fc3_icon_url_night' => { rtype => 'url_http' },
  152. 'fc3_low_c' =>
  153. { rtype => 'c', format => '%i', formula_symbol => 'Tl', },
  154. 'fc3_low_f' =>
  155. { rtype => 'f', format => '%i', formula_symbol => 'Tl', },
  156. 'fc3_rain_day' => { rtype => 'mm' },
  157. 'fc3_rain_day_in' => { rtype => 'in' },
  158. 'fc3_rain_night' => { rtype => 'mm' },
  159. 'fc3_rain_night_in' => { rtype => 'in' },
  160. 'fc3_snow_day' => { rtype => 'cm' },
  161. 'fc3_snow_day_in' => { rtype => 'in' },
  162. 'fc3_snow_night' => { rtype => 'cm' },
  163. 'fc3_snow_night_in' => { rtype => 'in' },
  164. 'fc3_title' => { rtype => 'weekday', showLong => 1 },
  165. 'fc3_title_night' => { rtype => 'weekday_night', showLong => 1 },
  166. 'fc3_wind_direction' => { rtype => 'compasspoint' },
  167. 'fc3_wind_direction_max' => { rtype => 'compasspoint' },
  168. 'fc3_wind_speed' => { rtype => 'kmph', formula_symbol => 'Ws' },
  169. 'fc3_wind_speed_max' => { rtype => 'kmph', formula_symbol => 'Ws' },
  170. 'fc3_wind_speed_max_mph' => { rtype => 'mph', formula_symbol => 'Ws' },
  171. 'fc3_wind_speed_mph' => { rtype => 'mph', formula_symbol => 'Ws' },
  172. 'feelslike_c' => { rtype => 'c', formula_symbol => 'Tf', },
  173. 'feelslike_f' => { rtype => 'f', formula_symbol => 'Tf', },
  174. 'forecast_url' => { rtype => 'url_http' },
  175. 'history_url' => { rtype => 'url_http' },
  176. 'humidity' => { rtype => 'pct', formula_symbol => 'H' },
  177. 'icon_url' => { rtype => 'url_http' },
  178. 'israining' => { rtype => 'bool', },
  179. 'lastQueryResult' => { rtype => 'oknok' },
  180. 'moon_age' => { rtype => 'd' },
  181. 'moon_pct' => { rtype => 'pct' },
  182. 'moon_rise' => { rtype => 'time' },
  183. 'moon_set' => { rtype => 'time' },
  184. 'ob_url' => { rtype => 'url_http' },
  185. 'pressure' => { rtype => 'hpamb' },
  186. 'pressure_in' => { rtype => 'inhg' },
  187. 'pressure_trend' => { rtype => 'trend' },
  188. 'rain' => { rtype => 'mm' },
  189. 'rain_day' => { rtype => 'mm' },
  190. 'rain_day_in' => { rtype => 'in' },
  191. 'rain_in' => { rtype => 'in' },
  192. 'solarradiation' => { rtype => 'wpsm' },
  193. 'sunrise' => { rtype => 'time' },
  194. 'sunset' => { rtype => 'time' },
  195. 'temp_c' => { rtype => 'c' },
  196. 'temp_f' => { rtype => 'f' },
  197. 'visibility' => {
  198. rtype => 'km',
  199. scope => { empty_replace => '--.-' }
  200. },
  201. 'visibility_mi' => {
  202. rtype => 'mi',
  203. scope => { empty_replace => '--.-' }
  204. },
  205. 'wind_chill' => {
  206. rtype => 'c',
  207. formula_symbol => 'Wc',
  208. scope => { empty_replace => '--.-' },
  209. },
  210. 'wind_chill_f' => {
  211. rtype => 'f',
  212. formula_symbol => 'Wc',
  213. scope => { empty_replace => '--.-' }
  214. },
  215. 'wind_direction' => { rtype => 'compasspoint' },
  216. 'wind_gust' => { rtype => 'kmph', formula_symbol => 'Wg' },
  217. 'wind_gust_mph' => { rtype => 'mph', formula_symbol => 'Wg' },
  218. 'wind_speed' => { rtype => 'kmph', formula_symbol => 'Ws' },
  219. 'wind_speed_mph' => { rtype => 'mph', formula_symbol => 'Ws' }
  220. };
  221. return;
  222. }
  223. #####################################
  224. sub Wunderground_GetStatus($;$) {
  225. my ( $hash, $delay ) = @_;
  226. my $name = $hash->{NAME};
  227. $hash->{INTERVAL} = AttrVal( $name, "pollInterval", "300" );
  228. my $bestfct = AttrVal( $name, "wu_bestfct", undef );
  229. my $features =
  230. AttrVal( $name, "wu_features", "astronomy,conditions,forecast" );
  231. my $lang = AttrVal( $name, "wu_lang", "en" );
  232. my $pws = AttrVal( $name, "wu_pws", undef );
  233. my $interval = (
  234. $delay
  235. ? $delay
  236. : $hash->{INTERVAL}
  237. );
  238. Log3 $name, 5,
  239. "Wunderground $name: called function Wunderground_GetStatus()";
  240. RemoveInternalTimer($hash);
  241. InternalTimer( gettimeofday() + $interval,
  242. "Wunderground_GetStatus", $hash, 0 );
  243. return
  244. if ( $delay || AttrVal( $name, "disable", 0 ) == 1 );
  245. my $langmap = {
  246. at => 'OS',
  247. de => 'DL',
  248. 'en-gb' => 'LI',
  249. gb => 'LI',
  250. uk => 'LI',
  251. };
  252. if ( defined( $langmap->{ lc($lang) } ) ) {
  253. $hash->{LANG} = $langmap->{ lc($lang) };
  254. }
  255. else {
  256. $hash->{LANG} = uc($lang);
  257. }
  258. $features =~ s/,{2,}/,/g;
  259. $features =~ s/\s//g;
  260. $features =~ s/,/\//g;
  261. $features .= "/lang:" . $hash->{LANG};
  262. $features .= "/pws:$pws" if ( defined($pws) );
  263. $features .= "/bestfct:$bestfct" if ( defined($bestfct) );
  264. Wunderground_SendCommand( $hash, $features );
  265. return;
  266. }
  267. ###################################
  268. sub Wunderground_Set($$$) {
  269. my ( $hash, $a, $h ) = @_;
  270. my $name = $hash->{NAME};
  271. Log3 $name, 5, "Wunderground $name: called function Wunderground_Set()";
  272. return "Argument is missing" if ( int(@$a) < 1 );
  273. my $usage = "Unknown argument " . @$a[1] . ", choose one of update:noArg";
  274. my $cmd = '';
  275. my $result;
  276. # update
  277. if ( lc( @$a[1] ) eq "update" ) {
  278. Log3 $name, 3, "Wunderground set $name " . @$a[1];
  279. Wunderground_GetStatus($hash);
  280. }
  281. # return usage hint
  282. else {
  283. return $usage;
  284. }
  285. return $result;
  286. }
  287. ###################################
  288. sub Wunderground_Define($$$) {
  289. my ( $hash, $a, $h ) = @_;
  290. my $name = $hash->{NAME};
  291. my $infix = "Wunderground";
  292. Log3 $name, 5, "Wunderground $name: called function Wunderground_Define()";
  293. eval {
  294. require JSON;
  295. import JSON qw( decode_json );
  296. };
  297. return "Please install Perl JSON to use module Wunderground"
  298. if ($@);
  299. if ( int(@$a) < 2 ) {
  300. my $msg = "Wrong syntax: define <name> Wunderground <api-key> <pws-id>";
  301. Log3 $name, 4, $msg;
  302. return $msg;
  303. }
  304. $hash->{TYPE} = "Wunderground";
  305. $hash->{API_KEY} = @$a[2];
  306. $hash->{QUERY} = @$a[3];
  307. $hash->{QUERY} = "pws:" . $hash->{QUERY}
  308. if ( $hash->{QUERY} =~ /^[A-Z]{3,}\d{2,}$/ );
  309. if ( $init_done && !defined( $hash->{OLDDEF} ) ) {
  310. fhem 'attr ' . $name . ' stateReadings temp_c humidity';
  311. fhem 'attr ' . $name . ' stateReadingsFormat 1';
  312. fhem 'attr ' . $name . ' wu_features astronomy,conditions,forecast';
  313. }
  314. # start the status update timer
  315. Wunderground_GetStatus( $hash, 2 );
  316. return;
  317. }
  318. ###################################
  319. sub Wunderground_Attr(@) {
  320. my ( $cmd, $name, $attrName, $attrVal ) = @_;
  321. my $hash = $defs{$name};
  322. Log3 $name, 5, "Wunderground $name: called function Wunderground_Attr()";
  323. return
  324. "Invalid value for attribute $attrName: minimum value is 1 second, maximum 5 seconds"
  325. if ( $attrVal
  326. && $attrName eq "timeout"
  327. && ( $attrVal < 1 || $attrVal > 5 ) );
  328. return
  329. "Invalid value for attribute $attrName: minimum value is 300 seconds"
  330. if ( $attrVal && $attrName eq "pollInterval" && $attrVal < 300 );
  331. return undef;
  332. }
  333. ############################################################################################################
  334. #
  335. # Begin of helper functions
  336. #
  337. ############################################################################################################
  338. ###################################
  339. sub Wunderground_SendCommand($$) {
  340. my ( $hash, $features ) = @_;
  341. my $name = $hash->{NAME};
  342. my $apikey = $hash->{API_KEY};
  343. my $query = $hash->{QUERY};
  344. my $URL =
  345. "https://api.wunderground.com/api/%APIKEY%/%FEATURES%/q/%QUERY%.json";
  346. Log3 $name, 5,
  347. "Wunderground $name: called function Wunderground_SendCommand()";
  348. $URL =~ s/%APIKEY%/$apikey/;
  349. $URL =~ s/%FEATURES%/$features/;
  350. $URL =~ s/%QUERY%/$query/;
  351. Log3 $name, 5, "Wunderground $name: GET " . urlDecode($URL);
  352. HttpUtils_NonblockingGet(
  353. {
  354. url => $URL,
  355. timeout => AttrVal( $name, "timeout", "3" ),
  356. hash => $hash,
  357. method => "GET",
  358. header =>
  359. "agent: FHEM-Wunderground/1.0.0\r\nUser-Agent: FHEM-Wunderground/1.0.0\r\nAccept: application/json",
  360. httpversion => "1.1",
  361. callback => \&Wunderground_ReceiveCommand,
  362. }
  363. );
  364. return;
  365. }
  366. ###################################
  367. sub Wunderground_ReceiveCommand($$$) {
  368. my ( $param, $err, $data ) = @_;
  369. my $hash = $param->{hash};
  370. my $name = $hash->{NAME};
  371. my $lastQueryResult =
  372. ReadingsVal( $name, "lastQueryResult", "Initialized" );
  373. my $state = "Initialized";
  374. my $return;
  375. Log3 $name, 5,
  376. "Wunderground $name: called function Wunderground_ReceiveCommand()";
  377. readingsBeginUpdate($hash);
  378. # service not reachable
  379. if ($err) {
  380. Log3 $name, 4, "Wunderground $name: RCV TIMEOUT: $err";
  381. $lastQueryResult = "unavailable";
  382. }
  383. # data received
  384. elsif ($data) {
  385. Log3 $name, 4, "Wunderground $name: RCV";
  386. if ( $data ne "" ) {
  387. # fix malformed JSON ...
  388. $data =~ s/^[\s\r\n0-9a-zA-Z]*//;
  389. $data =~ s/[\s\r\n0-9a-zA-Z]*$//;
  390. $data =~ s/[\r\n]+[0-9a-zA-Z]+[\r\n]+//g;
  391. eval '$return = decode_json( Encode::encode_utf8($data) ); 1';
  392. if ($@) {
  393. Log3 $name, 5,
  394. "Wunderground $name: RES ERROR - unable to parse malformed JSON: $@\n"
  395. . $data;
  396. return undef;
  397. }
  398. else {
  399. Log3 $name, 5, "Wunderground $name: RES";
  400. }
  401. }
  402. $lastQueryResult = "undefined";
  403. #######################
  404. # process return data
  405. #
  406. if ( $return && ref($return) eq "HASH" ) {
  407. $lastQueryResult = Wunderground_Hash2Readings( $hash, $return );
  408. }
  409. }
  410. # state
  411. my $stateReadings = AttrVal( $name, "stateReadings", "" );
  412. my $stateReadingsLang = AttrVal( $name, "stateReadingsLang", "en" );
  413. my $stateReadingsFormat = AttrVal( $name, "stateReadingsFormat", "0" );
  414. # $state =
  415. # makeSTATE( $name, $stateReadings,
  416. # $stateReadingsLang, $stateReadingsFormat );
  417. $state = makeSTATE( $name, $stateReadings, $stateReadingsFormat );
  418. readingsBulkUpdate( $hash, "state", $state );
  419. readingsBulkUpdateIfChanged( $hash, "lastQueryResult", $lastQueryResult );
  420. readingsEndUpdate( $hash, 1 );
  421. return;
  422. }
  423. ###################################
  424. sub Wunderground_Hash2Readings($$;$) {
  425. my ( $hash, $h, $r ) = @_;
  426. my $name = $hash->{NAME};
  427. my $lang = AttrVal( $name, "wu_lang", "en" );
  428. my $loop = 0;
  429. $loop = 1 if ( defined($r) );
  430. if ( ref($h) eq "HASH" ) {
  431. foreach my $k ( keys %{$h} ) {
  432. # error
  433. return $h->{response}{error}{type}
  434. if ( $k eq "response"
  435. && defined( $h->{response}{error}{type} ) );
  436. next
  437. if ( $k eq "image"
  438. || $k eq "response"
  439. || $k eq "station_id"
  440. || $k =~ /^.*_string$/ );
  441. my $reading;
  442. my $cr = $k;
  443. # hash level1 renaming
  444. $cr = "" if ( $cr eq "current_observation" );
  445. # custom reading
  446. $cr = "condition" if ( $cr eq "weather" );
  447. $cr = "dewpoint" if ( $cr eq "dewpoint_c" );
  448. $cr = "humidity" if ( $cr eq "relative_humidity" );
  449. $cr = "pressure" if ( $cr eq "pressure_mb" );
  450. $cr = "rain" if ( $cr eq "precip_1hr_metric" );
  451. $cr = "rain_day" if ( $cr eq "precip_today_metric" );
  452. $cr = "rain_in" if ( $cr eq "precip_1hr_in" );
  453. $cr = "rain_day_in" if ( $cr eq "precip_today_in" );
  454. $cr = "wind_direction" if ( $cr eq "wind_degrees" );
  455. $cr = "wind_gust" if ( $cr eq "wind_gust_kph" );
  456. $cr = "wind_gust_mph" if ( $cr eq "wind_gust_mph" );
  457. $cr = "wind_speed" if ( $cr eq "wind_kph" );
  458. $cr = "wind_speed_mph" if ( $cr eq "wind_mph" );
  459. $cr = "wind_chill" if ( $cr eq "windchill_c" );
  460. $cr = "wind_chill_f" if ( $cr eq "windchill_f" );
  461. $cr = "visibility" if ( $cr eq "visibility_km" );
  462. next
  463. if ( $cr =~ /^sun_phase(.*)$/
  464. || $cr eq "date"
  465. || $cr eq "wind_dir" );
  466. next if ( $r && $r =~ /^display_location.*$/ );
  467. # observation_*
  468. if ( $cr =~ /^observation_.*$/ ) {
  469. $hash->{LAST_OBSERVATION} = $h->{observation_epoch};
  470. next;
  471. }
  472. # local_*
  473. elsif ( $cr =~ /^local_.*$/ ) {
  474. $hash->{LAST} = $h->{local_epoch};
  475. next;
  476. }
  477. # moon_phase
  478. elsif ( $cr =~ /^moon_phase(.*)$/ ) {
  479. my $sunrise = $h->{moon_phase}{sunrise}{hour} . ":"
  480. . $h->{moon_phase}{sunrise}{minute};
  481. my $sunset = $h->{moon_phase}{sunset}{hour} . ":"
  482. . $h->{moon_phase}{sunset}{minute};
  483. my $moonrise = $h->{moon_phase}{moonrise}{hour} . ":"
  484. . $h->{moon_phase}{moonrise}{minute};
  485. my $moonset = $h->{moon_phase}{moonset}{hour} . ":"
  486. . $h->{moon_phase}{moonset}{minute};
  487. $sunrise =~ s/^(\d):(\d\d)$/0$1:$2/;
  488. $sunrise =~ s/^(\d\d):(\d)$/$1:0$2/;
  489. $sunset =~ s/^(\d):(\d\d)$/0$1:$2/;
  490. $sunset =~ s/^(\d\d):(\d)$/$1:0$2/;
  491. $moonrise =~ s/^(\d):(\d\d)$/0$1:$2/;
  492. $moonrise =~ s/^(\d\d):(\d)$/$1:0$2/;
  493. $moonset =~ s/^(\d):(\d\d)$/0$1:$2/;
  494. $moonset =~ s/^(\d\d):(\d)$/$1:0$2/;
  495. readingsBulkUpdate( $hash, "sunrise", $sunrise );
  496. readingsBulkUpdate( $hash, "sunset", $sunset );
  497. readingsBulkUpdate( $hash, "moonrise", $moonrise );
  498. readingsBulkUpdate( $hash, "moonset", $moonset );
  499. readingsBulkUpdate( $hash, "moon_age",
  500. $h->{moon_phase}{ageOfMoon} );
  501. readingsBulkUpdate( $hash, "moon_pct",
  502. $h->{moon_phase}{percentIlluminated} );
  503. readingsBulkUpdate( $hash, "moon_phase",
  504. $h->{moon_phase}{phaseofMoon} );
  505. }
  506. # hourly_forecast
  507. elsif ($r
  508. && $r =~ /^hourly_forecast(\d+)$/ )
  509. {
  510. my $period = $r;
  511. $period =~ s/[^\d]//g;
  512. $reading = "hfc" . $period . "_";
  513. readingsBulkUpdate( $hash, $reading . "condition",
  514. $h->{conditions} );
  515. readingsBulkUpdate(
  516. $hash,
  517. $reading . "dewpoint_c",
  518. $h->{dewpoint}{metric}
  519. );
  520. readingsBulkUpdate(
  521. $hash,
  522. $reading . "dewpoint_f",
  523. $h->{dewpoint}{english}
  524. );
  525. readingsBulkUpdate(
  526. $hash,
  527. $reading . "feelslike_c",
  528. $h->{feelslike}{metric}
  529. );
  530. readingsBulkUpdate(
  531. $hash,
  532. $reading . "feelslike_f",
  533. $h->{feelslike}{english}
  534. );
  535. readingsBulkUpdate(
  536. $hash,
  537. $reading . "heatindex_c",
  538. $h->{heatindex}{metric}
  539. );
  540. readingsBulkUpdate(
  541. $hash,
  542. $reading . "heatindex_f",
  543. $h->{heatindex}{english}
  544. );
  545. readingsBulkUpdate( $hash, $reading . "humidity",
  546. $h->{humidity} );
  547. readingsBulkUpdate( $hash, $reading . "icon", $h->{icon} );
  548. readingsBulkUpdate( $hash, $reading . "icon_url",
  549. $h->{icon_url} );
  550. readingsBulkUpdate(
  551. $hash,
  552. $reading . "mslp_c",
  553. $h->{mslp}{metric}
  554. );
  555. readingsBulkUpdate(
  556. $hash,
  557. $reading . "mslp_f",
  558. $h->{mslp}{english}
  559. );
  560. readingsBulkUpdate( $hash, $reading . "pop", $h->{pop} );
  561. readingsBulkUpdate(
  562. $hash,
  563. $reading . "temp_c",
  564. $h->{temp}{metric}
  565. );
  566. readingsBulkUpdate(
  567. $hash,
  568. $reading . "temp_f",
  569. $h->{temp}{english}
  570. );
  571. readingsBulkUpdate(
  572. $hash,
  573. $reading . "rain",
  574. $h->{qpf}{metric}
  575. );
  576. readingsBulkUpdate(
  577. $hash,
  578. $reading . "rain_in",
  579. $h->{qpf}{english}
  580. );
  581. readingsBulkUpdate(
  582. $hash,
  583. $reading . "snow",
  584. $h->{snow}{metric}
  585. );
  586. readingsBulkUpdate(
  587. $hash,
  588. $reading . "snow_in",
  589. $h->{snow}{english}
  590. );
  591. readingsBulkUpdate(
  592. $hash,
  593. $reading . "wind_direction",
  594. $h->{wdir}{degrees}
  595. );
  596. readingsBulkUpdate(
  597. $hash,
  598. $reading . "wind_chill",
  599. $h->{windchill}{metric}
  600. );
  601. readingsBulkUpdate(
  602. $hash,
  603. $reading . "wind_chill_f",
  604. $h->{windchill}{english}
  605. );
  606. readingsBulkUpdate(
  607. $hash,
  608. $reading . "wind_speed",
  609. $h->{wspd}{metric}
  610. );
  611. readingsBulkUpdate(
  612. $hash,
  613. $reading . "wind_speed_mph",
  614. $h->{wspd}{english}
  615. );
  616. readingsBulkUpdate( $hash, $reading . "sky", $h->{sky} );
  617. readingsBulkUpdate( $hash, $reading . "UV", $h->{uvi} );
  618. my $time = $h->{FCTTIME}{hour} . ":" . $h->{FCTTIME}{min};
  619. $time =~ s/^(\d):(\d\d)$/0$1:$2/;
  620. readingsBulkUpdate( $hash, $reading . "time", $time );
  621. }
  622. # simpleforecast
  623. elsif ($r
  624. && $r =~ /^forecast\/simpleforecast\/forecastday(\d+)$/ )
  625. {
  626. my $period = $h->{period} - 1;
  627. $reading = "fc" . $period . "_";
  628. readingsBulkUpdate( $hash, $reading . "condition",
  629. $h->{conditions} );
  630. readingsBulkUpdate(
  631. $hash,
  632. $reading . "high_c",
  633. $h->{high}{celsius}
  634. );
  635. readingsBulkUpdate(
  636. $hash,
  637. $reading . "high_f",
  638. $h->{high}{fahrenheit}
  639. );
  640. readingsBulkUpdate( $hash, $reading . "humidity",
  641. $h->{avehumidity} );
  642. readingsBulkUpdate( $hash, $reading . "humidity_min",
  643. $h->{minhumidity} );
  644. readingsBulkUpdate( $hash, $reading . "humidity_max",
  645. $h->{maxhumidity} );
  646. readingsBulkUpdate( $hash, $reading . "icon", $h->{icon} );
  647. readingsBulkUpdate( $hash, $reading . "icon_url",
  648. $h->{icon_url} );
  649. readingsBulkUpdate(
  650. $hash,
  651. $reading . "low_c",
  652. $h->{low}{celsius}
  653. );
  654. readingsBulkUpdate(
  655. $hash,
  656. $reading . "low_f",
  657. $h->{low}{fahrenheit}
  658. );
  659. readingsBulkUpdate( $hash, $reading . "pop", $h->{pop} );
  660. readingsBulkUpdate(
  661. $hash,
  662. $reading . "rain_day",
  663. $h->{qpf_allday}{mm}
  664. );
  665. readingsBulkUpdate(
  666. $hash,
  667. $reading . "rain_day_in",
  668. $h->{qpf_allday}{in}
  669. );
  670. readingsBulkUpdate(
  671. $hash,
  672. $reading . "rain_night",
  673. $h->{qpf_night}{mm}
  674. );
  675. readingsBulkUpdate(
  676. $hash,
  677. $reading . "rain_night_in",
  678. $h->{qpf_night}{in}
  679. );
  680. readingsBulkUpdate(
  681. $hash,
  682. $reading . "snow_day",
  683. $h->{snow_allday}{cm}
  684. );
  685. readingsBulkUpdate(
  686. $hash,
  687. $reading . "snow_day_in",
  688. $h->{snow_allday}{in}
  689. );
  690. readingsBulkUpdate(
  691. $hash,
  692. $reading . "snow_night",
  693. $h->{snow_night}{cm}
  694. );
  695. readingsBulkUpdate(
  696. $hash,
  697. $reading . "snow_night_in",
  698. $h->{snow_night}{in}
  699. );
  700. readingsBulkUpdate(
  701. $hash,
  702. $reading . "wind_direction",
  703. $h->{avewind}{degrees}
  704. );
  705. readingsBulkUpdate(
  706. $hash,
  707. $reading . "wind_direction_max",
  708. $h->{maxwind}{degrees}
  709. );
  710. readingsBulkUpdate(
  711. $hash,
  712. $reading . "wind_speed",
  713. $h->{avewind}{kph}
  714. );
  715. readingsBulkUpdate(
  716. $hash,
  717. $reading . "wind_speed_mph",
  718. $h->{avewind}{mph}
  719. );
  720. readingsBulkUpdate(
  721. $hash,
  722. $reading . "wind_speed_max",
  723. $h->{maxwind}{kph}
  724. );
  725. readingsBulkUpdate(
  726. $hash,
  727. $reading . "wind_speed_max_mph",
  728. $h->{maxwind}{mph}
  729. );
  730. }
  731. # txt_forecast
  732. elsif ($r
  733. && $r =~ /^forecast\/txt_forecast\/forecastday(\d+)$/ )
  734. {
  735. my $period = $h->{period};
  736. my $night =
  737. ( $period eq "1"
  738. || $period eq "3"
  739. || $period eq "5"
  740. || $period eq "7" ? "_night" : "" );
  741. if ( $period < 2 ) {
  742. $period = "0";
  743. }
  744. elsif ( $period < 4 ) {
  745. $period = "1";
  746. }
  747. elsif ( $period < 6 ) {
  748. $period = "2";
  749. }
  750. elsif ( $period < 8 ) {
  751. $period = "3";
  752. }
  753. $reading = "fc" . $period . "_";
  754. my $symbol_c =
  755. Encode::encode_utf8( chr(0x202F) . chr(0x00B0) . 'C' );
  756. my $symbol_f =
  757. Encode::encode_utf8( chr(0x202F) . chr(0x00B0) . 'F' );
  758. my $symbol_pct = Encode::encode_utf8( chr(0x202F) . '%' );
  759. my $symbol_kmh = Encode::encode_utf8( chr(0x00A0) . 'km/h' );
  760. my $symbol_mph = Encode::encode_utf8( chr(0x00A0) . 'mph' );
  761. $h->{fcttext_metric} =~ s/(\d)C/$1$symbol_c/g;
  762. $h->{fcttext} =~ s/(\d)F/$1$symbol_f/g;
  763. $h->{fcttext_metric} =~ s/(\d)\s*%/$1$symbol_pct/g;
  764. $h->{fcttext} =~ s/(\d)\s*%/$1$symbol_pct/g;
  765. $h->{fcttext_metric} =~ s/(\d)\s*km\/h/$1$symbol_kmh/g;
  766. $h->{fcttext} =~ s/(\d)\s*km\/h/$1$symbol_kmh/g;
  767. $h->{fcttext_metric} =~ s/(\d)\s*mph/$1$symbol_mph/g;
  768. $h->{fcttext} =~ s/(\d)\s*mph/$1$symbol_mph/g;
  769. readingsBulkUpdate( $hash, $reading . "icon$night",
  770. $h->{icon} );
  771. readingsBulkUpdate( $hash, $reading . "icon_url$night",
  772. $h->{icon_url} );
  773. readingsBulkUpdate( $hash, $reading . "pop$night", $h->{pop} );
  774. readingsBulkUpdate( $hash, $reading . "title$night",
  775. $h->{title} );
  776. readingsBulkUpdate( $hash, $reading . "text$night",
  777. $h->{fcttext_metric} );
  778. readingsBulkUpdate( $hash, $reading . "text_f$night",
  779. $h->{fcttext} );
  780. $hash->{readingDesc}{"title$night"}{lang} = $lang if ($lang);
  781. $hash->{readingDesc}{"text$night"}{lang} = $lang if ($lang);
  782. $hash->{readingDesc}{"text_f$night"}{lang} = $lang if ($lang);
  783. }
  784. # almanac/temp_high
  785. elsif ($r
  786. && $r =~ /^almanac\/temp_high(.*)$/ )
  787. {
  788. $reading = "almanac_high_";
  789. readingsBulkUpdate( $hash,
  790. $reading . "year", $h->{recordyear} );
  791. readingsBulkUpdate( $hash, $reading . "c", $h->{normal}{C} );
  792. readingsBulkUpdate( $hash, $reading . "f", $h->{normal}{F} );
  793. readingsBulkUpdate( $hash, $reading . "record_c",
  794. $h->{record}{C} );
  795. readingsBulkUpdate( $hash, $reading . "record_f",
  796. $h->{record}{F} );
  797. }
  798. elsif ($r
  799. && $r =~ /^almanac\/temp_low(.*)$/ )
  800. {
  801. $reading = "almanac_low_";
  802. readingsBulkUpdate( $hash,
  803. $reading . "year", $h->{recordyear} );
  804. readingsBulkUpdate( $hash, $reading . "c", $h->{normal}{C} );
  805. readingsBulkUpdate( $hash, $reading . "f", $h->{normal}{F} );
  806. readingsBulkUpdate( $hash, $reading . "record_c",
  807. $h->{record}{C} );
  808. readingsBulkUpdate( $hash, $reading . "record_f",
  809. $h->{record}{F} );
  810. }
  811. elsif ( ref( $h->{$k} ) eq "HASH" || ref( $h->{$k} ) eq "ARRAY" ) {
  812. $reading .= $r . "/" if ( $r && $r ne "" );
  813. $reading .= $cr;
  814. Wunderground_Hash2Readings( $hash, $h->{$k}, $reading );
  815. }
  816. else {
  817. $reading .= $r . "_." if ( $r && $r ne "" );
  818. $reading .= $cr;
  819. my $value = $h->{$k};
  820. $value = "" if ( !defined($value) || $value =~ m/^n\/?a$/i );
  821. if ( $reading eq "icon" || $reading eq "icon_url" ) {
  822. my $sunrise = ReadingsVal( $hash->{NAME}, "sunrise", 0 );
  823. my $sunset = ReadingsVal( $hash->{NAME}, "sunset", 0 );
  824. $sunrise = time_str2num("1970-01-01 $sunrise:00")
  825. if ($sunrise);
  826. $sunset = time_str2num("1970-01-01 $sunset:00")
  827. if ($sunset);
  828. my (
  829. $second, $minute, $hour,
  830. $dayOfMonth, $month, $yearOffset,
  831. $dayOfWeek, $dayOfYear, $daylightSavings
  832. ) = localtime();
  833. my $currentTime = ( ( $hour * 3600 ) + ( $minute * 60 ) );
  834. # for icon and icon_url, make sure to add nt_ prefix
  835. # during the night
  836. if (
  837. ( $currentTime > 43200 && $currentTime >= $sunset )
  838. || ( $currentTime <= 43200
  839. && $currentTime < $sunrise )
  840. )
  841. {
  842. $value = "nt_" . $value if ( $reading eq "icon" );
  843. $value = $1 . "nt_" . $2 . $3
  844. if ( $reading eq "icon_url"
  845. && $value =~ /^(.*\/)([A-Za-z0-9]+)(\.[a-z]+)$/ );
  846. }
  847. # for icon and icon_url, make sure to remove nt_ prefix
  848. # during the daytime
  849. else {
  850. $value =~ s/^nt_// if ( $reading eq "icon" );
  851. $value =~ s/\/nt_// if ( $reading eq "icon_url" );
  852. }
  853. }
  854. elsif ( $reading eq "almanac_.airport_code" ) {
  855. $reading = "almanac_airport_code";
  856. }
  857. $value = "0"
  858. if ( $reading =~ /^wind_(gust|speed).*$/
  859. && looks_like_number($value)
  860. && $value < 0 );
  861. $value =~ s/^(\d+)%$/$1/;
  862. readingsBulkUpdate( $hash, $reading, $value );
  863. }
  864. }
  865. }
  866. elsif ( ref($h) eq "ARRAY" ) {
  867. my $i = 0;
  868. foreach ( @{$h} ) {
  869. if ( ref($_) eq "HASH" || ref($_) eq "ARRAY" ) {
  870. Wunderground_Hash2Readings( $hash, $_, $r . $i );
  871. }
  872. else {
  873. readingsBulkUpdate( $hash, $r . $i, $_ );
  874. }
  875. $i++;
  876. }
  877. }
  878. return "ok" if ( !$loop );
  879. }
  880. ###################################
  881. sub Wunderground_Undefine($$$) {
  882. my ( $hash, $a, $h ) = @_;
  883. my $name = $hash->{NAME};
  884. if ( defined( $hash->{fhem}{infix} ) ) {
  885. Wunderground_removeExtension( $hash->{fhem}{infix} );
  886. }
  887. Log3 $name, 5,
  888. "Wunderground $name: called function Wunderground_Undefine()";
  889. # Stop the internal GetStatus-Loop and exit
  890. RemoveInternalTimer($hash);
  891. # release reverse pointer
  892. delete $modules{Wunderground}{defptr}{$name};
  893. return;
  894. }
  895. 1;
  896. =pod
  897. =item device
  898. =item summary Get weather data and forecast from Weather Underground
  899. =item summary_DE Ruft Wetterdaten und Vorhersage von Weather Underground ab
  900. =begin html
  901. <a name="Wunderground" id="Wunderground"></a>
  902. <h3>Wunderground</h3>
  903. <ul>
  904. This module gets weather data and forecast from <a href="http://www.wunderground.com/">Weather Underground</a> weather service.
  905. <br><br>
  906. <a name="Wundergrounddefine"></a>
  907. <b>Define</b>
  908. <ul><br>
  909. <code>define &lt;name&gt; Wunderground &lt;api-key&gt; &lt;query&gt;</code>
  910. <br><br>
  911. Example:
  912. <ul><br>
  913. <code>
  914. define WUweather Wunderground d123ab11bb2c3456 IBAYERNM70<br>
  915. define WUweather Wunderground d123ab11bb2c3456 Germany/Berlin<br>
  916. </code><br>
  917. </ul>
  918. <br>
  919. </ul>
  920. <br><br>
  921. <a name="Wundergroundset"></a>
  922. <b>Set</b>
  923. <ul>
  924. <li>update - refresh data</li>
  925. </ul>
  926. <br><br>
  927. <a name="Wundergroundattr"></a>
  928. <b>Attributes</b>
  929. <ul>
  930. <li>pollInterval - Set regular polling interval in seconds (default=300)</li>
  931. <li>wu_bestfct - Use Weather Undergrond Best Forecast for forecast (default=1)</li>
  932. <li>wu_features - One or more of the data features to be fetched (default=astronomy,conditions,forecast)</li>
  933. <li>wu_lang - Returns the API response in the specified language (default=en)</li>
  934. <li>wu_pws - Use personal weather stations for conditions (default=1)</li>
  935. </ul>
  936. <br><br>
  937. </ul>
  938. =end html
  939. =begin html_DE
  940. <a name="Wunderground" id="Wunderground"></a>
  941. <h3>Wunderground</h3>
  942. <ul>
  943. Eine deutsche Version der Dokumentation ist derzeit nicht vorhanden. Die englische Version ist hier zu finden:
  944. </ul>
  945. <ul>
  946. <a href='http://fhem.de/commandref.html#Wunderground'>Wunderground</a>
  947. </ul>
  948. =end html_DE
  949. =cut