Compare commits
521 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
500147e130 | ||
|
d90de18d99 | ||
|
ba4f48662f | ||
|
d208437c05 | ||
|
01faa2e1d7 | ||
|
c630c387d6 | ||
|
a774718607 | ||
|
9430c70d0d | ||
|
f3e893fddb | ||
|
11e144ca64 | ||
|
fbceab707e | ||
|
5b2efc43b9 | ||
|
8d85d1aa2d | ||
|
b469fc7577 | ||
|
9db54831c8 | ||
|
9b88bde09a | ||
|
aad03a74c5 | ||
|
55eb6e2e5c | ||
|
ca07355873 | ||
|
11a59e26b2 | ||
|
ec6d9e3521 | ||
|
230accd31e | ||
|
a24a4a747e | ||
|
1e97b5c27a | ||
|
a314ea1aa3 | ||
|
a77128d5f7 | ||
|
1b2673367e | ||
|
ce10e91a60 | ||
|
5244b37d2c | ||
|
1239c6716b | ||
|
67f6258ab0 | ||
|
9f63172b43 | ||
|
bd8bfeb525 | ||
|
4918c4ef4b | ||
|
00d9ea9344 | ||
|
33537cde76 | ||
|
3d5db5c9ca | ||
|
fdf339514d | ||
|
937c4e485a | ||
|
837c060e1f | ||
|
00bacd7dde | ||
|
781031775e | ||
|
97aee3d375 | ||
|
34698751f2 | ||
|
3c31460f2f | ||
|
d5cb60b19c | ||
|
3a7cfe3208 | ||
|
f079cdad64 | ||
|
2822303138 | ||
|
e38de75520 | ||
|
2723604d3e | ||
|
82ee051c1a | ||
|
7bdf49b7e0 | ||
|
32521aba6b | ||
|
819c4cde1c | ||
|
776c486b1a | ||
|
bcd97120a4 | ||
|
312bfb8509 | ||
|
11c9a50931 | ||
|
94c0656bcd | ||
|
13313d0b25 | ||
|
3a20db1d76 | ||
|
00148b4cc8 | ||
|
a31546b1ff | ||
|
361b62b8e2 | ||
|
37327b77a7 | ||
|
7ef8a5bb11 | ||
|
11cfb8af32 | ||
|
7315f7d283 | ||
|
7d58eb718e | ||
|
651be76776 | ||
|
4a5c6f1d39 | ||
|
5745d71d6a | ||
|
db62b7421a | ||
|
4084c57789 | ||
|
f90bec985a | ||
|
90f911c529 | ||
|
6c88b106db | ||
|
cef69d1b97 | ||
|
f76a7fb331 | ||
|
3e7b8b0663 | ||
|
a6eb3ad037 | ||
|
9468749384 | ||
|
37417fa1bb | ||
|
217146351e | ||
|
818ec33cef | ||
|
cd1671830a | ||
|
a5fca87dd0 | ||
|
f06ce55626 | ||
|
853085e755 | ||
|
2b7accaf68 | ||
|
5533d93172 | ||
|
f0e8c865fe | ||
|
36400c0a83 | ||
|
c5383557b5 | ||
|
b645007884 | ||
|
63ac137206 | ||
|
808cbf8e0b | ||
|
8ed77ba0c7 | ||
|
66c74c51e4 | ||
|
7a272ef0ab | ||
|
60b817ec8e | ||
|
a41ecaf7cc | ||
|
d41afa0e53 | ||
|
a6284e05e5 | ||
|
e56f61441d | ||
|
7b4b7dffa2 | ||
|
77a214ef9c | ||
|
cf2723aafb | ||
|
499e99cfc5 | ||
|
a7b83e9fe3 | ||
|
964504b9c3 | ||
|
ec65e66c58 | ||
|
e694b080be | ||
|
70894b3938 | ||
|
fb7115fc13 | ||
|
a619fc4fef | ||
|
7c6c5fd06f | ||
|
2970568eab | ||
|
62cb3a610e | ||
|
f600c163ca | ||
|
77cb68e5ac | ||
|
c6314576aa | ||
|
515c183070 | ||
|
63b9c0e6b8 | ||
|
84893b1664 | ||
|
835668d96d | ||
|
2bce15dc6e | ||
|
8f1a212b52 | ||
|
5c08bde0fa | ||
|
98a84c031e | ||
|
ea1715384e | ||
|
d9a4ee4f65 | ||
|
62017c4661 | ||
|
702b98f510 | ||
|
69aafd7d6a | ||
|
c1559dd8c8 | ||
|
4df1895560 | ||
|
cac92da6e4 | ||
|
5d39d85215 | ||
|
99b4c43fd5 | ||
|
b2f59d6813 | ||
|
c7d79bb893 | ||
|
aa80c468c4 | ||
|
6008cba2db | ||
|
caf56671dc | ||
|
44eccf5ee4 | ||
|
8f96e4847c | ||
|
ae3e307f33 | ||
|
3fe0c758ed | ||
|
7240fb32d2 | ||
|
e23a3461ba | ||
|
727eb0cfd7 | ||
|
3796076360 | ||
|
ef9576f8c4 | ||
|
94fc4cb8a2 | ||
|
ccb248db91 | ||
|
e7de447725 | ||
|
7ff5429cb7 | ||
|
17c581b4aa | ||
|
41e5c2939f | ||
|
7e2ab51298 | ||
|
03f917fd9c | ||
|
d24e10a728 | ||
|
a17ac1c16e | ||
|
dd6b972be4 | ||
|
7d0c9ba0d9 | ||
|
6fa211634f | ||
|
1ca24c7f38 | ||
|
bcfbccae59 | ||
|
20b75ce6ed | ||
|
2ea15d7bf5 | ||
|
18e14c597f | ||
|
06d75999d7 | ||
|
7047a7cae6 | ||
|
20d2124867 | ||
|
3c7a85361e | ||
|
1ffbbdac99 | ||
|
eeccca8842 | ||
|
e2d2dbd2ba | ||
|
c61f0409fb | ||
|
806be39a6d | ||
|
6170b0d059 | ||
|
bca838495e | ||
|
e31a747250 | ||
|
4cf430e146 | ||
|
ef554cf6ec | ||
|
fcd91daee6 | ||
|
396c78b46a | ||
|
4677a3fd89 | ||
|
834ab5c6b9 | ||
|
1599e8f7ff | ||
|
7430704002 | ||
|
4d7b19c8cb | ||
|
40f535cf3c | ||
|
17425dcaf7 | ||
|
6b87fc64af | ||
|
35174b0348 | ||
|
fd53541719 | ||
|
7c68bff9f5 | ||
|
6c64991951 | ||
|
ca04ff0f37 | ||
|
7742575cab | ||
|
cdcdce702d | ||
|
c80e04fe8d | ||
|
c3b3ea107a | ||
|
1dc530c549 | ||
|
8b0b70e757 | ||
|
b508a629e8 | ||
|
abb0dadead | ||
|
e6fb18df56 | ||
|
43ba13f3bc | ||
|
ba705f5563 | ||
|
34e188ec1f | ||
|
b0d97dd170 | ||
|
7caeae61f5 | ||
|
416ace4c86 | ||
|
979041ee91 | ||
|
f0939b8af5 | ||
|
d7a7002bdd | ||
|
d9601de075 | ||
|
ef570558cf | ||
|
db3e81408f | ||
|
e8771cdea8 | ||
|
057eab2173 | ||
|
e2a7024eeb | ||
|
4650986dfa | ||
|
868b5e4617 | ||
|
f12860c7b1 | ||
|
8f751812a6 | ||
|
07a5092eb3 | ||
|
29c9c92ba6 | ||
|
edfa327158 | ||
|
869a6e66cc | ||
|
36abbfc048 | ||
|
cfc3e6d2f4 | ||
|
e38dbee6a6 | ||
|
68a7c857c0 | ||
|
80eef2ab8c | ||
|
1d652aa746 | ||
|
b07c43aa36 | ||
|
3880c8dc2c | ||
|
c0ab2ac297 | ||
|
de684dcb63 | ||
|
29ef1db86b | ||
|
5dfd8a61be | ||
|
358e2b3ccf | ||
|
4203065a06 | ||
|
bc4e0190a0 | ||
|
dd7004cbc9 | ||
|
bdc5c8f620 | ||
|
02d36e22ee | ||
|
331e8c4aa6 | ||
|
d622277c11 | ||
|
8f781ea4ab | ||
|
ebc1e5bf12 | ||
|
ce9a61622e | ||
|
e4891e699f | ||
|
d8765578c8 | ||
|
3a034ecec8 | ||
|
a3dea45089 | ||
|
de99c8a5e4 | ||
|
d3b8dbeea0 | ||
|
cff2f64155 | ||
|
7b8de35405 | ||
|
02ae0df2cc | ||
|
b386cea69d | ||
|
758ffb75a9 | ||
|
78fbc7f392 | ||
|
9c58472576 | ||
|
f8c4afc228 | ||
|
b169d65619 | ||
|
9bf0d4f804 | ||
|
4443f57f8a | ||
|
ea5d8590d5 | ||
|
5d5feb4c71 | ||
|
feb5351ec3 | ||
|
7630c25ef3 | ||
|
24238094e5 | ||
|
7cc9a03db8 | ||
|
2b2e8508d9 | ||
|
a70716f225 | ||
|
d9fcc46994 | ||
|
2d8acec6f0 | ||
|
cd06d8c63a | ||
|
a06ca55107 | ||
|
9686a9ba77 | ||
|
f7f4043ccd | ||
|
b7b55173a6 | ||
|
954253c7e2 | ||
|
cbe4d2cd7f | ||
|
40101129b5 | ||
|
4bb32c6d09 | ||
|
6e09ceeda6 | ||
|
d6a6a53623 | ||
|
4a97052708 | ||
|
3a4902ad4a | ||
|
77d14bc218 | ||
|
1d2a39a855 | ||
|
98b53b6b3d | ||
|
0148d8beaf | ||
|
5bfd84d3be | ||
|
351eb95feb | ||
|
56788f0933 | ||
|
017a376616 | ||
|
c5888cec66 | ||
|
3d5ad29eac | ||
|
c608636b7a | ||
|
1a97107b2d | ||
|
5ca3fbeaea | ||
|
44896db668 | ||
|
12efb87a23 | ||
|
9181be86ba | ||
|
053b01e036 | ||
|
86041d0968 | ||
|
bd87f63e91 | ||
|
b79b49e8f3 | ||
|
a0dde39d97 | ||
|
2e03868021 | ||
|
29384c2ba3 | ||
|
320743ab8d | ||
|
399e171083 | ||
|
184164b677 | ||
|
a8bd196234 | ||
|
239d425940 | ||
|
baa3c1461c | ||
|
fa8e398e90 | ||
|
6d9675a299 | ||
|
91e8ce62d4 | ||
|
06e641015f | ||
|
1c83059482 | ||
|
90b24d824a | ||
|
62457d0e48 | ||
|
90c96f7479 | ||
|
f87adebe41 | ||
|
8f24cc8d13 | ||
|
992802d196 | ||
|
8546d6730c | ||
|
0092289105 | ||
|
7c3923ad00 | ||
|
ef82039401 | ||
|
b01b9758e0 | ||
|
88b00f689b | ||
|
0a340d5d57 | ||
|
50545a83b8 | ||
|
4a57ff40d8 | ||
|
0238455a5a | ||
|
a53e963001 | ||
|
984608e23f | ||
|
f680c83d2d | ||
|
a79e51c76f | ||
|
2dfb349609 | ||
|
766f21b525 | ||
|
733dfa1467 | ||
|
63aa840b55 | ||
|
8b2d544576 | ||
|
d6046d2422 | ||
|
409939360f | ||
|
1d21f39fbc | ||
|
a477140a4b | ||
|
1bbf2d8ce6 | ||
|
40a65eec51 | ||
|
8431ebf2e8 | ||
|
bdcc0c5373 | ||
|
9cbf331533 | ||
|
77640714cc | ||
|
9457d95c3f | ||
|
c2ff949f2d | ||
|
ba8685a122 | ||
|
55464ed0dd | ||
|
fdf3691c87 | ||
|
aa6699cf3e | ||
|
b79b48baac | ||
|
5d22dbd99e | ||
|
4686bb5584 | ||
|
1f62b8f0b6 | ||
|
5759ed3728 | ||
|
827fbfb78f | ||
|
dc363de610 | ||
|
b9d6a235e3 | ||
|
ebc57fe494 | ||
|
e224ec4ae0 | ||
|
a5da347177 | ||
|
a257b15f86 | ||
|
a70cc53d82 | ||
|
1df2de9202 | ||
|
9e394ea349 | ||
|
b55685d610 | ||
|
2156aac046 | ||
|
6914465e3d | ||
|
f3847ec6f3 | ||
|
e1fe8d1d89 | ||
|
675c937a4a | ||
|
c8f53bdf8e | ||
|
3541d5adde | ||
|
b52da7c9fc | ||
|
de57daa3cd | ||
|
e70e011a9c | ||
|
99febb99f1 | ||
|
874d79be36 | ||
|
b33663c9f8 | ||
|
7e69fa39eb | ||
|
de06490539 | ||
|
40a30c24a0 | ||
|
de04c12d3c | ||
|
38fb53b058 | ||
|
ed617c5943 | ||
|
986337da0c | ||
|
8dd7621f29 | ||
|
88d862303d | ||
|
cc274ffebe | ||
|
28a108f79b | ||
|
b9f75bf7d2 | ||
|
39994d5797 | ||
|
8e28be6558 | ||
|
8a65bef004 | ||
|
b94dc5044b | ||
|
b853c00dd4 | ||
|
7a0bc81f48 | ||
|
10bc326490 | ||
|
1920f8158e | ||
|
0ed2ba0183 | ||
|
95adc0aec1 | ||
|
ebee80d10e | ||
|
63836185d9 | ||
|
d0195e0509 | ||
|
5d9bcd9918 | ||
|
db04c26d24 | ||
|
56b399655e | ||
|
24e15c0568 | ||
|
f0c516e82d | ||
|
f38203ef62 | ||
|
a77c026803 | ||
|
5b6306671c | ||
|
25610222bc | ||
|
c17f941fb9 | ||
|
ae6ab1d203 | ||
|
92accf99b4 | ||
|
b02702fe80 | ||
|
5c549ec6e5 | ||
|
af459a5a28 | ||
|
07770601f6 | ||
|
cc96b86b3a | ||
|
1547f4d8b2 | ||
|
75054fcc70 | ||
|
390b3b173b | ||
|
78daa65d28 | ||
|
3bbdd08d24 | ||
|
cec1f12918 | ||
|
d923ae2107 | ||
|
85931155e6 | ||
|
4fd87aca09 | ||
|
600e0ec7e3 | ||
|
51fbff1a4a | ||
|
03b1389ee5 | ||
|
8f014e9d82 | ||
|
cecc6f7561 | ||
|
62ba81c6a6 | ||
|
c5e3422fcd | ||
|
bd5a46b4ab | ||
|
3a972bbbab | ||
|
7768ea28bd | ||
|
75add44e86 | ||
|
7d94365cbf | ||
|
2d830fb8e7 | ||
|
633bf36fe7 | ||
|
c0a5e23d95 | ||
|
d3798344dd | ||
|
ed37460402 | ||
|
9b6ba65cdb | ||
|
42a9631926 | ||
|
676a8a6421 | ||
|
cdbf022ce0 | ||
|
db79e1271e | ||
|
d2b3efacf9 | ||
|
a2ab94f971 | ||
|
a0d92d764b | ||
|
649b78e3f2 | ||
|
e7df1c3e56 | ||
|
53833ae0c3 | ||
|
66b914774a | ||
|
fc89feec4e | ||
|
d311dbd9d5 | ||
|
f97aa67100 | ||
|
9a8add780c | ||
|
3b48f1d042 | ||
|
332b54e7a5 | ||
|
007b2f0c88 | ||
|
3f083862e7 | ||
|
39619d5277 | ||
|
d4fe01f9b9 | ||
|
3c7e507ca1 | ||
|
5eb0b77a8a | ||
|
0fe79b5288 | ||
|
66ceafd010 | ||
|
3341c9e3bf | ||
|
91ddc00f7e | ||
|
07d35a8513 | ||
|
16c887814e | ||
|
22a50b72fd | ||
|
a79e1b6ca1 | ||
|
995296ef53 | ||
|
ab732b5435 | ||
|
241ff5cb6e | ||
|
ec2169e079 | ||
|
d567fd4842 | ||
|
0776dfc80e | ||
|
681a845ef3 | ||
|
ad240cf52f | ||
|
99e3a47dde | ||
|
3fa810b7b8 | ||
|
cd129fb055 | ||
|
837e275bfd | ||
|
7be6031e19 | ||
|
ff9c6bac0a | ||
|
713111254b | ||
|
5b1462a3e8 | ||
|
2bf18d8bda | ||
|
7f2e643e62 | ||
|
ef172592b8 | ||
|
f34407fc43 |
@@ -3,4 +3,3 @@ vendor/*
|
||||
!/modules/default/**
|
||||
!/modules/node_helper
|
||||
!/modules/node_helper/**
|
||||
!/modules/default/defaultmodules.js
|
||||
|
@@ -2,9 +2,11 @@
|
||||
"rules": {
|
||||
"indent": ["error", "tab"],
|
||||
"quotes": ["error", "double"],
|
||||
"semi": ["error"],
|
||||
"max-len": ["error", 250],
|
||||
"curly": "error",
|
||||
"camelcase": ["error", {"properties": "never"}],
|
||||
"no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 1 }],
|
||||
"no-trailing-spaces": ["error", {"ignoreComments": false }],
|
||||
"no-irregular-whitespace": ["error"]
|
||||
},
|
||||
@@ -15,6 +17,7 @@
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2017,
|
||||
"ecmaFeatures": {
|
||||
"globalReturn": true
|
||||
}
|
||||
|
19
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- under investigation
|
||||
- pr welcome
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
6
.gitignore
vendored
@@ -11,7 +11,9 @@ coverage
|
||||
.grunt
|
||||
.lock-wscript
|
||||
build/Release
|
||||
node_modules
|
||||
/node_modules/**/*
|
||||
fonts/node_modules/**/*
|
||||
vendor/node_modules/**/*
|
||||
jspm_modules
|
||||
.npm
|
||||
.node_repl_history
|
||||
@@ -81,3 +83,5 @@ Temporary Items
|
||||
*.orig
|
||||
*.rej
|
||||
*.bak
|
||||
|
||||
!/tests/node_modules/**/*
|
@@ -1,6 +1,7 @@
|
||||
dist: trusty
|
||||
language: node_js
|
||||
node_js:
|
||||
- "7"
|
||||
- "8"
|
||||
before_script:
|
||||
- yarn danger ci
|
||||
- npm install grunt-cli -g
|
||||
|
204
CHANGELOG.md
Normal file → Executable file
@@ -4,9 +4,179 @@ All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
---
|
||||
|
||||
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror² core.
|
||||
|
||||
## [2.9.0] - 2019-10-01
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues running Electron, make sure your [Raspbian is up to date](https://www.raspberrypi.org/documentation/raspbian/updating.md).
|
||||
|
||||
### Added
|
||||
- Spanish translation for "PRECIP".
|
||||
- Adding a Malay (Malaysian) translation for MagicMirror².
|
||||
- Add test check URLs of vendors 200 and 404 HTTP CODE.
|
||||
- Add tests for new weather module and helper to stub ajax requests.
|
||||
|
||||
### Updated
|
||||
- Updatenotification module: Display update notification for a limited (configurable) time.
|
||||
- Enabled e2e/vendor_spec.js tests.
|
||||
- The css/custom.css will be rename after the next release. We've add into `run-start.sh` a instruction by GIT to ignore with `--skip-worktree` and `rm --cached`. [#1540](https://github.com/MichMich/MagicMirror/issues/1540)
|
||||
- Disable sending of notification CLOCK_SECOND when displaySeconds is false.
|
||||
|
||||
### Fixed
|
||||
- Updatenotification module: Properly handle race conditions, prevent crash.
|
||||
- Send `NEWS_FEED` notification also for the first news messages which are shown.
|
||||
- Fixed issue where weather module would not refresh data after a network or API outage. [#1722](https://github.com/MichMich/MagicMirror/issues/1722)
|
||||
- Fixed weatherforecast module not displaying rain amount on fallback endpoint.
|
||||
- Notifications CLOCK_SECOND & CLOCK_MINUTE being from startup instead of matched against the clock and avoid drifting.
|
||||
|
||||
## [2.8.0] - 2019-07-01
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues running Electron, make sure your [Raspbian is up to date](https://www.raspberrypi.org/documentation/raspbian/updating.md).
|
||||
|
||||
### Added
|
||||
- Option to show event location in calendar
|
||||
- Finnish translation for "Feels" and "Weeks"
|
||||
- Russian translation for “Feels”
|
||||
- Calendar module: added `nextDaysRelative` config option
|
||||
- Add `broadcastPastEvents` config option for calendars to include events from the past `maximumNumberOfDays` in event broadcasts
|
||||
- Added feature to broadcast news feed items `NEWS_FEED` and updated news items `NEWS_FEED_UPDATED` in default [newsfeed](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/newsfeed) module (when news is updated) with documented default and `config.js` options in [README.md](https://github.com/MichMich/MagicMirror/blob/develop/modules/default/newsfeed/README.md)
|
||||
- Added notifications to default `clock` module broadcasting `CLOCK_SECOND` and `CLOCK_MINUTE` for the respective time elapsed.
|
||||
- Added UK Met Office Datapoint feed as a provider in the default weather module.
|
||||
- Added new provider class
|
||||
- Added suncalc.js dependency to calculate sun times (not provided in UK Met Office feed)
|
||||
- Added "tempUnits" and "windUnits" to allow, for example, temp in metric (i.e. celsius) and wind in imperial (i.e. mph). These will override "units" if specified, otherwise the "units" value will be used.
|
||||
- Use Feels Like temp from feed if present
|
||||
- Optionally display probability of precipitation (PoP) in current weather (UK Met Office data)
|
||||
- Automatically try to fix eslint errors by passing `--fix` option to it
|
||||
- Added sunrise and sunset times to weathergov weather provider [#1705](https://github.com/MichMich/MagicMirror/issues/1705)
|
||||
- Added "useLocationAsHeader" to display "location" in `config.js` as header when location name is not returned
|
||||
- Added to `newsfeed.js`: in order to design the news article better with css, three more class-names were introduced: newsfeed-desc, newsfeed-desc, newsfeed-desc
|
||||
|
||||
### Updated
|
||||
- English translation for "Feels" to "Feels like"
|
||||
- Fixed the example calender url in `config.js.sample`
|
||||
- Update `ical.js` to solve various calendar issues.
|
||||
- Update weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676)
|
||||
- Only update clock once per minute when seconds aren't shown
|
||||
|
||||
### Fixed
|
||||
- Fixed uncaught exception, race condition on module update
|
||||
- Fixed issue [#1696](https://github.com/MichMich/MagicMirror/issues/1696), some ical files start date to not parse to date type
|
||||
- Allowance HTML5 autoplay-policy (policy is changed from Chrome 66 updates)
|
||||
- Handle SIGTERM messages
|
||||
- Fixes sliceMultiDayEvents so it respects maximumNumberOfDays
|
||||
- Minor types in default NewsFeed [README.md](https://github.com/MichMich/MagicMirror/blob/develop/modules/default/newsfeed/README.md)
|
||||
- Fix typos and small syntax errors, cleanup dependencies, remove multiple-empty-lines, add semi-rule
|
||||
- Fixed issues with calendar not displaying one-time changes to repeating events
|
||||
- Updated the fetchedLocationName variable in currentweather.js so that city shows up in the header
|
||||
|
||||
### Updated installer
|
||||
- give non-pi2+ users (pi0, odroid, jetson nano, mac, windows, ...) option to continue install
|
||||
- use current username vs hardcoded 'pi' to support non-pi install
|
||||
- check for npm installed. node install doesn't do npm anymore
|
||||
- check for mac as part of PM2 install, add install option string
|
||||
- update pm2 config with current username instead of hard coded 'pi'
|
||||
- check for screen saver config, "/etc/xdg/lxsession", bypass if not setup
|
||||
|
||||
## [2.7.1] - 2019-04-02
|
||||
|
||||
Fixed `package.json` version number.
|
||||
|
||||
## [2.7.0] - 2019-04-01
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues running Electron, make sure your [Raspbian is up to date](https://www.raspberrypi.org/documentation/raspbian/updating.md).
|
||||
|
||||
### Added
|
||||
- Italian translation for "Feels"
|
||||
- Basic Klingon (tlhIngan Hol) translations
|
||||
- Disabled the screensaver on raspbian with installation script
|
||||
- Added option to truncate the number of vertical lines a calendar item can span if `wrapEvents` is enabled.
|
||||
- Danish translation for "Feels" and "Weeks"
|
||||
- Added option to split multiple day events in calendar to separate numbered events
|
||||
- Slovakian translation
|
||||
- Alerts now can contain Font Awesome icons
|
||||
- Notifications display time can be set in request
|
||||
- Newsfeed: added support for `ARTICLE_INFO_REQUEST` notification
|
||||
- Add `name` config option for calendars to be sent along with event broadcasts
|
||||
|
||||
### Updated
|
||||
- Bumped the Electron dependency to v3.0.13 to support the most recent Raspbian. [#1500](https://github.com/MichMich/MagicMirror/issues/1500)
|
||||
- Updated modernizr code in alert module, fixed a small typo there too
|
||||
- More verbose error message on console if the config is malformed
|
||||
- Updated installer script to install Node.js version 10.x
|
||||
|
||||
### Fixed
|
||||
- Fixed temperature displays in currentweather and weatherforecast modules [#1503](https://github.com/MichMich/MagicMirror/issues/1503), [#1511](https://github.com/MichMich/MagicMirror/issues/1511).
|
||||
- Fixed unhandled error on bad git data in updatenotification module [#1285](https://github.com/MichMich/MagicMirror/issues/1285).
|
||||
- Weather forecast now works with openweathermap in new weather module. Daily data are displayed, see issue [#1504](https://github.com/MichMich/MagicMirror/issues/1504).
|
||||
- Fixed analogue clock border display issue where non-black backgrounds used (previous fix for issue 611)
|
||||
- Fixed compatibility issues caused when modules request different versions of Font Awesome, see issue [#1522](https://github.com/MichMich/MagicMirror/issues/1522). MagicMirror now uses [Font Awesome 5 with v4 shims included for backwards compatibility](https://fontawesome.com/how-to-use/on-the-web/setup/upgrading-from-version-4#shims).
|
||||
- Installation script problems with raspbian
|
||||
- Calendar: only show repeating count if the event is actually repeating [#1534](https://github.com/MichMich/MagicMirror/pull/1534)
|
||||
- Calendar: Fix exdate handling when multiple values are specified (comma separated)
|
||||
- Calendar: Fix relative date handling for fulldate events, calculate difference always from start of day [#1572](https://github.com/MichMich/MagicMirror/issues/1572)
|
||||
- Fix null dereference in moduleNeedsUpdate when the module isn't visible
|
||||
- Calendar: Fixed event end times by setting default calendarEndTime to "LT" (Local time format). [#1479]
|
||||
- Calendar: Fixed missing calendar fetchers after server process restarts [#1589](https://github.com/MichMich/MagicMirror/issues/1589)
|
||||
- Notification: fixed background color (was white text on white background)
|
||||
- Use getHeader instead of data.header when creating the DOM so overwriting the function also propagates into it
|
||||
- Fix documentation of `useKMPHwind` option in currentweather
|
||||
|
||||
### New weather module
|
||||
- Fixed weather forecast table display [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||
- Dimmed loading indicator for weather forecast.
|
||||
- Implemented config option `decimalSymbol` [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||
- Aligned indoor values in current weather vertical [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||
- Added humidity support to nunjuck unit filter.
|
||||
- Do not display degree symbol for temperature in Kelvin [#1503](https://github.com/MichMich/MagicMirror/issues/1503).
|
||||
- Weather forecast now works with openweathermap for both, `/forecast` and `/forecast/daily`, in new weather module. If you use the `/forecast`-weatherEndpoint, the hourly data are converted to daily data, see issues [#1504](https://github.com/MichMich/MagicMirror/issues/1504), [#1513](https://github.com/MichMich/MagicMirror/issues/1513).
|
||||
- Added fade, fadePoint and maxNumberOfDays properties to the forecast mode [#1516](https://github.com/MichMich/MagicMirror/issues/1516)
|
||||
- Fixed Loading string and decimalSymbol string replace [#1538](https://github.com/MichMich/MagicMirror/issues/1538)
|
||||
- Show Snow amounts in new weather module [#1545](https://github.com/MichMich/MagicMirror/issues/1545)
|
||||
- Added weather.gov as a new weather provider for US locations
|
||||
|
||||
## [2.6.0] - 2019-01-01
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues updating, make sure you are running the latest version of Node.
|
||||
|
||||
### ✨ Experimental ✨
|
||||
- New default [module weather](modules/default/weather). This module will eventually replace the current `currentweather` and `weatherforecast` modules. The new module is still pretty experimental, but it's included so you can give it a try and help us improve this module. Please give us you feedback using [this forum post](https://forum.magicmirror.builders/topic/9335/default-weather-module-refactoring).
|
||||
|
||||
A huge, huge, huge thanks to user @fewieden for all his hard work on the new `weather` module!
|
||||
|
||||
### Added
|
||||
- Possibility to add classes to the cell of symbol, title and time of the events of calendar.
|
||||
- Font-awesome 5, still has 4 for backwards compatibility.
|
||||
- Missing `showEnd` in calendar documentation
|
||||
- Screenshot for the new feed module
|
||||
- Screenshot for the compliments module
|
||||
- Screenshot for the clock module
|
||||
- Screenshot for the current weather
|
||||
- Screenshot for the weather forecast module
|
||||
- Portuguese translation for "Feels"
|
||||
- Croatian translation
|
||||
- Fading for dateheaders timeFormat in Calendar [#1464](https://github.com/MichMich/MagicMirror/issues/1464)
|
||||
- Documentation for the existing `scale` option in the Weather Forecast module.
|
||||
|
||||
### Fixed
|
||||
- Allow to parse recurring calendar events where the start date is before 1900
|
||||
- Fixed Polish translation for Single Update Info
|
||||
- Ignore entries with unparseable details in the calendar module
|
||||
- Bug showing FullDayEvents one day too long in calendar fixed
|
||||
- Bug in newsfeed when `removeStartTags` is used on the description [#1478](https://github.com/MichMich/MagicMirror/issues/1478)
|
||||
|
||||
### Updated
|
||||
- The default calendar setting `showEnd` is changed to `false`.
|
||||
|
||||
### Changed
|
||||
- The Weather Forecast module by default displays the ° symbol after every numeric value to be consistent with the Current Weather module.
|
||||
|
||||
|
||||
## [2.5.0] - 2018-10-01
|
||||
|
||||
### Added
|
||||
- Romanian translation for "Feels"
|
||||
- Support multi-line compliments
|
||||
- Simplified Chinese translation for "Feels"
|
||||
- Polish translate for "Feels"
|
||||
@@ -23,7 +193,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Fixed gzip encoded calendar loading issue #1400.
|
||||
- Mixup between german and spanish translation for newsfeed.
|
||||
- Fixed close dates to be absolute, if no configured in the config.js - module Calendar
|
||||
- Fixed the UpdateNotification module message about new commits in the repository, so they can be correctly localized in singular and plural form.
|
||||
- Fixed the updatenotification module message about new commits in the repository, so they can be correctly localized in singular and plural form.
|
||||
- Fix for weatherforecast rainfall rounding [#1374](https://github.com/MichMich/MagicMirror/issues/1374)
|
||||
- Fix calendar parsing issue for Midori on RasperryPi Zero w, related to issue #694.
|
||||
- Fix weather city ID link in sample config
|
||||
@@ -173,7 +343,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Add `clientonly` script to start only the electron client for a remote server.
|
||||
- Add symbol and color properties of event when `CALENDAR_EVENTS` notification is broadcasted from `default/calendar` module.
|
||||
- Add `.vscode/` folder to `.gitignore` to keep custom Visual Studio Code config out of git.
|
||||
- Add unit test the capitalizeFirstLetter function of newfeed module.
|
||||
- Add unit test the capitalizeFirstLetter function of newsfeed module.
|
||||
- Add new unit tests for function `shorten` in calendar module.
|
||||
- Add new unit tests for function `getLocaleSpecification` in calendar module.
|
||||
- Add unit test for js/class.js.
|
||||
@@ -194,7 +364,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Set version of the `express-ipfilter` on 0.3.1.
|
||||
|
||||
### Fixed
|
||||
- Fixed issue with incorrect allignment of analog clock when displayed in the center column of the MM.
|
||||
- Fixed issue with incorrect alignment of analog clock when displayed in the center column of the MM.
|
||||
- Fixed ipWhitelist behaviour to make empty whitelist ([]) allow any and all hosts access to the MM.
|
||||
- Fixed issue with calendar module where 'excludedEvents' count towards 'maximumEntries'.
|
||||
- Fixed issue with calendar module where global configuration of maximumEntries was not overridden by calendar specific config (see module doc).
|
||||
@@ -218,7 +388,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Add unit test calendar_modules function capFirst.
|
||||
- Add test for check if exists the directories present in defaults modules.
|
||||
- Add support for showing wind direction as an arrow instead of abbreviation in currentWeather module.
|
||||
- Add support for writing translation fucntions to support flexible word order
|
||||
- Add support for writing translation functions to support flexible word order
|
||||
- Add test for check if exits the directories present in defaults modules.
|
||||
- Add calendar option to set a separate date format for full day events.
|
||||
- Add ability for `currentweather` module to display indoor temperature via INDOOR_TEMPERATURE notification
|
||||
@@ -237,7 +407,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Fix double message about port when server is starting
|
||||
- Corrected Swedish translations for TODAY/TOMORROW/DAYAFTERTOMORROW.
|
||||
- Removed unused import from js/electron.js
|
||||
- Made calendar.js respect config.timeFormat irrespecive of locale setting.
|
||||
- Made calendar.js respect config.timeFormat irrespective of locale setting.
|
||||
- Fixed alignment of analog clock when a large calendar is displayed in the same side bar.
|
||||
|
||||
## [2.1.1] - 2017-04-01
|
||||
@@ -255,7 +425,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Added `DAYAFTERTOMORROW`, `UPDATE_NOTIFICATION` and `UPDATE_NOTIFICATION_MODULE` to Finnish translations.
|
||||
- Run `npm test` on Travis automatically.
|
||||
- Show the splash screen image even when is reboot or halted.
|
||||
- Added some missing translaton strings in the sv.json file.
|
||||
- Added some missing translation strings in the sv.json file.
|
||||
- Run task jsonlint to check translation files.
|
||||
- Restructured Test Suite.
|
||||
|
||||
@@ -272,12 +442,12 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Option to use RegExp in Calendar's titleReplace.
|
||||
- Hungarian Translation.
|
||||
- Icelandic Translation.
|
||||
- Add use a script to prevent when is run by SSH session set DISPLAY enviroment.
|
||||
- Enable ability to set configuration file by the enviroment variable called MM_CONFIG_FILE.
|
||||
- Add use a script to prevent when is run by SSH session set DISPLAY environment.
|
||||
- Enable ability to set configuration file by the environment variable called MM_CONFIG_FILE.
|
||||
- Option to give each calendar a different color.
|
||||
- Option for colored min-temp and max-temp.
|
||||
- Add test e2e helloworld.
|
||||
- Add test e2e enviroment.
|
||||
- Add test e2e environment.
|
||||
- Add `chai-as-promised` npm module to devDependencies.
|
||||
- Basic set of tests for clock module.
|
||||
- Run e2e test in Travis.
|
||||
@@ -295,10 +465,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Added tests for Translations, dev argument, version, dev console.
|
||||
- Added test anytime feature compliments module.
|
||||
- Added test ipwhitelist configuration directive.
|
||||
- Added test for calendar module: default, basic-auth, backward compability, fail-basic-auth.
|
||||
- Added test for calendar module: default, basic-auth, backward compatibility, fail-basic-auth.
|
||||
- Added meta tags to support fullscreen mode on iOS (for server mode)
|
||||
- Added `ignoreOldItems` and `ignoreOlderThan` options to the News Feed module
|
||||
- Added test for MM_PORT enviroment variable.
|
||||
- Added test for MM_PORT environment variable.
|
||||
- Added a configurable Week section to the clock module.
|
||||
|
||||
### Fixed
|
||||
@@ -310,7 +480,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Module currentWeather: check if temperature received from api is defined.
|
||||
- Fix an issue with module hidden status changing to `true` although lock string prevented showing it.
|
||||
- Fix newsfeed module bug (removeStartTags)
|
||||
- Fix when is set MM_PORT enviroment variable.
|
||||
- Fix when is set MM_PORT environment variable.
|
||||
- Fixed missing animation on `this.show(speed)` when module is alone in a region.
|
||||
|
||||
## [2.1.0] - 2016-12-31
|
||||
@@ -332,8 +502,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Calendar module now broadcasts the event list to all other modules using the notification system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/calendar) for more information.
|
||||
- Possibility to use the the calendar feed as the source for the weather (currentweather & weatherforecast) location data. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/weatherforecast) for more information.
|
||||
- Added option to show rain amount in the weatherforecast default module
|
||||
- Add module `updatenotification` to get an update whenever a new version is availabe. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/updatenotification) for more information.
|
||||
- Add the abilty to set timezone on the date display in the Clock Module
|
||||
- Add module `updatenotification` to get an update whenever a new version is available. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/updatenotification) for more information.
|
||||
- Add the ability to set timezone on the date display in the Clock Module
|
||||
- Ability to set date format in calendar module
|
||||
- Possibility to use currentweather for the compliments
|
||||
- Added option `disabled` for modules.
|
||||
@@ -372,7 +542,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Added ability to define "the day after tomorrow" for calendar events (Definition for German and Dutch already included).
|
||||
- Added CII Badge (we are compliant with the CII Best Practices)
|
||||
- Add support for doing http basic auth when loading calendars
|
||||
- Add the abilty to turn off and on the date display in the Clock Module
|
||||
- Add the ability to turn off and on the date display in the Clock Module
|
||||
|
||||
### Fixed
|
||||
- Fix typo in installer.
|
||||
@@ -395,8 +565,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
### Fixed
|
||||
- Prevent `getModules()` selectors from returning duplicate entries.
|
||||
- Append endpoints of weather modules with `/` to retreive the correct data. (Issue [#337](https://github.com/MichMich/MagicMirror/issues/337))
|
||||
- Corrected grammer in `module.js` from 'suspend' to 'suspended'.
|
||||
- Append endpoints of weather modules with `/` to retrieve the correct data. (Issue [#337](https://github.com/MichMich/MagicMirror/issues/337))
|
||||
- Corrected grammar in `module.js` from 'suspend' to 'suspended'.
|
||||
- Fixed openweathermap.org URL in config sample.
|
||||
- Prevent currentweather module from crashing when received data object is incorrect.
|
||||
- Fix issue where translation loading prevented the UI start-up when the language was set to 'en'. (Issue [#388](https://github.com/MichMich/MagicMirror/issues/388))
|
||||
|
@@ -4,6 +4,7 @@ module.exports = function(grunt) {
|
||||
pkg: grunt.file.readJSON("package.json"),
|
||||
eslint: {
|
||||
options: {
|
||||
fix: "true",
|
||||
configFile: ".eslintrc.json"
|
||||
},
|
||||
target: [
|
||||
@@ -26,7 +27,7 @@ module.exports = function(grunt) {
|
||||
stylelint: {
|
||||
simple: {
|
||||
options: {
|
||||
configFile: ".stylelintrc"
|
||||
configFile: ".stylelintrc.json"
|
||||
},
|
||||
src: [
|
||||
"css/main.css",
|
||||
@@ -42,11 +43,11 @@ module.exports = function(grunt) {
|
||||
src: [
|
||||
"package.json",
|
||||
".eslintrc.json",
|
||||
".stylelintrc",
|
||||
".stylelintrc.json",
|
||||
"installers/pm2_MagicMirror.json",
|
||||
"translations/*.json",
|
||||
"modules/default/*/translations/*.json",
|
||||
"installers/pm2_MagicMirror.json",
|
||||
"vendor/package.js"
|
||||
"vendor/package.json"
|
||||
],
|
||||
options: {
|
||||
reporter: "jshint"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
The MIT License (MIT)
|
||||
=====================
|
||||
|
||||
Copyright © 2016-2017 Michael Teeuw
|
||||
Copyright © 2016-2019 Michael Teeuw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
|
27
README.md
@@ -5,9 +5,8 @@
|
||||
<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="http://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
||||
<a href="https://travis-ci.org/MichMich/MagicMirror"><img src="https://travis-ci.org/MichMich/MagicMirror.svg" alt="Travis"></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="http://slack.magicmirror.builders"><img src="http://slack.magicmirror.builders:3000/badge.svg" alt="Slack Status"></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](http://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).
|
||||
@@ -16,18 +15,22 @@ MagicMirror² focuses on a modular plugin system and uses [Electron](http://elec
|
||||
|
||||
## Table Of Contents
|
||||
|
||||
- [Table Of Contents](#table-of-contents)
|
||||
- [Installation](#installation)
|
||||
- [Raspberry Pi](#raspberry-pi)
|
||||
- [General](#general)
|
||||
- [Automatic Installation (Raspberry Pi only!)](#automatic-installation-raspberry-pi-only)
|
||||
- [Manual Installation](#manual-installation)
|
||||
- [Server Only](#server-only)
|
||||
- [Client Only](#client-only)
|
||||
- [Docker](#docker)
|
||||
- [Configuration](#configuration)
|
||||
- [Raspberry Specific](#raspberry-specific)
|
||||
- [General](#general)
|
||||
- [Modules](#modules)
|
||||
- [Updating](#updating)
|
||||
- [Known Issues](#known-issues)
|
||||
- [Community](#community)
|
||||
- [Contributing Guidelines](#contributing-guidelines)
|
||||
- [Enjoying MagicMirror? Consider a donation!](#enjoying-magicmirror-consider-a-donation)
|
||||
- [Manifesto](#manifesto)
|
||||
|
||||
## Installation
|
||||
@@ -38,7 +41,7 @@ MagicMirror² focuses on a modular plugin system and uses [Electron](http://elec
|
||||
|
||||
*Electron*, the app wrapper around MagicMirror², only supports the Raspberry Pi 2/3. The Raspberry Pi 0/1 is currently **not** supported. If you want to run this on a Raspberry Pi 1, use the [server only](#server-only) feature and setup a fullscreen browser yourself. (Yes, people have managed to run MM² also on a Pi0, so if you insist, search in the forums.)
|
||||
|
||||
Note that you will need to install the lastest full version of Raspbian, **don't use the Lite version**.
|
||||
Note that you will need to install the latest full version of Raspbian, **don't use the Lite version**.
|
||||
|
||||
Execute the following command on your Raspberry Pi to install MagicMirror²:
|
||||
|
||||
@@ -125,7 +128,7 @@ The following wiki links are helpful for the initial configuration of your Magic
|
||||
**Note:** If you used the installer script. This step is already done for you.
|
||||
|
||||
2. Modify your required settings. \
|
||||
Note: You'll can check your configuration running `npm run config:check` in `/home/pi/MagicMirror`.
|
||||
Note: You can check your configuration running `npm run config:check` in `/home/pi/MagicMirror`.
|
||||
|
||||
|
||||
The following properties can be configured:
|
||||
@@ -140,7 +143,7 @@ The following properties can be configured:
|
||||
| `timeFormat` | The form of time notation that will be used. Possible values are `12` or `24`. The default is `24`. |
|
||||
| `units` | The units that will be used in the default weather modules. Possible values are `metric` or `imperial`. The default is `metric`. |
|
||||
| `modules` | An array of active modules. **The array must contain objects. See the next table below for more information.** |
|
||||
| `electronOptions` | An optional array of Electron (browser) options. This allows configuration of e.g. the browser screen size and position (example: `electronOptions: { fullscreen: false, width: 800, height: 600 }`). Kiosk mode can be enabled by setting `kiosk = true`, `autoHideMenuBar = false` and `fullscreen = false`. More options can be found [here](https://github.com/electron/electron/blob/master/docs/api/browser-window.md). |
|
||||
| `electronOptions` | An optional array of Electron (browser) options. This allows configuration of e.g. the browser screen size and position (example: `electronOptions: { fullscreen: false, width: 800, height: 600 }`). Kiosk mode can be enabled by setting `kiosk: true`, `autoHideMenuBar: false` and `fullscreen: false`. More options can be found [here](https://github.com/electron/electron/blob/master/docs/api/browser-window.md). |
|
||||
| `customCss` | The path of the `custom.css` stylesheet. The default is `css/custom.css`. |
|
||||
|
||||
Module configuration:
|
||||
@@ -199,6 +202,16 @@ Please keep the following in mind:
|
||||
|
||||
Thanks for your help in making MagicMirror² better!
|
||||
|
||||
|
||||
## Enjoying MagicMirror? Consider a donation!
|
||||
|
||||
MagicMirror² is opensource and free. That doesn't mean we don't need any money.
|
||||
|
||||
Please consider a donation to help us cover the ongoing costs like webservers and email services.
|
||||
If we receive enough donations we might even be able to free up some working hours and spend some extra time improving the MagicMirror² core.
|
||||
|
||||
To donate, please follow [this](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G5D8E9MR5DTD2&source=url) link.
|
||||
|
||||
## Manifesto
|
||||
|
||||
A real Manifesto is still to be written. Till then, Michael's response on [one of the repository issues](https://github.com/MichMich/MagicMirror/issues/1174) gives a great summary:
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
// Use seperate scope to prevent global scope pollution
|
||||
// Use separate scope to prevent global scope pollution
|
||||
(function () {
|
||||
var config = {};
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
// Prefer command line arguments over environment variables
|
||||
["address", "port"].forEach((key) => {
|
||||
config[key] = getCommandLineParameter(key, process.env[key.toUpperCase()]);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function getServerConfig(url) {
|
||||
@@ -30,7 +30,7 @@
|
||||
const request = lib.get(url, (response) => {
|
||||
var configData = "";
|
||||
|
||||
// Gather incomming data
|
||||
// Gather incoming data
|
||||
response.on("data", function(chunk) {
|
||||
configData += chunk;
|
||||
});
|
||||
@@ -43,8 +43,8 @@
|
||||
request.on("error", function(error) {
|
||||
reject(new Error(`Unable to read config from server (${url} (${error.message}`));
|
||||
});
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function fail(message, code = 1) {
|
||||
if (message !== undefined && typeof message === "string") {
|
||||
@@ -89,7 +89,7 @@
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code != 0) {
|
||||
if (code !== 0) {
|
||||
console.log(`There something wrong. The clientonly is not running code ${code}`);
|
||||
}
|
||||
});
|
||||
|
@@ -44,9 +44,8 @@ var config = {
|
||||
config: {
|
||||
calendars: [
|
||||
{
|
||||
symbol: "calendar-check-o ",
|
||||
url: "webcal://www.calendarlabs.com/templates/ical/US-Holidays.ics"
|
||||
}
|
||||
symbol: "calendar-check",
|
||||
url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics" }
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -59,7 +58,7 @@ var config = {
|
||||
position: "top_right",
|
||||
config: {
|
||||
location: "New York",
|
||||
locationID: "", //ID from http://bulk.openweathermap.org/sample/; unzip the gz file and find your city
|
||||
locationID: "", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
||||
appid: "YOUR_OPENWEATHER_API_KEY"
|
||||
}
|
||||
},
|
||||
@@ -69,7 +68,7 @@ var config = {
|
||||
header: "Weather Forecast",
|
||||
config: {
|
||||
location: "New York",
|
||||
locationID: "5128581", //ID from http://www.openweathermap.org/help/city_list.txt
|
||||
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"
|
||||
}
|
||||
},
|
||||
@@ -84,7 +83,9 @@ var config = {
|
||||
}
|
||||
],
|
||||
showSourceTitle: true,
|
||||
showPublishDate: true
|
||||
showPublishDate: true,
|
||||
broadcastNewsFeeds: true,
|
||||
broadcastNewsUpdates: true
|
||||
}
|
||||
},
|
||||
]
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { danger, fail, warn } from "danger"
|
||||
import { danger, fail, warn } from "danger";
|
||||
|
||||
// Check if the CHANGELOG.md file has been edited
|
||||
// Fail the build and post a comment reminding submitters to do so if it wasn't changed
|
||||
if (!danger.git.modified_files.includes("CHANGELOG.md")) {
|
||||
warn("Please include an updated `CHANGELOG.md` file.<br>This way we can keep track of all the contributions.")
|
||||
warn("Please include an updated `CHANGELOG.md` file.<br>This way we can keep track of all the contributions.");
|
||||
}
|
||||
|
||||
// Check if the PR request is send to the master branch.
|
||||
@@ -12,6 +12,6 @@ if (danger.github.pr.base.ref === "master" && danger.github.pr.user.login !== "M
|
||||
// Check if the PR body or title includes the text: #accepted.
|
||||
// If not, the PR will fail.
|
||||
if ((danger.github.pr.body + danger.github.pr.title).includes("#accepted")) {
|
||||
fail("Please send all your pull requests to the `develop` branch.<br>Pull requests on the `master` branch will not be accepted.")
|
||||
fail("Please send all your pull requests to the `develop` branch.<br>Pull requests on the `master` branch will not be accepted.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
|
||||
# This is an installer script for MagicMirror2. It works well enough
|
||||
# that it can detect if you have Node installed, run a binary script
|
||||
@@ -20,21 +20,29 @@ echo -e "\e[0m"
|
||||
|
||||
# Define the tested version of Node.js.
|
||||
NODE_TESTED="v5.1.0"
|
||||
NPM_TESTED="V6.0.0"
|
||||
USER=`whoami`
|
||||
PM2_FILE=~/MagicMirror/installers/pm2_MagicMirror.json
|
||||
|
||||
# Determine which Pi is running.
|
||||
ARM=$(uname -m)
|
||||
|
||||
# Check the Raspberry Pi version.
|
||||
if [ "$ARM" != "armv7l" ]; then
|
||||
echo -e "\e[91mSorry, your Raspberry Pi is not supported."
|
||||
echo -e "\e[91mPlease run MagicMirror on a Raspberry Pi 2 or 3."
|
||||
echo -e "\e[91mIf this is a Pi Zero, you are in the same boat as the original Raspberry Pi. You must run in server only mode."
|
||||
read -p "this appears not to be a Raspberry Pi 2 or 3, do you want to continue installtion (y/N)?" choice
|
||||
if [[ $choice =~ ^[Nn]$ ]]; then
|
||||
echo -e "\e[91mSorry, your Raspberry Pi is not supported."
|
||||
echo -e "\e[91mPlease run MagicMirror on a Raspberry Pi 2 or 3."
|
||||
echo -e "\e[91mIf this is a Pi Zero, you are in the same boat as the original Raspberry Pi. You must run in server only mode."
|
||||
exit;
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
# Define helper methods.
|
||||
function version_gt() { test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" != "$1"; }
|
||||
function command_exists () { type "$1" &> /dev/null ;}
|
||||
function verlte() { [ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ];}
|
||||
function verlt() { [ "$1" = "$2" ] && return 1 || verlte $1 $2 ;}
|
||||
|
||||
# Update before first apt-get
|
||||
echo -e "\e[96mUpdating packages ...\e[90m"
|
||||
@@ -52,7 +60,7 @@ if command_exists node; then
|
||||
NODE_CURRENT=$(node -v)
|
||||
echo -e "\e[0mMinimum Node version: \e[1m$NODE_TESTED\e[0m"
|
||||
echo -e "\e[0mInstalled Node version: \e[1m$NODE_CURRENT\e[0m"
|
||||
if version_gt $NODE_TESTED $NODE_CURRENT; then
|
||||
if verlte $NODE_CURRENT $NODE_TESTED; then
|
||||
echo -e "\e[96mNode should be upgraded.\e[0m"
|
||||
NODE_INSTALL=true
|
||||
|
||||
@@ -82,12 +90,50 @@ if $NODE_INSTALL; then
|
||||
# The NODE_STABLE_BRANCH variable will need to be manually adjusted when a new branch is released. (e.g. 7.x)
|
||||
# Only tested (stable) versions are recommended as newer versions could break MagicMirror.
|
||||
|
||||
NODE_STABLE_BRANCH="9.x"
|
||||
curl -sL https://deb.nodesource.com/setup_$NODE_STABLE_BRANCH | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
NODE_STABLE_BRANCH="10.x"
|
||||
curl -sL https://deb.nodesource.com/setup_$NODE_STABLE_BRANCH | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
echo -e "\e[92mNode.js installation Done!\e[0m"
|
||||
fi
|
||||
|
||||
# Check if we need to install or upgrade npm.
|
||||
echo -e "\e[96mCheck current NPM installation ...\e[0m"
|
||||
NPM_INSTALL=false
|
||||
if command_exists npm; then
|
||||
echo -e "\e[0mNPM currently installed. Checking version number.";
|
||||
NPM_CURRENT='V'$(npm -v)
|
||||
echo -e "\e[0mMinimum npm version: \e[1m$NPM_TESTED\e[0m"
|
||||
echo -e "\e[0mInstalled npm version: \e[1m$NPM_CURRENT\e[0m"
|
||||
if verlte $NPM_CURRENT $NPM_TESTED; then
|
||||
echo -e "\e[96mnpm should be upgraded.\e[0m"
|
||||
NPM_INSTALL=true
|
||||
|
||||
# Check if a node process is currently running.
|
||||
# If so abort installation.
|
||||
if pgrep "npm" > /dev/null; then
|
||||
echo -e "\e[91mA npm process is currently running. Can't upgrade."
|
||||
echo "Please quit all npm processes and restart the installer."
|
||||
exit;
|
||||
fi
|
||||
|
||||
else
|
||||
echo -e "\e[92mNo npm upgrade necessary.\e[0m"
|
||||
fi
|
||||
|
||||
else
|
||||
echo -e "\e[93mnpm is not installed.\e[0m";
|
||||
NPM_INSTALL=true
|
||||
fi
|
||||
|
||||
# Install or upgrade node if necessary.
|
||||
if $NPM_INSTALL; then
|
||||
|
||||
echo -e "\e[96mInstalling npm ...\e[90m"
|
||||
|
||||
sudo apt-get install -y npm
|
||||
echo -e "\e[92mnpm installation Done!\e[0m"
|
||||
fi
|
||||
|
||||
# Install MagicMirror
|
||||
cd ~
|
||||
if [ -d "$HOME/MagicMirror" ] ; then
|
||||
@@ -151,10 +197,38 @@ fi
|
||||
# Use pm2 control like a service MagicMirror
|
||||
read -p "Do you want use pm2 for auto starting of your MagicMirror (y/N)?" choice
|
||||
if [[ $choice =~ ^[Yy]$ ]]; then
|
||||
sudo npm install -g pm2
|
||||
sudo su -c "env PATH=$PATH:/usr/bin pm2 startup linux -u pi --hp /home/pi"
|
||||
pm2 start ~/MagicMirror/installers/pm2_MagicMirror.json
|
||||
pm2 save
|
||||
#
|
||||
# check if this is a mac
|
||||
#
|
||||
mac=$(uname -s)
|
||||
up=""
|
||||
if [ $mac == 'Darwin' ]; then
|
||||
up="--unsafe-perm"
|
||||
fi
|
||||
sudo npm install $up -g pm2
|
||||
if [[ "$(ps --no-headers -o comm 1)" =~ systemd ]]; then #Checking for systemd
|
||||
pm2 startup systemd -u $USER --hp /home/$USER
|
||||
else
|
||||
sudo su -c "env PATH=$PATH:/usr/bin pm2 startup linux -u $USER --hp /home/$USER"
|
||||
fi
|
||||
if [ "USER" != "pi" ]; then
|
||||
sed 's/pi/'$USER'/g' mm.sh >mm.sh
|
||||
sed 's/pi/'$USER'/g' $PM2_FILE > ~/MagicMirror/installers/pm2_MagicMirror_new.json
|
||||
PM2_FILE=~/MagicMirror/installers/pm2_MagicMirror_new.json
|
||||
fi
|
||||
pm2 start $PM2_FILE
|
||||
pm2 save
|
||||
fi
|
||||
# Disable Screensaver
|
||||
if [ -d "/etc/xdg/lxsession" ]; then
|
||||
read -p "Do you want to disable the screen saver? (y/N)?" choice
|
||||
if [[ $choice =~ ^[Yy]$ ]]; then
|
||||
sudo su -c "echo -e '@xset s noblank\n@xset s off\n@xset -dpms' >> /etc/xdg/lxsession/LXDE-pi/autostart"
|
||||
export DISPLAY=:0; xset s noblank;xset s off;xset -dpms
|
||||
fi
|
||||
else
|
||||
echo " "
|
||||
echo -e "unable to disable screen saver, /etc/xdg/lxsession does not exist"
|
||||
fi
|
||||
|
||||
echo " "
|
||||
|
21
js/app.js
@@ -48,7 +48,6 @@ var App = function() {
|
||||
*
|
||||
* argument callback function - The callback function.
|
||||
*/
|
||||
|
||||
var loadConfig = function(callback) {
|
||||
console.log("Loading config ...");
|
||||
var defaults = require(__dirname + "/defaults.js");
|
||||
@@ -67,10 +66,10 @@ var App = function() {
|
||||
var config = Object.assign(defaults, c);
|
||||
callback(config);
|
||||
} catch (e) {
|
||||
if (e.code == "ENOENT") {
|
||||
if (e.code === "ENOENT") {
|
||||
console.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
|
||||
} else if (e instanceof ReferenceError || e instanceof SyntaxError) {
|
||||
console.error(Utils.colors.error("WARNING! Could not validate config file. Please correct syntax errors. Starting with default configuration."));
|
||||
console.error(Utils.colors.error("WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: " + e.stack));
|
||||
} else {
|
||||
console.error(Utils.colors.error("WARNING! Could not load config file. Starting with default configuration. Error found: " + e));
|
||||
}
|
||||
@@ -96,7 +95,7 @@ var App = function() {
|
||||
". Check README and CHANGELOG for more up-to-date ways of getting the same functionality.")
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* loadModule(module)
|
||||
* Loads a specific module.
|
||||
@@ -173,7 +172,7 @@ var App = function() {
|
||||
};
|
||||
|
||||
/* cmpVersions(a,b)
|
||||
* Compare two symantic version numbers and return the difference.
|
||||
* Compare two semantic version numbers and return the difference.
|
||||
*
|
||||
* argument a string - Version number a.
|
||||
* argument a string - Version number b.
|
||||
@@ -197,7 +196,7 @@ var App = function() {
|
||||
/* start(callback)
|
||||
* This methods starts the core app.
|
||||
* It loads the config, then it loads all modules.
|
||||
* When it"s done it executs the callback with the config as argument.
|
||||
* When it's done it executes the callback with the config as argument.
|
||||
*
|
||||
* argument callback function - The callback function.
|
||||
*/
|
||||
@@ -231,7 +230,6 @@ var App = function() {
|
||||
if (typeof callback === "function") {
|
||||
callback(config);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -263,6 +261,15 @@ var App = function() {
|
||||
this.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
/* We also need to listen to SIGTERM signals so we stop everything when we are asked to stop by the OS.
|
||||
*/
|
||||
process.on("SIGTERM", () => {
|
||||
console.log("[SIGTERM] Received. Shutting down server...");
|
||||
setTimeout(() => { process.exit(0); }, 3000); // Force quit after 3 seconds
|
||||
this.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = new App();
|
||||
|
@@ -21,7 +21,7 @@
|
||||
var prototype = new this();
|
||||
initializing = false;
|
||||
|
||||
// Make a copy of all prototype properies, to prevent reference issues.
|
||||
// Make a copy of all prototype properties, to prevent reference issues.
|
||||
for (var name in prototype) {
|
||||
prototype[name] = cloneObject(prototype[name]);
|
||||
}
|
||||
@@ -29,8 +29,8 @@
|
||||
// Copy the properties over onto the new prototype
|
||||
for (var name in prop) {
|
||||
// Check if we're overwriting an existing function
|
||||
prototype[name] = typeof prop[name] == "function" &&
|
||||
typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function (name, fn) {
|
||||
prototype[name] = typeof prop[name] === "function" &&
|
||||
typeof _super[name] === "function" && fnTest.test(prop[name]) ? (function (name, fn) {
|
||||
return function () {
|
||||
var tmp = this._super;
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
var ret = fn.apply(this, arguments);
|
||||
this._super = tmp;
|
||||
|
||||
|
||||
return ret;
|
||||
};
|
||||
})(name, prop[name]) : prop[name];
|
||||
|
@@ -17,6 +17,7 @@ const BrowserWindow = electron.BrowserWindow;
|
||||
let mainWindow;
|
||||
|
||||
function createWindow() {
|
||||
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
|
||||
var electronOptionsDefaults = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
|
@@ -8,7 +8,7 @@
|
||||
|
||||
var Loader = (function() {
|
||||
|
||||
/* Create helper valiables */
|
||||
/* Create helper variables */
|
||||
|
||||
var loadedModuleFiles = [];
|
||||
var loadedFiles = [];
|
||||
@@ -55,7 +55,7 @@ var Loader = (function() {
|
||||
module.start();
|
||||
}
|
||||
|
||||
// Notifiy core of loded modules.
|
||||
// Notify core of loaded modules.
|
||||
MM.modulesStarted(moduleObjects);
|
||||
};
|
||||
|
||||
@@ -104,7 +104,6 @@ var Loader = (function() {
|
||||
config: moduleData.config,
|
||||
classes: (typeof moduleData.classes !== "undefined") ? moduleData.classes + " " + module : module
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return moduleFiles;
|
||||
@@ -138,7 +137,6 @@ var Loader = (function() {
|
||||
afterLoad();
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/* bootstrapModule(module, mObj)
|
||||
@@ -164,7 +162,6 @@ var Loader = (function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/* loadFile(fileName)
|
||||
@@ -210,7 +207,6 @@ var Loader = (function() {
|
||||
document.getElementsByTagName("head")[0].appendChild(stylesheet);
|
||||
break;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/* Public Methods */
|
||||
@@ -261,5 +257,4 @@ var Loader = (function() {
|
||||
loadFile(module.file(fileName), callback);
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
|
20
js/main.js
@@ -39,9 +39,9 @@ var MM = (function() {
|
||||
dom.opacity = 0;
|
||||
wrapper.appendChild(dom);
|
||||
|
||||
if (typeof module.data.header !== "undefined" && module.data.header !== "") {
|
||||
if (typeof module.getHeader() !== "undefined" && module.getHeader() !== "") {
|
||||
var moduleHeader = document.createElement("header");
|
||||
moduleHeader.innerHTML = module.data.header;
|
||||
moduleHeader.innerHTML = module.getHeader();
|
||||
moduleHeader.className = "module-header";
|
||||
dom.appendChild(moduleHeader);
|
||||
}
|
||||
@@ -173,6 +173,10 @@ var MM = (function() {
|
||||
*/
|
||||
var moduleNeedsUpdate = function(module, newHeader, newContent) {
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||
|
||||
@@ -199,6 +203,7 @@ var MM = (function() {
|
||||
*/
|
||||
var updateModuleContent = function(module, newHeader, newContent) {
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper === null) {return;}
|
||||
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||
|
||||
@@ -287,7 +292,7 @@ var MM = (function() {
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper !== null) {
|
||||
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
|
||||
// Restore the postition. See hideModule() for more info.
|
||||
// Restore the position. See hideModule() for more info.
|
||||
moduleWrapper.style.position = "static";
|
||||
|
||||
updateWrapperStates();
|
||||
@@ -307,7 +312,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 schould be called by the show and hide methods.
|
||||
* This method should be called by the show and hide methods.
|
||||
*
|
||||
* Example:
|
||||
* If the top_bar only contains the update notification. And no update is available,
|
||||
@@ -315,7 +320,6 @@ var MM = (function() {
|
||||
* an ugly top margin. By using this function, the top bar will be hidden if the
|
||||
* update notification is not visible.
|
||||
*/
|
||||
|
||||
var updateWrapperStates = 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"];
|
||||
|
||||
@@ -325,7 +329,7 @@ var MM = (function() {
|
||||
|
||||
var showWrapper = false;
|
||||
Array.prototype.forEach.call(moduleWrappers, function(moduleWrapper) {
|
||||
if (moduleWrapper.style.position == "" || moduleWrapper.style.position == "static") {
|
||||
if (moduleWrapper.style.position === "" || moduleWrapper.style.position === "static") {
|
||||
showWrapper = true;
|
||||
}
|
||||
});
|
||||
@@ -474,7 +478,7 @@ var MM = (function() {
|
||||
/* sendNotification(notification, payload, sender)
|
||||
* Send a notification to all modules.
|
||||
*
|
||||
* argument notification string - The identifier of the noitication.
|
||||
* 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.
|
||||
*/
|
||||
@@ -554,7 +558,7 @@ var MM = (function() {
|
||||
})();
|
||||
|
||||
// Add polyfill for Object.assign.
|
||||
if (typeof Object.assign != "function") {
|
||||
if (typeof Object.assign !== "function") {
|
||||
(function() {
|
||||
Object.assign = function(target) {
|
||||
"use strict";
|
||||
|
21
js/module.js
@@ -76,7 +76,7 @@ var Module = Class.extend({
|
||||
/* getDom()
|
||||
* This method 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 getTemplete method could be subclassed.
|
||||
* Alternatively, the getTemplate method could be subclassed.
|
||||
*
|
||||
* return DomObject | Promise - The dom or a promise with the dom to display.
|
||||
*/
|
||||
@@ -92,7 +92,7 @@ var Module = Class.extend({
|
||||
// the template is a filename
|
||||
self.nunjucksEnvironment().render(template, templateData, function (err, res) {
|
||||
if (err) {
|
||||
Log.error(err)
|
||||
Log.error(err);
|
||||
}
|
||||
|
||||
div.innerHTML = res;
|
||||
@@ -121,7 +121,7 @@ var Module = Class.extend({
|
||||
|
||||
/* getTemplate()
|
||||
* This method 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 tempate.
|
||||
* 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.
|
||||
*
|
||||
@@ -138,7 +138,7 @@ var Module = Class.extend({
|
||||
* return Object
|
||||
*/
|
||||
getTemplateData: function () {
|
||||
return {}
|
||||
return {};
|
||||
},
|
||||
|
||||
/* notificationReceived(notification, payload, sender)
|
||||
@@ -164,7 +164,7 @@ var Module = Class.extend({
|
||||
* @returns Nunjucks Environment
|
||||
*/
|
||||
nunjucksEnvironment: function() {
|
||||
if (this._nunjucksEnvironment != null) {
|
||||
if (this._nunjucksEnvironment !== null) {
|
||||
return this._nunjucksEnvironment;
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ var Module = Class.extend({
|
||||
lstripBlocks: true
|
||||
});
|
||||
this._nunjucksEnvironment.addFilter("translate", function(str) {
|
||||
return self.translate(str)
|
||||
return self.translate(str);
|
||||
});
|
||||
|
||||
return this._nunjucksEnvironment;
|
||||
@@ -212,7 +212,7 @@ var Module = Class.extend({
|
||||
/* setData(data)
|
||||
* Set the module data.
|
||||
*
|
||||
* argument data obejct - Module data.
|
||||
* argument data object - Module data.
|
||||
*/
|
||||
setData: function (data) {
|
||||
this.data = data;
|
||||
@@ -226,14 +226,14 @@ var Module = Class.extend({
|
||||
/* setConfig(config)
|
||||
* Set the module config and combine it with the module defaults.
|
||||
*
|
||||
* argument config obejct - Module config.
|
||||
* argument config object - Module config.
|
||||
*/
|
||||
setConfig: function (config) {
|
||||
this.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.
|
||||
*/
|
||||
socket: function () {
|
||||
@@ -438,11 +438,10 @@ Module.create = function (name) {
|
||||
var ModuleClass = Module.extend(clonedDefinition);
|
||||
|
||||
return new ModuleClass();
|
||||
|
||||
};
|
||||
|
||||
/* cmpVersions(a,b)
|
||||
* Compare two symantic version numbers and return the difference.
|
||||
* Compare two semantic version numbers and return the difference.
|
||||
*
|
||||
* argument a string - Version number a.
|
||||
* argument a string - Version number b.
|
||||
|
@@ -26,8 +26,8 @@ var Server = function(config, callback) {
|
||||
|
||||
server.listen(port, config.address ? config.address : null);
|
||||
|
||||
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length == 0) {
|
||||
console.info(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"))
|
||||
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
||||
console.info(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
|
||||
}
|
||||
|
||||
app.use(function(req, res, next) {
|
||||
|
@@ -18,7 +18,7 @@ var Translator = (function() {
|
||||
xhr.overrideMimeType("application/json");
|
||||
xhr.open("GET", file, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState == 4 && xhr.status == "200") {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
callback(JSON.parse(stripComments(xhr.responseText)));
|
||||
}
|
||||
};
|
||||
@@ -159,6 +159,7 @@ var Translator = (function() {
|
||||
|
||||
return key;
|
||||
},
|
||||
|
||||
/* load(module, file, isFallback, callback)
|
||||
* Load a translation file (json) and remember the data.
|
||||
*
|
||||
|
@@ -33,8 +33,8 @@ Therefore **we highly recommend you to include the following information in your
|
||||
|
||||
- A high quality screenshot of your working module
|
||||
- A short, one sentence, clear description what it does (duh!)
|
||||
- What external API's it depend on, including web links to those
|
||||
- Wheteher the API/request require a key and the user limitations of those. (Is it free?)
|
||||
- What external API's it depends upon, including web links to those
|
||||
- Whether the API/request require a key and the user limitations of those. (Is it free?)
|
||||
|
||||
Surely this also help you get better recognition and feedback for your work.
|
||||
|
||||
@@ -46,8 +46,8 @@ A module can be placed in one single folder. Or multiple modules can be grouped
|
||||
|
||||
### Files
|
||||
- **modulename/modulename.js** - This is your core module script.
|
||||
- **modulename/node_helper.js** - This is an optional helper that will be loaded by the node script. The node helper and module script can communicate with each other using an intergrated socket system.
|
||||
- **modulename/public** - Any files in this folder can be accesed via the browser on `/modulename/filename.ext`.
|
||||
- **modulename/node_helper.js** - This is an optional helper that will be loaded by the node script. The node helper and module script can communicate with each other using an integrated socket system.
|
||||
- **modulename/public** - Any files in this folder can be accessed via the browser on `/modulename/filename.ext`.
|
||||
- **modulename/anyfileorfolder** Any other file or folder in the module folder can be used by the core module script. For example: *modulename/css/modulename.css* would be a good path for your additional module styles.
|
||||
|
||||
## The Core module file: modulename.js
|
||||
@@ -89,7 +89,7 @@ After the module is initialized, the module instance has a few available module
|
||||
| `this.data` | Object | The data object contain additional metadata about the module instance. (See below) |
|
||||
|
||||
|
||||
The `this.data` data object contain the follwoing metadata:
|
||||
The `this.data` data object contain the following metadata:
|
||||
- `data.classes` - The classes which are added to the module dom wrapper.
|
||||
- `data.file` - The filename of the core module file.
|
||||
- `data.path` - The path of the module folder.
|
||||
@@ -98,7 +98,7 @@ The `this.data` data object contain the follwoing metadata:
|
||||
|
||||
|
||||
#### `defaults: {}`
|
||||
Any properties defined in the defaults object, will be merged with the module config as defined in the user's config.js file. This is the best place to set your modules's configuration defaults. Any of the module configuration properties can be accessed using `this.config.propertyName`, but more about that later.
|
||||
Any properties defined in the defaults object, will be merged with the module config as defined in the user's config.js file. This is the best place to set your modules' configuration defaults. Any of the module configuration properties can be accessed using `this.config.propertyName`, but more about that later.
|
||||
|
||||
#### `requiresVersion:`
|
||||
|
||||
@@ -134,7 +134,7 @@ loaded: function(callback) {
|
||||
````
|
||||
|
||||
#### `start()`
|
||||
This method is called when all modules are loaded an the system is ready to boot up. Keep in mind that the dom object for the module is not yet created. The start method is a perfect place to define any additional module properties:
|
||||
This method is called when all modules are loaded and the system is ready to boot up. Keep in mind that the dom object for the module is not yet created. The start method is a perfect place to define any additional module properties:
|
||||
|
||||
**Example:**
|
||||
````javascript
|
||||
@@ -161,7 +161,7 @@ getScripts: function() {
|
||||
}
|
||||
|
||||
````
|
||||
**Note:** If a file can not be loaded, the boot up of the mirror will stall. Therefore it's advised not to use any external urls.
|
||||
**Note:** If a file can not be loaded, the boot up of the mirror will stall. Therefore, it's advised not to use any external urls.
|
||||
|
||||
|
||||
#### `getStyles()`
|
||||
@@ -174,14 +174,14 @@ The getStyles method is called to request any additional stylesheets that need t
|
||||
getStyles: function() {
|
||||
return [
|
||||
'script.css', // will try to load it from the vendor folder, otherwise it will load is from the module folder.
|
||||
'font-awesome.css', // this file is available in the vendor folder, so it doesn't need to be avialable in the module folder.
|
||||
'font-awesome.css', // this file is available in the vendor folder, so it doesn't need to be available in the module folder.
|
||||
this.file('anotherfile.css'), // this file will be loaded straight from the module folder.
|
||||
'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css', // this file will be loaded from the bootstrapcdn servers.
|
||||
]
|
||||
}
|
||||
|
||||
````
|
||||
**Note:** If a file can not be loaded, the boot up of the mirror will stall. Therefore it's advised not to use any external urls.
|
||||
**Note:** If a file can not be loaded, the boot up of the mirror will stall. Therefore, it's advised not to use any external URLs.
|
||||
|
||||
#### `getTranslations()`
|
||||
**Should return: Dictionary**
|
||||
@@ -239,7 +239,7 @@ That MagicMirror core has the ability to send notifications to modules. Or even
|
||||
|
||||
- `notification` - String - The notification identifier.
|
||||
- `payload` - AnyType - The payload of a notification.
|
||||
- `sender` - Module - The sender of the notification. If this argument is `undefined`, the sender of the notififiction is the core system.
|
||||
- `sender` - Module - The sender of the notification. If this argument is `undefined`, the sender of the notification is the core system.
|
||||
|
||||
**Example:**
|
||||
````javascript
|
||||
@@ -267,7 +267,7 @@ When using a node_helper, the node helper can send your module notifications. Wh
|
||||
- `payload` - AnyType - The payload of a notification.
|
||||
|
||||
**Note 1:** When a node helper sends a notification, all modules of that module type receive the same notifications. <br>
|
||||
**Note 2:** The socket connection is established as soon as the module sends its first message using [sendSocketNotification](thissendsocketnotificationnotification-payload).
|
||||
**Note 2:** The socket connection is established as soon as the module sends its first message using [sendSocketNotification](#thissendsocketnotificationnotification-payload).
|
||||
|
||||
**Example:**
|
||||
````javascript
|
||||
@@ -346,7 +346,7 @@ Possible configurable options:
|
||||
- `lockString` - String - When setting lock string, the module can not be shown without passing the correct lockstring. This way (multiple) modules can prevent a module from showing. It's considered best practice to use your modules identifier as the locksString: `this.identifier`. See *visibility locking* below.
|
||||
|
||||
|
||||
**Note 1:** If the hide animation is canceled, for instance because the show method is called before the hide animation was finished, the callback will not be called.<br>
|
||||
**Note 1:** If the hide animation is cancelled, for instance because the show method is called before the hide animation was finished, the callback will not be called.<br>
|
||||
**Note 2:** If the hide animation is hijacked (an other method calls hide on the same module), the callback will not be called.<br>
|
||||
**Note 3:** If the dom is not yet created, the hide method won't work. Wait for the `DOM_OBJECTS_CREATED` [notification](#notificationreceivednotification-payload-sender).
|
||||
|
||||
@@ -371,7 +371,7 @@ Possible configurable options:
|
||||
|
||||
(*Introduced in version: 2.1.0.*)
|
||||
|
||||
Visiblity locking helps the module system to prevent unwanted hide/show actions. The following scenario explains the concept:
|
||||
Visibility locking helps the module system to prevent unwanted hide/show actions. The following scenario explains the concept:
|
||||
|
||||
**Module B asks module A to hide:**
|
||||
````javascript
|
||||
@@ -436,7 +436,7 @@ If no translation is found, a fallback will be used. The fallback sequence is as
|
||||
- 4. Translation as defined in core translation file of the fallback language (the first defined core translation file).
|
||||
- 5. The key (identifier) of the translation.
|
||||
|
||||
When adding translations to your module, it's a good idea to see if an apropriate translation is already available in the [core translation files](https://github.com/MichMich/MagicMirror/tree/master/translations). This way, your module can benefit from the existing translations.
|
||||
When adding translations to your module, it's a good idea to see if an appropriate translation is already available in the [core translation files](https://github.com/MichMich/MagicMirror/tree/master/translations). This way, your module can benefit from the existing translations.
|
||||
|
||||
**Example:**
|
||||
````javascript
|
||||
@@ -490,7 +490,7 @@ this.translate("RUNNING", {
|
||||
)}); // Will return a translated string for the identifier RUNNING, replacing `{timeUntilEnd}` with the contents of the variable `timeUntilEnd` in the order that translator intended. (has a fallback)
|
||||
````
|
||||
|
||||
**Example swedish .json file that does not have the variable in it:**
|
||||
**Example Swedish .json file that does not have the variable in it:**
|
||||
````javascript
|
||||
{
|
||||
"RUNNING": "Slutar",
|
||||
|
@@ -43,10 +43,11 @@ self.sendNotification("SHOW_ALERT", {});
|
||||
```
|
||||
|
||||
### Notification params
|
||||
| Option | Description
|
||||
| --------- | -----------
|
||||
| `title` | The title of the notification. <br><br> **Possible values:** `text` or `html`
|
||||
| `message` | The message of the notification. <br><br> **Possible values:** `text` or `html`
|
||||
| Option | Description
|
||||
| ------------------ | -----------
|
||||
| `title` | The title of the notification. <br><br> **Possible values:** `text` or `html`
|
||||
| `message` | The message of the notification. <br><br> **Possible values:** `text` or `html`
|
||||
| `timer` (optional) | How long the notification should stay visible in ms. <br> If absent, the default `display_time` is used. <br> **Possible values:** `int` `float`
|
||||
|
||||
|
||||
### Alert params
|
||||
@@ -61,4 +62,4 @@ self.sendNotification("SHOW_ALERT", {});
|
||||
|
||||
## Open Source Licenses
|
||||
### [NotificationStyles](https://github.com/codrops/NotificationStyles)
|
||||
See [ympanus.net](http://tympanus.net/codrops/licensing/) for license.
|
||||
See [tympanus.net](http://tympanus.net/codrops/licensing/) for license.
|
||||
|
@@ -24,7 +24,7 @@ Module.register("alert",{
|
||||
return ["classie.js", "modernizr.custom.js", "notificationFx.js"];
|
||||
},
|
||||
getStyles: function() {
|
||||
return ["ns-default.css"];
|
||||
return ["ns-default.css", "font-awesome.css"];
|
||||
},
|
||||
// Define required translations.
|
||||
getTranslations: function() {
|
||||
@@ -35,13 +35,13 @@ Module.register("alert",{
|
||||
};
|
||||
},
|
||||
show_notification: function(message) {
|
||||
if (this.config.effect == "slide") {this.config.effect = this.config.effect + "-" + this.config.position;}
|
||||
if (this.config.effect === "slide") {this.config.effect = this.config.effect + "-" + this.config.position;}
|
||||
msg = "";
|
||||
if (message.title) {
|
||||
msg += "<span class='thin dimmed medium'>" + message.title + "</span>";
|
||||
}
|
||||
if (message.message){
|
||||
if (msg != ""){
|
||||
if (msg !== ""){
|
||||
msg+= "<br />";
|
||||
}
|
||||
msg += "<span class='light bright small'>" + message.message + "</span>";
|
||||
@@ -51,7 +51,7 @@ Module.register("alert",{
|
||||
message: msg,
|
||||
layout: "growl",
|
||||
effect: this.config.effect,
|
||||
ttl: this.config.display_time
|
||||
ttl: message.timer !== undefined ? message.timer : this.config.display_time
|
||||
}).show();
|
||||
},
|
||||
show_alert: function(params, sender) {
|
||||
@@ -132,7 +132,7 @@ Module.register("alert",{
|
||||
if (typeof payload.type === "undefined") { payload.type = "alert"; }
|
||||
if (payload.type === "alert") {
|
||||
this.show_alert(payload, sender);
|
||||
} else if (payload.type = "notification") {
|
||||
} else if (payload.type === "notification") {
|
||||
this.show_notification(payload);
|
||||
}
|
||||
} else if (notification === "HIDE_ALERT") {
|
||||
@@ -152,5 +152,4 @@ Module.register("alert",{
|
||||
}
|
||||
Log.info("Starting module: " + this.name);
|
||||
}
|
||||
|
||||
});
|
||||
|
@@ -1,7 +1,7 @@
|
||||
/* Based on work by http://tympanus.net/codrops/licensing/ */
|
||||
|
||||
.ns-box {
|
||||
background: #fff;
|
||||
background-color: rgba(0, 0, 0, 0.93);
|
||||
padding: 17px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 10px;
|
||||
@@ -12,7 +12,10 @@
|
||||
display: table;
|
||||
word-wrap: break-word;
|
||||
max-width: 100%;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
border-style: solid;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.ns-alert {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
# Module: Calendar
|
||||
The `calendar` module is one of the default modules of the MagicMirror.
|
||||
This module displays events from a public .ical calendar. It can combine multiple calendars.
|
||||
Note that calendars may not contain any entry before 1st January 1970, otherwise the calendar won't be displayed and the module will crash.
|
||||
|
||||
## Using the module
|
||||
|
||||
@@ -31,8 +30,10 @@ The following properties can be configured:
|
||||
| `maximumNumberOfDays` | The maximum number of days in the future. <br><br> **Default value:** `365`
|
||||
| `displaySymbol` | Display a symbol in front of an entry. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `defaultSymbol` | The default symbol. <br><br> **Possible values:** See [Font Awsome](http://fontawesome.io/icons/) website. <br> **Default value:** `calendar`
|
||||
| `showLocation` | Whether to show event locations. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `maxTitleLength` | The maximum title length. <br><br> **Possible values:** `10` - `50` <br> **Default value:** `25`
|
||||
| `wrapEvents` | Wrap event titles to multiple lines. Breaks lines at the length defined by `maxTitleLength`. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `maxTitleLines` | The maximum number of lines a title will wrap vertically before being cut (Only enabled if `wrapEvents` is also enabled). <br><br> **Possible values:** `0` - `10` <br> **Default value:** `3`
|
||||
| `fetchInterval` | How often does the content needs to be fetched? (Milliseconds) <br><br> **Possible values:** `1000` - `86400000` <br> **Default value:** `300000` (5 minutes)
|
||||
| `animationSpeed` | Speed of the update animation. (Milliseconds) <br><br> **Possible values:** `0` - `5000` <br> **Default value:** `2000` (2 seconds)
|
||||
| `fade` | Fade the future events to black. (Gradient) <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
@@ -42,14 +43,20 @@ The following properties can be configured:
|
||||
| `titleReplace` | An object of textual replacements applied to the tile of the event. This allow to remove or replace certains words in the title. <br><br> **Example:** `{'Birthday of ' : '', 'foo':'bar'}` <br> **Default value:** `{ "De verjaardag van ": "", "'s birthday": "" }`
|
||||
| `displayRepeatingCountTitle` | Show count title for yearly repeating events (e.g. "X. Birthday", "X. Anniversary") <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `dateFormat` | Format to use for the date of events (when using absolute dates) <br><br> **Possible values:** See [Moment.js formats](http://momentjs.com/docs/#/parsing/string-format/) <br> **Default value:** `MMM Do` (e.g. Jan 18th)
|
||||
| `dateEndFormat` | Format to use for the end time of events <br><br> **Possible values:** See [Moment.js formats](http://momentjs.com/docs/#/parsing/string-format/) <br> **Default value:** `HH:mm` (e.g. 16:30)
|
||||
| `showEnd` | Show end time of events <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `fullDayEventDateFormat` | Format to use for the date of full day events (when using absolute dates) <br><br> **Possible values:** See [Moment.js formats](http://momentjs.com/docs/#/parsing/string-format/) <br> **Default value:** `MMM Do` (e.g. Jan 18th)
|
||||
| `timeFormat` | Display event times as absolute dates, or relative time, or using absolute date headers with times for each event next to it <br><br> **Possible values:** `absolute` or `relative` or `dateheaders` <br> **Default value:** `relative`
|
||||
| `showEnd` | Display the end of a date as well <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `getRelative` | How much time (in hours) should be left until calendar events start getting relative? <br><br> **Possible values:** `0` (events stay absolute) - `48` (48 hours before the event starts) <br> **Default value:** `6`
|
||||
| `urgency` | When using a timeFormat of `absolute`, the `urgency` setting allows you to display events within a specific time frame as `relative`. This allows events within a certain time frame to be displayed as relative (in xx days) while others are displayed as absolute dates <br><br> **Possible values:** a positive integer representing the number of days for which you want a relative date, for example `7` (for 7 days) <br><br> **Default value:** `7`
|
||||
| `broadcastEvents` | If this property is set to true, the calendar will broadcast all the events to all other modules with the notification message: `CALENDAR_EVENTS`. The event objects are stored in an array and contain the following fields: `title`, `startDate`, `endDate`, `fullDayEvent`, `location` and `geo`. <br><br> **Possible values:** `true`, `false` <br><br> **Default value:** `true`
|
||||
| `hidePrivate` | Hides private calendar events. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `hideOngoing` | Hides calendar events that have already started. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `excludedEvents` | An array of words / phrases from event titles that will be excluded from being shown. <br><br>Additionally advanced filter objects can be passed in. Below is the configuration for the advance filtering object.<br>**Required**<br>`filterBy` - string used to determine if filter is applied.<br>**Optional**<br>`until` - Time before an event to display it Ex: [`'3 days'`, `'2 months'`, `'1 week'`]<br>`caseSensitive` - By default, excludedEvents are case insensitive, set this to true to enforce case sensitivity<br>`regex` - set to `true` if filterBy is a regex. For those not familiar with regex it is used for pattern matching, please see [here](https://regexr.com/) for more info.<br><br> **Example:** `['Birthday', 'Hide This Event', {filterBy: 'Payment', until: '6 days', caseSensitive: true}, {filterBy: '^[0-9]{1,}.*', regex: true}]` <br> **Default value:** `[]`
|
||||
| `broadcastPastEvents` | If this is set to true, events from the past `maximumNumberOfDays` will be included in event broadcasts <br> **Default value:** `false`
|
||||
| `sliceMultiDayEvents` | If this is set to true, events exceeding at least one midnight will be sliced into separate events including a counter like (1/2). This is especially helpful in "dateheaders" mode. Events will be sliced at midnight, end time for all events but the last will be 23:59 <br> **Default value:** `true`
|
||||
| `nextDaysRelative ` | If this is set to true, the appointments of today and tomorrow are displayed relatively, even if the timeformat is set to absolute. <br> **Default value:** `false`
|
||||
|
||||
### Calendar configuration
|
||||
|
||||
@@ -85,7 +92,12 @@ config: {
|
||||
| `repeatingCountTitle` | The count title for yearly repating events in this calendar. <br><br> **Example:** `'Birthday'`
|
||||
| `maximumEntries` | The maximum number of events shown. Overrides global setting. **Possible values:** `0` - `100`
|
||||
| `maximumNumberOfDays` | The maximum number of days in the future. Overrides global setting
|
||||
| `name` | The name of the calendar. Included in event broadcasts as `calendarName`.
|
||||
| `auth` | The object containing options for authentication against the calendar.
|
||||
| `symbolClass` | Add a class to the cell of symbol.
|
||||
| `titleClass` | Add a class to the title's cell.
|
||||
| `timeClass` | Add a class to the time's cell.
|
||||
| `broadcastPastEvents` | Whether to include past events from this calendar. Overrides global setting
|
||||
|
||||
|
||||
#### Calendar authentication options:
|
||||
|
@@ -15,19 +15,21 @@ Module.register("calendar", {
|
||||
maximumNumberOfDays: 365,
|
||||
displaySymbol: true,
|
||||
defaultSymbol: "calendar", // Fontawesome Symbol see http://fontawesome.io/cheatsheet/
|
||||
showLocation: false,
|
||||
displayRepeatingCountTitle: false,
|
||||
defaultRepeatingCountTitle: "",
|
||||
maxTitleLength: 25,
|
||||
wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength
|
||||
maxTitleLines: 3,
|
||||
fetchInterval: 5 * 60 * 1000, // Update every 5 minutes.
|
||||
animationSpeed: 2000,
|
||||
fade: true,
|
||||
urgency: 7,
|
||||
timeFormat: "relative",
|
||||
dateFormat: "MMM Do",
|
||||
dateEndFormat: "HH:mm",
|
||||
dateEndFormat: "LT",
|
||||
fullDayEventDateFormat: "MMM Do",
|
||||
showEnd: true,
|
||||
showEnd: false,
|
||||
getRelative: 6,
|
||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
||||
hidePrivate: false,
|
||||
@@ -46,7 +48,10 @@ Module.register("calendar", {
|
||||
"'s birthday": ""
|
||||
},
|
||||
broadcastEvents: true,
|
||||
excludedEvents: []
|
||||
excludedEvents: [],
|
||||
sliceMultiDayEvents: false,
|
||||
broadcastPastEvents: false,
|
||||
nextDaysRelative: false
|
||||
},
|
||||
|
||||
// Define required scripts.
|
||||
@@ -80,8 +85,18 @@ Module.register("calendar", {
|
||||
|
||||
var calendarConfig = {
|
||||
maximumEntries: calendar.maximumEntries,
|
||||
maximumNumberOfDays: calendar.maximumNumberOfDays
|
||||
maximumNumberOfDays: calendar.maximumNumberOfDays,
|
||||
broadcastPastEvents: calendar.broadcastPastEvents,
|
||||
};
|
||||
if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
|
||||
calendarConfig.symbolClass = "";
|
||||
}
|
||||
if (calendar.titleClass === "undefined" || calendar.titleClass === null) {
|
||||
calendarConfig.titleClass = "";
|
||||
}
|
||||
if (calendar.timeClass === "undefined" || calendar.timeClass === null) {
|
||||
calendarConfig.timeClass = "";
|
||||
}
|
||||
|
||||
// we check user and password here for backwards compatibility with old configs
|
||||
if(calendar.user && calendar.pass) {
|
||||
@@ -90,10 +105,17 @@ Module.register("calendar", {
|
||||
calendar.auth = {
|
||||
user: calendar.user,
|
||||
pass: calendar.pass
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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 = {};
|
||||
@@ -113,6 +135,7 @@ Module.register("calendar", {
|
||||
}
|
||||
} else if (notification === "FETCH_ERROR") {
|
||||
Log.error("Calendar Error. Could not fetch calendar: " + payload.url);
|
||||
this.loaded = true;
|
||||
} else if (notification === "INCORRECT_URL") {
|
||||
Log.error("Calendar Error. Incorrect url: " + payload.url);
|
||||
} else {
|
||||
@@ -135,6 +158,15 @@ Module.register("calendar", {
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
if (this.config.fade && this.config.fadePoint < 1) {
|
||||
if (this.config.fadePoint < 0) {
|
||||
this.config.fadePoint = 0;
|
||||
}
|
||||
var startFade = events.length * this.config.fadePoint;
|
||||
var fadeSteps = events.length - startFade;
|
||||
}
|
||||
|
||||
var currentFadeStep = 0;
|
||||
var lastSeenDate = "";
|
||||
|
||||
for (var e in events) {
|
||||
@@ -143,7 +175,7 @@ Module.register("calendar", {
|
||||
if(this.config.timeFormat === "dateheaders"){
|
||||
if(lastSeenDate !== dateAsString){
|
||||
var dateRow = document.createElement("tr");
|
||||
dateRow.className = "normal"
|
||||
dateRow.className = "normal";
|
||||
var dateCell = document.createElement("td");
|
||||
|
||||
dateCell.colSpan = "3";
|
||||
@@ -151,12 +183,15 @@ Module.register("calendar", {
|
||||
dateRow.appendChild(dateCell);
|
||||
wrapper.appendChild(dateRow);
|
||||
|
||||
if (e >= startFade) { //fading
|
||||
currentFadeStep = e - startFade;
|
||||
dateRow.style.opacity = 1 - (1 / fadeSteps * currentFadeStep);
|
||||
}
|
||||
|
||||
lastSeenDate = dateAsString;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var eventWrapper = document.createElement("tr");
|
||||
|
||||
if (this.config.colored && !this.config.coloredSymbolOnly) {
|
||||
@@ -172,7 +207,9 @@ Module.register("calendar", {
|
||||
symbolWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
|
||||
}
|
||||
|
||||
symbolWrapper.className = "symbol align-right";
|
||||
var symbolClass = this.symbolClassForUrl(event.url);
|
||||
symbolWrapper.className = "symbol align-right " + symbolClass;
|
||||
|
||||
var symbols = this.symbolsForUrl(event.url);
|
||||
if(typeof symbols === "string") {
|
||||
symbols = [symbols];
|
||||
@@ -187,16 +224,16 @@ Module.register("calendar", {
|
||||
symbolWrapper.appendChild(symbol);
|
||||
}
|
||||
eventWrapper.appendChild(symbolWrapper);
|
||||
}else if(this.config.timeFormat === "dateheaders"){
|
||||
} else if(this.config.timeFormat === "dateheaders"){
|
||||
var blankCell = document.createElement("td");
|
||||
blankCell.innerHTML = " "
|
||||
blankCell.innerHTML = " ";
|
||||
eventWrapper.appendChild(blankCell);
|
||||
}
|
||||
|
||||
var titleWrapper = document.createElement("td"),
|
||||
repeatingCountTitle = "";
|
||||
|
||||
if (this.config.displayRepeatingCountTitle) {
|
||||
if (this.config.displayRepeatingCountTitle && event.firstYear !== undefined) {
|
||||
|
||||
repeatingCountTitle = this.countTitleForUrl(event.url);
|
||||
|
||||
@@ -210,10 +247,12 @@ Module.register("calendar", {
|
||||
|
||||
titleWrapper.innerHTML = this.titleTransform(event.title) + repeatingCountTitle;
|
||||
|
||||
var titleClass = this.titleClassForUrl(event.url);
|
||||
|
||||
if (!this.config.colored) {
|
||||
titleWrapper.className = "title bright";
|
||||
titleWrapper.className = "title bright " + titleClass;
|
||||
} else {
|
||||
titleWrapper.className = "title";
|
||||
titleWrapper.className = "title " + titleClass;
|
||||
}
|
||||
|
||||
if(this.config.timeFormat === "dateheaders"){
|
||||
@@ -222,33 +261,20 @@ Module.register("calendar", {
|
||||
titleWrapper.colSpan = "2";
|
||||
titleWrapper.align = "left";
|
||||
|
||||
}else{
|
||||
} else {
|
||||
|
||||
var timeClass = this.timeClassForUrl(event.url);
|
||||
var timeWrapper = document.createElement("td");
|
||||
timeWrapper.className = "time light";
|
||||
timeWrapper.className = "time light " + timeClass;
|
||||
timeWrapper.align = "left";
|
||||
timeWrapper.style.paddingLeft = "2px";
|
||||
var timeFormatString = "";
|
||||
switch (config.timeFormat) {
|
||||
case 12: {
|
||||
timeFormatString = "h:mm A";
|
||||
break;
|
||||
}
|
||||
case 24: {
|
||||
timeFormatString = "HH:mm";
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
timeFormatString = "HH:mm";
|
||||
break;
|
||||
}
|
||||
}
|
||||
timeWrapper.innerHTML = moment(event.startDate, "x").format(timeFormatString);
|
||||
timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
titleWrapper.align = "right";
|
||||
}
|
||||
|
||||
eventWrapper.appendChild(titleWrapper);
|
||||
}else{
|
||||
} else {
|
||||
var timeWrapper = document.createElement("td");
|
||||
|
||||
eventWrapper.appendChild(titleWrapper);
|
||||
@@ -260,6 +286,8 @@ Module.register("calendar", {
|
||||
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) {
|
||||
@@ -281,12 +309,12 @@ Module.register("calendar", {
|
||||
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());
|
||||
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").fromNow());
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").from(moment().format("YYYYMMDD")));
|
||||
}
|
||||
}
|
||||
if(this.config.showEnd){
|
||||
@@ -301,7 +329,7 @@ Module.register("calendar", {
|
||||
// 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") {
|
||||
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'
|
||||
@@ -343,22 +371,41 @@ Module.register("calendar", {
|
||||
}
|
||||
//timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll');
|
||||
//console.log(event);
|
||||
timeWrapper.className = "time light";
|
||||
var timeClass = this.timeClassForUrl(event.url);
|
||||
timeWrapper.className = "time light " + timeClass;
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
}
|
||||
|
||||
wrapper.appendChild(eventWrapper);
|
||||
|
||||
// Create fade effect.
|
||||
if (this.config.fade && this.config.fadePoint < 1) {
|
||||
if (this.config.fadePoint < 0) {
|
||||
this.config.fadePoint = 0;
|
||||
}
|
||||
var startingPoint = events.length * this.config.fadePoint;
|
||||
var steps = events.length - startingPoint;
|
||||
if (e >= startingPoint) {
|
||||
var currentStep = e - startingPoint;
|
||||
eventWrapper.style.opacity = 1 - (1 / steps * currentStep);
|
||||
if (e >= startFade) {
|
||||
currentFadeStep = e - startFade;
|
||||
eventWrapper.style.opacity = 1 - (1 / fadeSteps * currentFadeStep);
|
||||
}
|
||||
|
||||
if (this.config.showLocation) {
|
||||
if (event.location !== false) {
|
||||
var locationRow = document.createElement("tr");
|
||||
locationRow.className = "normal xsmall light";
|
||||
|
||||
if (this.config.displaySymbol) {
|
||||
var symbolCell = document.createElement("td");
|
||||
locationRow.appendChild(symbolCell);
|
||||
}
|
||||
|
||||
var descCell = document.createElement("td");
|
||||
descCell.className = "location";
|
||||
descCell.colSpan = "2";
|
||||
descCell.innerHTML = event.location;
|
||||
locationRow.appendChild(descCell);
|
||||
|
||||
wrapper.appendChild(locationRow);
|
||||
|
||||
if (e >= startFade) {
|
||||
currentFadeStep = e - startFade;
|
||||
locationRow.style.opacity = 1 - (1 / fadeSteps * currentFadeStep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,10 +465,14 @@ Module.register("calendar", {
|
||||
var events = [];
|
||||
var today = moment().startOf("day");
|
||||
var now = new Date();
|
||||
var future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
|
||||
for (var c in this.calendarData) {
|
||||
var calendar = this.calendarData[c];
|
||||
for (var e in calendar) {
|
||||
var event = calendar[e];
|
||||
var event = JSON.parse(JSON.stringify(calendar[e])); // clone object
|
||||
if(event.endDate < now) {
|
||||
continue;
|
||||
}
|
||||
if(this.config.hidePrivate) {
|
||||
if(event.class === "PRIVATE") {
|
||||
// do not add the current event, skip it
|
||||
@@ -438,18 +489,47 @@ Module.register("calendar", {
|
||||
}
|
||||
event.url = c;
|
||||
event.today = event.startDate >= today && event.startDate < (today + 24 * 60 * 60 * 1000);
|
||||
events.push(event);
|
||||
|
||||
/* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
|
||||
* otherwise, esp. in dateheaders mode it is not clear how long these events are.
|
||||
*/
|
||||
var maxCount = Math.ceil(((event.endDate - 1) - moment(event.startDate, "x").endOf("day").format("x"))/(1000*60*60*24)) + 1;
|
||||
if (this.config.sliceMultiDayEvents && maxCount > 1) {
|
||||
var splitEvents = [];
|
||||
var midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
|
||||
var count = 1;
|
||||
while (event.endDate > midnight) {
|
||||
var thisEvent = JSON.parse(JSON.stringify(event)); // clone object
|
||||
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < (today + 24 * 60 * 60 * 1000);
|
||||
thisEvent.endDate = midnight;
|
||||
thisEvent.title += " (" + count + "/" + maxCount + ")";
|
||||
splitEvents.push(thisEvent);
|
||||
|
||||
event.startDate = midnight;
|
||||
count += 1;
|
||||
midnight = moment(midnight, "x").add(1, "day").format("x"); // next day
|
||||
}
|
||||
// Last day
|
||||
event.title += " ("+count+"/"+maxCount+")";
|
||||
splitEvents.push(event);
|
||||
|
||||
for (event of splitEvents) {
|
||||
if ((event.endDate > now) && (event.endDate <= future)) {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
events.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
return events.slice(0, this.config.maximumEntries);
|
||||
},
|
||||
|
||||
|
||||
listContainsEvent: function(eventList, event){
|
||||
for(var evt of eventList){
|
||||
if(evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)){
|
||||
@@ -457,7 +537,6 @@ Module.register("calendar", {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
},
|
||||
|
||||
/* createEventList(url)
|
||||
@@ -472,11 +551,16 @@ Module.register("calendar", {
|
||||
maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries,
|
||||
maximumNumberOfDays: calendarConfig.maximumNumberOfDays || this.config.maximumNumberOfDays,
|
||||
fetchInterval: this.config.fetchInterval,
|
||||
auth: auth
|
||||
symbolClass: calendarConfig.symbolClass,
|
||||
titleClass: calendarConfig.titleClass,
|
||||
timeClass: calendarConfig.timeClass,
|
||||
auth: auth,
|
||||
broadcastPastEvents: calendarConfig.broadcastPastEvents || this.config.broadcastPastEvents,
|
||||
});
|
||||
},
|
||||
|
||||
/* symbolsForUrl(url)
|
||||
/**
|
||||
* symbolsForUrl(url)
|
||||
* Retrieves the symbols for a specific url.
|
||||
*
|
||||
* argument url string - Url to look for.
|
||||
@@ -487,6 +571,53 @@ Module.register("calendar", {
|
||||
return this.getCalendarProperty(url, "symbol", this.config.defaultSymbol);
|
||||
},
|
||||
|
||||
/**
|
||||
* symbolClassForUrl(url)
|
||||
* Retrieves the symbolClass for a specific url.
|
||||
*
|
||||
* @param url string - Url to look for.
|
||||
*
|
||||
* @returns string
|
||||
*/
|
||||
symbolClassForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "symbolClass", "");
|
||||
},
|
||||
|
||||
/**
|
||||
* titleClassForUrl(url)
|
||||
* Retrieves the titleClass for a specific url.
|
||||
*
|
||||
* @param url string - Url to look for.
|
||||
*
|
||||
* @returns string
|
||||
*/
|
||||
titleClassForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "titleClass", "");
|
||||
},
|
||||
|
||||
/**
|
||||
* timeClassForUrl(url)
|
||||
* Retrieves the timeClass for a specific url.
|
||||
*
|
||||
* @param url string - Url to look for.
|
||||
*
|
||||
* @returns string
|
||||
*/
|
||||
timeClassForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "timeClass", "");
|
||||
},
|
||||
|
||||
/* calendarNameForUrl(url)
|
||||
* Retrieves the calendar name for a specific url.
|
||||
*
|
||||
* argument url string - Url to look for.
|
||||
*
|
||||
* return string - The name of the calendar
|
||||
*/
|
||||
calendarNameForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "name", "");
|
||||
},
|
||||
|
||||
/* colorForUrl(url)
|
||||
* Retrieves the color for a specific url.
|
||||
*
|
||||
@@ -535,9 +666,10 @@ Module.register("calendar", {
|
||||
* @param {string} string Text string to shorten
|
||||
* @param {number} maxLength The max length of the string
|
||||
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
|
||||
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
|
||||
* @returns {string} The shortened string
|
||||
*/
|
||||
shorten: function (string, maxLength, wrapEvents) {
|
||||
shorten: function (string, maxLength, wrapEvents, maxTitleLines) {
|
||||
if (typeof string !== "string") {
|
||||
return "";
|
||||
}
|
||||
@@ -546,12 +678,21 @@ Module.register("calendar", {
|
||||
var temp = "";
|
||||
var currentLine = "";
|
||||
var words = string.split(" ");
|
||||
var line = 0;
|
||||
|
||||
for (var i = 0; i < words.length; i++) {
|
||||
var word = words[i];
|
||||
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) { // max - 1 to account for a space
|
||||
currentLine += (word + " ");
|
||||
} else {
|
||||
line++;
|
||||
if (line > maxTitleLines - 1) {
|
||||
if (i < words.length) {
|
||||
currentLine += "…";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentLine.length > 0) {
|
||||
temp += (currentLine + "<br>" + word + " ");
|
||||
} else {
|
||||
@@ -575,7 +716,6 @@ Module.register("calendar", {
|
||||
* Capitalize the first letter of a string
|
||||
* Return capitalized string
|
||||
*/
|
||||
|
||||
capFirst: function (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
},
|
||||
@@ -602,7 +742,7 @@ Module.register("calendar", {
|
||||
title = title.replace(needle, replacement);
|
||||
}
|
||||
|
||||
title = this.shorten(title, this.config.maxTitleLength, this.config.wrapEvents);
|
||||
title = this.shorten(title, this.config.maxTitleLength, this.config.wrapEvents, this.config.maxTitleLines);
|
||||
return title;
|
||||
},
|
||||
|
||||
@@ -617,6 +757,7 @@ Module.register("calendar", {
|
||||
for (var e in calendar) {
|
||||
var event = cloneObject(calendar[e]);
|
||||
event.symbol = this.symbolsForUrl(url);
|
||||
event.calendarName = this.calendarNameForUrl(url);
|
||||
event.color = this.colorForUrl(url);
|
||||
delete event.url;
|
||||
eventList.push(event);
|
||||
|
@@ -8,7 +8,7 @@
|
||||
var ical = require("./vendor/ical.js");
|
||||
var moment = require("moment");
|
||||
|
||||
var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth) {
|
||||
var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents) {
|
||||
var self = this;
|
||||
|
||||
var reloadTimer = null;
|
||||
@@ -37,9 +37,9 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
if(auth.method === "bearer"){
|
||||
opts.auth = {
|
||||
bearer: auth.pass
|
||||
}
|
||||
};
|
||||
|
||||
}else{
|
||||
} else {
|
||||
opts.auth = {
|
||||
user: auth.user,
|
||||
pass: auth.pass
|
||||
@@ -47,7 +47,7 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
|
||||
if(auth.method === "digest"){
|
||||
opts.auth.sendImmediately = false;
|
||||
}else{
|
||||
} else {
|
||||
opts.auth.sendImmediately = true;
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,8 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
// console.log(data);
|
||||
newEvents = [];
|
||||
|
||||
var limitFunction = function(date, i) {return i < maximumEntries;};
|
||||
// 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
|
||||
var limitFunction = function(date, i) {return true;};
|
||||
|
||||
var eventDate = function(event, time) {
|
||||
return (event[time].length === 8) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||
@@ -74,6 +75,11 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
var now = new Date();
|
||||
var today = moment().startOf("day").toDate();
|
||||
var 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.
|
||||
var past = today;
|
||||
|
||||
if (includePastEvents) {
|
||||
past = moment().startOf("day").subtract(maximumNumberOfDays, "days").toDate();
|
||||
}
|
||||
|
||||
// FIXME:
|
||||
// Ugly fix to solve the facebook birthday issue.
|
||||
@@ -102,7 +108,6 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// calculate the duration f the event for use with recurring events.
|
||||
var duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
|
||||
@@ -110,12 +115,7 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
startDate = startDate.startOf("day");
|
||||
}
|
||||
|
||||
var title = "Event";
|
||||
if (event.summary) {
|
||||
title = (typeof event.summary.val !== "undefined") ? event.summary.val : event.summary;
|
||||
} else if(event.description) {
|
||||
title = event.description;
|
||||
}
|
||||
var title = getTitleFromEvent(event);
|
||||
|
||||
var excluded = false,
|
||||
dateFilter = null;
|
||||
@@ -171,21 +171,98 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
var geo = event.geo || false;
|
||||
var description = event.description || false;
|
||||
|
||||
if (typeof event.rrule != "undefined" && !isFacebookBirthday) {
|
||||
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
|
||||
var rule = event.rrule;
|
||||
var dates = rule.between(today, future, true, limitFunction);
|
||||
var addedEvents = 0;
|
||||
|
||||
// can cause problems with e.g. birthdays before 1900
|
||||
if(rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900 ||
|
||||
rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900){
|
||||
rule.origOptions.dtstart.setYear(1900);
|
||||
rule.options.dtstart.setYear(1900);
|
||||
}
|
||||
|
||||
// For recurring events, get the set of start dates that fall within the range
|
||||
// of dates we"re looking for.
|
||||
var dates = rule.between(past, future, true, limitFunction);
|
||||
|
||||
// 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,
|
||||
// we"ll handle this by adding *all* recurrence entries into the set of dates that we check,
|
||||
// because the logic below will filter out any recurrences that don"t actually belong within
|
||||
// our display range.
|
||||
// Would be great if there was a better way to handle this.
|
||||
if (event.recurrences != undefined)
|
||||
{
|
||||
var pastMoment = moment(past);
|
||||
var futureMoment = moment(future);
|
||||
|
||||
for (var r in event.recurrences)
|
||||
{
|
||||
// Only add dates that weren't already in the range we added from the rrule so that
|
||||
// we don"t double-add those events.
|
||||
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) != true)
|
||||
{
|
||||
dates.push(new Date(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through the set of date entries to see which recurrences should be added to our event list.
|
||||
for (var d in dates) {
|
||||
startDate = moment(new Date(dates[d]));
|
||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
||||
var 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 )
|
||||
var dateKey = date.toISOString().substring(0,10);
|
||||
var curEvent = event;
|
||||
var showRecurrence = true;
|
||||
|
||||
if (timeFilterApplies(now, endDate, dateFilter)) {
|
||||
continue;
|
||||
// Stop parsing this event's recurrences if we've already found maximumEntries worth of recurrences.
|
||||
// (The logic below would still filter the extras, but the check is simple since we're already tracking the count)
|
||||
if (addedEvents >= maximumEntries) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (endDate.format("x") > now) {
|
||||
startDate = moment(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.
|
||||
curEvent = curEvent.recurrences[dateKey];
|
||||
startDate = moment(curEvent.start);
|
||||
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
|
||||
}
|
||||
// If there"s no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
|
||||
else if ((curEvent.exdate != undefined) && (curEvent.exdate[dateKey] != undefined))
|
||||
{
|
||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
||||
if (startDate.format("x") == endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
}
|
||||
|
||||
var recurrenceTitle = getTitleFromEvent(curEvent);
|
||||
|
||||
// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
|
||||
// it to the event list.
|
||||
if (endDate.isBefore(past) || startDate.isAfter(future)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (timeFilterApplies(now, endDate, dateFilter)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if ((showRecurrence === true) && (addedEvents < maximumEntries)) {
|
||||
addedEvents++;
|
||||
newEvents.push({
|
||||
title: title,
|
||||
title: recurrenceTitle,
|
||||
startDate: startDate.format("x"),
|
||||
endDate: endDate.format("x"),
|
||||
fullDayEvent: isFullDayEvent(event),
|
||||
@@ -197,19 +274,27 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
});
|
||||
}
|
||||
}
|
||||
// end recurring event parsing
|
||||
} else {
|
||||
// console.log("Single event ...");
|
||||
// Single event.
|
||||
var fullDayEvent = (isFacebookBirthday) ? true : isFullDayEvent(event);
|
||||
|
||||
if (!fullDayEvent && endDate < new Date()) {
|
||||
//console.log("It's not a fullday event, and it is in the past. So skip: " + title);
|
||||
continue;
|
||||
}
|
||||
if (includePastEvents) {
|
||||
if (endDate < past) {
|
||||
//console.log("Past event is too far in the past. So skip: " + title);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (!fullDayEvent && endDate < new Date()) {
|
||||
//console.log("It's not a fullday event, and it is in the past. So skip: " + title);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fullDayEvent && endDate <= today) {
|
||||
//console.log("It's a fullday event, and it is before today. So skip: " + title);
|
||||
continue;
|
||||
if (fullDayEvent && endDate <= today) {
|
||||
//console.log("It's a fullday event, and it is before today. So skip: " + title);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (startDate > future) {
|
||||
@@ -265,7 +350,7 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
/* isFullDayEvent(event)
|
||||
* Checks if an event is a fullday event.
|
||||
*
|
||||
* argument event obejct - The event object to check.
|
||||
* argument event object - The event object to check.
|
||||
*
|
||||
* return bool - The event is a fullday event.
|
||||
*/
|
||||
@@ -307,6 +392,24 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
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.
|
||||
*/
|
||||
var getTitleFromEvent = function (event) {
|
||||
var title = "Event";
|
||||
if (event.summary) {
|
||||
title = (typeof event.summary.val !== "undefined") ? event.summary.val : event.summary;
|
||||
} else if (event.description) {
|
||||
title = event.description;
|
||||
}
|
||||
|
||||
return title;
|
||||
};
|
||||
|
||||
var testTitleByFilter = function (title, filter, useRegex, regexFlags) {
|
||||
if (useRegex) {
|
||||
// Assume if leading slash, there is also trailing slash
|
||||
@@ -321,7 +424,7 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
} else {
|
||||
return title.includes(filter);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* public methods */
|
||||
|
||||
@@ -375,8 +478,6 @@ var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntri
|
||||
this.events = function() {
|
||||
return events;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
|
||||
module.exports = CalendarFetcher;
|
||||
|
@@ -15,6 +15,7 @@ var maximumEntries = 10;
|
||||
var maximumNumberOfDays = 365;
|
||||
var user = "magicmirror";
|
||||
var pass = "MyStrongPass";
|
||||
var broadcastPastEvents = false;
|
||||
|
||||
var auth = {
|
||||
user: user,
|
||||
|
@@ -24,7 +24,7 @@ module.exports = NodeHelper.create({
|
||||
socketNotificationReceived: function(notification, payload) {
|
||||
if (notification === "ADD_CALENDAR") {
|
||||
//console.log('ADD_CALENDAR: ');
|
||||
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth);
|
||||
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@ module.exports = NodeHelper.create({
|
||||
* attribute reloadInterval number - Reload interval in milliseconds.
|
||||
*/
|
||||
|
||||
createFetcher: function(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth) {
|
||||
createFetcher: function(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents) {
|
||||
var self = this;
|
||||
|
||||
if (!validUrl.isUri(url)) {
|
||||
@@ -47,7 +47,7 @@ module.exports = NodeHelper.create({
|
||||
var fetcher;
|
||||
if (typeof self.fetchers[url] === "undefined") {
|
||||
console.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents);
|
||||
|
||||
fetcher.onReceive(function(fetcher) {
|
||||
//console.log('Broadcast events.');
|
||||
@@ -60,6 +60,7 @@ module.exports = NodeHelper.create({
|
||||
});
|
||||
|
||||
fetcher.onError(function(fetcher, error) {
|
||||
console.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error);
|
||||
self.sendSocketNotification("FETCH_ERROR", {
|
||||
url: fetcher.url(),
|
||||
error: error
|
||||
|
@@ -1,6 +1,4 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
- "0.12"
|
||||
- "4.2"
|
||||
- "8.9"
|
||||
install: npm install
|
||||
|
@@ -1,13 +1,16 @@
|
||||
var ical = require('ical')
|
||||
, months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||
'use strict';
|
||||
|
||||
const ical = require('ical');
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function(err, data){
|
||||
for (var k in data){
|
||||
if (data.hasOwnProperty(k)){
|
||||
var ev = data[k]
|
||||
console.log("Conference", ev.summary, 'is in', ev.location, 'on the', ev.start.getDate(), 'of', months[ev.start.getMonth()] );
|
||||
}
|
||||
}
|
||||
})
|
||||
ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function (err, data) {
|
||||
for (let k in data) {
|
||||
if (data.hasOwnProperty(k)) {
|
||||
var ev = data[k];
|
||||
if (data[k].type == 'VEVENT') {
|
||||
console.log(`${ev.summary} is in ${ev.location} on the ${ev.start.getDate()} of ${months[ev.start.getMonth()]} at ${ev.start.toLocaleTimeString('en-GB')}`);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
118
modules/default/calendar/vendor/ical.js/example_rrule.js
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
var ical = require('./node-ical')
|
||||
var moment = require('moment')
|
||||
|
||||
var data = ical.parseFile('./examples/example_rrule.ics');
|
||||
|
||||
// Complicated example demonstrating how to handle recurrence rules and exceptions.
|
||||
|
||||
for (var k in data) {
|
||||
|
||||
// When dealing with calendar recurrences, you need a range of dates to query against,
|
||||
// because otherwise you can get an infinite number of calendar events.
|
||||
var rangeStart = moment("2017-01-01");
|
||||
var rangeEnd = moment("2017-12-31");
|
||||
|
||||
|
||||
var event = data[k]
|
||||
if (event.type === 'VEVENT') {
|
||||
|
||||
var title = event.summary;
|
||||
var startDate = moment(event.start);
|
||||
var endDate = moment(event.end);
|
||||
|
||||
// Calculate the duration of the event for use with recurring events.
|
||||
var duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
|
||||
// Simple case - no recurrences, just print out the calendar event.
|
||||
if (typeof event.rrule === 'undefined')
|
||||
{
|
||||
console.log('title:' + title);
|
||||
console.log('startDate:' + startDate.format('MMMM Do YYYY, h:mm:ss a'));
|
||||
console.log('endDate:' + endDate.format('MMMM Do YYYY, h:mm:ss a'));
|
||||
console.log('duration:' + moment.duration(duration).humanize());
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Complicated case - if an RRULE exists, handle multiple recurrences of the event.
|
||||
else if (typeof event.rrule !== 'undefined')
|
||||
{
|
||||
// For recurring events, get the set of event start dates that fall within the range
|
||||
// of dates we're looking for.
|
||||
var dates = event.rrule.between(
|
||||
rangeStart.toDate(),
|
||||
rangeEnd.toDate(),
|
||||
true,
|
||||
function(date, i) {return true;}
|
||||
)
|
||||
|
||||
// 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. One way to handle this is
|
||||
// to add *all* recurrence override entries into the set of dates that we check, and then later
|
||||
// filter out any recurrences that don't actually belong within our range.
|
||||
if (event.recurrences != undefined)
|
||||
{
|
||||
for (var r in event.recurrences)
|
||||
{
|
||||
// Only add dates that weren't already in the range we added from the rrule so that
|
||||
// we don't double-add those events.
|
||||
if (moment(new Date(r)).isBetween(rangeStart, rangeEnd) != true)
|
||||
{
|
||||
dates.push(new Date(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through the set of date entries to see which recurrences should be printed.
|
||||
for(var i in dates) {
|
||||
|
||||
var date = dates[i];
|
||||
var curEvent = event;
|
||||
var showRecurrence = true;
|
||||
var curDuration = duration;
|
||||
|
||||
startDate = moment(date);
|
||||
|
||||
// Use just the date of the recurrence to look up overrides and exceptions (i.e. chop off time information)
|
||||
var dateLookupKey = date.toISOString().substring(0, 10);
|
||||
|
||||
// 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[dateLookupKey] != undefined))
|
||||
{
|
||||
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||
curEvent = curEvent.recurrences[dateLookupKey];
|
||||
startDate = moment(curEvent.start);
|
||||
curDuration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
|
||||
}
|
||||
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
|
||||
else if ((curEvent.exdate != undefined) && (curEvent.exdate[dateLookupKey] != undefined))
|
||||
{
|
||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
// Set the the title and the end date from either the regular event or the recurrence override.
|
||||
var recurrenceTitle = curEvent.summary;
|
||||
endDate = moment(parseInt(startDate.format("x")) + curDuration, 'x');
|
||||
|
||||
// If this recurrence ends before the start of the date range, or starts after the end of the date range,
|
||||
// don't process it.
|
||||
if (endDate.isBefore(rangeStart) || startDate.isAfter(rangeEnd)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (showRecurrence === true) {
|
||||
|
||||
console.log('title:' + recurrenceTitle);
|
||||
console.log('startDate:' + startDate.format('MMMM Do YYYY, h:mm:ss a'));
|
||||
console.log('endDate:' + endDate.format('MMMM Do YYYY, h:mm:ss a'));
|
||||
console.log('duration:' + moment.duration(curDuration).humanize());
|
||||
console.log();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
40
modules/default/calendar/vendor/ical.js/examples/example_rrule.ics
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-WR-CALNAME:ical
|
||||
X-WR-TIMEZONE:US/Central
|
||||
X-WR-CALDESC:
|
||||
BEGIN:VEVENT
|
||||
UID:98765432-ABCD-DCBB-999A-987765432123
|
||||
DTSTART;TZID=US/Central:20170601T090000
|
||||
DTEND;TZID=US/Central:20170601T170000
|
||||
DTSTAMP:20170727T044436Z
|
||||
EXDATE;TZID=US/Central:20170706T090000,20170713T090000,20170720T090000,20
|
||||
170803T090000
|
||||
LAST-MODIFIED:20170727T044435Z
|
||||
RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20170814T045959Z;BYDAY=TH
|
||||
SEQUENCE:0
|
||||
SUMMARY:Recurring weekly meeting from June 1 - Aug 14 (except July 6, July 13, July 20, Aug 3)
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:98765432-ABCD-DCBB-999A-987765432123
|
||||
RECURRENCE-ID;TZID=US/Central:20170629T090000
|
||||
DTSTART;TZID=US/Central:20170703T090000
|
||||
DTEND;TZID=US/Central:20170703T120000
|
||||
DTSTAMP:20170727T044436Z
|
||||
LAST-MODIFIED:20170216T143445Z
|
||||
SEQUENCE:0
|
||||
SUMMARY:Last meeting in June moved to Monday July 3 and shortened to half day
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:12354454-ABCD-DCBB-999A-2349872354897
|
||||
DTSTART;TZID=US/Central:20171201T130000
|
||||
DTEND;TZID=US/Central:20171201T150000
|
||||
DTSTAMP:20170727T044436Z
|
||||
LAST-MODIFIED:20170727T044435Z
|
||||
SEQUENCE:0
|
||||
SUMMARY:Single event on Dec 1
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
233
modules/default/calendar/vendor/ical.js/ical.js
vendored
@@ -33,9 +33,9 @@
|
||||
for (var i = 0; i<p.length; i++){
|
||||
if (p[i].indexOf('=') > -1){
|
||||
var segs = p[i].split('=');
|
||||
|
||||
|
||||
out[segs[0]] = parseValue(segs.slice(1).join('='));
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
return out || sp
|
||||
@@ -44,7 +44,7 @@
|
||||
var parseValue = function(val){
|
||||
if ('TRUE' === val)
|
||||
return true;
|
||||
|
||||
|
||||
if ('FALSE' === val)
|
||||
return false;
|
||||
|
||||
@@ -55,46 +55,52 @@
|
||||
return val;
|
||||
}
|
||||
|
||||
var storeParam = function(name){
|
||||
return function(val, params, curr){
|
||||
var data;
|
||||
if (params && params.length && !(params.length==1 && params[0]==='CHARSET=utf-8')){
|
||||
data = {params:parseParams(params), val:text(val)}
|
||||
}
|
||||
else
|
||||
data = text(val)
|
||||
var storeValParam = function (name) {
|
||||
return function (val, curr) {
|
||||
var current = curr[name];
|
||||
if (Array.isArray(current)) {
|
||||
current.push(val);
|
||||
return curr;
|
||||
}
|
||||
|
||||
var current = curr[name];
|
||||
if (Array.isArray(current)){
|
||||
current.push(data);
|
||||
return curr;
|
||||
}
|
||||
if (current != null) {
|
||||
curr[name] = [current, val];
|
||||
return curr;
|
||||
}
|
||||
|
||||
if (current != null){
|
||||
curr[name] = [current, data];
|
||||
return curr;
|
||||
curr[name] = val;
|
||||
return curr
|
||||
}
|
||||
|
||||
curr[name] = data;
|
||||
return curr
|
||||
}
|
||||
}
|
||||
|
||||
var addTZ = function(dt, name, params){
|
||||
var storeParam = function (name) {
|
||||
return function (val, params, curr) {
|
||||
var data;
|
||||
if (params && params.length && !(params.length == 1 && params[0] === 'CHARSET=utf-8')) {
|
||||
data = { params: parseParams(params), val: text(val) }
|
||||
}
|
||||
else
|
||||
data = text(val)
|
||||
|
||||
return storeValParam(name)(data, curr);
|
||||
}
|
||||
}
|
||||
|
||||
var addTZ = function (dt, params) {
|
||||
var p = parseParams(params);
|
||||
|
||||
if (params && p){
|
||||
dt[name].tz = p.TZID
|
||||
dt.tz = p.TZID
|
||||
}
|
||||
|
||||
return dt
|
||||
}
|
||||
|
||||
var dateParam = function(name){
|
||||
return function(val, params, curr){
|
||||
return function (val, params, curr) {
|
||||
|
||||
var newDate = text(val);
|
||||
|
||||
// Store as string - worst case scenario
|
||||
storeParam(name)(val, undefined, curr)
|
||||
|
||||
if (params && params[0] === "VALUE=DATE") {
|
||||
// Just Date
|
||||
@@ -102,13 +108,17 @@
|
||||
var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(val);
|
||||
if (comps !== null) {
|
||||
// No TZ info - assume same timezone as this computer
|
||||
curr[name] = new Date(
|
||||
newDate = new Date(
|
||||
comps[1],
|
||||
parseInt(comps[2], 10)-1,
|
||||
comps[3]
|
||||
);
|
||||
|
||||
return addTZ(curr, name, params);
|
||||
newDate = addTZ(newDate, params);
|
||||
newDate.dateOnly = true;
|
||||
|
||||
// Store as string - worst case scenario
|
||||
return storeValParam(name)(newDate, curr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +127,7 @@
|
||||
var comps = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(val);
|
||||
if (comps !== null) {
|
||||
if (comps[7] == 'Z'){ // GMT
|
||||
curr[name] = new Date(Date.UTC(
|
||||
newDate = new Date(Date.UTC(
|
||||
parseInt(comps[1], 10),
|
||||
parseInt(comps[2], 10)-1,
|
||||
parseInt(comps[3], 10),
|
||||
@@ -127,7 +137,7 @@
|
||||
));
|
||||
// TODO add tz
|
||||
} else {
|
||||
curr[name] = new Date(
|
||||
newDate = new Date(
|
||||
parseInt(comps[1], 10),
|
||||
parseInt(comps[2], 10)-1,
|
||||
parseInt(comps[3], 10),
|
||||
@@ -136,22 +146,16 @@
|
||||
parseInt(comps[6], 10)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return addTZ(curr, name, params)
|
||||
newDate = addTZ(newDate, params);
|
||||
}
|
||||
|
||||
|
||||
// Store as string - worst case scenario
|
||||
return storeValParam(name)(newDate, curr)
|
||||
}
|
||||
}
|
||||
|
||||
var exdateParam = function(name){
|
||||
return function(val, params, curr){
|
||||
var date = dateParam(name)(val, params, curr);
|
||||
if (date.exdates === undefined) {
|
||||
date.exdates = [];
|
||||
}
|
||||
date.exdates.push(date.exdate);
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
var geoParam = function(name){
|
||||
return function(val, params, curr){
|
||||
@@ -175,7 +179,52 @@
|
||||
}
|
||||
}
|
||||
|
||||
var addFBType = function(fb, params){
|
||||
// EXDATE is an entry that represents exceptions to a recurrence rule (ex: "repeat every day except on 7/4").
|
||||
// The EXDATE entry itself can also contain a comma-separated list, so we make sure to parse each date out separately.
|
||||
// There can also be more than one EXDATE entries in a calendar record.
|
||||
// Since there can be multiple dates, we create an array of them. The index into the array is the ISO string of the date itself, for ease of use.
|
||||
// i.e. You can check if ((curr.exdate != undefined) && (curr.exdate[date iso string] != undefined)) to see if a date is an exception.
|
||||
// NOTE: This specifically uses date only, and not time. This is to avoid a few problems:
|
||||
// 1. The ISO string with time wouldn't work for "floating dates" (dates without timezones).
|
||||
// ex: "20171225T060000" - this is supposed to mean 6 AM in whatever timezone you're currently in
|
||||
// 2. Daylight savings time potentially affects the time you would need to look up
|
||||
// 3. Some EXDATE entries in the wild seem to have times different from the recurrence rule, but are still excluded by calendar programs. Not sure how or why.
|
||||
// These would fail any sort of sane time lookup, because the time literally doesn't match the event. So we'll ignore time and just use date.
|
||||
// ex: DTSTART:20170814T140000Z
|
||||
// RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=2;BYDAY=MO,TU
|
||||
// EXDATE:20171219T060000
|
||||
// Even though "T060000" doesn't match or overlap "T1400000Z", it's still supposed to be excluded? Odd. :(
|
||||
// TODO: See if this causes any problems with events that recur multiple times a day.
|
||||
var exdateParam = function (name) {
|
||||
return function (val, params, curr) {
|
||||
var separatorPattern = /\s*,\s*/g;
|
||||
curr[name] = curr[name] || [];
|
||||
var dates = val ? val.split(separatorPattern) : [];
|
||||
dates.forEach(function (entry) {
|
||||
var exdate = new Array();
|
||||
dateParam(name)(entry, params, exdate);
|
||||
|
||||
if (exdate[name])
|
||||
{
|
||||
if (typeof exdate[name].toISOString === 'function') {
|
||||
curr[name][exdate[name].toISOString().substring(0, 10)] = exdate[name];
|
||||
} else {
|
||||
console.error("No toISOString function in exdate[name]", exdate[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
return curr;
|
||||
}
|
||||
}
|
||||
|
||||
// RECURRENCE-ID is the ID of a specific recurrence within a recurrence rule.
|
||||
// TODO: It's also possible for it to have a range, like "THISANDPRIOR", "THISANDFUTURE". This isn't currently handled.
|
||||
var recurrenceParam = function (name) {
|
||||
return dateParam(name);
|
||||
}
|
||||
|
||||
var addFBType = function (fb, params) {
|
||||
var p = parseParams(params);
|
||||
|
||||
if (params && p){
|
||||
@@ -219,7 +268,7 @@
|
||||
//scan all high level object in curr and drop all strings
|
||||
var key,
|
||||
obj;
|
||||
|
||||
|
||||
for (key in curr) {
|
||||
if(curr.hasOwnProperty(key)) {
|
||||
obj = curr[key];
|
||||
@@ -228,14 +277,93 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return curr
|
||||
}
|
||||
|
||||
|
||||
var par = stack.pop()
|
||||
|
||||
if (curr.uid)
|
||||
par[curr.uid] = curr
|
||||
{
|
||||
// If this is the first time we run into this UID, just save it.
|
||||
if (par[curr.uid] === undefined)
|
||||
{
|
||||
par[curr.uid] = curr;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we have multiple ical entries with the same UID, it's either going to be a
|
||||
// modification to a recurrence (RECURRENCE-ID), and/or a significant modification
|
||||
// to the entry (SEQUENCE).
|
||||
|
||||
// TODO: Look into proper sequence logic.
|
||||
|
||||
if (curr.recurrenceid === undefined)
|
||||
{
|
||||
// If we have the same UID as an existing record, and it *isn't* a specific recurrence ID,
|
||||
// not quite sure what the correct behaviour should be. For now, just take the new information
|
||||
// and merge it with the old record by overwriting only the fields that appear in the new record.
|
||||
var key;
|
||||
for (key in curr) {
|
||||
par[curr.uid][key] = curr[key];
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// If we have recurrence-id entries, list them as an array of recurrences keyed off of recurrence-id.
|
||||
// To use - as you're running through the dates of an rrule, you can try looking it up in the recurrences
|
||||
// array. If it exists, then use the data from the calendar object in the recurrence instead of the parent
|
||||
// for that day.
|
||||
|
||||
// NOTE: Sometimes the RECURRENCE-ID record will show up *before* the record with the RRULE entry. In that
|
||||
// case, what happens is that the RECURRENCE-ID record ends up becoming both the parent record and an entry
|
||||
// in the recurrences array, and then when we process the RRULE entry later it overwrites the appropriate
|
||||
// fields in the parent record.
|
||||
|
||||
if (curr.recurrenceid != null)
|
||||
{
|
||||
|
||||
// TODO: Is there ever a case where we have to worry about overwriting an existing entry here?
|
||||
|
||||
// Create a copy of the current object to save in our recurrences array. (We *could* just do par = curr,
|
||||
// except for the case that we get the RECURRENCE-ID record before the RRULE record. In that case, we
|
||||
// would end up with a shared reference that would cause us to overwrite *both* records at the point
|
||||
// that we try and fix up the parent record.)
|
||||
var recurrenceObj = new Object();
|
||||
var key;
|
||||
for (key in curr) {
|
||||
recurrenceObj[key] = curr[key];
|
||||
}
|
||||
|
||||
if (recurrenceObj.recurrences != undefined) {
|
||||
delete recurrenceObj.recurrences;
|
||||
}
|
||||
|
||||
|
||||
// If we don't have an array to store recurrences in yet, create it.
|
||||
if (par[curr.uid].recurrences === undefined) {
|
||||
par[curr.uid].recurrences = new Array();
|
||||
}
|
||||
|
||||
// Save off our cloned recurrence object into the array, keyed by date but not time.
|
||||
// We key by date only to avoid timezone and "floating time" problems (where the time isn't associated with a timezone).
|
||||
// TODO: See if this causes a problem with events that have multiple recurrences per day.
|
||||
if (typeof curr.recurrenceid.toISOString === 'function') {
|
||||
par[curr.uid].recurrences[curr.recurrenceid.toISOString().substring(0,10)] = recurrenceObj;
|
||||
} else {
|
||||
console.error("No toISOString function in curr.recurrenceid", curr.recurrenceid);
|
||||
}
|
||||
}
|
||||
|
||||
// One more specific fix - in the case that an RRULE entry shows up after a RECURRENCE-ID entry,
|
||||
// let's make sure to clear the recurrenceid off the parent field.
|
||||
if ((par[curr.uid].rrule != undefined) && (par[curr.uid].recurrenceid != undefined))
|
||||
{
|
||||
delete par[curr.uid].recurrenceid;
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
par[Math.random()*100000] = curr // Randomly assign ID : TODO - use true GUID
|
||||
|
||||
@@ -257,6 +385,11 @@
|
||||
, 'COMPLETED': dateParam('completed')
|
||||
, 'CATEGORIES': categoriesParam('categories')
|
||||
, 'FREEBUSY': freebusyParam('freebusy')
|
||||
, 'DTSTAMP': dateParam('dtstamp')
|
||||
, 'CREATED': dateParam('created')
|
||||
, 'LAST-MODIFIED': dateParam('lastmodified')
|
||||
, 'RECURRENCE-ID': recurrenceParam('recurrenceid')
|
||||
|
||||
},
|
||||
|
||||
|
||||
@@ -272,7 +405,7 @@
|
||||
name = name.substring(2);
|
||||
return (storeParam(name))(val, params, ctx, stack, line);
|
||||
}
|
||||
|
||||
|
||||
return storeParam(name.toLowerCase())(val, params, ctx);
|
||||
},
|
||||
|
||||
|
@@ -6,9 +6,16 @@ exports.fromURL = function(url, opts, cb){
|
||||
if (!cb)
|
||||
return;
|
||||
request(url, opts, function(err, r, data){
|
||||
if (err)
|
||||
return cb(err, null);
|
||||
cb(undefined, ical.parseICS(data));
|
||||
if (err)
|
||||
{
|
||||
return cb(err, null);
|
||||
}
|
||||
else if (r.statusCode != 200)
|
||||
{
|
||||
return cb(r.statusCode + ": " + r.statusMessage, null);
|
||||
}
|
||||
|
||||
cb(undefined, ical.parseICS(data));
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,34 +24,43 @@ exports.parseFile = function(filename){
|
||||
}
|
||||
|
||||
|
||||
var rrule = require('rrule-alt').RRule
|
||||
var rrulestr = rrule.rrulestr
|
||||
var rrule = require('rrule').RRule
|
||||
|
||||
ical.objectHandlers['RRULE'] = function(val, params, curr, stack, line){
|
||||
curr.rrule = line;
|
||||
return curr
|
||||
}
|
||||
var originalEnd = ical.objectHandlers['END'];
|
||||
ical.objectHandlers['END'] = function(val, params, curr, stack){
|
||||
if (curr.rrule) {
|
||||
var rule = curr.rrule;
|
||||
if (rule.indexOf('DTSTART') === -1) {
|
||||
ical.objectHandlers['END'] = function (val, params, curr, stack) {
|
||||
// Recurrence rules are only valid for VEVENT, VTODO, and VJOURNAL.
|
||||
// More specifically, we need to filter the VCALENDAR type because we might end up with a defined rrule
|
||||
// due to the subtypes.
|
||||
if ((val === "VEVENT") || (val === "VTODO") || (val === "VJOURNAL")) {
|
||||
if (curr.rrule) {
|
||||
var rule = curr.rrule.replace('RRULE:', '');
|
||||
if (rule.indexOf('DTSTART') === -1) {
|
||||
|
||||
if (curr.start.length === 8) {
|
||||
var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(curr.start);
|
||||
if (comps) {
|
||||
curr.start = new Date (comps[1], comps[2] - 1, comps[3]);
|
||||
}
|
||||
}
|
||||
if (curr.start.length === 8) {
|
||||
var comps = /^(\d{4})(\d{2})(\d{2})$/.exec(curr.start);
|
||||
if (comps) {
|
||||
curr.start = new Date(comps[1], comps[2] - 1, comps[3]);
|
||||
}
|
||||
}
|
||||
|
||||
rule += ' DTSTART:' + curr.start.toISOString().replace(/[-:]/g, '');
|
||||
rule = rule.replace(/\.[0-9]{3}/, '');
|
||||
}
|
||||
for (var i in curr.exdates) {
|
||||
rule += ' EXDATE:' + curr.exdates[i].toISOString().replace(/[-:]/g, '');
|
||||
rule = rule.replace(/\.[0-9]{3}/, '');
|
||||
}
|
||||
curr.rrule = rrulestr(rule);
|
||||
}
|
||||
|
||||
if (typeof curr.start.toISOString === 'function') {
|
||||
try {
|
||||
rule += ';DTSTART=' + curr.start.toISOString().replace(/[-:]/g, '');
|
||||
rule = rule.replace(/\.[0-9]{3}/, '');
|
||||
} catch (error) {
|
||||
console.error("ERROR when trying to convert to ISOString", error);
|
||||
}
|
||||
} else {
|
||||
console.error("No toISOString function in curr.start", curr.start);
|
||||
}
|
||||
}
|
||||
curr.rrule = rrule.fromString(rule);
|
||||
}
|
||||
}
|
||||
return originalEnd.call(this, val, params, curr, stack);
|
||||
}
|
||||
|
@@ -10,17 +10,18 @@
|
||||
],
|
||||
"homepage": "https://github.com/peterbraden/ical.js",
|
||||
"author": "Peter Braden <peterbraden@peterbraden.co.uk> (peterbraden.co.uk)",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/peterbraden/ical.js.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"request": "2.68.0",
|
||||
"rrule": "2.0.0"
|
||||
"request": "^2.88.0",
|
||||
"rrule": "2.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vows": "0.7.0",
|
||||
"underscore": "1.3.0"
|
||||
"vows": "0.8.2",
|
||||
"underscore": "1.9.1"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "./node_modules/vows/bin/vows ./test/test.js"
|
||||
|
@@ -7,6 +7,7 @@ A tolerant, minimal icalendar parser for javascript/node
|
||||
(http://tools.ietf.org/html/rfc5545)
|
||||
|
||||
|
||||
|
||||
## Install - Node.js ##
|
||||
|
||||
ical.js is availble on npm:
|
||||
@@ -33,19 +34,29 @@ Use the request library to fetch the specified URL (```opts``` gets passed on to
|
||||
|
||||
## Example 1 - Print list of upcoming node conferences (see example.js)
|
||||
```javascript
|
||||
var ical = require('ical')
|
||||
, months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||
'use strict';
|
||||
|
||||
ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function(err, data) {
|
||||
for (var k in data){
|
||||
if (data.hasOwnProperty(k)) {
|
||||
var ev = data[k]
|
||||
console.log("Conference",
|
||||
ev.summary,
|
||||
'is in',
|
||||
ev.location,
|
||||
'on the', ev.start.getDate(), 'of', months[ev.start.getMonth()]);
|
||||
}
|
||||
}
|
||||
});
|
||||
const ical = require('ical');
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
ical.fromURL('http://lanyrd.com/topics/nodejs/nodejs.ics', {}, function (err, data) {
|
||||
for (let k in data) {
|
||||
if (data.hasOwnProperty(k)) {
|
||||
var ev = data[k];
|
||||
if (data[k].type == 'VEVENT') {
|
||||
console.log(`${ev.summary} is in ${ev.location} on the ${ev.start.getDate()} of ${months[ev.start.getMonth()]} at ${ev.start.toLocaleTimeString('en-GB')}`);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Recurrences and Exceptions ##
|
||||
Calendar events with recurrence rules can be significantly more complicated to handle correctly. There are three parts to handling them:
|
||||
|
||||
1. rrule - the recurrence rule specifying the pattern of recurring dates and times for the event.
|
||||
2. recurrences - an optional array of event data that can override specific occurrences of the event.
|
||||
3. exdate - an optional array of dates that should be excluded from the recurrence pattern.
|
||||
|
||||
See example_rrule.js for an example of handling recurring calendar events.
|
||||
|
131
modules/default/calendar/vendor/ical.js/test/test.js
vendored
@@ -43,6 +43,12 @@ vows.describe('node-ical').addBatch({
|
||||
, 'has a summary (invalid colon handling tolerance)' : function(topic){
|
||||
assert.equal(topic.summary, '[Async]: Everything Express')
|
||||
}
|
||||
, 'has a date only start datetime' : function(topic){
|
||||
assert.equal(topic.start.dateOnly, true)
|
||||
}
|
||||
, 'has a date only end datetime' : function(topic){
|
||||
assert.equal(topic.end.dateOnly, true)
|
||||
}
|
||||
}
|
||||
, 'event d4c8' :{
|
||||
topic : function(events){
|
||||
@@ -108,7 +114,7 @@ vows.describe('node-ical').addBatch({
|
||||
assert.equal(topic.end.getFullYear(), 1998);
|
||||
assert.equal(topic.end.getUTCMonth(), 2);
|
||||
assert.equal(topic.end.getUTCDate(), 15);
|
||||
assert.equal(topic.end.getUTCHours(), 0);
|
||||
assert.equal(topic.end.getUTCHours(), 00);
|
||||
assert.equal(topic.end.getUTCMinutes(), 30);
|
||||
}
|
||||
}
|
||||
@@ -146,7 +152,7 @@ vows.describe('node-ical').addBatch({
|
||||
}
|
||||
, 'has a start datetime' : function(topic) {
|
||||
assert.equal(topic.start.getFullYear(), 2011);
|
||||
assert.equal(topic.start.getMonth(), 9);
|
||||
assert.equal(topic.start.getMonth(), 09);
|
||||
assert.equal(topic.start.getDate(), 11);
|
||||
}
|
||||
|
||||
@@ -192,12 +198,12 @@ vows.describe('node-ical').addBatch({
|
||||
}
|
||||
, 'has a start' : function(topic){
|
||||
assert.equal(topic.start.tz, 'America/Phoenix')
|
||||
assert.equal(topic.start.toISOString(), new Date(2011, 10, 9, 19, 0,0).toISOString())
|
||||
assert.equal(topic.start.toISOString(), new Date(2011, 10, 09, 19, 0,0).toISOString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'with test6.ics (testing assembly.org)' : {
|
||||
, 'with test6.ics (testing assembly.org)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test6.ics')
|
||||
}
|
||||
@@ -208,13 +214,13 @@ vows.describe('node-ical').addBatch({
|
||||
})[0];
|
||||
}
|
||||
, 'has a start' : function(topic){
|
||||
assert.equal(topic.start.toISOString(), new Date(2011, 7, 4, 12, 0,0).toISOString())
|
||||
assert.equal(topic.start.toISOString(), new Date(2011, 07, 04, 12, 0,0).toISOString())
|
||||
}
|
||||
}
|
||||
, 'event with rrule' :{
|
||||
topic: function(events){
|
||||
return _.select(_.values(events), function(x){
|
||||
return x.summary == "foobarTV broadcast starts"
|
||||
return x.summary === "foobarTV broadcast starts"
|
||||
})[0];
|
||||
}
|
||||
, "Has an RRULE": function(topic){
|
||||
@@ -249,7 +255,7 @@ vows.describe('node-ical').addBatch({
|
||||
},
|
||||
'task completed': function(task){
|
||||
assert.equal(task.completion, 100);
|
||||
assert.equal(task.completed.toISOString(), new Date(2013, 6, 16, 10, 57, 45).toISOString());
|
||||
assert.equal(task.completed.toISOString(), new Date(2013, 06, 16, 10, 57, 45).toISOString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,7 +278,7 @@ vows.describe('node-ical').addBatch({
|
||||
},
|
||||
'grabbing custom properties': {
|
||||
topic: function(topic) {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -367,14 +373,115 @@ vows.describe('node-ical').addBatch({
|
||||
assert.equal(topic.end.getFullYear(), 2014);
|
||||
assert.equal(topic.end.getMonth(), 3);
|
||||
assert.equal(topic.end.getUTCHours(), 19);
|
||||
assert.equal(topic.end.getUTCMinutes(), 0);
|
||||
assert.equal(topic.end.getUTCMinutes(), 00);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
'url request errors' : {
|
||||
, 'with test12.ics (testing recurrences and exdates)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test12.ics')
|
||||
}
|
||||
, 'event with rrule': {
|
||||
topic: function (events) {
|
||||
return _.select(_.values(events), function (x) {
|
||||
return x.uid === '0000001';
|
||||
})[0];
|
||||
}
|
||||
, "Has an RRULE": function (topic) {
|
||||
assert.notEqual(topic.rrule, undefined);
|
||||
}
|
||||
, "Has summary Treasure Hunting": function (topic) {
|
||||
assert.equal(topic.summary, 'Treasure Hunting');
|
||||
}
|
||||
, "Has two EXDATES": function (topic) {
|
||||
assert.notEqual(topic.exdate, undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2015, 06, 08, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2015, 06, 10, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
}
|
||||
, "Has a RECURRENCE-ID override": function (topic) {
|
||||
assert.notEqual(topic.recurrences, undefined);
|
||||
assert.notEqual(topic.recurrences[new Date(2015, 06, 07, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.equal(topic.recurrences[new Date(2015, 06, 07, 12, 0, 0).toISOString().substring(0, 10)].summary, 'More Treasure Hunting');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'with test13.ics (testing recurrence-id before rrule)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test13.ics')
|
||||
}
|
||||
, 'event with rrule': {
|
||||
topic: function (events) {
|
||||
return _.select(_.values(events), function (x) {
|
||||
return x.uid === '6m2q7kb2l02798oagemrcgm6pk@google.com';
|
||||
})[0];
|
||||
}
|
||||
, "Has an RRULE": function (topic) {
|
||||
assert.notEqual(topic.rrule, undefined);
|
||||
}
|
||||
, "Has summary 'repeated'": function (topic) {
|
||||
assert.equal(topic.summary, 'repeated');
|
||||
}
|
||||
, "Has a RECURRENCE-ID override": function (topic) {
|
||||
assert.notEqual(topic.recurrences, undefined);
|
||||
assert.notEqual(topic.recurrences[new Date(2016, 7, 26, 14, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.equal(topic.recurrences[new Date(2016, 7, 26, 14, 0, 0).toISOString().substring(0, 10)].summary, 'bla bla');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'with test14.ics (testing comma-separated exdates)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test14.ics')
|
||||
}
|
||||
, 'event with comma-separated exdate': {
|
||||
topic: function (events) {
|
||||
return _.select(_.values(events), function (x) {
|
||||
return x.uid === '98765432-ABCD-DCBB-999A-987765432123';
|
||||
})[0];
|
||||
}
|
||||
, "Has summary 'Example of comma-separated exdates'": function (topic) {
|
||||
assert.equal(topic.summary, 'Example of comma-separated exdates');
|
||||
}
|
||||
, "Has four comma-separated EXDATES": function (topic) {
|
||||
assert.notEqual(topic.exdate, undefined);
|
||||
// Verify the four comma-separated EXDATES are there
|
||||
assert.notEqual(topic.exdate[new Date(2017, 6, 6, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2017, 6, 17, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2017, 6, 20, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2017, 7, 3, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
// Verify an arbitrary date isn't there
|
||||
assert.equal(topic.exdate[new Date(2017, 4, 5, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'with test14.ics (testing exdates with bad times)': {
|
||||
topic: function () {
|
||||
return ical.parseFile('./test/test14.ics')
|
||||
}
|
||||
, 'event with exdates with bad times': {
|
||||
topic: function (events) {
|
||||
return _.select(_.values(events), function (x) {
|
||||
return x.uid === '1234567-ABCD-ABCD-ABCD-123456789012';
|
||||
})[0];
|
||||
}
|
||||
, "Has summary 'Example of exdate with bad times'": function (topic) {
|
||||
assert.equal(topic.summary, 'Example of exdate with bad times');
|
||||
}
|
||||
, "Has two EXDATES even though they have bad times": function (topic) {
|
||||
assert.notEqual(topic.exdate, undefined);
|
||||
// Verify the two EXDATES are there, even though they have bad times
|
||||
assert.notEqual(topic.exdate[new Date(2017, 11, 18, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
assert.notEqual(topic.exdate[new Date(2017, 11, 19, 12, 0, 0).toISOString().substring(0, 10)], undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
, 'url request errors': {
|
||||
topic : function () {
|
||||
ical.fromURL('http://not.exist/', {}, this.callback);
|
||||
ical.fromURL('http://255.255.255.255/', {}, this.callback);
|
||||
}
|
||||
, 'are passed back to the callback' : function (err, result) {
|
||||
assert.instanceOf(err, Error);
|
||||
|
19
modules/default/calendar/vendor/ical.js/test/test12.ics
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
BEGIN:VCALENDAR
|
||||
BEGIN:VEVENT
|
||||
UID:0000001
|
||||
SUMMARY:Treasure Hunting
|
||||
DTSTART;TZID=America/Los_Angeles:20150706T120000
|
||||
DTEND;TZID=America/Los_Angeles:20150706T130000
|
||||
RRULE:FREQ=DAILY;COUNT=10
|
||||
EXDATE;TZID=America/Los_Angeles:20150708T120000
|
||||
EXDATE;TZID=America/Los_Angeles:20150710T120000
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:0000001
|
||||
SUMMARY:More Treasure Hunting
|
||||
LOCATION:The other island
|
||||
DTSTART;TZID=America/Los_Angeles:20150709T150000
|
||||
DTEND;TZID=America/Los_Angeles:20150707T160000
|
||||
RECURRENCE-ID;TZID=America/Los_Angeles:20150707T120000
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
57
modules/default/calendar/vendor/ical.js/test/test13.ics
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-WR-CALNAME:ical
|
||||
X-WR-TIMEZONE:Europe/Kiev
|
||||
X-WR-CALDESC:
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Kiev
|
||||
X-LIC-LOCATION:Europe/Kiev
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0300
|
||||
TZNAME:EEST
|
||||
DTSTART:19700329T030000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+0300
|
||||
TZOFFSETTO:+0200
|
||||
TZNAME:EET
|
||||
DTSTART:19701025T040000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=Europe/Kiev:20160826T140000
|
||||
DTEND;TZID=Europe/Kiev:20160826T150000
|
||||
DTSTAMP:20160825T061505Z
|
||||
UID:6m2q7kb2l02798oagemrcgm6pk@google.com
|
||||
RECURRENCE-ID;TZID=Europe/Kiev:20160826T140000
|
||||
CREATED:20160823T125221Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20160823T130320Z
|
||||
LOCATION:
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:bla bla
|
||||
TRANSP:OPAQUE
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=Europe/Kiev:20160825T140000
|
||||
DTEND;TZID=Europe/Kiev:20160825T150000
|
||||
RRULE:FREQ=DAILY;UNTIL=20160828T110000Z
|
||||
DTSTAMP:20160825T061505Z
|
||||
UID:6m2q7kb2l02798oagemrcgm6pk@google.com
|
||||
CREATED:20160823T125221Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20160823T125221Z
|
||||
LOCATION:
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:repeated
|
||||
TRANSP:OPAQUE
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
33
modules/default/calendar/vendor/ical.js/test/test14.ics
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-WR-CALNAME:ical
|
||||
X-WR-TIMEZONE:Europe/Kiev
|
||||
X-WR-CALDESC:
|
||||
BEGIN:VEVENT
|
||||
UID:98765432-ABCD-DCBB-999A-987765432123
|
||||
DTSTART;TZID=US/Central:20170216T090000
|
||||
DTEND;TZID=US/Central:20170216T190000
|
||||
DTSTAMP:20170727T044436Z
|
||||
EXDATE;TZID=US/Central:20170706T090000,20170717T090000,20170720T090000,20
|
||||
170803T090000
|
||||
LAST-MODIFIED:20170727T044435Z
|
||||
RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20170814T045959Z;INTERVAL=2;BYDAY=MO,TH
|
||||
SEQUENCE:0
|
||||
SUMMARY:Example of comma-separated exdates
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:1234567-ABCD-ABCD-ABCD-123456789012
|
||||
DTSTART:20170814T140000Z
|
||||
DTEND:20170815T000000Z
|
||||
DTSTAMP:20171204T134925Z
|
||||
EXDATE:20171219T060000
|
||||
EXDATE:20171218T060000
|
||||
LAST-MODIFIED:20171024T140004Z
|
||||
RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=2;BYDAY=MO,TU
|
||||
SEQUENCE:0
|
||||
SUMMARY:Example of exdate with bad times
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
@@ -2,6 +2,11 @@
|
||||
The `clock` module is one of the default modules of the MagicMirror.
|
||||
This module displays the current date and time. The information will be updated realtime.
|
||||
|
||||
## Screenshot
|
||||
|
||||
- Current time
|
||||

|
||||
|
||||
## Using the module
|
||||
|
||||
To use this module, add it to the modules array in the `config/config.js` file:
|
||||
@@ -39,3 +44,14 @@ The following properties can be configured:
|
||||
| `analogPlacement` | **Specific to the analog clock. _(requires displayType set to `'both'`)_** Specifies where the analog clock is in relation to the digital clock <br><br> **Possible values:** `top`, `right`, `bottom`, or `left` <br> **Default value:** `bottom`
|
||||
| `analogShowDate` | **Specific to the analog clock.** If the clock is used as a separate module and set to analog only, this configures whether a date is also displayed with the clock. <br><br> **Possible values:** `false`, `top`, or `bottom` <br> **Default value:** `top`
|
||||
| `timezone` | Specific a timezone to show clock. <br><br> **Possible examples values:** `America/New_York`, `America/Santiago`, `Etc/GMT+10` <br> **Default value:** `none`. See more informations about configuration value [here](https://momentjs.com/timezone/docs/#/data-formats/packed-format/)
|
||||
|
||||
## Notifications
|
||||
|
||||
The clock makes use of the built-in [Notification Mechanism](https://github.com/michMich/MagicMirror/wiki/notifications) to relay notifications to all modules.
|
||||
|
||||
Current notifications are:
|
||||
|
||||
| Notification | Description
|
||||
| ----------------- | -----------
|
||||
| `CLOCK_SECOND` | A second has elapsed. <br> *Parameter*: second value
|
||||
| `CLOCK_MINUTE` | A minute has elapsed <br> *Parameter*: minute value
|
||||
|
@@ -41,9 +41,40 @@ Module.register("clock",{
|
||||
|
||||
// Schedule update interval.
|
||||
var self = this;
|
||||
setInterval(function() {
|
||||
self.second = moment().second();
|
||||
self.minute = moment().minute();
|
||||
|
||||
//Calculate how many ms should pass until next update depending on if seconds is displayed or not
|
||||
var delayCalculator = function(reducedSeconds) {
|
||||
if (self.config.displaySeconds) {
|
||||
return 1000 - moment().milliseconds();
|
||||
} else {
|
||||
return ((60 - reducedSeconds) * 1000) - moment().milliseconds();
|
||||
}
|
||||
};
|
||||
|
||||
//A recursive timeout function instead of interval to avoid drifting
|
||||
var notificationTimer = function() {
|
||||
self.updateDom();
|
||||
}, 1000);
|
||||
|
||||
//If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent)
|
||||
if (self.config.displaySeconds) {
|
||||
self.second = (self.second + 1) % 60;
|
||||
if (self.second !== 0) {
|
||||
self.sendNotification("CLOCK_SECOND", self.second);
|
||||
setTimeout(notificationTimer, delayCalculator(0));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
|
||||
self.minute = (self.minute + 1) % 60;
|
||||
self.sendNotification("CLOCK_MINUTE", self.minute);
|
||||
setTimeout(notificationTimer, delayCalculator(0));
|
||||
};
|
||||
|
||||
//Set the initial timeout with the amount of seconds elapsed as reducedSeconds so it will trigger when the minute changes
|
||||
setTimeout(notificationTimer, delayCalculator(self.second));
|
||||
|
||||
// Set locale.
|
||||
moment.locale(config.language);
|
||||
@@ -62,12 +93,12 @@ Module.register("clock",{
|
||||
var timeWrapper = document.createElement("div");
|
||||
var secondsWrapper = document.createElement("sup");
|
||||
var periodWrapper = document.createElement("span");
|
||||
var weekWrapper = document.createElement("div")
|
||||
var weekWrapper = document.createElement("div");
|
||||
// Style Wrappers
|
||||
dateWrapper.className = "date normal medium";
|
||||
timeWrapper.className = "time bright large light";
|
||||
secondsWrapper.className = "dimmed";
|
||||
weekWrapper.className = "week dimmed medium"
|
||||
weekWrapper.className = "week dimmed medium";
|
||||
|
||||
// Set content of wrappers.
|
||||
// The moment().format("h") method has a bug on the Raspberry Pi.
|
||||
@@ -75,6 +106,7 @@ Module.register("clock",{
|
||||
// See issue: https://github.com/MichMich/MagicMirror/issues/181
|
||||
var timeString;
|
||||
var now = moment();
|
||||
this.lastDisplayedMinute = now.minute();
|
||||
if (this.config.timezone) {
|
||||
now.tz(this.config.timezone);
|
||||
}
|
||||
@@ -132,14 +164,15 @@ Module.register("clock",{
|
||||
clockCircle.style.width = this.config.analogSize;
|
||||
clockCircle.style.height = this.config.analogSize;
|
||||
|
||||
if (this.config.analogFace != "" && this.config.analogFace != "simple" && this.config.analogFace != "none") {
|
||||
if (this.config.analogFace !== "" && this.config.analogFace !== "simple" && this.config.analogFace !== "none") {
|
||||
clockCircle.style.background = "url("+ this.data.path + "faces/" + this.config.analogFace + ".svg)";
|
||||
clockCircle.style.backgroundSize = "100%";
|
||||
|
||||
// The following line solves issue: https://github.com/MichMich/MagicMirror/issues/611
|
||||
clockCircle.style.border = "1px solid black";
|
||||
// clockCircle.style.border = "1px solid black";
|
||||
clockCircle.style.border = "rgba(0, 0, 0, 0.1)"; //Updated fix for Issue 611 where non-black backgrounds are used
|
||||
|
||||
} else if (this.config.analogFace != "none") {
|
||||
} else if (this.config.analogFace !== "none") {
|
||||
clockCircle.style.border = "2px solid white";
|
||||
}
|
||||
var clockFace = document.createElement("div");
|
||||
|
BIN
modules/default/clock/clock_screenshot.png
Normal file
After Width: | Height: | Size: 30 KiB |
@@ -2,6 +2,10 @@
|
||||
The `compliments` module is one of the default modules of the MagicMirror.
|
||||
This module displays a random compliment.
|
||||
|
||||
## Screenshots
|
||||
- Compliments Screenshot
|
||||

|
||||
|
||||
## Using the module
|
||||
|
||||
To use this module, add it to the modules array in the `config/config.js` file:
|
||||
@@ -45,22 +49,22 @@ The `compliments` property contains an object with four arrays: <code>morning</c
|
||||
|
||||
|
||||
If use the currentweather is possible use a actual weather for set compliments. The availables properties are:
|
||||
* `day_sunny`
|
||||
* `day_cloudy`
|
||||
* `cloudy`
|
||||
* `cloudy_windy`
|
||||
* `showers`
|
||||
* `rain`
|
||||
* `thunderstorm`
|
||||
* `snow`
|
||||
* `fog`
|
||||
* `night_clear`
|
||||
* `night_cloudy`
|
||||
* `night_showers`
|
||||
* `night_rain`
|
||||
* `night_thunderstorm`
|
||||
* `night_snow`
|
||||
* `night_alt_cloudy_windy`
|
||||
- `day_sunny`
|
||||
- `day_cloudy`
|
||||
- `cloudy`
|
||||
- `cloudy_windy`
|
||||
- `showers`
|
||||
- `rain`
|
||||
- `thunderstorm`
|
||||
- `snow`
|
||||
- `fog`
|
||||
- `night_clear`
|
||||
- `night_cloudy`
|
||||
- `night_showers`
|
||||
- `night_rain`
|
||||
- `night_thunderstorm`
|
||||
- `night_snow`
|
||||
- `night_alt_cloudy_windy`
|
||||
|
||||
#### Example use with currentweather module
|
||||
````javascript
|
||||
|
@@ -54,7 +54,7 @@ Module.register("compliments", {
|
||||
this.lastComplimentIndex = -1;
|
||||
|
||||
var self = this;
|
||||
if (this.config.remoteFile != null) {
|
||||
if (this.config.remoteFile !== null) {
|
||||
this.complimentFile(function(response) {
|
||||
self.config.compliments = JSON.parse(response);
|
||||
self.updateDom();
|
||||
@@ -134,7 +134,7 @@ Module.register("compliments", {
|
||||
xobj.overrideMimeType("application/json");
|
||||
xobj.open("GET", path, true);
|
||||
xobj.onreadystatechange = function() {
|
||||
if (xobj.readyState == 4 && xobj.status == "200") {
|
||||
if (xobj.readyState === 4 && xobj.status === 200) {
|
||||
callback(xobj.responseText);
|
||||
}
|
||||
};
|
||||
@@ -165,7 +165,6 @@ Module.register("compliments", {
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
|
||||
// From data currentweather set weather type
|
||||
setCurrentWeatherType: function(data) {
|
||||
var weatherIconTable = {
|
||||
@@ -191,10 +190,9 @@ Module.register("compliments", {
|
||||
this.currentWeatherType = weatherIconTable[data.weather[0].icon];
|
||||
},
|
||||
|
||||
|
||||
// Override notification handler.
|
||||
notificationReceived: function(notification, payload, sender) {
|
||||
if (notification == "CURRENTWEATHER_DATA") {
|
||||
if (notification === "CURRENTWEATHER_DATA") {
|
||||
this.setCurrentWeatherType(payload.data);
|
||||
}
|
||||
},
|
||||
|
BIN
modules/default/compliments/compliments_screenshot.png
Normal file
After Width: | Height: | Size: 26 KiB |
@@ -2,6 +2,11 @@
|
||||
The `currentweather` module is one of the default modules of the MagicMirror.
|
||||
This module displays the current weather, including the windspeed, the sunset or sunrise time, the temperature and an icon to display the current conditions.
|
||||
|
||||
## Screenshot
|
||||
|
||||
- Current weather screenshot
|
||||

|
||||
|
||||
## Using the module
|
||||
|
||||
To use this module, add it to the modules array in the `config/config.js` file:
|
||||
@@ -14,7 +19,7 @@ modules: [
|
||||
config: {
|
||||
// See 'Configuration options' for more information.
|
||||
location: "Amsterdam,Netherlands",
|
||||
locationID: "", //Location ID from http://openweathermap.org/help/city_list.txt
|
||||
locationID: "", //Location ID from http://bulk.openweathermap.org/sample/city.list.json.gz
|
||||
appid: "abcde12345abcde12345abcde12345ab" //openweathermap.org API key.
|
||||
}
|
||||
}
|
||||
@@ -29,7 +34,7 @@ The following properties can be configured:
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `location` | The location used for weather information. <br><br> **Example:** `'Amsterdam,Netherlands'` <br> **Default value:** `false` <br><br> **Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
|
||||
| `locationID` | Location ID from [OpenWeatherMap](http://openweathermap.org/help/city_list.txt) **This will override anything you put in location.** <br> Leave blank if you want to use location. <br> **Example:** `1234567` <br> **Default value:** `false` <br><br> **Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
|
||||
| `locationID` | Location ID from [OpenWeatherMap](https://openweathermap.org/find) **This will override anything you put in location.** <br> Leave blank if you want to use location. <br> **Example:** `1234567` <br> **Default value:** `false` <br><br> **Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
|
||||
| `appid` | The [OpenWeatherMap](https://home.openweathermap.org) API key, which can be obtained by creating an OpenWeatherMap account. <br><br> This value is **REQUIRED**
|
||||
| `units` | What units to use. Specified by config.js <br><br> **Possible values:** `config.units` = Specified by config.js, `default` = Kelvin, `metric` = Celsius, `imperial` =Fahrenheit <br> **Default value:** `config.units`
|
||||
| `roundTemp` | Round temperature value to nearest integer. <br><br> **Possible values:** `true` (round to integer) or `false` (display exact value with decimal point) <br> **Default value:** `false`
|
||||
@@ -45,7 +50,7 @@ The following properties can be configured:
|
||||
| `showIndoorTemperature` | If you have another module that emits the INDOOR_TEMPERATURE notification, the indoor temperature will be displayed <br> **Default value:** `false`
|
||||
| `onlyTemp` | Show only current Temperature and weather icon without windspeed, sunset, sunrise time and feels like. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `showFeelsLike` | Shows the Feels like temperature weather. <br><br> **Possible values:**`true` or `false`<br>**Default value:** `true`
|
||||
| `useKMPHWind` | Uses KMPH as units for windspeed. <br><br> **Possible values:**`true` or `false`<br>**Default value:** `false`
|
||||
| `useKMPHwind` | Uses KMPH as units for windspeed. <br><br> **Possible values:**`true` or `false`<br>**Default value:** `false`
|
||||
| `useBeaufort` | Pick between using the Beaufort scale for wind speed or using the default units. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `lang` | The language of the days. <br><br> **Possible values:** `en`, `nl`, `ru`, etc ... <br> **Default value:** uses value of _config.language_
|
||||
| `decimalSymbol` | The decimal symbol to use.<br><br> **Possible values:** `.`, `,` or any other symbol.<br> **Default value:** `.`
|
||||
@@ -55,6 +60,7 @@ The following properties can be configured:
|
||||
| `apiBase` | The OpenWeatherMap base URL. <br><br> **Default value:** `'http://api.openweathermap.org/data/'`
|
||||
| `weatherEndpoint` | The OpenWeatherMap API endPoint. <br><br> **Default value:** `'weather'`
|
||||
| `appendLocationNameToHeader` | If set to `true`, the returned location name will be appended to the header of the module, if the header is enabled. This is mainly intresting when using calender based weather. <br><br> **Default value:** `true`
|
||||
| `useLocationAsHeader` | If set to `true` and location is given a value, the value of location will be used as the header. This is useful if `locationName` was not returned. <br><br> **Default value:** `false`
|
||||
| `calendarClass` | The class for the calender module to base the event based weather information on. <br><br> **Default value:** `'calendar'`
|
||||
| `iconTable` | The conversion table to convert the weather conditions to weather-icons. <br><br> **Default value:** view tabel below.
|
||||
|
||||
|
@@ -23,6 +23,7 @@ Module.register("currentweather",{
|
||||
showWindDirection: true,
|
||||
showWindDirectionAsArrow: false,
|
||||
useBeaufort: true,
|
||||
appendLocationNameToHeader: false,
|
||||
useKMPHwind: false,
|
||||
lang: config.language,
|
||||
decimalSymbol: ".",
|
||||
@@ -71,7 +72,7 @@ Module.register("currentweather",{
|
||||
firstEvent: false,
|
||||
|
||||
// create a variable to hold the location name based on the API result.
|
||||
fetchedLocatioName: "",
|
||||
fetchedLocationName: "",
|
||||
|
||||
// Define required scripts.
|
||||
getScripts: function() {
|
||||
@@ -198,16 +199,19 @@ Module.register("currentweather",{
|
||||
large.appendChild(weatherIcon);
|
||||
|
||||
var degreeLabel = "";
|
||||
if (this.config.degreeLabel) {
|
||||
switch (this.config.units ) {
|
||||
if (this.config.units === "metric" || this.config.units === "imperial") {
|
||||
degreeLabel += "°";
|
||||
}
|
||||
if(this.config.degreeLabel) {
|
||||
switch(this.config.units) {
|
||||
case "metric":
|
||||
degreeLabel = "C";
|
||||
degreeLabel += "C";
|
||||
break;
|
||||
case "imperial":
|
||||
degreeLabel = "F";
|
||||
degreeLabel += "F";
|
||||
break;
|
||||
case "default":
|
||||
degreeLabel = "K";
|
||||
degreeLabel += "K";
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -218,7 +222,7 @@ Module.register("currentweather",{
|
||||
|
||||
var temperature = document.createElement("span");
|
||||
temperature.className = "bright";
|
||||
temperature.innerHTML = " " + this.temperature.replace(".", this.config.decimalSymbol) + "°" + degreeLabel;
|
||||
temperature.innerHTML = " " + this.temperature.replace(".", this.config.decimalSymbol) + degreeLabel;
|
||||
large.appendChild(temperature);
|
||||
|
||||
if (this.config.showIndoorTemperature && this.indoorTemperature) {
|
||||
@@ -228,7 +232,7 @@ Module.register("currentweather",{
|
||||
|
||||
var indoorTemperatureElem = document.createElement("span");
|
||||
indoorTemperatureElem.className = "bright";
|
||||
indoorTemperatureElem.innerHTML = " " + this.indoorTemperature.replace(".", this.config.decimalSymbol) + "°" + degreeLabel;
|
||||
indoorTemperatureElem.innerHTML = " " + this.indoorTemperature.replace(".", this.config.decimalSymbol) + degreeLabel;
|
||||
large.appendChild(indoorTemperatureElem);
|
||||
}
|
||||
|
||||
@@ -251,7 +255,7 @@ Module.register("currentweather",{
|
||||
|
||||
var feelsLike = document.createElement("span");
|
||||
feelsLike.className = "dimmed";
|
||||
feelsLike.innerHTML = this.translate("FEELS") + " " + this.feelsLike + "°" + degreeLabel;
|
||||
feelsLike.innerHTML = this.translate("FEELS") + " " + this.feelsLike + degreeLabel;
|
||||
small.appendChild(feelsLike);
|
||||
|
||||
wrapper.appendChild(small);
|
||||
@@ -262,8 +266,12 @@ Module.register("currentweather",{
|
||||
|
||||
// Override getHeader method.
|
||||
getHeader: function() {
|
||||
if (this.config.appendLocationNameToHeader) {
|
||||
return this.data.header + " " + this.fetchedLocatioName;
|
||||
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;
|
||||
@@ -350,7 +358,7 @@ Module.register("currentweather",{
|
||||
} else if(this.config.location) {
|
||||
params += "q=" + this.config.location;
|
||||
} else if (this.firstEvent && this.firstEvent.geo) {
|
||||
params += "lat=" + this.firstEvent.geo.lat + "&lon=" + this.firstEvent.geo.lon
|
||||
params += "lat=" + this.firstEvent.geo.lat + "&lon=" + this.firstEvent.geo.lon;
|
||||
} else if (this.firstEvent && this.firstEvent.location) {
|
||||
params += "q=" + this.firstEvent.location;
|
||||
} else {
|
||||
@@ -380,6 +388,7 @@ Module.register("currentweather",{
|
||||
|
||||
this.humidity = parseFloat(data.main.humidity);
|
||||
this.temperature = this.roundValue(data.main.temp);
|
||||
this.fetchedLocationName = data.name;
|
||||
this.feelsLike = 0;
|
||||
|
||||
if (this.config.useBeaufort){
|
||||
|
BIN
modules/default/currentweather/weather_screenshot.png
Normal file
After Width: | Height: | Size: 38 KiB |
@@ -16,7 +16,8 @@ var defaultModules = [
|
||||
"helloworld",
|
||||
"newsfeed",
|
||||
"weatherforecast",
|
||||
"updatenotification"
|
||||
"updatenotification",
|
||||
"weather"
|
||||
];
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
|
@@ -15,10 +15,10 @@ Module.register("helloworld",{
|
||||
},
|
||||
|
||||
getTemplate: function () {
|
||||
return "helloworld.njk"
|
||||
return "helloworld.njk";
|
||||
},
|
||||
|
||||
getTemplateData: function () {
|
||||
return this.config
|
||||
return this.config;
|
||||
}
|
||||
});
|
||||
|
@@ -2,6 +2,10 @@
|
||||
The `newsfeed ` module is one of the default modules of the MagicMirror.
|
||||
This module displays news headlines based on an RSS feed. Scrolling through news headlines happens time-based (````updateInterval````), but can also be controlled by sending news feed specific notifications to the module.
|
||||
|
||||
## Screenshot
|
||||
- News Feed Screenshot using the NYT
|
||||

|
||||
|
||||
## Using the module
|
||||
|
||||
### Configuration
|
||||
@@ -41,7 +45,16 @@ MagicMirror's [notification mechanism](https://github.com/MichMich/MagicMirror/t
|
||||
| `ARTICLE_PREVIOUS` | Shows the previous news title (hiding the summary or previously fully displayed article)
|
||||
| `ARTICLE_MORE_DETAILS` | When received the _first time_, shows the corresponding description of the currently displayed news title. <br> The module expects that the module's configuration option `showDescription` is set to `false` (default value). <br><br> When received a _second consecutive time_, shows the full news article in an IFRAME. <br> This requires that the news page can be embedded in an IFRAME, e.g. doesn't have the HTTP response header [X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) set to e.g. `DENY`.<br><br>When received the _next consecutive times_, reloads the page and scrolls down by `scrollLength` pixels to paginate through the article.
|
||||
| `ARTICLE_LESS_DETAILS` | Hides the summary or full news article and only displays the news title of the currently viewed news item.
|
||||
| `ARTICLE_TOGGLE_FULL` | Toogles article in fullscreen.
|
||||
| `ARTICLE_TOGGLE_FULL` | Toggles article in fullscreen.
|
||||
| `ARTICLE_INFO_REQUEST` | Causes `newsfeed` to respond with the notification `ARTICLE_INFO_RESPONSE`, the payload of which provides the `title`, `source`, `date`, `desc` and `url` of the current news title.
|
||||
|
||||
#### Notifications sent by the module
|
||||
MagicMirror's [notification mechanism](https://github.com/MichMich/MagicMirror/tree/master/modules#thissendnotificationnotification-payload) can also be used to send notifications from the current module to all other modules. The following notifications are broadcasted from this module:
|
||||
|
||||
| Notification Identifier | Description
|
||||
| ----------------------- | -----------
|
||||
| `NEWS_FEED` | Broadcast the current list of news items.
|
||||
| `NEWS_FEED_UPDATE` | Broadcasts the list of updates news items.
|
||||
|
||||
Note the payload of the sent notification event is ignored.
|
||||
|
||||
@@ -63,6 +76,8 @@ The following properties can be configured:
|
||||
| `feeds` | An array of feed urls that will be used as source. <br> More info about this object can be found below. <br> **Default value:** `[{ title: "New York Times", url: "http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml", encoding: "UTF-8" }]`<br>You can add `reloadInterval` option to set particular reloadInterval to a feed.
|
||||
| `showSourceTitle` | Display the title of the source. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `showPublishDate` | Display the publish date of an headline. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `broadcastNewsFeeds` | Gives the ability to broadcast news feeds to all modules, by using ```sendNotification()``` when set to `true`, rather than ```sendSocketNotification()``` when `false` <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `broadcastNewsUpdates` | Gives the ability to broadcast news feed updates to all modules <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `showDescription` | Display the description of an item. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `wrapTitle` | Wrap the title of the item to multiple lines. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `wrapDescription` | Wrap the description of the item to multiple lines. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
@@ -75,7 +90,7 @@ The following properties can be configured:
|
||||
| `maxNewsItems` | Total amount of news items to cycle through. (0 for unlimited) <br><br> **Possible values:**`0` - `...` <br> **Default value:** `0`
|
||||
| `ignoreOldItems` | Ignore news items that are outdated. <br><br> **Possible values:**`true` or `false` <br> **Default value:** `false`
|
||||
| `ignoreOlderThan` | How old should news items be before they are considered outdated? (Milliseconds) <br><br> **Possible values:**`1` - `...` <br> **Default value:** `86400000` (1 day)
|
||||
| `removeStartTags` | Some newsfeeds feature tags at the **beginning** of their titles or descriptions, such as _[VIDEO]_. This setting allows for the removal of specified tags from the beginning of an item's description and/or title. <br><br> **Possible values:**`'title'`, `'description'`, `'both'`
|
||||
| `removeStartTags` | Some news feeds feature tags at the **beginning** of their titles or descriptions, such as _[VIDEO]_. This setting allows for the removal of specified tags from the beginning of an item's description and/or title. <br><br> **Possible values:**`'title'`, `'description'`, `'both'`
|
||||
| `startTags` | List the tags you would like to have removed at the beginning of the feed item <br><br> **Possible values:** `['TAG']` or `['TAG1','TAG2',...]`
|
||||
| `removeEndTags` | Remove specified tags from the **end** of an item's description and/or title. <br><br> **Possible values:**`'title'`, `'description'`, `'both'`
|
||||
| `endTags` | List the tags you would like to have removed at the end of the feed item <br><br> **Possible values:** `['TAG']` or `['TAG1','TAG2',...]`
|
||||
|
@@ -81,11 +81,10 @@ var Fetcher = function(url, reloadInterval, encoding, logFeedWarnings) {
|
||||
scheduleTimer();
|
||||
});
|
||||
|
||||
|
||||
nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
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"}
|
||||
"Pragma": "no-cache"};
|
||||
|
||||
request({uri: url, encoding: null, headers: headers})
|
||||
.on("error", function(error) {
|
||||
|
@@ -20,6 +20,8 @@ Module.register("newsfeed",{
|
||||
],
|
||||
showSourceTitle: true,
|
||||
showPublishDate: true,
|
||||
broadcastNewsFeeds: true,
|
||||
broadcastNewsUpdates: true,
|
||||
showDescription: false,
|
||||
wrapTitle: true,
|
||||
wrapDescription: true,
|
||||
@@ -103,7 +105,7 @@ Module.register("newsfeed",{
|
||||
// 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");
|
||||
sourceAndTimestamp.className = "light small dimmed";
|
||||
sourceAndTimestamp.className = "newsfeed-source light small dimmed";
|
||||
|
||||
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") {
|
||||
sourceAndTimestamp.innerHTML = this.newsItems[this.activeItem].sourceTitle;
|
||||
@@ -138,7 +140,7 @@ Module.register("newsfeed",{
|
||||
if (this.isShowingDescription) {
|
||||
for (f=0; f<this.config.startTags.length;f++) {
|
||||
if (this.newsItems[this.activeItem].description.slice(0,this.config.startTags[f].length) === this.config.startTags[f]) {
|
||||
this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].description.slice(this.config.startTags[f].length,this.newsItems[this.activeItem].description.length);
|
||||
this.newsItems[this.activeItem].description = this.newsItems[this.activeItem].description.slice(this.config.startTags[f].length,this.newsItems[this.activeItem].description.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,14 +168,14 @@ Module.register("newsfeed",{
|
||||
|
||||
if(!this.config.showFullArticle){
|
||||
var title = document.createElement("div");
|
||||
title.className = "bright medium light" + (!this.config.wrapTitle ? " no-wrap" : "");
|
||||
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");
|
||||
description.className = "small light" + (!this.config.wrapDescription ? " no-wrap" : "");
|
||||
description.className = "newsfeed-desc small light" + (!this.config.wrapDescription ? " no-wrap" : "");
|
||||
var 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);
|
||||
@@ -189,7 +191,7 @@ Module.register("newsfeed",{
|
||||
fullArticle.style.top = "0";
|
||||
fullArticle.style.left = "0";
|
||||
fullArticle.style.border = "none";
|
||||
fullArticle.src = typeof this.newsItems[this.activeItem].url === "string" ? this.newsItems[this.activeItem].url : this.newsItems[this.activeItem].url.href;
|
||||
fullArticle.src = this.getActiveItemURL();
|
||||
fullArticle.style.zIndex = 1;
|
||||
wrapper.appendChild(fullArticle);
|
||||
}
|
||||
@@ -210,6 +212,10 @@ Module.register("newsfeed",{
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
getActiveItemURL: function() {
|
||||
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.
|
||||
*/
|
||||
@@ -262,6 +268,20 @@ Module.register("newsfeed",{
|
||||
}, this);
|
||||
}
|
||||
|
||||
// get updated news items and broadcast them
|
||||
var updatedItems = [];
|
||||
newsItems.forEach(value => {
|
||||
if (this.newsItems.findIndex(value1 => value1 === value) === -1) {
|
||||
// Add item to updated items list
|
||||
updatedItems.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
// check if updated items exist, if so and if we should broadcast these updates, then lets do so
|
||||
if (this.config.broadcastNewsUpdates && updatedItems.length > 0) {
|
||||
this.sendNotification("NEWS_FEED_UPDATE", {items: updatedItems});
|
||||
}
|
||||
|
||||
this.newsItems = newsItems;
|
||||
},
|
||||
|
||||
@@ -307,9 +327,19 @@ Module.register("newsfeed",{
|
||||
|
||||
self.updateDom(self.config.animationSpeed);
|
||||
|
||||
// Broadcast NewsFeed if needed
|
||||
if (self.config.broadcastNewsFeeds) {
|
||||
self.sendNotification("NEWS_FEED", {items: self.newsItems});
|
||||
}
|
||||
|
||||
timer = setInterval(function() {
|
||||
self.activeItem++;
|
||||
self.updateDom(self.config.animationSpeed);
|
||||
|
||||
// Broadcast NewsFeed if needed
|
||||
if (self.config.broadcastNewsFeeds) {
|
||||
self.sendNotification("NEWS_FEED", {items: self.newsItems});
|
||||
}
|
||||
}, this.config.updateInterval);
|
||||
},
|
||||
|
||||
@@ -387,6 +417,14 @@ Module.register("newsfeed",{
|
||||
} else {
|
||||
this.showFullArticle();
|
||||
}
|
||||
} else if (notification === "ARTICLE_INFO_REQUEST"){
|
||||
this.sendNotification("ARTICLE_INFO_RESPONSE", {
|
||||
title: this.newsItems[this.activeItem].title,
|
||||
source: this.newsItems[this.activeItem].sourceTitle,
|
||||
date: this.newsItems[this.activeItem].pubdate,
|
||||
desc: this.newsItems[this.activeItem].description,
|
||||
url: this.getActiveItemURL()
|
||||
});
|
||||
} else {
|
||||
Log.info(this.name + " - unknown notification, ignoring: " + notification);
|
||||
}
|
||||
|
BIN
modules/default/newsfeed/newsfeed_screenshot.png
Normal file
After Width: | Height: | Size: 44 KiB |
@@ -10,11 +10,17 @@ module.exports = NodeHelper.create({
|
||||
config: {},
|
||||
|
||||
updateTimer: null,
|
||||
updateProcessStarted: false,
|
||||
|
||||
start: function () {
|
||||
},
|
||||
|
||||
configureModules: function(modules) {
|
||||
|
||||
// Push MagicMirror itself , biggest chance it'll show up last in UI and isn't overwritten
|
||||
// others will be added in front, asynchronously
|
||||
simpleGits.push({"module": "default", "git": SimpleGit(path.normalize(__dirname + "/../../../"))});
|
||||
|
||||
for (moduleName in modules) {
|
||||
if (defaultModules.indexOf(moduleName) < 0) {
|
||||
// Default modules are included in the main MagicMirror repo
|
||||
@@ -22,6 +28,7 @@ module.exports = NodeHelper.create({
|
||||
|
||||
var stat;
|
||||
try {
|
||||
//console.log("checking git for module="+moduleName)
|
||||
stat = fs.statSync(path.join(moduleFolder, ".git"));
|
||||
} catch(err) {
|
||||
// Error when directory .git doesn't exist
|
||||
@@ -36,37 +43,38 @@ module.exports = NodeHelper.create({
|
||||
// No valid remote for folder, skip
|
||||
return;
|
||||
}
|
||||
|
||||
// Folder has .git and has at least one git remote, watch this folder
|
||||
simpleGits.push({"module": mn, "git": git});
|
||||
simpleGits.unshift({"module": mn, "git": git});
|
||||
});
|
||||
}(moduleName, moduleFolder);
|
||||
}
|
||||
}
|
||||
|
||||
// Push MagicMirror itself last, biggest chance it'll show up last in UI and isn't overwritten
|
||||
simpleGits.push({"module": "default", "git": SimpleGit(path.normalize(__dirname + "/../../../"))});
|
||||
},
|
||||
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
if (notification === "CONFIG") {
|
||||
this.config = payload;
|
||||
} else if(notification === "MODULES") {
|
||||
this.configureModules(payload);
|
||||
this.preformFetch();
|
||||
// if this is the 1st time thru the update check process
|
||||
if(this.updateProcessStarted==false ){
|
||||
this.updateProcessStarted=true;
|
||||
this.configureModules(payload);
|
||||
this.preformFetch();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
preformFetch() {
|
||||
var self = this;
|
||||
|
||||
simpleGits.forEach(function(sg) {
|
||||
sg.git.fetch().status(function(err, data) {
|
||||
data.module = sg.module;
|
||||
if (!err) {
|
||||
sg.git.log({"-1": null}, function(err, data2) {
|
||||
data.hash = data2.latest.hash;
|
||||
self.sendSocketNotification("STATUS", data);
|
||||
if (!err && data2.latest && "hash" in data2.latest) {
|
||||
data.hash = data2.latest.hash;
|
||||
self.sendSocketNotification("STATUS", data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -77,7 +85,7 @@ module.exports = NodeHelper.create({
|
||||
|
||||
scheduleNextFetch: function(delay) {
|
||||
if (delay < 60 * 1000) {
|
||||
delay = 60 * 1000
|
||||
delay = 60 * 1000;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
@@ -2,41 +2,56 @@ Module.register("updatenotification", {
|
||||
|
||||
defaults: {
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
refreshInterval: 24 * 60 * 60 * 1000, // one day
|
||||
},
|
||||
|
||||
status: false,
|
||||
suspended: false,
|
||||
moduleList: {},
|
||||
|
||||
start: function () {
|
||||
var self = this;
|
||||
Log.log("Start updatenotification");
|
||||
|
||||
setInterval( () => { self.moduleList = {};self.updateDom(2); } , self.config.refreshInterval);
|
||||
},
|
||||
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
if (notification === "DOM_OBJECTS_CREATED") {
|
||||
this.sendSocketNotification("CONFIG", this.config);
|
||||
this.sendSocketNotification("MODULES", Module.definitions);
|
||||
this.hide(0, { lockString: self.identifier });
|
||||
//this.hide(0, { lockString: self.identifier });
|
||||
}
|
||||
},
|
||||
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
if (notification === "STATUS") {
|
||||
this.status = payload;
|
||||
this.updateUI();
|
||||
this.updateUI(payload);
|
||||
}
|
||||
},
|
||||
|
||||
updateUI: function () {
|
||||
updateUI: function (payload) {
|
||||
var self = this;
|
||||
if (this.status && this.status.behind > 0) {
|
||||
self.updateDom(0);
|
||||
self.show(1000, { lockString: self.identifier });
|
||||
if (payload && payload.behind > 0) {
|
||||
// if we haven't seen info for this module
|
||||
if(this.moduleList[payload.module] == undefined){
|
||||
// save it
|
||||
this.moduleList[payload.module]=payload;
|
||||
self.updateDom(2);
|
||||
}
|
||||
//self.show(1000, { lockString: self.identifier });
|
||||
|
||||
} else if (payload && payload.behind == 0){
|
||||
// if the module WAS in the list, but shouldn't be
|
||||
if(this.moduleList[payload.module] != undefined){
|
||||
// remove it
|
||||
delete this.moduleList[payload.module];
|
||||
self.updateDom(2);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
diffLink: function(text) {
|
||||
var localRef = this.status.hash;
|
||||
var remoteRef = this.status.tracking.replace(/.*\//, "");
|
||||
diffLink: function(module, text) {
|
||||
var localRef = module.hash;
|
||||
var remoteRef = module.tracking.replace(/.*\//, "");
|
||||
return "<a href=\"https://github.com/MichMich/MagicMirror/compare/"+localRef+"..."+remoteRef+"\" "+
|
||||
"class=\"xsmall dimmed\" "+
|
||||
"style=\"text-decoration: none;\" "+
|
||||
@@ -48,41 +63,53 @@ Module.register("updatenotification", {
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
var wrapper = document.createElement("div");
|
||||
if(this.suspended==false){
|
||||
// process the hash of module info found
|
||||
for(key of Object.keys(this.moduleList)){
|
||||
let m= this.moduleList[key];
|
||||
|
||||
if (this.status && this.status.behind > 0) {
|
||||
var message = document.createElement("div");
|
||||
message.className = "small bright";
|
||||
var message = document.createElement("div");
|
||||
message.className = "small bright";
|
||||
|
||||
var icon = document.createElement("i");
|
||||
icon.className = "fa fa-exclamation-circle";
|
||||
icon.innerHTML = " ";
|
||||
message.appendChild(icon);
|
||||
var icon = document.createElement("i");
|
||||
icon.className = "fa fa-exclamation-circle";
|
||||
icon.innerHTML = " ";
|
||||
message.appendChild(icon);
|
||||
|
||||
var updateInfoKeyName = this.status.behind == 1 ? "UPDATE_INFO_SINGLE" : "UPDATE_INFO_MULTIPLE";
|
||||
var subtextHtml = this.translate(updateInfoKeyName, {
|
||||
COMMIT_COUNT: this.status.behind,
|
||||
BRANCH_NAME: this.status.current
|
||||
});
|
||||
var updateInfoKeyName = m.behind == 1 ? "UPDATE_INFO_SINGLE" : "UPDATE_INFO_MULTIPLE";
|
||||
|
||||
var text = document.createElement("span");
|
||||
if (this.status.module == "default") {
|
||||
text.innerHTML = this.translate("UPDATE_NOTIFICATION");
|
||||
subtextHtml = this.diffLink(subtextHtml);
|
||||
} else {
|
||||
text.innerHTML = this.translate("UPDATE_NOTIFICATION_MODULE", {
|
||||
MODULE_NAME: this.status.module
|
||||
var subtextHtml = this.translate(updateInfoKeyName, {
|
||||
COMMIT_COUNT: m.behind,
|
||||
BRANCH_NAME: m.current
|
||||
});
|
||||
|
||||
var text = document.createElement("span");
|
||||
if (m.module == "default") {
|
||||
text.innerHTML = this.translate("UPDATE_NOTIFICATION");
|
||||
subtextHtml = this.diffLink(m,subtextHtml);
|
||||
} else {
|
||||
text.innerHTML = this.translate("UPDATE_NOTIFICATION_MODULE", {
|
||||
MODULE_NAME: m.module
|
||||
});
|
||||
}
|
||||
message.appendChild(text);
|
||||
|
||||
wrapper.appendChild(message);
|
||||
|
||||
var subtext = document.createElement("div");
|
||||
subtext.innerHTML = subtextHtml;
|
||||
subtext.className = "xsmall dimmed";
|
||||
wrapper.appendChild(subtext);
|
||||
}
|
||||
message.appendChild(text);
|
||||
|
||||
wrapper.appendChild(message);
|
||||
|
||||
var subtext = document.createElement("div");
|
||||
subtext.innerHTML = subtextHtml;
|
||||
subtext.className = "xsmall dimmed";
|
||||
wrapper.appendChild(subtext);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
suspend: function() {
|
||||
this.suspended=true;
|
||||
},
|
||||
resume: function() {
|
||||
this.suspended=false;
|
||||
this.updateDom(2);
|
||||
}
|
||||
});
|
||||
|
120
modules/default/weather/README.md
Executable file
@@ -0,0 +1,120 @@
|
||||
# Weather Module
|
||||
|
||||
This module is aimed 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.
|
||||
|
||||
The biggest change is the use of weather providers. This way we are not bound to one API source. And users can choose which API they want to use as their source.
|
||||
|
||||
The module is in a very early stage, and needs a lot of work. It's API isn't set in stone, so keep that in mind when you want to contribute.
|
||||
|
||||
## Example
|
||||
|
||||
 
|
||||
|
||||
## Usage
|
||||
|
||||
To use this module, add it to the modules array in the `config/config.js` file:
|
||||
|
||||
````javascript
|
||||
modules: [
|
||||
{
|
||||
module: "weather",
|
||||
position: "top_right",
|
||||
config: {
|
||||
// See 'Configuration options' for more information.
|
||||
type: 'current'
|
||||
}
|
||||
}
|
||||
]
|
||||
````
|
||||
|
||||
## Configuration options
|
||||
|
||||
The following properties can be configured:
|
||||
|
||||
### General options
|
||||
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `weatherProvider` | Which weather provider should be used. <br><br> **Possible values:** `openweathermap` , `darksky` , `weathergov` or `ukmetoffice`<br> **Default value:** `openweathermap`
|
||||
| `type` | Which type of weather data should be displayed. <br><br> **Possible values:** `current` or `forecast` <br> **Default value:** `current`
|
||||
| `units` | What units to use. Specified by config.js <br><br> **Possible values:** `config.units` = Specified by config.js, `default` = Kelvin, `metric` = Celsius, `imperial` = Fahrenheit <br> **Default value:** `config.units`
|
||||
| `tempUnits` | What units to use for temperature. If specified overrides `units` setting. Specified by config.js <br><br> **Possible values:** `config.units` = Specified by config.js, `default` = Kelvin, `metric` = Celsius, `imperial` = Fahrenheit <br> **Default value:** `units`
|
||||
| `windUnits` | What units to use for wind speed. If specified overrides `units` setting. Specified by config.js <br><br> **Possible values:** `config.units` = Specified by config.js, `default` = Kelvin, `metric` = Celsius, `imperial` = Fahrenheit <br> **Default value:** `units`
|
||||
| `roundTemp` | Round temperature value to nearest integer. <br><br> **Possible values:** `true` (round to integer) or `false` (display exact value with decimal point) <br> **Default value:** `false`
|
||||
| `degreeLabel` | Show the degree label for your chosen units (Metric = C, Imperial = F, Kelvin = K). <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `updateInterval` | How often does the content needs to be fetched? (Milliseconds) <br><br> **Possible values:** `1000` - `86400000` <br> **Default value:** `600000` (10 minutes)
|
||||
| `animationSpeed` | Speed of the update animation. (Milliseconds) <br><br> **Possible values:** `0` - `5000` <br> **Default value:** `1000` (1 second)
|
||||
| `timeFormat` | Use 12 or 24 hour format. <br><br> **Possible values:** `12` or `24` <br> **Default value:** uses value of _config.timeFormat_
|
||||
| `showPeriod` | Show the period (am/pm) with 12 hour format <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `showPeriodUpper` | Show the period (AM/PM) with 12 hour format as uppercase <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `lang` | The language of the days. <br><br> **Possible values:** `en`, `nl`, `ru`, etc ... <br> **Default value:** uses value of _config.language_
|
||||
| `decimalSymbol` | The decimal symbol to use.<br><br> **Possible values:** `.`, `,` or any other symbol.<br> **Default value:** `.`
|
||||
| `initialLoadDelay` | The initial delay before loading. If you have multiple modules that use the same API key, you might want to delay one of the requests. (Milliseconds) <br><br> **Possible values:** `1000` - `5000` <br> **Default value:** `0`
|
||||
| `appendLocationNameToHeader` | If set to `true`, the returned location name will be appended to the header of the module, if the header is enabled. This is mainly interesting when using calender based weather. <br><br> **Default value:** `true`
|
||||
| `calendarClass` | The class for the calender module to base the event based weather information on. <br><br> **Default value:** `'calendar'`
|
||||
|
||||
#### Current weather options
|
||||
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `onlyTemp` | Show only current Temperature and weather icon without windspeed, sunset, sunrise time and feels like. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `useBeaufort` | Pick between using the Beaufort scale for wind speed or using the default units. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `showWindDirection` | Show the wind direction next to the wind speed. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `showWindDirectionAsArrow` | Show the wind direction as an arrow instead of abbreviation <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `showHumidity` | Show the current humidity <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `showIndoorTemperature` | If you have another module that emits the `INDOOR_TEMPERATURE` notification, the indoor temperature will be displayed <br> **Default value:** `false`
|
||||
| `showIndoorHumidity` | If you have another module that emits the `INDOOR_HUMIDITY` notification, the indoor humidity will be displayed <br> **Default value:** `false`
|
||||
| `showFeelsLike` | Shows the Feels like temperature weather. <br><br> **Possible values:** `true` or `false`<br>**Default value:** `true`
|
||||
|
||||
#### Weather forecast options
|
||||
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `tableClass` | The class for the forecast table. <br><br> **Default value:** `'small'`
|
||||
| `colored` | If set to `true`, the min and max temperature are color coded. <br><br> **Default value:** `false`
|
||||
| `showPrecipitationAmount` | Show the amount of rain/snow in the forecast <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
|
||||
| `fade` | Fade the future events to black. (Gradient) <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
|
||||
| `fadePoint` | Where to start fade? <br><br> **Possible values:** `0` (top of the list) - `1` (bottom of list) <br> **Default value:** `0.25`
|
||||
| `maxNumberOfDays` | How many days of forecast to return. Specified by config.js <br><br> **Possible values:** `1` - `16` <br> **Default value:** `5` (5 days) <br> This value is optional. By default the weatherforecast module will return 5 days.
|
||||
|
||||
### Openweathermap options
|
||||
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `apiVersion` | The OpenWeatherMap API version to use. <br><br> **Default value:** `2.5`
|
||||
| `apiBase` | The OpenWeatherMap base URL. <br><br> **Default value:** `'http://api.openweathermap.org/data/'`
|
||||
| `weatherEndpoint` | The OpenWeatherMap API endPoint. <br><br> **Possible values:** `/weather`, `/forecast` (free users) or `/forecast/daily` (paying users or old apiKey only) <br> **Default value:** `'/weather'`
|
||||
| `locationID` | Location ID from [OpenWeatherMap](https://openweathermap.org/find) **This will override anything you put in location.** <br> Leave blank if you want to use location. <br> **Example:** `1234567` <br> **Default value:** `false` <br><br> **Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
|
||||
| `location` | The location used for weather information. <br><br> **Example:** `'Amsterdam,Netherlands'` <br> **Default value:** `false` <br><br> **Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
|
||||
| `apiKey` | The [OpenWeatherMap](https://home.openweathermap.org) API key, which can be obtained by creating an OpenWeatherMap account. <br><br> This value is **REQUIRED**
|
||||
|
||||
### Darksky options
|
||||
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `apiBase` | The DarkSky base URL. The darksky api has disabled [cors](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), therefore a proxy is required. <br><br> **Possible value:** `'https://cors-anywhere.herokuapp.com/https://api.darksky.net'` <br> This value is **REQUIRED**
|
||||
| `weatherEndpoint` | The DarkSky API endPoint. <br><br> **Possible values:** `/forecast` <br> This value is **REQUIRED**
|
||||
| `apiKey` | The [DarkSky](https://darksky.net/dev/register) API key, which can be obtained by creating an DarkSky account. <br><br> This value is **REQUIRED**
|
||||
| `lat` | The geo coordinate latitude. <br><br> This value is **REQUIRED**
|
||||
| `lon` | The geo coordinate longitude. <br><br> This value is **REQUIRED**
|
||||
|
||||
### Weather.gov options
|
||||
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `apiBase` | The weather.gov base URL. <br><br> **Possible value:** `'https://api.weather.gov/points/'` <br> This value is **REQUIRED**
|
||||
| `weatherEndpoint` | The weather.gov API endPoint. <br><br> **Possible values:** `/forecast` for forecast and `/forecast/hourly` for current. <br> This value is **REQUIRED**
|
||||
| `lat` | The geo coordinate latitude. <br><br> This value is **REQUIRED**
|
||||
| `lon` | The geo coordinate longitude. <br><br> This value is **REQUIRED**
|
||||
|
||||
### UK Met Office options
|
||||
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `apiBase` | The UKMO base URL. <br><br> **Possible value:** `'http://datapoint.metoffice.gov.uk/public/data/val/wxfcs/all/json/'` <br> This value is **REQUIRED**
|
||||
| `locationId` | The UKMO API location code. <br><br> **Possible values:** `322942` <br> This value is **REQUIRED**
|
||||
| `apiKey` | The [UK Met Office](https://www.metoffice.gov.uk/datapoint/getting-started) API key, which can be obtained by creating an UKMO Datapoint account. <br><br> This value is **REQUIRED**
|
||||
|
||||
## API Provider Development
|
||||
|
||||
If you want to add another API provider checkout the [Guide](providers).
|
80
modules/default/weather/current.njk
Executable file
@@ -0,0 +1,80 @@
|
||||
{% if current %}
|
||||
{% if not config.onlyTemp %}
|
||||
<div class="normal medium">
|
||||
<span class="wi wi-strong-wind dimmed"></span>
|
||||
<span>
|
||||
{% if config.useBeaufort %}
|
||||
{{ current.beaufortWindSpeed() | round }}
|
||||
{% else %}
|
||||
{{ current.windSpeed | round }}
|
||||
{% endif %}
|
||||
{% if config.showWindDirection %}
|
||||
<sup>
|
||||
{% if config.showWindDirectionAsArrow %}
|
||||
<i class="fa fa-long-arrow-up" style="transform:rotate({{ current.windDirection }}deg);"></i>
|
||||
{% else %}
|
||||
{{ current.cardinalWindDirection() | translate }}
|
||||
{% endif %}
|
||||
|
||||
</sup>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if config.showHumidity and current.humidity %}
|
||||
<span>{{ current.humidity | decimalSymbol }}</span><sup> <i class="wi wi-humidity humidityIcon"></i></sup>
|
||||
{% endif %}
|
||||
<span class="wi dimmed wi-{{ current.nextSunAction() }}"></span>
|
||||
<span>
|
||||
{% if current.nextSunAction() === "sunset" %}
|
||||
{{ current.sunset | formatTime }}
|
||||
{% else %}
|
||||
{{ current.sunrise | formatTime }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="large light">
|
||||
<span class="wi weathericon wi-{{current.weatherType}}"></span>
|
||||
<span class="bright">
|
||||
{{ current.temperature | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="normal light indoor">
|
||||
{% if config.showIndoorTemperature and indoor.temperature %}
|
||||
<div>
|
||||
<span class="fa fa-home"></span>
|
||||
<span class="bright">
|
||||
{{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if config.showIndoorHumidity and indoor.humidity %}
|
||||
<div>
|
||||
<span class="fa fa-tint"></span>
|
||||
<span class="bright">
|
||||
{{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if (config.showFeelsLike or config.showPrecipitationAmount) and not config.onlyTemp %}
|
||||
<div class="normal medium">
|
||||
{% if config.showFeelsLike %}
|
||||
<span class="dimmed">
|
||||
{{ "FEELS" | translate }} {{ current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationAmount %}
|
||||
<span class="dimmed">
|
||||
{{ "PRECIP" | translate }} {{ current.precipitation | unit("precip") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="dimmed light small">
|
||||
{{ "LOADING" | translate | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Uncomment the line below to see the contents of the `current` object. -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{current | dump}}</div> -->
|
BIN
modules/default/weather/current.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
32
modules/default/weather/forecast.njk
Normal file
@@ -0,0 +1,32 @@
|
||||
{% if forecast %}
|
||||
{% set numSteps = forecast | calcNumSteps %}
|
||||
{% 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>
|
||||
<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") }}
|
||||
</td>
|
||||
<td class="align-right min-temp">
|
||||
{{ f.minTemperature | roundValue | unit("temperature") }}
|
||||
</td>
|
||||
{% if config.showPrecipitationAmount %}
|
||||
<td class="align-right bright precipitation">
|
||||
{{ f.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 `forecast` object. -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{forecast | dump}}</div> -->
|
BIN
modules/default/weather/forecast.png
Normal file
After Width: | Height: | Size: 16 KiB |
133
modules/default/weather/providers/README.md
Executable file
@@ -0,0 +1,133 @@
|
||||
# MagicMirror² Weather Module Weather Provider Development Documentation
|
||||
|
||||
This document describes the way to develop your own MagicMirror² weather module weather provider.
|
||||
|
||||
Table of Contents:
|
||||
|
||||
- The weather provider file: yourprovider.js
|
||||
- [Weather provider methods to implement](#weather-provider-methods-to-implement)
|
||||
- [Weather Provider instance methods](#weather-provider-instance-methods)
|
||||
- [WeatherObject](#weatherobject)
|
||||
|
||||
---
|
||||
|
||||
## The weather provider file: yourprovider.js
|
||||
|
||||
This is the script in which the weather provider will be defined. In it's most simple form, the weather provider must implement the following:
|
||||
|
||||
````javascript
|
||||
WeatherProvider.register("yourprovider", {
|
||||
providerName: "YourProvider",
|
||||
|
||||
fetchCurrentWeather() {},
|
||||
|
||||
fetchWeatherForecast() {}
|
||||
});
|
||||
````
|
||||
|
||||
### Weather provider methods to implement
|
||||
|
||||
#### `fetchCurrentWeather()`
|
||||
|
||||
This method is called when the weather module tries to fetch the current weather of your provider. The implementation of this method is required.
|
||||
The implementation can make use of the already implemented function `this.fetchData(url, method, data);`, which is returning a promise.
|
||||
After the response is processed, the current weather information (as a [WeatherObject](#weatherobject)) needs to be set with `this.setCurrentWeather(currentWeather);`.
|
||||
It will then automatically refresh the module DOM with the new data.
|
||||
|
||||
#### `fetchWeatherForecast()`
|
||||
|
||||
This method is called when the weather module tries to fetch the weather weather of your provider. The implementation of this method is required.
|
||||
The implementation can make use of the already implemented function `this.fetchData(url, method, data);`, which is returning a promise.
|
||||
After the response is processed, the weather forecast information (as an array of [WeatherObject](#weatherobject)s) needs to be set with `this.setCurrentWeather(forecast);`.
|
||||
It will then automatically refresh the module DOM with the new data.
|
||||
|
||||
### Weather Provider instance methods
|
||||
|
||||
#### `init()`
|
||||
|
||||
Called when a weather provider is initialized.
|
||||
|
||||
#### `setConfig(config)`
|
||||
|
||||
Called to set the config, this config is the same as the weather module's config.
|
||||
|
||||
#### `start()`
|
||||
|
||||
Called when the weather provider is about to start.
|
||||
|
||||
#### `currentWeather()`
|
||||
|
||||
This returns a WeatherDay object for the current weather.
|
||||
|
||||
#### `weatherForecast()`
|
||||
|
||||
This returns an array of WeatherDay objects for the weather forecast.
|
||||
|
||||
#### `fetchedLocation()`
|
||||
|
||||
This returns the name of the fetched location or an empty string.
|
||||
|
||||
#### `setCurrentWeather(currentWeatherObject)`
|
||||
|
||||
Set the currentWeather and notify the delegate that new information is available.
|
||||
|
||||
#### `setWeatherForecast(weatherForecastArray)`
|
||||
|
||||
Set the weatherForecastArray and notify the delegate that new information is available.
|
||||
|
||||
#### `setFetchedLocation(name)`
|
||||
|
||||
Set the fetched location name.
|
||||
|
||||
#### `updateAvailable()`
|
||||
|
||||
Notify the delegate that new weather is available.
|
||||
|
||||
#### `fetchData(url, method, data)`
|
||||
|
||||
A convenience function to make requests. It returns a promise.
|
||||
|
||||
### WeatherObject
|
||||
|
||||
| Property | Type | Value/Unit |
|
||||
| --- | --- | --- |
|
||||
| units | `string` | Gets initialized with the constructor. <br> Possible values: `metric`, `imperial` |
|
||||
| tempUnits | `string` | Gets initialized with the constructor. <br> Possible values: `metric`, `imperial` |
|
||||
| windUnits | `string` | Gets initialized with the constructor. <br> Possible values: `metric`, `imperial` |
|
||||
| date | `object` | [Moment.js](https://momentjs.com/) object of the time/date. |
|
||||
| windSpeed |`number` | Metric: `meter/second` <br> Imperial: `miles/hour` |
|
||||
| windDirection |`number` | Direction of the wind in degrees. |
|
||||
| sunrise |`object` | [Moment.js](https://momentjs.com/) object of sunrise. |
|
||||
| sunset |`object` | [Moment.js](https://momentjs.com/) object of sunset. |
|
||||
| temperature | `number` | Current temperature |
|
||||
| minTemperature | `number` | Lowest temperature of the day. |
|
||||
| maxTemperature | `number` | Highest temperature of the day. |
|
||||
| weatherType | `string` | Icon name of the weather type. <br> Possible values: [WeatherIcons](https://www.npmjs.com/package/weathericons) |
|
||||
| humidity | `number` | Percentage of humidity |
|
||||
| rain | `number` | Metric: `millimeters` <br> Imperial: `inches` |
|
||||
| snow | `number` | Metric: `millimeters` <br> Imperial: `inches` |
|
||||
| precipitation | `number` | Metric: `millimeters` <br> Imperial: `inches` <br> UK Met Office provider: `percent` |
|
||||
|
||||
#### Current weather
|
||||
|
||||
For the current weather object the following properties are required:
|
||||
|
||||
- humidity
|
||||
- sunrise
|
||||
- sunset
|
||||
- temperature
|
||||
- units
|
||||
- weatherType
|
||||
- windDirection
|
||||
- windSpeed
|
||||
|
||||
#### Weather forecast
|
||||
|
||||
For the forecast weather object the following properties are required:
|
||||
|
||||
- date
|
||||
- maxTemperature
|
||||
- minTemperature
|
||||
- rain
|
||||
- units
|
||||
- weatherType
|
124
modules/default/weather/providers/darksky.js
Executable file
@@ -0,0 +1,124 @@
|
||||
/* global WeatherProvider, WeatherDay */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
* Provider: Dark Sky
|
||||
*
|
||||
* By Nicholas Hubbard https://github.com/nhubbard
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Dark Sky.
|
||||
* Note that the Dark Sky API does not provide rainfall. Instead it provides snowfall and precipitation probability
|
||||
*/
|
||||
WeatherProvider.register("darksky", {
|
||||
// Set the name of the provider.
|
||||
// Not strictly required, but helps for debugging.
|
||||
providerName: "Dark Sky",
|
||||
|
||||
units: {
|
||||
imperial: 'us',
|
||||
metric: 'si'
|
||||
},
|
||||
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then(data => {
|
||||
if(!data || !data.currently || typeof data.currently.temperature === "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.daily || !data.daily.data.length) {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data.daily.data);
|
||||
this.setWeatherForecast(forecast);
|
||||
}).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}/${this.config.apiKey}/${this.config.lat},${this.config.lon}?units=${units}&lang=${this.config.lang}`;
|
||||
},
|
||||
|
||||
// Implement WeatherDay generator.
|
||||
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
currentWeather.date = moment();
|
||||
currentWeather.humidity = parseFloat(currentWeatherData.currently.humidity);
|
||||
currentWeather.temperature = parseFloat(currentWeatherData.currently.temperature);
|
||||
currentWeather.windSpeed = parseFloat(currentWeatherData.currently.windSpeed);
|
||||
currentWeather.windDirection = currentWeatherData.currently.windBearing;
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.currently.icon);
|
||||
currentWeather.sunrise = moment(currentWeatherData.daily.data[0].sunriseTime, "X");
|
||||
currentWeather.sunset = moment(currentWeatherData.daily.data[0].sunsetTime, "X");
|
||||
|
||||
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.time, "X");
|
||||
weather.minTemperature = forecast.temperatureMin;
|
||||
weather.maxTemperature = forecast.temperatureMax;
|
||||
weather.weatherType = this.convertWeatherType(forecast.icon);
|
||||
weather.snow = 0;
|
||||
|
||||
// The API will return centimeters if units is 'si' and will return inches for 'us'
|
||||
// Note that the Dark Sky API does not provide rainfall. Instead it provides snowfall and precipitation probability
|
||||
if (forecast.hasOwnProperty("precipAccumulation")) {
|
||||
if (this.config.units === "imperial" && !isNaN(forecast.precipAccumulation)) {
|
||||
weather.snow = forecast.precipAccumulation;
|
||||
} else if (!isNaN(forecast.precipAccumulation)) {
|
||||
weather.snow = forecast.precipAccumulation * 10;
|
||||
}
|
||||
}
|
||||
|
||||
weather.precipitation = weather.snow;
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
// Map icons from Dark Sky to our icons.
|
||||
convertWeatherType(weatherType) {
|
||||
const weatherTypes = {
|
||||
"clear-day": "day-sunny",
|
||||
"clear-night": "night-clear",
|
||||
"rain": "rain",
|
||||
"snow": "snow",
|
||||
"sleet": "snow",
|
||||
"wind": "wind",
|
||||
"fog": "fog",
|
||||
"cloudy": "cloudy",
|
||||
"partly-cloudy-day": "day-cloudy",
|
||||
"partly-cloudy-night": "night-cloudy"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
}
|
||||
});
|
283
modules/default/weather/providers/openweathermap.js
Executable file
@@ -0,0 +1,283 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is the blueprint for a weather provider.
|
||||
*/
|
||||
|
||||
WeatherProvider.register("openweathermap", {
|
||||
|
||||
// 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: "OpenWeatherMap",
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then(data => {
|
||||
if (!data || !data.main || typeof data.main.temp === "undefined") {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
this.setFetchedLocation(`${data.name}, ${data.sys.country}`);
|
||||
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function(request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable())
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then(data => {
|
||||
if (!data || !data.list || !data.list.length) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
this.setFetchedLocation(`${data.city.name}, ${data.city.country}`);
|
||||
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data.list);
|
||||
this.setWeatherForecast(forecast);
|
||||
})
|
||||
.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
|
||||
*/
|
||||
getUrl() {
|
||||
return this.config.apiBase + this.config.apiVersion + this.config.weatherEndpoint + this.getParams();
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
currentWeather.humidity = currentWeatherData.main.humidity;
|
||||
currentWeather.temperature = currentWeatherData.main.temp;
|
||||
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");
|
||||
currentWeather.sunset = moment(currentWeatherData.sys.sunset, "X");
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate WeatherObjects based on forecast information
|
||||
*/
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
|
||||
if (this.config.weatherEndpoint === "/forecast") {
|
||||
return this.fetchForecastHourly(forecasts);
|
||||
} else if (this.config.weatherEndpoint === "/forecast/daily") {
|
||||
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)];
|
||||
return days;
|
||||
},
|
||||
|
||||
/*
|
||||
* fetch forecast information for 3-hourly forecast (available for free subscription).
|
||||
*/
|
||||
fetchForecastHourly(forecasts) {
|
||||
// initial variable declaration
|
||||
const days = [];
|
||||
// variables for temperature range and rain
|
||||
let minTemp = [];
|
||||
let maxTemp = [];
|
||||
let rain = 0;
|
||||
let snow = 0;
|
||||
// variable for date
|
||||
let date = "";
|
||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
|
||||
if (date !== moment(forecast.dt, "X").format("YYYY-MM-DD")) {
|
||||
// calculate minimum/maximum temperature, specify rain amount
|
||||
weather.minTemperature = Math.min.apply(null, minTemp);
|
||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||
weather.rain = rain;
|
||||
weather.snow = snow;
|
||||
weather.precipitation = weather.rain + weather.snow;
|
||||
// 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);
|
||||
|
||||
minTemp = [];
|
||||
maxTemp = [];
|
||||
rain = 0;
|
||||
snow = 0;
|
||||
|
||||
// set new date
|
||||
date = moment(forecast.dt, "X").format("YYYY-MM-DD");
|
||||
|
||||
// specify date
|
||||
weather.date = moment(forecast.dt, "X");
|
||||
|
||||
// If the first value of today is later than 17:00, we have an icon at least!
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||
|
||||
}
|
||||
|
||||
if (moment(forecast.dt, "X").format("H") >= 8 && moment(forecast.dt, "X").format("H") <= 17) {
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||
}
|
||||
|
||||
// the same day as before
|
||||
// add values from forecast to corresponding variables
|
||||
minTemp.push(forecast.main.temp_min);
|
||||
maxTemp.push(forecast.main.temp_max);
|
||||
|
||||
if (forecast.hasOwnProperty("rain")) {
|
||||
if (this.config.units === "imperial" && !isNaN(forecast.rain["3h"])) {
|
||||
rain += forecast.rain["3h"] / 25.4;
|
||||
} else if (!isNaN(forecast.rain["3h"])) {
|
||||
rain += forecast.rain["3h"];
|
||||
}
|
||||
}
|
||||
|
||||
if (forecast.hasOwnProperty("snow")) {
|
||||
if (this.config.units === "imperial" && !isNaN(forecast.snow["3h"])) {
|
||||
snow += forecast.snow["3h"] / 25.4;
|
||||
} else if (!isNaN(forecast.snow["3h"])) {
|
||||
snow += forecast.snow["3h"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// last day
|
||||
// calculate minimum/maximum temperature, specify rain amount
|
||||
weather.minTemperature = Math.min.apply(null, minTemp);
|
||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||
weather.rain = rain;
|
||||
weather.snow = snow;
|
||||
weather.precipitation = weather.rain + weather.snow;
|
||||
// push weather information to days array
|
||||
days.push(weather);
|
||||
return days.slice(1);
|
||||
},
|
||||
|
||||
/*
|
||||
* fetch forecast information for daily forecast (available for paid subscription or old apiKey).
|
||||
*/
|
||||
fetchForecastDaily(forecasts) {
|
||||
// initial variable declaration
|
||||
const days = [];
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
weather.date = moment(forecast.dt, "X");
|
||||
weather.minTemperature = forecast.temp.min;
|
||||
weather.maxTemperature = forecast.temp.max;
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||
weather.rain = 0;
|
||||
weather.snow = 0;
|
||||
|
||||
// forecast.rain not available if amount is zero
|
||||
// The API always returns in millimeters
|
||||
if (forecast.hasOwnProperty("rain")) {
|
||||
if (this.config.units === "imperial" && !isNaN(forecast.rain)) {
|
||||
weather.rain = forecast.rain / 25.4;
|
||||
} else if (!isNaN(forecast.rain)) {
|
||||
weather.rain = forecast.rain;
|
||||
}
|
||||
}
|
||||
|
||||
// forecast.snow not available if amount is zero
|
||||
// The API always returns in millimeters
|
||||
if (forecast.hasOwnProperty("snow")) {
|
||||
if (this.config.units === "imperial" && !isNaN(forecast.snow)) {
|
||||
weather.snow = forecast.snow / 25.4;
|
||||
} else if (!isNaN(forecast.snow)) {
|
||||
weather.snow = forecast.snow;
|
||||
}
|
||||
}
|
||||
|
||||
weather.precipitation = weather.rain + weather.snow;
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert the OpenWeatherMap icons to a more usable name.
|
||||
*/
|
||||
convertWeatherType(weatherType) {
|
||||
const weatherTypes = {
|
||||
"01d": "day-sunny",
|
||||
"02d": "day-cloudy",
|
||||
"03d": "cloudy",
|
||||
"04d": "cloudy-windy",
|
||||
"09d": "showers",
|
||||
"10d": "rain",
|
||||
"11d": "thunderstorm",
|
||||
"13d": "snow",
|
||||
"50d": "fog",
|
||||
"01n": "night-clear",
|
||||
"02n": "night-cloudy",
|
||||
"03n": "night-cloudy",
|
||||
"04n": "night-cloudy",
|
||||
"09n": "night-showers",
|
||||
"10n": "night-rain",
|
||||
"11n": "night-thunderstorm",
|
||||
"13n": "night-snow",
|
||||
"50n": "night-alt-cloudy-windy"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
},
|
||||
|
||||
/* getParams(compliments)
|
||||
* Generates an url with api parameters based on the config.
|
||||
*
|
||||
* return String - URL params.
|
||||
*/
|
||||
getParams() {
|
||||
let params = "?";
|
||||
if(this.config.locationID) {
|
||||
params += "id=" + this.config.locationID;
|
||||
} else if(this.config.location) {
|
||||
params += "q=" + this.config.location;
|
||||
} else if (this.firstEvent && this.firstEvent.geo) {
|
||||
params += "lat=" + this.firstEvent.geo.lat + "&lon=" + this.firstEvent.geo.lon;
|
||||
} else if (this.firstEvent && this.firstEvent.location) {
|
||||
params += "q=" + this.firstEvent.location;
|
||||
} else {
|
||||
this.hide(this.config.animationSpeed, {lockString:this.identifier});
|
||||
return;
|
||||
}
|
||||
|
||||
params += "&units=" + this.config.units;
|
||||
params += "&lang=" + this.config.lang;
|
||||
params += "&APPID=" + this.config.apiKey;
|
||||
|
||||
return params;
|
||||
}
|
||||
});
|
264
modules/default/weather/providers/ukmetoffice.js
Executable file
@@ -0,0 +1,264 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
*
|
||||
* By Malcolm Oakes https://github.com/maloakes
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is a provider for UK Met Office Datapoint.
|
||||
*/
|
||||
|
||||
|
||||
WeatherProvider.register("ukmetoffice", {
|
||||
|
||||
// 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: "UK Met Office",
|
||||
|
||||
units: {
|
||||
imperial: "us",
|
||||
metric: "si"
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl("3hourly"))
|
||||
.then(data => {
|
||||
if (!data || !data.SiteRep || !data.SiteRep.DV || !data.SiteRep.DV.Location ||
|
||||
!data.SiteRep.DV.Location.Period || data.SiteRep.DV.Location.Period.length == 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
this.setFetchedLocation(`${data.SiteRep.DV.Location.name}, ${data.SiteRep.DV.Location.country}`);
|
||||
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function(request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable())
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl("daily"))
|
||||
.then(data => {
|
||||
if (!data || !data.SiteRep || !data.SiteRep.DV || !data.SiteRep.DV.Location ||
|
||||
!data.SiteRep.DV.Location.Period || data.SiteRep.DV.Location.Period.length == 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
this.setFetchedLocation(`${data.SiteRep.DV.Location.name}, ${data.SiteRep.DV.Location.country}`);
|
||||
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data);
|
||||
this.setWeatherForecast(forecast);
|
||||
})
|
||||
.catch(function(request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable())
|
||||
},
|
||||
|
||||
|
||||
|
||||
/** UK Met Office Specific Methods - These are not part of the default provider methods */
|
||||
/*
|
||||
* Gets the complete url for the request
|
||||
*/
|
||||
getUrl(forecastType) {
|
||||
return this.config.apiBase + this.config.locationID + this.getParams(forecastType);
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
// data times are always UTC
|
||||
let nowUtc = moment.utc()
|
||||
let midnightUtc = nowUtc.clone().startOf("day")
|
||||
let timeInMins = nowUtc.diff(midnightUtc, "minutes");
|
||||
|
||||
// loop round each of the (5) periods, look for today (the first period may be yesterday)
|
||||
for (i in currentWeatherData.SiteRep.DV.Location.Period) {
|
||||
let periodDate = moment.utc(currentWeatherData.SiteRep.DV.Location.Period[i].value.substr(0,10), "YYYY-MM-DD")
|
||||
|
||||
// ignore if period is before today
|
||||
if (periodDate.isSameOrAfter(moment.utc().startOf("day"))) {
|
||||
|
||||
// check this is the period we want, after today the diff will be -ve
|
||||
if (moment().diff(periodDate, "minutes") > 0) {
|
||||
// loop round the reports looking for the one we are in
|
||||
// $ value specifies the time in minutes-of-the-day: 0, 180, 360,...1260
|
||||
for (j in currentWeatherData.SiteRep.DV.Location.Period[i].Rep){
|
||||
let p = currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].$;
|
||||
if (timeInMins >= p && timeInMins-180 < p) {
|
||||
// finally got the one we want, so populate weather object
|
||||
currentWeather.humidity = currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].H;
|
||||
currentWeather.temperature = this.convertTemp(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].T);
|
||||
currentWeather.feelsLikeTemp = this.convertTemp(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].F);
|
||||
currentWeather.precipitation = parseInt(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].Pp);
|
||||
currentWeather.windSpeed = this.convertWindSpeed(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].S);
|
||||
currentWeather.windDirection = this.convertWindDirection(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].D);
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].W);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// determine the sunrise/sunset times - not supplied in UK Met Office data
|
||||
let times = this.calcAstroData(currentWeatherData.SiteRep.DV.Location)
|
||||
currentWeather.sunrise = times[0];
|
||||
currentWeather.sunset = times[1];
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate WeatherObjects based on forecast information
|
||||
*/
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
|
||||
const days = [];
|
||||
|
||||
// loop round the (5) periods getting the data
|
||||
// for each period array, Day is [0], Night is [1]
|
||||
for (j in forecasts.SiteRep.DV.Location.Period) {
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
// data times are always UTC
|
||||
dateStr = forecasts.SiteRep.DV.Location.Period[j].value
|
||||
let periodDate = moment.utc(dateStr.substr(0,10), "YYYY-MM-DD")
|
||||
|
||||
// ignore if period is before today
|
||||
if (periodDate.isSameOrAfter(moment.utc().startOf("day"))) {
|
||||
// populate the weather object
|
||||
weather.date = moment.utc(dateStr.substr(0,10), "YYYY-MM-DD");
|
||||
weather.minTemperature = this.convertTemp(forecasts.SiteRep.DV.Location.Period[j].Rep[1].Nm);
|
||||
weather.maxTemperature = this.convertTemp(forecasts.SiteRep.DV.Location.Period[j].Rep[0].Dm);
|
||||
weather.weatherType = this.convertWeatherType(forecasts.SiteRep.DV.Location.Period[j].Rep[0].W);
|
||||
weather.precipitation = parseInt(forecasts.SiteRep.DV.Location.Period[j].Rep[0].PPd);
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
}
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
/*
|
||||
* calculate the astronomical data
|
||||
*/
|
||||
calcAstroData(location) {
|
||||
const sunTimes = [];
|
||||
|
||||
// determine the sunrise/sunset times
|
||||
let times = SunCalc.getTimes(new Date(), location.lat, location.lon);
|
||||
sunTimes.push(moment(times.sunrise, "X"));
|
||||
sunTimes.push(moment(times.sunset, "X"));
|
||||
|
||||
return sunTimes;
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert the Met Office icons to a more usable name.
|
||||
*/
|
||||
convertWeatherType(weatherType) {
|
||||
const weatherTypes = {
|
||||
0: "night-clear",
|
||||
1: "day-sunny",
|
||||
2: "night-alt-cloudy",
|
||||
3: "day-cloudy",
|
||||
5: "fog",
|
||||
6: "fog",
|
||||
7: "cloudy",
|
||||
8: "cloud",
|
||||
9: "night-sprinkle",
|
||||
10: "day-sprinkle",
|
||||
11: "raindrops",
|
||||
12: "sprinkle",
|
||||
13: "night-alt-showers",
|
||||
14: "day-showers",
|
||||
15: "rain",
|
||||
16: "night-alt-sleet",
|
||||
17: "day-sleet",
|
||||
18: "sleet",
|
||||
19: "night-alt-hail",
|
||||
20: "day-hail",
|
||||
21: "hail",
|
||||
22: "night-alt-snow",
|
||||
23: "day-snow",
|
||||
24: "snow",
|
||||
25: "night-alt-snow",
|
||||
26: "day-snow",
|
||||
27: "snow",
|
||||
28: "night-alt-thunderstorm",
|
||||
29: "day-thunderstorm",
|
||||
30: "thunderstorm"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert temp (from degrees C) if required
|
||||
*/
|
||||
convertTemp(tempInC) {
|
||||
return this.tempUnits === "imperial" ? tempInC * 9 / 5 + 32 : tempInC;
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert wind speed (from mph) if required
|
||||
*/
|
||||
convertWindSpeed(windInMph) {
|
||||
return this.windUnits === "metric" ? windInMph * 2.23694 : windInMph;
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert the wind direction cardinal to value
|
||||
*/
|
||||
convertWindDirection(windDirection) {
|
||||
const windCardinals = {
|
||||
"N": 0,
|
||||
"NNE": 22,
|
||||
"NE": 45,
|
||||
"ENE": 67,
|
||||
"E": 90,
|
||||
"ESE": 112,
|
||||
"SE": 135,
|
||||
"SSE": 157,
|
||||
"S": 180,
|
||||
"SSW": 202,
|
||||
"SW": 225,
|
||||
"WSW": 247,
|
||||
"W": 270,
|
||||
"WNW": 292,
|
||||
"NW": 315,
|
||||
"NNW": 337
|
||||
};
|
||||
|
||||
return windCardinals.hasOwnProperty(windDirection) ? windCardinals[windDirection] : null;
|
||||
},
|
||||
|
||||
/*
|
||||
* Generates an url with api parameters based on the config.
|
||||
*
|
||||
* return String - URL params.
|
||||
*/
|
||||
getParams(forecastType) {
|
||||
let params = "?";
|
||||
params += "res=" + forecastType;
|
||||
params += "&key=" + this.config.apiKey;
|
||||
|
||||
return params;
|
||||
}
|
||||
});
|
264
modules/default/weather/providers/weathergov.js
Executable file
@@ -0,0 +1,264 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
* Provider: weather.gov
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
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",
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.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 currentWeather = this.generateWeatherObjectFromCurrentWeather(data.properties.periods[0]);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function(request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable())
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.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);
|
||||
})
|
||||
.finally(() => this.updateAvailable())
|
||||
},
|
||||
|
||||
/** Weather.gov Specific Methods - These are not part of the default provider methods */
|
||||
/*
|
||||
* Gets the complete url for the request
|
||||
*/
|
||||
getUrl() {
|
||||
return this.config.apiBase + this.config.lat + "," + this.config.lon + this.config.weatherEndpoint;
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
currentWeather.temperature = currentWeatherData.temperature;
|
||||
currentWeather.windSpeed = currentWeatherData.windSpeed.split(" ", 1);
|
||||
currentWeather.windDirection = this.convertWindDirection(currentWeatherData.windDirection);
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.shortForecast, currentWeatherData.isDaytime);
|
||||
|
||||
// determine the sunrise/sunset times - not supplied in weather.gov data
|
||||
let times = this.calcAstroData(this.config.lat, this.config.lon)
|
||||
currentWeather.sunrise = times[0];
|
||||
currentWeather.sunset = times[1];
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate WeatherObjects based on forecast information
|
||||
*/
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
return this.fetchForecastDaily(forecasts);
|
||||
},
|
||||
|
||||
/*
|
||||
* fetch forecast information for daily forecast.
|
||||
*/
|
||||
fetchForecastDaily(forecasts) {
|
||||
// initial variable declaration
|
||||
const days = [];
|
||||
// variables for temperature range and rain
|
||||
let minTemp = [];
|
||||
let maxTemp = [];
|
||||
// variable for date
|
||||
let date = "";
|
||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
weather.precipitation = 0;
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
|
||||
if (date !== moment(forecast.startTime).format("YYYY-MM-DD")) {
|
||||
|
||||
// calculate minimum/maximum temperature, specify rain amount
|
||||
weather.minTemperature = Math.min.apply(null, minTemp);
|
||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||
|
||||
// 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);
|
||||
|
||||
minTemp = [];
|
||||
maxTemp = [];
|
||||
weather.precipitation = 0;
|
||||
|
||||
// set new date
|
||||
date = moment(forecast.startTime).format("YYYY-MM-DD");
|
||||
|
||||
// specify date
|
||||
weather.date = moment(forecast.startTime);
|
||||
|
||||
// If the first value of today is later than 17:00, we have an icon at least!
|
||||
weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime);
|
||||
}
|
||||
|
||||
if (moment(forecast.startTime).format("H") >= 8 && moment(forecast.startTime).format("H") <= 17) {
|
||||
weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime);
|
||||
}
|
||||
|
||||
// the same day as before
|
||||
// add values from forecast to corresponding variables
|
||||
minTemp.push(forecast.temperature);
|
||||
maxTemp.push(forecast.temperature);
|
||||
}
|
||||
|
||||
// last day
|
||||
// calculate minimum/maximum temperature
|
||||
weather.minTemperature = Math.min.apply(null, minTemp);
|
||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||
|
||||
// push weather information to days array
|
||||
days.push(weather);
|
||||
return days.slice(1);
|
||||
},
|
||||
|
||||
/*
|
||||
* Calculate the astronomical data
|
||||
*/
|
||||
calcAstroData(lat, lon) {
|
||||
const sunTimes = [];
|
||||
|
||||
// determine the sunrise/sunset times
|
||||
let times = SunCalc.getTimes(new Date(), lat, lon);
|
||||
sunTimes.push(moment(times.sunrise, "X"));
|
||||
sunTimes.push(moment(times.sunset, "X"));
|
||||
|
||||
return sunTimes;
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert the icons to a more usable name.
|
||||
*/
|
||||
convertWeatherType(weatherType, isDaytime) {
|
||||
//https://w1.weather.gov/xml/current_obs/weather.php
|
||||
// There are way too many types to create, so lets just look for certain strings
|
||||
|
||||
if (weatherType.includes("Cloudy") || weatherType.includes("Partly")) {
|
||||
if (isDaytime) {
|
||||
return "day-cloudy";
|
||||
}
|
||||
|
||||
return "night-cloudy";
|
||||
} else if (weatherType.includes("Overcast")) {
|
||||
if (isDaytime) {
|
||||
return "cloudy";
|
||||
}
|
||||
|
||||
return "night-cloudy";
|
||||
} else if (weatherType.includes("Freezing") || weatherType.includes("Ice")) {
|
||||
return "rain-mix";
|
||||
} else if (weatherType.includes("Snow")) {
|
||||
if (isDaytime) {
|
||||
return "snow";
|
||||
}
|
||||
|
||||
return "night-snow";
|
||||
} else if (weatherType.includes("Thunderstorm")) {
|
||||
if (isDaytime) {
|
||||
return "thunderstorm";
|
||||
}
|
||||
|
||||
return "night-thunderstorm";
|
||||
} else if (weatherType.includes("Showers")) {
|
||||
if (isDaytime) {
|
||||
return "showers";
|
||||
}
|
||||
|
||||
return "night-showers";
|
||||
} else if (weatherType.includes("Rain") || weatherType.includes("Drizzle")) {
|
||||
if (isDaytime) {
|
||||
return "rain";
|
||||
}
|
||||
|
||||
return "night-rain";
|
||||
} else if (weatherType.includes("Breezy") || weatherType.includes("Windy")) {
|
||||
if (isDaytime) {
|
||||
return "cloudy-windy";
|
||||
}
|
||||
|
||||
return "night-alt-cloudy-windy";
|
||||
} else if (weatherType.includes("Fair") || weatherType.includes("Clear") || weatherType.includes("Few") || weatherType.includes("Sunny")) {
|
||||
if (isDaytime) {
|
||||
return "day-sunny";
|
||||
}
|
||||
|
||||
return "night-clear";
|
||||
} else if (weatherType.includes("Dust") || weatherType.includes("Sand")) {
|
||||
return "dust";
|
||||
} else if (weatherType.includes("Fog")) {
|
||||
return "fog";
|
||||
} else if (weatherType.includes("Smoke")) {
|
||||
return "smoke";
|
||||
} else if (weatherType.includes("Haze")) {
|
||||
return "day-haze";
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/*
|
||||
Convert the direction into Degrees
|
||||
*/
|
||||
convertWindDirection(windDirection) {
|
||||
const windCardinals = {
|
||||
"N": 0,
|
||||
"NNE": 22,
|
||||
"NE": 45,
|
||||
"ENE": 67,
|
||||
"E": 90,
|
||||
"ESE": 112,
|
||||
"SE": 135,
|
||||
"SSE": 157,
|
||||
"S": 180,
|
||||
"SSW": 202,
|
||||
"SW": 225,
|
||||
"WSW": 247,
|
||||
"W": 270,
|
||||
"WNW": 292,
|
||||
"NW": 315,
|
||||
"NNW": 337
|
||||
};
|
||||
|
||||
return windCardinals.hasOwnProperty(windDirection) ? windCardinals[windDirection] : null;
|
||||
}
|
||||
});
|
49
modules/default/weather/weather.css
Normal file
@@ -0,0 +1,49 @@
|
||||
.weather .weathericon,
|
||||
.weather .fa-home {
|
||||
font-size: 75%;
|
||||
line-height: 65px;
|
||||
display: inline-block;
|
||||
-ms-transform: translate(0, -3px); /* IE 9 */
|
||||
-webkit-transform: translate(0, -3px); /* Safari */
|
||||
transform: translate(0, -3px);
|
||||
}
|
||||
|
||||
.weather .humidityIcon {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.weather .humidity-padding {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.weather .day {
|
||||
padding-left: 0;
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
.weather .weather-icon {
|
||||
padding-right: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.weather .min-temp {
|
||||
padding-left: 20px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.weather .precipitation {
|
||||
padding-left: 20px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.weather tr .weathericon {
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.weather tr.colored .min-temp {
|
||||
color: #bcddff;
|
||||
}
|
||||
|
||||
.weather tr.colored .max-temp {
|
||||
color: #ff8e99;
|
||||
}
|
255
modules/default/weather/weather.js
Normal file
@@ -0,0 +1,255 @@
|
||||
/* global Module, WeatherProvider */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
Module.register("weather",{
|
||||
// Default module config.
|
||||
defaults: {
|
||||
updateInterval: 10 * 60 * 1000,
|
||||
weatherProvider: "openweathermap",
|
||||
roundTemp: false,
|
||||
type: "current", //current, forecast
|
||||
|
||||
location: false,
|
||||
locationID: false,
|
||||
appid: "",
|
||||
units: config.units,
|
||||
|
||||
tempUnits: config.units,
|
||||
windUnits: config.units,
|
||||
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
animationSpeed: 1000,
|
||||
timeFormat: config.timeFormat,
|
||||
showPeriod: true,
|
||||
showPeriodUpper: false,
|
||||
showWindDirection: true,
|
||||
showWindDirectionAsArrow: false,
|
||||
useBeaufort: true,
|
||||
lang: config.language,
|
||||
showHumidity: false,
|
||||
degreeLabel: false,
|
||||
decimalSymbol: ".",
|
||||
showIndoorTemperature: false,
|
||||
showIndoorHumidity: false,
|
||||
maxNumberOfDays: 5,
|
||||
fade: true,
|
||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
||||
|
||||
initialLoadDelay: 0, // 0 seconds delay
|
||||
retryDelay: 2500,
|
||||
|
||||
apiVersion: "2.5",
|
||||
apiBase: "http://api.openweathermap.org/data/",
|
||||
weatherEndpoint: "/weather",
|
||||
|
||||
appendLocationNameToHeader: true,
|
||||
calendarClass: "calendar",
|
||||
tableClass: "small",
|
||||
|
||||
onlyTemp: false,
|
||||
showPrecipitationAmount: false,
|
||||
colored: false,
|
||||
showFeelsLike: true
|
||||
},
|
||||
|
||||
// Module properties.
|
||||
weatherProvider: null,
|
||||
|
||||
// Define required scripts.
|
||||
getStyles: function() {
|
||||
return ["font-awesome.css", "weather-icons.css", "weather.css"];
|
||||
},
|
||||
|
||||
// Return the scripts that are necessary for the weather module.
|
||||
getScripts: function () {
|
||||
return [
|
||||
"moment.js",
|
||||
"weatherprovider.js",
|
||||
"weatherobject.js",
|
||||
"suncalc.js",
|
||||
this.file("providers/" + this.config.weatherProvider.toLowerCase() + ".js")
|
||||
];
|
||||
},
|
||||
|
||||
// Override getHeader method.
|
||||
getHeader: function() {
|
||||
if (this.config.appendLocationNameToHeader && this.data.header !== undefined && this.weatherProvider) {
|
||||
return this.data.header + " " + this.weatherProvider.fetchedLocation();
|
||||
}
|
||||
|
||||
return this.data.header;
|
||||
},
|
||||
|
||||
// Start the weather module.
|
||||
start: function () {
|
||||
moment.locale(this.config.lang);
|
||||
|
||||
// Initialize the weather provider.
|
||||
this.weatherProvider = WeatherProvider.initialize(this.config.weatherProvider, this);
|
||||
|
||||
// Let the weather provider know we are starting.
|
||||
this.weatherProvider.start();
|
||||
|
||||
// Add custom filters
|
||||
this.addFilters();
|
||||
|
||||
// Schedule the first update.
|
||||
this.scheduleUpdate(this.config.initialLoadDelay);
|
||||
},
|
||||
|
||||
// Override notification handler.
|
||||
notificationReceived: function(notification, payload, sender) {
|
||||
if (notification === "CALENDAR_EVENTS") {
|
||||
var senderClasses = sender.data.classes.toLowerCase().split(" ");
|
||||
if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
|
||||
this.firstEvent = false;
|
||||
|
||||
for (var e in payload) {
|
||||
var event = payload[e];
|
||||
if (event.location || event.geo) {
|
||||
this.firstEvent = event;
|
||||
//Log.log("First upcoming event with location: ", event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (notification === "INDOOR_TEMPERATURE") {
|
||||
this.indoorTemperature = this.roundValue(payload);
|
||||
this.updateDom(300);
|
||||
} else if (notification === "INDOOR_HUMIDITY") {
|
||||
this.indoorHumidity = this.roundValue(payload);
|
||||
this.updateDom(300);
|
||||
}
|
||||
},
|
||||
|
||||
// Select the template depending on the display type.
|
||||
getTemplate: function () {
|
||||
return `${this.config.type.toLowerCase()}.njk`;
|
||||
},
|
||||
|
||||
// Add all the data to the template.
|
||||
getTemplateData: function () {
|
||||
return {
|
||||
config: this.config,
|
||||
current: this.weatherProvider.currentWeather(),
|
||||
forecast: this.weatherProvider.weatherForecast(),
|
||||
indoor: {
|
||||
humidity: this.indoorHumidity,
|
||||
temperature: this.indoorTemperature
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
// What to do when the weather provider has new information available?
|
||||
updateAvailable: function() {
|
||||
Log.log("New weather information available.");
|
||||
this.updateDom(0);
|
||||
this.scheduleUpdate();
|
||||
},
|
||||
|
||||
scheduleUpdate: function(delay = null) {
|
||||
var nextLoad = this.config.updateInterval;
|
||||
if (delay !== null && delay >= 0) {
|
||||
nextLoad = delay;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.config.type === "forecast") {
|
||||
this.weatherProvider.fetchWeatherForecast();
|
||||
} else {
|
||||
this.weatherProvider.fetchCurrentWeather();
|
||||
}
|
||||
}, nextLoad);
|
||||
},
|
||||
|
||||
roundValue: function(temperature) {
|
||||
var decimals = this.config.roundTemp ? 0 : 1;
|
||||
return parseFloat(temperature).toFixed(decimals);
|
||||
},
|
||||
|
||||
addFilters() {
|
||||
this.nunjucksEnvironment().addFilter("formatTime", function(date) {
|
||||
date = moment(date);
|
||||
|
||||
if (this.config.timeFormat !== 24) {
|
||||
if (this.config.showPeriod) {
|
||||
if (this.config.showPeriodUpper) {
|
||||
return date.format("h:mm A");
|
||||
} else {
|
||||
return date.format("h:mm a");
|
||||
}
|
||||
} else {
|
||||
return date.format("h:mm");
|
||||
}
|
||||
}
|
||||
|
||||
return date.format("HH:mm");
|
||||
}.bind(this));
|
||||
|
||||
this.nunjucksEnvironment().addFilter("unit", function (value, type) {
|
||||
if (type === "temperature") {
|
||||
if (this.config.tempUnits === "metric" || this.config.tempUnits === "imperial") {
|
||||
value += "°";
|
||||
}
|
||||
if (this.config.degreeLabel) {
|
||||
if (this.config.tempUnits === "metric") {
|
||||
value += "C";
|
||||
} else if (this.config.tempUnits === "imperial") {
|
||||
value += "F";
|
||||
} else {
|
||||
value += "K";
|
||||
}
|
||||
}
|
||||
} else if (type === "precip") {
|
||||
if (isNaN(value) || value === 0 || value.toFixed(2) === "0.00") {
|
||||
value = "";
|
||||
} else {
|
||||
if (this.config.weatherProvider === "ukmetoffice") {
|
||||
value += "%";
|
||||
} else {
|
||||
value = `${value.toFixed(2)} ${this.config.units === "imperial" ? "in" : "mm"}`;
|
||||
}
|
||||
}
|
||||
} else if (type === "humidity") {
|
||||
value += "%";
|
||||
}
|
||||
|
||||
return value;
|
||||
}.bind(this));
|
||||
|
||||
this.nunjucksEnvironment().addFilter("roundValue", function(value) {
|
||||
return this.roundValue(value);
|
||||
}.bind(this));
|
||||
|
||||
this.nunjucksEnvironment().addFilter("decimalSymbol", function(value) {
|
||||
return value.toString().replace(/\./g, this.config.decimalSymbol);
|
||||
}.bind(this));
|
||||
|
||||
this.nunjucksEnvironment().addFilter("calcNumSteps", function(forecast) {
|
||||
return Math.min(forecast.length, this.config.maxNumberOfDays);
|
||||
}.bind(this));
|
||||
|
||||
this.nunjucksEnvironment().addFilter("opacity", function(currentStep, numSteps) {
|
||||
if (this.config.fade && this.config.fadePoint < 1) {
|
||||
if (this.config.fadePoint < 0) {
|
||||
this.config.fadePoint = 0;
|
||||
}
|
||||
var startingPoint = numSteps * this.config.fadePoint;
|
||||
var numFadesteps = numSteps - startingPoint;
|
||||
if (currentStep >= startingPoint) {
|
||||
return 1 - (currentStep - startingPoint) / numFadesteps;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
});
|
110
modules/default/weather/weatherobject.js
Executable file
@@ -0,0 +1,110 @@
|
||||
/* global Class */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is the blueprint for a day which includes weather information.
|
||||
*/
|
||||
|
||||
// Currently this is focused on the information which is necessary for the current weather.
|
||||
// As soon as we start implementing the forecast, mode properties will be added.
|
||||
|
||||
class WeatherObject {
|
||||
constructor(units, tempUnits, windUnits) {
|
||||
|
||||
this.units = units;
|
||||
this.tempUnits = tempUnits;
|
||||
this.windUnits = windUnits;
|
||||
this.date = null;
|
||||
this.windSpeed = null;
|
||||
this.windDirection = null;
|
||||
this.sunrise = null;
|
||||
this.sunset = null;
|
||||
this.temperature = null;
|
||||
this.minTemperature = null;
|
||||
this.maxTemperature = null;
|
||||
this.weatherType = null;
|
||||
this.humidity = null;
|
||||
this.rain = null;
|
||||
this.snow = null;
|
||||
this.precipitation = null;
|
||||
this.feelsLikeTemp = null;
|
||||
|
||||
}
|
||||
|
||||
cardinalWindDirection() {
|
||||
if (this.windDirection > 11.25 && this.windDirection <= 33.75){
|
||||
return "NNE";
|
||||
} else if (this.windDirection > 33.75 && this.windDirection <= 56.25) {
|
||||
return "NE";
|
||||
} else if (this.windDirection > 56.25 && this.windDirection <= 78.75) {
|
||||
return "ENE";
|
||||
} else if (this.windDirection > 78.75 && this.windDirection <= 101.25) {
|
||||
return "E";
|
||||
} else if (this.windDirection > 101.25 && this.windDirection <= 123.75) {
|
||||
return "ESE";
|
||||
} else if (this.windDirection > 123.75 && this.windDirection <= 146.25) {
|
||||
return "SE";
|
||||
} else if (this.windDirection > 146.25 && this.windDirection <= 168.75) {
|
||||
return "SSE";
|
||||
} else if (this.windDirection > 168.75 && this.windDirection <= 191.25) {
|
||||
return "S";
|
||||
} else if (this.windDirection > 191.25 && this.windDirection <= 213.75) {
|
||||
return "SSW";
|
||||
} else if (this.windDirection > 213.75 && this.windDirection <= 236.25) {
|
||||
return "SW";
|
||||
} else if (this.windDirection > 236.25 && this.windDirection <= 258.75) {
|
||||
return "WSW";
|
||||
} else if (this.windDirection > 258.75 && this.windDirection <= 281.25) {
|
||||
return "W";
|
||||
} else if (this.windDirection > 281.25 && this.windDirection <= 303.75) {
|
||||
return "WNW";
|
||||
} else if (this.windDirection > 303.75 && this.windDirection <= 326.25) {
|
||||
return "NW";
|
||||
} else if (this.windDirection > 326.25 && this.windDirection <= 348.75) {
|
||||
return "NNW";
|
||||
} else {
|
||||
return "N";
|
||||
}
|
||||
}
|
||||
|
||||
beaufortWindSpeed() {
|
||||
const windInKmh = (this.windUnits === "imperial") ? this.windSpeed * 1.609344 : 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) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return 12;
|
||||
}
|
||||
|
||||
nextSunAction() {
|
||||
return moment().isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise";
|
||||
}
|
||||
|
||||
feelsLike() {
|
||||
if (this.feelsLikeTemp) {
|
||||
return this.feelsLikeTemp;
|
||||
}
|
||||
const windInMph = (this.windUnits === "imperial") ? this.windSpeed : this.windSpeed * 2.23694;
|
||||
const tempInF = this.tempUnits === "imperial" ? this.temperature : this.temperature * 9 / 5 + 32;
|
||||
let feelsLike = tempInF;
|
||||
|
||||
if (windInMph > 3 && tempInF < 50) {
|
||||
feelsLike = Math.round(35.74 + 0.6215 * tempInF - 35.75 * Math.pow(windInMph, 0.16) + 0.4275 * tempInF * Math.pow(windInMph, 0.16));
|
||||
} else if (tempInF > 80 && this.humidity > 40) {
|
||||
feelsLike = -42.379 + 2.04901523 * tempInF + 10.14333127 * this.humidity
|
||||
- 0.22475541 * tempInF * this.humidity - 6.83783 * Math.pow(10, -3) * tempInF * tempInF
|
||||
- 5.481717 * Math.pow(10, -2) * this.humidity * this.humidity
|
||||
+ 1.22874 * Math.pow(10, -3) * tempInF * tempInF * this.humidity
|
||||
+ 8.5282 * Math.pow(10, -4) * tempInF * this.humidity * this.humidity
|
||||
- 1.99 * Math.pow(10, -6) * tempInF * tempInF * this.humidity * this.humidity;
|
||||
}
|
||||
|
||||
return this.tempUnits === "imperial" ? feelsLike : (feelsLike - 32) * 5 / 9;
|
||||
}
|
||||
}
|
148
modules/default/weather/weatherprovider.js
Normal file
@@ -0,0 +1,148 @@
|
||||
/* global Class */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
*
|
||||
* By Michael Teeuw http://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is the blueprint for a weather provider.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base BluePrint for the WeatherProvider
|
||||
*/
|
||||
var WeatherProvider = Class.extend({
|
||||
// Weather Provider Properties
|
||||
providerName: null,
|
||||
|
||||
// The following properties have accestor methods.
|
||||
// Try to not access them directly.
|
||||
currentWeatherObject: null,
|
||||
weatherForecastArray: null,
|
||||
fetchedLocationName: null,
|
||||
|
||||
// The following properties will be set automatically.
|
||||
// You do not need to overwrite these properties.
|
||||
config: null,
|
||||
delegate: null,
|
||||
providerIdentifier: null,
|
||||
|
||||
// Weather Provider Methods
|
||||
// All the following methods can be overwritten, although most are good as they are.
|
||||
|
||||
// Called when a weather provider is initialized.
|
||||
init: function(config) {
|
||||
this.config = config;
|
||||
Log.info(`Weather provider: ${this.providerName} initialized.`);
|
||||
},
|
||||
|
||||
// Called to set the config, this config is the same as the weather module's config.
|
||||
setConfig: function(config) {
|
||||
this.config = config;
|
||||
Log.info(`Weather provider: ${this.providerName} config set.`, this.config);
|
||||
},
|
||||
|
||||
// Called when the weather provider is about to start.
|
||||
start: function() {
|
||||
Log.info(`Weather provider: ${this.providerName} started.`);
|
||||
},
|
||||
|
||||
// This method should start the API request to fetch the current weather.
|
||||
// This method should definitely be overwritten in the provider.
|
||||
fetchCurrentWeather: function() {
|
||||
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchCurrentWeather method.`);
|
||||
},
|
||||
|
||||
// This method should start the API request to fetch the weather forecast.
|
||||
// This method should definitely be overwritten in the provider.
|
||||
fetchWeatherForecast: function() {
|
||||
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherForecast method.`);
|
||||
},
|
||||
|
||||
// This returns a WeatherDay object for the current weather.
|
||||
currentWeather: function() {
|
||||
return this.currentWeatherObject;
|
||||
},
|
||||
|
||||
// This returns an array of WeatherDay objects for the weather forecast.
|
||||
weatherForecast: function() {
|
||||
return this.weatherForecastArray;
|
||||
},
|
||||
|
||||
// This returns the name of the fetched location or an empty string.
|
||||
fetchedLocation: function() {
|
||||
return this.fetchedLocationName || "";
|
||||
},
|
||||
|
||||
// Set the currentWeather and notify the delegate that new information is available.
|
||||
setCurrentWeather: function(currentWeatherObject) {
|
||||
// We should check here if we are passing a WeatherDay
|
||||
this.currentWeatherObject = currentWeatherObject;
|
||||
},
|
||||
|
||||
// Set the weatherForecastArray and notify the delegate that new information is available.
|
||||
setWeatherForecast: function(weatherForecastArray) {
|
||||
// We should check here if we are passing a WeatherDay
|
||||
this.weatherForecastArray = weatherForecastArray;
|
||||
},
|
||||
|
||||
// Set the fetched location name.
|
||||
setFetchedLocation: function(name) {
|
||||
this.fetchedLocationName = name;
|
||||
},
|
||||
|
||||
// Notify the delegate that new weather is available.
|
||||
updateAvailable: function() {
|
||||
this.delegate.updateAvailable(this);
|
||||
},
|
||||
|
||||
// A convenience function to make requests. It returns a promise.
|
||||
fetchData: function(url, method = "GET", data = null) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open(method, url, true);
|
||||
request.onreadystatechange = function() {
|
||||
if (this.readyState === 4) {
|
||||
if (this.status === 200) {
|
||||
resolve(JSON.parse(this.response));
|
||||
} else {
|
||||
reject(request);
|
||||
}
|
||||
}
|
||||
};
|
||||
request.send();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Collection of registered weather providers.
|
||||
*/
|
||||
WeatherProvider.providers = [];
|
||||
|
||||
/**
|
||||
* Static method to register a new weather provider.
|
||||
*/
|
||||
WeatherProvider.register = function(providerIdentifier, providerDetails) {
|
||||
WeatherProvider.providers[providerIdentifier.toLowerCase()] = WeatherProvider.extend(providerDetails);
|
||||
};
|
||||
|
||||
/**
|
||||
* Static method to initialize a new weather provider.
|
||||
*/
|
||||
WeatherProvider.initialize = function(providerIdentifier, delegate) {
|
||||
providerIdentifier = providerIdentifier.toLowerCase();
|
||||
|
||||
var provider = new WeatherProvider.providers[providerIdentifier]();
|
||||
|
||||
provider.delegate = delegate;
|
||||
provider.setConfig(delegate.config);
|
||||
|
||||
provider.providerIdentifier = providerIdentifier;
|
||||
if (!provider.providerName) {
|
||||
provider.providerName = providerIdentifier;
|
||||
}
|
||||
|
||||
return provider;
|
||||
};
|
@@ -2,6 +2,11 @@
|
||||
The `weatherforecast` module is one of the default modules of the MagicMirror.
|
||||
This module displays the weather forecast for the coming week, including an an icon to display the current conditions, the minimum temperature and the maximum temperature.
|
||||
|
||||
## Screenshots
|
||||
|
||||
- 5 day forecast
|
||||

|
||||
|
||||
## Using the module
|
||||
|
||||
To use this module, add it to the modules array in the `config/config.js` file:
|
||||
@@ -14,7 +19,7 @@ modules: [
|
||||
config: {
|
||||
// See 'Configuration options' for more information.
|
||||
location: "Amsterdam,Netherlands",
|
||||
locationID: "", //Location ID from http://openweathermap.org/help/city_list.txt
|
||||
locationID: "", //Location ID from http://bulk.openweathermap.org/sample/city.list.json.gz
|
||||
appid: "abcde12345abcde12345abcde12345ab" //openweathermap.org API key.
|
||||
}
|
||||
}
|
||||
@@ -28,7 +33,7 @@ The following properties can be configured:
|
||||
| Option | Description
|
||||
| ---------------------------- | -----------
|
||||
| `location` | The location used for weather information. <br><br> **Example:** `'Amsterdam,Netherlands'` <br> **Default value:** `false` <br><br> **Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
|
||||
| `locationID` | Location ID from [OpenWeatherMap](http://openweathermap.org/help/city_list.txt) **This will override anything you put in location.** <br> Leave blank if you want to use location. <br> **Example:** `1234567` <br> **Default value:** `false` <br><br> **Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
|
||||
| `locationID` | Location ID from [OpenWeatherMap](https://openweathermap.org/find) **This will override anything you put in location.** <br> Leave blank if you want to use location. <br> **Example:** `1234567` <br> **Default value:** `false` <br><br> **Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
|
||||
| `appid` | The [OpenWeatherMap](https://home.openweathermap.org) API key, which can be obtained by creating an OpenWeatherMap account. <br><br> This value is **REQUIRED**
|
||||
| `units` | What units to use. Specified by config.js <br><br> **Possible values:** `config.units` = Specified by config.js, `default` = Kelvin, `metric` = Celsius, `imperial` =Fahrenheit <br> **Default value:** `config.units`
|
||||
| `roundTemp` | Round temperature values to nearest integer. <br><br> **Possible values:** `true` (round to integer) or `false` (display exact value with decimal point) <br> **Default value:** `false`
|
||||
@@ -46,10 +51,11 @@ The following properties can be configured:
|
||||
| `apiBase` | The OpenWeatherMap base URL. <br><br> **Default value:** `'http://api.openweathermap.org/data/'`
|
||||
| `forecastEndpoint` | The OpenWeatherMap API endPoint. <br><br> **Default value:** `'forecast/daily'`
|
||||
| `appendLocationNameToHeader` | If set to `true`, the returned location name will be appended to the header of the module, if the header is enabled. This is mainly intresting when using calender based weather. <br><br> **Default value:** `true`
|
||||
| `calendarClass` | The class for the calender module to base the event based weather information on. <br><br> **Default value:** `'calendar'`
|
||||
| `calendarClass` | The class for the calendar module to base the event based weather information on. <br><br> **Default value:** `'calendar'`
|
||||
| `tableClass` | Name of the classes issued from `main.css`. <br><br> **Possible values:** xsmall, small, medium, large, xlarge. <br> **Default value:** _small._
|
||||
| `iconTable` | The conversion table to convert the weather conditions to weather-icons. <br><br> **Default value:** view table below
|
||||
`colored` | If set 'colored' to true the min-temp get a blue tone and the max-temp get a red tone. <br><br> **Default value:** `'false'`
|
||||
| `colored` | If set `colored` to `true` the min-temp gets a blue tone and the max-temp gets a red tone. <br><br> **Default value:** `'false'`
|
||||
| `scale ` | If set to `true` the module will display `C` for Celsius degrees and `F` for Fahrenheit degrees after the number, based on the value of the `units` option, otherwise only the ° character is displayed. <br><br> **Default value:** `false`
|
||||
|
||||
#### Default Icon Table
|
||||
````javascript
|
||||
|
BIN
modules/default/weatherforecast/forecast_screenshot.png
Normal file
After Width: | Height: | Size: 84 KiB |
@@ -19,9 +19,9 @@
|
||||
}
|
||||
|
||||
.weatherforecast tr.colored .min-temp {
|
||||
color: #BCDDFF;
|
||||
color: #BCDDFF;
|
||||
}
|
||||
|
||||
.weatherforecast tr.colored .max-temp {
|
||||
color: #FF8E99;
|
||||
color: #FF8E99;
|
||||
}
|
||||
|
@@ -82,7 +82,7 @@ Module.register("weatherforecast",{
|
||||
getTranslations: function() {
|
||||
// The translations for the default modules are defined in the core translation files.
|
||||
// Therefor we can just return false. Otherwise we should have returned a dictionary.
|
||||
// If you're trying to build yiur own module including translations, check out the documentation.
|
||||
// If you're trying to build your own module including translations, check out the documentation.
|
||||
return false;
|
||||
},
|
||||
|
||||
@@ -143,13 +143,16 @@ Module.register("weatherforecast",{
|
||||
iconCell.appendChild(icon);
|
||||
|
||||
var degreeLabel = "";
|
||||
if (this.config.units === "metric" || this.config.units === "imperial") {
|
||||
degreeLabel += "°";
|
||||
}
|
||||
if(this.config.scale) {
|
||||
switch(this.config.units) {
|
||||
case "metric":
|
||||
degreeLabel = " °C";
|
||||
degreeLabel += "C";
|
||||
break;
|
||||
case "imperial":
|
||||
degreeLabel = " °F";
|
||||
degreeLabel += "F";
|
||||
break;
|
||||
case "default":
|
||||
degreeLabel = "K";
|
||||
@@ -237,7 +240,7 @@ Module.register("weatherforecast",{
|
||||
|
||||
/* updateWeather(compliments)
|
||||
* Requests new data from openweather.org.
|
||||
* Calls processWeather on succesfull response.
|
||||
* Calls processWeather on successful response.
|
||||
*/
|
||||
updateWeather: function() {
|
||||
if (this.config.appid === "") {
|
||||
@@ -258,7 +261,7 @@ Module.register("weatherforecast",{
|
||||
} else if (this.status === 401) {
|
||||
self.updateDom(self.config.animationSpeed);
|
||||
|
||||
if (self.config.forecastEndpoint == "forecast/daily") {
|
||||
if (self.config.forecastEndpoint === "forecast/daily") {
|
||||
self.config.forecastEndpoint = "forecast";
|
||||
Log.warn(self.name + ": Your AppID does not support long term forecasts. Switching to fallback endpoint.");
|
||||
}
|
||||
@@ -288,7 +291,7 @@ Module.register("weatherforecast",{
|
||||
} else if(this.config.location) {
|
||||
params += "q=" + this.config.location;
|
||||
} else if (this.firstEvent && this.firstEvent.geo) {
|
||||
params += "lat=" + this.firstEvent.geo.lat + "&lon=" + this.firstEvent.geo.lon
|
||||
params += "lat=" + this.firstEvent.geo.lat + "&lon=" + this.firstEvent.geo.lon;
|
||||
} else if (this.firstEvent && this.firstEvent.location) {
|
||||
params += "q=" + this.firstEvent.location;
|
||||
} else {
|
||||
@@ -312,7 +315,7 @@ Module.register("weatherforecast",{
|
||||
*/
|
||||
parserDataWeather: function(data) {
|
||||
if (data.hasOwnProperty("main")) {
|
||||
data["temp"] = {"min": data.main.temp_min, "max": data.main.temp_max}
|
||||
data["temp"] = {"min": data.main.temp_min, "max": data.main.temp_max};
|
||||
}
|
||||
return data;
|
||||
},
|
||||
@@ -327,7 +330,7 @@ Module.register("weatherforecast",{
|
||||
|
||||
this.forecast = [];
|
||||
var lastDay = null;
|
||||
var forecastData = {}
|
||||
var forecastData = {};
|
||||
|
||||
for (var i = 0, count = data.list.length; i < count; i++) {
|
||||
|
||||
@@ -350,7 +353,7 @@ Module.register("weatherforecast",{
|
||||
icon: this.config.iconTable[forecast.weather[0].icon],
|
||||
maxTemp: this.roundValue(forecast.temp.max),
|
||||
minTemp: this.roundValue(forecast.temp.min),
|
||||
rain: forecast.rain
|
||||
rain: this.processRain(forecast, data.list)
|
||||
};
|
||||
|
||||
this.forecast.push(forecastData);
|
||||
@@ -431,5 +434,38 @@ Module.register("weatherforecast",{
|
||||
roundValue: function(temperature) {
|
||||
var decimals = this.config.roundTemp ? 0 : 1;
|
||||
return parseFloat(temperature).toFixed(decimals);
|
||||
},
|
||||
|
||||
/* processRain(forecast, allForecasts)
|
||||
* Calculates the amount of rain for a whole day even if long term forecasts isn't available for the appid.
|
||||
*
|
||||
* When using the the fallback endpoint forecasts are provided in 3h intervals and the rain-property is an object instead of number.
|
||||
* That object has a property "3h" which contains the amount of rain since the previous forecast in the list.
|
||||
* This code finds all forecasts that is for the same day and sums the amount of rain and returns that.
|
||||
*/
|
||||
processRain: function(forecast, allForecasts) {
|
||||
//If the amount of rain actually is a number, return it
|
||||
if (!isNaN(forecast.rain)) {
|
||||
return forecast.rain;
|
||||
}
|
||||
|
||||
//Find all forecasts that is for the same day
|
||||
var checkDateTime = (!!forecast.dt_txt) ? moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss") : moment(forecast.dt, "X");
|
||||
var daysForecasts = allForecasts.filter(function(item) {
|
||||
var itemDateTime = (!!item.dt_txt) ? moment(item.dt_txt, "YYYY-MM-DD hh:mm:ss") : moment(item.dt, "X");
|
||||
return itemDateTime.isSame(checkDateTime, "day") && item.rain instanceof Object;
|
||||
});
|
||||
|
||||
//If no rain this day return undefined so it wont be displayed for this day
|
||||
if (daysForecasts.length == 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
//Summarize all the rain from the matching days
|
||||
return daysForecasts.map(function(item) {
|
||||
return Object.values(item.rain)[0];
|
||||
}).reduce(function(a, b) {
|
||||
return a + b;
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
5980
package-lock.json
generated
30
package.json
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "magicmirror",
|
||||
"version": "2.5.0",
|
||||
"version": "2.9.0",
|
||||
"description": "The open source modular smart mirror platform.",
|
||||
"main": "js/electron.js",
|
||||
"scripts": {
|
||||
"start": "sh run-start.sh",
|
||||
"install": "cd vendor && npm install",
|
||||
"install-fonts": "cd fonts && npm install",
|
||||
"postinstall": "sh installers/postinstall/postinstall.sh && npm run install-fonts",
|
||||
"postinstall": "sh untrack-css.sh && sh installers/postinstall/postinstall.sh && npm run install-fonts",
|
||||
"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",
|
||||
@@ -41,35 +41,35 @@
|
||||
"grunt": "latest",
|
||||
"grunt-eslint": "latest",
|
||||
"grunt-jsonlint": "latest",
|
||||
"grunt-markdownlint": "^1.0.43",
|
||||
"grunt-markdownlint": "latest",
|
||||
"grunt-stylelint": "latest",
|
||||
"grunt-yamllint": "latest",
|
||||
"http-auth": "^3.2.3",
|
||||
"jsdom": "^11.6.2",
|
||||
"jshint": "^2.9.5",
|
||||
"jshint": "^2.10.2",
|
||||
"mocha": "^4.1.0",
|
||||
"mocha-each": "^1.1.0",
|
||||
"spectron": "3.7.x",
|
||||
"stylelint": "^8.4.0",
|
||||
"mocha-logger": "^1.0.6",
|
||||
"spectron": "^3.8.0",
|
||||
"stylelint": "latest",
|
||||
"stylelint-config-standard": "latest",
|
||||
"time-grunt": "latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.18.2",
|
||||
"colors": "^1.1.2",
|
||||
"electron": "^2.0.0",
|
||||
"electron": "^3.0.13",
|
||||
"express": "^4.16.2",
|
||||
"express-ipfilter": "0.3.1",
|
||||
"express-ipfilter": "^1.0.1",
|
||||
"feedme": "latest",
|
||||
"helmet": "^3.9.0",
|
||||
"iconv-lite": "latest",
|
||||
"mocha-logger": "^1.0.5",
|
||||
"lodash": "^4.17.11",
|
||||
"moment": "latest",
|
||||
"request": "^2.83.0",
|
||||
"rrule-alt": "^2.2.7",
|
||||
"request": "^2.88.0",
|
||||
"rrule": "^2.6.2",
|
||||
"rrule-alt": "^2.2.8",
|
||||
"simple-git": "^1.85.0",
|
||||
"socket.io": "^2.0.4",
|
||||
"valid-url": "latest",
|
||||
"walk": "latest"
|
||||
"socket.io": "^2.1.1",
|
||||
"valid-url": "latest"
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,6 @@
|
||||
|
||||
./untrack-css.sh
|
||||
|
||||
if [ -z "$DISPLAY" ]; then #If not set DISPLAY is SSH remote or tty
|
||||
export DISPLAY=:0 # Set by default display
|
||||
fi
|
||||
|
@@ -1,6 +1,5 @@
|
||||
var app = require("../js/app.js");
|
||||
app.start(function(config) {
|
||||
console.log("");
|
||||
var bindAddress = config.address ? config.address : "localhost";
|
||||
console.log("Ready to go! Please point your browser to: http://" + bindAddress + ":" + config.port);
|
||||
console.log("\nReady to go! Please point your browser to: http://" + bindAddress + ":" + config.port);
|
||||
});
|
||||
|
@@ -16,7 +16,7 @@ var Utils = require(__dirname + "/../../js/utils.js");
|
||||
|
||||
/* getConfigFile()
|
||||
* Return string with path of configuration file
|
||||
* Check if set by enviroment variable MM_CONFIG_FILE
|
||||
* Check if set by environment variable MM_CONFIG_FILE
|
||||
*/
|
||||
function getConfigFile() {
|
||||
// FIXME: This function should be in core. Do you want refactor me ;) ?, be good!
|
||||
@@ -35,7 +35,7 @@ function checkConfigFile() {
|
||||
console.error(Utils.colors.error("File not found: "), configFileName);
|
||||
return;
|
||||
}
|
||||
// check permision
|
||||
// check permission
|
||||
try {
|
||||
fs.accessSync(configFileName, fs.F_OK);
|
||||
} catch (e) {
|
||||
@@ -52,12 +52,12 @@ function checkConfigFile() {
|
||||
if (err) { throw err; }
|
||||
v.JSHINT(data); // Parser by jshint
|
||||
|
||||
if (v.JSHINT.errors.length == 0) {
|
||||
if (v.JSHINT.errors.length === 0) {
|
||||
console.log("Your configuration file doesn't contain syntax errors :)");
|
||||
return true;
|
||||
} else {
|
||||
errors = v.JSHINT.data().errors;
|
||||
for (idx in errors) {
|
||||
for (var idx in errors) {
|
||||
error = errors[idx];
|
||||
console.log("Line", error.line, "col", error.character, error.reason);
|
||||
}
|
||||
@@ -67,4 +67,4 @@ function checkConfigFile() {
|
||||
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
checkConfigFile();
|
||||
};
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror Test config sample enviroment
|
||||
/* Magic Mirror Test config sample environment
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
|
@@ -6,7 +6,6 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
|
||||
var config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
35
tests/configs/modules/weather/currentweather_default.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/* Magic Mirror Test config default weather
|
||||
*
|
||||
* By fewieden https://github.com/fewieden
|
||||
*
|
||||
* 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: "weather",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
location: "Munich",
|
||||
apiKey: "fake key",
|
||||
initialLoadDelay: 3000
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {module.exports = config;}
|
40
tests/configs/modules/weather/currentweather_options.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/* Magic Mirror Test config default weather
|
||||
*
|
||||
* By fewieden https://github.com/fewieden
|
||||
*
|
||||
* 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: "weather",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
location: "Munich",
|
||||
apiKey: "fake key",
|
||||
initialLoadDelay: 3000,
|
||||
useBeaufort: false,
|
||||
showWindDirectionAsArrow: true,
|
||||
showHumidity: true,
|
||||
roundTemp: true,
|
||||
degreeLabel: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {module.exports = config;}
|
37
tests/configs/modules/weather/currentweather_units.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/* Magic Mirror Test config default weather
|
||||
*
|
||||
* By fewieden https://github.com/fewieden
|
||||
*
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
let config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
timeFormat: 24,
|
||||
units: "imperial",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
},
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
module: "weather",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
location: "Munich",
|
||||
apiKey: "fake key",
|
||||
initialLoadDelay: 3000,
|
||||
decimalSymbol: ",",
|
||||
showHumidity: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {module.exports = config;}
|
37
tests/configs/modules/weather/forecastweather_default.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/* Magic Mirror Test config default weather
|
||||
*
|
||||
* By fewieden https://github.com/fewieden
|
||||
*
|
||||
* 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: "weather",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
type: "forecast",
|
||||
location: "Munich",
|
||||
apiKey: "fake key",
|
||||
weatherEndpoint: "/forecast/daily",
|
||||
initialLoadDelay: 3000
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {module.exports = config;}
|
40
tests/configs/modules/weather/forecastweather_options.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/* Magic Mirror Test config default weather
|
||||
*
|
||||
* By fewieden https://github.com/fewieden
|
||||
*
|
||||
* 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: "weather",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
type: "forecast",
|
||||
location: "Munich",
|
||||
apiKey: "fake key",
|
||||
weatherEndpoint: "/forecast/daily",
|
||||
initialLoadDelay: 3000,
|
||||
showPrecipitationAmount: true,
|
||||
colored: true,
|
||||
tableClass: "myTableClass"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {module.exports = config;}
|
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror Test config sample enviroment set por 8090
|
||||
/* Magic Mirror Test config sample environment set port 8090
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
|
@@ -1,13 +1,8 @@
|
||||
const helpers = require("./global-setup");
|
||||
const path = require("path");
|
||||
const request = require("request");
|
||||
|
||||
const expect = require("chai").expect;
|
||||
|
||||
const describe = global.describe;
|
||||
const it = global.it;
|
||||
const beforeEach = global.beforeEach;
|
||||
const afterEach = global.afterEach;
|
||||
|
||||
describe("Development console tests", function() {
|
||||
// This tests fail and crash another tests
|
||||
|
@@ -1,7 +1,5 @@
|
||||
const helpers = require("./global-setup");
|
||||
const path = require("path");
|
||||
const request = require("request");
|
||||
|
||||
const expect = require("chai").expect;
|
||||
|
||||
const describe = global.describe;
|
||||
@@ -36,7 +34,7 @@ describe("Electron app environment", function() {
|
||||
it("should open a browserwindow", function() {
|
||||
return app.client
|
||||
.waitUntilWindowLoaded()
|
||||
.browserWindow.focus()
|
||||
// .browserWindow.focus()
|
||||
.getWindowCount()
|
||||
.should.eventually.equal(1)
|
||||
.browserWindow.isMinimized()
|
||||
|
@@ -1,14 +1,9 @@
|
||||
const helpers = require("./global-setup");
|
||||
const path = require("path");
|
||||
const request = require("request");
|
||||
|
||||
const expect = require("chai").expect;
|
||||
const forEach = require("mocha-each");
|
||||
|
||||
const describe = global.describe;
|
||||
const it = global.it;
|
||||
const beforeEach = global.beforeEach;
|
||||
const afterEach = global.afterEach;
|
||||
const forEach = require("mocha-each");
|
||||
|
||||
describe("All font files from roboto.css should be downloadable", function() {
|
||||
helpers.setupTimeout(this);
|
||||
@@ -18,7 +13,7 @@ describe("All font files from roboto.css should be downloadable", function() {
|
||||
var fileContent = require("fs").readFileSync(__dirname + "/../../fonts/roboto.css", "utf8");
|
||||
var regex = /\burl\(['"]([^'"]+)['"]\)/g;
|
||||
var match = regex.exec(fileContent);
|
||||
while (match != null) {
|
||||
while (match !== null) {
|
||||
// Push 1st match group onto fontFiles stack
|
||||
fontFiles.push(match[1]);
|
||||
// Find the next one
|
||||
|
@@ -12,7 +12,6 @@ const Application = require("spectron").Application;
|
||||
const assert = require("assert");
|
||||
const chai = require("chai");
|
||||
const chaiAsPromised = require("chai-as-promised");
|
||||
|
||||
const path = require("path");
|
||||
|
||||
global.before(function() {
|
||||
|
@@ -1,7 +1,5 @@
|
||||
const helpers = require("./global-setup");
|
||||
const path = require("path");
|
||||
const request = require("request");
|
||||
|
||||
const expect = require("chai").expect;
|
||||
|
||||
const describe = global.describe;
|
||||
@@ -17,7 +15,7 @@ describe("ipWhitelist directive configuration", function () {
|
||||
beforeEach(function () {
|
||||
return helpers.startApplication({
|
||||
args: ["js/electron.js"]
|
||||
}).then(function (startedApp) { app = startedApp; })
|
||||
}).then(function (startedApp) { app = startedApp; });
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
|
@@ -1,10 +1,6 @@
|
||||
const helpers = require("../global-setup");
|
||||
const path = require("path");
|
||||
const request = require("request");
|
||||
const serverBasicAuth = require("../../servers/basic-auth.js");
|
||||
|
||||
const expect = require("chai").expect;
|
||||
|
||||
const describe = global.describe;
|
||||
const it = global.it;
|
||||
const beforeEach = global.beforeEach;
|
||||
@@ -72,7 +68,7 @@ describe("Calendar module", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Basic auth backward compatibilty configuration: DEPRECATED", function() {
|
||||
describe("Basic auth backward compatibility configuration: DEPRECATED", function() {
|
||||
before(function() {
|
||||
serverBasicAuth.listen(8012);
|
||||
// Set config sample for use in test
|
||||
|
@@ -1,8 +1,4 @@
|
||||
const helpers = require("../global-setup");
|
||||
const path = require("path");
|
||||
const request = require("request");
|
||||
|
||||
const expect = require("chai").expect;
|
||||
|
||||
const describe = global.describe;
|
||||
const it = global.it;
|
||||
@@ -86,5 +82,4 @@ describe("Clock set to spanish language module", function() {
|
||||
.getText(".clock .week").should.eventually.match(weekRegex);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -1,8 +1,4 @@
|
||||
const helpers = require("../global-setup");
|
||||
const path = require("path");
|
||||
const request = require("request");
|
||||
|
||||
const expect = require("chai").expect;
|
||||
|
||||
const describe = global.describe;
|
||||
const it = global.it;
|
||||
|