Compare commits

...

448 Commits

Author SHA1 Message Date
Michael Teeuw
10dc315f3b Merge pull request #2400 from MichMich/develop
Release 2.14.0
2021-01-01 19:16:03 +01:00
Michael Teeuw
0f1457b5d7 Prepare release 2.14.0 - Fix Version 2021-01-01 19:08:19 +01:00
Michael Teeuw
090873d4c2 Prepare release 2.14.0 2021-01-01 19:07:14 +01:00
Michael Teeuw
da00c168ae Merge pull request #2398 from fewieden/patch-2
Highlight required version mismatch
2021-01-01 15:59:45 +01:00
fewieden
8286d5a06e Highlight required version mismatch 2021-01-01 14:44:39 +01:00
fewieden
dc5fb978a7 Update CHANGELOG.md 2021-01-01 14:43:16 +01:00
Michael Teeuw
a4ab0cbe09 Merge pull request #2397 from ashishtank/develop
Added support for variables in nunjucks templates for translate filter
2020-12-31 19:57:03 +01:00
Ashish Tank
4a341b381e Added support for variables in nunjucks templates for translate filter 2020-12-31 18:58:21 +01:00
ashishtank
3fa98bc1aa Merge pull request #8 from MichMich/develop
Develop
2020-12-31 18:51:38 +01:00
Michael Teeuw
87e2e87ce6 Merge pull request #2395 from ashishtank/develop
Added support for optional DEGREE position in "FEELS" label
2020-12-31 16:44:25 +01:00
Michael Teeuw
c25b6dc16c Merge pull request #2392 from sdetweil/fix-update
Fix simple-git version for unhandle promise rejection
2020-12-31 16:43:26 +01:00
sam detweiler
6a786aa090 Merge branch 'develop' into fix-update 2020-12-31 09:35:29 -06:00
Michael Teeuw
a97d87bce8 Merge pull request #2387 from bugsounet/patch-1
No Text Select for Touch Screen use
2020-12-31 16:29:14 +01:00
Ashish Tank
fba91329f1 Added support optional support for DEGREE position in FEELS translation 2020-12-31 16:28:00 +01:00
Michael Teeuw
353cc3b00f Merge pull request #2393 from MikeBishop/translator_falsy_vars
Permit substituting a falsy value in translator variables
2020-12-31 16:27:43 +01:00
ashishtank
057ef63586 Merge pull request #7 from MichMich/develop
Develop
2020-12-31 16:15:26 +01:00
Mike Bishop
c9fb38981e CHANGELOG 2020-12-31 10:11:43 -05:00
Mike Bishop
9ea7de8b44 Permit substituting a falsy value in translator variables 2020-12-31 10:10:01 -05:00
Sam Detweiler
020c8ccd2a fix update notification 2383 2020-12-31 09:08:23 -06:00
Sam Detweiler
4d09abe725 fix node-ical version 0.12.7 for bad rrule throw 2020-12-31 07:40:40 -06:00
Sam Detweiler
5079e30caf Merge branch 'develop' of https://github.com/MichMich/MagicMirror into develop 2020-12-31 07:36:37 -06:00
Bugsounet - Cédric
3e5c1a278b Update CHANGELOG.md 2020-12-30 11:38:28 +01:00
Bugsounet - Cédric
4ded60874f No Text Select for Touch Screen use 2020-12-30 11:34:09 +01:00
Michael Teeuw
ff1a843de6 Merge pull request #2386 from khassel/fix_modules_array
removes undefined objects from modules array, see issue #2382
2020-12-29 23:01:25 +01:00
Karsten Hassel
e6cefcf948 Merge branch 'develop' into fix_modules_array
# Conflicts:
#	CHANGELOG.md
2020-12-29 22:24:19 +01:00
Michael Teeuw
a857412f13 Merge pull request #2385 from rejas/missing_function_call
Add missing function call in module.js
2020-12-29 22:15:55 +01:00
Karsten Hassel
e507f95b2a added suggestion from @rejas 2020-12-29 21:45:35 +01:00
Karsten Hassel
61cf92c67a removes undefined objects from modules array, see issue #2382 2020-12-29 21:14:42 +01:00
rejas
7a4eddc592 Adjust some log levels 2020-12-29 18:50:27 +01:00
rejas
3fcbf15915 Update CHANGELOG 2020-12-29 18:50:10 +01:00
rejas
efafb1c28a Cleanup jsdoc 2020-12-29 18:48:45 +01:00
rejas
67bedf8648 Add missing function () 2020-12-29 18:38:38 +01:00
Michael Teeuw
c1d35b0f91 Merge pull request #2384 from rejas/log_level_sample
Update config sample
2020-12-29 16:23:39 +01:00
Michael Teeuw
d3a715bd6b Merge pull request #2381 from sdetweil/node-ical-again
change node-ical version again
2020-12-29 16:22:52 +01:00
Michael Teeuw
28b52cd24f Merge pull request #2379 from sdetweil/fix-package
Fix package.json electron dependency
2020-12-29 16:22:33 +01:00
Michael Teeuw
46e77f805c Merge pull request #2377 from buxxi/gitignore-merge
Merging config/.gitignore with .gitignore
2020-12-29 16:21:47 +01:00
buxxi
dd23db0ad8 Running prettier for CHANGELOG 2020-12-29 12:43:40 +01:00
rejas
30cf7f8afe Update CHANGELOG 2020-12-29 10:23:57 +01:00
rejas
7802e0bb88 Set locationID everywhere on config sample 2020-12-29 10:23:12 +01:00
rejas
07e75b8550 Adjust log level in sample 2020-12-29 10:21:50 +01:00
Sam Detweiler
1e9fad8278 Merge branch 'develop' of https://github.com/MichMich/MagicMirror into develop 2020-12-28 12:21:48 -06:00
Sam Detweiler
0975826457 update node-ical version 2020-12-28 08:24:19 -06:00
Sam Detweiler
e5ff320591 fix package.json for optional dependency 2020-12-28 08:15:40 -06:00
Sam Detweiler
02c1e47749 Merge branch 'develop' of https://github.com/MichMich/MagicMirror into fix-package 2020-12-28 08:13:50 -06:00
buxxi
a50824eeee Merging config/.gitignore with .gitignore 2020-12-28 11:50:01 +01:00
Michael Teeuw
0c3f9f4ed9 Merge pull request #2372 from sdetweil/update_node-ical
update node-ical to 0.12.4
2020-12-23 17:51:42 +01:00
Sam Detweiler
6b12601c6f update node-ical to 0.12.4 2020-12-23 08:57:22 -06:00
Sam Detweiler
225bece44e update node-ical to 0.12.4 2020-12-23 08:53:45 -06:00
Michael Teeuw
a5a5e73196 Fix Prettier issue. 2020-12-21 13:59:03 +01:00
Michael Teeuw
f8085ed78f Update CHANGELOG.md 2020-12-21 13:56:19 +01:00
Michael Teeuw
4e6e84c637 Replace Badge 2020-12-21 13:33:03 +01:00
Michael Teeuw
846212798c Update node-ci.js.yml 2020-12-21 13:29:10 +01:00
Michael Teeuw
571f95eb2f Merge pull request #2370 from MichMich/fix-changelog
Fix changelog special character issues.
2020-12-21 13:19:26 +01:00
Michael Teeuw
202eeea33f Fix changelog special character issues. 2020-12-21 13:16:54 +01:00
Michael Teeuw
af212057db Merge pull request #2369 from MichMich/enforce-changelog
Create enforce-changelog.yml
2020-12-21 13:07:17 +01:00
Michael Teeuw
0fd0fea7e7 Update enforce-changelog.yml 2020-12-21 13:06:39 +01:00
Michael Teeuw
eb6ef3c8ff Update enforce-changelog.yml 2020-12-21 13:02:40 +01:00
Michael Teeuw
cdb8d35cf6 Create enforce-changelog.yml 2020-12-21 12:36:59 +01:00
Michael Teeuw
684dcdcef3 Merge pull request #2224 from ashishtank/develop
Issue 2221 - Weather module always shows night icons for locale other then english
2020-12-21 11:33:05 +01:00
Michael Teeuw
155351f5dd Merge pull request #2226 from rejas/update_dependencies
Update dependencies incl ini
2020-12-21 11:32:52 +01:00
Michael Teeuw
0419e06e7c Merge pull request #2361 from buxxi/smhi-provider
Adding SMHI as a provider for the weather module
2020-12-21 11:29:20 +01:00
Michael Teeuw
2ea38bd9a4 Stupid commit to check github actions. 2020-12-21 11:27:12 +01:00
Michael Teeuw
30db9c30c8 Stupid commit to check github actions. 2020-12-21 11:25:36 +01:00
Michael Teeuw
a8ef594dab Stupid commit to check github actions. 2020-12-21 11:23:02 +01:00
Michael Teeuw
9858d5b495 Merge pull request #2360 from rejas/issue_2228
Hide alert overlay when dismissed manually
2020-12-21 11:20:55 +01:00
Michael Teeuw
20bd85b676 Delete danger-ci.yml
Unfortunately Danger Actions currently can't work with PR's:
https://github.com/PrismJS/prism/issues/2627
2020-12-21 11:20:34 +01:00
Michael Teeuw
83ec8ca24f Restore. 2020-12-21 11:13:41 +01:00
Michael Teeuw
9622d02230 Yolo. 2020-12-21 11:11:48 +01:00
Michael Teeuw
b72d0ed37e Add ENV variable. 2020-12-21 11:02:14 +01:00
Michael Teeuw
f966e504a5 Cleanup. 2020-12-21 10:59:55 +01:00
Michael Teeuw
a0366e794b Stupid commit to check github actions. 2020-12-21 10:57:18 +01:00
Michael Teeuw
3c3ce24397 Cleanup Danger-CI 2020-12-21 10:51:51 +01:00
Michael Teeuw
30290f3eb2 Merge pull request #2367 from MichMich/danger-ci
Danger ci
2020-12-21 10:47:27 +01:00
Michael Teeuw
616431e04b Disable Transpile. 2020-12-21 10:27:06 +01:00
Michael Teeuw
d0aeb90f0a Merge pull request #2365 from MichMich/ci-test
Replace Travis with Github Actions
2020-12-21 10:24:26 +01:00
Michael Teeuw
86220fa721 Update danger-ci.yml 2020-12-21 10:22:25 +01:00
Michael Teeuw
0f58a56a07 Add env variable. 2020-12-21 09:47:36 +01:00
Michael Teeuw
29451562e3 Create danger-ci.yml 2020-12-21 09:37:08 +01:00
Michael Teeuw
911687af2a Remove Travis 2020-12-21 09:28:26 +01:00
Michael Teeuw
1c9c33b87b Merge branch 'develop' into ci-test 2020-12-21 09:16:52 +01:00
Michael Teeuw
f485462af1 Merge pull request #2364 from khassel/ci-test
test github actions
2020-12-21 08:25:40 +01:00
Karsten Hassel
39d7ceb017 test github actions 2020-12-20 22:49:41 +01:00
Michael Teeuw
8251792a0d Update .prettierignore 2020-12-20 20:04:06 +01:00
Michael Teeuw
54ac450f92 Update and rename node.js.yml to node-ci.js.yml 2020-12-20 20:00:54 +01:00
Michael Teeuw
299e4a497f Create node.js.yml 2020-12-20 19:58:06 +01:00
buxxi
3f851c1fd6 Adding SMHI as a provider for the weather module 2020-12-19 11:13:46 +01:00
veeck
8cf16f1049 Update changelog 2020-12-17 18:32:01 +01:00
veeck
b373aa6250 Hide alert overlay when dismissed manually 2020-12-17 18:31:18 +01:00
veeck
2f4b8cd642 Update dependencies incl ini 2020-12-11 11:09:52 +01:00
Ashish Tank
85b6df3738 Issue #2221 2020-12-09 15:27:36 +01:00
Ashish Tank
c675421a6a #Issue 2221 2020-12-09 12:08:20 +01:00
ashishtank
a7b571e5d0 Merge pull request #6 from MichMich/develop
Develop
2020-12-09 12:04:15 +01:00
Ashish Tank
b3a9b7ef0e Merge branch 'develop' of https://github.com/ashishtank/MagicMirror into develop
Pull from github
2020-12-09 11:59:30 +01:00
Ashish Tank
e984893853 Issue #2221 - Night icons are always shown for locale other then english 2020-12-09 11:58:47 +01:00
Michael Teeuw
3024319027 Merge pull request #2222 from khassel/update_dependencies
update depencencies
2020-12-08 16:36:06 +01:00
Michael Teeuw
21ba652413 Regenerate Package.lock 2020-12-08 16:33:57 +01:00
Michael Teeuw
e4b8cd92f2 Merge branch 'develop' into update_dependencies 2020-12-08 16:31:47 +01:00
Michael Teeuw
1a4a9f6501 Merge pull request #2190 from Sub028/develop
Fix windspeed convertion error in ukmetoffice weather provider
2020-12-08 16:28:02 +01:00
Michael Teeuw
e950cdaf32 Prettier fixes. 2020-12-08 16:20:48 +01:00
Michael Teeuw
f97be2f8f3 Fix prettier issue. 2020-12-08 16:07:11 +01:00
Michael Teeuw
be0c8f4f16 Prettier fix. 2020-12-08 16:01:19 +01:00
Michael Teeuw
46fd2de315 Merge branch 'develop' into develop 2020-12-08 15:53:21 +01:00
Michael Teeuw
72f0d77865 Merge pull request #2217 from sdetweil/cal-again2
Cal again2
2020-12-08 15:51:56 +01:00
Michael Teeuw
d43679d59e Merge pull request #2185 from Sub028/master
Weather config enhancement
2020-12-08 15:51:41 +01:00
Michael Teeuw
ce46fb5384 Fix Prettier Issue 2020-12-08 15:43:41 +01:00
Michael Teeuw
43b33cb6de Fix prettier issue. 2020-12-08 15:42:01 +01:00
Michael Teeuw
0344399253 Fix prettier issue. 2020-12-08 15:40:46 +01:00
Michael Teeuw
3a9b154cb2 Fix code style issue. 2020-12-08 15:38:07 +01:00
Michael Teeuw
1074fbacfe Merge pull request #2215 from Alvinger/limitDays
New option "limitDays" - limit the number of discrete days to be displayed
2020-12-08 15:24:04 +01:00
Michael Teeuw
00ff3ab380 Merge branch 'develop' into limitDays 2020-12-08 15:23:54 +01:00
Michael Teeuw
db874a011c Merge pull request #2214 from Alvinger/customEvents
Custom events - Use custom symbol/color based on keyword in title
2020-12-08 15:21:39 +01:00
Michael Teeuw
35a2839a2f Merge branch 'develop' into customEvents 2020-12-08 15:21:29 +01:00
Michael Teeuw
cdb9b9bb87 Merge branch 'develop' into cal-again2 2020-12-08 15:17:45 +01:00
Michael Teeuw
87a3e4d440 Merge branch 'develop' into limitDays 2020-12-08 15:16:21 +01:00
Michael Teeuw
284bed677e Merge pull request #2209 from jakobsarwary1/patch-1
Create ps.json
2020-12-08 15:13:59 +01:00
Michael Teeuw
667be460e5 Merge pull request #2210 from rejas/issue-2022_catch-ical-parsing-errors
Catch errors when parsing calendar data with ical
2020-12-08 15:13:42 +01:00
Michael Teeuw
c49386bb58 Merge branch 'develop' into issue-2022_catch-ical-parsing-errors 2020-12-08 15:13:34 +01:00
Michael Teeuw
adb50a9623 Merge pull request #2208 from rejas/issue-2199_console-debug
Add timestamp to Log.debug
2020-12-08 15:12:39 +01:00
Michael Teeuw
ac1d2372f4 Merge branch 'develop' into issue-2199_console-debug 2020-12-08 15:12:32 +01:00
Michael Teeuw
f54690c829 Merge pull request #2206 from Alvinger/calendar-enhance
Calendar fixes and updates
2020-12-08 15:11:32 +01:00
Michael Teeuw
053f9c34aa Merge pull request #2205 from marvai-vgtu/patch-1
Update lt.json
2020-12-08 15:11:03 +01:00
Michael Teeuw
e7dd2b4aee Merge pull request #2187 from AndyPoms/weatherbit
Add support for Weatherbit in the Weather Module
2020-12-08 15:10:45 +01:00
sam detweiler
6f82f9e01b update changelog 2020-12-07 07:27:40 -06:00
sam detweiler
d531730dc1 Merge pull request #1 from MichMich/develop
sync Develop
2020-12-07 07:08:30 -06:00
Karsten Hassel
00bdf6aaa6 update depencencies 2020-12-04 22:42:56 +01:00
Johan Alvinger
97f3514677 Bugfix after Travis CI error (redeclaring variables) 2020-11-30 16:48:07 +01:00
sam detweiler
137facf95a Merge branch 'develop' into cal-again2 2020-11-28 19:23:15 -06:00
Sam Detweiler
8afba3a5c4 fix between.from to use now, instead of yesterday for non-full day events, unless includePastEvents:true 2020-11-28 19:19:52 -06:00
Sam Detweiler
9c5383dc37 fix between.from to use now, instead of yesterday for non-full day events, unless includePastEvents:true 2020-11-28 19:13:56 -06:00
Johan Alvinger
260bc9664e Fixed variable redeclaration 2020-11-28 14:05:35 +01:00
Johan Alvinger
655ca83356 New option "limitDays" - limit the number of discreet days to be displayed 2020-11-28 14:00:38 +01:00
Johan Alvinger
472bf1665c New option "limitDays" - limit the number of discreet days to be displayed 2020-11-28 13:59:13 +01:00
Johan Alvinger
db129cc19b Added "customEvents" to changelog 2020-11-28 13:19:04 +01:00
Johan Alvinger
b735f8a524 New option "customEvents"
Use custom symbol and/or color based on keyword in event titles
2020-11-28 13:17:14 +01:00
Johan Alvinger
1e34764588 coloredEvents should also color the symbol if that is displayed 2020-11-26 17:14:59 +01:00
Johan Alvinger
99aaae491c Reverted changes to test case for calendar 2020-11-25 23:35:01 +01:00
Johan Alvinger
f288581c69 Reverted changes to test case for calendar 2020-11-25 23:31:26 +01:00
Johan Alvinger
3c5d50bce9 Include all past events (if broadcastPastEvents set) and up to maximumEntries of current or upcoming events 2020-11-25 23:29:10 +01:00
Johan Alvinger
a01f08391b Removed test on maximumEntries 2020-11-25 22:45:38 +01:00
Johan Alvinger
51a1399bca Change custom calendar test to not include past events 2020-11-25 22:36:00 +01:00
Johan Alvinger
b735cb96a0 Fetch maximumEntries of current events (and all past events if
broadcastPastEvents is true)
2020-11-25 21:59:58 +01:00
Johan Alvinger
d00c25e107 Fetch maximumEntries of current events (and all past events if broadcastPastEvents is true) 2020-11-25 21:53:34 +01:00
Johan Alvinger
8a5e87b116 All events from the beginning of today were fetched but we only want
the ones that are ongoing or upcoming.
2020-11-24 23:17:26 +01:00
Johan Alvinger
2779d19d5c All events from the beginning of today were fetched but we only want the ones that are ongoing or upcoming. 2020-11-24 23:06:41 +01:00
rejas
38d4a8b198 Update CHANGELOG 2020-11-24 21:33:14 +01:00
rejas
ccf98c0c22 Surround ical parsing with try/catch to catch unknown bugs 2020-11-24 21:32:16 +01:00
Jakob Sarwary
bd0d91d1b4 Create ps.json
ISO 639-1 Language Code: ps
ISO 639-2 Language Code: pus
English name of Language: Pushto; Pashto
French name of Language: pachto

