From 58bbbaf6de8540830500169a0bfda61a50e60c19 Mon Sep 17 00:00:00 2001 From: zhongjin Date: Sat, 22 Sep 2018 10:26:24 +0800 Subject: [PATCH] Initial commit --- .gitignore | 7 + .npmignore | 9 + .travis.yml | 19 + LICENSE | 21 + README.md | 123 +++ admin/icons/01d.svg | 23 + admin/icons/01m.svg | 32 + admin/icons/01n.svg | 16 + admin/icons/02d.svg | 34 + admin/icons/02m.svg | 43 + admin/icons/02n.svg | 27 + admin/icons/03d.svg | 37 + admin/icons/03m.svg | 46 + admin/icons/03n.svg | 30 + admin/icons/04.svg | 12 + admin/icons/05d.svg | 43 + admin/icons/05m.svg | 52 ++ admin/icons/05n.svg | 36 + admin/icons/06d.svg | 53 ++ admin/icons/06m.svg | 62 ++ admin/icons/06n.svg | 46 + admin/icons/07d.svg | 46 + admin/icons/07m.svg | 55 ++ admin/icons/07n.svg | 39 + admin/icons/08d.svg | 43 + admin/icons/08m.svg | 52 ++ admin/icons/08n.svg | 36 + admin/icons/09.svg | 18 + admin/icons/10.svg | 20 + admin/icons/11.svg | 31 + admin/icons/12.svg | 21 + admin/icons/13.svg | 18 + admin/icons/14.svg | 29 + admin/icons/15.svg | 20 + admin/icons/20d.svg | 56 ++ admin/icons/20m.svg | 65 ++ admin/icons/20n.svg | 49 ++ admin/icons/21d.svg | 53 ++ admin/icons/21m.svg | 62 ++ admin/icons/21n.svg | 46 + admin/icons/22.svg | 29 + admin/icons/23.svg | 32 + admin/icons/24d.svg | 52 ++ admin/icons/24m.svg | 61 ++ admin/icons/24n.svg | 45 + admin/icons/25d.svg | 55 ++ admin/icons/25m.svg | 64 ++ admin/icons/25n.svg | 48 ++ admin/icons/26d.svg | 55 ++ admin/icons/26m.svg | 64 ++ admin/icons/26n.svg | 48 ++ admin/icons/27d.svg | 58 ++ admin/icons/27m.svg | 67 ++ admin/icons/27n.svg | 51 ++ admin/icons/28d.svg | 52 ++ admin/icons/28m.svg | 61 ++ admin/icons/28n.svg | 45 + admin/icons/29d.svg | 55 ++ admin/icons/29m.svg | 64 ++ admin/icons/29n.svg | 48 ++ admin/icons/30.svg | 28 + admin/icons/31.svg | 31 + admin/icons/32.svg | 34 + admin/icons/33.svg | 28 + admin/icons/34.svg | 31 + admin/icons/40d.svg | 42 + admin/icons/40m.svg | 51 ++ admin/icons/40n.svg | 35 + admin/icons/41d.svg | 45 + admin/icons/41m.svg | 54 ++ admin/icons/41n.svg | 38 + admin/icons/42d.svg | 45 + admin/icons/42m.svg | 54 ++ admin/icons/42n.svg | 38 + admin/icons/43d.svg | 48 ++ admin/icons/43m.svg | 57 ++ admin/icons/43n.svg | 41 + admin/icons/44d.svg | 42 + admin/icons/44m.svg | 51 ++ admin/icons/44n.svg | 35 + admin/icons/45d.svg | 45 + admin/icons/45m.svg | 54 ++ admin/icons/45n.svg | 38 + admin/icons/46.svg | 17 + admin/icons/47.svg | 20 + admin/icons/48.svg | 23 + admin/icons/49.svg | 17 + admin/icons/50.svg | 20 + admin/icons/LICENSE.txt | 24 + admin/index.html | 98 +++ admin/index_m.html | 171 ++++ admin/words.js | 12 + admin/yr.png | Bin 0 -> 24656 bytes appveyor.yml | 21 + forecast.xml | 403 +++++++++ gulpfile.js | 426 ++++++++++ io-package.json | 544 ++++++++++++ lib/utils.js | 83 ++ lib/words.js | 109 +++ main.js | 348 ++++++++ package.json | 43 + server/translations.sql | 4 + server/yr.php | 39 + tasks/jscs.js | 17 + tasks/jscsRules.js | 36 + tasks/jshint.js | 17 + test/lib/example.xml | 413 +++++++++ test/lib/forecast.xml | 413 +++++++++ test/lib/setup.js | 728 ++++++++++++++++ test/lib/warnings.json | 1297 +++++++++++++++++++++++++++++ test/testFunctions.js | 135 +++ test/testPackageFiles.js | 91 ++ widgets/yr.html | 33 + widgets/yr/img/Prev_YrAdapter.png | Bin 0 -> 5233 bytes 114 files changed, 9151 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 admin/icons/01d.svg create mode 100644 admin/icons/01m.svg create mode 100644 admin/icons/01n.svg create mode 100644 admin/icons/02d.svg create mode 100644 admin/icons/02m.svg create mode 100644 admin/icons/02n.svg create mode 100644 admin/icons/03d.svg create mode 100644 admin/icons/03m.svg create mode 100644 admin/icons/03n.svg create mode 100644 admin/icons/04.svg create mode 100644 admin/icons/05d.svg create mode 100644 admin/icons/05m.svg create mode 100644 admin/icons/05n.svg create mode 100644 admin/icons/06d.svg create mode 100644 admin/icons/06m.svg create mode 100644 admin/icons/06n.svg create mode 100644 admin/icons/07d.svg create mode 100644 admin/icons/07m.svg create mode 100644 admin/icons/07n.svg create mode 100644 admin/icons/08d.svg create mode 100644 admin/icons/08m.svg create mode 100644 admin/icons/08n.svg create mode 100644 admin/icons/09.svg create mode 100644 admin/icons/10.svg create mode 100644 admin/icons/11.svg create mode 100644 admin/icons/12.svg create mode 100644 admin/icons/13.svg create mode 100644 admin/icons/14.svg create mode 100644 admin/icons/15.svg create mode 100644 admin/icons/20d.svg create mode 100644 admin/icons/20m.svg create mode 100644 admin/icons/20n.svg create mode 100644 admin/icons/21d.svg create mode 100644 admin/icons/21m.svg create mode 100644 admin/icons/21n.svg create mode 100644 admin/icons/22.svg create mode 100644 admin/icons/23.svg create mode 100644 admin/icons/24d.svg create mode 100644 admin/icons/24m.svg create mode 100644 admin/icons/24n.svg create mode 100644 admin/icons/25d.svg create mode 100644 admin/icons/25m.svg create mode 100644 admin/icons/25n.svg create mode 100644 admin/icons/26d.svg create mode 100644 admin/icons/26m.svg create mode 100644 admin/icons/26n.svg create mode 100644 admin/icons/27d.svg create mode 100644 admin/icons/27m.svg create mode 100644 admin/icons/27n.svg create mode 100644 admin/icons/28d.svg create mode 100644 admin/icons/28m.svg create mode 100644 admin/icons/28n.svg create mode 100644 admin/icons/29d.svg create mode 100644 admin/icons/29m.svg create mode 100644 admin/icons/29n.svg create mode 100644 admin/icons/30.svg create mode 100644 admin/icons/31.svg create mode 100644 admin/icons/32.svg create mode 100644 admin/icons/33.svg create mode 100644 admin/icons/34.svg create mode 100644 admin/icons/40d.svg create mode 100644 admin/icons/40m.svg create mode 100644 admin/icons/40n.svg create mode 100644 admin/icons/41d.svg create mode 100644 admin/icons/41m.svg create mode 100644 admin/icons/41n.svg create mode 100644 admin/icons/42d.svg create mode 100644 admin/icons/42m.svg create mode 100644 admin/icons/42n.svg create mode 100644 admin/icons/43d.svg create mode 100644 admin/icons/43m.svg create mode 100644 admin/icons/43n.svg create mode 100644 admin/icons/44d.svg create mode 100644 admin/icons/44m.svg create mode 100644 admin/icons/44n.svg create mode 100644 admin/icons/45d.svg create mode 100644 admin/icons/45m.svg create mode 100644 admin/icons/45n.svg create mode 100644 admin/icons/46.svg create mode 100644 admin/icons/47.svg create mode 100644 admin/icons/48.svg create mode 100644 admin/icons/49.svg create mode 100644 admin/icons/50.svg create mode 100644 admin/icons/LICENSE.txt create mode 100644 admin/index.html create mode 100644 admin/index_m.html create mode 100644 admin/words.js create mode 100644 admin/yr.png create mode 100644 appveyor.yml create mode 100644 forecast.xml create mode 100644 gulpfile.js create mode 100644 io-package.json create mode 100644 lib/utils.js create mode 100644 lib/words.js create mode 100644 main.js create mode 100644 package.json create mode 100644 server/translations.sql create mode 100644 server/yr.php create mode 100644 tasks/jscs.js create mode 100644 tasks/jscsRules.js create mode 100644 tasks/jshint.js create mode 100644 test/lib/example.xml create mode 100644 test/lib/forecast.xml create mode 100644 test/lib/setup.js create mode 100644 test/lib/warnings.json create mode 100644 test/testFunctions.js create mode 100644 test/testPackageFiles.js create mode 100644 widgets/yr.html create mode 100644 widgets/yr/img/Prev_YrAdapter.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4a6c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +.idea +tmp +admin/i18n/flat.txt +admin/i18n/*/flat.txt +iob_npm.done +package-lock.json \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..0f2ae68 --- /dev/null +++ b/.npmignore @@ -0,0 +1,9 @@ +gulpfile.js +tasks +tmp +test +.travis.yml +appveyor.yml +admin/i18n +iob_npm.done +package-lock.json \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e04170f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +os: + - linux + - osx +language: node_js +node_js: + - '6' + - '8' + - '10' +before_script: + - npm install winston@2.3.1 + - 'npm install https://github.com/yunkong2/yunkong2.js-controller/tarball/master --production' +env: + - CXX=g++-4.8 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2394c68 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 hobbyquaker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7404cc0 --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +![Logo](admin/yr.png) +# yunkong2.yr +================================= +[![NPM version](http://img.shields.io/npm/v/yunkong2.yr.svg)](https://www.npmjs.com/package/yunkong2.yr) +[![Downloads](https://img.shields.io/npm/dm/yunkong2.yr.svg)](https://www.npmjs.com/package/yunkong2.yr) +[![Tests](https://travis-ci.org/yunkong2/yunkong2.yr.svg?branch=master)](https://travis-ci.org/yunkong2/yunkong2.yr) + +[![NPM](https://nodei.co/npm/yunkong2.yr.png?downloads=true)](https://nodei.co/npm/yunkong2.yr/) + +fetches 48h weather forecast from [yr.no](yr.no) + +[yr.no](yr.no) is a joint service by the [Norwegian Meteorological Institute](met.no) and the [Norwegian Broadcasting Corporation](nrk.no) + +http://om.yr.no/verdata/free-weather-data/ + +**Note** - if _"Send missing translations to yunkong2.net"_ is activated (default) missing translations will be sent to yunkong2.net server. No ips or any additional info will be stored or analysed. Just missing translation. + +## Icons +Icons are taken from here [https://github.com/YR/weather-symbols](https://github.com/YR/weather-symbols) and belongs to yr.no. + +## Changelog +### 2.0.2 [2018-08-01] +* (bluefox) Warning! Breaking changes! States are renamed. +* (bluefox) Refactoring of states and roles + +### 1.0.6 [2017-05-27] +* (Andre) Update iconset + +### 1.0.5 [2016-10-10] +* (bluefox) move weather widgets to this adapter + +## 1.0.4 [2016-07-06] +* (bluefox) fix link to readme + +### 1.0.3 [2016-05-17] +* (bluefox) change readme path + +### 1.0.2 [2016-05-16] +* (bluefox) add translations + +### 1.0.1 [2016-04-25] +* (bluefox) add translations + +### 1.0.0 [2016-03-15] +* (bluefox) change parsing of cities + +### 0.1.9 [2015-10-28] +* (bluefox) fix error with translations + +### 0.1.8 [2015-10-27] +* (bluefox) translations +* (bluefox) automatically upload of missing translations to yunkong2.net + +### 0.1.7 [2015-07-10] +* (bluefox) make yr works with metro widgets + +### 0.1.6 [2015-06-12] +* (bluefox) translations + +### 0.1.5 [2015-03-26] +* (bluefox) translations + +### 0.1.4 [2015-03-24] +* (bluefox) remove unit "%" for "wind direction" + +### 0.1.3 [2015-03-22] +* (bluefox) fix error with tomorrow and day after tomorrow + +### 0.1.2 [2015-03-08] +* (bluefox) correct links to yr.no web site + +### 0.1.1 +* (bluefox) add translates for the weather states in other languages + +### 0.1.0 +* (bluefox) update yr on the new objects model + +### 0.0.4 +* (hobbyquaker) prepend "forecast." to state IDs + +### 0.0.3 +* (hobbyquaker) settings ui with autocomplete for location +* (hobbyquaker) renamed yr_forecast to forecast +* (hobbyquaker) added children attribute +* (hobbyquaker) decreased log verbosity +* (hobbyquaker) fixes + +### 0.0.2 + +* (hobbyquaker) fixes + + +### 0.0.1 + +* (hobbyquaker) first release + +## Todo + +* setState forecast_object + +## License + +The MIT License (MIT) + +Copyright (c) 2014-2018 hobbyquaker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/admin/icons/01d.svg b/admin/icons/01d.svg new file mode 100644 index 0000000..e8f7541 --- /dev/null +++ b/admin/icons/01d.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/01m.svg b/admin/icons/01m.svg new file mode 100644 index 0000000..e6afadc --- /dev/null +++ b/admin/icons/01m.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/01n.svg b/admin/icons/01n.svg new file mode 100644 index 0000000..23f9b5e --- /dev/null +++ b/admin/icons/01n.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/02d.svg b/admin/icons/02d.svg new file mode 100644 index 0000000..2daaed3 --- /dev/null +++ b/admin/icons/02d.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/02m.svg b/admin/icons/02m.svg new file mode 100644 index 0000000..1d557ca --- /dev/null +++ b/admin/icons/02m.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/02n.svg b/admin/icons/02n.svg new file mode 100644 index 0000000..1814563 --- /dev/null +++ b/admin/icons/02n.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/03d.svg b/admin/icons/03d.svg new file mode 100644 index 0000000..827e688 --- /dev/null +++ b/admin/icons/03d.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/03m.svg b/admin/icons/03m.svg new file mode 100644 index 0000000..cb3647b --- /dev/null +++ b/admin/icons/03m.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/03n.svg b/admin/icons/03n.svg new file mode 100644 index 0000000..eeb643f --- /dev/null +++ b/admin/icons/03n.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/04.svg b/admin/icons/04.svg new file mode 100644 index 0000000..eb98fe8 --- /dev/null +++ b/admin/icons/04.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/05d.svg b/admin/icons/05d.svg new file mode 100644 index 0000000..0226b47 --- /dev/null +++ b/admin/icons/05d.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/05m.svg b/admin/icons/05m.svg new file mode 100644 index 0000000..80ede84 --- /dev/null +++ b/admin/icons/05m.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/05n.svg b/admin/icons/05n.svg new file mode 100644 index 0000000..58fe847 --- /dev/null +++ b/admin/icons/05n.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/06d.svg b/admin/icons/06d.svg new file mode 100644 index 0000000..e5da5a1 --- /dev/null +++ b/admin/icons/06d.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/06m.svg b/admin/icons/06m.svg new file mode 100644 index 0000000..4fca933 --- /dev/null +++ b/admin/icons/06m.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/06n.svg b/admin/icons/06n.svg new file mode 100644 index 0000000..1eb876d --- /dev/null +++ b/admin/icons/06n.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/07d.svg b/admin/icons/07d.svg new file mode 100644 index 0000000..97e5afa --- /dev/null +++ b/admin/icons/07d.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/07m.svg b/admin/icons/07m.svg new file mode 100644 index 0000000..e233169 --- /dev/null +++ b/admin/icons/07m.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/07n.svg b/admin/icons/07n.svg new file mode 100644 index 0000000..c43a73b --- /dev/null +++ b/admin/icons/07n.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/08d.svg b/admin/icons/08d.svg new file mode 100644 index 0000000..b80f82a --- /dev/null +++ b/admin/icons/08d.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/08m.svg b/admin/icons/08m.svg new file mode 100644 index 0000000..90e936a --- /dev/null +++ b/admin/icons/08m.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/08n.svg b/admin/icons/08n.svg new file mode 100644 index 0000000..b502064 --- /dev/null +++ b/admin/icons/08n.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/09.svg b/admin/icons/09.svg new file mode 100644 index 0000000..1da5ea7 --- /dev/null +++ b/admin/icons/09.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/10.svg b/admin/icons/10.svg new file mode 100644 index 0000000..9a85ccd --- /dev/null +++ b/admin/icons/10.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/11.svg b/admin/icons/11.svg new file mode 100644 index 0000000..0e73a8c --- /dev/null +++ b/admin/icons/11.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/12.svg b/admin/icons/12.svg new file mode 100644 index 0000000..28a297f --- /dev/null +++ b/admin/icons/12.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/13.svg b/admin/icons/13.svg new file mode 100644 index 0000000..25fed85 --- /dev/null +++ b/admin/icons/13.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/14.svg b/admin/icons/14.svg new file mode 100644 index 0000000..c955133 --- /dev/null +++ b/admin/icons/14.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/15.svg b/admin/icons/15.svg new file mode 100644 index 0000000..3cb13b4 --- /dev/null +++ b/admin/icons/15.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/20d.svg b/admin/icons/20d.svg new file mode 100644 index 0000000..01cb6e7 --- /dev/null +++ b/admin/icons/20d.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/20m.svg b/admin/icons/20m.svg new file mode 100644 index 0000000..1b53807 --- /dev/null +++ b/admin/icons/20m.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/20n.svg b/admin/icons/20n.svg new file mode 100644 index 0000000..904f987 --- /dev/null +++ b/admin/icons/20n.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/21d.svg b/admin/icons/21d.svg new file mode 100644 index 0000000..08752db --- /dev/null +++ b/admin/icons/21d.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/21m.svg b/admin/icons/21m.svg new file mode 100644 index 0000000..7fc3610 --- /dev/null +++ b/admin/icons/21m.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/21n.svg b/admin/icons/21n.svg new file mode 100644 index 0000000..37e7df2 --- /dev/null +++ b/admin/icons/21n.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/22.svg b/admin/icons/22.svg new file mode 100644 index 0000000..6a99951 --- /dev/null +++ b/admin/icons/22.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/23.svg b/admin/icons/23.svg new file mode 100644 index 0000000..907428f --- /dev/null +++ b/admin/icons/23.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/24d.svg b/admin/icons/24d.svg new file mode 100644 index 0000000..016786a --- /dev/null +++ b/admin/icons/24d.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/24m.svg b/admin/icons/24m.svg new file mode 100644 index 0000000..e7fff4f --- /dev/null +++ b/admin/icons/24m.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/24n.svg b/admin/icons/24n.svg new file mode 100644 index 0000000..3831919 --- /dev/null +++ b/admin/icons/24n.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/25d.svg b/admin/icons/25d.svg new file mode 100644 index 0000000..e2922a2 --- /dev/null +++ b/admin/icons/25d.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/25m.svg b/admin/icons/25m.svg new file mode 100644 index 0000000..e559dd6 --- /dev/null +++ b/admin/icons/25m.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/25n.svg b/admin/icons/25n.svg new file mode 100644 index 0000000..4389a49 --- /dev/null +++ b/admin/icons/25n.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/26d.svg b/admin/icons/26d.svg new file mode 100644 index 0000000..bdca36d --- /dev/null +++ b/admin/icons/26d.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/26m.svg b/admin/icons/26m.svg new file mode 100644 index 0000000..42308f9 --- /dev/null +++ b/admin/icons/26m.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/26n.svg b/admin/icons/26n.svg new file mode 100644 index 0000000..4b8be4d --- /dev/null +++ b/admin/icons/26n.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/27d.svg b/admin/icons/27d.svg new file mode 100644 index 0000000..dbe2715 --- /dev/null +++ b/admin/icons/27d.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/27m.svg b/admin/icons/27m.svg new file mode 100644 index 0000000..18ed7fc --- /dev/null +++ b/admin/icons/27m.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/27n.svg b/admin/icons/27n.svg new file mode 100644 index 0000000..234bac9 --- /dev/null +++ b/admin/icons/27n.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/28d.svg b/admin/icons/28d.svg new file mode 100644 index 0000000..62bf909 --- /dev/null +++ b/admin/icons/28d.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/28m.svg b/admin/icons/28m.svg new file mode 100644 index 0000000..58d0649 --- /dev/null +++ b/admin/icons/28m.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/28n.svg b/admin/icons/28n.svg new file mode 100644 index 0000000..6b0b6d0 --- /dev/null +++ b/admin/icons/28n.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/29d.svg b/admin/icons/29d.svg new file mode 100644 index 0000000..2e296a3 --- /dev/null +++ b/admin/icons/29d.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/29m.svg b/admin/icons/29m.svg new file mode 100644 index 0000000..9abac75 --- /dev/null +++ b/admin/icons/29m.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/29n.svg b/admin/icons/29n.svg new file mode 100644 index 0000000..47c25ab --- /dev/null +++ b/admin/icons/29n.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/30.svg b/admin/icons/30.svg new file mode 100644 index 0000000..cdab6e2 --- /dev/null +++ b/admin/icons/30.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/31.svg b/admin/icons/31.svg new file mode 100644 index 0000000..503811e --- /dev/null +++ b/admin/icons/31.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/32.svg b/admin/icons/32.svg new file mode 100644 index 0000000..2729d5c --- /dev/null +++ b/admin/icons/32.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/33.svg b/admin/icons/33.svg new file mode 100644 index 0000000..d8b8d25 --- /dev/null +++ b/admin/icons/33.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/34.svg b/admin/icons/34.svg new file mode 100644 index 0000000..719ed5d --- /dev/null +++ b/admin/icons/34.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/40d.svg b/admin/icons/40d.svg new file mode 100644 index 0000000..f02e940 --- /dev/null +++ b/admin/icons/40d.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/40m.svg b/admin/icons/40m.svg new file mode 100644 index 0000000..2648cb4 --- /dev/null +++ b/admin/icons/40m.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/40n.svg b/admin/icons/40n.svg new file mode 100644 index 0000000..5a483fc --- /dev/null +++ b/admin/icons/40n.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/41d.svg b/admin/icons/41d.svg new file mode 100644 index 0000000..89f60eb --- /dev/null +++ b/admin/icons/41d.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/41m.svg b/admin/icons/41m.svg new file mode 100644 index 0000000..b97fbe2 --- /dev/null +++ b/admin/icons/41m.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/41n.svg b/admin/icons/41n.svg new file mode 100644 index 0000000..adb85ef --- /dev/null +++ b/admin/icons/41n.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/42d.svg b/admin/icons/42d.svg new file mode 100644 index 0000000..7a162bd --- /dev/null +++ b/admin/icons/42d.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/42m.svg b/admin/icons/42m.svg new file mode 100644 index 0000000..d31bb3f --- /dev/null +++ b/admin/icons/42m.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/42n.svg b/admin/icons/42n.svg new file mode 100644 index 0000000..cd283b5 --- /dev/null +++ b/admin/icons/42n.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/43d.svg b/admin/icons/43d.svg new file mode 100644 index 0000000..d9e146d --- /dev/null +++ b/admin/icons/43d.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/43m.svg b/admin/icons/43m.svg new file mode 100644 index 0000000..c275176 --- /dev/null +++ b/admin/icons/43m.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/43n.svg b/admin/icons/43n.svg new file mode 100644 index 0000000..aa71b14 --- /dev/null +++ b/admin/icons/43n.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/44d.svg b/admin/icons/44d.svg new file mode 100644 index 0000000..0b9f99e --- /dev/null +++ b/admin/icons/44d.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/44m.svg b/admin/icons/44m.svg new file mode 100644 index 0000000..e2d8eb8 --- /dev/null +++ b/admin/icons/44m.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/44n.svg b/admin/icons/44n.svg new file mode 100644 index 0000000..54b0cf2 --- /dev/null +++ b/admin/icons/44n.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/45d.svg b/admin/icons/45d.svg new file mode 100644 index 0000000..06ab067 --- /dev/null +++ b/admin/icons/45d.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/45m.svg b/admin/icons/45m.svg new file mode 100644 index 0000000..a1321a8 --- /dev/null +++ b/admin/icons/45m.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/45n.svg b/admin/icons/45n.svg new file mode 100644 index 0000000..fadfc89 --- /dev/null +++ b/admin/icons/45n.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/46.svg b/admin/icons/46.svg new file mode 100644 index 0000000..67a1c8e --- /dev/null +++ b/admin/icons/46.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/47.svg b/admin/icons/47.svg new file mode 100644 index 0000000..eaf49fe --- /dev/null +++ b/admin/icons/47.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/48.svg b/admin/icons/48.svg new file mode 100644 index 0000000..e67ff4e --- /dev/null +++ b/admin/icons/48.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/49.svg b/admin/icons/49.svg new file mode 100644 index 0000000..4ece601 --- /dev/null +++ b/admin/icons/49.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/50.svg b/admin/icons/50.svg new file mode 100644 index 0000000..6e6a05d --- /dev/null +++ b/admin/icons/50.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/admin/icons/LICENSE.txt b/admin/icons/LICENSE.txt new file mode 100644 index 0000000..efa0e4a --- /dev/null +++ b/admin/icons/LICENSE.txt @@ -0,0 +1,24 @@ +- https://github.com/YR/weather-symbols + +The MIT License (MIT) + +Copyright (c) 2015-2017 Yr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..1f87da4 --- /dev/null +++ b/admin/index.html @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + +
+ + + +

yr.no adapter settings

+

+ +

+

+ + +

+

+ +

+

yrDescription

+
+ + diff --git a/admin/index_m.html b/admin/index_m.html new file mode 100644 index 0000000..dafae6a --- /dev/null +++ b/admin/index_m.html @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + Use english metric system +
+
+
+
+ + Send missing translations to yunkong2.net +
+
+
+
+ yrDescription +
+
+
+ + diff --git a/admin/words.js b/admin/words.js new file mode 100644 index 0000000..5239fee --- /dev/null +++ b/admin/words.js @@ -0,0 +1,12 @@ +// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM /admin/i18n +/* global systemDictionary:true */ +'use strict'; + +systemDictionary = { + "yr.no adapter settings": { "en": "yr.no adapter settings", "de": "Yr.no Adaptereinstellungen", "ru": "Настройки адаптера yr.no", "pt": "yr.no configurações do adaptador", "nl": "yr.no adapter-instellingen", "fr": "yr.no paramètres de la carte", "it": "yr.no impostazioni della scheda", "es": "configuración del adaptador yr.no", "pl": "yr.no ustawienia adaptera"}, + "Place": { "en": "Place", "de": "Ort", "ru": "Место", "pt": "Lugar, colocar", "nl": "Plaats", "fr": "Endroit", "it": "Posto", "es": "Lugar", "pl": "Miejsce"}, + "Language": { "en": "Language", "de": "Sprache", "ru": "Язык", "pt": "Língua", "nl": "Taal", "fr": "La langue", "it": "linguaggio", "es": "Idioma", "pl": "Język"}, + "Send missing translations to yunkong2.net": { "en": "Send missing translations to yunkong2.net", "de": "Senden Sie fehlende Übersetzungen an yunkong2.net", "ru": "Отправить недостающие переводы на yunkong2.net", "pt": "Envie traduções faltando para yunkong2.net", "nl": "Verzend ontbrekende vertalingen naar yunkong2.net", "fr": "Envoyer des traductions manquantes à yunkong2.net", "it": "Invia traduzioni mancanti a yunkong2.net", "es": "Envía traducciones faltantes a yunkong2.net", "pl": "Wyślij brakujące tłumaczenia do yunkong2.net"}, + "Use english metric system": { "en": "Use english metric system(°F, miles)", "de": "Verwenden Sie das englische metrische System (°F, Meilen)", "ru": "Используйте английскую метрическую систему (°F, мили)", "pt": "Use o sistema métrico inglês (°F, milhas)", "nl": "Gebruik het Engelse metrische systeem (°F, mijl)", "fr": "Utiliser le système métrique anglais (°F, miles)", "it": "Usa il sistema metrico inglese (°F, miglia)", "es": "Utilice el sistema métrico inglés (°F, millas)", "pl": "Użyj angielskiego systemu metrycznego (°F, mile)"}, + "yrDescription": { "en": "yr.no is a joint service by the Norwegian Meteorological Institute and the Norwegian Broadcasting Corporation", "de": "yr.no ist ein gemeinsamer Service des Norwegisches Meteorologisches Institut und die Norwegische Rundfunkgesellschaft", "ru": "yr.no является совместным сервисом с помощью Норвежский метеорологический институт и Норвежская вещательная корпорация", "pt": "yr.no é um serviço conjunto da Instituto Meteorológico Norueguês e a Corporação Norueguesa de Radiodifusão", "nl": "yr.no is een gezamenlijke service van het doel Norwegian Meteorological Institute en de Norwegian Broadcasting Corporation", "fr": "yr.no est un service commun de la cible Norwegian Meteorological Institute et Norwegian Broadcasting Corporation", "it": "yr.no è un servizio comune di Norwegian Meteorological Institute e Norwegian Broadcasting Corporation", "es": "yr.no es un servicio conjunto del objetivo Norwegian Meteorological Institute y la Corporación noruega de radiodifusión", "pl": "yr.no to usługa wspólna według celu Norweski Instytut Meteorologiczny i Norweska korporacja radiowa"} +}; \ No newline at end of file diff --git a/admin/yr.png b/admin/yr.png new file mode 100644 index 0000000000000000000000000000000000000000..ce5087ee8ee6078451b63554138b68874e4a9d06 GIT binary patch literal 24656 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 O0000= literal 0 HcmV?d00001 diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..afce81d --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,21 @@ +version: 'test-{build}' +environment: + matrix: + - nodejs_version: '6' + - nodejs_version: '8' + - nodejs_version: '10' +platform: + - x86 + - x64 +clone_folder: 'c:\projects\%APPVEYOR_PROJECT_NAME%' +install: + - ps: 'Install-Product node $env:nodejs_version $env:platform' + - npm install + - npm install winston@2.3.1 + - 'npm install https://github.com/yunkong2/yunkong2.js-controller/tarball/master --production' +test_script: + - echo %cd% + - node --version + - npm --version + - npm test +build: 'off' diff --git a/forecast.xml b/forecast.xml new file mode 100644 index 0000000..770842d --- /dev/null +++ b/forecast.xml @@ -0,0 +1,403 @@ + + + + Karlsruhe + Administration centre + Germany + + + + + + + + + + + + + + + + + 2015-01-30T16:39:15 + 2015-01-31T05:00:00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..bb9412e --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,426 @@ +'use strict'; + +const gulp = require('gulp'); +const fs = require('fs'); +const pkg = require('./package.json'); +const iopackage = require('./io-package.json'); +const version = (pkg && pkg.version) ? pkg.version : iopackage.common.version; +/*const appName = getAppName(); + +function getAppName() { + const parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 1].split('.')[0].toLowerCase(); +} +*/ +const fileName = 'words.js'; +const languages = { + en: {}, + de: {}, + ru: {}, + pt: {}, + nl: {}, + fr: {}, + it: {}, + es: {}, + pl: {} +}; + +function lang2data(lang, isFlat) { + let str = isFlat ? '' : '{\n'; + let count = 0; + for (const w in lang) { + if (lang.hasOwnProperty(w)) { + count++; + if (isFlat) { + str += (lang[w] === '' ? (isFlat[w] || w) : lang[w]) + '\n'; + } else { + const key = ' "' + w.replace(/"/g, '\\"') + '": '; + str += key + '"' + lang[w].replace(/"/g, '\\"') + '",\n'; + } + } + } + if (!count) return isFlat ? '' : '{\n}'; + if (isFlat) { + return str; + } else { + return str.substring(0, str.length - 2) + '\n}'; + } +} + +function readWordJs(src) { + try { + let words; + if (fs.existsSync(src + 'js/' + fileName)) { + words = fs.readFileSync(src + 'js/' + fileName).toString(); + } else { + words = fs.readFileSync(src + fileName).toString(); + } + + const lines = words.split(/\r\n|\r|\n/g); + let i = 0; + while (!lines[i].match(/^systemDictionary = {/) && !lines[i].match(/^module.exports = {/)) { + i++; + } + lines.splice(0, i); + + // remove last empty lines + i = lines.length - 1; + while (!lines[i]) { + i--; + } + if (i < lines.length - 1) { + lines.splice(i + 1); + } + + lines[0] = lines[0].replace('systemDictionary = ', ''); + lines[0] = lines[0].replace('module.exports = ', ''); + lines[lines.length - 1] = lines[lines.length - 1].trim().replace(/};$/, '}'); + words = lines.join('\n'); + const resultFunc = new Function('return ' + words + ';'); + + return resultFunc(); + } catch (e) { + return null; + } +} +function padRight(text, totalLength) { + return text + (text.length < totalLength ? new Array(totalLength - text.length).join(' ') : ''); +} +function writeWordJs(data, src, isModule) { + let text = '// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM ' + src.replace(/^\./, '') + 'i18n\n'; + text += '/* global systemDictionary:true */\n'; + text += '\'use strict\';\n\n'; + + if (isModule) { + text += 'module.exports = {\n'; + } else { + text += 'systemDictionary = {\n'; + } + for (const word in data) { + if (data.hasOwnProperty(word)) { + text += ' ' + padRight('"' + word.replace(/"/g, '\\"') + '": {', 50); + let line = ''; + for (const lang in data[word]) { + if (data[word].hasOwnProperty(lang)) { + line += '"' + lang + '": "' + padRight(data[word][lang].replace(/"/g, '\\"') + '",', 50) + ' '; + } + } + if (line) { + line = line.trim(); + line = line.substring(0, line.length - 1); + } + text += line + '},\n'; + } + } + text = text.replace(/},\n$/, '}\n'); + text += '};'; + + if (fs.existsSync(src + 'js/' + fileName)) { + fs.writeFileSync(src + 'js/' + fileName, text); + } else { + fs.writeFileSync(src + '' + fileName, text); + } +} + +const EMPTY = ''; + +function words2languages(src) { + const langs = Object.assign({}, languages); + const data = readWordJs(src); + if (data) { + for (const word in data) { + if (data.hasOwnProperty(word)) { + for (const lang in data[word]) { + if (data[word].hasOwnProperty(lang)) { + langs[lang][word] = data[word][lang]; + // pre-fill all other languages + for (const j in langs) { + if (langs.hasOwnProperty(j)) { + langs[j][word] = langs[j][word] || EMPTY; + } + } + } + } + } + } + if (!fs.existsSync(src + 'i18n/')) { + fs.mkdirSync(src + 'i18n/'); + } + for (const l in langs) { + if (!langs.hasOwnProperty(l)) continue; + const keys = Object.keys(langs[l]); + //keys.sort(); + const obj = {}; + for (let k = 0; k < keys.length; k++) { + obj[keys[k]] = langs[l][keys[k]]; + } + if (!fs.existsSync(src + 'i18n/' + l)) { + fs.mkdirSync(src + 'i18n/' + l); + } + + fs.writeFileSync(src + 'i18n/' + l + '/translations.json', lang2data(obj)); + } + } else { + console.error('Cannot read or parse ' + fileName); + } +} +function words2languagesFlat(src) { + const langs = Object.assign({}, languages); + const data = readWordJs(src); + if (data) { + for (const word in data) { + if (data.hasOwnProperty(word)) { + for (const lang in data[word]) { + if (data[word].hasOwnProperty(lang)) { + langs[lang][word] = data[word][lang]; + // pre-fill all other languages + for (const j in langs) { + if (langs.hasOwnProperty(j)) { + langs[j][word] = langs[j][word] || EMPTY; + } + } + } + } + } + } + const keys = Object.keys(langs.en); + //keys.sort(); + for (const l in langs) { + if (!langs.hasOwnProperty(l)) continue; + const obj = {}; + for (let k = 0; k < keys.length; k++) { + obj[keys[k]] = langs[l][keys[k]]; + } + langs[l] = obj; + } + if (!fs.existsSync(src + 'i18n/')) { + fs.mkdirSync(src + 'i18n/'); + } + for (const ll in langs) { + if (!langs.hasOwnProperty(ll)) continue; + if (!fs.existsSync(src + 'i18n/' + ll)) { + fs.mkdirSync(src + 'i18n/' + ll); + } + + fs.writeFileSync(src + 'i18n/' + ll + '/flat.txt', lang2data(langs[ll], langs.en)); + } + fs.writeFileSync(src + 'i18n/flat.txt', keys.join('\n')); + } else { + console.error('Cannot read or parse ' + fileName); + } +} +function languagesFlat2words(src, isModule) { + const dirs = fs.readdirSync(src + 'i18n/'); + const langs = {}; + const bigOne = {}; + const order = Object.keys(languages); + dirs.sort(function (a, b) { + const posA = order.indexOf(a); + const posB = order.indexOf(b); + if (posA === -1 && posB === -1) { + if (a > b) return 1; + if (a < b) return -1; + return 0; + } else if (posA === -1) { + return -1; + } else if (posB === -1) { + return 1; + } else { + if (posA > posB) return 1; + if (posA < posB) return -1; + return 0; + } + }); + const keys = fs.readFileSync(src + 'i18n/flat.txt').toString().split('\n'); + + for (let l = 0; l < dirs.length; l++) { + if (dirs[l] === 'flat.txt') continue; + const lang = dirs[l]; + const values = fs.readFileSync(src + 'i18n/' + lang + '/flat.txt').toString().split('\n'); + langs[lang] = {}; + keys.forEach(function (word, i) { + langs[lang][word] = values[i].replace(/<\/ i>/g, '').replace(/<\/ b>/g, '').replace(/<\/ span>/g, '').replace(/% s/g, ' %s'); + }); + + const words = langs[lang]; + for (const word in words) { + if (words.hasOwnProperty(word)) { + bigOne[word] = bigOne[word] || {}; + if (words[word] !== EMPTY) { + bigOne[word][lang] = words[word]; + } + } + } + } + // read actual words.js + const aWords = readWordJs(); + + const temporaryIgnore = ['pt', 'fr', 'nl', 'flat.txt']; + if (aWords) { + // Merge words together + for (const w in aWords) { + if (aWords.hasOwnProperty(w)) { + if (!bigOne[w]) { + console.warn('Take from actual words.js: ' + w); + bigOne[w] = aWords[w] + } + dirs.forEach(function (lang) { + if (temporaryIgnore.indexOf(lang) !== -1) return; + if (!bigOne[w][lang]) { + console.warn('Missing "' + lang + '": ' + w); + } + }); + } + } + + } + + writeWordJs(bigOne, src, isModule); +} +function languages2words(src, isModule) { + const dirs = fs.readdirSync(src + 'i18n/'); + const langs = {}; + const bigOne = {}; + const order = Object.keys(languages); + dirs.sort(function (a, b) { + const posA = order.indexOf(a); + const posB = order.indexOf(b); + if (posA === -1 && posB === -1) { + if (a > b) return 1; + if (a < b) return -1; + return 0; + } else if (posA === -1) { + return -1; + } else if (posB === -1) { + return 1; + } else { + if (posA > posB) return 1; + if (posA < posB) return -1; + return 0; + } + }); + for (let l = 0; l < dirs.length; l++) { + if (dirs[l] === 'flat.txt') continue; + const lang = dirs[l]; + langs[lang] = fs.readFileSync(src + 'i18n/' + lang + '/translations.json').toString(); + langs[lang] = JSON.parse(langs[lang]); + const words = langs[lang]; + for (const word in words) { + if (words.hasOwnProperty(word)) { + bigOne[word] = bigOne[word] || {}; + if (words[word] !== EMPTY) { + bigOne[word][lang] = words[word]; + } + } + } + } + // read actual words.js + const aWords = readWordJs(); + + const temporaryIgnore = ['pt', 'fr', 'nl', 'it']; + if (aWords) { + // Merge words together + for (const w in aWords) { + if (aWords.hasOwnProperty(w)) { + if (!bigOne[w]) { + console.warn('Take from actual words.js: ' + w); + bigOne[w] = aWords[w] + } + dirs.forEach(function (lang) { + if (temporaryIgnore.indexOf(lang) !== -1) return; + if (!bigOne[w][lang]) { + console.warn('Missing "' + lang + '": ' + w); + } + }); + } + } + + } + + writeWordJs(bigOne, src, isModule); +} + +gulp.task('[Admin] words.js => json', function (done) { + words2languages('./admin/'); + done(); +}); + +gulp.task('[Admin] words.js => flat', function (done) { + words2languagesFlat('./admin/'); + done(); +}); + +gulp.task('[Admin] flat => words.js', function (done) { + languagesFlat2words('./admin/'); + done(); +}); + +gulp.task('[Admin] json => words.js', function (done) { + languages2words('./admin/'); + done(); +}); + +gulp.task('[lib] words.js => json', function (done) { + words2languages('./lib/'); + done(); +}); + +gulp.task('[lib] words.js => flat', function (done) { + words2languagesFlat('./lib/'); + done(); +}); + +gulp.task('[lib] flat => words.js', function (done) { + languagesFlat2words('./lib/', true); + done(); +}); + +gulp.task('[lib] json => words.js', function (done) { + languages2words('./lib/', true); + done(); +}); + + +gulp.task('updatePackages', function (done) { + iopackage.common.version = pkg.version; + iopackage.common.news = iopackage.common.news || {}; + if (!iopackage.common.news[pkg.version]) { + const news = iopackage.common.news; + const newNews = {}; + + newNews[pkg.version] = { + en: 'news', + de: 'neues', + ru: 'новое' + }; + iopackage.common.news = Object.assign(newNews, news); + } + fs.writeFileSync('io-package.json', JSON.stringify(iopackage, null, 4)); + done(); +}); + +gulp.task('updateReadme', function (done) { + const readme = fs.readFileSync('README.md').toString(); + const pos = readme.indexOf('## Changelog\n'); + if (pos !== -1) { + const readmeStart = readme.substring(0, pos + '## Changelog\n'.length); + const readmeEnd = readme.substring(pos + '## Changelog\n'.length); + + if (readme.indexOf(version) === -1) { + const timestamp = new Date(); + const date = timestamp.getFullYear() + '-' + + ('0' + (timestamp.getMonth() + 1).toString(10)).slice(-2) + '-' + + ('0' + (timestamp.getDate()).toString(10)).slice(-2); + + let news = ''; + if (iopackage.common.news && iopackage.common.news[pkg.version]) { + news += '* ' + iopackage.common.news[pkg.version].en; + } + + fs.writeFileSync('README.md', readmeStart + '### ' + version + ' (' + date + ')\n' + (news ? news + '\n\n' : '\n') + readmeEnd); + } + } + done(); +}); + +gulp.task('default', ['updatePackages', 'updateReadme']); \ No newline at end of file diff --git a/io-package.json b/io-package.json new file mode 100644 index 0000000..a961a05 --- /dev/null +++ b/io-package.json @@ -0,0 +1,544 @@ +{ + "common": { + "name": "yr", + "version": "2.0.2", + "news": { + "2.0.2": { + "en": "Warning! Breaking changes!\nRefactoring of states and roles", + "de": "Warnung! Breaking Änderungen!\nRefactoring von Zuständen und Rollen", + "ru": "Предупреждение! Нарушение изменений!\nРефакторинг состояний и ролей", + "pt": "Atenção! Quebrando mudanças!\nRefatoração de estados e papéis", + "nl": "Waarschuwing! Veranderingen doorbreken!\nRefactoring van staten en rollen", + "fr": "Attention! Briser les changements!\nRefactorisation des états et des rôles", + "it": "Avvertimento! Ultime modifiche!\nRefactoring di stati e ruoli", + "es": "¡Advertencia! ¡Rompiendo cambios!\nRefactorización de estados y roles", + "pl": "Ostrzeżenie! Łamanie zmian!\nRefaktoryzacja stanów i ról" + }, + "1.0.6": { + "en": "Update iconset", + "de": "Iconset geändert", + "ru": "Update iconset" + }, + "1.0.5": { + "en": "move weather widgets to this adapter", + "de": "Verschiebe Wetter-Widgets ins Adapter", + "ru": "Добавлены виджеты" + }, + "1.0.4": { + "en": "fix link to readme", + "de": "fix link to readme", + "ru": "fix link to readme" + } + }, + "title": "yr.no", + "titleLang": { + "en": "yr.no weather", + "de": "yr.no Wetter", + "ru": "yr.no погода", + "pt": "yr.no tempo", + "nl": "yr.nr weer", + "fr": "yr.no météo", + "it": "anno vecchio", + "es": "yr.no tiempo", + "pl": "yr.no pogody" + }, + "desc": { + "en": "Fetches 48h weather forecast from yr.no", + "de": "Holt 48h Wettervorhersage von yr.no", + "ru": "Получает прогноз погоды на 48h с yr.no", + "pt": "Fetches 48h previsão do tempo a partir de yr.no", + "nl": "Haal 48h weersvoorspelling op van yr.no", + "fr": "Fetchs 48h météo de yr.no", + "it": "Calcola le previsioni del tempo a 48 ore dall'anno n", + "es": "Obtiene 48h pronóstico del tiempo de yr.no", + "pl": "Pobiera 48-godzinną prognozę pogody z yr.no" + }, + "authors": [ + "hobbyquaker " + ], + "license": "MIT", + "platform": "Javascript/Node.js", + "mode": "schedule", + "schedule": "6 * * * *", + "enabled": true, + "loglevel": "info", + "materialize": true, + "keywords": ["weather", "forecast", "temperature", "rain", "yr", "meteorology"], + "main": "main.js", + "icon": "yr.png", + "extIcon": "https://git.spacen.net/yunkong2/yunkong2.yr/raw/master/admin/yr.png", + "readme": "https://github.com/yunkong2/yunkong2.yr/blob/master/README.md", + "allowInit": true, + "type": "weather" + }, + "native": { + "location": "", + "language": "", + "sendTranslations": true, + "nonMetric": false + }, + "objects": [ + + ], + "instanceObjects": [ + { + "_id": "forecast", + "type": "device", + "common": { + "name": "actual weather or forecast", + "role": "weather" + }, + "native": {} + }, + { + "_id": "forecast.day0", + "type": "channel", + "common": { + "name": "actual weather or forecast for today", + "role": "weather.forecast" + }, + "native": {} + }, + { + "_id": "forecast.day0.date", + "type": "state", + "common": { + "name": "Actual date", + "type": "string", + "role": "date", + "read": true, + "write": false, + "desc": "Actual date as string" + }, + "native": {} + }, + { + "_id": "forecast.day0.icon", + "type": "state", + "common": { + "name": "Actual icon", + "type": "string", + "role": "weather.icon", + "read": true, + "write": false, + "desc": "Path to icon" + }, + "native": {} + }, + { + "_id": "forecast.day0.state", + "type": "state", + "common": { + "name": "Actual state", + "type": "string", + "role": "weather.state", + "read": true, + "write": false, + "desc": "Description of weather" + }, + "native": {} + }, + { + "_id": "forecast.day0.temperatureActual", + "type": "state", + "common": { + "name": "Actual temperature", + "type": "number", + "role": "value.temperature", + "read": true, + "unit": "°C", + "write": false + }, + "native": {} + }, + { + "_id": "forecast.day0.temperatureMin", + "type": "state", + "common": { + "name": "Forecast of temperature for today", + "type": "number", + "role": "value.temperature.min.forecast.0", + "read": true, + "write": false, + "unit": "°C", + "desc": "Forecast of temperature for today. Minimum value." + }, + "native": {} + }, + { + "_id": "forecast.day0.temperatureMax", + "type": "state", + "common": { + "name": "Forecast of temperature for today", + "type": "number", + "role": "value.temperature.max.forecast.0", + "read": true, + "write": false, + "unit": "°C", + "desc": "Forecast of temperature for today. Maximum value." + }, + "native": {} + }, + { + "_id": "forecast.day0.precipitation", + "type": "state", + "common": { + "name": "Forecast of precipitation level for today (rain/snow)", + "type": "number", + "role": "weather.precipitation", + "read": true, + "write": false, + "unit": "mm", + "desc": "Forecast for rain or snow level in mm" + }, + "native": {} + }, + { + "_id": "forecast.day0.windDirection", + "type": "state", + "common": { + "name": "Forecast for wind direction for today", + "type": "string", + "role": "weather.direction.wind.forecast.0", + "read": true, + "write": false, + "desc": "Forecast for wind direction for today" + }, + "native": {} + }, + { + "_id": "forecast.day0.windSpeed", + "type": "state", + "common": { + "name": "Forecast for wind speed for today", + "type": "number", + "role": "value.speed.wind.forecast.0", + "read": true, + "write": false, + "unit": "km/h", + "desc": "Forecast for wind speed in km/h" + }, + "native": {} + }, + { + "_id": "forecast.day0.pressure", + "type": "state", + "common": { + "name": "Forecast for pressure for today", + "type": "number", + "role": "value.pressure.forecast.0", + "read": true, + "write": false, + "unit": "hPa", + "desc": "Forecast for pressure in hPa" + }, + "native": {} + }, + { + "_id": "forecast.day1", + "type": "channel", + "common": { + "name": "Forecast for tomorrow", + "role": "weather.forecast" + }, + "native": {} + }, + { + "_id": "forecast.day1.date", + "type": "state", + "common": { + "name": "Tomorrow date", + "type": "string", + "role": "date.forecast.1", + "read": true, + "write": false, + "desc": "Tomorrow date as string" + }, + "native": {} + }, + { + "_id": "forecast.day1.icon", + "type": "state", + "common": { + "name": "Icon for tomorrow", + "type": "string", + "role": "weather.icon.forecast.1", + "read": true, + "write": false, + "desc": "Path to icon for tomorrow" + }, + "native": {} + }, + { + "_id": "forecast.day1.state", + "type": "state", + "common": { + "name": "Text for tomorrow", + "type": "string", + "role": "weather.state.forecast.1", + "read": true, + "write": false, + "desc": "Description of weather for tomorrow" + }, + "native": {} + }, + { + "_id": "forecast.day1.temperatureMin", + "type": "state", + "common": { + "name": "Forecast of temperature for tomorrow", + "type": "number", + "role": "value.temperature.min.forecast.1", + "read": true, + "write": false, + "unit": "°C", + "desc": "Forecast of temperature for tomorrow. Minimum value." + }, + "native": {} + }, + { + "_id": "forecast.day1.temperatureMax", + "type": "state", + "common": { + "name": "Forecast of temperature for tomorrow", + "type": "number", + "role": "value.temperature.max.forecast.1", + "read": true, + "write": false, + "unit": "°C", + "desc": "Forecast of temperature for tomorrow. Maximum value." + }, + "native": {} + }, + { + "_id": "forecast.day1.precipitation", + "type": "state", + "common": { + "name": "Forecast of precipitation level for tomorrow (rain/snow)", + "type": "number", + "role": "value.precipitation.forecast.1", + "read": true, + "write": false, + "unit": "mm", + "desc": "Forecast for rain or snow level in mm" + }, + "native": {} + }, + { + "_id": "forecast.day1.windDirection", + "type": "state", + "common": { + "name": "Forecast for wind direction for tomorrow", + "type": "string", + "role": "weather.direction.wind.forecast.1", + "read": true, + "write": false, + "desc": "Forecast wind direction as text" + }, + "native": {} + }, + { + "_id": "forecast.day1.windSpeed", + "type": "state", + "common": { + "name": "Forecast for wind speed for tomorrow", + "type": "number", + "role": "value.speed.wind.forecast.1", + "read": true, + "write": false, + "unit": "km/h", + "desc": "Forecast for wind speed in km/h" + }, + "native": {} + }, + { + "_id": "forecast.day1.pressure", + "type": "state", + "common": { + "name": "Forecast for pressure for tomorrow", + "type": "number", + "role": "value.pressure.forecast.1", + "read": true, + "write": false, + "unit": "hPa", + "desc": "Forecast for pressure in hPa for tomorrow" + }, + "native": {} + }, + { + "_id": "forecast.day2", + "type": "channel", + "common": { + "name": "actual weather or forecast for the day after tomorrow", + "role": "weather.forecast" + }, + "native": {} + }, + { + "_id": "forecast.day2.date", + "type": "state", + "common": { + "name": "The day after tomorrow date", + "type": "string", + "role": "date.forecast.2", + "read": true, + "write": false, + "desc": "The day after tomorrow date as string" + }, + "native": {} + }, + { + "_id": "forecast.day2.icon", + "type": "state", + "common": { + "name": "Icon for the day after tomorrow", + "type": "string", + "role": "weather.icon.forecast.2", + "read": true, + "write": false, + "desc": "Path to icon for the day after tomorrow" + }, + "native": {} + }, + { + "_id": "forecast.day2.state", + "type": "state", + "common": { + "name": "Day after tomorrow state", + "type": "string", + "role": "weather.state.forecast.2", + "read": true, + "write": false, + "desc": "Description of weather for the day after tomorrow" + }, + "native": {} + }, + { + "_id": "forecast.day2.temperatureMin", + "type": "state", + "common": { + "name": "Forecast of temperature for the day after tomorrow", + "type": "number", + "role": "value.temperature.min.forecast.2", + "read": true, + "write": false, + "unit": "°C", + "desc": "Forecast of temperature for the day after tomorrow. Minimum value." + }, + "native": {} + }, + { + "_id": "forecast.day2.temperatureMax", + "type": "state", + "common": { + "name": "Forecast of temperature for the day after tomorrow", + "type": "number", + "role": "value.temperature.max.forecast.2", + "read": true, + "write": false, + "unit": "°C", + "desc": "Forecast of temperature for the day after tomorrow. Maximum value." + }, + "native": {} + }, + { + "_id": "forecast.day2.precipitation", + "type": "state", + "common": { + "name": "Forecast of precipitation level for the day after tomorrow (rain/snow)", + "type": "number", + "role": "value.precipitation.forecast.2", + "read": true, + "write": false, + "unit": "mm", + "desc": "Forecast for rain or snow level in mm" + }, + "native": {} + }, + { + "_id": "forecast.day2.windDirection", + "type": "state", + "common": { + "name": "Forecast for wind direction for the day after tomorrow", + "type": "string", + "role": "weather.direction.wind.forecast.2", + "read": true, + "write": false, + "desc": "Forecast for wind direction as text" + }, + "native": {} + }, + { + "_id": "forecast.day2.windSpeed", + "type": "state", + "common": { + "name": "Forecast for wind speed for the day after tomorrow", + "type": "number", + "role": "value.speed.wind.forecast.2", + "read": true, + "write": false, + "unit": "km/h", + "desc": "Forecast for wind speed in km/h" + }, + "native": {} + }, + { + "_id": "forecast.day2.pressure", + "type": "state", + "common": { + "name": "Forecast for pressure for the day after tomorrow", + "type": "number", + "role": "value.pressure.forecast.2", + "read": true, + "write": false, + "unit": "hPa", + "desc": "Forecast for pressure in hPa for the day after tomorrow" + }, + "native": {} + }, + { + "_id": "forecast.info", + "type": "channel", + "common": { + "name": "forecast links and objects" + }, + "native": {} + }, + { + "_id": "forecast.info.object", + "type": "state", + "common": { + "type": "object", + "name": "forecast table", + "role": "weather.table", + "read": true, + "write": false + }, + "native": {} + }, + { + "_id": "forecast.info.html", + "type": "state", + "common": { + "type": "string", + "name": "forecast html", + "role": "weather.html", + "read": true, + "write": false + }, + "native": {} + }, + { + "_id": "forecast.info.diagram", + "type": "state", + "common": { + "type": "string", + "name": "forecast diagram url", + "role": "weather.chart.url.forecast", + "read": true, + "write": false + }, + "native": {} + } + ] +} diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..a40fe76 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,83 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +let controllerDir; +let appName; + +/** + * returns application name + * + * The name of the application can be different and this function finds it out. + * + * @returns {string} + */ + function getAppName() { + const parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 2].split('.')[0]; +} + +/** + * looks for js-controller home folder + * + * @param {boolean} isInstall + * @returns {string} + */ +function getControllerDir(isInstall) { + // Find the js-controller location + const possibilities = [ + 'yunkong2.js-controller', + 'yunkong2.js-controller', + ]; + /** @type {string} */ + let controllerPath; + for (const pkg of possibilities) { + try { + const possiblePath = require.resolve(pkg); + if (fs.existsSync(possiblePath)) { + controllerPath = possiblePath; + break; + } + } catch (e) { /* not found */ } + } + if (!controllerPath) { + if (!isInstall) { + console.log('Cannot find js-controller'); + process.exit(10); + } else { + process.exit(); + } + } + // we found the controller + return path.dirname(controllerPath); +} + +/** + * reads controller base settings + * + * @alias getConfig + * @returns {object} + */ + function getConfig() { + let configPath; + if (fs.existsSync( + configPath = path.join(controllerDir, 'conf', appName + '.json') + )) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } else if (fs.existsSync( + configPath = path.join(controllerDir, 'conf', + appName.toLowerCase() + '.json') + )) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } else { + throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json'); + } +} +appName = getAppName(); +controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1); +const adapter = require(path.join(controllerDir, 'lib/adapter.js')); + +exports.controllerDir = controllerDir; +exports.getConfig = getConfig; +exports.Adapter = adapter; +exports.appName = appName; diff --git a/lib/words.js b/lib/words.js new file mode 100644 index 0000000..5b42094 --- /dev/null +++ b/lib/words.js @@ -0,0 +1,109 @@ +// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM /lib/i18n +/* global systemDictionary:true */ +'use strict'; + +module.exports = { + "Now": { "en": "Now", "de": "Jetzt", "ru": "Cейчас", "pt": "Now", "nl": "Now", "fr": "Now", "it": "Now", "es": "Now", "pl": "Now"}, + "Today": { "en": "Today", "de": "Heute", "ru": "Cегодня", "pt": "Today", "nl": "Today", "fr": "Today", "it": "Today", "es": "Today", "pl": "Today"}, + "Tomorrow": { "en": "Tomorrow", "de": "Morgen", "ru": "Завтра", "pt": "Tomorrow", "nl": "Tomorrow", "fr": "Tomorrow", "it": "Tomorrow", "es": "Tomorrow", "pl": "Tomorrow"}, + "After tomorrow": { "en": "After tomorrow", "de": "Übermorgen", "ru": "Послезавтра", "pt": "After tomorrow", "nl": "After tomorrow", "fr": "After tomorrow", "it": "After tomorrow", "es": "After tomorrow", "pl": "After tomorrow"}, + "Cloudy": { "en": "Cloudy", "de": "Wolkig", "ru": "Облачно", "pt": "Cloudy", "nl": "Cloudy", "fr": "Cloudy", "it": "Cloudy", "es": "Cloudy", "pl": "Cloudy"}, + "Snow": { "en": "Snow", "de": "Schnee", "ru": "Снег", "pt": "Snow", "nl": "Snow", "fr": "Snow", "it": "Snow", "es": "Snow", "pl": "Snow"}, + "Partly cloudy": { "en": "Partly cloudy", "de": "Teils wolkig", "ru": "Переменная облачность", "pt": "Partly cloudy", "nl": "Partly cloudy", "fr": "Partly cloudy", "it": "Partly cloudy", "es": "Partly cloudy", "pl": "Partly cloudy"}, + "Sleet": { "en": "Sleet", "de": "Schneeregen", "ru": "Снег с дождём", "pt": "Sleet", "nl": "Sleet", "fr": "Sleet", "it": "Sleet", "es": "Sleet", "pl": "Sleet"}, + "Tornado": { "en": "Tornado", "de": "Tornado", "ru": "Торнадо - сиди дома!", "pt": "Tornado", "nl": "Tornado", "fr": "Tornado", "it": "Tornado", "es": "Tornado", "pl": "Tornado"}, + "Tropical storm": { "en": "Tropical storm", "de": "Tropischer Sturm", "ru": "Тропический шторм", "pt": "Tropical storm", "nl": "Tropical storm", "fr": "Tropical storm", "it": "Tropical storm", "es": "Tropical storm", "pl": "Tropical storm"}, + "Hurricane": { "en": "Hurricane", "de": "Hurrikan", "ru": "Ураган", "pt": "Hurricane", "nl": "Hurricane", "fr": "Hurricane", "it": "Hurricane", "es": "Hurricane", "pl": "Hurricane"}, + "Severe thunderstorms": { "en": "Severe thunderstorms", "de": "Heftiges Gewitter", "ru": "Сильная непогода", "pt": "Severe thunderstorms", "nl": "Severe thunderstorms", "fr": "Severe thunderstorms", "it": "Severe thunderstorms", "es": "Severe thunderstorms", "pl": "Severe thunderstorms"}, + "Thunderstorms": { "en": "Thunderstorms", "de": "Gewitter", "ru": "Грозы", "pt": "Thunderstorms", "nl": "Thunderstorms", "fr": "Thunderstorms", "it": "Thunderstorms", "es": "Thunderstorms", "pl": "Thunderstorms"}, + "Mixed rain and snow": { "en": "Mixed rain and snow", "de": "Regen mit Schnee", "ru": "Дождь со снегом", "pt": "Mixed rain and snow", "nl": "Mixed rain and snow", "fr": "Mixed rain and snow", "it": "Mixed rain and snow", "es": "Mixed rain and snow", "pl": "Mixed rain and snow"}, + "Mixed rain and sleet": { "en": "Mixed rain and sleet", "de": "Regen mit Graupel", "ru": "Дождь с градом", "pt": "Mixed rain and sleet", "nl": "Mixed rain and sleet", "fr": "Mixed rain and sleet", "it": "Mixed rain and sleet", "es": "Mixed rain and sleet", "pl": "Mixed rain and sleet"}, + "Mixed snow and sleet": { "en": "Mixed snow and sleet", "de": "Schnee mit Graupel", "ru": "Снег с градом", "pt": "Mixed snow and sleet", "nl": "Mixed snow and sleet", "fr": "Mixed snow and sleet", "it": "Mixed snow and sleet", "es": "Mixed snow and sleet", "pl": "Mixed snow and sleet"}, + "Freezing drizzle": { "en": "Freezing drizzle", "de": "Eisnieselregen", "ru": "Изморозь", "pt": "Freezing drizzle", "nl": "Freezing drizzle", "fr": "Freezing drizzle", "it": "Freezing drizzle", "es": "Freezing drizzle", "pl": "Freezing drizzle"}, + "Drizzle": { "en": "Drizzle", "de": "Nieselregen", "ru": "Моросящий дождь", "pt": "Drizzle", "nl": "Drizzle", "fr": "Drizzle", "it": "Drizzle", "es": "Drizzle", "pl": "Drizzle"}, + "Freezing rain": { "en": "Freezing rain", "de": "Eisregen", "ru": "Ледяной дождь", "pt": "Freezing rain", "nl": "Freezing rain", "fr": "Freezing rain", "it": "Freezing rain", "es": "Freezing rain", "pl": "Freezing rain"}, + "Showers": { "en": "Showers", "de": "Regenschauer", "ru": "Ливень", "pt": "Showers", "nl": "Showers", "fr": "Showers", "it": "Showers", "es": "Showers", "pl": "Showers"}, + "Snow flurries": { "en": "Snow flurries", "de": "Schneetreiben", "ru": "Снегопад", "pt": "Snow flurries", "nl": "Snow flurries", "fr": "Snow flurries", "it": "Snow flurries", "es": "Snow flurries", "pl": "Snow flurries"}, + "Light snow showers": { "en": "Light snow showers", "de": "Leichter Regen mit Schnee", "ru": "Небольшой дождь со снегом", "pt": "Light snow showers", "nl": "Light snow showers", "fr": "Light snow showers", "it": "Light snow showers", "es": "Light snow showers", "pl": "Light snow showers"}, + "Bowing snow": { "en": "Bowing snow", "de": "Schnee", "ru": "Снег", "pt": "Bowing snow", "nl": "Bowing snow", "fr": "Bowing snow", "it": "Bowing snow", "es": "Bowing snow", "pl": "Bowing snow"}, + "Hail": { "en": "Hail", "de": "Hagel", "ru": "Град", "pt": "Hail", "nl": "Hail", "fr": "Hail", "it": "Hail", "es": "Hail", "pl": "Hail"}, + "Dust": { "en": "Dust", "de": "Staubig", "ru": "Пыльно", "pt": "Dust", "nl": "Dust", "fr": "Dust", "it": "Dust", "es": "Dust", "pl": "Dust"}, + "Foggy": { "en": "Foggy", "de": "Neblig", "ru": "Туманно", "pt": "Foggy", "nl": "Foggy", "fr": "Foggy", "it": "Foggy", "es": "Foggy", "pl": "Foggy"}, + "Haze": { "en": "Haze", "de": "Dunst", "ru": "Лёгкий туман", "pt": "Haze", "nl": "Haze", "fr": "Haze", "it": "Haze", "es": "Haze", "pl": "Haze"}, + "Smoky": { "en": "Smoky", "de": "Qualmig", "ru": "Задымление", "pt": "Smoky", "nl": "Smoky", "fr": "Smoky", "it": "Smoky", "es": "Smoky", "pl": "Smoky"}, + "Blustery": { "en": "Blustery", "de": "Stürmisch", "ru": "Порывистый ветер", "pt": "Blustery", "nl": "Blustery", "fr": "Blustery", "it": "Blustery", "es": "Blustery", "pl": "Blustery"}, + "Windy": { "en": "Windy", "de": "Windig", "ru": "Ветрянно", "pt": "Windy", "nl": "Windy", "fr": "Windy", "it": "Windy", "es": "Windy", "pl": "Windy"}, + "Cold": { "en": "Cold", "de": "Kalt", "ru": "Холодно", "pt": "Cold", "nl": "Cold", "fr": "Cold", "it": "Cold", "es": "Cold", "pl": "Cold"}, + "Mostly cloudy (night)": { "en": "Mostly cloudy (night)", "de": "Ü,berwiegend wölkig", "ru": "В основном облачно", "pt": "Mostly cloudy (night)", "nl": "Mostly cloudy (night)", "fr": "Mostly cloudy (night)", "it": "Mostly cloudy (night)", "es": "Mostly cloudy (night)", "pl": "Mostly cloudy (night)"}, + "Mostly cloudy (day)": { "en": "Mostly cloudy (day)", "de": "Ü,berwiegend wölkig", "ru": "В основном облачно", "pt": "Mostly cloudy (day)", "nl": "Mostly cloudy (day)", "fr": "Mostly cloudy (day)", "it": "Mostly cloudy (day)", "es": "Mostly cloudy (day)", "pl": "Mostly cloudy (day)"}, + "partly cloudy (night)": { "en": "partly cloudy (night)", "de": "Teilweise wolkig", "ru": "Местами облачно", "pt": "partly cloudy (night)", "nl": "partly cloudy (night)", "fr": "partly cloudy (night)", "it": "partly cloudy (night)", "es": "partly cloudy (night)", "pl": "partly cloudy (night)"}, + "partly cloudy (day)": { "en": "partly cloudy (day)", "de": "Meistens sonnig", "ru": "Приемущественно солнечно", "pt": "partly cloudy (day)", "nl": "partly cloudy (day)", "fr": "partly cloudy (day)", "it": "partly cloudy (day)", "es": "partly cloudy (day)", "pl": "partly cloudy (day)"}, + "Clear (night)": { "en": "Clear (night)", "de": "Klar", "ru": "Ясно", "pt": "Clear (night)", "nl": "Clear (night)", "fr": "Clear (night)", "it": "Clear (night)", "es": "Clear (night)", "pl": "Clear (night)"}, + "Sunny": { "en": "Sunny", "de": "Sonnig", "ru": "Солнечно", "pt": "Sunny", "nl": "Sunny", "fr": "Sunny", "it": "Sunny", "es": "Sunny", "pl": "Sunny"}, + "Fair (night)": { "en": "Fair (night)", "de": "Schönwetter", "ru": "Прекрасная погода", "pt": "Fair (night)", "nl": "Fair (night)", "fr": "Fair (night)", "it": "Fair (night)", "es": "Fair (night)", "pl": "Fair (night)"}, + "Fair (day)": { "en": "Fair (day)", "de": "Schöunwetter", "ru": "Прекрасная погода", "pt": "Fair (day)", "nl": "Fair (day)", "fr": "Fair (day)", "it": "Fair (day)", "es": "Fair (day)", "pl": "Fair (day)"}, + "Mixed rain and hail": { "en": "Mixed rain and hail", "de": "Regen mit Hagel", "ru": "Снег с градом", "pt": "Mixed rain and hail", "nl": "Mixed rain and hail", "fr": "Mixed rain and hail", "it": "Mixed rain and hail", "es": "Mixed rain and hail", "pl": "Mixed rain and hail"}, + "Hot": { "en": "Hot", "de": "Warm", "ru": "Жарко", "pt": "Hot", "nl": "Hot", "fr": "Hot", "it": "Hot", "es": "Hot", "pl": "Hot"}, + "Isolated thunderstorms": { "en": "Isolated thunderstorms", "de": "Vereinzeltes Gewitter", "ru": "Одиночные грозы", "pt": "Isolated thunderstorms", "nl": "Isolated thunderstorms", "fr": "Isolated thunderstorms", "it": "Isolated thunderstorms", "es": "Isolated thunderstorms", "pl": "Isolated thunderstorms"}, + "scattered thunderstorms": { "en": "scattered thunderstorms", "de": "Verstreutes Gewitter", "ru": "Грозы", "pt": "scattered thunderstorms", "nl": "scattered thunderstorms", "fr": "scattered thunderstorms", "it": "scattered thunderstorms", "es": "scattered thunderstorms", "pl": "scattered thunderstorms"}, + "scattered showers": { "en": "scattered showers", "de": "Verstreuter Regen", "ru": "Дождь", "pt": "scattered showers", "nl": "scattered showers", "fr": "scattered showers", "it": "scattered showers", "es": "scattered showers", "pl": "scattered showers"}, + "Heavy snow": { "en": "Heavy snow", "de": "Starker Schneefall", "ru": "Сильный снегопад", "pt": "Heavy snow", "nl": "Heavy snow", "fr": "Heavy snow", "it": "Heavy snow", "es": "Heavy snow", "pl": "Heavy snow"}, + "Scattered snow showers": { "en": "Scattered snow showers", "de": "Verstreuter Schneeregen", "ru": "Ливень с дождем", "pt": "Scattered snow showers", "nl": "Scattered snow showers", "fr": "Scattered snow showers", "it": "Scattered snow showers", "es": "Scattered snow showers", "pl": "Scattered snow showers"}, + "Thundershowers": { "en": "Thundershowers", "de": "Gewitterschauer", "ru": "Штормовой дождь", "pt": "Thundershowers", "nl": "Thundershowers", "fr": "Thundershowers", "it": "Thundershowers", "es": "Thundershowers", "pl": "Thundershowers"}, + "Snow showers": { "en": "Snow showers", "de": "Schneeregen", "ru": "Снег с дождем", "pt": "Snow showers", "nl": "Snow showers", "fr": "Snow showers", "it": "Snow showers", "es": "Snow showers", "pl": "Snow showers"}, + "Isolated thundershowers": { "en": "Isolated thundershowers", "de": "Vereinzelter Gewitterschauer", "ru": "Местами грозы", "pt": "Isolated thundershowers", "nl": "Isolated thundershowers", "fr": "Isolated thundershowers", "it": "Isolated thundershowers", "es": "Isolated thundershowers", "pl": "Isolated thundershowers"}, + "Fair": { "en": "Fair", "de": "Schönwetter", "ru": "Ясно", "pt": "Fair", "nl": "Fair", "fr": "Fair", "it": "Fair", "es": "Fair", "pl": "Fair"}, + "Clear sky": { "en": "Clear sky", "de": "Klarer Himmel", "ru": "Чистое небо", "pt": "Clear sky", "nl": "Clear sky", "fr": "Clear sky", "it": "Clear sky", "es": "Clear sky", "pl": "Clear sky"}, + "Rain": { "en": "Rain", "de": "Regen", "ru": "Дождь", "pt": "Rain", "nl": "Rain", "fr": "Rain", "it": "Rain", "es": "Rain", "pl": "Rain"}, + "Light rain": { "en": "Light rain", "de": "Leichter Regen", "ru": "Слабый дождик", "pt": "Light rain", "nl": "Light rain", "fr": "Light rain", "it": "Light rain", "es": "Light rain", "pl": "Light rain"}, + "Heavy rain": { "en": "Heavy rain", "de": "Heftiger Regen", "ru": "Сильный дождь", "pt": "Heavy rain", "nl": "Heavy rain", "fr": "Heavy rain", "it": "Heavy rain", "es": "Heavy rain", "pl": "Heavy rain"}, + "Rain showers": { "en": "Rain showers", "de": "Regenschauer", "ru": "Ливневый дождь", "pt": "Rain showers", "nl": "Rain showers", "fr": "Rain showers", "it": "Rain showers", "es": "Rain showers", "pl": "Rain showers"}, + "Light rain showers": { "en": "Light rain showers", "de": "Leichter Regenschauer", "ru": "Слабый ливневый дождь", "pt": "Light rain showers", "nl": "Light rain showers", "fr": "Light rain showers", "it": "Light rain showers", "es": "Light rain showers", "pl": "Light rain showers"}, + "Fog": { "en": "Fog", "de": "Nebel", "ru": "Туман", "pt": "Fog", "nl": "Fog", "fr": "Fog", "it": "Fog", "es": "Fog", "pl": "Fog"}, + "Heavy rain showers": { "en": "Heavy rain showers", "de": "Starke Regenschauer", "ru": "Сильный ливневый дождь", "pt": "Heavy rain showers", "nl": "Heavy rain showers", "fr": "Heavy rain showers", "it": "Heavy rain showers", "es": "Heavy rain showers", "pl": "Heavy rain showers"}, + "Light sleet": { "en": "Light sleet", "de": "Schneeregen", "ru": "Мокрый снег", "pt": "Light sleet", "nl": "Light sleet", "fr": "Light sleet", "it": "Light sleet", "es": "Light sleet", "pl": "Light sleet"}, + "Light snow": { "en": "Light snow", "de": "Leichter Schnee", "ru": "Слабый снег", "pt": "Light snow", "nl": "Light snow", "fr": "Light snow", "it": "Light snow", "es": "Light snow", "pl": "Light snow"}, + "Light sleet showers": { "en": "Light sleet showers", "de": "Leichter Graupelschauer", "ru": "Слабый дождь со снегом", "pt": "Light sleet showers", "nl": "Light sleet showers", "fr": "Light sleet showers", "it": "Light sleet showers", "es": "Light sleet showers", "pl": "Light sleet showers"}, + "Heavy rain and thunder": { "en": "Heavy rain and thunder", "de": "Heftiger Regen und Blitze", "ru": "Сильный дождь и молнии", "pt": "Heavy rain and thunder", "nl": "Heavy rain and thunder", "fr": "Heavy rain and thunder", "it": "Heavy rain and thunder", "es": "Heavy rain and thunder", "pl": "Heavy rain and thunder"}, + "Heavy rain showers and thunder": { "en": "Heavy rain showers and thunder", "de": "Heftiger Regenschauer und Blitze", "ru": "Сильный ливневый дождь и молнии", "pt": "Heavy rain showers and thunder", "nl": "Heavy rain showers and thunder", "fr": "Heavy rain showers and thunder", "it": "Heavy rain showers and thunder", "es": "Heavy rain showers and thunder", "pl": "Heavy rain showers and thunder"}, + "Heavy sleet": { "en": "Heavy sleet", "de": "Heftiger Schneeregen", "ru": "Сильный снегопад с дождем", "pt": "Heavy sleet", "nl": "Heavy sleet", "fr": "Heavy sleet", "it": "Heavy sleet", "es": "Heavy sleet", "pl": "Heavy sleet"}, + "Heavy sleet showers": { "en": "Heavy sleet showers", "de": "Heftiger Schneeregen", "ru": "Сильный снегопад с дождем", "pt": "Heavy sleet showers", "nl": "Heavy sleet showers", "fr": "Heavy sleet showers", "it": "Heavy sleet showers", "es": "Heavy sleet showers", "pl": "Heavy sleet showers"}, + "Heavy snow showers": { "en": "Heavy snow showers", "de": "Heftiger Schnee", "ru": "Сильный снегопад", "pt": "Heavy snow showers", "nl": "Heavy snow showers", "fr": "Heavy snow showers", "it": "Heavy snow showers", "es": "Heavy snow showers", "pl": "Heavy snow showers"}, + "Light rain and thunder": { "en": "Light rain and thunder", "de": "Leichter Regen und Blitze", "ru": "Слабый дождик и молнии", "pt": "Light rain and thunder", "nl": "Light rain and thunder", "fr": "Light rain and thunder", "it": "Light rain and thunder", "es": "Light rain and thunder", "pl": "Light rain and thunder"}, + "Light rain showers and thunder": { "en": "Light rain showers and thunder", "de": "Leichter Regenschauer und Blitze", "ru": "Слабый ливневый дождь и молнии", "pt": "Light rain showers and thunder", "nl": "Light rain showers and thunder", "fr": "Light rain showers and thunder", "it": "Light rain showers and thunder", "es": "Light rain showers and thunder", "pl": "Light rain showers and thunder"}, + "Rain and thunder": { "en": "Rain and thunder", "de": "Regen und Blitze", "ru": "Дождь с молниями", "pt": "Rain and thunder", "nl": "Rain and thunder", "fr": "Rain and thunder", "it": "Rain and thunder", "es": "Rain and thunder", "pl": "Rain and thunder"}, + "Rain showers and thunder": { "en": "Rain showers and thunder", "de": "Regenschauer und Blitze", "ru": "Ливневый дождь и молнии", "pt": "Rain showers and thunder", "nl": "Rain showers and thunder", "fr": "Rain showers and thunder", "it": "Rain showers and thunder", "es": "Rain showers and thunder", "pl": "Rain showers and thunder"}, + "Sleet showers": { "en": "Sleet showers", "de": "Schneeregen", "ru": "Дождь со снегом", "pt": "Sleet showers", "nl": "Sleet showers", "fr": "Sleet showers", "it": "Sleet showers", "es": "Sleet showers", "pl": "Sleet showers"}, + "N": { "en": "N", "de": "N", "ru": "C", "pt": "N", "nl": "N", "fr": "N", "it": "N", "es": "N", "pl": "N"}, + "S": { "en": "S", "de": "S", "ru": "Ю", "pt": "S", "nl": "S", "fr": "S", "it": "S", "es": "S", "pl": "S"}, + "W": { "en": "W", "de": "W", "ru": "З", "pt": "W", "nl": "W", "fr": "W", "it": "W", "es": "W", "pl": "W"}, + "E": { "en": "E", "de": "O", "ru": "В", "pt": "E", "nl": "E", "fr": "E", "it": "E", "es": "E", "pl": "E"}, + "NW": { "en": "NW", "de": "NW", "ru": "CЗ", "pt": "NW", "nl": "NW", "fr": "NW", "it": "NW", "es": "NW", "pl": "NW"}, + "NNW": { "en": "NNW", "de": "NNW", "ru": "CСЗ", "pt": "NNW", "nl": "NNW", "fr": "NNW", "it": "NNW", "es": "NNW", "pl": "NNW"}, + "WNW": { "en": "WNW", "de": "WNW", "ru": "ЗСЗ", "pt": "WNW", "nl": "WNW", "fr": "WNW", "it": "WNW", "es": "WNW", "pl": "WNW"}, + "NE": { "en": "NE", "de": "NO", "ru": "CВ", "pt": "NE", "nl": "NE", "fr": "NE", "it": "NE", "es": "NE", "pl": "NE"}, + "NNE": { "en": "NNE", "de": "NNO", "ru": "CСВ", "pt": "NNE", "nl": "NNE", "fr": "NNE", "it": "NNE", "es": "NNE", "pl": "NNE"}, + "ENE": { "en": "ENE", "de": "ONO", "ru": "ВCВ", "pt": "ENE", "nl": "ENE", "fr": "ENE", "it": "ENE", "es": "ENE", "pl": "ENE"}, + "SW": { "en": "SW", "de": "SW", "ru": "ЮЗ", "pt": "SW", "nl": "SW", "fr": "SW", "it": "SW", "es": "SW", "pl": "SW"}, + "SSW": { "en": "SSW", "de": "SSW", "ru": "ЮЮЗ", "pt": "SSW", "nl": "SSW", "fr": "SSW", "it": "SSW", "es": "SSW", "pl": "SSW"}, + "WSW": { "en": "WSW", "de": "WSW", "ru": "ЗЮЗ", "pt": "WSW", "nl": "WSW", "fr": "WSW", "it": "WSW", "es": "WSW", "pl": "WSW"}, + "SE": { "en": "SE", "de": "SO", "ru": "ЮВ", "pt": "SE", "nl": "SE", "fr": "SE", "it": "SE", "es": "SE", "pl": "SE"}, + "SSE": { "en": "SSE", "de": "SSO", "ru": "ЮЮВ", "pt": "SSE", "nl": "SSE", "fr": "SSE", "it": "SSE", "es": "SSE", "pl": "SSE"}, + "ESE": { "en": "ESE", "de": "OSO", "ru": "ВЮВ", "pt": "ESE", "nl": "ESE", "fr": "ESE", "it": "ESE", "es": "ESE", "pl": "ESE"}, + "North": { "en": "North", "de": "Norden", "ru": "Север", "pt": "Norte", "nl": "noorden", "fr": "Nord", "it": "Nord", "es": "Norte", "pl": "Północ"}, + "North-northeast": { "en": "North-northeast", "de": "Nord-Nordost", "ru": "Северо-северо-восток", "pt": "Norte-nordeste", "nl": "Noord-noordoosten", "fr": "Nord-nord-est", "it": "Nord-nordest", "es": "Norte-noreste", "pl": "Północno-północny wschód"}, + "Northeast": { "en": "Northeast", "de": "Nordost", "ru": "Северо-восток", "pt": "Nordeste", "nl": "Noordoosten", "fr": "Nord-Est", "it": "nord-est", "es": "Noreste", "pl": "Północny wschód"}, + "East-Northeast": { "en": "East-Northeast", "de": "Ost-Nordost", "ru": "Ост-норд-ост", "pt": "Leste-Nordeste", "nl": "Oost-Noordoost", "fr": "Est-Nord-Est", "it": "Est-Nord-Est", "es": "Este-Noreste", "pl": "Wschód-północny wschód"}, + "East": { "en": "East", "de": "Osten", "ru": "Восток", "pt": "Leste", "nl": "Oosten", "fr": "est", "it": "est", "es": "Este", "pl": "Wschód"}, + "East-Southeast": { "en": "East-Southeast", "de": "Ost-Südost", "ru": "Восток-Юго", "pt": "Leste-Sudeste", "nl": "Oost-Zuidoost", "fr": "Est-Sud-Est", "it": "Est-Sud-Est", "es": "Este-Sureste", "pl": "Wschód-południowy-wschód"}, + "Southeast": { "en": "Southeast", "de": "Süd-Ost", "ru": "Юго-восток", "pt": "Sudeste", "nl": "zuidoosten", "fr": "Sud-Est", "it": "sud-est", "es": "Sureste", "pl": "Południowy wschód"}, + "South-southeast": { "en": "South-southeast", "de": "Süd-Südost", "ru": "Юго-юго-восток", "pt": "Sul-sudeste", "nl": "Zuid-zuidoost", "fr": "Sud-sud-est", "it": "Sud-sud-est", "es": "Sur-sureste", "pl": "Południowo-południowy wschód"}, + "South": { "en": "South", "de": "Süd", "ru": "Юг", "pt": "Sul", "nl": "zuiden", "fr": "Sud", "it": "Sud", "es": "Sur", "pl": "południe"}, + "South-southwest": { "en": "South-southwest", "de": "Süd-Südwesten", "ru": "Юго-юго-запад", "pt": "Sul-sudoeste", "nl": "Zuid-zuidwest", "fr": "Sud-sud-ouest", "it": "Sud-sud-ovest", "es": "Sur-suroeste", "pl": "Południowo-południowy zachód"}, + "Southwest": { "en": "Southwest", "de": "Südwesten", "ru": "Юго-запад", "pt": "Sudoeste", "nl": "Southwest", "fr": "Sud-ouest", "it": "sud-ovest", "es": "Sur oeste", "pl": "Południowy zachód"}, + "West-southwest": { "en": "West-southwest", "de": "West-Südwesten", "ru": "Западно-юго-запад", "pt": "Oeste-sudoeste", "nl": "West-zuidwesten", "fr": "Ouest-sud-ouest", "it": "Ovest-sud-ovest", "es": "Oeste-suroeste", "pl": "Zachód-południowy zachód"}, + "West": { "en": "West", "de": "Westen", "ru": "Запад", "pt": "Oeste", "nl": "westen", "fr": "Ouest", "it": "ovest", "es": "Oeste", "pl": "Zachód"}, + "West-northwest": { "en": "West-northwest", "de": "West-Nordwesten", "ru": "Вест-норд-вест", "pt": "Oeste-noroeste", "nl": "West-noordwesten", "fr": "Ouest-nord-ouest", "it": "Ovest-Nord-Ovest", "es": "Oeste-noroeste", "pl": "Zachód-północny-zachód"}, + "Northwest": { "en": "Northwest", "de": "Nordwest", "ru": "Северо-Запад", "pt": "Noroeste", "nl": "Noord West", "fr": "Nord Ouest", "it": "Nord Ovest", "es": "Noroeste", "pl": "Północny zachód"}, + "North-northwest": { "en": "North-northwest", "de": "Nord Nord West", "ru": "Северо-северо-запад", "pt": "Norte-noroeste", "nl": "Noordnoordwest", "fr": "Nord-nord-ouest", "it": "Nord-Nord-Ovest", "es": "Norte-noroeste", "pl": "Północno-północny zachód"} +}; \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..a440ab9 --- /dev/null +++ b/main.js @@ -0,0 +1,348 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +'use strict'; + +const xml2js = require('xml2js'); +const http = require('http'); +const utils = require(__dirname + '/lib/utils'); // Get common adapter utils +const dictionary = require(__dirname + '/lib/words'); + +const adapter = utils.Adapter({ + name: 'yr', // adapter name + useFormatDate: true // read date format from config +}); + +adapter.on('ready', main); + +function main() { + const tmp = adapter.config.location.split('/'); + const city = decodeURI(tmp.pop()); + + adapter.config.language = adapter.config.language || 'en'; + if (adapter.config.sendTranslations === undefined) adapter.config.sendTranslations = true; + if (adapter.config.sendTranslations === 'true') adapter.config.sendTranslations = true; + if (adapter.config.sendTranslations === 'false') adapter.config.sendTranslations = false; + + adapter.getObject('forecast.day0.temperatureActual', (err, obj) => { + if (obj && obj.common && obj.common.unit) { + if (obj.common.unit === '°C' && adapter.config.nonMetric) { + obj.common.unit = '°F'; + adapter.setObject(obj._id, obj, () => { + adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`); + }); + } else if (obj.common.unit !== '°C' && !adapter.config.nonMetric) { + obj.common.unit = '°C'; + adapter.setObject(obj._id, obj, () => { + adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`); + }); + } + } + }); + + for (let d = 0; d < 3; d++) { + adapter.getObject('forecast.day' + d + '.windSpeed', (err, obj) => { + if (obj && obj.common && obj.common.unit) { + if (obj.common.unit === 'km/h' && adapter.config.nonMetric) { + obj.common.unit = 'm/h'; + adapter.setObject(obj._id, obj, () => { + adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`); + }); + } else if (obj.common.unit !== 'km/h' && !adapter.config.nonMetric) { + obj.common.unit = 'km/h'; + adapter.setObject(obj._id, obj, () => { + adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`); + }); + } + } + }); + adapter.getObject('forecast.day' + d + '.temperatureMin', (err, obj) => { + if (obj && obj.common && obj.common.unit) { + if (obj.common.unit === '°C' && adapter.config.nonMetric) { + obj.common.unit = '°F'; + adapter.setObject(obj._id, obj, () => { + adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`); + }); + } else if (obj.common.unit !== '°C' && !adapter.config.nonMetric) { + obj.common.unit = '°C'; + adapter.setObject(obj._id, obj, () => { + adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`); + }); + } + } + }); + adapter.getObject('forecast.day' + d + '.temperatureMax', (err, obj) => { + if (obj && obj.common && obj.common.unit) { + if (obj.common.unit === '°C' && adapter.config.nonMetric) { + obj.common.unit = '°F'; + adapter.setObject(obj._id, obj, () => { + adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`); + }); + } else if (obj.common.unit !== '°C' && !adapter.config.nonMetric) { + obj.common.unit = '°C'; + adapter.setObject(obj._id, obj, () => { + adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`); + }); + } + } + }); + adapter.getObject('forecast.day' + d + '.precipitation', (err, obj) => { + if (obj && obj.common && obj.common.unit) { + if (obj.common.unit === 'mm' && adapter.config.nonMetric) { + obj.common.unit = 'in'; + adapter.setObject(obj._id, obj, () => { + adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`); + }); + } else if (obj.common.unit !== 'mm' && !adapter.config.nonMetric) { + obj.common.unit = 'mm'; + adapter.setObject(obj._id, obj, () => { + adapter.log.info(`Metrics changed for ${obj._id} to ${obj.common.unit}`); + }); + } + } + }); + } + + adapter.getObject('forecast', (err, obj) => { + if (!obj || !obj.common || obj.common.name !== 'yr.no forecast ' + city) { + adapter.setObject('forecast', { + type: 'device', + role: 'forecast', + common: { + name: 'yr.no forecast ' + city + }, + native: { + url: adapter.config.location, + country: decodeURI(tmp[0]), + state: decodeURI(tmp[1]), + city: city + } + }); + } + }); + + if (adapter.config.location.indexOf('forecast.xml') === -1) { + if (adapter.config.location.indexOf('%') === -1) adapter.config.location = encodeURI(adapter.config.location); + + adapter.setState('forecast.info.diagram', 'http://www.yr.no/place/' + adapter.config.location + '/avansert_meteogram.png', true); + + const reqOptions = { + hostname: 'www.yr.no', + port: 80, + path: '/place/' + adapter.config.location + '/forecast.xml', + method: 'GET' + }; + + adapter.log.debug('get http://' + reqOptions.hostname + reqOptions.path); + + const req = http.request(reqOptions, res => { + let data = ''; + + res.on('data', chunk => data += chunk); + + res.on('end', () => { + adapter.log.debug('received data from yr.no'); + parseData(data.toString()); + }); + }); + + req.on('error', e => { + adapter.log.error(e.message); + parseData(null); + }); + + req.end(); + } else { + parseData(require('fs').readFileSync(adapter.config.location).toString()); + } + + // Force terminate after 5min + setTimeout(() => { + adapter.log.error('force terminate'); + process.exit(1); + }, 300000); +} + +function _(text) { + if (!text) return ''; + + if (dictionary[text]) { + let newText = dictionary[text][adapter.config.language]; + if (newText) { + return newText; + } else if (adapter.config.language !== 'en') { + newText = dictionary[text].en; + if (newText) { + return newText; + } + } + } else { + if (adapter.config.sendTranslations) { + const options = { + hostname: 'download.yunkong2.net', + port: 80, + path: '/yr.php?word=' + encodeURIComponent(text) + }; + const req = http.request(options, res => { + console.log('STATUS: ' + res.statusCode); + adapter.log.info('Missing translation sent to yunkong2.net: "' + text + '"'); + }); + req.on('error', e => { + adapter.log.error('Cannot send to server missing translation for "' + text + '": ' + e.message); + }); + req.end(); + } else { + adapter.log.warn('Translate: "' + text + '": {"en": "' + text + '", "de": "' + text + '", "ru": "' + text + '"}, please send to developer'); + } + } + return text; +} + +function celsius2fahrenheit(degree, isConvert) { + if (isConvert) { + return degree * 9 / 5 + 32; + } else { + return degree; + } +} + +function parseData(xml) { + if (!xml) { + setTimeout(() => process.exit(0), 5000); + return; + } + const options = { + explicitArray: false, + mergeAttrs: true + }; + const parser = new xml2js.Parser(options); + parser.parseString(xml, (err, result) => { + if (err) { + adapter.log.error(err); + } else { + adapter.log.info('got weather data from yr.no'); + const forecastArr = result.weatherdata.forecast.tabular.time; + + let tableDay = ''; + let tableHead = ''; + let tableMiddle = ''; + let tableBottom = ''; + const dateObj = new Date(); + const dayEnd = dateObj.getFullYear() + '-' + ('0' + (dateObj.getMonth() + 1)).slice(-2) + '-' + ('0' + dateObj.getDate()).slice(-2) + 'T24:00:00'; + let daySwitch = false; + + let day = -1; // Start from today + const days = []; + for (let i = 0; i < 12 && i < forecastArr.length; i++) { + const period = forecastArr[i]; + + if (!period.period || period.period === '0') day++; + + // We want to process only today, tomorrow and the day after tomorrow + if (day === 3) break; + period.symbol.url = '/adapter/yr/icons/' + period.symbol.var + '.svg'; + period.symbol.name = _(period.symbol.name); + period.windDirection.code = _(period.windDirection.code); + period.windDirection.name = _(period.windDirection.name); + + if (i < 8) { + switch (i) { + case 0: + tableHead += ''; + break; + default: + if (period.from > dayEnd) { + if (!daySwitch) { + daySwitch = true; + tableDay += ''; + if (i < 3) tableDay += ''; + tableHead += ''; + } else { + tableHead += ''; + } + + } else { + tableHead += ''; + } + } + + tableMiddle += ''; + } + + if (day === -1 && !i) day = 0; + if (!days[day]) { + days[day] = { + date: new Date(period.from), + icon: period.symbol.url, + state: period.symbol.name, + temperatureMin: celsius2fahrenheit(parseFloat(period.temperature.value), adapter.config.nonMetric), + temperatureMax: celsius2fahrenheit(parseFloat(period.temperature.value), adapter.config.nonMetric), + precipitation: adapter.config.nonMetric ? parseFloat(period.precipitation.value) / 25.4 : parseFloat(period.precipitation.value), + windDirection: period.windDirection.code, + windSpeed: adapter.config.nonMetric ? parseFloat(period.windSpeed.mps) : parseFloat(period.windSpeed.mps) * 3.6, + pressure: parseFloat(period.pressure.value), + count: 1 + }; + } else { + // Summarize + let t; + // Take icon for day always from 12:00 to 18:00 if possible + if (i === 2) { + days[day].icon = period.symbol.url; + days[day].state = period.symbol.name; + days[day].windDirection = period.windDirection.code; + } + t = celsius2fahrenheit(parseFloat(period.temperature.value), adapter.config.nonMetric); + if (t < days[day].temperatureMin) { + days[day].temperatureMin = t; + } else + if (t > days[day].temperatureMax) { + days[day].temperatureMax = t; + } + + days[day].precipitation += adapter.config.nonMetric ? parseFloat(period.precipitation.value) / 25.4 : parseFloat(period.precipitation.value); + days[day].windSpeed += adapter.config.nonMetric ? parseFloat(period.windSpeed.mps) : parseFloat(period.windSpeed.mps) * 3.6; + days[day].pressure += parseFloat(period.pressure.value); + days[day].count++; + } + // Set actual temperature + if (!day && !i) { + days[day].temperatureActual = celsius2fahrenheit(parseInt(period.temperature.value, 10), adapter.config.nonMetric); + } + } + const style = ''; + const table = style + tableDay + tableHead + tableMiddle + tableBottom + '
' + _('Now') + '' + _('Today') + '' + _('Tomorrow') + '' + _('After tomorrow') + '' + parseInt(period.from.substring(11, 13), 10).toString() + '-' + parseInt(period.to.substring(11, 13), 10).toString() + '' + parseInt(period.from.substring(11, 13), 10).toString() + '-' + parseInt(period.to.substring(11, 13), 10).toString() + '' + parseInt(period.from.substring(11, 13), 10).toString() + '-' + parseInt(period.to.substring(11, 13), 10).toString() + '' + period.symbol.name + '
'; + tableBottom += '
' + period.temperature.value + '°C
'; + //console.log(JSON.stringify(result, null, " ")); + + for (day = 0; day < days.length; day++) { + // Take the average + if (days[day].count > 1) { + days[day].precipitation /= days[day].count; + days[day].windSpeed /= days[day].count; + days[day].pressure /= days[day].count; + } + days[day].temperatureMin = Math.round(days[day].temperatureMin); + days[day].temperatureMax = Math.round(days[day].temperatureMax); + days[day].precipitation = Math.round(days[day].precipitation); + days[day].windSpeed = Math.round(days[day].windSpeed * 10) / 10; + days[day].pressure = Math.round(days[day].pressure); + + days[day].date = adapter.formatDate(days[day].date); + + delete days[day].count; + for (const name in days[day]) { + if (days[day].hasOwnProperty(name)) { + adapter.setState('forecast.day' + day + '.' + name, {val: days[day][name], ack: true}); + } + } + } + + adapter.log.debug('data successfully parsed. setting states'); + + adapter.setState('forecast.info.html', {val: table, ack: true}); + adapter.setState('forecast.info.object', {val: JSON.stringify(days), ack: true}, () => { + setTimeout(() => process.exit(0), 5000); + }); + } + }); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..90dd0da --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "yunkong2.yr", + "description": "Fetches 48h weather forecast from yr.no", + "version": "2.0.2", + "contributors": [ + "bluefox ", + "hobbyquaker " + ], + "homepage": "https://github.com/yunkong2/yunkong2.yr", + "repository": { + "type": "git", + "url": "https://github.com/yunkong2/yunkong2.yr" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://github.com/yunkong2/yunkong2.yr/blob/master/LICENSE" + } + ], + "keywords": [ + "yunkong2", + "weather", + "Home automation", + "yr.no" + ], + "dependencies": { + "xml2js": "^0.4.19" + }, + "devDependencies": { + "gulp": "^3.9.1", + "mocha": "^5.2.0", + "chai": "^4.1.2" + }, + "bugs": { + "url": "https://github.com/yunkong2/yunkong2.yr/issues" + }, + "main": "main.js", + "scripts": { + "test": "node node_modules/mocha/bin/mocha --exit" + }, + "author": "hobbyquaker ", + "license": "MIT" +} diff --git a/server/translations.sql b/server/translations.sql new file mode 100644 index 0000000..e0c681d --- /dev/null +++ b/server/translations.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS `translations` ( + `sentence` varchar(100) COLLATE utf8_bin NOT NULL, + KEY `sentence` (`sentence`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin; diff --git a/server/yr.php b/server/yr.php new file mode 100644 index 0000000..14e2b55 --- /dev/null +++ b/server/yr.php @@ -0,0 +1,39 @@ +connect_error) { + die("Connection failed: " . $conn->connect_error); +} + + +$sql = "SELECT COUNT(*) FROM translations WHERE sentence='".$word."'"; +$result = $conn->query($sql); +//var_dump($result); +if ($result->num_rows > 0) { + $row = $result->fetch_assoc(); + $total = $row["COUNT(*)"]; + //var_dump($total); + if ($total == 0) { + $sql = "INSERT INTO translations (sentence) VALUES ('".$word."')"; + $conn->query($sql); + } +} else { + //var_dump("p*".$total); + $sql = "INSERT INTO translations (sentence) VALUES ('".$word."')"; + $conn->query($sql); +} + +$conn->close(); + +?> diff --git a/tasks/jscs.js b/tasks/jscs.js new file mode 100644 index 0000000..588b6f2 --- /dev/null +++ b/tasks/jscs.js @@ -0,0 +1,17 @@ +var srcDir = __dirname + "/../"; + +module.exports = { + all: { + src: [ + srcDir + "*.js", + srcDir + "lib/*.js", + srcDir + "adapter/example/*.js", + srcDir + "tasks/**/*.js", + srcDir + "www/**/*.js", + '!' + srcDir + "www/lib/**/*.js", + '!' + srcDir + 'node_modules/**/*.js', + '!' + srcDir + 'adapter/*/node_modules/**/*.js' + ], + options: require('./jscsRules.js') + } +}; \ No newline at end of file diff --git a/tasks/jscsRules.js b/tasks/jscsRules.js new file mode 100644 index 0000000..ded301d --- /dev/null +++ b/tasks/jscsRules.js @@ -0,0 +1,36 @@ +module.exports = { + force: true, + "requireCurlyBraces": ["else", "for", "while", "do", "try", "catch"], /*"if",*/ + "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], + "requireSpaceBeforeBlockStatements": true, + "requireParenthesesAroundIIFE": true, + "disallowSpacesInFunctionDeclaration": {"beforeOpeningRoundBrace": true}, + "disallowSpacesInNamedFunctionExpression": {"beforeOpeningRoundBrace": true}, + "requireSpacesInFunctionExpression": {"beforeOpeningCurlyBrace": true}, + "requireSpacesInAnonymousFunctionExpression": {"beforeOpeningRoundBrace": true, "beforeOpeningCurlyBrace": true}, + "requireSpacesInNamedFunctionExpression": {"beforeOpeningCurlyBrace": true}, + "requireSpacesInFunctionDeclaration": {"beforeOpeningCurlyBrace": true}, + "disallowMultipleVarDecl": true, + "requireBlocksOnNewline": true, + "disallowEmptyBlocks": true, + "disallowSpacesInsideObjectBrackets": true, + "disallowSpacesInsideArrayBrackets": true, + "disallowSpaceAfterObjectKeys": true, + "disallowSpacesInsideParentheses": true, + "requireCommaBeforeLineBreak": true, + //"requireAlignedObjectValues": "all", + "requireOperatorBeforeLineBreak": ["?", "+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], +// "disallowLeftStickedOperators": ["?", "+", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], +// "requireRightStickedOperators": ["!"], +// "requireSpaceAfterBinaryOperators": ["?", "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], + //"disallowSpaceAfterBinaryOperators": [","], + "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], + "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], + "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], + "requireSpaceAfterBinaryOperators": ["?", ">", ",", ">=", "<=", "<", "+", "-", "/", "*", "=", "==", "===", "!=", "!=="], + //"validateIndentation": 4, + //"validateQuoteMarks": { "mark": "\"", "escape": true }, + "disallowMixedSpacesAndTabs": true, + "disallowKeywordsOnNewLine": ["else", "catch"] + +}; diff --git a/tasks/jshint.js b/tasks/jshint.js new file mode 100644 index 0000000..f823ebc --- /dev/null +++ b/tasks/jshint.js @@ -0,0 +1,17 @@ +var srcDir = __dirname + "/../"; + +module.exports = { + options: { + force: true + }, + all: [ + srcDir + "*.js", + srcDir + "lib/*.js", + srcDir + "adapter/example/*.js", + srcDir + "tasks/**/*.js", + srcDir + "www/**/*.js", + '!' + srcDir + "www/lib/**/*.js", + '!' + srcDir + 'node_modules/**/*.js', + '!' + srcDir + 'adapter/*/node_modules/**/*.js' + ] +}; \ No newline at end of file diff --git a/test/lib/example.xml b/test/lib/example.xml new file mode 100644 index 0000000..676e1c4 --- /dev/null +++ b/test/lib/example.xml @@ -0,0 +1,413 @@ + + + + Karlsruhe + Administration centre + Germany + + + + + + + + + + + + + + + + + 2018-07-28T09:38:00 + 2018-07-28T22:00:00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/lib/forecast.xml b/test/lib/forecast.xml new file mode 100644 index 0000000..773e916 --- /dev/null +++ b/test/lib/forecast.xml @@ -0,0 +1,413 @@ + + + + Stuttgart + Regional capital + Germany + + + + + + + + + + + + + + + + + 2016-03-15T04:39:00 + 2016-03-15T17:00:00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/lib/setup.js b/test/lib/setup.js new file mode 100644 index 0000000..09cc5f3 --- /dev/null +++ b/test/lib/setup.js @@ -0,0 +1,728 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +// check if tmp directory exists +var fs = require('fs'); +var path = require('path'); +var child_process = require('child_process'); +var rootDir = path.normalize(__dirname + '/../../'); +var pkg = require(rootDir + 'package.json'); +var debug = typeof v8debug === 'object'; +pkg.main = pkg.main || 'main.js'; + +var adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/'); +adapterName = adapterName[adapterName.length - 2]; +var adapterStarted = false; + +function getAppName() { + var parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 3].split('.')[0]; +} + +var appName = getAppName().toLowerCase(); + +var objects; +var states; + +var pid = null; + +function copyFileSync(source, target) { + + var targetFile = target; + + //if target is a directory a new file with the same name will be created + if (fs.existsSync(target)) { + if ( fs.lstatSync( target ).isDirectory() ) { + targetFile = path.join(target, path.basename(source)); + } + } + + try { + fs.writeFileSync(targetFile, fs.readFileSync(source)); + } + catch (err) { + console.log("file copy error: " +source +" -> " + targetFile + " (error ignored)"); + } +} + +function copyFolderRecursiveSync(source, target, ignore) { + var files = []; + + var base = path.basename(source); + if (base === adapterName) { + base = pkg.name; + } + //check if folder needs to be created or integrated + var targetFolder = path.join(target, base); + if (!fs.existsSync(targetFolder)) { + fs.mkdirSync(targetFolder); + } + + //copy + if (fs.lstatSync(source).isDirectory()) { + files = fs.readdirSync(source); + files.forEach(function (file) { + if (ignore && ignore.indexOf(file) !== -1) { + return; + } + + var curSource = path.join(source, file); + var curTarget = path.join(targetFolder, file); + if (fs.lstatSync(curSource).isDirectory()) { + // ignore grunt files + if (file.indexOf('grunt') !== -1) return; + if (file === 'chai') return; + if (file === 'mocha') return; + copyFolderRecursiveSync(curSource, targetFolder, ignore); + } else { + copyFileSync(curSource, curTarget); + } + }); + } +} + +if (!fs.existsSync(rootDir + 'tmp')) { + fs.mkdirSync(rootDir + 'tmp'); +} + +function storeOriginalFiles() { + console.log('Store original files...'); + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0'] && objects['system.adapter.admin.0'].common) { + objects['system.adapter.admin.0'].common.enabled = false; + } + if (objects['system.adapter.admin.1'] && objects['system.adapter.admin.1'].common) { + objects['system.adapter.admin.1'].common.enabled = false; + } + + fs.writeFileSync(dataDir + 'objects.json.original', JSON.stringify(objects)); + try { + f = fs.readFileSync(dataDir + 'states.json'); + fs.writeFileSync(dataDir + 'states.json.original', f); + } + catch (err) { + console.log('no states.json found - ignore'); + } +} + +function restoreOriginalFiles() { + console.log('restoreOriginalFiles...'); + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var f = fs.readFileSync(dataDir + 'objects.json.original'); + fs.writeFileSync(dataDir + 'objects.json', f); + try { + f = fs.readFileSync(dataDir + 'states.json.original'); + fs.writeFileSync(dataDir + 'states.json', f); + } + catch (err) { + console.log('no states.json.original found - ignore'); + } + +} + +function checkIsAdapterInstalled(cb, counter, customName) { + customName = customName || pkg.name.split('.').pop(); + counter = counter || 0; + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + console.log('checkIsAdapterInstalled...'); + + try { + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.' + customName + '.0']) { + console.log('checkIsAdapterInstalled: ready!'); + setTimeout(function () { + if (cb) cb(); + }, 100); + return; + } else { + console.warn('checkIsAdapterInstalled: still not ready'); + } + } catch (err) { + + } + + if (counter > 20) { + console.error('checkIsAdapterInstalled: Cannot install!'); + if (cb) cb('Cannot install'); + } else { + console.log('checkIsAdapterInstalled: wait...'); + setTimeout(function() { + checkIsAdapterInstalled(cb, counter + 1); + }, 1000); + } +} + +function checkIsControllerInstalled(cb, counter) { + counter = counter || 0; + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + console.log('checkIsControllerInstalled...'); + try { + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0']) { + console.log('checkIsControllerInstalled: installed!'); + setTimeout(function () { + if (cb) cb(); + }, 100); + return; + } + } catch (err) { + + } + + if (counter > 20) { + console.log('checkIsControllerInstalled: Cannot install!'); + if (cb) cb('Cannot install'); + } else { + console.log('checkIsControllerInstalled: wait...'); + setTimeout(function() { + checkIsControllerInstalled(cb, counter + 1); + }, 1000); + } +} + +function installAdapter(customName, cb) { + if (typeof customName === 'function') { + cb = customName; + customName = null; + } + customName = customName || pkg.name.split('.').pop(); + console.log('Install adapter...'); + var startFile = 'node_modules/' + appName + '.js-controller/' + appName + '.js'; + // make first install + if (debug) { + child_process.execSync('node ' + startFile + ' add ' + customName + ' --enabled false', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + checkIsAdapterInstalled(function (error) { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + } else { + // add controller + var _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + + waitForEnd(_pid, function () { + checkIsAdapterInstalled(function (error) { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + }); + } +} + +function waitForEnd(_pid, cb) { + if (!_pid) { + cb(-1, -1); + return; + } + _pid.on('exit', function (code, signal) { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); + _pid.on('close', function (code, signal) { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); +} + +function installJsController(cb) { + console.log('installJsController...'); + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') || + !fs.existsSync(rootDir + 'tmp/' + appName + '-data')) { + // try to detect appName.js-controller in node_modules/appName.js-controller + // travis CI installs js-controller into node_modules + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller')) { + console.log('installJsController: no js-controller => copy it from "' + rootDir + 'node_modules/' + appName + '.js-controller"'); + // copy all + // stop controller + console.log('Stop controller if running...'); + var _pid; + if (debug) { + // start controller + _pid = child_process.exec('node ' + appName + '.js stop', { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, function () { + // copy all files into + if (!fs.existsSync(rootDir + 'tmp')) fs.mkdirSync(rootDir + 'tmp'); + if (!fs.existsSync(rootDir + 'tmp/node_modules')) fs.mkdirSync(rootDir + 'tmp/node_modules'); + + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')){ + console.log('Copy js-controller...'); + copyFolderRecursiveSync(rootDir + 'node_modules/' + appName + '.js-controller', rootDir + 'tmp/node_modules/'); + } + + console.log('Setup js-controller...'); + var __pid; + if (debug) { + // start controller + _pid = child_process.exec('node ' + appName + '.js setup first --console', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + __pid = child_process.fork(appName + '.js', ['setup', 'first', '--console'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + waitForEnd(__pid, function () { + checkIsControllerInstalled(function () { + // change ports for object and state DBs + var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + console.log('Setup finished.'); + + copyAdapterToController(); + + installAdapter(function () { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }); + } else { + // check if port 9000 is free, else admin adapter will be added to running instance + var client = new require('net').Socket(); + client.connect(9000, '127.0.0.1', function() { + console.error('Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.'); + process.exit(0); + }); + + setTimeout(function () { + client.destroy(); + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')) { + console.log('installJsController: no js-controller => install from git'); + + child_process.execSync('npm install https://github.com/' + appName + '/' + appName + '.js-controller/tarball/master --prefix ./ --production', { + cwd: rootDir + 'tmp/', + stdio: [0, 1, 2] + }); + } else { + console.log('Setup js-controller...'); + var __pid; + if (debug) { + // start controller + child_process.exec('node ' + appName + '.js setup first', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + child_process.fork(appName + '.js', ['setup', 'first'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + } + + // let npm install admin and run setup + checkIsControllerInstalled(function () { + var _pid; + + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, function () { + // change ports for object and state DBs + var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + + copyAdapterToController(); + + installAdapter(function () { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }, 1000); + } + } else { + setTimeout(function () { + console.log('installJsController: js-controller installed'); + if (cb) cb(false); + }, 0); + } +} + +function copyAdapterToController() { + console.log('Copy adapter...'); + // Copy adapter to tmp/node_modules/appName.adapter + copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']); + console.log('Adapter copied.'); +} + +function clearControllerLog() { + var dirPath = rootDir + 'tmp/log'; + var files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear controller log...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (var i = 0; i < files.length; i++) { + var filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Controller log cleared'); + } catch (err) { + console.error('cannot clear log: ' + err); + } + } +} + +function clearDB() { + var dirPath = rootDir + 'tmp/yunkong2-data/sqlite'; + var files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear sqlite DB...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (var i = 0; i < files.length; i++) { + var filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Clear sqlite DB'); + } catch (err) { + console.error('cannot clear DB: ' + err); + } + } +} + +function setupController(cb) { + installJsController(function (isInited) { + clearControllerLog(); + clearDB(); + + if (!isInited) { + restoreOriginalFiles(); + copyAdapterToController(); + } + // read system.config object + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var objs; + try { + objs = fs.readFileSync(dataDir + 'objects.json'); + objs = JSON.parse(objs); + } + catch (e) { + console.log('ERROR reading/parsing system configuration. Ignore'); + objs = {'system.config': {}}; + } + if (!objs || !objs['system.config']) { + objs = {'system.config': {}}; + } + + if (cb) cb(objs['system.config']); + }); +} + +function startAdapter(objects, states, callback) { + if (adapterStarted) { + console.log('Adapter already started ...'); + if (callback) callback(objects, states); + return; + } + adapterStarted = true; + console.log('startAdapter...'); + if (fs.existsSync(rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main)) { + try { + if (debug) { + // start controller + pid = child_process.exec('node node_modules/' + pkg.name + '/' + pkg.main + ' --console silly', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + } else { + // start controller + pid = child_process.fork('node_modules/' + pkg.name + '/' + pkg.main, ['--console', 'silly'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + } + } catch (error) { + console.error(JSON.stringify(error)); + } + } else { + console.error('Cannot find: ' + rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main); + } + if (callback) callback(objects, states); +} + +function startController(isStartAdapter, onObjectChange, onStateChange, callback) { + if (typeof isStartAdapter === 'function') { + callback = onStateChange; + onStateChange = onObjectChange; + onObjectChange = isStartAdapter; + isStartAdapter = true; + } + + if (onStateChange === undefined) { + callback = onObjectChange; + onObjectChange = undefined; + } + + if (pid) { + console.error('Controller is already started!'); + } else { + console.log('startController...'); + adapterStarted = false; + var isObjectConnected; + var isStatesConnected; + + var Objects = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/objects/objectsInMemServer'); + objects = new Objects({ + connection: { + "type" : "file", + "host" : "127.0.0.1", + "port" : 19001, + "user" : "", + "pass" : "", + "noFileCache": false, + "connectTimeout": 2000 + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.warn(msg); + }, + error: function (msg) { + console.error(msg); + } + }, + connected: function () { + isObjectConnected = true; + if (isStatesConnected) { + console.log('startController: started!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) { + callback(objects, states); + callback = null; + } + } + } + }, + change: onObjectChange + }); + + // Just open in memory DB itself + var States = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/states/statesInMemServer'); + states = new States({ + connection: { + type: 'file', + host: '127.0.0.1', + port: 19000, + options: { + auth_pass: null, + retry_max_delay: 15000 + } + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }, + connected: function () { + isStatesConnected = true; + if (isObjectConnected) { + console.log('startController: started!!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) { + callback(objects, states); + callback = null; + } + } + } + }, + change: onStateChange + }); + } +} + +function stopAdapter(cb) { + if (!pid) { + console.error('Controller is not running!'); + if (cb) { + setTimeout(function () { + cb(false); + }, 0); + } + } else { + adapterStarted = false; + pid.on('exit', function (code, signal) { + if (pid) { + console.log('child process terminated due to receipt of signal ' + signal); + if (cb) cb(); + pid = null; + } + }); + + pid.on('close', function (code, signal) { + if (pid) { + if (cb) cb(); + pid = null; + } + }); + + pid.kill('SIGTERM'); + } +} + +function _stopController() { + if (objects) { + objects.destroy(); + objects = null; + } + if (states) { + states.destroy(); + states = null; + } +} + +function stopController(cb) { + var timeout; + if (objects) { + console.log('Set system.adapter.' + pkg.name + '.0'); + objects.setObject('system.adapter.' + pkg.name + '.0', { + common:{ + enabled: false + } + }); + } + + stopAdapter(function () { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + + _stopController(); + + if (cb) { + cb(true); + cb = null; + } + }); + + timeout = setTimeout(function () { + timeout = null; + console.log('child process NOT terminated'); + + _stopController(); + + if (cb) { + cb(false); + cb = null; + } + pid = null; + }, 5000); +} + +// Setup the adapter +function setAdapterConfig(common, native, instance) { + var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + if (common) objects[id].common = common; + if (native) objects[id].native = native; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/objects.json', JSON.stringify(objects)); +} + +// Read config of the adapter +function getAdapterConfig(instance) { + var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + return objects[id]; +} + +if (typeof module !== undefined && module.parent) { + module.exports.getAdapterConfig = getAdapterConfig; + module.exports.setAdapterConfig = setAdapterConfig; + module.exports.startController = startController; + module.exports.stopController = stopController; + module.exports.setupController = setupController; + module.exports.stopAdapter = stopAdapter; + module.exports.startAdapter = startAdapter; + module.exports.installAdapter = installAdapter; + module.exports.appName = appName; + module.exports.adapterName = adapterName; + module.exports.adapterStarted = adapterStarted; +} diff --git a/test/lib/warnings.json b/test/lib/warnings.json new file mode 100644 index 0000000..eae8f1a --- /dev/null +++ b/test/lib/warnings.json @@ -0,0 +1,1297 @@ +warnWetter.loadWarnings({ + "time" : 1456998601000, + "warnings" : { + "108237000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Freudenstadt", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "107335000" : [{ + "stateShort" : "RP", + "regionName" : "Kreis und Stadt Kaiserslautern", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457010000000, + "start" : 1456993020000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + }, { + "stateShort" : "RP", + "regionName" : "Kreis und Stadt Kaiserslautern", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457002800000, + "start" : 1456986960000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "108235000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Calw", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "108135000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Heidenheim", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen bis 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "107316000" : [{ + "stateShort" : "RP", + "regionName" : "Stadt Neustadt an der Weinstraße", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457010000000, + "start" : 1456993020000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "108436000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Ravensburg", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen bis 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + }, { + "stateShort" : "BW", + "regionName" : "Kreis Ravensburg", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen um 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457046000000, + "start" : 1457002800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "109763000" : [{ + "stateShort" : "BY", + "regionName" : "Stadt Kempten (Allgäu)", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen um 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457046000000, + "start" : 1457002800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Bayern" + } + ], + "107231000" : [{ + "stateShort" : "RP", + "regionName" : "Kreis Bernkastel-Wittlich", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "105354000" : [{ + "stateShort" : "NRW", + "regionName" : "StädteRegion Aachen", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Nordrhein-Westfalen" + } + ], + "105382000" : [{ + "stateShort" : "NRW", + "regionName" : "Rhein-Sieg-Kreis", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457006400000, + "start" : 1456991760000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Nordrhein-Westfalen" + } + ], + "108336000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Lörrach", + "description" : "Es treten oberhalb 1000 m Sturmböen mit Geschwindigkeiten bis 70 km/h (20m/s, 38kn, Bft 8) anfangs aus südwestlicher, später aus westlicher Richtung auf. In exponierten Lagen muss mit Sturmböen bis 80 km/h (22m/s, 44kn, Bft 9) gerechnet werden.", + "end" : 1457017200000, + "start" : 1456995600000, + "headline" : "Amtliche WARNUNG vor STURMBÖEN", + "event" : "STURMBÖEN", + "instruction" : "ACHTUNG! Hinweis auf mögliche Gefahren: Es können zum Beispiel einzelne Äste herabstürzen. Achten Sie besonders auf herabfallende Gegenstände.", + "altitudeStart" : 1000, + "altitudeEnd" : null, + "type" : 1, + "level" : 3, + "state" : "Baden-Württemberg" + }, { + "stateShort" : "BW", + "regionName" : "Kreis Lörrach", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "110046000" : [{ + "stateShort" : "SL", + "regionName" : "Kreis St. Wendel", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Saarland" + } + ], + "108116000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Esslingen", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen bis 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "107138000" : [{ + "stateShort" : "RP", + "regionName" : "Kreis Neuwied", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457006400000, + "start" : 1456991760000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "108327000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Tuttlingen", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen bis 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "108316000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Emmendingen", + "description" : "Es treten oberhalb 1000 m Sturmböen mit Geschwindigkeiten bis 70 km/h (20m/s, 38kn, Bft 8) anfangs aus südwestlicher, später aus westlicher Richtung auf. In exponierten Lagen muss mit Sturmböen bis 80 km/h (22m/s, 44kn, Bft 9) gerechnet werden.", + "end" : 1457017200000, + "start" : 1456995600000, + "headline" : "Amtliche WARNUNG vor STURMBÖEN", + "event" : "STURMBÖEN", + "instruction" : "ACHTUNG! Hinweis auf mögliche Gefahren: Es können zum Beispiel einzelne Äste herabstürzen. Achten Sie besonders auf herabfallende Gegenstände.", + "altitudeStart" : 1000, + "altitudeEnd" : null, + "type" : 1, + "level" : 3, + "state" : "Baden-Württemberg" + }, { + "stateShort" : "BW", + "regionName" : "Kreis Emmendingen", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "107332000" : [{ + "stateShort" : "RP", + "regionName" : "Kreis Bad Dürkheim", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457010000000, + "start" : 1456993020000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "110045000" : [{ + "stateShort" : "SL", + "regionName" : "Saarpfalz-Kreis", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Saarland" + } + ], + "108211000" : [{ + "stateShort" : "BW", + "regionName" : "Stadt Baden-Baden", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "108417000" : [{ + "stateShort" : "BW", + "regionName" : "Zollernalbkreis", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen bis 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "108415000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Reutlingen", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen bis 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "107340000" : [{ + "stateShort" : "RP", + "regionName" : "Kreis Südwestpfalz und Stadt Pirmasens", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457010000000, + "start" : 1456993020000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + }, { + "stateShort" : "RP", + "regionName" : "Kreis Südwestpfalz und Stadt Pirmasens", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457002800000, + "start" : 1456986960000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "108425000" : [{ + "stateShort" : "BW", + "regionName" : "Alb-Donau-Kreis und Stadt Ulm", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen bis 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "108337000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Waldshut", + "description" : "Es treten oberhalb 1000 m Sturmböen mit Geschwindigkeiten bis 70 km/h (20m/s, 38kn, Bft 8) anfangs aus südwestlicher, später aus westlicher Richtung auf. In exponierten Lagen muss mit Sturmböen bis 80 km/h (22m/s, 44kn, Bft 9) gerechnet werden.", + "end" : 1457017200000, + "start" : 1456995600000, + "headline" : "Amtliche WARNUNG vor STURMBÖEN", + "event" : "STURMBÖEN", + "instruction" : "ACHTUNG! Hinweis auf mögliche Gefahren: Es können zum Beispiel einzelne Äste herabstürzen. Achten Sie besonders auf herabfallende Gegenstände.", + "altitudeStart" : 1000, + "altitudeEnd" : null, + "type" : 1, + "level" : 3, + "state" : "Baden-Württemberg" + }, { + "stateShort" : "BW", + "regionName" : "Kreis Waldshut", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "110041000" : [{ + "stateShort" : "SL", + "regionName" : "Regionalverband Saarbrücken", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Saarland" + } + ], + "108216000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Rastatt", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "109187000" : [{ + "stateShort" : "BY", + "regionName" : "Kreis und Stadt Rosenheim", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457002800000, + "start" : 1456978200000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Bayern" + } + ], + "109189000" : [{ + "stateShort" : "BY", + "regionName" : "Kreis Traunstein", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457002800000, + "start" : 1456978200000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Bayern" + } + ], + "107337000" : [{ + "stateShort" : "RP", + "regionName" : "Kreis Südliche Weinstraße", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457010000000, + "start" : 1456993020000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + }, { + "stateShort" : "RP", + "regionName" : "Kreis Südliche Weinstraße", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457002800000, + "start" : 1456986960000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "108325000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Rottweil", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "108435000" : [{ + "stateShort" : "BW", + "regionName" : "Bodenseekreis", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen bis 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "105358000" : [{ + "stateShort" : "NRW", + "regionName" : "Kreis Düren", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Nordrhein-Westfalen" + } + ], + "109777000" : [{ + "stateShort" : "BY", + "regionName" : "Kreis Ostallgäu", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen um 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457046000000, + "start" : 1457002800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Bayern" + } + ], + "107232000" : [{ + "stateShort" : "RP", + "regionName" : "Eifelkreis Bitburg-Prüm", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "107333000" : [{ + "stateShort" : "RP", + "regionName" : "Donnersbergkreis", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457010000000, + "start" : 1456993020000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "110044000" : [{ + "stateShort" : "SL", + "regionName" : "Kreis Saarlouis", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Saarland" + } + ], + "107235000" : [{ + "stateShort" : "RP", + "regionName" : "Kreis Trier-Saarburg und Stadt Trier", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "108236000" : [{ + "stateShort" : "BW", + "regionName" : "Enzkreis und Stadt Pforzheim", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "109776000" : [{ + "stateShort" : "BY", + "regionName" : "Kreis Lindau", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen um 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457046000000, + "start" : 1457002800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Bayern" + } + ], + "108136000" : [{ + "stateShort" : "BW", + "regionName" : "Ostalbkreis", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen bis 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "107320000" : [{ + "stateShort" : "RP", + "regionName" : "Stadt Zweibrücken", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457002800000, + "start" : 1456986960000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "107133000" : [{ + "stateShort" : "RP", + "regionName" : "Kreis Bad Kreuznach", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457010000000, + "start" : 1456993020000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "108317000" : [{ + "stateShort" : "BW", + "regionName" : "Ortenaukreis", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "107140000" : [{ + "stateShort" : "RP", + "regionName" : "Rhein-Hunsrück-Kreis", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457006400000, + "start" : 1456991760000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + }, { + "stateShort" : "RP", + "regionName" : "Rhein-Hunsrück-Kreis", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457010000000, + "start" : 1456993020000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + }, { + "stateShort" : "RP", + "regionName" : "Rhein-Hunsrück-Kreis", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "109780000" : [{ + "stateShort" : "BY", + "regionName" : "Kreis Oberallgäu", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen um 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457046000000, + "start" : 1457002800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Bayern" + } + ], + "110042000" : [{ + "stateShort" : "SL", + "regionName" : "Kreis Merzig-Wadern", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Saarland" + } + ], + "107336000" : [{ + "stateShort" : "RP", + "regionName" : "Kreis Kusel", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457010000000, + "start" : 1456993020000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + }, { + "stateShort" : "RP", + "regionName" : "Kreis Kusel", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457002800000, + "start" : 1456986960000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + }, { + "stateShort" : "RP", + "regionName" : "Kreis Kusel", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "108437000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Sigmaringen", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen bis 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "108416000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Tübingen", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen bis 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "109180000" : [{ + "stateShort" : "BY", + "regionName" : "Kreis Garmisch-Partenkirchen", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen um 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457046000000, + "start" : 1457002800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Bayern" + } + ], + "107134000" : [{ + "stateShort" : "RP", + "regionName" : "Kreis Birkenfeld", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457006400000, + "start" : 1456991760000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + }, { + "stateShort" : "RP", + "regionName" : "Kreis Birkenfeld", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457010000000, + "start" : 1456993020000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + }, { + "stateShort" : "RP", + "regionName" : "Kreis Birkenfeld", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457002800000, + "start" : 1456986960000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + }, { + "stateShort" : "RP", + "regionName" : "Kreis Birkenfeld", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "107313000" : [{ + "stateShort" : "RP", + "regionName" : "Stadt Landau in der Pfalz", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457010000000, + "start" : 1456993020000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "107233000" : [{ + "stateShort" : "RP", + "regionName" : "Kreis Vulkaneifel", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "110043000" : [{ + "stateShort" : "SL", + "regionName" : "Kreis Neunkirchen", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Saarland" + } + ], + "108335000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Konstanz", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen bis 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "108117000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Göppingen", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen bis 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "108426000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Biberach", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen bis 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "109172000" : [{ + "stateShort" : "BY", + "regionName" : "Kreis Berchtesgadener Land", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457002800000, + "start" : 1456978200000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Bayern" + } + ], + "107131000" : [{ + "stateShort" : "RP", + "regionName" : "Kreis Ahrweiler", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457006400000, + "start" : 1456991760000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + }, { + "stateShort" : "RP", + "regionName" : "Kreis Ahrweiler", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "108315000" : [{ + "stateShort" : "BW", + "regionName" : "Kreis Breisgau-Hochschwarzwald und Stadt Freiburg", + "description" : "Es treten oberhalb 1000 m Sturmböen mit Geschwindigkeiten bis 70 km/h (20m/s, 38kn, Bft 8) anfangs aus südwestlicher, später aus westlicher Richtung auf. In exponierten Lagen muss mit Sturmböen bis 80 km/h (22m/s, 44kn, Bft 9) gerechnet werden.", + "end" : 1457017200000, + "start" : 1456995600000, + "headline" : "Amtliche WARNUNG vor STURMBÖEN", + "event" : "STURMBÖEN", + "instruction" : "ACHTUNG! Hinweis auf mögliche Gefahren: Es können zum Beispiel einzelne Äste herabstürzen. Achten Sie besonders auf herabfallende Gegenstände.", + "altitudeStart" : 1000, + "altitudeEnd" : null, + "type" : 1, + "level" : 3, + "state" : "Baden-Württemberg" + }, { + "stateShort" : "BW", + "regionName" : "Kreis Breisgau-Hochschwarzwald und Stadt Freiburg", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "108326000" : [{ + "stateShort" : "BW", + "regionName" : "Schwarzwald-Baar-Kreis", + "description" : "Es treten oberhalb 1000 m Sturmböen mit Geschwindigkeiten bis 70 km/h (20m/s, 38kn, Bft 8) anfangs aus südwestlicher, später aus westlicher Richtung auf. In exponierten Lagen muss mit Sturmböen bis 80 km/h (22m/s, 44kn, Bft 9) gerechnet werden.", + "end" : 1457017200000, + "start" : 1456995600000, + "headline" : "Amtliche WARNUNG vor STURMBÖEN", + "event" : "STURMBÖEN", + "instruction" : "ACHTUNG! Hinweis auf mögliche Gefahren: Es können zum Beispiel einzelne Äste herabstürzen. Achten Sie besonders auf herabfallende Gegenstände.", + "altitudeStart" : 1000, + "altitudeEnd" : null, + "type" : 1, + "level" : 3, + "state" : "Baden-Württemberg" + }, { + "stateShort" : "BW", + "regionName" : "Schwarzwald-Baar-Kreis", + "description" : "Es tritt im Warnzeitraum oberhalb 600 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1457038800000, + "start" : 1456986060000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 600, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Baden-Württemberg" + } + ], + "107135000" : [{ + "stateShort" : "RP", + "regionName" : "Kreis Cochem-Zell", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457006400000, + "start" : 1456991760000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + }, { + "stateShort" : "RP", + "regionName" : "Kreis Cochem-Zell", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "107137000" : [{ + "stateShort" : "RP", + "regionName" : "Kreis Mayen-Koblenz", + "description" : "Es tritt im Warnzeitraum oberhalb 400 m leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. Verbreitet wird es glatt.", + "end" : 1457006400000, + "start" : 1456991760000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : 400, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + }, { + "stateShort" : "RP", + "regionName" : "Kreis Mayen-Koblenz", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Rheinland-Pfalz" + } + ], + "105366000" : [{ + "stateShort" : "NRW", + "regionName" : "Kreis Euskirchen", + "description" : "Es tritt im Warnzeitraum leichter Schneefall mit Mengen zwischen 1 cm und 5 cm auf. In Staulagen werden Mengen bis 10 cm erreicht. Verbreitet wird es glatt.", + "end" : 1456999200000, + "start" : 1456975800000, + "headline" : "Amtliche WARNUNG vor LEICHTEM SCHNEEFALL", + "event" : "LEICHTER SCHNEEFALL", + "instruction" : "", + "altitudeStart" : null, + "altitudeEnd" : null, + "type" : 3, + "level" : 2, + "state" : "Nordrhein-Westfalen" + } + ] + }, + "vorabInformation" : {}, + "copyright" : "Copyright Deutscher Wetterdienst" +}); diff --git a/test/testFunctions.js b/test/testFunctions.js new file mode 100644 index 0000000..ba36641 --- /dev/null +++ b/test/testFunctions.js @@ -0,0 +1,135 @@ +const expect = require('chai').expect; +const setup = require(__dirname + '/lib/setup'); +let name = __dirname.replace(/\\/g, '/').split('/'); +name = name[name.length - 2].split('.')[1]; + +let objects = null; +let states = null; +let onStateChanged = null; +let onObjectChanged = null; + +function checkConnectionOfAdapter(cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check connection'); + return; + } + + states.getState('system.adapter.' + name + '.0.alive', function (err, state) { + if (err) console.error(err); + if (state && state.val) { + cb && cb(); + } else { + setTimeout(function () { + checkConnectionOfAdapter(cb, counter + 1); + }, 1000); + } + }); +} + +function checkValueOfState(id, value, cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check value Of State ' + id); + return; + } + + states.getState(id, function (err, state) { + if (err) console.error(err); + if (value === null && !state) { + cb && cb(); + } else + if (state && (value === undefined || state.val === value)) { + cb && cb(); + } else { + setTimeout(function () { + checkValueOfState(id, value, cb, counter + 1); + }, 500); + } + }); +} + +describe('Test ' + name, function () { + before('Test ' + name + ': Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + setup.setupController(function () { + const config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + config.native.location = __dirname + '/lib/forecast.xml'; + config.native.language = ''; + config.native.sendTranslations = false; + + setup.setAdapterConfig(config.common, config.native); + + setup.startController(true, function (id, obj) { + if (onObjectChanged) onObjectChanged(id, obj); + }, function (id, state) { + console.log(id + ': ' + state.val); + if (onStateChanged) onStateChanged(id, state); + }, + function (_objects, _states) { + objects = _objects; + states = _states; + states.subscribe('*'); + objects.subscribe('*'); + _done(); + }); + }); + }); + + it('Test ' + name + ': Check if adapter started', function (done) { + this.timeout(5000); + checkConnectionOfAdapter(done); + }); + + it('Test ' + name + ': channel must have name of city', function (done) { + setTimeout(function () { + objects.getObject('yr.0.forecast', function (err, obj) { + expect(err).to.be.not.ok; + expect(obj).to.be.ok; + expect(obj.common.name).to.be.equal('yr.no forecast forecast.xml'); + done(); + }); + }, 500); + }); + + it('Test ' + name + ': check states', function (done) { + this.timeout(5000); + + setTimeout(function () { + states.getState('yr.0.forecast.day0.temperatureMin', function (err, state) { + expect(err).to.be.not.ok; + expect(state).to.be.ok; + expect(state.val).to.be.not.undefined; + expect(state.val).to.be.a('number'); + + states.getState('yr.0.forecast.day0.temperatureMax', function (err, state) { + expect(err).to.be.not.ok; + expect(state).to.be.ok; + expect(state.val).to.be.not.undefined; + expect(state.val).to.be.a('number'); + + states.getState('yr.0.forecast.day0.temperatureActual', function (err, state) { + expect(err).to.be.not.ok; + expect(state).to.be.ok; + expect(state.val).to.be.not.undefined; + expect(state.val).to.be.a('number'); + done(); + }); + }); + }); + }, 100); + }); + + after('Test ' + name + ': Stop js-controller', function (done) { + this.timeout(7000); + + setup.stopController(function (normalTerminated) { + console.log('Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +}); \ No newline at end of file diff --git a/test/testPackageFiles.js b/test/testPackageFiles.js new file mode 100644 index 0000000..d0759c0 --- /dev/null +++ b/test/testPackageFiles.js @@ -0,0 +1,91 @@ +/* jshint -W097 */ +/* jshint strict:false */ +/* jslint node: true */ +/* jshint expr: true */ +var expect = require('chai').expect; +var fs = require('fs'); + +describe('Test package.json and io-package.json', function() { + it('Test package files', function (done) { + console.log(); + + var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8'); + var ioPackage = JSON.parse(fileContentIOPackage); + + var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8'); + var npmPackage = JSON.parse(fileContentNPMPackage); + + expect(ioPackage).to.be.an('object'); + expect(npmPackage).to.be.an('object'); + + expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist; + expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist; + + expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version); + + if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) { + console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!'); + console.log(); + } + + expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist; + expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist; + + if (ioPackage.common.name.indexOf('template') !== 0) { + if (Array.isArray(ioPackage.common.authors)) { + expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0); + if (ioPackage.common.authors.length === 1) { + expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + else { + expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + else { + console.log('WARNING: Testing for set authors field in io-package skipped because template adapter'); + console.log(); + } + expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true; + if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') { + console.log('WARNING: titleLang is not existing in io-package.json. Please add'); + console.log(); + } + if ( + ioPackage.common.title.indexOf('yunkong2') !== -1 || + ioPackage.common.title.indexOf('yunkong2') !== -1 || + ioPackage.common.title.indexOf('adapter') !== -1 || + ioPackage.common.title.indexOf('Adapter') !== -1 + ) { + console.log('WARNING: title contains Adapter or yunkong2. It is clear anyway, that it is adapter for yunkong2.'); + console.log(); + } + + if (ioPackage.common.name.indexOf('vis-') !== 0) { + if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) { + console.log('WARNING: Admin3 support is missing! Please add it'); + console.log(); + } + if (ioPackage.common.materialize) { + expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true; + } + } + + var licenseFileExists = fs.existsSync(__dirname + '/../LICENSE'); + var fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8'); + if (fileContentReadme.indexOf('## Changelog') === -1) { + console.log('Warning: The README.md should have a section ## Changelog'); + console.log(); + } + expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true; + if (!licenseFileExists) { + console.log('Warning: The License should also exist as LICENSE file'); + console.log(); + } + if (fileContentReadme.indexOf('## License') === -1) { + console.log('Warning: The README.md should also have a section ## License to be shown in Admin3'); + console.log(); + } + done(); + }); +}); diff --git a/widgets/yr.html b/widgets/yr.html new file mode 100644 index 0000000..1d33991 --- /dev/null +++ b/widgets/yr.html @@ -0,0 +1,33 @@ + + + + \ No newline at end of file diff --git a/widgets/yr/img/Prev_YrAdapter.png b/widgets/yr/img/Prev_YrAdapter.png new file mode 100644 index 0000000000000000000000000000000000000000..881b37b67e8131e2d5de9c4b3f884bcd032e2e5c GIT binary patch literal 5233 zcmV-%6prhOP)004&%004{+008|`004nN004b?008NW002DY000@xb3BE2000U( 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 literal 0 HcmV?d00001