59_Wunderground.pm 40 KB

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