Pashto, sometimes spelled Pukhto or Pakhto, is an Eastern Iranian language of the Indo-European family. It is known in Persian literature as Afghani. Speakers of the language are called Pashtuns or Pukhtuns/Pakhtuns. Pashto and Dari are the two official languages of Afghanistan.
Native speakers: 40-60 million

https://g.co/kgs/y4xaLh
2020-11-24 20:20:07 +01:00
Johan Alvinger
056f3a6ccb limitDays and coloredEvents are now only module-wide options, not per calendar. 2020-11-24 15:41:28 +01:00
Johan Alvinger
20a50f8382 Reverted changes in custom.js for testing 2020-11-24 14:57:51 +01:00
Johan Alvinger
21284e7795 Reverted change in calendarfetcher so events are limited to maximumEntries 2020-11-24 14:52:08 +01:00
Johan Alvinger
e0ceed5a63 Correct error in custom.js in calendar tests 2020-11-24 12:52:21 +01:00
veeck
958a2ee6ec Update CHANGELOG 2020-11-24 09:56:54 +01:00
veeck
ea264cb15e Update console-stamp to latest version and configure it 2020-11-24 09:54:59 +01:00
Johan Alvinger
d8f19e631c Fixed variable declarations to pass Travic CI check 2020-11-24 01:20:19 +01:00
Johan Alvinger
ce5c0ed5ba Fixed typo in condition 2020-11-24 00:37:21 +01:00
Johan Alvinger
839ca9ecfb Lines that were commented out has been removed 2020-11-24 00:23:55 +01:00
Johan Alvinger
720bc12c00 Changelog updated 2020-11-24 00:13:07 +01:00
Johan Alvinger
1ba845fb06 Make calendarfetcher return all events, not just a slice 2020-11-23 21:53:20 +01:00
Johan Alvinger
e86fa9d24a Revised handling of timeFormat "absolute" and "relative".
Added new option "coloredEvents" which contain an array with keyword and color for coloring events matching keyword
2020-11-23 21:48:34 +01:00
marvai-vgtu
e348a61085 Update CHANGELOG.md 2020-11-22 14:02:21 +02:00
marvai-vgtu
2f70366299 Update CHANGELOG.md 2020-11-22 14:00:41 +02:00
marvai-vgtu
c466b20558 Update lt.json 2020-11-22 13:56:24 +02:00
marvai-vgtu
021f8d25a5 Update lt.json
Geographic translation changes with "FEELS" change
2020-11-22 13:54:31 +02:00
Johan Alvinger
a19c3a43d8 New option "limitDays" that will limit the number of days displayed. 2020-11-21 18:03:34 +01:00
Andrew
819923e66d Merge branch 'develop' into weatherbit 2020-11-20 15:09:34 -05:00
Michael Teeuw
6c3100e250 Merge pull request #2202 from sdetweil/cal-again
fix full day events east of UTC start time and duration
2020-11-20 20:39:59 +01:00
Andrew
1065eda47f Update CHANGELOG.md 2020-11-20 14:37:02 -05:00
Andrew
afd676a958 Merge branch 'develop' into weatherbit 2020-11-16 18:29:11 -05:00
Sam Detweiler
12405b66d0 fix full date start time and duration, east of UTC, make getCorrection more understandable with variable name changes 2020-11-16 13:49:44 -06:00
Sam Detweiler
ecd0b6fa83 Merge branch 'develop' of https://github.com/MichMich/MagicMirror into cal-again 2020-11-16 09:50:09 -06:00
Sam Detweiler
3a8587378c fix full date start time and duration, east of UTC, make getCorrection more understandable with variable name changes 2020-11-16 10:32:29 -05:00
Michael Teeuw
a05c08ed48 Merge pull request #2196 from ashishtank/develop
Added Hindi and Gujarati translations
2020-11-16 16:29:59 +01:00
Sam Detweiler
469a90787b fix full date start time and duration, east of UTC, make getCorrection more understandable with variable name changes 2020-11-16 10:23:33 -05:00
Sam Detweiler
9e5a9b5ced fix full date start time and duration, east of UTC, make getCorrection more understandable with variable name changes 2020-11-16 10:17:48 -05:00
Michael Teeuw
f311ba3f7c Merge branch 'develop' into develop 2020-11-15 20:14:37 +01:00
Michael Teeuw
fc68321bd5 Merge branch 'develop' into develop 2020-11-15 20:13:58 +01:00
Michael Teeuw
8a4173d0ad Merge pull request #2175 from jakemulley/packages
Update npm packages and resolve breaking changes from packages
2020-11-15 20:06:40 +01:00
Michael Teeuw
90af31cb2f Merge branch 'develop' into develop 2020-11-15 20:03:10 +01:00
Michael Teeuw
61bcd9337b Merge pull request #2193 from mirontoli/develop
Add Chuvash translation
2020-11-15 20:01:40 +01:00
Michael Teeuw
7944045893 Merge branch 'develop' into weatherbit 2020-11-15 19:59:12 +01:00
Michael Teeuw
2c3f83c70d Merge pull request #2186 from rejas/use_debug
Use Log.debug where applicable
2020-11-15 19:58:06 +01:00
Andrew
92ab705ff5 Update weatherbit.js
Post npm lint:prettier
2020-11-14 18:32:18 -05:00
Andrew
2951f0c40c Update weatherbit.js 2020-11-14 18:28:20 -05:00
Andrew
8a23bccb70 Update CHANGELOG.md 2020-11-14 18:20:02 -05:00
Ashish Tank
aa6ad01fb9 Change log 2020-11-11 16:12:03 +01:00
Ashish Tank
ef325896c5 Merge branch 'develop' of https://github.com/ashishtank/MagicMirror into develop
# Conflicts:
#	CHANGELOG.md
2020-11-11 16:03:10 +01:00
Ashish Tank
a270c73d7c Hindi and Gujarati Language 2020-11-11 16:01:03 +01:00
ashishtank
16feda895d Merge pull request #5 from MichMich/develop
Develop merge
2020-11-11 14:46:18 +01:00
Anatoly Mironov
874a50d142 update changelog for Chuvash translation addition 2020-11-10 23:09:50 +01:00
Anatoly Mironov
ae32645470 improve Chuvash translation 2020-11-10 23:05:28 +01:00
Anatoly Mironov
d4412fe06f Add Chuvash 2020-11-10 23:01:08 +01:00
Anatoly Mironov
b42c05fb56 Create cv.json 2020-11-10 23:00:04 +01:00
Jake Mulley
8d0da61bd4 Allow node-ical minor version upgrades again 2020-11-10 20:55:33 +00:00
Jake Mulley
8b8be261cd Update packages 2020-11-10 20:40:18 +00:00
Jake Mulley
2b6ceed897 Sync package-lock.json 2020-11-08 21:27:47 +00:00
Jake Mulley
8abca801a2 Revert node-ical to 0.12.2 2020-11-08 21:22:11 +00:00
Jake Mulley
1eae4425d8 Merge branch 'develop' into packages 2020-11-08 21:21:31 +00:00
Jake Mulley
a6386bd60e Update npm packages and resolve package changes 2020-11-08 21:18:19 +00:00
Aurélien Veillard
1460f002ab Add new parameter "useKmh" to weather module
Add new parameter "useKmh" to weather module for displaying wind speed as km/h instead of m/s when windUnits is set to metric.
2020-11-08 14:01:02 +01:00
Aurélien Veillard
0d6f736c2c Fix windspeed convertion error in ukmetoffice weather provider
Fix windspeed convertion error from mph to m/s in ukmetoffice weather provider (#2189)
2020-11-08 12:37:21 +01:00
Andrew
da88e11d04 Add files via upload
Adds support for Weatherbit.io

API Key needed, based on existing classes for Dark Sky and Weather.gov
2020-11-07 20:15:54 -05:00
rejas
0a58767563 Use Log.debug where applicable 2020-11-07 11:59:01 +01:00
Aurélien Veillard
198525f2ce Weather config enhancement
Add new parameter 'useKmh' to display wind speed in km/h instead of m/s.
2020-11-07 09:54:13 +01:00
Michael Teeuw
3220702034 Merge pull request #2178 from sdetweil/fix-cal2
Fix calendar when no DTEND record in event
2020-11-06 21:48:59 +01:00
Michael Teeuw
262711a127 Merge pull request #2184 from rejas/debug_log_level
Add new log level "debug"
2020-11-06 21:47:41 +01:00
veeck
078438442a Cleanup some log levels 2020-11-06 11:47:29 +01:00
veeck
5ac20d65ac Add new log level "debug" for such a purpose 2020-11-06 11:47:09 +01:00
Sam Detweiler
5d2f706c32 fix time offset calcs for any timezone 2020-11-01 09:05:33 -06:00
Sam Detweiler
844a59d880 one more offset only test 2020-10-28 13:10:24 -05:00
Sam Detweiler
82c742f964 fix prettier problems, was run on commit before 2020-10-28 16:55:53 +01:00
Sam Detweiler
b73cfd8a60 fix time adjustment routine 2020-10-28 16:42:20 +01:00
Sam Detweiler
8466ff0c1a change debug logging from console. to Log. 2020-10-28 09:42:00 -05:00
Sam Detweiler
1e0fc7eb0d add package-lock for test 2020-10-28 09:38:31 -05:00
Sam Detweiler
74b0c3ea57 update node-ical dependency version 2020-10-28 09:32:38 -05:00
Sam Detweiler
a735275864 fix full day recurring events east of UTC for RRULE/Luxon confusion, also update windows zones to include old offset based strings (UTC+05:00) US Eastern and Canada 2020-10-28 09:31:05 -05:00
Sam Detweiler
8a1d46deb4 fix fullday event compare operator 2020-10-26 12:52:32 -05:00
Sam Detweiler
b624aeec45 fix the fullDayEvent checker function 2020-10-26 12:41:03 -05:00
Sam Detweiler
e83b4d7d42 fix calendar when event has no DTEND record 2020-10-26 09:55:35 -05:00
Michael Teeuw
da302fce32 Merge pull request #2158 from flopp999/patch-2
Weather - forecast change day of week
2020-10-20 09:34:16 +02:00
flopp999
e09fec56e2 Update weather_spec.js
try to change the default to Today and Tomorrow
2020-10-20 07:26:39 +02:00
sam detweiler
568f952573 Merge pull request #3 from MichMich/develop
sync
2020-10-16 14:47:40 -05:00
Michael Teeuw
fc1e488391 Merge pull request #2169 from bluemanos/patch-1
A space after icon of sunrise and sunset
2020-10-16 13:21:43 +02:00
Szymon Bluma
5781c258e0 A space after icon of sunrise and sunset 2020-10-16 13:10:02 +02:00
Michael Teeuw
13c542aeda Merge pull request #2167 from MichMich/fix-greek-translation
Fix greek translation. Rename GR to EL. (Fixes: #2155)
2020-10-15 09:39:50 +02:00
Michael Teeuw
02148f68e5 Rename greek translation. (#2155) 2020-10-15 09:26:56 +02:00
Michael Teeuw
3c84002abd Fix linter issues. 2020-10-15 09:20:44 +02:00
sam detweiler
01aa57e493 Merge pull request #2 from MichMich/develop
resynch after revert
2020-10-13 10:13:47 -05:00
Michael Teeuw
af09b4214a Merge pull request #2165 from MichMich/revert-2161-develop
Revert "Fixes the un-hide problem with currentwether and weatherforcast modules."
2020-10-13 16:15:16 +02:00
Michael Teeuw
43b6c71205 Revert "Fixes the un-hide problem with currentwether and weatherforcast modules." 2020-10-13 16:14:52 +02:00
sam detweiler
480e1adfbf Merge pull request #1 from MichMich/develop
synch fork
2020-10-13 08:51:10 -05:00
Michael Teeuw
8c04712784 Merge pull request #2162 from sdetweil/fixfetch
Fix calendar subsequent fetch timing with multiple calendar entries
2020-10-13 09:13:28 +02:00
Michael Teeuw
0c61ba8f2d Merge pull request #2161 from Snille/develop
Fixes the un-hide problem with currentwether and weatherforcast modules.
2020-10-13 09:13:05 +02:00
Sam Detweiler
485f662d75 revert fetcher filtering results 2020-10-12 10:12:47 -05:00
Sam Detweiler
26caeec0c1 Merge branch 'fixfetch' of https://github.com/sdetweil/MagicMirror into fixfetch 2020-10-12 09:02:15 -05:00
Sam Detweiler
4a7cb88a3e typo, remove dead code, comments, fix recurring refresh, let fetcher handle it 2020-10-12 09:01:50 -05:00
flopp999
d11696015d Update forecast.njk 2020-10-12 09:10:51 +02:00
flopp999
3b76ca4f9b Update forecast.njk 2020-10-12 09:09:42 +02:00
sam detweiler
6f3239d514 Merge branch 'develop' into fixfetch 2020-10-11 22:46:31 -05:00
Sam Detweiler
e8f60d39de fix subsequent calendar fetcher timing 2020-10-11 22:43:11 -05:00
Sam Detweiler
a3bad8aec4 fix subsequent calendar fetcher timing 2020-10-11 22:39:42 -05:00
Erik Pettersson
644aa26b75 Update CHANGELOG.md
Added the "currentwather" and "watherforcast" update show fixes information.
2020-10-11 21:41:59 +02:00
Erik Pettersson
ecd9828afc Makes the module stay hidden without a lock.
When this module is hidden from another module, it un-hides itself when it updates itself. With this change it stays hidden, as it should. :)
2020-10-11 21:37:00 +02:00
Erik Pettersson
7462d61e16 Makes the module stay hidden without a lock.
When this module is hidden from another module, it un-hides itself when it updates itself. With this change it stays hidden, as it should. :)
2020-10-11 21:35:42 +02:00
Ashish Tank
b95ef7250d Merge branch 'develop' of https://github.com/ashishtank/MagicMirror into develop 2020-10-11 15:43:03 +02:00
flopp999
569dec1b0b Update CHANGELOG.md 2020-10-11 09:42:09 +02:00
flopp999
1bc0270d7b Update forecast.njk
change name of day to today and tomorrow
2020-10-11 09:36:52 +02:00
ashishtank
ebfeebc40b Merge pull request #4 from MichMich/develop
Merge from Develop
2020-10-07 15:58:47 +02:00
Michael Teeuw
ec80b25087 Merge pull request #2150 from sdetweil/fixparse
add error handler to json parsing of translation files
2020-10-06 10:23:46 +02:00
Michael Teeuw
7a2278d7b6 Merge branch 'develop' into fixparse 2020-10-06 10:23:34 +02:00
Michael Teeuw
28b93b70fa Merge pull request #2152 from sdetweil/new_cal
update calendar handler code for rrule and daylight/standard time adjustments
2020-10-06 10:19:58 +02:00
Michael Teeuw
0431f45190 Merge branch 'develop' into new_cal 2020-10-06 10:16:08 +02:00
Michael Teeuw
96ce444061 Merge pull request #2154 from sdetweil/fixical
fix node-ical version
2020-10-06 10:15:10 +02:00
sam detweiler
32776e7ca3 Merge branch 'develop' into fixparse 2020-10-05 13:11:55 -05:00
sam detweiler
35d516ad4d Merge branch 'develop' into new_cal 2020-10-05 13:11:11 -05:00
sam detweiler
e6a17d27b9 Merge branch 'develop' into fixical 2020-10-05 13:10:25 -05:00
Sam Detweiler
52edec1b64 fix wrong node-ical version requested, package.json 2020-10-05 12:59:30 -05:00
Sam Detweiler
6440d2289c fix translation files with comments crashing UI 2020-10-05 12:58:10 -05:00
Sam Detweiler
11afadcea8 fix RRULE bad date and add Windows Timezone name support 2020-10-05 12:56:25 -05:00
Sam Detweiler
2e981987f2 resolve conflict 2020-10-05 11:06:21 -05:00
Sam Detweiler
69efca0bdb use Log.error instead of console.log for error message, shows in output 2020-10-05 10:45:07 -05:00
Sam Detweiler
eefb92367e fix node-ical version 2020-10-05 10:02:29 -05:00
Sam Detweiler
8fa96c2836 add error handler to json parsing of translation files 2020-10-04 12:13:08 -05:00
Sam Detweiler
04dae52a6c correct daylight/standard adjustments, windows timezones and east of london rrule bug 2020-10-04 12:10:19 -05:00
Ashish Tank
37c9da1351 Merge branch 'develop' of https://github.com/ashishtank/MagicMirror into develop 2020-10-02 22:01:50 +02:00
ashishtank
86d7ec6270 Merge pull request #3 from MichMich/develop
Sync with Develop
2020-10-02 18:06:24 +02:00
Ashish Tank
82f1e0fe32 Added Hindi and Gujarati languages 2020-10-02 17:51:41 +02:00
Michael Teeuw
6ec0aa8894 Prepare 2.14.0-develop 2020-10-01 12:19:57 +02:00
Michael Teeuw
3dbe8bfbbf Merge pull request #2143 from MichMich/develop
Release 2.13.0
2020-10-01 12:13:38 +02:00
Michael Teeuw
20d82bab78 Prepare 2.13.0 2020-10-01 11:55:18 +02:00
Michael Teeuw
60f6123b64 Merge pull request #2139 from sdetweil/develop
restore ical for 3rd party modules
2020-09-29 17:45:49 +02:00
Sam Detweiler
74887a58f6 remove regression on event duration 2020-09-29 10:33:51 -05:00
Sam Detweiler
76821d16fc restore ical for 3rd party modules 2020-09-24 10:13:25 -05:00
Michael Teeuw
233a6b5e90 Merge pull request #2137 from rejas/md
Update Changelog and Licsense file
2020-09-22 16:16:22 +02:00
Michael Teeuw
a1d9337c81 Merge pull request #2136 from sdetweil/develop
fix package.json
2020-09-22 16:16:08 +02:00
rejas
e3fec6c3e4 Update Changelog and License 2020-09-22 15:47:16 +02:00
Sam Detweiler
5de88b4cc1 remove added lodash 2020-09-22 08:40:40 -05:00
Sam Detweiler
af5e6655e0 fix pull 2020-09-22 08:38:15 -05:00
Sam Detweiler
a8716799c9 fix dependency versioning error 2020-09-22 08:33:21 -05:00
Michael Teeuw
1672027091 Merge pull request #2135 from sdetweil/develop
update to node-ical, handle MS timezones and timezones in general in cal entries
2020-09-22 14:55:27 +02:00
sam detweiler
934ac886cb Merge branch 'develop' into develop 2020-09-22 07:49:19 -05:00
sam detweiler
1b47c4d16f add package-lock 2020-09-22 07:31:02 -05:00
sam detweiler
e8fd906aa1 move calendar to node-ical for ms timezones and timezones in general 2020-09-22 07:25:48 -05:00
Michael Teeuw
57b6ea1297 Merge pull request #2134 from bugsounet/patch-1
New ConfigMerge code purpose
2020-09-22 10:54:35 +02:00
bugsounet
102ff15a99 Checking formatting...
All matched files use Prettier code style!
2020-09-22 00:26:24 +02:00
Cédric
19148cfc84 Update loader.js 2020-09-22 00:04:05 +02:00
Cédric
97c2bab58a apply ConfigMerge() if configDeepMerge: true 2020-09-21 23:21:43 +02:00
Cédric
1f473228ce New purpose for configMerge() 2020-09-21 23:17:41 +02:00
Cédric
340a8e4176 (test react for in my repo) 2020-09-21 22:36:54 +02:00
Michael Teeuw
67201a52cc Merge pull request #2016 from Travelbacon/develop
Changed "Gevoelstemperatuur" to "Voelt als". This is shorter and save…
2020-09-21 12:48:09 +02:00
Michael Teeuw
30e164fe0a Fix merge conflict. 2020-09-21 12:21:08 +02:00
Michael Teeuw
cfcb20a468 Merge pull request #2133 from rejas/dependencies
Cleanup dependencies
2020-09-21 12:18:37 +02:00
Joris
24eac7ef41 For the mergne 2020-09-20 20:57:20 +02:00
ashishtank
94162f1b45 Merge pull request #1 from MichMich/develop
Catching up on 19th Sep 2020
2020-09-19 17:20:35 +02:00
rejas
01c19efc0c Update CHANGELOG 2020-09-19 12:30:17 +02:00
rejas
e4f2a8a23b Update dependencies 2020-09-19 12:25:11 +02:00
rejas
89b33d2c62 Move lodash to devDependencies sicne its used only in testing 2020-09-19 10:50:06 +02:00
rejas
3ced35a2f8 Dont generally use latest versions of dependencies 2020-09-19 10:49:59 +02:00
Michael Teeuw
fd4576b234 Merge pull request #2081 from bryanzzhu/bryanzzhu-weather
adds current, hourly, and daily forecasts to the Weather module (OpenWeatherMap One Call API)
2020-09-18 12:28:20 +02:00
Michael Teeuw
812ad829c9 Merge pull request #2125 from MichMich/dependabot/npm_and_yarn/node-fetch-2.6.1
Bump node-fetch from 2.6.0 to 2.6.1
2020-09-18 12:27:57 +02:00
Michael Teeuw
207a9ba723 Merge branch 'develop' into bryanzzhu-weather 2020-09-18 12:15:44 +02:00
Michael Teeuw
f9bf17d701 Merge pull request #2105 from rejas/tests
Cleanup clock tests
2020-09-18 12:12:29 +02:00
dependabot[bot]
43b96ccd1e Bump node-fetch from 2.6.0 to 2.6.1
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-10 22:35:52 +00:00
rejas
14e8cdd9b2 Update Changelog 2020-09-02 13:42:35 +02:00
rejas
259068b860 Remove unused dependency, update eslint 2020-09-02 13:42:35 +02:00
rejas
a1a4192835 Fix clock test, remove now unneeded dependency 2020-09-02 13:42:35 +02:00
rejas
45f09dcc8c Add test for analog clock face 2020-09-02 13:42:35 +02:00
Michael Teeuw
15c3f11f4a Merge pull request #2121 from sdetweil/develop
fix full day recurring events not showing
2020-09-02 09:04:08 +02:00
Michael Teeuw
1a21027850 Fix Prettier issues. 2020-09-02 09:03:46 +02:00
Michael Teeuw
8427a9fc68 Merge pull request #2103 from rejas/issue_1985
Fix "undefined" in weather modules header.
2020-09-02 08:59:53 +02:00
Sam Detweiler
f09b89f975 fix , prettier not run 2020-09-01 15:13:42 -05:00
sam detweiler
7bec84f767 fix formatting, prettier did not run 2020-09-01 21:00:14 +01:00
sam detweiler
8f4cbcf817 Merge branch 'develop' of https://github.com/sdetweil/MagicMirror into develop 2020-09-01 14:41:09 +01:00
sam detweiler
c3382274a2 remove old master branch code 2020-09-01 14:40:07 +01:00
sam detweiler
493055f861 Merge branch 'develop' into develop 2020-09-01 08:15:24 -05:00
sam detweiler
11fbbd49f3 add changelog 2020-09-01 14:10:25 +01:00
sam detweiler
8ce37d53cd fix recurring full date events 2020-09-01 14:04:35 +01:00
sam detweiler
baa4012872 Merge pull request #98 from MichMich/master
catch up
2020-08-31 19:15:08 -05:00
rejas
16c5eba2be Update Changelog 2020-08-13 21:51:18 +02:00
rejas
ec08cb32aa Set visibility of header more explicitly 2020-08-13 21:51:18 +02:00
rejas
86fb1b938b Check for undefined header data in weather modules 2020-08-13 21:51:18 +02:00
rejas
5aa7097a6e Add test for displaying the header 2020-08-13 21:51:18 +02:00
rejas
fd3f520e95 Fix typos 2020-08-13 21:51:18 +02:00
rejas
49ff92892f Warn user if he uses the sample value in the config for the apiid 2020-08-13 21:51:18 +02:00
Michael Teeuw
904f5a2656 Merge pull request #2112 from rejas/issue_2109
Fix config check after merge of prettier
2020-08-13 21:44:08 +02:00
Michael Teeuw
bb105dd832 Merge pull request #2101 from rejas/jsdoc
Cleanup jsdoc comments
2020-08-13 21:43:27 +02:00
Michael Teeuw
fd934c0514 Merge pull request #2100 from Bee-Mar/develop
Added --dry-run to fetch call within updatednotification module
2020-08-13 21:42:38 +02:00
rejas
66609428a2 Remove thrown Errors and add some color to the ouput 2020-08-08 22:16:27 +02:00
rejas
0056e0bc6d Update CHANGELOG 2020-08-07 09:58:55 +02:00
rejas
056b66a764 Dont use the prettier based config for verifying the syntax 2020-08-07 09:57:06 +02:00
rejas
3438a5a374 Cleanup newsfeed jsdoc 2020-08-03 11:36:29 +02:00
rejas
9f3806dabf Update eslint jsdoc plugin 2020-08-03 11:20:11 +02:00
rejas
1d4d5cc4e7 Cleanup calendar jsdoc 2020-08-03 11:19:54 +02:00
Bryan Zhu
3901543697 readded processing for numEntries option that I accidentally took out 2020-08-02 00:22:19 -04:00
Bryan Zhu
a4d73e2a67 amended code according to pull request reviews 2020-08-01 17:39:58 -04:00
Bryan Zhu
f6854f58ff ran prettier on weather module code 2020-08-01 14:03:48 -04:00
Bryan Zhu
ad7f7d2890 Merge branch 'bryanzzhu-weather' of https://github.com/bryanzzhu/MagicMirror into bryanzzhu-weather 2020-08-01 13:33:21 -04:00
Bryan Zhu
498db63cac Merge branch 'develop' of https://github.com/MichMich/MagicMirror into develop 2020-08-01 13:32:29 -04:00
rejas
05659820d0 Minor cleanups 2020-08-01 17:06:56 +02:00
rejas
02779ef725 Cleanup main jsdoc 2020-08-01 17:06:46 +02:00
rejas
935c9b6a42 Cleanup loader jsdoc 2020-08-01 16:38:42 +02:00
rejas
522f7644a3 Add typedef for Module, use it in other jsdocs 2020-08-01 16:31:42 +02:00
Bryan Zhu
b1a67d1fc5 integrated onecall usage into existing types 2020-08-01 02:59:08 -04:00
rejas
0674c0aff6 Updated changelog 2020-07-30 13:00:02 +02:00
rejas
c8664d5952 Cleanup weatherprovider jsdoc 2020-07-30 12:58:35 +02:00
rejas
8ce1e2c956 Cleanup calendar jsdoc 2020-07-30 12:54:39 +02:00
rejas
90cca28e01 Cleanup clock jsdoc 2020-07-30 09:36:01 +02:00
rejas
e489b7101c Cleanup notificationFx jsdoc 2020-07-30 09:33:19 +02:00
rejas
eee64c8064 Cleanup clientonly jsdoc 2020-07-29 22:13:19 +02:00
Brandon Marlowe
a0d4e8dafc updated changelog 2020-07-28 23:55:39 -04:00
Brandon Marlowe
e6b94df06d Merge branch 'mmpm-integration' of github.com:Bee-Mar/MagicMirror into develop 2020-07-28 23:53:03 -04:00
Brandon Marlowe
6f1c7d6253 added --dry-run option to prevent updatenotification module from consuming fetch result 2020-07-28 23:51:38 -04:00
rejas
2d5a19b676 Cleanup module jsdoc 2020-07-28 17:29:21 +02:00
rejas
d1e8dded68 Cleanup class jsdoc 2020-07-28 17:29:16 +02:00
rejas
9888f66c84 Cleanup check_config jsdoc 2020-07-28 17:29:10 +02:00
rejas
5ec51d0ccc Cleanup app jsdoc 2020-07-28 16:49:02 +02:00
rejas
79f9331073 Cleanup translator jsdoc 2020-07-27 21:37:08 +02:00
rejas
43bcf4ab98 Run eslint over files, see what gets fixed automatically and clean up 2020-07-27 14:25:43 +02:00
rejas
f4eae72c48 Install eslint jsdoc plugin 2020-07-27 13:10:07 +02:00
Michael Teeuw
9d14d3e5b7 Merge pull request #2095 from rejas/issue_2072_take_2
Fix clock face-006
2020-07-20 19:27:55 +02:00
rejas
26d3141489 Fix face-006 too 2020-07-20 19:13:12 +02:00
Michael Teeuw
5c44f51d6d Merge pull request #2079 from rejas/issue_2023
Added config option to calendar-icons for recurring- and fullday-events
2020-07-19 20:41:22 +02:00
rejas
50f3f32ba8 Add tests for custom icons 2020-07-19 11:54:03 +02:00
rejas
8fa858ca8c Cleanup test descriptions 2020-07-18 22:16:44 +02:00
rejas
8b1d1671f7 Add test for changing the calendar symbol 2020-07-18 21:10:36 +02:00
rejas
73aa35ea2c Pass maximumEntries for each calender to fetcher
Fixes failing test
2020-07-18 17:51:21 +02:00
rejas
a391445e5f Add test setup for custom calendar configuration 2020-07-18 13:58:05 +02:00
rejas
530c5d416a Remove now unused test data 2020-07-18 11:32:01 +02:00
rejas
319a13c0ef Remove orphaned ical entry 2020-07-17 18:20:48 +02:00
rejas
ec2fedd797 Make sure default symbol is first from right-to-left 2020-07-17 18:00:27 +02:00
rejas
7489d19784 Merge calendar symbols 2020-07-17 18:00:27 +02:00
rejas
3b5a0e8d66 Added config option to calendar-icons for recurring- and fullday-events 2020-07-17 18:00:27 +02:00
Michael Teeuw
29ed63286c Merge pull request #2089 from rejas/newsfeed
Cleaned up newsfeed module code
2020-07-17 16:28:44 +02:00
Michael Teeuw
108da4b6d4 Merge pull request #2084 from easyas314/wxGov
Wx gov
2020-07-17 16:26:20 +02:00
Michael Teeuw
a00a1c64d1 Merge pull request #2083 from sthuber90/backward-browser-compatability
Backward browser compatability
2020-07-17 16:25:35 +02:00
Michael Teeuw
275825cfe1 Merge pull request #2082 from rejas/istanbul
Add test coverage tool Istanbul
2020-07-17 16:25:00 +02:00
Michael Teeuw
cd2fce56a6 Merge branch 'develop' into istanbul 2020-07-17 16:23:59 +02:00
Michael Teeuw
cab850fb35 Merge pull request #2080 from larryare/develop
Add lithuanian translation
2020-07-17 16:21:19 +02:00
Michael Teeuw
045dc600b0 Merge branch 'develop' into develop 2020-07-17 16:21:12 +02:00
Michael Teeuw
1b199c1682 Merge pull request #2077 from oemel09/fix-maxNumberOfDays-2018
Fixes getting only full day forecasts
2020-07-17 16:18:57 +02:00
Michael Teeuw
936fa637ec Merge pull request #2078 from cjbrunner/cbrunner/weatherforecast-openweather-onecall-api-support
weatherforecast OpenWeather onecall api support
2020-07-17 16:18:17 +02:00
rejas
b9ccb7a892 Add new test to see if coverage improves
it does :-)

  calendar.js                       |    8.97 |     9.57 |   11.54 |     8.7 | 63-413,446-659,690-693,699,733-771

