55_GDS.pm 66 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157
  1. # $Id: 55_GDS.pm 11734 2016-07-03 14:12:48Z betateilchen $
  2. # copyright and license informations
  3. =pod
  4. ###################################################################################################
  5. #
  6. # 55_GDS.pm
  7. #
  8. # An FHEM Perl module to retrieve data from "Deutscher Wetterdienst"
  9. #
  10. # Copyright: betateilchen ®
  11. #
  12. # includes: some patches provided by jensb
  13. # forecasts provided by jensb
  14. #
  15. # This file is part of fhem.
  16. #
  17. # Fhem is free software: you can redistribute it and/or modify
  18. # it under the terms of the GNU General Public License as published by
  19. # the Free Software Foundation, either version 2 of the License, or
  20. # (at your option) any later version.
  21. #
  22. # Fhem 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. # You should have received a copy of the GNU General Public License
  28. # along with fhem. If not, see <http://www.gnu.org/licenses/>.
  29. #
  30. ###################################################################################################
  31. =cut
  32. package main;
  33. use strict;
  34. use warnings;
  35. use feature qw/switch/;
  36. use Blocking;
  37. #use Archive::Extract;
  38. use Net::FTP;
  39. use XML::Simple;
  40. #use Data::Dumper;
  41. eval "use GDSweblink";
  42. no if $] >= 5.017011, warnings => 'experimental';
  43. my ($bulaList, $cmapList, %rmapList, $fmapList, %bula2bulaShort,
  44. %bulaShort2dwd, %dwd2Dir, %dwd2Name, $alertsXml, %capCityHash,
  45. %capCellHash, $sList, $aList, $fList, $fcList, $fcmapList, $tempDir, @weekdays);
  46. my %allForecastData;
  47. ###################################################################################################
  48. #
  49. # Main routines
  50. sub GDS_Initialize($) {
  51. my ($hash) = @_;
  52. my $name = $hash->{NAME};
  53. return "This module must not be used on microso... platforms!" if($^O =~ m/Win/);
  54. $hash->{DefFn} = "GDS_Define";
  55. $hash->{UndefFn} = "GDS_Undef";
  56. $hash->{DeleteFn} = "GDS_Delete";
  57. $hash->{GetFn} = "GDS_Get";
  58. $hash->{SetFn} = "GDS_Set";
  59. $hash->{RenameFn} = "GDS_Rename";
  60. $hash->{ShutdownFn} = "GDS_Shutdown";
  61. # $hash->{NotifyFn} = "GDS_Notify";
  62. $hash->{NOTIFYDEV} = "global";
  63. $hash->{AttrFn} = "GDS_Attr";
  64. no warnings 'qw';
  65. my @attrList = qw(
  66. disable:0,1
  67. gdsAll:0,1
  68. gdsDebug:0,1
  69. gdsFwName
  70. gdsFwType:0,1,2,3,4,5,6,7
  71. gdsHideFiles:0,1
  72. gdsLong:0,1
  73. gdsPassiveFtp:1,0
  74. gdsPolygon:0,1
  75. gdsSetForecast
  76. gdsUseAlerts:0,1
  77. gdsUseForecasts:0,1
  78. );
  79. use warnings 'qw';
  80. $hash->{AttrList} = join(" ", @attrList);
  81. $hash->{AttrList} .= " $readingFnAttributes";
  82. $tempDir = "/tmp/";
  83. _fillMappingTables($hash);
  84. }
  85. sub _fillMappingTables($){
  86. my ($hash) = @_;
  87. $tempDir = "/tmp/";
  88. $aList = "no_data";
  89. $sList = $aList;
  90. $fList = $aList;
  91. $fcList = $aList;
  92. $bulaList = "Baden-Württemberg,Bayern,Berlin,Brandenburg,Bremen,".
  93. "Hamburg,Hessen,Mecklenburg-Vorpommern,Niedersachsen,".
  94. "Nordrhein-Westfalen,Rheinland-Pfalz,Saarland,Sachsen,".
  95. "Sachsen-Anhalt,Schleswig-Holstein,Thüringen";
  96. $cmapList = "Deutschland,Mitte,Nordost,Nordwest,Ost,Suedost,Suedwest,West";
  97. %rmapList = (
  98. Deutschland => "",
  99. Mitte => "central/",
  100. Nordost => "northeast/",
  101. Nordwest => "northwest/",
  102. Ost => "east/",
  103. Suedost => "southeast/",
  104. Suedwest => "southwest/",
  105. West => "west/");
  106. $fmapList = "Deutschland_heute_frueh,Deutschland_heute_mittag,Deutschland_heute_spaet,Deutschland_heute_nacht,".
  107. "Deutschland_morgen_frueh,Deutschland_morgen_spaet,".
  108. "Deutschland_ueberm_frueh,Deutschland_ueberm_spaet,".
  109. "Deutschland_tag4_frueh,Deutschland_tag4_spaet,".
  110. "Mitte_heute_frueh,Mitte_heute_mittag,Mitte_heute_spaet,Mitte_heute_nacht,".
  111. "Mitte_morgen_frueh,Mitte_morgen_spaet,".
  112. "Mitte_ueberm_frueh,Mitte_ueberm_spaet,".
  113. "Mitte_tag4_frueh,Mitte_tag4_spaet,".
  114. "Nordost_heute_frueh,Nordost_heute_mittag,Nordost_heute_spaet,Nordost_heute_nacht,".
  115. "Nordost_morgen_frueh,Nordost_morgen_spaet,".
  116. "Nordost_ueberm_frueh,Nordost_ueberm_spaet,".
  117. "Nordost_tag4_frueh,Nordost_tag4_spaet,".
  118. "Nordwest_heute_frueh,Nordwest_heute_mittag,Nordwest_heute_spaet,Nordwest_heute_nacht,".
  119. "Nordwest_morgen_frueh,Nordwest_morgen_spaet,".
  120. "Nordwest_ueberm_frueh,Nordwest_ueberm_spaet,".
  121. "Nordwest_tag4_frueh,Nordwest_tag4_spaet,".
  122. "Ost_heute_frueh,Ost_heute_mittag,Ost_heute_spaet,Ost_heute_nacht,".
  123. "Ost_morgen_frueh,Ost_morgen_spaet,".
  124. "Ost_ueberm_frueh,Ost_ueberm_spaet,".
  125. "Ost_tag4_frueh,Ost_tag4_spaet,".
  126. "Suedost_heute_frueh,Suedost_heute_mittag,Suedost_heute_spaet,Suedost_heute_nacht,".
  127. "Suedost_morgen_frueh,Suedost_morgen_spaet,".
  128. "Suedost_ueberm_frueh,Suedost_ueberm_spaet,".
  129. "Suedost_tag4_frueh,Suedost_tag4_spaet,".
  130. "Suedwest_heute_frueh,Suedwest_heute_mittag,Suedwest_heute_spaet,Suedwest_heute_nacht,".
  131. "Suedwest_morgen_frueh,Suedwest_morgen_spaet,".
  132. "Suedwest_ueberm_frueh,Suedwest_ueberm_spaet,".
  133. "Suedwest_tag4_frueh,Suedwest_tag4_spaet,".
  134. "West_heute_frueh,West_heute_mittag,West_heute_spaet,West_heute_nacht,".
  135. "West_morgen_frueh,West_morgen_spaet,".
  136. "West_ueberm_frueh,West_ueberm_spaet,".
  137. "West_tag4_frueh,West_tag4_spaet";
  138. $fcmapList = "Deutschland_frueh,Deutschland_mittag,Deutschland_spaet,Deutschland_nacht,".
  139. "Deutschland_morgen_frueh,Deutschland_morgen_spaet,".
  140. "Deutschland_uebermorgen_frueh,Deutschland_uebermorgen_spaet,".
  141. "Deutschland_Tag4_frueh,Deutschland_Tag4_spaet,".
  142. "Mitte_frueh,Mitte_mittag,Mitte_spaet,Mitte_nacht,".
  143. "Mitte_morgen_frueh,Mitte_morgen_spaet,".
  144. "Mitte_uebermorgen_frueh,Mitte_uebermorgen_spaet,".
  145. "Mitte_Tag4_frueh,Mitte_Tag4_spaet,".
  146. "Nordost_frueh,Nordost_mittag,Nordost_spaet,Nordost_nacht,".
  147. "Nordost_morgen_frueh,Nordost_morgen_spaet,".
  148. "Nordost_uebermorgen_frueh,Nordost_uebermorgen_spaet,".
  149. "Nordost_Tag4_frueh,Nordost_Tag4_spaet,".
  150. "Nordwest_frueh,Nordwest_mittag,Nordwest_spaet,Nordwest_nacht,".
  151. "Nordwest_morgen_frueh,Nordwest_morgen_spaet,".
  152. "Nordwest_uebermorgen_frueh,Nordwest_uebermorgen_spaet,".
  153. "Nordwest_Tag4_frueh,Nordwest_Tag4_spaet,".
  154. "Ost_frueh,Ost_mittag,Ost_spaet,Ost_nacht,".
  155. "Ost_morgen_frueh,Ost_morgen_spaet,".
  156. "Ost_uebermorgen_frueh,Ost_uebermorgen_spaet,".
  157. "Ost_Tag4_frueh,Ost_Tag4_spaet,".
  158. "Suedost_frueh,Suedost_mittag,Suedost_spaet,Suedost_nacht,".
  159. "Suedost_morgen_frueh,Suedost_morgen_spaet,".
  160. "Suedost_uebermorgen_frueh,Suedost_uebermorgen_spaet,".
  161. "Suedost_Tag4_frueh,Suedost_Tag4_spaet,".
  162. "Suedwest_frueh,Suedwest_mittag,Suedwest_spaet,Suedwest_nacht,".
  163. "Suedwest_morgen_frueh,Suedwest_morgen_spaet,".
  164. "Suedwest_uebermorgen_frueh,Suedwest_uebermorgen_spaet,".
  165. "Suedwest_Tag4_frueh,Suedwest_Tag4_spaet,".
  166. "West_frueh,West_mittag,West_spaet,West_nacht,".
  167. "West_morgen_frueh,West_morgen_spaet,".
  168. "West_uebermorgen_frueh,West_uebermorgen_spaet,".
  169. "West_Tag4_frueh,West_Tag4_spaet";
  170. #
  171. # Bundesländer den entsprechenden Dienststellen zuordnen
  172. #
  173. %bula2bulaShort = (
  174. "baden-württemberg" => "bw",
  175. "bayern" => "by",
  176. "berlin" => "be",
  177. "brandenburg" => "bb",
  178. "bremen" => "hb",
  179. "hamburg" => "hh",
  180. "hessen" => "he",
  181. "mecklenburg-vorpommern" => "mv",
  182. "niedersachsen" => "ni",
  183. "nordrhein-westfalen" => "nw",
  184. "rheinland-pfalz" => "rp",
  185. "saarland" => "sl",
  186. "sachsen" => "sn",
  187. "sachsen-anhalt" => "st",
  188. "schleswig-holstein" => "sh",
  189. "thüringen" => "th",
  190. "deutschland" => "xde",
  191. "bodensee" => "xbo" );
  192. %bulaShort2dwd = (
  193. bw => "DWSG",
  194. by => "DWMG",
  195. be => "DWPG",
  196. bb => "DWPG",
  197. hb => "DWHG",
  198. hh => "DWHH",
  199. he => "DWOH",
  200. mv => "DWPH",
  201. ni => "DWHG",
  202. nw => "DWEH",
  203. rp => "DWOI",
  204. sl => "DWOI",
  205. sn => "DWLG",
  206. st => "DWLH",
  207. sh => "DWHH",
  208. th => "DWLI",
  209. xde => "xde",
  210. xbo => "xbo" );
  211. #
  212. # Dienststellen den entsprechenden Serververzeichnissen zuordnen
  213. #
  214. %dwd2Dir = (
  215. DWSG => "SU", # Stuttgart
  216. DWMG => "MS", # München
  217. DWPG => "PD", # Potsdam
  218. DWHG => "HA", # Hamburg
  219. DWHH => "HA", # Hamburg
  220. DWOH => "OF", # Offenbach
  221. DWPH => "PD", # Potsdam
  222. DWHG => "HA", # Hamburg
  223. DWEH => "EM", # Essen
  224. DWOI => "OF", # Offenbach
  225. DWLG => "LZ", # Leipzig
  226. DWLH => "LZ", # Leipzig
  227. DWLI => "LZ", # Leipzig
  228. DWHC => "HA", # Hamburg
  229. DWHB => "HA", # Hamburg
  230. DWPD => "PD", # Potsdam
  231. DWRW => "PD", # Potsdam
  232. DWEM => "EM", # Essen
  233. LSAX => "LZ", # Leipzig
  234. LSNX => "LZ", # Leipzig
  235. THLX => "LZ", # Leipzig
  236. DWOF => "OF", # Offenbach
  237. DWTR => "OF", # Offenbach
  238. DWSU => "SU", # Stuttgart
  239. DWMS => "MS", # München
  240. xde => "D",
  241. xbo => "Bodensee");
  242. # ???? => "FG" # Freiburg);
  243. %dwd2Name = (
  244. EM => "Essen",
  245. FG => "Freiburg",
  246. HA => "Hamburg",
  247. LZ => "Leipzig",
  248. MS => "München",
  249. OF => "Offenbach",
  250. PD => "Potsdam",
  251. SU => "Stuttgart");
  252. # German weekdays
  253. @weekdays = ("So", "Mo", "Di", "Mi", "Do", "Fr", "Sa");
  254. return;
  255. }
  256. sub GDS_Define($$$) {
  257. my ($hash, $def) = @_;
  258. my $name = $hash->{NAME};
  259. my @a = split("[ \t][ \t]*", $def);
  260. my $skuser = getKeyValue($name."_user");
  261. my $skpass = getKeyValue($name."_pass");
  262. my $skhost = getKeyValue($name."_host");
  263. unless(defined($skuser) && defined($skpass)) {
  264. return "syntax: define <name> GDS <username> <password> [<host>]" if(int(@a) < 4 );
  265. }
  266. $hash->{helper}{URL} = defined($a[4]) ? $a[4] : "ftp-outgoing2.dwd.de";
  267. $hash->{helper}{INTERVAL} = 1200;
  268. setKeyValue($name."_user",$a[2]) unless(defined($skuser));
  269. setKeyValue($name."_pass",$a[3]) unless(defined($skpass));
  270. setKeyValue($name."_host",$hash->{helper}{URL}) unless(defined($skhost));
  271. $hash->{DEF} = undef;
  272. Log3($name, 4, "GDS $name: created");
  273. Log3($name, 4, "GDS $name: tempDir=".$tempDir);
  274. _GDS_addExtension("GDS_CGI","gds","GDS Files");
  275. # $hash->{firstrun} = 1;
  276. # retrieveData($hash,'conditions') unless (time > 1457996400);
  277. # delete $hash->{firstrun};
  278. readingsSingleUpdate($hash, 'state', 'active',1);
  279. return undef;
  280. }
  281. sub GDS_Undef($$) {
  282. my ($hash, $arg) = @_;
  283. my $name = $hash->{NAME};
  284. RemoveInternalTimer($hash);
  285. my $url = '/gds';
  286. delete $data{FWEXT}{$url} if int(devspec2array('TYPE=GDS')) == 1;
  287. return undef;
  288. }
  289. sub GDS_Delete() {
  290. my ($hash, $arg) = @_;
  291. my $name = $hash->{NAME};
  292. setKeyValue($name."_user",undef);
  293. setKeyValue($name."_pass",undef);
  294. setKeyValue($name."_host",undef);
  295. }
  296. sub GDS_Rename() {
  297. my ($new,$old) = @_;
  298. setKeyValue($new."_user",getKeyValue($old."_user"));
  299. setKeyValue($new."_pass",getKeyValue($old."_pass"));
  300. setKeyValue($new."_host",getKeyValue($old."_host"));
  301. setKeyValue($old."_user",undef);
  302. setKeyValue($old."_pass",undef);
  303. setKeyValue($old."_host",undef);
  304. return undef;
  305. }
  306. sub GDS_Shutdown($) {
  307. my ($hash) = @_;
  308. my $name = $hash->{NAME};
  309. RemoveInternalTimer($hash);
  310. Log3 ($name,4,"GDS $name: shutdown requested");
  311. return undef;
  312. }
  313. sub GDS_Notify ($$) {
  314. my ($hash,$dev) = @_;
  315. my $name = $hash->{NAME};
  316. return if($dev->{NAME} ne "global");
  317. my $type = $dev->{CHANGED}[0];
  318. return unless (grep(m/^INITIALIZED/, $type));
  319. $aList = "disabled_by_attribute" unless AttrVal($name,'gdsUseAlerts',0);
  320. $fList = "disabled_by_attribute" unless AttrVal($name,'gdsUseForecasts',0);
  321. $fcmapList = "disabled_by_attribute" unless AttrVal($name,'gdsUseForecasts',0);
  322. GDS_GetUpdate($hash);
  323. return undef;
  324. }
  325. sub GDS_Set($@) {
  326. my ($hash, @a) = @_;
  327. my $name = $hash->{NAME};
  328. my $usage = "Unknown argument, choose one of ".
  329. "clear:alerts,conditions,forecasts,all ".
  330. "forecasts:$fList ".
  331. "help:noArg ".
  332. "update:noArg "; ;
  333. my $command = lc($a[1]);
  334. my $parameter = $a[2] if(defined($a[2]));
  335. my ($result, $next);
  336. return $usage if $command eq '?';
  337. if(IsDisabled($name)) {
  338. readingsSingleUpdate($hash, 'state', 'disabled', 0);
  339. return "GDS $name is disabled. Aborting..." if IsDisabled($name);
  340. }
  341. readingsSingleUpdate($hash, 'state', 'active', 0);
  342. given($command) {
  343. when("clear"){
  344. CommandDeleteReading(undef, "$name a_.*")
  345. if(defined($parameter) && ($parameter eq "all" || $parameter eq "alerts"));
  346. CommandDeleteReading(undef, "$name c_.*")
  347. if(defined($parameter) && ($parameter eq "all" || $parameter eq "conditions"));
  348. CommandDeleteReading(undef, "$name g_.*")
  349. if(defined($parameter) && ($parameter eq "all" || $parameter eq "conditions"));
  350. CommandDeleteReading(undef, "$name fc.?_.*")
  351. if(defined($parameter) && ($parameter eq "all" || $parameter eq "forecasts"));
  352. }
  353. when("help"){
  354. $result = setHelp();
  355. break;
  356. }
  357. when("update"){
  358. RemoveInternalTimer($hash);
  359. GDS_GetUpdate($hash,'set update');
  360. break;
  361. }
  362. when("forecasts"){
  363. return "Error: Forecasts disabled by attribute." unless AttrVal($name,'gdsUseForecasts',0);
  364. CommandDeleteReading(undef, "$name fc.*") if($parameter ne AttrVal($name,'gdsSetForecast',''));
  365. $attr{$name}{gdsSetForecast} = $parameter;
  366. GDS_GetUpdate($hash,'set forecasts');
  367. break;
  368. }
  369. default { return $usage; };
  370. }
  371. return $result;
  372. }
  373. sub GDS_Get($@) {
  374. my ($hash, @a) = @_;
  375. my $command = lc($a[1]);
  376. my $parameter = $a[2] if(defined($a[2]));
  377. my $name = $hash->{NAME};
  378. my $usage = "Unknown argument $command, choose one of help:noArg rereadcfg:noArg ".
  379. "list:capstations ".
  380. "alerts:".$aList." ".
  381. "conditionsmap:".$cmapList." ".
  382. "forecasts:".$fcList." ".
  383. "forecastsmap:".$fmapList." ".
  384. "headlines ".
  385. "radarmap:".$cmapList." ".
  386. "warningsmap:"."Deutschland,Bodensee,".$bulaList." ".
  387. "warnings:".$bulaList;
  388. return $usage if $command eq '?';
  389. if(IsDisabled($name)) {
  390. readingsSingleUpdate($hash, 'state', 'disabled', 0);
  391. return "GDS $name is disabled. Aborting..." if IsDisabled($name);
  392. }
  393. readingsSingleUpdate($hash, 'state', 'active', 0);
  394. my $_gdsAll = AttrVal($name,"gdsAll", 0);
  395. my $gdsDebug = AttrVal($name,"gdsDebug", 0);
  396. my ($result, @datensatz, $found);
  397. given($command) {
  398. when("conditionsmap"){
  399. # retrieve map: current conditions
  400. $hash->{file}{dir} = "gds/specials/observations/maps/germany/";
  401. $hash->{file}{dwd} = $parameter."*";
  402. $hash->{file}{target} = $tempDir.$name."_conditionsmap.jpg";
  403. retrieveData($hash,'FILE');
  404. break;
  405. }
  406. when("forecastsmap"){
  407. # retrieve map: forecasts
  408. $hash->{file}{dir} = "gds/specials/forecasts/maps/germany/";
  409. $hash->{file}{dwd} = $parameter."*";
  410. $hash->{file}{target} = $tempDir.$name."_forecastsmap.jpg";
  411. retrieveData($hash,'FILE');
  412. break;
  413. }
  414. when("headlines"){
  415. return "Error: Alerts disabled by attribute." unless AttrVal($name,'gdsUseAlerts',0);
  416. $parameter //= "|";
  417. return gdsAlertsHeadlines($name,$parameter);
  418. }
  419. when("warningsmap"){
  420. # retrieve map: warnings
  421. if(length($parameter) != 2){
  422. $parameter = $bula2bulaShort{lc($parameter)};
  423. }
  424. $hash->{file}{dwd} = "Schilder".$dwd2Dir{$bulaShort2dwd{lc($parameter)}}.".jpg";
  425. $hash->{file}{dir} = "gds/specials/alerts/maps/";
  426. $hash->{file}{target} = $tempDir.$name."_warningsmap.jpg";
  427. retrieveData($hash,'FILE');
  428. break;
  429. }
  430. when("radarmap"){
  431. # retrieve map: radar
  432. $parameter = ucfirst($parameter);
  433. $hash->{file}{dir} = "gds/specials/radar/".$rmapList{$parameter};
  434. $hash->{file}{dwd} = "Webradar_".$parameter."*";
  435. $hash->{file}{target} = $tempDir.$name."_radarmap.jpg";
  436. retrieveData($hash,'FILE');
  437. break;
  438. }
  439. when("help"){
  440. $result = getHelp();
  441. break;
  442. }
  443. when("list"){
  444. given($parameter){
  445. when("capstations") {
  446. return "Error: Alerts disabled by attribute." unless AttrVal($name,'gdsUseAlerts',0);
  447. $result = getListCapStations($hash,$parameter); }
  448. default { $usage = "get <name> list <parameter>"; return $usage; }
  449. }
  450. break;
  451. }
  452. when("alerts"){
  453. return "Error: Alerts disabled by attribute." unless AttrVal($name,'gdsUseAlerts',0);
  454. if($parameter =~ y/0-9// == length($parameter)){
  455. while ( my( $key, $val ) = each %capCellHash ) {
  456. push @datensatz,$val if $key =~ m/^$parameter/;
  457. }
  458. # push @datensatz,$capCellHash{$parameter};
  459. } else {
  460. push @datensatz,$capCityHash{$parameter};
  461. }
  462. CommandDeleteReading(undef, "$name a_.*");
  463. if($datensatz[0]){
  464. my $anum = 0;
  465. foreach(@datensatz) {
  466. decodeCAPData($hash,$_,$anum);
  467. $anum++;
  468. };
  469. readingsSingleUpdate($hash,'a_count',$anum,1);
  470. } else {
  471. $result = "Keine Warnmeldung für die gesuchte Region vorhanden.";
  472. }
  473. my $_gdsAll = AttrVal($name,"gdsAll", 0);
  474. my $gdsDebug = AttrVal($name,"gdsDebug", 0);
  475. break;
  476. }
  477. when("rereadcfg"){
  478. DoTrigger($name, "REREAD", 1);
  479. $hash->{GDS_REREAD} = int(time());
  480. if (AttrVal($name,'gdsUseAlerts',0)) {
  481. %capCityHash = ();
  482. %capCellHash = ();
  483. retrieveData($hash,'capdata');
  484. retrieveListCapStations($hash);
  485. }
  486. retrieveData($hash,'forecast') if AttrVal($name,'gdsUseForecasts',0);
  487. # GDS_GetUpdate($hash);
  488. break;
  489. }
  490. when("warnings"){
  491. my $vhdl;
  492. $result = " VHDL30 = current | VHDL31 = weekend or holiday\n".
  493. " VHDL32 = preliminary | VHDL33 = cancel VHDL32\n".
  494. sepLine(31)."+".sepLine(38)."\n";
  495. if(length($parameter) != 2){
  496. $parameter = $bula2bulaShort{lc($parameter)};
  497. }
  498. my $dwd = $bulaShort2dwd{lc($parameter)};
  499. my $dir = "gds/specials/warnings/".$dwd2Dir{$dwd}."/";
  500. $hash->{file}{dir} = $dir;
  501. for ($vhdl=30; $vhdl <=33; $vhdl++){
  502. my $dwd2 = "VHDL".$vhdl."_".$dwd."*";
  503. my $target = $tempDir.$name."_warnings_$vhdl";
  504. unlink $target;
  505. $hash->{file}{dwd} = $dwd2;
  506. $hash->{file}{target} = $target;
  507. retrieveData($hash,'FILE');
  508. }
  509. sleep 2;
  510. for ($vhdl=30; $vhdl <=33; $vhdl++){
  511. my $target = $tempDir.$name."_warnings_$vhdl";
  512. $result .= retrieveText($hash, "warnings_$vhdl", "") if (-e $target);
  513. $result .= "\n".sepLine(70);
  514. }
  515. $result .= "\n\n";
  516. break;
  517. }
  518. when("forecasts"){
  519. return "Error: Forecasts disabled by attribute." unless AttrVal($name,'gdsUseForecasts',0);
  520. $parameter = "Daten_$parameter";
  521. my ($k,$v,$data);
  522. $result = sepLine(67)."\n";
  523. # retrieve from hash
  524. $data = undef;
  525. while(($k, $v) = each %allForecastData){
  526. if ($k eq $parameter) {
  527. $data = $v;
  528. last;
  529. };
  530. }
  531. $data //= "No forecast data found.";
  532. $data =~ s/\$/\n/g;
  533. $result .= $data;
  534. $result .= "\n".sepLine(67)."\n";
  535. break;
  536. }
  537. default { return $usage; };
  538. }
  539. return $result;
  540. }
  541. sub GDS_Attr(@){
  542. my @a = @_;
  543. my $hash = $defs{$a[1]};
  544. my ($cmd, $name, $attrName, $attrValue) = @a;
  545. $attrValue //= '';
  546. my $useUpdate = 0;
  547. given($attrName){
  548. when("gdsUseAlerts"){
  549. if ($attrValue == 0 || $cmd eq 'del') {
  550. $aList = "disabled_by_attribute";
  551. } else {
  552. $aList = "data_retrieval_running";
  553. retrieveData($hash,'capdata');
  554. retrieveListCapStations($hash);
  555. }
  556. }
  557. when("gdsUseForecasts"){
  558. if ($attrValue == 0 || $cmd eq 'del') {
  559. $fList = "disabled_by_attribute";
  560. $fcList = "disabled_by_attribute";
  561. } else {
  562. $fcList = $fcmapList;
  563. $fList = "data_retrieval_running";
  564. $attr{$name}{$attrName} = $attrValue;
  565. retrieveData($hash,'forecast');
  566. $useUpdate = 1;
  567. }
  568. }
  569. when("gdsHideFiles"){
  570. my $hR = AttrVal($FW_wname,'hiddenroom','');
  571. $hR =~ s/\,GDS.Files//g;
  572. if($attrValue) {
  573. $hR .= "," if(length($hR));
  574. $hR .= "GDS Files";
  575. }
  576. CommandAttr(undef,"$FW_wname hiddenroom $hR");
  577. break;
  578. }
  579. default {$attr{$name}{$attrName} = $attrValue;}
  580. }
  581. if(IsDisabled($name)) {
  582. readingsSingleUpdate($hash, 'state', 'disabled', 0);
  583. } else {
  584. readingsSingleUpdate($hash, 'state', 'active', 0);
  585. if ($useUpdate) {
  586. RemoveInternalTimer($hash);
  587. my $next = gettimeofday()+$hash->{helper}{INTERVAL};
  588. InternalTimer($next, "GDS_GetUpdate", $hash, 0);
  589. }
  590. }
  591. return;
  592. }
  593. sub GDS_GetUpdate($;$) {
  594. my ($hash,$local) = @_;
  595. $local //= 0;
  596. my $name = $hash->{NAME};
  597. RemoveInternalTimer($hash);
  598. my $fs = AttrVal($name, "gdsSetForecast", 0);
  599. if(IsDisabled($name)) {
  600. readingsSingleUpdate($hash, 'state', 'disabled', 0);
  601. Log3 ($name, 2, "GDS $name is disabled, data update cancelled.");
  602. } else {
  603. readingsSingleUpdate($hash, 'state', 'active', 0);
  604. if($fs) {
  605. retrieveData($hash,'forecast') ;
  606. my @a;
  607. push @a, undef;
  608. push @a, undef;
  609. push @a, $fs;
  610. retrieveForecasts($hash, "fc", @a);
  611. }
  612. }
  613. # schedule next update
  614. my $next = gettimeofday()+$hash->{helper}{INTERVAL};
  615. my $gdsAll = AttrVal($name,"gdsAll", 0);
  616. my $gdsDebug = AttrVal($name,"gdsDebug", 0);
  617. InternalTimer($next, "GDS_GetUpdate", $hash, 1);
  618. readingsSingleUpdate($hash, "_nextUpdate", localtime($next), 1) if($gdsAll || $gdsDebug);
  619. return 1;
  620. }
  621. ###################################################################################################
  622. #
  623. # FWEXT implementation
  624. sub _GDS_addExtension($$$) {
  625. my ($func,$link,$friendlyname)= @_;
  626. my $url = "/" . $link;
  627. Log3(undef,4,"Register gds webservice in FWEXT");
  628. $data{FWEXT}{$url}{FUNC} = $func;
  629. $data{FWEXT}{$url}{LINK} = "+$link";
  630. $data{FWEXT}{$url}{NAME} = $friendlyname;
  631. $data{FWEXT}{$url}{FORKABLE} = 0;
  632. }
  633. sub GDS_CGI {
  634. my ($request) = @_;
  635. my ($name,$ext)= _GDS_splitRequest($request);
  636. if(defined($name)) {
  637. my $filename= "$tempDir/$name.$ext";
  638. my $MIMEtype= filename2MIMEType($filename);
  639. my @contents;
  640. if(open(INPUTFILE, $filename)) {
  641. binmode(INPUTFILE);
  642. @contents= <INPUTFILE>;
  643. close(INPUTFILE);
  644. return("$MIMEtype; charset=utf-8", join("", @contents));
  645. } else {
  646. return("text/plain; charset=utf-8", "File not found: $filename");
  647. }
  648. } else {
  649. return _GDS_Overview();
  650. }
  651. }
  652. sub _GDS_splitRequest($) {
  653. my ($request) = @_;
  654. if($request =~ /^.*\/gds$/) {
  655. # http://localhost:8083/fhem/gds2
  656. return (undef,undef); # name, ext
  657. } else {
  658. my $call= $request;
  659. $call =~ s/^.*\/gds\/([^\/]*)$/$1/;
  660. my $name= $call;
  661. $name =~ s/^(.*)\.(jpg)$/$1/;
  662. my $ext= $call;
  663. $ext =~ s/^$name\.(.*)$/$1/;
  664. return ($name,$ext);
  665. }
  666. }
  667. sub _GDS_Overview {
  668. my ($name, $url);
  669. my $html= __GDS_HTMLHead("GDS Overview") . "<body>\n\n";
  670. foreach my $def (sort keys %defs) {
  671. if($defs{$def}{TYPE} eq "GDS") {
  672. $name = $defs{$def}{NAME};
  673. $url = __GDS_getURL();
  674. $html .= "$name<br>\n<ul>\n";
  675. $html .= "<a href=\"$url/gds/$name\_conditionsmap.jpg\" target=\"_blank\">Aktuelle Wetterkarte: Wetterlage</a><br/>\n";
  676. $html .= "<a href=\"$url/gds/$name\_forecastsmap.jpg\" target=\"_blank\">Aktuelle Wetterkarte: Vorhersage</a><br/>\n";
  677. $html .= "<a href=\"$url/gds/$name\_warningsmap.jpg\" target=\"_blank\">Aktuelle Wetterkarte: Warnungen</a><br/>\n";
  678. $html .= "<a href=\"$url/gds/$name\_radarmap.jpg\" target=\"_blank\">Aktuelle Wetterkarte: Radarkarte</a><br/>\n";
  679. $html .= "</ul>\n\n";
  680. }
  681. }
  682. $html.="</body>\n" . __GDS_HTMLTail();
  683. return ("text/html; charset=utf-8", $html);
  684. }
  685. sub __GDS_HTMLHead($) {
  686. my ($title) = @_;
  687. my $doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" '.
  688. '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
  689. my $xmlns = 'xmlns="http://www.w3.org/1999/xhtml"';
  690. my $code = "$doctype\n<html $xmlns>\n<head>\n<title>$title</title>\n</head>\n";
  691. return $code;
  692. }
  693. sub __GDS_getURL {
  694. my $proto = (AttrVal($FW_wname, 'HTTPS', 0) == 1) ? 'https' : 'http';
  695. return $proto."://$FW_httpheader{Host}$FW_ME"; #".$FW_ME;
  696. }
  697. sub __GDS_HTMLTail {
  698. return "</html>";
  699. }
  700. ###################################################################################################
  701. #
  702. # Tools
  703. sub gdsAlertsHeadlines($;$) {
  704. my ($d,$sep) = @_;
  705. my $text = "";
  706. $sep = (defined($sep)) ? $sep : '|';
  707. my $count = ReadingsVal($d,'a_count',0);
  708. for (my $i = 0; $i < $count; $i++) {
  709. $text .= $sep if $i;
  710. $text .= ReadingsVal('gds','a_'.$i.'_headline','')
  711. }
  712. return $text;
  713. }
  714. sub gdsHeadlines($;$) {
  715. my $text = "GDS error: gdsHeadlines() is deprecated. Please use gdsAlertsHeadlines()";
  716. Log 1, $text;
  717. return $text;
  718. }
  719. sub setHelp(){
  720. return "Use one of the following commands:\n".
  721. sepLine(35)."\n".
  722. "set <name> clear alerts|all\n".
  723. "set <name> forecasts <regionName>/<stationName>\n".
  724. "set <name> help\n".
  725. "set <name> rereadcfg\n".
  726. "set <name> update\n";
  727. }
  728. sub getHelp(){
  729. return "Use one of the following commands:\n".
  730. sepLine(35)."\n".
  731. "get <name> alerts <region>\n".
  732. "get <name> forecasts <regionName>\n".
  733. "get <name> help\n".
  734. "get <name> list capstations|data\n".
  735. "get <name> rereadcfg\n".
  736. "get <name> warnings <region>\n";
  737. }
  738. sub sepLine($;$) {
  739. my ($len,$c) = @_;
  740. $c //= '-';
  741. my ($output, $i);
  742. for ($i=0; $i<$len; $i++) { $output .= $c; }
  743. return $output;
  744. }
  745. sub _readDir($) {
  746. my ($destinationDirectory) = @_;
  747. eval { opendir(DIR,$destinationDirectory) or warn "$!"; };
  748. if ($@) {
  749. Log3(undef,1,'GDS: file system error '.$@);
  750. return ("");
  751. }
  752. my @files = readdir(DIR);
  753. close(DIR);
  754. return @files;
  755. }
  756. sub retrieveText($$$) {
  757. my ($hash, $fileName, $separator) = @_;
  758. my $name = $hash->{NAME};
  759. my ($err,@a);
  760. $fileName = $tempDir.$name."_$fileName";
  761. ($err,@a) = FileRead({FileName=>$fileName,ForceType=>"file" });
  762. return "GDS error reading $fileName" if($err);
  763. @a = map (latin1ToUtf8($_), @a);
  764. return join($separator, @a);
  765. }
  766. sub gds_calctz($@) {
  767. my ($nt,@lt) = @_;
  768. my $off = $lt[2]*3600+$lt[1]*60+$lt[0];
  769. $off = 12*3600-$off;
  770. $nt += $off; # This is noon, localtime
  771. my @gt = gmtime($nt);
  772. return (12-$gt[2]);
  773. }
  774. ###################################################################################################
  775. #
  776. # Data retrieval (internal)
  777. sub getListCapStations($$){
  778. my ($hash, $command) = @_;
  779. my $name = $hash->{NAME};
  780. my (%capHash, $file, @columns, $key, $cList, $count);
  781. $file = $tempDir.'capstations.csv';
  782. return "GDS error: $file not found." unless(-e $file);
  783. # CSV öffnen und parsen
  784. my ($err,@a) = FileRead({FileName=>$file,ForceType=>"file" });
  785. return "GDS error reading $file" if($err);
  786. foreach my $l (@a) {
  787. next if (substr($l,0,1) eq '#');
  788. @columns = split(";",$l);
  789. $capHash{latin1ToUtf8($columns[4])} = $columns[0];
  790. }
  791. # Ausgabe sortieren und zusammenstellen
  792. foreach $key (sort keys %capHash) {
  793. $cList .= $capHash{$key}."\t".$key."\n";
  794. }
  795. return $cList;
  796. }
  797. sub retrieveListCapStations($){
  798. my ($hash) = @_;
  799. $hash->{file}{dir} = "gds/help/";
  800. $hash->{file}{dwd} = "legend_warnings_CAP_WarnCellsID.csv";
  801. $hash->{file}{target} = $tempDir."capstations.csv";
  802. unless(-e $hash->{file}{target}) {
  803. retrieveData($hash,'FILE');
  804. } else {
  805. # read capstationslist once a day
  806. my $alter = time() - (stat($hash->{file}{target}))[9];
  807. retrieveData($hash,'FILE') if ($alter > 86400);
  808. }
  809. }
  810. sub decodeCAPData($$$){
  811. my ($hash, $datensatz, $anum) = @_;
  812. my $name = $hash->{NAME};
  813. my $info = 9999; # to be deleted
  814. my $alert = int($datensatz/100);
  815. my $area = $datensatz-$alert*100;
  816. my (%readings, @dummy, $i, $k, $n, $v, $t);
  817. my $gdsAll = AttrVal($name,"gdsAll", 0);
  818. my $gdsDebug = AttrVal($name,"gdsDebug", 0);
  819. my $gdsLong = AttrVal($name,"gdsLong", 0);
  820. my $gdsPolygon = AttrVal($name,"gdsPolygon", 0);
  821. Log3($name, 4, "GDS $name: Decoding CAP record #".$datensatz);
  822. # topLevel informations
  823. if($gdsAll || $gdsDebug) {
  824. @dummy = split(/\./, $alertsXml->{alert}[$alert]{identifier});
  825. $readings{"a_".$anum."_identifier"} = $alertsXml->{alert}[$alert]{identifier};
  826. $readings{"a_".$anum."_idPublisher"} = $dummy[5];
  827. $readings{"a_".$anum."_idSysten"} = $dummy[6];
  828. $readings{"a_".$anum."_idTimeStamp"} = $dummy[7];
  829. $readings{"a_".$anum."_idIndex"} = $dummy[8];
  830. }
  831. $readings{"a_".$anum."_sent"} = $alertsXml->{alert}[$alert]{sent};
  832. $readings{"a_".$anum."_status"} = $alertsXml->{alert}[$alert]{status};
  833. $readings{"a_".$anum."_msgType"} = $alertsXml->{alert}[$alert]{msgType};
  834. # infoSet informations
  835. if($gdsAll || $gdsDebug) {
  836. $readings{"a_".$anum."_language"} = $alertsXml->{alert}[$alert]{info}{language};
  837. $readings{"a_".$anum."_urgency"} = $alertsXml->{alert}[$alert]{info}{urgency};
  838. $readings{"a_".$anum."_severity"} = $alertsXml->{alert}[$alert]{info}{severity};
  839. $readings{"a_".$anum."_certainty"} = $alertsXml->{alert}[$alert]{info}{certainty};
  840. }
  841. $readings{"a_".$anum."_category"} = $alertsXml->{alert}[$alert]{info}{category};
  842. $readings{"a_".$anum."_event"} = $alertsXml->{alert}[$alert]{info}{event};
  843. $readings{"a_".$anum."_responseType"} = $alertsXml->{alert}[$alert]{info}{responseType};
  844. # eventCode informations
  845. # loop through array
  846. $i = 0;
  847. while(1){
  848. ($n, $v) = (undef, undef);
  849. $n = $alertsXml->{alert}[$alert]{info}{eventCode}[$i]{valueName};
  850. if(!$n) {last;}
  851. $n = "a_".$anum."_eventCode_".$n;
  852. $v = $alertsXml->{alert}[$alert]{info}{eventCode}[$i]{value};
  853. $readings{$n} .= $v." " if($v);
  854. $i++;
  855. }
  856. # time/validity informations
  857. $readings{"a_".$anum."_effective"} = $alertsXml->{alert}[$alert]{info}{effective} if($gdsAll);
  858. $readings{"a_".$anum."_onset"} = $alertsXml->{alert}[$alert]{info}{onset};
  859. $readings{"a_".$anum."_expires"} = $alertsXml->{alert}[$alert]{info}{expires};
  860. $readings{"a_".$anum."_valid"} = _checkCAPValid($readings{"a_".$anum."_onset"},$readings{"a_".$anum."_expires"});
  861. $readings{"a_".$anum."_onset_local"} = _capTrans($readings{"a_".$anum."_onset"});
  862. $readings{"a_".$anum."_expires_local"} = _capTrans($readings{"a_".$anum."_expires"})
  863. if(defined($alertsXml->{alert}[$alert]{info}{expires}));
  864. $readings{"a_".$anum."_sent_local"} = _capTrans($readings{"a_".$anum."_sent"});
  865. $readings{a_valid} = ReadingsVal($name,'a_valid',0) || $readings{"a_".$anum."_valid"};
  866. # text informations
  867. $readings{"a_".$anum."_headline"} = $alertsXml->{alert}[$alert]{info}{headline};
  868. $readings{"a_".$anum."_description"} = $alertsXml->{alert}[$alert]{info}{description} if($gdsAll || $gdsLong);
  869. $readings{"a_".$anum."_instruction"} = $alertsXml->{alert}[$alert]{info}{instruction}
  870. if($readings{"a_".$anum."_responseType"} eq "Prepare" & ($gdsAll || $gdsLong));
  871. # area informations
  872. $readings{"a_".$anum."_areaDesc"} = $alertsXml->{alert}[$alert]{info}{area}[$area]{areaDesc};
  873. $readings{"a_".$anum."_areaPolygon"} = $alertsXml->{alert}[$alert]{info}{area}[$area]{polygon} if($gdsAll || $gdsPolygon);
  874. # area geocode informations
  875. # loop through array
  876. $i = 0;
  877. while(1){
  878. ($n, $v) = (undef, undef);
  879. $n = $alertsXml->{alert}[$alert]{info}{area}[$area]{geocode}[$i]{valueName};
  880. if(!$n) {last;}
  881. $n = "a_".$anum."_geoCode_".$n;
  882. $v = $alertsXml->{alert}[$alert]{info}{area}[$area]{geocode}[$i]{value};
  883. $readings{$n} .= $v." " if($v);
  884. $i++;
  885. }
  886. $readings{"a_".$anum."_altitude"} = $alertsXml->{alert}[$alert]{info}{area}[$area]{altitude} if($gdsAll);
  887. $readings{"a_".$anum."_ceiling"} = $alertsXml->{alert}[$alert]{info}{area}[$area]{ceiling} if($gdsAll);
  888. readingsBeginUpdate($hash);
  889. readingsBulkUpdate($hash, "_dataSource", "Quelle: Deutscher Wetterdienst");
  890. while(($k, $v) = each %readings){
  891. # skip update if no valid data is available
  892. next unless(defined($v));
  893. readingsSingleUpdate($hash, $k, latin1ToUtf8($v),1);
  894. }
  895. # convert color value to hex
  896. my $r = ReadingsVal($name, 'a_'.$anum.'_eventCode_AREA_COLOR', '');
  897. if(length($r)) {
  898. my $v = sprintf( "%02x%02x%02x", split(" ", $r));
  899. readingsSingleUpdate($hash, 'a_'.$anum.'_eventCode_AREA_COLOR_hex', $v,1);
  900. }
  901. readingsEndUpdate($hash, 1);
  902. return;
  903. }
  904. sub _checkCAPValid($$;$$){
  905. my ($onset,$expires,$t,$tmax) = @_;
  906. my $valid = 0;
  907. $t = time() if (!defined($t));
  908. my $offset = gds_calctz($t,localtime($t))*3600;
  909. $t -= $offset;
  910. $tmax -= $offset if (defined($tmax));
  911. $onset =~ s/T/ /;
  912. $onset =~ s/\+/ \+/;
  913. $onset = time_str2num($onset);
  914. $expires =~ s/T/ /;
  915. $expires =~ s/\+/ \+/;
  916. $expires = time_str2num($expires);
  917. if (defined($tmax)) {
  918. $valid = 1 if($tmax ge $onset && $t lt $expires);
  919. } else {
  920. $valid = 1 if($onset lt $t && $expires gt $t);
  921. }
  922. return $valid;
  923. }
  924. sub _capTrans($) {
  925. my ($t) = @_;
  926. my $valid = 0;
  927. my $offset = gds_calctz(time,localtime(time))*3600; # used from 99_SUNRISE_EL
  928. $t =~ s/T/ /;
  929. $t =~ s/\+/ \+/;
  930. $t = time_str2num($t);
  931. my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($t+$offset);
  932. $mon += 1;
  933. $year += 1900;
  934. $t = sprintf "%02s.%02s.%02s %02s:%02s:%02s", $mday, $mon, $year, $hour, $min, $sec;
  935. return $t;
  936. }
  937. ###################################################################################################
  938. #
  939. # nonblocking data retrieval
  940. sub retrieveData($$){
  941. my ($hash,$req) = @_;
  942. $req = uc($req);
  943. if ($req eq "FORECAST") {
  944. my $busyTag = "GDS_".$req."_BUSY";
  945. if (defined($hash->{$busyTag})) {
  946. return;
  947. } else {
  948. $hash->{$busyTag} = localtime(time());
  949. }
  950. }
  951. my $tag = "GDS_".$req."_READ";
  952. delete $hash->{$tag};
  953. $tag = "GDS_".$req."_ABORTED";
  954. delete $hash->{$tag};
  955. BlockingCall("_retrieve$req",$hash,"_finished$req",60,"_aborted$req",$hash);
  956. }
  957. # any file
  958. sub _retrieveFILE {
  959. my ($hash) = shift;
  960. my $name = $hash->{NAME};
  961. my $user = getKeyValue($name."_user");
  962. my $pass = getKeyValue($name."_pass");
  963. my $host = getKeyValue($name."_host");
  964. my $proxyName = AttrVal($name, "gdsProxyName", "");
  965. my $proxyType = AttrVal($name, "gdsProxyType", "");
  966. my $passive = AttrVal($name, "gdsPassiveFtp", 1);
  967. my $dir = $hash->{file}{dir};
  968. my $dwd = $hash->{file}{dwd};
  969. my $target = $hash->{file}{target};
  970. my $ret = "";
  971. eval {
  972. my $ftp = Net::FTP->new( $host,
  973. Debug => 0,
  974. Timeout => 10,
  975. Passive => $passive,
  976. FirewallType => $proxyType,
  977. Firewall => $proxyName);
  978. if(defined($ftp)){
  979. Log3($name, 4, "GDS $name: ftp connection established.");
  980. $ftp->login($user, $pass);
  981. $ftp->binary;
  982. $ftp->cwd($dir);
  983. my @files = $ftp->ls($dwd);
  984. if(@files) {
  985. @files = sort(@files);
  986. $dwd = $files[-1];
  987. Log3($name, 4, "GDS $name: file found.");
  988. Log3($name, 4, "GDS $name: retrieving $dwd");
  989. if(defined($target)) {
  990. $ftp->get($dwd,$target);
  991. my $s = -s $target;
  992. Log3($name, 4, "GDS: ftp transferred $s bytes");
  993. } else {
  994. my ($file_content,$file_handle);
  995. open($file_handle, '>', \$file_content);
  996. $ftp->get($dwd,$file_handle);
  997. $file_content = latin1ToUtf8($file_content);
  998. $file_content =~ s/\r\n/\$/g;
  999. $ret = $file_content;
  1000. }
  1001. }
  1002. $ftp->quit;
  1003. }
  1004. };
  1005. return "$name;;;$dwd;;;$ret";
  1006. }
  1007. sub _finishedFILE {
  1008. my ($name,$file,$ret) = split(/;;;/,shift); #@_;
  1009. my $hash = $defs{$name};
  1010. DoTrigger($name,"REREADFILE $file",1);
  1011. }
  1012. sub _abortedFILE {
  1013. my ($hash) = shift;
  1014. }
  1015. # CapData
  1016. sub _retrieveCAPDATA {
  1017. my ($hash) = shift;
  1018. my $name = $hash->{NAME};
  1019. my $user = getKeyValue($name."_user");
  1020. my $pass = getKeyValue($name."_pass");
  1021. my $host = getKeyValue($name."_host");
  1022. my $proxyName = AttrVal($name, "gdsProxyName", "");
  1023. my $proxyType = AttrVal($name, "gdsProxyType", "");
  1024. my $passive = AttrVal($name, "gdsPassiveFtp", 1);
  1025. my $dir = "gds/specials/alerts/cap/GER/status/";
  1026. my $dwd = "Z_CAP*";
  1027. my $datafile = "";
  1028. my $targetDir = $tempDir.$name."_alerts.dir";
  1029. my $targetFile = $tempDir.$name."_alerts.zip";
  1030. mkdir $targetDir unless -d $targetDir;
  1031. # delete archive file
  1032. unlink $targetFile;
  1033. eval {
  1034. my $ftp = Net::FTP->new( $host,
  1035. Debug => 0,
  1036. Timeout => 10,
  1037. Passive => $passive,
  1038. FirewallType => $proxyType,
  1039. Firewall => $proxyName);
  1040. if(defined($ftp)){
  1041. Log3($name, 4, "GDS $name: ftp connection established.");
  1042. $ftp->login($user, $pass);
  1043. $ftp->binary;
  1044. $ftp->cwd("$dir");
  1045. my @files = $ftp->ls($dwd);
  1046. if(@files) {
  1047. Log3($name, 4, "GDS $name: filelist found.");
  1048. @files = sort(@files);
  1049. $datafile = $files[-1];
  1050. Log3($name, 5, "GDS $name: retrieving $datafile");
  1051. $ftp->get($datafile,$targetFile);
  1052. my $s = -s $targetFile;
  1053. Log3($name, 5, "GDS: ftp transferred $s bytes");
  1054. }
  1055. $ftp->quit;
  1056. }
  1057. };
  1058. # delete old files in directory
  1059. if (-d $targetDir) {
  1060. my @remove = _readDir($targetDir);
  1061. foreach my $f (@remove){
  1062. next if -d $f;
  1063. next if $targetFile =~ m/$f$/;
  1064. Log3($name, 4, "GDS $name: deleting $targetDir/$f");
  1065. unlink("$targetDir/$f");
  1066. }
  1067. }
  1068. # unzip
  1069. qx(unzip -o $targetFile -d $targetDir);
  1070. # merge
  1071. my ($countInfo,$cF) = _mergeCapFile($hash);
  1072. my ($aList,$cellData) = _buildCAPList($hash,$countInfo,$cF);
  1073. unlink $targetFile unless AttrVal($name,'gdsDebug',0);
  1074. return "$name;;;$datafile;;;$aList;;;$cF;;;$cellData";
  1075. }
  1076. sub _finishedCAPDATA {
  1077. my ($name,$datafile,$aL,$capFile,$cellData) = split(/;;;/,shift);
  1078. my $hash = $defs{$name};
  1079. $aList = $aL;
  1080. my @h = split(/;;/,$cellData);
  1081. foreach(@h) {
  1082. my ($n,$city,$cell) = split(/:/,$_);
  1083. $capCityHash{$city} = $n;
  1084. $capCellHash{"$cell$n"} = $n;
  1085. }
  1086. my $xml = new XML::Simple;
  1087. eval {
  1088. $alertsXml = $xml->XMLin($capFile, KeyAttr => {}, ForceArray => [ 'alert', 'eventCode', 'area', 'geocode' ]);
  1089. };
  1090. if ($@) {
  1091. Log3($name,1,'GDS: error analyzing alerts XML:'.$@);
  1092. return;
  1093. }
  1094. readingsSingleUpdate($hash, "_dF_alerts",$datafile,0) if(AttrVal($name, "gdsDebug", 0));
  1095. $hash->{GDS_CAPDATA_READ} = int(time());
  1096. DoTrigger($name,"REREADALERTS",1);
  1097. }
  1098. sub _abortedCAPDATA {
  1099. my ($hash) = shift;
  1100. delete $hash->{GDS_CAPDATA_READ};
  1101. $hash->{GDS_CAPDATA_ABORTED} = localtime(time());
  1102. }
  1103. sub _mergeCapFile($) {
  1104. my ($hash) = @_;
  1105. my $name = $hash->{NAME};
  1106. my $destinationDirectory = $tempDir.$name."_alerts.dir";
  1107. my @capFiles = _readDir($destinationDirectory);
  1108. my @alertsArray;
  1109. my $xmlHeader = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>';
  1110. push (@alertsArray,$xmlHeader);
  1111. push (@alertsArray,"<gds>");
  1112. my $countInfo = 0;
  1113. foreach my $cF (@capFiles){
  1114. # merge all capFiles
  1115. $cF = $destinationDirectory."/".$cF;
  1116. next if -d $cF;
  1117. next unless -s $cF;
  1118. next unless $cF =~ m/\.xml$/; # read xml files only!
  1119. Log3($name, 4, "GDS $name: analyzing $cF");
  1120. my ($err,@a) = FileRead({FileName=>$cF,ForceType=>"file" });
  1121. foreach my $l (@a) {
  1122. next unless length($l);
  1123. next if($l =~ m/^\<\?xml version.*/);
  1124. $l = "<alert>" if($l =~ m/^\<alert.*/);
  1125. # next if($l =~ m/^\<alert.*/);
  1126. # next if($l =~ m/^\<\/alert.*/);
  1127. next if($l =~ m/^\<sender\>.*/);
  1128. $countInfo++ if($l =~ m/^\<info\>/);
  1129. push (@alertsArray,$l);
  1130. }
  1131. }
  1132. push (@alertsArray,"</gds>");
  1133. # write the big XML file if needed
  1134. if(AttrVal($name,"gdsDebug", 0)) {
  1135. my $cF = $destinationDirectory."/gds_alerts";
  1136. unlink $cF if -e $cF;
  1137. FileWrite({ FileName=>$cF,ForceType=>"file" },@alertsArray);
  1138. }
  1139. my $xmlContent = join('',@alertsArray);
  1140. return ($countInfo,$xmlContent);
  1141. }
  1142. sub _buildCAPList($$$){
  1143. my ($hash,$countInfo,$cF) = @_;
  1144. my $name = $hash->{NAME};
  1145. $alertsXml = undef;
  1146. my $xml = new XML::Simple;
  1147. my $area = 0;
  1148. my $record = 0;
  1149. my $n = 0;
  1150. my ($capCity, $capCell, $capEvent, $capEvt, @a);
  1151. my $destinationDirectory = $tempDir.$name."_alerts.dir";
  1152. # make XML array and analyze data
  1153. eval {
  1154. $alertsXml = $xml->XMLin($cF, KeyAttr => {}, ForceArray => [ 'alert', 'eventCode', 'area', 'geocode' ]);
  1155. };
  1156. if ($@) {
  1157. Log3($name,1,'GDS: error analyzing alerts XML:'.$@);
  1158. return (undef,undef);
  1159. }
  1160. # analyze entries based on info and area array
  1161. # array elements are determined by $info and $area
  1162. #
  1163. my $cellData = '';
  1164. for (my $info=0; $info<=$countInfo;$info++) {
  1165. $area = 0;
  1166. while(1){
  1167. $capCity = $alertsXml->{alert}[$info]{info}{area}[$area]{areaDesc};
  1168. $capEvent = $alertsXml->{alert}[$info]{info}{event};
  1169. last unless $capCity;
  1170. $capCell = __findCAPWarnCellId($info, $area);
  1171. $n = 100*$info+$area;
  1172. $capCity = latin1ToUtf8($capCity.' '.$capEvent);
  1173. push @a, $capCity;
  1174. $capCity =~ s/\s/_/g;
  1175. $cellData .= "$n:$capCity:$capCell$n;;";
  1176. $area++;
  1177. $record++;
  1178. $capCity = undef;
  1179. }
  1180. }
  1181. @a = sort(@a);
  1182. $aList = undef;
  1183. $aList = join(",", @a);
  1184. $aList =~ s/\s/_/g;
  1185. $aList = "No_alerts_published!" if !$record;
  1186. return($aList,$cellData);
  1187. }
  1188. sub __findCAPWarnCellId($$){
  1189. my ($info, $area) = @_;
  1190. my $i = 0;
  1191. while($i < 100){
  1192. if($alertsXml->{alert}[$info]{info}{area}[$area]{geocode}[$i]{valueName} eq "WARNCELLID"){
  1193. return $alertsXml->{alert}[$info]{info}{area}[$area]{geocode}[$i]{value};
  1194. last;
  1195. }
  1196. $i++; # emergency exit :)
  1197. }
  1198. }
  1199. # ForecastData
  1200. sub _retrieveFORECAST {
  1201. my ($hash) = shift;
  1202. my $name = $hash->{NAME};
  1203. my $user = getKeyValue($name."_user");
  1204. my $pass = getKeyValue($name."_pass");
  1205. my $host = getKeyValue($name."_host");
  1206. my $proxyName = AttrVal($name, "gdsProxyName", "");
  1207. my $proxyType = AttrVal($name, "gdsProxyType", "");
  1208. my $passive = AttrVal($name, "gdsPassiveFtp", 1);
  1209. my $useFritz = AttrVal($name, "gdsUseFritzkotz", 0);
  1210. my $dir = "gds/specials/forecasts/tables/germany/";
  1211. my $ret = "";
  1212. eval {
  1213. my $ftp = Net::FTP->new( $host,
  1214. Debug => 0,
  1215. Timeout => 10,
  1216. Passive => $passive,
  1217. FirewallType => $proxyType,
  1218. Firewall => $proxyName);
  1219. if(defined($ftp)){
  1220. Log3($name, 4, "GDS $name: ftp connection established.");
  1221. $ftp->login($user, $pass);
  1222. $ftp->binary;
  1223. $ftp->cwd("$dir");
  1224. my @files = $ftp->ls();
  1225. if(@files) {
  1226. Log3($name, 4, "GDS $name: filelist found.");
  1227. @files = sort(@files);
  1228. $fcmapList = undef;
  1229. map ( $fcmapList .= (split(/Daten_/,$_,2))[1].",", @files );
  1230. my $count = 0;
  1231. foreach my $file (@files) {
  1232. my ($file_content,$file_handle);
  1233. open($file_handle, '>', \$file_content);
  1234. $ftp->get($file,$file_handle);
  1235. next unless (length($file_content));
  1236. $file_content = latin1ToUtf8($file_content);
  1237. $file_content =~ s/\r\n/\$/g;
  1238. $ret .= "$file:$file_content;";
  1239. $count++;
  1240. Log3 ($name, 5, "GDS $name retrieved forecast $file");
  1241. }
  1242. }
  1243. $ftp->quit;
  1244. }
  1245. };
  1246. return $name.";;;".$ret;
  1247. }
  1248. sub _finishedFORECAST {
  1249. my ($name,$ret) = split(/;;;/,shift); #@_;
  1250. my $hash = $defs{$name};
  1251. my @a = split(/;/,$ret);
  1252. %allForecastData = ();
  1253. foreach my $l (@a) {
  1254. my ($fn,$fc) = split(/\:/,$l);
  1255. $allForecastData{$fn} = $fc;
  1256. }
  1257. $hash->{GDS_FORECAST_READ} = int(time());
  1258. DoTrigger($name,"REREADFORECAST",1);
  1259. getListForecastStations($hash);
  1260. my $sf = AttrVal($name,'gdsSetForecast',0);
  1261. # GDS_GetUpdate($hash,1) if $sf;
  1262. my @b;
  1263. push @b, undef;
  1264. push @b, undef;
  1265. push @b, $sf;
  1266. retrieveForecasts($hash, "fc", @b);
  1267. delete $hash->{GDS_FORECAST_BUSY};
  1268. }
  1269. sub _abortedFORECAST {
  1270. my ($hash) = shift;
  1271. delete $hash->{GDS_FORECAST_READ};
  1272. $hash->{GDS_FORECAST_ABORTED} = localtime(time());
  1273. delete $hash->{GDS_FORECAST_BUSY};
  1274. }
  1275. ###################################################################################################
  1276. #
  1277. # forecast retrieval - provided by jensb
  1278. sub retrieveForecasts($$@) {
  1279. #
  1280. # parameter: hash, prefix, region/station, forecast index (0 .. 10)
  1281. #
  1282. my ($hash, $prefix, @a) = @_;
  1283. my $name = $hash->{NAME};
  1284. # extract region and station name
  1285. return unless defined($a[2]);
  1286. my ($area,$station) = split(/\//,$a[2]);
  1287. return unless $station;
  1288. my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime();
  1289. my ($dataFile, $found, $line, %fread, $k, $v, $data);
  1290. $area = utf8ToLatin1($area);
  1291. $station =~ s/_/ /g; # replace underscore in station name by space
  1292. my $searchLen = length($station);
  1293. %fread = ();
  1294. # define fetch scope (all forecasts or single forecast)
  1295. my $fc = 0;
  1296. my $fcStep = 1;
  1297. if (defined($a[3]) && $a[3] > 0) {
  1298. # single forecast
  1299. $fc = $a[3] - 1;
  1300. $fcStep = 10;
  1301. }
  1302. # fetch up to 10 forecasts for today and the next 3 days
  1303. do {
  1304. my $day;
  1305. my $early;
  1306. if ($fc < 4) {
  1307. $day = 0;
  1308. $early = 0;
  1309. } else {
  1310. $day = int(($fc - 2)/2);
  1311. $early = $fc%2 == 0;
  1312. }
  1313. my $areaAndTime = $area;
  1314. if ($day == 1) {
  1315. $areaAndTime .= "_morgen";
  1316. } elsif ($day == 2) {
  1317. $areaAndTime .= "_uebermorgen";
  1318. } elsif ($day == 3) {
  1319. $areaAndTime .= "_Tag4";
  1320. }
  1321. my $timeLabel = undef;
  1322. my $tempLabel = '_tAvgAir';
  1323. my $copyDay = undef;
  1324. my $copyTimeLabel = undef;
  1325. if ($day == 0) {
  1326. if ($fc == 0) {
  1327. $areaAndTime .= "_frueh"; # .. 6 h
  1328. $timeLabel = '06';
  1329. $tempLabel ='_tMinAir';
  1330. $copyDay = 1;
  1331. $copyTimeLabel = '12';
  1332. } elsif ($fc == 1) {
  1333. $areaAndTime .= "_mittag"; # .. 12 h
  1334. $timeLabel = '12';
  1335. $tempLabel .= $timeLabel;
  1336. } elsif ($fc == 2) {
  1337. $areaAndTime .= "_spaet"; # .. 18 h
  1338. $timeLabel = '18';
  1339. $tempLabel ='_tMaxAir';
  1340. $copyDay = 1;
  1341. $copyTimeLabel = '24';
  1342. } elsif ($fc == 3) {
  1343. $areaAndTime .= "_nacht"; # .. 24 h
  1344. $timeLabel = '24';
  1345. $tempLabel .= $timeLabel;
  1346. }
  1347. } else {
  1348. if ($early) {
  1349. $areaAndTime .= "_frueh"; # .. 12 h
  1350. $timeLabel = '12';
  1351. $tempLabel ='_tMinAir';
  1352. if ($day < 3) {
  1353. $copyDay = $day + 1;
  1354. $copyTimeLabel = '12';
  1355. }
  1356. } else {
  1357. $areaAndTime .= "_spaet"; # .. 24 h
  1358. $timeLabel .= '24';
  1359. $tempLabel ='_tMaxAir';
  1360. if ($day < 3) {
  1361. $copyDay = $day + 1;
  1362. $copyTimeLabel = '24';
  1363. }
  1364. }
  1365. } # if ($day == 0) {
  1366. # define forecast date (based on "now" + day)
  1367. my $fcEpoch = time() + $day*86400;
  1368. if ($fc == 3) {
  1369. # night continues at next day
  1370. $fcEpoch += 86400;
  1371. }
  1372. my ($fcSec,$fcMin,$fcHour,$fcMday,$fcMon,$fcYear,$fcWday,$fcYday,$fcIsdst) = localtime($fcEpoch);
  1373. my $fcWeekday = $weekdays[$fcWday];
  1374. my $fcDate = sprintf("%02d.%02d.%04d", $fcMday, 1+$fcMon, 1900+$fcYear);
  1375. my $fcDateFound = 0;
  1376. # retrieve from hash
  1377. my $noDataFound = 1;
  1378. $data = undef;
  1379. my $hashSize = keys%allForecastData; # checking size of hash seems to make all entries "visible" in loop
  1380. while(($k, $v) = each %allForecastData){
  1381. if ($k eq "Daten_$areaAndTime") {
  1382. $data = $v;
  1383. last;
  1384. };
  1385. }
  1386. if (defined($data) && $data) {
  1387. my @data = split(/\$/,$data);
  1388. foreach my $l (@data) {
  1389. if (index($l, $fcDate) > 0) {
  1390. # forecast date found
  1391. $fcDateFound = 1;
  1392. } # if
  1393. if (index(substr(lc($l),0,$searchLen), substr(lc($station),0,$searchLen)) != -1) {
  1394. # station found
  1395. $line = $l;
  1396. last;
  1397. } # if
  1398. } # foreach
  1399. # parse file
  1400. if ($fcDateFound && length($line) > 0) {
  1401. if (index(substr(lc($line),0,$searchLen), substr(lc($station),0,$searchLen)) != -1) {
  1402. # station found but there is no header line and column width varies:
  1403. $line =~ s/---/ ---/g; # column distance may drop to zero between station name
  1404. # and invalid temp "---" -> prepend 3 spaces
  1405. $line =~ s/ /;/g; # now min. column distance is 3 spaces -> convert to semicolon
  1406. $line =~ s/;+/;/g; # replace multiple consecutive semicolons by one semicolon
  1407. my @b = split(';', $line); # split columns by semicolon
  1408. $b[0] =~ s/^\s+|\s+$//g; # trim station name
  1409. $b[1] =~ s/^\s+|\s+$//g; # trim temperature
  1410. $b[2] =~ s/^\s+|\s+$//g; # trim weather
  1411. if (scalar(@b) > 3) {
  1412. $b[3] =~ s/^\s+|\s+$//g; # trim wind gust
  1413. } else {
  1414. $b[3] = ' ';
  1415. }
  1416. $fread{$prefix."_stationName"} = $area.'/'.$b[0];
  1417. $fread{$prefix.$day.$tempLabel} = $b[1];
  1418. $fread{$prefix.$day."_weather".$timeLabel} = $b[2];
  1419. $fread{$prefix.$day."_windGust".$timeLabel} = $b[3];
  1420. if ($fc != 3) {
  1421. $fread{$prefix.$day."_weekday"} = $fcWeekday;
  1422. }
  1423. $noDataFound = 0;
  1424. } else {
  1425. # station not found, abort
  1426. $fread{$prefix."_stationName"} = "unknown: $station in $area";
  1427. last;
  1428. }
  1429. }
  1430. } # unless
  1431. if ($noDataFound) {
  1432. # forecast period already passed or no data available
  1433. $fread{$prefix.$day.$tempLabel} = "---";
  1434. $fread{$prefix.$day."_weather".$timeLabel} = "---";
  1435. $fread{$prefix.$day."_windGust".$timeLabel} = "---";
  1436. if ($fc != 3) {
  1437. $fread{$prefix.$day."_weekday"} = $fcWeekday;
  1438. }
  1439. }
  1440. # day change preset by rotation
  1441. my $ltime = ReadingsTimestamp($name, $prefix.$day."_weather".$timeLabel, undef);
  1442. my ($lsec,$lmin,$lhour,$lmday,$lmon,$lyear,$lwday,$lyday,$lisdst);
  1443. if (defined($ltime)) {
  1444. ($lsec,$lmin,$lhour,$lmday,$lmon,$lyear,$lwday,$lyday,$lisdst) = localtime(time_str2num($ltime));
  1445. }
  1446. if (!defined($ltime) || $mday != $lmday) {
  1447. # day has changed, rotate old forecast forward by one day because new forecast is not immediately available
  1448. my $temp = $fread{$prefix.$day.$tempLabel};
  1449. if (defined($temp) && substr($temp, 0, 2) eq '--') {
  1450. if (defined($copyTimeLabel)) {
  1451. $fread{$prefix.$day.$tempLabel} = ReadingsVal($name, $prefix.$copyDay.$tempLabel, '---');
  1452. } else {
  1453. # today noon/night and 3rd day is undefined
  1454. $fread{$prefix.$day.$tempLabel} = ' ';
  1455. }
  1456. }
  1457. my $weather = $fread{$prefix.$day."_weather".$timeLabel};
  1458. if (defined($weather) && substr($weather, 0, 2) eq '--') {
  1459. if (defined($copyTimeLabel)) {
  1460. $fread{$prefix.$day."_weather".$timeLabel} =
  1461. ReadingsVal($name, $prefix.$copyDay."_weather".$copyTimeLabel, '---');
  1462. } else {
  1463. # today noon/night and 3rd day is undefined
  1464. $fread{$prefix.$day."_weather".$timeLabel} = ' ';
  1465. }
  1466. }
  1467. my $windGust = $fread{$prefix.$day."_windGust".$timeLabel};
  1468. if (defined($windGust) && substr($windGust, 0, 2) eq '--') {
  1469. if (defined($copyTimeLabel)) {
  1470. $fread{$prefix.$day."_windGust".$timeLabel} =
  1471. ReadingsVal($name, $prefix.$copyDay."_windGust".$copyTimeLabel, '---');
  1472. } else {
  1473. # today noon/night and 3rd day is undefined
  1474. $fread{$prefix.$day."_windGust".$timeLabel} = ' ';
  1475. }
  1476. }
  1477. }
  1478. $fc += $fcStep;
  1479. } while ($fc < 10);
  1480. readingsBeginUpdate($hash);
  1481. while (($k, $v) = each %fread) {
  1482. # skip update if no valid data is available
  1483. unless(defined($v)) {delete($defs{$name}{READINGS}{$k}); next;}
  1484. if($v =~ m/^--/) {delete($defs{$name}{READINGS}{$k}); next;};
  1485. unless(length(trim($v))) {delete($defs{$name}{READINGS}{$k}); next;};
  1486. readingsBulkUpdate($hash, $k, $v);
  1487. }
  1488. readingsEndUpdate($hash, 1);
  1489. }
  1490. sub getListForecastStations($) {
  1491. my ($hash) = @_;
  1492. my $name = $hash->{NAME};
  1493. my @regions = keys(%rmapList);
  1494. my (@a,$data,$k,$v);
  1495. eval {
  1496. foreach my $region (@regions) {
  1497. $data = "";
  1498. my $areaAndTime = 'Daten_'.$region.'_morgen_spaet';
  1499. while(($k, $v) = each %allForecastData){
  1500. if ($k eq $areaAndTime) {
  1501. $data = $v;
  1502. last;
  1503. };
  1504. }
  1505. next unless $data;
  1506. my @data = split(/\$/,$data);
  1507. splice(@data, 0,2);
  1508. splice(@data,-2);
  1509. map ( push(@a,"$region/".(split(/(\s|--)/,$_,2))[0]), @data );
  1510. }
  1511. };
  1512. Log3($name, 4, "GDS $name: forecast data not found") unless (@a);
  1513. @a = sort(@a);
  1514. $fList = join(",", @a);
  1515. $fList =~ s/\s+,/,/g; # replace multiple spaces followed by comma with comma
  1516. $fList =~ s/\s/_/g; # replace spaces in stationName with underscore for list in frontend
  1517. return;
  1518. }
  1519. 1;
  1520. # development documentation
  1521. =pod
  1522. ###################################################################################################
  1523. #
  1524. # ToDo
  1525. #
  1526. ###################################################################################################
  1527. #
  1528. # Changelog
  1529. #
  1530. ###################################################################################################
  1531. #
  1532. # 2016-06-05 changed use os based unzip for decoding capdata
  1533. #
  1534. # 2016-03-29 changed remove all conditions code
  1535. #
  1536. # 2016-01-27 changed use setKeyValue/getKeyValue for username and password
  1537. #
  1538. # 2016-01-01 fixed use txt file instead html for conditions (adopt DWD changes)
  1539. #
  1540. # 2015-12-31 fixed conditions retrieval on startup
  1541. #
  1542. # 2015-11-26 fixed wrong region handling
  1543. # added gdsAlertsHeadlines()
  1544. #
  1545. # 2015-11-17 changed decodeCAPData - fix wrong cumulation (first try)
  1546. # fixed minor bugs
  1547. #
  1548. # 2015-11-06 changed character encoding in forecast readings (jensb)
  1549. # fixed problems after global rereadcfg
  1550. # fixed delete CAP-zipfile unless gdsDebug set
  1551. #
  1552. # 2015-11-01 changed getListForecastStations: fixed inverted logging "data not found"
  1553. # changed GDS_GetUpdate, retrieveData, _finishedFORECAST, _abortedFORECAST:
  1554. # prevent multiple parallel processing
  1555. # changed retrieveForecasts: make available data in hash "visible" for processing
  1556. #
  1557. # 2015-10-31 public new version released, SVN #9739
  1558. #
  1559. # 2015-10-30 public RC6 published, SVN #9727
  1560. # changed use passive ftp per default
  1561. #
  1562. # 2015-10-27 changed add own function gds_calcTz due to announced
  1563. # changes in SUNRISE_EL
  1564. #
  1565. # 2015-10-26 changed multiple instances are forbidden
  1566. #
  1567. # 2015-10-25 public RC5 published, SVN #9663
  1568. # changed a lot of code cleanup
  1569. #
  1570. # 2015-10-24 public RC3 published, SVN #9627
  1571. #
  1572. # 2015-10-13 changed getListForecastStations() completed
  1573. # changed retrieveForecasts() completed
  1574. # added DoTrigger() according to reread
  1575. #
  1576. # 2015-10-12 changed conditions completed
  1577. # changed capstationlist completed
  1578. # changed conditionsmap completed
  1579. # changed forecastsmap completed
  1580. # changed radarmap completed
  1581. # changed warningsmap completed
  1582. # changed warnings completed
  1583. # changed get alerts completed
  1584. #
  1585. # 2015-10-11 changed use Archive::Extract for unzip
  1586. # changed code cleanup
  1587. # changed forecast nonblocking retrieval:
  1588. # hash generation completed
  1589. # changed capstations nonblocking retrieval:
  1590. # alertslist dropdown completed
  1591. # datafile retrieval completed
  1592. #
  1593. #
  1594. # ---------- public RC2 published, SVN #9429
  1595. #
  1596. # 2015-10-11 renamed 99_gdsUtils.pm to GDSweblink.pm
  1597. # changed load GDSweblink.pm in eval() on module startup
  1598. #
  1599. # 2015-10-10 added attribute gdsHideFile to hide "GDS File" Menu
  1600. # added optional parameter "host" in define() to override default hostname
  1601. #
  1602. # changed weblink generator moved into 99_gdsUtils.pm
  1603. # changed perl module List::MoreUtils is no longer used
  1604. # changed perl module Text::CSV is no longer needed
  1605. # changed use binary mode for all ftp transfers to preven errors in images
  1606. #
  1607. # fixed handling for alert items msgType, sent, status
  1608. # fixed handling for alert messages without "expires" data
  1609. #
  1610. # updated commandref documentation
  1611. #
  1612. # ---------- public RC1 published, SVN #9416
  1613. #
  1614. # 2015-10-09 removed createIndexFile()
  1615. # added forecast retrieval
  1616. # added weblink generator
  1617. # added more "set clear ..." commands
  1618. # changed lots and lots of code cleanup
  1619. # feature make retrieveFile() nonblocking
  1620. #
  1621. #
  1622. # 2015-10-08 changed added mergeCapFile()
  1623. # code cleanup in buildCAPList()
  1624. # use system call "unzip" instead of Archive::Zip
  1625. # added NotifyFn for rereadcfg after INITIALIZED
  1626. # improved startup data retrieval
  1627. # improved attribute handling
  1628. #
  1629. # ---------- public first publication in ./contrib/55_GDS.2015 for testing
  1630. #
  1631. # 2015-10-07 changed remove LWP - we will only use ftp for transfers
  1632. # added first solution for filemerge
  1633. # added reliable counter for XML analyzes instead of while(1) loops
  1634. # added (implementation started) forecast retrieval by jensb
  1635. # changed make text file retrieval more generic
  1636. #
  1637. # 2015-10-06 removed Coro Support
  1638. # removed $useFTP - always use http internally
  1639. # changed use LWP::Parallel::UserAgent for nonblocking transfers
  1640. # changed use Archive::ZIP for alert files transfer and unzip
  1641. #
  1642. # 2015-10-05 started redesign for new data structures provided by DWD
  1643. #
  1644. # ----------
  1645. #
  1646. # 2015-09-24 fixed prevent fhem crash on empty conditions file
  1647. #
  1648. # 2015-04-07 fixed a_X_valid calculation: use onset, too
  1649. #
  1650. # 2015-01-30 changed use own FWEXT instead of HTTPSRV
  1651. #
  1652. # 2015-01-03 added multiple alerts handling
  1653. #
  1654. # 2014-10-15 added attr disable
  1655. #
  1656. # 2014-05-23 added set <name> clear alerts|all
  1657. # fixed some typos in docu and help
  1658. #
  1659. # 2014-05-22 added reading a_sent_local
  1660. #
  1661. # 2014-05-07 added readings a_onset_local & a_expires_local
  1662. #
  1663. # 2014-02-26 added attribute gdsPassiveFtp
  1664. #
  1665. # 2014-02-04 added ShutdownFn
  1666. # changed FTP Timeout
  1667. #
  1668. # 2013-11-03 added error handling for malformed XML files from GDS
  1669. #
  1670. # 2013-08-13 fixed some minor bugs to prevent annoying console messages
  1671. # added support for fhem installtions running on windows-based systems
  1672. #
  1673. # 2013-08-11 added retrieval for condition maps
  1674. # added retrieval for forecast maps
  1675. # added retrieval for warning maps
  1676. # added retrieval for radar maps
  1677. # modi use LWP::ua for some file transfers instead of ftp
  1678. # due to transfer errors on image files
  1679. # use parameter #5 = 1 in RetrieveFile for ftp
  1680. # added get <name> caplist
  1681. #
  1682. # 2013-08-10 added some more tolerance on text inputs
  1683. # modi switched from GetLogList to Log3
  1684. #
  1685. # 2013-08-09 added more logging
  1686. # fixed missing error message if WARNCELLID does not exist
  1687. # update commandref
  1688. #
  1689. # 2013-08-08 added logging
  1690. # added firewall/proxy support
  1691. # fixed XMLin missing parameter
  1692. # added :noArg to setlist-definitions
  1693. # added AttrFn
  1694. # modi retrieval of VHDL messages 30-33
  1695. #
  1696. # 2013-08-07 public initial release
  1697. #
  1698. ###################################################################################################
  1699. #
  1700. # Further informations
  1701. #
  1702. # DWD's data format is unpleasant to read,
  1703. # since the data columns change depending on the available data
  1704. # (e.g. the SSS column for snow disappears when there is no snow).
  1705. # It's also in ISO8859-1, i.e. it contains non-ASCII characters. To
  1706. # avoid problems, we need some conversion subs in this program.
  1707. #
  1708. # Höhe : m über NN
  1709. # Luftd.: reduzierter Luftdruck auf Meereshöhe in hPa
  1710. # TT : Lufttemperatur in Grad Celsius
  1711. # Tn12 : Minimum der Lufttemperatur, 18 UTC Vortag bis 06 UTC heute, Grad Celsius
  1712. # Tx12 : Maximum der Lufttemperatur, 18 UTC Vortag bis 06 UTC heute, Grad Celsius
  1713. # Tg24 : Temperaturminimum 5cm ¸ber Erdboden, 22.05.2014 00 UTC bis 24 UTC, Grad Celsius
  1714. # Tn24 : Minimum der Lufttemperatur, 22.05.2014 00 UTC bis 24 UTC, Grad Celsius
  1715. # Tm24 : Mittel der Lufttemperatur, 22.05.2014 00 UTC bis 24 UTC, Grad Celsius
  1716. # Tx24 : Maximum der Lufttemperatur, 22.05.2014 00 UTC bis 24 UTC, Grad Celsius
  1717. # Tmin : Minimum der Lufttemperatur, 06 UTC Vortag bis 06 UTC heute, Grad Celsius
  1718. # Tmax : Maximum der Lufttemperatur, 06 UTC Vortag bis 06 UTC heute, Grad Celsius
  1719. # RR1 : Niederschlagsmenge, einstündig, mm = l/qm
  1720. # RR12 : Niederschlagsmenge, 12st¸ndig, 18 UTC Vortag bis 06 UTC heute, mm = l/qm
  1721. # RR24 : Niederschlagsmenge, 24stündig, 06 UTC Vortag bis 06 UTC heute, mm = l/qm
  1722. # SSS : Gesamtschneehöhe in cm
  1723. # SSS24 : Sonnenscheindauer 22.05.2014 in Stunden
  1724. # SGLB24: Tagessumme Globalstrahlung am 22.05.2014 in J/qcm
  1725. # DD : Windrichtung
  1726. # FF : Windgeschwindigkeit letztes 10-Minutenmittel in km/h
  1727. # FX : höchste Windspitze im Bezugszeitraum in km/h
  1728. # --- : Wert nicht vorhanden
  1729. #
  1730. ###################################################################################################
  1731. =cut
  1732. # commandref documentation
  1733. =pod
  1734. =item device
  1735. =begin html
  1736. <a name="GDS"></a>
  1737. <h3>GDS</h3>
  1738. <ul>
  1739. <b>Prerequesits</b>
  1740. <ul>
  1741. <br/>
  1742. Module uses following additional Perl modules:<br/><br/>
  1743. <code>Net::FTP, XML::Simple, Archive::Extract, Archive::Zip</code><br/><br/>
  1744. If not already installed in your environment,
  1745. please install them using appropriate commands from your environment.
  1746. </ul>
  1747. <br/><br/>
  1748. <a name="GDSdefine"></a>
  1749. <b>Define</b>
  1750. <ul>
  1751. <br>
  1752. <code>define &lt;name&gt; GDS &lt;username&gt; &lt;password&gt; [&lt;host&gt;]</code><br>
  1753. <br>
  1754. This module provides connection to <a href="http://www.dwd.de/grundversorgung">GDS service</a>
  1755. generated by <a href="http://www.dwd.de">DWD</a><br>
  1756. <br/>
  1757. Optional paramater host is used to overwrite default host "ftp-outgoing2.dwd.de".<br/>
  1758. <br>
  1759. </ul>
  1760. <br/><br/>
  1761. <a name="GDSset"></a>
  1762. <b>Set-Commands</b><br/>
  1763. <ul>
  1764. <br/>
  1765. <code>set &lt;name&gt; clear alerts|conditions|forecasts|all</code>
  1766. <br/><br/>
  1767. <ul>
  1768. <li>alerts: Delete all a_* readings</li>
  1769. <li>all: Delete all a_*, c_*, g_* and fc_* readings</li>
  1770. </ul>
  1771. <br/>
  1772. <code>set &lt;name&gt; forecasts &lt;region&gt;/&lt;stationName&gt;</code>
  1773. <br/><br/>
  1774. <ul>Retrieve forecasts for today and the following 3 days for selected station.<br/>
  1775. Data will be updated periodically.</ul>
  1776. <br/>
  1777. <code>set &lt;name&gt; help</code>
  1778. <br/><br/>
  1779. <ul>Show a help text with available commands</ul>
  1780. <br/>
  1781. <code>set &lt;name&gt; update</code>
  1782. <br/><br/>
  1783. <ul>Update forecasts readings at selected station and restart update-timer</ul>
  1784. <br/>
  1785. <li>forecast readings generated by SET use prefix "fcd_" and a postfix of "hh"<br/>
  1786. with d=relative day (0=today) and hh=last hour of forecast (exclusive)</li>
  1787. <li>readings generated by SET will be updated automatically every 20 minutes</li>
  1788. </ul>
  1789. <br/><br/>
  1790. <a name="GDSget"></a>
  1791. <b>Get-Commands</b><br/>
  1792. <ul>
  1793. <br/>
  1794. <code>get &lt;name&gt; alerts &lt;region&gt;</code>
  1795. <br/><br/>
  1796. <ul>Retrieve alert message for selected region from previously read alert file (see rereadcfg)</ul>
  1797. <br/>
  1798. <code>get &lt;name&gt; conditionsmap &lt;region&gt;</code>
  1799. <br/><br/>
  1800. <ul>Retrieve map (imagefile) showing current conditions at selected station</ul>
  1801. <br/>
  1802. <code>get &lt;name&gt; forecasts &lt;region&gt;</code>
  1803. <br/><br/>
  1804. <ul>Retrieve forecasts for today and the following 3 days for selected region as text</ul>
  1805. <br/>
  1806. <code>get &lt;name&gt; forecastsmap &lt;stationName&gt;</code>
  1807. <br/><br/>
  1808. <ul>Retrieve map (imagefile) showing forecasts for selected region</ul>
  1809. <br/>
  1810. <code>get &lt;name&gt; headlines [separator]</code>
  1811. <br/><br/>
  1812. <ul>Returns a string, containing all alert headlines. <br/>
  1813. Default separator is | but can be overriden.</ul>
  1814. <br/>
  1815. <code>get &lt;name&gt; help</code>
  1816. <br/><br/>
  1817. <ul>Show a help text with available commands</ul>
  1818. <br/>
  1819. <code>get &lt;name&gt; list capstations</code>
  1820. <br/><br/>
  1821. <ul>
  1822. <li><b>capstations:</b> Retrieve list showing all defined warning regions.
  1823. You can find your WARNCELLID with this list.</li>
  1824. </ul>
  1825. <br/>
  1826. <code>get &lt;name&gt; radarmap &lt;region&gt;</code>
  1827. <br/><br/>
  1828. <ul>Retrieve map (imagefile) containig radar view from selected region</ul>
  1829. <br/>
  1830. <code>get &lt;name&gt; rereadcfg</code>
  1831. <br/><br/>
  1832. <ul>Reread all required data from DWD Server manually: station list and CAP data</ul>
  1833. <br/>
  1834. <code>get &lt;name&gt; warnings &lt;region&gt;</code>
  1835. <br/><br/>
  1836. <ul>Retrieve current warnings report for selected region
  1837. <ul>
  1838. <br/>
  1839. <li>report type VHDL30 = regular report, issued daily</li>
  1840. <li>report type VHDL31 = regular report, issued before weekend or national holiday</li>
  1841. <li>report type VHDL32 = preliminary report, issued on special conditions</li>
  1842. <li>report type VHDL33 = cancel report, issued if necessary to cancel VHDL32</li>
  1843. </ul>
  1844. </ul>
  1845. <br/>
  1846. <code>get &lt;name&gt; warningssmap &lt;region&gt;</code>
  1847. <br/><br/>
  1848. <ul>Retrieve map (imagefile) containig current warnings for selected region marked with symbols</ul>
  1849. <br/><br/>
  1850. <b>All downloaded mapfiles</b> can be found inside "GDS Files" area in left navigation bar.
  1851. </ul>
  1852. <br/><br/>
  1853. <a name="GDSattr"></a>
  1854. <b>Attributes</b><br/><br/>
  1855. <ul>
  1856. <li><a href="#do_not_notify">do_not_notify</a></li>
  1857. <li><a href="#readingFnAttributes">readingFnAttributes</a></li>
  1858. <br/>
  1859. <li><b>disable</b> - if set, gds will not try to connect to internet</li>
  1860. <li><b>gdsAll</b> - defines filter for "all data" from alert message</li>
  1861. <li><b>gdsDebug</b> - defines filter for debug informations</li>
  1862. <li><b>gdsSetForecast</b> - defines forecasts region/station to be used after system restart</li>
  1863. <li><b>gdsLong</b> - show long text fields "description" and "instruction" from alert message in readings</li>
  1864. <li><b>gdsPolygon</b> - show polygon data from alert message in a reading</li>
  1865. <li><b>gdsHideFiles</b> - if set to 1, the "GDS Files" menu in the left navigation bar will not be shown</li>
  1866. <br/>
  1867. <li><b>gdsPassiveFtp</b> - set to 1 to use passive FTP transfer</li>
  1868. <li><b>gdsFwName</b> - define firewall hostname in format &lt;hostname&gt;:&lt;port&gt;</li>
  1869. <li><b>gdsFwType</b> - define firewall type in a value 0..7 please refer to
  1870. <a href="http://search.cpan.org/~gbarr/libnet-1.22/Net/Config.pm#NetConfig_VALUES">cpan documentation</a>
  1871. for further informations regarding firewall settings.</li>
  1872. </ul>
  1873. <br/><br/>
  1874. <b>Generated Readings:</b>
  1875. <br/><br/>
  1876. <ul>
  1877. <li><b>_&lt;readingName&gt;</b> - debug informations</li>
  1878. <li><b>a_X_&lt;readingName&gt;</b> - weather data from CAP alert messages. Readings will NOT be updated automatically<br/>
  1879. a_ readings contain a set of alert inforamtions, X represents a numeric set identifier starting with 0<br/>
  1880. that will be increased for every valid alert message in selected area<br/></li>
  1881. <li><b>a_count</b> - number of currently valid alert messages, can be used for own loop iterations on alert messages</li>
  1882. <li><b>a_valid</b> - returns 1 if at least one of decoded alert messages is valid</li>
  1883. <li><b>fc?_&lt;readingName&gt;??</b> - weather data from SET weather forecasts,
  1884. prefix by relative day and postfixed by last hour. Readings will be updated every 20 minutes.<br>
  1885. <i><ul>
  1886. <li>0_weather06 and ?_weather12 (with ? greater 0) is the weather in the morning</li>
  1887. <li>0_weather12 is the weather at noon</li>
  1888. <li>0_weather18 and ?_weather24 (with ? greater 0) is the weather in the afternoon</li>
  1889. <li>0_weather24 is the weather at midnight</li>
  1890. <li>0_windGust06 and ?_windGust12 (with ? greater 0) is the wind in the morning</li>
  1891. <li>0_windGust12 is the wind at noon</li>
  1892. <li>0_windGust18 and ?_windGust24 (with ? greater 0) is the wind in the afternoon</li>
  1893. <li>0_windGust24 is the wind at midnight</li>
  1894. <li>?_tMinAir is minimum temperature in the morning</li>
  1895. <li>0_tAvgAir12 is the average temperature at noon</li>
  1896. <li>?_tMaxAir is the maximum temperature in the afternoon</li>
  1897. <li>0_tAvgAir24 is the average temperature at midnight</li>
  1898. </ul></i>
  1899. </li>
  1900. </ul>
  1901. <br/><br/>
  1902. <b>Other events:</b>
  1903. <br/><br/>
  1904. <ul>
  1905. <li><b>REREAD</b> - start of rereadcfg</li>
  1906. <li><b>REREADFILE</b> - end of file read</li>
  1907. <li><b>REREADALERTS</b> - end of alerts read and analyzing</li>
  1908. <li><b>REREADFORECAST</b> - end of forecast read and analyzing</li>
  1909. </ul>
  1910. <br/><br/>
  1911. <b>Author's notes</b><br/><br/>
  1912. <ul>
  1913. <li>Module uses following additional Perl modules:<br/><br/>
  1914. <code>Net::FTP, XML::Simple, Archive::Extract, Archive::Zip</code><br/><br/>
  1915. If not already installed in your environment, please install them using appropriate commands from your environment.</li>
  1916. <br/><br/>
  1917. <li>Have fun!</li><br/>
  1918. </ul>
  1919. </ul>
  1920. =end html
  1921. =begin html_DE
  1922. <a name="GDS"></a>
  1923. <h3>GDS</h3>
  1924. <ul>
  1925. Sorry, keine deutsche Dokumentation vorhanden.<br/><br/>
  1926. Die englische Doku gibt es hier: <a href='http://fhem.de/commandref.html#GDS'>GDS</a><br/>
  1927. </ul>
  1928. =end html_DE
  1929. =cut