Compare commits
702 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
491f5aa776 | ||
|
d401e59031 | ||
|
63ce69ce82 | ||
|
f15972c823 | ||
|
b210fcdcf3 | ||
|
8ea37a5a45 | ||
|
daf98c36fc | ||
|
6a0e6eb84e | ||
|
7bcdd2a42c | ||
|
670a760404 | ||
|
a99de1ad13 | ||
|
108fe2082d | ||
|
e67dc38890 | ||
|
20a87656ff | ||
|
70c3b68e67 | ||
|
bfcb7dc601 | ||
|
39e16221fb | ||
|
d11e4e5c92 | ||
|
cccaa1f33d | ||
|
37d3aa25b1 | ||
|
ebfe96d31d | ||
|
e902b8c52f | ||
|
201b36d62c | ||
|
7b29070516 | ||
|
9ed2e4d557 | ||
|
38544f2368 | ||
|
df39408411 | ||
|
2e01f988f5 | ||
|
2b940c9cfb | ||
|
b6f4737ecc | ||
|
89d6473052 | ||
|
99d838f9cd | ||
|
f3bdddfaa9 | ||
|
87f952c87b | ||
|
639305ecc8 | ||
|
a70bb517e1 | ||
|
e4f671c898 | ||
|
a269b5cd93 | ||
|
2ec275957f | ||
|
4f9fc032e5 | ||
|
5c9dbccc10 | ||
|
b2cf470ec9 | ||
|
bceb181b47 | ||
|
1a314b741a | ||
|
7635dea3e9 | ||
|
90112d1a7d | ||
|
ad0cf12e53 | ||
|
a53029f11e | ||
|
848529f9f4 | ||
|
b5dc91fd07 | ||
|
0a2b939514 | ||
|
52584f36d7 | ||
|
30c7a24fc2 | ||
|
0643a103ac | ||
|
71bd45dfd4 | ||
|
85c9d3b331 | ||
|
5e6cbeb9ba | ||
|
3d4429d418 | ||
|
d3d64d3ca0 | ||
|
6de983aeb2 | ||
|
05c3a5bf83 | ||
|
c2f5d038de | ||
|
0ac5032db9 | ||
|
9b93066cbe | ||
|
bc60ae21c4 | ||
|
0b37ed072c | ||
|
57174f09b9 | ||
|
61057d1a25 | ||
|
03964d6f68 | ||
|
7515fc10d1 | ||
|
6583f05858 | ||
|
2b1dbbde68 | ||
|
49c95a9f46 | ||
|
a6214c8da3 | ||
|
d2b3414ac6 | ||
|
401a6f3417 | ||
|
3ed223a550 | ||
|
47bd48e0a3 | ||
|
cdd1853369 | ||
|
8f2980c23d | ||
|
b72556b9a9 | ||
|
49be3cbd6b | ||
|
fbb0c59b4e | ||
|
d83e696a8d | ||
|
a467d900c9 | ||
|
96be8d6fea | ||
|
8b1ce26fa6 | ||
|
514b9453f8 | ||
|
fa0f997928 | ||
|
504e7cadd7 | ||
|
c80aeff945 | ||
|
454206c803 | ||
|
e4f47178fc | ||
|
1f9109f8e4 | ||
|
f09c54184a | ||
|
92a35692f2 | ||
|
53e300bd31 | ||
|
e8be6ad4f1 | ||
|
01b9ecb339 | ||
|
6ff85ebe54 | ||
|
ea6eebd809 | ||
|
b2a21b937d | ||
|
13010ecaee | ||
|
2e2e157017 | ||
|
e23e33852e | ||
|
5b270b84b4 | ||
|
9094240024 | ||
|
1db0dbf52b | ||
|
beb5faef8b | ||
|
c6aff8b7dc | ||
|
1aacc37c83 | ||
|
b18d98f5ea | ||
|
09ddd3d925 | ||
|
e2b4823e43 | ||
|
04491ac5ac | ||
|
e3bee5aae7 | ||
|
89152e537e | ||
|
652e1a528f | ||
|
9d85baee37 | ||
|
bdc4ed4d86 | ||
|
35f3b315a2 | ||
|
efec49bb47 | ||
|
68bc77c81e | ||
|
dbea348779 | ||
|
b46160bcbc | ||
|
ddb06ca214 | ||
|
ef2fd16b69 | ||
|
d874ad9988 | ||
|
57dc349675 | ||
|
120f0299b2 | ||
|
b5b6df5e48 | ||
|
5ffd20b843 | ||
|
3dacce6675 | ||
|
8e4ba4fe93 | ||
|
37d488760f | ||
|
7cdeceedf1 | ||
|
7ba76020d8 | ||
|
aab1b97653 | ||
|
221eadcc20 | ||
|
4a11cdc564 | ||
|
b9333134c7 | ||
|
8c83dc9494 | ||
|
1ed721fb15 | ||
|
88ed5ed373 | ||
|
84995c9252 | ||
|
6fadc76fe3 | ||
|
d240986fdc | ||
|
afc73920ad | ||
|
16615c3da2 | ||
|
88c80973f1 | ||
|
e322be2624 | ||
|
059b87bbb4 | ||
|
911675f995 | ||
|
e76fe5e25a | ||
|
308774c2a6 | ||
|
db24f20289 | ||
|
94bb8e6c03 | ||
|
41da6f455a | ||
|
d2a7a3b0bb | ||
|
afbdacf136 | ||
|
2324579057 | ||
|
3c357f057b | ||
|
5116a2fc82 | ||
|
c0ddc020d5 | ||
|
0683734d5a | ||
|
6cbd267384 | ||
|
925113fa20 | ||
|
3696d45e94 | ||
|
cb3ae66652 | ||
|
948b6c8de8 | ||
|
3c4d7a33e0 | ||
|
5a421220c9 | ||
|
41508931be | ||
|
e2cfa24686 | ||
|
d48113f2d9 | ||
|
16c5bddbb7 | ||
|
ef7556f6d3 | ||
|
a3cb0b7b96 | ||
|
4d28688f30 | ||
|
01ff00fa31 | ||
|
78190d9c7f | ||
|
b1565e4047 | ||
|
db220b7861 | ||
|
eaf27b837e | ||
|
74410344af | ||
|
998f64f983 | ||
|
684dfb643b | ||
|
314c3cb516 | ||
|
58939bfd8c | ||
|
33592b3c0e | ||
|
ad9c2549bc | ||
|
5152e0b114 | ||
|
ca48663efd | ||
|
b520b4c37a | ||
|
90f07295b1 | ||
|
3895c18466 | ||
|
2b6a9fc5bb | ||
|
052f0b8709 | ||
|
a5bb9d962d | ||
|
2d9d28aa0f | ||
|
69c053a94f | ||
|
aaaf1f660c | ||
|
8538d83520 | ||
|
132c98b767 | ||
|
42cac81953 | ||
|
3ee4bd65c6 | ||
|
fcc7e80bf9 | ||
|
e0d43a4c1e | ||
|
4966d6c920 | ||
|
1fd506f25d | ||
|
a99698d1a9 | ||
|
774b86c7dc | ||
|
2c3e8533c7 | ||
|
3eda8af671 | ||
|
aa61874848 | ||
|
2deab31187 | ||
|
b177a56fa2 | ||
|
6f0f75cf27 | ||
|
39bb2eb9b0 | ||
|
7b36bb025a | ||
|
aa9a1b7af2 | ||
|
caf3552d6b | ||
|
6e9897f7fc | ||
|
fa9258761e | ||
|
003e948899 | ||
|
9cd998f219 | ||
|
d75b894d9a | ||
|
5a3d3b76a7 | ||
|
5bc2c207db | ||
|
612cf25878 | ||
|
5ae4912b45 | ||
|
a9a70fd2e9 | ||
|
f90856808b | ||
|
7dbcaa83bc | ||
|
38f10b6e3e | ||
|
9c8fa06ce1 | ||
|
4efe04774c | ||
|
5d60534dc9 | ||
|
ac141a4316 | ||
|
d22064c6f4 | ||
|
189721ebba | ||
|
7aa9c63dba | ||
|
16e894e300 | ||
|
d466705ec0 | ||
|
0e97d863ce | ||
|
5de64d2ae8 | ||
|
ced0398e49 | ||
|
d4b57924a7 | ||
|
d2def2bea3 | ||
|
e65bb84f9f | ||
|
69b0aa6118 | ||
|
10dc315f3b | ||
|
0f1457b5d7 | ||
|
090873d4c2 | ||
|
da00c168ae | ||
|
8286d5a06e | ||
|
dc5fb978a7 | ||
|
a4ab0cbe09 | ||
|
4a341b381e | ||
|
3fa98bc1aa | ||
|
87e2e87ce6 | ||
|
c25b6dc16c | ||
|
6a786aa090 | ||
|
a97d87bce8 | ||
|
fba91329f1 | ||
|
353cc3b00f | ||
|
057ef63586 | ||
|
c9fb38981e | ||
|
9ea7de8b44 | ||
|
020c8ccd2a | ||
|
4d09abe725 | ||
|
5079e30caf | ||
|
e51f6597ed | ||
|
e80a65a3cd | ||
|
3e5c1a278b | ||
|
4ded60874f | ||
|
7c3675c9e1 | ||
|
ff1a843de6 | ||
|
e6cefcf948 | ||
|
a857412f13 | ||
|
e507f95b2a | ||
|
61cf92c67a | ||
|
7a4eddc592 | ||
|
3fcbf15915 | ||
|
efafb1c28a | ||
|
67bedf8648 | ||
|
c1d35b0f91 | ||
|
d3a715bd6b | ||
|
28b52cd24f | ||
|
46e77f805c | ||
|
dd23db0ad8 | ||
|
30cf7f8afe | ||
|
7802e0bb88 | ||
|
07e75b8550 | ||
|
1e9fad8278 | ||
|
0975826457 | ||
|
e5ff320591 | ||
|
02c1e47749 | ||
|
a50824eeee | ||
|
0c3f9f4ed9 | ||
|
6b12601c6f | ||
|
225bece44e | ||
|
a5a5e73196 | ||
|
f8085ed78f | ||
|
4e6e84c637 | ||
|
846212798c | ||
|
571f95eb2f | ||
|
202eeea33f | ||
|
af212057db | ||
|
0fd0fea7e7 | ||
|
eb6ef3c8ff | ||
|
cdb8d35cf6 | ||
|
684dcdcef3 | ||
|
155351f5dd | ||
|
0419e06e7c | ||
|
2ea38bd9a4 | ||
|
30db9c30c8 | ||
|
a8ef594dab | ||
|
9858d5b495 | ||
|
20bd85b676 | ||
|
83ec8ca24f | ||
|
9622d02230 | ||
|
b72d0ed37e | ||
|
f966e504a5 | ||
|
a0366e794b | ||
|
3c3ce24397 | ||
|
30290f3eb2 | ||
|
616431e04b | ||
|
d0aeb90f0a | ||
|
86220fa721 | ||
|
0f58a56a07 | ||
|
29451562e3 | ||
|
911687af2a | ||
|
1c9c33b87b | ||
|
f485462af1 | ||
|
39d7ceb017 | ||
|
8251792a0d | ||
|
54ac450f92 | ||
|
299e4a497f | ||
|
3f851c1fd6 | ||
|
8cf16f1049 | ||
|
b373aa6250 | ||
|
2f4b8cd642 | ||
|
85b6df3738 | ||
|
c675421a6a | ||
|
a7b571e5d0 | ||
|
b3a9b7ef0e | ||
|
e984893853 | ||
|
3024319027 | ||
|
21ba652413 | ||
|
e4b8cd92f2 | ||
|
1a4a9f6501 | ||
|
e950cdaf32 | ||
|
f97be2f8f3 | ||
|
be0c8f4f16 | ||
|
46fd2de315 | ||
|
72f0d77865 | ||
|
d43679d59e | ||
|
ce46fb5384 | ||
|
43b33cb6de | ||
|
0344399253 | ||
|
3a9b154cb2 | ||
|
1074fbacfe | ||
|
00ff3ab380 | ||
|
db874a011c | ||
|
35a2839a2f | ||
|
cdb9b9bb87 | ||
|
87a3e4d440 | ||
|
284bed677e | ||
|
667be460e5 | ||
|
c49386bb58 | ||
|
adb50a9623 | ||
|
ac1d2372f4 | ||
|
f54690c829 | ||
|
053f9c34aa | ||
|
e7dd2b4aee | ||
|
6f82f9e01b | ||
|
d531730dc1 | ||
|
00bdf6aaa6 | ||
|
97f3514677 | ||
|
137facf95a | ||
|
8afba3a5c4 | ||
|
9c5383dc37 | ||
|
260bc9664e | ||
|
655ca83356 | ||
|
472bf1665c | ||
|
db129cc19b | ||
|
b735f8a524 | ||
|
1e34764588 | ||
|
99aaae491c | ||
|
f288581c69 | ||
|
3c5d50bce9 | ||
|
a01f08391b | ||
|
51a1399bca | ||
|
b735cb96a0 | ||
|
d00c25e107 | ||
|
8a5e87b116 | ||
|
2779d19d5c | ||
|
38d4a8b198 | ||
|
ccf98c0c22 | ||
|
bd0d91d1b4 | ||
|
056f3a6ccb | ||
|
20a50f8382 | ||
|
21284e7795 | ||
|
e0ceed5a63 | ||
|
958a2ee6ec | ||
|
ea264cb15e | ||
|
d8f19e631c | ||
|
ce5c0ed5ba | ||
|
839ca9ecfb | ||
|
720bc12c00 | ||
|
1ba845fb06 | ||
|
e86fa9d24a | ||
|
e348a61085 | ||
|
2f70366299 | ||
|
c466b20558 | ||
|
021f8d25a5 | ||
|
a19c3a43d8 | ||
|
819923e66d | ||
|
6c3100e250 | ||
|
1065eda47f | ||
|
afd676a958 | ||
|
12405b66d0 | ||
|
ecd0b6fa83 | ||
|
3a8587378c | ||
|
a05c08ed48 | ||
|
469a90787b | ||
|
9e5a9b5ced | ||
|
f311ba3f7c | ||
|
fc68321bd5 | ||
|
8a4173d0ad | ||
|
90af31cb2f | ||
|
61bcd9337b | ||
|
7944045893 | ||
|
2c3f83c70d | ||
|
92ab705ff5 | ||
|
2951f0c40c | ||
|
8a23bccb70 | ||
|
aa6ad01fb9 | ||
|
ef325896c5 | ||
|
a270c73d7c | ||
|
16feda895d | ||
|
874a50d142 | ||
|
ae32645470 | ||
|
d4412fe06f | ||
|
b42c05fb56 | ||
|
8d0da61bd4 | ||
|
8b8be261cd | ||
|
2b6ceed897 | ||
|
8abca801a2 | ||
|
1eae4425d8 | ||
|
a6386bd60e | ||
|
1460f002ab | ||
|
0d6f736c2c | ||
|
da88e11d04 | ||
|
0a58767563 | ||
|
198525f2ce | ||
|
3220702034 | ||
|
262711a127 | ||
|
078438442a | ||
|
5ac20d65ac | ||
|
5d2f706c32 | ||
|
844a59d880 | ||
|
82c742f964 | ||
|
b73cfd8a60 | ||
|
8466ff0c1a | ||
|
1e0fc7eb0d | ||
|
74b0c3ea57 | ||
|
a735275864 | ||
|
8a1d46deb4 | ||
|
b624aeec45 | ||
|
e83b4d7d42 | ||
|
da302fce32 | ||
|
e09fec56e2 | ||
|
568f952573 | ||
|
fc1e488391 | ||
|
5781c258e0 | ||
|
13c542aeda | ||
|
02148f68e5 | ||
|
3c84002abd | ||
|
01aa57e493 | ||
|
af09b4214a | ||
|
43b6c71205 | ||
|
480e1adfbf | ||
|
8c04712784 | ||
|
0c61ba8f2d | ||
|
485f662d75 | ||
|
26caeec0c1 | ||
|
4a7cb88a3e | ||
|
d11696015d | ||
|
3b76ca4f9b | ||
|
6f3239d514 | ||
|
e8f60d39de | ||
|
a3bad8aec4 | ||
|
644aa26b75 | ||
|
ecd9828afc | ||
|
7462d61e16 | ||
|
b95ef7250d | ||
|
569dec1b0b | ||
|
1bc0270d7b | ||
|
ebfeebc40b | ||
|
ec80b25087 | ||
|
7a2278d7b6 | ||
|
28b93b70fa | ||
|
0431f45190 | ||
|
96ce444061 | ||
|
32776e7ca3 | ||
|
35d516ad4d | ||
|
e6a17d27b9 | ||
|
52edec1b64 | ||
|
6440d2289c | ||
|
11afadcea8 | ||
|
2e981987f2 | ||
|
69efca0bdb | ||
|
eefb92367e | ||
|
8fa96c2836 | ||
|
04dae52a6c | ||
|
37c9da1351 | ||
|
86d7ec6270 | ||
|
82f1e0fe32 | ||
|
6ec0aa8894 | ||
|
3dbe8bfbbf | ||
|
20d82bab78 | ||
|
60f6123b64 | ||
|
74887a58f6 | ||
|
76821d16fc | ||
|
233a6b5e90 | ||
|
a1d9337c81 | ||
|
e3fec6c3e4 | ||
|
5de88b4cc1 | ||
|
af5e6655e0 | ||
|
a8716799c9 | ||
|
1672027091 | ||
|
934ac886cb | ||
|
1b47c4d16f | ||
|
e8fd906aa1 | ||
|
57b6ea1297 | ||
|
102ff15a99 | ||
|
19148cfc84 | ||
|
97c2bab58a | ||
|
1f473228ce | ||
|
340a8e4176 | ||
|
67201a52cc | ||
|
30e164fe0a | ||
|
cfcb20a468 | ||
|
24eac7ef41 | ||
|
94162f1b45 | ||
|
01c19efc0c | ||
|
e4f2a8a23b | ||
|
89b33d2c62 | ||
|
3ced35a2f8 | ||
|
fd4576b234 | ||
|
812ad829c9 | ||
|
207a9ba723 | ||
|
f9bf17d701 | ||
|
43b96ccd1e | ||
|
14e8cdd9b2 | ||
|
259068b860 | ||
|
a1a4192835 | ||
|
45f09dcc8c | ||
|
15c3f11f4a | ||
|
1a21027850 | ||
|
8427a9fc68 | ||
|
f09b89f975 | ||
|
7bec84f767 | ||
|
8f4cbcf817 | ||
|
c3382274a2 | ||
|
493055f861 | ||
|
11fbbd49f3 | ||
|
8ce37d53cd | ||
|
baa4012872 | ||
|
16c5eba2be | ||
|
ec08cb32aa | ||
|
86fb1b938b | ||
|
5aa7097a6e | ||
|
fd3f520e95 | ||
|
49ff92892f | ||
|
904f5a2656 | ||
|
bb105dd832 | ||
|
fd934c0514 | ||
|
66609428a2 | ||
|
0056e0bc6d | ||
|
056b66a764 | ||
|
3438a5a374 | ||
|
9f3806dabf | ||
|
1d4d5cc4e7 | ||
|
3901543697 | ||
|
a4d73e2a67 | ||
|
f6854f58ff | ||
|
ad7f7d2890 | ||
|
498db63cac | ||
|
05659820d0 | ||
|
02779ef725 | ||
|
935c9b6a42 | ||
|
522f7644a3 | ||
|
b1a67d1fc5 | ||
|
0674c0aff6 | ||
|
c8664d5952 | ||
|
8ce1e2c956 | ||
|
90cca28e01 | ||
|
e489b7101c | ||
|
eee64c8064 | ||
|
a0d4e8dafc | ||
|
e6b94df06d | ||
|
6f1c7d6253 | ||
|
2d5a19b676 | ||
|
d1e8dded68 | ||
|
9888f66c84 | ||
|
5ec51d0ccc | ||
|
79f9331073 | ||
|
43bcf4ab98 | ||
|
f4eae72c48 | ||
|
9d14d3e5b7 | ||
|
26d3141489 | ||
|
5c44f51d6d | ||
|
50f3f32ba8 | ||
|
8fa858ca8c | ||
|
8b1d1671f7 | ||
|
73aa35ea2c | ||
|
a391445e5f | ||
|
530c5d416a | ||
|
319a13c0ef | ||
|
ec2fedd797 | ||
|
7489d19784 | ||
|
3b5a0e8d66 | ||
|
29ed63286c | ||
|
108da4b6d4 | ||
|
a00a1c64d1 | ||
|
275825cfe1 | ||
|
cd2fce56a6 | ||
|
cab850fb35 | ||
|
045dc600b0 | ||
|
1b199c1682 | ||
|
936fa637ec | ||
|
b9ccb7a892 | ||
|
95b33be3cf | ||
|
cde27b89c2 | ||
|
476e52e004 | ||
|
8dc88fe64b | ||
|
d00da790af | ||
|
c61fac75e4 | ||
|
e949af6fd5 | ||
|
c1e38b917e | ||
|
1244121216 | ||
|
46543daa13 | ||
|
6277384bf9 | ||
|
17549fed9c | ||
|
377e42affc | ||
|
0ac5d56865 | ||
|
b59ee6ad7e | ||
|
2d7c5a827f | ||
|
85c32ef843 | ||
|
dc089d7db9 | ||
|
d570f910f8 | ||
|
8f2731911b | ||
|
9d22420027 | ||
|
029da3791b | ||
|
caa51b1029 | ||
|
2973a03d40 | ||
|
679d464f4c | ||
|
93c0787efd | ||
|
a1708a1469 | ||
|
862bf78e63 | ||
|
8c01df50a2 | ||
|
921932920d | ||
|
d021c9b4ae | ||
|
309a18e9c0 | ||
|
73322c96a6 | ||
|
121e7db330 | ||
|
9f5e1b59fb | ||
|
3282ed4fea | ||
|
c7d9192f18 | ||
|
bd0f707aed | ||
|
8b30634ebe | ||
|
ca8acb14f1 | ||
|
26b8f8d0d5 | ||
|
a066153556 | ||
|
2ee7131c28 | ||
|
cb1f65732e | ||
|
37237d9c10 | ||
|
92cc41dec6 | ||
|
246dc663c4 | ||
|
4ed3235590 | ||
|
0939e405b4 | ||
|
584c51ef56 | ||
|
26ed05ba3a | ||
|
5bb72cfed8 | ||
|
85ed1b85ae | ||
|
1d2f929d3f | ||
|
ffbf0804d9 | ||
|
ca0b89ecd3 | ||
|
dc9d6f6b79 | ||
|
f73520559e | ||
|
a4df38d963 | ||
|
4a162543f6 | ||
|
d5caadd906 | ||
|
9d2d170c2d | ||
|
df3aa22c59 | ||
|
a4aabfcdae | ||
|
ce99e70bf9 | ||
|
9d249406e3 | ||
|
42d9e7a090 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
|
||||
"plugins": ["prettier"],
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"],
|
||||
"plugins": ["prettier", "jsdoc"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
|
5
.github/CONTRIBUTING.md
vendored
@@ -25,13 +25,14 @@ To run StyleLint, use `npm run lint:style`.
|
||||
Please only submit reproducible issues.
|
||||
|
||||
If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt)
|
||||
|
||||
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
|
||||
|
||||
When submitting a new issue, please supply the following information:
|
||||
|
||||
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3, Windows, Mac, Linux, System V UNIX).
|
||||
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX).
|
||||
|
||||
**Node Version**: Make sure it's version 0.12.13 or later.
|
||||
**Node Version**: Make sure it's version 10 or later.
|
||||
|
||||
**MagicMirror Version**: Now that the versions have split, tell us if you are using the PHP version (v1) or the newer JavaScript version (v2).
|
||||
|
||||
|
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: MichMich
|
||||
custom: ['https://magicmirror.builders/#donate']
|
6
.github/ISSUE_TEMPLATE.md
vendored
@@ -6,6 +6,8 @@ If you're not sure if it's a real bug or if it's just you, please open a topic o
|
||||
|
||||
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
|
||||
|
||||
A common problem is that your config file could be invalid. Please run in your MagicMirror directory: `npm run config:check` and see if it reports an error.
|
||||
|
||||
## I found a bug in the MagicMirror installer
|
||||
|
||||
If you are facing an issue or found a bug while trying to install MagicMirror via the installer please report it in the respective GitHub repository:
|
||||
@@ -23,9 +25,9 @@ If you are facing an issue or found a bug while running MagicMirror inside a Doc
|
||||
Please make sure to only submit reproducible issues. You can safely remove everything above the dividing line.
|
||||
When submitting a new issue, please supply the following information:
|
||||
|
||||
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3, Windows, Mac, Linux, System V UNIX).
|
||||
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX).
|
||||
|
||||
**Node Version**: Make sure it's version 8 or later.
|
||||
**Node Version**: Make sure it's version 10 or later.
|
||||
|
||||
**MagicMirror Version**: Please let us now which version of MagicMirror you are running. It can be found in the `package.log` file.
|
||||
|
||||
|
29
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,13 +1,28 @@
|
||||
> Please send your pull requests the develop branch.
|
||||
> Don't forget to add the change to CHANGELOG.md.
|
||||
Hello and thank you for wanting to contribute to the MagicMirror project
|
||||
|
||||
**Please make sure that you have followed these 4 rules before submitting your Pull Request:**
|
||||
|
||||
> 1) Base your pull requests against the `develop` branch.
|
||||
>
|
||||
>
|
||||
> 2) Include these infos in the description:
|
||||
> * Does the pull request solve a **related** issue?
|
||||
> * If so, can you reference the issue like this `Fixes #<issue_number>`?
|
||||
> * What does the pull request accomplish? Use a list if needed.
|
||||
> * If it includes major visual changes please add screenshots.
|
||||
>
|
||||
>
|
||||
> 3) Please run `npm run lint:prettier` before submitting so that
|
||||
> style issues are fixed.
|
||||
>
|
||||
>
|
||||
> 4) Don't forget to add an entry about your changes to
|
||||
> the CHANGELOG.md file.
|
||||
|
||||
|
||||
**Note**: Sometimes the development moves very fast. It is highly
|
||||
recommended that you update your branch of `develop` before creating a
|
||||
pull request to send us your changes. This makes everyone's lives
|
||||
easier (including yours) and helps us out on the development team.
|
||||
Thanks!
|
||||
|
||||
- Does the pull request solve a **related** issue?
|
||||
- If so, can you reference the issue?
|
||||
- What does the pull request accomplish? Use a list if needed.
|
||||
- If it includes major visual changes please add screenshots.
|
||||
Thanks again and have a nice day!
|
||||
|
24
.github/workflows/codecov-test-suites.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# This workflow runs the automated test and uploads the coverage results to codecov.io
|
||||
|
||||
name: "Run Codecov Tests"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, develop ]
|
||||
pull_request:
|
||||
branches: [ master, develop ]
|
||||
|
||||
jobs:
|
||||
run-and-upload-coverage-report:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
npm ci
|
||||
npm run test:coverage
|
||||
- uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
fail_ci_if_error: true
|
17
.github/workflows/enforce-changelog.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# This workflow enforces the update of a changelog file on every pull request
|
||||
|
||||
name: "Enforce Changelog"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: dangoslen/changelog-enforcer@v1.6.1
|
||||
with:
|
||||
changeLogPath: 'CHANGELOG.md'
|
||||
skipLabels: 'Skip Changelog'
|
32
.github/workflows/node-ci.js.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: "Run Automated Tests"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, develop ]
|
||||
pull_request:
|
||||
branches: [ master, develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
npm install
|
||||
npm run test:prettier
|
||||
npm run test:js
|
||||
npm run test:css
|
||||
npm run test:e2e
|
||||
npm run test:unit
|
10
.gitignore
vendored
@@ -1,5 +1,4 @@
|
||||
# Various Node ignoramuses.
|
||||
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
@@ -13,9 +12,11 @@ build/Release
|
||||
/node_modules/**/*
|
||||
fonts/node_modules/**/*
|
||||
vendor/node_modules/**/*
|
||||
!/tests/node_modules/**/*
|
||||
jspm_modules
|
||||
.npm
|
||||
.node_repl_history
|
||||
.nyc_output/
|
||||
|
||||
# Visual Studio Code ignoramuses.
|
||||
.vscode/
|
||||
@@ -53,7 +54,6 @@ Temporary Items
|
||||
.apdisk
|
||||
|
||||
# Various Linux ignoramuses.
|
||||
|
||||
.fuse_hidden*
|
||||
.directory
|
||||
.Trash-*
|
||||
@@ -67,6 +67,10 @@ Temporary Items
|
||||
# Ignore changes to the custom css files.
|
||||
/css/custom.css
|
||||
|
||||
# Ignore users config file but keep the sample.
|
||||
/config/*
|
||||
!/config/config.js.sample
|
||||
|
||||
# Vim
|
||||
## swap
|
||||
[._]*.s[a-w][a-z]
|
||||
@@ -76,5 +80,3 @@ Temporary Items
|
||||
*.orig
|
||||
*.rej
|
||||
*.bak
|
||||
|
||||
!/tests/node_modules/**/*
|
||||
|
@@ -1,5 +1,5 @@
|
||||
package-lock.json
|
||||
/config/**/*
|
||||
/modules/default/calendar/vendor/ical.js/**/*
|
||||
/vendor/**/*
|
||||
!/vendor/vendor.js
|
||||
.github/**/*
|
||||
|
25
.travis.yml
@@ -1,25 +0,0 @@
|
||||
dist: trusty
|
||||
language: node_js
|
||||
node_js:
|
||||
- 10
|
||||
- lts/*
|
||||
- node
|
||||
before_install:
|
||||
- npm i -g npm
|
||||
before_script:
|
||||
- yarn danger ci
|
||||
- "export DISPLAY=:99.0"
|
||||
- "export ELECTRON_DISABLE_SANDBOX=1"
|
||||
- "sh -e /etc/init.d/xvfb start"
|
||||
- sleep 5
|
||||
script:
|
||||
- npm run test:prettier
|
||||
- npm run test:js
|
||||
- npm run test:css
|
||||
- npm run test:e2e
|
||||
- npm run test:unit
|
||||
after_script:
|
||||
- npm list
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
171
CHANGELOG.md
@@ -5,6 +5,171 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror²
|
||||
|
||||
## [2.15.0] - 2021-04-01
|
||||
|
||||
Special thanks to the following contributors: @EdgardosReis, @MystaraTheGreat, @TheDuffman85, @ashishtank, @buxxi, @codac, @fewieden, @khassel, @klaernie, @qu1que, @rejas, @sdetweil & @thomasrockhu.
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
|
||||
|
||||
### Added
|
||||
|
||||
- Added Galician language.
|
||||
- Added GitHub workflows for automated testing and changelog enforcement.
|
||||
- Added CodeCov badge to Readme.
|
||||
- Added CURRENTWEATHER_TYPE notification to currentweather and weather module, use it in compliments module.
|
||||
- Added `start:dev` command to the npm scripts for starting electron with devTools open.
|
||||
- Added logging when using deprecated modules weatherforecast or currentweather.
|
||||
- Added Portuguese translations for "MODULE_CONFIG_CHANGED" and "PRECIP".
|
||||
- Respect parameter ColoredSymbolOnly also for custom events.
|
||||
- Added a new parameter to hide time portion on relative times.
|
||||
- `module.show` has now the option for a callback on error.
|
||||
- Added locale to sample config file.
|
||||
- Added support for self-signed certificates for the default calendar module (#466).
|
||||
- Added hiddenOnStartup flag to module config (#2475).
|
||||
|
||||
### Updated
|
||||
|
||||
- Updated markdown files for github.
|
||||
- Cleaned up old code on server side.
|
||||
- Convert `-0` to `0` when displaying temperature.
|
||||
- Code cleanup for FEELS like and added {DEGREE} placeholder for FEELSLIKE for each language.
|
||||
- Converted newsfeed module to use templates.
|
||||
- Updated documentation and help screen about invalid config files.
|
||||
- Moving weather provider specific code and configuration into each provider and making hourly part of the interface.
|
||||
- Bump electron to v11 and enable contextIsolation.
|
||||
- Don't update the DOM when a module is not displayed.
|
||||
- Cleaned up jsdoc and tests.
|
||||
- Exposed logger as node module for easier access for 3rd party modules.
|
||||
- Replaced deprecated `request` package with `node-fetch` and `digest-fetch`.
|
||||
- Refactored calendar fetcher.
|
||||
- Cleaned up newsfeed module.
|
||||
- Cleaned up translations and translator code.
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed danger.js library.
|
||||
- Removed `ical` which was substituted by `node-ical` in release `v2.13.0`. Module developers must install this dependency themselves in the module folder if needed.
|
||||
- Removed valid-url library.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Added default log levels to stop calendar log spamming.
|
||||
- Fix socket.io cors errors, see [breaking change since socket.io v3](https://socket.io/docs/v3/handling-cors/).
|
||||
- Fix Issue with weather forecast icons due to fixed day start and end time (#2221).
|
||||
- Fix empty directory for each module's main javascript file in the inspector.
|
||||
- Fix Issue with weather forecast icons unit tests with different timezones (#2221).
|
||||
- Fix issue with unencoded characters in translated strings when using nunjuck template (`Loading …` as an example).
|
||||
- Fix socket.io backward compatibility with socket v2 clients.
|
||||
- Fix 3rd party module language loading if language is English.
|
||||
- Fix e2e tests after spectron update.
|
||||
- Fix updatenotification creating zombie processes by setting a timeout for the git process.
|
||||
- Fix weather module openweathermap not loading if lat and lon set without onecall.
|
||||
- Fix calendar daylight savings offset calculation if recurring start date before 2007.
|
||||
- Fix calendar time/date adjustment when time with GMT offset is different day (#2488).
|
||||
- Fix calendar daylight savings offset calculation if recurring FULL DAY start date before 2007 (#2483).
|
||||
- Fix newsreaders template, for wrong test for nowrap in 2 places (should be if not).
|
||||
|
||||
## [2.14.0] - 2021-01-01
|
||||
|
||||
Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank, @bluemanos, @flopp999, @jakemulley, @jakobsarwary1, @marvai-vgtu, @mirontoli, @rejas, @sdetweil, @Snille & @Sub028.
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
|
||||
|
||||
### Added
|
||||
|
||||
- Added new log level "debug" to the logger.
|
||||
- Added new parameter "useKmh" to weather module for displaying wind speed as kmh.
|
||||
- Added Chuvash translation.
|
||||
- Added Weatherbit as a provider to Weather module.
|
||||
- Added SMHI as a provider to Weather module.
|
||||
- Added Hindi & Gujarati translation.
|
||||
- Added optional support for DEGREE position in Feels like translation.
|
||||
- Added support for variables in nunjucks templates for translate filter.
|
||||
- Added Chuvash translation.
|
||||
- Added new option "limitDays" - limit the number of discreet days displayed.
|
||||
- Added new option "customEvents" - use custom symbol/color based on keyword in event title.
|
||||
|
||||
### Updated
|
||||
|
||||
- Merging .gitignore in the config-folder with the .gitignore in the root-folder.
|
||||
- Weather module - forecast now show TODAY and TOMORROW instead of weekday, to make it easier to understand.
|
||||
- Update dependencies to latest versions.
|
||||
- Update dependencies eslint, feedme, simple-git and socket.io to latest versions.
|
||||
- Update lithuanian translation.
|
||||
- Update config sample.
|
||||
- Highlight required version mismatch.
|
||||
- No select Text for TouchScreen use.
|
||||
- Corrected logic for timeFormat "relative" and "absolute".
|
||||
- Added missing function call in module.show()
|
||||
- Translator variables can have falsy values (e.g. empty string)
|
||||
- Fix issue with weather module with DEGREE label in FEELS like
|
||||
|
||||
### Deleted
|
||||
|
||||
- Removed Travis CI integration.
|
||||
|
||||
### Fixed
|
||||
|
||||
- JSON Parse translation files with comments crashing UI. (#2149)
|
||||
- Calendar parsing where RRULE bug returns wrong date, add Windows timezone name support. (#2145, #2151)
|
||||
- Wrong node-ical version installed (package.json) requested version. (#2153)
|
||||
- Fix calendar fetcher subsequent timing. (#2160)
|
||||
- Rename Greek translation to correct ISO 639-1 alpha-2 code (gr > el). (#2155)
|
||||
- Add a space after icons of sunrise and sunset. (#2169)
|
||||
- Fix calendar when no DTEND record found in event, startDate overlay when endDate set. (#2177)
|
||||
- Fix windspeed convertion error in ukmetoffice weather provider. (#2189)
|
||||
- Fix console.debug not having timestamps. (#2199)
|
||||
- Fix calendar full day event east of UTC start time. (#2200)
|
||||
- Fix non-fullday recurring rule processing. (#2216)
|
||||
- Catch errors when parsing calendar data with ical. (#2022)
|
||||
- Fix Default Alert Module does not hide black overlay when alert is dismissed manually. (#2228)
|
||||
- Weather module - Always displays night icons when local is other than English. (#2221)
|
||||
- Update node-ical 0.12.4, fix invalid RRULE format in cal entries
|
||||
- Fix package.json for optional electron dependency (2378)
|
||||
- Update node-ical version again, 0.12.5, change RRULE fix (#2371, #2379)
|
||||
- Remove undefined objects from modules array (#2382)
|
||||
- Update node-ical version again, 0.12.7, change RRULE fix (#2371, #2379), node-ical now throws error (which we catch)
|
||||
- Update simple-git version to 2.31 unhandled promise rejection (#2383)
|
||||
|
||||
## [2.13.0] - 2020-10-01
|
||||
|
||||
Special thanks to the following contributors: @bryanzzhu, @bugsounet, @chamakura, @cjbrunner, @easyas314, @larryare, @oemel09, @rejas, @sdetweil & @sthuber90.
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
|
||||
|
||||
### Added
|
||||
|
||||
- `--dry-run` Added option in fetch call within updatenotification node_helper. This is to prevent
|
||||
MagicMirror from consuming any fetch result. Causes conflict with MMPM when attempting to check
|
||||
for updates to MagicMirror and/or MagicMirror modules.
|
||||
- Test coverage with Istanbul, run it with `npm run test:coverage`.
|
||||
- Added lithuanian language.
|
||||
- Added support in weatherforecast for OpenWeather onecall API.
|
||||
- Added config option to calendar-icons for recurring- and fullday-events.
|
||||
- Added current, hourly (max 48), and daily (max 7) weather forecasts to weather module via OpenWeatherMap One Call API.
|
||||
- Added eslint-plugin for jsdoc comments.
|
||||
- Added new configDeepMerge option for module developers.
|
||||
|
||||
### Updated
|
||||
|
||||
- Change incorrect weather.js default properties.
|
||||
- Cleaned up newsfeed module.
|
||||
- Cleaned up jsdoc comments.
|
||||
- Cleaned up clock tests.
|
||||
- Move lodash into devDependencies, update other dependencies.
|
||||
- Switch from ical to node-ical library.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix backward compatibility issues for Safari < 11.
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast depending on the endpoint (forecast/daily or forecast)". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
|
||||
- Fix calendar display. Account for current timezone. [#2068](https://github.com/MichMich/MagicMirror/issues/2068)
|
||||
- Fix logLevel being set before loading config.
|
||||
- Fix incorrect namespace links in svg clockfaces. [#2072](https://github.com/MichMich/MagicMirror/issues/2072)
|
||||
- Fix weather/providers/weathergov for API guidelines. [#2045](https://github.com/MichMich/MagicMirror/issues/2045)
|
||||
- Fix "undefined" in weather modules header. [#1985](https://github.com/MichMich/MagicMirror/issues/1985)
|
||||
- Fix #2110, #2111, #2118: Recurring full day events should not use timezone adjustment. Just compare month/day.
|
||||
|
||||
## [2.12.0] - 2020-07-01
|
||||
|
||||
Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryanzzhu, @chamakura, @DarthBrento, @Ekristoffe, @khassel, @Legion2, @ndom91, @radokristof, @rejas, @XBCreepinJesus & @ZoneMR.
|
||||
@@ -42,7 +207,8 @@ Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryan
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
|
||||
- Throw error when check_config fails. [#1928](https://github.com/MichMich/MagicMirror/issues/1928)
|
||||
- Bug fix related to 'maxEntries' not displaying Calendar events. [#2050](https://github.com/MichMich/MagicMirror/issues/2050)
|
||||
- Updated ical library to latest version. [#1926](https://github.com/MichMich/MagicMirror/issues/1926)
|
||||
- Updated ical library to the latest version. [#1926](https://github.com/MichMich/MagicMirror/issues/1926)
|
||||
- Fix config check after merge of prettier [#2109](https://github.com/MichMich/MagicMirror/issues/2109)
|
||||
|
||||
## [2.11.0] - 2020-04-01
|
||||
|
||||
@@ -95,6 +261,7 @@ For more information regarding this major change, please check issue [#1860](htt
|
||||
- Timestamp in log output now also contains the date
|
||||
- Turkish translation.
|
||||
- Option to configure the size of the currentweather module.
|
||||
- Changed "Gevoelstemperatuur" to "Voelt als" shorter text.
|
||||
|
||||
## [2.10.1] - 2020-01-10
|
||||
|
||||
@@ -334,7 +501,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
### Fixed
|
||||
|
||||
- Fixed gzip encoded calendar loading issue #1400.
|
||||
- Mixup between german and spanish translation for newsfeed.
|
||||
- Fixed 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.
|
||||
- Fix for weatherforecast rainfall rounding [#1374](https://github.com/MichMich/MagicMirror/issues/1374)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# The MIT License (MIT)
|
||||
|
||||
Copyright © 2016-2019 Michael Teeuw
|
||||
Copyright © 2016-2021 Michael Teeuw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
|
22
README.md
@@ -1,12 +1,13 @@
|
||||

|
||||
|
||||
<p align="center">
|
||||
<p style="text-align: center">
|
||||
<a href="https://david-dm.org/MichMich/MagicMirror"><img src="https://david-dm.org/MichMich/MagicMirror.svg" alt="Dependency Status"></a>
|
||||
<a href="https://david-dm.org/MichMich/MagicMirror#info=devDependencies"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a>
|
||||
<a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge"></a>
|
||||
<a href="https://david-dm.org/MichMich/MagicMirror?type=dev"><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" alt="CLI Best Practices"></a>
|
||||
<a href="https://codecov.io/gh/MichMich/MagicMirror"><img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg?token=LEG1KitZR6" alt="CodeCov Status"/></a>
|
||||
<a href="https://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
||||
<a href="https://travis-ci.com/MichMich/MagicMirror"><img src="https://travis-ci.com/MichMich/MagicMirror.svg" alt="Travis"></a>
|
||||
<a href="https://snyk.io/test/github/MichMich/MagicMirror"><img src="https://snyk.io/test/github/MichMich/MagicMirror/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/MichMich/MagicMirror" style="max-width:100%;"></a>
|
||||
<a href="https://github.com/MichMich/MagicMirror/actions?query=workflow%3A%22Automated+Tests%22"><img src="https://github.com/MichMich/MagicMirror/workflows/Automated%20Tests/badge.svg" alt="Tests"></a>
|
||||
<a href="https://codecov.io/gh/MichMich/MagicMirror"><img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg" /></a>
|
||||
</p>
|
||||
|
||||
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).
|
||||
@@ -28,7 +29,13 @@ For the full documentation including **[installation instructions](https://docs.
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
Contributions of all kinds are welcome, not only in the form of code but also with regards bug reports and documentation. For the full contribution guidelines, check out: [https://docs.magicmirror.builders/getting-started/contributing.html](https://docs.magicmirror.builders/getting-started/contributing.html)
|
||||
Contributions of all kinds are welcome, not only in the form of code but also with regards to
|
||||
|
||||
- bug reports
|
||||
- documentation
|
||||
- translations
|
||||
|
||||
For the full contribution guidelines, check out: [https://docs.magicmirror.builders/getting-started/contributing.html](https://docs.magicmirror.builders/getting-started/contributing.html)
|
||||
|
||||
## Enjoying MagicMirror? Consider a donation!
|
||||
|
||||
@@ -39,7 +46,6 @@ If we receive enough donations we might even be able to free up some working hou
|
||||
|
||||
To donate, please follow [this](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G5D8E9MR5DTD2&source=url) link.
|
||||
|
||||
<p align="center">
|
||||
<br>
|
||||
<p style="text-align: center">
|
||||
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
|
||||
</p>
|
||||
|
@@ -4,10 +4,18 @@
|
||||
(function () {
|
||||
var config = {};
|
||||
|
||||
// Helper function to get server address/hostname from either the commandline or env
|
||||
/**
|
||||
* Helper function to get server address/hostname from either the commandline or env
|
||||
*/
|
||||
function getServerAddress() {
|
||||
// Helper function to get command line parameters
|
||||
// Assumes that a cmdline parameter is defined with `--key [value]`
|
||||
/**
|
||||
* Get command line parameters
|
||||
* Assumes that a cmdline parameter is defined with `--key [value]`
|
||||
*
|
||||
* @param {string} key key to look for at the command line
|
||||
* @param {string} defaultValue value if no key is given at the command line
|
||||
* @returns {string} the value of the parameter
|
||||
*/
|
||||
function getCommandLineParameter(key, defaultValue = undefined) {
|
||||
var index = process.argv.indexOf(`--${key}`);
|
||||
var value = index > -1 ? process.argv[index + 1] : undefined;
|
||||
@@ -23,10 +31,16 @@
|
||||
config["tls"] = process.argv.indexOf("--use-tls") > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the config from the specified server url
|
||||
*
|
||||
* @param {string} url location where the server is running.
|
||||
* @returns {Promise} the config
|
||||
*/
|
||||
function getServerConfig(url) {
|
||||
// Return new pending promise
|
||||
return new Promise((resolve, reject) => {
|
||||
// Select http or https module, depending on reqested url
|
||||
// Select http or https module, depending on requested url
|
||||
const lib = url.startsWith("https") ? require("https") : require("http");
|
||||
const request = lib.get(url, (response) => {
|
||||
var configData = "";
|
||||
@@ -47,6 +61,12 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a message to the console in case of errors
|
||||
*
|
||||
* @param {string} message error message to print
|
||||
* @param {number} code error code for the exit call
|
||||
*/
|
||||
function fail(message, code = 1) {
|
||||
if (message !== undefined && typeof message === "string") {
|
||||
console.log(message);
|
||||
|
2
config/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
*
|
||||
!config.js.sample
|
@@ -28,7 +28,8 @@ var config = {
|
||||
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
|
||||
|
||||
language: "en",
|
||||
logLevel: ["INFO", "LOG", "WARN", "ERROR"],
|
||||
locale: "en-US",
|
||||
logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
// serverOnly: true/false/"local" ,
|
||||
@@ -66,22 +67,26 @@ var config = {
|
||||
position: "lower_third"
|
||||
},
|
||||
{
|
||||
module: "currentweather",
|
||||
module: "weather",
|
||||
position: "top_right",
|
||||
config: {
|
||||
weatherProvider: "openweathermap",
|
||||
type: "current",
|
||||
location: "New York",
|
||||
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"
|
||||
locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
||||
apiKey: "YOUR_OPENWEATHER_API_KEY"
|
||||
}
|
||||
},
|
||||
{
|
||||
module: "weatherforecast",
|
||||
module: "weather",
|
||||
position: "top_right",
|
||||
header: "Weather Forecast",
|
||||
config: {
|
||||
weatherProvider: "openweathermap",
|
||||
type: "forecast",
|
||||
location: "New York",
|
||||
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"
|
||||
apiKey: "YOUR_OPENWEATHER_API_KEY"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@@ -2,6 +2,7 @@ html {
|
||||
cursor: none;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
|
@@ -1,17 +0,0 @@
|
||||
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.");
|
||||
}
|
||||
|
||||
// Check if the PR request is send to the master branch.
|
||||
// This should only be done by MichMich.
|
||||
if (danger.github.pr.base.ref === "master" && danger.github.pr.user.login !== "MichMich") {
|
||||
// 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.");
|
||||
}
|
||||
}
|
201
js/app.js
@@ -4,22 +4,23 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var Log = require(__dirname + "/logger.js");
|
||||
var Server = require(__dirname + "/server.js");
|
||||
var Utils = require(__dirname + "/utils.js");
|
||||
var defaultModules = require(__dirname + "/../modules/default/defaultmodules.js");
|
||||
|
||||
// Alias modules mentioned in package.js under _moduleAliases.
|
||||
require("module-alias/register");
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Log = require("logger");
|
||||
const Server = require(`${__dirname}/server`);
|
||||
const Utils = require(`${__dirname}/utils`);
|
||||
const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`);
|
||||
|
||||
// Get version number.
|
||||
global.version = JSON.parse(fs.readFileSync("package.json", "utf8")).version;
|
||||
global.version = require(`${__dirname}/../package.json`).version;
|
||||
Log.log("Starting MagicMirror: v" + global.version);
|
||||
|
||||
// global absolute root path
|
||||
global.root_path = path.resolve(__dirname + "/../");
|
||||
global.root_path = path.resolve(`${__dirname}/../`);
|
||||
|
||||
if (process.env.MM_CONFIG_FILE) {
|
||||
global.configuration_file = process.env.MM_CONFIG_FILE;
|
||||
@@ -40,96 +41,97 @@ process.on("uncaughtException", function (err) {
|
||||
Log.error("If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues");
|
||||
});
|
||||
|
||||
/* App - The core app.
|
||||
*/
|
||||
var App = function () {
|
||||
var nodeHelpers = [];
|
||||
|
||||
/* loadConfig(callback)
|
||||
* Loads the config file. combines it with the defaults,
|
||||
* and runs the callback with the found config as argument.
|
||||
/**
|
||||
* The core app.
|
||||
*
|
||||
* argument callback function - The callback function.
|
||||
* @class
|
||||
*/
|
||||
var loadConfig = function (callback) {
|
||||
function App() {
|
||||
let nodeHelpers = [];
|
||||
|
||||
/**
|
||||
* Loads the config file. Combines it with the defaults, and runs the
|
||||
* callback with the found config as argument.
|
||||
*
|
||||
* @param {Function} callback Function to be called after loading the config
|
||||
*/
|
||||
function loadConfig(callback) {
|
||||
Log.log("Loading config ...");
|
||||
var defaults = require(__dirname + "/defaults.js");
|
||||
const defaults = require(`${__dirname}/defaults`);
|
||||
|
||||
// For this check proposed to TestSuite
|
||||
// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8
|
||||
var configFilename = path.resolve(global.root_path + "/config/config.js");
|
||||
if (typeof global.configuration_file !== "undefined") {
|
||||
configFilename = path.resolve(global.configuration_file);
|
||||
}
|
||||
const configFilename = path.resolve(global.configuration_file || `${global.root_path}/config/config.js`);
|
||||
|
||||
try {
|
||||
fs.accessSync(configFilename, fs.F_OK);
|
||||
var c = require(configFilename);
|
||||
const c = require(configFilename);
|
||||
checkDeprecatedOptions(c);
|
||||
var config = Object.assign(defaults, c);
|
||||
const config = Object.assign(defaults, c);
|
||||
callback(config);
|
||||
} catch (e) {
|
||||
if (e.code === "ENOENT") {
|
||||
Log.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) {
|
||||
Log.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));
|
||||
Log.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 {
|
||||
Log.error(Utils.colors.error("WARNING! Could not load config file. Starting with default configuration. Error found: " + e));
|
||||
Log.error(Utils.colors.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`));
|
||||
}
|
||||
callback(defaults);
|
||||
}
|
||||
};
|
||||
|
||||
var checkDeprecatedOptions = function (userConfig) {
|
||||
var deprecated = require(global.root_path + "/js/deprecated.js");
|
||||
var deprecatedOptions = deprecated.configs;
|
||||
|
||||
var usedDeprecated = [];
|
||||
|
||||
deprecatedOptions.forEach(function (option) {
|
||||
if (userConfig.hasOwnProperty(option)) {
|
||||
usedDeprecated.push(option);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Checks the config for deprecated options and throws a warning in the logs
|
||||
* if it encounters one option from the deprecated.js list
|
||||
*
|
||||
* @param {object} userConfig The user config
|
||||
*/
|
||||
function checkDeprecatedOptions(userConfig) {
|
||||
const deprecated = require(`${global.root_path}/js/deprecated`);
|
||||
const deprecatedOptions = deprecated.configs;
|
||||
|
||||
const usedDeprecated = deprecatedOptions.filter((option) => userConfig.hasOwnProperty(option));
|
||||
if (usedDeprecated.length > 0) {
|
||||
Log.warn(Utils.colors.warn("WARNING! Your config is using deprecated options: " + usedDeprecated.join(", ") + ". Check README and CHANGELOG for more up-to-date ways of getting the same functionality."));
|
||||
Log.warn(Utils.colors.warn(`WARNING! Your config is using deprecated options: ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* loadModule(module)
|
||||
/**
|
||||
* Loads a specific module.
|
||||
*
|
||||
* argument module string - The name of the module (including subpath).
|
||||
* @param {string} module The name of the module (including subpath).
|
||||
* @param {Function} callback Function to be called after loading
|
||||
*/
|
||||
var loadModule = function (module, callback) {
|
||||
var elements = module.split("/");
|
||||
var moduleName = elements[elements.length - 1];
|
||||
var moduleFolder = __dirname + "/../modules/" + module;
|
||||
function loadModule(module, callback) {
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = `${__dirname}/../modules/${module}`;
|
||||
|
||||
if (defaultModules.indexOf(moduleName) !== -1) {
|
||||
moduleFolder = __dirname + "/../modules/default/" + module;
|
||||
if (defaultModules.includes(moduleName)) {
|
||||
moduleFolder = `${__dirname}/../modules/default/${module}`;
|
||||
}
|
||||
|
||||
var helperPath = moduleFolder + "/node_helper.js";
|
||||
const helperPath = `${moduleFolder}/node_helper.js`;
|
||||
|
||||
var loadModule = true;
|
||||
let loadHelper = true;
|
||||
try {
|
||||
fs.accessSync(helperPath, fs.R_OK);
|
||||
} catch (e) {
|
||||
loadModule = false;
|
||||
Log.log("No helper found for module: " + moduleName + ".");
|
||||
loadHelper = false;
|
||||
Log.log(`No helper found for module: ${moduleName}.`);
|
||||
}
|
||||
|
||||
if (loadModule) {
|
||||
var Module = require(helperPath);
|
||||
var m = new Module();
|
||||
if (loadHelper) {
|
||||
const Module = require(helperPath);
|
||||
let m = new Module();
|
||||
|
||||
if (m.requiresVersion) {
|
||||
Log.log("Check MagicMirror version for node helper '" + moduleName + "' - Minimum version: " + m.requiresVersion + " - Current version: " + global.version);
|
||||
Log.log(`Check MagicMirror version for node helper '${moduleName}' - Minimum version: ${m.requiresVersion} - Current version: ${global.version}`);
|
||||
if (cmpVersions(global.version, m.requiresVersion) >= 0) {
|
||||
Log.log("Version is ok!");
|
||||
} else {
|
||||
Log.log("Version is incorrect. Skip module: '" + moduleName + "'");
|
||||
Log.warn(`Version is incorrect. Skip module: '${moduleName}'`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -142,19 +144,23 @@ var App = function () {
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* loadModules(modules)
|
||||
/**
|
||||
* Loads all modules.
|
||||
*
|
||||
* argument module string - The name of the module (including subpath).
|
||||
* @param {Module[]} modules All modules to be loaded
|
||||
* @param {Function} callback Function to be called after loading
|
||||
*/
|
||||
var loadModules = function (modules, callback) {
|
||||
function loadModules(modules, callback) {
|
||||
Log.log("Loading module helpers ...");
|
||||
|
||||
var loadNextModule = function () {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function loadNextModule() {
|
||||
if (modules.length > 0) {
|
||||
var nextModule = modules[0];
|
||||
const nextModule = modules[0];
|
||||
loadModule(nextModule, function () {
|
||||
modules = modules.slice(1);
|
||||
loadNextModule();
|
||||
@@ -164,23 +170,26 @@ var App = function () {
|
||||
Log.log("All module helpers loaded.");
|
||||
callback();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
loadNextModule();
|
||||
};
|
||||
}
|
||||
|
||||
/* cmpVersions(a,b)
|
||||
/**
|
||||
* Compare two semantic version numbers and return the difference.
|
||||
*
|
||||
* argument a string - Version number a.
|
||||
* argument a string - Version number b.
|
||||
* @param {string} a Version number a.
|
||||
* @param {string} b Version number b.
|
||||
*
|
||||
* @returns {number} A positive number if a is larger than b, a negative
|
||||
* number if a is smaller and 0 if they are the same
|
||||
*/
|
||||
function cmpVersions(a, b) {
|
||||
var i, diff;
|
||||
var regExStrip0 = /(\.0+)+$/;
|
||||
var segmentsA = a.replace(regExStrip0, "").split(".");
|
||||
var segmentsB = b.replace(regExStrip0, "").split(".");
|
||||
var l = Math.min(segmentsA.length, segmentsB.length);
|
||||
let i, diff;
|
||||
const regExStrip0 = /(\.0+)+$/;
|
||||
const segmentsA = a.replace(regExStrip0, "").split(".");
|
||||
const segmentsB = b.replace(regExStrip0, "").split(".");
|
||||
const l = Math.min(segmentsA.length, segmentsB.length);
|
||||
|
||||
for (i = 0; i < l; i++) {
|
||||
diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
|
||||
@@ -191,32 +200,33 @@ var App = function () {
|
||||
return segmentsA.length - segmentsB.length;
|
||||
}
|
||||
|
||||
/* start(callback)
|
||||
* This methods starts the core app.
|
||||
* It loads the config, then it loads all modules.
|
||||
* When it's done it executes the callback with the config as argument.
|
||||
/**
|
||||
* Start the core app.
|
||||
*
|
||||
* argument callback function - The callback function.
|
||||
* It loads the config, then it loads all modules. When it's done it
|
||||
* executes the callback with the config as argument.
|
||||
*
|
||||
* @param {Function} callback Function to be called after start
|
||||
*/
|
||||
this.start = function (callback) {
|
||||
loadConfig(function (c) {
|
||||
config = c;
|
||||
|
||||
var modules = [];
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
for (var m in config.modules) {
|
||||
var module = config.modules[m];
|
||||
if (modules.indexOf(module.module) === -1 && !module.disabled) {
|
||||
let modules = [];
|
||||
|
||||
for (const module of config.modules) {
|
||||
if (!modules.includes(module.module) && !module.disabled) {
|
||||
modules.push(module.module);
|
||||
}
|
||||
}
|
||||
|
||||
loadModules(modules, function () {
|
||||
var server = new Server(config, function (app, io) {
|
||||
const server = new Server(config, function (app, io) {
|
||||
Log.log("Server started ...");
|
||||
|
||||
for (var h in nodeHelpers) {
|
||||
var nodeHelper = nodeHelpers[h];
|
||||
for (let nodeHelper of nodeHelpers) {
|
||||
nodeHelper.setExpressApp(app);
|
||||
nodeHelper.setSocketIO(io);
|
||||
nodeHelper.start();
|
||||
@@ -232,21 +242,22 @@ var App = function () {
|
||||
});
|
||||
};
|
||||
|
||||
/* stop()
|
||||
* This methods stops the core app.
|
||||
* This calls each node_helper's STOP() function, if it exists.
|
||||
/**
|
||||
* Stops the core app. This calls each node_helper's STOP() function, if it
|
||||
* exists.
|
||||
*
|
||||
* Added to fix #1056
|
||||
*/
|
||||
this.stop = function () {
|
||||
for (var h in nodeHelpers) {
|
||||
var nodeHelper = nodeHelpers[h];
|
||||
for (const nodeHelper of nodeHelpers) {
|
||||
if (typeof nodeHelper.stop === "function") {
|
||||
nodeHelper.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* Listen for SIGINT signal and call stop() function.
|
||||
/**
|
||||
* Listen for SIGINT signal and call stop() function.
|
||||
*
|
||||
* Added to fix #1056
|
||||
* Note: this is only used if running `server-only`. Otherwise
|
||||
@@ -261,7 +272,9 @@ var App = function () {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
/* We also need to listen to SIGTERM signals so we stop everything when we are asked to stop by the OS.
|
||||
/**
|
||||
* Listen to SIGTERM signals so we can stop everything when we
|
||||
* are asked to stop by the OS.
|
||||
*/
|
||||
process.on("SIGTERM", () => {
|
||||
Log.log("[SIGTERM] Received. Shutting down server...");
|
||||
@@ -271,6 +284,6 @@ var App = function () {
|
||||
this.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = new App();
|
||||
|
@@ -11,24 +11,24 @@ const linter = new Linter();
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const rootPath = path.resolve(__dirname + "/../");
|
||||
const config = require(rootPath + "/.eslintrc.json");
|
||||
const Log = require(rootPath + "/js/logger.js");
|
||||
const Utils = require(rootPath + "/js/utils.js");
|
||||
const rootPath = path.resolve(`${__dirname}/../`);
|
||||
const Log = require(`${rootPath}/js/logger.js`);
|
||||
const Utils = require(`${rootPath}/js/utils.js`);
|
||||
|
||||
/* getConfigFile()
|
||||
* Return string with path of configuration file
|
||||
/**
|
||||
* Returns a string with path of configuration file.
|
||||
* Check if set by environment variable MM_CONFIG_FILE
|
||||
*
|
||||
* @returns {string} path and filename of the config file
|
||||
*/
|
||||
function getConfigFile() {
|
||||
// FIXME: This function should be in core. Do you want refactor me ;) ?, be good!
|
||||
let configFileName = path.resolve(rootPath + "/config/config.js");
|
||||
if (process.env.MM_CONFIG_FILE) {
|
||||
configFileName = path.resolve(process.env.MM_CONFIG_FILE);
|
||||
}
|
||||
return configFileName;
|
||||
return path.resolve(process.env.MM_CONFIG_FILE || `${rootPath}/config/config.js`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the config file using eslint.
|
||||
*/
|
||||
function checkConfigFile() {
|
||||
const configFileName = getConfigFile();
|
||||
|
||||
@@ -38,11 +38,11 @@ function checkConfigFile() {
|
||||
throw new Error("No config file present!");
|
||||
}
|
||||
|
||||
// check permission
|
||||
// Check permission
|
||||
try {
|
||||
fs.accessSync(configFileName, fs.F_OK);
|
||||
} catch (e) {
|
||||
Log.log(Utils.colors.error(e));
|
||||
Log.error(Utils.colors.error(e));
|
||||
throw new Error("No permission to access config file!");
|
||||
}
|
||||
|
||||
@@ -50,22 +50,18 @@ function checkConfigFile() {
|
||||
Log.info(Utils.colors.info("Checking file... "), configFileName);
|
||||
|
||||
// I'm not sure if all ever is utf-8
|
||||
fs.readFile(configFileName, "utf-8", function (err, data) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
const messages = linter.verify(data, config);
|
||||
if (messages.length === 0) {
|
||||
Log.log("Your configuration file doesn't contain syntax errors :)");
|
||||
return true;
|
||||
const configFile = fs.readFileSync(configFileName, "utf-8");
|
||||
|
||||
const errors = linter.verify(configFile);
|
||||
if (errors.length === 0) {
|
||||
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
|
||||
} else {
|
||||
// In case the there errors show messages and return
|
||||
messages.forEach((error) => {
|
||||
Log.log("Line", error.line, "col", error.column, error.message);
|
||||
});
|
||||
throw new Error("Wrong syntax in config file!");
|
||||
Log.error(Utils.colors.error("Your configuration file contains syntax errors :("));
|
||||
|
||||
for (const error of errors) {
|
||||
Log.error(`Line ${error.line} column ${error.column}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checkConfigFile();
|
||||
|
13
js/class.js
@@ -57,7 +57,9 @@
|
||||
: prop[name];
|
||||
}
|
||||
|
||||
// The dummy class constructor
|
||||
/**
|
||||
* The dummy class constructor
|
||||
*/
|
||||
function Class() {
|
||||
// All construction is actually done in the init method
|
||||
if (!initializing && this.init) {
|
||||
@@ -78,8 +80,13 @@
|
||||
};
|
||||
})();
|
||||
|
||||
//Define the clone method for later use.
|
||||
//Helper Method
|
||||
/**
|
||||
* Define the clone method for later use. Helper Method.
|
||||
*
|
||||
* @param {object} obj Object to be cloned
|
||||
*
|
||||
* @returns {object} the cloned object
|
||||
*/
|
||||
function cloneObject(obj) {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj;
|
||||
|
@@ -20,6 +20,7 @@ var defaults = {
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
logLevel: ["INFO", "LOG", "WARN", "ERROR"],
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
zoom: 1,
|
||||
@@ -42,7 +43,7 @@ var defaults = {
|
||||
module: "helloworld",
|
||||
position: "middle_center",
|
||||
config: {
|
||||
text: "Please create a config file."
|
||||
text: "Please create a config file or check the existing one for errors."
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58,7 +59,7 @@ var defaults = {
|
||||
position: "middle_center",
|
||||
classes: "xsmall",
|
||||
config: {
|
||||
text: "If you get this message while your config file is already<br>created, your config file probably contains an error.<br>Use a JavaScript linter to validate your file."
|
||||
text: "If you get this message while your config file is already created,<br>" + "it probably contains an error. To validate your config file run in your MagicMirror directory<br>" + "<pre>npm run config:check</pre>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@@ -6,11 +6,6 @@
|
||||
* Olex S. original idea this deprecated option
|
||||
*/
|
||||
|
||||
var deprecated = {
|
||||
module.exports = {
|
||||
configs: ["kioskmode"]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = deprecated;
|
||||
}
|
||||
|
@@ -2,10 +2,10 @@
|
||||
|
||||
const electron = require("electron");
|
||||
const core = require("./app.js");
|
||||
const Log = require("./logger.js");
|
||||
const Log = require("logger");
|
||||
|
||||
// Config
|
||||
var config = process.env.config ? JSON.parse(process.env.config) : {};
|
||||
let config = process.env.config ? JSON.parse(process.env.config) : {};
|
||||
// Module to control application life.
|
||||
const app = electron.app;
|
||||
// Module to create native browser window.
|
||||
@@ -15,15 +15,19 @@ const BrowserWindow = electron.BrowserWindow;
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
let mainWindow;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function createWindow() {
|
||||
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
|
||||
var electronOptionsDefaults = {
|
||||
let electronOptionsDefaults = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
x: 0,
|
||||
y: 0,
|
||||
darkTheme: true,
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
zoomFactor: config.zoom
|
||||
},
|
||||
@@ -39,7 +43,7 @@ function createWindow() {
|
||||
electronOptionsDefaults.autoHideMenuBar = true;
|
||||
}
|
||||
|
||||
var electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
|
||||
const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
|
||||
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow(electronOptions);
|
||||
@@ -47,14 +51,14 @@ function createWindow() {
|
||||
// and load the index.html of the app.
|
||||
// If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
|
||||
|
||||
var prefix;
|
||||
let prefix;
|
||||
if (config["tls"] !== null && config["tls"]) {
|
||||
prefix = "https://";
|
||||
} else {
|
||||
prefix = "http://";
|
||||
}
|
||||
|
||||
var address = (config.address === void 0) | (config.address === "") ? (config.address = "localhost") : config.address;
|
||||
let address = (config.address === void 0) | (config.address === "") ? (config.address = "localhost") : config.address;
|
||||
mainWindow.loadURL(`${prefix}${address}:${config.port}`);
|
||||
|
||||
// Open the DevTools if run with "npm start dev"
|
||||
@@ -122,7 +126,7 @@ app.on("before-quit", (event) => {
|
||||
|
||||
// Start the core application if server is run on localhost
|
||||
// This starts all node helpers and starts the webserver.
|
||||
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) > -1) {
|
||||
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) {
|
||||
core.start(function (c) {
|
||||
config = c;
|
||||
});
|
||||
|
56
js/loader.js
@@ -15,7 +15,7 @@ var Loader = (function () {
|
||||
|
||||
/* Private Methods */
|
||||
|
||||
/* loadModules()
|
||||
/**
|
||||
* Loops thru all modules and requests load for every module.
|
||||
*/
|
||||
var loadModules = function () {
|
||||
@@ -43,7 +43,7 @@ var Loader = (function () {
|
||||
loadNextModule();
|
||||
};
|
||||
|
||||
/* startModules()
|
||||
/**
|
||||
* Loops thru all modules and requests start for every module.
|
||||
*/
|
||||
var startModules = function () {
|
||||
@@ -54,21 +54,29 @@ var Loader = (function () {
|
||||
|
||||
// Notify core of loaded modules.
|
||||
MM.modulesStarted(moduleObjects);
|
||||
|
||||
// Starting modules also hides any modules that have requested to be initially hidden
|
||||
for (let thisModule of moduleObjects) {
|
||||
if (thisModule.data.hiddenOnStartup) {
|
||||
Log.info("Initially hiding " + thisModule.name);
|
||||
thisModule.hide();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* getAllModules()
|
||||
/**
|
||||
* Retrieve list of all modules.
|
||||
*
|
||||
* return array - module data as configured in config
|
||||
* @returns {object[]} module data as configured in config
|
||||
*/
|
||||
var getAllModules = function () {
|
||||
return config.modules;
|
||||
};
|
||||
|
||||
/* getModuleData()
|
||||
/**
|
||||
* Generate array with module information including module paths.
|
||||
*
|
||||
* return array - Module information.
|
||||
* @returns {object[]} Module information.
|
||||
*/
|
||||
var getModuleData = function () {
|
||||
var modules = getAllModules();
|
||||
@@ -97,7 +105,9 @@ var Loader = (function () {
|
||||
path: moduleFolder + "/",
|
||||
file: moduleName + ".js",
|
||||
position: moduleData.position,
|
||||
hiddenOnStartup: moduleData.hiddenOnStartup,
|
||||
header: moduleData.header,
|
||||
configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false,
|
||||
config: moduleData.config,
|
||||
classes: typeof moduleData.classes !== "undefined" ? moduleData.classes + " " + module : module
|
||||
});
|
||||
@@ -106,14 +116,14 @@ var Loader = (function () {
|
||||
return moduleFiles;
|
||||
};
|
||||
|
||||
/* loadModule(module)
|
||||
* Load modules via ajax request and create module objects.
|
||||
/**
|
||||
* Load modules via ajax request and create module objects.s
|
||||
*
|
||||
* argument callback function - Function called when done.
|
||||
* argument module object - Information about the module we want to load.
|
||||
* @param {object} module Information about the module we want to load.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
var loadModule = function (module, callback) {
|
||||
var url = module.path + "/" + module.file;
|
||||
var url = module.path + module.file;
|
||||
|
||||
var afterLoad = function () {
|
||||
var moduleObject = Module.create(module.name);
|
||||
@@ -136,12 +146,12 @@ var Loader = (function () {
|
||||
}
|
||||
};
|
||||
|
||||
/* bootstrapModule(module, mObj)
|
||||
/**
|
||||
* Bootstrap modules by setting the module data and loading the scripts & styles.
|
||||
*
|
||||
* argument module object - Information about the module we want to load.
|
||||
* argument mObj object - Modules instance.
|
||||
* argument callback function - Function called when done.
|
||||
* @param {object} module Information about the module we want to load.
|
||||
* @param {Module} mObj Modules instance.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
var bootstrapModule = function (module, mObj, callback) {
|
||||
Log.info("Bootstrapping module: " + module.name);
|
||||
@@ -161,11 +171,11 @@ var Loader = (function () {
|
||||
});
|
||||
};
|
||||
|
||||
/* loadFile(fileName)
|
||||
/**
|
||||
* Load a script or stylesheet by adding it to the dom.
|
||||
*
|
||||
* argument fileName string - Path of the file we want to load.
|
||||
* argument callback function - Function called when done.
|
||||
* @param {string} fileName Path of the file we want to load.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
var loadFile = function (fileName, callback) {
|
||||
var extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1);
|
||||
@@ -215,20 +225,20 @@ var Loader = (function () {
|
||||
|
||||
/* Public Methods */
|
||||
return {
|
||||
/* loadModules()
|
||||
/**
|
||||
* Load all modules as defined in the config.
|
||||
*/
|
||||
loadModules: function () {
|
||||
loadModules();
|
||||
},
|
||||
|
||||
/* loadFile()
|
||||
/**
|
||||
* Load a file (script or stylesheet).
|
||||
* Prevent double loading and search for files in the vendor folder.
|
||||
*
|
||||
* argument fileName string - Path of the file we want to load.
|
||||
* argument module Module Object - the module that calls the loadFile function.
|
||||
* argument callback function - Function called when done.
|
||||
* @param {string} fileName Path of the file we want to load.
|
||||
* @param {Module} module The module that calls the loadFile function.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
loadFile: function (fileName, module, callback) {
|
||||
if (loadedFiles.indexOf(fileName.toLowerCase()) !== -1) {
|
||||
|
18
js/logger.js
@@ -10,7 +10,10 @@
|
||||
(function (root, factory) {
|
||||
if (typeof exports === "object") {
|
||||
// add timestamps in front of log messages
|
||||
require("console-stamp")(console, "yyyy-mm-dd HH:MM:ss.l");
|
||||
require("console-stamp")(console, {
|
||||
pattern: "yyyy-mm-dd HH:MM:ss.l",
|
||||
include: ["debug", "log", "info", "warn", "error"]
|
||||
});
|
||||
|
||||
// Node, CommonJS-like
|
||||
module.exports = factory(root.config);
|
||||
@@ -19,11 +22,12 @@
|
||||
root.Log = factory(root.config);
|
||||
}
|
||||
})(this, function (config) {
|
||||
let logLevel = {
|
||||
info: Function.prototype.bind.call(console.info, console),
|
||||
const logLevel = {
|
||||
debug: Function.prototype.bind.call(console.debug, console),
|
||||
log: Function.prototype.bind.call(console.log, console),
|
||||
error: Function.prototype.bind.call(console.error, console),
|
||||
info: Function.prototype.bind.call(console.info, console),
|
||||
warn: Function.prototype.bind.call(console.warn, console),
|
||||
error: Function.prototype.bind.call(console.error, console),
|
||||
group: Function.prototype.bind.call(console.group, console),
|
||||
groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console),
|
||||
groupEnd: Function.prototype.bind.call(console.groupEnd, console),
|
||||
@@ -32,13 +36,15 @@
|
||||
timeStamp: Function.prototype.bind.call(console.timeStamp, console)
|
||||
};
|
||||
|
||||
if (config && config.logLevel) {
|
||||
logLevel.setLogLevel = function (newLevel) {
|
||||
if (newLevel) {
|
||||
Object.keys(logLevel).forEach(function (key, index) {
|
||||
if (!config.logLevel.includes(key.toLocaleUpperCase())) {
|
||||
if (!newLevel.includes(key.toLocaleUpperCase())) {
|
||||
logLevel[key] = function () {};
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return logLevel;
|
||||
});
|
||||
|
194
js/main.js
@@ -11,9 +11,8 @@ var MM = (function () {
|
||||
|
||||
/* Private Methods */
|
||||
|
||||
/* createDomObjects()
|
||||
* Create dom objects for all modules that
|
||||
* are configured for a specific position.
|
||||
/**
|
||||
* Create dom objects for all modules that are configured for a specific position.
|
||||
*/
|
||||
var createDomObjects = function () {
|
||||
var domCreationPromises = [];
|
||||
@@ -42,7 +41,9 @@ var MM = (function () {
|
||||
dom.appendChild(moduleHeader);
|
||||
|
||||
if (typeof module.getHeader() === "undefined" || module.getHeader() !== "") {
|
||||
moduleHeader.style = "display: none;";
|
||||
moduleHeader.style.display = "none;";
|
||||
} else {
|
||||
moduleHeader.style.display = "block;";
|
||||
}
|
||||
|
||||
var moduleContent = document.createElement("div");
|
||||
@@ -65,10 +66,12 @@ var MM = (function () {
|
||||
});
|
||||
};
|
||||
|
||||
/* selectWrapper(position)
|
||||
/**
|
||||
* Select the wrapper dom object for a specific position.
|
||||
*
|
||||
* argument position string - The name of the position.
|
||||
* @param {string} position The name of the position.
|
||||
*
|
||||
* @returns {HTMLElement} the wrapper element
|
||||
*/
|
||||
var selectWrapper = function (position) {
|
||||
var classes = position.replace("_", " ");
|
||||
@@ -81,13 +84,13 @@ var MM = (function () {
|
||||
}
|
||||
};
|
||||
|
||||
/* sendNotification(notification, payload, sender)
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* argument sender Module - The module that sent the notification.
|
||||
* argument sendTo Module - The module to send the notification to. (optional)
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
* @param {Module} [sendTo] The (optional) module to send the notification to.
|
||||
*/
|
||||
var sendNotification = function (notification, payload, sender, sendTo) {
|
||||
for (var m in modules) {
|
||||
@@ -98,13 +101,13 @@ var MM = (function () {
|
||||
}
|
||||
};
|
||||
|
||||
/* updateDom(module, speed)
|
||||
/**
|
||||
* Update the dom for a specific module.
|
||||
*
|
||||
* argument module Module - The module that needs an update.
|
||||
* argument speed Number - The number of microseconds for the animation. (optional)
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The (optional) number of microseconds for the animation.
|
||||
*
|
||||
* return Promise - Resolved when the dom is fully updated.
|
||||
* @returns {Promise} Resolved when the dom is fully updated.
|
||||
*/
|
||||
var updateDom = function (module, speed) {
|
||||
return new Promise(function (resolve) {
|
||||
@@ -126,15 +129,15 @@ var MM = (function () {
|
||||
});
|
||||
};
|
||||
|
||||
/* updateDomWithContent(module, speed, newHeader, newContent)
|
||||
/**
|
||||
* Update the dom with the specified content
|
||||
*
|
||||
* argument module Module - The module that needs an update.
|
||||
* argument speed Number - The number of microseconds for the animation. (optional)
|
||||
* argument newHeader String - The new header that is generated.
|
||||
* argument newContent Domobject - The new content that is generated.
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The (optional) number of microseconds for the animation.
|
||||
* @param {string} newHeader The new header that is generated.
|
||||
* @param {HTMLElement} newContent The new content that is generated.
|
||||
*
|
||||
* return Promise - Resolved when the module dom has been updated.
|
||||
* @returns {Promise} Resolved when the module dom has been updated.
|
||||
*/
|
||||
var updateDomWithContent = function (module, speed, newHeader, newContent) {
|
||||
return new Promise(function (resolve) {
|
||||
@@ -165,14 +168,14 @@ var MM = (function () {
|
||||
});
|
||||
};
|
||||
|
||||
/* moduleNeedsUpdate(module, newContent)
|
||||
/**
|
||||
* Check if the content has changed.
|
||||
*
|
||||
* argument module Module - The module to check.
|
||||
* argument newHeader String - The new header that is generated.
|
||||
* argument newContent Domobject - The new content that is generated.
|
||||
* @param {Module} module The module to check.
|
||||
* @param {string} newHeader The new header that is generated.
|
||||
* @param {HTMLElement} newContent The new content that is generated.
|
||||
*
|
||||
* return bool - Does the module need an update?
|
||||
* @returns {boolean} True if the module need an update, false otherwise
|
||||
*/
|
||||
var moduleNeedsUpdate = function (module, newHeader, newContent) {
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
@@ -197,12 +200,12 @@ var MM = (function () {
|
||||
return headerNeedsUpdate || contentNeedsUpdate;
|
||||
};
|
||||
|
||||
/* moduleNeedsUpdate(module, newContent)
|
||||
/**
|
||||
* Update the content of a module on screen.
|
||||
*
|
||||
* argument module Module - The module to check.
|
||||
* argument newHeader String - The new header that is generated.
|
||||
* argument newContent Domobject - The new content that is generated.
|
||||
* @param {Module} module The module to check.
|
||||
* @param {string} newHeader The new header that is generated.
|
||||
* @param {HTMLElement} newContent The new content that is generated.
|
||||
*/
|
||||
var updateModuleContent = function (module, newHeader, newContent) {
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
@@ -216,15 +219,20 @@ var MM = (function () {
|
||||
contentWrapper[0].appendChild(newContent);
|
||||
|
||||
headerWrapper[0].innerHTML = newHeader;
|
||||
headerWrapper[0].style = headerWrapper.length > 0 && newHeader ? undefined : "display: none;";
|
||||
if (headerWrapper.length > 0 && newHeader) {
|
||||
headerWrapper[0].style.display = "block";
|
||||
} else {
|
||||
headerWrapper[0].style.display = "none";
|
||||
}
|
||||
};
|
||||
|
||||
/* hideModule(module, speed, callback)
|
||||
/**
|
||||
* Hide the module.
|
||||
*
|
||||
* argument module Module - The module to hide.
|
||||
* argument speed Number - The speed of the hide animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* @param {Module} module The module to hide.
|
||||
* @param {number} speed The speed of the hide animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the hide method.
|
||||
*/
|
||||
var hideModule = function (module, speed, callback, options) {
|
||||
options = options || {};
|
||||
@@ -264,12 +272,13 @@ var MM = (function () {
|
||||
}
|
||||
};
|
||||
|
||||
/* showModule(module, speed, callback)
|
||||
/**
|
||||
* Show the module.
|
||||
*
|
||||
* argument module Module - The module to show.
|
||||
* argument speed Number - The speed of the show animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* @param {Module} module The module to show.
|
||||
* @param {number} speed The speed of the show animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the show method.
|
||||
*/
|
||||
var showModule = function (module, speed, callback, options) {
|
||||
options = options || {};
|
||||
@@ -286,6 +295,9 @@ var MM = (function () {
|
||||
// Otherwise cancel show action.
|
||||
if (module.lockStrings.length !== 0 && options.force !== true) {
|
||||
Log.log("Will not show " + module.name + ". LockStrings active: " + module.lockStrings.join(","));
|
||||
if (typeof options.onError === "function") {
|
||||
options.onError(new Error("LOCK_STRING_ACTIVE"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -323,7 +335,7 @@ var MM = (function () {
|
||||
}
|
||||
};
|
||||
|
||||
/* updateWrapperStates()
|
||||
/**
|
||||
* Checks for all positions if it has visible content.
|
||||
* If not, if will hide the position to prevent unwanted margins.
|
||||
* This method should be called by the show and hide methods.
|
||||
@@ -352,8 +364,8 @@ var MM = (function () {
|
||||
});
|
||||
};
|
||||
|
||||
/* loadConfig()
|
||||
* Loads the core config and combines it with de system defaults.
|
||||
/**
|
||||
* Loads the core config and combines it with the system defaults.
|
||||
*/
|
||||
var loadConfig = function () {
|
||||
// FIXME: Think about how to pass config around without breaking tests
|
||||
@@ -368,41 +380,41 @@ var MM = (function () {
|
||||
/* eslint-enable */
|
||||
};
|
||||
|
||||
/* setSelectionMethodsForModules()
|
||||
/**
|
||||
* Adds special selectors on a collection of modules.
|
||||
*
|
||||
* argument modules array - Array of modules.
|
||||
* @param {Module[]} modules Array of modules.
|
||||
*/
|
||||
var setSelectionMethodsForModules = function (modules) {
|
||||
/* withClass(className)
|
||||
* calls modulesByClass to filter modules with the specified classes.
|
||||
/**
|
||||
* Filter modules with the specified classes.
|
||||
*
|
||||
* argument className string/array - one or multiple classnames. (array or space divided)
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
*
|
||||
* return array - Filtered collection of modules.
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var withClass = function (className) {
|
||||
return modulesByClass(className, true);
|
||||
};
|
||||
|
||||
/* exceptWithClass(className)
|
||||
* calls modulesByClass to filter modules without the specified classes.
|
||||
/**
|
||||
* Filter modules without the specified classes.
|
||||
*
|
||||
* argument className string/array - one or multiple classnames. (array or space divided)
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
*
|
||||
* return array - Filtered collection of modules.
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var exceptWithClass = function (className) {
|
||||
return modulesByClass(className, false);
|
||||
};
|
||||
|
||||
/* modulesByClass(className, include)
|
||||
* filters a collection of modules based on classname(s).
|
||||
/**
|
||||
* Filters a collection of modules based on classname(s).
|
||||
*
|
||||
* argument className string/array - one or multiple classnames. (array or space divided)
|
||||
* argument include boolean - if the filter should include or exclude the modules with the specific classes.
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
* @param {boolean} include if the filter should include or exclude the modules with the specific classes.
|
||||
*
|
||||
* return array - Filtered collection of modules.
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var modulesByClass = function (className, include) {
|
||||
var searchClasses = className;
|
||||
@@ -427,12 +439,11 @@ var MM = (function () {
|
||||
return newModules;
|
||||
};
|
||||
|
||||
/* exceptModule(module)
|
||||
/**
|
||||
* Removes a module instance from the collection.
|
||||
*
|
||||
* argument module Module object - The module instance to remove from the collection.
|
||||
*
|
||||
* return array - Filtered collection of modules.
|
||||
* @param {object} module The module instance to remove from the collection.
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var exceptModule = function (module) {
|
||||
var newModules = modules.filter(function (mod) {
|
||||
@@ -443,10 +454,10 @@ var MM = (function () {
|
||||
return newModules;
|
||||
};
|
||||
|
||||
/* enumerate(callback)
|
||||
/**
|
||||
* Walks thru a collection of modules and executes the callback with the module as an argument.
|
||||
*
|
||||
* argument callback function - The function to execute with the module as an argument.
|
||||
* @param {Function} callback The function to execute with the module as an argument.
|
||||
*/
|
||||
var enumerate = function (callback) {
|
||||
modules.map(function (module) {
|
||||
@@ -471,27 +482,27 @@ var MM = (function () {
|
||||
return {
|
||||
/* Public Methods */
|
||||
|
||||
/* init()
|
||||
/**
|
||||
* Main init method.
|
||||
*/
|
||||
init: function () {
|
||||
Log.info("Initializing MagicMirror.");
|
||||
loadConfig();
|
||||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
Translator.loadCoreTranslations(config.language);
|
||||
Loader.loadModules();
|
||||
},
|
||||
|
||||
/* modulesStarted(moduleObjects)
|
||||
/**
|
||||
* Gets called when all modules are started.
|
||||
*
|
||||
* argument moduleObjects array<Module> - All module instances.
|
||||
* @param {Module[]} moduleObjects All module instances.
|
||||
*/
|
||||
modulesStarted: function (moduleObjects) {
|
||||
modules = [];
|
||||
for (var m in moduleObjects) {
|
||||
var module = moduleObjects[m];
|
||||
modules[module.data.index] = module;
|
||||
}
|
||||
moduleObjects.forEach((module) => modules.push(module));
|
||||
|
||||
Log.info("All modules started!");
|
||||
sendNotification("ALL_MODULES_STARTED");
|
||||
@@ -499,12 +510,12 @@ var MM = (function () {
|
||||
createDomObjects();
|
||||
},
|
||||
|
||||
/* sendNotification(notification, payload, sender)
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* argument sender Module - The module that sent the notification.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
*/
|
||||
sendNotification: function (notification, payload, sender) {
|
||||
if (arguments.length < 3) {
|
||||
@@ -526,11 +537,11 @@ var MM = (function () {
|
||||
sendNotification(notification, payload, sender);
|
||||
},
|
||||
|
||||
/* updateDom(module, speed)
|
||||
/**
|
||||
* Update the dom for a specific module.
|
||||
*
|
||||
* argument module Module - The module that needs an update.
|
||||
* argument speed Number - The number of microseconds for the animation. (optional)
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The number of microseconds for the animation.
|
||||
*/
|
||||
updateDom: function (module, speed) {
|
||||
if (!(module instanceof Module)) {
|
||||
@@ -538,40 +549,45 @@ var MM = (function () {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!module.data.position) {
|
||||
Log.warn("module tries to update the DOM without being displayed.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Further implementation is done in the private method.
|
||||
updateDom(module, speed);
|
||||
},
|
||||
|
||||
/* getModules(module, speed)
|
||||
/**
|
||||
* Returns a collection of all modules currently active.
|
||||
*
|
||||
* return array - A collection of all modules currently active.
|
||||
* @returns {Module[]} A collection of all modules currently active.
|
||||
*/
|
||||
getModules: function () {
|
||||
setSelectionMethodsForModules(modules);
|
||||
return modules;
|
||||
},
|
||||
|
||||
/* hideModule(module, speed, callback)
|
||||
/**
|
||||
* Hide the module.
|
||||
*
|
||||
* argument module Module - The module hide.
|
||||
* argument speed Number - The speed of the hide animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* argument options object - Optional settings for the hide method.
|
||||
* @param {Module} module The module to hide.
|
||||
* @param {number} speed The speed of the hide animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the hide method.
|
||||
*/
|
||||
hideModule: function (module, speed, callback, options) {
|
||||
module.hidden = true;
|
||||
hideModule(module, speed, callback, options);
|
||||
},
|
||||
|
||||
/* showModule(module, speed, callback)
|
||||
/**
|
||||
* Show the module.
|
||||
*
|
||||
* argument module Module - The module show.
|
||||
* argument speed Number - The speed of the show animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* argument options object - Optional settings for the hide method.
|
||||
* @param {Module} module The module to show.
|
||||
* @param {number} speed The speed of the show animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the show method.
|
||||
*/
|
||||
showModule: function (module, speed, callback, options) {
|
||||
// do not change module.hidden yet, only if we really show it later
|
||||
|
302
js/module.js
@@ -2,9 +2,11 @@
|
||||
|
||||
/* Magic Mirror
|
||||
* Module Blueprint.
|
||||
* @typedef {Object} Module
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
*/
|
||||
var Module = Class.extend({
|
||||
/*********************************************************
|
||||
@@ -29,53 +31,55 @@ var Module = Class.extend({
|
||||
// Use the nunjucksEnvironment() to get it.
|
||||
_nunjucksEnvironment: null,
|
||||
|
||||
/* init()
|
||||
* Is called when the module is instantiated.
|
||||
/**
|
||||
* Called when the module is instantiated.
|
||||
*/
|
||||
init: function () {
|
||||
//Log.log(this.defaults);
|
||||
},
|
||||
|
||||
/* start()
|
||||
* Is called when the module is started.
|
||||
/**
|
||||
* Called when the module is started.
|
||||
*/
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
},
|
||||
|
||||
/* getScripts()
|
||||
/**
|
||||
* Returns a list of scripts the module requires to be loaded.
|
||||
*
|
||||
* return Array<String> - An array with filenames.
|
||||
* @returns {string[]} An array with filenames.
|
||||
*/
|
||||
getScripts: function () {
|
||||
return [];
|
||||
},
|
||||
|
||||
/* getStyles()
|
||||
/**
|
||||
* Returns a list of stylesheets the module requires to be loaded.
|
||||
*
|
||||
* return Array<String> - An array with filenames.
|
||||
* @returns {string[]} An array with filenames.
|
||||
*/
|
||||
getStyles: function () {
|
||||
return [];
|
||||
},
|
||||
|
||||
/* getTranslations()
|
||||
/**
|
||||
* Returns a map of translation files the module requires to be loaded.
|
||||
*
|
||||
* return Map<String, String> - A map with langKeys and filenames.
|
||||
* return Map<String, String> -
|
||||
*
|
||||
* @returns {*} A map with langKeys and filenames.
|
||||
*/
|
||||
getTranslations: function () {
|
||||
return false;
|
||||
},
|
||||
|
||||
/* getDom()
|
||||
* This method generates the dom which needs to be displayed. This method is called by the Magic Mirror core.
|
||||
/**
|
||||
* Generates the dom which needs to be displayed. This method is called by the Magic Mirror core.
|
||||
* This method can to be subclassed if the module wants to display info on the mirror.
|
||||
* Alternatively, the getTemplate method could be subclassed.
|
||||
*
|
||||
* return DomObject | Promise - The dom or a promise with the dom to display.
|
||||
* @returns {HTMLElement|Promise} The dom or a promise with the dom to display.
|
||||
*/
|
||||
getDom: function () {
|
||||
var self = this;
|
||||
@@ -105,46 +109,45 @@ var Module = Class.extend({
|
||||
});
|
||||
},
|
||||
|
||||
/* getHeader()
|
||||
* This method generates the header string which needs to be displayed if a user has a header configured for this module.
|
||||
/**
|
||||
* Generates the header string which needs to be displayed if a user has a header configured for this module.
|
||||
* This method is called by the Magic Mirror core, but only if the user has configured a default header for the module.
|
||||
* This method needs to be subclassed if the module wants to display modified headers on the mirror.
|
||||
*
|
||||
* return string - The header to display above the header.
|
||||
* @returns {string} The header to display above the header.
|
||||
*/
|
||||
getHeader: function () {
|
||||
return this.data.header;
|
||||
},
|
||||
|
||||
/* getTemplate()
|
||||
* This method returns the template for the module which is used by the default getDom implementation.
|
||||
/**
|
||||
* Returns the template for the module which is used by the default getDom implementation.
|
||||
* This method needs to be subclassed if the module wants to use a template.
|
||||
* It can either return a template sting, or a template filename.
|
||||
* If the string ends with '.html' it's considered a file from within the module's folder.
|
||||
*
|
||||
* return string - The template string of filename.
|
||||
* @returns {string} The template string of filename.
|
||||
*/
|
||||
getTemplate: function () {
|
||||
return '<div class="normal">' + this.name + '</div><div class="small dimmed">' + this.identifier + "</div>";
|
||||
},
|
||||
|
||||
/* getTemplateData()
|
||||
* This method returns the data to be used in the template.
|
||||
/**
|
||||
* Returns the data to be used in the template.
|
||||
* This method needs to be subclassed if the module wants to use a custom data.
|
||||
*
|
||||
* return Object
|
||||
* @returns {object} The data for the template
|
||||
*/
|
||||
getTemplateData: function () {
|
||||
return {};
|
||||
},
|
||||
|
||||
/* notificationReceived(notification, payload, sender)
|
||||
* This method is called when a notification arrives.
|
||||
* This method is called by the Magic Mirror core.
|
||||
/**
|
||||
* Called by the Magic Mirror core when a notification arrives.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* argument sender Module - The module that sent the notification.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
*/
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
if (sender) {
|
||||
@@ -154,11 +157,11 @@ var Module = Class.extend({
|
||||
}
|
||||
},
|
||||
|
||||
/** nunjucksEnvironment()
|
||||
/**
|
||||
* Returns the nunjucks environment for the current module.
|
||||
* The environment is checked in the _nunjucksEnvironment instance variable.
|
||||
|
||||
* @returns Nunjucks Environment
|
||||
*
|
||||
* @returns {object} The Nunjucks Environment
|
||||
*/
|
||||
nunjucksEnvironment: function () {
|
||||
if (this._nunjucksEnvironment !== null) {
|
||||
@@ -171,32 +174,33 @@ var Module = Class.extend({
|
||||
trimBlocks: true,
|
||||
lstripBlocks: true
|
||||
});
|
||||
this._nunjucksEnvironment.addFilter("translate", function (str) {
|
||||
return self.translate(str);
|
||||
|
||||
this._nunjucksEnvironment.addFilter("translate", function (str, variables) {
|
||||
return nunjucks.runtime.markSafe(self.translate(str, variables));
|
||||
});
|
||||
|
||||
return this._nunjucksEnvironment;
|
||||
},
|
||||
|
||||
/* socketNotificationReceived(notification, payload)
|
||||
* This method is called when a socket notification arrives.
|
||||
/**
|
||||
* Called when a socket notification arrives.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
|
||||
},
|
||||
|
||||
/* suspend()
|
||||
* This method is called when a module is hidden.
|
||||
/*
|
||||
* Called when the module is hidden.
|
||||
*/
|
||||
suspend: function () {
|
||||
Log.log(this.name + " is suspended.");
|
||||
},
|
||||
|
||||
/* resume()
|
||||
* This method is called when a module is shown.
|
||||
/*
|
||||
* Called when the module is shown.
|
||||
*/
|
||||
resume: function () {
|
||||
Log.log(this.name + " is resumed.");
|
||||
@@ -206,10 +210,10 @@ var Module = Class.extend({
|
||||
* The methods below don"t need subclassing. *
|
||||
*********************************************/
|
||||
|
||||
/* setData(data)
|
||||
/**
|
||||
* Set the module data.
|
||||
*
|
||||
* argument data object - Module data.
|
||||
* @param {Module} data The module data
|
||||
*/
|
||||
setData: function (data) {
|
||||
this.data = data;
|
||||
@@ -217,21 +221,24 @@ var Module = Class.extend({
|
||||
this.identifier = data.identifier;
|
||||
this.hidden = false;
|
||||
|
||||
this.setConfig(data.config);
|
||||
this.setConfig(data.config, data.configDeepMerge);
|
||||
},
|
||||
|
||||
/* setConfig(config)
|
||||
/**
|
||||
* Set the module config and combine it with the module defaults.
|
||||
*
|
||||
* argument config object - Module config.
|
||||
* @param {object} config The combined module config.
|
||||
* @param {boolean} deep Merge module config in deep.
|
||||
*/
|
||||
setConfig: function (config) {
|
||||
this.config = Object.assign({}, this.defaults, config);
|
||||
setConfig: function (config, deep) {
|
||||
this.config = deep ? configMerge({}, this.defaults, config) : Object.assign({}, this.defaults, config);
|
||||
},
|
||||
|
||||
/* socket()
|
||||
* Returns a socket object. If it doesn't exist, it"s created.
|
||||
/**
|
||||
* Returns a socket object. If it doesn't exist, it's created.
|
||||
* It also registers the notification callback.
|
||||
*
|
||||
* @returns {MMSocket} a socket object
|
||||
*/
|
||||
socket: function () {
|
||||
if (typeof this._socket === "undefined") {
|
||||
@@ -246,40 +253,39 @@ var Module = Class.extend({
|
||||
return this._socket;
|
||||
},
|
||||
|
||||
/* file(file)
|
||||
/**
|
||||
* Retrieve the path to a module file.
|
||||
*
|
||||
* argument file string - Filename.
|
||||
*
|
||||
* return string - File path.
|
||||
* @param {string} file Filename
|
||||
* @returns {string} the file path
|
||||
*/
|
||||
file: function (file) {
|
||||
return (this.data.path + "/" + file).replace("//", "/");
|
||||
},
|
||||
|
||||
/* loadStyles()
|
||||
/**
|
||||
* Load all required stylesheets by requesting the MM object to load the files.
|
||||
*
|
||||
* argument callback function - Function called when done.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
loadStyles: function (callback) {
|
||||
this.loadDependencies("getStyles", callback);
|
||||
},
|
||||
|
||||
/* loadScripts()
|
||||
/**
|
||||
* Load all required scripts by requesting the MM object to load the files.
|
||||
*
|
||||
* argument callback function - Function called when done.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
loadScripts: function (callback) {
|
||||
this.loadDependencies("getScripts", callback);
|
||||
},
|
||||
|
||||
/* loadDependencies(funcName, callback)
|
||||
/**
|
||||
* Helper method to load all dependencies.
|
||||
*
|
||||
* argument funcName string - Function name to call to get scripts or styles.
|
||||
* argument callback function - Function called when done.
|
||||
* @param {string} funcName Function name to call to get scripts or styles.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
loadDependencies: function (funcName, callback) {
|
||||
var self = this;
|
||||
@@ -300,46 +306,47 @@ var Module = Class.extend({
|
||||
loadNextDependency();
|
||||
},
|
||||
|
||||
/* loadScripts()
|
||||
* Load all required scripts by requesting the MM object to load the files.
|
||||
/**
|
||||
* Load all translations.
|
||||
*
|
||||
* argument callback function - Function called when done.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
loadTranslations: function (callback) {
|
||||
var self = this;
|
||||
var translations = this.getTranslations();
|
||||
var lang = config.language.toLowerCase();
|
||||
loadTranslations(callback) {
|
||||
const translations = this.getTranslations() || {};
|
||||
const language = config.language.toLowerCase();
|
||||
|
||||
// The variable `first` will contain the first
|
||||
// defined translation after the following line.
|
||||
for (var first in translations) {
|
||||
break;
|
||||
const languages = Object.keys(translations);
|
||||
const fallbackLanguage = languages[0];
|
||||
|
||||
if (languages.length === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (translations) {
|
||||
var translationFile = translations[lang] || undefined;
|
||||
var translationsFallbackFile = translations[first];
|
||||
const translationFile = translations[language];
|
||||
const translationsFallbackFile = translations[fallbackLanguage];
|
||||
|
||||
// If a translation file is set, load it and then also load the fallback translation file.
|
||||
// Otherwise only load the fallback translation file.
|
||||
if (translationFile !== undefined && translationFile !== translationsFallbackFile) {
|
||||
Translator.load(self, translationFile, false, function () {
|
||||
Translator.load(self, translationsFallbackFile, true, callback);
|
||||
});
|
||||
} else {
|
||||
Translator.load(self, translationsFallbackFile, true, callback);
|
||||
if (!translationFile) {
|
||||
Translator.load(this, translationsFallbackFile, true, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
Translator.load(this, translationFile, false, () => {
|
||||
if (translationFile !== translationsFallbackFile) {
|
||||
Translator.load(this, translationsFallbackFile, true, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* translate(key, defaultValueOrVariables, defaultValue)
|
||||
/**
|
||||
* Request the translation for a given key with optional variables and default value.
|
||||
*
|
||||
* argument key string - The key of the string to translate
|
||||
* argument defaultValueOrVariables string/object - The default value or variables for translating. (Optional)
|
||||
* argument defaultValue string - The default value with variables. (Optional)
|
||||
* @param {string} key The key of the string to translate
|
||||
* @param {string|object} [defaultValueOrVariables] The default value or variables for translating.
|
||||
* @param {string} [defaultValue] The default value with variables.
|
||||
* @returns {string} the translated key
|
||||
*/
|
||||
translate: function (key, defaultValueOrVariables, defaultValue) {
|
||||
if (typeof defaultValueOrVariables === "object") {
|
||||
@@ -348,41 +355,41 @@ var Module = Class.extend({
|
||||
return Translator.translate(this, key) || defaultValueOrVariables || "";
|
||||
},
|
||||
|
||||
/* updateDom(speed)
|
||||
/**
|
||||
* Request an (animated) update of the module.
|
||||
*
|
||||
* argument speed Number - The speed of the animation. (Optional)
|
||||
* @param {number} [speed] The speed of the animation.
|
||||
*/
|
||||
updateDom: function (speed) {
|
||||
MM.updateDom(this, speed);
|
||||
},
|
||||
|
||||
/* sendNotification(notification, payload)
|
||||
/**
|
||||
* Send a notification to all modules.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
sendNotification: function (notification, payload) {
|
||||
MM.sendNotification(notification, payload, this);
|
||||
},
|
||||
|
||||
/* sendSocketNotification(notification, payload)
|
||||
/**
|
||||
* Send a socket notification to the node helper.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
sendSocketNotification: function (notification, payload) {
|
||||
this.socket().sendNotification(notification, payload);
|
||||
},
|
||||
|
||||
/* hideModule(module, speed, callback)
|
||||
/**
|
||||
* Hide this module.
|
||||
*
|
||||
* argument speed Number - The speed of the hide animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* argument options object - Optional settings for the hide method.
|
||||
* @param {number} speed The speed of the hide animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the hide method.
|
||||
*/
|
||||
hide: function (speed, callback, options) {
|
||||
if (typeof callback === "object") {
|
||||
@@ -405,12 +412,12 @@ var Module = Class.extend({
|
||||
);
|
||||
},
|
||||
|
||||
/* showModule(module, speed, callback)
|
||||
/**
|
||||
* Show this module.
|
||||
*
|
||||
* argument speed Number - The speed of the show animation.
|
||||
* argument callback function - Called when the animation is done.
|
||||
* argument options object - Optional settings for the hide method.
|
||||
* @param {number} speed The speed of the show animation.
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the show method.
|
||||
*/
|
||||
show: function (speed, callback, options) {
|
||||
if (typeof callback === "object") {
|
||||
@@ -421,19 +428,64 @@ var Module = Class.extend({
|
||||
callback = callback || function () {};
|
||||
options = options || {};
|
||||
|
||||
var self = this;
|
||||
MM.showModule(
|
||||
this,
|
||||
speed,
|
||||
function () {
|
||||
self.resume();
|
||||
callback;
|
||||
() => {
|
||||
this.resume();
|
||||
callback();
|
||||
},
|
||||
options
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Merging MagicMirror (or other) default/config script by @bugsounet
|
||||
* Merge 2 objects or/with array
|
||||
*
|
||||
* Usage:
|
||||
* -------
|
||||
* this.config = configMerge({}, this.defaults, this.config)
|
||||
* -------
|
||||
* arg1: initial object
|
||||
* arg2: config model
|
||||
* arg3: config to merge
|
||||
* -------
|
||||
* why using it ?
|
||||
* Object.assign() function don't to all job
|
||||
* it don't merge all thing in deep
|
||||
* -> object in object and array is not merging
|
||||
* -------
|
||||
*
|
||||
* Todo: idea of Mich determinate what do you want to merge or not
|
||||
*
|
||||
* @param {object} result the initial object
|
||||
* @returns {object} the merged config
|
||||
*/
|
||||
function configMerge(result) {
|
||||
var stack = Array.prototype.slice.call(arguments, 1);
|
||||
var item;
|
||||
var key;
|
||||
while (stack.length) {
|
||||
item = stack.shift();
|
||||
for (key in item) {
|
||||
if (item.hasOwnProperty(key)) {
|
||||
if (typeof result[key] === "object" && result[key] && Object.prototype.toString.call(result[key]) !== "[object Array]") {
|
||||
if (typeof item[key] === "object" && item[key] !== null) {
|
||||
result[key] = configMerge({}, result[key], item[key]);
|
||||
} else {
|
||||
result[key] = item[key];
|
||||
}
|
||||
} else {
|
||||
result[key] = item[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Module.definitions = {};
|
||||
|
||||
Module.create = function (name) {
|
||||
@@ -451,11 +503,27 @@ Module.create = function (name) {
|
||||
return new ModuleClass();
|
||||
};
|
||||
|
||||
/* cmpVersions(a,b)
|
||||
Module.register = function (name, moduleDefinition) {
|
||||
if (moduleDefinition.requiresVersion) {
|
||||
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.version);
|
||||
if (cmpVersions(window.version, moduleDefinition.requiresVersion) >= 0) {
|
||||
Log.log("Version is ok!");
|
||||
} else {
|
||||
Log.warn("Version is incorrect. Skip module: '" + name + "'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Log.log("Module registered: " + name);
|
||||
Module.definitions[name] = moduleDefinition;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare two semantic version numbers and return the difference.
|
||||
*
|
||||
* argument a string - Version number a.
|
||||
* argument a string - Version number b.
|
||||
* @param {string} a Version number a.
|
||||
* @param {string} b Version number b.
|
||||
* @returns {number} A positive number if a is larger than b, a negative
|
||||
* number if a is smaller and 0 if they are the same
|
||||
*/
|
||||
function cmpVersions(a, b) {
|
||||
var i, diff;
|
||||
@@ -472,17 +540,3 @@ function cmpVersions(a, b) {
|
||||
}
|
||||
return segmentsA.length - segmentsB.length;
|
||||
}
|
||||
|
||||
Module.register = function (name, moduleDefinition) {
|
||||
if (moduleDefinition.requiresVersion) {
|
||||
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.version);
|
||||
if (cmpVersions(window.version, moduleDefinition.requiresVersion) >= 0) {
|
||||
Log.log("Version is ok!");
|
||||
} else {
|
||||
Log.log("Version is incorrect. Skip module: '" + name + "'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Log.log("Module registered: " + name);
|
||||
Module.definitions[name] = moduleDefinition;
|
||||
};
|
||||
|
@@ -5,21 +5,21 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Class = require("./class.js");
|
||||
const Log = require("./logger.js");
|
||||
const Log = require("logger");
|
||||
const express = require("express");
|
||||
|
||||
var NodeHelper = Class.extend({
|
||||
init: function () {
|
||||
const NodeHelper = Class.extend({
|
||||
init() {
|
||||
Log.log("Initializing new module helper ...");
|
||||
},
|
||||
|
||||
loaded: function (callback) {
|
||||
Log.log("Module helper loaded: " + this.name);
|
||||
loaded(callback) {
|
||||
Log.log(`Module helper loaded: ${this.name}`);
|
||||
callback();
|
||||
},
|
||||
|
||||
start: function () {
|
||||
Log.log("Starting module helper: " + this.name);
|
||||
start() {
|
||||
Log.log(`Starting module helper: ${this.name}`);
|
||||
},
|
||||
|
||||
/* stop()
|
||||
@@ -28,8 +28,8 @@ var NodeHelper = Class.extend({
|
||||
* gracefully exit the module.
|
||||
*
|
||||
*/
|
||||
stop: function () {
|
||||
Log.log("Stopping module helper: " + this.name);
|
||||
stop() {
|
||||
Log.log(`Stopping module helper: ${this.name}`);
|
||||
},
|
||||
|
||||
/* socketNotificationReceived(notification, payload)
|
||||
@@ -38,8 +38,8 @@ var NodeHelper = Class.extend({
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
*/
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
|
||||
socketNotificationReceived(notification, payload) {
|
||||
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
|
||||
},
|
||||
|
||||
/* setName(name)
|
||||
@@ -47,7 +47,7 @@ var NodeHelper = Class.extend({
|
||||
*
|
||||
* argument name string - Module name.
|
||||
*/
|
||||
setName: function (name) {
|
||||
setName(name) {
|
||||
this.name = name;
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ var NodeHelper = Class.extend({
|
||||
*
|
||||
* argument path string - Module path.
|
||||
*/
|
||||
setPath: function (path) {
|
||||
setPath(path) {
|
||||
this.path = path;
|
||||
},
|
||||
|
||||
@@ -66,7 +66,7 @@ var NodeHelper = Class.extend({
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
*/
|
||||
sendSocketNotification: function (notification, payload) {
|
||||
sendSocketNotification(notification, payload) {
|
||||
this.io.of(this.name).emit(notification, payload);
|
||||
},
|
||||
|
||||
@@ -76,11 +76,10 @@ var NodeHelper = Class.extend({
|
||||
*
|
||||
* argument app Express app - The Express app object.
|
||||
*/
|
||||
setExpressApp: function (app) {
|
||||
setExpressApp(app) {
|
||||
this.expressApp = app;
|
||||
|
||||
var publicPath = this.path + "/public";
|
||||
app.use("/" + this.name, express.static(publicPath));
|
||||
app.use(`/${this.name}`, express.static(`${this.path}/public`));
|
||||
},
|
||||
|
||||
/* setSocketIO(io)
|
||||
@@ -89,27 +88,25 @@ var NodeHelper = Class.extend({
|
||||
*
|
||||
* argument io Socket.io - The Socket io object.
|
||||
*/
|
||||
setSocketIO: function (io) {
|
||||
var self = this;
|
||||
self.io = io;
|
||||
setSocketIO(io) {
|
||||
this.io = io;
|
||||
|
||||
Log.log("Connecting socket for: " + this.name);
|
||||
var namespace = this.name;
|
||||
io.of(namespace).on("connection", function (socket) {
|
||||
Log.log(`Connecting socket for: ${this.name}`);
|
||||
|
||||
io.of(this.name).on("connection", (socket) => {
|
||||
// add a catch all event.
|
||||
var onevent = socket.onevent;
|
||||
const onevent = socket.onevent;
|
||||
socket.onevent = function (packet) {
|
||||
var args = packet.data || [];
|
||||
const args = packet.data || [];
|
||||
onevent.call(this, packet); // original call
|
||||
packet.data = ["*"].concat(args);
|
||||
onevent.call(this, packet); // additional call to catch-all
|
||||
};
|
||||
|
||||
// register catch all.
|
||||
socket.on("*", function (notification, payload) {
|
||||
socket.on("*", (notification, payload) => {
|
||||
if (notification !== "*") {
|
||||
//Log.log('received message in namespace: ' + namespace);
|
||||
self.socketNotificationReceived(notification, payload);
|
||||
this.socketNotificationReceived(notification, payload);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -120,7 +117,4 @@ NodeHelper.create = function (moduleDefinition) {
|
||||
return NodeHelper.extend(moduleDefinition);
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = NodeHelper;
|
||||
}
|
||||
module.exports = NodeHelper;
|
||||
|
65
js/server.js
@@ -4,25 +4,29 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var express = require("express");
|
||||
var app = require("express")();
|
||||
var path = require("path");
|
||||
var ipfilter = require("express-ipfilter").IpFilter;
|
||||
var fs = require("fs");
|
||||
var helmet = require("helmet");
|
||||
const express = require("express");
|
||||
const app = require("express")();
|
||||
const path = require("path");
|
||||
const ipfilter = require("express-ipfilter").IpFilter;
|
||||
const fs = require("fs");
|
||||
const helmet = require("helmet");
|
||||
|
||||
var Log = require("./logger.js");
|
||||
var Utils = require("./utils.js");
|
||||
const Log = require("logger");
|
||||
const Utils = require("./utils.js");
|
||||
|
||||
var Server = function (config, callback) {
|
||||
var port = config.port;
|
||||
if (process.env.MM_PORT) {
|
||||
port = process.env.MM_PORT;
|
||||
}
|
||||
/**
|
||||
* Server
|
||||
*
|
||||
* @param {object} config The MM config
|
||||
* @param {Function} callback Function called when done.
|
||||
* @class
|
||||
*/
|
||||
function Server(config, callback) {
|
||||
const port = process.env.MM_PORT || config.port;
|
||||
|
||||
var server = null;
|
||||
let server = null;
|
||||
if (config.useHttps) {
|
||||
var options = {
|
||||
const options = {
|
||||
key: fs.readFileSync(config.httpsPrivateKey),
|
||||
cert: fs.readFileSync(config.httpsCertificate)
|
||||
};
|
||||
@@ -30,18 +34,24 @@ var Server = function (config, callback) {
|
||||
} else {
|
||||
server = require("http").Server(app);
|
||||
}
|
||||
var io = require("socket.io")(server);
|
||||
const io = require("socket.io")(server, {
|
||||
cors: {
|
||||
origin: /.*$/,
|
||||
credentials: true
|
||||
},
|
||||
allowEIO3: true
|
||||
});
|
||||
|
||||
Log.log("Starting server on port " + port + " ... ");
|
||||
Log.log(`Starting server on port ${port} ... `);
|
||||
|
||||
server.listen(port, config.address ? config.address : "localhost");
|
||||
server.listen(port, config.address || "localhost");
|
||||
|
||||
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
||||
Log.info(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
|
||||
Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
|
||||
}
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
var result = ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) {
|
||||
ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) {
|
||||
if (err === undefined) {
|
||||
return next();
|
||||
}
|
||||
@@ -49,13 +59,12 @@ var Server = function (config, callback) {
|
||||
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
|
||||
});
|
||||
});
|
||||
app.use(helmet());
|
||||
app.use(helmet({ contentSecurityPolicy: false }));
|
||||
|
||||
app.use("/js", express.static(__dirname));
|
||||
var directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
|
||||
var directory;
|
||||
for (var i in directories) {
|
||||
directory = directories[i];
|
||||
|
||||
const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
|
||||
for (const directory of directories) {
|
||||
app.use(directory, express.static(path.resolve(global.root_path + directory)));
|
||||
}
|
||||
|
||||
@@ -68,10 +77,10 @@ var Server = function (config, callback) {
|
||||
});
|
||||
|
||||
app.get("/", function (req, res) {
|
||||
var html = fs.readFileSync(path.resolve(global.root_path + "/index.html"), { encoding: "utf8" });
|
||||
let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" });
|
||||
html = html.replace("#VERSION#", global.version);
|
||||
|
||||
var configFile = "config/config.js";
|
||||
let configFile = "config/config.js";
|
||||
if (typeof global.configuration_file !== "undefined") {
|
||||
configFile = global.configuration_file;
|
||||
}
|
||||
@@ -83,6 +92,6 @@ var Server = function (config, callback) {
|
||||
if (typeof callback === "function") {
|
||||
callback(app, io);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Server;
|
||||
|
178
js/translator.js
@@ -7,124 +7,59 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var Translator = (function () {
|
||||
/* loadJSON(file, callback)
|
||||
/**
|
||||
* Load a JSON file via XHR.
|
||||
*
|
||||
* argument file string - Path of the file we want to load.
|
||||
* argument callback function - Function called when done.
|
||||
* @param {string} file Path of the file we want to load.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
function loadJSON(file, callback) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.overrideMimeType("application/json");
|
||||
xhr.open("GET", file, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
callback(JSON.parse(stripComments(xhr.responseText)));
|
||||
// needs error handler try/catch at least
|
||||
let fileinfo = null;
|
||||
try {
|
||||
fileinfo = JSON.parse(xhr.responseText);
|
||||
} catch (exception) {
|
||||
// nothing here, but don't die
|
||||
Log.error(" loading json file =" + file + " failed");
|
||||
}
|
||||
callback(fileinfo);
|
||||
}
|
||||
};
|
||||
xhr.send(null);
|
||||
}
|
||||
|
||||
/* loadJSON(str, options)
|
||||
* Remove any commenting from a json file so it can be parsed.
|
||||
*
|
||||
* argument str string - The string that contains json with comments.
|
||||
* argument opts function - Strip options.
|
||||
*
|
||||
* return the stripped string.
|
||||
*/
|
||||
function stripComments(str, opts) {
|
||||
// strip comments copied from: https://github.com/sindresorhus/strip-json-comments
|
||||
|
||||
var singleComment = 1;
|
||||
var multiComment = 2;
|
||||
|
||||
function stripWithoutWhitespace() {
|
||||
return "";
|
||||
}
|
||||
|
||||
function stripWithWhitespace(str, start, end) {
|
||||
return str.slice(start, end).replace(/\S/g, " ");
|
||||
}
|
||||
|
||||
opts = opts || {};
|
||||
|
||||
var currentChar;
|
||||
var nextChar;
|
||||
var insideString = false;
|
||||
var insideComment = false;
|
||||
var offset = 0;
|
||||
var ret = "";
|
||||
var strip = opts.whitespace === false ? stripWithoutWhitespace : stripWithWhitespace;
|
||||
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
currentChar = str[i];
|
||||
nextChar = str[i + 1];
|
||||
|
||||
if (!insideComment && currentChar === '"') {
|
||||
var escaped = str[i - 1] === "\\" && str[i - 2] !== "\\";
|
||||
if (!escaped) {
|
||||
insideString = !insideString;
|
||||
}
|
||||
}
|
||||
|
||||
if (insideString) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!insideComment && currentChar + nextChar === "//") {
|
||||
ret += str.slice(offset, i);
|
||||
offset = i;
|
||||
insideComment = singleComment;
|
||||
i++;
|
||||
} else if (insideComment === singleComment && currentChar + nextChar === "\r\n") {
|
||||
i++;
|
||||
insideComment = false;
|
||||
ret += strip(str, offset, i);
|
||||
offset = i;
|
||||
continue;
|
||||
} else if (insideComment === singleComment && currentChar === "\n") {
|
||||
insideComment = false;
|
||||
ret += strip(str, offset, i);
|
||||
offset = i;
|
||||
} else if (!insideComment && currentChar + nextChar === "/*") {
|
||||
ret += str.slice(offset, i);
|
||||
offset = i;
|
||||
insideComment = multiComment;
|
||||
i++;
|
||||
continue;
|
||||
} else if (insideComment === multiComment && currentChar + nextChar === "*/") {
|
||||
i++;
|
||||
insideComment = false;
|
||||
ret += strip(str, offset, i + 1);
|
||||
offset = i + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return ret + (insideComment ? strip(str.substr(offset)) : str.substr(offset));
|
||||
}
|
||||
|
||||
return {
|
||||
coreTranslations: {},
|
||||
coreTranslationsFallback: {},
|
||||
translations: {},
|
||||
translationsFallback: {},
|
||||
|
||||
/* translate(module, key, variables)
|
||||
/**
|
||||
* Load a translation for a given key for a given module.
|
||||
*
|
||||
* argument module Module - The module to load the translation for.
|
||||
* argument key string - The key of the text to translate.
|
||||
* argument variables - The variables to use within the translation template (optional)
|
||||
* @param {Module} module The module to load the translation for.
|
||||
* @param {string} key The key of the text to translate.
|
||||
* @param {object} variables The variables to use within the translation template (optional)
|
||||
* @returns {string} the translated key
|
||||
*/
|
||||
translate: function (module, key, variables) {
|
||||
variables = variables || {}; //Empty object by default
|
||||
|
||||
// Combines template and variables like:
|
||||
// template: "Please wait for {timeToWait} before continuing with {work}."
|
||||
// variables: {timeToWait: "2 hours", work: "painting"}
|
||||
// to: "Please wait for 2 hours before continuing with painting."
|
||||
/**
|
||||
* Combines template and variables like:
|
||||
* template: "Please wait for {timeToWait} before continuing with {work}."
|
||||
* variables: {timeToWait: "2 hours", work: "painting"}
|
||||
* to: "Please wait for 2 hours before continuing with painting."
|
||||
*
|
||||
* @param {string} template Text with placeholder
|
||||
* @param {object} variables Variables for the placeholder
|
||||
* @returns {string} the template filled with the variables
|
||||
*/
|
||||
function createStringFromTemplate(template, variables) {
|
||||
if (Object.prototype.toString.call(template) !== "[object String]") {
|
||||
return template;
|
||||
@@ -133,7 +68,7 @@ var Translator = (function () {
|
||||
template = variables.fallback;
|
||||
}
|
||||
return template.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) {
|
||||
return variables[varName] || "{" + varName + "}";
|
||||
return varName in variables ? variables[varName] : "{" + varName + "}";
|
||||
});
|
||||
}
|
||||
|
||||
@@ -160,63 +95,52 @@ var Translator = (function () {
|
||||
return key;
|
||||
},
|
||||
|
||||
/* load(module, file, isFallback, callback)
|
||||
/**
|
||||
* Load a translation file (json) and remember the data.
|
||||
*
|
||||
* argument module Module - The module to load the translation file for.
|
||||
* argument file string - Path of the file we want to load.
|
||||
* argument isFallback boolean - Flag to indicate fallback translations.
|
||||
* argument callback function - Function called when done.
|
||||
* @param {Module} module The module to load the translation file for.
|
||||
* @param {string} file Path of the file we want to load.
|
||||
* @param {boolean} isFallback Flag to indicate fallback translations.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
load: function (module, file, isFallback, callback) {
|
||||
if (!isFallback) {
|
||||
Log.log(module.name + " - Load translation: " + file);
|
||||
} else {
|
||||
Log.log(module.name + " - Load translation fallback: " + file);
|
||||
load(module, file, isFallback, callback) {
|
||||
Log.log(`${module.name} - Load translation${isFallback && " fallback"}: ${file}`);
|
||||
|
||||
if (this.translationsFallback[module.name]) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
if (!this.translationsFallback[module.name]) {
|
||||
loadJSON(module.file(file), function (json) {
|
||||
if (!isFallback) {
|
||||
self.translations[module.name] = json;
|
||||
} else {
|
||||
self.translationsFallback[module.name] = json;
|
||||
}
|
||||
loadJSON(module.file(file), (json) => {
|
||||
const property = isFallback ? "translationsFallback" : "translations";
|
||||
this[property][module.name] = json;
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
/* loadCoreTranslations(lang)
|
||||
/**
|
||||
* Load the core translations.
|
||||
*
|
||||
* argument lang String - The language identifier of the core language.
|
||||
* @param {string} lang The language identifier of the core language.
|
||||
*/
|
||||
loadCoreTranslations: function (lang) {
|
||||
var self = this;
|
||||
|
||||
if (lang in translations) {
|
||||
Log.log("Loading core translation file: " + translations[lang]);
|
||||
loadJSON(translations[lang], function (translations) {
|
||||
self.coreTranslations = translations;
|
||||
loadJSON(translations[lang], (translations) => {
|
||||
this.coreTranslations = translations;
|
||||
});
|
||||
} else {
|
||||
Log.log("Configured language not found in core translations.");
|
||||
}
|
||||
|
||||
self.loadCoreTranslationsFallback();
|
||||
this.loadCoreTranslationsFallback();
|
||||
},
|
||||
|
||||
/* loadCoreTranslationsFallback()
|
||||
/**
|
||||
* Load the core translations fallback.
|
||||
* The first language defined in translations.js will be used.
|
||||
*/
|
||||
loadCoreTranslationsFallback: function () {
|
||||
var self = this;
|
||||
|
||||
// The variable `first` will contain the first
|
||||
// defined translation after the following line.
|
||||
for (var first in translations) {
|
||||
@@ -225,8 +149,8 @@ var Translator = (function () {
|
||||
|
||||
if (first) {
|
||||
Log.log("Loading core translation fallback file: " + translations[first]);
|
||||
loadJSON(translations[first], function (translations) {
|
||||
self.coreTranslationsFallback = translations;
|
||||
loadJSON(translations[first], (translations) => {
|
||||
this.coreTranslationsFallback = translations;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
11
js/utils.js
@@ -4,16 +4,13 @@
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var colors = require("colors/safe");
|
||||
const colors = require("colors/safe");
|
||||
|
||||
var Utils = {
|
||||
module.exports = {
|
||||
colors: {
|
||||
warn: colors.yellow,
|
||||
error: colors.red,
|
||||
info: colors.blue
|
||||
info: colors.blue,
|
||||
pass: colors.green
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = Utils;
|
||||
}
|
||||
|
@@ -100,10 +100,13 @@ Module.register("alert", {
|
||||
message: image + message,
|
||||
effect: this.config.alert_effect,
|
||||
ttl: params.timer,
|
||||
onClose: () => this.hide_alert(sender),
|
||||
al_no: "ns-alert"
|
||||
});
|
||||
|
||||
//Show alert
|
||||
this.alerts[sender.name].show();
|
||||
|
||||
//Add timer to dismiss alert and overlay
|
||||
if (params.timer) {
|
||||
setTimeout(() => {
|
||||
|
@@ -12,7 +12,11 @@
|
||||
*/
|
||||
(function (window) {
|
||||
/**
|
||||
* extend obj function
|
||||
* Extend one object with another one
|
||||
*
|
||||
* @param {object} a The object to extend
|
||||
* @param {object} b The object which extends the other, overwrites existing keys
|
||||
* @returns {object} The merged object
|
||||
*/
|
||||
function extend(a, b) {
|
||||
for (let key in b) {
|
||||
@@ -24,7 +28,10 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* NotificationFx function
|
||||
* NotificationFx constructor
|
||||
*
|
||||
* @param {object} options The configuration options
|
||||
* @class
|
||||
*/
|
||||
function NotificationFx(options) {
|
||||
this.options = extend({}, this.options);
|
||||
@@ -66,8 +73,7 @@
|
||||
};
|
||||
|
||||
/**
|
||||
* init function
|
||||
* initialize and cache some vars
|
||||
* Initialize and cache some vars
|
||||
*/
|
||||
NotificationFx.prototype._init = function () {
|
||||
// create HTML structure
|
||||
@@ -95,7 +101,7 @@
|
||||
};
|
||||
|
||||
/**
|
||||
* init events
|
||||
* Init events
|
||||
*/
|
||||
NotificationFx.prototype._initEvents = function () {
|
||||
// dismiss notification by tapping on it if someone has a touchscreen
|
||||
@@ -105,7 +111,7 @@
|
||||
};
|
||||
|
||||
/**
|
||||
* show the notification
|
||||
* Show the notification
|
||||
*/
|
||||
NotificationFx.prototype.show = function () {
|
||||
this.active = true;
|
||||
@@ -115,7 +121,7 @@
|
||||
};
|
||||
|
||||
/**
|
||||
* dismiss the notification
|
||||
* Dismiss the notification
|
||||
*/
|
||||
NotificationFx.prototype.dismiss = function () {
|
||||
this.active = false;
|
||||
@@ -144,7 +150,7 @@
|
||||
};
|
||||
|
||||
/**
|
||||
* add to global namespace
|
||||
* Add to global namespace
|
||||
*/
|
||||
window.NotificationFx = NotificationFx;
|
||||
})(window);
|
||||
|
@@ -11,6 +11,7 @@ Module.register("calendar", {
|
||||
defaults: {
|
||||
maximumEntries: 10, // Total Maximum Entries
|
||||
maximumNumberOfDays: 365,
|
||||
limitDays: 0, // Limit the number of days shown, 0 = no limit
|
||||
displaySymbol: true,
|
||||
defaultSymbol: "calendar", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
|
||||
showLocation: false,
|
||||
@@ -35,8 +36,10 @@ Module.register("calendar", {
|
||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
||||
hidePrivate: false,
|
||||
hideOngoing: false,
|
||||
hideTime: false,
|
||||
colored: false,
|
||||
coloredSymbolOnly: false,
|
||||
customEvents: [], // Array of {keyword: "", symbol: "", color: ""} where Keyword is a regexp and symbol/color are to be applied for matched
|
||||
tableClass: "small",
|
||||
calendars: [
|
||||
{
|
||||
@@ -55,9 +58,12 @@ Module.register("calendar", {
|
||||
excludedEvents: [],
|
||||
sliceMultiDayEvents: false,
|
||||
broadcastPastEvents: false,
|
||||
nextDaysRelative: false
|
||||
nextDaysRelative: false,
|
||||
selfSignedCert: false
|
||||
},
|
||||
|
||||
requiresVersion: "2.1.0",
|
||||
|
||||
// Define required scripts.
|
||||
getStyles: function () {
|
||||
return ["calendar.css", "font-awesome.css"];
|
||||
@@ -71,7 +77,7 @@ Module.register("calendar", {
|
||||
// Define required translations.
|
||||
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.
|
||||
// Therefore we can just return false. Otherwise we should have returned a dictionary.
|
||||
// If you're trying to build your own module including translations, check out the documentation.
|
||||
return false;
|
||||
},
|
||||
@@ -83,15 +89,22 @@ Module.register("calendar", {
|
||||
// Set locale.
|
||||
moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
|
||||
|
||||
for (var c in this.config.calendars) {
|
||||
var calendar = this.config.calendars[c];
|
||||
// clear data holder before start
|
||||
this.calendarData = {};
|
||||
|
||||
// indicate no data available yet
|
||||
this.loaded = false;
|
||||
|
||||
this.config.calendars.forEach((calendar) => {
|
||||
calendar.url = calendar.url.replace("webcal://", "http://");
|
||||
|
||||
var calendarConfig = {
|
||||
const calendarConfig = {
|
||||
maximumEntries: calendar.maximumEntries,
|
||||
maximumNumberOfDays: calendar.maximumNumberOfDays,
|
||||
broadcastPastEvents: calendar.broadcastPastEvents
|
||||
broadcastPastEvents: calendar.broadcastPastEvents,
|
||||
selfSignedCert: calendar.selfSignedCert
|
||||
};
|
||||
|
||||
if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
|
||||
calendarConfig.symbolClass = "";
|
||||
}
|
||||
@@ -112,18 +125,10 @@ Module.register("calendar", {
|
||||
};
|
||||
}
|
||||
|
||||
// tell helper to start a fetcher for this calendar
|
||||
// fetcher till cycle
|
||||
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
||||
|
||||
// Trigger ADD_CALENDAR every fetchInterval to make sure there is always a calendar
|
||||
// fetcher running on the server side.
|
||||
var self = this;
|
||||
setInterval(function () {
|
||||
self.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
||||
}, self.config.fetchInterval);
|
||||
}
|
||||
|
||||
this.calendarData = {};
|
||||
this.loaded = false;
|
||||
});
|
||||
},
|
||||
|
||||
// Override socket notification handler.
|
||||
@@ -153,8 +158,14 @@ Module.register("calendar", {
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
var events = this.createEventList();
|
||||
var wrapper = document.createElement("table");
|
||||
// Define second, minute, hour, and day constants
|
||||
const oneSecond = 1000; // 1,000 milliseconds
|
||||
const oneMinute = oneSecond * 60;
|
||||
const oneHour = oneMinute * 60;
|
||||
const oneDay = oneHour * 24;
|
||||
|
||||
const events = this.createEventList();
|
||||
const wrapper = document.createElement("table");
|
||||
wrapper.className = this.config.tableClass;
|
||||
|
||||
if (events.length === 0) {
|
||||
@@ -163,35 +174,37 @@ Module.register("calendar", {
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
let currentFadeStep = 0;
|
||||
let startFade;
|
||||
let fadeSteps;
|
||||
|
||||
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;
|
||||
startFade = events.length * this.config.fadePoint;
|
||||
fadeSteps = events.length - startFade;
|
||||
}
|
||||
|
||||
var currentFadeStep = 0;
|
||||
var lastSeenDate = "";
|
||||
let lastSeenDate = "";
|
||||
|
||||
for (var e in events) {
|
||||
var event = events[e];
|
||||
var dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
|
||||
events.forEach((event, index) => {
|
||||
const dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
|
||||
if (this.config.timeFormat === "dateheaders") {
|
||||
if (lastSeenDate !== dateAsString) {
|
||||
var dateRow = document.createElement("tr");
|
||||
const dateRow = document.createElement("tr");
|
||||
dateRow.className = "normal";
|
||||
var dateCell = document.createElement("td");
|
||||
|
||||
const dateCell = document.createElement("td");
|
||||
dateCell.colSpan = "3";
|
||||
dateCell.innerHTML = dateAsString;
|
||||
dateCell.style.paddingTop = "10px";
|
||||
dateRow.appendChild(dateCell);
|
||||
wrapper.appendChild(dateRow);
|
||||
|
||||
if (e >= startFade) {
|
||||
if (this.config.fade && index >= startFade) {
|
||||
//fading
|
||||
currentFadeStep = e - startFade;
|
||||
currentFadeStep = index - startFade;
|
||||
dateRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
|
||||
}
|
||||
|
||||
@@ -199,61 +212,89 @@ Module.register("calendar", {
|
||||
}
|
||||
}
|
||||
|
||||
var eventWrapper = document.createElement("tr");
|
||||
const eventWrapper = document.createElement("tr");
|
||||
|
||||
if (this.config.colored && !this.config.coloredSymbolOnly) {
|
||||
eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
|
||||
}
|
||||
|
||||
eventWrapper.className = "normal";
|
||||
eventWrapper.className = "normal event";
|
||||
|
||||
const symbolWrapper = document.createElement("td");
|
||||
|
||||
if (this.config.displaySymbol) {
|
||||
var symbolWrapper = document.createElement("td");
|
||||
|
||||
if (this.config.colored && this.config.coloredSymbolOnly) {
|
||||
symbolWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
|
||||
}
|
||||
|
||||
var symbolClass = this.symbolClassForUrl(event.url);
|
||||
const symbolClass = this.symbolClassForUrl(event.url);
|
||||
symbolWrapper.className = "symbol align-right " + symbolClass;
|
||||
|
||||
var symbols = this.symbolsForUrl(event.url);
|
||||
if (typeof symbols === "string") {
|
||||
symbols = [symbols];
|
||||
const symbols = this.symbolsForEvent(event);
|
||||
// If symbols are displayed and custom symbol is set, replace event symbol
|
||||
if (this.config.displaySymbol && this.config.customEvents.length > 0) {
|
||||
for (let ev in this.config.customEvents) {
|
||||
if (typeof this.config.customEvents[ev].symbol !== "undefined" && this.config.customEvents[ev].symbol !== "") {
|
||||
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||
if (needle.test(event.title)) {
|
||||
symbols[0] = this.config.customEvents[ev].symbol;
|
||||
break;
|
||||
}
|
||||
|
||||
for (var i = 0; i < symbols.length; i++) {
|
||||
var symbol = document.createElement("span");
|
||||
symbol.className = "fa fa-fw fa-" + symbols[i];
|
||||
if (i > 0) {
|
||||
}
|
||||
}
|
||||
}
|
||||
symbols.forEach((s, index) => {
|
||||
const symbol = document.createElement("span");
|
||||
symbol.className = "fa fa-fw fa-" + s;
|
||||
if (index > 0) {
|
||||
symbol.style.paddingLeft = "5px";
|
||||
}
|
||||
symbolWrapper.appendChild(symbol);
|
||||
}
|
||||
});
|
||||
eventWrapper.appendChild(symbolWrapper);
|
||||
} else if (this.config.timeFormat === "dateheaders") {
|
||||
var blankCell = document.createElement("td");
|
||||
const blankCell = document.createElement("td");
|
||||
blankCell.innerHTML = " ";
|
||||
eventWrapper.appendChild(blankCell);
|
||||
}
|
||||
|
||||
var titleWrapper = document.createElement("td"),
|
||||
repeatingCountTitle = "";
|
||||
const titleWrapper = document.createElement("td");
|
||||
let repeatingCountTitle = "";
|
||||
|
||||
if (this.config.displayRepeatingCountTitle && event.firstYear !== undefined) {
|
||||
repeatingCountTitle = this.countTitleForUrl(event.url);
|
||||
|
||||
if (repeatingCountTitle !== "") {
|
||||
var thisYear = new Date(parseInt(event.startDate)).getFullYear(),
|
||||
const thisYear = new Date(parseInt(event.startDate)).getFullYear(),
|
||||
yearDiff = thisYear - event.firstYear;
|
||||
|
||||
repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle;
|
||||
}
|
||||
}
|
||||
|
||||
// Color events if custom color is specified
|
||||
if (this.config.customEvents.length > 0) {
|
||||
for (let ev in this.config.customEvents) {
|
||||
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
|
||||
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||
if (needle.test(event.title)) {
|
||||
// Respect parameter ColoredSymbolOnly also for custom events
|
||||
if (!this.config.coloredSymbolOnly) {
|
||||
eventWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||
titleWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||
}
|
||||
if (this.config.displaySymbol) {
|
||||
symbolWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle;
|
||||
|
||||
var titleClass = this.titleClassForUrl(event.url);
|
||||
const titleClass = this.titleClassForUrl(event.url);
|
||||
|
||||
if (!this.config.colored) {
|
||||
titleWrapper.className = "title bright " + titleClass;
|
||||
@@ -261,14 +302,12 @@ Module.register("calendar", {
|
||||
titleWrapper.className = "title " + titleClass;
|
||||
}
|
||||
|
||||
var timeWrapper;
|
||||
|
||||
if (this.config.timeFormat === "dateheaders") {
|
||||
if (event.fullDayEvent) {
|
||||
titleWrapper.colSpan = "2";
|
||||
titleWrapper.align = "left";
|
||||
} else {
|
||||
timeWrapper = document.createElement("td");
|
||||
const timeWrapper = document.createElement("td");
|
||||
timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
|
||||
timeWrapper.align = "left";
|
||||
timeWrapper.style.paddingLeft = "2px";
|
||||
@@ -279,18 +318,39 @@ Module.register("calendar", {
|
||||
|
||||
eventWrapper.appendChild(titleWrapper);
|
||||
} else {
|
||||
timeWrapper = document.createElement("td");
|
||||
const timeWrapper = document.createElement("td");
|
||||
|
||||
eventWrapper.appendChild(titleWrapper);
|
||||
var now = new Date();
|
||||
// Define second, minute, hour, and day variables
|
||||
var oneSecond = 1000; // 1,000 milliseconds
|
||||
var oneMinute = oneSecond * 60;
|
||||
var oneHour = oneMinute * 60;
|
||||
var oneDay = oneHour * 24;
|
||||
const now = new Date();
|
||||
|
||||
if (this.config.timeFormat === "absolute") {
|
||||
// Use dateFormat
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||
// Add end time if showEnd
|
||||
if (this.config.showEnd) {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
||||
}
|
||||
// For full day events we use the fullDayEventDateFormat
|
||||
if (event.fullDayEvent) {
|
||||
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
||||
event.endDate -= oneSecond;
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
||||
}
|
||||
if (this.config.getRelative > 0 && event.startDate < now) {
|
||||
// Ongoing and getRelative is set
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
||||
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
||||
})
|
||||
);
|
||||
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * oneDay) {
|
||||
// Within urgency days
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
if (event.fullDayEvent && this.config.nextDaysRelative) {
|
||||
// Full days events within the next two days
|
||||
if (event.today) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
|
||||
} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
|
||||
@@ -298,67 +358,30 @@ Module.register("calendar", {
|
||||
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
} else {
|
||||
/* Check to see if the user displays absolute or relative dates with their events
|
||||
* Also check to see if an event is happening within an 'urgency' time frameElement
|
||||
* For example, if the user set an .urgency of 7 days, those events that fall within that
|
||||
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
|
||||
*
|
||||
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim
|
||||
*/
|
||||
if (this.config.timeFormat === "absolute") {
|
||||
if (this.config.urgency > 1 && event.startDate - now < this.config.urgency * oneDay) {
|
||||
// This event falls within the config.urgency period that the user has set
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").from(moment().format("YYYYMMDD")));
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
||||
}
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").from(moment().format("YYYYMMDD")));
|
||||
}
|
||||
}
|
||||
if (this.config.showEnd) {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.fullDayEventDateFormat));
|
||||
}
|
||||
} else {
|
||||
if (event.startDate >= new Date()) {
|
||||
if (event.startDate - now < 2 * oneDay) {
|
||||
// This event is within the next 48 hours (2 days)
|
||||
if (event.startDate - now < this.config.getRelative * oneHour) {
|
||||
// If event is within 6 hour, display 'in xxx' time format or moment.fromNow()
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
} else {
|
||||
if (this.config.timeFormat === "absolute" && !this.config.nextDaysRelative) {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||
} else {
|
||||
// Otherwise just say 'Today/Tomorrow at such-n-such time'
|
||||
// Show relative times
|
||||
if (event.startDate >= now) {
|
||||
// Use relative time
|
||||
if (!this.config.hideTime) {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Check to see if the user displays absolute or relative dates with their events
|
||||
* Also check to see if an event is happening within an 'urgency' time frameElement
|
||||
* For example, if the user set an .urgency of 7 days, those events that fall within that
|
||||
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
|
||||
*
|
||||
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim
|
||||
*/
|
||||
if (this.config.timeFormat === "absolute") {
|
||||
if (this.config.urgency > 1 && event.startDate - now < this.config.urgency * oneDay) {
|
||||
// This event falls within the config.urgency period that the user has set
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
moment(event.startDate, "x").calendar(null, {
|
||||
sameDay: "[" + this.translate("TODAY") + "]",
|
||||
nextDay: "[" + this.translate("TOMORROW") + "]",
|
||||
nextWeek: "dddd"
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (event.startDate - now < this.config.getRelative * oneHour) {
|
||||
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ongoing event
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
||||
@@ -366,12 +389,7 @@ Module.register("calendar", {
|
||||
})
|
||||
);
|
||||
}
|
||||
if (this.config.showEnd) {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
||||
}
|
||||
}
|
||||
//timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll');
|
||||
timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
}
|
||||
@@ -379,22 +397,22 @@ Module.register("calendar", {
|
||||
wrapper.appendChild(eventWrapper);
|
||||
|
||||
// Create fade effect.
|
||||
if (e >= startFade) {
|
||||
currentFadeStep = e - startFade;
|
||||
if (index >= startFade) {
|
||||
currentFadeStep = index - startFade;
|
||||
eventWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
|
||||
}
|
||||
|
||||
if (this.config.showLocation) {
|
||||
if (event.location !== false) {
|
||||
var locationRow = document.createElement("tr");
|
||||
const locationRow = document.createElement("tr");
|
||||
locationRow.className = "normal xsmall light";
|
||||
|
||||
if (this.config.displaySymbol) {
|
||||
var symbolCell = document.createElement("td");
|
||||
const symbolCell = document.createElement("td");
|
||||
locationRow.appendChild(symbolCell);
|
||||
}
|
||||
|
||||
var descCell = document.createElement("td");
|
||||
const descCell = document.createElement("td");
|
||||
descCell.className = "location";
|
||||
descCell.colSpan = "2";
|
||||
descCell.innerHTML = this.titleTransform(event.location, this.config.locationTitleReplace, this.config.wrapLocationEvents, this.config.maxLocationTitleLength, this.config.maxEventTitleLines);
|
||||
@@ -402,13 +420,13 @@ Module.register("calendar", {
|
||||
|
||||
wrapper.appendChild(locationRow);
|
||||
|
||||
if (e >= startFade) {
|
||||
currentFadeStep = e - startFade;
|
||||
if (index >= startFade) {
|
||||
currentFadeStep = index - startFade;
|
||||
locationRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
@@ -419,7 +437,7 @@ Module.register("calendar", {
|
||||
* it will a localeSpecification object with the system locale time format.
|
||||
*
|
||||
* @param {number} timeFormat Specifies either 12 or 24 hour time format
|
||||
* @returns {moment.LocaleSpecification}
|
||||
* @returns {moment.LocaleSpecification} formatted time
|
||||
*/
|
||||
getLocaleSpecification: function (timeFormat) {
|
||||
switch (timeFormat) {
|
||||
@@ -435,16 +453,14 @@ Module.register("calendar", {
|
||||
}
|
||||
},
|
||||
|
||||
/* hasCalendarURL(url)
|
||||
* Check if this config contains the calendar url.
|
||||
/**
|
||||
* Checks if this config contains the calendar url.
|
||||
*
|
||||
* argument url string - Url to look for.
|
||||
*
|
||||
* return bool - Has calendar url
|
||||
* @param {string} url The calendar url
|
||||
* @returns {boolean} True if the calendar config contains the url, False otherwise
|
||||
*/
|
||||
hasCalendarURL: function (url) {
|
||||
for (var c in this.config.calendars) {
|
||||
var calendar = this.config.calendars[c];
|
||||
for (const calendar of this.config.calendars) {
|
||||
if (calendar.url === url) {
|
||||
return true;
|
||||
}
|
||||
@@ -453,20 +469,22 @@ Module.register("calendar", {
|
||||
return false;
|
||||
},
|
||||
|
||||
/* createEventList()
|
||||
/**
|
||||
* Creates the sorted list of all events.
|
||||
*
|
||||
* return array - Array with events.
|
||||
* @returns {object[]} Array with events.
|
||||
*/
|
||||
createEventList: function () {
|
||||
var events = [];
|
||||
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 = JSON.parse(JSON.stringify(calendar[e])); // clone object
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day");
|
||||
const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
|
||||
let events = [];
|
||||
|
||||
for (const calendarUrl in this.calendarData) {
|
||||
const calendar = this.calendarData[calendarUrl];
|
||||
for (const e in calendar) {
|
||||
const event = JSON.parse(JSON.stringify(calendar[e])); // clone object
|
||||
|
||||
if (event.endDate < now) {
|
||||
continue;
|
||||
}
|
||||
@@ -484,19 +502,19 @@ Module.register("calendar", {
|
||||
if (this.listContainsEvent(events, event)) {
|
||||
continue;
|
||||
}
|
||||
event.url = c;
|
||||
event.url = calendarUrl;
|
||||
event.today = event.startDate >= today && event.startDate < today + 24 * 60 * 60 * 1000;
|
||||
|
||||
/* 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;
|
||||
const 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;
|
||||
const splitEvents = [];
|
||||
let midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
|
||||
let count = 1;
|
||||
while (event.endDate > midnight) {
|
||||
var thisEvent = JSON.parse(JSON.stringify(event)); // clone object
|
||||
const 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 + ")";
|
||||
@@ -510,9 +528,9 @@ Module.register("calendar", {
|
||||
event.title += " (" + count + "/" + maxCount + ")";
|
||||
splitEvents.push(event);
|
||||
|
||||
for (event of splitEvents) {
|
||||
if (event.endDate > now && event.endDate <= future) {
|
||||
events.push(event);
|
||||
for (let splitEvent of splitEvents) {
|
||||
if (splitEvent.endDate > now && splitEvent.endDate <= future) {
|
||||
events.push(splitEvent);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -524,11 +542,39 @@ Module.register("calendar", {
|
||||
events.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
// Limit the number of days displayed
|
||||
// If limitDays is set > 0, limit display to that number of days
|
||||
if (this.config.limitDays > 0) {
|
||||
let newEvents = [];
|
||||
let lastDate = today.clone().subtract(1, "days").format("YYYYMMDD");
|
||||
let days = 0;
|
||||
for (const ev of events) {
|
||||
let eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
|
||||
// if date of event is later than lastdate
|
||||
// check if we already are showing max unique days
|
||||
if (eventDate > lastDate) {
|
||||
// if the only entry in the first day is a full day event that day is not counted as unique
|
||||
if (newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
|
||||
days--;
|
||||
}
|
||||
days++;
|
||||
if (days > this.config.limitDays) {
|
||||
continue;
|
||||
} else {
|
||||
lastDate = eventDate;
|
||||
}
|
||||
}
|
||||
newEvents.push(ev);
|
||||
}
|
||||
events = newEvents;
|
||||
}
|
||||
|
||||
return events.slice(0, this.config.maximumEntries);
|
||||
},
|
||||
|
||||
listContainsEvent: function (eventList, event) {
|
||||
for (var evt of eventList) {
|
||||
for (const evt of eventList) {
|
||||
if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)) {
|
||||
return true;
|
||||
}
|
||||
@@ -536,10 +582,12 @@ Module.register("calendar", {
|
||||
return false;
|
||||
},
|
||||
|
||||
/* createEventList(url)
|
||||
/**
|
||||
* Requests node helper to add calendar url.
|
||||
*
|
||||
* argument url string - Url to add.
|
||||
* @param {string} url The calendar url to add
|
||||
* @param {object} auth The authentication method and credentials
|
||||
* @param {object} calendarConfig The config of the specific calendar
|
||||
*/
|
||||
addCalendar: function (url, auth, calendarConfig) {
|
||||
this.sendSocketNotification("ADD_CALENDAR", {
|
||||
@@ -553,103 +601,109 @@ Module.register("calendar", {
|
||||
titleClass: calendarConfig.titleClass,
|
||||
timeClass: calendarConfig.timeClass,
|
||||
auth: auth,
|
||||
broadcastPastEvents: calendarConfig.broadcastPastEvents || this.config.broadcastPastEvents
|
||||
broadcastPastEvents: calendarConfig.broadcastPastEvents || this.config.broadcastPastEvents,
|
||||
selfSignedCert: calendarConfig.selfSignedCert || this.config.selfSignedCert
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* symbolsForUrl(url)
|
||||
* Retrieves the symbols for a specific url.
|
||||
* Retrieves the symbols for a specific event.
|
||||
*
|
||||
* argument url string - Url to look for.
|
||||
*
|
||||
* return string/array - The Symbols
|
||||
* @param {object} event Event to look for.
|
||||
* @returns {string[]} The symbols
|
||||
*/
|
||||
symbolsForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "symbol", this.config.defaultSymbol);
|
||||
symbolsForEvent: function (event) {
|
||||
let symbols = this.getCalendarPropertyAsArray(event.url, "symbol", this.config.defaultSymbol);
|
||||
|
||||
if (event.recurringEvent === true && this.hasCalendarProperty(event.url, "recurringSymbol")) {
|
||||
symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "recurringSymbol", this.config.defaultSymbol), symbols);
|
||||
}
|
||||
|
||||
if (event.fullDayEvent === true && this.hasCalendarProperty(event.url, "fullDaySymbol")) {
|
||||
symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "fullDaySymbol", this.config.defaultSymbol), symbols);
|
||||
}
|
||||
|
||||
return symbols;
|
||||
},
|
||||
|
||||
mergeUnique: function (arr1, arr2) {
|
||||
return arr1.concat(
|
||||
arr2.filter(function (item) {
|
||||
return arr1.indexOf(item) === -1;
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* symbolClassForUrl(url)
|
||||
* Retrieves the symbolClass for a specific url.
|
||||
* Retrieves the symbolClass for a specific calendar url.
|
||||
*
|
||||
* @param url string - Url to look for.
|
||||
*
|
||||
* @returns string
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The class to be used for the symbols of the calendar
|
||||
*/
|
||||
symbolClassForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "symbolClass", "");
|
||||
},
|
||||
|
||||
/**
|
||||
* titleClassForUrl(url)
|
||||
* Retrieves the titleClass for a specific url.
|
||||
* Retrieves the titleClass for a specific calendar url.
|
||||
*
|
||||
* @param url string - Url to look for.
|
||||
*
|
||||
* @returns string
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The class to be used for the title of the calendar
|
||||
*/
|
||||
titleClassForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "titleClass", "");
|
||||
},
|
||||
|
||||
/**
|
||||
* timeClassForUrl(url)
|
||||
* Retrieves the timeClass for a specific url.
|
||||
* Retrieves the timeClass for a specific calendar url.
|
||||
*
|
||||
* @param url string - Url to look for.
|
||||
*
|
||||
* @returns string
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The class to be used for the time of the calendar
|
||||
*/
|
||||
timeClassForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "timeClass", "");
|
||||
},
|
||||
|
||||
/* calendarNameForUrl(url)
|
||||
* Retrieves the calendar name for a specific url.
|
||||
/**
|
||||
* Retrieves the calendar name for a specific calendar url.
|
||||
*
|
||||
* argument url string - Url to look for.
|
||||
*
|
||||
* return string - The name of the calendar
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The name of the calendar
|
||||
*/
|
||||
calendarNameForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "name", "");
|
||||
},
|
||||
|
||||
/* colorForUrl(url)
|
||||
* Retrieves the color for a specific url.
|
||||
/**
|
||||
* Retrieves the color for a specific calendar url.
|
||||
*
|
||||
* argument url string - Url to look for.
|
||||
*
|
||||
* return string - The Color
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The color
|
||||
*/
|
||||
colorForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "color", "#fff");
|
||||
},
|
||||
|
||||
/* countTitleForUrl(url)
|
||||
* Retrieves the name for a specific url.
|
||||
/**
|
||||
* Retrieves the count title for a specific calendar url.
|
||||
*
|
||||
* argument url string - Url to look for.
|
||||
*
|
||||
* return string - The Symbol
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The title
|
||||
*/
|
||||
countTitleForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "repeatingCountTitle", this.config.defaultRepeatingCountTitle);
|
||||
},
|
||||
|
||||
/* getCalendarProperty(url, property, defaultValue)
|
||||
* Helper method to retrieve the property for a specific url.
|
||||
/**
|
||||
* Helper method to retrieve the property for a specific calendar url.
|
||||
*
|
||||
* argument url string - Url to look for.
|
||||
* argument property string - Property to look for.
|
||||
* argument defaultValue string - Value if property is not found.
|
||||
*
|
||||
* return string - The Property
|
||||
* @param {string} url The calendar url
|
||||
* @param {string} property The property to look for
|
||||
* @param {string} defaultValue The value if the property is not found
|
||||
* @returns {*} The property
|
||||
*/
|
||||
getCalendarProperty: function (url, property, defaultValue) {
|
||||
for (var c in this.config.calendars) {
|
||||
var calendar = this.config.calendars[c];
|
||||
for (const calendar of this.config.calendars) {
|
||||
if (calendar.url === url && calendar.hasOwnProperty(property)) {
|
||||
return calendar[property];
|
||||
}
|
||||
@@ -658,6 +712,16 @@ Module.register("calendar", {
|
||||
return defaultValue;
|
||||
},
|
||||
|
||||
getCalendarPropertyAsArray: function (url, property, defaultValue) {
|
||||
let p = this.getCalendarProperty(url, property, defaultValue);
|
||||
if (!(p instanceof Array)) p = [p];
|
||||
return p;
|
||||
},
|
||||
|
||||
hasCalendarProperty: function (url, property) {
|
||||
return !!this.getCalendarProperty(url, property, undefined);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shortens a string if it's longer than maxLength and add a ellipsis to the end
|
||||
*
|
||||
@@ -673,13 +737,13 @@ Module.register("calendar", {
|
||||
}
|
||||
|
||||
if (wrapEvents === true) {
|
||||
var temp = "";
|
||||
var currentLine = "";
|
||||
var words = string.split(" ");
|
||||
var line = 0;
|
||||
const words = string.split(" ");
|
||||
let temp = "";
|
||||
let currentLine = "";
|
||||
let line = 0;
|
||||
|
||||
for (var i = 0; i < words.length; i++) {
|
||||
var word = words[i];
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const word = words[i];
|
||||
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
|
||||
// max - 1 to account for a space
|
||||
currentLine += word + " ";
|
||||
@@ -711,28 +775,33 @@ Module.register("calendar", {
|
||||
}
|
||||
},
|
||||
|
||||
/* capFirst(string)
|
||||
/**
|
||||
* Capitalize the first letter of a string
|
||||
* Return capitalized string
|
||||
*
|
||||
* @param {string} string The string to capitalize
|
||||
* @returns {string} The capitalized string
|
||||
*/
|
||||
capFirst: function (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
},
|
||||
|
||||
/* titleTransform(title)
|
||||
/**
|
||||
* Transforms the title of an event for usage.
|
||||
* Replaces parts of the text as defined in config.titleReplace.
|
||||
* Shortens title based on config.maxTitleLength and config.wrapEvents
|
||||
*
|
||||
* argument title string - The title to transform.
|
||||
*
|
||||
* return string - The transformed title.
|
||||
* @param {string} title The title to transform.
|
||||
* @param {object} titleReplace Pairs of strings to be replaced in the title
|
||||
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
|
||||
* @param {number} maxTitleLength The max length of the string
|
||||
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
|
||||
* @returns {string} The transformed title.
|
||||
*/
|
||||
titleTransform: function (title, titleReplace, wrapEvents, maxTitleLength, maxTitleLines) {
|
||||
for (var needle in titleReplace) {
|
||||
var replacement = titleReplace[needle];
|
||||
for (let needle in titleReplace) {
|
||||
const replacement = titleReplace[needle];
|
||||
|
||||
var regParts = needle.match(/^\/(.+)\/([gim]*)$/);
|
||||
const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
|
||||
if (regParts) {
|
||||
// the parsed pattern is a regexp.
|
||||
needle = new RegExp(regParts[1], regParts[2]);
|
||||
@@ -745,17 +814,16 @@ Module.register("calendar", {
|
||||
return title;
|
||||
},
|
||||
|
||||
/* broadcastEvents()
|
||||
/**
|
||||
* Broadcasts the events to all other modules for reuse.
|
||||
* The all events available in one array, sorted on startdate.
|
||||
*/
|
||||
broadcastEvents: function () {
|
||||
var eventList = [];
|
||||
for (var url in this.calendarData) {
|
||||
var calendar = this.calendarData[url];
|
||||
for (var e in calendar) {
|
||||
var event = cloneObject(calendar[e]);
|
||||
event.symbol = this.symbolsForUrl(url);
|
||||
const eventList = [];
|
||||
for (const url in this.calendarData) {
|
||||
for (const ev of this.calendarData[url]) {
|
||||
const event = cloneObject(ev);
|
||||
event.symbol = this.symbolsForEvent(event);
|
||||
event.calendarName = this.calendarNameForUrl(url);
|
||||
event.color = this.colorForUrl(url);
|
||||
delete event.url;
|
||||
|
@@ -4,330 +4,99 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Log = require("../../../js/logger.js");
|
||||
const ical = require("ical");
|
||||
const moment = require("moment");
|
||||
const request = require("request");
|
||||
|
||||
const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNumberOfDays, auth, includePastEvents) {
|
||||
const self = this;
|
||||
const CalendarUtils = require("./calendarutils");
|
||||
const Log = require("logger");
|
||||
const ical = require("node-ical");
|
||||
const fetch = require("node-fetch");
|
||||
const digest = require("digest-fetch");
|
||||
const https = require("https");
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url The url of the calendar to fetch
|
||||
* @param {number} reloadInterval Time in ms the calendar is fetched again
|
||||
* @param {string[]} excludedEvents An array of words / phrases from event titles that will be excluded from being shown.
|
||||
* @param {number} maximumEntries The maximum number of events fetched.
|
||||
* @param {number} maximumNumberOfDays The maximum number of days an event should be in the future.
|
||||
* @param {object} auth The object containing options for authentication against the calendar.
|
||||
* @param {boolean} includePastEvents If true events from the past maximumNumberOfDays will be fetched too
|
||||
* @param {boolean} selfSignedCert If true, the server certificate is not verified against the list of supplied CAs.
|
||||
* @class
|
||||
*/
|
||||
const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents, selfSignedCert) {
|
||||
let reloadTimer = null;
|
||||
let events = [];
|
||||
|
||||
let fetchFailedCallback = function () {};
|
||||
let eventsReceivedCallback = function () {};
|
||||
|
||||
/* fetchCalendar()
|
||||
/**
|
||||
* Initiates calendar fetch.
|
||||
*/
|
||||
const fetchCalendar = function () {
|
||||
const fetchCalendar = () => {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = null;
|
||||
|
||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
const opts = {
|
||||
headers: {
|
||||
let fetcher = null;
|
||||
let httpsAgent = null;
|
||||
let headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
|
||||
},
|
||||
gzip: true
|
||||
};
|
||||
|
||||
if (selfSignedCert) {
|
||||
httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
}
|
||||
if (auth) {
|
||||
if (auth.method === "bearer") {
|
||||
opts.auth = {
|
||||
bearer: auth.pass
|
||||
};
|
||||
headers.Authorization = "Bearer " + auth.pass;
|
||||
} else if (auth.method === "digest") {
|
||||
fetcher = new digest(auth.user, auth.pass).fetch(url, { headers: headers, httpsAgent: httpsAgent });
|
||||
} else {
|
||||
opts.auth = {
|
||||
user: auth.user,
|
||||
pass: auth.pass,
|
||||
sendImmediately: auth.method !== "digest"
|
||||
};
|
||||
headers.Authorization = "Basic " + Buffer.from(auth.user + ":" + auth.pass).toString("base64");
|
||||
}
|
||||
}
|
||||
if (fetcher === null) {
|
||||
fetcher = fetch(url, { headers: headers, httpsAgent: httpsAgent });
|
||||
}
|
||||
|
||||
request(url, opts, function (err, r, requestData) {
|
||||
if (err) {
|
||||
fetchFailedCallback(self, err);
|
||||
fetcher
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
return;
|
||||
} else if (r.statusCode !== 200) {
|
||||
fetchFailedCallback(self, r.statusCode + ": " + r.statusMessage);
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
fetchFailedCallback(this, response.statusText);
|
||||
scheduleTimer();
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.then((response) => response.text())
|
||||
.then((responseData) => {
|
||||
let data = [];
|
||||
|
||||
try {
|
||||
data = ical.parseICS(responseData);
|
||||
Log.debug("parsed data=" + JSON.stringify(data));
|
||||
events = CalendarUtils.filterEvents(data, {
|
||||
excludedEvents,
|
||||
includePastEvents,
|
||||
maximumEntries,
|
||||
maximumNumberOfDays
|
||||
});
|
||||
} catch (error) {
|
||||
fetchFailedCallback(this, error.message);
|
||||
scheduleTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = ical.parseICS(requestData);
|
||||
const newEvents = [];
|
||||
|
||||
// limitFunction doesn't do much limiting, see comment re: the dates array in rrule section below as to why we need to do the filtering ourselves
|
||||
const limitFunction = function (date, i) {
|
||||
return true;
|
||||
};
|
||||
|
||||
const eventDate = function (event, time) {
|
||||
return event[time].length === 8 ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||
};
|
||||
|
||||
Object.entries(data).forEach(([key, event]) => {
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day").toDate();
|
||||
const future = moment().startOf("day").add(maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||
let past = today;
|
||||
|
||||
if (includePastEvents) {
|
||||
past = moment().startOf("day").subtract(maximumNumberOfDays, "days").toDate();
|
||||
}
|
||||
|
||||
// FIXME: Ugly fix to solve the facebook birthday issue.
|
||||
// Otherwise, the recurring events only show the birthday for next year.
|
||||
let isFacebookBirthday = false;
|
||||
if (typeof event.uid !== "undefined") {
|
||||
if (event.uid.indexOf("@facebook.com") !== -1) {
|
||||
isFacebookBirthday = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "VEVENT") {
|
||||
let startDate = eventDate(event, "start");
|
||||
let endDate;
|
||||
|
||||
if (typeof event.end !== "undefined") {
|
||||
endDate = eventDate(event, "end");
|
||||
} else if (typeof event.duration !== "undefined") {
|
||||
endDate = startDate.clone().add(moment.duration(event.duration));
|
||||
} else {
|
||||
if (!isFacebookBirthday) {
|
||||
endDate = startDate;
|
||||
} else {
|
||||
endDate = moment(startDate).add(1, "days");
|
||||
}
|
||||
}
|
||||
|
||||
// calculate the duration of the event for use with recurring events.
|
||||
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
|
||||
if (event.start.length === 8) {
|
||||
startDate = startDate.startOf("day");
|
||||
}
|
||||
|
||||
const title = getTitleFromEvent(event);
|
||||
|
||||
let excluded = false,
|
||||
dateFilter = null;
|
||||
|
||||
for (let f in excludedEvents) {
|
||||
let filter = excludedEvents[f],
|
||||
testTitle = title.toLowerCase(),
|
||||
until = null,
|
||||
useRegex = false,
|
||||
regexFlags = "g";
|
||||
|
||||
if (filter instanceof Object) {
|
||||
if (typeof filter.until !== "undefined") {
|
||||
until = filter.until;
|
||||
}
|
||||
|
||||
if (typeof filter.regex !== "undefined") {
|
||||
useRegex = filter.regex;
|
||||
}
|
||||
|
||||
// If additional advanced filtering is added in, this section
|
||||
// must remain last as we overwrite the filter object with the
|
||||
// filterBy string
|
||||
if (filter.caseSensitive) {
|
||||
filter = filter.filterBy;
|
||||
testTitle = title;
|
||||
} else if (useRegex) {
|
||||
filter = filter.filterBy;
|
||||
testTitle = title;
|
||||
regexFlags += "i";
|
||||
} else {
|
||||
filter = filter.filterBy.toLowerCase();
|
||||
}
|
||||
} else {
|
||||
filter = filter.toLowerCase();
|
||||
}
|
||||
|
||||
if (testTitleByFilter(testTitle, filter, useRegex, regexFlags)) {
|
||||
if (until) {
|
||||
dateFilter = until;
|
||||
} else {
|
||||
excluded = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (excluded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const location = event.location || false;
|
||||
const geo = event.geo || false;
|
||||
const description = event.description || false;
|
||||
|
||||
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
|
||||
const rule = event.rrule;
|
||||
let addedEvents = 0;
|
||||
|
||||
const pastMoment = moment(past);
|
||||
const futureMoment = moment(future);
|
||||
|
||||
// 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.
|
||||
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
|
||||
const pastLocal = pastMoment.subtract(past.getTimezoneOffset(), "minutes").toDate();
|
||||
const futureLocal = futureMoment.subtract(future.getTimezoneOffset(), "minutes").toDate();
|
||||
const datesLocal = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
const dates = datesLocal.map(function (dateLocal) {
|
||||
return moment(dateLocal).add(dateLocal.getTimezoneOffset(), "minutes").toDate();
|
||||
});
|
||||
|
||||
// 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) {
|
||||
for (let 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 (let d in dates) {
|
||||
const 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 )
|
||||
const dateKey = date.toISOString().substring(0, 10);
|
||||
let curEvent = event;
|
||||
let showRecurrence = true;
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
const 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++;
|
||||
newEvents.push({
|
||||
title: recurrenceTitle,
|
||||
startDate: startDate.format("x"),
|
||||
endDate: endDate.format("x"),
|
||||
fullDayEvent: isFullDayEvent(event),
|
||||
class: event.class,
|
||||
firstYear: event.start.getFullYear(),
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
}
|
||||
}
|
||||
// end recurring event parsing
|
||||
} else {
|
||||
// Single event.
|
||||
const fullDayEvent = isFacebookBirthday ? true : isFullDayEvent(event);
|
||||
|
||||
if (includePastEvents) {
|
||||
// Past event is too far in the past, so skip.
|
||||
if (endDate < past) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// It's not a fullday event, and it is in the past, so skip.
|
||||
if (!fullDayEvent && endDate < new Date()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a fullday event, and it is before today, So skip.
|
||||
if (fullDayEvent && endDate <= today) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// It exceeds the maximumNumberOfDays limit, so skip.
|
||||
if (startDate > future) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeFilterApplies(now, endDate, dateFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust start date so multiple day events will be displayed as happening today even though they started some days ago already
|
||||
if (fullDayEvent && startDate <= today) {
|
||||
startDate = moment(today);
|
||||
}
|
||||
|
||||
// Every thing is good. Add it to the list.
|
||||
newEvents.push({
|
||||
title: title,
|
||||
startDate: startDate.format("x"),
|
||||
endDate: endDate.format("x"),
|
||||
fullDayEvent: fullDayEvent,
|
||||
class: event.class,
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
newEvents.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
events = newEvents;
|
||||
|
||||
self.broadcastEvents();
|
||||
this.broadcastEvents();
|
||||
scheduleTimer();
|
||||
});
|
||||
};
|
||||
|
||||
/* scheduleTimer()
|
||||
/**
|
||||
* Schedule the timer for the next update.
|
||||
*/
|
||||
const scheduleTimer = function () {
|
||||
@@ -337,133 +106,54 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
|
||||
}, reloadInterval);
|
||||
};
|
||||
|
||||
/* isFullDayEvent(event)
|
||||
* Checks if an event is a fullday event.
|
||||
*
|
||||
* argument event object - The event object to check.
|
||||
*
|
||||
* return bool - The event is a fullday event.
|
||||
*/
|
||||
const isFullDayEvent = function (event) {
|
||||
if (event.start.length === 8 || event.start.dateOnly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const start = event.start || 0;
|
||||
const startDate = new Date(start);
|
||||
const end = event.end || 0;
|
||||
if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
|
||||
// Is 24 hours, and starts on the middle of the night.
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/* timeFilterApplies()
|
||||
* Determines if the user defined time filter should apply
|
||||
*
|
||||
* argument now Date - Date object using previously created object for consistency
|
||||
* argument endDate Moment - Moment object representing the event end date
|
||||
* argument filter string - The time to subtract from the end date to determine if an event should be shown
|
||||
*
|
||||
* return bool - The event should be filtered out
|
||||
*/
|
||||
const timeFilterApplies = function (now, endDate, filter) {
|
||||
if (filter) {
|
||||
const until = filter.split(" "),
|
||||
value = parseInt(until[0]),
|
||||
increment = until[1].slice(-1) === "s" ? until[1] : until[1] + "s", // Massage the data for moment js
|
||||
filterUntil = moment(endDate.format()).subtract(value, increment);
|
||||
|
||||
return now < filterUntil.format("x");
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
const getTitleFromEvent = function (event) {
|
||||
let 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;
|
||||
};
|
||||
|
||||
const testTitleByFilter = function (title, filter, useRegex, regexFlags) {
|
||||
if (useRegex) {
|
||||
// Assume if leading slash, there is also trailing slash
|
||||
if (filter[0] === "/") {
|
||||
// Strip leading and trailing slashes
|
||||
filter = filter.substr(1).slice(0, -1);
|
||||
}
|
||||
|
||||
filter = new RegExp(filter, regexFlags);
|
||||
|
||||
return filter.test(title);
|
||||
} else {
|
||||
return title.includes(filter);
|
||||
}
|
||||
};
|
||||
|
||||
/* public methods */
|
||||
|
||||
/* startFetch()
|
||||
/**
|
||||
* Initiate fetchCalendar();
|
||||
*/
|
||||
this.startFetch = function () {
|
||||
fetchCalendar();
|
||||
};
|
||||
|
||||
/* broadcastItems()
|
||||
/**
|
||||
* Broadcast the existing events.
|
||||
*/
|
||||
this.broadcastEvents = function () {
|
||||
Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events.");
|
||||
eventsReceivedCallback(self);
|
||||
eventsReceivedCallback(this);
|
||||
};
|
||||
|
||||
/* onReceive(callback)
|
||||
/**
|
||||
* Sets the on success callback
|
||||
*
|
||||
* argument callback function - The on success callback.
|
||||
* @param {Function} callback The on success callback.
|
||||
*/
|
||||
this.onReceive = function (callback) {
|
||||
eventsReceivedCallback = callback;
|
||||
};
|
||||
|
||||
/* onError(callback)
|
||||
/**
|
||||
* Sets the on error callback
|
||||
*
|
||||
* argument callback function - The on error callback.
|
||||
* @param {Function} callback The on error callback.
|
||||
*/
|
||||
this.onError = function (callback) {
|
||||
fetchFailedCallback = callback;
|
||||
};
|
||||
|
||||
/* url()
|
||||
/**
|
||||
* Returns the url of this fetcher.
|
||||
*
|
||||
* return string - The url of this fetcher.
|
||||
* @returns {string} The url of this fetcher.
|
||||
*/
|
||||
this.url = function () {
|
||||
return url;
|
||||
};
|
||||
|
||||
/* events()
|
||||
/**
|
||||
* Returns current available events for this fetcher.
|
||||
*
|
||||
* return array - The current available events for this fetcher.
|
||||
* @returns {object[]} The current available events for this fetcher.
|
||||
*/
|
||||
this.events = function () {
|
||||
return events;
|
||||
|
600
modules/default/calendar/calendarutils.js
Normal file
@@ -0,0 +1,600 @@
|
||||
/* Magic Mirror
|
||||
* Calendar Util Methods
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external Moment
|
||||
*/
|
||||
const moment = require("moment");
|
||||
const path = require("path");
|
||||
const zoneTable = require(path.join(__dirname, "windowsZones.json"));
|
||||
const Log = require("../../../js/logger.js");
|
||||
|
||||
const CalendarUtils = {
|
||||
/**
|
||||
* Calculate the time correction, either dst/std or full day in cases where
|
||||
* utc time is day before plus offset
|
||||
*
|
||||
* @param {object} event
|
||||
* @param {Date} date
|
||||
* @returns {number} the necessary adjustment in hours
|
||||
*/
|
||||
calculateTimezoneAdjustment: function (event, date) {
|
||||
let adjustHours = 0;
|
||||
// if a timezone was specified
|
||||
if (!event.start.tz) {
|
||||
Log.debug(" if no tz, guess based on now");
|
||||
event.start.tz = moment.tz.guess();
|
||||
}
|
||||
Log.debug("initial tz=" + event.start.tz);
|
||||
|
||||
// if there is a start date specified
|
||||
if (event.start.tz) {
|
||||
// if this is a windows timezone
|
||||
if (event.start.tz.includes(" ")) {
|
||||
// use the lookup table to get theIANA name as moment and date don't know MS timezones
|
||||
let tz = CalendarUtils.getIanaTZFromMS(event.start.tz);
|
||||
Log.debug("corrected TZ=" + tz);
|
||||
// watch out for unregistered windows timezone names
|
||||
// if we had a successful lookup
|
||||
if (tz) {
|
||||
// change the timezone to the IANA name
|
||||
event.start.tz = tz;
|
||||
// Log.debug("corrected timezone="+event.start.tz)
|
||||
}
|
||||
}
|
||||
Log.debug("corrected tz=" + event.start.tz);
|
||||
let current_offset = 0; // offset from TZ string or calculated
|
||||
let mm = 0; // date with tz or offset
|
||||
let start_offset = 0; // utc offset of created with tz
|
||||
// if there is still an offset, lookup failed, use it
|
||||
if (event.start.tz.startsWith("(")) {
|
||||
const regex = /[+|-]\d*:\d*/;
|
||||
const start_offsetString = event.start.tz.match(regex).toString().split(":");
|
||||
let start_offset = parseInt(start_offsetString[0]);
|
||||
start_offset *= event.start.tz[1] === "-" ? -1 : 1;
|
||||
adjustHours = start_offset;
|
||||
Log.debug("defined offset=" + start_offset + " hours");
|
||||
current_offset = start_offset;
|
||||
event.start.tz = "";
|
||||
Log.debug("ical offset=" + current_offset + " date=" + date);
|
||||
mm = moment(date);
|
||||
let x = parseInt(moment(new Date()).utcOffset());
|
||||
Log.debug("net mins=" + (current_offset * 60 - x));
|
||||
|
||||
mm = mm.add(x - current_offset * 60, "minutes");
|
||||
adjustHours = (current_offset * 60 - x) / 60;
|
||||
event.start = mm.toDate();
|
||||
Log.debug("adjusted date=" + event.start);
|
||||
} else {
|
||||
// get the start time in that timezone
|
||||
let es = moment(event.start);
|
||||
// check for start date prior to start of daylight changing date
|
||||
if (es.format("YYYY") < 2007) {
|
||||
es.set("year", 2013); // if so, use a closer date
|
||||
}
|
||||
Log.debug("start date/time=" + es.toDate());
|
||||
start_offset = moment.tz(es, event.start.tz).utcOffset();
|
||||
Log.debug("start offset=" + start_offset);
|
||||
|
||||
Log.debug("start date/time w tz =" + moment.tz(moment(event.start), event.start.tz).toDate());
|
||||
|
||||
// get the specified date in that timezone
|
||||
mm = moment.tz(moment(date), event.start.tz);
|
||||
Log.debug("event date=" + mm.toDate());
|
||||
current_offset = mm.utcOffset();
|
||||
}
|
||||
Log.debug("event offset=" + current_offset + " hour=" + mm.format("H") + " event date=" + mm.toDate());
|
||||
|
||||
// if the offset is greater than 0, east of london
|
||||
if (current_offset !== start_offset) {
|
||||
// big offset
|
||||
Log.debug("offset");
|
||||
let h = parseInt(mm.format("H"));
|
||||
// check if the event time is less than the offset
|
||||
if (h > 0 && h < Math.abs(current_offset) / 60) {
|
||||
// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
|
||||
// we need to fix that
|
||||
adjustHours = 24;
|
||||
// Log.debug("adjusting date")
|
||||
}
|
||||
//-300 > -240
|
||||
//if (Math.abs(current_offset) > Math.abs(start_offset)){
|
||||
if (current_offset > start_offset) {
|
||||
adjustHours -= 1;
|
||||
Log.debug("adjust down 1 hour dst change");
|
||||
//} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
|
||||
} else if (current_offset < start_offset) {
|
||||
adjustHours += 1;
|
||||
Log.debug("adjust up 1 hour dst change");
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.debug("adjustHours=" + adjustHours);
|
||||
return adjustHours;
|
||||
},
|
||||
|
||||
filterEvents: function (data, config) {
|
||||
const newEvents = [];
|
||||
|
||||
// limitFunction doesn't do much limiting, see comment re: the dates
|
||||
// array in rrule section below as to why we need to do the filtering
|
||||
// ourselves
|
||||
const limitFunction = function (date, i) {
|
||||
return true;
|
||||
};
|
||||
|
||||
const eventDate = function (event, time) {
|
||||
return CalendarUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||
};
|
||||
|
||||
Log.debug("there are " + Object.entries(data).length + " calendar entries");
|
||||
Object.entries(data).forEach(([key, event]) => {
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day").toDate();
|
||||
const future = moment().startOf("day").add(config.maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||
let past = today;
|
||||
Log.debug("have entries ");
|
||||
if (config.includePastEvents) {
|
||||
past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
|
||||
}
|
||||
|
||||
// FIXME: Ugly fix to solve the facebook birthday issue.
|
||||
// Otherwise, the recurring events only show the birthday for next year.
|
||||
let isFacebookBirthday = false;
|
||||
if (typeof event.uid !== "undefined") {
|
||||
if (event.uid.indexOf("@facebook.com") !== -1) {
|
||||
isFacebookBirthday = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "VEVENT") {
|
||||
let startDate = eventDate(event, "start");
|
||||
let endDate;
|
||||
|
||||
Log.debug("\nevent=" + JSON.stringify(event));
|
||||
if (typeof event.end !== "undefined") {
|
||||
endDate = eventDate(event, "end");
|
||||
} else if (typeof event.duration !== "undefined") {
|
||||
endDate = startDate.clone().add(moment.duration(event.duration));
|
||||
} else {
|
||||
if (!isFacebookBirthday) {
|
||||
// make copy of start date, separate storage area
|
||||
endDate = moment(startDate.format("x"), "x");
|
||||
} else {
|
||||
endDate = moment(startDate).add(1, "days");
|
||||
}
|
||||
}
|
||||
|
||||
Log.debug(" start=" + startDate.toDate() + " end=" + endDate.toDate());
|
||||
|
||||
// calculate the duration of the event for use with recurring events.
|
||||
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
|
||||
if (event.start.length === 8) {
|
||||
startDate = startDate.startOf("day");
|
||||
}
|
||||
|
||||
const title = CalendarUtils.getTitleFromEvent(event);
|
||||
|
||||
let excluded = false,
|
||||
dateFilter = null;
|
||||
|
||||
for (let f in config.excludedEvents) {
|
||||
let filter = config.excludedEvents[f],
|
||||
testTitle = title.toLowerCase(),
|
||||
until = null,
|
||||
useRegex = false,
|
||||
regexFlags = "g";
|
||||
|
||||
if (filter instanceof Object) {
|
||||
if (typeof filter.until !== "undefined") {
|
||||
until = filter.until;
|
||||
}
|
||||
|
||||
if (typeof filter.regex !== "undefined") {
|
||||
useRegex = filter.regex;
|
||||
}
|
||||
|
||||
// If additional advanced filtering is added in, this section
|
||||
// must remain last as we overwrite the filter object with the
|
||||
// filterBy string
|
||||
if (filter.caseSensitive) {
|
||||
filter = filter.filterBy;
|
||||
testTitle = title;
|
||||
} else if (useRegex) {
|
||||
filter = filter.filterBy;
|
||||
testTitle = title;
|
||||
regexFlags += "i";
|
||||
} else {
|
||||
filter = filter.filterBy.toLowerCase();
|
||||
}
|
||||
} else {
|
||||
filter = filter.toLowerCase();
|
||||
}
|
||||
|
||||
if (CalendarUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
|
||||
if (until) {
|
||||
dateFilter = until;
|
||||
} else {
|
||||
excluded = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (excluded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const location = event.location || false;
|
||||
const geo = event.geo || false;
|
||||
const description = event.description || false;
|
||||
|
||||
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
|
||||
const rule = event.rrule;
|
||||
let addedEvents = 0;
|
||||
|
||||
const pastMoment = moment(past);
|
||||
const futureMoment = moment(future);
|
||||
|
||||
// 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.
|
||||
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
|
||||
let pastLocal = 0;
|
||||
let futureLocal = 0;
|
||||
if (CalendarUtils.isFullDayEvent(event)) {
|
||||
// if full day event, only use the date part of the ranges
|
||||
pastLocal = pastMoment.toDate();
|
||||
futureLocal = futureMoment.toDate();
|
||||
} else {
|
||||
// if we want past events
|
||||
if (config.includePastEvents) {
|
||||
// use the calculated past time for the between from
|
||||
pastLocal = pastMoment.toDate();
|
||||
} else {
|
||||
// otherwise use NOW.. cause we shouldn't use any before now
|
||||
pastLocal = moment().toDate(); //now
|
||||
}
|
||||
futureLocal = futureMoment.toDate(); // future
|
||||
}
|
||||
Log.debug(" between=" + pastLocal + " to " + futureLocal);
|
||||
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
Log.debug("title=" + event.summary + " dates=" + JSON.stringify(dates));
|
||||
// The "dates" array contains the set of dates within our desired date range range that are valid
|
||||
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
|
||||
// had its date changed from outside the range to inside the range. For the time being,
|
||||
// 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) {
|
||||
for (let 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 (let d in dates) {
|
||||
let date = dates[d];
|
||||
// ical.js started returning recurrences and exdates as ISOStrings without time information.
|
||||
// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same
|
||||
// (see https://github.com/peterbraden/ical.js/pull/84 )
|
||||
const dateKey = date.toISOString().substring(0, 10);
|
||||
let curEvent = event;
|
||||
let showRecurrence = true;
|
||||
|
||||
// get the offset of today where we are processing
|
||||
// this will be the correction we need to apply
|
||||
let nowOffset = new Date().getTimezoneOffset();
|
||||
// for full day events, the time might be off from RRULE/Luxon problem
|
||||
// get time zone offset of the rule calculated event
|
||||
let dateoffset = date.getTimezoneOffset();
|
||||
// reduce the time by the offset
|
||||
Log.debug(" recurring date is " + date + " offset is " + dateoffset);
|
||||
let dh = moment(date).format("HH");
|
||||
Log.debug(" recurring date is " + date + " offset is " + dateoffset / 60 + " Hour is " + dh);
|
||||
if (CalendarUtils.isFullDayEvent(event)) {
|
||||
Log.debug("fullday");
|
||||
// if the offset is negative, east of GMT where the problem is
|
||||
if (dateoffset < 0) {
|
||||
// if the date hour is less than the offset
|
||||
if (dh < Math.abs(dateoffset / 60)) {
|
||||
// reduce the time by the offset
|
||||
Log.debug(" recurring date is " + date + " offset is " + dateoffset);
|
||||
// apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date1 is " + date);
|
||||
}
|
||||
} else {
|
||||
// if the timezones are the same, correct date if needed
|
||||
if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh < Math.abs(dateoffset / 60)) {
|
||||
// apply the correction to the date/time back to right day
|
||||
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date2 is " + date);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// not full day, but luxon can still screw up the date on the rule processing
|
||||
// we need to correct the date to get back to the right event for
|
||||
if (dateoffset < 0) {
|
||||
// if the date hour is less than the offset
|
||||
if (dh < Math.abs(dateoffset / 60)) {
|
||||
// reduce the time by the offset
|
||||
Log.debug(" recurring date is " + date + " offset is " + dateoffset);
|
||||
// apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date1 is " + date);
|
||||
}
|
||||
} else {
|
||||
// if the timezones are the same, correct date if needed
|
||||
if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh < Math.abs(dateoffset / 60)) {
|
||||
// apply the correction to the date/time back to right day
|
||||
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date2 is " + date);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
startDate = moment(date);
|
||||
|
||||
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date);
|
||||
|
||||
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
|
||||
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
|
||||
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||
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;
|
||||
}
|
||||
Log.debug("duration=" + duration);
|
||||
|
||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
||||
if (startDate.format("x") === endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
}
|
||||
|
||||
const recurrenceTitle = CalendarUtils.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 (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (showRecurrence === true) {
|
||||
Log.debug("saving event =" + description);
|
||||
addedEvents++;
|
||||
newEvents.push({
|
||||
title: recurrenceTitle,
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
fullDayEvent: CalendarUtils.isFullDayEvent(event),
|
||||
recurringEvent: true,
|
||||
class: event.class,
|
||||
firstYear: event.start.getFullYear(),
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
}
|
||||
}
|
||||
// end recurring event parsing
|
||||
} else {
|
||||
// Single event.
|
||||
const fullDayEvent = isFacebookBirthday ? true : CalendarUtils.isFullDayEvent(event);
|
||||
// Log.debug("full day event")
|
||||
|
||||
if (config.includePastEvents) {
|
||||
// Past event is too far in the past, so skip.
|
||||
if (endDate < past) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// It's not a fullday event, and it is in the past, so skip.
|
||||
if (!fullDayEvent && endDate < new Date()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a fullday event, and it is before today, So skip.
|
||||
if (fullDayEvent && endDate <= today) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// It exceeds the maximumNumberOfDays limit, so skip.
|
||||
if (startDate > future) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust start date so multiple day events will be displayed as happening today even though they started some days ago already
|
||||
if (fullDayEvent && startDate <= today) {
|
||||
startDate = moment(today);
|
||||
}
|
||||
// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
|
||||
if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
}
|
||||
// get correction for date saving and dst change between now and then
|
||||
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, startDate.toDate());
|
||||
// Every thing is good. Add it to the list.
|
||||
newEvents.push({
|
||||
title: title,
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
fullDayEvent: fullDayEvent,
|
||||
class: event.class,
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
newEvents.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
// include up to maximumEntries current or upcoming events
|
||||
// If past events should be included, include all past events
|
||||
const now = moment();
|
||||
let entries = 0;
|
||||
let events = [];
|
||||
for (let ne of newEvents) {
|
||||
if (moment(ne.endDate, "x").isBefore(now)) {
|
||||
if (config.includePastEvents) events.push(ne);
|
||||
continue;
|
||||
}
|
||||
entries++;
|
||||
// If max events has been saved, skip the rest
|
||||
if (entries > config.maximumEntries) break;
|
||||
events.push(ne);
|
||||
}
|
||||
|
||||
return events;
|
||||
},
|
||||
|
||||
/**
|
||||
* Lookup iana tz from windows
|
||||
*
|
||||
* @param msTZName
|
||||
* @returns {*|null}
|
||||
*/
|
||||
getIanaTZFromMS: function (msTZName) {
|
||||
// Get hash entry
|
||||
const he = zoneTable[msTZName];
|
||||
// If found return iana name, else null
|
||||
return he ? he.iana[0] : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the title from the event.
|
||||
*
|
||||
* @param {object} event The event object to check.
|
||||
* @returns {string} The title of the event, or "Event" if no title is found.
|
||||
*/
|
||||
getTitleFromEvent: function (event) {
|
||||
let 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;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if an event is a fullday event.
|
||||
*
|
||||
* @param {object} event The event object to check.
|
||||
* @returns {boolean} True if the event is a fullday event, false otherwise
|
||||
*/
|
||||
isFullDayEvent: function (event) {
|
||||
if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const start = event.start || 0;
|
||||
const startDate = new Date(start);
|
||||
const end = event.end || 0;
|
||||
if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
|
||||
// Is 24 hours, and starts on the middle of the night.
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if the user defined time filter should apply
|
||||
*
|
||||
* @param {Date} now Date object using previously created object for consistency
|
||||
* @param {Moment} endDate Moment object representing the event end date
|
||||
* @param {string} filter The time to subtract from the end date to determine if an event should be shown
|
||||
* @returns {boolean} True if the event should be filtered out, false otherwise
|
||||
*/
|
||||
timeFilterApplies: function (now, endDate, filter) {
|
||||
if (filter) {
|
||||
const until = filter.split(" "),
|
||||
value = parseInt(until[0]),
|
||||
increment = until[1].slice(-1) === "s" ? until[1] : until[1] + "s", // Massage the data for moment js
|
||||
filterUntil = moment(endDate.format()).subtract(value, increment);
|
||||
|
||||
return now < filterUntil.format("x");
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param title
|
||||
* @param filter
|
||||
* @param useRegex
|
||||
* @param regexFlags
|
||||
* @returns {boolean|*}
|
||||
*/
|
||||
titleFilterApplies: function (title, filter, useRegex, regexFlags) {
|
||||
if (useRegex) {
|
||||
// Assume if leading slash, there is also trailing slash
|
||||
if (filter[0] === "/") {
|
||||
// Strip leading and trailing slashes
|
||||
filter = filter.substr(1).slice(0, -1);
|
||||
}
|
||||
|
||||
filter = new RegExp(filter, regexFlags);
|
||||
|
||||
return filter.test(title);
|
||||
} else {
|
||||
return title.includes(filter);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = CalendarUtils;
|
||||
}
|
@@ -4,11 +4,9 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
const NodeHelper = require("node_helper");
|
||||
const validUrl = require("valid-url");
|
||||
const CalendarFetcher = require("./calendarfetcher.js");
|
||||
const Log = require("../../../js/logger");
|
||||
const Log = require("logger");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
@@ -20,54 +18,70 @@ module.exports = NodeHelper.create({
|
||||
// Override socketNotificationReceived method.
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
if (notification === "ADD_CALENDAR") {
|
||||
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.id);
|
||||
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.selfSignedCert, payload.id);
|
||||
}
|
||||
},
|
||||
|
||||
/* createFetcher(url, reloadInterval)
|
||||
/**
|
||||
* Creates a fetcher for a new url if it doesn't exist yet.
|
||||
* Otherwise it reuses the existing one.
|
||||
*
|
||||
* attribute url string - URL of the news feed.
|
||||
* attribute reloadInterval number - Reload interval in milliseconds.
|
||||
* @param {string} url The url of the calendar
|
||||
* @param {number} fetchInterval How often does the calendar needs to be fetched in ms
|
||||
* @param {string[]} excludedEvents An array of words / phrases from event titles that will be excluded from being shown.
|
||||
* @param {number} maximumEntries The maximum number of events fetched.
|
||||
* @param {number} maximumNumberOfDays The maximum number of days an event should be in the future.
|
||||
* @param {object} auth The object containing options for authentication against the calendar.
|
||||
* @param {boolean} broadcastPastEvents If true events from the past maximumNumberOfDays will be included in event broadcasts
|
||||
* @param {boolean} selfSignedCert If true, the server certificate is not verified against the list of supplied CAs.
|
||||
* @param {string} identifier ID of the module
|
||||
*/
|
||||
createFetcher: function (url, fetchInterval, excludedEvents, maximumNumberOfDays, auth, broadcastPastEvents, identifier) {
|
||||
var self = this;
|
||||
|
||||
if (!validUrl.isUri(url)) {
|
||||
self.sendSocketNotification("INCORRECT_URL", { id: identifier, url: url });
|
||||
createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (error) {
|
||||
this.sendSocketNotification("INCORRECT_URL", { id: identifier, url: url });
|
||||
return;
|
||||
}
|
||||
|
||||
var fetcher;
|
||||
if (typeof self.fetchers[identifier + url] === "undefined") {
|
||||
let fetcher;
|
||||
if (typeof this.fetchers[identifier + url] === "undefined") {
|
||||
Log.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumNumberOfDays, auth, broadcastPastEvents);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
|
||||
|
||||
fetcher.onReceive(function (fetcher) {
|
||||
self.sendSocketNotification("CALENDAR_EVENTS", {
|
||||
id: identifier,
|
||||
url: fetcher.url(),
|
||||
events: fetcher.events()
|
||||
});
|
||||
fetcher.onReceive((fetcher) => {
|
||||
this.broadcastEvents(fetcher, identifier);
|
||||
});
|
||||
|
||||
fetcher.onError(function (fetcher, error) {
|
||||
fetcher.onError((fetcher, error) => {
|
||||
Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error);
|
||||
self.sendSocketNotification("FETCH_ERROR", {
|
||||
this.sendSocketNotification("FETCH_ERROR", {
|
||||
id: identifier,
|
||||
url: fetcher.url(),
|
||||
error: error
|
||||
});
|
||||
});
|
||||
|
||||
self.fetchers[identifier + url] = fetcher;
|
||||
this.fetchers[identifier + url] = fetcher;
|
||||
} else {
|
||||
Log.log("Use existing calendar fetcher for url: " + url);
|
||||
fetcher = self.fetchers[identifier + url];
|
||||
fetcher = this.fetchers[identifier + url];
|
||||
fetcher.broadcastEvents();
|
||||
}
|
||||
|
||||
fetcher.startFetch();
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} fetcher the fetcher associated with the calendar
|
||||
* @param {string} identifier the identifier of the calendar
|
||||
*/
|
||||
broadcastEvents: function (fetcher, identifier) {
|
||||
this.sendSocketNotification("CALENDAR_EVENTS", {
|
||||
id: identifier,
|
||||
url: fetcher.url(),
|
||||
events: fetcher.events()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
237
modules/default/calendar/windowsZones.json
Normal file
@@ -0,0 +1,237 @@
|
||||
{
|
||||
"Dateline Standard Time": { "iana": ["Etc/GMT+12"] },
|
||||
"UTC-11": { "iana": ["Etc/GMT+11"] },
|
||||
"Aleutian Standard Time": { "iana": ["America/Adak"] },
|
||||
"Hawaiian Standard Time": { "iana": ["Pacific/Honolulu"] },
|
||||
"Marquesas Standard Time": { "iana": ["Pacific/Marquesas"] },
|
||||
"Alaskan Standard Time": { "iana": ["America/Anchorage"] },
|
||||
"UTC-09": { "iana": ["Etc/GMT+9"] },
|
||||
"Pacific Standard Time (Mexico)": { "iana": ["America/Tijuana"] },
|
||||
"UTC-08": { "iana": ["Etc/GMT+8"] },
|
||||
"Pacific Standard Time": { "iana": ["America/Los_Angeles"] },
|
||||
"US Mountain Standard Time": { "iana": ["America/Phoenix"] },
|
||||
"Mountain Standard Time (Mexico)": { "iana": ["America/Chihuahua"] },
|
||||
"Mountain Standard Time": { "iana": ["America/Denver"] },
|
||||
"Central America Standard Time": { "iana": ["America/Guatemala"] },
|
||||
"Central Standard Time": { "iana": ["America/Chicago"] },
|
||||
"Easter Island Standard Time": { "iana": ["Pacific/Easter"] },
|
||||
"Central Standard Time (Mexico)": { "iana": ["America/Mexico_City"] },
|
||||
"Canada Central Standard Time": { "iana": ["America/Regina"] },
|
||||
"SA Pacific Standard Time": { "iana": ["America/Bogota"] },
|
||||
"Eastern Standard Time (Mexico)": { "iana": ["America/Cancun"] },
|
||||
"Eastern Standard Time": { "iana": ["America/New_York"] },
|
||||
"Haiti Standard Time": { "iana": ["America/Port-au-Prince"] },
|
||||
"Cuba Standard Time": { "iana": ["America/Havana"] },
|
||||
"US Eastern Standard Time": { "iana": ["America/Indianapolis"] },
|
||||
"Turks And Caicos Standard Time": { "iana": ["America/Grand_Turk"] },
|
||||
"Paraguay Standard Time": { "iana": ["America/Asuncion"] },
|
||||
"Atlantic Standard Time": { "iana": ["America/Halifax"] },
|
||||
"Venezuela Standard Time": { "iana": ["America/Caracas"] },
|
||||
"Central Brazilian Standard Time": { "iana": ["America/Cuiaba"] },
|
||||
"SA Western Standard Time": { "iana": ["America/La_Paz"] },
|
||||
"Pacific SA Standard Time": { "iana": ["America/Santiago"] },
|
||||
"Newfoundland Standard Time": { "iana": ["America/St_Johns"] },
|
||||
"Tocantins Standard Time": { "iana": ["America/Araguaina"] },
|
||||
"E. South America Standard Time": { "iana": ["America/Sao_Paulo"] },
|
||||
"SA Eastern Standard Time": { "iana": ["America/Cayenne"] },
|
||||
"Argentina Standard Time": { "iana": ["America/Buenos_Aires"] },
|
||||
"Greenland Standard Time": { "iana": ["America/Godthab"] },
|
||||
"Montevideo Standard Time": { "iana": ["America/Montevideo"] },
|
||||
"Magallanes Standard Time": { "iana": ["America/Punta_Arenas"] },
|
||||
"Saint Pierre Standard Time": { "iana": ["America/Miquelon"] },
|
||||
"Bahia Standard Time": { "iana": ["America/Bahia"] },
|
||||
"UTC-02": { "iana": ["Etc/GMT+2"] },
|
||||
"Azores Standard Time": { "iana": ["Atlantic/Azores"] },
|
||||
"Cape Verde Standard Time": { "iana": ["Atlantic/Cape_Verde"] },
|
||||
"UTC": { "iana": ["Etc/GMT"] },
|
||||
"GMT Standard Time": { "iana": ["Europe/London"] },
|
||||
"Greenwich Standard Time": { "iana": ["Atlantic/Reykjavik"] },
|
||||
"Sao Tome Standard Time": { "iana": ["Africa/Sao_Tome"] },
|
||||
"Morocco Standard Time": { "iana": ["Africa/Casablanca"] },
|
||||
"W. Europe Standard Time": { "iana": ["Europe/Berlin"] },
|
||||
"Central Europe Standard Time": { "iana": ["Europe/Budapest"] },
|
||||
"Romance Standard Time": { "iana": ["Europe/Paris"] },
|
||||
"Central European Standard Time": { "iana": ["Europe/Warsaw"] },
|
||||
"W. Central Africa Standard Time": { "iana": ["Africa/Lagos"] },
|
||||
"Jordan Standard Time": { "iana": ["Asia/Amman"] },
|
||||
"GTB Standard Time": { "iana": ["Europe/Bucharest"] },
|
||||
"Middle East Standard Time": { "iana": ["Asia/Beirut"] },
|
||||
"Egypt Standard Time": { "iana": ["Africa/Cairo"] },
|
||||
"E. Europe Standard Time": { "iana": ["Europe/Chisinau"] },
|
||||
"Syria Standard Time": { "iana": ["Asia/Damascus"] },
|
||||
"West Bank Standard Time": { "iana": ["Asia/Hebron"] },
|
||||
"South Africa Standard Time": { "iana": ["Africa/Johannesburg"] },
|
||||
"FLE Standard Time": { "iana": ["Europe/Kiev"] },
|
||||
"Israel Standard Time": { "iana": ["Asia/Jerusalem"] },
|
||||
"Kaliningrad Standard Time": { "iana": ["Europe/Kaliningrad"] },
|
||||
"Sudan Standard Time": { "iana": ["Africa/Khartoum"] },
|
||||
"Libya Standard Time": { "iana": ["Africa/Tripoli"] },
|
||||
"Namibia Standard Time": { "iana": ["Africa/Windhoek"] },
|
||||
"Arabic Standard Time": { "iana": ["Asia/Baghdad"] },
|
||||
"Turkey Standard Time": { "iana": ["Europe/Istanbul"] },
|
||||
"Arab Standard Time": { "iana": ["Asia/Riyadh"] },
|
||||
"Belarus Standard Time": { "iana": ["Europe/Minsk"] },
|
||||
"Russian Standard Time": { "iana": ["Europe/Moscow"] },
|
||||
"E. Africa Standard Time": { "iana": ["Africa/Nairobi"] },
|
||||
"Iran Standard Time": { "iana": ["Asia/Tehran"] },
|
||||
"Arabian Standard Time": { "iana": ["Asia/Dubai"] },
|
||||
"Astrakhan Standard Time": { "iana": ["Europe/Astrakhan"] },
|
||||
"Azerbaijan Standard Time": { "iana": ["Asia/Baku"] },
|
||||
"Russia Time Zone 3": { "iana": ["Europe/Samara"] },
|
||||
"Mauritius Standard Time": { "iana": ["Indian/Mauritius"] },
|
||||
"Saratov Standard Time": { "iana": ["Europe/Saratov"] },
|
||||
"Georgian Standard Time": { "iana": ["Asia/Tbilisi"] },
|
||||
"Volgograd Standard Time": { "iana": ["Europe/Volgograd"] },
|
||||
"Caucasus Standard Time": { "iana": ["Asia/Yerevan"] },
|
||||
"Afghanistan Standard Time": { "iana": ["Asia/Kabul"] },
|
||||
"West Asia Standard Time": { "iana": ["Asia/Tashkent"] },
|
||||
"Ekaterinburg Standard Time": { "iana": ["Asia/Yekaterinburg"] },
|
||||
"Pakistan Standard Time": { "iana": ["Asia/Karachi"] },
|
||||
"Qyzylorda Standard Time": { "iana": ["Asia/Qyzylorda"] },
|
||||
"India Standard Time": { "iana": ["Asia/Calcutta"] },
|
||||
"Sri Lanka Standard Time": { "iana": ["Asia/Colombo"] },
|
||||
"Nepal Standard Time": { "iana": ["Asia/Katmandu"] },
|
||||
"Central Asia Standard Time": { "iana": ["Asia/Almaty"] },
|
||||
"Bangladesh Standard Time": { "iana": ["Asia/Dhaka"] },
|
||||
"Omsk Standard Time": { "iana": ["Asia/Omsk"] },
|
||||
"Myanmar Standard Time": { "iana": ["Asia/Rangoon"] },
|
||||
"SE Asia Standard Time": { "iana": ["Asia/Bangkok"] },
|
||||
"Altai Standard Time": { "iana": ["Asia/Barnaul"] },
|
||||
"W. Mongolia Standard Time": { "iana": ["Asia/Hovd"] },
|
||||
"North Asia Standard Time": { "iana": ["Asia/Krasnoyarsk"] },
|
||||
"N. Central Asia Standard Time": { "iana": ["Asia/Novosibirsk"] },
|
||||
"Tomsk Standard Time": { "iana": ["Asia/Tomsk"] },
|
||||
"China Standard Time": { "iana": ["Asia/Shanghai"] },
|
||||
"North Asia East Standard Time": { "iana": ["Asia/Irkutsk"] },
|
||||
"Singapore Standard Time": { "iana": ["Asia/Singapore"] },
|
||||
"W. Australia Standard Time": { "iana": ["Australia/Perth"] },
|
||||
"Taipei Standard Time": { "iana": ["Asia/Taipei"] },
|
||||
"Ulaanbaatar Standard Time": { "iana": ["Asia/Ulaanbaatar"] },
|
||||
"Aus Central W. Standard Time": { "iana": ["Australia/Eucla"] },
|
||||
"Transbaikal Standard Time": { "iana": ["Asia/Chita"] },
|
||||
"Tokyo Standard Time": { "iana": ["Asia/Tokyo"] },
|
||||
"North Korea Standard Time": { "iana": ["Asia/Pyongyang"] },
|
||||
"Korea Standard Time": { "iana": ["Asia/Seoul"] },
|
||||
"Yakutsk Standard Time": { "iana": ["Asia/Yakutsk"] },
|
||||
"Cen. Australia Standard Time": { "iana": ["Australia/Adelaide"] },
|
||||
"AUS Central Standard Time": { "iana": ["Australia/Darwin"] },
|
||||
"E. Australia Standard Time": { "iana": ["Australia/Brisbane"] },
|
||||
"AUS Eastern Standard Time": { "iana": ["Australia/Sydney"] },
|
||||
"West Pacific Standard Time": { "iana": ["Pacific/Port_Moresby"] },
|
||||
"Tasmania Standard Time": { "iana": ["Australia/Hobart"] },
|
||||
"Vladivostok Standard Time": { "iana": ["Asia/Vladivostok"] },
|
||||
"Lord Howe Standard Time": { "iana": ["Australia/Lord_Howe"] },
|
||||
"Bougainville Standard Time": { "iana": ["Pacific/Bougainville"] },
|
||||
"Russia Time Zone 10": { "iana": ["Asia/Srednekolymsk"] },
|
||||
"Magadan Standard Time": { "iana": ["Asia/Magadan"] },
|
||||
"Norfolk Standard Time": { "iana": ["Pacific/Norfolk"] },
|
||||
"Sakhalin Standard Time": { "iana": ["Asia/Sakhalin"] },
|
||||
"Central Pacific Standard Time": { "iana": ["Pacific/Guadalcanal"] },
|
||||
"Russia Time Zone 11": { "iana": ["Asia/Kamchatka"] },
|
||||
"New Zealand Standard Time": { "iana": ["Pacific/Auckland"] },
|
||||
"UTC+12": { "iana": ["Etc/GMT-12"] },
|
||||
"Fiji Standard Time": { "iana": ["Pacific/Fiji"] },
|
||||
"Chatham Islands Standard Time": { "iana": ["Pacific/Chatham"] },
|
||||
"UTC+13": { "iana": ["Etc/GMT-13"] },
|
||||
"Tonga Standard Time": { "iana": ["Pacific/Tongatapu"] },
|
||||
"Samoa Standard Time": { "iana": ["Pacific/Apia"] },
|
||||
"Line Islands Standard Time": { "iana": ["Pacific/Kiritimati"] },
|
||||
"(UTC-12:00) International Date Line West": { "iana": ["Etc/GMT+12"] },
|
||||
"(UTC-11:00) Midway Island, Samoa": { "iana": ["Pacific/Apia"] },
|
||||
"(UTC-10:00) Hawaii": { "iana": ["Pacific/Honolulu"] },
|
||||
"(UTC-09:00) Alaska": { "iana": ["America/Anchorage"] },
|
||||
"(UTC-08:00) Pacific Time (US & Canada); Tijuana": { "iana": ["America/Los_Angeles"] },
|
||||
"(UTC-08:00) Pacific Time (US and Canada); Tijuana": { "iana": ["America/Los_Angeles"] },
|
||||
"(UTC-07:00) Mountain Time (US & Canada)": { "iana": ["America/Denver"] },
|
||||
"(UTC-07:00) Mountain Time (US and Canada)": { "iana": ["America/Denver"] },
|
||||
"(UTC-07:00) Chihuahua, La Paz, Mazatlan": { "iana": [null] },
|
||||
"(UTC-07:00) Arizona": { "iana": ["America/Phoenix"] },
|
||||
"(UTC-06:00) Central Time (US & Canada)": { "iana": ["America/Chicago"] },
|
||||
"(UTC-06:00) Central Time (US and Canada)": { "iana": ["America/Chicago"] },
|
||||
"(UTC-06:00) Saskatchewan": { "iana": ["America/Regina"] },
|
||||
"(UTC-06:00) Guadalajara, Mexico City, Monterrey": { "iana": [null] },
|
||||
"(UTC-06:00) Central America": { "iana": ["America/Guatemala"] },
|
||||
"(UTC-05:00) Eastern Time (US & Canada)": { "iana": ["America/New_York"] },
|
||||
"(UTC-05:00) Eastern Time (US and Canada)": { "iana": ["America/New_York"] },
|
||||
"(UTC-05:00) Indiana (East)": { "iana": ["America/Indianapolis"] },
|
||||
"(UTC-05:00) Bogota, Lima, Quito": { "iana": ["America/Bogota"] },
|
||||
"(UTC-04:00) Atlantic Time (Canada)": { "iana": ["America/Halifax"] },
|
||||
"(UTC-04:00) Georgetown, La Paz, San Juan": { "iana": ["America/La_Paz"] },
|
||||
"(UTC-04:00) Santiago": { "iana": ["America/Santiago"] },
|
||||
"(UTC-03:30) Newfoundland": { "iana": [null] },
|
||||
"(UTC-03:00) Brasilia": { "iana": ["America/Sao_Paulo"] },
|
||||
"(UTC-03:00) Georgetown": { "iana": ["America/Cayenne"] },
|
||||
"(UTC-03:00) Greenland": { "iana": ["America/Godthab"] },
|
||||
"(UTC-02:00) Mid-Atlantic": { "iana": [null] },
|
||||
"(UTC-01:00) Azores": { "iana": ["Atlantic/Azores"] },
|
||||
"(UTC-01:00) Cape Verde Islands": { "iana": ["Atlantic/Cape_Verde"] },
|
||||
"(UTC) Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London": { "iana": [null] },
|
||||
"(UTC) Monrovia, Reykjavik": { "iana": ["Atlantic/Reykjavik"] },
|
||||
"(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague": { "iana": ["Europe/Budapest"] },
|
||||
"(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb": { "iana": ["Europe/Warsaw"] },
|
||||
"(UTC+01:00) Brussels, Copenhagen, Madrid, Paris": { "iana": ["Europe/Paris"] },
|
||||
"(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna": { "iana": ["Europe/Berlin"] },
|
||||
"(UTC+01:00) West Central Africa": { "iana": ["Africa/Lagos"] },
|
||||
"(UTC+02:00) Minsk": { "iana": ["Europe/Chisinau"] },
|
||||
"(UTC+02:00) Cairo": { "iana": ["Africa/Cairo"] },
|
||||
"(UTC+02:00) Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius": { "iana": ["Europe/Kiev"] },
|
||||
"(UTC+02:00) Athens, Bucharest, Istanbul": { "iana": ["Europe/Bucharest"] },
|
||||
"(UTC+02:00) Jerusalem": { "iana": ["Asia/Jerusalem"] },
|
||||
"(UTC+02:00) Harare, Pretoria": { "iana": ["Africa/Johannesburg"] },
|
||||
"(UTC+03:00) Moscow, St. Petersburg, Volgograd": { "iana": ["Europe/Moscow"] },
|
||||
"(UTC+03:00) Kuwait, Riyadh": { "iana": ["Asia/Riyadh"] },
|
||||
"(UTC+03:00) Nairobi": { "iana": ["Africa/Nairobi"] },
|
||||
"(UTC+03:00) Baghdad": { "iana": ["Asia/Baghdad"] },
|
||||
"(UTC+03:30) Tehran": { "iana": ["Asia/Tehran"] },
|
||||
"(UTC+04:00) Abu Dhabi, Muscat": { "iana": ["Asia/Dubai"] },
|
||||
"(UTC+04:00) Baku, Tbilisi, Yerevan": { "iana": ["Asia/Yerevan"] },
|
||||
"(UTC+04:30) Kabul": { "iana": [null] },
|
||||
"(UTC+05:00) Ekaterinburg": { "iana": ["Asia/Yekaterinburg"] },
|
||||
"(UTC+05:00) Tashkent": { "iana": ["Asia/Tashkent"] },
|
||||
"(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi": { "iana": ["Asia/Calcutta"] },
|
||||
"(UTC+05:45) Kathmandu": { "iana": ["Asia/Katmandu"] },
|
||||
"(UTC+06:00) Astana, Dhaka": { "iana": ["Asia/Almaty"] },
|
||||
"(UTC+06:00) Sri Jayawardenepura": { "iana": ["Asia/Colombo"] },
|
||||
"(UTC+06:00) Almaty, Novosibirsk": { "iana": ["Asia/Novosibirsk"] },
|
||||
"(UTC+06:30) Yangon (Rangoon)": { "iana": ["Asia/Rangoon"] },
|
||||
"(UTC+07:00) Bangkok, Hanoi, Jakarta": { "iana": ["Asia/Bangkok"] },
|
||||
"(UTC+07:00) Krasnoyarsk": { "iana": ["Asia/Krasnoyarsk"] },
|
||||
"(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi": { "iana": ["Asia/Shanghai"] },
|
||||
"(UTC+08:00) Kuala Lumpur, Singapore": { "iana": ["Asia/Singapore"] },
|
||||
"(UTC+08:00) Taipei": { "iana": ["Asia/Taipei"] },
|
||||
"(UTC+08:00) Perth": { "iana": ["Australia/Perth"] },
|
||||
"(UTC+08:00) Irkutsk, Ulaanbaatar": { "iana": ["Asia/Irkutsk"] },
|
||||
"(UTC+09:00) Seoul": { "iana": ["Asia/Seoul"] },
|
||||
"(UTC+09:00) Osaka, Sapporo, Tokyo": { "iana": ["Asia/Tokyo"] },
|
||||
"(UTC+09:00) Yakutsk": { "iana": ["Asia/Yakutsk"] },
|
||||
"(UTC+09:30) Darwin": { "iana": ["Australia/Darwin"] },
|
||||
"(UTC+09:30) Adelaide": { "iana": ["Australia/Adelaide"] },
|
||||
"(UTC+10:00) Canberra, Melbourne, Sydney": { "iana": ["Australia/Sydney"] },
|
||||
"(GMT+10:00) Canberra, Melbourne, Sydney": { "iana": ["Australia/Sydney"] },
|
||||
"(UTC+10:00) Brisbane": { "iana": ["Australia/Brisbane"] },
|
||||
"(UTC+10:00) Hobart": { "iana": ["Australia/Hobart"] },
|
||||
"(UTC+10:00) Vladivostok": { "iana": ["Asia/Vladivostok"] },
|
||||
"(UTC+10:00) Guam, Port Moresby": { "iana": ["Pacific/Port_Moresby"] },
|
||||
"(UTC+11:00) Magadan, Solomon Islands, New Caledonia": { "iana": ["Pacific/Guadalcanal"] },
|
||||
"(UTC+12:00) Fiji, Kamchatka, Marshall Is.": { "iana": [null] },
|
||||
"(UTC+12:00) Auckland, Wellington": { "iana": ["Pacific/Auckland"] },
|
||||
"(UTC+13:00) Nuku'alofa": { "iana": ["Pacific/Tongatapu"] },
|
||||
"(UTC-03:00) Buenos Aires": { "iana": ["America/Buenos_Aires"] },
|
||||
"(UTC+02:00) Beirut": { "iana": ["Asia/Beirut"] },
|
||||
"(UTC+02:00) Amman": { "iana": ["Asia/Amman"] },
|
||||
"(UTC-06:00) Guadalajara, Mexico City, Monterrey - New": { "iana": ["America/Mexico_City"] },
|
||||
"(UTC-07:00) Chihuahua, La Paz, Mazatlan - New": { "iana": ["America/Chihuahua"] },
|
||||
"(UTC-08:00) Tijuana, Baja California": { "iana": ["America/Tijuana"] },
|
||||
"(UTC+02:00) Windhoek": { "iana": ["Africa/Windhoek"] },
|
||||
"(UTC+03:00) Tbilisi": { "iana": ["Asia/Tbilisi"] },
|
||||
"(UTC-04:00) Manaus": { "iana": ["America/Cuiaba"] },
|
||||
"(UTC-03:00) Montevideo": { "iana": ["America/Montevideo"] },
|
||||
"(UTC+04:00) Yerevan": { "iana": [null] },
|
||||
"(UTC-04:30) Caracas": { "iana": ["America/Caracas"] },
|
||||
"(UTC) Casablanca": { "iana": ["Africa/Casablanca"] },
|
||||
"(UTC+05:00) Islamabad, Karachi": { "iana": ["Asia/Karachi"] },
|
||||
"(UTC+04:00) Port Louis": { "iana": ["Indian/Mauritius"] },
|
||||
"(UTC) Coordinated Universal Time": { "iana": ["Etc/GMT"] },
|
||||
"(UTC-04:00) Asuncion": { "iana": ["America/Asuncion"] },
|
||||
"(UTC+12:00) Petropavlovsk-Kamchatsky": { "iana": [null] }
|
||||
}
|
@@ -152,6 +152,13 @@ Module.register("clock", {
|
||||
timeWrapper.appendChild(periodWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the time according to the config
|
||||
*
|
||||
* @param {object} config The config of the module
|
||||
* @param {object} time time to format
|
||||
* @returns {string} The formatted time string
|
||||
*/
|
||||
function formatTime(config, time) {
|
||||
var formatString = hourSymbol + ":mm";
|
||||
if (config.showPeriod && config.timeFormat !== 24) {
|
||||
@@ -159,6 +166,7 @@ Module.register("clock", {
|
||||
}
|
||||
return moment(time).format(formatString);
|
||||
}
|
||||
|
||||
if (this.config.showSunTimes) {
|
||||
const sunTimes = SunCalc.getTimes(now, this.config.lat, this.config.lon);
|
||||
const isVisible = now.isBetween(sunTimes.sunrise, sunTimes.sunset);
|
||||
@@ -179,10 +187,10 @@ Module.register("clock", {
|
||||
'"><i class="fa fa-sun-o" aria-hidden="true"></i> ' +
|
||||
untilNextEventString +
|
||||
"</span>" +
|
||||
'<span><i class="fa fa-arrow-up" aria-hidden="true"></i>' +
|
||||
'<span><i class="fa fa-arrow-up" aria-hidden="true"></i> ' +
|
||||
formatTime(this.config, sunTimes.sunrise) +
|
||||
"</span>" +
|
||||
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i>' +
|
||||
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i> ' +
|
||||
formatTime(this.config, sunTimes.sunset) +
|
||||
"</span>";
|
||||
}
|
||||
|
@@ -1 +1 @@
|
||||
<svg id="Hour_Markers_-_Singlets" data-name="Hour Markers - Singlets" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1,.cls-2{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;}.cls-2{stroke-width:0.5px;}</style></defs><title>face-001</title><line class="cls-1" x1="125" y1="1.25" x2="125" y2="16.23"/><line class="cls-1" x1="186.87" y1="17.83" x2="179.39" y2="30.8"/><line class="cls-1" x1="232.17" y1="63.12" x2="219.2" y2="70.61"/><line class="cls-1" x1="248.75" y1="125" x2="233.77" y2="125"/><line class="cls-1" x1="232.17" y1="186.87" x2="219.2" y2="179.39"/><line class="cls-1" x1="186.88" y1="232.17" x2="179.39" y2="219.2"/><line class="cls-1" x1="125" y1="248.75" x2="125" y2="233.77"/><line class="cls-1" x1="63.13" y1="232.17" x2="70.61" y2="219.2"/><line class="cls-1" x1="17.83" y1="186.88" x2="30.8" y2="179.39"/><line class="cls-1" x1="1.25" y1="125" x2="16.23" y2="125"/><line class="cls-1" x1="17.83" y1="63.13" x2="30.8" y2="70.61"/><line class="cls-1" x1="63.12" y1="17.83" x2="70.61" y2="30.8"/><line class="cls-2" x1="138.01" y1="1.25" x2="136.96" y2="11.23"/><line class="cls-2" x1="150.87" y1="3.29" x2="148.78" y2="13.11"/><line class="cls-2" x1="163.45" y1="6.66" x2="160.35" y2="16.21"/><line class="cls-2" x1="175.61" y1="11.33" x2="171.53" y2="20.5"/><line class="cls-2" x1="198.14" y1="24.33" x2="192.24" y2="32.45"/><line class="cls-2" x1="208.26" y1="32.53" x2="201.54" y2="39.99"/><line class="cls-2" x1="217.47" y1="41.74" x2="210.01" y2="48.46"/><line class="cls-2" x1="225.67" y1="51.86" x2="217.55" y2="57.76"/><line class="cls-2" x1="238.67" y1="74.39" x2="229.5" y2="78.47"/><line class="cls-2" x1="243.34" y1="86.55" x2="233.79" y2="89.65"/><line class="cls-2" x1="246.71" y1="99.13" x2="236.89" y2="101.22"/><line class="cls-2" x1="248.75" y1="111.99" x2="238.77" y2="113.04"/><line class="cls-2" x1="248.75" y1="138.01" x2="238.77" y2="136.96"/><line class="cls-2" x1="246.71" y1="150.87" x2="236.89" y2="148.78"/><line class="cls-2" x1="243.34" y1="163.45" x2="233.79" y2="160.35"/><line class="cls-2" x1="238.67" y1="175.61" x2="229.5" y2="171.53"/><line class="cls-2" x1="225.67" y1="198.14" x2="217.55" y2="192.24"/><line class="cls-2" x1="217.47" y1="208.26" x2="210.01" y2="201.54"/><line class="cls-2" x1="208.26" y1="217.47" x2="201.54" y2="210.01"/><line class="cls-2" x1="198.14" y1="225.67" x2="192.24" y2="217.55"/><line class="cls-2" x1="175.61" y1="238.67" x2="171.53" y2="229.5"/><line class="cls-2" x1="163.45" y1="243.34" x2="160.35" y2="233.79"/><line class="cls-2" x1="150.87" y1="246.71" x2="148.78" y2="236.89"/><line class="cls-2" x1="138.01" y1="248.75" x2="136.96" y2="238.77"/><line class="cls-2" x1="111.99" y1="248.75" x2="113.04" y2="238.77"/><line class="cls-2" x1="99.13" y1="246.71" x2="101.22" y2="236.89"/><line class="cls-2" x1="86.55" y1="243.34" x2="89.65" y2="233.79"/><line class="cls-2" x1="74.39" y1="238.67" x2="78.47" y2="229.5"/><line class="cls-2" x1="51.86" y1="225.67" x2="57.76" y2="217.55"/><line class="cls-2" x1="41.74" y1="217.47" x2="48.46" y2="210.01"/><line class="cls-2" x1="32.53" y1="208.26" x2="39.99" y2="201.54"/><line class="cls-2" x1="24.33" y1="198.14" x2="32.45" y2="192.24"/><line class="cls-2" x1="11.33" y1="175.61" x2="20.5" y2="171.53"/><line class="cls-2" x1="6.66" y1="163.45" x2="16.21" y2="160.35"/><line class="cls-2" x1="3.29" y1="150.87" x2="13.11" y2="148.78"/><line class="cls-2" x1="1.25" y1="138.01" x2="11.23" y2="136.96"/><line class="cls-2" x1="1.25" y1="111.99" x2="11.23" y2="113.04"/><line class="cls-2" x1="3.29" y1="99.13" x2="13.11" y2="101.22"/><line class="cls-2" x1="6.66" y1="86.55" x2="16.21" y2="89.65"/><line class="cls-2" x1="11.33" y1="74.39" x2="20.5" y2="78.47"/><line class="cls-2" x1="24.33" y1="51.86" x2="32.45" y2="57.76"/><line class="cls-2" x1="32.53" y1="41.74" x2="39.99" y2="48.46"/><line class="cls-2" x1="41.74" y1="32.53" x2="48.46" y2="39.99"/><line class="cls-2" x1="51.86" y1="24.33" x2="57.76" y2="32.45"/><line class="cls-2" x1="74.39" y1="11.33" x2="78.47" y2="20.5"/><line class="cls-2" x1="86.55" y1="6.66" x2="89.65" y2="16.21"/><line class="cls-2" x1="99.13" y1="3.29" x2="101.22" y2="13.11"/><line class="cls-2" x1="111.99" y1="1.25" x2="113.04" y2="11.23"/></svg>
|
||||
<svg id="Hour_Markers_-_Singlets" data-name="Hour Markers - Singlets" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1,.cls-2{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;}.cls-2{stroke-width:0.5px;}</style></defs><title>face-001</title><line class="cls-1" x1="125" y1="1.25" x2="125" y2="16.23"/><line class="cls-1" x1="186.87" y1="17.83" x2="179.39" y2="30.8"/><line class="cls-1" x1="232.17" y1="63.12" x2="219.2" y2="70.61"/><line class="cls-1" x1="248.75" y1="125" x2="233.77" y2="125"/><line class="cls-1" x1="232.17" y1="186.87" x2="219.2" y2="179.39"/><line class="cls-1" x1="186.88" y1="232.17" x2="179.39" y2="219.2"/><line class="cls-1" x1="125" y1="248.75" x2="125" y2="233.77"/><line class="cls-1" x1="63.13" y1="232.17" x2="70.61" y2="219.2"/><line class="cls-1" x1="17.83" y1="186.88" x2="30.8" y2="179.39"/><line class="cls-1" x1="1.25" y1="125" x2="16.23" y2="125"/><line class="cls-1" x1="17.83" y1="63.13" x2="30.8" y2="70.61"/><line class="cls-1" x1="63.12" y1="17.83" x2="70.61" y2="30.8"/><line class="cls-2" x1="138.01" y1="1.25" x2="136.96" y2="11.23"/><line class="cls-2" x1="150.87" y1="3.29" x2="148.78" y2="13.11"/><line class="cls-2" x1="163.45" y1="6.66" x2="160.35" y2="16.21"/><line class="cls-2" x1="175.61" y1="11.33" x2="171.53" y2="20.5"/><line class="cls-2" x1="198.14" y1="24.33" x2="192.24" y2="32.45"/><line class="cls-2" x1="208.26" y1="32.53" x2="201.54" y2="39.99"/><line class="cls-2" x1="217.47" y1="41.74" x2="210.01" y2="48.46"/><line class="cls-2" x1="225.67" y1="51.86" x2="217.55" y2="57.76"/><line class="cls-2" x1="238.67" y1="74.39" x2="229.5" y2="78.47"/><line class="cls-2" x1="243.34" y1="86.55" x2="233.79" y2="89.65"/><line class="cls-2" x1="246.71" y1="99.13" x2="236.89" y2="101.22"/><line class="cls-2" x1="248.75" y1="111.99" x2="238.77" y2="113.04"/><line class="cls-2" x1="248.75" y1="138.01" x2="238.77" y2="136.96"/><line class="cls-2" x1="246.71" y1="150.87" x2="236.89" y2="148.78"/><line class="cls-2" x1="243.34" y1="163.45" x2="233.79" y2="160.35"/><line class="cls-2" x1="238.67" y1="175.61" x2="229.5" y2="171.53"/><line class="cls-2" x1="225.67" y1="198.14" x2="217.55" y2="192.24"/><line class="cls-2" x1="217.47" y1="208.26" x2="210.01" y2="201.54"/><line class="cls-2" x1="208.26" y1="217.47" x2="201.54" y2="210.01"/><line class="cls-2" x1="198.14" y1="225.67" x2="192.24" y2="217.55"/><line class="cls-2" x1="175.61" y1="238.67" x2="171.53" y2="229.5"/><line class="cls-2" x1="163.45" y1="243.34" x2="160.35" y2="233.79"/><line class="cls-2" x1="150.87" y1="246.71" x2="148.78" y2="236.89"/><line class="cls-2" x1="138.01" y1="248.75" x2="136.96" y2="238.77"/><line class="cls-2" x1="111.99" y1="248.75" x2="113.04" y2="238.77"/><line class="cls-2" x1="99.13" y1="246.71" x2="101.22" y2="236.89"/><line class="cls-2" x1="86.55" y1="243.34" x2="89.65" y2="233.79"/><line class="cls-2" x1="74.39" y1="238.67" x2="78.47" y2="229.5"/><line class="cls-2" x1="51.86" y1="225.67" x2="57.76" y2="217.55"/><line class="cls-2" x1="41.74" y1="217.47" x2="48.46" y2="210.01"/><line class="cls-2" x1="32.53" y1="208.26" x2="39.99" y2="201.54"/><line class="cls-2" x1="24.33" y1="198.14" x2="32.45" y2="192.24"/><line class="cls-2" x1="11.33" y1="175.61" x2="20.5" y2="171.53"/><line class="cls-2" x1="6.66" y1="163.45" x2="16.21" y2="160.35"/><line class="cls-2" x1="3.29" y1="150.87" x2="13.11" y2="148.78"/><line class="cls-2" x1="1.25" y1="138.01" x2="11.23" y2="136.96"/><line class="cls-2" x1="1.25" y1="111.99" x2="11.23" y2="113.04"/><line class="cls-2" x1="3.29" y1="99.13" x2="13.11" y2="101.22"/><line class="cls-2" x1="6.66" y1="86.55" x2="16.21" y2="89.65"/><line class="cls-2" x1="11.33" y1="74.39" x2="20.5" y2="78.47"/><line class="cls-2" x1="24.33" y1="51.86" x2="32.45" y2="57.76"/><line class="cls-2" x1="32.53" y1="41.74" x2="39.99" y2="48.46"/><line class="cls-2" x1="41.74" y1="32.53" x2="48.46" y2="39.99"/><line class="cls-2" x1="51.86" y1="24.33" x2="57.76" y2="32.45"/><line class="cls-2" x1="74.39" y1="11.33" x2="78.47" y2="20.5"/><line class="cls-2" x1="86.55" y1="6.66" x2="89.65" y2="16.21"/><line class="cls-2" x1="99.13" y1="3.29" x2="101.22" y2="13.11"/><line class="cls-2" x1="111.99" y1="1.25" x2="113.04" y2="11.23"/></svg>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -1 +1 @@
|
||||
<svg id="Hour_Markers_-_Doubles" data-name="Hour Markers - Doubles" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;stroke-width:2.98px;}</style></defs><title>face-002</title><line class="cls-1" x1="122.01" y1="1.75" x2="122.01" y2="16.67"/><line class="cls-1" x1="186.62" y1="18.26" x2="179.17" y2="31.18"/><line class="cls-1" x1="231.74" y1="63.37" x2="218.82" y2="70.83"/><line class="cls-1" x1="248.25" y1="127.99" x2="233.33" y2="127.99"/><line class="cls-1" x1="231.74" y1="186.62" x2="218.82" y2="179.17"/><line class="cls-1" x1="186.63" y1="231.74" x2="179.17" y2="218.82"/><line class="cls-1" x1="127.99" y1="248.25" x2="127.99" y2="233.33"/><line class="cls-1" x1="63.38" y1="231.74" x2="70.83" y2="218.82"/><line class="cls-1" x1="18.26" y1="186.63" x2="31.18" y2="179.17"/><line class="cls-1" x1="1.75" y1="122.01" x2="16.67" y2="122.01"/><line class="cls-1" x1="18.26" y1="63.38" x2="31.18" y2="70.83"/><line class="cls-1" x1="63.37" y1="18.26" x2="70.83" y2="31.18"/><line class="cls-1" x1="127.99" y1="1.75" x2="127.99" y2="16.67"/><line class="cls-1" x1="248.25" y1="122.01" x2="233.33" y2="122.01"/><line class="cls-1" x1="122.01" y1="248.25" x2="122.01" y2="233.33"/><line class="cls-1" x1="1.75" y1="127.99" x2="16.67" y2="127.99"/></svg>
|
||||
<svg id="Hour_Markers_-_Doubles" data-name="Hour Markers - Doubles" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 250"><defs><style>.cls-1{fill:none;stroke:#fff;stroke-linecap:round;stroke-miterlimit:10;stroke-width:2.98px;}</style></defs><title>face-002</title><line class="cls-1" x1="122.01" y1="1.75" x2="122.01" y2="16.67"/><line class="cls-1" x1="186.62" y1="18.26" x2="179.17" y2="31.18"/><line class="cls-1" x1="231.74" y1="63.37" x2="218.82" y2="70.83"/><line class="cls-1" x1="248.25" y1="127.99" x2="233.33" y2="127.99"/><line class="cls-1" x1="231.74" y1="186.62" x2="218.82" y2="179.17"/><line class="cls-1" x1="186.63" y1="231.74" x2="179.17" y2="218.82"/><line class="cls-1" x1="127.99" y1="248.25" x2="127.99" y2="233.33"/><line class="cls-1" x1="63.38" y1="231.74" x2="70.83" y2="218.82"/><line class="cls-1" x1="18.26" y1="186.63" x2="31.18" y2="179.17"/><line class="cls-1" x1="1.75" y1="122.01" x2="16.67" y2="122.01"/><line class="cls-1" x1="18.26" y1="63.38" x2="31.18" y2="70.83"/><line class="cls-1" x1="63.37" y1="18.26" x2="70.83" y2="31.18"/><line class="cls-1" x1="127.99" y1="1.75" x2="127.99" y2="16.67"/><line class="cls-1" x1="248.25" y1="122.01" x2="233.33" y2="122.01"/><line class="cls-1" x1="122.01" y1="248.25" x2="122.01" y2="233.33"/><line class="cls-1" x1="1.75" y1="127.99" x2="16.67" y2="127.99"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
@@ -182,34 +182,14 @@ Module.register("compliments", {
|
||||
},
|
||||
|
||||
// From data currentweather set weather type
|
||||
setCurrentWeatherType: function (data) {
|
||||
var weatherIconTable = {
|
||||
"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"
|
||||
};
|
||||
this.currentWeatherType = weatherIconTable[data.weather[0].icon];
|
||||
setCurrentWeatherType: function (type) {
|
||||
this.currentWeatherType = type;
|
||||
},
|
||||
|
||||
// Override notification handler.
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
if (notification === "CURRENTWEATHER_DATA") {
|
||||
this.setCurrentWeatherType(payload.data);
|
||||
if (notification === "CURRENTWEATHER_TYPE") {
|
||||
this.setCurrentWeatherType(payload.type);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -1,5 +1,7 @@
|
||||
# Module: Current Weather
|
||||
|
||||
> :warning: **This module is deprecated in favor of the [weather](https://docs.magicmirror.builders/modules/weather.html) module.**
|
||||
|
||||
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.
|
||||
|
||||
|
@@ -3,6 +3,8 @@
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This module is deprecated. Any additional feature will no longer be merged.
|
||||
*/
|
||||
Module.register("currentweather", {
|
||||
// Default module config.
|
||||
@@ -37,6 +39,8 @@ Module.register("currentweather", {
|
||||
weatherEndpoint: "weather",
|
||||
|
||||
appendLocationNameToHeader: true,
|
||||
useLocationAsHeader: false,
|
||||
|
||||
calendarClass: "calendar",
|
||||
tableClass: "large",
|
||||
|
||||
@@ -45,24 +49,24 @@ Module.register("currentweather", {
|
||||
roundTemp: false,
|
||||
|
||||
iconTable: {
|
||||
"01d": "wi-day-sunny",
|
||||
"02d": "wi-day-cloudy",
|
||||
"03d": "wi-cloudy",
|
||||
"04d": "wi-cloudy-windy",
|
||||
"09d": "wi-showers",
|
||||
"10d": "wi-rain",
|
||||
"11d": "wi-thunderstorm",
|
||||
"13d": "wi-snow",
|
||||
"50d": "wi-fog",
|
||||
"01n": "wi-night-clear",
|
||||
"02n": "wi-night-cloudy",
|
||||
"03n": "wi-night-cloudy",
|
||||
"04n": "wi-night-cloudy",
|
||||
"09n": "wi-night-showers",
|
||||
"10n": "wi-night-rain",
|
||||
"11n": "wi-night-thunderstorm",
|
||||
"13n": "wi-night-snow",
|
||||
"50n": "wi-night-alt-cloudy-windy"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -217,7 +221,7 @@ Module.register("currentweather", {
|
||||
|
||||
if (this.config.hideTemp === false) {
|
||||
var weatherIcon = document.createElement("span");
|
||||
weatherIcon.className = "wi weathericon " + this.weatherType;
|
||||
weatherIcon.className = "wi weathericon wi-" + this.weatherType;
|
||||
large.appendChild(weatherIcon);
|
||||
|
||||
var temperature = document.createElement("span");
|
||||
@@ -256,7 +260,9 @@ Module.register("currentweather", {
|
||||
|
||||
var feelsLike = document.createElement("span");
|
||||
feelsLike.className = "dimmed";
|
||||
feelsLike.innerHTML = this.translate("FEELS") + " " + this.feelsLike + degreeLabel;
|
||||
feelsLike.innerHTML = this.translate("FEELS", {
|
||||
DEGREE: this.feelsLike + degreeLabel
|
||||
});
|
||||
small.appendChild(feelsLike);
|
||||
|
||||
wrapper.appendChild(small);
|
||||
@@ -267,15 +273,16 @@ Module.register("currentweather", {
|
||||
|
||||
// Override getHeader method.
|
||||
getHeader: function () {
|
||||
if (this.config.appendLocationNameToHeader && this.data.header !== undefined) {
|
||||
return this.data.header + " " + this.fetchedLocationName;
|
||||
}
|
||||
|
||||
if (this.config.useLocationAsHeader && this.config.location !== false) {
|
||||
return this.config.location;
|
||||
}
|
||||
|
||||
return this.data.header;
|
||||
if (this.config.appendLocationNameToHeader) {
|
||||
if (this.data.header) return this.data.header + " " + this.fetchedLocationName;
|
||||
else return this.fetchedLocationName;
|
||||
}
|
||||
|
||||
return this.data.header ? this.data.header : "";
|
||||
},
|
||||
|
||||
// Override notification handler.
|
||||
@@ -497,6 +504,7 @@ Module.register("currentweather", {
|
||||
this.loaded = true;
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
this.sendNotification("CURRENTWEATHER_DATA", { data: data });
|
||||
this.sendNotification("CURRENTWEATHER_TYPE", { type: this.config.iconTable[data.weather[0].icon].replace("-", "_") });
|
||||
},
|
||||
|
||||
/* scheduleUpdate()
|
||||
@@ -584,6 +592,7 @@ Module.register("currentweather", {
|
||||
*/
|
||||
roundValue: function (temperature) {
|
||||
var decimals = this.config.roundTemp ? 0 : 1;
|
||||
return parseFloat(temperature).toFixed(decimals);
|
||||
var roundValue = parseFloat(temperature).toFixed(decimals);
|
||||
return roundValue === "-0" ? 0 : roundValue;
|
||||
}
|
||||
});
|
||||
|
9
modules/default/currentweather/node_helper.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const NodeHelper = require("node_helper");
|
||||
const Log = require("logger");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
start: function () {
|
||||
Log.warn(`The module '${this.name}' is deprecated in favor of the 'weather'-module, please refer to the documentation for a migration path`);
|
||||
}
|
||||
});
|
3
modules/default/newsfeed/fullarticle.njk
Normal file
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
<iframe class="newsfeed-fullarticle" src="{{ url }}"></iframe>
|
||||
</div>
|
14
modules/default/newsfeed/newsfeed.css
Normal file
@@ -0,0 +1,14 @@
|
||||
iframe.newsfeed-fullarticle {
|
||||
width: 100vw;
|
||||
/* very large height value to allow scrolling */
|
||||
height: 3000px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.region.bottom.bar.newsfeed-fullarticle {
|
||||
bottom: inherit;
|
||||
top: -90px;
|
||||
}
|
@@ -44,6 +44,11 @@ Module.register("newsfeed", {
|
||||
return ["moment.js"];
|
||||
},
|
||||
|
||||
//Define required styles.
|
||||
getStyles: function () {
|
||||
return ["newsfeed.css"];
|
||||
},
|
||||
|
||||
// Define required translations.
|
||||
getTranslations: function () {
|
||||
// The translations for the default modules are defined in the core translation files.
|
||||
@@ -61,6 +66,7 @@ Module.register("newsfeed", {
|
||||
|
||||
this.newsItems = [];
|
||||
this.loaded = false;
|
||||
this.error = null;
|
||||
this.activeItem = 0;
|
||||
this.scrollPosition = 0;
|
||||
|
||||
@@ -75,142 +81,73 @@ Module.register("newsfeed", {
|
||||
this.generateFeed(payload);
|
||||
|
||||
if (!this.loaded) {
|
||||
if (this.config.hideLoading) {
|
||||
this.show();
|
||||
}
|
||||
this.scheduleUpdateInterval();
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
this.error = null;
|
||||
} else if (notification === "INCORRECT_URL") {
|
||||
this.error = `Incorrect url: ${payload.url}`;
|
||||
this.scheduleUpdateInterval();
|
||||
}
|
||||
},
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
var wrapper = document.createElement("div");
|
||||
|
||||
//Override fetching of template name
|
||||
getTemplate: function () {
|
||||
if (this.config.feedUrl) {
|
||||
wrapper.className = "small bright";
|
||||
wrapper.innerHTML = this.translate("configuration_changed");
|
||||
return wrapper;
|
||||
return "oldconfig.njk";
|
||||
} else if (this.config.showFullArticle) {
|
||||
return "fullarticle.njk";
|
||||
}
|
||||
return "newsfeed.njk";
|
||||
},
|
||||
|
||||
//Override template data and return whats used for the current template
|
||||
getTemplateData: function () {
|
||||
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications
|
||||
if (this.config.showFullArticle) {
|
||||
return {
|
||||
url: this.getActiveItemURL()
|
||||
};
|
||||
}
|
||||
if (this.error) {
|
||||
return {
|
||||
error: this.error
|
||||
};
|
||||
}
|
||||
if (this.newsItems.length === 0) {
|
||||
return {
|
||||
loaded: false
|
||||
};
|
||||
}
|
||||
if (this.activeItem >= this.newsItems.length) {
|
||||
this.activeItem = 0;
|
||||
}
|
||||
|
||||
if (this.newsItems.length > 0) {
|
||||
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications
|
||||
if (!this.config.showFullArticle && (this.config.showSourceTitle || this.config.showPublishDate)) {
|
||||
var sourceAndTimestamp = document.createElement("div");
|
||||
sourceAndTimestamp.className = "newsfeed-source light small dimmed";
|
||||
const item = this.newsItems[this.activeItem];
|
||||
|
||||
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") {
|
||||
sourceAndTimestamp.innerHTML = this.newsItems[this.activeItem].sourceTitle;
|
||||
}
|
||||
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "" && this.config.showPublishDate) {
|
||||
sourceAndTimestamp.innerHTML += ", ";
|
||||
}
|
||||
if (this.config.showPublishDate) {
|
||||
sourceAndTimestamp.innerHTML += moment(new Date(this.newsItems[this.activeItem].pubdate)).fromNow();
|
||||
}
|
||||
if ((this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") || this.config.showPublishDate) {
|
||||
sourceAndTimestamp.innerHTML += ":";
|
||||
}
|
||||
|
||||
wrapper.appendChild(sourceAndTimestamp);
|
||||
}
|
||||
|
||||
//Remove selected tags from the beginning of rss feed items (title or description)
|
||||
|
||||
if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") {
|
||||
for (let f = 0; f < this.config.startTags.length; f++) {
|
||||
if (this.newsItems[this.activeItem].title.slice(0, this.config.startTags[f].length) === this.config.startTags[f]) {
|
||||
this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].title.slice(this.config.startTags[f].length, this.newsItems[this.activeItem].title.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.removeStartTags === "description" || this.config.removeStartTags === "both") {
|
||||
if (this.isShowingDescription) {
|
||||
for (let 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].description = this.newsItems[this.activeItem].description.slice(this.config.startTags[f].length, this.newsItems[this.activeItem].description.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Remove selected tags from the end of rss feed items (title or description)
|
||||
|
||||
if (this.config.removeEndTags) {
|
||||
for (let f = 0; f < this.config.endTags.length; f++) {
|
||||
if (this.newsItems[this.activeItem].title.slice(-this.config.endTags[f].length) === this.config.endTags[f]) {
|
||||
this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].title.slice(0, -this.config.endTags[f].length);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isShowingDescription) {
|
||||
for (let f = 0; f < this.config.endTags.length; f++) {
|
||||
if (this.newsItems[this.activeItem].description.slice(-this.config.endTags[f].length) === this.config.endTags[f]) {
|
||||
this.newsItems[this.activeItem].description = this.newsItems[this.activeItem].description.slice(0, -this.config.endTags[f].length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.config.showFullArticle) {
|
||||
var title = document.createElement("div");
|
||||
title.className = "newsfeed-title bright medium light" + (!this.config.wrapTitle ? " no-wrap" : "");
|
||||
title.innerHTML = this.newsItems[this.activeItem].title;
|
||||
wrapper.appendChild(title);
|
||||
}
|
||||
|
||||
if (this.isShowingDescription) {
|
||||
var description = document.createElement("div");
|
||||
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);
|
||||
}
|
||||
|
||||
if (this.config.showFullArticle) {
|
||||
var fullArticle = document.createElement("iframe");
|
||||
fullArticle.className = "";
|
||||
fullArticle.style.width = "100vw";
|
||||
// very large height value to allow scrolling
|
||||
fullArticle.height = "3000";
|
||||
fullArticle.style.height = "3000";
|
||||
fullArticle.style.top = "0";
|
||||
fullArticle.style.left = "0";
|
||||
fullArticle.style.border = "none";
|
||||
fullArticle.src = this.getActiveItemURL();
|
||||
fullArticle.style.zIndex = 1;
|
||||
wrapper.appendChild(fullArticle);
|
||||
}
|
||||
|
||||
if (this.config.hideLoading) {
|
||||
this.show();
|
||||
}
|
||||
} else {
|
||||
if (this.config.hideLoading) {
|
||||
this.hide();
|
||||
} else {
|
||||
wrapper.innerHTML = this.translate("LOADING");
|
||||
wrapper.className = "small dimmed";
|
||||
}
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
return {
|
||||
loaded: true,
|
||||
config: this.config,
|
||||
sourceTitle: item.sourceTitle,
|
||||
publishDate: moment(new Date(item.pubdate)).fromNow(),
|
||||
title: item.title,
|
||||
description: item.description
|
||||
};
|
||||
},
|
||||
|
||||
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.
|
||||
/**
|
||||
* Registers the feeds to be used by the backend.
|
||||
*/
|
||||
registerFeeds: function () {
|
||||
for (var f in this.config.feeds) {
|
||||
var feed = this.config.feeds[f];
|
||||
for (let feed of this.config.feeds) {
|
||||
this.sendSocketNotification("ADD_FEED", {
|
||||
feed: feed,
|
||||
config: this.config
|
||||
@@ -218,18 +155,17 @@ Module.register("newsfeed", {
|
||||
}
|
||||
},
|
||||
|
||||
/* generateFeed()
|
||||
/**
|
||||
* Generate an ordered list of items for this configured module.
|
||||
*
|
||||
* attribute feeds object - An object with feeds returned by the node helper.
|
||||
* @param {object} feeds An object with feeds returned by the node helper.
|
||||
*/
|
||||
generateFeed: function (feeds) {
|
||||
var newsItems = [];
|
||||
for (var feed in feeds) {
|
||||
var feedItems = feeds[feed];
|
||||
let newsItems = [];
|
||||
for (let feed in feeds) {
|
||||
const feedItems = feeds[feed];
|
||||
if (this.subscribedToFeed(feed)) {
|
||||
for (var i in feedItems) {
|
||||
var item = feedItems[i];
|
||||
for (let item of feedItems) {
|
||||
item.sourceTitle = this.titleForFeed(feed);
|
||||
if (!(this.config.ignoreOldItems && Date.now() - new Date(item.pubdate) > this.config.ignoreOlderThan)) {
|
||||
newsItems.push(item);
|
||||
@@ -238,8 +174,8 @@ Module.register("newsfeed", {
|
||||
}
|
||||
}
|
||||
newsItems.sort(function (a, b) {
|
||||
var dateA = new Date(a.pubdate);
|
||||
var dateB = new Date(b.pubdate);
|
||||
const dateA = new Date(a.pubdate);
|
||||
const dateB = new Date(b.pubdate);
|
||||
return dateB - dateA;
|
||||
});
|
||||
if (this.config.maxNewsItems > 0) {
|
||||
@@ -248,8 +184,8 @@ Module.register("newsfeed", {
|
||||
|
||||
if (this.config.prohibitedWords.length > 0) {
|
||||
newsItems = newsItems.filter(function (value) {
|
||||
for (var i = 0; i < this.config.prohibitedWords.length; i++) {
|
||||
if (value["title"].toLowerCase().indexOf(this.config.prohibitedWords[i].toLowerCase()) > -1) {
|
||||
for (let word of this.config.prohibitedWords) {
|
||||
if (value["title"].toLowerCase().indexOf(word.toLowerCase()) > -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -257,8 +193,47 @@ Module.register("newsfeed", {
|
||||
}, this);
|
||||
}
|
||||
|
||||
newsItems.forEach((item) => {
|
||||
//Remove selected tags from the beginning of rss feed items (title or description)
|
||||
if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") {
|
||||
for (let startTag of this.config.startTags) {
|
||||
if (item.title.slice(0, startTag.length) === startTag) {
|
||||
item.title = item.title.slice(startTag.length, item.title.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.removeStartTags === "description" || this.config.removeStartTags === "both") {
|
||||
if (this.isShowingDescription) {
|
||||
for (let startTag of this.config.startTags) {
|
||||
if (item.description.slice(0, startTag.length) === startTag) {
|
||||
item.description = item.description.slice(startTag.length, item.description.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Remove selected tags from the end of rss feed items (title or description)
|
||||
|
||||
if (this.config.removeEndTags) {
|
||||
for (let endTag of this.config.endTags) {
|
||||
if (item.title.slice(-endTag.length) === endTag) {
|
||||
item.title = item.title.slice(0, -endTag.length);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isShowingDescription) {
|
||||
for (let endTag of this.config.endTags) {
|
||||
if (item.description.slice(-endTag.length) === endTag) {
|
||||
item.description = item.description.slice(0, -endTag.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// get updated news items and broadcast them
|
||||
var updatedItems = [];
|
||||
const updatedItems = [];
|
||||
newsItems.forEach((value) => {
|
||||
if (this.newsItems.findIndex((value1) => value1 === value) === -1) {
|
||||
// Add item to updated items list
|
||||
@@ -274,16 +249,14 @@ Module.register("newsfeed", {
|
||||
this.newsItems = newsItems;
|
||||
},
|
||||
|
||||
/* subscribedToFeed(feedUrl)
|
||||
/**
|
||||
* Check if this module is configured to show this feed.
|
||||
*
|
||||
* attribute feedUrl string - Url of the feed to check.
|
||||
*
|
||||
* returns bool
|
||||
* @param {string} feedUrl Url of the feed to check.
|
||||
* @returns {boolean} True if it is subscribed, false otherwise
|
||||
*/
|
||||
subscribedToFeed: function (feedUrl) {
|
||||
for (var f in this.config.feeds) {
|
||||
var feed = this.config.feeds[f];
|
||||
for (let feed of this.config.feeds) {
|
||||
if (feed.url === feedUrl) {
|
||||
return true;
|
||||
}
|
||||
@@ -291,16 +264,14 @@ Module.register("newsfeed", {
|
||||
return false;
|
||||
},
|
||||
|
||||
/* titleForFeed(feedUrl)
|
||||
* Returns title for a specific feed Url.
|
||||
/**
|
||||
* Returns title for the specific feed url.
|
||||
*
|
||||
* attribute feedUrl string - Url of the feed to check.
|
||||
*
|
||||
* returns string
|
||||
* @param {string} feedUrl Url of the feed
|
||||
* @returns {string} The title of the feed
|
||||
*/
|
||||
titleForFeed: function (feedUrl) {
|
||||
for (var f in this.config.feeds) {
|
||||
var feed = this.config.feeds[f];
|
||||
for (let feed of this.config.feeds) {
|
||||
if (feed.url === feedUrl) {
|
||||
return feed.title || "";
|
||||
}
|
||||
@@ -308,62 +279,50 @@ Module.register("newsfeed", {
|
||||
return "";
|
||||
},
|
||||
|
||||
/* scheduleUpdateInterval()
|
||||
/**
|
||||
* Schedule visual update.
|
||||
*/
|
||||
scheduleUpdateInterval: function () {
|
||||
var self = this;
|
||||
|
||||
self.updateDom(self.config.animationSpeed);
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
|
||||
// Broadcast NewsFeed if needed
|
||||
if (self.config.broadcastNewsFeeds) {
|
||||
self.sendNotification("NEWS_FEED", { items: self.newsItems });
|
||||
if (this.config.broadcastNewsFeeds) {
|
||||
this.sendNotification("NEWS_FEED", { items: this.newsItems });
|
||||
}
|
||||
|
||||
this.timer = setInterval(function () {
|
||||
self.activeItem++;
|
||||
self.updateDom(self.config.animationSpeed);
|
||||
this.timer = setInterval(() => {
|
||||
this.activeItem++;
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
|
||||
// Broadcast NewsFeed if needed
|
||||
if (self.config.broadcastNewsFeeds) {
|
||||
self.sendNotification("NEWS_FEED", { items: self.newsItems });
|
||||
if (this.config.broadcastNewsFeeds) {
|
||||
this.sendNotification("NEWS_FEED", { items: this.newsItems });
|
||||
}
|
||||
}, this.config.updateInterval);
|
||||
},
|
||||
|
||||
/* capitalizeFirstLetter(string)
|
||||
* Capitalizes the first character of a string.
|
||||
*
|
||||
* argument string string - Input string.
|
||||
*
|
||||
* return string - Capitalized output string.
|
||||
*/
|
||||
capitalizeFirstLetter: function (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
},
|
||||
|
||||
resetDescrOrFullArticleAndTimer: function () {
|
||||
this.isShowingDescription = this.config.showDescription;
|
||||
this.config.showFullArticle = false;
|
||||
this.scrollPosition = 0;
|
||||
// reset bottom bar alignment
|
||||
document.getElementsByClassName("region bottom bar")[0].style.bottom = "0";
|
||||
document.getElementsByClassName("region bottom bar")[0].style.top = "inherit";
|
||||
document.getElementsByClassName("region bottom bar")[0].classList.remove("newsfeed-fullarticle");
|
||||
if (!this.timer) {
|
||||
this.scheduleUpdateInterval();
|
||||
}
|
||||
},
|
||||
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
var before = this.activeItem;
|
||||
if (notification === "ARTICLE_NEXT") {
|
||||
const before = this.activeItem;
|
||||
if (notification === "MODULE_DOM_CREATED" && this.config.hideLoading) {
|
||||
this.hide();
|
||||
} else if (notification === "ARTICLE_NEXT") {
|
||||
this.activeItem++;
|
||||
if (this.activeItem >= this.newsItems.length) {
|
||||
this.activeItem = 0;
|
||||
}
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.info(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
||||
Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
||||
this.updateDom(100);
|
||||
} else if (notification === "ARTICLE_PREVIOUS") {
|
||||
this.activeItem--;
|
||||
@@ -371,7 +330,7 @@ Module.register("newsfeed", {
|
||||
this.activeItem = this.newsItems.length - 1;
|
||||
}
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.info(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
||||
Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
||||
this.updateDom(100);
|
||||
}
|
||||
// if "more details" is received the first time: show article summary, on second time show full article
|
||||
@@ -380,8 +339,8 @@ Module.register("newsfeed", {
|
||||
if (this.config.showFullArticle === true) {
|
||||
this.scrollPosition += this.config.scrollLength;
|
||||
window.scrollTo(0, this.scrollPosition);
|
||||
Log.info(this.name + " - scrolling down");
|
||||
Log.info(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength);
|
||||
Log.debug(this.name + " - scrolling down");
|
||||
Log.debug(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength);
|
||||
} else {
|
||||
this.showFullArticle();
|
||||
}
|
||||
@@ -389,12 +348,12 @@ Module.register("newsfeed", {
|
||||
if (this.config.showFullArticle === true) {
|
||||
this.scrollPosition -= this.config.scrollLength;
|
||||
window.scrollTo(0, this.scrollPosition);
|
||||
Log.info(this.name + " - scrolling up");
|
||||
Log.info(this.name + " - ARTICLE_SCROLL_UP, scroll position: " + this.config.scrollLength);
|
||||
Log.debug(this.name + " - scrolling up");
|
||||
Log.debug(this.name + " - ARTICLE_SCROLL_UP, scroll position: " + this.config.scrollLength);
|
||||
}
|
||||
} else if (notification === "ARTICLE_LESS_DETAILS") {
|
||||
this.resetDescrOrFullArticleAndTimer();
|
||||
Log.info(this.name + " - showing only article titles again");
|
||||
Log.debug(this.name + " - showing only article titles again");
|
||||
this.updateDom(100);
|
||||
} else if (notification === "ARTICLE_TOGGLE_FULL") {
|
||||
if (this.config.showFullArticle) {
|
||||
@@ -419,12 +378,11 @@ Module.register("newsfeed", {
|
||||
this.config.showFullArticle = !this.isShowingDescription;
|
||||
// make bottom bar align to top to allow scrolling
|
||||
if (this.config.showFullArticle === true) {
|
||||
document.getElementsByClassName("region bottom bar")[0].style.bottom = "inherit";
|
||||
document.getElementsByClassName("region bottom bar")[0].style.top = "-90px";
|
||||
document.getElementsByClassName("region bottom bar")[0].classList.add("newsfeed-fullarticle");
|
||||
}
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
Log.info(this.name + " - showing " + this.isShowingDescription ? "article description" : "full article");
|
||||
Log.debug(this.name + " - showing " + this.isShowingDescription ? "article description" : "full article");
|
||||
this.updateDom(100);
|
||||
}
|
||||
});
|
||||
|
32
modules/default/newsfeed/newsfeed.njk
Normal file
@@ -0,0 +1,32 @@
|
||||
{% if loaded %}
|
||||
<div>
|
||||
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if sourceTitle and config.showSourceTitle %}
|
||||
{{ sourceTitle }}{% if config.showPublishDate %}, {% else %}: {% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}
|
||||
{{ publishDate }}:
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ description | truncate(config.lengthDescription) }}
|
||||
{% else %}
|
||||
{{ description }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% elseif error %}
|
||||
<div class="small dimmed">
|
||||
{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="small dimmed">
|
||||
{{ "LOADING" | translate | safe }}
|
||||
</div>
|
||||
{% endif %}
|
@@ -1,55 +1,54 @@
|
||||
/* Magic Mirror
|
||||
* Fetcher
|
||||
* Node Helper: Newsfeed - NewsfeedFetcher
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
const Log = require("../../../js/logger.js");
|
||||
const Log = require("logger");
|
||||
const FeedMe = require("feedme");
|
||||
const request = require("request");
|
||||
const fetch = require("node-fetch");
|
||||
const iconv = require("iconv-lite");
|
||||
|
||||
/* Fetcher
|
||||
/**
|
||||
* Responsible for requesting an update on the set interval and broadcasting the data.
|
||||
*
|
||||
* attribute url string - URL of the news feed.
|
||||
* attribute reloadInterval number - Reload interval in milliseconds.
|
||||
* attribute logFeedWarnings boolean - Log warnings when there is an error parsing a news article.
|
||||
* @param {string} url URL of the news feed.
|
||||
* @param {number} reloadInterval Reload interval in milliseconds.
|
||||
* @param {string} encoding Encoding of the feed.
|
||||
* @param {boolean} logFeedWarnings If true log warnings when there is an error parsing a news article.
|
||||
* @class
|
||||
*/
|
||||
const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
let reloadTimer = null;
|
||||
let items = [];
|
||||
|
||||
let fetchFailedCallback = function () {};
|
||||
let itemsReceivedCallback = function () {};
|
||||
|
||||
var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
var self = this;
|
||||
if (reloadInterval < 1000) {
|
||||
reloadInterval = 1000;
|
||||
}
|
||||
|
||||
var reloadTimer = null;
|
||||
var items = [];
|
||||
|
||||
var fetchFailedCallback = function () {};
|
||||
var itemsReceivedCallback = function () {};
|
||||
|
||||
/* private methods */
|
||||
|
||||
/* fetchNews()
|
||||
/**
|
||||
* Request the new items.
|
||||
*/
|
||||
var fetchNews = function () {
|
||||
const fetchNews = () => {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = null;
|
||||
items = [];
|
||||
|
||||
var parser = new FeedMe();
|
||||
const parser = new FeedMe();
|
||||
|
||||
parser.on("item", function (item) {
|
||||
var title = item.title;
|
||||
var description = item.description || item.summary || item.content || "";
|
||||
var pubdate = item.pubdate || item.published || item.updated || item["dc:date"];
|
||||
var url = item.url || item.link || "";
|
||||
parser.on("item", (item) => {
|
||||
const title = item.title;
|
||||
let description = item.description || item.summary || item.content || "";
|
||||
const pubdate = item.pubdate || item.published || item.updated || item["dc:date"];
|
||||
const url = item.url || item.link || "";
|
||||
|
||||
if (title && pubdate) {
|
||||
var regex = /(<([^>]+)>)/gi;
|
||||
const regex = /(<([^>]+)>)/gi;
|
||||
description = description.toString().replace(regex, "");
|
||||
|
||||
items.push({
|
||||
@@ -67,32 +66,37 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
}
|
||||
});
|
||||
|
||||
parser.on("end", function () {
|
||||
self.broadcastItems();
|
||||
parser.on("end", () => {
|
||||
this.broadcastItems();
|
||||
scheduleTimer();
|
||||
});
|
||||
|
||||
parser.on("error", function (error) {
|
||||
fetchFailedCallback(self, error);
|
||||
parser.on("error", (error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
});
|
||||
|
||||
var nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
var headers = { "User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)", "Cache-Control": "max-age=0, no-cache, no-store, must-revalidate", Pragma: "no-cache" };
|
||||
|
||||
request({ uri: url, encoding: null, headers: headers })
|
||||
.on("error", function (error) {
|
||||
fetchFailedCallback(self, error);
|
||||
scheduleTimer();
|
||||
})
|
||||
.pipe(iconv.decodeStream(encoding))
|
||||
.pipe(parser);
|
||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
const 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"
|
||||
};
|
||||
|
||||
/* scheduleTimer()
|
||||
fetch(url, { headers: headers })
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
})
|
||||
.then((res) => {
|
||||
res.body.pipe(iconv.decodeStream(encoding)).pipe(parser);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Schedule the timer for the next update.
|
||||
*/
|
||||
var scheduleTimer = function () {
|
||||
const scheduleTimer = function () {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = setTimeout(function () {
|
||||
fetchNews();
|
||||
@@ -101,10 +105,10 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
|
||||
/* public methods */
|
||||
|
||||
/* setReloadInterval()
|
||||
/**
|
||||
* Update the reload interval, but only if we need to increase the speed.
|
||||
*
|
||||
* attribute interval number - Interval for the update in milliseconds.
|
||||
* @param {number} interval Interval for the update in milliseconds.
|
||||
*/
|
||||
this.setReloadInterval = function (interval) {
|
||||
if (interval > 1000 && interval < reloadInterval) {
|
||||
@@ -112,14 +116,14 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
}
|
||||
};
|
||||
|
||||
/* startFetch()
|
||||
/**
|
||||
* Initiate fetchNews();
|
||||
*/
|
||||
this.startFetch = function () {
|
||||
fetchNews();
|
||||
};
|
||||
|
||||
/* broadcastItems()
|
||||
/**
|
||||
* Broadcast the existing items.
|
||||
*/
|
||||
this.broadcastItems = function () {
|
||||
@@ -128,7 +132,7 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
return;
|
||||
}
|
||||
Log.info("Newsfeed-Fetcher: Broadcasting " + items.length + " items.");
|
||||
itemsReceivedCallback(self);
|
||||
itemsReceivedCallback(this);
|
||||
};
|
||||
|
||||
this.onReceive = function (callback) {
|
||||
@@ -148,4 +152,4 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Fetcher;
|
||||
module.exports = NewsfeedFetcher;
|
@@ -6,9 +6,8 @@
|
||||
*/
|
||||
|
||||
const NodeHelper = require("node_helper");
|
||||
const validUrl = require("valid-url");
|
||||
const Fetcher = require("./fetcher.js");
|
||||
const Log = require("../../../js/logger");
|
||||
const NewsfeedFetcher = require("./newsfeedfetcher.js");
|
||||
const Log = require("logger");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
@@ -24,45 +23,45 @@ module.exports = NodeHelper.create({
|
||||
}
|
||||
},
|
||||
|
||||
/* createFetcher(feed, config)
|
||||
/**
|
||||
* Creates a fetcher for a new feed if it doesn't exist yet.
|
||||
* Otherwise it reuses the existing one.
|
||||
*
|
||||
* attribute feed object - A feed object.
|
||||
* attribute config object - A configuration object containing reload interval in milliseconds.
|
||||
* @param {object} feed The feed object.
|
||||
* @param {object} config The configuration object.
|
||||
*/
|
||||
createFetcher: function (feed, config) {
|
||||
var self = this;
|
||||
const url = feed.url || "";
|
||||
const encoding = feed.encoding || "UTF-8";
|
||||
const reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
|
||||
|
||||
var url = feed.url || "";
|
||||
var encoding = feed.encoding || "UTF-8";
|
||||
var reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
|
||||
|
||||
if (!validUrl.isUri(url)) {
|
||||
self.sendSocketNotification("INCORRECT_URL", url);
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (error) {
|
||||
this.sendSocketNotification("INCORRECT_URL", { url: url });
|
||||
return;
|
||||
}
|
||||
|
||||
var fetcher;
|
||||
if (typeof self.fetchers[url] === "undefined") {
|
||||
let fetcher;
|
||||
if (typeof this.fetchers[url] === "undefined") {
|
||||
Log.log("Create new news fetcher for url: " + url + " - Interval: " + reloadInterval);
|
||||
fetcher = new Fetcher(url, reloadInterval, encoding, config.logFeedWarnings);
|
||||
fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings);
|
||||
|
||||
fetcher.onReceive(function (fetcher) {
|
||||
self.broadcastFeeds();
|
||||
fetcher.onReceive(() => {
|
||||
this.broadcastFeeds();
|
||||
});
|
||||
|
||||
fetcher.onError(function (fetcher, error) {
|
||||
self.sendSocketNotification("FETCH_ERROR", {
|
||||
fetcher.onError((fetcher, error) => {
|
||||
this.sendSocketNotification("FETCH_ERROR", {
|
||||
url: fetcher.url(),
|
||||
error: error
|
||||
});
|
||||
});
|
||||
|
||||
self.fetchers[url] = fetcher;
|
||||
this.fetchers[url] = fetcher;
|
||||
} else {
|
||||
Log.log("Use existing news fetcher for url: " + url);
|
||||
fetcher = self.fetchers[url];
|
||||
fetcher = this.fetchers[url];
|
||||
fetcher.setReloadInterval(reloadInterval);
|
||||
fetcher.broadcastItems();
|
||||
}
|
||||
@@ -70,13 +69,13 @@ module.exports = NodeHelper.create({
|
||||
fetcher.startFetch();
|
||||
},
|
||||
|
||||
/* broadcastFeeds()
|
||||
/**
|
||||
* Creates an object with all feed items of the different registered feeds,
|
||||
* and broadcasts these using sendSocketNotification.
|
||||
*/
|
||||
broadcastFeeds: function () {
|
||||
var feeds = {};
|
||||
for (var f in this.fetchers) {
|
||||
const feeds = {};
|
||||
for (let f in this.fetchers) {
|
||||
feeds[f] = this.fetchers[f].items();
|
||||
}
|
||||
this.sendSocketNotification("NEWS_ITEMS", feeds);
|
||||
|
3
modules/default/newsfeed/oldconfig.njk
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="small bright">
|
||||
{{ "MODULE_CONFIG_CHANGED" | translate({MODULE_NAME: "Newsfeed"}) | safe }}
|
||||
</div>
|
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"configuration_changed": "Die Konfigurationsoptionen für das Newsfeed-Modul haben sich geändert. \nBitte überprüfen Sie die Dokumentation."
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"configuration_changed": "The configuration options for the newsfeed module have changed.\nPlease check the documentation."
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"configuration_changed": "Las opciones de configuración para el módulo de suministro de noticias han cambiado. \nVerifique la documentación."
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"configuration_changed": "Les options de configuration du module newsfeed ont changé. \nVeuillez consulter la documentation."
|
||||
}
|
@@ -3,7 +3,7 @@ const simpleGits = [];
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const defaultModules = require(__dirname + "/../defaultmodules.js");
|
||||
const Log = require(__dirname + "/../../../js/logger.js");
|
||||
const Log = require("logger");
|
||||
const NodeHelper = require("node_helper");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
@@ -14,32 +14,32 @@ module.exports = NodeHelper.create({
|
||||
|
||||
start: function () {},
|
||||
|
||||
configureModules: function (modules) {
|
||||
configureModules: async function (modules) {
|
||||
// Push MagicMirror itself , biggest chance it'll show up last in UI and isn't overwritten
|
||||
// others will be added in front
|
||||
// this method returns promises so we can't wait for every one to resolve before continuing
|
||||
simpleGits.push({ module: "default", git: SimpleGit(path.normalize(__dirname + "/../../../")) });
|
||||
simpleGits.push({ module: "default", git: this.createGit(path.normalize(__dirname + "/../../../")) });
|
||||
|
||||
var promises = [];
|
||||
|
||||
for (var moduleName in modules) {
|
||||
for (let moduleName in modules) {
|
||||
if (!this.ignoreUpdateChecking(moduleName)) {
|
||||
// Default modules are included in the main MagicMirror repo
|
||||
var moduleFolder = path.normalize(__dirname + "/../../" + moduleName);
|
||||
let moduleFolder = path.normalize(__dirname + "/../../" + moduleName);
|
||||
|
||||
try {
|
||||
Log.info("Checking git for module: " + moduleName);
|
||||
let stat = fs.statSync(path.join(moduleFolder, ".git"));
|
||||
promises.push(this.resolveRemote(moduleName, moduleFolder));
|
||||
// Throws error if file doesn't exist
|
||||
fs.statSync(path.join(moduleFolder, ".git"));
|
||||
// Fetch the git or throw error if no remotes
|
||||
let git = await this.resolveRemote(moduleFolder);
|
||||
// Folder has .git and has at least one git remote, watch this folder
|
||||
simpleGits.unshift({ module: moduleName, git: git });
|
||||
} catch (err) {
|
||||
// Error when directory .git doesn't exist
|
||||
// Error when directory .git doesn't exist or doesn't have any remotes
|
||||
// This module is not managed with git, skip
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
},
|
||||
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
@@ -54,36 +54,36 @@ module.exports = NodeHelper.create({
|
||||
}
|
||||
},
|
||||
|
||||
resolveRemote: function (moduleName, moduleFolder) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var git = SimpleGit(moduleFolder);
|
||||
git.getRemotes(true, (err, remotes) => {
|
||||
resolveRemote: async function (moduleFolder) {
|
||||
let git = this.createGit(moduleFolder);
|
||||
let remotes = await git.getRemotes(true);
|
||||
|
||||
if (remotes.length < 1 || remotes[0].name.length < 1) {
|
||||
// No valid remote for folder, skip
|
||||
return resolve();
|
||||
throw new Error("No valid remote for folder " + moduleFolder);
|
||||
}
|
||||
// Folder has .git and has at least one git remote, watch this folder
|
||||
simpleGits.unshift({ module: moduleName, git: git });
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return git;
|
||||
},
|
||||
|
||||
performFetch: function () {
|
||||
var self = this;
|
||||
simpleGits.forEach((sg) => {
|
||||
sg.git.fetch().status((err, data) => {
|
||||
data.module = sg.module;
|
||||
if (!err) {
|
||||
sg.git.log({ "-1": null }, (err, data2) => {
|
||||
if (!err && data2.latest && "hash" in data2.latest) {
|
||||
data.hash = data2.latest.hash;
|
||||
self.sendSocketNotification("STATUS", data);
|
||||
}
|
||||
performFetch: async function () {
|
||||
for (let sg of simpleGits) {
|
||||
try {
|
||||
let fetchData = await sg.git.fetch(["--dry-run"]).status();
|
||||
let logData = await sg.git.log({ "-1": null });
|
||||
|
||||
if (logData.latest && "hash" in logData.latest) {
|
||||
this.sendSocketNotification("STATUS", {
|
||||
module: sg.module,
|
||||
behind: fetchData.behind,
|
||||
current: fetchData.current,
|
||||
hash: logData.latest.hash,
|
||||
tracking: fetchData.tracking
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
Log.error("Failed to fetch git data for " + sg.module + ": " + err);
|
||||
}
|
||||
}
|
||||
|
||||
this.scheduleNextFetch(this.config.updateInterval);
|
||||
},
|
||||
@@ -93,13 +93,17 @@ module.exports = NodeHelper.create({
|
||||
delay = 60 * 1000;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
let self = this;
|
||||
clearTimeout(this.updateTimer);
|
||||
this.updateTimer = setTimeout(function () {
|
||||
self.performFetch();
|
||||
}, delay);
|
||||
},
|
||||
|
||||
createGit: function (folder) {
|
||||
return SimpleGit({ baseDir: folder, timeout: { block: this.config.timeout } });
|
||||
},
|
||||
|
||||
ignoreUpdateChecking: function (moduleName) {
|
||||
// Should not check for updates for default modules
|
||||
if (defaultModules.indexOf(moduleName) >= 0) {
|
||||
|
@@ -8,7 +8,8 @@ Module.register("updatenotification", {
|
||||
defaults: {
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
refreshInterval: 24 * 60 * 60 * 1000, // one day
|
||||
ignoreModules: []
|
||||
ignoreModules: [],
|
||||
timeout: 1000
|
||||
},
|
||||
|
||||
suspended: false,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# Weather Module
|
||||
|
||||
This module aims to be the replacement for the current `currentweather` and `weatherforcast` modules. The module will be configurable to be used as a current weather view, or to show the forecast. This way the module can be used twice to fullfil both purposes.
|
||||
This module aims to be the replacement for the current `currentweather` and `weatherforcast` modules. The module will be configurable to be used as a current weather view, or to show the forecast. This way the module can be used twice to fulfill both purposes.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/weather.html).
|
||||
|
@@ -5,9 +5,13 @@
|
||||
<span>
|
||||
{% if config.useBeaufort %}
|
||||
{{ current.beaufortWindSpeed() | round }}
|
||||
{% else %}
|
||||
{% if config.useKmh %}
|
||||
{{ current.kmhWindSpeed() | round }}
|
||||
{% else %}
|
||||
{{ current.windSpeed | round }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showWindDirection %}
|
||||
<sup>
|
||||
{% if config.showWindDirectionAsArrow %}
|
||||
@@ -59,10 +63,10 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if (config.showFeelsLike or config.showPrecipitationAmount) and not config.onlyTemp %}
|
||||
<div class="normal medium">
|
||||
<div class="normal medium feelslike">
|
||||
{% if config.showFeelsLike %}
|
||||
<span class="dimmed">
|
||||
{{ "FEELS" | translate }} {{ current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }}
|
||||
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationAmount %}
|
||||
@@ -74,7 +78,7 @@
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="dimmed light small">
|
||||
{{ "LOADING" | translate | safe }}
|
||||
{{ "LOADING" | translate }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@@ -5,7 +5,13 @@
|
||||
{% 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 %}>
|
||||
{% if (currentStep == 0) %}
|
||||
<td class="day">{{ "TODAY" | translate }}</td>
|
||||
{% elif (currentStep == 1) %}
|
||||
<td class="day">{{ "TOMORROW" | translate }}</td>
|
||||
{% else %}
|
||||
<td class="day">{{ f.date.format('ddd') }}</td>
|
||||
{% endif %}
|
||||
<td class="bright weather-icon"><span class="wi weathericon wi-{{ f.weatherType }}"></span></td>
|
||||
<td class="align-right bright max-temp">
|
||||
{{ f.maxTemperature | roundValue | unit("temperature") }}
|
||||
@@ -24,7 +30,7 @@
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="dimmed light small">
|
||||
{{ "LOADING" | translate | safe }}
|
||||
{{ "LOADING" | translate }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
29
modules/default/weather/hourly.njk
Normal file
@@ -0,0 +1,29 @@
|
||||
{% if hourly %}
|
||||
{% set numSteps = hourly | calcNumEntries %}
|
||||
{% set currentStep = 0 %}
|
||||
<table class="{{ config.tableClass }}">
|
||||
{% set hours = hourly.slice(0, numSteps) %}
|
||||
{% for hour in hours %}
|
||||
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
|
||||
<td class="day">{{ hour.date | formatTime }}</td>
|
||||
<td class="bright weather-icon"><span class="wi weathericon wi-{{ hour.weatherType }}"></span></td>
|
||||
<td class="align-right bright">
|
||||
{{ hour.temperature | roundValue | unit("temperature") }}
|
||||
</td>
|
||||
{% if config.showPrecipitationAmount %}
|
||||
<td class="align-right bright precipitation">
|
||||
{{ hour.precipitation | unit("precip") }}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% set currentStep = currentStep + 1 %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="dimmed light small">
|
||||
{{ "LOADING" | translate }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Uncomment the line below to see the contents of the `hourly` object. -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{hourly | dump}}</div> -->
|
@@ -29,16 +29,23 @@ WeatherProvider.register("yourprovider", {
|
||||
|
||||
#### `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.
|
||||
This method is called when the weather module tries to fetch the current weather of your provider. The implementation of this method is required for current weather support.
|
||||
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 of your provider. The implementation of this method is required.
|
||||
This method is called when the weather module tries to fetch the weather of your provider. The implementation of this method is required for forecast support.
|
||||
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);`.
|
||||
After the response is processed, the weather forecast information (as an array of [WeatherObject](#weatherobject)s) needs to be set with `this.setWeatherForecast(forecast);`.
|
||||
It will then automatically refresh the module DOM with the new data.
|
||||
|
||||
#### `fetchWeatherHourly()`
|
||||
|
||||
This method is called when the weather module tries to fetch the weather of your provider. The implementation of this method is required for hourly support.
|
||||
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 hourly weather forecast information (as an array of [WeatherObject](#weatherobject)s) needs to be set with `this.setWeatherHourly(forecast);`.
|
||||
It will then automatically refresh the module DOM with the new data.
|
||||
|
||||
### Weather Provider instance methods
|
||||
@@ -63,6 +70,10 @@ This returns a WeatherDay object for the current weather.
|
||||
|
||||
This returns an array of WeatherDay objects for the weather forecast.
|
||||
|
||||
#### `weatherHourly()`
|
||||
|
||||
This returns an array of WeatherDay objects for the hourly weather forecast.
|
||||
|
||||
#### `fetchedLocation()`
|
||||
|
||||
This returns the name of the fetched location or an empty string.
|
||||
@@ -75,6 +86,10 @@ Set the currentWeather and notify the delegate that new information is available
|
||||
|
||||
Set the weatherForecastArray and notify the delegate that new information is available.
|
||||
|
||||
#### `setWeatherHourly(weatherHourlyArray)`
|
||||
|
||||
Set the weatherHourlyArray and notify the delegate that new information is available.
|
||||
|
||||
#### `setFetchedLocation(name)`
|
||||
|
||||
Set the fetched location name.
|
||||
|
@@ -15,6 +15,15 @@ WeatherProvider.register("darksky", {
|
||||
// Not strictly required, but helps for debugging.
|
||||
providerName: "Dark Sky",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "https://cors-anywhere.herokuapp.com/https://api.darksky.net",
|
||||
weatherEndpoint: "/forecast",
|
||||
apiKey: "",
|
||||
lat: 0,
|
||||
lon: 0
|
||||
},
|
||||
|
||||
units: {
|
||||
imperial: "us",
|
||||
metric: "si"
|
||||
@@ -62,7 +71,7 @@ WeatherProvider.register("darksky", {
|
||||
|
||||
// Implement WeatherDay generator.
|
||||
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
currentWeather.date = moment();
|
||||
currentWeather.humidity = parseFloat(currentWeatherData.currently.humidity);
|
||||
@@ -80,7 +89,7 @@ WeatherProvider.register("darksky", {
|
||||
const days = [];
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
weather.date = moment(forecast.time, "X");
|
||||
weather.minTemperature = forecast.temperatureMin;
|
||||
|
@@ -14,6 +14,18 @@ WeatherProvider.register("openweathermap", {
|
||||
// But for debugging (and future alerts) it would be nice to have the real name.
|
||||
providerName: "OpenWeatherMap",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiVersion: "2.5",
|
||||
apiBase: "https://api.openweathermap.org/data/",
|
||||
weatherEndpoint: "",
|
||||
locationID: false,
|
||||
location: false,
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
apiKey: ""
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
@@ -35,7 +47,7 @@ WeatherProvider.register("openweathermap", {
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
// Overwrite the fetchWeatherForecast method.
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
@@ -56,6 +68,52 @@ WeatherProvider.register("openweathermap", {
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Overwrite the fetchWeatherHourly method.
|
||||
fetchWeatherHourly() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
this.setFetchedLocation(`(${data.lat},${data.lon})`);
|
||||
|
||||
const weatherData = this.generateWeatherObjectsFromOnecall(data);
|
||||
this.setWeatherHourly(weatherData.hours);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides method for setting config to check if endpoint is correct for hourly
|
||||
*
|
||||
* @param config
|
||||
*/
|
||||
setConfig(config) {
|
||||
this.config = config;
|
||||
if (!this.config.weatherEndpoint) {
|
||||
switch (this.config.type) {
|
||||
case "hourly":
|
||||
this.config.weatherEndpoint = "/onecall";
|
||||
break;
|
||||
case "daily":
|
||||
case "forecast":
|
||||
this.config.weatherEndpoint = "/forecast";
|
||||
break;
|
||||
case "current":
|
||||
this.config.weatherEndpoint = "/weather";
|
||||
break;
|
||||
default:
|
||||
Log.error("weatherEndpoint not configured and could not resolve it based on type");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** OpenWeatherMap Specific Methods - These are not part of the default provider methods */
|
||||
/*
|
||||
* Gets the complete url for the request
|
||||
@@ -68,11 +126,15 @@ WeatherProvider.register("openweathermap", {
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
currentWeather.humidity = currentWeatherData.main.humidity;
|
||||
currentWeather.temperature = currentWeatherData.main.temp;
|
||||
if (this.config.windUnits === "metric") {
|
||||
currentWeather.windSpeed = this.config.useKmh ? currentWeatherData.wind.speed * 3.6 : currentWeatherData.wind.speed;
|
||||
} else {
|
||||
currentWeather.windSpeed = currentWeatherData.wind.speed;
|
||||
}
|
||||
currentWeather.windDirection = currentWeatherData.wind.deg;
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon);
|
||||
currentWeather.sunrise = moment(currentWeatherData.sys.sunrise, "X");
|
||||
@@ -91,10 +153,22 @@ WeatherProvider.register("openweathermap", {
|
||||
return this.fetchForecastDaily(forecasts);
|
||||
}
|
||||
// if weatherEndpoint does not match forecast or forecast/daily, what should be returned?
|
||||
const days = [new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits)];
|
||||
const days = [new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh)];
|
||||
return days;
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate WeatherObjects based on One Call forecast information
|
||||
*/
|
||||
generateWeatherObjectsFromOnecall(data) {
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
return this.fetchOnecall(data);
|
||||
}
|
||||
// if weatherEndpoint does not match onecall, what should be returned?
|
||||
const weatherData = { current: new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh), hours: [], days: [] };
|
||||
return weatherData;
|
||||
},
|
||||
|
||||
/*
|
||||
* fetch forecast information for 3-hourly forecast (available for free subscription).
|
||||
*/
|
||||
@@ -108,7 +182,7 @@ WeatherProvider.register("openweathermap", {
|
||||
let snow = 0;
|
||||
// variable for date
|
||||
let date = "";
|
||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
if (date !== moment(forecast.dt, "X").format("YYYY-MM-DD")) {
|
||||
@@ -121,7 +195,7 @@ WeatherProvider.register("openweathermap", {
|
||||
// push weather information to days array
|
||||
days.push(weather);
|
||||
// create new weather-object
|
||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
minTemp = [];
|
||||
maxTemp = [];
|
||||
@@ -184,7 +258,7 @@ WeatherProvider.register("openweathermap", {
|
||||
const days = [];
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
weather.date = moment(forecast.dt, "X");
|
||||
weather.minTemperature = forecast.temp.min;
|
||||
@@ -221,6 +295,129 @@ WeatherProvider.register("openweathermap", {
|
||||
return days;
|
||||
},
|
||||
|
||||
/*
|
||||
* Fetch One Call forecast information (available for free subscription).
|
||||
* Factors in timezone offsets.
|
||||
* Minutely forecasts are excluded for the moment, see getParams().
|
||||
*/
|
||||
fetchOnecall(data) {
|
||||
let precip = false;
|
||||
|
||||
// get current weather, if requested
|
||||
const current = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
if (data.hasOwnProperty("current")) {
|
||||
current.date = moment(data.current.dt, "X").utcOffset(data.timezone_offset / 60);
|
||||
current.windSpeed = data.current.wind_speed;
|
||||
current.windDirection = data.current.wind_deg;
|
||||
current.sunrise = moment(data.current.sunrise, "X").utcOffset(data.timezone_offset / 60);
|
||||
current.sunset = moment(data.current.sunset, "X").utcOffset(data.timezone_offset / 60);
|
||||
current.temperature = data.current.temp;
|
||||
current.weatherType = this.convertWeatherType(data.current.weather[0].icon);
|
||||
current.humidity = data.current.humidity;
|
||||
if (data.current.hasOwnProperty("rain") && !isNaN(data.current["rain"]["1h"])) {
|
||||
if (this.config.units === "imperial") {
|
||||
current.rain = data.current["rain"]["1h"] / 25.4;
|
||||
} else {
|
||||
current.rain = data.current["rain"]["1h"];
|
||||
}
|
||||
precip = true;
|
||||
}
|
||||
if (data.current.hasOwnProperty("snow") && !isNaN(data.current["snow"]["1h"])) {
|
||||
if (this.config.units === "imperial") {
|
||||
current.snow = data.current["snow"]["1h"] / 25.4;
|
||||
} else {
|
||||
current.snow = data.current["snow"]["1h"];
|
||||
}
|
||||
precip = true;
|
||||
}
|
||||
if (precip) {
|
||||
current.precipitation = current.rain + current.snow;
|
||||
}
|
||||
current.feelsLikeTemp = data.current.feels_like;
|
||||
}
|
||||
|
||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
// get hourly weather, if requested
|
||||
const hours = [];
|
||||
if (data.hasOwnProperty("hourly")) {
|
||||
for (const hour of data.hourly) {
|
||||
weather.date = moment(hour.dt, "X").utcOffset(data.timezone_offset / 60);
|
||||
// weather.date = moment(hour.dt, "X").utcOffset(data.timezone_offset/60).format(onecallDailyFormat+","+onecallHourlyFormat);
|
||||
weather.temperature = hour.temp;
|
||||
weather.feelsLikeTemp = hour.feels_like;
|
||||
weather.humidity = hour.humidity;
|
||||
weather.windSpeed = hour.wind_speed;
|
||||
weather.windDirection = hour.wind_deg;
|
||||
weather.weatherType = this.convertWeatherType(hour.weather[0].icon);
|
||||
precip = false;
|
||||
if (hour.hasOwnProperty("rain") && !isNaN(hour.rain["1h"])) {
|
||||
if (this.config.units === "imperial") {
|
||||
weather.rain = hour.rain["1h"] / 25.4;
|
||||
} else {
|
||||
weather.rain = hour.rain["1h"];
|
||||
}
|
||||
precip = true;
|
||||
}
|
||||
if (hour.hasOwnProperty("snow") && !isNaN(hour.snow["1h"])) {
|
||||
if (this.config.units === "imperial") {
|
||||
weather.snow = hour.snow["1h"] / 25.4;
|
||||
} else {
|
||||
weather.snow = hour.snow["1h"];
|
||||
}
|
||||
precip = true;
|
||||
}
|
||||
if (precip) {
|
||||
weather.precipitation = weather.rain + weather.snow;
|
||||
}
|
||||
|
||||
hours.push(weather);
|
||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
}
|
||||
}
|
||||
|
||||
// get daily weather, if requested
|
||||
const days = [];
|
||||
if (data.hasOwnProperty("daily")) {
|
||||
for (const day of data.daily) {
|
||||
weather.date = moment(day.dt, "X").utcOffset(data.timezone_offset / 60);
|
||||
weather.sunrise = moment(day.sunrise, "X").utcOffset(data.timezone_offset / 60);
|
||||
weather.sunset = moment(day.sunset, "X").utcOffset(data.timezone_offset / 60);
|
||||
weather.minTemperature = day.temp.min;
|
||||
weather.maxTemperature = day.temp.max;
|
||||
weather.humidity = day.humidity;
|
||||
weather.windSpeed = day.wind_speed;
|
||||
weather.windDirection = day.wind_deg;
|
||||
weather.weatherType = this.convertWeatherType(day.weather[0].icon);
|
||||
precip = false;
|
||||
if (!isNaN(day.rain)) {
|
||||
if (this.config.units === "imperial") {
|
||||
weather.rain = day.rain / 25.4;
|
||||
} else {
|
||||
weather.rain = day.rain;
|
||||
}
|
||||
precip = true;
|
||||
}
|
||||
if (!isNaN(day.snow)) {
|
||||
if (this.config.units === "imperial") {
|
||||
weather.snow = day.snow / 25.4;
|
||||
} else {
|
||||
weather.snow = day.snow;
|
||||
}
|
||||
precip = true;
|
||||
}
|
||||
if (precip) {
|
||||
weather.precipitation = weather.rain + weather.snow;
|
||||
}
|
||||
|
||||
days.push(weather);
|
||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
}
|
||||
}
|
||||
|
||||
return { current: current, hours: hours, days: days };
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert the OpenWeatherMap icons to a more usable name.
|
||||
*/
|
||||
@@ -256,7 +453,21 @@ WeatherProvider.register("openweathermap", {
|
||||
*/
|
||||
getParams() {
|
||||
let params = "?";
|
||||
if (this.config.locationID) {
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
params += "lat=" + this.config.lat;
|
||||
params += "&lon=" + this.config.lon;
|
||||
if (this.config.type === "current") {
|
||||
params += "&exclude=minutely,hourly,daily";
|
||||
} else if (this.config.type === "hourly") {
|
||||
params += "&exclude=current,minutely,daily";
|
||||
} else if (this.config.type === "daily" || this.config.type === "forecast") {
|
||||
params += "&exclude=current,minutely,hourly";
|
||||
} else {
|
||||
params += "&exclude=minutely";
|
||||
}
|
||||
} else if (this.config.lat && this.config.lon) {
|
||||
params += "lat=" + this.config.lat + "&lon=" + this.config.lon;
|
||||
} else if (this.config.locationID) {
|
||||
params += "id=" + this.config.locationID;
|
||||
} else if (this.config.location) {
|
||||
params += "q=" + this.config.location;
|
||||
|
316
modules/default/weather/providers/smhi.js
Normal file
@@ -0,0 +1,316 @@
|
||||
/* global WeatherProvider, WeatherObject, SunCalc */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
* Provider: SMHI
|
||||
*
|
||||
* By BuXXi https://github.com/buxxi
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for SMHI (Sweden only).
|
||||
* Note that SMHI doesn't provide sunrise and sundown, use SunCalc to calculate it.
|
||||
* Metric system is the only supported unit.
|
||||
*/
|
||||
WeatherProvider.register("smhi", {
|
||||
providerName: "SMHI",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
precipitationValue: "pmedian"
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements method in interface for fetching current weather
|
||||
*/
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getURL())
|
||||
.then((data) => {
|
||||
let closest = this.getClosestToCurrentTime(data.timeSeries);
|
||||
let coordinates = this.resolveCoordinates(data);
|
||||
let weatherObject = this.convertWeatherDataToObject(closest, coordinates);
|
||||
this.setFetchedLocation(`(${coordinates.lat},${coordinates.lon})`);
|
||||
this.setCurrentWeather(weatherObject);
|
||||
})
|
||||
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements method in interface for fetching a forecast.
|
||||
* Handling hourly forecast would be easy as not grouping by day but it seems really specific for one weather provider for now.
|
||||
*/
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getURL())
|
||||
.then((data) => {
|
||||
let coordinates = this.resolveCoordinates(data);
|
||||
let weatherObjects = this.convertWeatherDataGroupedByDay(data.timeSeries, coordinates);
|
||||
this.setFetchedLocation(`(${coordinates.lat},${coordinates.lon})`);
|
||||
this.setWeatherForecast(weatherObjects);
|
||||
})
|
||||
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides method for setting config with checks for the precipitationValue being unset or invalid
|
||||
*
|
||||
* @param config
|
||||
*/
|
||||
setConfig(config) {
|
||||
this.config = config;
|
||||
if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) == -1) {
|
||||
console.log("invalid or not set: " + config.precipitationValue);
|
||||
config.precipitationValue = this.defaults.precipitationValue;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Of all the times returned find out which one is closest to the current time, should be the first if the data isn't old.
|
||||
*
|
||||
* @param times
|
||||
*/
|
||||
getClosestToCurrentTime(times) {
|
||||
let now = moment();
|
||||
let minDiff = undefined;
|
||||
for (time of times) {
|
||||
let diff = Math.abs(moment(time.validTime).diff(now));
|
||||
if (!minDiff || diff < Math.abs(moment(minDiff.validTime).diff(now))) {
|
||||
minDiff = time;
|
||||
}
|
||||
}
|
||||
return minDiff;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the forecast url for the configured coordinates
|
||||
*/
|
||||
getURL() {
|
||||
let lon = this.config.lon;
|
||||
let lat = this.config.lat;
|
||||
return `https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/${lon}/lat/${lat}/data.json`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts the returned data into a WeatherObject with required properties set for both current weather and forecast.
|
||||
* The returned units is always in metric system.
|
||||
* Requires coordinates to determine if its daytime or nighttime to know which icon to use and also to set sunrise and sunset.
|
||||
*
|
||||
* @param weatherData
|
||||
* @param coordinates
|
||||
* @param weatherData
|
||||
* @param coordinates
|
||||
*/
|
||||
convertWeatherDataToObject(weatherData, coordinates) {
|
||||
let currentWeather = new WeatherObject("metric", "metric", "metric"); //Weather data is only for Sweden and nobody in Sweden would use imperial
|
||||
|
||||
currentWeather.date = moment(weatherData.validTime);
|
||||
let times = SunCalc.getTimes(currentWeather.date.toDate(), coordinates.lat, coordinates.lon);
|
||||
currentWeather.sunrise = moment(times.sunrise, "X");
|
||||
currentWeather.sunset = moment(times.sunset, "X");
|
||||
currentWeather.humidity = this.paramValue(weatherData, "r");
|
||||
currentWeather.temperature = this.paramValue(weatherData, "t");
|
||||
currentWeather.windSpeed = this.paramValue(weatherData, "ws");
|
||||
currentWeather.windDirection = this.paramValue(weatherData, "wd");
|
||||
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), this.isDayTime(currentWeather));
|
||||
|
||||
//Determine the precipitation amount and category and update the weatherObject with it, the valuetype to use can be configured or uses median as default.
|
||||
let precipitationValue = this.paramValue(weatherData, this.config.precipitationValue);
|
||||
switch (this.paramValue(weatherData, "pcat")) {
|
||||
// 0 = No precipitation
|
||||
case 1: // Snow
|
||||
currentWeather.snow += precipitationValue;
|
||||
currentWeather.precipitation += precipitationValue;
|
||||
break;
|
||||
case 2: // Snow and rain, treat it as 50/50 snow and rain
|
||||
currentWeather.snow += precipitationValue / 2;
|
||||
currentWeather.rain += precipitationValue / 2;
|
||||
currentWeather.precipitation += precipitationValue;
|
||||
break;
|
||||
case 3: // Rain
|
||||
case 4: // Drizzle
|
||||
case 5: // Freezing rain
|
||||
case 6: // Freezing drizzle
|
||||
currentWeather.rain += precipitationValue;
|
||||
currentWeather.precipitation += precipitationValue;
|
||||
break;
|
||||
}
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
/**
|
||||
* Takes all of the data points and converts it to one WeatherObject per day.
|
||||
*
|
||||
* @param allWeatherData
|
||||
* @param coordinates
|
||||
* @param allWeatherData
|
||||
* @param coordinates
|
||||
*/
|
||||
convertWeatherDataGroupedByDay(allWeatherData, coordinates) {
|
||||
var currentWeather;
|
||||
let result = [];
|
||||
|
||||
let allWeatherObjects = this.fillInGaps(allWeatherData).map((weatherData) => this.convertWeatherDataToObject(weatherData, coordinates));
|
||||
var dayWeatherTypes = [];
|
||||
|
||||
for (weatherObject of allWeatherObjects) {
|
||||
//If its the first object or if a day change we need to reset the summary object
|
||||
if (!currentWeather || !currentWeather.date.isSame(weatherObject.date, "day")) {
|
||||
currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
dayWeatherTypes = [];
|
||||
currentWeather.date = weatherObject.date;
|
||||
currentWeather.minTemperature = Infinity;
|
||||
currentWeather.maxTemperature = -Infinity;
|
||||
currentWeather.snow = 0;
|
||||
currentWeather.rain = 0;
|
||||
currentWeather.precipitation = 0;
|
||||
result.push(currentWeather);
|
||||
}
|
||||
|
||||
//Keep track of what icons has been used for each hour of daytime and use the middle one for the forecast
|
||||
if (this.isDayTime(weatherObject)) {
|
||||
dayWeatherTypes.push(weatherObject.weatherType);
|
||||
}
|
||||
if (dayWeatherTypes.length > 0) {
|
||||
currentWeather.weatherType = dayWeatherTypes[Math.floor(dayWeatherTypes.length / 2)];
|
||||
} else {
|
||||
currentWeather.weatherType = weatherObject.weatherType;
|
||||
}
|
||||
|
||||
//All other properties is either a sum, min or max of each hour
|
||||
currentWeather.minTemperature = Math.min(currentWeather.minTemperature, weatherObject.temperature);
|
||||
currentWeather.maxTemperature = Math.max(currentWeather.maxTemperature, weatherObject.temperature);
|
||||
currentWeather.snow += weatherObject.snow;
|
||||
currentWeather.rain += weatherObject.rain;
|
||||
currentWeather.precipitation += weatherObject.precipitation;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolve coordinates from the response data (probably preferably to use this if it's not matching the config values exactly)
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
resolveCoordinates(data) {
|
||||
return { lat: data.geometry.coordinates[0][1], lon: data.geometry.coordinates[0][0] };
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the weatherObject is at dayTime.
|
||||
*
|
||||
* @param weatherObject
|
||||
*/
|
||||
isDayTime(weatherObject) {
|
||||
return weatherObject.date.isBetween(weatherObject.sunrise, weatherObject.sunset, undefined, "[]");
|
||||
},
|
||||
|
||||
/**
|
||||
* The distance between the data points is increasing in the data the more distant the prediction is.
|
||||
* Find these gaps and fill them with the previous hours data to make the data returned a complete set.
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
fillInGaps(data) {
|
||||
let result = [];
|
||||
for (var i = 1; i < data.length; i++) {
|
||||
let to = moment(data[i].validTime);
|
||||
let from = moment(data[i - 1].validTime);
|
||||
let hours = moment.duration(to.diff(from)).asHours();
|
||||
// For each hour add a datapoint but change the validTime
|
||||
for (var j = 0; j < hours; j++) {
|
||||
let current = Object.assign({}, data[i]);
|
||||
current.validTime = from.clone().add(j, "hours").toISOString();
|
||||
result.push(current);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method to fetch a property from the returned data set.
|
||||
* The returned values is an array with always one value in it.
|
||||
*
|
||||
* @param currentWeatherData
|
||||
* @param name
|
||||
* @param currentWeatherData
|
||||
* @param name
|
||||
*/
|
||||
paramValue(currentWeatherData, name) {
|
||||
return currentWeatherData.parameters.filter((p) => p.name == name).flatMap((p) => p.values)[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Map the icon value from SHMI to an icon that MagicMirror understands.
|
||||
* Uses different icons depending if its daytime or nighttime.
|
||||
* SHMI's description of what the numeric value means is the comment after the case.
|
||||
*
|
||||
* @param input
|
||||
* @param isDayTime
|
||||
* @param input
|
||||
* @param isDayTime
|
||||
*/
|
||||
convertWeatherType(input, isDayTime) {
|
||||
switch (input) {
|
||||
case 1:
|
||||
return isDayTime ? "day-sunny" : "night-clear"; // Clear sky
|
||||
case 2:
|
||||
return isDayTime ? "day-sunny-overcast" : "night-partly-cloudy"; //Nearly clear sky
|
||||
case 3:
|
||||
return isDayTime ? "day-cloudy" : "night-cloudy"; //Variable cloudiness
|
||||
case 4:
|
||||
return isDayTime ? "day-cloudy" : "night-cloudy"; //Halfclear sky
|
||||
case 5:
|
||||
return "cloudy"; //Cloudy sky
|
||||
case 6:
|
||||
return "cloudy"; //Overcast
|
||||
case 7:
|
||||
return "fog"; //Fog
|
||||
case 8:
|
||||
return "showers"; //Light rain showers
|
||||
case 9:
|
||||
return "showers"; //Moderate rain showers
|
||||
case 10:
|
||||
return "showers"; //Heavy rain showers
|
||||
case 11:
|
||||
return "thunderstorm"; //Thunderstorm
|
||||
case 12:
|
||||
return "sleet"; //Light sleet showers
|
||||
case 13:
|
||||
return "sleet"; //Moderate sleet showers
|
||||
case 14:
|
||||
return "sleet"; //Heavy sleet showers
|
||||
case 15:
|
||||
return "snow"; //Light snow showers
|
||||
case 16:
|
||||
return "snow"; //Moderate snow showers
|
||||
case 17:
|
||||
return "snow"; //Heavy snow showers
|
||||
case 18:
|
||||
return "rain"; //Light rain
|
||||
case 19:
|
||||
return "rain"; //Moderate rain
|
||||
case 20:
|
||||
return "rain"; //Heavy rain
|
||||
case 21:
|
||||
return "thunderstorm"; //Thunder
|
||||
case 22:
|
||||
return "sleet"; // Light sleet
|
||||
case 23:
|
||||
return "sleet"; //Moderate sleet
|
||||
case 24:
|
||||
return "sleet"; // Heavy sleet
|
||||
case 25:
|
||||
return "snow"; // Light snowfall
|
||||
case 26:
|
||||
return "snow"; //Moderate snowfall
|
||||
case 27:
|
||||
return "snow"; //Heavy snowfall
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
});
|
@@ -14,6 +14,13 @@ WeatherProvider.register("ukmetoffice", {
|
||||
// But for debugging (and future alerts) it would be nice to have the real name.
|
||||
providerName: "UK Met Office",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "http://datapoint.metoffice.gov.uk/public/data/val/wxfcs/all/json/",
|
||||
locationID: false,
|
||||
apiKey: ""
|
||||
},
|
||||
|
||||
units: {
|
||||
imperial: "us",
|
||||
metric: "si"
|
||||
@@ -73,7 +80,7 @@ WeatherProvider.register("ukmetoffice", {
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
// data times are always UTC
|
||||
let nowUtc = moment.utc();
|
||||
@@ -124,7 +131,7 @@ WeatherProvider.register("ukmetoffice", {
|
||||
// loop round the (5) periods getting the data
|
||||
// for each period array, Day is [0], Night is [1]
|
||||
for (var j in forecasts.SiteRep.DV.Location.Period) {
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
// data times are always UTC
|
||||
const dateStr = forecasts.SiteRep.DV.Location.Period[j].value;
|
||||
@@ -208,10 +215,10 @@ WeatherProvider.register("ukmetoffice", {
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert wind speed (from mph) if required
|
||||
* Convert wind speed (from mph to m/s or km/h) if required
|
||||
*/
|
||||
convertWindSpeed(windInMph) {
|
||||
return this.windUnits === "metric" ? windInMph * 2.23694 : windInMph;
|
||||
return this.windUnits === "metric" ? (this.useKmh ? windInMph * 1.60934 : windInMph / 2.23694) : windInMph;
|
||||
},
|
||||
|
||||
/*
|
||||
|
@@ -44,6 +44,16 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
// Set the name of the provider.
|
||||
providerName: "UK Met Office (DataHub)",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "https://api-metoffice.apiconnect.ibmcloud.com/metoffice/production/v0/forecasts/point/",
|
||||
apiKey: "",
|
||||
apiSecret: "",
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
windUnits: "mph"
|
||||
},
|
||||
|
||||
// Build URL with query strings according to DataHub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
|
||||
getUrl(forecastType) {
|
||||
let queryStrings = "?";
|
||||
@@ -87,7 +97,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
Log.error("Possibly bad current/hourly data?");
|
||||
Log.info(data);
|
||||
Log.error(data);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -108,7 +118,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
|
||||
// Create a WeatherObject using current weather data (data for the current hour)
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
// Extract the actual forecasts
|
||||
let forecastDataHours = currentWeatherData.features[0].properties.timeSeries;
|
||||
@@ -158,7 +168,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
Log.error("Possibly bad forecast data?");
|
||||
Log.info(data);
|
||||
Log.error(data);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -189,7 +199,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
|
||||
// Go through each day in the forecasts
|
||||
for (day in forecastDataDays) {
|
||||
const forecastWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const forecastWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
// Get date of forecast
|
||||
let forecastDate = moment.utc(forecastDataDays[day].time);
|
||||
@@ -254,7 +264,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
return windInMpS;
|
||||
}
|
||||
|
||||
if (this.config.windUnits == "kph" || this.config.windUnits == "metric") {
|
||||
if (this.config.windUnits == "kph" || this.config.windUnits == "metric" || this.config.useKmh) {
|
||||
return windInMpS * 3.6;
|
||||
}
|
||||
|
||||
|
190
modules/default/weather/providers/weatherbit.js
Normal file
@@ -0,0 +1,190 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
* Provider: Weatherbit
|
||||
*
|
||||
* By Andrew Pometti
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Weatherbit, based on Nicholas Hubbard's class for Dark Sky & Vince Peri's class for Weather.gov.
|
||||
*/
|
||||
WeatherProvider.register("weatherbit", {
|
||||
// Set the name of the provider.
|
||||
// Not strictly required, but helps for debugging.
|
||||
providerName: "Weatherbit",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "https://api.weatherbit.io/v2.0",
|
||||
weatherEndpoint: "/current",
|
||||
apiKey: "",
|
||||
lat: 0,
|
||||
lon: 0
|
||||
},
|
||||
|
||||
units: {
|
||||
imperial: "I",
|
||||
metric: "M"
|
||||
},
|
||||
|
||||
fetchedLocation: function () {
|
||||
return this.fetchedLocationName || "";
|
||||
},
|
||||
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data || !data.data[0] || typeof data.data[0].temp === "undefined") {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWeather = this.generateWeatherDayFromCurrentWeather(data);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data || !data.data) {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data.data);
|
||||
this.setWeatherForecast(forecast);
|
||||
|
||||
this.fetchedLocationName = data.city_name + ", " + data.state_code;
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Create a URL from the config and base URL.
|
||||
getUrl() {
|
||||
const units = this.units[this.config.units] || "auto";
|
||||
return `${this.config.apiBase}${this.config.weatherEndpoint}?lat=${this.config.lat}&lon=${this.config.lon}&units=${units}&key=${this.config.apiKey}`;
|
||||
},
|
||||
|
||||
// Implement WeatherDay generator.
|
||||
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
||||
//Calculate TZ Offset and invert to convert Sunrise/Sunset times to Local
|
||||
const d = new Date();
|
||||
let tzOffset = d.getTimezoneOffset();
|
||||
tzOffset = tzOffset * -1;
|
||||
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
currentWeather.date = moment(currentWeatherData.data[0].ts, "X");
|
||||
currentWeather.humidity = parseFloat(currentWeatherData.data[0].rh);
|
||||
currentWeather.temperature = parseFloat(currentWeatherData.data[0].temp);
|
||||
currentWeather.windSpeed = parseFloat(currentWeatherData.data[0].wind_spd);
|
||||
currentWeather.windDirection = currentWeatherData.data[0].wind_dir;
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.data[0].weather.icon);
|
||||
Log.log("Wx Icon: " + currentWeatherData.data[0].weather.icon);
|
||||
currentWeather.sunrise = moment(currentWeatherData.data[0].sunrise, "HH:mm").add(tzOffset, "m");
|
||||
currentWeather.sunset = moment(currentWeatherData.data[0].sunset, "HH:mm").add(tzOffset, "m");
|
||||
|
||||
this.fetchedLocationName = currentWeatherData.data[0].city_name + ", " + currentWeatherData.data[0].state_code;
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
const days = [];
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
weather.date = moment(forecast.datetime, "YYYY-MM-DD");
|
||||
weather.minTemperature = forecast.min_temp;
|
||||
weather.maxTemperature = forecast.max_temp;
|
||||
weather.precipitation = forecast.precip;
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather.icon);
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
// Map icons from Dark Sky to our icons.
|
||||
convertWeatherType(weatherType) {
|
||||
const weatherTypes = {
|
||||
t01d: "day-thunderstorm",
|
||||
t01n: "night-alt-thunderstorm",
|
||||
t02d: "day-thunderstorm",
|
||||
t02n: "night-alt-thunderstorm",
|
||||
t03d: "thunderstorm",
|
||||
t03n: "thunderstorm",
|
||||
t04d: "day-thunderstorm",
|
||||
t04n: "night-alt-thunderstorm",
|
||||
t05d: "day-sleet-storm",
|
||||
t05n: "night-alt-sleet-storm",
|
||||
d01d: "day-sprinkle",
|
||||
d01n: "night-alt-sprinkle",
|
||||
d02d: "day-sprinkle",
|
||||
d02n: "night-alt-sprinkle",
|
||||
d03d: "day-shower",
|
||||
d03n: "night-alt-shower",
|
||||
r01d: "day-shower",
|
||||
r01n: "night-alt-shower",
|
||||
r02d: "day-rain",
|
||||
r02n: "night-alt-rain",
|
||||
r03d: "day-rain",
|
||||
r03n: "night-alt-rain",
|
||||
r04d: "day-sprinkle",
|
||||
r04n: "night-alt-sprinkle",
|
||||
r05d: "day-shower",
|
||||
r05n: "night-alt-shower",
|
||||
r06d: "day-shower",
|
||||
r06n: "night-alt-shower",
|
||||
f01d: "day-sleet",
|
||||
f01n: "night-alt-sleet",
|
||||
s01d: "day-snow",
|
||||
s01n: "night-alt-snow",
|
||||
s02d: "day-snow-wind",
|
||||
s02n: "night-alt-snow-wind",
|
||||
s03d: "snowflake-cold",
|
||||
s03n: "snowflake-cold",
|
||||
s04d: "day-rain-mix",
|
||||
s04n: "night-alt-rain-mix",
|
||||
s05d: "day-sleet",
|
||||
s05n: "night-alt-sleet",
|
||||
s06d: "day-snow",
|
||||
s06n: "night-alt-snow",
|
||||
a01d: "day-haze",
|
||||
a01n: "dust",
|
||||
a02d: "smoke",
|
||||
a02n: "smoke",
|
||||
a03d: "day-haze",
|
||||
a03n: "dust",
|
||||
a04d: "dust",
|
||||
a04n: "dust",
|
||||
a05d: "day-fog",
|
||||
a05n: "night-fog",
|
||||
a06d: "fog",
|
||||
a06n: "fog",
|
||||
c01d: "day-sunny",
|
||||
c01n: "night-clear",
|
||||
c02d: "day-sunny-overcast",
|
||||
c02n: "night-alt-partly-cloudy",
|
||||
c03d: "day-cloudy",
|
||||
c03n: "night-alt-cloudy",
|
||||
c04d: "cloudy",
|
||||
c04n: "cloudy",
|
||||
u00d: "rain-mix",
|
||||
u00n: "rain-mix"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
}
|
||||
});
|
@@ -3,76 +3,163 @@
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
* Provider: weather.gov
|
||||
* https://weather-gov.github.io/api/general-faqs
|
||||
*
|
||||
* By Vince Peri
|
||||
* Original by Vince Peri
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is a provider for weather.gov.
|
||||
* Note that this is only for US locations (lat and lon) and does not require an API key
|
||||
* Since it is free, there are some items missing - like sunrise, sunset, humidity, etc.
|
||||
* Since it is free, there are some items missing - like sunrise, sunset
|
||||
*/
|
||||
|
||||
WeatherProvider.register("weathergov", {
|
||||
// Set the name of the provider.
|
||||
// This isn't strictly necessary, since it will fallback to the provider identifier
|
||||
// But for debugging (and future alerts) it would be nice to have the real name.
|
||||
providerName: "Weather.gov",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "https://api.weatherbit.io/v2.0",
|
||||
weatherEndpoint: "/forecast",
|
||||
lat: 0,
|
||||
lon: 0
|
||||
},
|
||||
|
||||
// Flag all needed URLs availability
|
||||
configURLs: false,
|
||||
|
||||
//This API has multiple urls involved
|
||||
forecastURL: "tbd",
|
||||
forecastHourlyURL: "tbd",
|
||||
forecastGridDataURL: "tbd",
|
||||
observationStationsURL: "tbd",
|
||||
stationObsURL: "tbd",
|
||||
|
||||
// Called to set the config, this config is the same as the weather module's config.
|
||||
setConfig: function (config) {
|
||||
this.config = config;
|
||||
(this.config.apiBase = "https://api.weather.gov"), this.fetchWxGovURLs(this.config);
|
||||
},
|
||||
|
||||
// Called when the weather provider is about to start.
|
||||
start: function () {
|
||||
Log.info(`Weather provider: ${this.providerName} started.`);
|
||||
},
|
||||
|
||||
// This returns the name of the fetched location or an empty string.
|
||||
fetchedLocation: function () {
|
||||
return this.fetchedLocationName || "";
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.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?
|
||||
if (!this.configURLs) {
|
||||
Log.info("fetch wx waiting on config URLs");
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data.properties.periods[0]);
|
||||
this.fetchData(this.stationObsURL)
|
||||
.then((data) => {
|
||||
if (!data || !data.properties) {
|
||||
// Did not receive usable new data.
|
||||
return;
|
||||
}
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data.properties);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
Log.error("Could not load station obs data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
// Overwrite the fetchWeatherForecast method.
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
if (!this.configURLs) {
|
||||
Log.info("fetch wx waiting on config URLs");
|
||||
return;
|
||||
}
|
||||
this.fetchData(this.forecastURL)
|
||||
.then((data) => {
|
||||
if (!data || !data.properties || !data.properties.periods || !data.properties.periods.length) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data.properties.periods);
|
||||
this.setWeatherForecast(forecast);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
Log.error("Could not load forecast hourly data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/** Weather.gov Specific Methods - These are not part of the default provider methods */
|
||||
|
||||
/*
|
||||
* Gets the complete url for the request
|
||||
* Get specific URLs
|
||||
*/
|
||||
getUrl() {
|
||||
return this.config.apiBase + this.config.lat + "," + this.config.lon + this.config.weatherEndpoint;
|
||||
fetchWxGovURLs(config) {
|
||||
this.fetchData(`${config.apiBase}/points/${config.lat},${config.lon}`)
|
||||
.then((data) => {
|
||||
if (!data || !data.properties) {
|
||||
// points URL did not respond with usable data.
|
||||
return;
|
||||
}
|
||||
this.fetchedLocationName = data.properties.relativeLocation.properties.city + ", " + data.properties.relativeLocation.properties.state;
|
||||
Log.log("Forecast location is " + this.fetchedLocationName);
|
||||
this.forecastURL = data.properties.forecast;
|
||||
this.forecastHourlyURL = data.properties.forecastHourly;
|
||||
this.forecastGridDataURL = data.properties.forecastGridData;
|
||||
this.observationStationsURL = data.properties.observationStations;
|
||||
// with this URL, we chain another promise for the station obs URL
|
||||
return this.fetchData(data.properties.observationStations);
|
||||
})
|
||||
.then((obsData) => {
|
||||
if (!obsData || !obsData.features) {
|
||||
// obs station URL did not respond with usable data.
|
||||
return;
|
||||
}
|
||||
this.stationObsURL = obsData.features[0].id + "/observations/latest";
|
||||
})
|
||||
.catch((err) => {
|
||||
Log.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
// excellent, let's fetch some actual wx data
|
||||
this.configURLs = true;
|
||||
this.fetchCurrentWeather();
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
* Weather.gov API uses specific units; API does not include choice of units
|
||||
* ... object needs data in units based on config!
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
currentWeather.temperature = currentWeatherData.temperature;
|
||||
currentWeather.windSpeed = currentWeatherData.windSpeed.split(" ", 1);
|
||||
currentWeather.windDirection = this.convertWindDirection(currentWeatherData.windDirection);
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.shortForecast, currentWeatherData.isDaytime);
|
||||
currentWeather.date = moment(currentWeatherData.timestamp);
|
||||
currentWeather.temperature = this.convertTemp(currentWeatherData.temperature.value);
|
||||
currentWeather.windSpeed = this.convertSpeed(currentWeatherData.windSpeed.value);
|
||||
currentWeather.windDirection = currentWeatherData.windDirection.value;
|
||||
currentWeather.minTemperature = this.convertTemp(currentWeatherData.minTemperatureLast24Hours.value);
|
||||
currentWeather.maxTemperature = this.convertTemp(currentWeatherData.maxTemperatureLast24Hours.value);
|
||||
currentWeather.humidity = Math.round(currentWeatherData.relativeHumidity.value);
|
||||
currentWeather.rain = null;
|
||||
currentWeather.snow = null;
|
||||
currentWeather.precipitation = this.convertLength(currentWeatherData.precipitationLastHour.value);
|
||||
currentWeather.feelsLikeTemp = this.convertTemp(currentWeatherData.heatIndex.value);
|
||||
|
||||
let isDaytime = true;
|
||||
if (currentWeatherData.icon.includes("day")) {
|
||||
isDaytime = true;
|
||||
} else {
|
||||
isDaytime = false;
|
||||
}
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.textDescription, isDaytime);
|
||||
|
||||
// determine the sunrise/sunset times - not supplied in weather.gov data
|
||||
let times = this.calcAstroData(this.config.lat, this.config.lon);
|
||||
@@ -100,7 +187,7 @@ WeatherProvider.register("weathergov", {
|
||||
let maxTemp = [];
|
||||
// variable for date
|
||||
let date = "";
|
||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
weather.precipitation = 0;
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
@@ -112,7 +199,7 @@ WeatherProvider.register("weathergov", {
|
||||
// push weather information to days array
|
||||
days.push(weather);
|
||||
// create new weather-object
|
||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
minTemp = [];
|
||||
maxTemp = [];
|
||||
@@ -124,7 +211,7 @@ WeatherProvider.register("weathergov", {
|
||||
// specify date
|
||||
weather.date = moment(forecast.startTime);
|
||||
|
||||
// If the first value of today is later than 17:00, we have an icon at least!
|
||||
// use the forecast isDayTime attribute to help build the weatherType label
|
||||
weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime);
|
||||
}
|
||||
|
||||
@@ -148,6 +235,38 @@ WeatherProvider.register("weathergov", {
|
||||
return days.slice(1);
|
||||
},
|
||||
|
||||
/*
|
||||
* Unit conversions
|
||||
*/
|
||||
// conversion to fahrenheit
|
||||
convertTemp(temp) {
|
||||
if (this.config.tempUnits === "imperial") {
|
||||
return (9 / 5) * temp + 32;
|
||||
} else {
|
||||
return temp;
|
||||
}
|
||||
},
|
||||
// conversion to mph or kmh
|
||||
convertSpeed(metSec) {
|
||||
if (this.config.windUnits === "imperial") {
|
||||
return metSec * 2.23694;
|
||||
} else {
|
||||
if (this.config.useKmh) {
|
||||
return metSec * 3.6;
|
||||
} else {
|
||||
return metSec;
|
||||
}
|
||||
}
|
||||
},
|
||||
// conversion to inches
|
||||
convertLength(meters) {
|
||||
if (this.config.units === "imperial") {
|
||||
return meters * 39.3701;
|
||||
} else {
|
||||
return meters;
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Calculate the astronomical data
|
||||
*/
|
||||
|
@@ -11,16 +11,11 @@ Module.register("weather", {
|
||||
defaults: {
|
||||
weatherProvider: "openweathermap",
|
||||
roundTemp: false,
|
||||
type: "current", //current, forecast
|
||||
|
||||
location: false,
|
||||
locationID: false,
|
||||
appid: "",
|
||||
type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint)
|
||||
units: config.units,
|
||||
|
||||
useKmh: false,
|
||||
tempUnits: config.units,
|
||||
windUnits: config.units,
|
||||
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
animationSpeed: 1000,
|
||||
timeFormat: config.timeFormat,
|
||||
@@ -37,20 +32,13 @@ Module.register("weather", {
|
||||
showIndoorTemperature: false,
|
||||
showIndoorHumidity: false,
|
||||
maxNumberOfDays: 5,
|
||||
maxEntries: 5,
|
||||
fade: true,
|
||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
||||
|
||||
initialLoadDelay: 0, // 0 seconds delay
|
||||
retryDelay: 2500,
|
||||
|
||||
apiVersion: "2.5",
|
||||
apiBase: "https://api.openweathermap.org/data/",
|
||||
weatherEndpoint: "/weather",
|
||||
|
||||
appendLocationNameToHeader: true,
|
||||
calendarClass: "calendar",
|
||||
tableClass: "small",
|
||||
|
||||
onlyTemp: false,
|
||||
showPrecipitationAmount: false,
|
||||
colored: false,
|
||||
@@ -72,11 +60,12 @@ Module.register("weather", {
|
||||
|
||||
// Override getHeader method.
|
||||
getHeader: function () {
|
||||
if (this.config.appendLocationNameToHeader && this.data.header !== undefined && this.weatherProvider) {
|
||||
return this.data.header + " " + this.weatherProvider.fetchedLocation();
|
||||
if (this.config.appendLocationNameToHeader && this.weatherProvider) {
|
||||
if (this.data.header) return this.data.header + " " + this.weatherProvider.fetchedLocation();
|
||||
else return this.weatherProvider.fetchedLocation();
|
||||
}
|
||||
|
||||
return this.data.header;
|
||||
return this.data.header ? this.data.header : "";
|
||||
},
|
||||
|
||||
// Start the weather module.
|
||||
@@ -123,7 +112,18 @@ Module.register("weather", {
|
||||
|
||||
// Select the template depending on the display type.
|
||||
getTemplate: function () {
|
||||
return `${this.config.type.toLowerCase()}.njk`;
|
||||
switch (this.config.type.toLowerCase()) {
|
||||
case "current":
|
||||
return `current.njk`;
|
||||
case "hourly":
|
||||
return `hourly.njk`;
|
||||
case "daily":
|
||||
case "forecast":
|
||||
return `forecast.njk`;
|
||||
//Make the invalid values use the "Loading..." from forecast
|
||||
default:
|
||||
return `forecast.njk`;
|
||||
}
|
||||
},
|
||||
|
||||
// Add all the data to the template.
|
||||
@@ -132,6 +132,7 @@ Module.register("weather", {
|
||||
config: this.config,
|
||||
current: this.weatherProvider.currentWeather(),
|
||||
forecast: this.weatherProvider.weatherForecast(),
|
||||
hourly: this.weatherProvider.weatherHourly(),
|
||||
indoor: {
|
||||
humidity: this.indoorHumidity,
|
||||
temperature: this.indoorTemperature
|
||||
@@ -144,6 +145,10 @@ Module.register("weather", {
|
||||
Log.log("New weather information available.");
|
||||
this.updateDom(0);
|
||||
this.scheduleUpdate();
|
||||
|
||||
if (this.weatherProvider.currentWeather()) {
|
||||
this.sendNotification("CURRENTWEATHER_TYPE", { type: this.weatherProvider.currentWeather().weatherType.replace("-", "_") });
|
||||
}
|
||||
},
|
||||
|
||||
scheduleUpdate: function (delay = null) {
|
||||
@@ -153,17 +158,27 @@ Module.register("weather", {
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.config.type === "forecast") {
|
||||
this.weatherProvider.fetchWeatherForecast();
|
||||
} else {
|
||||
switch (this.config.type.toLowerCase()) {
|
||||
case "current":
|
||||
this.weatherProvider.fetchCurrentWeather();
|
||||
break;
|
||||
case "hourly":
|
||||
this.weatherProvider.fetchWeatherHourly();
|
||||
break;
|
||||
case "daily":
|
||||
case "forecast":
|
||||
this.weatherProvider.fetchWeatherForecast();
|
||||
break;
|
||||
default:
|
||||
Log.error(`Invalid type ${this.config.type} configured (must be one of 'current', 'hourly', 'daily' or 'forecast')`);
|
||||
}
|
||||
}, nextLoad);
|
||||
},
|
||||
|
||||
roundValue: function (temperature) {
|
||||
var decimals = this.config.roundTemp ? 0 : 1;
|
||||
return parseFloat(temperature).toFixed(decimals);
|
||||
var roundValue = parseFloat(temperature).toFixed(decimals);
|
||||
return roundValue === "-0" ? 0 : roundValue;
|
||||
},
|
||||
|
||||
addFilters() {
|
||||
@@ -205,7 +220,7 @@ Module.register("weather", {
|
||||
}
|
||||
}
|
||||
} else if (type === "precip") {
|
||||
if (isNaN(value) || value === 0 || value.toFixed(2) === "0.00") {
|
||||
if (value === null || isNaN(value) || value === 0 || value.toFixed(2) === "0.00") {
|
||||
value = "";
|
||||
} else {
|
||||
if (this.config.weatherProvider === "ukmetoffice" || this.config.weatherProvider === "ukmetofficedatahub") {
|
||||
@@ -243,6 +258,13 @@ Module.register("weather", {
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
this.nunjucksEnvironment().addFilter(
|
||||
"calcNumEntries",
|
||||
function (dataArray) {
|
||||
return Math.min(dataArray.length, this.config.maxEntries);
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
this.nunjucksEnvironment().addFilter(
|
||||
"opacity",
|
||||
function (currentStep, numSteps) {
|
||||
|
@@ -10,10 +10,11 @@
|
||||
* As soon as we start implementing the forecast, mode properties will be added.
|
||||
*/
|
||||
class WeatherObject {
|
||||
constructor(units, tempUnits, windUnits) {
|
||||
constructor(units, tempUnits, windUnits, useKmh) {
|
||||
this.units = units;
|
||||
this.tempUnits = tempUnits;
|
||||
this.windUnits = windUnits;
|
||||
this.useKmh = useKmh;
|
||||
this.date = null;
|
||||
this.windSpeed = null;
|
||||
this.windDirection = null;
|
||||
@@ -67,7 +68,7 @@ class WeatherObject {
|
||||
}
|
||||
|
||||
beaufortWindSpeed() {
|
||||
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : (this.windSpeed * 60 * 60) / 1000;
|
||||
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : this.useKmh ? this.windSpeed : (this.windSpeed * 60 * 60) / 1000;
|
||||
const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
|
||||
for (const [index, speed] of speeds.entries()) {
|
||||
if (speed > windInKmh) {
|
||||
@@ -77,6 +78,11 @@ class WeatherObject {
|
||||
return 12;
|
||||
}
|
||||
|
||||
kmhWindSpeed() {
|
||||
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : (this.windSpeed * 60 * 60) / 1000;
|
||||
return windInKmh;
|
||||
}
|
||||
|
||||
nextSunAction() {
|
||||
return moment().isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise";
|
||||
}
|
||||
|
@@ -11,11 +11,13 @@
|
||||
var WeatherProvider = Class.extend({
|
||||
// Weather Provider Properties
|
||||
providerName: null,
|
||||
defaults: {},
|
||||
|
||||
// The following properties have accestor methods.
|
||||
// The following properties have accessor methods.
|
||||
// Try to not access them directly.
|
||||
currentWeatherObject: null,
|
||||
weatherForecastArray: null,
|
||||
weatherHourlyArray: null,
|
||||
fetchedLocationName: null,
|
||||
|
||||
// The following properties will be set automatically.
|
||||
@@ -56,6 +58,12 @@ var WeatherProvider = Class.extend({
|
||||
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherForecast method.`);
|
||||
},
|
||||
|
||||
// This method should start the API request to fetch the weather hourly.
|
||||
// This method should definitely be overwritten in the provider.
|
||||
fetchWeatherHourly: function () {
|
||||
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherHourly method.`);
|
||||
},
|
||||
|
||||
// This returns a WeatherDay object for the current weather.
|
||||
currentWeather: function () {
|
||||
return this.currentWeatherObject;
|
||||
@@ -66,6 +74,11 @@ var WeatherProvider = Class.extend({
|
||||
return this.weatherForecastArray;
|
||||
},
|
||||
|
||||
// This returns an object containing WeatherDay object(s) depending on the type of call.
|
||||
weatherHourly: function () {
|
||||
return this.weatherHourlyArray;
|
||||
},
|
||||
|
||||
// This returns the name of the fetched location or an empty string.
|
||||
fetchedLocation: function () {
|
||||
return this.fetchedLocationName || "";
|
||||
@@ -83,6 +96,11 @@ var WeatherProvider = Class.extend({
|
||||
this.weatherForecastArray = weatherForecastArray;
|
||||
},
|
||||
|
||||
// Set the weatherHourlyArray and notify the delegate that new information is available.
|
||||
setWeatherHourly: function (weatherHourlyArray) {
|
||||
this.weatherHourlyArray = weatherHourlyArray;
|
||||
},
|
||||
|
||||
// Set the fetched location name.
|
||||
setFetchedLocation: function (name) {
|
||||
this.fetchedLocationName = name;
|
||||
@@ -119,6 +137,9 @@ WeatherProvider.providers = [];
|
||||
|
||||
/**
|
||||
* Static method to register a new weather provider.
|
||||
*
|
||||
* @param {string} providerIdentifier The name of the weather provider
|
||||
* @param {object} providerDetails The details of the weather provider
|
||||
*/
|
||||
WeatherProvider.register = function (providerIdentifier, providerDetails) {
|
||||
WeatherProvider.providers[providerIdentifier.toLowerCase()] = WeatherProvider.extend(providerDetails);
|
||||
@@ -126,14 +147,19 @@ WeatherProvider.register = function (providerIdentifier, providerDetails) {
|
||||
|
||||
/**
|
||||
* Static method to initialize a new weather provider.
|
||||
*
|
||||
* @param {string} providerIdentifier The name of the weather provider
|
||||
* @param {object} delegate The weather module
|
||||
* @returns {object} The new weather provider
|
||||
*/
|
||||
WeatherProvider.initialize = function (providerIdentifier, delegate) {
|
||||
providerIdentifier = providerIdentifier.toLowerCase();
|
||||
|
||||
var provider = new WeatherProvider.providers[providerIdentifier]();
|
||||
const provider = new WeatherProvider.providers[providerIdentifier]();
|
||||
const config = Object.assign({}, provider.defaults, delegate.config);
|
||||
|
||||
provider.delegate = delegate;
|
||||
provider.setConfig(delegate.config);
|
||||
provider.setConfig(config);
|
||||
|
||||
provider.providerIdentifier = providerIdentifier;
|
||||
if (!provider.providerName) {
|
||||
|
@@ -1,5 +1,7 @@
|
||||
# Module: Weather Forecast
|
||||
|
||||
> :warning: **This module is deprecated in favor of the [weather](https://docs.magicmirror.builders/modules/weather.html) module.**
|
||||
|
||||
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.
|
||||
|
||||
|
9
modules/default/weatherforecast/node_helper.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const NodeHelper = require("node_helper");
|
||||
const Log = require("logger");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
start: function () {
|
||||
Log.warn(`The module '${this.name}' is deprecated in favor of the 'weather'-module, please refer to the documentation for a migration path`);
|
||||
}
|
||||
});
|
@@ -3,12 +3,16 @@
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This module is deprecated. Any additional feature will no longer be merged.
|
||||
*/
|
||||
Module.register("weatherforecast", {
|
||||
// Default module config.
|
||||
defaults: {
|
||||
location: false,
|
||||
locationID: false,
|
||||
lat: false,
|
||||
lon: false,
|
||||
appid: "",
|
||||
units: config.units,
|
||||
maxNumberOfDays: 7,
|
||||
@@ -29,6 +33,7 @@ Module.register("weatherforecast", {
|
||||
apiVersion: "2.5",
|
||||
apiBase: "https://api.openweathermap.org/data/",
|
||||
forecastEndpoint: "forecast/daily",
|
||||
excludes: false,
|
||||
|
||||
appendLocationNameToHeader: true,
|
||||
calendarClass: "calendar",
|
||||
@@ -100,7 +105,7 @@ Module.register("weatherforecast", {
|
||||
getDom: function () {
|
||||
var wrapper = document.createElement("div");
|
||||
|
||||
if (this.config.appid === "") {
|
||||
if (this.config.appid === "" || this.config.appid === "YOUR_OPENWEATHER_API_KEY") {
|
||||
wrapper.innerHTML = "Please set the correct openweather <i>appid</i> in the config for module: " + this.name + ".";
|
||||
wrapper.className = "dimmed light small";
|
||||
return wrapper;
|
||||
@@ -203,10 +208,11 @@ Module.register("weatherforecast", {
|
||||
// Override getHeader method.
|
||||
getHeader: function () {
|
||||
if (this.config.appendLocationNameToHeader) {
|
||||
return this.data.header + " " + this.fetchedLocationName;
|
||||
if (this.data.header) return this.data.header + " " + this.fetchedLocationName;
|
||||
else return this.fetchedLocationName;
|
||||
}
|
||||
|
||||
return this.data.header;
|
||||
return this.data.header ? this.data.header : "";
|
||||
},
|
||||
|
||||
// Override notification handler.
|
||||
@@ -283,6 +289,8 @@ Module.register("weatherforecast", {
|
||||
var params = "?";
|
||||
if (this.config.locationID) {
|
||||
params += "id=" + this.config.locationID;
|
||||
} else if (this.config.lat && this.config.lon) {
|
||||
params += "lat=" + this.config.lat + "&lon=" + this.config.lon;
|
||||
} else if (this.config.location) {
|
||||
params += "q=" + this.config.location;
|
||||
} else if (this.firstEvent && this.firstEvent.geo) {
|
||||
@@ -294,8 +302,17 @@ Module.register("weatherforecast", {
|
||||
return;
|
||||
}
|
||||
|
||||
params += "&cnt=" + (this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 17 ? 7 : this.config.maxNumberOfDays);
|
||||
let numberOfDays;
|
||||
if (this.config.forecastEndpoint === "forecast") {
|
||||
numberOfDays = this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 5 ? 5 : this.config.maxNumberOfDays;
|
||||
// don't get forecasts for the next day, as it would not represent the whole day
|
||||
numberOfDays = numberOfDays * 8 - (Math.round(new Date().getHours() / 3) % 8);
|
||||
} else {
|
||||
numberOfDays = this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 17 ? 7 : this.config.maxNumberOfDays;
|
||||
}
|
||||
params += "&cnt=" + numberOfDays;
|
||||
|
||||
params += "&exclude=" + this.config.excludes;
|
||||
params += "&units=" + this.config.units;
|
||||
params += "&lang=" + this.config.lang;
|
||||
params += "&APPID=" + this.config.appid;
|
||||
@@ -323,24 +340,50 @@ Module.register("weatherforecast", {
|
||||
* argument data object - Weather information received form openweather.org.
|
||||
*/
|
||||
processWeather: function (data) {
|
||||
// Forcast16 (paid) API endpoint provides this data. Onecall endpoint
|
||||
// does not.
|
||||
if (data.city) {
|
||||
this.fetchedLocationName = data.city.name + ", " + data.city.country;
|
||||
} else if (this.config.location) {
|
||||
this.fetchedLocationName = this.config.location;
|
||||
} else {
|
||||
this.fetchedLocationName = "Unknown";
|
||||
}
|
||||
|
||||
this.forecast = [];
|
||||
var lastDay = null;
|
||||
var forecastData = {};
|
||||
var dayStarts = 8;
|
||||
var dayEnds = 17;
|
||||
|
||||
for (var i = 0, count = data.list.length; i < count; i++) {
|
||||
var forecast = data.list[i];
|
||||
this.parserDataWeather(forecast); // hack issue #1017
|
||||
if (data.city && data.city.sunrise && data.city.sunset) {
|
||||
dayStarts = new Date(moment.unix(data.city.sunrise).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours();
|
||||
dayEnds = new Date(moment.unix(data.city.sunset).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours();
|
||||
}
|
||||
|
||||
// Handle different structs between forecast16 and onecall endpoints
|
||||
var forecastList = null;
|
||||
if (data.list) {
|
||||
forecastList = data.list;
|
||||
} else if (data.daily) {
|
||||
forecastList = data.daily;
|
||||
} else {
|
||||
Log.error("Unexpected forecast data");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (var i = 0, count = forecastList.length; i < count; i++) {
|
||||
var forecast = forecastList[i];
|
||||
forecast = this.parserDataWeather(forecast); // hack issue #1017
|
||||
|
||||
var day;
|
||||
var hour;
|
||||
if (forecast.dt_txt) {
|
||||
day = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd");
|
||||
hour = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("H");
|
||||
hour = new Date(moment(forecast.dt_txt).locale("en").format("YYYY-MM-DD HH:mm:ss")).getHours();
|
||||
} else {
|
||||
day = moment(forecast.dt, "X").format("ddd");
|
||||
hour = moment(forecast.dt, "X").format("H");
|
||||
hour = new Date(moment(forecast.dt, "X")).getHours();
|
||||
}
|
||||
|
||||
if (day !== lastDay) {
|
||||
@@ -349,9 +392,8 @@ Module.register("weatherforecast", {
|
||||
icon: this.config.iconTable[forecast.weather[0].icon],
|
||||
maxTemp: this.roundValue(forecast.temp.max),
|
||||
minTemp: this.roundValue(forecast.temp.min),
|
||||
rain: this.processRain(forecast, data.list)
|
||||
rain: this.processRain(forecast, forecastList)
|
||||
};
|
||||
|
||||
this.forecast.push(forecastData);
|
||||
lastDay = day;
|
||||
|
||||
@@ -367,7 +409,7 @@ Module.register("weatherforecast", {
|
||||
|
||||
// Since we don't want an icon from the start of the day (in the middle of the night)
|
||||
// we update the icon as long as it's somewhere during the day.
|
||||
if (hour >= 8 && hour <= 17) {
|
||||
if (hour > dayStarts && hour < dayEnds) {
|
||||
forecastData.icon = this.config.iconTable[forecast.weather[0].icon];
|
||||
}
|
||||
}
|
||||
@@ -429,7 +471,8 @@ Module.register("weatherforecast", {
|
||||
*/
|
||||
roundValue: function (temperature) {
|
||||
var decimals = this.config.roundTemp ? 0 : 1;
|
||||
return parseFloat(temperature).toFixed(decimals);
|
||||
var roundValue = parseFloat(temperature).toFixed(decimals);
|
||||
return roundValue === "-0" ? 0 : roundValue;
|
||||
},
|
||||
|
||||
/* processRain(forecast, allForecasts)
|
||||
|
9018
package-lock.json
generated
88
package.json
@@ -1,24 +1,26 @@
|
||||
{
|
||||
"name": "magicmirror",
|
||||
"version": "2.12.0",
|
||||
"version": "2.15.0",
|
||||
"description": "The open source modular smart mirror platform.",
|
||||
"main": "js/electron.js",
|
||||
"scripts": {
|
||||
"start": "DISPLAY=\"${DISPLAY:=:0}\" ./node_modules/.bin/electron js/electron.js",
|
||||
"start:dev": "DISPLAY=\"${DISPLAY:=:0}\" ./node_modules/.bin/electron js/electron.js dev",
|
||||
"server": "node ./serveronly",
|
||||
"install": "echo \"Installing vendor files ...\n\" && cd vendor && npm install --loglevel=error",
|
||||
"install-fonts": "echo \"Installing fonts ...\n\" && cd fonts && npm install --loglevel=error",
|
||||
"postinstall": "npm run install-fonts && echo \"MagicMirror installation finished successfully! \n\"",
|
||||
"test": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests --recursive",
|
||||
"test:unit": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests/unit --recursive",
|
||||
"test:e2e": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests/e2e --recursive",
|
||||
"test": "NODE_ENV=test mocha tests --recursive",
|
||||
"test:coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text mocha tests --recursive --timeout=3000",
|
||||
"test:e2e": "NODE_ENV=test mocha tests/e2e --recursive",
|
||||
"test:unit": "NODE_ENV=test mocha tests/unit --recursive",
|
||||
"test:prettier": "prettier --check **/*.{js,css,json,md,yml}",
|
||||
"test:js": "eslint *.js js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --quiet",
|
||||
"test:js": "eslint js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --quiet",
|
||||
"test:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json",
|
||||
"test:calendar": "node ./modules/default/calendar/debug.js",
|
||||
"config:check": "node js/check_config.js",
|
||||
"lint:prettier": "prettier --write **/*.{js,css,json,md,yml}",
|
||||
"lint:js": "eslint *.js js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --fix",
|
||||
"lint:js": "eslint js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --fix",
|
||||
"lint:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json --fix"
|
||||
},
|
||||
"repository": {
|
||||
@@ -41,52 +43,56 @@
|
||||
},
|
||||
"homepage": "https://magicmirror.builders",
|
||||
"devDependencies": {
|
||||
"@prantlf/jsonlint": "^10.2.0",
|
||||
"chai": "^4.2.0",
|
||||
"chai": "^4.3.4",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"current-week-number": "^1.0.7",
|
||||
"danger": "^3.1.3",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"http-auth": "^3.2.3",
|
||||
"husky": "^4.2.5",
|
||||
"jsdom": "^11.6.2",
|
||||
"mocha": "^7.1.2",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-jsdoc": "^32.3.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"husky": "^4.3.8",
|
||||
"jsdom": "^16.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mocha": "^8.3.2",
|
||||
"mocha-each": "^2.0.1",
|
||||
"mocha-logger": "^1.0.6",
|
||||
"prettier": "^2.0.5",
|
||||
"pretty-quick": "^2.0.1",
|
||||
"spectron": "^8.0.0",
|
||||
"stylelint": "^13.6.1",
|
||||
"mocha-logger": "^1.0.7",
|
||||
"nyc": "^15.1.0",
|
||||
"prettier": "^2.2.1",
|
||||
"pretty-quick": "^3.1.0",
|
||||
"sinon": "^10.0.0",
|
||||
"spectron": "^13.0.0",
|
||||
"stylelint": "^13.12.0",
|
||||
"stylelint-config-prettier": "^8.0.2",
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"stylelint-prettier": "^1.1.2"
|
||||
"stylelint-config-standard": "^21.0.0",
|
||||
"stylelint-prettier": "^1.2.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"electron": "^6.1.7"
|
||||
"electron": "^11.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"colors": "^1.1.2",
|
||||
"console-stamp": "^0.2.9",
|
||||
"eslint": "^7.3.0",
|
||||
"express": "^4.16.2",
|
||||
"express-ipfilter": "^1.0.1",
|
||||
"feedme": "latest",
|
||||
"helmet": "^3.21.2",
|
||||
"ical": "^0.8.0",
|
||||
"iconv-lite": "latest",
|
||||
"lodash": "^4.17.15",
|
||||
"colors": "^1.4.0",
|
||||
"console-stamp": "^3.0.0-rc4.2",
|
||||
"digest-fetch": "^1.1.6",
|
||||
"eslint": "^7.23.0",
|
||||
"express": "^4.17.1",
|
||||
"express-ipfilter": "^1.1.2",
|
||||
"feedme": "^2.0.2",
|
||||
"helmet": "^4.4.1",
|
||||
"iconv-lite": "^0.6.2",
|
||||
"module-alias": "^2.2.2",
|
||||
"moment": "latest",
|
||||
"request": "^2.88.2",
|
||||
"rrule": "^2.6.2",
|
||||
"moment": "^2.29.1",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-ical": "^0.12.9",
|
||||
"rrule": "^2.6.8",
|
||||
"rrule-alt": "^2.2.8",
|
||||
"simple-git": "^1.85.0",
|
||||
"socket.io": "^2.1.1",
|
||||
"valid-url": "latest"
|
||||
"simple-git": "^2.37.0",
|
||||
"socket.io": "^4.0.0"
|
||||
},
|
||||
"_moduleAliases": {
|
||||
"node_helper": "js/node_helper.js"
|
||||
"node_helper": "js/node_helper.js",
|
||||
"logger": "js/logger.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
const app = require("../js/app.js");
|
||||
const Log = require("../js/logger.js");
|
||||
const Log = require("logger");
|
||||
|
||||
app.start(function (config) {
|
||||
var bindAddress = config.address ? config.address : "localhost";
|
||||
|
@@ -1,13 +0,0 @@
|
||||
{
|
||||
// Escaped
|
||||
"FOO\"BAR": "Today",
|
||||
|
||||
/*
|
||||
* The following lines
|
||||
* represent cardinal directions
|
||||
*/
|
||||
"N": "N",
|
||||
"E": "E",
|
||||
"S": "S",
|
||||
"W": "W"
|
||||
}
|
56
tests/configs/data/calendar_test_icons.ics
Normal file
@@ -0,0 +1,56 @@
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//ical.marudot.com//iCal Event Maker
|
||||
X-WR-CALNAME:TestEvents
|
||||
NAME:TestEvents
|
||||
CALSCALE:GREGORIAN
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Berlin
|
||||
TZURL:http://tzurl.org/zoneinfo-outlook/Europe/Berlin
|
||||
X-LIC-LOCATION:Europe/Berlin
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
TZNAME:CEST
|
||||
DTSTART:19700329T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
TZNAME:CET
|
||||
DTSTART:19701025T030000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20200719T094531Z
|
||||
UID:20200719T094531Z-1871115387@marudot.com
|
||||
DTSTART;TZID=Europe/Berlin:20300101T120000
|
||||
DTEND;TZID=Europe/Berlin:20300101T130000
|
||||
SUMMARY:TestEvent
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20200719T094531Z
|
||||
UID:20200719T094531Z-1929725136@marudot.com
|
||||
DTSTART;TZID=Europe/Berlin:20300701T120000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=7;BYMONTHDAY=1
|
||||
DTEND;TZID=Europe/Berlin:20300701T130000
|
||||
SUMMARY:TestEventRepeat
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20200719T094531Z
|
||||
UID:20200719T094531Z-371801474@marudot.com
|
||||
DTSTART;VALUE=DATE:20300401
|
||||
DTEND;VALUE=DATE:20300402
|
||||
SUMMARY:TestEventDay
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20200719T094531Z
|
||||
UID:20200719T094531Z-133401084@marudot.com
|
||||
DTSTART;VALUE=DATE:20301001
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYMONTHDAY=1
|
||||
DTEND;VALUE=DATE:20301002
|
||||
SUMMARY:TestEventRepeatDay
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,7 +26,7 @@ var config = {
|
||||
calendars: [
|
||||
{
|
||||
maximumNumberOfDays: 10000,
|
||||
url: "http://localhost:8011/tests/configs/data/calendar_test.ics",
|
||||
url: "http://localhost:8080/tests/configs/data/calendar_test.ics",
|
||||
auth: {
|
||||
user: "MagicMirror",
|
||||
pass: "CallMeADog"
|
||||
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,7 +26,7 @@ var config = {
|
||||
calendars: [
|
||||
{
|
||||
maximumNumberOfDays: 10000,
|
||||
url: "http://localhost:8010/tests/configs/data/calendar_test.ics",
|
||||
url: "http://localhost:8080/tests/configs/data/calendar_test.ics",
|
||||
auth: {
|
||||
user: "MagicMirror",
|
||||
pass: "CallMeADog",
|
||||
|
44
tests/configs/modules/calendar/changed-port.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/* Magic Mirror Test config default calendar with auth by default
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
module: "calendar",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
calendars: [
|
||||
{
|
||||
maximumNumberOfDays: 10000,
|
||||
url: "http://localhost:8010/tests/configs/data/calendar_test.ics",
|
||||
auth: {
|
||||
user: "MagicMirror",
|
||||
pass: "CallMeADog"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
42
tests/configs/modules/calendar/custom.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/* Magic Mirror Test config custom calendar
|
||||
*
|
||||
* MIT Licensed.
|
||||
*/
|
||||
let config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
module: "calendar",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
calendars: [
|
||||
{
|
||||
maximumEntries: 4,
|
||||
maximumNumberOfDays: 10000,
|
||||
symbol: "birthday-cake",
|
||||
fullDaySymbol: "calendar-day",
|
||||
recurringSymbol: "undo",
|
||||
url: "http://localhost:8080/tests/configs/data/calendar_test_icons.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
@@ -13,7 +13,8 @@ var config = {
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
|