->

  calendar.js                       |    9.97 |    10.64 |   11.54 |     9.7 | 63-413,446-659,699,733-771
2020-07-15 13:15:03 +02:00
rejas
95b33be3cf Update vendor dependencies 2020-07-15 12:14:48 +02:00
rejas
cde27b89c2 Update CHANGELOG 2020-07-14 07:51:19 +02:00
rejas
476e52e004 Use let/const and arrow functions 2020-07-12 13:21:36 +02:00
rejas
8dc88fe64b Remove unused helper function and its unit test 2020-07-12 10:32:16 +02:00
rejas
d00da790af Move configuration change warning into general translation 2020-07-12 08:25:07 +02:00
rejas
c61fac75e4 Rename Newsfeed Fetcher for clarity 2020-07-12 08:10:43 +02:00
easyas314
e949af6fd5 revert package.json 2020-07-11 16:13:31 -04:00
easyas314
c1e38b917e update CHANGELOG 2020-07-11 15:06:24 -04:00
easyas314
1244121216 change weathergov.js for multi-URL api 2020-07-11 15:01:16 -04:00
easyas314
46543daa13 Merge branch 'wxGov' of github.com:easyas314/MagicMirror into wxGov 2020-07-11 14:56:35 -04:00
Brad Simmons
6277384bf9 Merge pull request #3 from MichMich/develop
Merge MM/Develop
2020-07-11 14:54:22 -04:00
Stephan Huber
17549fed9c Update CHANGELOG.md 2020-07-11 08:25:01 +02:00
Stephan Huber
377e42affc fix display style for older browsers 2020-07-10 23:12:17 +02:00
Stephan Huber
0ac5d56865 must be const for backward compatibility 2020-07-10 23:03:36 +02:00
rejas
b59ee6ad7e Update dependencies 2020-07-09 22:43:08 +02:00
rejas
2d7c5a827f Cleanup test expectation 2020-07-09 20:44:52 +02:00
Veeck
85c32ef843 Use const in test requires 2020-07-09 20:30:43 +02:00
rejas
dc089d7db9 Remove stripCommentsFromJson code and test
Since we dont allow comments in the json anymore anyway
2020-07-09 20:30:26 +02:00
rejas
d570f910f8 Added test coverage with istanbul 2020-07-09 20:29:38 +02:00
Bryan Zhu
8f2731911b added changelog entry 2020-07-08 23:12:01 -04:00
Bryan Zhu
9d22420027 Merge branch 'develop' of https://github.com/MichMich/MagicMirror into bryanzzhu-weather 2020-07-08 23:06:29 -04:00
larryare
029da3791b Update CHANGELOG.md
Added lithuanian language entry.
2020-07-08 23:57:57 +03:00
Laurynas Kerezius
caa51b1029 Add lithuanian language 2020-07-08 20:52:24 +00:00
Chris Brunner
2973a03d40 Prettier... 2020-07-08 10:11:12 -07:00
Chris Brunner
679d464f4c Update chagelog 2020-07-08 09:53:50 -07:00
Chris Brunner
93c0787efd Add support for OpenWeather onecall API 2020-07-08 09:48:03 -07:00
oemel09
a1708a1469 Fixes getting only full day forecasts 2020-07-08 10:19:49 +02:00
Michael Teeuw
862bf78e63 Merge pull request #2071 from chamakura/develop
Fixing double conversion of time between UTC and local timezone
2020-07-05 22:06:59 +02:00
Michael Teeuw
8c01df50a2 Merge pull request #2073 from rejas/issue_2072
Fix incorrect namespace links in svg clockfaces
2020-07-05 12:42:42 +02:00
rejas
921932920d Fix incorrect namespace links in svg clockfaces
Fixes #2072
2020-07-05 11:58:37 +02:00
chamakura
d021c9b4ae Removing debug log statements 2020-07-04 20:23:09 -07:00
chamakura
309a18e9c0 Fixing double conversion of time between UTC and local timezone 2020-07-04 20:12:34 -07:00
Michael Teeuw
73322c96a6 Merge pull request #2070 from rejas/fix_log_level
Fix log level
2020-07-04 22:10:16 +02:00
rejas
121e7db330 Add CHANGELOG entry 2020-07-04 22:03:13 +02:00
rejas
9f5e1b59fb Set logLevel after loading config 2020-07-04 22:02:39 +02:00
Michael Teeuw
3282ed4fea Add TODO 2020-07-04 21:49:14 +02:00
Michael Teeuw
c7d9192f18 Merge pull request #2060 from bryanzzhu/bryanzzhu-weather-patch
fixed minor typo (apiKey vs. appid) in weather.js
2020-07-04 21:47:52 +02:00
Michael Teeuw
bd0f707aed Add changelog. 2020-07-04 21:40:31 +02:00
Michael Teeuw
8b30634ebe Merge branch 'develop' into pr/bryanzzhu/2060 2020-07-04 21:38:29 +02:00
Michael Teeuw
ca8acb14f1 Merge pull request #2057 from wolfen351/wolfen351-patch-1
Correct calendar display - account for current timezone
2020-07-04 21:34:32 +02:00
Michael Teeuw
26b8f8d0d5 Merge branch 'develop' into pr/bryanzzhu/2060 2020-07-04 21:33:50 +02:00
Michael Teeuw
a066153556 Add updated changelog. 2020-07-04 21:26:22 +02:00
Michael Teeuw
2ee7131c28 Merge branch 'develop' into pr/wolfen351/2057 2020-07-04 21:24:48 +02:00
Michael Teeuw
cb1f65732e Merge branch 'develop' into pr/wolfen351/2057 2020-07-04 21:15:14 +02:00
Michael Teeuw
37237d9c10 Merge pull request #2066 from oemel09/fix-maxNumberOfDays-2018
Adjusts maxNumberOfDays depending on the endpoint
2020-07-03 22:02:26 +02:00
Michael Teeuw
92cc41dec6 Fix linting issue. 2020-07-03 21:53:18 +02:00
oemel09
246dc663c4 Applies suggestions from prettier, floor instead of ceil 2020-07-02 22:22:04 +02:00
oemel09
4ed3235590 Adds fix to changelog 2020-07-02 20:56:48 +02:00
oemel09
0939e405b4 Adjusts maxNumberOfDays depending on the endpoint 2020-07-02 20:54:20 +02:00
Bryan Zhu
584c51ef56 Merge branch 'develop' of https://github.com/MichMich/MagicMirror into bryanzzhu-weather 2020-07-01 14:49:05 -04:00
Michael Teeuw
26ed05ba3a Prepare 2.13.0-develop 2020-07-01 20:07:12 +02:00
Bryan Zhu
5bb72cfed8 Merge branch 'develop' of https://github.com/MichMich/MagicMirror into bryanzzhu-weather 2020-07-01 08:38:46 -04:00
Bryan Zhu
85ed1b85ae added functional current, hourly, and daily forecasts via OpenWeatherMap One Call API 2020-07-01 05:08:04 -04:00
Bryan Zhu
1d2f929d3f Rename wdataHourly.njk to wdatahourly.njk 2020-06-30 13:21:50 -04:00
Bryan Zhu
ffbf0804d9 added default values for lat and lon 2020-06-30 12:41:14 -04:00
Bryan Zhu
ca0b89ecd3 backtracked apiKey change to appid 2020-06-30 12:36:23 -04:00
Bryan Zhu
dc9d6f6b79 fixed minor typo (apiKey vs. appid) 2020-06-30 12:29:00 -04:00
Bryan Zhu
f73520559e typo and bug fixes 2020-06-30 12:06:16 -04:00
Bryan Zhu
a4df38d963 fixed config parameter typo 2020-06-30 04:18:03 -04:00
Bryan Zhu
4a162543f6 added OpenWeatherMap One Call API function to default Weather module, added wDataHourly type 2020-06-30 02:40:41 -04:00
wolfen351
d5caadd906 Correct calendar display - account for current timezone 2020-06-27 19:43:09 +12:00
easyas314
9d2d170c2d add additional URLs per API 2020-06-13 11:58:06 -04:00
easyas314
df3aa22c59 current appears to be working 2020-06-04 23:35:32 -04:00
easyas314
a4aabfcdae add the lock file 2020-06-04 19:26:09 -04:00
easyas314
ce99e70bf9 tweak package; my wxgov.js 2020-06-04 19:25:13 -04:00
GeorgeTravelbacon
9d249406e3 Forgot CHANGELOG.MD 2020-05-10 17:07:34 +02:00
Joris
42d9e7a090 Changed "Gevoelstemperatuur" to "Voelt als". This is shorter and saves mirror space. 2020-05-10 15:18:24 +02:00
105 changed files with 12805 additions and 10656 deletions

