Compare commits
448 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
10dc315f3b | ||
|
0f1457b5d7 | ||
|
090873d4c2 | ||
|
da00c168ae | ||
|
8286d5a06e | ||
|
dc5fb978a7 | ||
|
a4ab0cbe09 | ||
|
4a341b381e | ||
|
3fa98bc1aa | ||
|
87e2e87ce6 | ||
|
c25b6dc16c | ||
|
6a786aa090 | ||
|
a97d87bce8 | ||
|
fba91329f1 | ||
|
353cc3b00f | ||
|
057ef63586 | ||
|
c9fb38981e | ||
|
9ea7de8b44 | ||
|
020c8ccd2a | ||
|
4d09abe725 | ||
|
5079e30caf | ||
|
3e5c1a278b | ||
|
4ded60874f | ||
|
ff1a843de6 | ||
|
e6cefcf948 | ||
|
a857412f13 | ||
|
e507f95b2a | ||
|
61cf92c67a | ||
|
7a4eddc592 | ||
|
3fcbf15915 | ||
|
efafb1c28a | ||
|
67bedf8648 | ||
|
c1d35b0f91 | ||
|
d3a715bd6b | ||
|
28b52cd24f | ||
|
46e77f805c | ||
|
dd23db0ad8 | ||
|
30cf7f8afe | ||
|
7802e0bb88 | ||
|
07e75b8550 | ||
|
1e9fad8278 | ||
|
0975826457 | ||
|
e5ff320591 | ||
|
02c1e47749 | ||
|
a50824eeee | ||
|
0c3f9f4ed9 | ||
|
6b12601c6f | ||
|
225bece44e | ||
|
a5a5e73196 | ||
|
f8085ed78f | ||
|
4e6e84c637 | ||
|
846212798c | ||
|
571f95eb2f | ||
|
202eeea33f | ||
|
af212057db | ||
|
0fd0fea7e7 | ||
|
eb6ef3c8ff | ||
|
cdb8d35cf6 | ||
|
684dcdcef3 | ||
|
155351f5dd | ||
|
0419e06e7c | ||
|
2ea38bd9a4 | ||
|
30db9c30c8 | ||
|
a8ef594dab | ||
|
9858d5b495 | ||
|
20bd85b676 | ||
|
83ec8ca24f | ||
|
9622d02230 | ||
|
b72d0ed37e | ||
|
f966e504a5 | ||
|
a0366e794b | ||
|
3c3ce24397 | ||
|
30290f3eb2 | ||
|
616431e04b | ||
|
d0aeb90f0a | ||
|
86220fa721 | ||
|
0f58a56a07 | ||
|
29451562e3 | ||
|
911687af2a | ||
|
1c9c33b87b | ||
|
f485462af1 | ||
|
39d7ceb017 | ||
|
8251792a0d | ||
|
54ac450f92 | ||
|
299e4a497f | ||
|
3f851c1fd6 | ||
|
8cf16f1049 | ||
|
b373aa6250 | ||
|
2f4b8cd642 | ||
|
85b6df3738 | ||
|
c675421a6a | ||
|
a7b571e5d0 | ||
|
b3a9b7ef0e | ||
|
e984893853 | ||
|
3024319027 | ||
|
21ba652413 | ||
|
e4b8cd92f2 | ||
|
1a4a9f6501 | ||
|
e950cdaf32 | ||
|
f97be2f8f3 | ||
|
be0c8f4f16 | ||
|
46fd2de315 | ||
|
72f0d77865 | ||
|
d43679d59e | ||
|
ce46fb5384 | ||
|
43b33cb6de | ||
|
0344399253 | ||
|
3a9b154cb2 | ||
|
1074fbacfe | ||
|
00ff3ab380 | ||
|
db874a011c | ||
|
35a2839a2f | ||
|
cdb9b9bb87 | ||
|
87a3e4d440 | ||
|
284bed677e | ||
|
667be460e5 | ||
|
c49386bb58 | ||
|
adb50a9623 | ||
|
ac1d2372f4 | ||
|
f54690c829 | ||
|
053f9c34aa | ||
|
e7dd2b4aee | ||
|
6f82f9e01b | ||
|
d531730dc1 | ||
|
00bdf6aaa6 | ||
|
97f3514677 | ||
|
137facf95a | ||
|
8afba3a5c4 | ||
|
9c5383dc37 | ||
|
260bc9664e | ||
|
655ca83356 | ||
|
472bf1665c | ||
|
db129cc19b | ||
|
b735f8a524 | ||
|
1e34764588 | ||
|
99aaae491c | ||
|
f288581c69 | ||
|
3c5d50bce9 | ||
|
a01f08391b | ||
|
51a1399bca | ||
|
b735cb96a0 | ||
|
d00c25e107 | ||
|
8a5e87b116 | ||
|
2779d19d5c | ||
|
38d4a8b198 | ||
|
ccf98c0c22 | ||
|
bd0d91d1b4 | ||
|
056f3a6ccb | ||
|
20a50f8382 | ||
|
21284e7795 | ||
|
e0ceed5a63 | ||
|
958a2ee6ec | ||
|
ea264cb15e | ||
|
d8f19e631c | ||
|
ce5c0ed5ba | ||
|
839ca9ecfb | ||
|
720bc12c00 | ||
|
1ba845fb06 | ||
|
e86fa9d24a | ||
|
e348a61085 | ||
|
2f70366299 | ||
|
c466b20558 | ||
|
021f8d25a5 | ||
|
a19c3a43d8 | ||
|
819923e66d | ||
|
6c3100e250 | ||
|
1065eda47f | ||
|
afd676a958 | ||
|
12405b66d0 | ||
|
ecd0b6fa83 | ||
|
3a8587378c | ||
|
a05c08ed48 | ||
|
469a90787b | ||
|
9e5a9b5ced | ||
|
f311ba3f7c | ||
|
fc68321bd5 | ||
|
8a4173d0ad | ||
|
90af31cb2f | ||
|
61bcd9337b | ||
|
7944045893 | ||
|
2c3f83c70d | ||
|
92ab705ff5 | ||
|
2951f0c40c | ||
|
8a23bccb70 | ||
|
aa6ad01fb9 | ||
|
ef325896c5 | ||
|
a270c73d7c | ||
|
16feda895d | ||
|
874a50d142 | ||
|
ae32645470 | ||
|
d4412fe06f | ||
|
b42c05fb56 | ||
|
8d0da61bd4 | ||
|
8b8be261cd | ||
|
2b6ceed897 | ||
|
8abca801a2 | ||
|
1eae4425d8 | ||
|
a6386bd60e | ||
|
1460f002ab | ||
|
0d6f736c2c | ||
|
da88e11d04 | ||
|
0a58767563 | ||
|
198525f2ce | ||
|
3220702034 | ||
|
262711a127 | ||
|
078438442a | ||
|
5ac20d65ac | ||
|
5d2f706c32 | ||
|
844a59d880 | ||
|
82c742f964 | ||
|
b73cfd8a60 | ||
|
8466ff0c1a | ||
|
1e0fc7eb0d | ||
|
74b0c3ea57 | ||
|
a735275864 | ||
|
8a1d46deb4 | ||
|
b624aeec45 | ||
|
e83b4d7d42 | ||
|
da302fce32 | ||
|
e09fec56e2 | ||
|
568f952573 | ||
|
fc1e488391 | ||
|
5781c258e0 | ||
|
13c542aeda | ||
|
02148f68e5 | ||
|
3c84002abd | ||
|
01aa57e493 | ||
|
af09b4214a | ||
|
43b6c71205 | ||
|
480e1adfbf | ||
|
8c04712784 | ||
|
0c61ba8f2d | ||
|
485f662d75 | ||
|
26caeec0c1 | ||
|
4a7cb88a3e | ||
|
d11696015d | ||
|
3b76ca4f9b | ||
|
6f3239d514 | ||
|
e8f60d39de | ||
|
a3bad8aec4 | ||
|
644aa26b75 | ||
|
ecd9828afc | ||
|
7462d61e16 | ||
|
b95ef7250d | ||
|
569dec1b0b | ||
|
1bc0270d7b | ||
|
ebfeebc40b | ||
|
ec80b25087 | ||
|
7a2278d7b6 | ||
|
28b93b70fa | ||
|
0431f45190 | ||
|
96ce444061 | ||
|
32776e7ca3 | ||
|
35d516ad4d | ||
|
e6a17d27b9 | ||
|
52edec1b64 | ||
|
6440d2289c | ||
|
11afadcea8 | ||
|
2e981987f2 | ||
|
69efca0bdb | ||
|
eefb92367e | ||
|
8fa96c2836 | ||
|
04dae52a6c | ||
|
37c9da1351 | ||
|
86d7ec6270 | ||
|
82f1e0fe32 | ||
|
6ec0aa8894 | ||
|
3dbe8bfbbf | ||
|
20d82bab78 | ||
|
60f6123b64 | ||
|
74887a58f6 | ||
|
76821d16fc | ||
|
233a6b5e90 | ||
|
a1d9337c81 | ||
|
e3fec6c3e4 | ||
|
5de88b4cc1 | ||
|
af5e6655e0 | ||
|
a8716799c9 | ||
|
1672027091 | ||
|
934ac886cb | ||
|
1b47c4d16f | ||
|
e8fd906aa1 | ||
|
57b6ea1297 | ||
|
102ff15a99 | ||
|
19148cfc84 | ||
|
97c2bab58a | ||
|
1f473228ce | ||
|
340a8e4176 | ||
|
67201a52cc | ||
|
30e164fe0a | ||
|
cfcb20a468 | ||
|
24eac7ef41 | ||
|
94162f1b45 | ||
|
01c19efc0c | ||
|
e4f2a8a23b | ||
|
89b33d2c62 | ||
|
3ced35a2f8 | ||
|
fd4576b234 | ||
|
812ad829c9 | ||
|
207a9ba723 | ||
|
f9bf17d701 | ||
|
43b96ccd1e | ||
|
14e8cdd9b2 | ||
|
259068b860 | ||
|
a1a4192835 | ||
|
45f09dcc8c | ||
|
15c3f11f4a | ||
|
1a21027850 | ||
|
8427a9fc68 | ||
|
f09b89f975 | ||
|
7bec84f767 | ||
|
8f4cbcf817 | ||
|
c3382274a2 | ||
|
493055f861 | ||
|
11fbbd49f3 | ||
|
8ce37d53cd | ||
|
baa4012872 | ||
|
16c5eba2be | ||
|
ec08cb32aa | ||
|
86fb1b938b | ||
|
5aa7097a6e | ||
|
fd3f520e95 | ||
|
49ff92892f | ||
|
904f5a2656 | ||
|
bb105dd832 | ||
|
fd934c0514 | ||
|
66609428a2 | ||
|
0056e0bc6d | ||
|
056b66a764 | ||
|
3438a5a374 | ||
|
9f3806dabf | ||
|
1d4d5cc4e7 | ||
|
3901543697 | ||
|
a4d73e2a67 | ||
|
f6854f58ff | ||
|
ad7f7d2890 | ||
|
498db63cac | ||
|
05659820d0 | ||
|
02779ef725 | ||
|
935c9b6a42 | ||
|
522f7644a3 | ||
|
b1a67d1fc5 | ||
|
0674c0aff6 | ||
|
c8664d5952 | ||
|
8ce1e2c956 | ||
|
90cca28e01 | ||
|
e489b7101c | ||
|
eee64c8064 | ||
|
a0d4e8dafc | ||
|
e6b94df06d | ||
|
6f1c7d6253 | ||
|
2d5a19b676 | ||
|
d1e8dded68 | ||
|
9888f66c84 | ||
|
5ec51d0ccc | ||
|
79f9331073 | ||
|
43bcf4ab98 | ||
|
f4eae72c48 | ||
|
9d14d3e5b7 | ||
|
26d3141489 | ||
|
5c44f51d6d | ||
|
50f3f32ba8 | ||
|
8fa858ca8c | ||
|
8b1d1671f7 | ||
|
73aa35ea2c | ||
|
a391445e5f | ||
|
530c5d416a | ||
|
319a13c0ef | ||
|
ec2fedd797 | ||
|
7489d19784 | ||
|
3b5a0e8d66 | ||
|
29ed63286c | ||
|
108da4b6d4 | ||
|
a00a1c64d1 | ||
|
275825cfe1 | ||
|
cd2fce56a6 | ||
|
cab850fb35 | ||
|
045dc600b0 | ||
|
1b199c1682 | ||
|
936fa637ec | ||
|
b9ccb7a892 | ||
|
95b33be3cf | ||
|
cde27b89c2 | ||
|
476e52e004 | ||
|
8dc88fe64b | ||
|
d00da790af | ||
|
c61fac75e4 | ||
|
e949af6fd5 | ||
|
c1e38b917e | ||
|
1244121216 | ||
|
46543daa13 | ||
|
6277384bf9 | ||
|
17549fed9c | ||
|
377e42affc | ||
|
0ac5d56865 | ||
|
b59ee6ad7e | ||
|
2d7c5a827f | ||
|
85c32ef843 | ||
|
dc089d7db9 | ||
|
d570f910f8 | ||
|
8f2731911b | ||
|
9d22420027 | ||
|
029da3791b | ||
|
caa51b1029 | ||
|
2973a03d40 | ||
|
679d464f4c | ||
|
93c0787efd | ||
|
a1708a1469 | ||
|
862bf78e63 | ||
|
8c01df50a2 | ||
|
921932920d | ||
|
d021c9b4ae | ||
|
309a18e9c0 | ||
|
73322c96a6 | ||
|
121e7db330 | ||
|
9f5e1b59fb | ||
|
3282ed4fea | ||
|
c7d9192f18 | ||
|
bd0f707aed | ||
|
8b30634ebe | ||
|
ca8acb14f1 | ||
|
26b8f8d0d5 | ||
|
a066153556 | ||
|
2ee7131c28 | ||
|
cb1f65732e | ||
|
37237d9c10 | ||
|
92cc41dec6 | ||
|
246dc663c4 | ||
|
4ed3235590 | ||
|
0939e405b4 | ||
|
584c51ef56 | ||
|
26ed05ba3a | ||
|
5bb72cfed8 | ||
|
85ed1b85ae | ||
|
1d2f929d3f | ||
|
ffbf0804d9 | ||
|
ca0b89ecd3 | ||
|
dc9d6f6b79 | ||
|
f73520559e | ||
|
a4df38d963 | ||
|
4a162543f6 | ||
|
d5caadd906 | ||
|
9d2d170c2d | ||
|
df3aa22c59 | ||
|
a4aabfcdae | ||
|
ce99e70bf9 | ||
|
9d249406e3 | ||
|
42d9e7a090 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
|
||||
"plugins": ["prettier"],
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"],
|
||||
"plugins": ["prettier", "jsdoc"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
|
15
.github/workflows/enforce-changelog.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: "Enforce Changelog"
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||
|
||||
jobs:
|
||||
# Enforces the update of a changelog file on every pull request
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: dangoslen/changelog-enforcer@v1.6.1
|
||||
with:
|
||||
changeLogPath: 'CHANGELOG.md'
|
||||
skipLabels: 'Skip Changelog'
|
35
.github/workflows/node-ci.js.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Automated Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, develop ]
|
||||
pull_request:
|
||||
branches: [ master, develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
npm install
|
||||
npm run test:prettier
|
||||
npm run test:js
|
||||
npm run test:css
|
||||
npm run test:e2e
|
||||
npm run test:unit
|
10
.gitignore
vendored
@@ -1,5 +1,4 @@
|
||||
# Various Node ignoramuses.
|
||||
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
@@ -13,9 +12,11 @@ build/Release
|
||||
/node_modules/**/*
|
||||
fonts/node_modules/**/*
|
||||
vendor/node_modules/**/*
|
||||
!/tests/node_modules/**/*
|
||||
jspm_modules
|
||||
.npm
|
||||
.node_repl_history
|
||||
.nyc_output/
|
||||
|
||||
# Visual Studio Code ignoramuses.
|
||||
.vscode/
|
||||
@@ -53,7 +54,6 @@ Temporary Items
|
||||
.apdisk
|
||||
|
||||
# Various Linux ignoramuses.
|
||||
|
||||
.fuse_hidden*
|
||||
.directory
|
||||
.Trash-*
|
||||
@@ -67,6 +67,10 @@ Temporary Items
|
||||
# Ignore changes to the custom css files.
|
||||
/css/custom.css
|
||||
|
||||
# Ignore users config file but keep the sample.
|
||||
/config/*
|
||||
!/config/config.js.sample
|
||||
|
||||
# Vim
|
||||
## swap
|
||||
[._]*.s[a-w][a-z]
|
||||
@@ -76,5 +80,3 @@ Temporary Items
|
||||
*.orig
|
||||
*.rej
|
||||
*.bak
|
||||
|
||||
!/tests/node_modules/**/*
|
||||
|
@@ -1,5 +1,5 @@
|
||||
package-lock.json
|
||||
/config/**/*
|
||||
/modules/default/calendar/vendor/ical.js/**/*
|
||||
/vendor/**/*
|
||||
!/vendor/vendor.js
|
||||
.github/**/*
|
||||
|
25
.travis.yml
@@ -1,25 +0,0 @@
|
||||
dist: trusty
|
||||
language: node_js
|
||||
node_js:
|
||||
- 10
|
||||
- lts/*
|
||||
- node
|
||||
before_install:
|
||||
- npm i -g npm
|
||||
before_script:
|
||||
- yarn danger ci
|
||||
- "export DISPLAY=:99.0"
|
||||
- "export ELECTRON_DISABLE_SANDBOX=1"
|
||||
- "sh -e /etc/init.d/xvfb start"
|
||||
- sleep 5
|
||||
script:
|
||||
- npm run test:prettier
|
||||
- npm run test:js
|
||||
- npm run test:css
|
||||
- npm run test:e2e
|
||||
- npm run test:unit
|
||||
after_script:
|
||||
- npm list
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
105
CHANGELOG.md
@@ -5,6 +5,109 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror²
|
||||
|
||||
## [2.14.0] - 2021-01-01
|
||||
|
||||
Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank, @bluemanos, @flopp999, @jakemulley, @jakobsarwary1, @marvai-vgtu, @mirontoli, @rejas, @sdetweil, @Snille & @Sub028.
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
|
||||
|
||||
### Added
|
||||
|
||||
- Added new log level "debug" to the logger.
|
||||
- Added new parameter "useKmh" to weather module for displaying wind speed as kmh.
|
||||
- Chuvash translation.
|
||||
- Added Weatherbit as a provider to Weather module.
|
||||
- Added SMHI as a provider to Weather module.
|
||||
- Added Hindi & Gujarati translation.
|
||||
- Added optional support for DEGREE position in Feels like translation.
|
||||
- Added support for variables in nunjucks templates for translate filter.
|
||||
- Added Chuvash translation.
|
||||
- Calendar: new options "limitDays" and "coloredEvents".
|
||||
- Added new option "limitDays" - limit the number of discreet days displayed.
|
||||
- Added new option "customEvents" - use custom symbol/color based on keyword in event title.
|
||||
- Added GitHub workflows for automated testing and changelog enforcement.
|
||||
|
||||
### Updated
|
||||
|
||||
- Merging .gitignore in the config-folder with the .gitignore in the root-folder.
|
||||
- Weather module - forecast now show TODAY and TOMORROW instead of weekday, to make it easier to understand.
|
||||
- Update dependencies to latest versions.
|
||||
- Update dependencies eslint, feedme, simple-git and socket.io to latest versions.
|
||||
- Update lithuanian translation.
|
||||
- Update config sample.
|
||||
- Highlight required version mismatch.
|
||||
- No select Text for TouchScreen use.
|
||||
- Corrected logic for timeFormat "relative" and "absolute".
|
||||
- Added missing function call in module.show()
|
||||
- Translator variables can have falsy values (e.g. empty string)
|
||||
- Fix issue with weather module with DEGREE label in FEELS like
|
||||
|
||||
### Deleted
|
||||
|
||||
- Removed Travis CI intergration.
|
||||
|
||||
### Fixed
|
||||
|
||||
- JSON Parse translation files with comments crashing UI. (#2149)
|
||||
- Calendar parsing where RRULE bug returns wrong date, add Windows timezone name support. (#2145, #2151)
|
||||
- Wrong node-ical version installed (package.json) requested version. (#2153)
|
||||
- Fix calendar fetcher subsequent timing. (#2160)
|
||||
- Rename Greek translation to correct ISO 639-1 alpha-2 code (gr > el). (#2155)
|
||||
- Add a space after icons of sunrise and sunset. (#2169)
|
||||
- Fix calendar when no DTEND record found in event, startDate overlay when endDate set. (#2177)
|
||||
- Fix windspeed convertion error in ukmetoffice weather provider. (#2189)
|
||||
- Fix console.debug not having timestamps. (#2199)
|
||||
- Fix calendar full day event east of UTC start time. (#2200)
|
||||
- Fix non-fullday recurring rule processing. (#2216)
|
||||
- Catch errors when parsing calendar data with ical. (#2022)
|
||||
- Fix Default Alert Module does not hide black overlay when alert is dismissed manually. (#2228)
|
||||
- Weather module - Always displays night icons when local is other then English. (#2221)
|
||||
- Update Node-ical 0.12.4 , fix invalid RRULE format in cal entries
|
||||
- Fix package.json for optional electron dependency (2378)
|
||||
- Update node-ical version again, 0.12.5, change RRULE fix (#2371, #2379)
|
||||
- Remove undefined objects from modules array (#2382)
|
||||
- Update node-ical version again, 0.12.7, change RRULE fix (#2371, #2379), node-ical now throws error (which we catch)
|
||||
- Update simple-git version to 2.31 unhandled promise rejection (#2383)
|
||||
|
||||
## [2.13.0] - 2020-10-01
|
||||
|
||||
Special thanks to the following contributors: @bryanzzhu, @bugsounet, @chamakura, @cjbrunner, @easyas314, @larryare, @oemel09, @rejas, @sdetweil & @sthuber90.
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
|
||||
|
||||
### Added
|
||||
|
||||
- `--dry-run` option adde in fetch call within updatenotification node_helper. This is to prevent
|
||||
MagicMirror from consuming any fetch result. Causes conflict with MMPM when attempting to check
|
||||
for updates to MagicMirror and/or MagicMirror modules.
|
||||
- Test coverage with Istanbul, run it with `npm run test:coverage`.
|
||||
- Add lithuanian language.
|
||||
- Added support in weatherforecast for OpenWeather onecall API.
|
||||
- Added config option to calendar-icons for recurring- and fullday-events.
|
||||
- Added current, hourly (max 48), and daily (max 7) weather forecasts to weather module via OpenWeatherMap One Call API.
|
||||
- Added eslint-plugin for jsdoc comments.
|
||||
- Added new configDeepMerge option for module developers.
|
||||
|
||||
### Updated
|
||||
|
||||
- Change incorrect weather.js default properties.
|
||||
- Cleaned up newsfeed module.
|
||||
- Cleaned up jsdoc comments.
|
||||
- Cleaned up clock tests.
|
||||
- Move lodash into devDependencies, update other dependencies.
|
||||
- Switch from ical to node-ical library.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix backward compatibility issues for Safari < 11.
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast depending on the endpoint (forecast/daily or forecast)". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
|
||||
- Fix calendar display. Account for current timezone. [#2068](https://github.com/MichMich/MagicMirror/issues/2068)
|
||||
- Fix logLevel being set before loading config.
|
||||
- Fix incorrect namespace links in svg clockfaces. [#2072](https://github.com/MichMich/MagicMirror/issues/2072)
|
||||
- Fix weather/providers/weathergov for API guidelines. [#2045](https://github.com/MichMich/MagicMirror/issues/2045)
|
||||
- Fix "undefined" in weather modules header. [#1985](https://github.com/MichMich/MagicMirror/issues/1985)
|
||||
- Fix #2110, #2111, #2118: Recurring full day events should not use timezone adjustment. Just compare month/day.
|
||||
|
||||
## [2.12.0] - 2020-07-01
|
||||
|
||||
Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryanzzhu, @chamakura, @DarthBrento, @Ekristoffe, @khassel, @Legion2, @ndom91, @radokristof, @rejas, @XBCreepinJesus & @ZoneMR.
|
||||
@@ -43,6 +146,7 @@ Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryan
|
||||
- Throw error when check_config fails. [#1928](https://github.com/MichMich/MagicMirror/issues/1928)
|
||||
- Bug fix related to 'maxEntries' not displaying Calendar events. [#2050](https://github.com/MichMich/MagicMirror/issues/2050)
|
||||
- Updated ical library to latest version. [#1926](https://github.com/MichMich/MagicMirror/issues/1926)
|
||||
- Fix config check after merge of prettier [#2109](https://github.com/MichMich/MagicMirror/issues/2109)
|
||||
|
||||
## [2.11.0] - 2020-04-01
|
||||
|
||||
@@ -95,6 +199,7 @@ For more information regarding this major change, please check issue [#1860](htt
|
||||
- Timestamp in log output now also contains the date
|
||||
- Turkish translation.
|
||||
- Option to configure the size of the currentweather module.
|
||||
- Changed "Gevoelstemperatuur" to "Voelt als" shorter text.
|
||||
|
||||
## [2.10.1] - 2020-01-10
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# The MIT License (MIT)
|
||||
|
||||
Copyright © 2016-2019 Michael Teeuw
|
||||
Copyright © 2016-2020 Michael Teeuw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
|
@@ -5,8 +5,7 @@
|
||||
<a href="https://david-dm.org/MichMich/MagicMirror#info=devDependencies"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a>
|
||||
<a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge"></a>
|
||||
<a href="https://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
||||
<a href="https://travis-ci.com/MichMich/MagicMirror"><img src="https://travis-ci.com/MichMich/MagicMirror.svg" alt="Travis"></a>
|
||||
<a href="https://snyk.io/test/github/MichMich/MagicMirror"><img src="https://snyk.io/test/github/MichMich/MagicMirror/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/MichMich/MagicMirror" style="max-width:100%;"></a>
|
||||
<a href="https://github.com/MichMich/MagicMirror/actions?query=workflow%3A%22Automated+Tests%22"><img src="https://github.com/MichMich/MagicMirror/workflows/Automated%20Tests/badge.svg" alt="Tests"></a>
|
||||
</p>
|
||||
|
||||
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).
|
||||
@@ -40,6 +39,6 @@ If we receive enough donations we might even be able to free up some working hou
|
||||
To donate, please follow [this](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G5D8E9MR5DTD2&source=url) link.
|
||||
|
||||
<p align="center">
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
|
||||
</p>
|
||||
|
@@ -4,10 +4,19 @@
|
||||
(function () {
|
||||
var config = {};
|
||||
|
||||
// Helper function to get server address/hostname from either the commandline or env
|
||||
/**
|
||||
* Helper function to get server address/hostname from either the commandline or env
|
||||
*/
|
||||
function getServerAddress() {
|
||||
// Helper function to get command line parameters
|
||||
// Assumes that a cmdline parameter is defined with `--key [value]`
|
||||
/**
|
||||
* Get command line parameters
|
||||
* Assumes that a cmdline parameter is defined with `--key [value]`
|
||||
*
|
||||
* @param {string} key key to look for at the command line
|
||||
* @param {string} defaultValue value if no key is given at the command line
|
||||
*
|
||||
* @returns {string} the value of the parameter
|
||||
*/
|
||||
function getCommandLineParameter(key, defaultValue = undefined) {
|
||||
var index = process.argv.indexOf(`--${key}`);
|
||||
var value = index > -1 ? process.argv[index + 1] : undefined;
|
||||
@@ -23,10 +32,17 @@
|
||||
config["tls"] = process.argv.indexOf("--use-tls") > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the config from the specified server url
|
||||
*
|
||||
* @param {string} url location where the server is running.
|
||||
*
|
||||
* @returns {Promise} the config
|
||||
*/
|
||||
function getServerConfig(url) {
|
||||
// Return new pending promise
|
||||
return new Promise((resolve, reject) => {
|
||||
// Select http or https module, depending on reqested url
|
||||
// Select http or https module, depending on requested url
|
||||
const lib = url.startsWith("https") ? require("https") : require("http");
|
||||
const request = lib.get(url, (response) => {
|
||||
var configData = "";
|
||||
@@ -47,6 +63,12 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a message to the console in case of errors
|
||||
*
|
||||
* @param {string} [message] error message to print
|
||||
* @param {number} code error code for the exit call
|
||||
*/
|
||||
function fail(message, code = 1) {
|
||||
if (message !== undefined && typeof message === "string") {
|
||||
console.log(message);
|
||||
|
2
config/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
*
|
||||
!config.js.sample
|
@@ -28,7 +28,7 @@ var config = {
|
||||
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
|
||||
|
||||
language: "en",
|
||||
logLevel: ["INFO", "LOG", "WARN", "ERROR"],
|
||||
logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
// serverOnly: true/false/"local" ,
|
||||
@@ -70,7 +70,7 @@ var config = {
|
||||
position: "top_right",
|
||||
config: {
|
||||
location: "New York",
|
||||
locationID: "", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
||||
locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
||||
appid: "YOUR_OPENWEATHER_API_KEY"
|
||||
}
|
||||
},
|
||||
|
@@ -2,6 +2,7 @@ html {
|
||||
cursor: none;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
|
65
js/app.js
@@ -40,16 +40,19 @@ process.on("uncaughtException", function (err) {
|
||||
Log.error("If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues");
|
||||
});
|
||||
|
||||
/* App - The core app.
|
||||
/**
|
||||
* The core app.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
var App = function () {
|
||||
var nodeHelpers = [];
|
||||
|
||||
/* loadConfig(callback)
|
||||
* Loads the config file. combines it with the defaults,
|
||||
* and runs the callback with the found config as argument.
|
||||
/**
|
||||
* Loads the config file. Combines it with the defaults, and runs the
|
||||
* callback with the found config as argument.
|
||||
*
|
||||
* argument callback function - The callback function.
|
||||
* @param {Function} callback Function to be called after loading the config
|
||||
*/
|
||||
var loadConfig = function (callback) {
|
||||
Log.log("Loading config ...");
|
||||
@@ -80,6 +83,12 @@ var App = function () {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks the config for deprecated options and throws a warning in the logs
|
||||
* if it encounters one option from the deprecated.js list
|
||||
*
|
||||
* @param {object} userConfig The user config
|
||||
*/
|
||||
var checkDeprecatedOptions = function (userConfig) {
|
||||
var deprecated = require(global.root_path + "/js/deprecated.js");
|
||||
var deprecatedOptions = deprecated.configs;
|
||||
@@ -96,10 +105,11 @@ var App = function () {
|
||||
}
|
||||
};
|
||||
|
||||
/* loadModule(module)
|
||||
/**
|
||||
* Loads a specific module.
|
||||
*
|
||||
* argument module string - The name of the module (including subpath).
|
||||
* @param {string} module The name of the module (including subpath).
|
||||
* @param {Function} callback Function to be called after loading
|
||||
*/
|
||||
var loadModule = function (module, callback) {
|
||||
var elements = module.split("/");
|
||||
@@ -144,10 +154,11 @@ var App = function () {
|
||||
}
|
||||
};
|
||||
|
||||
/* loadModules(modules)
|
||||
/**
|
||||
* Loads all modules.
|
||||
*
|
||||
* argument module string - The name of the module (including subpath).
|
||||
* @param {Module[]} modules All modules to be loaded
|
||||
* @param {Function} callback Function to be called after loading
|
||||
*/
|
||||
var loadModules = function (modules, callback) {
|
||||
Log.log("Loading module helpers ...");
|
||||
@@ -169,11 +180,14 @@ var App = function () {
|
||||
loadNextModule();
|
||||
};
|
||||
|
||||
/* cmpVersions(a,b)
|
||||
/**
|
||||
* Compare two semantic version numbers and return the difference.
|
||||
*
|
||||
* argument a string - Version number a.
|
||||
* argument a string - Version number b.
|
||||
* @param {string} a Version number a.
|
||||
* @param {string} b Version number b.
|
||||
*
|
||||
* @returns {number} A positive number if a is larger than b, a negative
|
||||
* number if a is smaller and 0 if they are the same
|
||||
*/
|
||||
function cmpVersions(a, b) {
|
||||
var i, diff;
|
||||
@@ -191,17 +205,20 @@ var App = function () {
|
||||
return segmentsA.length - segmentsB.length;
|
||||
}
|
||||
|
||||
/* start(callback)
|
||||
* This methods starts the core app.
|
||||
* It loads the config, then it loads all modules.
|
||||
* When it's done it executes the callback with the config as argument.
|
||||
/**
|
||||
* Start the core app.
|
||||
*
|
||||
* argument callback function - The callback function.
|
||||
* It loads the config, then it loads all modules. When it's done it
|
||||
* executes the callback with the config as argument.
|
||||
*
|
||||
* @param {Function} callback Function to be called after start
|
||||
*/
|
||||
this.start = function (callback) {
|
||||
loadConfig(function (c) {
|
||||
config = c;
|
||||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
var modules = [];
|
||||
|
||||
for (var m in config.modules) {
|
||||
@@ -232,9 +249,10 @@ var App = function () {
|
||||
});
|
||||
};
|
||||
|
||||
/* stop()
|
||||
* This methods stops the core app.
|
||||
* This calls each node_helper's STOP() function, if it exists.
|
||||
/**
|
||||
* Stops the core app. This calls each node_helper's STOP() function, if it
|
||||
* exists.
|
||||
*
|
||||
* Added to fix #1056
|
||||
*/
|
||||
this.stop = function () {
|
||||
@@ -246,7 +264,8 @@ var App = function () {
|
||||
}
|
||||
};
|
||||
|
||||
/* Listen for SIGINT signal and call stop() function.
|
||||
/**
|
||||
* Listen for SIGINT signal and call stop() function.
|
||||
*
|
||||
* Added to fix #1056
|
||||
* Note: this is only used if running `server-only`. Otherwise
|
||||
@@ -261,7 +280,9 @@ var App = function () {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
/* We also need to listen to SIGTERM signals so we stop everything when we are asked to stop by the OS.
|
||||
/**
|
||||
* Listen to SIGTERM signals so we can stop everything when we
|
||||
* are asked to stop by the OS.
|
||||
*/
|
||||
process.on("SIGTERM", () => {
|
||||
Log.log("[SIGTERM] Received. Shutting down server...");
|
||||
|
@@ -12,13 +12,14 @@ const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const rootPath = path.resolve(__dirname + "/../");
|
||||
const config = require(rootPath + "/.eslintrc.json");
|
||||
const Log = require(rootPath + "/js/logger.js");
|
||||
const Utils = require(rootPath + "/js/utils.js");
|
||||
|
||||
/* getConfigFile()
|
||||
* Return string with path of configuration file
|
||||
/**
|
||||
* Returns a string with path of configuration file.
|
||||
* Check if set by environment variable MM_CONFIG_FILE
|
||||
*
|
||||
* @returns {string} path and filename of the config file
|
||||
*/
|
||||
function getConfigFile() {
|
||||
// FIXME: This function should be in core. Do you want refactor me ;) ?, be good!
|
||||
@@ -29,6 +30,9 @@ function getConfigFile() {
|
||||
return configFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the config file using eslint.
|
||||
*/
|
||||
function checkConfigFile() {
|
||||
const configFileName = getConfigFile();
|
||||
|
||||
@@ -38,11 +42,11 @@ function checkConfigFile() {
|
||||
throw new Error("No config file present!");
|
||||
}
|
||||
|
||||
// check permission
|
||||
// Check permission
|
||||
try {
|
||||
fs.accessSync(configFileName, fs.F_OK);
|
||||
} catch (e) {
|
||||
Log.log(Utils.colors.error(e));
|
||||
Log.error(Utils.colors.error(e));
|
||||
throw new Error("No permission to access config file!");
|
||||
}
|
||||
|
||||
@@ -54,16 +58,15 @@ function checkConfigFile() {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
const messages = linter.verify(data, config);
|
||||
const messages = linter.verify(data);
|
||||
if (messages.length === 0) {
|
||||
Log.log("Your configuration file doesn't contain syntax errors :)");
|
||||
return true;
|
||||
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
|
||||
} else {
|
||||
Log.error(Utils.colors.error("Your configuration file contains syntax errors :("));
|
||||
// In case the there errors show messages and return
|
||||
messages.forEach((error) => {
|
||||
Log.log("Line", error.line, "col", error.column, error.message);
|
||||
Log.error("Line", error.line, "col", error.column, error.message);
|
||||
});
|
||||
throw new Error("Wrong syntax in config file!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
13
js/class.js
@@ -57,7 +57,9 @@
|
||||
: prop[name];
|
||||
}
|
||||
|
||||
// The dummy class constructor
|
||||
/**
|
||||
* The dummy class constructor
|
||||
*/
|
||||
function Class() {
|
||||
// All construction is actually done in the init method
|
||||
if (!initializing && this.init) {
|
||||
@@ -78,8 +80,13 @@
|
||||
};
|
||||
})();
|
||||
|
||||
//Define the clone method for later use.
|
||||
//Helper Method
|
||||
/**
|
||||
* Define the clone method for later use. Helper Method.
|
||||
*
|
||||
* @param {object} obj Object to be cloned
|
||||
*
|
||||
* @returns {object} the cloned object
|
||||
*/
|
||||
function cloneObject(obj) {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj;
|
||||
|
@@ -15,6 +15,9 @@ const BrowserWindow = electron.BrowserWindow;
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let mainWindow;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function createWindow() {
|
||||
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
|
||||
var electronOptionsDefaults = {
|
||||
|
45
js/loader.js
@@ -15,7 +15,7 @@ var Loader = (function () {
|
||||
|
||||
/* Private Methods */
|
||||
|
||||
/* loadModules()
|
||||
/**
|
||||
* Loops thru all modules and requests load for every module.
|
||||
*/
|
||||
var loadModules = function () {
|
||||
@@ -43,7 +43,7 @@ var Loader = (function () {
|
||||
loadNextModule();
|
||||
};
|
||||
|
||||
/* startModules()
|
||||
/**
|
||||
* Loops thru all modules and requests start for every module.
|
||||
*/
|
||||
var startModules = function () {
|
||||
@@ -56,19 +56,19 @@ var Loader = (function () {
|
||||
MM.modulesStarted(moduleObjects);
|
||||
};
|
||||
|
||||
/* getAllModules()
|
||||
/**
|
||||
* Retrieve list of all modules.
|
||||
*
|
||||
* return array - module data as configured in config
|
||||
* @returns {object[]} module data as configured in config
|
||||
*/
|
||||
var getAllModules = function () {
|
||||
return config.modules;
|
||||
};
|
||||
|
||||
/* getModuleData()
|
||||
/**
|
||||
* Generate array with module information including module paths.
|
||||
*
|
||||
* return array - Module information.
|
||||
* @returns {object[]} Module information.
|
||||
*/
|
||||
var getModuleData = function () {
|
||||
var modules = getAllModules();
|
||||
@@ -98,6 +98,7 @@ var Loader = (function () {
|
||||
file: moduleName + ".js",
|
||||
position: moduleData.position,
|
||||
header: moduleData.header,
|
||||
configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false,
|
||||
config: moduleData.config,
|
||||
classes: typeof moduleData.classes !== "undefined" ? moduleData.classes + " " + module : module
|
||||
});
|
||||
@@ -106,11 +107,11 @@ var Loader = (function () {
|
||||
return moduleFiles;
|
||||
};
|
||||
|
||||
/* loadModule(module)
|
||||
* Load modules via ajax request and create module objects.
|
||||
/**
|
||||
* Load modules via ajax request and create module objects.s
|
||||
*
|
||||
* argument callback function - Function called when done.
|
||||
* argument module object - Information about the module we want to load.
|
||||
* @param {object} module Information about the module we want to load.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
var loadModule = function (module, callback) {
|
||||
var url = module.path + "/" + module.file;
|
||||
@@ -136,12 +137,12 @@ var Loader = (function () {
|
||||
}
|
||||
};
|
||||
|
||||
/* bootstrapModule(module, mObj)
|
||||
/**
|
||||
* Bootstrap modules by setting the module data and loading the scripts & styles.
|
||||
*
|
||||
* argument module object - Information about the module we want to load.
|
||||
* argument mObj object - Modules instance.
|
||||
* argument callback function - Function called when done.
|
||||
* @param {object} module Information about the module we want to load.
|
||||
* @param {Module} mObj Modules instance.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
var bootstrapModule = function (module, mObj, callback) {
|
||||
Log.info("Bootstrapping module: " + module.name);
|
||||
@@ -161,11 +162,11 @@ var Loader = (function () {
|
||||
});
|
||||
};
|
||||
|
||||
/* loadFile(fileName)
|
||||
/**
|
||||
* Load a script or stylesheet by adding it to the dom.
|
||||
*
|
||||
* argument fileName string - Path of the file we want to load.
|
||||
* argument callback function - Function called when done.
|
||||
* @param {string} fileName Path of the file we want to load.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
var loadFile = function (fileName, callback) {
|
||||
var extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1);
|
||||
@@ -215,20 +216,20 @@ var Loader = (function () {
|
||||
|
||||
/* Public Methods */
|
||||
return {
|
||||
/* loadModules()
|
||||
/**
|
||||
* Load all modules as defined in the config.
|
||||
*/
|
||||
loadModules: function () {
|
||||
loadModules();
|
||||
},
|
||||
|
||||
/* loadFile()
|
||||
/**
|
||||
* Load a file (script or stylesheet).
|
||||
* Prevent double loading and search for files in the vendor folder.
|
||||
*
|
||||
* argument fileName string - Path of the file we want to load.
|
||||
* argument module Module Object - the module that calls the loadFile function.
|
||||
* argument callback function - Function called when done.
|
||||
* @param {string} fileName Path of the file we want to load.
|
||||
* @param {Module} module The module that calls the loadFile function.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
loadFile: function (fileName, module, callback) {
|
||||
if (loadedFiles.indexOf(fileName.toLowerCase()) !== -1) {
|
||||
|
28
js/logger.js
@@ -10,7 +10,10 @@
|
||||
(function (root, factory) {
|
||||
if (typeof exports === "object") {
|
||||
// add timestamps in front of log messages
|
||||
require("console-stamp")(console, "yyyy-mm-dd HH:MM:ss.l");
|
||||
require("console-stamp")(console, {
|
||||
pattern: "yyyy-mm-dd HH:MM:ss.l",
|
||||
include: ["debug", "log", "info", "warn", "error"]
|
||||
});
|
||||
|
||||
// Node, CommonJS-like
|
||||
module.exports = factory(root.config);
|
||||
@@ -19,11 +22,12 @@
|
||||
root.Log = factory(root.config);
|
||||
}
|
||||
})(this, function (config) {
|
||||
let logLevel = {
|
||||
info: Function.prototype.bind.call(console.info, console),
|
||||
const logLevel = {
|
||||
debug: Function.prototype.bind.call(console.debug, console),
|
||||
log: Function.prototype.bind.call(console.log, console),
|
||||
error: Function.prototype.bind.call(console.error, console),
|
||||
info: Function.prototype.bind.call(console.info, console),
|
||||
warn: Function.prototype.bind.call(console.warn, console),
|
||||
error: Function.prototype.bind.call(console.error, console),
|
||||
group: Function.prototype.bind.call(console.group, console),
|
||||
groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console),
|
||||
groupEnd: Function.prototype.bind.call(console.groupEnd, console),
|
||||
@@ -32,13 +36,15 @@
|
||||
timeStamp: Function.prototype.bind.call(console.timeStamp, console)
|
||||
};
|
||||
|
||||
if (config && config.logLevel) {
|
||||
Object.keys(logLevel).forEach(function (key, index) {
|
||||
if (!config.logLevel.includes(key.toLocaleUpperCase())) {
|
||||
logLevel[key] = function () {};
|
||||
}
|
||||
});
|
||||
}
|
||||
logLevel.setLogLevel = function (newLevel) {
|
||||
if (newLevel) {
|
||||
Object.keys(logLevel).forEach(function (key, index) {
|
||||
if (!newLevel.includes(key.toLocaleUpperCase())) {
|
||||
logLevel[key] = function () {};
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return logLevel;
|
||||
});
|
||||
|
185
js/main.js
@@ -11,9 +11,8 @@ var MM = (function () {
|
||||
|
||||
/* Private Methods */
|
||||
|
||||
/* createDomObjects()
|
||||
* Create dom objects for all modules that
|
||||
* are configured for a specific position.
|
||||
/**
|
||||
* Create dom objects for all modules that are configured for a specific position.
|
||||
*/
|
||||
var createDomObjects = function () {
|
||||
var domCreationPromises = [];
|
||||
@@ -42,7 +41,9 @@ var MM = (function () {
|
||||
dom.appendChild(moduleHeader);
|
||||
|
||||
if (typeof module.getHeader() === "undefined" || module.getHeader() !== "") {
|
||||
moduleHeader.style = "display: none;";
|
||||
moduleHeader.style.display = "none;";
|
||||
} else {
|
||||
moduleHeader.style.display = "block;";
|
||||
}
|
||||
|
||||
var moduleContent = document.createElement("div");
|
||||
@@ -65,10 +66,12 @@ var MM = (function () {
|
||||
});
|
||||
};
|
||||
|
||||
/* selectWrapper(position)
|
||||
/**
|
||||
* Select the wrapper dom object for a specific position.
|
||||
*
|
||||
* argument position string - The name of the position.
|
||||
* @param {string} position The name of the position.
|
||||
*
|
||||
* @returns {HTMLElement} the wrapper element
|
||||
*/
|
||||
var selectWrapper = function (position) {
|
||||
var classes = position.replace("_", " ");
|
||||
@@ -81,13 +84,13 @@ var MM = (function () {
|
||||
}
|
||||
};
|
||||
|
||||
/* sendNotification(notification, payload, sender)
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* argument sender Module - The module that sent the notification.
|
||||
* argument sendTo Module - The module to send the notification to. (optional)
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
* @param {Module} [sendTo] The (optional) module to send the notification to.
|
||||
*/
|
||||
var sendNotification = function (notification, payload, sender, sendTo) {
|
||||
for (var m in modules) {
|
||||
@@ -98,13 +101,13 @@ var MM = (function () {
|
||||
}
|
||||
};
|
||||
|
||||
/* updateDom(module, speed)
|
||||
/**
|
||||
* Update the dom for a specific module.
|
||||
*
|
||||
* argument module Module - The module that needs an update.
|
||||
* argument speed Number - The number of microseconds for the animation. (optional)
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The (optional) number of microseconds for the animation.
|
||||
*
|
||||
* return Promise - Resolved when the dom is fully updated.
|
||||
* @returns {Promise} Resolved when the dom is fully updated.
|
||||
*/
|
||||
var updateDom = function (module, speed) {
|
||||
return new Promise(function (resolve) {
|
||||
@@ -126,15 +129,15 @@ var MM = (function () {
|
||||
});
|
||||
};
|
||||
|
||||
/* updateDomWithContent(module, speed, newHeader, newContent)
|
||||
/**
|
||||
* Update the dom with the specified content
|
||||
*
|
||||
* argument module Module - The module that needs an update.
|
||||
* argument speed Number - The number of microseconds for the animation. (optional)
|
||||
* argument newHeader String - The new header that is generated.
|
||||
* argument newContent Domobject - The new content that is generated.
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The (optional) number of microseconds for the animation.
|
||||
* @param {string} newHeader The new header that is generated.
|
||||
* @param {HTMLElement} newContent The new content that is generated.
|
||||
*
|
||||
* return Promise - Resolved when the module dom has been updated.
|
||||
* @returns {Promise} Resolved when the module dom has been updated.
|
||||
*/
|
||||
var updateDomWithContent = function (module, speed, newHeader, newContent) {
|
||||
return new Promise(function (resolve) {
|
||||
@@ -165,14 +168,14 @@ var MM = (function () {
|
||||
});
|
||||
};
|
||||
|
||||
/* moduleNeedsUpdate(module, newContent)
|
||||
/**
|
||||
* Check if the content has changed.
|
||||
*
|
||||
* argument module Module - The module to check.
|
||||
* argument newHeader String - The new header that is generated.
|
||||
* argument newContent Domobject - The new content that is generated.
|
||||
* @param {Module} module The module to check.
|
||||
* @param {string} newHeader The new header that is generated.
|
||||
* @param {HTMLElement} newContent The new content that is generated.
|
||||
*
|
||||
* return bool - Does the module need an update?
|
||||
* @returns {boolean} True if the module need an update, false otherwise
|
||||
*/
|
||||
var moduleNeedsUpdate = function (module, newHeader, newContent) {
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
@@ -197,12 +200,12 @@ var MM = (function () {
|
||||
return headerNeedsUpdate || contentNeedsUpdate;
|
||||
};
|
||||
|
||||
/* moduleNeedsUpdate(module, newContent)
|
||||
/**
|
||||
* Update the content of a module on screen.
|
||||
*
|
||||
* argument module Module - The module to check.
|
||||
* argument newHeader String - The new header that is generated.
|
||||
* argument newContent Domobject - The new content that is generated.
|
||||
* @param {Module} module The module to check.
|
||||
* @param {string} newHeader The new header that is generated.
|
||||
* @param {HTMLElement} newContent The new content that is generated.
|
||||
*/
|
||||
var updateModuleContent = function (module, newHeader, newContent) {
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
@@ -216,15 +219,20 @@ var MM = (function () {
|
||||
contentWrapper[0].appendChild(newContent);
|
||||
|
||||
headerWrapper[0].innerHTML = newHeader;
|
||||
headerWrapper[0].style = headerWrapper.length > 0 && newHeader ? undefined : "display: none;";
|
||||
if (headerWrapper.length > 0 && newHeader) {
|
||||
headerWrapper[0].style.display = "block";
|
||||
} else {
|
||||
headerWrapper[0].style.display = "none";
|
||||
}
|
||||
};
|
||||
|
||||
/* hideModule(module, speed, callback)
|
||||
/**
|
||||
* Hide the module.
|
||||
*
|
||||
* argument module Module - The module to hide.
|
||||
* argument speed Number - The speed of the hide animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* @param {Module} module The module to hide.
|
||||
* @param {number} speed The speed of the hide animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the hide method.
|
||||
*/
|
||||
var hideModule = function (module, speed, callback, options) {
|
||||
options = options || {};
|
||||
@@ -264,12 +272,13 @@ var MM = (function () {
|
||||
}
|
||||
};
|
||||
|
||||
/* showModule(module, speed, callback)
|
||||
/**
|
||||
* Show the module.
|
||||
*
|
||||
* argument module Module - The module to show.
|
||||
* argument speed Number - The speed of the show animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* @param {Module} module The module to show.
|
||||
* @param {number} speed The speed of the show animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the show method.
|
||||
*/
|
||||
var showModule = function (module, speed, callback, options) {
|
||||
options = options || {};
|
||||
@@ -323,7 +332,7 @@ var MM = (function () {
|
||||
}
|
||||
};
|
||||
|
||||
/* updateWrapperStates()
|
||||
/**
|
||||
* Checks for all positions if it has visible content.
|
||||
* If not, if will hide the position to prevent unwanted margins.
|
||||
* This method should be called by the show and hide methods.
|
||||
@@ -352,8 +361,8 @@ var MM = (function () {
|
||||
});
|
||||
};
|
||||
|
||||
/* loadConfig()
|
||||
* Loads the core config and combines it with de system defaults.
|
||||
/**
|
||||
* Loads the core config and combines it with the system defaults.
|
||||
*/
|
||||
var loadConfig = function () {
|
||||
// FIXME: Think about how to pass config around without breaking tests
|
||||
@@ -368,41 +377,41 @@ var MM = (function () {
|
||||
/* eslint-enable */
|
||||
};
|
||||
|
||||
/* setSelectionMethodsForModules()
|
||||
/**
|
||||
* Adds special selectors on a collection of modules.
|
||||
*
|
||||
* argument modules array - Array of modules.
|
||||
* @param {Module[]} modules Array of modules.
|
||||
*/
|
||||
var setSelectionMethodsForModules = function (modules) {
|
||||
/* withClass(className)
|
||||
* calls modulesByClass to filter modules with the specified classes.
|
||||
/**
|
||||
* Filter modules with the specified classes.
|
||||
*
|
||||
* argument className string/array - one or multiple classnames. (array or space divided)
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
*
|
||||
* return array - Filtered collection of modules.
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var withClass = function (className) {
|
||||
return modulesByClass(className, true);
|
||||
};
|
||||
|
||||
/* exceptWithClass(className)
|
||||
* calls modulesByClass to filter modules without the specified classes.
|
||||
/**
|
||||
* Filter modules without the specified classes.
|
||||
*
|
||||
* argument className string/array - one or multiple classnames. (array or space divided)
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
*
|
||||
* return array - Filtered collection of modules.
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var exceptWithClass = function (className) {
|
||||
return modulesByClass(className, false);
|
||||
};
|
||||
|
||||
/* modulesByClass(className, include)
|
||||
* filters a collection of modules based on classname(s).
|
||||
/**
|
||||
* Filters a collection of modules based on classname(s).
|
||||
*
|
||||
* argument className string/array - one or multiple classnames. (array or space divided)
|
||||
* argument include boolean - if the filter should include or exclude the modules with the specific classes.
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
* @param {boolean} include if the filter should include or exclude the modules with the specific classes.
|
||||
*
|
||||
* return array - Filtered collection of modules.
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var modulesByClass = function (className, include) {
|
||||
var searchClasses = className;
|
||||
@@ -427,12 +436,12 @@ var MM = (function () {
|
||||
return newModules;
|
||||
};
|
||||
|
||||
/* exceptModule(module)
|
||||
/**
|
||||
* Removes a module instance from the collection.
|
||||
*
|
||||
* argument module Module object - The module instance to remove from the collection.
|
||||
* @param {object} module The module instance to remove from the collection.
|
||||
*
|
||||
* return array - Filtered collection of modules.
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var exceptModule = function (module) {
|
||||
var newModules = modules.filter(function (mod) {
|
||||
@@ -443,10 +452,10 @@ var MM = (function () {
|
||||
return newModules;
|
||||
};
|
||||
|
||||
/* enumerate(callback)
|
||||
/**
|
||||
* Walks thru a collection of modules and executes the callback with the module as an argument.
|
||||
*
|
||||
* argument callback function - The function to execute with the module as an argument.
|
||||
* @param {Function} callback The function to execute with the module as an argument.
|
||||
*/
|
||||
var enumerate = function (callback) {
|
||||
modules.map(function (module) {
|
||||
@@ -471,27 +480,27 @@ var MM = (function () {
|
||||
return {
|
||||
/* Public Methods */
|
||||
|
||||
/* init()
|
||||
/**
|
||||
* Main init method.
|
||||
*/
|
||||
init: function () {
|
||||
Log.info("Initializing MagicMirror.");
|
||||
loadConfig();
|
||||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
Translator.loadCoreTranslations(config.language);
|
||||
Loader.loadModules();
|
||||
},
|
||||
|
||||
/* modulesStarted(moduleObjects)
|
||||
/**
|
||||
* Gets called when all modules are started.
|
||||
*
|
||||
* argument moduleObjects array<Module> - All module instances.
|
||||
* @param {Module[]} moduleObjects All module instances.
|
||||
*/
|
||||
modulesStarted: function (moduleObjects) {
|
||||
modules = [];
|
||||
for (var m in moduleObjects) {
|
||||
var module = moduleObjects[m];
|
||||
modules[module.data.index] = module;
|
||||
}
|
||||
moduleObjects.forEach((module) => modules.push(module));
|
||||
|
||||
Log.info("All modules started!");
|
||||
sendNotification("ALL_MODULES_STARTED");
|
||||
@@ -499,12 +508,12 @@ var MM = (function () {
|
||||
createDomObjects();
|
||||
},
|
||||
|
||||
/* sendNotification(notification, payload, sender)
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* argument sender Module - The module that sent the notification.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
*/
|
||||
sendNotification: function (notification, payload, sender) {
|
||||
if (arguments.length < 3) {
|
||||
@@ -526,11 +535,11 @@ var MM = (function () {
|
||||
sendNotification(notification, payload, sender);
|
||||
},
|
||||
|
||||
/* updateDom(module, speed)
|
||||
/**
|
||||
* Update the dom for a specific module.
|
||||
*
|
||||
* argument module Module - The module that needs an update.
|
||||
* argument speed Number - The number of microseconds for the animation. (optional)
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The number of microseconds for the animation.
|
||||
*/
|
||||
updateDom: function (module, speed) {
|
||||
if (!(module instanceof Module)) {
|
||||
@@ -542,36 +551,36 @@ var MM = (function () {
|
||||
updateDom(module, speed);
|
||||
},
|
||||
|
||||
/* getModules(module, speed)
|
||||
/**
|
||||
* Returns a collection of all modules currently active.
|
||||
*
|
||||
* return array - A collection of all modules currently active.
|
||||
* @returns {Module[]} A collection of all modules currently active.
|
||||
*/
|
||||
getModules: function () {
|
||||
setSelectionMethodsForModules(modules);
|
||||
return modules;
|
||||
},
|
||||
|
||||
/* hideModule(module, speed, callback)
|
||||
/**
|
||||
* Hide the module.
|
||||
*
|
||||
* argument module Module - The module hide.
|
||||
* argument speed Number - The speed of the hide animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* argument options object - Optional settings for the hide method.
|
||||
* @param {Module} module The module to hide.
|
||||
* @param {number} speed The speed of the hide animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the hide method.
|
||||
*/
|
||||
hideModule: function (module, speed, callback, options) {
|
||||
module.hidden = true;
|
||||
hideModule(module, speed, callback, options);
|
||||
},
|
||||
|
||||
/* showModule(module, speed, callback)
|
||||
/**
|
||||
* Show the module.
|
||||
*
|
||||
* argument module Module - The module show.
|
||||
* argument speed Number - The speed of the show animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* argument options object - Optional settings for the hide method.
|
||||
* @param {Module} module The module to show.
|
||||
* @param {number} speed The speed of the show animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the show method.
|
||||
*/
|
||||
showModule: function (module, speed, callback, options) {
|
||||
// do not change module.hidden yet, only if we really show it later
|
||||
|
259
js/module.js
@@ -2,9 +2,11 @@
|
||||
|
||||
/* Magic Mirror
|
||||
* Module Blueprint.
|
||||
* @typedef {Object} Module
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
*/
|
||||
var Module = Class.extend({
|
||||
/*********************************************************
|
||||
@@ -29,53 +31,55 @@ var Module = Class.extend({
|
||||
// Use the nunjucksEnvironment() to get it.
|
||||
_nunjucksEnvironment: null,
|
||||
|
||||
/* init()
|
||||
* Is called when the module is instantiated.
|
||||
/**
|
||||
* Called when the module is instantiated.
|
||||
*/
|
||||
init: function () {
|
||||
//Log.log(this.defaults);
|
||||
},
|
||||
|
||||
/* start()
|
||||
* Is called when the module is started.
|
||||
/**
|
||||
* Called when the module is started.
|
||||
*/
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
},
|
||||
|
||||
/* getScripts()
|
||||
/**
|
||||
* Returns a list of scripts the module requires to be loaded.
|
||||
*
|
||||
* return Array<String> - An array with filenames.
|
||||
* @returns {string[]} An array with filenames.
|
||||
*/
|
||||
getScripts: function () {
|
||||
return [];
|
||||
},
|
||||
|
||||
/* getStyles()
|
||||
/**
|
||||
* Returns a list of stylesheets the module requires to be loaded.
|
||||
*
|
||||
* return Array<String> - An array with filenames.
|
||||
* @returns {string[]} An array with filenames.
|
||||
*/
|
||||
getStyles: function () {
|
||||
return [];
|
||||
},
|
||||
|
||||
/* getTranslations()
|
||||
/**
|
||||
* Returns a map of translation files the module requires to be loaded.
|
||||
*
|
||||
* return Map<String, String> - A map with langKeys and filenames.
|
||||
* return Map<String, String> -
|
||||
*
|
||||
* @returns {*} A map with langKeys and filenames.
|
||||
*/
|
||||
getTranslations: function () {
|
||||
return false;
|
||||
},
|
||||
|
||||
/* getDom()
|
||||
* This method generates the dom which needs to be displayed. This method is called by the Magic Mirror core.
|
||||
/**
|
||||
* Generates the dom which needs to be displayed. This method is called by the Magic Mirror core.
|
||||
* This method can to be subclassed if the module wants to display info on the mirror.
|
||||
* Alternatively, the getTemplate method could be subclassed.
|
||||
*
|
||||
* return DomObject | Promise - The dom or a promise with the dom to display.
|
||||
* @returns {HTMLElement|Promise} The dom or a promise with the dom to display.
|
||||
*/
|
||||
getDom: function () {
|
||||
var self = this;
|
||||
@@ -105,46 +109,45 @@ var Module = Class.extend({
|
||||
});
|
||||
},
|
||||
|
||||
/* getHeader()
|
||||
* This method generates the header string which needs to be displayed if a user has a header configured for this module.
|
||||
/**
|
||||
* Generates the header string which needs to be displayed if a user has a header configured for this module.
|
||||
* This method is called by the Magic Mirror core, but only if the user has configured a default header for the module.
|
||||
* This method needs to be subclassed if the module wants to display modified headers on the mirror.
|
||||
*
|
||||
* return string - The header to display above the header.
|
||||
* @returns {string} The header to display above the header.
|
||||
*/
|
||||
getHeader: function () {
|
||||
return this.data.header;
|
||||
},
|
||||
|
||||
/* getTemplate()
|
||||
* This method returns the template for the module which is used by the default getDom implementation.
|
||||
/**
|
||||
* Returns the template for the module which is used by the default getDom implementation.
|
||||
* This method needs to be subclassed if the module wants to use a template.
|
||||
* It can either return a template sting, or a template filename.
|
||||
* If the string ends with '.html' it's considered a file from within the module's folder.
|
||||
*
|
||||
* return string - The template string of filename.
|
||||
* @returns {string} The template string of filename.
|
||||
*/
|
||||
getTemplate: function () {
|
||||
return '<div class="normal">' + this.name + '</div><div class="small dimmed">' + this.identifier + "</div>";
|
||||
},
|
||||
|
||||
/* getTemplateData()
|
||||
* This method returns the data to be used in the template.
|
||||
/**
|
||||
* Returns the data to be used in the template.
|
||||
* This method needs to be subclassed if the module wants to use a custom data.
|
||||
*
|
||||
* return Object
|
||||
* @returns {object} The data for the template
|
||||
*/
|
||||
getTemplateData: function () {
|
||||
return {};
|
||||
},
|
||||
|
||||
/* notificationReceived(notification, payload, sender)
|
||||
* This method is called when a notification arrives.
|
||||
* This method is called by the Magic Mirror core.
|
||||
/**
|
||||
* Called by the Magic Mirror core when a notification arrives.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* argument sender Module - The module that sent the notification.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
*/
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
if (sender) {
|
||||
@@ -154,11 +157,11 @@ var Module = Class.extend({
|
||||
}
|
||||
},
|
||||
|
||||
/** nunjucksEnvironment()
|
||||
/**
|
||||
* Returns the nunjucks environment for the current module.
|
||||
* The environment is checked in the _nunjucksEnvironment instance variable.
|
||||
|
||||
* @returns Nunjucks Environment
|
||||
*
|
||||
* @returns {object} The Nunjucks Environment
|
||||
*/
|
||||
nunjucksEnvironment: function () {
|
||||
if (this._nunjucksEnvironment !== null) {
|
||||
@@ -171,32 +174,33 @@ var Module = Class.extend({
|
||||
trimBlocks: true,
|
||||
lstripBlocks: true
|
||||
});
|
||||
this._nunjucksEnvironment.addFilter("translate", function (str) {
|
||||
return self.translate(str);
|
||||
|
||||
this._nunjucksEnvironment.addFilter("translate", function (str, variables) {
|
||||
return self.translate(str, variables);
|
||||
});
|
||||
|
||||
return this._nunjucksEnvironment;
|
||||
},
|
||||
|
||||
/* socketNotificationReceived(notification, payload)
|
||||
* This method is called when a socket notification arrives.
|
||||
/**
|
||||
* Called when a socket notification arrives.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
|
||||
},
|
||||
|
||||
/* suspend()
|
||||
* This method is called when a module is hidden.
|
||||
/*
|
||||
* Called when the module is hidden.
|
||||
*/
|
||||
suspend: function () {
|
||||
Log.log(this.name + " is suspended.");
|
||||
},
|
||||
|
||||
/* resume()
|
||||
* This method is called when a module is shown.
|
||||
/*
|
||||
* Called when the module is shown.
|
||||
*/
|
||||
resume: function () {
|
||||
Log.log(this.name + " is resumed.");
|
||||
@@ -206,10 +210,10 @@ var Module = Class.extend({
|
||||
* The methods below don"t need subclassing. *
|
||||
*********************************************/
|
||||
|
||||
/* setData(data)
|
||||
/**
|
||||
* Set the module data.
|
||||
*
|
||||
* argument data object - Module data.
|
||||
* @param {Module} data The module data
|
||||
*/
|
||||
setData: function (data) {
|
||||
this.data = data;
|
||||
@@ -217,21 +221,24 @@ var Module = Class.extend({
|
||||
this.identifier = data.identifier;
|
||||
this.hidden = false;
|
||||
|
||||
this.setConfig(data.config);
|
||||
this.setConfig(data.config, data.configDeepMerge);
|
||||
},
|
||||
|
||||
/* setConfig(config)
|
||||
/**
|
||||
* Set the module config and combine it with the module defaults.
|
||||
*
|
||||
* argument config object - Module config.
|
||||
* @param {object} config The combined module config.
|
||||
* @param {boolean} deep Merge module config in deep.
|
||||
*/
|
||||
setConfig: function (config) {
|
||||
this.config = Object.assign({}, this.defaults, config);
|
||||
setConfig: function (config, deep) {
|
||||
this.config = deep ? configMerge({}, this.defaults, config) : Object.assign({}, this.defaults, config);
|
||||
},
|
||||
|
||||
/* socket()
|
||||
* Returns a socket object. If it doesn't exist, it"s created.
|
||||
/**
|
||||
* Returns a socket object. If it doesn't exist, it's created.
|
||||
* It also registers the notification callback.
|
||||
*
|
||||
* @returns {MMSocket} a socket object
|
||||
*/
|
||||
socket: function () {
|
||||
if (typeof this._socket === "undefined") {
|
||||
@@ -246,40 +253,39 @@ var Module = Class.extend({
|
||||
return this._socket;
|
||||
},
|
||||
|
||||
/* file(file)
|
||||
/**
|
||||
* Retrieve the path to a module file.
|
||||
*
|
||||
* argument file string - Filename.
|
||||
*
|
||||
* return string - File path.
|
||||
* @param {string} file Filename
|
||||
* @returns {string} the file path
|
||||
*/
|
||||
file: function (file) {
|
||||
return (this.data.path + "/" + file).replace("//", "/");
|
||||
},
|
||||
|
||||
/* loadStyles()
|
||||
/**
|
||||
* Load all required stylesheets by requesting the MM object to load the files.
|
||||
*
|
||||
* argument callback function - Function called when done.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
loadStyles: function (callback) {
|
||||
this.loadDependencies("getStyles", callback);
|
||||
},
|
||||
|
||||
/* loadScripts()
|
||||
/**
|
||||
* Load all required scripts by requesting the MM object to load the files.
|
||||
*
|
||||
* argument callback function - Function called when done.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
loadScripts: function (callback) {
|
||||
this.loadDependencies("getScripts", callback);
|
||||
},
|
||||
|
||||
/* loadDependencies(funcName, callback)
|
||||
/**
|
||||
* Helper method to load all dependencies.
|
||||
*
|
||||
* argument funcName string - Function name to call to get scripts or styles.
|
||||
* argument callback function - Function called when done.
|
||||
* @param {string} funcName Function name to call to get scripts or styles.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
loadDependencies: function (funcName, callback) {
|
||||
var self = this;
|
||||
@@ -300,10 +306,10 @@ var Module = Class.extend({
|
||||
loadNextDependency();
|
||||
},
|
||||
|
||||
/* loadScripts()
|
||||
* Load all required scripts by requesting the MM object to load the files.
|
||||
/**
|
||||
* Load all translations.
|
||||
*
|
||||
* argument callback function - Function called when done.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
loadTranslations: function (callback) {
|
||||
var self = this;
|
||||
@@ -334,12 +340,13 @@ var Module = Class.extend({
|
||||
}
|
||||
},
|
||||
|
||||
/* translate(key, defaultValueOrVariables, defaultValue)
|
||||
/**
|
||||
* Request the translation for a given key with optional variables and default value.
|
||||
*
|
||||
* argument key string - The key of the string to translate
|
||||
* argument defaultValueOrVariables string/object - The default value or variables for translating. (Optional)
|
||||
* argument defaultValue string - The default value with variables. (Optional)
|
||||
* @param {string} key The key of the string to translate
|
||||
* @param {string|object} [defaultValueOrVariables] The default value or variables for translating.
|
||||
* @param {string} [defaultValue] The default value with variables.
|
||||
* @returns {string} the translated key
|
||||
*/
|
||||
translate: function (key, defaultValueOrVariables, defaultValue) {
|
||||
if (typeof defaultValueOrVariables === "object") {
|
||||
@@ -348,41 +355,41 @@ var Module = Class.extend({
|
||||
return Translator.translate(this, key) || defaultValueOrVariables || "";
|
||||
},
|
||||
|
||||
/* updateDom(speed)
|
||||
/**
|
||||
* Request an (animated) update of the module.
|
||||
*
|
||||
* argument speed Number - The speed of the animation. (Optional)
|
||||
* @param {number} [speed] The speed of the animation.
|
||||
*/
|
||||
updateDom: function (speed) {
|
||||
MM.updateDom(this, speed);
|
||||
},
|
||||
|
||||
/* sendNotification(notification, payload)
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
sendNotification: function (notification, payload) {
|
||||
MM.sendNotification(notification, payload, this);
|
||||
},
|
||||
|
||||
/* sendSocketNotification(notification, payload)
|
||||
/**
|
||||
* Send a socket notification to the node helper.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
sendSocketNotification: function (notification, payload) {
|
||||
this.socket().sendNotification(notification, payload);
|
||||
},
|
||||
|
||||
/* hideModule(module, speed, callback)
|
||||
/**
|
||||
* Hide this module.
|
||||
*
|
||||
* argument speed Number - The speed of the hide animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* argument options object - Optional settings for the hide method.
|
||||
* @param {number} speed The speed of the hide animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the hide method.
|
||||
*/
|
||||
hide: function (speed, callback, options) {
|
||||
if (typeof callback === "object") {
|
||||
@@ -405,12 +412,12 @@ var Module = Class.extend({
|
||||
);
|
||||
},
|
||||
|
||||
/* showModule(module, speed, callback)
|
||||
/**
|
||||
* Show this module.
|
||||
*
|
||||
* argument speed Number - The speed of the show animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* argument options object - Optional settings for the hide method.
|
||||
* @param {number} speed The speed of the show animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the show method.
|
||||
*/
|
||||
show: function (speed, callback, options) {
|
||||
if (typeof callback === "object") {
|
||||
@@ -427,13 +434,59 @@ var Module = Class.extend({
|
||||
speed,
|
||||
function () {
|
||||
self.resume();
|
||||
callback;
|
||||
callback();
|
||||
},
|
||||
options
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Merging MagicMirror (or other) default/config script by @bugsounet
|
||||
* Merge 2 objects or/with array
|
||||
*
|
||||
* Usage:
|
||||
* -------
|
||||
* this.config = configMerge({}, this.defaults, this.config)
|
||||
* -------
|
||||
* arg1: initial object
|
||||
* arg2: config model
|
||||
* arg3: config to merge
|
||||
* -------
|
||||
* why using it ?
|
||||
* Object.assign() function don't to all job
|
||||
* it don't merge all thing in deep
|
||||
* -> object in object and array is not merging
|
||||
* -------
|
||||
*
|
||||
* Todo: idea of Mich determinate what do you want to merge or not
|
||||
*
|
||||
* @param {object} result the initial object
|
||||
* @returns {object} the merged config
|
||||
*/
|
||||
function configMerge(result) {
|
||||
var stack = Array.prototype.slice.call(arguments, 1);
|
||||
var item;
|
||||
var key;
|
||||
while (stack.length) {
|
||||
item = stack.shift();
|
||||
for (key in item) {
|
||||
if (item.hasOwnProperty(key)) {
|
||||
if (typeof result[key] === "object" && result[key] && Object.prototype.toString.call(result[key]) !== "[object Array]") {
|
||||
if (typeof item[key] === "object" && item[key] !== null) {
|
||||
result[key] = configMerge({}, result[key], item[key]);
|
||||
} else {
|
||||
result[key] = item[key];
|
||||
}
|
||||
} else {
|
||||
result[key] = item[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Module.definitions = {};
|
||||
|
||||
Module.create = function (name) {
|
||||
@@ -451,11 +504,27 @@ Module.create = function (name) {
|
||||
return new ModuleClass();
|
||||
};
|
||||
|
||||
/* cmpVersions(a,b)
|
||||
Module.register = function (name, moduleDefinition) {
|
||||
if (moduleDefinition.requiresVersion) {
|
||||
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.version);
|
||||
if (cmpVersions(window.version, moduleDefinition.requiresVersion) >= 0) {
|
||||
Log.log("Version is ok!");
|
||||
} else {
|
||||
Log.warn("Version is incorrect. Skip module: '" + name + "'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Log.log("Module registered: " + name);
|
||||
Module.definitions[name] = moduleDefinition;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare two semantic version numbers and return the difference.
|
||||
*
|
||||
* argument a string - Version number a.
|
||||
* argument a string - Version number b.
|
||||
* @param {string} a Version number a.
|
||||
* @param {string} b Version number b.
|
||||
* @returns {number} A positive number if a is larger than b, a negative
|
||||
* number if a is smaller and 0 if they are the same
|
||||
*/
|
||||
function cmpVersions(a, b) {
|
||||
var i, diff;
|
||||
@@ -472,17 +541,3 @@ function cmpVersions(a, b) {
|
||||
}
|
||||
return segmentsA.length - segmentsB.length;
|
||||
}
|
||||
|
||||
Module.register = function (name, moduleDefinition) {
|
||||
if (moduleDefinition.requiresVersion) {
|
||||
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.version);
|
||||
if (cmpVersions(window.version, moduleDefinition.requiresVersion) >= 0) {
|
||||
Log.log("Version is ok!");
|
||||
} else {
|
||||
Log.log("Version is incorrect. Skip module: '" + name + "'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Log.log("Module registered: " + name);
|
||||
Module.definitions[name] = moduleDefinition;
|
||||
};
|
||||
|
@@ -37,7 +37,7 @@ var Server = function (config, callback) {
|
||||
server.listen(port, config.address ? config.address : "localhost");
|
||||
|
||||
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
||||
Log.info(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
|
||||
Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
|
||||
}
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
@@ -49,7 +49,7 @@ var Server = function (config, callback) {
|
||||
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
|
||||
});
|
||||
});
|
||||
app.use(helmet());
|
||||
app.use(helmet({ contentSecurityPolicy: false }));
|
||||
|
||||
app.use("/js", express.static(__dirname));
|
||||
var directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
|
||||
|
137
js/translator.js
@@ -7,11 +7,11 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var Translator = (function () {
|
||||
/* loadJSON(file, callback)
|
||||
/**
|
||||
* Load a JSON file via XHR.
|
||||
*
|
||||
* argument file string - Path of the file we want to load.
|
||||
* argument callback function - Function called when done.
|
||||
* @param {string} file Path of the file we want to load.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
function loadJSON(file, callback) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
@@ -19,112 +19,47 @@ var Translator = (function () {
|
||||
xhr.open("GET", file, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
callback(JSON.parse(stripComments(xhr.responseText)));
|
||||
// needs error handler try/catch at least
|
||||
let fileinfo = null;
|
||||
try {
|
||||
fileinfo = JSON.parse(xhr.responseText);
|
||||
} catch (exception) {
|
||||
// nothing here, but don't die
|
||||
Log.error(" loading json file =" + file + " failed");
|
||||
}
|
||||
callback(fileinfo);
|
||||
}
|
||||
};
|
||||
xhr.send(null);
|
||||
}
|
||||
|
||||
/* loadJSON(str, options)
|
||||
* Remove any commenting from a json file so it can be parsed.
|
||||
*
|
||||
* argument str string - The string that contains json with comments.
|
||||
* argument opts function - Strip options.
|
||||
*
|
||||
* return the stripped string.
|
||||
*/
|
||||
function stripComments(str, opts) {
|
||||
// strip comments copied from: https://github.com/sindresorhus/strip-json-comments
|
||||
|
||||
var singleComment = 1;
|
||||
var multiComment = 2;
|
||||
|
||||
function stripWithoutWhitespace() {
|
||||
return "";
|
||||
}
|
||||
|
||||
function stripWithWhitespace(str, start, end) {
|
||||
return str.slice(start, end).replace(/\S/g, " ");
|
||||
}
|
||||
|
||||
opts = opts || {};
|
||||
|
||||
var currentChar;
|
||||
var nextChar;
|
||||
var insideString = false;
|
||||
var insideComment = false;
|
||||
var offset = 0;
|
||||
var ret = "";
|
||||
var strip = opts.whitespace === false ? stripWithoutWhitespace : stripWithWhitespace;
|
||||
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
currentChar = str[i];
|
||||
nextChar = str[i + 1];
|
||||
|
||||
if (!insideComment && currentChar === '"') {
|
||||
var escaped = str[i - 1] === "\\" && str[i - 2] !== "\\";
|
||||
if (!escaped) {
|
||||
insideString = !insideString;
|
||||
}
|
||||
}
|
||||
|
||||
if (insideString) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!insideComment && currentChar + nextChar === "//") {
|
||||
ret += str.slice(offset, i);
|
||||
offset = i;
|
||||
insideComment = singleComment;
|
||||
i++;
|
||||
} else if (insideComment === singleComment && currentChar + nextChar === "\r\n") {
|
||||
i++;
|
||||
insideComment = false;
|
||||
ret += strip(str, offset, i);
|
||||
offset = i;
|
||||
continue;
|
||||
} else if (insideComment === singleComment && currentChar === "\n") {
|
||||
insideComment = false;
|
||||
ret += strip(str, offset, i);
|
||||
offset = i;
|
||||
} else if (!insideComment && currentChar + nextChar === "/*") {
|
||||
ret += str.slice(offset, i);
|
||||
offset = i;
|
||||
insideComment = multiComment;
|
||||
i++;
|
||||
continue;
|
||||
} else if (insideComment === multiComment && currentChar + nextChar === "*/") {
|
||||
i++;
|
||||
insideComment = false;
|
||||
ret += strip(str, offset, i + 1);
|
||||
offset = i + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return ret + (insideComment ? strip(str.substr(offset)) : str.substr(offset));
|
||||
}
|
||||
|
||||
return {
|
||||
coreTranslations: {},
|
||||
coreTranslationsFallback: {},
|
||||
translations: {},
|
||||
translationsFallback: {},
|
||||
|
||||
/* translate(module, key, variables)
|
||||
/**
|
||||
* Load a translation for a given key for a given module.
|
||||
*
|
||||
* argument module Module - The module to load the translation for.
|
||||
* argument key string - The key of the text to translate.
|
||||
* argument variables - The variables to use within the translation template (optional)
|
||||
* @param {Module} module The module to load the translation for.
|
||||
* @param {string} key The key of the text to translate.
|
||||
* @param {object} variables The variables to use within the translation template (optional)
|
||||
* @returns {string} the translated key
|
||||
*/
|
||||
translate: function (module, key, variables) {
|
||||
variables = variables || {}; //Empty object by default
|
||||
|
||||
// Combines template and variables like:
|
||||
// template: "Please wait for {timeToWait} before continuing with {work}."
|
||||
// variables: {timeToWait: "2 hours", work: "painting"}
|
||||
// to: "Please wait for 2 hours before continuing with painting."
|
||||
/**
|
||||
* Combines template and variables like:
|
||||
* template: "Please wait for {timeToWait} before continuing with {work}."
|
||||
* variables: {timeToWait: "2 hours", work: "painting"}
|
||||
* to: "Please wait for 2 hours before continuing with painting."
|
||||
*
|
||||
* @param {string} template Text with placeholder
|
||||
* @param {object} variables Variables for the placeholder
|
||||
* @returns {string} the template filled with the variables
|
||||
*/
|
||||
function createStringFromTemplate(template, variables) {
|
||||
if (Object.prototype.toString.call(template) !== "[object String]") {
|
||||
return template;
|
||||
@@ -133,7 +68,7 @@ var Translator = (function () {
|
||||
template = variables.fallback;
|
||||
}
|
||||
return template.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) {
|
||||
return variables[varName] || "{" + varName + "}";
|
||||
return varName in variables ? variables[varName] : "{" + varName + "}";
|
||||
});
|
||||
}
|
||||
|
||||
@@ -160,13 +95,13 @@ var Translator = (function () {
|
||||
return key;
|
||||
},
|
||||
|
||||
/* load(module, file, isFallback, callback)
|
||||
/**
|
||||
* Load a translation file (json) and remember the data.
|
||||
*
|
||||
* argument module Module - The module to load the translation file for.
|
||||
* argument file string - Path of the file we want to load.
|
||||
* argument isFallback boolean - Flag to indicate fallback translations.
|
||||
* argument callback function - Function called when done.
|
||||
* @param {Module} module The module to load the translation file for.
|
||||
* @param {string} file Path of the file we want to load.
|
||||
* @param {boolean} isFallback Flag to indicate fallback translations.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
load: function (module, file, isFallback, callback) {
|
||||
if (!isFallback) {
|
||||
@@ -190,10 +125,10 @@ var Translator = (function () {
|
||||
}
|
||||
},
|
||||
|
||||
/* loadCoreTranslations(lang)
|
||||
/**
|
||||
* Load the core translations.
|
||||
*
|
||||
* argument lang String - The language identifier of the core language.
|
||||
* @param {string} lang The language identifier of the core language.
|
||||
*/
|
||||
loadCoreTranslations: function (lang) {
|
||||
var self = this;
|
||||
@@ -210,7 +145,7 @@ var Translator = (function () {
|
||||
self.loadCoreTranslationsFallback();
|
||||
},
|
||||
|
||||
/* loadCoreTranslationsFallback()
|
||||
/**
|
||||
* Load the core translations fallback.
|
||||
* The first language defined in translations.js will be used.
|
||||
*/
|
||||
|
@@ -10,7 +10,8 @@ var Utils = {
|
||||
colors: {
|
||||
warn: colors.yellow,
|
||||
error: colors.red,
|
||||
info: colors.blue
|
||||
info: colors.blue,
|
||||
pass: colors.green
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -100,10 +100,13 @@ Module.register("alert", {
|
||||
message: image + message,
|
||||
effect: this.config.alert_effect,
|
||||
ttl: params.timer,
|
||||
onClose: () => this.hide_alert(sender),
|
||||
al_no: "ns-alert"
|
||||
});
|
||||
|
||||
//Show alert
|
||||
this.alerts[sender.name].show();
|
||||
|
||||
//Add timer to dismiss alert and overlay
|
||||
if (params.timer) {
|
||||
setTimeout(() => {
|
||||
|
@@ -12,7 +12,11 @@
|
||||
*/
|
||||
(function (window) {
|
||||
/**
|
||||
* extend obj function
|
||||
* Extend one object with another one
|
||||
*
|
||||
* @param {object} a The object to extend
|
||||
* @param {object} b The object which extends the other, overwrites existing keys
|
||||
* @returns {object} The merged object
|
||||
*/
|
||||
function extend(a, b) {
|
||||
for (let key in b) {
|
||||
@@ -24,7 +28,10 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* NotificationFx function
|
||||
* NotificationFx constructor
|
||||
*
|
||||
* @param {object} options The configuration options
|
||||
* @class
|
||||
*/
|
||||
function NotificationFx(options) {
|
||||
this.options = extend({}, this.options);
|
||||
@@ -66,8 +73,7 @@
|
||||
};
|
||||
|
||||
/**
|
||||
* init function
|
||||
* initialize and cache some vars
|
||||
* Initialize and cache some vars
|
||||
*/
|
||||
NotificationFx.prototype._init = function () {
|
||||
// create HTML structure
|
||||
@@ -95,7 +101,7 @@
|
||||
};
|
||||
|
||||
/**
|
||||
* init events
|
||||
* Init events
|
||||
*/
|
||||
NotificationFx.prototype._initEvents = function () {
|
||||
// dismiss notification by tapping on it if someone has a touchscreen
|
||||
@@ -105,7 +111,7 @@
|
||||
};
|
||||
|
||||
/**
|
||||
* show the notification
|
||||
* Show the notification
|
||||
*/
|
||||
NotificationFx.prototype.show = function () {
|
||||
this.active = true;
|
||||
@@ -115,7 +121,7 @@
|
||||
};
|
||||
|
||||
/**
|
||||
* dismiss the notification
|
||||
* Dismiss the notification
|
||||
*/
|
||||
NotificationFx.prototype.dismiss = function () {
|
||||
this.active = false;
|
||||
@@ -144,7 +150,7 @@
|
||||
};
|
||||
|
||||
/**
|
||||
* add to global namespace
|
||||
* Add to global namespace
|
||||
*/
|
||||
window.NotificationFx = NotificationFx;
|
||||
})(window);
|
||||
|
@@ -11,6 +11,7 @@ Module.register("calendar", {
|
||||
defaults: {
|
||||
maximumEntries: 10, // Total Maximum Entries
|
||||
maximumNumberOfDays: 365,
|
||||
limitDays: 0, // Limit the number of days shown, 0 = no limit
|
||||
displaySymbol: true,
|
||||
defaultSymbol: "calendar", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
|
||||
showLocation: false,
|
||||
@@ -37,6 +38,7 @@ Module.register("calendar", {
|
||||
hideOngoing: false,
|
||||
colored: false,
|
||||
coloredSymbolOnly: false,
|
||||
customEvents: [], // Array of {keyword: "", symbol: "", color: ""} where Keyword is a regexp and symbol/color are to be applied for matched
|
||||
tableClass: "small",
|
||||
calendars: [
|
||||
{
|
||||
@@ -58,6 +60,8 @@ Module.register("calendar", {
|
||||
nextDaysRelative: false
|
||||
},
|
||||
|
||||
requiresVersion: "2.1.0",
|
||||
|
||||
// Define required scripts.
|
||||
getStyles: function () {
|
||||
return ["calendar.css", "font-awesome.css"];
|
||||
@@ -83,6 +87,12 @@ Module.register("calendar", {
|
||||
// Set locale.
|
||||
moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
|
||||
|
||||
// clear data holder before start
|
||||
this.calendarData = {};
|
||||
|
||||
// indicate no data available yet
|
||||
this.loaded = false;
|
||||
|
||||
for (var c in this.config.calendars) {
|
||||
var calendar = this.config.calendars[c];
|
||||
calendar.url = calendar.url.replace("webcal://", "http://");
|
||||
@@ -112,18 +122,10 @@ Module.register("calendar", {
|
||||
};
|
||||
}
|
||||
|
||||
// tell helper to start a fetcher for this calendar
|
||||
// fetcher till cycle
|
||||
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
||||
|
||||
// Trigger ADD_CALENDAR every fetchInterval to make sure there is always a calendar
|
||||
// fetcher running on the server side.
|
||||
var self = this;
|
||||
setInterval(function () {
|
||||
self.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
||||
}, self.config.fetchInterval);
|
||||
}
|
||||
|
||||
this.calendarData = {};
|
||||
this.loaded = false;
|
||||
},
|
||||
|
||||
// Override socket notification handler.
|
||||
@@ -153,6 +155,12 @@ Module.register("calendar", {
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
// Define second, minute, hour, and day constants
|
||||
const oneSecond = 1000; // 1,000 milliseconds
|
||||
const oneMinute = oneSecond * 60;
|
||||
const oneHour = oneMinute * 60;
|
||||
const oneDay = oneHour * 24;
|
||||
|
||||
var events = this.createEventList();
|
||||
var wrapper = document.createElement("table");
|
||||
wrapper.className = this.config.tableClass;
|
||||
@@ -173,6 +181,8 @@ Module.register("calendar", {
|
||||
|
||||
var currentFadeStep = 0;
|
||||
var lastSeenDate = "";
|
||||
var ev;
|
||||
var needle;
|
||||
|
||||
for (var e in events) {
|
||||
var event = events[e];
|
||||
@@ -205,7 +215,7 @@ Module.register("calendar", {
|
||||
eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
|
||||
}
|
||||
|
||||
eventWrapper.className = "normal";
|
||||
eventWrapper.className = "normal event";
|
||||
|
||||
if (this.config.displaySymbol) {
|
||||
var symbolWrapper = document.createElement("td");
|
||||
@@ -217,9 +227,18 @@ Module.register("calendar", {
|
||||
var symbolClass = this.symbolClassForUrl(event.url);
|
||||
symbolWrapper.className = "symbol align-right " + symbolClass;
|
||||
|
||||
var symbols = this.symbolsForUrl(event.url);
|
||||
if (typeof symbols === "string") {
|
||||
symbols = [symbols];
|
||||
var symbols = this.symbolsForEvent(event);
|
||||
// If symbols are displayed and custom symbol is set, replace event symbol
|
||||
if (this.config.displaySymbol && this.config.customEvents.length > 0) {
|
||||
for (ev in this.config.customEvents) {
|
||||
if (typeof this.config.customEvents[ev].symbol !== "undefined" && this.config.customEvents[ev].symbol !== "") {
|
||||
needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||
if (needle.test(event.title)) {
|
||||
symbols[0] = this.config.customEvents[ev].symbol;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < symbols.length; i++) {
|
||||
@@ -230,6 +249,7 @@ Module.register("calendar", {
|
||||
}
|
||||
symbolWrapper.appendChild(symbol);
|
||||
}
|
||||
|
||||
eventWrapper.appendChild(symbolWrapper);
|
||||
} else if (this.config.timeFormat === "dateheaders") {
|
||||
var blankCell = document.createElement("td");
|
||||
@@ -251,6 +271,23 @@ Module.register("calendar", {
|
||||
}
|
||||
}
|
||||
|
||||
// Color events if custom color is specified
|
||||
if (this.config.customEvents.length > 0) {
|
||||
for (ev in this.config.customEvents) {
|
||||
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
|
||||
needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||
if (needle.test(event.title)) {
|
||||
eventWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||
titleWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||
if (this.config.displaySymbol) {
|
||||
symbolWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle;
|
||||
|
||||
var titleClass = this.titleClassForUrl(event.url);
|
||||
@@ -283,82 +320,56 @@ Module.register("calendar", {
|
||||
|
||||
eventWrapper.appendChild(titleWrapper);
|
||||
var now = new Date();
|
||||
// Define second, minute, hour, and day variables
|
||||
var oneSecond = 1000; // 1,000 milliseconds
|
||||
var oneMinute = oneSecond * 60;
|
||||
var oneHour = oneMinute * 60;
|
||||
var oneDay = oneHour * 24;
|
||||
if (event.fullDayEvent) {
|
||||
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
||||
event.endDate -= oneSecond;
|
||||
if (event.today) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
|
||||
} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
|
||||
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
} else {
|
||||
|
||||
if (this.config.timeFormat === "absolute") {
|
||||
// Use dateFormat
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||
// Add end time if showEnd
|
||||
if (this.config.showEnd) {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
||||
}
|
||||
// For full day events we use the fullDayEventDateFormat
|
||||
if (event.fullDayEvent) {
|
||||
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
||||
event.endDate -= oneSecond;
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
||||
}
|
||||
if (this.config.getRelative > 0 && event.startDate < now) {
|
||||
// Ongoing and getRelative is set
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
||||
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
||||
})
|
||||
);
|
||||
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * oneDay) {
|
||||
// Within urgency days
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
if (event.fullDayEvent && this.config.nextDaysRelative) {
|
||||
// Full days events within the next two days
|
||||
if (event.today) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
|
||||
} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
|
||||
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Show relative times
|
||||
if (event.startDate >= now) {
|
||||
// Use relative time
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
|
||||
if (event.startDate - now < this.config.getRelative * oneHour) {
|
||||
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
} else {
|
||||
/* Check to see if the user displays absolute or relative dates with their events
|
||||
* Also check to see if an event is happening within an 'urgency' time frameElement
|
||||
* For example, if the user set an .urgency of 7 days, those events that fall within that
|
||||
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
|
||||
*
|
||||
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim
|
||||
*/
|
||||
if (this.config.timeFormat === "absolute") {
|
||||
if (this.config.urgency > 1 && event.startDate - now < this.config.urgency * oneDay) {
|
||||
// This event falls within the config.urgency period that the user has set
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").from(moment().format("YYYYMMDD")));
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
||||
}
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").from(moment().format("YYYYMMDD")));
|
||||
}
|
||||
}
|
||||
if (this.config.showEnd) {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.fullDayEventDateFormat));
|
||||
}
|
||||
} else {
|
||||
if (event.startDate >= new Date()) {
|
||||
if (event.startDate - now < 2 * oneDay) {
|
||||
// This event is within the next 48 hours (2 days)
|
||||
if (event.startDate - now < this.config.getRelative * oneHour) {
|
||||
// If event is within 6 hour, display 'in xxx' time format or moment.fromNow()
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
} else {
|
||||
if (this.config.timeFormat === "absolute" && !this.config.nextDaysRelative) {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||
} else {
|
||||
// Otherwise just say 'Today/Tomorrow at such-n-such time'
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Check to see if the user displays absolute or relative dates with their events
|
||||
* Also check to see if an event is happening within an 'urgency' time frameElement
|
||||
* For example, if the user set an .urgency of 7 days, those events that fall within that
|
||||
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
|
||||
*
|
||||
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim
|
||||
*/
|
||||
if (this.config.timeFormat === "absolute") {
|
||||
if (this.config.urgency > 1 && event.startDate - now < this.config.urgency * oneDay) {
|
||||
// This event falls within the config.urgency period that the user has set
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||
}
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ongoing event
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
||||
@@ -366,12 +377,7 @@ Module.register("calendar", {
|
||||
})
|
||||
);
|
||||
}
|
||||
if (this.config.showEnd) {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
||||
}
|
||||
}
|
||||
//timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll');
|
||||
timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
}
|
||||
@@ -419,7 +425,7 @@ Module.register("calendar", {
|
||||
* it will a localeSpecification object with the system locale time format.
|
||||
*
|
||||
* @param {number} timeFormat Specifies either 12 or 24 hour time format
|
||||
* @returns {moment.LocaleSpecification}
|
||||
* @returns {moment.LocaleSpecification} formatted time
|
||||
*/
|
||||
getLocaleSpecification: function (timeFormat) {
|
||||
switch (timeFormat) {
|
||||
@@ -435,12 +441,11 @@ Module.register("calendar", {
|
||||
}
|
||||
},
|
||||
|
||||
/* hasCalendarURL(url)
|
||||
* Check if this config contains the calendar url.
|
||||
/**
|
||||
* Checks if this config contains the calendar url.
|
||||
*
|
||||
* argument url string - Url to look for.
|
||||
*
|
||||
* return bool - Has calendar url
|
||||
* @param {string} url The calendar url
|
||||
* @returns {boolean} True if the calendar config contains the url, False otherwise
|
||||
*/
|
||||
hasCalendarURL: function (url) {
|
||||
for (var c in this.config.calendars) {
|
||||
@@ -453,10 +458,10 @@ Module.register("calendar", {
|
||||
return false;
|
||||
},
|
||||
|
||||
/* createEventList()
|
||||
/**
|
||||
* Creates the sorted list of all events.
|
||||
*
|
||||
* return array - Array with events.
|
||||
* @returns {object[]} Array with events.
|
||||
*/
|
||||
createEventList: function () {
|
||||
var events = [];
|
||||
@@ -467,6 +472,7 @@ Module.register("calendar", {
|
||||
var calendar = this.calendarData[c];
|
||||
for (var e in calendar) {
|
||||
var event = JSON.parse(JSON.stringify(calendar[e])); // clone object
|
||||
|
||||
if (event.endDate < now) {
|
||||
continue;
|
||||
}
|
||||
@@ -524,6 +530,35 @@ Module.register("calendar", {
|
||||
events.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
// Limit the number of days displayed
|
||||
// If limitDays is set > 0, limit display to that number of days
|
||||
if (this.config.limitDays > 0) {
|
||||
var newEvents = [];
|
||||
var lastDate = today.clone().subtract(1, "days").format("YYYYMMDD");
|
||||
var days = 0;
|
||||
var eventDate;
|
||||
for (var ev of events) {
|
||||
eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
|
||||
// if date of event is later than lastdate
|
||||
// check if we already are showing max unique days
|
||||
if (eventDate > lastDate) {
|
||||
// if the only entry in the first day is a full day event that day is not counted as unique
|
||||
if (newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
|
||||
days--;
|
||||
}
|
||||
days++;
|
||||
if (days > this.config.limitDays) {
|
||||
continue;
|
||||
} else {
|
||||
lastDate = eventDate;
|
||||
}
|
||||
}
|
||||
newEvents.push(ev);
|
||||
}
|
||||
events = newEvents;
|
||||
}
|
||||
|
||||
return events.slice(0, this.config.maximumEntries);
|
||||
},
|
||||
|
||||
@@ -536,12 +571,16 @@ Module.register("calendar", {
|
||||
return false;
|
||||
},
|
||||
|
||||
/* createEventList(url)
|
||||
/**
|
||||
* Requests node helper to add calendar url.
|
||||
*
|
||||
* argument url string - Url to add.
|
||||
* @param {string} url The calendar url to add
|
||||
* @param {object} auth The authentication method and credentials
|
||||
* @param {object} calendarConfig The config of the specific calendar
|
||||
*/
|
||||
addCalendar: function (url, auth, calendarConfig) {
|
||||
var self = this;
|
||||
|
||||
this.sendSocketNotification("ADD_CALENDAR", {
|
||||
id: this.identifier,
|
||||
url: url,
|
||||
@@ -558,94 +597,100 @@ Module.register("calendar", {
|
||||
},
|
||||
|
||||
/**
|
||||
* symbolsForUrl(url)
|
||||
* Retrieves the symbols for a specific url.
|
||||
* Retrieves the symbols for a specific event.
|
||||
*
|
||||
* argument url string - Url to look for.
|
||||
*
|
||||
* return string/array - The Symbols
|
||||
* @param {object} event Event to look for.
|
||||
* @returns {string[]} The symbols
|
||||
*/
|
||||
symbolsForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "symbol", this.config.defaultSymbol);
|
||||
symbolsForEvent: function (event) {
|
||||
let symbols = this.getCalendarPropertyAsArray(event.url, "symbol", this.config.defaultSymbol);
|
||||
|
||||
if (event.recurringEvent === true && this.hasCalendarProperty(event.url, "recurringSymbol")) {
|
||||
symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "recurringSymbol", this.config.defaultSymbol), symbols);
|
||||
}
|
||||
|
||||
if (event.fullDayEvent === true && this.hasCalendarProperty(event.url, "fullDaySymbol")) {
|
||||
symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "fullDaySymbol", this.config.defaultSymbol), symbols);
|
||||
}
|
||||
|
||||
return symbols;
|
||||
},
|
||||
|
||||
mergeUnique: function (arr1, arr2) {
|
||||
return arr1.concat(
|
||||
arr2.filter(function (item) {
|
||||
return arr1.indexOf(item) === -1;
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* symbolClassForUrl(url)
|
||||
* Retrieves the symbolClass for a specific url.
|
||||
* Retrieves the symbolClass for a specific calendar url.
|
||||
*
|
||||
* @param url string - Url to look for.
|
||||
*
|
||||
* @returns string
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The class to be used for the symbols of the calendar
|
||||
*/
|
||||
symbolClassForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "symbolClass", "");
|
||||
},
|
||||
|
||||
/**
|
||||
* titleClassForUrl(url)
|
||||
* Retrieves the titleClass for a specific url.
|
||||
* Retrieves the titleClass for a specific calendar url.
|
||||
*
|
||||
* @param url string - Url to look for.
|
||||
*
|
||||
* @returns string
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The class to be used for the title of the calendar
|
||||
*/
|
||||
titleClassForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "titleClass", "");
|
||||
},
|
||||
|
||||
/**
|
||||
* timeClassForUrl(url)
|
||||
* Retrieves the timeClass for a specific url.
|
||||
* Retrieves the timeClass for a specific calendar url.
|
||||
*
|
||||
* @param url string - Url to look for.
|
||||
*
|
||||
* @returns string
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The class to be used for the time of the calendar
|
||||
*/
|
||||
timeClassForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "timeClass", "");
|
||||
},
|
||||
|
||||
/* calendarNameForUrl(url)
|
||||
* Retrieves the calendar name for a specific url.
|
||||
/**
|
||||
* Retrieves the calendar name for a specific calendar url.
|
||||
*
|
||||
* argument url string - Url to look for.
|
||||
*
|
||||
* return string - The name of the calendar
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The name of the calendar
|
||||
*/
|
||||
calendarNameForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "name", "");
|
||||
},
|
||||
|
||||
/* colorForUrl(url)
|
||||
* Retrieves the color for a specific url.
|
||||
/**
|
||||
* Retrieves the color for a specific calendar url.
|
||||
*
|
||||
* argument url string - Url to look for.
|
||||
*
|
||||
* return string - The Color
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The color
|
||||
*/
|
||||
colorForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "color", "#fff");
|
||||
},
|
||||
|
||||
/* countTitleForUrl(url)
|
||||
* Retrieves the name for a specific url.
|
||||
/**
|
||||
* Retrieves the count title for a specific calendar url.
|
||||
*
|
||||
* argument url string - Url to look for.
|
||||
*
|
||||
* return string - The Symbol
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The title
|
||||
*/
|
||||
countTitleForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "repeatingCountTitle", this.config.defaultRepeatingCountTitle);
|
||||
},
|
||||
|
||||
/* getCalendarProperty(url, property, defaultValue)
|
||||
* Helper method to retrieve the property for a specific url.
|
||||
/**
|
||||
* Helper method to retrieve the property for a specific calendar url.
|
||||
*
|
||||
* argument url string - Url to look for.
|
||||
* argument property string - Property to look for.
|
||||
* argument defaultValue string - Value if property is not found.
|
||||
*
|
||||
* return string - The Property
|
||||
* @param {string} url The calendar url
|
||||
* @param {string} property The property to look for
|
||||
* @param {string} defaultValue The value if the property is not found
|
||||
* @returns {*} The property
|
||||
*/
|
||||
getCalendarProperty: function (url, property, defaultValue) {
|
||||
for (var c in this.config.calendars) {
|
||||
@@ -658,6 +703,16 @@ Module.register("calendar", {
|
||||
return defaultValue;
|
||||
},
|
||||
|
||||
getCalendarPropertyAsArray: function (url, property, defaultValue) {
|
||||
let p = this.getCalendarProperty(url, property, defaultValue);
|
||||
if (!(p instanceof Array)) p = [p];
|
||||
return p;
|
||||
},
|
||||
|
||||
hasCalendarProperty: function (url, property) {
|
||||
return !!this.getCalendarProperty(url, property, undefined);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shortens a string if it's longer than maxLength and add a ellipsis to the end
|
||||
*
|
||||
@@ -711,22 +766,27 @@ Module.register("calendar", {
|
||||
}
|
||||
},
|
||||
|
||||
/* capFirst(string)
|
||||
/**
|
||||
* Capitalize the first letter of a string
|
||||
* Return capitalized string
|
||||
*
|
||||
* @param {string} string The string to capitalize
|
||||
* @returns {string} The capitalized string
|
||||
*/
|
||||
capFirst: function (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
},
|
||||
|
||||
/* titleTransform(title)
|
||||
/**
|
||||
* Transforms the title of an event for usage.
|
||||
* Replaces parts of the text as defined in config.titleReplace.
|
||||
* Shortens title based on config.maxTitleLength and config.wrapEvents
|
||||
*
|
||||
* argument title string - The title to transform.
|
||||
*
|
||||
* return string - The transformed title.
|
||||
* @param {string} title The title to transform.
|
||||
* @param {object} titleReplace Pairs of strings to be replaced in the title
|
||||
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
|
||||
* @param {number} maxTitleLength The max length of the string
|
||||
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
|
||||
* @returns {string} The transformed title.
|
||||
*/
|
||||
titleTransform: function (title, titleReplace, wrapEvents, maxTitleLength, maxTitleLines) {
|
||||
for (var needle in titleReplace) {
|
||||
@@ -745,7 +805,7 @@ Module.register("calendar", {
|
||||
return title;
|
||||
},
|
||||
|
||||
/* broadcastEvents()
|
||||
/**
|
||||
* Broadcasts the events to all other modules for reuse.
|
||||
* The all events available in one array, sorted on startdate.
|
||||
*/
|
||||
@@ -755,7 +815,7 @@ Module.register("calendar", {
|
||||
var calendar = this.calendarData[url];
|
||||
for (var e in calendar) {
|
||||
var event = cloneObject(calendar[e]);
|
||||
event.symbol = this.symbolsForUrl(url);
|
||||
event.symbol = this.symbolsForEvent(event);
|
||||
event.calendarName = this.calendarNameForUrl(url);
|
||||
event.color = this.colorForUrl(url);
|
||||
delete event.url;
|
||||
|
@@ -5,11 +5,29 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Log = require("../../../js/logger.js");
|
||||
const ical = require("ical");
|
||||
const moment = require("moment");
|
||||
const ical = require("node-ical");
|
||||
const request = require("request");
|
||||
|
||||
const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNumberOfDays, auth, includePastEvents) {
|
||||
/**
|
||||
* Moment date
|
||||
*
|
||||
* @external Moment
|
||||
* @see {@link http://momentjs.com}
|
||||
*/
|
||||
const moment = require("moment");
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url The url of the calendar to fetch
|
||||
* @param {number} reloadInterval Time in ms the calendar is fetched again
|
||||
* @param {string[]} excludedEvents An array of words / phrases from event titles that will be excluded from being shown.
|
||||
* @param {number} maximumEntries The maximum number of events fetched.
|
||||
* @param {number} maximumNumberOfDays The maximum number of days an event should be in the future.
|
||||
* @param {object} auth The object containing options for authentication against the calendar.
|
||||
* @param {boolean} includePastEvents If true events from the past maximumNumberOfDays will be fetched too
|
||||
* @class
|
||||
*/
|
||||
const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents) {
|
||||
const self = this;
|
||||
|
||||
let reloadTimer = null;
|
||||
@@ -18,7 +36,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
let fetchFailedCallback = function () {};
|
||||
let eventsReceivedCallback = function () {};
|
||||
|
||||
/* fetchCalendar()
|
||||
/**
|
||||
* Initiates calendar fetch.
|
||||
*/
|
||||
const fetchCalendar = function () {
|
||||
@@ -58,7 +76,18 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
return;
|
||||
}
|
||||
|
||||
const data = ical.parseICS(requestData);
|
||||
let data = [];
|
||||
|
||||
try {
|
||||
data = ical.parseICS(requestData);
|
||||
} catch (error) {
|
||||
fetchFailedCallback(self, error.message);
|
||||
scheduleTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
Log.debug(" parsed data=" + JSON.stringify(data));
|
||||
|
||||
const newEvents = [];
|
||||
|
||||
// limitFunction doesn't do much limiting, see comment re: the dates array in rrule section below as to why we need to do the filtering ourselves
|
||||
@@ -67,15 +96,15 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
};
|
||||
|
||||
const eventDate = function (event, time) {
|
||||
return event[time].length === 8 ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||
return isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||
};
|
||||
|
||||
Log.debug("there are " + Object.entries(data).length + " calendar entries");
|
||||
Object.entries(data).forEach(([key, event]) => {
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day").toDate();
|
||||
const future = moment().startOf("day").add(maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||
let past = today;
|
||||
|
||||
Log.debug("have entries ");
|
||||
if (includePastEvents) {
|
||||
past = moment().startOf("day").subtract(maximumNumberOfDays, "days").toDate();
|
||||
}
|
||||
@@ -93,18 +122,22 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
let startDate = eventDate(event, "start");
|
||||
let endDate;
|
||||
|
||||
Log.debug("\nevent=" + JSON.stringify(event));
|
||||
if (typeof event.end !== "undefined") {
|
||||
endDate = eventDate(event, "end");
|
||||
} else if (typeof event.duration !== "undefined") {
|
||||
endDate = startDate.clone().add(moment.duration(event.duration));
|
||||
} else {
|
||||
if (!isFacebookBirthday) {
|
||||
endDate = startDate;
|
||||
// make copy of start date, separate storage area
|
||||
endDate = moment(startDate.format("x"), "x");
|
||||
} else {
|
||||
endDate = moment(startDate).add(1, "days");
|
||||
}
|
||||
}
|
||||
|
||||
Log.debug(" start=" + startDate.toDate() + " end=" + endDate.toDate());
|
||||
|
||||
// calculate the duration of the event for use with recurring events.
|
||||
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
|
||||
@@ -184,13 +217,26 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
// For recurring events, get the set of start dates that fall within the range
|
||||
// of dates we're looking for.
|
||||
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
|
||||
const pastLocal = pastMoment.subtract(past.getTimezoneOffset(), "minutes").toDate();
|
||||
const futureLocal = futureMoment.subtract(future.getTimezoneOffset(), "minutes").toDate();
|
||||
const datesLocal = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
const dates = datesLocal.map(function (dateLocal) {
|
||||
return moment(dateLocal).add(dateLocal.getTimezoneOffset(), "minutes").toDate();
|
||||
});
|
||||
|
||||
let pastLocal = 0;
|
||||
let futureLocal = 0;
|
||||
if (isFullDayEvent(event)) {
|
||||
// if full day event, only use the date part of the ranges
|
||||
pastLocal = pastMoment.toDate();
|
||||
futureLocal = futureMoment.toDate();
|
||||
} else {
|
||||
// if we want past events
|
||||
if (includePastEvents) {
|
||||
// use the calculated past time for the between from
|
||||
pastLocal = pastMoment.toDate();
|
||||
} else {
|
||||
// otherwise use NOW.. cause we shouldnt use any before now
|
||||
pastLocal = moment().toDate(); //now
|
||||
}
|
||||
futureLocal = futureMoment.toDate(); // future
|
||||
}
|
||||
Log.debug(" between=" + pastLocal + " to " + futureLocal);
|
||||
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
Log.debug("title=" + event.summary + " dates=" + JSON.stringify(dates));
|
||||
// The "dates" array contains the set of dates within our desired date range range that are valid
|
||||
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
|
||||
// had its date changed from outside the range to inside the range. For the time being,
|
||||
@@ -207,10 +253,9 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through the set of date entries to see which recurrences should be added to our event list.
|
||||
for (let d in dates) {
|
||||
const date = dates[d];
|
||||
let date = dates[d];
|
||||
// ical.js started returning recurrences and exdates as ISOStrings without time information.
|
||||
// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same
|
||||
// (see https://github.com/peterbraden/ical.js/pull/84 )
|
||||
@@ -218,8 +263,29 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
let curEvent = event;
|
||||
let showRecurrence = true;
|
||||
|
||||
// for full day events, the time might be off from RRULE/Luxon problem
|
||||
if (isFullDayEvent(event)) {
|
||||
Log.debug("fullday");
|
||||
// if the offset is negative, east of GMT where the problem is
|
||||
if (date.getTimezoneOffset() < 0) {
|
||||
// get the offset of today where we are processing
|
||||
// this will be the correction we need to apply
|
||||
let nowOffset = new Date().getTimezoneOffset();
|
||||
Log.debug("now offset is " + nowOffset);
|
||||
// reduce the time by the offset
|
||||
Log.debug(" recurring date is " + date + " offset is " + date.getTimezoneOffset());
|
||||
// apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date is " + date);
|
||||
}
|
||||
}
|
||||
startDate = moment(date);
|
||||
|
||||
let adjustDays = getCorrection(event, date);
|
||||
|
||||
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
|
||||
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
|
||||
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||
@@ -232,6 +298,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||
showRecurrence = false;
|
||||
}
|
||||
Log.debug("duration=" + duration);
|
||||
|
||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
||||
if (startDate.format("x") === endDate.format("x")) {
|
||||
@@ -251,12 +318,14 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
}
|
||||
|
||||
if (showRecurrence === true) {
|
||||
Log.debug("saving event =" + description);
|
||||
addedEvents++;
|
||||
newEvents.push({
|
||||
title: recurrenceTitle,
|
||||
startDate: startDate.format("x"),
|
||||
endDate: endDate.format("x"),
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
fullDayEvent: isFullDayEvent(event),
|
||||
recurringEvent: true,
|
||||
class: event.class,
|
||||
firstYear: event.start.getFullYear(),
|
||||
location: location,
|
||||
@@ -269,6 +338,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
} else {
|
||||
// Single event.
|
||||
const fullDayEvent = isFacebookBirthday ? true : isFullDayEvent(event);
|
||||
// Log.debug("full day event")
|
||||
|
||||
if (includePastEvents) {
|
||||
// Past event is too far in the past, so skip.
|
||||
@@ -300,12 +370,17 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
if (fullDayEvent && startDate <= today) {
|
||||
startDate = moment(today);
|
||||
}
|
||||
|
||||
// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
|
||||
if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
}
|
||||
// get correction for date saving and dst change between now and then
|
||||
let adjustDays = getCorrection(event, startDate.toDate());
|
||||
// Every thing is good. Add it to the list.
|
||||
newEvents.push({
|
||||
title: title,
|
||||
startDate: startDate.format("x"),
|
||||
endDate: endDate.format("x"),
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
fullDayEvent: fullDayEvent,
|
||||
class: event.class,
|
||||
location: location,
|
||||
@@ -320,14 +395,139 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
events = newEvents;
|
||||
// include up to maximumEntries current or upcoming events
|
||||
// If past events should be included, include all past events
|
||||
const now = moment();
|
||||
var entries = 0;
|
||||
events = [];
|
||||
for (let ne of newEvents) {
|
||||
if (moment(ne.endDate, "x").isBefore(now)) {
|
||||
if (includePastEvents) events.push(ne);
|
||||
continue;
|
||||
}
|
||||
entries++;
|
||||
// If max events has been saved, skip the rest
|
||||
if (entries > maximumEntries) break;
|
||||
events.push(ne);
|
||||
}
|
||||
|
||||
self.broadcastEvents();
|
||||
scheduleTimer();
|
||||
});
|
||||
};
|
||||
|
||||
/* scheduleTimer()
|
||||
/*
|
||||
*
|
||||
* get the time correction, either dst/std or full day in cases where utc time is day before plus offset
|
||||
*
|
||||
*/
|
||||
const getCorrection = function (event, date) {
|
||||
let adjustHours = 0;
|
||||
// if a timezone was specified
|
||||
if (!event.start.tz) {
|
||||
Log.debug(" if no tz, guess based on now");
|
||||
event.start.tz = moment.tz.guess();
|
||||
}
|
||||
Log.debug("initial tz=" + event.start.tz);
|
||||
|
||||
// if there is a start date specified
|
||||
if (event.start.tz) {
|
||||
// if this is a windows timezone
|
||||
if (event.start.tz.includes(" ")) {
|
||||
// use the lookup table to get theIANA name as moment and date don't know MS timezones
|
||||
let tz = getIanaTZFromMS(event.start.tz);
|
||||
Log.debug("corrected TZ=" + tz);
|
||||
// watch out for unregistered windows timezone names
|
||||
// if we had a successfule lookup
|
||||
if (tz) {
|
||||
// change the timezone to the IANA name
|
||||
event.start.tz = tz;
|
||||
// Log.debug("corrected timezone="+event.start.tz)
|
||||
}
|
||||
}
|
||||
Log.debug("corrected tz=" + event.start.tz);
|
||||
let current_offset = 0; // offset from TZ string or calculated
|
||||
let mm = 0; // date with tz or offset
|
||||
let start_offset = 0; // utc offset of created with tz
|
||||
// if there is still an offset, lookup failed, use it
|
||||
if (event.start.tz.startsWith("(")) {
|
||||
const regex = /[+|-]\d*:\d*/;
|
||||
const start_offsetString = event.start.tz.match(regex).toString().split(":");
|
||||
let start_offset = parseInt(start_offsetString[0]);
|
||||
start_offset *= event.start.tz[1] === "-" ? -1 : 1;
|
||||
adjustHours = start_offset;
|
||||
Log.debug("defined offset=" + start_offset + " hours");
|
||||
current_offset = start_offset;
|
||||
event.start.tz = "";
|
||||
Log.debug("ical offset=" + current_offset + " date=" + date);
|
||||
mm = moment(date);
|
||||
let x = parseInt(moment(new Date()).utcOffset());
|
||||
Log.debug("net mins=" + (current_offset * 60 - x));
|
||||
|
||||
mm = mm.add(x - current_offset * 60, "minutes");
|
||||
adjustHours = (current_offset * 60 - x) / 60;
|
||||
event.start = mm.toDate();
|
||||
Log.debug("adjusted date=" + event.start);
|
||||
} else {
|
||||
// get the start time in that timezone
|
||||
Log.debug("start date/time=" + moment(event.start).toDate());
|
||||
start_offset = moment.tz(moment(event.start), event.start.tz).utcOffset();
|
||||
Log.debug("start offset=" + start_offset);
|
||||
|
||||
Log.debug("start date/time w tz =" + moment.tz(moment(event.start), event.start.tz).toDate());
|
||||
|
||||
// get the specified date in that timezone
|
||||
mm = moment.tz(moment(date), event.start.tz);
|
||||
Log.debug("event date=" + mm.toDate());
|
||||
current_offset = mm.utcOffset();
|
||||
}
|
||||
Log.debug("event offset=" + current_offset + " hour=" + mm.format("H") + " event date=" + mm.toDate());
|
||||
|
||||
// if the offset is greater than 0, east of london
|
||||
if (current_offset !== start_offset) {
|
||||
// big offset
|
||||
Log.debug("offset");
|
||||
let h = parseInt(mm.format("H"));
|
||||
// check if the event time is less than the offset
|
||||
if (h > 0 && h < Math.abs(current_offset) / 60) {
|
||||
// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
|
||||
// we need to fix that
|
||||
adjustHours = 24;
|
||||
// Log.debug("adjusting date")
|
||||
}
|
||||
//-300 > -240
|
||||
//if (Math.abs(current_offset) > Math.abs(start_offset)){
|
||||
if (current_offset > start_offset) {
|
||||
adjustHours -= 1;
|
||||
Log.debug("adjust down 1 hour dst change");
|
||||
//} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
|
||||
} else if (current_offset < start_offset) {
|
||||
adjustHours += 1;
|
||||
Log.debug("adjust up 1 hour dst change");
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.debug("adjustHours=" + adjustHours);
|
||||
return adjustHours;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* lookup iana tz from windows
|
||||
*/
|
||||
let zoneTable = null;
|
||||
const getIanaTZFromMS = function (msTZName) {
|
||||
if (!zoneTable) {
|
||||
const p = require("path");
|
||||
zoneTable = require(p.join(__dirname, "windowsZones.json"));
|
||||
}
|
||||
// Get hash entry
|
||||
const he = zoneTable[msTZName];
|
||||
// If found return iana name, else null
|
||||
return he ? he.iana[0] : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Schedule the timer for the next update.
|
||||
*/
|
||||
const scheduleTimer = function () {
|
||||
@@ -337,15 +537,14 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
}, reloadInterval);
|
||||
};
|
||||
|
||||
/* isFullDayEvent(event)
|
||||
/**
|
||||
* Checks if an event is a fullday event.
|
||||
*
|
||||
* argument event object - The event object to check.
|
||||
*
|
||||
* return bool - The event is a fullday event.
|
||||
* @param {object} event The event object to check.
|
||||
* @returns {boolean} True if the event is a fullday event, false otherwise
|
||||
*/
|
||||
const isFullDayEvent = function (event) {
|
||||
if (event.start.length === 8 || event.start.dateOnly) {
|
||||
if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -360,14 +559,13 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
return false;
|
||||
};
|
||||
|
||||
/* timeFilterApplies()
|
||||
/**
|
||||
* Determines if the user defined time filter should apply
|
||||
*
|
||||
* argument now Date - Date object using previously created object for consistency
|
||||
* argument endDate Moment - Moment object representing the event end date
|
||||
* argument filter string - The time to subtract from the end date to determine if an event should be shown
|
||||
*
|
||||
* return bool - The event should be filtered out
|
||||
* @param {Date} now Date object using previously created object for consistency
|
||||
* @param {Moment} endDate Moment object representing the event end date
|
||||
* @param {string} filter The time to subtract from the end date to determine if an event should be shown
|
||||
* @returns {boolean} True if the event should be filtered out, false otherwise
|
||||
*/
|
||||
const timeFilterApplies = function (now, endDate, filter) {
|
||||
if (filter) {
|
||||
@@ -382,12 +580,11 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
return false;
|
||||
};
|
||||
|
||||
/* getTitleFromEvent(event)
|
||||
/**
|
||||
* Gets the title from the event.
|
||||
*
|
||||
* argument event object - The event object to check.
|
||||
*
|
||||
* return string - The title of the event, or "Event" if no title is found.
|
||||
* @param {object} event The event object to check.
|
||||
* @returns {string} The title of the event, or "Event" if no title is found.
|
||||
*/
|
||||
const getTitleFromEvent = function (event) {
|
||||
let title = "Event";
|
||||
@@ -418,14 +615,14 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
|
||||
/* public methods */
|
||||
|
||||
/* startFetch()
|
||||
/**
|
||||
* Initiate fetchCalendar();
|
||||
*/
|
||||
this.startFetch = function () {
|
||||
fetchCalendar();
|
||||
};
|
||||
|
||||
/* broadcastItems()
|
||||
/**
|
||||
* Broadcast the existing events.
|
||||
*/
|
||||
this.broadcastEvents = function () {
|
||||
@@ -433,37 +630,37 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
eventsReceivedCallback(self);
|
||||
};
|
||||
|
||||
/* onReceive(callback)
|
||||
/**
|
||||
* Sets the on success callback
|
||||
*
|
||||
* argument callback function - The on success callback.
|
||||
* @param {Function} callback The on success callback.
|
||||
*/
|
||||
this.onReceive = function (callback) {
|
||||
eventsReceivedCallback = callback;
|
||||
};
|
||||
|
||||
/* onError(callback)
|
||||
/**
|
||||
* Sets the on error callback
|
||||
*
|
||||
* argument callback function - The on error callback.
|
||||
* @param {Function} callback The on error callback.
|
||||
*/
|
||||
this.onError = function (callback) {
|
||||
fetchFailedCallback = callback;
|
||||
};
|
||||
|
||||
/* url()
|
||||
/**
|
||||
* Returns the url of this fetcher.
|
||||
*
|
||||
* return string - The url of this fetcher.
|
||||
* @returns {string} The url of this fetcher.
|
||||
*/
|
||||
this.url = function () {
|
||||
return url;
|
||||
};
|
||||
|
||||
/* events()
|
||||
/**
|
||||
* Returns current available events for this fetcher.
|
||||
*
|
||||
* return array - The current available events for this fetcher.
|
||||
* @returns {object[]} The current available events for this fetcher.
|
||||
*/
|
||||
this.events = function () {
|
||||
return events;
|
||||
|
@@ -4,7 +4,6 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
const NodeHelper = require("node_helper");
|
||||
const validUrl = require("valid-url");
|
||||
const CalendarFetcher = require("./calendarfetcher.js");
|
||||
@@ -20,18 +19,24 @@ module.exports = NodeHelper.create({
|
||||
// Override socketNotificationReceived method.
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
if (notification === "ADD_CALENDAR") {
|
||||
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.id);
|
||||
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.id);
|
||||
}
|
||||
},
|
||||
|
||||
/* createFetcher(url, reloadInterval)
|
||||
/**
|
||||
* Creates a fetcher for a new url if it doesn't exist yet.
|
||||
* Otherwise it reuses the existing one.
|
||||
*
|
||||
* attribute url string - URL of the news feed.
|
||||
* attribute reloadInterval number - Reload interval in milliseconds.
|
||||
* @param {string} url The url of the calendar
|
||||
* @param {number} fetchInterval How often does the calendar needs to be fetched in ms
|
||||
* @param {string[]} excludedEvents An array of words / phrases from event titles that will be excluded from being shown.
|
||||
* @param {number} maximumEntries The maximum number of events fetched.
|
||||
* @param {number} maximumNumberOfDays The maximum number of days an event should be in the future.
|
||||
* @param {object} auth The object containing options for authentication against the calendar.
|
||||
* @param {boolean} broadcastPastEvents If true events from the past maximumNumberOfDays will be included in event broadcasts
|
||||
* @param {string} identifier ID of the module
|
||||
*/
|
||||
createFetcher: function (url, fetchInterval, excludedEvents, maximumNumberOfDays, auth, broadcastPastEvents, identifier) {
|
||||
createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, identifier) {
|
||||
var self = this;
|
||||
|
||||
if (!validUrl.isUri(url)) {
|
||||
@@ -42,7 +47,7 @@ module.exports = NodeHelper.create({
|
||||
var fetcher;
|
||||
if (typeof self.fetchers[identifier + url] === "undefined") {
|
||||
Log.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumNumberOfDays, auth, broadcastPastEvents);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents);
|
||||
|
||||
fetcher.onReceive(function (fetcher) {
|
||||
self.sendSocketNotification("CALENDAR_EVENTS", {
|
||||
@@ -65,7 +70,6 @@ module.exports = NodeHelper.create({
|
||||
} else {
|
||||
Log.log("Use existing calendar fetcher for url: " + url);
|
||||
fetcher = self.fetchers[identifier + url];
|
||||
fetcher.broadcastEvents();
|
||||
}
|
||||
|
||||
fetcher.startFetch();
|
||||
|
237
modules/default/calendar/windowsZones.json
Normal file
@@ -0,0 +1,237 @@
|
||||
{
|
||||
"Dateline Standard Time": { "iana": ["Etc/GMT+12"] },
|
||||
"UTC-11": { "iana": ["Etc/GMT+11"] },
|
||||
"Aleutian Standard Time": { "iana": ["America/Adak"] },
|
||||
"Hawaiian Standard Time": { "iana": ["Pacific/Honolulu"] },
|
||||
"Marquesas Standard Time": { "iana": ["Pacific/Marquesas"] },
|
||||
"Alaskan Standard Time": { "iana": ["America/Anchorage"] },
|
||||
"UTC-09": { "iana": ["Etc/GMT+9"] },
|
||||
"Pacific Standard Time (Mexico)": { "iana": ["America/Tijuana"] },
|
||||
"UTC-08": { "iana": ["Etc/GMT+8"] },
|
||||
"Pacific Standard Time": { "iana": ["America/Los_Angeles"] },
|
||||
"US Mountain Standard Time": { "iana": ["America/Phoenix"] },
|
||||
"Mountain Standard Time (Mexico)": { "iana": ["America/Chihuahua"] },
|
||||
"Mountain Standard Time": { "iana": ["America/Denver"] },
|
||||
"Central America Standard Time": { "iana": ["America/Guatemala"] },
|
||||
"Central Standard Time": { "iana": ["America/Chicago"] },
|
||||
"Easter Island Standard Time": { "iana": ["Pacific/Easter"] },
|
||||
"Central Standard Time (Mexico)": { "iana": ["America/Mexico_City"] },
|
||||
"Canada Central Standard Time": { "iana": ["America/Regina"] },
|
||||
"SA Pacific Standard Time": { "iana": ["America/Bogota"] },
|
||||
"Eastern Standard Time (Mexico)": { "iana": ["America/Cancun"] },
|
||||
"Eastern Standard Time": { "iana": ["America/New_York"] },
|
||||
"Haiti Standard Time": { "iana": ["America/Port-au-Prince"] },
|
||||
"Cuba Standard Time": { "iana": ["America/Havana"] },
|
||||
"US Eastern Standard Time": { "iana": ["America/Indianapolis"] },
|
||||
"Turks And Caicos Standard Time": { "iana": ["America/Grand_Turk"] },
|
||||
"Paraguay Standard Time": { "iana": ["America/Asuncion"] },
|
||||
"Atlantic Standard Time": { "iana": ["America/Halifax"] },
|
||||
"Venezuela Standard Time": { "iana": ["America/Caracas"] },
|
||||
"Central Brazilian Standard Time": { "iana": ["America/Cuiaba"] },
|
||||
"SA Western Standard Time": { "iana": ["America/La_Paz"] },
|
||||
"Pacific SA Standard Time": { "iana": ["America/Santiago"] },
|
||||
"Newfoundland Standard Time": { "iana": ["America/St_Johns"] },
|
||||
"Tocantins Standard Time": { "iana": ["America/Araguaina"] },
|
||||
"E. South America Standard Time": { "iana": ["America/Sao_Paulo"] },
|
||||
"SA Eastern Standard Time": { "iana": ["America/Cayenne"] },
|
||||
"Argentina Standard Time": { "iana": ["America/Buenos_Aires"] },
|
||||
"Greenland Standard Time": { "iana": ["America/Godthab"] },
|
||||
"Montevideo Standard Time": { "iana": ["America/Montevideo"] },
|
||||
"Magallanes Standard Time": { "iana": ["America/Punta_Arenas"] },
|
||||
"Saint Pierre Standard Time": { "iana": ["America/Miquelon"] },
|
||||
"Bahia Standard Time": { "iana": ["America/Bahia"] },
|
||||
"UTC-02": { "iana": ["Etc/GMT+2"] },
|
||||
"Azores Standard Time": { "iana": ["Atlantic/Azores"] },
|
||||
"Cape Verde Standard Time": { "iana": ["Atlantic/Cape_Verde"] },
|
||||
"UTC": { "iana": ["Etc/GMT"] },
|
||||
"GMT Standard Time": { "iana": ["Europe/London"] },
|
||||
"Greenwich Standard Time": { "iana": ["Atlantic/Reykjavik"] },
|
||||
"Sao Tome Standard Time": { "iana": ["Africa/Sao_Tome"] },
|
||||
"Morocco Standard Time": { "iana": ["Africa/Casablanca"] },
|
||||
"W. Europe Standard Time": { "iana": ["Europe/Berlin"] },
|
||||
"Central Europe Standard Time": { "iana": ["Europe/Budapest"] },
|
||||
"Romance Standard Time": { "iana": ["Europe/Paris"] },
|
||||
"Central European Standard Time": { "iana": ["Europe/Warsaw"] },
|
||||
"W. Central Africa Standard Time": { "iana": ["Africa/Lagos"] },
|
||||
"Jordan Standard Time": { "iana": ["Asia/Amman"] },
|
||||
"GTB Standard Time": { "iana": ["Europe/Bucharest"] },
|
||||
"Middle East Standard Time": { "iana": ["Asia/Beirut"] },
|
||||
"Egypt Standard Time": { "iana": ["Africa/Cairo"] },
|
||||
"E. Europe Standard Time": { "iana": ["Europe/Chisinau"] },
|
||||
"Syria Standard Time": { "iana": ["Asia/Damascus"] },
|
||||
"West Bank Standard Time": { "iana": ["Asia/Hebron"] },
|
||||
"South Africa Standard Time": { "iana": ["Africa/Johannesburg"] },
|
||||
"FLE Standard Time": { "iana": ["Europe/Kiev"] },
|
||||
"Israel Standard Time": { "iana": ["Asia/Jerusalem"] },
|
||||
"Kaliningrad Standard Time": { "iana": ["Europe/Kaliningrad"] },
|
||||
"Sudan Standard Time": { "iana": ["Africa/Khartoum"] },
|
||||
"Libya Standard Time": { "iana": ["Africa/Tripoli"] },
|
||||
"Namibia Standard Time": { "iana": ["Africa/Windhoek"] },
|
||||
"Arabic Standard Time": { "iana": ["Asia/Baghdad"] },
|
||||
"Turkey Standard Time": { "iana": ["Europe/Istanbul"] },
|
||||
"Arab Standard Time": { "iana": ["Asia/Riyadh"] },
|
||||
"Belarus Standard Time": { "iana": ["Europe/Minsk"] },
|
||||
"Russian Standard Time": { "iana": ["Europe/Moscow"] },
|
||||
"E. Africa Standard Time": { "iana": ["Africa/Nairobi"] },
|
||||
"Iran Standard Time": { "iana": ["Asia/Tehran"] },
|
||||
"Arabian Standard Time": { "iana": ["Asia/Dubai"] },
|
||||
"Astrakhan Standard Time": { "iana": ["Europe/Astrakhan"] },
|
||||
"Azerbaijan Standard Time": { "iana": ["Asia/Baku"] },
|
||||
"Russia Time Zone 3": { "iana": ["Europe/Samara"] },
|
||||
"Mauritius Standard Time": { "iana": ["Indian/Mauritius"] },
|
||||
"Saratov Standard Time": { "iana": ["Europe/Saratov"] },
|
||||
"Georgian Standard Time": { "iana": ["Asia/Tbilisi"] },
|
||||
"Volgograd Standard Time": { "iana": ["Europe/Volgograd"] },
|
||||
"Caucasus Standard Time": { "iana": ["Asia/Yerevan"] },
|
||||
"Afghanistan Standard Time": { "iana": ["Asia/Kabul"] },
|
||||
"West Asia Standard Time": { "iana": ["Asia/Tashkent"] },
|
||||
"Ekaterinburg Standard Time": { "iana": ["Asia/Yekaterinburg"] },
|
||||
"Pakistan Standard Time": { "iana": ["Asia/Karachi"] },
|
||||
"Qyzylorda Standard Time": { "iana": ["Asia/Qyzylorda"] },
|
||||
"India Standard Time": { "iana": ["Asia/Calcutta"] },
|
||||
"Sri Lanka Standard Time": { "iana": ["Asia/Colombo"] },
|
||||
"Nepal Standard Time": { "iana": ["Asia/Katmandu"] },
|
||||
"Central Asia Standard Time": { "iana": ["Asia/Almaty"] },
|
||||
"Bangladesh Standard Time": { "iana": ["Asia/Dhaka"] },
|
||||
"Omsk Standard Time": { "iana": ["Asia/Omsk"] },
|
||||
"Myanmar Standard Time": { "iana": ["Asia/Rangoon"] },
|
||||
"SE Asia Standard Time": { "iana": ["Asia/Bangkok"] },
|
||||
"Altai Standard Time": { "iana": ["Asia/Barnaul"] },
|
||||
"W. Mongolia Standard Time": { "iana": ["Asia/Hovd"] },
|
||||
"North Asia Standard Time": { "iana": ["Asia/Krasnoyarsk"] },
|
||||
"N. Central Asia Standard Time": { "iana": ["Asia/Novosibirsk"] },
|
||||
"Tomsk Standard Time": { "iana": ["Asia/Tomsk"] },
|
||||
"China Standard Time": { "iana": ["Asia/Shanghai"] },
|
||||
"North Asia East Standard Time": { "iana": ["Asia/Irkutsk"] },
|
||||
"Singapore Standard Time": { "iana": ["Asia/Singapore"] },
|
||||
"W. Australia Standard Time": { "iana": ["Australia/Perth"] },
|
||||
"Taipei Standard Time": { "iana": ["Asia/Taipei"] },
|
||||
"Ulaanbaatar Standard Time": { "iana": ["Asia/Ulaanbaatar"] },
|
||||
"Aus Central W. Standard Time": { "iana": ["Australia/Eucla"] },
|
||||
"Transbaikal Standard Time": { "iana": ["Asia/Chita"] },
|
||||
"Tokyo Standard Time": { "iana": ["Asia/Tokyo"] },
|
||||
"North Korea Standard Time": { "iana": ["Asia/Pyongyang"] },
|
||||
"Korea Standard Time": { "iana": ["Asia/Seoul"] },
|
||||
"Yakutsk Standard Time": { "iana": ["Asia/Yakutsk"] },
|
||||
"Cen. Australia Standard Time": { "iana": ["Australia/Adelaide"] },
|
||||
"AUS Central Standard Time": { "iana": ["Australia/Darwin"] },
|
||||
"E. Australia Standard Time": { "iana": ["Australia/Brisbane"] },
|
||||
"AUS Eastern Standard Time": { "iana": ["Australia/Sydney"] },
|
||||
"West Pacific Standard Time": { "iana": ["Pacific/Port_Moresby"] },
|
||||
"Tasmania Standard Time": { "iana": ["Australia/Hobart"] },
|
||||
"Vladivostok Standard Time": { "iana": ["Asia/Vladivostok"] },
|
||||
"Lord Howe Standard Time": { "iana": ["Australia/Lord_Howe"] },
|
||||
"Bougainville Standard Time": { "iana": ["Pacific/Bougainville"] },
|
||||
"Russia Time Zone 10": { "iana": ["Asia/Srednekolymsk"] },
|
||||
"Magadan Standard Time": { "iana": ["Asia/Magadan"] },
|
||||
"Norfolk Standard Time": { "iana": ["Pacific/Norfolk"] },
|
||||
"Sakhalin Standard Time": { "iana": ["Asia/Sakhalin"] },
|
||||
"Central Pacific Standard Time": { "iana": ["Pacific/Guadalcanal"] },
|
||||
"Russia Time Zone 11": { "iana": ["Asia/Kamchatka"] },
|
||||
"New Zealand Standard Time": { "iana": ["Pacific/Auckland"] },
|
||||
"UTC+12": { "iana": ["Etc/GMT-12"] },
|
||||
"Fiji Standard Time": { "iana": ["Pacific/Fiji"] },
|
||||
"Chatham Islands Standard Time": { "iana": ["Pacific/Chatham"] },
|
||||
"UTC+13": { "iana": ["Etc/GMT-13"] },
|
||||
"Tonga Standard Time": { "iana": ["Pacific/Tongatapu"] },
|
||||
"Samoa Standard Time": { "iana": ["Pacific/Apia"] },
|
||||
"Line Islands Standard Time": { "iana": ["Pacific/Kiritimati"] },
|
||||
"(UTC-12:00) International Date Line West": { "iana": ["Etc/GMT+12"] },
|
||||
"(UTC-11:00) Midway Island, Samoa": { "iana": ["Pacific/Apia"] },
|
||||
"(UTC-10:00) Hawaii": { "iana": ["Pacific/Honolulu"] },
|
||||
"(UTC-09:00) Alaska": { "iana": ["America/Anchorage"] },
|
||||
"(UTC-08:00) Pacific Time (US & Canada); Tijuana": { "iana": ["America/Los_Angeles"] },
|
||||
"(UTC-08:00) Pacific Time (US and Canada); Tijuana": { "iana": ["America/Los_Angeles"] },
|
||||
"(UTC-07:00) Mountain Time (US & Canada)": { "iana": ["America/Denver"] },
|
||||
"(UTC-07:00) Mountain Time (US and Canada)": { "iana": ["America/Denver"] },
|
||||
"(UTC-07:00) Chihuahua, La Paz, Mazatlan": { "iana": [null] },
|
||||
"(UTC-07:00) Arizona": { "iana": ["America/Phoenix"] },
|
||||
"(UTC-06:00) Central Time (US & Canada)": { "iana": ["America/Chicago"] },
|
||||
"(UTC-06:00) Central Time (US and Canada)": { "iana": ["America/Chicago"] },
|
||||
"(UTC-06:00) Saskatchewan": { "iana": ["America/Regina"] },
|
||||
"(UTC-06:00) Guadalajara, Mexico City, Monterrey": { "iana": [null] },
|
||||
"(UTC-06:00) Central America": { "iana": ["America/Guatemala"] },
|
||||
"(UTC-05:00) Eastern Time (US & Canada)": { "iana": ["America/New_York"] },
|
||||
"(UTC-05:00) Eastern Time (US and Canada)": { "iana": ["America/New_York"] },
|
||||
"(UTC-05:00) Indiana (East)": { "iana": ["America/Indianapolis"] },
|
||||
"(UTC-05:00) Bogota, Lima, Quito": { "iana": ["America/Bogota"] },
|
||||
"(UTC-04:00) Atlantic Time (Canada)": { "iana": ["America/Halifax"] },
|
||||
"(UTC-04:00) Georgetown, La Paz, San Juan": { "iana": ["America/La_Paz"] },
|
||||
"(UTC-04:00) Santiago": { "iana": ["America/Santiago"] },
|
||||
"(UTC-03:30) Newfoundland": { "iana": [null] },
|
||||
"(UTC-03:00) Brasilia": { "iana": ["America/Sao_Paulo"] },
|
||||
"(UTC-03:00) Georgetown": { "iana": ["America/Cayenne"] },
|
||||
"(UTC-03:00) Greenland": { "iana": ["America/Godthab"] },
|
||||
"(UTC-02:00) Mid-Atlantic": { "iana": [null] },
|
||||
"(UTC-01:00) Azores": { "iana": ["Atlantic/Azores"] },
|
||||
"(UTC-01:00) Cape Verde Islands": { "iana": ["Atlantic/Cape_Verde"] },
|
||||
"(UTC) Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London": { "iana": [null] },
|
||||
"(UTC) Monrovia, Reykjavik": { "iana": ["Atlantic/Reykjavik"] },
|
||||
"(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague": { "iana": ["Europe/Budapest"] },
|
||||
"(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb": { "iana": ["Europe/Warsaw"] },
|
||||
"(UTC+01:00) Brussels, Copenhagen, Madrid, Paris": { "iana": ["Europe/Paris"] },
|
||||
"(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna": { "iana": ["Europe/Berlin"] },
|
||||
"(UTC+01:00) West Central Africa": { "iana": ["Africa/Lagos"] },
|
||||
"(UTC+02:00) Minsk": { "iana": ["Europe/Chisinau"] },
|
||||
"(UTC+02:00) Cairo": { "iana": ["Africa/Cairo"] },
|
||||
"(UTC+02:00) Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius": { "iana": ["Europe/Kiev"] },
|
||||
"(UTC+02:00) Athens, Bucharest, Istanbul": { "iana": ["Europe/Bucharest"] },
|
||||
"(UTC+02:00) Jerusalem": { "iana": ["Asia/Jerusalem"] },
|
||||
"(UTC+02:00) Harare, Pretoria": { "iana": ["Africa/Johannesburg"] },
|
||||
"(UTC+03:00) Moscow, St. Petersburg, Volgograd": { "iana": ["Europe/Moscow"] },
|
||||
"(UTC+03:00) Kuwait, Riyadh": { "iana": ["Asia/Riyadh"] },
|
||||
"(UTC+03:00) Nairobi": { "iana": ["Africa/Nairobi"] },
|
||||
"(UTC+03:00) Baghdad": { "iana": ["Asia/Baghdad"] },
|
||||
"(UTC+03:30) Tehran": { "iana": ["Asia/Tehran"] },
|
||||
"(UTC+04:00) Abu Dhabi, Muscat": { "iana": ["Asia/Dubai"] },
|
||||
"(UTC+04:00) Baku, Tbilisi, Yerevan": { "iana": ["Asia/Yerevan"] },
|
||||
"(UTC+04:30) Kabul": { "iana": [null] },
|
||||
"(UTC+05:00) Ekaterinburg": { "iana": ["Asia/Yekaterinburg"] },
|
||||
"(UTC+05:00) Tashkent": { "iana": ["Asia/Tashkent"] },
|
||||
"(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi": { "iana": ["Asia/Calcutta"] },
|
||||
"(UTC+05:45) Kathmandu": { "iana": ["Asia/Katmandu"] },
|
||||
"(UTC+06:00) Astana, Dhaka": { "iana": ["Asia/Almaty"] },
|
||||
"(UTC+06:00) Sri Jayawardenepura": { "iana": ["Asia/Colombo"] },
|
||||
"(UTC+06:00) Almaty, Novosibirsk": { "iana": ["Asia/Novosibirsk"] },
|
||||
"(UTC+06:30) Yangon (Rangoon)": { "iana": ["Asia/Rangoon"] },
|
||||
"(UTC+07:00) Bangkok, Hanoi, Jakarta": { "iana": ["Asia/Bangkok"] },
|
||||
"(UTC+07:00) Krasnoyarsk": { "iana": ["Asia/Krasnoyarsk"] },
|
||||
"(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi": { "iana": ["Asia/Shanghai"] },
|
||||
"(UTC+08:00) Kuala Lumpur, Singapore": { "iana": ["Asia/Singapore"] },
|
||||
"(UTC+08:00) Taipei": { "iana": ["Asia/Taipei"] },
|
||||
"(UTC+08:00) Perth": { "iana": ["Australia/Perth"] },
|
||||
"(UTC+08:00) Irkutsk, Ulaanbaatar": { "iana": ["Asia/Irkutsk"] },
|
||||
"(UTC+09:00) Seoul": { "iana": ["Asia/Seoul"] },
|
||||
"(UTC+09:00) Osaka, Sapporo, Tokyo": { "iana": ["Asia/Tokyo"] },
|
||||
"(UTC+09:00) Yakutsk": { "iana": ["Asia/Yakutsk"] },
|
||||
"(UTC+09:30) Darwin": { "iana": ["Australia/Darwin"] },
|
||||
"(UTC+09:30) Adelaide": { "iana": ["Australia/Adelaide"] },
|
||||
"(UTC+10:00) Canberra, Melbourne, Sydney": { "iana": ["Australia/Sydney"] },
|
||||
"(GMT+10:00) Canberra, Melbourne, Sydney": { "iana": ["Australia/Sydney"] },
|
||||
"(UTC+10:00) Brisbane": { "iana": ["Australia/Brisbane"] },
|
||||
"(UTC+10:00) Hobart": { "iana": ["Australia/Hobart"] },
|
||||
"(UTC+10:00) Vladivostok": { "iana": ["Asia/Vladivostok"] },
|
||||
"(UTC+10:00) Guam, Port Moresby": { "iana": ["Pacific/Port_Moresby"] },
|
||||
"(UTC+11:00) Magadan, Solomon Islands, New Caledonia": { "iana": ["Pacific/Guadalcanal"] },
|
||||
"(UTC+12:00) Fiji, Kamchatka, Marshall Is.": { "iana": [null] },
|
||||
"(UTC+12:00) Auckland, Wellington": { "iana": ["Pacific/Auckland"] },
|
||||
"(UTC+13:00) Nuku'alofa": { "iana": ["Pacific/Tongatapu"] },
|
||||
"(UTC-03:00) Buenos Aires": { "iana": ["America/Buenos_Aires"] },
|
||||
"(UTC+02:00) Beirut": { "iana": ["Asia/Beirut"] },
|
||||
"(UTC+02:00) Amman": { "iana": ["Asia/Amman"] },
|
||||
"(UTC-06:00) Guadalajara, Mexico City, Monterrey - New": { "iana": ["America/Mexico_City"] },
|
||||
"(UTC-07:00) Chihuahua, La Paz, Mazatlan - New": { "iana": ["America/Chihuahua"] },
|
||||
"(UTC-08:00) Tijuana, Baja California": { "iana": ["America/Tijuana"] },
|
||||
"(UTC+02:00) Windhoek": { "iana": ["Africa/Windhoek"] },
|
||||
"(UTC+03:00) Tbilisi": { "iana": ["Asia/Tbilisi"] },
|
||||
"(UTC-04:00) Manaus": { "iana": ["America/Cuiaba"] },
|
||||
"(UTC-03:00) Montevideo": { "iana": ["America/Montevideo"] },
|
||||
"(UTC+04:00) Yerevan": { "iana": [null] },
|
||||
"(UTC-04:30) Caracas": { "iana": ["America/Caracas"] },
|
||||
"(UTC) Casablanca": { "iana": ["Africa/Casablanca"] },
|
||||
"(UTC+05:00) Islamabad, Karachi": { "iana": ["Asia/Karachi"] },
|
||||
"(UTC+04:00) Port Louis": { "iana": ["Indian/Mauritius"] },
|
||||
"(UTC) Coordinated Universal Time": { "iana": ["Etc/GMT"] },
|
||||
"(UTC-04:00) Asuncion": { "iana": ["America/Asuncion"] },
|
||||
"(UTC+12:00) Petropavlovsk-Kamchatsky": { "iana": [null] }
|
||||
}
|
@@ -152,6 +152,13 @@ Module.register("clock", {
|
||||
timeWrapper.appendChild(periodWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the time according to the config
|
||||
*
|
||||
* @param {object} config The config of the module
|
||||
* @param {object} time time to format
|
||||
* @returns {string} The formatted time string
|
||||
*/
|
||||
function formatTime(config, time) {
|
||||
var formatString = hourSymbol + ":mm";
|
||||
if (config.showPeriod && config.timeFormat !== 24) {
|
||||
@@ -159,6 +166,7 @@ Module.register("clock", {
|
||||
}
|
||||
return moment(time).format(formatString);
|
||||
}
|
||||
|
||||
if (this.config.showSunTimes) {
|
||||
const sunTimes = SunCalc.getTimes(now, this.config.lat, this.config.lon);
|
||||
const isVisible = now.isBetween(sunTimes.sunrise, sunTimes.sunset);
|
||||
@@ -179,10 +187,10 @@ Module.register("clock", {
|
||||
'"><i class="fa fa-sun-o" aria-hidden="true"></i> ' +
|
||||
untilNextEventString +
|
||||
"</span>" +
|
||||
'<span><i class="fa fa-arrow-up" aria-hidden="true"></i>' +
|
||||
'<span><i class="fa fa-arrow-up" aria-hidden="true"></i> ' +
|
||||
formatTime(this.config, sunTimes.sunrise) +
|
||||
"</span>" +
|
||||
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i>' +
|
||||
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i> ' +
|
||||
formatTime(this.config, sunTimes.sunset) +
|
||||
"</span>";
|
||||
}
|
||||
|
@@ -1 +1 @@
|
||||
<svg id="Hour_Markers_-_Singlets" data-name="Hour Markers - Singlets" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1,.cls-2{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;}.cls-2{stroke-width:0.5px;}</style></defs><title>face-001</title><line class="cls-1" x1="125" y1="1.25" x2="125" y2="16.23"/><line class="cls-1" x1="186.87" y1="17.83" x2="179.39" y2="30.8"/><line class="cls-1" x1="232.17" y1="63.12" x2="219.2" y2="70.61"/><line class="cls-1" x1="248.75" y1="125" x2="233.77" y2="125"/><line class="cls-1" x1="232.17" y1="186.87" x2="219.2" y2="179.39"/><line class="cls-1" x1="186.88" y1="232.17" x2="179.39" y2="219.2"/><line class="cls-1" x1="125" y1="248.75" x2="125" y2="233.77"/><line class="cls-1" x1="63.13" y1="232.17" x2="70.61" y2="219.2"/><line class="cls-1" x1="17.83" y1="186.88" x2="30.8" y2="179.39"/><line class="cls-1" x1="1.25" y1="125" x2="16.23" y2="125"/><line class="cls-1" x1="17.83" y1="63.13" x2="30.8" y2="70.61"/><line class="cls-1" x1="63.12" y1="17.83" x2="70.61" y2="30.8"/><line class="cls-2" x1="138.01" y1="1.25" x2="136.96" y2="11.23"/><line class="cls-2" x1="150.87" y1="3.29" x2="148.78" y2="13.11"/><line class="cls-2" x1="163.45" y1="6.66" x2="160.35" y2="16.21"/><line class="cls-2" x1="175.61" y1="11.33" x2="171.53" y2="20.5"/><line class="cls-2" x1="198.14" y1="24.33" x2="192.24" y2="32.45"/><line class="cls-2" x1="208.26" y1="32.53" x2="201.54" y2="39.99"/><line class="cls-2" x1="217.47" y1="41.74" x2="210.01" y2="48.46"/><line class="cls-2" x1="225.67" y1="51.86" x2="217.55" y2="57.76"/><line class="cls-2" x1="238.67" y1="74.39" x2="229.5" y2="78.47"/><line class="cls-2" x1="243.34" y1="86.55" x2="233.79" y2="89.65"/><line class="cls-2" x1="246.71" y1="99.13" x2="236.89" y2="101.22"/><line class="cls-2" x1="248.75" y1="111.99" x2="238.77" y2="113.04"/><line class="cls-2" x1="248.75" y1="138.01" x2="238.77" y2="136.96"/><line class="cls-2" x1="246.71" y1="150.87" x2="236.89" y2="148.78"/><line class="cls-2" x1="243.34" y1="163.45" x2="233.79" y2="160.35"/><line class="cls-2" x1="238.67" y1="175.61" x2="229.5" y2="171.53"/><line class="cls-2" x1="225.67" y1="198.14" x2="217.55" y2="192.24"/><line class="cls-2" x1="217.47" y1="208.26" x2="210.01" y2="201.54"/><line class="cls-2" x1="208.26" y1="217.47" x2="201.54" y2="210.01"/><line class="cls-2" x1="198.14" y1="225.67" x2="192.24" y2="217.55"/><line class="cls-2" x1="175.61" y1="238.67" x2="171.53" y2="229.5"/><line class="cls-2" x1="163.45" y1="243.34" x2="160.35" y2="233.79"/><line class="cls-2" x1="150.87" y1="246.71" x2="148.78" y2="236.89"/><line class="cls-2" x1="138.01" y1="248.75" x2="136.96" y2="238.77"/><line class="cls-2" x1="111.99" y1="248.75" x2="113.04" y2="238.77"/><line class="cls-2" x1="99.13" y1="246.71" x2="101.22" y2="236.89"/><line class="cls-2" x1="86.55" y1="243.34" x2="89.65" y2="233.79"/><line class="cls-2" x1="74.39" y1="238.67" x2="78.47" y2="229.5"/><line class="cls-2" x1="51.86" y1="225.67" x2="57.76" y2="217.55"/><line class="cls-2" x1="41.74" y1="217.47" x2="48.46" y2="210.01"/><line class="cls-2" x1="32.53" y1="208.26" x2="39.99" y2="201.54"/><line class="cls-2" x1="24.33" y1="198.14" x2="32.45" y2="192.24"/><line class="cls-2" x1="11.33" y1="175.61" x2="20.5" y2="171.53"/><line class="cls-2" x1="6.66" y1="163.45" x2="16.21" y2="160.35"/><line class="cls-2" x1="3.29" y1="150.87" x2="13.11" y2="148.78"/><line class="cls-2" x1="1.25" y1="138.01" x2="11.23" y2="136.96"/><line class="cls-2" x1="1.25" y1="111.99" x2="11.23" y2="113.04"/><line class="cls-2" x1="3.29" y1="99.13" x2="13.11" y2="101.22"/><line class="cls-2" x1="6.66" y1="86.55" x2="16.21" y2="89.65"/><line class="cls-2" x1="11.33" y1="74.39" x2="20.5" y2="78.47"/><line class="cls-2" x1="24.33" y1="51.86" x2="32.45" y2="57.76"/><line class="cls-2" x1="32.53" y1="41.74" x2="39.99" y2="48.46"/><line class="cls-2" x1="41.74" y1="32.53" x2="48.46" y2="39.99"/><line class="cls-2" x1="51.86" y1="24.33" x2="57.76" y2="32.45"/><line class="cls-2" x1="74.39" y1="11.33" x2="78.47" y2="20.5"/><line class="cls-2" x1="86.55" y1="6.66" x2="89.65" y2="16.21"/><line class="cls-2" x1="99.13" y1="3.29" x2="101.22" y2="13.11"/><line class="cls-2" x1="111.99" y1="1.25" x2="113.04" y2="11.23"/></svg>
|
||||
<svg id="Hour_Markers_-_Singlets" data-name="Hour Markers - Singlets" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1,.cls-2{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;}.cls-2{stroke-width:0.5px;}</style></defs><title>face-001</title><line class="cls-1" x1="125" y1="1.25" x2="125" y2="16.23"/><line class="cls-1" x1="186.87" y1="17.83" x2="179.39" y2="30.8"/><line class="cls-1" x1="232.17" y1="63.12" x2="219.2" y2="70.61"/><line class="cls-1" x1="248.75" y1="125" x2="233.77" y2="125"/><line class="cls-1" x1="232.17" y1="186.87" x2="219.2" y2="179.39"/><line class="cls-1" x1="186.88" y1="232.17" x2="179.39" y2="219.2"/><line class="cls-1" x1="125" y1="248.75" x2="125" y2="233.77"/><line class="cls-1" x1="63.13" y1="232.17" x2="70.61" y2="219.2"/><line class="cls-1" x1="17.83" y1="186.88" x2="30.8" y2="179.39"/><line class="cls-1" x1="1.25" y1="125" x2="16.23" y2="125"/><line class="cls-1" x1="17.83" y1="63.13" x2="30.8" y2="70.61"/><line class="cls-1" x1="63.12" y1="17.83" x2="70.61" y2="30.8"/><line class="cls-2" x1="138.01" y1="1.25" x2="136.96" y2="11.23"/><line class="cls-2" x1="150.87" y1="3.29" x2="148.78" y2="13.11"/><line class="cls-2" x1="163.45" y1="6.66" x2="160.35" y2="16.21"/><line class="cls-2" x1="175.61" y1="11.33" x2="171.53" y2="20.5"/><line class="cls-2" x1="198.14" y1="24.33" x2="192.24" y2="32.45"/><line class="cls-2" x1="208.26" y1="32.53" x2="201.54" y2="39.99"/><line class="cls-2" x1="217.47" y1="41.74" x2="210.01" y2="48.46"/><line class="cls-2" x1="225.67" y1="51.86" x2="217.55" y2="57.76"/><line class="cls-2" x1="238.67" y1="74.39" x2="229.5" y2="78.47"/><line class="cls-2" x1="243.34" y1="86.55" x2="233.79" y2="89.65"/><line class="cls-2" x1="246.71" y1="99.13" x2="236.89" y2="101.22"/><line class="cls-2" x1="248.75" y1="111.99" x2="238.77" y2="113.04"/><line class="cls-2" x1="248.75" y1="138.01" x2="238.77" y2="136.96"/><line class="cls-2" x1="246.71" y1="150.87" x2="236.89" y2="148.78"/><line class="cls-2" x1="243.34" y1="163.45" x2="233.79" y2="160.35"/><line class="cls-2" x1="238.67" y1="175.61" x2="229.5" y2="171.53"/><line class="cls-2" x1="225.67" y1="198.14" x2="217.55" y2="192.24"/><line class="cls-2" x1="217.47" y1="208.26" x2="210.01" y2="201.54"/><line class="cls-2" x1="208.26" y1="217.47" x2="201.54" y2="210.01"/><line class="cls-2" x1="198.14" y1="225.67" x2="192.24" y2="217.55"/><line class="cls-2" x1="175.61" y1="238.67" x2="171.53" y2="229.5"/><line class="cls-2" x1="163.45" y1="243.34" x2="160.35" y2="233.79"/><line class="cls-2" x1="150.87" y1="246.71" x2="148.78" y2="236.89"/><line class="cls-2" x1="138.01" y1="248.75" x2="136.96" y2="238.77"/><line class="cls-2" x1="111.99" y1="248.75" x2="113.04" y2="238.77"/><line class="cls-2" x1="99.13" y1="246.71" x2="101.22" y2="236.89"/><line class="cls-2" x1="86.55" y1="243.34" x2="89.65" y2="233.79"/><line class="cls-2" x1="74.39" y1="238.67" x2="78.47" y2="229.5"/><line class="cls-2" x1="51.86" y1="225.67" x2="57.76" y2="217.55"/><line class="cls-2" x1="41.74" y1="217.47" x2="48.46" y2="210.01"/><line class="cls-2" x1="32.53" y1="208.26" x2="39.99" y2="201.54"/><line class="cls-2" x1="24.33" y1="198.14" x2="32.45" y2="192.24"/><line class="cls-2" x1="11.33" y1="175.61" x2="20.5" y2="171.53"/><line class="cls-2" x1="6.66" y1="163.45" x2="16.21" y2="160.35"/><line class="cls-2" x1="3.29" y1="150.87" x2="13.11" y2="148.78"/><line class="cls-2" x1="1.25" y1="138.01" x2="11.23" y2="136.96"/><line class="cls-2" x1="1.25" y1="111.99" x2="11.23" y2="113.04"/><line class="cls-2" x1="3.29" y1="99.13" x2="13.11" y2="101.22"/><line class="cls-2" x1="6.66" y1="86.55" x2="16.21" y2="89.65"/><line class="cls-2" x1="11.33" y1="74.39" x2="20.5" y2="78.47"/><line class="cls-2" x1="24.33" y1="51.86" x2="32.45" y2="57.76"/><line class="cls-2" x1="32.53" y1="41.74" x2="39.99" y2="48.46"/><line class="cls-2" x1="41.74" y1="32.53" x2="48.46" y2="39.99"/><line class="cls-2" x1="51.86" y1="24.33" x2="57.76" y2="32.45"/><line class="cls-2" x1="74.39" y1="11.33" x2="78.47" y2="20.5"/><line class="cls-2" x1="86.55" y1="6.66" x2="89.65" y2="16.21"/><line class="cls-2" x1="99.13" y1="3.29" x2="101.22" y2="13.11"/><line class="cls-2" x1="111.99" y1="1.25" x2="113.04" y2="11.23"/></svg>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -1 +1 @@
|
||||
<svg id="Hour_Markers_-_Doubles" data-name="Hour Markers - Doubles" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;stroke-width:2.98px;}</style></defs><title>face-002</title><line class="cls-1" x1="122.01" y1="1.75" x2="122.01" y2="16.67"/><line class="cls-1" x1="186.62" y1="18.26" x2="179.17" y2="31.18"/><line class="cls-1" x1="231.74" y1="63.37" x2="218.82" y2="70.83"/><line class="cls-1" x1="248.25" y1="127.99" x2="233.33" y2="127.99"/><line class="cls-1" x1="231.74" y1="186.62" x2="218.82" y2="179.17"/><line class="cls-1" x1="186.63" y1="231.74" x2="179.17" y2="218.82"/><line class="cls-1" x1="127.99" y1="248.25" x2="127.99" y2="233.33"/><line class="cls-1" x1="63.38" y1="231.74" x2="70.83" y2="218.82"/><line class="cls-1" x1="18.26" y1="186.63" x2="31.18" y2="179.17"/><line class="cls-1" x1="1.75" y1="122.01" x2="16.67" y2="122.01"/><line class="cls-1" x1="18.26" y1="63.38" x2="31.18" y2="70.83"/><line class="cls-1" x1="63.37" y1="18.26" x2="70.83" y2="31.18"/><line class="cls-1" x1="127.99" y1="1.75" x2="127.99" y2="16.67"/><line class="cls-1" x1="248.25" y1="122.01" x2="233.33" y2="122.01"/><line class="cls-1" x1="122.01" y1="248.25" x2="122.01" y2="233.33"/><line class="cls-1" x1="1.75" y1="127.99" x2="16.67" y2="127.99"/></svg>
|
||||
<svg id="Hour_Markers_-_Doubles" data-name="Hour Markers - Doubles" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;stroke-width:2.98px;}</style></defs><title>face-002</title><line class="cls-1" x1="122.01" y1="1.75" x2="122.01" y2="16.67"/><line class="cls-1" x1="186.62" y1="18.26" x2="179.17" y2="31.18"/><line class="cls-1" x1="231.74" y1="63.37" x2="218.82" y2="70.83"/><line class="cls-1" x1="248.25" y1="127.99" x2="233.33" y2="127.99"/><line class="cls-1" x1="231.74" y1="186.62" x2="218.82" y2="179.17"/><line class="cls-1" x1="186.63" y1="231.74" x2="179.17" y2="218.82"/><line class="cls-1" x1="127.99" y1="248.25" x2="127.99" y2="233.33"/><line class="cls-1" x1="63.38" y1="231.74" x2="70.83" y2="218.82"/><line class="cls-1" x1="18.26" y1="186.63" x2="31.18" y2="179.17"/><line class="cls-1" x1="1.75" y1="122.01" x2="16.67" y2="122.01"/><line class="cls-1" x1="18.26" y1="63.38" x2="31.18" y2="70.83"/><line class="cls-1" x1="63.37" y1="18.26" x2="70.83" y2="31.18"/><line class="cls-1" x1="127.99" y1="1.75" x2="127.99" y2="16.67"/><line class="cls-1" x1="248.25" y1="122.01" x2="233.33" y2="122.01"/><line class="cls-1" x1="122.01" y1="248.25" x2="122.01" y2="233.33"/><line class="cls-1" x1="1.75" y1="127.99" x2="16.67" y2="127.99"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
@@ -37,6 +37,8 @@ Module.register("currentweather", {
|
||||
weatherEndpoint: "weather",
|
||||
|
||||
appendLocationNameToHeader: true,
|
||||
useLocationAsHeader: false,
|
||||
|
||||
calendarClass: "calendar",
|
||||
tableClass: "large",
|
||||
|
||||
@@ -256,7 +258,13 @@ Module.register("currentweather", {
|
||||
|
||||
var feelsLike = document.createElement("span");
|
||||
feelsLike.className = "dimmed";
|
||||
feelsLike.innerHTML = this.translate("FEELS") + " " + this.feelsLike + degreeLabel;
|
||||
var feelsLikeHtml = this.translate("FEELS");
|
||||
if (feelsLikeHtml.indexOf("{DEGREE}") > -1) {
|
||||
feelsLikeHtml = this.translate("FEELS", {
|
||||
DEGREE: this.feelsLike + degreeLabel
|
||||
});
|
||||
} else feelsLikeHtml += " " + this.feelsLike + degreeLabel;
|
||||
feelsLike.innerHTML = feelsLikeHtml;
|
||||
small.appendChild(feelsLike);
|
||||
|
||||
wrapper.appendChild(small);
|
||||
@@ -267,15 +275,16 @@ Module.register("currentweather", {
|
||||
|
||||
// Override getHeader method.
|
||||
getHeader: function () {
|
||||
if (this.config.appendLocationNameToHeader && this.data.header !== undefined) {
|
||||
return this.data.header + " " + this.fetchedLocationName;
|
||||
}
|
||||
|
||||
if (this.config.useLocationAsHeader && this.config.location !== false) {
|
||||
return this.config.location;
|
||||
}
|
||||
|
||||
return this.data.header;
|
||||
if (this.config.appendLocationNameToHeader) {
|
||||
if (this.data.header) return this.data.header + " " + this.fetchedLocationName;
|
||||
else return this.fetchedLocationName;
|
||||
}
|
||||
|
||||
return this.data.header ? this.data.header : "";
|
||||
},
|
||||
|
||||
// Override notification handler.
|
||||
|
@@ -84,11 +84,11 @@ Module.register("newsfeed", {
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
var wrapper = document.createElement("div");
|
||||
const wrapper = document.createElement("div");
|
||||
|
||||
if (this.config.feedUrl) {
|
||||
wrapper.className = "small bright";
|
||||
wrapper.innerHTML = this.translate("configuration_changed");
|
||||
wrapper.innerHTML = this.translate("MODULE_CONFIG_CHANGED", { MODULE_NAME: "Newsfeed" });
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ Module.register("newsfeed", {
|
||||
if (this.newsItems.length > 0) {
|
||||
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications
|
||||
if (!this.config.showFullArticle && (this.config.showSourceTitle || this.config.showPublishDate)) {
|
||||
var sourceAndTimestamp = document.createElement("div");
|
||||
const sourceAndTimestamp = document.createElement("div");
|
||||
sourceAndTimestamp.className = "newsfeed-source light small dimmed";
|
||||
|
||||
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") {
|
||||
@@ -157,22 +157,22 @@ Module.register("newsfeed", {
|
||||
}
|
||||
|
||||
if (!this.config.showFullArticle) {
|
||||
var title = document.createElement("div");
|
||||
const title = document.createElement("div");
|
||||
title.className = "newsfeed-title bright medium light" + (!this.config.wrapTitle ? " no-wrap" : "");
|
||||
title.innerHTML = this.newsItems[this.activeItem].title;
|
||||
wrapper.appendChild(title);
|
||||
}
|
||||
|
||||
if (this.isShowingDescription) {
|
||||
var description = document.createElement("div");
|
||||
const description = document.createElement("div");
|
||||
description.className = "newsfeed-desc small light" + (!this.config.wrapDescription ? " no-wrap" : "");
|
||||
var txtDesc = this.newsItems[this.activeItem].description;
|
||||
const txtDesc = this.newsItems[this.activeItem].description;
|
||||
description.innerHTML = this.config.truncDescription ? (txtDesc.length > this.config.lengthDescription ? txtDesc.substring(0, this.config.lengthDescription) + "..." : txtDesc) : txtDesc;
|
||||
wrapper.appendChild(description);
|
||||
}
|
||||
|
||||
if (this.config.showFullArticle) {
|
||||
var fullArticle = document.createElement("iframe");
|
||||
const fullArticle = document.createElement("iframe");
|
||||
fullArticle.className = "";
|
||||
fullArticle.style.width = "100vw";
|
||||
// very large height value to allow scrolling
|
||||
@@ -205,8 +205,8 @@ Module.register("newsfeed", {
|
||||
return typeof this.newsItems[this.activeItem].url === "string" ? this.newsItems[this.activeItem].url : this.newsItems[this.activeItem].url.href;
|
||||
},
|
||||
|
||||
/* registerFeeds()
|
||||
* registers the feeds to be used by the backend.
|
||||
/**
|
||||
* Registers the feeds to be used by the backend.
|
||||
*/
|
||||
registerFeeds: function () {
|
||||
for (var f in this.config.feeds) {
|
||||
@@ -218,10 +218,10 @@ Module.register("newsfeed", {
|
||||
}
|
||||
},
|
||||
|
||||
/* generateFeed()
|
||||
/**
|
||||
* Generate an ordered list of items for this configured module.
|
||||
*
|
||||
* attribute feeds object - An object with feeds returned by the node helper.
|
||||
* @param {object} feeds An object with feeds returned by the node helper.
|
||||
*/
|
||||
generateFeed: function (feeds) {
|
||||
var newsItems = [];
|
||||
@@ -274,12 +274,11 @@ Module.register("newsfeed", {
|
||||
this.newsItems = newsItems;
|
||||
},
|
||||
|
||||
/* subscribedToFeed(feedUrl)
|
||||
/**
|
||||
* Check if this module is configured to show this feed.
|
||||
*
|
||||
* attribute feedUrl string - Url of the feed to check.
|
||||
*
|
||||
* returns bool
|
||||
* @param {string} feedUrl Url of the feed to check.
|
||||
* @returns {boolean} True if it is subscribed, false otherwise
|
||||
*/
|
||||
subscribedToFeed: function (feedUrl) {
|
||||
for (var f in this.config.feeds) {
|
||||
@@ -291,12 +290,11 @@ Module.register("newsfeed", {
|
||||
return false;
|
||||
},
|
||||
|
||||
/* titleForFeed(feedUrl)
|
||||
* Returns title for a specific feed Url.
|
||||
/**
|
||||
* Returns title for the specific feed url.
|
||||
*
|
||||
* attribute feedUrl string - Url of the feed to check.
|
||||
*
|
||||
* returns string
|
||||
* @param {string} feedUrl Url of the feed
|
||||
* @returns {string} The title of the feed
|
||||
*/
|
||||
titleForFeed: function (feedUrl) {
|
||||
for (var f in this.config.feeds) {
|
||||
@@ -308,7 +306,7 @@ Module.register("newsfeed", {
|
||||
return "";
|
||||
},
|
||||
|
||||
/* scheduleUpdateInterval()
|
||||
/**
|
||||
* Schedule visual update.
|
||||
*/
|
||||
scheduleUpdateInterval: function () {
|
||||
@@ -332,17 +330,6 @@ Module.register("newsfeed", {
|
||||
}, this.config.updateInterval);
|
||||
},
|
||||
|
||||
/* capitalizeFirstLetter(string)
|
||||
* Capitalizes the first character of a string.
|
||||
*
|
||||
* argument string string - Input string.
|
||||
*
|
||||
* return string - Capitalized output string.
|
||||
*/
|
||||
capitalizeFirstLetter: function (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
},
|
||||
|
||||
resetDescrOrFullArticleAndTimer: function () {
|
||||
this.isShowingDescription = this.config.showDescription;
|
||||
this.config.showFullArticle = false;
|
||||
@@ -356,14 +343,14 @@ Module.register("newsfeed", {
|
||||
},
|
||||
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
var before = this.activeItem;
|
||||
const before = this.activeItem;
|
||||
if (notification === "ARTICLE_NEXT") {
|
||||
this.activeItem++;
|
||||
if (this.activeItem >= this.newsItems.length) {
|
||||
this.activeItem = 0;
|
||||
}
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.info(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
||||
Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
||||
this.updateDom(100);
|
||||
} else if (notification === "ARTICLE_PREVIOUS") {
|
||||
this.activeItem--;
|
||||
@@ -371,7 +358,7 @@ Module.register("newsfeed", {
|
||||
this.activeItem = this.newsItems.length - 1;
|
||||
}
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.info(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
||||
Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
||||
this.updateDom(100);
|
||||
}
|
||||
// if "more details" is received the first time: show article summary, on second time show full article
|
||||
@@ -380,8 +367,8 @@ Module.register("newsfeed", {
|
||||
if (this.config.showFullArticle === true) {
|
||||
this.scrollPosition += this.config.scrollLength;
|
||||
window.scrollTo(0, this.scrollPosition);
|
||||
Log.info(this.name + " - scrolling down");
|
||||
Log.info(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength);
|
||||
Log.debug(this.name + " - scrolling down");
|
||||
Log.debug(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength);
|
||||
} else {
|
||||
this.showFullArticle();
|
||||
}
|
||||
@@ -389,12 +376,12 @@ Module.register("newsfeed", {
|
||||
if (this.config.showFullArticle === true) {
|
||||
this.scrollPosition -= this.config.scrollLength;
|
||||
window.scrollTo(0, this.scrollPosition);
|
||||
Log.info(this.name + " - scrolling up");
|
||||
Log.info(this.name + " - ARTICLE_SCROLL_UP, scroll position: " + this.config.scrollLength);
|
||||
Log.debug(this.name + " - scrolling up");
|
||||
Log.debug(this.name + " - ARTICLE_SCROLL_UP, scroll position: " + this.config.scrollLength);
|
||||
}
|
||||
} else if (notification === "ARTICLE_LESS_DETAILS") {
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.info(this.name + " - showing only article titles again");
|
||||
Log.debug(this.name + " - showing only article titles again");
|
||||
this.updateDom(100);
|
||||
} else if (notification === "ARTICLE_TOGGLE_FULL") {
|
||||
if (this.config.showFullArticle) {
|
||||
@@ -424,7 +411,7 @@ Module.register("newsfeed", {
|
||||
}
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
Log.info(this.name + " - showing " + this.isShowingDescription ? "article description" : "full article");
|
||||
Log.debug(this.name + " - showing " + this.isShowingDescription ? "article description" : "full article");
|
||||
this.updateDom(100);
|
||||
}
|
||||
});
|
||||
|
@@ -1,55 +1,56 @@
|
||||
/* Magic Mirror
|
||||
* Fetcher
|
||||
* Node Helper: Newsfeed - NewsfeedFetcher
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
const Log = require("../../../js/logger.js");
|
||||
const FeedMe = require("feedme");
|
||||
const request = require("request");
|
||||
const iconv = require("iconv-lite");
|
||||
|
||||
/* Fetcher
|
||||
/**
|
||||
* Responsible for requesting an update on the set interval and broadcasting the data.
|
||||
*
|
||||
* attribute url string - URL of the news feed.
|
||||
* attribute reloadInterval number - Reload interval in milliseconds.
|
||||
* attribute logFeedWarnings boolean - Log warnings when there is an error parsing a news article.
|
||||
* @param {string} url URL of the news feed.
|
||||
* @param {number} reloadInterval Reload interval in milliseconds.
|
||||
* @param {string} encoding Encoding of the feed.
|
||||
* @param {boolean} logFeedWarnings If true log warnings when there is an error parsing a news article.
|
||||
* @class
|
||||
*/
|
||||
const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
const self = this;
|
||||
|
||||
let reloadTimer = null;
|
||||
let items = [];
|
||||
|
||||
let fetchFailedCallback = function () {};
|
||||
let itemsReceivedCallback = function () {};
|
||||
|
||||
var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
var self = this;
|
||||
if (reloadInterval < 1000) {
|
||||
reloadInterval = 1000;
|
||||
}
|
||||
|
||||
var reloadTimer = null;
|
||||
var items = [];
|
||||
|
||||
var fetchFailedCallback = function () {};
|
||||
var itemsReceivedCallback = function () {};
|
||||
|
||||
/* private methods */
|
||||
|
||||
/* fetchNews()
|
||||
/**
|
||||
* Request the new items.
|
||||
*/
|
||||
var fetchNews = function () {
|
||||
const fetchNews = function () {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = null;
|
||||
items = [];
|
||||
|
||||
var parser = new FeedMe();
|
||||
const parser = new FeedMe();
|
||||
|
||||
parser.on("item", function (item) {
|
||||
var title = item.title;
|
||||
var description = item.description || item.summary || item.content || "";
|
||||
var pubdate = item.pubdate || item.published || item.updated || item["dc:date"];
|
||||
var url = item.url || item.link || "";
|
||||
const title = item.title;
|
||||
let description = item.description || item.summary || item.content || "";
|
||||
const pubdate = item.pubdate || item.published || item.updated || item["dc:date"];
|
||||
const url = item.url || item.link || "";
|
||||
|
||||
if (title && pubdate) {
|
||||
var regex = /(<([^>]+)>)/gi;
|
||||
const regex = /(<([^>]+)>)/gi;
|
||||
description = description.toString().replace(regex, "");
|
||||
|
||||
items.push({
|
||||
@@ -77,10 +78,17 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
scheduleTimer();
|
||||
});
|
||||
|
||||
var nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
var headers = { "User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)", "Cache-Control": "max-age=0, no-cache, no-store, must-revalidate", Pragma: "no-cache" };
|
||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
const opts = {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)",
|
||||
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
||||
Pragma: "no-cache"
|
||||
},
|
||||
encoding: null
|
||||
};
|
||||
|
||||
request({ uri: url, encoding: null, headers: headers })
|
||||
request(url, opts)
|
||||
.on("error", function (error) {
|
||||
fetchFailedCallback(self, error);
|
||||
scheduleTimer();
|
||||
@@ -89,10 +97,10 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
.pipe(parser);
|
||||
};
|
||||
|
||||
/* scheduleTimer()
|
||||
/**
|
||||
* Schedule the timer for the next update.
|
||||
*/
|
||||
var scheduleTimer = function () {
|
||||
const scheduleTimer = function () {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = setTimeout(function () {
|
||||
fetchNews();
|
||||
@@ -101,10 +109,10 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
|
||||
/* public methods */
|
||||
|
||||
/* setReloadInterval()
|
||||
/**
|
||||
* Update the reload interval, but only if we need to increase the speed.
|
||||
*
|
||||
* attribute interval number - Interval for the update in milliseconds.
|
||||
* @param {number} interval Interval for the update in milliseconds.
|
||||
*/
|
||||
this.setReloadInterval = function (interval) {
|
||||
if (interval > 1000 && interval < reloadInterval) {
|
||||
@@ -112,14 +120,14 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
}
|
||||
};
|
||||
|
||||
/* startFetch()
|
||||
/**
|
||||
* Initiate fetchNews();
|
||||
*/
|
||||
this.startFetch = function () {
|
||||
fetchNews();
|
||||
};
|
||||
|
||||
/* broadcastItems()
|
||||
/**
|
||||
* Broadcast the existing items.
|
||||
*/
|
||||
this.broadcastItems = function () {
|
||||
@@ -148,4 +156,4 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Fetcher;
|
||||
module.exports = NewsfeedFetcher;
|
@@ -7,7 +7,7 @@
|
||||
|
||||
const NodeHelper = require("node_helper");
|
||||
const validUrl = require("valid-url");
|
||||
const Fetcher = require("./fetcher.js");
|
||||
const NewsfeedFetcher = require("./newsfeedfetcher.js");
|
||||
const Log = require("../../../js/logger");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
@@ -24,45 +24,43 @@ module.exports = NodeHelper.create({
|
||||
}
|
||||
},
|
||||
|
||||
/* createFetcher(feed, config)
|
||||
/**
|
||||
* Creates a fetcher for a new feed if it doesn't exist yet.
|
||||
* Otherwise it reuses the existing one.
|
||||
*
|
||||
* attribute feed object - A feed object.
|
||||
* attribute config object - A configuration object containing reload interval in milliseconds.
|
||||
* @param {object} feed The feed object.
|
||||
* @param {object} config The configuration object.
|
||||
*/
|
||||
createFetcher: function (feed, config) {
|
||||
var self = this;
|
||||
|
||||
var url = feed.url || "";
|
||||
var encoding = feed.encoding || "UTF-8";
|
||||
var reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
|
||||
const url = feed.url || "";
|
||||
const encoding = feed.encoding || "UTF-8";
|
||||
const reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
|
||||
|
||||
if (!validUrl.isUri(url)) {
|
||||
self.sendSocketNotification("INCORRECT_URL", url);
|
||||
this.sendSocketNotification("INCORRECT_URL", url);
|
||||
return;
|
||||
}
|
||||
|
||||
var fetcher;
|
||||
if (typeof self.fetchers[url] === "undefined") {
|
||||
let fetcher;
|
||||
if (typeof this.fetchers[url] === "undefined") {
|
||||
Log.log("Create new news fetcher for url: " + url + " - Interval: " + reloadInterval);
|
||||
fetcher = new Fetcher(url, reloadInterval, encoding, config.logFeedWarnings);
|
||||
fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings);
|
||||
|
||||
fetcher.onReceive(function (fetcher) {
|
||||
self.broadcastFeeds();
|
||||
fetcher.onReceive(() => {
|
||||
this.broadcastFeeds();
|
||||
});
|
||||
|
||||
fetcher.onError(function (fetcher, error) {
|
||||
self.sendSocketNotification("FETCH_ERROR", {
|
||||
fetcher.onError((fetcher, error) => {
|
||||
this.sendSocketNotification("FETCH_ERROR", {
|
||||
url: fetcher.url(),
|
||||
error: error
|
||||
});
|
||||
});
|
||||
|
||||
self.fetchers[url] = fetcher;
|
||||
this.fetchers[url] = fetcher;
|
||||
} else {
|
||||
Log.log("Use existing news fetcher for url: " + url);
|
||||
fetcher = self.fetchers[url];
|
||||
fetcher = this.fetchers[url];
|
||||
fetcher.setReloadInterval(reloadInterval);
|
||||
fetcher.broadcastItems();
|
||||
}
|
||||
@@ -70,7 +68,7 @@ module.exports = NodeHelper.create({
|
||||
fetcher.startFetch();
|
||||
},
|
||||
|
||||
/* broadcastFeeds()
|
||||
/**
|
||||
* Creates an object with all feed items of the different registered feeds,
|
||||
* and broadcasts these using sendSocketNotification.
|
||||
*/
|
||||
|
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"configuration_changed": "Die Konfigurationsoptionen für das Newsfeed-Modul haben sich geändert. \nBitte überprüfen Sie die Dokumentation."
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"configuration_changed": "The configuration options for the newsfeed module have changed.\nPlease check the documentation."
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"configuration_changed": "Las opciones de configuración para el módulo de suministro de noticias han cambiado. \nVerifique la documentación."
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"configuration_changed": "Les options de configuration du module newsfeed ont changé. \nVeuillez consulter la documentation."
|
||||
}
|
@@ -72,7 +72,7 @@ module.exports = NodeHelper.create({
|
||||
performFetch: function () {
|
||||
var self = this;
|
||||
simpleGits.forEach((sg) => {
|
||||
sg.git.fetch().status((err, data) => {
|
||||
sg.git.fetch(["--dry-run"]).status((err, data) => {
|
||||
data.module = sg.module;
|
||||
if (!err) {
|
||||
sg.git.log({ "-1": null }, (err, data2) => {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# Weather Module
|
||||
|
||||
This module aims to be the replacement for the current `currentweather` and `weatherforcast` modules. The module will be configurable to be used as a current weather view, or to show the forecast. This way the module can be used twice to fullfil both purposes.
|
||||
This module aims to be the replacement for the current `currentweather` and `weatherforcast` modules. The module will be configurable to be used as a current weather view, or to show the forecast. This way the module can be used twice to fulfill both purposes.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/weather.html).
|
||||
|
@@ -1,4 +1,7 @@
|
||||
{% if current %}
|
||||
{% if current or weatherData %}
|
||||
{% if weatherData %}
|
||||
{% set current = weatherData.current %}
|
||||
{% endif %}
|
||||
{% if not config.onlyTemp %}
|
||||
<div class="normal medium">
|
||||
<span class="wi wi-strong-wind dimmed"></span>
|
||||
@@ -6,7 +9,11 @@
|
||||
{% if config.useBeaufort %}
|
||||
{{ current.beaufortWindSpeed() | round }}
|
||||
{% else %}
|
||||
{{ current.windSpeed | round }}
|
||||
{% if config.useKmh %}
|
||||
{{ current.kmhWindSpeed() | round }}
|
||||
{% else %}
|
||||
{{ current.windSpeed | round }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showWindDirection %}
|
||||
<sup>
|
||||
@@ -62,7 +69,10 @@
|
||||
<div class="normal medium">
|
||||
{% if config.showFeelsLike %}
|
||||
<span class="dimmed">
|
||||
{{ "FEELS" | translate }} {{ current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }}
|
||||
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
|
||||
{% if not config.feelsLikeWithDegree %}
|
||||
{{ current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationAmount %}
|
||||
|
@@ -1,11 +1,22 @@
|
||||
{% if forecast %}
|
||||
{% set numSteps = forecast | calcNumSteps %}
|
||||
{% if forecast or weatherData %}
|
||||
{% if weatherData %}
|
||||
{% set forecast = weatherData.days %}
|
||||
{% set numSteps = forecast | calcNumEntries %}
|
||||
{% else %}
|
||||
{% set numSteps = forecast | calcNumSteps %}
|
||||
{% endif %}
|
||||
{% set currentStep = 0 %}
|
||||
<table class="{{ config.tableClass }}">
|
||||
{% set forecast = forecast.slice(0, numSteps) %}
|
||||
{% for f in forecast %}
|
||||
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
|
||||
<td class="day">{{ f.date.format('ddd') }}</td>
|
||||
{% if (currentStep == 0) %}
|
||||
<td class="day">{{ "TODAY" | translate }}</td>
|
||||
{% elif (currentStep == 1) %}
|
||||
<td class="day">{{ "TOMORROW" | translate }}</td>
|
||||
{% else %}
|
||||
<td class="day">{{ f.date.format('ddd') }}</td>
|
||||
{% endif %}
|
||||
<td class="bright weather-icon"><span class="wi weathericon wi-{{ f.weatherType }}"></span></td>
|
||||
<td class="align-right bright max-temp">
|
||||
{{ f.maxTemperature | roundValue | unit("temperature") }}
|
||||
|
32
modules/default/weather/hourly.njk
Normal file
@@ -0,0 +1,32 @@
|
||||
{% if hourly or weatherData %}
|
||||
{% if weatherData %}
|
||||
{% set hourly = weatherData.hours %}
|
||||
{% endif %}
|
||||
{% set numSteps = hourly | calcNumEntries %}
|
||||
{% set currentStep = 0 %}
|
||||
<table class="{{ config.tableClass }}">
|
||||
{% set hours = hourly.slice(0, numSteps) %}
|
||||
{% for hour in hours %}
|
||||
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
|
||||
<td class="day">{{ hour.date | formatTime }}</td>
|
||||
<td class="bright weather-icon"><span class="wi weathericon wi-{{ hour.weatherType }}"></span></td>
|
||||
<td class="align-right bright">
|
||||
{{ hour.temperature | roundValue | unit("temperature") }}
|
||||
</td>
|
||||
{% if config.showPrecipitationAmount %}
|
||||
<td class="align-right bright precipitation">
|
||||
{{ hour.precipitation | unit("precip") }}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% set currentStep = currentStep + 1 %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="dimmed light small">
|
||||
{{ "LOADING" | translate | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Uncomment the line below to see the contents of the `hourly` object. -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{weatherData | dump}}</div> -->
|
@@ -62,7 +62,7 @@ WeatherProvider.register("darksky", {
|
||||
|
||||
// Implement WeatherDay generator.
|
||||
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
currentWeather.date = moment();
|
||||
currentWeather.humidity = parseFloat(currentWeatherData.currently.humidity);
|
||||
@@ -80,7 +80,7 @@ WeatherProvider.register("darksky", {
|
||||
const days = [];
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
weather.date = moment(forecast.time, "X");
|
||||
weather.minTemperature = forecast.temperatureMin;
|
||||
|
@@ -35,7 +35,7 @@ WeatherProvider.register("openweathermap", {
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
// Overwrite the fetchWeatherForecast method.
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
@@ -56,6 +56,27 @@ WeatherProvider.register("openweathermap", {
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Overwrite the fetchWeatherData method.
|
||||
fetchWeatherData() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
this.setFetchedLocation(`(${data.lat},${data.lon})`);
|
||||
|
||||
const weatherData = this.generateWeatherObjectsFromOnecall(data);
|
||||
this.setWeatherData(weatherData);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/** OpenWeatherMap Specific Methods - These are not part of the default provider methods */
|
||||
/*
|
||||
* Gets the complete url for the request
|
||||
@@ -68,11 +89,15 @@ WeatherProvider.register("openweathermap", {
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
currentWeather.humidity = currentWeatherData.main.humidity;
|
||||
currentWeather.temperature = currentWeatherData.main.temp;
|
||||
currentWeather.windSpeed = currentWeatherData.wind.speed;
|
||||
if (this.config.windUnits === "metric") {
|
||||
currentWeather.windSpeed = this.config.useKmh ? currentWeatherData.wind.speed * 3.6 : currentWeatherData.wind.speed;
|
||||
} else {
|
||||
currentWeather.windSpeed = currentWeatherData.wind.speed;
|
||||
}
|
||||
currentWeather.windDirection = currentWeatherData.wind.deg;
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon);
|
||||
currentWeather.sunrise = moment(currentWeatherData.sys.sunrise, "X");
|
||||
@@ -91,10 +116,22 @@ WeatherProvider.register("openweathermap", {
|
||||
return this.fetchForecastDaily(forecasts);
|
||||
}
|
||||
// if weatherEndpoint does not match forecast or forecast/daily, what should be returned?
|
||||
const days = [new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits)];
|
||||
const days = [new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh)];
|
||||
return days;
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate WeatherObjects based on One Call forecast information
|
||||
*/
|
||||
generateWeatherObjectsFromOnecall(data) {
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
return this.fetchOnecall(data);
|
||||
}
|
||||
// if weatherEndpoint does not match onecall, what should be returned?
|
||||
const weatherData = { current: new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh), hours: [], days: [] };
|
||||
return weatherData;
|
||||
},
|
||||
|
||||
/*
|
||||
* fetch forecast information for 3-hourly forecast (available for free subscription).
|
||||
*/
|
||||
@@ -108,7 +145,7 @@ WeatherProvider.register("openweathermap", {
|
||||
let snow = 0;
|
||||
// variable for date
|
||||
let date = "";
|
||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
if (date !== moment(forecast.dt, "X").format("YYYY-MM-DD")) {
|
||||
@@ -121,7 +158,7 @@ WeatherProvider.register("openweathermap", {
|
||||
// push weather information to days array
|
||||
days.push(weather);
|
||||
// create new weather-object
|
||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
minTemp = [];
|
||||
maxTemp = [];
|
||||
@@ -184,7 +221,7 @@ WeatherProvider.register("openweathermap", {
|
||||
const days = [];
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
weather.date = moment(forecast.dt, "X");
|
||||
weather.minTemperature = forecast.temp.min;
|
||||
@@ -221,6 +258,129 @@ WeatherProvider.register("openweathermap", {
|
||||
return days;
|
||||
},
|
||||
|
||||
/*
|
||||
* Fetch One Call forecast information (available for free subscription).
|
||||
* Factors in timezone offsets.
|
||||
* Minutely forecasts are excluded for the moment, see getParams().
|
||||
*/
|
||||
fetchOnecall(data) {
|
||||
let precip = false;
|
||||
|
||||
// get current weather, if requested
|
||||
const current = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
if (data.hasOwnProperty("current")) {
|
||||
current.date = moment(data.current.dt, "X").utcOffset(data.timezone_offset / 60);
|
||||
current.windSpeed = data.current.wind_speed;
|
||||
current.windDirection = data.current.wind_deg;
|
||||
current.sunrise = moment(data.current.sunrise, "X").utcOffset(data.timezone_offset / 60);
|
||||
current.sunset = moment(data.current.sunset, "X").utcOffset(data.timezone_offset / 60);
|
||||
current.temperature = data.current.temp;
|
||||
current.weatherType = this.convertWeatherType(data.current.weather[0].icon);
|
||||
current.humidity = data.current.humidity;
|
||||
if (data.current.hasOwnProperty("rain") && !isNaN(data.current["rain"]["1h"])) {
|
||||
if (this.config.units === "imperial") {
|
||||
current.rain = data.current["rain"]["1h"] / 25.4;
|
||||
} else {
|
||||
current.rain = data.current["rain"]["1h"];
|
||||
}
|
||||
precip = true;
|
||||
}
|
||||
if (data.current.hasOwnProperty("snow") && !isNaN(data.current["snow"]["1h"])) {
|
||||
if (this.config.units === "imperial") {
|
||||
current.snow = data.current["snow"]["1h"] / 25.4;
|
||||
} else {
|
||||
current.snow = data.current["snow"]["1h"];
|
||||
}
|
||||
precip = true;
|
||||
}
|
||||
if (precip) {
|
||||
current.precipitation = current.rain + current.snow;
|
||||
}
|
||||
current.feelsLikeTemp = data.current.feels_like;
|
||||
}
|
||||
|
||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
// get hourly weather, if requested
|
||||
const hours = [];
|
||||
if (data.hasOwnProperty("hourly")) {
|
||||
for (const hour of data.hourly) {
|
||||
weather.date = moment(hour.dt, "X").utcOffset(data.timezone_offset / 60);
|
||||
// weather.date = moment(hour.dt, "X").utcOffset(data.timezone_offset/60).format(onecallDailyFormat+","+onecallHourlyFormat);
|
||||
weather.temperature = hour.temp;
|
||||
weather.feelsLikeTemp = hour.feels_like;
|
||||
weather.humidity = hour.humidity;
|
||||
weather.windSpeed = hour.wind_speed;
|
||||
weather.windDirection = hour.wind_deg;
|
||||
weather.weatherType = this.convertWeatherType(hour.weather[0].icon);
|
||||
precip = false;
|
||||
if (hour.hasOwnProperty("rain") && !isNaN(hour.rain["1h"])) {
|
||||
if (this.config.units === "imperial") {
|
||||
weather.rain = hour.rain["1h"] / 25.4;
|
||||
} else {
|
||||
weather.rain = hour.rain["1h"];
|
||||
}
|
||||
precip = true;
|
||||
}
|
||||
if (hour.hasOwnProperty("snow") && !isNaN(hour.snow["1h"])) {
|
||||
if (this.config.units === "imperial") {
|
||||
weather.snow = hour.snow["1h"] / 25.4;
|
||||
} else {
|
||||
weather.snow = hour.snow["1h"];
|
||||
}
|
||||
precip = true;
|
||||
}
|
||||
if (precip) {
|
||||
weather.precipitation = weather.rain + weather.snow;
|
||||
}
|
||||
|
||||
hours.push(weather);
|
||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
}
|
||||
}
|
||||
|
||||
// get daily weather, if requested
|
||||
const days = [];
|
||||
if (data.hasOwnProperty("daily")) {
|
||||
for (const day of data.daily) {
|
||||
weather.date = moment(day.dt, "X").utcOffset(data.timezone_offset / 60);
|
||||
weather.sunrise = moment(day.sunrise, "X").utcOffset(data.timezone_offset / 60);
|
||||
weather.sunset = moment(day.sunset, "X").utcOffset(data.timezone_offset / 60);
|
||||
weather.minTemperature = day.temp.min;
|
||||
weather.maxTemperature = day.temp.max;
|
||||
weather.humidity = day.humidity;
|
||||
weather.windSpeed = day.wind_speed;
|
||||
weather.windDirection = day.wind_deg;
|
||||
weather.weatherType = this.convertWeatherType(day.weather[0].icon);
|
||||
precip = false;
|
||||
if (!isNaN(day.rain)) {
|
||||
if (this.config.units === "imperial") {
|
||||
weather.rain = day.rain / 25.4;
|
||||
} else {
|
||||
weather.rain = day.rain;
|
||||
}
|
||||
precip = true;
|
||||
}
|
||||
if (!isNaN(day.snow)) {
|
||||
if (this.config.units === "imperial") {
|
||||
weather.snow = day.snow / 25.4;
|
||||
} else {
|
||||
weather.snow = day.snow;
|
||||
}
|
||||
precip = true;
|
||||
}
|
||||
if (precip) {
|
||||
weather.precipitation = weather.rain + weather.snow;
|
||||
}
|
||||
|
||||
days.push(weather);
|
||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
}
|
||||
}
|
||||
|
||||
return { current: current, hours: hours, days: days };
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert the OpenWeatherMap icons to a more usable name.
|
||||
*/
|
||||
@@ -256,7 +416,19 @@ WeatherProvider.register("openweathermap", {
|
||||
*/
|
||||
getParams() {
|
||||
let params = "?";
|
||||
if (this.config.locationID) {
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
params += "lat=" + this.config.lat;
|
||||
params += "&lon=" + this.config.lon;
|
||||
if (this.config.type === "current") {
|
||||
params += "&exclude=minutely,hourly,daily";
|
||||
} else if (this.config.type === "hourly") {
|
||||
params += "&exclude=current,minutely,daily";
|
||||
} else if (this.config.type === "daily" || this.config.type === "forecast") {
|
||||
params += "&exclude=current,minutely,hourly";
|
||||
} else {
|
||||
params += "&exclude=minutely";
|
||||
}
|
||||
} else if (this.config.locationID) {
|
||||
params += "id=" + this.config.locationID;
|
||||
} else if (this.config.location) {
|
||||
params += "q=" + this.config.location;
|
||||
|
309
modules/default/weather/providers/smhi.js
Normal file
@@ -0,0 +1,309 @@
|
||||
/* global WeatherProvider, WeatherObject, SunCalc */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
* Provider: SMHI
|
||||
*
|
||||
* By BuXXi https://github.com/buxxi
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for SMHI (Sweden only).
|
||||
* Note that SMHI doesn't provide sunrise and sundown, use SunCalc to calculate it.
|
||||
* Metric system is the only supported unit.
|
||||
*/
|
||||
WeatherProvider.register("smhi", {
|
||||
providerName: "SMHI",
|
||||
|
||||
/**
|
||||
* Implements method in interface for fetching current weather
|
||||
*/
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getURL())
|
||||
.then((data) => {
|
||||
let closest = this.getClosestToCurrentTime(data.timeSeries);
|
||||
let coordinates = this.resolveCoordinates(data);
|
||||
let weatherObject = this.convertWeatherDataToObject(closest, coordinates);
|
||||
this.setFetchedLocation(`(${coordinates.lat},${coordinates.lon})`);
|
||||
this.setCurrentWeather(weatherObject);
|
||||
})
|
||||
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements method in interface for fetching a forecast.
|
||||
* Handling hourly forecast would be easy as not grouping by day but it seems really specific for one weather provider for now.
|
||||
*/
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getURL())
|
||||
.then((data) => {
|
||||
let coordinates = this.resolveCoordinates(data);
|
||||
let weatherObjects = this.convertWeatherDataGroupedByDay(data.timeSeries, coordinates);
|
||||
this.setFetchedLocation(`(${coordinates.lat},${coordinates.lon})`);
|
||||
this.setWeatherForecast(weatherObjects);
|
||||
})
|
||||
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides method for setting config with checks for the precipitationValue being unset or invalid
|
||||
*
|
||||
* @param config
|
||||
*/
|
||||
setConfig(config) {
|
||||
this.config = config;
|
||||
if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) == -1) {
|
||||
console.log("invalid or not set: " + config.precipitationValue);
|
||||
config.precipitationValue = "pmedian";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Of all the times returned find out which one is closest to the current time, should be the first if the data isn't old.
|
||||
*
|
||||
* @param times
|
||||
*/
|
||||
getClosestToCurrentTime(times) {
|
||||
let now = moment();
|
||||
let minDiff = undefined;
|
||||
for (time of times) {
|
||||
let diff = Math.abs(moment(time.validTime).diff(now));
|
||||
if (!minDiff || diff < Math.abs(moment(minDiff.validTime).diff(now))) {
|
||||
minDiff = time;
|
||||
}
|
||||
}
|
||||
return minDiff;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the forecast url for the configured coordinates
|
||||
*/
|
||||
getURL() {
|
||||
let lon = this.config.lon;
|
||||
let lat = this.config.lat;
|
||||
return `https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/${lon}/lat/${lat}/data.json`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts the returned data into a WeatherObject with required properties set for both current weather and forecast.
|
||||
* The returned units is always in metric system.
|
||||
* Requires coordinates to determine if its daytime or nighttime to know which icon to use and also to set sunrise and sunset.
|
||||
*
|
||||
* @param weatherData
|
||||
* @param coordinates
|
||||
* @param weatherData
|
||||
* @param coordinates
|
||||
*/
|
||||
convertWeatherDataToObject(weatherData, coordinates) {
|
||||
let currentWeather = new WeatherObject("metric", "metric", "metric"); //Weather data is only for Sweden and nobody in Sweden would use imperial
|
||||
|
||||
currentWeather.date = moment(weatherData.validTime);
|
||||
let times = SunCalc.getTimes(currentWeather.date.toDate(), coordinates.lat, coordinates.lon);
|
||||
currentWeather.sunrise = moment(times.sunrise, "X");
|
||||
currentWeather.sunset = moment(times.sunset, "X");
|
||||
currentWeather.humidity = this.paramValue(weatherData, "r");
|
||||
currentWeather.temperature = this.paramValue(weatherData, "t");
|
||||
currentWeather.windSpeed = this.paramValue(weatherData, "ws");
|
||||
currentWeather.windDirection = this.paramValue(weatherData, "wd");
|
||||
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), this.isDayTime(currentWeather));
|
||||
|
||||
//Determine the precipitation amount and category and update the weatherObject with it, the valuetype to use can be configured or uses median as default.
|
||||
let precipitationValue = this.paramValue(weatherData, this.config.precipitationValue);
|
||||
switch (this.paramValue(weatherData, "pcat")) {
|
||||
// 0 = No precipitation
|
||||
case 1: // Snow
|
||||
currentWeather.snow += precipitationValue;
|
||||
currentWeather.precipitation += precipitationValue;
|
||||
break;
|
||||
case 2: // Snow and rain, treat it as 50/50 snow and rain
|
||||
currentWeather.snow += precipitationValue / 2;
|
||||
currentWeather.rain += precipitationValue / 2;
|
||||
currentWeather.precipitation += precipitationValue;
|
||||
break;
|
||||
case 3: // Rain
|
||||
case 4: // Drizzle
|
||||
case 5: // Freezing rain
|
||||
case 6: // Freezing drizzle
|
||||
currentWeather.rain += precipitationValue;
|
||||
currentWeather.precipitation += precipitationValue;
|
||||
break;
|
||||
}
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
/**
|
||||
* Takes all of the data points and converts it to one WeatherObject per day.
|
||||
*
|
||||
* @param allWeatherData
|
||||
* @param coordinates
|
||||
* @param allWeatherData
|
||||
* @param coordinates
|
||||
*/
|
||||
convertWeatherDataGroupedByDay(allWeatherData, coordinates) {
|
||||
var currentWeather;
|
||||
let result = [];
|
||||
|
||||
let allWeatherObjects = this.fillInGaps(allWeatherData).map((weatherData) => this.convertWeatherDataToObject(weatherData, coordinates));
|
||||
var dayWeatherTypes = [];
|
||||
|
||||
for (weatherObject of allWeatherObjects) {
|
||||
//If its the first object or if a day change we need to reset the summary object
|
||||
if (!currentWeather || !currentWeather.date.isSame(weatherObject.date, "day")) {
|
||||
currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
dayWeatherTypes = [];
|
||||
currentWeather.date = weatherObject.date;
|
||||
currentWeather.minTemperature = Infinity;
|
||||
currentWeather.maxTemperature = -Infinity;
|
||||
currentWeather.snow = 0;
|
||||
currentWeather.rain = 0;
|
||||
currentWeather.precipitation = 0;
|
||||
result.push(currentWeather);
|
||||
}
|
||||
|
||||
//Keep track of what icons has been used for each hour of daytime and use the middle one for the forecast
|
||||
if (this.isDayTime(weatherObject)) {
|
||||
dayWeatherTypes.push(weatherObject.weatherType);
|
||||
}
|
||||
if (dayWeatherTypes.length > 0) {
|
||||
currentWeather.weatherType = dayWeatherTypes[Math.floor(dayWeatherTypes.length / 2)];
|
||||
} else {
|
||||
currentWeather.weatherType = weatherObject.weatherType;
|
||||
}
|
||||
|
||||
//All other properties is either a sum, min or max of each hour
|
||||
currentWeather.minTemperature = Math.min(currentWeather.minTemperature, weatherObject.temperature);
|
||||
currentWeather.maxTemperature = Math.max(currentWeather.maxTemperature, weatherObject.temperature);
|
||||
currentWeather.snow += weatherObject.snow;
|
||||
currentWeather.rain += weatherObject.rain;
|
||||
currentWeather.precipitation += weatherObject.precipitation;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolve coordinates from the response data (probably preferably to use this if it's not matching the config values exactly)
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
resolveCoordinates(data) {
|
||||
return { lat: data.geometry.coordinates[0][1], lon: data.geometry.coordinates[0][0] };
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the weatherObject is at dayTime.
|
||||
*
|
||||
* @param weatherObject
|
||||
*/
|
||||
isDayTime(weatherObject) {
|
||||
return weatherObject.date.isBetween(weatherObject.sunrise, weatherObject.sunset, undefined, "[]");
|
||||
},
|
||||
|
||||
/**
|
||||
* The distance between the data points is increasing in the data the more distant the prediction is.
|
||||
* Find these gaps and fill them with the previous hours data to make the data returned a complete set.
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
fillInGaps(data) {
|
||||
let result = [];
|
||||
for (var i = 1; i < data.length; i++) {
|
||||
let to = moment(data[i].validTime);
|
||||
let from = moment(data[i - 1].validTime);
|
||||
let hours = moment.duration(to.diff(from)).asHours();
|
||||
// For each hour add a datapoint but change the validTime
|
||||
for (var j = 0; j < hours; j++) {
|
||||
let current = Object.assign({}, data[i]);
|
||||
current.validTime = from.clone().add(j, "hours").toISOString();
|
||||
result.push(current);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method to fetch a property from the returned data set.
|
||||
* The returned values is an array with always one value in it.
|
||||
*
|
||||
* @param currentWeatherData
|
||||
* @param name
|
||||
* @param currentWeatherData
|
||||
* @param name
|
||||
*/
|
||||
paramValue(currentWeatherData, name) {
|
||||
return currentWeatherData.parameters.filter((p) => p.name == name).flatMap((p) => p.values)[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Map the icon value from SHMI to an icon that MagicMirror understands.
|
||||
* Uses different icons depending if its daytime or nighttime.
|
||||
* SHMI's description of what the numeric value means is the comment after the case.
|
||||
*
|
||||
* @param input
|
||||
* @param isDayTime
|
||||
* @param input
|
||||
* @param isDayTime
|
||||
*/
|
||||
convertWeatherType(input, isDayTime) {
|
||||
switch (input) {
|
||||
case 1:
|
||||
return isDayTime ? "day-sunny" : "night-clear"; // Clear sky
|
||||
case 2:
|
||||
return isDayTime ? "day-sunny-overcast" : "night-partly-cloudy"; //Nearly clear sky
|
||||
case 3:
|
||||
return isDayTime ? "day-cloudy" : "night-cloudy"; //Variable cloudiness
|
||||
case 4:
|
||||
return isDayTime ? "day-cloudy" : "night-cloudy"; //Halfclear sky
|
||||
case 5:
|
||||
return "cloudy"; //Cloudy sky
|
||||
case 6:
|
||||
return "cloudy"; //Overcast
|
||||
case 7:
|
||||
return "fog"; //Fog
|
||||
case 8:
|
||||
return "showers"; //Light rain showers
|
||||
case 9:
|
||||
return "showers"; //Moderate rain showers
|
||||
case 10:
|
||||
return "showers"; //Heavy rain showers
|
||||
case 11:
|
||||
return "thunderstorm"; //Thunderstorm
|
||||
case 12:
|
||||
return "sleet"; //Light sleet showers
|
||||
case 13:
|
||||
return "sleet"; //Moderate sleet showers
|
||||
case 14:
|
||||
return "sleet"; //Heavy sleet showers
|
||||
case 15:
|
||||
return "snow"; //Light snow showers
|
||||
case 16:
|
||||
return "snow"; //Moderate snow showers
|
||||
case 17:
|
||||
return "snow"; //Heavy snow showers
|
||||
case 18:
|
||||
return "rain"; //Light rain
|
||||
case 19:
|
||||
return "rain"; //Moderate rain
|
||||
case 20:
|
||||
return "rain"; //Heavy rain
|
||||
case 21:
|
||||
return "thunderstorm"; //Thunder
|
||||
case 22:
|
||||
return "sleet"; // Light sleet
|
||||
case 23:
|
||||
return "sleet"; //Moderate sleet
|
||||
case 24:
|
||||
return "sleet"; // Heavy sleet
|
||||
case 25:
|
||||
return "snow"; // Light snowfall
|
||||
case 26:
|
||||
return "snow"; //Moderate snowfall
|
||||
case 27:
|
||||
return "snow"; //Heavy snowfall
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
});
|
@@ -73,7 +73,7 @@ WeatherProvider.register("ukmetoffice", {
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
// data times are always UTC
|
||||
let nowUtc = moment.utc();
|
||||
@@ -124,7 +124,7 @@ WeatherProvider.register("ukmetoffice", {
|
||||
// loop round the (5) periods getting the data
|
||||
// for each period array, Day is [0], Night is [1]
|
||||
for (var j in forecasts.SiteRep.DV.Location.Period) {
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
// data times are always UTC
|
||||
const dateStr = forecasts.SiteRep.DV.Location.Period[j].value;
|
||||
@@ -208,10 +208,10 @@ WeatherProvider.register("ukmetoffice", {
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert wind speed (from mph) if required
|
||||
* Convert wind speed (from mph to m/s or km/h) if required
|
||||
*/
|
||||
convertWindSpeed(windInMph) {
|
||||
return this.windUnits === "metric" ? windInMph * 2.23694 : windInMph;
|
||||
return this.windUnits === "metric" ? (this.useKmh ? windInMph * 1.60934 : windInMph / 2.23694) : windInMph;
|
||||
},
|
||||
|
||||
/*
|
||||
|
@@ -87,7 +87,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
Log.error("Possibly bad current/hourly data?");
|
||||
Log.info(data);
|
||||
Log.error(data);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
|
||||
// Create a WeatherObject using current weather data (data for the current hour)
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
// Extract the actual forecasts
|
||||
let forecastDataHours = currentWeatherData.features[0].properties.timeSeries;
|
||||
@@ -158,7 +158,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
Log.error("Possibly bad forecast data?");
|
||||
Log.info(data);
|
||||
Log.error(data);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
|
||||
// Go through each day in the forecasts
|
||||
for (day in forecastDataDays) {
|
||||
const forecastWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const forecastWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
// Get date of forecast
|
||||
let forecastDate = moment.utc(forecastDataDays[day].time);
|
||||
@@ -254,7 +254,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
return windInMpS;
|
||||
}
|
||||
|
||||
if (this.config.windUnits == "kph" || this.config.windUnits == "metric") {
|
||||
if (this.config.windUnits == "kph" || this.config.windUnits == "metric" || this.config.useKmh) {
|
||||
return windInMpS * 3.6;
|
||||
}
|
||||
|
||||
|
181
modules/default/weather/providers/weatherbit.js
Normal file
@@ -0,0 +1,181 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
* Provider: Weatherbit
|
||||
*
|
||||
* By Andrew Pometti
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Weatherbit, based on Nicholas Hubbard's class for Dark Sky & Vince Peri's class for Weather.gov.
|
||||
*/
|
||||
WeatherProvider.register("weatherbit", {
|
||||
// Set the name of the provider.
|
||||
// Not strictly required, but helps for debugging.
|
||||
providerName: "Weatherbit",
|
||||
|
||||
units: {
|
||||
imperial: "I",
|
||||
metric: "M"
|
||||
},
|
||||
|
||||
fetchedLocation: function () {
|
||||
return this.fetchedLocationName || "";
|
||||
},
|
||||
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data || !data.data[0] || typeof data.data[0].temp === "undefined") {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWeather = this.generateWeatherDayFromCurrentWeather(data);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data || !data.data) {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data.data);
|
||||
this.setWeatherForecast(forecast);
|
||||
|
||||
this.fetchedLocationName = data.city_name + ", " + data.state_code;
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Create a URL from the config and base URL.
|
||||
getUrl() {
|
||||
const units = this.units[this.config.units] || "auto";
|
||||
return `${this.config.apiBase}${this.config.weatherEndpoint}?lat=${this.config.lat}&lon=${this.config.lon}&units=${units}&key=${this.config.apiKey}`;
|
||||
},
|
||||
|
||||
// Implement WeatherDay generator.
|
||||
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
||||
//Calculate TZ Offset and invert to convert Sunrise/Sunset times to Local
|
||||
const d = new Date();
|
||||
let tzOffset = d.getTimezoneOffset();
|
||||
tzOffset = tzOffset * -1;
|
||||
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
currentWeather.date = moment(currentWeatherData.data[0].ts, "X");
|
||||
currentWeather.humidity = parseFloat(currentWeatherData.data[0].rh);
|
||||
currentWeather.temperature = parseFloat(currentWeatherData.data[0].temp);
|
||||
currentWeather.windSpeed = parseFloat(currentWeatherData.data[0].wind_spd);
|
||||
currentWeather.windDirection = currentWeatherData.data[0].wind_dir;
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.data[0].weather.icon);
|
||||
Log.log("Wx Icon: " + currentWeatherData.data[0].weather.icon);
|
||||
currentWeather.sunrise = moment(currentWeatherData.data[0].sunrise, "HH:mm").add(tzOffset, "m");
|
||||
currentWeather.sunset = moment(currentWeatherData.data[0].sunset, "HH:mm").add(tzOffset, "m");
|
||||
|
||||
this.fetchedLocationName = currentWeatherData.data[0].city_name + ", " + currentWeatherData.data[0].state_code;
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
const days = [];
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
weather.date = moment(forecast.datetime, "YYYY-MM-DD");
|
||||
weather.minTemperature = forecast.min_temp;
|
||||
weather.maxTemperature = forecast.max_temp;
|
||||
weather.precipitation = forecast.precip;
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather.icon);
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
// Map icons from Dark Sky to our icons.
|
||||
convertWeatherType(weatherType) {
|
||||
const weatherTypes = {
|
||||
t01d: "day-thunderstorm",
|
||||
t01n: "night-alt-thunderstorm",
|
||||
t02d: "day-thunderstorm",
|
||||
t02n: "night-alt-thunderstorm",
|
||||
t03d: "thunderstorm",
|
||||
t03n: "thunderstorm",
|
||||
t04d: "day-thunderstorm",
|
||||
t04n: "night-alt-thunderstorm",
|
||||
t05d: "day-sleet-storm",
|
||||
t05n: "night-alt-sleet-storm",
|
||||
d01d: "day-sprinkle",
|
||||
d01n: "night-alt-sprinkle",
|
||||
d02d: "day-sprinkle",
|
||||
d02n: "night-alt-sprinkle",
|
||||
d03d: "day-shower",
|
||||
d03n: "night-alt-shower",
|
||||
r01d: "day-shower",
|
||||
r01n: "night-alt-shower",
|
||||
r02d: "day-rain",
|
||||
r02n: "night-alt-rain",
|
||||
r03d: "day-rain",
|
||||
r03n: "night-alt-rain",
|
||||
r04d: "day-sprinkle",
|
||||
r04n: "night-alt-sprinkle",
|
||||
r05d: "day-shower",
|
||||
r05n: "night-alt-shower",
|
||||
r06d: "day-shower",
|
||||
r06n: "night-alt-shower",
|
||||
f01d: "day-sleet",
|
||||
f01n: "night-alt-sleet",
|
||||
s01d: "day-snow",
|
||||
s01n: "night-alt-snow",
|
||||
s02d: "day-snow-wind",
|
||||
s02n: "night-alt-snow-wind",
|
||||
s03d: "snowflake-cold",
|
||||
s03n: "snowflake-cold",
|
||||
s04d: "day-rain-mix",
|
||||
s04n: "night-alt-rain-mix",
|
||||
s05d: "day-sleet",
|
||||
s05n: "night-alt-sleet",
|
||||
s06d: "day-snow",
|
||||
s06n: "night-alt-snow",
|
||||
a01d: "day-haze",
|
||||
a01n: "dust",
|
||||
a02d: "smoke",
|
||||
a02n: "smoke",
|
||||
a03d: "day-haze",
|
||||
a03n: "dust",
|
||||
a04d: "dust",
|
||||
a04n: "dust",
|
||||
a05d: "day-fog",
|
||||
a05n: "night-fog",
|
||||
a06d: "fog",
|
||||
a06n: "fog",
|
||||
c01d: "day-sunny",
|
||||
c01n: "night-clear",
|
||||
c02d: "day-sunny-overcast",
|
||||
c02n: "night-alt-partly-cloudy",
|
||||
c03d: "day-cloudy",
|
||||
c03n: "night-alt-cloudy",
|
||||
c04d: "cloudy",
|
||||
c04n: "cloudy",
|
||||
u00d: "rain-mix",
|
||||
u00n: "rain-mix"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
}
|
||||
});
|
@@ -3,76 +3,155 @@
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
* Provider: weather.gov
|
||||
* https://weather-gov.github.io/api/general-faqs
|
||||
*
|
||||
* By Vince Peri
|
||||
* Original by Vince Peri
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is a provider for weather.gov.
|
||||
* Note that this is only for US locations (lat and lon) and does not require an API key
|
||||
* Since it is free, there are some items missing - like sunrise, sunset, humidity, etc.
|
||||
* Since it is free, there are some items missing - like sunrise, sunset
|
||||
*/
|
||||
|
||||
WeatherProvider.register("weathergov", {
|
||||
// Set the name of the provider.
|
||||
// This isn't strictly necessary, since it will fallback to the provider identifier
|
||||
// But for debugging (and future alerts) it would be nice to have the real name.
|
||||
providerName: "Weather.gov",
|
||||
|
||||
// Flag all needed URLs availability
|
||||
configURLs: false,
|
||||
|
||||
//This API has multiple urls involved
|
||||
forecastURL: "tbd",
|
||||
forecastHourlyURL: "tbd",
|
||||
forecastGridDataURL: "tbd",
|
||||
observationStationsURL: "tbd",
|
||||
stationObsURL: "tbd",
|
||||
|
||||
// Called to set the config, this config is the same as the weather module's config.
|
||||
setConfig: function (config) {
|
||||
this.config = config;
|
||||
(this.config.apiBase = "https://api.weather.gov"), this.fetchWxGovURLs(this.config);
|
||||
},
|
||||
|
||||
// Called when the weather provider is about to start.
|
||||
start: function () {
|
||||
Log.info(`Weather provider: ${this.providerName} started.`);
|
||||
},
|
||||
|
||||
// This returns the name of the fetched location or an empty string.
|
||||
fetchedLocation: function () {
|
||||
return this.fetchedLocationName || "";
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
if (!this.configURLs) {
|
||||
Log.info("fetch wx waiting on config URLs");
|
||||
return;
|
||||
}
|
||||
this.fetchData(this.stationObsURL)
|
||||
.then((data) => {
|
||||
if (!data || !data.properties || !data.properties.periods || !data.properties.periods.length) {
|
||||
if (!data || !data.properties) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data.properties.periods[0]);
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data.properties);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
Log.error("Could not load station obs data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
// Overwrite the fetchWeatherForecast method.
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
if (!this.configURLs) {
|
||||
Log.info("fetch wx waiting on config URLs");
|
||||
return;
|
||||
}
|
||||
this.fetchData(this.forecastURL)
|
||||
.then((data) => {
|
||||
if (!data || !data.properties || !data.properties.periods || !data.properties.periods.length) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data.properties.periods);
|
||||
this.setWeatherForecast(forecast);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
Log.error("Could not load forecast hourly data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/** Weather.gov Specific Methods - These are not part of the default provider methods */
|
||||
|
||||
/*
|
||||
* Gets the complete url for the request
|
||||
* Get specific URLs
|
||||
*/
|
||||
getUrl() {
|
||||
return this.config.apiBase + this.config.lat + "," + this.config.lon + this.config.weatherEndpoint;
|
||||
fetchWxGovURLs(config) {
|
||||
this.fetchData(`${config.apiBase}/points/${config.lat},${config.lon}`)
|
||||
.then((data) => {
|
||||
if (!data || !data.properties) {
|
||||
// points URL did not respond with usable data.
|
||||
return;
|
||||
}
|
||||
this.fetchedLocationName = data.properties.relativeLocation.properties.city + ", " + data.properties.relativeLocation.properties.state;
|
||||
Log.log("Forecast location is " + this.fetchedLocationName);
|
||||
this.forecastURL = data.properties.forecast;
|
||||
this.forecastHourlyURL = data.properties.forecastHourly;
|
||||
this.forecastGridDataURL = data.properties.forecastGridData;
|
||||
this.observationStationsURL = data.properties.observationStations;
|
||||
// with this URL, we chain another promise for the station obs URL
|
||||
return this.fetchData(data.properties.observationStations);
|
||||
})
|
||||
.then((obsData) => {
|
||||
if (!obsData || !obsData.features) {
|
||||
// obs station URL did not respond with usable data.
|
||||
return;
|
||||
}
|
||||
this.stationObsURL = obsData.features[0].id + "/observations/latest";
|
||||
})
|
||||
.catch((err) => {
|
||||
Log.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
// excellent, let's fetch some actual wx data
|
||||
this.configURLs = true;
|
||||
this.fetchCurrentWeather();
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
* Weather.gov API uses specific units; API does not include choice of units
|
||||
* ... object needs data in units based on config!
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
currentWeather.temperature = currentWeatherData.temperature;
|
||||
currentWeather.windSpeed = currentWeatherData.windSpeed.split(" ", 1);
|
||||
currentWeather.windDirection = this.convertWindDirection(currentWeatherData.windDirection);
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.shortForecast, currentWeatherData.isDaytime);
|
||||
currentWeather.date = moment(currentWeatherData.timestamp);
|
||||
currentWeather.temperature = this.convertTemp(currentWeatherData.temperature.value);
|
||||
currentWeather.windSpeed = this.convertSpeed(currentWeatherData.windSpeed.value);
|
||||
currentWeather.windDirection = currentWeatherData.windDirection.value;
|
||||
currentWeather.minTemperature = this.convertTemp(currentWeatherData.minTemperatureLast24Hours.value);
|
||||
currentWeather.maxTemperature = this.convertTemp(currentWeatherData.maxTemperatureLast24Hours.value);
|
||||
currentWeather.humidity = Math.round(currentWeatherData.relativeHumidity.value);
|
||||
currentWeather.rain = null;
|
||||
currentWeather.snow = null;
|
||||
currentWeather.precipitation = this.convertLength(currentWeatherData.precipitationLastHour.value);
|
||||
currentWeather.feelsLikeTemp = this.convertTemp(currentWeatherData.heatIndex.value);
|
||||
|
||||
let isDaytime = true;
|
||||
if (currentWeatherData.icon.includes("day")) {
|
||||
isDaytime = true;
|
||||
} else {
|
||||
isDaytime = false;
|
||||
}
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.textDescription, isDaytime);
|
||||
|
||||
// determine the sunrise/sunset times - not supplied in weather.gov data
|
||||
let times = this.calcAstroData(this.config.lat, this.config.lon);
|
||||
@@ -100,7 +179,7 @@ WeatherProvider.register("weathergov", {
|
||||
let maxTemp = [];
|
||||
// variable for date
|
||||
let date = "";
|
||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
weather.precipitation = 0;
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
@@ -112,7 +191,7 @@ WeatherProvider.register("weathergov", {
|
||||
// push weather information to days array
|
||||
days.push(weather);
|
||||
// create new weather-object
|
||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
minTemp = [];
|
||||
maxTemp = [];
|
||||
@@ -124,7 +203,7 @@ WeatherProvider.register("weathergov", {
|
||||
// specify date
|
||||
weather.date = moment(forecast.startTime);
|
||||
|
||||
// If the first value of today is later than 17:00, we have an icon at least!
|
||||
// use the forecast isDayTime attribute to help build the weatherType label
|
||||
weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime);
|
||||
}
|
||||
|
||||
@@ -148,6 +227,38 @@ WeatherProvider.register("weathergov", {
|
||||
return days.slice(1);
|
||||
},
|
||||
|
||||
/*
|
||||
* Unit conversions
|
||||
*/
|
||||
// conversion to fahrenheit
|
||||
convertTemp(temp) {
|
||||
if (this.config.tempUnits === "imperial") {
|
||||
return (9 / 5) * temp + 32;
|
||||
} else {
|
||||
return temp;
|
||||
}
|
||||
},
|
||||
// conversion to mph or kmh
|
||||
convertSpeed(metSec) {
|
||||
if (this.config.windUnits === "imperial") {
|
||||
return metSec * 2.23694;
|
||||
} else {
|
||||
if (this.config.useKmh) {
|
||||
return metSec * 3.6;
|
||||
} else {
|
||||
return metSec;
|
||||
}
|
||||
}
|
||||
},
|
||||
// conversion to inches
|
||||
convertLength(meters) {
|
||||
if (this.config.units === "imperial") {
|
||||
return meters * 39.3701;
|
||||
} else {
|
||||
return meters;
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Calculate the astronomical data
|
||||
*/
|
||||
|
@@ -11,16 +11,15 @@ Module.register("weather", {
|
||||
defaults: {
|
||||
weatherProvider: "openweathermap",
|
||||
roundTemp: false,
|
||||
type: "current", //current, forecast
|
||||
|
||||
type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint)
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
location: false,
|
||||
locationID: false,
|
||||
appid: "",
|
||||
units: config.units,
|
||||
|
||||
useKmh: false,
|
||||
tempUnits: config.units,
|
||||
windUnits: config.units,
|
||||
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
animationSpeed: 1000,
|
||||
timeFormat: config.timeFormat,
|
||||
@@ -37,24 +36,24 @@ Module.register("weather", {
|
||||
showIndoorTemperature: false,
|
||||
showIndoorHumidity: false,
|
||||
maxNumberOfDays: 5,
|
||||
maxEntries: 5,
|
||||
fade: true,
|
||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
||||
|
||||
initialLoadDelay: 0, // 0 seconds delay
|
||||
retryDelay: 2500,
|
||||
|
||||
apiKey: "",
|
||||
apiSecret: "",
|
||||
apiVersion: "2.5",
|
||||
apiBase: "https://api.openweathermap.org/data/",
|
||||
apiBase: "https://api.openweathermap.org/data/", // TODO: this should not be part of the weather.js file, but should be contained in the openweatherprovider
|
||||
weatherEndpoint: "/weather",
|
||||
|
||||
appendLocationNameToHeader: true,
|
||||
calendarClass: "calendar",
|
||||
tableClass: "small",
|
||||
|
||||
onlyTemp: false,
|
||||
showPrecipitationAmount: false,
|
||||
colored: false,
|
||||
showFeelsLike: true
|
||||
showFeelsLike: true,
|
||||
feelsLikeWithDegree: false
|
||||
},
|
||||
|
||||
// Module properties.
|
||||
@@ -72,11 +71,12 @@ Module.register("weather", {
|
||||
|
||||
// Override getHeader method.
|
||||
getHeader: function () {
|
||||
if (this.config.appendLocationNameToHeader && this.data.header !== undefined && this.weatherProvider) {
|
||||
return this.data.header + " " + this.weatherProvider.fetchedLocation();
|
||||
if (this.config.appendLocationNameToHeader && this.weatherProvider) {
|
||||
if (this.data.header) return this.data.header + " " + this.weatherProvider.fetchedLocation();
|
||||
else return this.weatherProvider.fetchedLocation();
|
||||
}
|
||||
|
||||
return this.data.header;
|
||||
return this.data.header ? this.data.header : "";
|
||||
},
|
||||
|
||||
// Start the weather module.
|
||||
@@ -89,6 +89,8 @@ Module.register("weather", {
|
||||
// Let the weather provider know we are starting.
|
||||
this.weatherProvider.start();
|
||||
|
||||
this.config.feelsLikeWithDegree = this.translate("FEELS").indexOf("{DEGREE}") > -1;
|
||||
|
||||
// Add custom filters
|
||||
this.addFilters();
|
||||
|
||||
@@ -123,7 +125,17 @@ Module.register("weather", {
|
||||
|
||||
// Select the template depending on the display type.
|
||||
getTemplate: function () {
|
||||
return `${this.config.type.toLowerCase()}.njk`;
|
||||
switch (this.config.type.toLowerCase()) {
|
||||
case "current":
|
||||
return `current.njk`;
|
||||
case "hourly":
|
||||
return `hourly.njk`;
|
||||
case "daily":
|
||||
case "forecast":
|
||||
return `forecast.njk`;
|
||||
default:
|
||||
return `${this.config.type.toLowerCase()}.njk`;
|
||||
}
|
||||
},
|
||||
|
||||
// Add all the data to the template.
|
||||
@@ -132,6 +144,7 @@ Module.register("weather", {
|
||||
config: this.config,
|
||||
current: this.weatherProvider.currentWeather(),
|
||||
forecast: this.weatherProvider.weatherForecast(),
|
||||
weatherData: this.weatherProvider.weatherData(),
|
||||
indoor: {
|
||||
humidity: this.indoorHumidity,
|
||||
temperature: this.indoorTemperature
|
||||
@@ -153,7 +166,9 @@ Module.register("weather", {
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.config.type === "forecast") {
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
this.weatherProvider.fetchWeatherData();
|
||||
} else if (this.config.type === "forecast") {
|
||||
this.weatherProvider.fetchWeatherForecast();
|
||||
} else {
|
||||
this.weatherProvider.fetchCurrentWeather();
|
||||
@@ -205,7 +220,7 @@ Module.register("weather", {
|
||||
}
|
||||
}
|
||||
} else if (type === "precip") {
|
||||
if (isNaN(value) || value === 0 || value.toFixed(2) === "0.00") {
|
||||
if (value === null || isNaN(value) || value === 0 || value.toFixed(2) === "0.00") {
|
||||
value = "";
|
||||
} else {
|
||||
if (this.config.weatherProvider === "ukmetoffice" || this.config.weatherProvider === "ukmetofficedatahub") {
|
||||
@@ -243,6 +258,13 @@ Module.register("weather", {
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
this.nunjucksEnvironment().addFilter(
|
||||
"calcNumEntries",
|
||||
function (dataArray) {
|
||||
return Math.min(dataArray.length, this.config.maxEntries);
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
this.nunjucksEnvironment().addFilter(
|
||||
"opacity",
|
||||
function (currentStep, numSteps) {
|
||||
|
@@ -10,10 +10,11 @@
|
||||
* As soon as we start implementing the forecast, mode properties will be added.
|
||||
*/
|
||||
class WeatherObject {
|
||||
constructor(units, tempUnits, windUnits) {
|
||||
constructor(units, tempUnits, windUnits, useKmh) {
|
||||
this.units = units;
|
||||
this.tempUnits = tempUnits;
|
||||
this.windUnits = windUnits;
|
||||
this.useKmh = useKmh;
|
||||
this.date = null;
|
||||
this.windSpeed = null;
|
||||
this.windDirection = null;
|
||||
@@ -67,7 +68,7 @@ class WeatherObject {
|
||||
}
|
||||
|
||||
beaufortWindSpeed() {
|
||||
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : (this.windSpeed * 60 * 60) / 1000;
|
||||
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : this.useKmh ? this.windSpeed : (this.windSpeed * 60 * 60) / 1000;
|
||||
const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
|
||||
for (const [index, speed] of speeds.entries()) {
|
||||
if (speed > windInKmh) {
|
||||
@@ -77,6 +78,11 @@ class WeatherObject {
|
||||
return 12;
|
||||
}
|
||||
|
||||
kmhWindSpeed() {
|
||||
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : (this.windSpeed * 60 * 60) / 1000;
|
||||
return windInKmh;
|
||||
}
|
||||
|
||||
nextSunAction() {
|
||||
return moment().isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise";
|
||||
}
|
||||
|
@@ -12,10 +12,11 @@ var WeatherProvider = Class.extend({
|
||||
// Weather Provider Properties
|
||||
providerName: null,
|
||||
|
||||
// The following properties have accestor methods.
|
||||
// The following properties have accessor methods.
|
||||
// Try to not access them directly.
|
||||
currentWeatherObject: null,
|
||||
weatherForecastArray: null,
|
||||
weatherDataObject: null,
|
||||
fetchedLocationName: null,
|
||||
|
||||
// The following properties will be set automatically.
|
||||
@@ -56,6 +57,12 @@ var WeatherProvider = Class.extend({
|
||||
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherForecast method.`);
|
||||
},
|
||||
|
||||
// This method should start the API request to fetch the weather forecast.
|
||||
// This method should definitely be overwritten in the provider.
|
||||
fetchWeatherData: function () {
|
||||
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherData method.`);
|
||||
},
|
||||
|
||||
// This returns a WeatherDay object for the current weather.
|
||||
currentWeather: function () {
|
||||
return this.currentWeatherObject;
|
||||
@@ -66,6 +73,11 @@ var WeatherProvider = Class.extend({
|
||||
return this.weatherForecastArray;
|
||||
},
|
||||
|
||||
// This returns an object containing WeatherDay object(s) depending on the type of call.
|
||||
weatherData: function () {
|
||||
return this.weatherDataObject;
|
||||
},
|
||||
|
||||
// This returns the name of the fetched location or an empty string.
|
||||
fetchedLocation: function () {
|
||||
return this.fetchedLocationName || "";
|
||||
@@ -83,6 +95,11 @@ var WeatherProvider = Class.extend({
|
||||
this.weatherForecastArray = weatherForecastArray;
|
||||
},
|
||||
|
||||
// Set the weatherDataObject and notify the delegate that new information is available.
|
||||
setWeatherData: function (weatherDataObject) {
|
||||
this.weatherDataObject = weatherDataObject;
|
||||
},
|
||||
|
||||
// Set the fetched location name.
|
||||
setFetchedLocation: function (name) {
|
||||
this.fetchedLocationName = name;
|
||||
@@ -119,6 +136,9 @@ WeatherProvider.providers = [];
|
||||
|
||||
/**
|
||||
* Static method to register a new weather provider.
|
||||
*
|
||||
* @param {string} providerIdentifier The name of the weather provider
|
||||
* @param {object} providerDetails The details of the weather provider
|
||||
*/
|
||||
WeatherProvider.register = function (providerIdentifier, providerDetails) {
|
||||
WeatherProvider.providers[providerIdentifier.toLowerCase()] = WeatherProvider.extend(providerDetails);
|
||||
@@ -126,6 +146,10 @@ WeatherProvider.register = function (providerIdentifier, providerDetails) {
|
||||
|
||||
/**
|
||||
* Static method to initialize a new weather provider.
|
||||
*
|
||||
* @param {string} providerIdentifier The name of the weather provider
|
||||
* @param {object} delegate The weather module
|
||||
* @returns {object} The new weather provider
|
||||
*/
|
||||
WeatherProvider.initialize = function (providerIdentifier, delegate) {
|
||||
providerIdentifier = providerIdentifier.toLowerCase();
|
||||
|
@@ -9,6 +9,8 @@ Module.register("weatherforecast", {
|
||||
defaults: {
|
||||
location: false,
|
||||
locationID: false,
|
||||
lat: false,
|
||||
lon: false,
|
||||
appid: "",
|
||||
units: config.units,
|
||||
maxNumberOfDays: 7,
|
||||
@@ -29,6 +31,7 @@ Module.register("weatherforecast", {
|
||||
apiVersion: "2.5",
|
||||
apiBase: "https://api.openweathermap.org/data/",
|
||||
forecastEndpoint: "forecast/daily",
|
||||
excludes: false,
|
||||
|
||||
appendLocationNameToHeader: true,
|
||||
calendarClass: "calendar",
|
||||
@@ -100,7 +103,7 @@ Module.register("weatherforecast", {
|
||||
getDom: function () {
|
||||
var wrapper = document.createElement("div");
|
||||
|
||||
if (this.config.appid === "") {
|
||||
if (this.config.appid === "" || this.config.appid === "YOUR_OPENWEATHER_API_KEY") {
|
||||
wrapper.innerHTML = "Please set the correct openweather <i>appid</i> in the config for module: " + this.name + ".";
|
||||
wrapper.className = "dimmed light small";
|
||||
return wrapper;
|
||||
@@ -203,10 +206,11 @@ Module.register("weatherforecast", {
|
||||
// Override getHeader method.
|
||||
getHeader: function () {
|
||||
if (this.config.appendLocationNameToHeader) {
|
||||
return this.data.header + " " + this.fetchedLocationName;
|
||||
if (this.data.header) return this.data.header + " " + this.fetchedLocationName;
|
||||
else return this.fetchedLocationName;
|
||||
}
|
||||
|
||||
return this.data.header;
|
||||
return this.data.header ? this.data.header : "";
|
||||
},
|
||||
|
||||
// Override notification handler.
|
||||
@@ -283,6 +287,8 @@ Module.register("weatherforecast", {
|
||||
var params = "?";
|
||||
if (this.config.locationID) {
|
||||
params += "id=" + this.config.locationID;
|
||||
} else if (this.config.lat && this.config.lon) {
|
||||
params += "lat=" + this.config.lat + "&lon=" + this.config.lon;
|
||||
} else if (this.config.location) {
|
||||
params += "q=" + this.config.location;
|
||||
} else if (this.firstEvent && this.firstEvent.geo) {
|
||||
@@ -294,8 +300,17 @@ Module.register("weatherforecast", {
|
||||
return;
|
||||
}
|
||||
|
||||
params += "&cnt=" + (this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 17 ? 7 : this.config.maxNumberOfDays);
|
||||
let numberOfDays;
|
||||
if (this.config.forecastEndpoint === "forecast") {
|
||||
numberOfDays = this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 5 ? 5 : this.config.maxNumberOfDays;
|
||||
// don't get forecasts for the next day, as it would not represent the whole day
|
||||
numberOfDays = numberOfDays * 8 - (Math.round(new Date().getHours() / 3) % 8);
|
||||
} else {
|
||||
numberOfDays = this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 17 ? 7 : this.config.maxNumberOfDays;
|
||||
}
|
||||
params += "&cnt=" + numberOfDays;
|
||||
|
||||
params += "&exclude=" + this.config.excludes;
|
||||
params += "&units=" + this.config.units;
|
||||
params += "&lang=" + this.config.lang;
|
||||
params += "&APPID=" + this.config.appid;
|
||||
@@ -323,24 +338,43 @@ Module.register("weatherforecast", {
|
||||
* argument data object - Weather information received form openweather.org.
|
||||
*/
|
||||
processWeather: function (data) {
|
||||
this.fetchedLocationName = data.city.name + ", " + data.city.country;
|
||||
// Forcast16 (paid) API endpoint provides this data. Onecall endpoint
|
||||
// does not.
|
||||
if (data.city) {
|
||||
this.fetchedLocationName = data.city.name + ", " + data.city.country;
|
||||
} else if (this.config.location) {
|
||||
this.fetchedLocationName = this.config.location;
|
||||
} else {
|
||||
this.fetchedLocationName = "Unknown";
|
||||
}
|
||||
|
||||
this.forecast = [];
|
||||
var lastDay = null;
|
||||
var forecastData = {};
|
||||
|
||||
for (var i = 0, count = data.list.length; i < count; i++) {
|
||||
var forecast = data.list[i];
|
||||
this.parserDataWeather(forecast); // hack issue #1017
|
||||
// Handle different structs between forecast16 and onecall endpoints
|
||||
var forecastList = null;
|
||||
if (data.list) {
|
||||
forecastList = data.list;
|
||||
} else if (data.daily) {
|
||||
forecastList = data.daily;
|
||||
} else {
|
||||
Log.error("Unexpected forecast data");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (var i = 0, count = forecastList.length; i < count; i++) {
|
||||
var forecast = forecastList[i];
|
||||
forecast = this.parserDataWeather(forecast); // hack issue #1017
|
||||
|
||||
var day;
|
||||
var hour;
|
||||
if (forecast.dt_txt) {
|
||||
day = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd");
|
||||
hour = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("H");
|
||||
hour = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").toDate().getHours();
|
||||
} else {
|
||||
day = moment(forecast.dt, "X").format("ddd");
|
||||
hour = moment(forecast.dt, "X").format("H");
|
||||
hour = moment(forecast.dt, "X").toDate().getHours();
|
||||
}
|
||||
|
||||
if (day !== lastDay) {
|
||||
@@ -349,9 +383,8 @@ Module.register("weatherforecast", {
|
||||
icon: this.config.iconTable[forecast.weather[0].icon],
|
||||
maxTemp: this.roundValue(forecast.temp.max),
|
||||
minTemp: this.roundValue(forecast.temp.min),
|
||||
rain: this.processRain(forecast, data.list)
|
||||
rain: this.processRain(forecast, forecastList)
|
||||
};
|
||||
|
||||
this.forecast.push(forecastData);
|
||||
lastDay = day;
|
||||
|
||||
|
18293
package-lock.json
generated
68
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "magicmirror",
|
||||
"version": "2.12.0",
|
||||
"version": "2.14.0",
|
||||
"description": "The open source modular smart mirror platform.",
|
||||
"main": "js/electron.js",
|
||||
"scripts": {
|
||||
@@ -9,9 +9,10 @@
|
||||
"install": "echo \"Installing vendor files ...\n\" && cd vendor && npm install --loglevel=error",
|
||||
"install-fonts": "echo \"Installing fonts ...\n\" && cd fonts && npm install --loglevel=error",
|
||||
"postinstall": "npm run install-fonts && echo \"MagicMirror installation finished successfully! \n\"",
|
||||
"test": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests --recursive",
|
||||
"test:unit": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests/unit --recursive",
|
||||
"test:e2e": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests/e2e --recursive",
|
||||
"test": "NODE_ENV=test mocha tests --recursive",
|
||||
"test:coverage": "NODE_ENV=test nyc mocha tests --recursive --timeout=3000",
|
||||
"test:e2e": "NODE_ENV=test mocha tests/e2e --recursive",
|
||||
"test:unit": "NODE_ENV=test mocha tests/unit --recursive",
|
||||
"test:prettier": "prettier --check **/*.{js,css,json,md,yml}",
|
||||
"test:js": "eslint *.js js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --quiet",
|
||||
"test:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json",
|
||||
@@ -41,49 +42,50 @@
|
||||
},
|
||||
"homepage": "https://magicmirror.builders",
|
||||
"devDependencies": {
|
||||
"@prantlf/jsonlint": "^10.2.0",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"current-week-number": "^1.0.7",
|
||||
"danger": "^3.1.3",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"http-auth": "^3.2.3",
|
||||
"husky": "^4.2.5",
|
||||
"jsdom": "^11.6.2",
|
||||
"mocha": "^7.1.2",
|
||||
"danger": "^10.5.4",
|
||||
"eslint-config-prettier": "^7.0.0",
|
||||
"eslint-plugin-jsdoc": "^30.7.8",
|
||||
"eslint-plugin-prettier": "^3.2.0",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"husky": "^4.3.5",
|
||||
"jsdom": "^16.4.0",
|
||||
"lodash": "^4.17.20",
|
||||
"mocha": "^8.2.1",
|
||||
"mocha-each": "^2.0.1",
|
||||
"mocha-logger": "^1.0.6",
|
||||
"prettier": "^2.0.5",
|
||||
"pretty-quick": "^2.0.1",
|
||||
"spectron": "^8.0.0",
|
||||
"stylelint": "^13.6.1",
|
||||
"mocha-logger": "^1.0.7",
|
||||
"nyc": "^15.1.0",
|
||||
"prettier": "^2.2.1",
|
||||
"pretty-quick": "^3.1.0",
|
||||
"spectron": "^10.0.1",
|
||||
"stylelint": "^13.8.0",
|
||||
"stylelint-config-prettier": "^8.0.2",
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"stylelint-prettier": "^1.1.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"electron": "^6.1.7"
|
||||
"electron": "^8.5.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"colors": "^1.1.2",
|
||||
"console-stamp": "^0.2.9",
|
||||
"eslint": "^7.3.0",
|
||||
"express": "^4.16.2",
|
||||
"express-ipfilter": "^1.0.1",
|
||||
"feedme": "latest",
|
||||
"helmet": "^3.21.2",
|
||||
"colors": "^1.4.0",
|
||||
"console-stamp": "^3.0.0-rc4.2",
|
||||
"eslint": "^7.15.0",
|
||||
"express": "^4.17.1",
|
||||
"express-ipfilter": "^1.1.2",
|
||||
"feedme": "^2.0.2",
|
||||
"helmet": "^4.2.0",
|
||||
"ical": "^0.8.0",
|
||||
"iconv-lite": "latest",
|
||||
"lodash": "^4.17.15",
|
||||
"iconv-lite": "^0.6.2",
|
||||
"module-alias": "^2.2.2",
|
||||
"moment": "latest",
|
||||
"moment": "^2.29.1",
|
||||
"node-ical": "^0.12.7",
|
||||
"request": "^2.88.2",
|
||||
"rrule": "^2.6.2",
|
||||
"rrule": "^2.6.6",
|
||||
"rrule-alt": "^2.2.8",
|
||||
"simple-git": "^1.85.0",
|
||||
"socket.io": "^2.1.1",
|
||||
"valid-url": "latest"
|
||||
"simple-git": "^2.31.0",
|
||||
"socket.io": "^3.0.4",
|
||||
"valid-url": "^1.0.9"
|
||||
},
|
||||
"_moduleAliases": {
|
||||
"node_helper": "js/node_helper.js"
|
||||
|
@@ -1,13 +0,0 @@
|
||||
{
|
||||
// Escaped
|
||||
"FOO\"BAR": "Today",
|
||||
|
||||
/*
|
||||
* The following lines
|
||||
* represent cardinal directions
|
||||
*/
|
||||
"N": "N",
|
||||
"E": "E",
|
||||
"S": "S",
|
||||
"W": "W"
|
||||
}
|
56
tests/configs/data/calendar_test_icons.ics
Normal file
@@ -0,0 +1,56 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//ical.marudot.com//iCal Event Maker
|
||||
X-WR-CALNAME:TestEvents
|
||||
NAME:TestEvents
|
||||
CALSCALE:GREGORIAN
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Berlin
|
||||
TZURL:http://tzurl.org/zoneinfo-outlook/Europe/Berlin
|
||||
X-LIC-LOCATION:Europe/Berlin
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
TZNAME:CEST
|
||||
DTSTART:19700329T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
TZNAME:CET
|
||||
DTSTART:19701025T030000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20200719T094531Z
|
||||
UID:20200719T094531Z-1871115387@marudot.com
|
||||
DTSTART;TZID=Europe/Berlin:20300101T120000
|
||||
DTEND;TZID=Europe/Berlin:20300101T130000
|
||||
SUMMARY:TestEvent
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20200719T094531Z
|
||||
UID:20200719T094531Z-1929725136@marudot.com
|
||||
DTSTART;TZID=Europe/Berlin:20300701T120000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=7;BYMONTHDAY=1
|
||||
DTEND;TZID=Europe/Berlin:20300701T130000
|
||||
SUMMARY:TestEventRepeat
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20200719T094531Z
|
||||
UID:20200719T094531Z-371801474@marudot.com
|
||||
DTSTART;VALUE=DATE:20300401
|
||||
DTEND;VALUE=DATE:20300402
|
||||
SUMMARY:TestEventDay
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20200719T094531Z
|
||||
UID:20200719T094531Z-133401084@marudot.com
|
||||
DTSTART;VALUE=DATE:20301001
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYMONTHDAY=1
|
||||
DTEND;VALUE=DATE:20301002
|
||||
SUMMARY:TestEventRepeatDay
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
41
tests/configs/modules/calendar/custom.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/* Magic Mirror Test config custom calendar
|
||||
*
|
||||
* MIT Licensed.
|
||||
*/
|
||||
let config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
module: "calendar",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
calendars: [
|
||||
{
|
||||
maximumEntries: 4,
|
||||
maximumNumberOfDays: 10000,
|
||||
symbol: "birthday-cake",
|
||||
fullDaySymbol: "calendar-day",
|
||||
recurringSymbol: "undo",
|
||||
url: "http://localhost:8080/tests/configs/data/calendar_test_icons.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
33
tests/configs/modules/clock/clock_analog.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/* Magic Mirror Test config for analog clock face
|
||||
*
|
||||
* MIT Licensed.
|
||||
*/
|
||||
let config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
module: "clock",
|
||||
position: "middle_center",
|
||||
config: {
|
||||
displayType: "analog",
|
||||
analogFace: "face-006"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
42
tests/configs/modules/display.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/* Magic Mirror Test config for display setters module using the helloworld module
|
||||
*
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
fullscreen: false,
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
module: "helloworld",
|
||||
position: "top_bar",
|
||||
header: "test_header",
|
||||
config: {
|
||||
text: "Test Display Header"
|
||||
}
|
||||
},
|
||||
{
|
||||
module: "helloworld",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
text: "Test Hide Header"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
@@ -20,7 +20,7 @@ var config = {
|
||||
},
|
||||
|
||||
modules:
|
||||
// Using exotic content. This is why dont accept go to JSON configuration file
|
||||
// Using exotic content. This is why don't accept go to JSON configuration file
|
||||
(function () {
|
||||
var positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||
var modules = Array();
|
||||
|
@@ -1,5 +1,6 @@
|
||||
const helpers = require("../global-setup");
|
||||
const serverBasicAuth = require("../../servers/basic-auth.js");
|
||||
const expect = require("chai").expect;
|
||||
|
||||
const describe = global.describe;
|
||||
const it = global.it;
|
||||
@@ -31,8 +32,47 @@ describe("Calendar module", function () {
|
||||
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/default.js";
|
||||
});
|
||||
|
||||
it("Should return TestEvents", function () {
|
||||
return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
|
||||
it("should show the default maximumEntries of 10", async () => {
|
||||
await app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
|
||||
const events = await app.client.$$(".calendar .event");
|
||||
return expect(events.length).equals(10);
|
||||
});
|
||||
|
||||
it("should show the default calendar symbol in each event", async () => {
|
||||
await app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
|
||||
const icons = await app.client.$$(".calendar .event .fa-calendar");
|
||||
return expect(icons.length).not.equals(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Custom configuration", function () {
|
||||
before(function () {
|
||||
// Set config sample for use in test
|
||||
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/custom.js";
|
||||
});
|
||||
|
||||
it("should show the custom maximumEntries of 4", async () => {
|
||||
await app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
|
||||
const events = await app.client.$$(".calendar .event");
|
||||
return expect(events.length).equals(4);
|
||||
});
|
||||
|
||||
it("should show the custom calendar symbol in each event", async () => {
|
||||
await app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
|
||||
const icons = await app.client.$$(".calendar .event .fa-birthday-cake");
|
||||
return expect(icons.length).equals(4);
|
||||
});
|
||||
|
||||
it("should show two custom icons for repeating events", async () => {
|
||||
await app.client.waitUntilTextExists(".calendar", "TestEventRepeat", 10000);
|
||||
const icons = await app.client.$$(".calendar .event .fa-undo");
|
||||
return expect(icons.length).equals(2);
|
||||
});
|
||||
|
||||
it("should show two custom icons for day events", async () => {
|
||||
await app.client.waitUntilTextExists(".calendar", "TestEventDay", 10000);
|
||||
const icons = await app.client.$$(".calendar .event .fa-calendar-day");
|
||||
return expect(icons.length).equals(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +87,7 @@ describe("Calendar module", function () {
|
||||
serverBasicAuth.close(done());
|
||||
});
|
||||
|
||||
it("Should return TestEvents", function () {
|
||||
it("should return TestEvents", function () {
|
||||
return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
|
||||
});
|
||||
});
|
||||
@@ -63,7 +103,7 @@ describe("Calendar module", function () {
|
||||
serverBasicAuth.close(done());
|
||||
});
|
||||
|
||||
it("Should return TestEvents", function () {
|
||||
it("should return TestEvents", function () {
|
||||
return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
|
||||
});
|
||||
});
|
||||
@@ -79,7 +119,7 @@ describe("Calendar module", function () {
|
||||
serverBasicAuth.close(done());
|
||||
});
|
||||
|
||||
it("Should return TestEvents", function () {
|
||||
it("should return TestEvents", function () {
|
||||
return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
|
||||
});
|
||||
});
|
||||
@@ -95,7 +135,7 @@ describe("Calendar module", function () {
|
||||
serverBasicAuth.close(done());
|
||||
});
|
||||
|
||||
it("Should return No upcoming events", function () {
|
||||
it("should return No upcoming events", function () {
|
||||
return app.client.waitUntilTextExists(".calendar", "No upcoming events.", 10000);
|
||||
});
|
||||
});
|
||||
|
@@ -1,4 +1,6 @@
|
||||
const helpers = require("../global-setup");
|
||||
const expect = require("chai").expect;
|
||||
const moment = require("moment");
|
||||
|
||||
const describe = global.describe;
|
||||
const it = global.it;
|
||||
@@ -30,12 +32,12 @@ describe("Clock module", function () {
|
||||
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_24hr.js";
|
||||
});
|
||||
|
||||
it("shows date with correct format", function () {
|
||||
it("should show the date in the correct format", function () {
|
||||
const dateRegex = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$/;
|
||||
return app.client.waitUntilWindowLoaded().getText(".clock .date").should.eventually.match(dateRegex);
|
||||
});
|
||||
|
||||
it("shows time in 24hr format", function () {
|
||||
it("should show the time in 24hr format", function () {
|
||||
const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/;
|
||||
return app.client.waitUntilWindowLoaded().getText(".clock .time").should.eventually.match(timeRegex);
|
||||
});
|
||||
@@ -47,12 +49,12 @@ describe("Clock module", function () {
|
||||
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_12hr.js";
|
||||
});
|
||||
|
||||
it("shows date with correct format", function () {
|
||||
it("should show the date in the correct format", function () {
|
||||
const dateRegex = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$/;
|
||||
return app.client.waitUntilWindowLoaded().getText(".clock .date").should.eventually.match(dateRegex);
|
||||
});
|
||||
|
||||
it("shows time in 12hr format", function () {
|
||||
it("should show the time in 12hr format", function () {
|
||||
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/;
|
||||
return app.client.waitUntilWindowLoaded().getText(".clock .time").should.eventually.match(timeRegex);
|
||||
});
|
||||
@@ -64,7 +66,7 @@ describe("Clock module", function () {
|
||||
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_showPeriodUpper.js";
|
||||
});
|
||||
|
||||
it("shows 12hr time with upper case AM/PM", function () {
|
||||
it("should show 12hr time with upper case AM/PM", function () {
|
||||
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/;
|
||||
return app.client.waitUntilWindowLoaded().getText(".clock .time").should.eventually.match(timeRegex);
|
||||
});
|
||||
@@ -76,7 +78,7 @@ describe("Clock module", function () {
|
||||
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_displaySeconds_false.js";
|
||||
});
|
||||
|
||||
it("shows 12hr time without seconds am/pm", function () {
|
||||
it("should show 12hr time without seconds am/pm", function () {
|
||||
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[ap]m$/;
|
||||
return app.client.waitUntilWindowLoaded().getText(".clock .time").should.eventually.match(timeRegex);
|
||||
});
|
||||
@@ -88,17 +90,28 @@ describe("Clock module", function () {
|
||||
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_showWeek.js";
|
||||
});
|
||||
|
||||
it("shows week with correct format", function () {
|
||||
it("should show the week in the correct format", function () {
|
||||
const weekRegex = /^Week [0-9]{1,2}$/;
|
||||
return app.client.waitUntilWindowLoaded().getText(".clock .week").should.eventually.match(weekRegex);
|
||||
});
|
||||
|
||||
it("shows week with correct number of week of year", function () {
|
||||
it("FIXME: if the day is a sunday this not match");
|
||||
// const currentWeekNumber = require("current-week-number")();
|
||||
// const weekToShow = "Week " + currentWeekNumber;
|
||||
// return app.client.waitUntilWindowLoaded()
|
||||
// .getText(".clock .week").should.eventually.equal(weekToShow);
|
||||
it("should show the week with the correct number of week of year", function () {
|
||||
const currentWeekNumber = moment().week();
|
||||
const weekToShow = "Week " + currentWeekNumber;
|
||||
return app.client.waitUntilWindowLoaded().getText(".clock .week").should.eventually.equal(weekToShow);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with analog clock face enabled", function () {
|
||||
before(function () {
|
||||
// Set config sample for use in test
|
||||
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_analog.js";
|
||||
});
|
||||
|
||||
it("should show the analog clock face", async () => {
|
||||
await app.client.waitUntilWindowLoaded(10000);
|
||||
const clock = await app.client.$$(".clockCircle");
|
||||
return expect(clock.length).equals(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -186,7 +186,7 @@ describe("Weather module", function () {
|
||||
const weather = generateWeatherForecast();
|
||||
await setup({ template, data: weather });
|
||||
|
||||
const days = ["Fri", "Sat", "Sun", "Mon", "Tue"];
|
||||
const days = ["Today", "Tomorrow", "Sun", "Mon", "Tue"];
|
||||
|
||||
for (const [index, day] of days.entries()) {
|
||||
await app.client.waitUntilTextExists(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day, 10000);
|
||||
|
41
tests/e2e/modules_display_spec.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const helpers = require("./global-setup");
|
||||
|
||||
const describe = global.describe;
|
||||
const it = global.it;
|
||||
|
||||
describe("Display of modules", function () {
|
||||
helpers.setupTimeout(this);
|
||||
|
||||
var app = null;
|
||||
|
||||
beforeEach(function () {
|
||||
return helpers
|
||||
.startApplication({
|
||||
args: ["js/electron.js"]
|
||||
})
|
||||
.then(function (startedApp) {
|
||||
app = startedApp;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
return helpers.stopApplication(app);
|
||||
});
|
||||
|
||||
describe("Using helloworld", function () {
|
||||
before(function () {
|
||||
// Set config sample for use in test
|
||||
process.env.MM_CONFIG_FILE = "tests/configs/modules/display.js";
|
||||
});
|
||||
|
||||
it("should show the test header", async () => {
|
||||
await app.client.waitForExist("#module_0_helloworld", 10000);
|
||||
return app.client.element("#module_0_helloworld .module-header").isVisible().should.eventually.equal(true).getText("#module_0_helloworld .module-header").should.eventually.equal("TEST_HEADER");
|
||||
});
|
||||
|
||||
it("should show no header if no header text is specified", async () => {
|
||||
await app.client.waitForExist("#module_1_helloworld", 10000);
|
||||
return app.client.element("#module_1_helloworld .module-header").isVisible().should.eventually.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
@@ -46,7 +46,7 @@ describe("Translations", function () {
|
||||
it(`should parse ${language}`, function (done) {
|
||||
const dom = new JSDOM(
|
||||
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
|
||||
<script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
|
||||
{ runScripts: "dangerously", resources: "usable" }
|
||||
);
|
||||
dom.window.onload = function () {
|
||||
@@ -68,7 +68,7 @@ describe("Translations", function () {
|
||||
before(function (done) {
|
||||
const dom = new JSDOM(
|
||||
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
|
||||
<script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
|
||||
{ runScripts: "dangerously", resources: "usable" }
|
||||
);
|
||||
dom.window.onload = function () {
|
||||
@@ -92,7 +92,7 @@ describe("Translations", function () {
|
||||
before(function (done) {
|
||||
const dom = new JSDOM(
|
||||
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
|
||||
<script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
|
||||
{ runScripts: "dangerously", resources: "usable" }
|
||||
);
|
||||
dom.window.onload = function () {
|
||||
|
@@ -1,20 +1,16 @@
|
||||
var path = require("path");
|
||||
var auth = require("http-auth");
|
||||
var express = require("express");
|
||||
var app = express();
|
||||
const path = require("path");
|
||||
const auth = require("express-basic-auth");
|
||||
const express = require("express");
|
||||
const app = express();
|
||||
|
||||
var server;
|
||||
|
||||
var basic = auth.basic(
|
||||
{
|
||||
realm: "MagicMirror Area restricted."
|
||||
},
|
||||
(username, password, callback) => {
|
||||
callback(username === "MagicMirror" && password === "CallMeADog");
|
||||
}
|
||||
);
|
||||
var basicAuth = auth({
|
||||
realm: "MagicMirror Area restricted.",
|
||||
users: { MagicMirror: "CallMeADog" }
|
||||
});
|
||||
|
||||
app.use(auth.connect(basic));
|
||||
app.use(basicAuth);
|
||||
|
||||
// Set available directories
|
||||
var directories = ["/tests/configs"];
|
||||
|
@@ -10,7 +10,7 @@ describe("File js/class", function () {
|
||||
before(function (done) {
|
||||
dom = new JSDOM(
|
||||
`<script>var Log = {log: function() {}};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "class.js")}">`,
|
||||
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "class.js")}">`,
|
||||
{ runScripts: "dangerously", resources: "usable" }
|
||||
);
|
||||
dom.window.onload = function () {
|
||||
|
@@ -65,7 +65,7 @@ describe("Translator", function () {
|
||||
}
|
||||
|
||||
it("should return custom module translation", function (done) {
|
||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
dom.window.onload = function () {
|
||||
const { Translator } = dom.window;
|
||||
setTranslations(Translator);
|
||||
@@ -78,7 +78,7 @@ describe("Translator", function () {
|
||||
});
|
||||
|
||||
it("should return core translation", function (done) {
|
||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
dom.window.onload = function () {
|
||||
const { Translator } = dom.window;
|
||||
setTranslations(Translator);
|
||||
@@ -91,7 +91,7 @@ describe("Translator", function () {
|
||||
});
|
||||
|
||||
it("should return custom module translation fallback", function (done) {
|
||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
dom.window.onload = function () {
|
||||
const { Translator } = dom.window;
|
||||
setTranslations(Translator);
|
||||
@@ -102,7 +102,7 @@ describe("Translator", function () {
|
||||
});
|
||||
|
||||
it("should return core translation fallback", function (done) {
|
||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
dom.window.onload = function () {
|
||||
const { Translator } = dom.window;
|
||||
setTranslations(Translator);
|
||||
@@ -113,7 +113,7 @@ describe("Translator", function () {
|
||||
});
|
||||
|
||||
it("should return translation with placeholder for missing variables", function (done) {
|
||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
dom.window.onload = function () {
|
||||
const { Translator } = dom.window;
|
||||
setTranslations(Translator);
|
||||
@@ -124,7 +124,7 @@ describe("Translator", function () {
|
||||
});
|
||||
|
||||
it("should return key if no translation was found", function (done) {
|
||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
dom.window.onload = function () {
|
||||
const { Translator } = dom.window;
|
||||
setTranslations(Translator);
|
||||
@@ -144,7 +144,7 @@ describe("Translator", function () {
|
||||
};
|
||||
|
||||
it("should load translations", function (done) {
|
||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
dom.window.onload = function () {
|
||||
const { Translator } = dom.window;
|
||||
const file = "TranslationTest.json";
|
||||
@@ -158,7 +158,7 @@ describe("Translator", function () {
|
||||
});
|
||||
|
||||
it("should load translation fallbacks", function (done) {
|
||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
dom.window.onload = function () {
|
||||
const { Translator } = dom.window;
|
||||
const file = "TranslationTest.json";
|
||||
@@ -171,27 +171,8 @@ describe("Translator", function () {
|
||||
};
|
||||
});
|
||||
|
||||
it("should strip comments", function (done) {
|
||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
dom.window.onload = function () {
|
||||
const { Translator } = dom.window;
|
||||
const file = "StripComments.json";
|
||||
|
||||
Translator.load(mmm, file, false, function () {
|
||||
expect(Translator.translations[mmm.name]).to.be.deep.equal({
|
||||
'FOO"BAR': "Today",
|
||||
N: "N",
|
||||
E: "E",
|
||||
S: "S",
|
||||
W: "W"
|
||||
});
|
||||
done();
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
it("should not load translations, if module fallback exists", function (done) {
|
||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||
dom.window.onload = function () {
|
||||
const { Translator, XMLHttpRequest } = dom.window;
|
||||
const file = "TranslationTest.json";
|
||||
@@ -205,7 +186,7 @@ describe("Translator", function () {
|
||||
};
|
||||
|
||||
Translator.load(mmm, file, false, function () {
|
||||
expect(Translator.translations[mmm.name]).to.be.undefined;
|
||||
expect(Translator.translations[mmm.name]).to.be.equal(undefined);
|
||||
expect(Translator.translationsFallback[mmm.name]).to.be.deep.equal({
|
||||
Hello: "Hallo"
|
||||
});
|
||||
@@ -219,7 +200,7 @@ describe("Translator", function () {
|
||||
it("should load core translations and fallback", function (done) {
|
||||
const dom = new JSDOM(
|
||||
`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
|
||||
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
|
||||
{ runScripts: "dangerously", resources: "usable" }
|
||||
);
|
||||
dom.window.onload = function () {
|
||||
@@ -238,7 +219,7 @@ describe("Translator", function () {
|
||||
it("should load core fallback if language cannot be found", function (done) {
|
||||
const dom = new JSDOM(
|
||||
`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
|
||||
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
|
||||
{ runScripts: "dangerously", resources: "usable" }
|
||||
);
|
||||
dom.window.onload = function () {
|
||||
@@ -259,7 +240,7 @@ describe("Translator", function () {
|
||||
it("should load core translations fallback", function (done) {
|
||||
const dom = new JSDOM(
|
||||
`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
|
||||
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
|
||||
{ runScripts: "dangerously", resources: "usable" }
|
||||
);
|
||||
dom.window.onload = function () {
|
||||
@@ -277,7 +258,7 @@ describe("Translator", function () {
|
||||
it("should load core fallback if language cannot be found", function (done) {
|
||||
const dom = new JSDOM(
|
||||
`<script>var translations = {}; var Log = {log: function(){}};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
|
||||
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
|
||||
{ runScripts: "dangerously", resources: "usable" }
|
||||
);
|
||||
dom.window.onload = function () {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
var expect = require("chai").expect;
|
||||
var Utils = require("../../../js/utils.js");
|
||||
var colors = require("colors/safe");
|
||||
const expect = require("chai").expect;
|
||||
const Utils = require("../../../js/utils.js");
|
||||
const colors = require("colors/safe");
|
||||
|
||||
describe("Utils", function () {
|
||||
describe("colors", function () {
|
||||
|
@@ -32,54 +32,54 @@ describe("Functions into modules/default/calendar/calendar.js", function () {
|
||||
});
|
||||
|
||||
describe("getLocaleSpecification", function () {
|
||||
it("Should return a valid moment.LocaleSpecification for a 12-hour format", function () {
|
||||
it("should return a valid moment.LocaleSpecification for a 12-hour format", function () {
|
||||
expect(Module.definitions.calendar.getLocaleSpecification(12)).to.deep.equal({ longDateFormat: { LT: "h:mm A" } });
|
||||
});
|
||||
|
||||
it("Should return a valid moment.LocaleSpecification for a 24-hour format", function () {
|
||||
it("should return a valid moment.LocaleSpecification for a 24-hour format", function () {
|
||||
expect(Module.definitions.calendar.getLocaleSpecification(24)).to.deep.equal({ longDateFormat: { LT: "HH:mm" } });
|
||||
});
|
||||
|
||||
it("Should return the current system locale when called without timeFormat number", function () {
|
||||
it("should return the current system locale when called without timeFormat number", function () {
|
||||
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: moment.localeData().longDateFormat("LT") } });
|
||||
});
|
||||
|
||||
it("Should return a 12-hour longDateFormat when using the 'en' locale", function () {
|
||||
it("should return a 12-hour longDateFormat when using the 'en' locale", function () {
|
||||
var localeBackup = moment.locale();
|
||||
moment.locale("en");
|
||||
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } });
|
||||
moment.locale(localeBackup);
|
||||
});
|
||||
|
||||
it("Should return a 12-hour longDateFormat when using the 'au' locale", function () {
|
||||
it("should return a 12-hour longDateFormat when using the 'au' locale", function () {
|
||||
var localeBackup = moment.locale();
|
||||
moment.locale("au");
|
||||
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } });
|
||||
moment.locale(localeBackup);
|
||||
});
|
||||
|
||||
it("Should return a 12-hour longDateFormat when using the 'eg' locale", function () {
|
||||
it("should return a 12-hour longDateFormat when using the 'eg' locale", function () {
|
||||
var localeBackup = moment.locale();
|
||||
moment.locale("eg");
|
||||
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } });
|
||||
moment.locale(localeBackup);
|
||||
});
|
||||
|
||||
it("Should return a 24-hour longDateFormat when using the 'nl' locale", function () {
|
||||
it("should return a 24-hour longDateFormat when using the 'nl' locale", function () {
|
||||
var localeBackup = moment.locale();
|
||||
moment.locale("nl");
|
||||
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } });
|
||||
moment.locale(localeBackup);
|
||||
});
|
||||
|
||||
it("Should return a 24-hour longDateFormat when using the 'fr' locale", function () {
|
||||
it("should return a 24-hour longDateFormat when using the 'fr' locale", function () {
|
||||
var localeBackup = moment.locale();
|
||||
moment.locale("fr");
|
||||
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } });
|
||||
moment.locale(localeBackup);
|
||||
});
|
||||
|
||||
it("Should return a 24-hour longDateFormat when using the 'uk' locale", function () {
|
||||
it("should return a 24-hour longDateFormat when using the 'uk' locale", function () {
|
||||
var localeBackup = moment.locale();
|
||||
moment.locale("uk");
|
||||
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } });
|
||||
@@ -120,5 +120,11 @@ describe("Functions into modules/default/calendar/calendar.js", function () {
|
||||
"This is a wrapEvent <br>test. Should wrap the string <br>instead of shorten it if called <br>with wrapEvent = true"
|
||||
);
|
||||
});
|
||||
|
||||
it("should wrap and shorten the string in the second line if called with wrapEvents = true and maxTitleLines = 2", function () {
|
||||
expect(Module.definitions.calendar.shorten("This is a wrapEvent and maxTitleLines test. Should wrap and shorten the string in the second line if called with wrapEvents = true and maxTitleLines = 2", undefined, true, 2)).to.equal(
|
||||
"This is a wrapEvent and <br>maxTitleLines test. Should wrap and …"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -8,7 +8,7 @@ describe("Test function cmpVersions in js/module.js", function () {
|
||||
before(function (done) {
|
||||
const dom = new JSDOM(
|
||||
`<script>var Class = {extend: function() { return {}; }};</script>\
|
||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "module.js")}">`,
|
||||
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "module.js")}">`,
|
||||
{ runScripts: "dangerously", resources: "usable" }
|
||||
);
|
||||
dom.window.onload = function () {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/* eslint no-multi-spaces: 0 */
|
||||
var expect = require("chai").expect;
|
||||
const expect = require("chai").expect;
|
||||
|
||||
describe("Functions module currentweather", function () {
|
||||
// Fake for use by currentweather.js
|
||||
|
@@ -1,29 +1,15 @@
|
||||
var expect = require("chai").expect;
|
||||
const expect = require("chai").expect;
|
||||
|
||||
describe("Functions into modules/default/newsfeed/newsfeed.js", function () {
|
||||
// Fake for use by newsletter.js
|
||||
Module = {};
|
||||
Module.definitions = {};
|
||||
Module.register = function (name, moduleDefinition) {
|
||||
Module.definitions[name] = moduleDefinition;
|
||||
};
|
||||
|
||||
// load newsfeed.js
|
||||
require("../../../modules/default/newsfeed/newsfeed.js");
|
||||
|
||||
describe("capitalizeFirstLetter", function () {
|
||||
const words = {
|
||||
rodrigo: "Rodrigo",
|
||||
"123m": "123m",
|
||||
"magic mirror": "Magic mirror",
|
||||
",a": ",a",
|
||||
ñandú: "Ñandú",
|
||||
".!": ".!"
|
||||
};
|
||||
|
||||
Object.keys(words).forEach((word) => {
|
||||
it(`for ${word} should return ${words[word]}`, function () {
|
||||
expect(Module.definitions.newsfeed.capitalizeFirstLetter(word)).to.equal(words[word]);
|
||||
});
|
||||
});
|
||||
before(function () {
|
||||
// load newsfeed.js
|
||||
require("../../../modules/default/newsfeed/newsfeed.js");
|
||||
});
|
||||
});
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/* eslint no-multi-spaces: 0 */
|
||||
var expect = require("chai").expect;
|
||||
const expect = require("chai").expect;
|
||||
|
||||
describe("Functions module weatherforecast", function () {
|
||||
before(function () {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var expect = require("chai").expect;
|
||||
var vm = require("vm");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const expect = require("chai").expect;
|
||||
const vm = require("vm");
|
||||
|
||||
before(function () {
|
||||
var basedir = path.join(__dirname, "../../..");
|
||||
|
@@ -1,7 +1,7 @@
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var expect = require("chai").expect;
|
||||
var vm = require("vm");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const expect = require("chai").expect;
|
||||
const vm = require("vm");
|
||||
|
||||
before(function () {
|
||||
var basedir = path.join(__dirname, "../../..");
|
||||
|
34
translations/cv.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"LOADING": "Тиенет …",
|
||||
|
||||
"TODAY": "Паян",
|
||||
"TOMORROW": "Ыран",
|
||||
"DAYAFTERTOMORROW": "Виҫмине",
|
||||
"RUNNING": "Хальхи",
|
||||
"EMPTY": "Пулас ӗҫ ҫук",
|
||||
|
||||
"WEEK": "{weekNumber} эрне",
|
||||
|
||||
"N": "Ҫ",
|
||||
"NNE": "ҪҪТ",
|
||||
"NE": "ҪТ",
|
||||
"ENE": "ТҪТ",
|
||||
"E": "Т",
|
||||
"ESE": "ТКТ",
|
||||
"SE": "КТ",
|
||||
"SSE": "ККТ",
|
||||
"S": "К",
|
||||
"SSW": "ККА",
|
||||
"SW": "КА",
|
||||
"WSW": "АКА",
|
||||
"W": "А",
|
||||
"WNW": "АҪА",
|
||||
"NW": "ҪА",
|
||||
"NNW": "ҪҪА",
|
||||
"FEELS": "Туйӑннӑ",
|
||||
|
||||
"UPDATE_NOTIFICATION": "MagicMirror² валли ҫӗнетӳ пур.",
|
||||
"UPDATE_NOTIFICATION_MODULE": "{MODULE_NAME} модуль валли ҫӗнетӳ пур.",
|
||||
"UPDATE_INFO_SINGLE": "Ҫак инсталляци {BRANCH_NAME} commit турат {COMMIT_COUNT} коммитпа кая уйрӑлса тӑрать.",
|
||||
"UPDATE_INFO_MULTIPLE": "Ҫак инсталляци {BRANCH_NAME} commit турат {COMMIT_COUNT} коммитпа кая уйрӑлса тӑрать."
|
||||
}
|
@@ -26,6 +26,8 @@
|
||||
"NW": "NW",
|
||||
"NNW": "NNW",
|
||||
|
||||
"MODULE_CONFIG_CHANGED": "Die Konfigurationsoptionen für das {MODULE_NAME} Modul haben sich geändert. \nBitte überprüfen Sie die Dokumentation.",
|
||||
|
||||
"UPDATE_NOTIFICATION": "Aktualisierung für MagicMirror² verfügbar.",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Aktualisierung für das {MODULE_NAME} Modul verfügbar.",
|
||||
"UPDATE_INFO_SINGLE": "Die aktuelle Installation ist {COMMIT_COUNT} Commit hinter dem {BRANCH_NAME} Branch.",
|
||||
|
@@ -26,6 +26,8 @@
|
||||
"NW": "NW",
|
||||
"NNW": "NNW",
|
||||
|
||||
"MODULE_CONFIG_CHANGED": "The configuration options for the {MODULE_NAME} module have changed.\nPlease check the documentation.",
|
||||
|
||||
"UPDATE_NOTIFICATION": "MagicMirror² update available.",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Update available for {MODULE_NAME} module.",
|
||||
"UPDATE_INFO_SINGLE": "The current installation is {COMMIT_COUNT} commit behind on the {BRANCH_NAME} branch.",
|
||||
|
@@ -26,6 +26,8 @@
|
||||
"NW": "NO",
|
||||
"NNW": "NNO",
|
||||
|
||||
"MODULE_CONFIG_CHANGED": "Las opciones de configuración para el módulo {MODULE_NAME} han cambiado. \nVerifique la documentación.",
|
||||
|
||||
"UPDATE_NOTIFICATION": "MagicMirror² actualización disponible.",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Disponible una actualización para el módulo {MODULE_NAME}.",
|
||||
"UPDATE_INFO_SINGLE": "Tu actual instalación está {COMMIT_COUNT} commit cambios detrás de la rama {BRANCH_NAME}.",
|
||||
|
@@ -26,6 +26,8 @@
|
||||
"NW": "NO",
|
||||
"NNW": "NNO",
|
||||
|
||||
"MODULE_CONFIG_CHANGED": "Les options de configuration du module {MODULE_NAME} ont changé. \nVeuillez consulter la documentation.",
|
||||
|
||||
"UPDATE_NOTIFICATION": "Une mise à jour de MagicMirror² est disponible",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Une mise à jour est disponible pour le module {MODULE_NAME} .",
|
||||
"UPDATE_INFO_SINGLE": "L'installation actuelle est {COMMIT_COUNT} commit en retard sur la branche {BRANCH_NAME} .",
|
||||
|
38
translations/gu.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"LOADING": "લોડ થઈ રહ્યું છે …",
|
||||
|
||||
"TODAY": "આજે",
|
||||
"TOMORROW": "આવતી કાલે",
|
||||
"DAYAFTERTOMORROW": "પરમ દિવસે",
|
||||
"RUNNING": "માં સમાપ્ત થાય છે",
|
||||
"EMPTY": "કોઈ આગામી કાર્યક્રમ નથી.",
|
||||
|
||||
"WEEK": "સપ્તાહ {weekNumber}",
|
||||
|
||||
"N": "ઉ",
|
||||
"NNE": "ઉઉપુ",
|
||||
"NE": "ઉપુ",
|
||||
"ENE": "પુઉપુ",
|
||||
"E": "પુ",
|
||||
"ESE": "પુદપુ",
|
||||
"SE": "દપુ",
|
||||
"SSE": "દદપુ",
|
||||
"S": "દ",
|
||||
"SSW": "દદપ",
|
||||
"SW": "દપ",
|
||||
"WSW": "પદપ",
|
||||
"W": "પ",
|
||||
"WNW": "પઉપ",
|
||||
"NW": "ઉપ",
|
||||
"NNW": "ઉઉપ",
|
||||
|
||||
"MODULE_CONFIG_CHANGED": "{MODULE_NAME} મોડ્યુલ માટે ગોઠવણી વિકલ્પો બદલાયા છે. \nકૃપા કરીને દસ્તાવેજોને તપાસો.",
|
||||
|
||||
"UPDATE_NOTIFICATION": "MagicMirror² અપડેટ ઉપલબ્ધ છે.",
|
||||
"UPDATE_NOTIFICATION_MODULE": "{MODULE_NAME} મોડ્યુલ માટે અપડેટ ઉપલબ્ધ છે.",
|
||||
"UPDATE_INFO_SINGLE": "વર્તમાન ઇન્સ્ટોલેશન એ {BRANCH_NAME} શાખા ની {COMMIT_COUNT} કમીટ પાછળ છે. ",
|
||||
"UPDATE_INFO_MULTIPLE": "વર્તમાન ઇન્સ્ટોલેશન એ {BRANCH_NAME} શાખા ની {COMMIT_COUNT} કમીટ પાછળ છે. ",
|
||||
|
||||
"FEELS": "{DEGREE} જેવું લાગશે",
|
||||
"PRECIP": "PoP"
|
||||
}
|
38
translations/hi.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"LOADING": "लोड हो रहा है …",
|
||||
|
||||
"TODAY": "आज",
|
||||
"TOMORROW": "आने वाला कल",
|
||||
"DAYAFTERTOMORROW": "2 दिनों में",
|
||||
"RUNNING": "में समाप्त",
|
||||
"EMPTY": "कोई आगामी कार्यक्रम नहीं।",
|
||||
|
||||
"WEEK": "सप्ताह {weekNumber}",
|
||||
|
||||
"N": "उ",
|
||||
"NNE": "उउपु",
|
||||
"NE": "उपु",
|
||||
"ENE": "पुउपु",
|
||||
"E": "पु",
|
||||
"ESE": "पुSपु",
|
||||
"SE": "दपु",
|
||||
"SSE": "ददपु",
|
||||
"S": "द",
|
||||
"SSW": "ददW",
|
||||
"SW": "दW",
|
||||
"WSW": "WदW",
|
||||
"W": "प",
|
||||
"WNW": "पउप",
|
||||
"NW": "उप",
|
||||
"NNW": "उउप",
|
||||
|
||||
"MODULE_CONFIG_CHANGED": "{MODULE_NAME} मॉड्यूल के लिए कॉन्फ़िगरेशन विकल्प बदल गए हैं। n कृपया दस्तावेज़ देखें।",
|
||||
|
||||
"UPDATE_NOTIFICATION": "MagicMirror² अपडेट उपलब्ध।",
|
||||
"UPDATE_NOTIFICATION_MODULE": "{MODULE_NAME} मॉड्यूल के लिए उपलब्ध अद्यतन।",
|
||||
"UPDATE_INFO_SINGLE": "वर्तमान स्थापना {COMMIT_COUNT} {BRANCH_NAME} शाखा के पीछे है।",
|
||||
"UPDATE_INFO_MULTIPLE": "वर्तमान स्थापना {COMMIT_COUNT} पीछे {BRANCH_NAME} शाखा पर है।",
|
||||
|
||||
"FEELS": "{DEGREE} की तरह लगना",
|
||||
"PRECIP": "PoP"
|
||||
}
|
36
translations/lt.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"LOADING": "Kraunasi …",
|
||||
|
||||
"TODAY": "Šiandien",
|
||||
"TOMORROW": "Rytoj",
|
||||
"DAYAFTERTOMORROW": "Už 2 dienų",
|
||||
"RUNNING": "Pasibaigs už",
|
||||
"EMPTY": "Nėra artimų įvykių.",
|
||||
|
||||
"WEEK": "{weekNumber} savaitė",
|
||||
|
||||
"N": "Š",
|
||||
"NNE": "ŠŠR",
|
||||
"NE": "ŠR",
|
||||
"ENE": "RŠR",
|
||||
"E": "R",
|
||||
"ESE": "RPR",
|
||||
"SE": "PR",
|
||||
"SSE": "PPR",
|
||||
"S": "P",
|
||||
"SSW": "PPV",
|
||||
"SW": "PV",
|
||||
"WSW": "VPV",
|
||||
"W": "V",
|
||||
"WNW": "VŠV",
|
||||
"NW": "ŠV",
|
||||
"NNW": "ŠŠV",
|
||||
|
||||
"UPDATE_NOTIFICATION": "Galimas MagicMirror² naujinimas.",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Galimas {MODULE_NAME} naujinimas.",
|
||||
"UPDATE_INFO_SINGLE": "Šis įdiegimas atsilieka {COMMIT_COUNT} įsipareigojimu {BRANCH_NAME} šakoje.",
|
||||
"UPDATE_INFO_MULTIPLE": "Šis įdiegimas atsilieka {COMMIT_COUNT} įsipareigojimais {BRANCH_NAME} šakoje.",
|
||||
|
||||
"FEELS": "Jutiminė temp.",
|
||||
"PRECIP": "Krituliai"
|
||||
}
|