View File

@@ -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
View 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
View 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
View File

@@ -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/**/*

View File

@@ -1,5 +1,5 @@
package-lock.json
/config/**/*
/modules/default/calendar/vendor/ical.js/**/*
/vendor/**/*
!/vendor/vendor.js
.github/**/*

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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
View File

@@ -1,2 +0,0 @@
*
!config.js.sample

View File

@@ -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"
}
},

View File

@@ -2,6 +2,7 @@ html {
cursor: none;
overflow: hidden;
background: #000;
user-select: none;
}
::-webkit-scrollbar {

View File

@@ -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...");

View File

@@ -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!");
}
});
}

View File

@@ -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;

View File

@@ -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 = {

View File

@@ -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) {

View File

@@ -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;
});

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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"];

View File

@@ -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.
*/

View File

@@ -10,7 +10,8 @@ var Utils = {
colors: {
warn: colors.yellow,
error: colors.red,
info: colors.blue
info: colors.blue,
pass: colors.green
}
};

View File

@@ -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(() => {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View 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] }
}

View File

@@ -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>";
}

View File

@@ -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

View File

@@ -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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -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.

View File

@@ -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);
}
});

View File

@@ -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;

View File

@@ -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.
*/

View File

@@ -1,3 +0,0 @@
{
"configuration_changed": "Die Konfigurationsoptionen für das Newsfeed-Modul haben sich geändert. \nBitte überprüfen Sie die Dokumentation."
}

View File

@@ -1,3 +0,0 @@
{
"configuration_changed": "The configuration options for the newsfeed module have changed.\nPlease check the documentation."
}

View File

@@ -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."
}

View File

@@ -1,3 +0,0 @@
{
"configuration_changed": "Les options de configuration du module newsfeed ont changé. \nVeuillez consulter la documentation."
}

View File

@@ -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) => {

View File

@@ -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).

View File

@@ -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 %}

View File

@@ -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") }}

View 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> -->

View File

@@ -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;

View File

@@ -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;

View 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 "";
}
}
});

View File

@@ -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;
},
/*

View File

@@ -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;
}

View 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;
}
});

View File

@@ -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
*/

View File

@@ -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) {

View File

@@ -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";
}

View File

@@ -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();

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -1,13 +0,0 @@
{
// Escaped
"FOO\"BAR": "Today",
/*
* The following lines
* represent cardinal directions
*/
"N": "N",
"E": "E",
"S": "S",
"W": "W"
}

View 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

View 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;
}

View 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;
}

View 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;
}

View File

@@ -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();

View File

@@ -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);
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);

View 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);
});
});
});

View File

@@ -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 () {

View File

@@ -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"];

View File

@@ -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 () {

View File

@@ -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 () {

View File

@@ -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 () {

View File

@@ -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 &hellip;"
);
});
});
});

View File

@@ -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 () {

View File

@@ -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

View File

@@ -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");
});
});

View File

@@ -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 () {

View File

@@ -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, "../../..");

View File

@@ -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
View File

@@ -0,0 +1,34 @@
{
"LOADING": "Тиенет &hellip;",
"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} коммитпа кая уйрӑлса тӑрать."
}

View File

@@ -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.",

View File

@@ -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.",

View File

@@ -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}.",

View File

@@ -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
View File

@@ -0,0 +1,38 @@
{
"LOADING": "લોડ થઈ રહ્યું છે &hellip;",
"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
View File

@@ -0,0 +1,38 @@
{
"LOADING": "लोड हो रहा है &hellip;",
"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
View File

@@ -0,0 +1,36 @@
{
"LOADING": "Kraunasi &hellip;",
"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"
}

Some files were not shown because too many files have changed in this diff Show More