mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-10-16 17:32:43 +00:00
Compare commits
429 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
5a4fbbf48a | ||
|
f7465679c0 | ||
|
2a5299ebcb | ||
|
b3bddb2c99 | ||
|
997aec8cc2 | ||
|
c67320f185 | ||
|
8224a6ac35 | ||
|
abcee8aa56 | ||
|
23c6b44921 | ||
|
1034171e91 | ||
|
bf49f79e6e | ||
|
f7f24dbdfe | ||
|
31ec848aec | ||
|
9eb08420b6 | ||
|
eb6d8d4f83 | ||
|
e10f620cf9 | ||
|
f750436b64 | ||
|
a4a8504558 | ||
|
385e5aabaa | ||
|
d831315e20 | ||
|
e0906f3462 | ||
|
6595b6a44f | ||
|
0183d7a080 | ||
|
89e803ee42 | ||
|
c0ce52abe3 | ||
|
a1c7f20990 | ||
|
0ef6f89d44 | ||
|
54b04962a8 | ||
|
60f8de282d | ||
|
b4350278a0 | ||
|
c3c5307624 | ||
|
879d585f2e | ||
|
9969fede35 | ||
|
c15b31b374 | ||
|
a3a6c33b32 | ||
|
236bf6e0fc | ||
|
974de179e0 | ||
|
60e03777f3 | ||
|
05f6b2510f | ||
|
f3274977f5 | ||
|
cf7fb1a3b9 | ||
|
12457a87d4 | ||
|
9a8de7db80 | ||
|
68e02a528e | ||
|
277055f44e | ||
|
c3fc745c7e | ||
|
8901ed219d | ||
|
d7c70dc021 | ||
|
53c789bff9 | ||
|
eb63745664 | ||
|
91d72e48ad | ||
|
1dcda63192 | ||
|
3ea6544f77 | ||
|
d12a587f11 | ||
|
2b147bb98b | ||
|
6529eaaf9a | ||
|
a68aa148b8 | ||
|
98942d6f9c | ||
|
690efc0aff | ||
|
627cfa1dff | ||
|
99aca932db | ||
|
dd43f35bbe | ||
|
093988e136 | ||
|
087a472765 | ||
|
ce13d7f98b | ||
|
b1fc766908 | ||
|
22384342db | ||
|
badce5146a | ||
|
0bf3ff9c17 | ||
|
860840c367 | ||
|
221b6325f6 | ||
|
06389e35f9 | ||
|
a7756cec13 | ||
|
9ee11654a6 | ||
|
a273266e5e | ||
|
e2158716d6 | ||
|
c132206543 | ||
|
f49312ed13 | ||
|
a9f69f07e6 | ||
|
d7429a4812 | ||
|
be76d5ce9a | ||
|
f2bc10c5c0 | ||
|
43eb760bce | ||
|
a7684e3e9f | ||
|
8949aa3bec | ||
|
e40ddd4b69 | ||
|
17637fb1f6 | ||
|
f71defe958 | ||
|
b8d6a6da1f | ||
|
fbc886b21c | ||
|
8879fb55de | ||
|
ed316e8bf3 | ||
|
45529f7de9 | ||
|
dbdff38d2e | ||
|
21c3179e03 | ||
|
c05d93aed8 | ||
|
6225abb010 | ||
|
c41fff8f5c | ||
|
8589d9c482 | ||
|
7f264953af | ||
|
cfff2ad72b | ||
|
c0258b352e | ||
|
3e1b051ec3 | ||
|
b34bb87d7a | ||
|
83b8cc6729 | ||
|
878c0be727 | ||
|
e7f06f5c0c | ||
|
a1fc38c5fe | ||
|
ff0ab24000 | ||
|
56a10d192d | ||
|
1a8413d8f0 | ||
|
934b156ebb | ||
|
f9639d9705 | ||
|
4c345c4f33 | ||
|
490151267a | ||
|
3d19a08cc7 | ||
|
385c4c32f9 | ||
|
3a5052c871 | ||
|
f84f590f1d | ||
|
5b9eba7819 | ||
|
cd18794fca | ||
|
ae3d552ad7 | ||
|
be5f71f4a7 | ||
|
745a5f0376 | ||
|
99114b2a61 | ||
|
df9bd2b0f9 | ||
|
e194b559ac | ||
|
af5344dccd | ||
|
2d7b8121d7 | ||
|
0297450702 | ||
|
6b17f6aa28 | ||
|
8a7abfe42d | ||
|
dd5041395c | ||
|
36d6a5bc15 | ||
|
2619f92d09 | ||
|
53720ae8ae | ||
|
bcff953fbb | ||
|
bcc0cc599d | ||
|
a1e3fed312 | ||
|
399dca2ef9 | ||
|
2e44e1626d | ||
|
39aa2dfe01 | ||
|
099929c677 | ||
|
af5d132410 | ||
|
79acbc3a98 | ||
|
e75e4e2284 | ||
|
9aa0af4f9c | ||
|
50e272efba | ||
|
209e049893 | ||
|
bbb3accf0c | ||
|
179989aa42 | ||
|
2881d19d43 | ||
|
7cfc3458ec | ||
|
659e1da79d | ||
|
a7ae79493d | ||
|
8b484ee707 | ||
|
d617d4aa09 | ||
|
99c04648b4 | ||
|
f945d50c0d | ||
|
e9fabd59ed | ||
|
ad13de3588 | ||
|
aad8141e27 | ||
|
26a76f80d6 | ||
|
53ead2087f | ||
|
faee811d67 | ||
|
b9c739df1f | ||
|
6e124842e8 | ||
|
7a5928ea24 | ||
|
7fdf7de11c | ||
|
b75eedb84e | ||
|
3b92ae49a9 | ||
|
775d1091db | ||
|
1f77b491fc | ||
|
eff2fd7cc0 | ||
|
58d2a0d874 | ||
|
ea9def997a | ||
|
a222c58047 | ||
|
39a838c2ab | ||
|
cfc0bcd5ad | ||
|
3418c9b50f | ||
|
e686611890 | ||
|
ebb5dee1fc | ||
|
d9edaffd9c | ||
|
cbe7b1a5b9 | ||
|
e758fd4093 | ||
|
14a99a3b25 | ||
|
1ba67506a0 | ||
|
2a6ca5d5ac | ||
|
aa12e6495a | ||
|
9269848f66 | ||
|
a71e61cd30 | ||
|
8be4604c97 | ||
|
a7bba903f5 | ||
|
1d8af5835d | ||
|
189c01fc74 | ||
|
e3a5bbf661 | ||
|
ee23c5f72c | ||
|
a2083be76b | ||
|
a1c4be83d6 | ||
|
d2fde2bfc8 | ||
|
e8956b0b55 | ||
|
2af4009a93 | ||
|
a5f7c946cc | ||
|
298542b531 | ||
|
fca6707a29 | ||
|
10d3a284e9 | ||
|
1a244726aa | ||
|
044935a164 | ||
|
99e5edf2c5 | ||
|
b26270bd13 | ||
|
65a8cb9ddb | ||
|
ba4b976e80 | ||
|
297ae1dbaf | ||
|
214614f740 | ||
|
0e14d3d6e8 | ||
|
67011c0c32 | ||
|
16bbb42b8d | ||
|
b85ac91e6c | ||
|
66759a33fa | ||
|
bf5e83861c | ||
|
d56a6fb06f | ||
|
5e7aa8e16d | ||
|
bace0ad339 | ||
|
6014eaf8eb | ||
|
3e96e8b3f5 | ||
|
95d1b8a6d0 | ||
|
0ecb66c99e | ||
|
af52b91799 | ||
|
3c50d6c30a | ||
|
2722c72c43 | ||
|
d5ab3101c6 | ||
|
32df76bdff | ||
|
68a06c3d1d | ||
|
1b42dc779b | ||
|
cedffd40f2 | ||
|
cdc8db4837 | ||
|
a0ee23d84e | ||
|
49d2d8c9d0 | ||
|
63620aa811 | ||
|
d5b11a1dba | ||
|
4a63af0490 | ||
|
a68019293f | ||
|
6b1c91f0dd | ||
|
ea93785581 | ||
|
32819c4fd5 | ||
|
fc5a438cdc | ||
|
57fe94f945 | ||
|
aa3a3bdf16 | ||
|
db89da3daa | ||
|
d6ba5796ce | ||
|
3968743b28 | ||
|
20c6226b84 | ||
|
463ce394fe | ||
|
1faefebe42 | ||
|
e2c9339ec4 | ||
|
4b1c7da171 | ||
|
bdfd6e5e9f | ||
|
4c8508b0a9 | ||
|
06b3f92963 | ||
|
d43a57af36 | ||
|
aeefe28710 | ||
|
e9de961a23 | ||
|
dcec778e02 | ||
|
b212641069 | ||
|
90aa50bb11 | ||
|
a6879e853b | ||
|
37fab7ac63 | ||
|
8b01ae08c5 | ||
|
b1cdf42790 | ||
|
974968d238 | ||
|
bf467cbba5 | ||
|
536aa2e96e | ||
|
3d84344b75 | ||
|
1054ba3b1e | ||
|
aa8ddb9a92 | ||
|
fcfe57e5e2 | ||
|
c4fd4e0317 | ||
|
3c76933824 | ||
|
fa83819bee | ||
|
b65ae88879 | ||
|
96db21f9bf | ||
|
6d356ff770 | ||
|
21790b32bf | ||
|
159f3d0aa2 | ||
|
012a7b0678 | ||
|
6595c85671 | ||
|
cf5c0464fe | ||
|
e31450f731 | ||
|
3653984a95 | ||
|
0c0b856c37 | ||
|
69f1b153ea | ||
|
43ba4bd00e | ||
|
bf5edcaac6 | ||
|
ac51709211 | ||
|
e1a578e819 | ||
|
591c9e53b0 | ||
|
87d543eb3a | ||
|
40c1521591 | ||
|
b31c2a6264 | ||
|
66a42f13f1 | ||
|
8de6ebbbd1 | ||
|
649de694ed | ||
|
8a52fde8fc | ||
|
fb8bd657de | ||
|
b04a0a6b61 | ||
|
f29c911a0f | ||
|
bd908123c2 | ||
|
cbdb0b67ab | ||
|
aa3848f420 | ||
|
6cf0748172 | ||
|
11122d3f81 | ||
|
2dea9398f2 | ||
|
de93b3294f | ||
|
7accb84eb9 | ||
|
ea90ed04d6 | ||
|
838eed2630 | ||
|
376b65c749 | ||
|
3b4432cb00 | ||
|
7bc71029de | ||
|
d736dd92be | ||
|
6eba8d681c | ||
|
5fe654c19d | ||
|
dd366f35a8 | ||
|
2ababa521d | ||
|
bda8f26511 | ||
|
7f1a3df25b | ||
|
ef2ff50089 | ||
|
0b3964c827 | ||
|
ccf5bb9342 | ||
|
4303882c6a | ||
|
c34028d549 | ||
|
8e76cdcb57 | ||
|
5eb66106b9 | ||
|
0abebc1e32 | ||
|
552e82f44d | ||
|
ada40e36db | ||
|
480f734a06 | ||
|
20bee9c334 | ||
|
de8267f41e | ||
|
7bfaf07980 | ||
|
a42fa8e9f9 | ||
|
5facad683a | ||
|
fd1913a72e | ||
|
54c98b4250 | ||
|
5d99baac21 | ||
|
cb95bdf6d7 | ||
|
25e803abfc | ||
|
7c6073e4ef | ||
|
09bcbe8dfc | ||
|
e0a9c7d0bb | ||
|
ac27d05fd5 | ||
|
d6ab56252f | ||
|
dab178ed50 | ||
|
6ed50b6a75 | ||
|
ee559ec650 | ||
|
4310238418 | ||
|
10c47a6c38 | ||
|
4b8043086e | ||
|
fd952b88bf | ||
|
e262d463c5 | ||
|
b02bce2510 | ||
|
58094531c0 | ||
|
d937e3ca7c | ||
|
9c58413209 | ||
|
db65ff12d7 | ||
|
f93b819ea6 | ||
|
e6fea297e5 | ||
|
5c2a0e5634 | ||
|
71aa21a82d | ||
|
23821360c7 | ||
|
79f5b938f5 | ||
|
f8769fcc2a | ||
|
a3ed24c766 | ||
|
82727b825c | ||
|
16e98496af | ||
|
62896ce1a3 | ||
|
7ea5b1ecbf | ||
|
eecc95f8fb | ||
|
85808d85c4 | ||
|
12cc670642 | ||
|
13fcb55df6 | ||
|
a4dfd15888 | ||
|
7fbd326298 | ||
|
331d147d50 | ||
|
acdcdc55bc | ||
|
fdb0c0acb3 | ||
|
564bf47fb2 | ||
|
eca35b2371 | ||
|
f97d2e2644 | ||
|
fdd6659139 | ||
|
0d698fb659 | ||
|
c68b39dda8 | ||
|
cb67286bc3 | ||
|
a49962b8de | ||
|
256d5ae14f | ||
|
799ee8bcfa | ||
|
fe8a317ef9 | ||
|
b6d6ee45e0 | ||
|
0151466a28 | ||
|
3588875e28 | ||
|
e8031aec39 | ||
|
1cabe107e6 | ||
|
ba0c59744b | ||
|
50c702c00a | ||
|
ff8c2fe227 | ||
|
f9c78a5263 | ||
|
d46784a4d5 | ||
|
ff43d082f2 | ||
|
83801736d7 | ||
|
e16986cf71 | ||
|
7ad12d954c | ||
|
fb74fadec2 | ||
|
c00bdf910e | ||
|
50cc1c56e5 | ||
|
0006099758 | ||
|
162dcec331 | ||
|
3e8bd022e2 | ||
|
01e5936671 | ||
|
d85e1c70d6 | ||
|
f329770194 | ||
|
08194925cb | ||
|
2b7aa3e810 | ||
|
549417f106 | ||
|
f0f370cc7d | ||
|
b842241f8c | ||
|
93aa4b7440 | ||
|
9e2eef7818 | ||
|
b29b65851e | ||
|
a6829bff4f |
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"],
|
||||
"plugins": ["prettier", "jsdoc"],
|
||||
"plugins": ["prettier", "jsdoc", "jest"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true,
|
||||
"jest/globals": true,
|
||||
"node": true
|
||||
},
|
||||
"globals": {
|
||||
@@ -25,6 +25,7 @@
|
||||
"prettier/prettier": "error",
|
||||
"eqeqeq": "error",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-unused-vars": "off"
|
||||
"no-unused-vars": "off",
|
||||
"no-useless-return": "error"
|
||||
}
|
||||
}
|
||||
|
6
.github/CONTRIBUTING.md
vendored
6
.github/CONTRIBUTING.md
vendored
@@ -18,7 +18,7 @@ To run ESLint, use `npm run lint:js`.
|
||||
|
||||
We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is in our .stylelintrc file.
|
||||
|
||||
To run StyleLint, use `npm run lint:style`.
|
||||
To run StyleLint, use `npm run lint:css`.
|
||||
|
||||
### Submitting Issues
|
||||
|
||||
@@ -32,9 +32,9 @@ 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/4, Windows, Mac, Linux, System V UNIX).
|
||||
|
||||
**Node Version**: Make sure it's version 10 or later.
|
||||
**Node Version**: Make sure it's version 12 or later (recommended is 14).
|
||||
|
||||
**MagicMirror Version**: Now that the versions have split, tell us if you are using the PHP version (v1) or the newer JavaScript version (v2).
|
||||
**MagicMirror Version**: Please let us know which version of MagicMirror you are running. It can be found in the `package.json` file.
|
||||
|
||||
**Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem.
|
||||
|
||||
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
||||
github: MichMich
|
||||
custom: ['https://magicmirror.builders/#donate']
|
||||
custom: ["https://magicmirror.builders/#donate"]
|
||||
|
14
.github/ISSUE_TEMPLATE.md
vendored
14
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,3 +1,7 @@
|
||||
Hello and thank you for opening an issue.
|
||||
|
||||
**Please make sure that you have read the following lines before submitting your Issue:**
|
||||
|
||||
## I'm not sure if this is a bug
|
||||
|
||||
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)
|
||||
@@ -15,8 +19,10 @@ If you are facing an issue or found a bug while trying to install MagicMirror vi
|
||||
|
||||
## I found a bug in the MagicMirror Docker image
|
||||
|
||||
If you are facing an issue or found a bug while running MagicMirror inside a Docker container please create an issue in the GitHub repository of the MagicMirror Docker image:
|
||||
[https://github.com/bastilimbach/docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror)
|
||||
If you are facing an issue or found a bug while running MagicMirror inside a Docker container please create an issue in the corresponding repository:
|
||||
|
||||
- karsten13/magicmirror: [https://gitlab.com/khassel/magicmirror](https://gitlab.com/khassel/magicmirror)
|
||||
- (deprecated) bastilimbach/docker-magicmirror: [https://github.com/bastilimbach/docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror)
|
||||
|
||||
---
|
||||
|
||||
@@ -27,9 +33,9 @@ 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/4, Windows, Mac, Linux, System V UNIX).
|
||||
|
||||
**Node Version**: Make sure it's version 10 or later.
|
||||
**Node Version**: Make sure it's version 12 or later (recommended is 14).
|
||||
|
||||
**MagicMirror Version**: Please let us now which version of MagicMirror you are running. It can be found in the `package.log` file.
|
||||
**MagicMirror Version**: Please let us know which version of MagicMirror you are running. It can be found in the `package.json` file.
|
||||
|
||||
**Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem.
|
||||
|
||||
|
23
.github/PULL_REQUEST_TEMPLATE.md
vendored
23
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -2,23 +2,20 @@ 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.
|
||||
> 1. Base your pull requests against the `develop` branch.
|
||||
>
|
||||
> 2. Include these infos in the description:
|
||||
>
|
||||
> 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.
|
||||
> - 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.
|
||||
>
|
||||
> 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.
|
||||
|
||||
> 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
|
||||
|
6
.github/codecov.yml
vendored
Normal file
6
.github/codecov.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# advanced settings
|
||||
informational: true
|
BIN
.github/header.psd
vendored
BIN
.github/header.psd
vendored
Binary file not shown.
37
.github/workflows/automated-tests.yml
vendored
Normal file
37
.github/workflows/automated-tests.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
# 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
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x, 14.x, 16.x]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies and run tests
|
||||
run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
npm install
|
||||
touch css/custom.css
|
||||
npm run test:prettier
|
||||
npm run test:js
|
||||
npm run test:css
|
||||
npm run test:unit
|
||||
npm run test:e2e
|
||||
npm run test:electron
|
15
.github/workflows/codecov-test-suites.yml
vendored
15
.github/workflows/codecov-test-suites.yml
vendored
@@ -4,21 +4,26 @@ name: "Run Codecov Tests"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, develop ]
|
||||
branches: [master, develop]
|
||||
pull_request:
|
||||
branches: [ master, develop ]
|
||||
branches: [master, develop]
|
||||
|
||||
jobs:
|
||||
run-and-upload-coverage-report:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: |
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install dependencies and run coverage
|
||||
run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
npm ci
|
||||
touch css/custom.css
|
||||
npm run test:coverage
|
||||
- uses: codecov/codecov-action@v1
|
||||
- name: Upload coverage results to codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
fail_ci_if_error: true
|
||||
|
15
.github/workflows/enforce-changelog.yml
vendored
15
.github/workflows/enforce-changelog.yml
vendored
@@ -4,14 +4,17 @@ name: "Enforce Changelog"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: dangoslen/changelog-enforcer@v1.6.1
|
||||
with:
|
||||
changeLogPath: 'CHANGELOG.md'
|
||||
skipLabels: 'Skip Changelog'
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Enforce changelog️
|
||||
uses: dangoslen/changelog-enforcer@v1.6.1
|
||||
with:
|
||||
changeLogPath: "CHANGELOG.md"
|
||||
skipLabels: "Skip Changelog"
|
||||
|
32
.github/workflows/node-ci.js.yml
vendored
32
.github/workflows/node-ci.js.yml
vendored
@@ -1,32 +0,0 @@
|
||||
# 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
|
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint:staged
|
@@ -1,5 +1,4 @@
|
||||
/config
|
||||
/coverage
|
||||
.nyc_output
|
||||
package-lock.json
|
||||
/config/**/*
|
||||
/vendor/**/*
|
||||
!/vendor/vendor.js
|
||||
.github/**/*
|
||||
|
88
CHANGELOG.md
88
CHANGELOG.md
@@ -5,6 +5,93 @@ 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.17.0] - 2021-10-01
|
||||
|
||||
Special thanks to the following contributors: @apiontek, @eouia, @jupadin, @khassel and @rejas.
|
||||
|
||||
### Added
|
||||
|
||||
- Added showTime parameter to clock module for enabling/disabling time display in analog clock.
|
||||
- Added custom electron switches from user config (`config.electronSwitches`).
|
||||
- Added unit tests for updatenotification module.
|
||||
|
||||
### Updated
|
||||
|
||||
- Bump electron to v13 (and spectron to v15) and update other dependencies in package.json.
|
||||
- Refactor test configs, use default test config for all tests.
|
||||
- Updated github templates.
|
||||
- Actually test all js and css files when lint script is run.
|
||||
- Update jsdocs and print warnings during testing too.
|
||||
- Update weathergov provider to try fetching not just current, but also foreacst, when API URLs available.
|
||||
- Refactored clock layout.
|
||||
- Refactored methods from weatherproviders into weatherobject (isDaytime, updateSunTime).
|
||||
- Use of `logger.js` in jest tests.
|
||||
- Run prettier over all relevant files.
|
||||
- Move tests needing electron in new category `electron`, use `server only` mode in `e2e` tests.
|
||||
- Update dependencies in package.json.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix undefined error with ignoreToday option in weather module (#2620).
|
||||
- Fix time zone correction in calendar module when the date hour is equal to the time zone correction value (#2632).
|
||||
- Fix black cursor on startup when using electron.
|
||||
- Fix update notification not working for own repository (#2644).
|
||||
|
||||
## [2.16.0] - 2021-07-01
|
||||
|
||||
Special thanks to the following contributors: @210954, @B1gG, @codac, @Crazylegstoo, @daniel, @earlman, @ezeholz, @FrancoisRmn, @jupadin, @khassel, @KristjanESPERANTO, @njwilliams, @oemel09, @r3wald, @rejas, @rico24, Faizan Ahmed.
|
||||
|
||||
### Added
|
||||
|
||||
- Added French translations for "MODULE_CONFIG_ERROR" and "PRECIP".
|
||||
- Added German translation for "PRECIP".
|
||||
- Added Dutch translation for "WEEK", "PRECIP", "MODULE_CONFIG_CHANGED" and "MODULE_CONFIG_ERROR".
|
||||
- Added first test for Alert module.
|
||||
- Added support for `dateFormat` when not using `timeFormat: "absolute"`.
|
||||
- Added custom-properties for colors and fonts for improved styling experience, see `custom.css.sample` file.
|
||||
- Added custom-properties for gaps around body and between modules.
|
||||
- Added test case for recurring calendar events.
|
||||
- Added new Environment Canada provider for default WEATHER module (weather data for Canadian locations only).
|
||||
- Added list view for newsfeed module.
|
||||
- Added dev dependency jest, switching from mocha to jest.
|
||||
|
||||
### Updated
|
||||
|
||||
- Bump node-ical to v0.13.0 (now last runtime dependency using deprecated `request` package is removed).
|
||||
- Use codecov in informational mode.
|
||||
- Refactor code into es6 where possible (e.g. var -> let/const).
|
||||
- Use node v16 in github workflow (replacing node v10).
|
||||
- Moved some files into better suited directories.
|
||||
- Update dependencies in package.json, require node >= v12, remove `rrule-alt` and `rrule`.
|
||||
- Update dependencies in package.json and migrate husky to v6, fix husky setup in prod environment.
|
||||
- Cleaned up error handling in newsfeed and calendar modules for real.
|
||||
- Updated default WEATHER module such that a provider can optionally set a custom unit-of-measure for precipitation (`weatherObject.precipitationUnits`).
|
||||
- Update documentation.
|
||||
- Update jest tests: Reset changes on js/logger.js, mock logger.js in global_vars tests.
|
||||
- Update dependencies in package.json.
|
||||
|
||||
### Removed
|
||||
|
||||
- Switching from mocha to jest so removed following dev dependencies: chai, chai-as-promised, mocha, mocha-each, mocha-logger.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix calendar start function logging inconsistency.
|
||||
- Fix updatenotification start function logging inconsistency.
|
||||
- Checks and applies the showDescription setting for the newsfeed module again.
|
||||
- Fix issue with openweathermap not showing current or forecast info when using onecall API.
|
||||
- Fix tests in weather module and add one for decimalPoint in forecast.
|
||||
- Fix decimalSymbol in the forecast part of the new weather module (#2530).
|
||||
- Fix wrong treatment of `appendLocationNameToHeader` when using `ukmetofficedatahub`.
|
||||
- Fix alert not recognizing multiple alerts (#2522).
|
||||
- Fix fetch option httpsAgent to agent in calendar module (#466).
|
||||
- Fix module updatenotification which did not work for repos with many refs (#1907).
|
||||
- Fix config check failing when encountering let syntax ("Parsing error: Unexpected token config").
|
||||
- Fix calendar debug check.
|
||||
- Really run prettier over all files.
|
||||
- Fix logger.js after jest changes, use --forceExit running jest.
|
||||
- Workaround for dev_console test using getWindowCount.
|
||||
|
||||
## [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.
|
||||
@@ -357,6 +444,7 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
- Update `ical.js` to solve various calendar issues.
|
||||
- Update weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676)
|
||||
- Only update clock once per minute when seconds aren't shown
|
||||
- Update weatherprovider documentation.
|
||||
|
||||
### Fixed
|
||||
|
||||
|
@@ -7,7 +7,6 @@
|
||||
<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://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).
|
||||
@@ -23,6 +22,7 @@ For the full documentation including **[installation instructions](https://docs.
|
||||
- Website: [https://magicmirror.builders](https://magicmirror.builders)
|
||||
- Documentation: [https://docs.magicmirror.builders](https://docs.magicmirror.builders)
|
||||
- Forum: [https://forum.magicmirror.builders](https://forum.magicmirror.builders)
|
||||
- Technical discussions: https://forum.magicmirror.builders/category/11/core-system
|
||||
- Discord: [https://discord.gg/J5BAtvx](https://discord.gg/J5BAtvx)
|
||||
- Blog: [https://michaelteeuw.nl/tagged/magicmirror](https://michaelteeuw.nl/tagged/magicmirror)
|
||||
- Donations: [https://magicmirror.builders/#donate](https://magicmirror.builders/#donate)
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
// Use separate scope to prevent global scope pollution
|
||||
(function () {
|
||||
var config = {};
|
||||
const config = {};
|
||||
|
||||
/**
|
||||
* Helper function to get server address/hostname from either the commandline or env
|
||||
@@ -17,8 +17,8 @@
|
||||
* @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;
|
||||
const index = process.argv.indexOf(`--${key}`);
|
||||
const value = index > -1 ? process.argv[index + 1] : undefined;
|
||||
return value !== undefined ? String(value) : defaultValue;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
// 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 = "";
|
||||
let configData = "";
|
||||
|
||||
// Gather incoming data
|
||||
response.on("data", function (chunk) {
|
||||
@@ -79,15 +79,15 @@
|
||||
getServerAddress();
|
||||
|
||||
(config.address && config.port) || fail();
|
||||
var prefix = config.tls ? "https://" : "http://";
|
||||
const prefix = config.tls ? "https://" : "http://";
|
||||
|
||||
// Only start the client if a non-local server was provided
|
||||
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) === -1) {
|
||||
getServerConfig(`${prefix}${config.address}:${config.port}/config/`)
|
||||
.then(function (configReturn) {
|
||||
// Pass along the server config via an environment variable
|
||||
var env = Object.create(process.env);
|
||||
var options = { env: env };
|
||||
const env = Object.create(process.env);
|
||||
const options = { env: env };
|
||||
configReturn.address = config.address;
|
||||
configReturn.port = config.port;
|
||||
configReturn.tls = config.tls;
|
||||
|
@@ -4,11 +4,10 @@
|
||||
* MIT Licensed.
|
||||
*
|
||||
* For more information on how you can configure this file
|
||||
* See https://github.com/MichMich/MagicMirror#configuration
|
||||
*
|
||||
* see https://docs.magicmirror.builders/getting-started/configuration.html#general
|
||||
* and https://docs.magicmirror.builders/modules/configuration.html
|
||||
*/
|
||||
|
||||
var config = {
|
||||
let config = {
|
||||
address: "localhost", // Address to listen on, can be:
|
||||
// - "localhost", "127.0.0.1", "::1" to listen on loopback interface
|
||||
// - another specific IPv4/6 to listen on a specific interface
|
||||
@@ -58,7 +57,8 @@ var config = {
|
||||
calendars: [
|
||||
{
|
||||
symbol: "calendar-check",
|
||||
url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics" }
|
||||
url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
31
css/custom.css.sample
Normal file
31
css/custom.css.sample
Normal file
@@ -0,0 +1,31 @@
|
||||
/* Magic Mirror Custom CSS Sample
|
||||
*
|
||||
* Change color and fonts here.
|
||||
*
|
||||
* Beware that properties cannot be unitless, so for example write '--gap-body: 0px;' instead of just '--gap-body: 0;'
|
||||
*
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
/* Uncomment and adjust accordingly if you want to import another font from the google-fonts-api: */
|
||||
/* @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;300;400;700&display=swap'); */
|
||||
|
||||
:root {
|
||||
--color-text: #999;
|
||||
--color-text-dimmed: #666;
|
||||
--color-text-bright: #fff;
|
||||
--color-background: black;
|
||||
|
||||
--font-primary: "Roboto Condensed";
|
||||
--font-secondary: "Roboto";
|
||||
|
||||
--font-size: 20px;
|
||||
--font-size-small: 0.75rem;
|
||||
|
||||
--gap-body-top: 60px;
|
||||
--gap-body-right: 60px;
|
||||
--gap-body-bottom: 60px;
|
||||
--gap-body-left: 60px;
|
||||
|
||||
--gap-modules: 30px;
|
||||
}
|
108
css/main.css
108
css/main.css
@@ -1,8 +1,29 @@
|
||||
:root {
|
||||
--color-text: #999;
|
||||
--color-text-dimmed: #666;
|
||||
--color-text-bright: #fff;
|
||||
--color-background: #000;
|
||||
|
||||
--font-primary: "Roboto Condensed";
|
||||
--font-secondary: "Roboto";
|
||||
|
||||
--font-size: 20px;
|
||||
--font-size-small: 0.75rem;
|
||||
|
||||
--gap-body-top: 60px;
|
||||
--gap-body-right: 60px;
|
||||
--gap-body-bottom: 60px;
|
||||
--gap-body-left: 60px;
|
||||
|
||||
--gap-modules: 30px;
|
||||
}
|
||||
|
||||
html {
|
||||
cursor: none;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
background: var(--color-background);
|
||||
user-select: none;
|
||||
font-size: var(--font-size);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@@ -10,16 +31,15 @@ html {
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 60px;
|
||||
margin: var(--gap-body-top) var(--gap-body-right) var(--gap-body-bottom) var(--gap-body-left);
|
||||
position: absolute;
|
||||
height: calc(100% - 120px);
|
||||
width: calc(100% - 120px);
|
||||
background: #000;
|
||||
color: #aaa;
|
||||
font-family: "Roboto Condensed", sans-serif;
|
||||
height: calc(100% - var(--gap-body-top) - var(--gap-body-bottom));
|
||||
width: calc(100% - var(--gap-body-right) - var(--gap-body-left));
|
||||
background: var(--color-background);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-primary), sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 2em;
|
||||
line-height: 1.5em;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
@@ -28,60 +48,60 @@ body {
|
||||
*/
|
||||
|
||||
.dimmed {
|
||||
color: #666;
|
||||
color: var(--color-text-dimmed);
|
||||
}
|
||||
|
||||
.normal {
|
||||
color: #999;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.bright {
|
||||
color: #fff;
|
||||
color: var(--color-text-bright);
|
||||
}
|
||||
|
||||
.xsmall {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
font-size: var(--font-size-small);
|
||||
line-height: 1.275;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 20px;
|
||||
line-height: 25px;
|
||||
font-size: 1rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.medium {
|
||||
font-size: 30px;
|
||||
line-height: 35px;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.225;
|
||||
}
|
||||
|
||||
.large {
|
||||
font-size: 65px;
|
||||
line-height: 65px;
|
||||
font-size: 3.25rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.xlarge {
|
||||
font-size: 75px;
|
||||
line-height: 75px;
|
||||
font-size: 3.75rem;
|
||||
line-height: 1;
|
||||
letter-spacing: -3px;
|
||||
}
|
||||
|
||||
.thin {
|
||||
font-family: Roboto, sans-serif;
|
||||
font-family: var(--font-secondary), sans-serif;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.light {
|
||||
font-family: "Roboto Condensed", sans-serif;
|
||||
font-family: var(--font-primary), sans-serif;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.regular {
|
||||
font-family: "Roboto Condensed", sans-serif;
|
||||
font-family: var(--font-primary), sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-family: "Roboto Condensed", sans-serif;
|
||||
font-family: var(--font-primary), sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -95,14 +115,14 @@ body {
|
||||
|
||||
header {
|
||||
text-transform: uppercase;
|
||||
font-size: 15px;
|
||||
font-family: "Roboto Condensed", Arial, Helvetica, sans-serif;
|
||||
font-size: var(--font-size-small);
|
||||
font-family: var(--font-primary), Arial, Helvetica, sans-serif;
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid #666;
|
||||
border-bottom: 1px solid var(--color-text-dimmed);
|
||||
line-height: 15px;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 10px;
|
||||
color: #999;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
sup {
|
||||
@@ -115,11 +135,11 @@ sup {
|
||||
*/
|
||||
|
||||
.module {
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: var(--gap-modules);
|
||||
}
|
||||
|
||||
.region.bottom .module {
|
||||
margin-top: 30px;
|
||||
margin-top: var(--gap-modules);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -143,10 +163,10 @@ sup {
|
||||
|
||||
.region.fullscreen {
|
||||
position: absolute;
|
||||
top: -60px;
|
||||
left: -60px;
|
||||
right: -60px;
|
||||
bottom: -60px;
|
||||
top: calc(-1 * var(--gap-body-top));
|
||||
left: calc(-1 * var(--gap-body-left));
|
||||
right: calc(-1 * var(--gap-body-right));
|
||||
bottom: calc(-1 * var(--gap-body-bottom));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -163,18 +183,6 @@ sup {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.region.top .container {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.region.bottom .container {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.region.top .container:empty {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.region.top.center,
|
||||
.region.bottom.center {
|
||||
left: 50%;
|
||||
@@ -191,10 +199,6 @@ sup {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.region.bottom .container:empty {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.region.bottom.right,
|
||||
.region.bottom.center,
|
||||
.region.bottom.left {
|
||||
|
34
fonts/package-lock.json
generated
34
fonts/package-lock.json
generated
@@ -1,12 +1,26 @@
|
||||
{
|
||||
"name": "magicmirror-fonts",
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"roboto-fontface": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz",
|
||||
"integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g=="
|
||||
}
|
||||
}
|
||||
"name": "magicmirror-fonts",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "magicmirror-fonts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"roboto-fontface": "^0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/roboto-fontface": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz",
|
||||
"integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g=="
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"roboto-fontface": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz",
|
||||
"integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
100
index.html
100
index.html
@@ -1,55 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>MagicMirror²</title>
|
||||
<meta name="google" content="notranslate" />
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<head>
|
||||
<title>MagicMirror²</title>
|
||||
<meta name="google" content="notranslate" />
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
|
||||
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
|
||||
<link rel="stylesheet" type="text/css" href="css/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="fonts/roboto.css">
|
||||
<!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. -->
|
||||
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
|
||||
<link rel="stylesheet" type="text/css" href="css/main.css" />
|
||||
<link rel="stylesheet" type="text/css" href="fonts/roboto.css" />
|
||||
<!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. -->
|
||||
|
||||
<script type="text/javascript">
|
||||
var version = "#VERSION#";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="region fullscreen below"><div class="container"></div></div>
|
||||
<div class="region top bar">
|
||||
<div class="container"></div>
|
||||
<div class="region top left"><div class="container"></div></div>
|
||||
<div class="region top center"><div class="container"></div></div>
|
||||
<div class="region top right"><div class="container"></div></div>
|
||||
</div>
|
||||
<div class="region upper third"><div class="container"></div></div>
|
||||
<div class="region middle center"><div class="container"></div></div>
|
||||
<div class="region lower third"><div class="container"><br/></div></div>
|
||||
<div class="region bottom bar">
|
||||
<div class="container"></div>
|
||||
<div class="region bottom left"><div class="container"></div></div>
|
||||
<div class="region bottom center"><div class="container"></div></div>
|
||||
<div class="region bottom right"><div class="container"></div></div>
|
||||
</div>
|
||||
<div class="region fullscreen above"><div class="container"></div></div>
|
||||
<script type="text/javascript" src="socket.io/socket.io.js"></script>
|
||||
<script type="text/javascript" src="vendor/node_modules/nunjucks/browser/nunjucks.min.js"></script>
|
||||
<script type="text/javascript" src="js/defaults.js"></script>
|
||||
<script type="text/javascript" src="#CONFIG_FILE#"></script>
|
||||
<script type="text/javascript" src="vendor/vendor.js"></script>
|
||||
<script type="text/javascript" src="modules/default/defaultmodules.js"></script>
|
||||
<script type="text/javascript" src="js/logger.js"></script>
|
||||
<script type="text/javascript" src="translations/translations.js"></script>
|
||||
<script type="text/javascript" src="js/translator.js"></script>
|
||||
<script type="text/javascript" src="js/class.js"></script>
|
||||
<script type="text/javascript" src="js/module.js"></script>
|
||||
<script type="text/javascript" src="js/loader.js"></script>
|
||||
<script type="text/javascript" src="js/socketclient.js"></script>
|
||||
<script type="text/javascript" src="js/main.js"></script>
|
||||
</body>
|
||||
<script type="text/javascript">
|
||||
window.mmVersion = "#VERSION#";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="region fullscreen below"><div class="container"></div></div>
|
||||
<div class="region top bar">
|
||||
<div class="container"></div>
|
||||
<div class="region top left"><div class="container"></div></div>
|
||||
<div class="region top center"><div class="container"></div></div>
|
||||
<div class="region top right"><div class="container"></div></div>
|
||||
</div>
|
||||
<div class="region upper third"><div class="container"></div></div>
|
||||
<div class="region middle center"><div class="container"></div></div>
|
||||
<div class="region lower third">
|
||||
<div class="container"><br /></div>
|
||||
</div>
|
||||
<div class="region bottom bar">
|
||||
<div class="container"></div>
|
||||
<div class="region bottom left"><div class="container"></div></div>
|
||||
<div class="region bottom center"><div class="container"></div></div>
|
||||
<div class="region bottom right"><div class="container"></div></div>
|
||||
</div>
|
||||
<div class="region fullscreen above"><div class="container"></div></div>
|
||||
<script type="text/javascript" src="socket.io/socket.io.js"></script>
|
||||
<script type="text/javascript" src="vendor/node_modules/nunjucks/browser/nunjucks.min.js"></script>
|
||||
<script type="text/javascript" src="js/defaults.js"></script>
|
||||
<script type="text/javascript" src="#CONFIG_FILE#"></script>
|
||||
<script type="text/javascript" src="vendor/vendor.js"></script>
|
||||
<script type="text/javascript" src="modules/default/defaultmodules.js"></script>
|
||||
<script type="text/javascript" src="js/logger.js"></script>
|
||||
<script type="text/javascript" src="translations/translations.js"></script>
|
||||
<script type="text/javascript" src="js/translator.js"></script>
|
||||
<script type="text/javascript" src="js/class.js"></script>
|
||||
<script type="text/javascript" src="js/module.js"></script>
|
||||
<script type="text/javascript" src="js/loader.js"></script>
|
||||
<script type="text/javascript" src="js/socketclient.js"></script>
|
||||
<script type="text/javascript" src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -48,6 +48,7 @@ process.on("uncaughtException", function (err) {
|
||||
*/
|
||||
function App() {
|
||||
let nodeHelpers = [];
|
||||
let httpServer;
|
||||
|
||||
/**
|
||||
* Loads the config file. Combines it with the defaults, and runs the
|
||||
@@ -180,7 +181,6 @@ function App() {
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
@@ -223,7 +223,7 @@ function App() {
|
||||
}
|
||||
|
||||
loadModules(modules, function () {
|
||||
const server = new Server(config, function (app, io) {
|
||||
httpServer = new Server(config, function (app, io) {
|
||||
Log.log("Server started ...");
|
||||
|
||||
for (let nodeHelper of nodeHelpers) {
|
||||
@@ -254,6 +254,7 @@ function App() {
|
||||
nodeHelper.stop();
|
||||
}
|
||||
}
|
||||
httpServer.close();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -52,7 +52,13 @@ function checkConfigFile() {
|
||||
// I'm not sure if all ever is utf-8
|
||||
const configFile = fs.readFileSync(configFileName, "utf-8");
|
||||
|
||||
const errors = linter.verify(configFile);
|
||||
// Explicitly tell linter that he might encounter es6 syntax ("let config = {...}")
|
||||
const errors = linter.verify(configFile, {
|
||||
env: {
|
||||
es6: true
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length === 0) {
|
||||
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
|
||||
} else {
|
||||
|
21
js/class.js
21
js/class.js
@@ -8,8 +8,8 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
(function () {
|
||||
var initializing = false;
|
||||
var fnTest = /xyz/.test(function () {
|
||||
let initializing = false;
|
||||
const fnTest = /xyz/.test(function () {
|
||||
xyz;
|
||||
})
|
||||
? /\b_super\b/
|
||||
@@ -20,27 +20,27 @@
|
||||
|
||||
// Create a new Class that inherits from this class
|
||||
Class.extend = function (prop) {
|
||||
var _super = this.prototype;
|
||||
let _super = this.prototype;
|
||||
|
||||
// Instantiate a base class (but only create the instance,
|
||||
// don't run the init constructor)
|
||||
initializing = true;
|
||||
var prototype = new this();
|
||||
const prototype = new this();
|
||||
initializing = false;
|
||||
|
||||
// Make a copy of all prototype properties, to prevent reference issues.
|
||||
for (var p in prototype) {
|
||||
for (const p in prototype) {
|
||||
prototype[p] = cloneObject(prototype[p]);
|
||||
}
|
||||
|
||||
// Copy the properties over onto the new prototype
|
||||
for (var name in prop) {
|
||||
for (const name in prop) {
|
||||
// Check if we're overwriting an existing function
|
||||
prototype[name] =
|
||||
typeof prop[name] === "function" && typeof _super[name] === "function" && fnTest.test(prop[name])
|
||||
? (function (name, fn) {
|
||||
return function () {
|
||||
var tmp = this._super;
|
||||
const tmp = this._super;
|
||||
|
||||
// Add a new ._super() method that is the same method
|
||||
// but on the super-class
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
// The method only need to be bound temporarily, so we
|
||||
// remove it when we're done executing
|
||||
var ret = fn.apply(this, arguments);
|
||||
const ret = fn.apply(this, arguments);
|
||||
this._super = tmp;
|
||||
|
||||
return ret;
|
||||
@@ -84,7 +84,6 @@
|
||||
* Define the clone method for later use. Helper Method.
|
||||
*
|
||||
* @param {object} obj Object to be cloned
|
||||
*
|
||||
* @returns {object} the cloned object
|
||||
*/
|
||||
function cloneObject(obj) {
|
||||
@@ -92,8 +91,8 @@ function cloneObject(obj) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
var temp = obj.constructor(); // give temp the original obj's constructor
|
||||
for (var key in obj) {
|
||||
const temp = obj.constructor(); // give temp the original obj's constructor
|
||||
for (const key in obj) {
|
||||
temp[key] = cloneObject(obj[key]);
|
||||
|
||||
if (key === "lockStrings") {
|
||||
|
@@ -6,12 +6,12 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var address = "localhost";
|
||||
var port = 8080;
|
||||
const address = "localhost";
|
||||
let port = 8080;
|
||||
if (typeof mmPort !== "undefined") {
|
||||
port = mmPort;
|
||||
}
|
||||
var defaults = {
|
||||
const defaults = {
|
||||
address: address,
|
||||
port: port,
|
||||
basePath: "/",
|
||||
|
@@ -19,7 +19,8 @@ let mainWindow;
|
||||
*
|
||||
*/
|
||||
function createWindow() {
|
||||
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
|
||||
let electronSwitchesDefaults = ["autoplay-policy", "no-user-gesture-required"];
|
||||
app.commandLine.appendSwitch(...new Set(electronSwitchesDefaults, config.electronSwitches));
|
||||
let electronOptionsDefaults = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
@@ -63,9 +64,19 @@ function createWindow() {
|
||||
|
||||
// Open the DevTools if run with "npm start dev"
|
||||
if (process.argv.includes("dev")) {
|
||||
if (process.env.JEST_WORKER_ID !== undefined) {
|
||||
// if we are running with jest
|
||||
const devtools = new BrowserWindow(electronOptions);
|
||||
mainWindow.webContents.setDevToolsWebContents(devtools.webContents);
|
||||
}
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
// simulate mouse move to hide black cursor on start
|
||||
mainWindow.webContents.on("dom-ready", (event) => {
|
||||
mainWindow.webContents.sendInputEvent({ type: "mouseMove", x: 0, y: 0 });
|
||||
});
|
||||
|
||||
// Set responders for window events.
|
||||
mainWindow.on("closed", function () {
|
||||
mainWindow = null;
|
||||
@@ -97,7 +108,12 @@ app.on("ready", function () {
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on("window-all-closed", function () {
|
||||
createWindow();
|
||||
if (process.env.JEST_WORKER_ID !== undefined) {
|
||||
// if we are running with jest
|
||||
app.quit();
|
||||
} else {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", function () {
|
||||
|
69
js/loader.js
69
js/loader.js
@@ -6,24 +6,24 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var Loader = (function () {
|
||||
const Loader = (function () {
|
||||
/* Create helper variables */
|
||||
|
||||
var loadedModuleFiles = [];
|
||||
var loadedFiles = [];
|
||||
var moduleObjects = [];
|
||||
const loadedModuleFiles = [];
|
||||
const loadedFiles = [];
|
||||
const moduleObjects = [];
|
||||
|
||||
/* Private Methods */
|
||||
|
||||
/**
|
||||
* Loops thru all modules and requests load for every module.
|
||||
*/
|
||||
var loadModules = function () {
|
||||
var moduleData = getModuleData();
|
||||
const loadModules = function () {
|
||||
let moduleData = getModuleData();
|
||||
|
||||
var loadNextModule = function () {
|
||||
const loadNextModule = function () {
|
||||
if (moduleData.length > 0) {
|
||||
var nextModule = moduleData[0];
|
||||
const nextModule = moduleData[0];
|
||||
loadModule(nextModule, function () {
|
||||
moduleData = moduleData.slice(1);
|
||||
loadNextModule();
|
||||
@@ -46,9 +46,8 @@ var Loader = (function () {
|
||||
/**
|
||||
* Loops thru all modules and requests start for every module.
|
||||
*/
|
||||
var startModules = function () {
|
||||
for (var m in moduleObjects) {
|
||||
var module = moduleObjects[m];
|
||||
const startModules = function () {
|
||||
for (const module of moduleObjects) {
|
||||
module.start();
|
||||
}
|
||||
|
||||
@@ -56,7 +55,7 @@ var Loader = (function () {
|
||||
MM.modulesStarted(moduleObjects);
|
||||
|
||||
// Starting modules also hides any modules that have requested to be initially hidden
|
||||
for (let thisModule of moduleObjects) {
|
||||
for (const thisModule of moduleObjects) {
|
||||
if (thisModule.data.hiddenOnStartup) {
|
||||
Log.info("Initially hiding " + thisModule.name);
|
||||
thisModule.hide();
|
||||
@@ -69,7 +68,7 @@ var Loader = (function () {
|
||||
*
|
||||
* @returns {object[]} module data as configured in config
|
||||
*/
|
||||
var getAllModules = function () {
|
||||
const getAllModules = function () {
|
||||
return config.modules;
|
||||
};
|
||||
|
||||
@@ -78,29 +77,28 @@ var Loader = (function () {
|
||||
*
|
||||
* @returns {object[]} Module information.
|
||||
*/
|
||||
var getModuleData = function () {
|
||||
var modules = getAllModules();
|
||||
var moduleFiles = [];
|
||||
const getModuleData = function () {
|
||||
const modules = getAllModules();
|
||||
const moduleFiles = [];
|
||||
|
||||
for (var m in modules) {
|
||||
var moduleData = modules[m];
|
||||
var module = moduleData.module;
|
||||
modules.forEach(function (moduleData, index) {
|
||||
const module = moduleData.module;
|
||||
|
||||
var elements = module.split("/");
|
||||
var moduleName = elements[elements.length - 1];
|
||||
var moduleFolder = config.paths.modules + "/" + module;
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = config.paths.modules + "/" + module;
|
||||
|
||||
if (defaultModules.indexOf(moduleName) !== -1) {
|
||||
moduleFolder = config.paths.modules + "/default/" + module;
|
||||
}
|
||||
|
||||
if (moduleData.disabled === true) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
moduleFiles.push({
|
||||
index: m,
|
||||
identifier: "module_" + m + "_" + module,
|
||||
index: index,
|
||||
identifier: "module_" + index + "_" + module,
|
||||
name: moduleName,
|
||||
path: moduleFolder + "/",
|
||||
file: moduleName + ".js",
|
||||
@@ -111,7 +109,7 @@ var Loader = (function () {
|
||||
config: moduleData.config,
|
||||
classes: typeof moduleData.classes !== "undefined" ? moduleData.classes + " " + module : module
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return moduleFiles;
|
||||
};
|
||||
@@ -122,11 +120,11 @@ var Loader = (function () {
|
||||
* @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;
|
||||
const loadModule = function (module, callback) {
|
||||
const url = module.path + module.file;
|
||||
|
||||
var afterLoad = function () {
|
||||
var moduleObject = Module.create(module.name);
|
||||
const afterLoad = function () {
|
||||
const moduleObject = Module.create(module.name);
|
||||
if (moduleObject) {
|
||||
bootstrapModule(module, moduleObject, function () {
|
||||
callback();
|
||||
@@ -153,7 +151,7 @@ var Loader = (function () {
|
||||
* @param {Module} mObj Modules instance.
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
var bootstrapModule = function (module, mObj, callback) {
|
||||
const bootstrapModule = function (module, mObj, callback) {
|
||||
Log.info("Bootstrapping module: " + module.name);
|
||||
|
||||
mObj.setData(module);
|
||||
@@ -177,13 +175,14 @@ var Loader = (function () {
|
||||
* @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);
|
||||
const loadFile = function (fileName, callback) {
|
||||
const extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1);
|
||||
let script, stylesheet;
|
||||
|
||||
switch (extension.toLowerCase()) {
|
||||
case "js":
|
||||
Log.log("Load script: " + fileName);
|
||||
var script = document.createElement("script");
|
||||
script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.src = fileName;
|
||||
script.onload = function () {
|
||||
@@ -202,7 +201,7 @@ var Loader = (function () {
|
||||
break;
|
||||
case "css":
|
||||
Log.log("Load stylesheet: " + fileName);
|
||||
var stylesheet = document.createElement("link");
|
||||
stylesheet = document.createElement("link");
|
||||
stylesheet.rel = "stylesheet";
|
||||
stylesheet.type = "text/css";
|
||||
stylesheet.href = fileName;
|
||||
|
85
js/logger.js
85
js/logger.js
@@ -9,12 +9,13 @@
|
||||
*/
|
||||
(function (root, factory) {
|
||||
if (typeof exports === "object") {
|
||||
// add timestamps in front of log messages
|
||||
require("console-stamp")(console, {
|
||||
pattern: "yyyy-mm-dd HH:MM:ss.l",
|
||||
include: ["debug", "log", "info", "warn", "error"]
|
||||
});
|
||||
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
// add timestamps in front of log messages
|
||||
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);
|
||||
} else {
|
||||
@@ -22,29 +23,57 @@
|
||||
root.Log = factory(root.config);
|
||||
}
|
||||
})(this, function (config) {
|
||||
const logLevel = {
|
||||
debug: Function.prototype.bind.call(console.debug, console),
|
||||
log: Function.prototype.bind.call(console.log, 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),
|
||||
time: Function.prototype.bind.call(console.time, console),
|
||||
timeEnd: Function.prototype.bind.call(console.timeEnd, console),
|
||||
timeStamp: Function.prototype.bind.call(console.timeStamp, console)
|
||||
};
|
||||
let logLevel;
|
||||
let enableLog;
|
||||
if (typeof exports === "object") {
|
||||
// in nodejs and not running with jest
|
||||
enableLog = process.env.JEST_WORKER_ID === undefined;
|
||||
} else {
|
||||
// in browser and not running with jsdom
|
||||
enableLog = typeof window === "object" && window.name !== "jsdom";
|
||||
}
|
||||
|
||||
logLevel.setLogLevel = function (newLevel) {
|
||||
if (newLevel) {
|
||||
Object.keys(logLevel).forEach(function (key, index) {
|
||||
if (!newLevel.includes(key.toLocaleUpperCase())) {
|
||||
logLevel[key] = function () {};
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
if (enableLog) {
|
||||
logLevel = {
|
||||
debug: Function.prototype.bind.call(console.debug, console),
|
||||
log: Function.prototype.bind.call(console.log, 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),
|
||||
time: Function.prototype.bind.call(console.time, console),
|
||||
timeEnd: Function.prototype.bind.call(console.timeEnd, console),
|
||||
timeStamp: Function.prototype.bind.call(console.timeStamp, console)
|
||||
};
|
||||
|
||||
logLevel.setLogLevel = function (newLevel) {
|
||||
if (newLevel) {
|
||||
Object.keys(logLevel).forEach(function (key, index) {
|
||||
if (!newLevel.includes(key.toLocaleUpperCase())) {
|
||||
logLevel[key] = function () {};
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
} else {
|
||||
logLevel = {
|
||||
debug: function () {},
|
||||
log: function () {},
|
||||
info: function () {},
|
||||
warn: function () {},
|
||||
error: function () {},
|
||||
group: function () {},
|
||||
groupCollapsed: function () {},
|
||||
groupEnd: function () {},
|
||||
time: function () {},
|
||||
timeEnd: function () {},
|
||||
timeStamp: function () {}
|
||||
};
|
||||
|
||||
logLevel.setLogLevel = function () {};
|
||||
}
|
||||
|
||||
return logLevel;
|
||||
});
|
||||
|
126
js/main.js
126
js/main.js
@@ -6,25 +6,25 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var MM = (function () {
|
||||
var modules = [];
|
||||
const MM = (function () {
|
||||
let modules = [];
|
||||
|
||||
/* Private Methods */
|
||||
|
||||
/**
|
||||
* Create dom objects for all modules that are configured for a specific position.
|
||||
*/
|
||||
var createDomObjects = function () {
|
||||
var domCreationPromises = [];
|
||||
const createDomObjects = function () {
|
||||
const domCreationPromises = [];
|
||||
|
||||
modules.forEach(function (module) {
|
||||
if (typeof module.data.position !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
var wrapper = selectWrapper(module.data.position);
|
||||
const wrapper = selectWrapper(module.data.position);
|
||||
|
||||
var dom = document.createElement("div");
|
||||
const dom = document.createElement("div");
|
||||
dom.id = module.identifier;
|
||||
dom.className = module.name;
|
||||
|
||||
@@ -35,7 +35,7 @@ var MM = (function () {
|
||||
dom.opacity = 0;
|
||||
wrapper.appendChild(dom);
|
||||
|
||||
var moduleHeader = document.createElement("header");
|
||||
const moduleHeader = document.createElement("header");
|
||||
moduleHeader.innerHTML = module.getHeader();
|
||||
moduleHeader.className = "module-header";
|
||||
dom.appendChild(moduleHeader);
|
||||
@@ -46,11 +46,11 @@ var MM = (function () {
|
||||
moduleHeader.style.display = "block;";
|
||||
}
|
||||
|
||||
var moduleContent = document.createElement("div");
|
||||
const moduleContent = document.createElement("div");
|
||||
moduleContent.className = "module-content";
|
||||
dom.appendChild(moduleContent);
|
||||
|
||||
var domCreationPromise = updateDom(module, 0);
|
||||
const domCreationPromise = updateDom(module, 0);
|
||||
domCreationPromises.push(domCreationPromise);
|
||||
domCreationPromise
|
||||
.then(function () {
|
||||
@@ -70,14 +70,13 @@ var MM = (function () {
|
||||
* Select the wrapper dom object for a specific position.
|
||||
*
|
||||
* @param {string} position The name of the position.
|
||||
*
|
||||
* @returns {HTMLElement} the wrapper element
|
||||
*/
|
||||
var selectWrapper = function (position) {
|
||||
var classes = position.replace("_", " ");
|
||||
var parentWrapper = document.getElementsByClassName(classes);
|
||||
const selectWrapper = function (position) {
|
||||
const classes = position.replace("_", " ");
|
||||
const parentWrapper = document.getElementsByClassName(classes);
|
||||
if (parentWrapper.length > 0) {
|
||||
var wrapper = parentWrapper[0].getElementsByClassName("container");
|
||||
const wrapper = parentWrapper[0].getElementsByClassName("container");
|
||||
if (wrapper.length > 0) {
|
||||
return wrapper[0];
|
||||
}
|
||||
@@ -92,9 +91,9 @@ var MM = (function () {
|
||||
* @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) {
|
||||
var module = modules[m];
|
||||
const sendNotification = function (notification, payload, sender, sendTo) {
|
||||
for (const m in modules) {
|
||||
const module = modules[m];
|
||||
if (module !== sender && (!sendTo || module === sendTo)) {
|
||||
module.notificationReceived(notification, payload, sender);
|
||||
}
|
||||
@@ -106,13 +105,12 @@ var MM = (function () {
|
||||
*
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The (optional) number of microseconds for the animation.
|
||||
*
|
||||
* @returns {Promise} Resolved when the dom is fully updated.
|
||||
*/
|
||||
var updateDom = function (module, speed) {
|
||||
const updateDom = function (module, speed) {
|
||||
return new Promise(function (resolve) {
|
||||
var newContentPromise = module.getDom();
|
||||
var newHeader = module.getHeader();
|
||||
const newHeader = module.getHeader();
|
||||
let newContentPromise = module.getDom();
|
||||
|
||||
if (!(newContentPromise instanceof Promise)) {
|
||||
// convert to a promise if not already one to avoid if/else's everywhere
|
||||
@@ -121,7 +119,7 @@ var MM = (function () {
|
||||
|
||||
newContentPromise
|
||||
.then(function (newContent) {
|
||||
var updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
|
||||
const updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
|
||||
|
||||
updatePromise.then(resolve).catch(Log.error);
|
||||
})
|
||||
@@ -136,10 +134,9 @@ var MM = (function () {
|
||||
* @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.
|
||||
*
|
||||
* @returns {Promise} Resolved when the module dom has been updated.
|
||||
*/
|
||||
var updateDomWithContent = function (module, speed, newHeader, newContent) {
|
||||
const updateDomWithContent = function (module, speed, newHeader, newContent) {
|
||||
return new Promise(function (resolve) {
|
||||
if (module.hidden || !speed) {
|
||||
updateModuleContent(module, newHeader, newContent);
|
||||
@@ -174,26 +171,25 @@ var MM = (function () {
|
||||
* @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.
|
||||
*
|
||||
* @returns {boolean} True if the module need an update, false otherwise
|
||||
*/
|
||||
var moduleNeedsUpdate = function (module, newHeader, newContent) {
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
const moduleNeedsUpdate = function (module, newHeader, newContent) {
|
||||
const moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||
const contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||
const headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||
|
||||
var headerNeedsUpdate = false;
|
||||
var contentNeedsUpdate = false;
|
||||
let headerNeedsUpdate = false;
|
||||
let contentNeedsUpdate;
|
||||
|
||||
if (headerWrapper.length > 0) {
|
||||
headerNeedsUpdate = newHeader !== headerWrapper[0].innerHTML;
|
||||
}
|
||||
|
||||
var tempContentWrapper = document.createElement("div");
|
||||
const tempContentWrapper = document.createElement("div");
|
||||
tempContentWrapper.appendChild(newContent);
|
||||
contentNeedsUpdate = tempContentWrapper.innerHTML !== contentWrapper[0].innerHTML;
|
||||
|
||||
@@ -207,13 +203,13 @@ var MM = (function () {
|
||||
* @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);
|
||||
const updateModuleContent = function (module, newHeader, newContent) {
|
||||
const moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper === null) {
|
||||
return;
|
||||
}
|
||||
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||
const headerWrapper = moduleWrapper.getElementsByClassName("module-header");
|
||||
const contentWrapper = moduleWrapper.getElementsByClassName("module-content");
|
||||
|
||||
contentWrapper[0].innerHTML = "";
|
||||
contentWrapper[0].appendChild(newContent);
|
||||
@@ -234,7 +230,7 @@ var MM = (function () {
|
||||
* @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) {
|
||||
const hideModule = function (module, speed, callback, options) {
|
||||
options = options || {};
|
||||
|
||||
// set lockString if set in options.
|
||||
@@ -245,7 +241,7 @@ var MM = (function () {
|
||||
}
|
||||
}
|
||||
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
const moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper !== null) {
|
||||
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
|
||||
moduleWrapper.style.opacity = 0;
|
||||
@@ -280,12 +276,12 @@ var MM = (function () {
|
||||
* @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) {
|
||||
const showModule = function (module, speed, callback, options) {
|
||||
options = options || {};
|
||||
|
||||
// remove lockString if set in options.
|
||||
if (options.lockString) {
|
||||
var index = module.lockStrings.indexOf(options.lockString);
|
||||
const index = module.lockStrings.indexOf(options.lockString);
|
||||
if (index !== -1) {
|
||||
module.lockStrings.splice(index, 1);
|
||||
}
|
||||
@@ -309,7 +305,7 @@ var MM = (function () {
|
||||
module.lockStrings = [];
|
||||
}
|
||||
|
||||
var moduleWrapper = document.getElementById(module.identifier);
|
||||
const moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper !== null) {
|
||||
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
|
||||
// Restore the position. See hideModule() for more info.
|
||||
@@ -318,7 +314,7 @@ var MM = (function () {
|
||||
updateWrapperStates();
|
||||
|
||||
// Waiting for DOM-changes done in updateWrapperStates before we can start the animation.
|
||||
var dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
|
||||
const dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
|
||||
moduleWrapper.style.opacity = 1;
|
||||
|
||||
clearTimeout(module.showHideTimer);
|
||||
@@ -346,14 +342,14 @@ var MM = (function () {
|
||||
* an ugly top margin. By using this function, the top bar will be hidden if the
|
||||
* update notification is not visible.
|
||||
*/
|
||||
var updateWrapperStates = function () {
|
||||
var positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||
const updateWrapperStates = function () {
|
||||
const positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||
|
||||
positions.forEach(function (position) {
|
||||
var wrapper = selectWrapper(position);
|
||||
var moduleWrappers = wrapper.getElementsByClassName("module");
|
||||
const wrapper = selectWrapper(position);
|
||||
const moduleWrappers = wrapper.getElementsByClassName("module");
|
||||
|
||||
var showWrapper = false;
|
||||
let showWrapper = false;
|
||||
Array.prototype.forEach.call(moduleWrappers, function (moduleWrapper) {
|
||||
if (moduleWrapper.style.position === "" || moduleWrapper.style.position === "static") {
|
||||
showWrapper = true;
|
||||
@@ -367,7 +363,7 @@ var MM = (function () {
|
||||
/**
|
||||
* Loads the core config and combines it with the system defaults.
|
||||
*/
|
||||
var loadConfig = function () {
|
||||
const loadConfig = function () {
|
||||
// FIXME: Think about how to pass config around without breaking tests
|
||||
/* eslint-disable */
|
||||
if (typeof config === "undefined") {
|
||||
@@ -385,15 +381,14 @@ var MM = (function () {
|
||||
*
|
||||
* @param {Module[]} modules Array of modules.
|
||||
*/
|
||||
var setSelectionMethodsForModules = function (modules) {
|
||||
const setSelectionMethodsForModules = function (modules) {
|
||||
/**
|
||||
* Filter modules with the specified classes.
|
||||
*
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
*
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var withClass = function (className) {
|
||||
const withClass = function (className) {
|
||||
return modulesByClass(className, true);
|
||||
};
|
||||
|
||||
@@ -401,10 +396,9 @@ var MM = (function () {
|
||||
* Filter modules without the specified classes.
|
||||
*
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
*
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var exceptWithClass = function (className) {
|
||||
const exceptWithClass = function (className) {
|
||||
return modulesByClass(className, false);
|
||||
};
|
||||
|
||||
@@ -413,20 +407,18 @@ var MM = (function () {
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @returns {Module[]} Filtered collection of modules.
|
||||
*/
|
||||
var modulesByClass = function (className, include) {
|
||||
var searchClasses = className;
|
||||
const modulesByClass = function (className, include) {
|
||||
let searchClasses = className;
|
||||
if (typeof className === "string") {
|
||||
searchClasses = className.split(" ");
|
||||
}
|
||||
|
||||
var newModules = modules.filter(function (module) {
|
||||
var classes = module.data.classes.toLowerCase().split(" ");
|
||||
const newModules = modules.filter(function (module) {
|
||||
const classes = module.data.classes.toLowerCase().split(" ");
|
||||
|
||||
for (var c in searchClasses) {
|
||||
var searchClass = searchClasses[c];
|
||||
for (const searchClass of searchClasses) {
|
||||
if (classes.indexOf(searchClass.toLowerCase()) !== -1) {
|
||||
return include;
|
||||
}
|
||||
@@ -445,8 +437,8 @@ var MM = (function () {
|
||||
* @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) {
|
||||
const exceptModule = function (module) {
|
||||
const newModules = modules.filter(function (mod) {
|
||||
return mod.identifier !== module.identifier;
|
||||
});
|
||||
|
||||
@@ -459,7 +451,7 @@ var MM = (function () {
|
||||
*
|
||||
* @param {Function} callback The function to execute with the module as an argument.
|
||||
*/
|
||||
var enumerate = function (callback) {
|
||||
const enumerate = function (callback) {
|
||||
modules.map(function (module) {
|
||||
callback(module);
|
||||
});
|
||||
@@ -604,11 +596,11 @@ if (typeof Object.assign !== "function") {
|
||||
if (target === undefined || target === null) {
|
||||
throw new TypeError("Cannot convert undefined or null to object");
|
||||
}
|
||||
var output = Object(target);
|
||||
for (var index = 1; index < arguments.length; index++) {
|
||||
var source = arguments[index];
|
||||
const output = Object(target);
|
||||
for (let index = 1; index < arguments.length; index++) {
|
||||
const source = arguments[index];
|
||||
if (source !== undefined && source !== null) {
|
||||
for (var nextKey in source) {
|
||||
for (const nextKey in source) {
|
||||
if (source.hasOwnProperty(nextKey)) {
|
||||
output[nextKey] = source[nextKey];
|
||||
}
|
||||
|
80
js/module.js
80
js/module.js
@@ -6,9 +6,8 @@
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
*/
|
||||
var Module = Class.extend({
|
||||
const Module = Class.extend({
|
||||
/*********************************************************
|
||||
* All methods (and properties) below can be subclassed. *
|
||||
*********************************************************/
|
||||
@@ -82,16 +81,15 @@ var Module = Class.extend({
|
||||
* @returns {HTMLElement|Promise} The dom or a promise with the dom to display.
|
||||
*/
|
||||
getDom: function () {
|
||||
var self = this;
|
||||
return new Promise(function (resolve) {
|
||||
var div = document.createElement("div");
|
||||
var template = self.getTemplate();
|
||||
var templateData = self.getTemplateData();
|
||||
return new Promise((resolve) => {
|
||||
const div = document.createElement("div");
|
||||
const template = this.getTemplate();
|
||||
const templateData = this.getTemplateData();
|
||||
|
||||
// Check to see if we need to render a template string or a file.
|
||||
if (/^.*((\.html)|(\.njk))$/.test(template)) {
|
||||
// the template is a filename
|
||||
self.nunjucksEnvironment().render(template, templateData, function (err, res) {
|
||||
this.nunjucksEnvironment().render(template, templateData, function (err, res) {
|
||||
if (err) {
|
||||
Log.error(err);
|
||||
}
|
||||
@@ -102,7 +100,7 @@ var Module = Class.extend({
|
||||
});
|
||||
} else {
|
||||
// the template is a template string.
|
||||
div.innerHTML = self.nunjucksEnvironment().renderString(template, templateData);
|
||||
div.innerHTML = this.nunjucksEnvironment().renderString(template, templateData);
|
||||
|
||||
resolve(div);
|
||||
}
|
||||
@@ -168,15 +166,13 @@ var Module = Class.extend({
|
||||
return this._nunjucksEnvironment;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
this._nunjucksEnvironment = new nunjucks.Environment(new nunjucks.WebLoader(this.file(""), { async: true }), {
|
||||
trimBlocks: true,
|
||||
lstripBlocks: true
|
||||
});
|
||||
|
||||
this._nunjucksEnvironment.addFilter("translate", function (str, variables) {
|
||||
return nunjucks.runtime.markSafe(self.translate(str, variables));
|
||||
this._nunjucksEnvironment.addFilter("translate", (str, variables) => {
|
||||
return nunjucks.runtime.markSafe(this.translate(str, variables));
|
||||
});
|
||||
|
||||
return this._nunjucksEnvironment;
|
||||
@@ -192,14 +188,14 @@ var Module = Class.extend({
|
||||
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
|
||||
},
|
||||
|
||||
/*
|
||||
/**
|
||||
* Called when the module is hidden.
|
||||
*/
|
||||
suspend: function () {
|
||||
Log.log(this.name + " is suspended.");
|
||||
},
|
||||
|
||||
/*
|
||||
/**
|
||||
* Called when the module is shown.
|
||||
*/
|
||||
resume: function () {
|
||||
@@ -213,7 +209,7 @@ var Module = Class.extend({
|
||||
/**
|
||||
* Set the module data.
|
||||
*
|
||||
* @param {Module} data The module data
|
||||
* @param {object} data The module data
|
||||
*/
|
||||
setData: function (data) {
|
||||
this.data = data;
|
||||
@@ -245,9 +241,8 @@ var Module = Class.extend({
|
||||
this._socket = new MMSocket(this.name);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this._socket.setNotificationCallback(function (notification, payload) {
|
||||
self.socketNotificationReceived(notification, payload);
|
||||
this._socket.setNotificationCallback((notification, payload) => {
|
||||
this.socketNotificationReceived(notification, payload);
|
||||
});
|
||||
|
||||
return this._socket;
|
||||
@@ -288,13 +283,12 @@ var Module = Class.extend({
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
loadDependencies: function (funcName, callback) {
|
||||
var self = this;
|
||||
var dependencies = this[funcName]();
|
||||
let dependencies = this[funcName]();
|
||||
|
||||
var loadNextDependency = function () {
|
||||
const loadNextDependency = () => {
|
||||
if (dependencies.length > 0) {
|
||||
var nextDependency = dependencies[0];
|
||||
Loader.loadFile(nextDependency, self, function () {
|
||||
const nextDependency = dependencies[0];
|
||||
Loader.loadFile(nextDependency, this, () => {
|
||||
dependencies = dependencies.slice(1);
|
||||
loadNextDependency();
|
||||
});
|
||||
@@ -400,12 +394,11 @@ var Module = Class.extend({
|
||||
callback = callback || function () {};
|
||||
options = options || {};
|
||||
|
||||
var self = this;
|
||||
MM.hideModule(
|
||||
self,
|
||||
this,
|
||||
speed,
|
||||
function () {
|
||||
self.suspend();
|
||||
() => {
|
||||
this.suspend();
|
||||
callback();
|
||||
},
|
||||
options
|
||||
@@ -464,9 +457,9 @@ var Module = Class.extend({
|
||||
* @returns {object} the merged config
|
||||
*/
|
||||
function configMerge(result) {
|
||||
var stack = Array.prototype.slice.call(arguments, 1);
|
||||
var item;
|
||||
var key;
|
||||
const stack = Array.prototype.slice.call(arguments, 1);
|
||||
let item, key;
|
||||
|
||||
while (stack.length) {
|
||||
item = stack.shift();
|
||||
for (key in item) {
|
||||
@@ -494,19 +487,19 @@ Module.create = function (name) {
|
||||
return;
|
||||
}
|
||||
|
||||
var moduleDefinition = Module.definitions[name];
|
||||
var clonedDefinition = cloneObject(moduleDefinition);
|
||||
const moduleDefinition = Module.definitions[name];
|
||||
const clonedDefinition = cloneObject(moduleDefinition);
|
||||
|
||||
// Note that we clone the definition. Otherwise the objects are shared, which gives problems.
|
||||
var ModuleClass = Module.extend(clonedDefinition);
|
||||
const ModuleClass = Module.extend(clonedDefinition);
|
||||
|
||||
return new ModuleClass();
|
||||
};
|
||||
|
||||
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("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.mmVersion);
|
||||
if (cmpVersions(window.mmVersion, moduleDefinition.requiresVersion) >= 0) {
|
||||
Log.log("Version is ok!");
|
||||
} else {
|
||||
Log.warn("Version is incorrect. Skip module: '" + name + "'");
|
||||
@@ -517,6 +510,8 @@ Module.register = function (name, moduleDefinition) {
|
||||
Module.definitions[name] = moduleDefinition;
|
||||
};
|
||||
|
||||
window.Module = Module;
|
||||
|
||||
/**
|
||||
* Compare two semantic version numbers and return the difference.
|
||||
*
|
||||
@@ -526,14 +521,13 @@ Module.register = function (name, moduleDefinition) {
|
||||
* 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);
|
||||
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);
|
||||
for (let i = 0; i < l; i++) {
|
||||
let diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
|
||||
if (diff) {
|
||||
return diff;
|
||||
}
|
||||
|
@@ -113,6 +113,32 @@ const NodeHelper = Class.extend({
|
||||
}
|
||||
});
|
||||
|
||||
NodeHelper.checkFetchStatus = function (response) {
|
||||
// response.status >= 200 && response.status < 300
|
||||
if (response.ok) {
|
||||
return response;
|
||||
} else {
|
||||
throw Error(response.statusText);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Look at the specified error and return an appropriate error type, that
|
||||
* can be translated to a detailed error message
|
||||
*
|
||||
* @param {Error} error the error from fetching something
|
||||
* @returns {string} the string of the detailed error message in the translations
|
||||
*/
|
||||
NodeHelper.checkFetchError = function (error) {
|
||||
let error_type = "MODULE_ERROR_UNSPECIFIED";
|
||||
if (error.code === "EAI_AGAIN") {
|
||||
error_type = "MODULE_ERROR_NO_CONNECTION";
|
||||
} else if (error.message === "Unauthorized") {
|
||||
error_type = "MODULE_ERROR_UNAUTHORIZED";
|
||||
}
|
||||
return error_type;
|
||||
};
|
||||
|
||||
NodeHelper.create = function (moduleDefinition) {
|
||||
return NodeHelper.extend(moduleDefinition);
|
||||
};
|
||||
|
15
js/server.js
15
js/server.js
@@ -23,6 +23,7 @@ const Utils = require("./utils.js");
|
||||
*/
|
||||
function Server(config, callback) {
|
||||
const port = process.env.MM_PORT || config.port;
|
||||
const serverSockets = new Set();
|
||||
|
||||
let server = null;
|
||||
if (config.useHttps) {
|
||||
@@ -42,6 +43,13 @@ function Server(config, callback) {
|
||||
allowEIO3: true
|
||||
});
|
||||
|
||||
server.on("connection", (socket) => {
|
||||
serverSockets.add(socket);
|
||||
socket.on("close", () => {
|
||||
serverSockets.delete(socket);
|
||||
});
|
||||
});
|
||||
|
||||
Log.log(`Starting server on port ${port} ... `);
|
||||
|
||||
server.listen(port, config.address || "localhost");
|
||||
@@ -92,6 +100,13 @@ function Server(config, callback) {
|
||||
if (typeof callback === "function") {
|
||||
callback(app, io);
|
||||
}
|
||||
|
||||
this.close = function () {
|
||||
for (const socket of serverSockets.values()) {
|
||||
socket.destroy();
|
||||
}
|
||||
server.close();
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Server;
|
||||
|
@@ -6,49 +6,48 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var MMSocket = function (moduleName) {
|
||||
var self = this;
|
||||
|
||||
const MMSocket = function (moduleName) {
|
||||
if (typeof moduleName !== "string") {
|
||||
throw new Error("Please set the module name for the MMSocket.");
|
||||
}
|
||||
|
||||
self.moduleName = moduleName;
|
||||
this.moduleName = moduleName;
|
||||
|
||||
// Private Methods
|
||||
var base = "/";
|
||||
let base = "/";
|
||||
if (typeof config !== "undefined" && typeof config.basePath !== "undefined") {
|
||||
base = config.basePath;
|
||||
}
|
||||
self.socket = io("/" + self.moduleName, {
|
||||
this.socket = io("/" + this.moduleName, {
|
||||
path: base + "socket.io"
|
||||
});
|
||||
var notificationCallback = function () {};
|
||||
|
||||
var onevent = self.socket.onevent;
|
||||
self.socket.onevent = function (packet) {
|
||||
var args = packet.data || [];
|
||||
onevent.call(this, packet); // original call
|
||||
let notificationCallback = function () {};
|
||||
|
||||
const onevent = this.socket.onevent;
|
||||
this.socket.onevent = (packet) => {
|
||||
const args = packet.data || [];
|
||||
onevent.call(this.socket, packet); // original call
|
||||
packet.data = ["*"].concat(args);
|
||||
onevent.call(this, packet); // additional call to catch-all
|
||||
onevent.call(this.socket, packet); // additional call to catch-all
|
||||
};
|
||||
|
||||
// register catch all.
|
||||
self.socket.on("*", function (notification, payload) {
|
||||
this.socket.on("*", (notification, payload) => {
|
||||
if (notification !== "*") {
|
||||
notificationCallback(notification, payload);
|
||||
}
|
||||
});
|
||||
|
||||
// Public Methods
|
||||
this.setNotificationCallback = function (callback) {
|
||||
this.setNotificationCallback = (callback) => {
|
||||
notificationCallback = callback;
|
||||
};
|
||||
|
||||
this.sendNotification = function (notification, payload) {
|
||||
this.sendNotification = (notification, payload) => {
|
||||
if (typeof payload === "undefined") {
|
||||
payload = {};
|
||||
}
|
||||
self.socket.emit(notification, payload);
|
||||
this.socket.emit(notification, payload);
|
||||
};
|
||||
};
|
||||
|
@@ -6,7 +6,7 @@
|
||||
* By Christopher Fenner https://github.com/CFenner
|
||||
* MIT Licensed.
|
||||
*/
|
||||
var Translator = (function () {
|
||||
const Translator = (function () {
|
||||
/**
|
||||
* Load a JSON file via XHR.
|
||||
*
|
||||
@@ -141,12 +141,7 @@ var Translator = (function () {
|
||||
* The first language defined in translations.js will be used.
|
||||
*/
|
||||
loadCoreTranslationsFallback: function () {
|
||||
// The variable `first` will contain the first
|
||||
// defined translation after the following line.
|
||||
for (var first in translations) {
|
||||
break;
|
||||
}
|
||||
|
||||
let first = Object.keys(translations)[0];
|
||||
if (first) {
|
||||
Log.log("Loading core translation fallback file: " + translations[first]);
|
||||
loadJSON(translations[first], (translations) => {
|
||||
@@ -156,3 +151,5 @@ var Translator = (function () {
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
window.Translator = Translator;
|
||||
|
@@ -1,16 +1,16 @@
|
||||
type ModuleProperties = {
|
||||
defaults?: object,
|
||||
start?(): void,
|
||||
getHeader?(): string,
|
||||
getTemplate?(): string,
|
||||
getTemplateData?(): object,
|
||||
notificationReceived?(notification: string, payload: any, sender: object): void,
|
||||
socketNotificationReceived?(notification: string, payload: any): void,
|
||||
suspend?(): void,
|
||||
resume?(): void,
|
||||
getDom?(): HTMLElement,
|
||||
getStyles?(): string[],
|
||||
[key: string]: any,
|
||||
defaults?: object;
|
||||
start?(): void;
|
||||
getHeader?(): string;
|
||||
getTemplate?(): string;
|
||||
getTemplateData?(): object;
|
||||
notificationReceived?(notification: string, payload: any, sender: object): void;
|
||||
socketNotificationReceived?(notification: string, payload: any): void;
|
||||
suspend?(): void;
|
||||
resume?(): void;
|
||||
getDom?(): HTMLElement;
|
||||
getStyles?(): string[];
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export declare const Module: {
|
||||
@@ -18,14 +18,14 @@ export declare const Module: {
|
||||
};
|
||||
|
||||
export declare const Log: {
|
||||
info(message?: any, ...optionalParams: any[]): void,
|
||||
log(message?: any, ...optionalParams: any[]): void,
|
||||
error(message?: any, ...optionalParams: any[]): void,
|
||||
warn(message?: any, ...optionalParams: any[]): void,
|
||||
group(groupTitle?: string, ...optionalParams: any[]): void,
|
||||
groupCollapsed(groupTitle?: string, ...optionalParams: any[]): void,
|
||||
groupEnd(): void,
|
||||
time(timerName?: string): void,
|
||||
timeEnd(timerName?: string): void,
|
||||
timeStamp(timerName?: string): void,
|
||||
};
|
||||
info(message?: any, ...optionalParams: any[]): void;
|
||||
log(message?: any, ...optionalParams: any[]): void;
|
||||
error(message?: any, ...optionalParams: any[]): void;
|
||||
warn(message?: any, ...optionalParams: any[]): void;
|
||||
group(groupTitle?: string, ...optionalParams: any[]): void;
|
||||
groupCollapsed(groupTitle?: string, ...optionalParams: any[]): void;
|
||||
groupEnd(): void;
|
||||
time(timerName?: string): void;
|
||||
timeEnd(timerName?: string): void;
|
||||
timeStamp(timerName?: string): void;
|
||||
};
|
||||
|
@@ -79,7 +79,7 @@ Module.register("alert", {
|
||||
|
||||
//If module already has an open alert close it
|
||||
if (this.alerts[sender.name]) {
|
||||
this.hide_alert(sender);
|
||||
this.hide_alert(sender, false);
|
||||
}
|
||||
|
||||
//Display title and message only if they are provided in notification parameters
|
||||
@@ -114,10 +114,10 @@ Module.register("alert", {
|
||||
}, params.timer);
|
||||
}
|
||||
},
|
||||
hide_alert: function (sender) {
|
||||
hide_alert: function (sender, close = true) {
|
||||
//Dismiss alert and remove from this.alerts
|
||||
if (this.alerts[sender.name]) {
|
||||
this.alerts[sender.name].dismiss();
|
||||
this.alerts[sender.name].dismiss(close);
|
||||
this.alerts[sender.name] = null;
|
||||
//Remove overlay
|
||||
const overlay = document.getElementById("overlay");
|
||||
|
@@ -6,7 +6,6 @@
|
||||
line-height: 1.4;
|
||||
margin-bottom: 10px;
|
||||
z-index: 1;
|
||||
color: black;
|
||||
font-size: 70%;
|
||||
position: relative;
|
||||
display: table;
|
||||
@@ -15,17 +14,17 @@
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
border-style: solid;
|
||||
border-color: #666;
|
||||
border-color: var(--color-text-dimmed);
|
||||
}
|
||||
|
||||
.ns-alert {
|
||||
border-style: solid;
|
||||
border-color: #fff;
|
||||
border-color: var(--color-text-bright);
|
||||
padding: 17px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 10px;
|
||||
z-index: 3;
|
||||
color: white;
|
||||
color: var(--color-text-bright);
|
||||
font-size: 70%;
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
|
@@ -122,8 +122,10 @@
|
||||
|
||||
/**
|
||||
* Dismiss the notification
|
||||
*
|
||||
* @param {boolean} [close] call the onClose callback at the end
|
||||
*/
|
||||
NotificationFx.prototype.dismiss = function () {
|
||||
NotificationFx.prototype.dismiss = function (close = true) {
|
||||
this.active = false;
|
||||
clearTimeout(this.dismissttl);
|
||||
this.ntf.classList.remove("ns-show");
|
||||
@@ -131,7 +133,7 @@
|
||||
this.ntf.classList.add("ns-hide");
|
||||
|
||||
// callback
|
||||
this.options.onClose();
|
||||
if (close) this.options.onClose();
|
||||
}, 25);
|
||||
|
||||
// after animation ends remove ntf from the DOM
|
||||
|
@@ -1,13 +1,14 @@
|
||||
.calendar .symbol {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding-left: 0;
|
||||
padding-right: 10px;
|
||||
font-size: 80%;
|
||||
vertical-align: top;
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.calendar .symbol span {
|
||||
display: inline-block;
|
||||
transform: translate(0, 2px);
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.calendar .title {
|
||||
|
@@ -84,7 +84,7 @@ Module.register("calendar", {
|
||||
|
||||
// Override start method.
|
||||
start: function () {
|
||||
Log.log("Starting module: " + this.name);
|
||||
Log.info("Starting module: " + this.name);
|
||||
|
||||
// Set locale.
|
||||
moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
|
||||
@@ -140,17 +140,17 @@ Module.register("calendar", {
|
||||
if (notification === "CALENDAR_EVENTS") {
|
||||
if (this.hasCalendarURL(payload.url)) {
|
||||
this.calendarData[payload.url] = payload.events;
|
||||
this.error = null;
|
||||
this.loaded = true;
|
||||
|
||||
if (this.config.broadcastEvents) {
|
||||
this.broadcastEvents();
|
||||
}
|
||||
}
|
||||
} else if (notification === "FETCH_ERROR") {
|
||||
Log.error("Calendar Error. Could not fetch calendar: " + payload.url);
|
||||
} else if (notification === "CALENDAR_ERROR") {
|
||||
let error_message = this.translate(payload.error_type);
|
||||
this.error = this.translate("MODULE_CONFIG_ERROR", { MODULE_NAME: this.name, ERROR: error_message });
|
||||
this.loaded = true;
|
||||
} else if (notification === "INCORRECT_URL") {
|
||||
Log.error("Calendar Error. Incorrect url: " + payload.url);
|
||||
}
|
||||
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
@@ -168,6 +168,12 @@ Module.register("calendar", {
|
||||
const wrapper = document.createElement("table");
|
||||
wrapper.className = this.config.tableClass;
|
||||
|
||||
if (this.error) {
|
||||
wrapper.innerHTML = this.error;
|
||||
wrapper.className = this.config.tableClass + " dimmed";
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
if (events.length === 0) {
|
||||
wrapper.innerHTML = this.loaded ? this.translate("EMPTY") : this.translate("LOADING");
|
||||
wrapper.className = this.config.tableClass + " dimmed";
|
||||
@@ -305,15 +311,14 @@ Module.register("calendar", {
|
||||
if (this.config.timeFormat === "dateheaders") {
|
||||
if (event.fullDayEvent) {
|
||||
titleWrapper.colSpan = "2";
|
||||
titleWrapper.align = "left";
|
||||
titleWrapper.classList.add("align-left");
|
||||
} else {
|
||||
const timeWrapper = document.createElement("td");
|
||||
timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
|
||||
timeWrapper.align = "left";
|
||||
timeWrapper.className = "time light align-left " + this.timeClassForUrl(event.url);
|
||||
timeWrapper.style.paddingLeft = "2px";
|
||||
timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
titleWrapper.align = "right";
|
||||
titleWrapper.classList.add("align-right");
|
||||
}
|
||||
|
||||
eventWrapper.appendChild(titleWrapper);
|
||||
@@ -366,13 +371,14 @@ Module.register("calendar", {
|
||||
if (event.startDate >= now) {
|
||||
// Use relative time
|
||||
if (!this.config.hideTime) {
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
|
||||
} else {
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
moment(event.startDate, "x").calendar(null, {
|
||||
sameDay: "[" + this.translate("TODAY") + "]",
|
||||
nextDay: "[" + this.translate("TOMORROW") + "]",
|
||||
nextWeek: "dddd"
|
||||
nextWeek: "dddd",
|
||||
sameElse: this.config.dateFormat
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
const CalendarUtils = require("./calendarutils");
|
||||
const Log = require("logger");
|
||||
const NodeHelper = require("node_helper");
|
||||
const ical = require("node-ical");
|
||||
const fetch = require("node-fetch");
|
||||
const digest = require("digest-fetch");
|
||||
@@ -52,27 +53,17 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
if (auth.method === "bearer") {
|
||||
headers.Authorization = "Bearer " + auth.pass;
|
||||
} else if (auth.method === "digest") {
|
||||
fetcher = new digest(auth.user, auth.pass).fetch(url, { headers: headers, httpsAgent: httpsAgent });
|
||||
fetcher = new digest(auth.user, auth.pass).fetch(url, { headers: headers, agent: httpsAgent });
|
||||
} else {
|
||||
headers.Authorization = "Basic " + Buffer.from(auth.user + ":" + auth.pass).toString("base64");
|
||||
}
|
||||
}
|
||||
if (fetcher === null) {
|
||||
fetcher = fetch(url, { headers: headers, httpsAgent: httpsAgent });
|
||||
fetcher = fetch(url, { headers: headers, agent: httpsAgent });
|
||||
}
|
||||
|
||||
fetcher
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
fetchFailedCallback(this, response.statusText);
|
||||
scheduleTimer();
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.then(NodeHelper.checkFetchStatus)
|
||||
.then((response) => response.text())
|
||||
.then((responseData) => {
|
||||
let data = [];
|
||||
@@ -87,12 +78,16 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
maximumNumberOfDays
|
||||
});
|
||||
} catch (error) {
|
||||
fetchFailedCallback(this, error.message);
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
return;
|
||||
}
|
||||
this.broadcastEvents();
|
||||
scheduleTimer();
|
||||
})
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
});
|
||||
};
|
||||
|
||||
|
@@ -18,8 +18,8 @@ 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
|
||||
* @param {object} event the event which needs adjustement
|
||||
* @param {Date} date the date on which this event happens
|
||||
* @returns {number} the necessary adjustment in hours
|
||||
*/
|
||||
calculateTimezoneAdjustment: function (event, date) {
|
||||
@@ -117,6 +117,13 @@ const CalendarUtils = {
|
||||
return adjustHours;
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter the events from ical according to the given config
|
||||
*
|
||||
* @param {object} data the calendar data from ical
|
||||
* @param {object} config The configuration object
|
||||
* @returns {string[]} the filtered events
|
||||
*/
|
||||
filterEvents: function (data, config) {
|
||||
const newEvents = [];
|
||||
|
||||
@@ -131,13 +138,14 @@ const CalendarUtils = {
|
||||
return CalendarUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||
};
|
||||
|
||||
Log.debug("there are " + Object.entries(data).length + " calendar entries");
|
||||
Log.debug("There are " + Object.entries(data).length + " calendar entries.");
|
||||
Object.entries(data).forEach(([key, event]) => {
|
||||
Log.debug("Processing entry...");
|
||||
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();
|
||||
}
|
||||
@@ -152,10 +160,10 @@ const CalendarUtils = {
|
||||
}
|
||||
|
||||
if (event.type === "VEVENT") {
|
||||
Log.debug("\nEvent: " + JSON.stringify(event));
|
||||
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") {
|
||||
@@ -169,16 +177,21 @@ const CalendarUtils = {
|
||||
}
|
||||
}
|
||||
|
||||
Log.debug(" start=" + startDate.toDate() + " end=" + endDate.toDate());
|
||||
Log.debug("startDate (local): " + startDate.toDate());
|
||||
Log.debug("endDate (local): " + endDate.toDate());
|
||||
|
||||
// calculate the duration of the event for use with recurring events.
|
||||
// Calculate the duration of the event for use with recurring events.
|
||||
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
Log.debug("duration: " + duration);
|
||||
|
||||
// FIXME: Since the parsed json object from node-ical comes with time information
|
||||
// this check could be removed (?)
|
||||
if (event.start.length === 8) {
|
||||
startDate = startDate.startOf("day");
|
||||
}
|
||||
|
||||
const title = CalendarUtils.getTitleFromEvent(event);
|
||||
Log.debug("title: " + title);
|
||||
|
||||
let excluded = false,
|
||||
dateFilter = null;
|
||||
@@ -253,9 +266,13 @@ const CalendarUtils = {
|
||||
let pastLocal = 0;
|
||||
let futureLocal = 0;
|
||||
if (CalendarUtils.isFullDayEvent(event)) {
|
||||
Log.debug("fullday");
|
||||
// if full day event, only use the date part of the ranges
|
||||
pastLocal = pastMoment.toDate();
|
||||
futureLocal = futureMoment.toDate();
|
||||
|
||||
Log.debug("pastLocal: " + pastLocal);
|
||||
Log.debug("futureLocal: " + futureLocal);
|
||||
} else {
|
||||
// if we want past events
|
||||
if (config.includePastEvents) {
|
||||
@@ -267,9 +284,9 @@ const CalendarUtils = {
|
||||
}
|
||||
futureLocal = futureMoment.toDate(); // future
|
||||
}
|
||||
Log.debug(" between=" + pastLocal + " to " + futureLocal);
|
||||
Log.debug("Search for recurring events between: " + pastLocal + " and " + futureLocal);
|
||||
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
Log.debug("title=" + event.summary + " dates=" + JSON.stringify(dates));
|
||||
Log.debug("Title: " + event.summary + ", with 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,
|
||||
@@ -277,6 +294,7 @@ const CalendarUtils = {
|
||||
// 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.
|
||||
Log.debug("event.recurrences: " + event.recurrences);
|
||||
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
|
||||
@@ -289,38 +307,42 @@ const CalendarUtils = {
|
||||
// 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 )
|
||||
// Remove the time information of each date by using its substring, using the following method:
|
||||
// .toISOString().substring(0,10).
|
||||
// since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
|
||||
// (see https://momentjs.com/docs/#/displaying/as-iso-string/).
|
||||
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
|
||||
// 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
|
||||
// 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
|
||||
|
||||
// Reduce the time by the following 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
|
||||
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);
|
||||
}
|
||||
// Remove the offset, independently of the comparison between the date hour and the offset,
|
||||
// since in the case that *date houre < offset*, the *new Date* command will handle this by
|
||||
// representing the day before.
|
||||
|
||||
// Reduce the time by the offset:
|
||||
// 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()) {
|
||||
@@ -341,9 +363,8 @@ const CalendarUtils = {
|
||||
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
|
||||
// Reduce the time by the offset:
|
||||
// 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
|
||||
@@ -366,6 +387,7 @@ const CalendarUtils = {
|
||||
}
|
||||
}
|
||||
startDate = moment(date);
|
||||
Log.debug("Corrected startDate (local): " + startDate.toDate());
|
||||
|
||||
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date);
|
||||
|
||||
@@ -381,7 +403,7 @@ const CalendarUtils = {
|
||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||
showRecurrence = false;
|
||||
}
|
||||
Log.debug("duration=" + duration);
|
||||
Log.debug("duration: " + duration);
|
||||
|
||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
||||
if (startDate.format("x") === endDate.format("x")) {
|
||||
@@ -401,7 +423,7 @@ const CalendarUtils = {
|
||||
}
|
||||
|
||||
if (showRecurrence === true) {
|
||||
Log.debug("saving event =" + description);
|
||||
Log.debug("saving event: " + description);
|
||||
addedEvents++;
|
||||
newEvents.push({
|
||||
title: recurrenceTitle,
|
||||
@@ -417,7 +439,7 @@ const CalendarUtils = {
|
||||
});
|
||||
}
|
||||
}
|
||||
// end recurring event parsing
|
||||
// End recurring event parsing.
|
||||
} else {
|
||||
// Single event.
|
||||
const fullDayEvent = isFacebookBirthday ? true : CalendarUtils.isFullDayEvent(event);
|
||||
@@ -500,8 +522,8 @@ const CalendarUtils = {
|
||||
/**
|
||||
* Lookup iana tz from windows
|
||||
*
|
||||
* @param msTZName
|
||||
* @returns {*|null}
|
||||
* @param {string} msTZName the timezone name to lookup
|
||||
* @returns {string|null} the iana name or null of none is found
|
||||
*/
|
||||
getIanaTZFromMS: function (msTZName) {
|
||||
// Get hash entry
|
||||
@@ -571,12 +593,13 @@ const CalendarUtils = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if the user defined title filter should apply
|
||||
*
|
||||
* @param title
|
||||
* @param filter
|
||||
* @param useRegex
|
||||
* @param regexFlags
|
||||
* @returns {boolean|*}
|
||||
* @param {string} title the title of the event
|
||||
* @param {string} filter the string to look for, can be a regex also
|
||||
* @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string
|
||||
* @param {string} regexFlags flags that should be applied to the regex
|
||||
* @returns {boolean} True if the title should be filtered out, false otherwise
|
||||
*/
|
||||
titleFilterApplies: function (title, filter, useRegex, regexFlags) {
|
||||
if (useRegex) {
|
||||
|
@@ -5,6 +5,9 @@
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
// Alias modules mentioned in package.js under _moduleAliases.
|
||||
require("module-alias/register");
|
||||
|
||||
const CalendarFetcher = require("./calendarfetcher.js");
|
||||
|
||||
const url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL
|
||||
@@ -26,11 +29,13 @@ const fetcher = new CalendarFetcher(url, fetchInterval, [], maximumEntries, maxi
|
||||
fetcher.onReceive(function (fetcher) {
|
||||
console.log(fetcher.events());
|
||||
console.log("------------------------------------------------------------");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
fetcher.onError(function (fetcher, error) {
|
||||
console.log("Fetcher error:");
|
||||
console.log(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
fetcher.startFetch();
|
||||
|
@@ -40,13 +40,14 @@ module.exports = NodeHelper.create({
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (error) {
|
||||
this.sendSocketNotification("INCORRECT_URL", { id: identifier, url: url });
|
||||
Log.error("Calendar Error. Malformed calendar url: ", url, error);
|
||||
this.sendSocketNotification("CALENDAR_ERROR", { error_type: "MODULE_ERROR_MALFORMED_URL" });
|
||||
return;
|
||||
}
|
||||
|
||||
let fetcher;
|
||||
if (typeof this.fetchers[identifier + url] === "undefined") {
|
||||
Log.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval);
|
||||
Log.log("Create new calendarfetcher for url: " + url + " - Interval: " + fetchInterval);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
|
||||
|
||||
fetcher.onReceive((fetcher) => {
|
||||
@@ -55,16 +56,16 @@ module.exports = NodeHelper.create({
|
||||
|
||||
fetcher.onError((fetcher, error) => {
|
||||
Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error);
|
||||
this.sendSocketNotification("FETCH_ERROR", {
|
||||
let error_type = NodeHelper.checkFetchError(error);
|
||||
this.sendSocketNotification("CALENDAR_ERROR", {
|
||||
id: identifier,
|
||||
url: fetcher.url(),
|
||||
error: error
|
||||
error_type
|
||||
});
|
||||
});
|
||||
|
||||
this.fetchers[identifier + url] = fetcher;
|
||||
} else {
|
||||
Log.log("Use existing calendar fetcher for url: " + url);
|
||||
Log.log("Use existing calendarfetcher for url: " + url);
|
||||
fetcher = this.fetchers[identifier + url];
|
||||
fetcher.broadcastEvents();
|
||||
}
|
||||
|
@@ -12,11 +12,14 @@ Module.register("clock", {
|
||||
displayType: "digital", // options: digital, analog, both
|
||||
|
||||
timeFormat: config.timeFormat,
|
||||
timezone: null,
|
||||
|
||||
displaySeconds: true,
|
||||
showPeriod: true,
|
||||
showPeriodUpper: false,
|
||||
clockBold: false,
|
||||
showDate: true,
|
||||
showTime: true,
|
||||
showWeek: false,
|
||||
dateFormat: "dddd, LL",
|
||||
|
||||
@@ -24,9 +27,8 @@ Module.register("clock", {
|
||||
analogSize: "200px",
|
||||
analogFace: "simple", // options: 'none', 'simple', 'face-###' (where ### is 001 to 012 inclusive)
|
||||
analogPlacement: "bottom", // options: 'top', 'bottom', 'left', 'right'
|
||||
analogShowDate: "top", // options: false, 'top', or 'bottom'
|
||||
analogShowDate: "top", // OBSOLETE, can be replaced with analogPlacement and showTime, options: false, 'top', or 'bottom'
|
||||
secondsColor: "#888888",
|
||||
timezone: null,
|
||||
|
||||
showSunTimes: false,
|
||||
showMoonTimes: false,
|
||||
@@ -46,66 +48,75 @@ Module.register("clock", {
|
||||
Log.info("Starting module: " + this.name);
|
||||
|
||||
// Schedule update interval.
|
||||
var self = this;
|
||||
self.second = moment().second();
|
||||
self.minute = moment().minute();
|
||||
this.second = moment().second();
|
||||
this.minute = moment().minute();
|
||||
|
||||
//Calculate how many ms should pass until next update depending on if seconds is displayed or not
|
||||
var delayCalculator = function (reducedSeconds) {
|
||||
var EXTRA_DELAY = 50; //Deliberate imperceptable delay to prevent off-by-one timekeeping errors
|
||||
// Calculate how many ms should pass until next update depending on if seconds is displayed or not
|
||||
const delayCalculator = (reducedSeconds) => {
|
||||
const EXTRA_DELAY = 50; // Deliberate imperceptible delay to prevent off-by-one timekeeping errors
|
||||
|
||||
if (self.config.displaySeconds) {
|
||||
if (this.config.displaySeconds) {
|
||||
return 1000 - moment().milliseconds() + EXTRA_DELAY;
|
||||
} else {
|
||||
return (60 - reducedSeconds) * 1000 - moment().milliseconds() + EXTRA_DELAY;
|
||||
}
|
||||
};
|
||||
|
||||
//A recursive timeout function instead of interval to avoid drifting
|
||||
var notificationTimer = function () {
|
||||
self.updateDom();
|
||||
// A recursive timeout function instead of interval to avoid drifting
|
||||
const notificationTimer = () => {
|
||||
this.updateDom();
|
||||
|
||||
//If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent)
|
||||
if (self.config.displaySeconds) {
|
||||
self.second = moment().second();
|
||||
if (self.second !== 0) {
|
||||
self.sendNotification("CLOCK_SECOND", self.second);
|
||||
// If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent)
|
||||
if (this.config.displaySeconds) {
|
||||
this.second = moment().second();
|
||||
if (this.second !== 0) {
|
||||
this.sendNotification("CLOCK_SECOND", this.second);
|
||||
setTimeout(notificationTimer, delayCalculator(0));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
|
||||
self.minute = moment().minute();
|
||||
self.sendNotification("CLOCK_MINUTE", self.minute);
|
||||
// If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
|
||||
this.minute = moment().minute();
|
||||
this.sendNotification("CLOCK_MINUTE", this.minute);
|
||||
setTimeout(notificationTimer, delayCalculator(0));
|
||||
};
|
||||
|
||||
//Set the initial timeout with the amount of seconds elapsed as reducedSeconds so it will trigger when the minute changes
|
||||
setTimeout(notificationTimer, delayCalculator(self.second));
|
||||
// Set the initial timeout with the amount of seconds elapsed as reducedSeconds so it will trigger when the minute changes
|
||||
setTimeout(notificationTimer, delayCalculator(this.second));
|
||||
|
||||
// Set locale.
|
||||
moment.locale(config.language);
|
||||
},
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
var wrapper = document.createElement("div");
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.classList.add("clockGrid");
|
||||
|
||||
/************************************
|
||||
* Create wrappers for analog and digital clock
|
||||
*/
|
||||
const analogWrapper = document.createElement("div");
|
||||
analogWrapper.className = "clockCircle";
|
||||
const digitalWrapper = document.createElement("div");
|
||||
digitalWrapper.className = "digital";
|
||||
digitalWrapper.style.gridArea = "center";
|
||||
|
||||
/************************************
|
||||
* Create wrappers for DIGITAL clock
|
||||
*/
|
||||
const dateWrapper = document.createElement("div");
|
||||
const timeWrapper = document.createElement("div");
|
||||
const secondsWrapper = document.createElement("sup");
|
||||
const periodWrapper = document.createElement("span");
|
||||
const sunWrapper = document.createElement("div");
|
||||
const moonWrapper = document.createElement("div");
|
||||
const weekWrapper = document.createElement("div");
|
||||
|
||||
var dateWrapper = document.createElement("div");
|
||||
var timeWrapper = document.createElement("div");
|
||||
var secondsWrapper = document.createElement("sup");
|
||||
var periodWrapper = document.createElement("span");
|
||||
var sunWrapper = document.createElement("div");
|
||||
var moonWrapper = document.createElement("div");
|
||||
var weekWrapper = document.createElement("div");
|
||||
// Style Wrappers
|
||||
dateWrapper.className = "date normal medium";
|
||||
timeWrapper.className = "time bright large light";
|
||||
secondsWrapper.className = "dimmed";
|
||||
secondsWrapper.className = "seconds dimmed";
|
||||
sunWrapper.className = "sun dimmed small";
|
||||
moonWrapper.className = "moon dimmed small";
|
||||
weekWrapper.className = "week dimmed medium";
|
||||
@@ -114,19 +125,18 @@ Module.register("clock", {
|
||||
// The moment().format("h") method has a bug on the Raspberry Pi.
|
||||
// So we need to generate the timestring manually.
|
||||
// See issue: https://github.com/MichMich/MagicMirror/issues/181
|
||||
var timeString;
|
||||
var now = moment();
|
||||
this.lastDisplayedMinute = now.minute();
|
||||
let timeString;
|
||||
const now = moment();
|
||||
if (this.config.timezone) {
|
||||
now.tz(this.config.timezone);
|
||||
}
|
||||
|
||||
var hourSymbol = "HH";
|
||||
let hourSymbol = "HH";
|
||||
if (this.config.timeFormat !== 24) {
|
||||
hourSymbol = "h";
|
||||
}
|
||||
|
||||
if (this.config.clockBold === true) {
|
||||
if (this.config.clockBold) {
|
||||
timeString = now.format(hourSymbol + '[<span class="bold">]mm[</span>]');
|
||||
} else {
|
||||
timeString = now.format(hourSymbol + ":mm");
|
||||
@@ -134,22 +144,24 @@ Module.register("clock", {
|
||||
|
||||
if (this.config.showDate) {
|
||||
dateWrapper.innerHTML = now.format(this.config.dateFormat);
|
||||
digitalWrapper.appendChild(dateWrapper);
|
||||
}
|
||||
if (this.config.showWeek) {
|
||||
weekWrapper.innerHTML = this.translate("WEEK", { weekNumber: now.week() });
|
||||
}
|
||||
timeWrapper.innerHTML = timeString;
|
||||
secondsWrapper.innerHTML = now.format("ss");
|
||||
if (this.config.showPeriodUpper) {
|
||||
periodWrapper.innerHTML = now.format("A");
|
||||
} else {
|
||||
periodWrapper.innerHTML = now.format("a");
|
||||
}
|
||||
if (this.config.displaySeconds) {
|
||||
timeWrapper.appendChild(secondsWrapper);
|
||||
}
|
||||
if (this.config.showPeriod && this.config.timeFormat !== 24) {
|
||||
timeWrapper.appendChild(periodWrapper);
|
||||
|
||||
if (this.config.displayType !== "analog" && this.config.showTime) {
|
||||
timeWrapper.innerHTML = timeString;
|
||||
secondsWrapper.innerHTML = now.format("ss");
|
||||
if (this.config.showPeriodUpper) {
|
||||
periodWrapper.innerHTML = now.format("A");
|
||||
} else {
|
||||
periodWrapper.innerHTML = now.format("a");
|
||||
}
|
||||
if (this.config.displaySeconds) {
|
||||
timeWrapper.appendChild(secondsWrapper);
|
||||
}
|
||||
if (this.config.showPeriod && this.config.timeFormat !== 24) {
|
||||
timeWrapper.appendChild(periodWrapper);
|
||||
}
|
||||
digitalWrapper.appendChild(timeWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,17 +172,20 @@ Module.register("clock", {
|
||||
* @returns {string} The formatted time string
|
||||
*/
|
||||
function formatTime(config, time) {
|
||||
var formatString = hourSymbol + ":mm";
|
||||
let formatString = hourSymbol + ":mm";
|
||||
if (config.showPeriod && config.timeFormat !== 24) {
|
||||
formatString += config.showPeriodUpper ? "A" : "a";
|
||||
}
|
||||
return moment(time).format(formatString);
|
||||
}
|
||||
|
||||
/****************************************************************
|
||||
* Create wrappers for Sun Times, only if specified in config
|
||||
*/
|
||||
if (this.config.showSunTimes) {
|
||||
const sunTimes = SunCalc.getTimes(now, this.config.lat, this.config.lon);
|
||||
const isVisible = now.isBetween(sunTimes.sunrise, sunTimes.sunset);
|
||||
var nextEvent;
|
||||
let nextEvent;
|
||||
if (now.isBefore(sunTimes.sunrise)) {
|
||||
nextEvent = sunTimes.sunrise;
|
||||
} else if (now.isBefore(sunTimes.sunset)) {
|
||||
@@ -193,12 +208,17 @@ Module.register("clock", {
|
||||
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i> ' +
|
||||
formatTime(this.config, sunTimes.sunset) +
|
||||
"</span>";
|
||||
digitalWrapper.appendChild(sunWrapper);
|
||||
}
|
||||
|
||||
/****************************************************************
|
||||
* Create wrappers for Moon Times, only if specified in config
|
||||
*/
|
||||
if (this.config.showMoonTimes) {
|
||||
const moonIllumination = SunCalc.getMoonIllumination(now.toDate());
|
||||
const moonTimes = SunCalc.getMoonTimes(now, this.config.lat, this.config.lon);
|
||||
const moonRise = moonTimes.rise;
|
||||
var moonSet;
|
||||
let moonSet;
|
||||
if (moment(moonTimes.set).isAfter(moonTimes.rise)) {
|
||||
moonSet = moonTimes.set;
|
||||
} else {
|
||||
@@ -219,12 +239,17 @@ Module.register("clock", {
|
||||
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i> ' +
|
||||
(moonSet ? formatTime(this.config, moonSet) : "...") +
|
||||
"</span>";
|
||||
digitalWrapper.appendChild(moonWrapper);
|
||||
}
|
||||
|
||||
if (this.config.showWeek) {
|
||||
weekWrapper.innerHTML = this.translate("WEEK", { weekNumber: now.week() });
|
||||
digitalWrapper.appendChild(weekWrapper);
|
||||
}
|
||||
|
||||
/****************************************************************
|
||||
* Create wrappers for ANALOG clock, only if specified in config
|
||||
*/
|
||||
|
||||
if (this.config.displayType !== "digital") {
|
||||
// If it isn't 'digital', then an 'analog' clock was also requested
|
||||
|
||||
@@ -232,34 +257,32 @@ Module.register("clock", {
|
||||
if (this.config.timezone) {
|
||||
now.tz(this.config.timezone);
|
||||
}
|
||||
var second = now.seconds() * 6,
|
||||
const second = now.seconds() * 6,
|
||||
minute = now.minute() * 6 + second / 60,
|
||||
hour = ((now.hours() % 12) / 12) * 360 + 90 + minute / 12;
|
||||
|
||||
// Create wrappers
|
||||
var clockCircle = document.createElement("div");
|
||||
clockCircle.className = "clockCircle";
|
||||
clockCircle.style.width = this.config.analogSize;
|
||||
clockCircle.style.height = this.config.analogSize;
|
||||
analogWrapper.style.width = this.config.analogSize;
|
||||
analogWrapper.style.height = this.config.analogSize;
|
||||
|
||||
if (this.config.analogFace !== "" && this.config.analogFace !== "simple" && this.config.analogFace !== "none") {
|
||||
clockCircle.style.background = "url(" + this.data.path + "faces/" + this.config.analogFace + ".svg)";
|
||||
clockCircle.style.backgroundSize = "100%";
|
||||
analogWrapper.style.background = "url(" + this.data.path + "faces/" + this.config.analogFace + ".svg)";
|
||||
analogWrapper.style.backgroundSize = "100%";
|
||||
|
||||
// The following line solves issue: https://github.com/MichMich/MagicMirror/issues/611
|
||||
// clockCircle.style.border = "1px solid black";
|
||||
clockCircle.style.border = "rgba(0, 0, 0, 0.1)"; //Updated fix for Issue 611 where non-black backgrounds are used
|
||||
// analogWrapper.style.border = "1px solid black";
|
||||
analogWrapper.style.border = "rgba(0, 0, 0, 0.1)"; //Updated fix for Issue 611 where non-black backgrounds are used
|
||||
} else if (this.config.analogFace !== "none") {
|
||||
clockCircle.style.border = "2px solid white";
|
||||
analogWrapper.style.border = "2px solid white";
|
||||
}
|
||||
var clockFace = document.createElement("div");
|
||||
const clockFace = document.createElement("div");
|
||||
clockFace.className = "clockFace";
|
||||
|
||||
var clockHour = document.createElement("div");
|
||||
const clockHour = document.createElement("div");
|
||||
clockHour.id = "clockHour";
|
||||
clockHour.style.transform = "rotate(" + hour + "deg)";
|
||||
clockHour.className = "clockHour";
|
||||
var clockMinute = document.createElement("div");
|
||||
const clockMinute = document.createElement("div");
|
||||
clockMinute.id = "clockMinute";
|
||||
clockMinute.style.transform = "rotate(" + minute + "deg)";
|
||||
clockMinute.className = "clockMinute";
|
||||
@@ -269,91 +292,35 @@ Module.register("clock", {
|
||||
clockFace.appendChild(clockMinute);
|
||||
|
||||
if (this.config.displaySeconds) {
|
||||
var clockSecond = document.createElement("div");
|
||||
const clockSecond = document.createElement("div");
|
||||
clockSecond.id = "clockSecond";
|
||||
clockSecond.style.transform = "rotate(" + second + "deg)";
|
||||
clockSecond.className = "clockSecond";
|
||||
clockSecond.style.backgroundColor = this.config.secondsColor;
|
||||
clockFace.appendChild(clockSecond);
|
||||
}
|
||||
clockCircle.appendChild(clockFace);
|
||||
analogWrapper.appendChild(clockFace);
|
||||
}
|
||||
|
||||
/*******************************************
|
||||
* Combine wrappers, check for .displayType
|
||||
* Update placement, respect old analogShowDate even if its not needed anymore
|
||||
*/
|
||||
|
||||
if (this.config.displayType === "digital") {
|
||||
// Display only a digital clock
|
||||
wrapper.appendChild(dateWrapper);
|
||||
wrapper.appendChild(timeWrapper);
|
||||
wrapper.appendChild(sunWrapper);
|
||||
wrapper.appendChild(moonWrapper);
|
||||
wrapper.appendChild(weekWrapper);
|
||||
} else if (this.config.displayType === "analog") {
|
||||
if (this.config.displayType === "analog") {
|
||||
// Display only an analog clock
|
||||
|
||||
if (this.config.showWeek) {
|
||||
weekWrapper.style.paddingBottom = "15px";
|
||||
} else {
|
||||
dateWrapper.style.paddingBottom = "15px";
|
||||
}
|
||||
|
||||
if (this.config.analogShowDate === "top") {
|
||||
wrapper.appendChild(dateWrapper);
|
||||
wrapper.appendChild(weekWrapper);
|
||||
wrapper.appendChild(clockCircle);
|
||||
wrapper.classList.add("clockGrid--bottom");
|
||||
} else if (this.config.analogShowDate === "bottom") {
|
||||
wrapper.appendChild(clockCircle);
|
||||
wrapper.appendChild(dateWrapper);
|
||||
wrapper.appendChild(weekWrapper);
|
||||
wrapper.classList.add("clockGrid--top");
|
||||
} else {
|
||||
wrapper.appendChild(clockCircle);
|
||||
}
|
||||
} else {
|
||||
// Both clocks have been configured, check position
|
||||
var placement = this.config.analogPlacement;
|
||||
|
||||
var analogWrapper = document.createElement("div");
|
||||
analogWrapper.id = "analog";
|
||||
analogWrapper.style.cssFloat = "none";
|
||||
analogWrapper.appendChild(clockCircle);
|
||||
|
||||
var digitalWrapper = document.createElement("div");
|
||||
digitalWrapper.id = "digital";
|
||||
digitalWrapper.style.cssFloat = "none";
|
||||
digitalWrapper.appendChild(dateWrapper);
|
||||
digitalWrapper.appendChild(timeWrapper);
|
||||
digitalWrapper.appendChild(sunWrapper);
|
||||
digitalWrapper.appendChild(moonWrapper);
|
||||
digitalWrapper.appendChild(weekWrapper);
|
||||
|
||||
var appendClocks = function (condition, pos1, pos2) {
|
||||
var padding = [0, 0, 0, 0];
|
||||
padding[placement === condition ? pos1 : pos2] = "20px";
|
||||
analogWrapper.style.padding = padding.join(" ");
|
||||
if (placement === condition) {
|
||||
wrapper.appendChild(analogWrapper);
|
||||
wrapper.appendChild(digitalWrapper);
|
||||
} else {
|
||||
wrapper.appendChild(digitalWrapper);
|
||||
wrapper.appendChild(analogWrapper);
|
||||
}
|
||||
};
|
||||
|
||||
if (placement === "left" || placement === "right") {
|
||||
digitalWrapper.style.display = "inline-block";
|
||||
digitalWrapper.style.verticalAlign = "top";
|
||||
analogWrapper.style.display = "inline-block";
|
||||
|
||||
appendClocks("left", 1, 3);
|
||||
} else {
|
||||
digitalWrapper.style.textAlign = "center";
|
||||
|
||||
appendClocks("top", 2, 0);
|
||||
//analogWrapper.style.gridArea = "center";
|
||||
}
|
||||
} else if (this.config.displayType === "both") {
|
||||
wrapper.classList.add("clockGrid--" + this.config.analogPlacement);
|
||||
}
|
||||
|
||||
wrapper.appendChild(analogWrapper);
|
||||
wrapper.appendChild(digitalWrapper);
|
||||
|
||||
// Return the wrapper to the dom.
|
||||
return wrapper;
|
||||
}
|
||||
|
@@ -1,5 +1,26 @@
|
||||
.clockGrid {
|
||||
display: inline-flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.clockGrid--left {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.clockGrid--right {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.clockGrid--top {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.clockGrid--bottom {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.clockCircle {
|
||||
margin: 0 auto;
|
||||
place-self: center;
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
background-size: 100%;
|
||||
@@ -17,7 +38,7 @@
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin: -3px 0 0 -3px;
|
||||
background: white;
|
||||
background: var(--color-text-bright);
|
||||
border-radius: 3px;
|
||||
content: "";
|
||||
display: block;
|
||||
@@ -29,9 +50,9 @@
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -2px 0 -2px -25%; /* numbers much match negative length & thickness */
|
||||
margin: -2px 0 -2px -25%; /* numbers must match negative length & thickness */
|
||||
padding: 2px 0 2px 25%; /* indicator length & thickness */
|
||||
background: white;
|
||||
background: var(--color-text-bright);
|
||||
transform-origin: 100% 50%;
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
@@ -44,7 +65,7 @@
|
||||
left: 50%;
|
||||
margin: -35% -2px 0; /* numbers must match negative length & thickness */
|
||||
padding: 35% 2px 0; /* indicator length & thickness */
|
||||
background: white;
|
||||
background: var(--color-text-bright);
|
||||
transform-origin: 50% 100%;
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
@@ -57,7 +78,7 @@
|
||||
left: 50%;
|
||||
margin: -38% -1px 0 0; /* numbers must match negative length & thickness */
|
||||
padding: 38% 1px 0 0; /* indicator length & thickness */
|
||||
background: #888;
|
||||
background: var(--color-text);
|
||||
transform-origin: 50% 100%;
|
||||
}
|
||||
|
||||
|
@@ -39,37 +39,35 @@ Module.register("compliments", {
|
||||
|
||||
this.lastComplimentIndex = -1;
|
||||
|
||||
var self = this;
|
||||
if (this.config.remoteFile !== null) {
|
||||
this.complimentFile(function (response) {
|
||||
self.config.compliments = JSON.parse(response);
|
||||
self.updateDom();
|
||||
this.complimentFile((response) => {
|
||||
this.config.compliments = JSON.parse(response);
|
||||
this.updateDom();
|
||||
});
|
||||
}
|
||||
|
||||
// Schedule update timer.
|
||||
setInterval(function () {
|
||||
self.updateDom(self.config.fadeSpeed);
|
||||
setInterval(() => {
|
||||
this.updateDom(this.config.fadeSpeed);
|
||||
}, this.config.updateInterval);
|
||||
},
|
||||
|
||||
/* randomIndex(compliments)
|
||||
/**
|
||||
* Generate a random index for a list of compliments.
|
||||
*
|
||||
* argument compliments Array<String> - Array with compliments.
|
||||
*
|
||||
* return Number - Random index.
|
||||
* @param {string[]} compliments Array with compliments.
|
||||
* @returns {number} a random index of given array
|
||||
*/
|
||||
randomIndex: function (compliments) {
|
||||
if (compliments.length === 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var generate = function () {
|
||||
const generate = function () {
|
||||
return Math.floor(Math.random() * compliments.length);
|
||||
};
|
||||
|
||||
var complimentIndex = generate();
|
||||
let complimentIndex = generate();
|
||||
|
||||
while (complimentIndex === this.lastComplimentIndex) {
|
||||
complimentIndex = generate();
|
||||
@@ -80,15 +78,15 @@ Module.register("compliments", {
|
||||
return complimentIndex;
|
||||
},
|
||||
|
||||
/* complimentArray()
|
||||
/**
|
||||
* Retrieve an array of compliments for the time of the day.
|
||||
*
|
||||
* return compliments Array<String> - Array with compliments for the time of the day.
|
||||
* @returns {string[]} array with compliments for the time of the day.
|
||||
*/
|
||||
complimentArray: function () {
|
||||
var hour = moment().hour();
|
||||
var date = this.config.mockDate ? this.config.mockDate : moment().format("YYYY-MM-DD");
|
||||
var compliments;
|
||||
const hour = moment().hour();
|
||||
const date = this.config.mockDate ? this.config.mockDate : moment().format("YYYY-MM-DD");
|
||||
let compliments;
|
||||
|
||||
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime && this.config.compliments.hasOwnProperty("morning")) {
|
||||
compliments = this.config.compliments.morning.slice(0);
|
||||
@@ -99,7 +97,7 @@ Module.register("compliments", {
|
||||
}
|
||||
|
||||
if (typeof compliments === "undefined") {
|
||||
compliments = new Array();
|
||||
compliments = [];
|
||||
}
|
||||
|
||||
if (this.currentWeatherType in this.config.compliments) {
|
||||
@@ -108,7 +106,7 @@ Module.register("compliments", {
|
||||
|
||||
compliments.push.apply(compliments, this.config.compliments.anytime);
|
||||
|
||||
for (var entry in this.config.compliments) {
|
||||
for (let entry in this.config.compliments) {
|
||||
if (new RegExp(entry).test(date)) {
|
||||
compliments.push.apply(compliments, this.config.compliments[entry]);
|
||||
}
|
||||
@@ -117,11 +115,13 @@ Module.register("compliments", {
|
||||
return compliments;
|
||||
},
|
||||
|
||||
/* complimentFile(callback)
|
||||
/**
|
||||
* Retrieve a file from the local filesystem
|
||||
*
|
||||
* @param {Function} callback Called when the file is retrieved.
|
||||
*/
|
||||
complimentFile: function (callback) {
|
||||
var xobj = new XMLHttpRequest(),
|
||||
const xobj = new XMLHttpRequest(),
|
||||
isRemote = this.config.remoteFile.indexOf("http://") === 0 || this.config.remoteFile.indexOf("https://") === 0,
|
||||
path = isRemote ? this.config.remoteFile : this.file(this.config.remoteFile);
|
||||
xobj.overrideMimeType("application/json");
|
||||
@@ -134,16 +134,16 @@ Module.register("compliments", {
|
||||
xobj.send(null);
|
||||
},
|
||||
|
||||
/* complimentArray()
|
||||
/**
|
||||
* Retrieve a random compliment.
|
||||
*
|
||||
* return compliment string - A compliment.
|
||||
* @returns {string} a compliment
|
||||
*/
|
||||
randomCompliment: function () {
|
||||
// get the current time of day compliments list
|
||||
var compliments = this.complimentArray();
|
||||
const compliments = this.complimentArray();
|
||||
// variable for index to next message to display
|
||||
let index = 0;
|
||||
let index;
|
||||
// are we randomizing
|
||||
if (this.config.random) {
|
||||
// yes
|
||||
@@ -159,16 +159,16 @@ Module.register("compliments", {
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
var wrapper = document.createElement("div");
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = this.config.classes ? this.config.classes : "thin xlarge bright pre-line";
|
||||
// get the compliment text
|
||||
var complimentText = this.randomCompliment();
|
||||
const complimentText = this.randomCompliment();
|
||||
// split it into parts on newline text
|
||||
var parts = complimentText.split("\n");
|
||||
const parts = complimentText.split("\n");
|
||||
// create a span to hold it all
|
||||
var compliment = document.createElement("span");
|
||||
const compliment = document.createElement("span");
|
||||
// process all the parts of the compliment text
|
||||
for (var part of parts) {
|
||||
for (const part of parts) {
|
||||
// create a text element for each part
|
||||
compliment.appendChild(document.createTextNode(part));
|
||||
// add a break `
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: CurrentWeather
|
||||
*
|
||||
|
@@ -1,13 +1,10 @@
|
||||
/* Magic Mirror
|
||||
* Default Modules List
|
||||
/* Magic Mirror Default Modules List
|
||||
* Modules listed below can be loaded without the 'default/' prefix. Omitting the default folder name.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
// Modules listed below can be loaded without the 'default/' prefix. Omitting the default folder name.
|
||||
|
||||
var defaultModules = ["alert", "calendar", "clock", "compliments", "currentweather", "helloworld", "newsfeed", "weatherforecast", "updatenotification", "weather"];
|
||||
const defaultModules = ["alert", "calendar", "clock", "compliments", "currentweather", "helloworld", "newsfeed", "weatherforecast", "updatenotification", "weather"];
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
|
@@ -12,3 +12,12 @@ iframe.newsfeed-fullarticle {
|
||||
bottom: inherit;
|
||||
top: -90px;
|
||||
}
|
||||
|
||||
.newsfeed-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.newsfeed-list li {
|
||||
text-align: justify;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ Module.register("newsfeed", {
|
||||
encoding: "UTF-8" //ISO-8859-1
|
||||
}
|
||||
],
|
||||
showAsList: false,
|
||||
showSourceTitle: true,
|
||||
showPublishDate: true,
|
||||
broadcastNewsFeeds: true,
|
||||
@@ -89,8 +90,8 @@ Module.register("newsfeed", {
|
||||
|
||||
this.loaded = true;
|
||||
this.error = null;
|
||||
} else if (notification === "INCORRECT_URL") {
|
||||
this.error = `Incorrect url: ${payload.url}`;
|
||||
} else if (notification === "NEWSFEED_ERROR") {
|
||||
this.error = this.translate(payload.error_type);
|
||||
this.scheduleUpdateInterval();
|
||||
}
|
||||
},
|
||||
@@ -128,6 +129,10 @@ Module.register("newsfeed", {
|
||||
}
|
||||
|
||||
const item = this.newsItems[this.activeItem];
|
||||
const items = this.newsItems.map(function (item) {
|
||||
item.publishDate = moment(new Date(item.pubdate)).fromNow();
|
||||
return item;
|
||||
});
|
||||
|
||||
return {
|
||||
loaded: true,
|
||||
@@ -135,7 +140,8 @@ Module.register("newsfeed", {
|
||||
sourceTitle: item.sourceTitle,
|
||||
publishDate: moment(new Date(item.pubdate)).fromNow(),
|
||||
title: item.title,
|
||||
description: item.description
|
||||
description: item.description,
|
||||
items: items
|
||||
};
|
||||
},
|
||||
|
||||
@@ -183,16 +189,15 @@ Module.register("newsfeed", {
|
||||
}
|
||||
|
||||
if (this.config.prohibitedWords.length > 0) {
|
||||
newsItems = newsItems.filter(function (value) {
|
||||
newsItems = newsItems.filter(function (item) {
|
||||
for (let word of this.config.prohibitedWords) {
|
||||
if (value["title"].toLowerCase().indexOf(word.toLowerCase()) > -1) {
|
||||
if (item.title.toLowerCase().indexOf(word.toLowerCase()) > -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}, 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") {
|
||||
|
@@ -1,26 +1,59 @@
|
||||
{% 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 %}
|
||||
{% if config.showAsList %}
|
||||
<ul class="newsfeed-list">
|
||||
{% for item in items %}
|
||||
<li>
|
||||
{% if (config.showSourceTitle and item.sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if item.sourceTitle and config.showSourceTitle %}
|
||||
{{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}: {% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}
|
||||
{{ item.publishDate }}:
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ item.description | truncate(config.lengthDescription) }}
|
||||
{% else %}
|
||||
{{ item.description }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<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>
|
||||
{% 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 }}
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ description | truncate(config.lengthDescription) }}
|
||||
{% else %}
|
||||
{{ description }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elseif error %}
|
||||
<div class="small dimmed">
|
||||
{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}
|
||||
|
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
const Log = require("logger");
|
||||
const FeedMe = require("feedme");
|
||||
const NodeHelper = require("node_helper");
|
||||
const fetch = require("node-fetch");
|
||||
const iconv = require("iconv-lite");
|
||||
|
||||
@@ -84,12 +85,13 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
};
|
||||
|
||||
fetch(url, { headers: headers })
|
||||
.then(NodeHelper.checkFetchStatus)
|
||||
.then((response) => {
|
||||
response.body.pipe(iconv.decodeStream(encoding)).pipe(parser);
|
||||
})
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
})
|
||||
.then((res) => {
|
||||
res.body.pipe(iconv.decodeStream(encoding)).pipe(parser);
|
||||
});
|
||||
};
|
||||
|
||||
|
@@ -27,8 +27,8 @@ module.exports = NodeHelper.create({
|
||||
* Creates a fetcher for a new feed if it doesn't exist yet.
|
||||
* Otherwise it reuses the existing one.
|
||||
*
|
||||
* @param {object} feed The feed object.
|
||||
* @param {object} config The configuration object.
|
||||
* @param {object} feed The feed object
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
createFetcher: function (feed, config) {
|
||||
const url = feed.url || "";
|
||||
@@ -38,13 +38,14 @@ module.exports = NodeHelper.create({
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (error) {
|
||||
this.sendSocketNotification("INCORRECT_URL", { url: url });
|
||||
Log.error("Newsfeed Error. Malformed newsfeed url: ", url, error);
|
||||
this.sendSocketNotification("NEWSFEED_ERROR", { error_type: "MODULE_ERROR_MALFORMED_URL" });
|
||||
return;
|
||||
}
|
||||
|
||||
let fetcher;
|
||||
if (typeof this.fetchers[url] === "undefined") {
|
||||
Log.log("Create new news fetcher for url: " + url + " - Interval: " + reloadInterval);
|
||||
Log.log("Create new newsfetcher for url: " + url + " - Interval: " + reloadInterval);
|
||||
fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings);
|
||||
|
||||
fetcher.onReceive(() => {
|
||||
@@ -52,15 +53,16 @@ module.exports = NodeHelper.create({
|
||||
});
|
||||
|
||||
fetcher.onError((fetcher, error) => {
|
||||
this.sendSocketNotification("FETCH_ERROR", {
|
||||
url: fetcher.url(),
|
||||
error: error
|
||||
Log.error("Newsfeed Error. Could not fetch newsfeed: ", url, error);
|
||||
let error_type = NodeHelper.checkFetchError(error);
|
||||
this.sendSocketNotification("NEWSFEED_ERROR", {
|
||||
error_type
|
||||
});
|
||||
});
|
||||
|
||||
this.fetchers[url] = fetcher;
|
||||
} else {
|
||||
Log.log("Use existing news fetcher for url: " + url);
|
||||
Log.log("Use existing newsfetcher for url: " + url);
|
||||
fetcher = this.fetchers[url];
|
||||
fetcher.setReloadInterval(reloadInterval);
|
||||
fetcher.broadcastItems();
|
||||
|
177
modules/default/updatenotification/git_helper.js
Normal file
177
modules/default/updatenotification/git_helper.js
Normal file
@@ -0,0 +1,177 @@
|
||||
const util = require("util");
|
||||
const exec = util.promisify(require("child_process").exec);
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Log = require("logger");
|
||||
|
||||
class gitHelper {
|
||||
constructor() {
|
||||
this.gitRepos = [];
|
||||
this.baseDir = path.normalize(__dirname + "/../../../");
|
||||
}
|
||||
|
||||
getRefRegex(branch) {
|
||||
return new RegExp("s*([a-z,0-9]+[.][.][a-z,0-9]+) " + branch, "g");
|
||||
}
|
||||
|
||||
async execShell(command) {
|
||||
let res = { stdout: "", stderr: "" };
|
||||
const { stdout, stderr } = await exec(command);
|
||||
|
||||
res.stdout = stdout;
|
||||
res.stderr = stderr;
|
||||
return res;
|
||||
}
|
||||
|
||||
async isGitRepo(moduleFolder) {
|
||||
let res = await this.execShell("cd " + moduleFolder + " && git remote -v");
|
||||
if (res.stderr) {
|
||||
Log.error("Failed to fetch git data for " + moduleFolder + ": " + res.stderr);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
add(moduleName) {
|
||||
let moduleFolder = this.baseDir;
|
||||
if (moduleName !== "default") {
|
||||
moduleFolder = moduleFolder + "modules/" + moduleName;
|
||||
}
|
||||
try {
|
||||
Log.info("Checking git for module: " + moduleName);
|
||||
// Throws error if file doesn't exist
|
||||
fs.statSync(path.join(moduleFolder, ".git"));
|
||||
// Fetch the git or throw error if no remotes
|
||||
if (this.isGitRepo(moduleFolder)) {
|
||||
// Folder has .git and has at least one git remote, watch this folder
|
||||
this.gitRepos.unshift({ module: moduleName, folder: moduleFolder });
|
||||
}
|
||||
} catch (err) {
|
||||
// Error when directory .git doesn't exist or doesn't have any remotes
|
||||
// This module is not managed with git, skip
|
||||
}
|
||||
}
|
||||
|
||||
async getStatusInfo(repo) {
|
||||
let gitInfo = {
|
||||
module: repo.module,
|
||||
// commits behind:
|
||||
behind: 0,
|
||||
// branch name:
|
||||
current: "",
|
||||
// current hash:
|
||||
hash: "",
|
||||
// remote branch:
|
||||
tracking: "",
|
||||
isBehindInStatus: false
|
||||
};
|
||||
let res;
|
||||
if (repo.module === "default") {
|
||||
// the hash is only needed for the mm repo
|
||||
res = await this.execShell("cd " + repo.folder + " && git rev-parse HEAD");
|
||||
if (res.stderr) {
|
||||
Log.error("Failed to get current commit hash for " + repo.module + ": " + res.stderr);
|
||||
}
|
||||
gitInfo.hash = res.stdout;
|
||||
}
|
||||
if (repo.res) {
|
||||
// mocking
|
||||
res = repo.res;
|
||||
} else {
|
||||
res = await this.execShell("cd " + repo.folder + " && git status -sb");
|
||||
}
|
||||
if (res.stderr) {
|
||||
Log.error("Failed to get git status for " + repo.module + ": " + res.stderr);
|
||||
// exit without git status info
|
||||
return;
|
||||
}
|
||||
// only the first line of stdout is evaluated
|
||||
let status = res.stdout.split("\n")[0];
|
||||
// examples for status:
|
||||
// ## develop...origin/develop
|
||||
// ## master...origin/master [behind 8]
|
||||
status = status.match(/(?![.#])([^.]*)/g);
|
||||
// examples for status:
|
||||
// [ ' develop', 'origin/develop', '' ]
|
||||
// [ ' master', 'origin/master [behind 8]', '' ]
|
||||
gitInfo.current = status[0].trim();
|
||||
status = status[1].split(" ");
|
||||
// examples for status:
|
||||
// [ 'origin/develop' ]
|
||||
// [ 'origin/master', '[behind', '8]' ]
|
||||
gitInfo.tracking = status[0].trim();
|
||||
if (status[2]) {
|
||||
// git fetch was already called before so `git status -sb` delivers already the behind number
|
||||
gitInfo.behind = parseInt(status[2].substring(0, status[2].length - 1));
|
||||
gitInfo.isBehindInStatus = true;
|
||||
}
|
||||
return gitInfo;
|
||||
}
|
||||
|
||||
async getRepoInfo(repo) {
|
||||
let gitInfo;
|
||||
if (repo.gitInfo) {
|
||||
// mocking
|
||||
gitInfo = repo.gitInfo;
|
||||
} else {
|
||||
gitInfo = await this.getStatusInfo(repo);
|
||||
}
|
||||
if (!gitInfo) {
|
||||
return;
|
||||
}
|
||||
if (gitInfo.isBehindInStatus) {
|
||||
return gitInfo;
|
||||
}
|
||||
let res;
|
||||
if (repo.res) {
|
||||
// mocking
|
||||
res = repo.res;
|
||||
} else {
|
||||
res = await this.execShell("cd " + repo.folder + " && git fetch --dry-run");
|
||||
}
|
||||
// example output:
|
||||
// From https://github.com/MichMich/MagicMirror
|
||||
// e40ddd4..06389e3 develop -> origin/develop
|
||||
// here the result is in stderr (this is a git default, don't ask why ...)
|
||||
const matches = res.stderr.match(this.getRefRegex(gitInfo.current));
|
||||
if (!matches || !matches[0]) {
|
||||
// no refs found, nothing to do
|
||||
return;
|
||||
}
|
||||
// get behind with refs
|
||||
try {
|
||||
res = await this.execShell("cd " + repo.folder + " && git rev-list --ancestry-path --count " + matches[0]);
|
||||
gitInfo.behind = parseInt(res.stdout);
|
||||
return gitInfo;
|
||||
} catch (err) {
|
||||
Log.error("Failed to get git revisions for " + repo.module + ": " + err);
|
||||
}
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
const gitResultList = [];
|
||||
for (let repo of this.gitRepos) {
|
||||
const gitInfo = await this.getStatusInfo(repo);
|
||||
if (gitInfo) {
|
||||
gitResultList.unshift(gitInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return gitResultList;
|
||||
}
|
||||
|
||||
async getRepos() {
|
||||
const gitResultList = [];
|
||||
for (let repo of this.gitRepos) {
|
||||
const gitInfo = await this.getRepoInfo(repo);
|
||||
if (gitInfo) {
|
||||
gitResultList.unshift(gitInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return gitResultList;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.gitHelper = gitHelper;
|
@@ -1,9 +1,5 @@
|
||||
const SimpleGit = require("simple-git");
|
||||
const simpleGits = [];
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const GitHelper = require(__dirname + "/git_helper.js");
|
||||
const defaultModules = require(__dirname + "/../defaultmodules.js");
|
||||
const Log = require("logger");
|
||||
const NodeHelper = require("node_helper");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
@@ -12,32 +8,19 @@ module.exports = NodeHelper.create({
|
||||
updateTimer: null,
|
||||
updateProcessStarted: false,
|
||||
|
||||
gitHelper: new GitHelper.gitHelper(),
|
||||
|
||||
start: function () {},
|
||||
|
||||
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: this.createGit(path.normalize(__dirname + "/../../../")) });
|
||||
this.gitHelper.add("default");
|
||||
|
||||
for (let moduleName in modules) {
|
||||
if (!this.ignoreUpdateChecking(moduleName)) {
|
||||
// Default modules are included in the main MagicMirror repo
|
||||
let moduleFolder = path.normalize(__dirname + "/../../" + moduleName);
|
||||
|
||||
try {
|
||||
Log.info("Checking git for module: " + moduleName);
|
||||
// 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 or doesn't have any remotes
|
||||
// This module is not managed with git, skip
|
||||
continue;
|
||||
}
|
||||
this.gitHelper.add(moduleName);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -54,35 +37,9 @@ module.exports = NodeHelper.create({
|
||||
}
|
||||
},
|
||||
|
||||
resolveRemote: async function (moduleFolder) {
|
||||
let git = this.createGit(moduleFolder);
|
||||
let remotes = await git.getRemotes(true);
|
||||
|
||||
if (remotes.length < 1 || remotes[0].name.length < 1) {
|
||||
throw new Error("No valid remote for folder " + moduleFolder);
|
||||
}
|
||||
|
||||
return git;
|
||||
},
|
||||
|
||||
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);
|
||||
}
|
||||
for (let gitInfo of await this.gitHelper.getRepos()) {
|
||||
this.sendSocketNotification("STATUS", gitInfo);
|
||||
}
|
||||
|
||||
this.scheduleNextFetch(this.config.updateInterval);
|
||||
@@ -100,10 +57,6 @@ module.exports = NodeHelper.create({
|
||||
}, 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) {
|
||||
|
@@ -5,33 +5,35 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("updatenotification", {
|
||||
// Define module defaults
|
||||
defaults: {
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
refreshInterval: 24 * 60 * 60 * 1000, // one day
|
||||
ignoreModules: [],
|
||||
timeout: 1000
|
||||
timeout: 5000
|
||||
},
|
||||
|
||||
suspended: false,
|
||||
moduleList: {},
|
||||
|
||||
// Override start method.
|
||||
start: function () {
|
||||
var self = this;
|
||||
Log.log("Start updatenotification");
|
||||
Log.info("Starting module: " + this.name);
|
||||
setInterval(() => {
|
||||
self.moduleList = {};
|
||||
self.updateDom(2);
|
||||
}, self.config.refreshInterval);
|
||||
this.moduleList = {};
|
||||
this.updateDom(2);
|
||||
}, this.config.refreshInterval);
|
||||
},
|
||||
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
if (notification === "DOM_OBJECTS_CREATED") {
|
||||
this.sendSocketNotification("CONFIG", this.config);
|
||||
this.sendSocketNotification("MODULES", Module.definitions);
|
||||
//this.hide(0, { lockString: self.identifier });
|
||||
//this.hide(0, { lockString: this.identifier });
|
||||
}
|
||||
},
|
||||
|
||||
// Override socket notification handler.
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
if (notification === "STATUS") {
|
||||
this.updateUI(payload);
|
||||
@@ -39,13 +41,12 @@ Module.register("updatenotification", {
|
||||
},
|
||||
|
||||
updateUI: function (payload) {
|
||||
var self = this;
|
||||
if (payload && payload.behind > 0) {
|
||||
// if we haven't seen info for this module
|
||||
if (this.moduleList[payload.module] === undefined) {
|
||||
// save it
|
||||
this.moduleList[payload.module] = payload;
|
||||
self.updateDom(2);
|
||||
this.updateDom(2);
|
||||
}
|
||||
//self.show(1000, { lockString: self.identifier });
|
||||
} else if (payload && payload.behind === 0) {
|
||||
@@ -53,41 +54,41 @@ Module.register("updatenotification", {
|
||||
if (this.moduleList[payload.module] !== undefined) {
|
||||
// remove it
|
||||
delete this.moduleList[payload.module];
|
||||
self.updateDom(2);
|
||||
this.updateDom(2);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
diffLink: function (module, text) {
|
||||
var localRef = module.hash;
|
||||
var remoteRef = module.tracking.replace(/.*\//, "");
|
||||
const localRef = module.hash;
|
||||
const remoteRef = module.tracking.replace(/.*\//, "");
|
||||
return '<a href="https://github.com/MichMich/MagicMirror/compare/' + localRef + "..." + remoteRef + '" ' + 'class="xsmall dimmed" ' + 'style="text-decoration: none;" ' + 'target="_blank" >' + text + "</a>";
|
||||
},
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
var wrapper = document.createElement("div");
|
||||
const wrapper = document.createElement("div");
|
||||
if (this.suspended === false) {
|
||||
// process the hash of module info found
|
||||
for (var key of Object.keys(this.moduleList)) {
|
||||
for (const key of Object.keys(this.moduleList)) {
|
||||
let m = this.moduleList[key];
|
||||
|
||||
var message = document.createElement("div");
|
||||
const message = document.createElement("div");
|
||||
message.className = "small bright";
|
||||
|
||||
var icon = document.createElement("i");
|
||||
const icon = document.createElement("i");
|
||||
icon.className = "fa fa-exclamation-circle";
|
||||
icon.innerHTML = " ";
|
||||
message.appendChild(icon);
|
||||
|
||||
var updateInfoKeyName = m.behind === 1 ? "UPDATE_INFO_SINGLE" : "UPDATE_INFO_MULTIPLE";
|
||||
const updateInfoKeyName = m.behind === 1 ? "UPDATE_INFO_SINGLE" : "UPDATE_INFO_MULTIPLE";
|
||||
|
||||
var subtextHtml = this.translate(updateInfoKeyName, {
|
||||
let subtextHtml = this.translate(updateInfoKeyName, {
|
||||
COMMIT_COUNT: m.behind,
|
||||
BRANCH_NAME: m.current
|
||||
});
|
||||
|
||||
var text = document.createElement("span");
|
||||
const text = document.createElement("span");
|
||||
if (m.module === "default") {
|
||||
text.innerHTML = this.translate("UPDATE_NOTIFICATION");
|
||||
subtextHtml = this.diffLink(m, subtextHtml);
|
||||
@@ -100,7 +101,7 @@ Module.register("updatenotification", {
|
||||
|
||||
wrapper.appendChild(message);
|
||||
|
||||
var subtext = document.createElement("div");
|
||||
const subtext = document.createElement("div");
|
||||
subtext.innerHTML = subtextHtml;
|
||||
subtext.className = "xsmall dimmed";
|
||||
wrapper.appendChild(subtext);
|
||||
|
@@ -2,27 +2,36 @@
|
||||
{% set numSteps = forecast | calcNumSteps %}
|
||||
{% set currentStep = 0 %}
|
||||
<table class="{{ config.tableClass }}">
|
||||
{% if config.ignoreToday %}
|
||||
{% set forecast = forecast.splice(1) %}
|
||||
{% endif %}
|
||||
{% 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) %}
|
||||
{% if (currentStep == 0) and config.ignoreToday == false %}
|
||||
<td class="day">{{ "TODAY" | translate }}</td>
|
||||
{% elif (currentStep == 1) %}
|
||||
{% elif (currentStep == 1) and config.ignoreToday == false %}
|
||||
<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") }}
|
||||
{{ f.maxTemperature | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</td>
|
||||
<td class="align-right min-temp">
|
||||
{{ f.minTemperature | roundValue | unit("temperature") }}
|
||||
{{ f.minTemperature | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</td>
|
||||
{% if config.showPrecipitationAmount %}
|
||||
<td class="align-right bright precipitation">
|
||||
{{ f.precipitation | unit("precip") }}
|
||||
</td>
|
||||
{% if f.precipitationUnits %}
|
||||
<td class="align-right bright precipitation">
|
||||
{{ f.precipitation }}{{ f.precipitationUnits }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="align-right bright precipitation">
|
||||
{{ f.precipitation | unit("precip") }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% set currentStep = currentStep + 1 %}
|
||||
|
@@ -11,9 +11,15 @@
|
||||
{{ hour.temperature | roundValue | unit("temperature") }}
|
||||
</td>
|
||||
{% if config.showPrecipitationAmount %}
|
||||
<td class="align-right bright precipitation">
|
||||
{{ hour.precipitation | unit("precip") }}
|
||||
</td>
|
||||
{% if hour.precipitationUnits %}
|
||||
<td class="align-right bright precipitation">
|
||||
{{ hour.precipitation }}{{ hour.precipitationUnits }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="align-right bright precipitation">
|
||||
{{ hour.precipitation | unit("precip") }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% set currentStep = currentStep + 1 %}
|
||||
|
@@ -1,148 +1,3 @@
|
||||
# MagicMirror² Weather Module Weather Provider Development Documentation
|
||||
# Weather Module Weather Provider Development Documentation
|
||||
|
||||
This document describes the way to develop your own MagicMirror² weather module weather provider.
|
||||
|
||||
Table of Contents:
|
||||
|
||||
- The weather provider file: yourprovider.js
|
||||
- [Weather provider methods to implement](#weather-provider-methods-to-implement)
|
||||
- [Weather Provider instance methods](#weather-provider-instance-methods)
|
||||
- [WeatherObject](#weatherobject)
|
||||
|
||||
---
|
||||
|
||||
## The weather provider file: yourprovider.js
|
||||
|
||||
This is the script in which the weather provider will be defined. In its most simple form, the weather provider must implement the following:
|
||||
|
||||
```javascript
|
||||
WeatherProvider.register("yourprovider", {
|
||||
providerName: "YourProvider",
|
||||
|
||||
fetchCurrentWeather() {},
|
||||
|
||||
fetchWeatherForecast() {}
|
||||
});
|
||||
```
|
||||
|
||||
### Weather provider methods to implement
|
||||
|
||||
#### `fetchCurrentWeather()`
|
||||
|
||||
This method is called when the weather module tries to fetch the current weather of your provider. The implementation of this method is required 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 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.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
|
||||
|
||||
#### `init()`
|
||||
|
||||
Called when a weather provider is initialized.
|
||||
|
||||
#### `setConfig(config)`
|
||||
|
||||
Called to set the config, this config is the same as the weather module's config.
|
||||
|
||||
#### `start()`
|
||||
|
||||
Called when the weather provider is about to start.
|
||||
|
||||
#### `currentWeather()`
|
||||
|
||||
This returns a WeatherDay object for the current weather.
|
||||
|
||||
#### `weatherForecast()`
|
||||
|
||||
This returns an array of WeatherDay objects for the weather forecast.
|
||||
|
||||
#### `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.
|
||||
|
||||
#### `setCurrentWeather(currentWeatherObject)`
|
||||
|
||||
Set the currentWeather and notify the delegate that new information is available.
|
||||
|
||||
#### `setWeatherForecast(weatherForecastArray)`
|
||||
|
||||
Set the weatherForecastArray and notify the delegate that new information is available.
|
||||
|
||||
#### `setWeatherHourly(weatherHourlyArray)`
|
||||
|
||||
Set the weatherHourlyArray and notify the delegate that new information is available.
|
||||
|
||||
#### `setFetchedLocation(name)`
|
||||
|
||||
Set the fetched location name.
|
||||
|
||||
#### `updateAvailable()`
|
||||
|
||||
Notify the delegate that new weather is available.
|
||||
|
||||
#### `fetchData(url, method, data)`
|
||||
|
||||
A convenience function to make requests. It returns a promise.
|
||||
|
||||
### WeatherObject
|
||||
|
||||
| Property | Type | Value/Unit |
|
||||
| -------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| units | `string` | Gets initialized with the constructor. <br> Possible values: `metric`, `imperial` |
|
||||
| tempUnits | `string` | Gets initialized with the constructor. <br> Possible values: `metric`, `imperial` |
|
||||
| windUnits | `string` | Gets initialized with the constructor. <br> Possible values: `metric`, `imperial` |
|
||||
| date | `object` | [Moment.js](https://momentjs.com/) object of the time/date. |
|
||||
| windSpeed | `number` | Metric: `meter/second` <br> Imperial: `miles/hour` |
|
||||
| windDirection | `number` | Direction of the wind in degrees. |
|
||||
| sunrise | `object` | [Moment.js](https://momentjs.com/) object of sunrise. |
|
||||
| sunset | `object` | [Moment.js](https://momentjs.com/) object of sunset. |
|
||||
| temperature | `number` | Current temperature |
|
||||
| minTemperature | `number` | Lowest temperature of the day. |
|
||||
| maxTemperature | `number` | Highest temperature of the day. |
|
||||
| weatherType | `string` | Icon name of the weather type. <br> Possible values: [WeatherIcons](https://www.npmjs.com/package/weathericons) |
|
||||
| humidity | `number` | Percentage of humidity |
|
||||
| rain | `number` | Metric: `millimeters` <br> Imperial: `inches` |
|
||||
| snow | `number` | Metric: `millimeters` <br> Imperial: `inches` |
|
||||
| precipitation | `number` | Metric: `millimeters` <br> Imperial: `inches` <br> UK Met Office provider: `percent` |
|
||||
|
||||
#### Current weather
|
||||
|
||||
For the current weather object the following properties are required:
|
||||
|
||||
- humidity
|
||||
- sunrise
|
||||
- sunset
|
||||
- temperature
|
||||
- units
|
||||
- weatherType
|
||||
- windDirection
|
||||
- windSpeed
|
||||
|
||||
#### Weather forecast
|
||||
|
||||
For the forecast weather object the following properties are required:
|
||||
|
||||
- date
|
||||
- maxTemperature
|
||||
- minTemperature
|
||||
- rain
|
||||
- units
|
||||
- weatherType
|
||||
For how to develop your own weather provider, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/development/weather-provider.html).
|
||||
|
@@ -8,7 +8,8 @@
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Dark Sky.
|
||||
* Note that the Dark Sky API does not provide rainfall. Instead it provides snowfall and precipitation probability
|
||||
* Note that the Dark Sky API does not provide rainfall. Instead it provides
|
||||
* snowfall and precipitation probability
|
||||
*/
|
||||
WeatherProvider.register("darksky", {
|
||||
// Set the name of the provider.
|
||||
@@ -98,7 +99,8 @@ WeatherProvider.register("darksky", {
|
||||
weather.snow = 0;
|
||||
|
||||
// The API will return centimeters if units is 'si' and will return inches for 'us'
|
||||
// Note that the Dark Sky API does not provide rainfall. Instead it provides snowfall and precipitation probability
|
||||
// Note that the Dark Sky API does not provide rainfall.
|
||||
// Instead it provides snowfall and precipitation probability
|
||||
if (forecast.hasOwnProperty("precipAccumulation")) {
|
||||
if (this.config.units === "imperial" && !isNaN(forecast.precipAccumulation)) {
|
||||
weather.snow = forecast.precipAccumulation;
|
||||
|
644
modules/default/weather/providers/envcanada.js
Normal file
644
modules/default/weather/providers/envcanada.js
Normal file
@@ -0,0 +1,644 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
* Provider: Environment Canada (EC)
|
||||
*
|
||||
* This class is a provider for Environment Canada MSC Datamart
|
||||
* Note that this is only for Canadian locations and does not require an API key (access is anonymous)
|
||||
*
|
||||
* EC Documentation at following links:
|
||||
* https://dd.weather.gc.ca/citypage_weather/schema/
|
||||
* https://eccc-msc.github.io/open-data/msc-datamart/readme_en/
|
||||
*
|
||||
* This module supports Canadian locations only and requires 2 additional config parms:
|
||||
*
|
||||
* siteCode - the city/town unique identifier for which weather is to be displayed. Format is 's0000000'.
|
||||
*
|
||||
* provCode - the 2-character province code for the selected city/town.
|
||||
*
|
||||
* Example: for Toronto, Ontario, the following parms would be used
|
||||
*
|
||||
* siteCode: 's0000458',
|
||||
* provCode: 'ON'
|
||||
*
|
||||
* To determine the siteCode and provCode values for a Canadian city/town, look at the Environment Canada document
|
||||
* at https://dd.weather.gc.ca/citypage_weather/docs/site_list_en.csv (or site_list_fr.csv). There you will find a table
|
||||
* with locations you can search under column B (English Names), with the corresponding siteCode under
|
||||
* column A (Codes) and provCode under column C (Province).
|
||||
*
|
||||
* Original by Kevin Godin
|
||||
*
|
||||
* License to use Environment Canada (EC) data is detailed here:
|
||||
* https://eccc-msc.github.io/open-data/licence/readme_en/
|
||||
*
|
||||
*/
|
||||
|
||||
WeatherProvider.register("envcanada", {
|
||||
// Set the name of the provider for debugging and alerting purposes (eg. provide eye-catcher)
|
||||
providerName: "Environment Canada",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
siteCode: "s1234567",
|
||||
provCode: "ON"
|
||||
},
|
||||
|
||||
//
|
||||
// Set config values (equates to weather module config values). Also set values pertaining to caching of
|
||||
// Today's temperature forecast (for use in the Forecast functions below)
|
||||
//
|
||||
setConfig: function (config) {
|
||||
this.config = config;
|
||||
|
||||
this.todayTempCacheMin = 0;
|
||||
this.todayTempCacheMax = 0;
|
||||
this.todayCached = false;
|
||||
this.cacheCurrentTemp = 999;
|
||||
},
|
||||
|
||||
//
|
||||
// Called when the weather provider is started
|
||||
//
|
||||
start: function () {
|
||||
Log.info(`Weather provider: ${this.providerName} started.`);
|
||||
this.setFetchedLocation(this.config.location);
|
||||
|
||||
// Ensure kmH are ignored since these are custom-handled by this Provider
|
||||
|
||||
this.config.useKmh = false;
|
||||
},
|
||||
|
||||
//
|
||||
// Override the fetchCurrentWeather method to query EC and construct a Current weather object
|
||||
//
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl(), "GET")
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
return;
|
||||
}
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
||||
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load EnvCanada site data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
//
|
||||
// Override the fetchWeatherForecast method to query EC and construct Forecast weather objects
|
||||
//
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl(), "GET")
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
return;
|
||||
}
|
||||
const forecastWeather = this.generateWeatherObjectsFromForecast(data);
|
||||
|
||||
this.setWeatherForecast(forecastWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load EnvCanada forecast data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
//
|
||||
// Override the fetchWeatherHourly method to query EC and construct Forecast weather objects
|
||||
//
|
||||
fetchWeatherHourly() {
|
||||
this.fetchData(this.getUrl(), "GET")
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
return;
|
||||
}
|
||||
const hourlyWeather = this.generateWeatherObjectsFromHourly(data);
|
||||
|
||||
this.setWeatherHourly(hourlyWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load EnvCanada hourly data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
//
|
||||
// Override fetchData function to handle XML document (base function assumes JSON)
|
||||
//
|
||||
fetchData: function (url, method = "GET", data = null) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const request = new XMLHttpRequest();
|
||||
request.open(method, url, true);
|
||||
request.onreadystatechange = function () {
|
||||
if (this.readyState === 4) {
|
||||
if (this.status === 200) {
|
||||
resolve(this.responseXML);
|
||||
} else {
|
||||
reject(request);
|
||||
}
|
||||
}
|
||||
};
|
||||
request.send();
|
||||
});
|
||||
},
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Environment Canada methods - not part of the standard Provider methods
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
//
|
||||
// Build the EC URL based on the Site Code and Province Code specified in the config parms. Note that the
|
||||
// URL defaults to the Englsih version simply because there is no language dependancy in the data
|
||||
// being accessed. This is only pertinent when using the EC data elements that contain a textual forecast.
|
||||
//
|
||||
// Also note that access is supported through a proxy service (thingproxy.freeboard.io) to mitigate
|
||||
// CORS errors when accessing EC
|
||||
//
|
||||
getUrl() {
|
||||
return "https://thingproxy.freeboard.io/fetch/https://dd.weather.gc.ca/citypage_weather/xml/" + this.config.provCode + "/" + this.config.siteCode + "_e.xml";
|
||||
},
|
||||
|
||||
//
|
||||
// Generate a WeatherObject based on current EC weather conditions
|
||||
//
|
||||
|
||||
generateWeatherObjectFromCurrentWeather(ECdoc) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
// There are instances where EC will update weather data and current temperature will not be
|
||||
// provided. While this is a defect in the EC systems, we need to accommodate to avoid a current temp
|
||||
// of NaN being displayed. Therefore... whenever we get a valid current temp from EC, we will cache
|
||||
// the value. Whenever EC data is missing current temp, we will provide the cached value
|
||||
// instead. This is reasonable since the cached value will typically be accurate within the previous
|
||||
// hour. The only time this does not work as expected is when MM is restarted and the first query to
|
||||
// EC finds no current temp. In this scenario, MM will end up displaying a current temp of null;
|
||||
|
||||
if (ECdoc.querySelector("siteData currentConditions temperature").textContent) {
|
||||
currentWeather.temperature = this.convertTemp(ECdoc.querySelector("siteData currentConditions temperature").textContent);
|
||||
this.cacheCurrentTemp = currentWeather.temperature;
|
||||
} else {
|
||||
currentWeather.temperature = this.cacheCurrentTemp;
|
||||
}
|
||||
|
||||
currentWeather.windSpeed = this.convertWind(ECdoc.querySelector("siteData currentConditions wind speed").textContent);
|
||||
|
||||
currentWeather.windDirection = ECdoc.querySelector("siteData currentConditions wind bearing").textContent;
|
||||
|
||||
currentWeather.humidity = ECdoc.querySelector("siteData currentConditions relativeHumidity").textContent;
|
||||
|
||||
// Ensure showPrecipitationAmount is forced to false. EC does not really provide POP for current day
|
||||
// and this feature for the weather module (current only) is sort of broken in that it wants
|
||||
// to say POP but will display precip as an accumulated amount vs. a percentage.
|
||||
|
||||
this.config.showPrecipitationAmount = false;
|
||||
|
||||
//
|
||||
// If the module config wants to showFeelsLike... default to the current temperature.
|
||||
// Check for EC wind chill and humidex values and overwrite the feelsLikeTemp value.
|
||||
// This assumes that the EC current conditions will never contain both a wind chill
|
||||
// and humidex temperature.
|
||||
//
|
||||
|
||||
if (this.config.showFeelsLike) {
|
||||
currentWeather.feelsLikeTemp = currentWeather.temperature;
|
||||
|
||||
if (ECdoc.querySelector("siteData currentConditions windChill")) {
|
||||
currentWeather.feelsLikeTemp = this.convertTemp(ECdoc.querySelector("siteData currentConditions windChill").textContent);
|
||||
}
|
||||
|
||||
if (ECdoc.querySelector("siteData currentConditions humidex")) {
|
||||
currentWeather.feelsLikeTemp = this.convertTemp(ECdoc.querySelector("siteData currentConditions humidex").textContent);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Need to map EC weather icon to MM weatherType values
|
||||
//
|
||||
|
||||
currentWeather.weatherType = this.convertWeatherType(ECdoc.querySelector("siteData currentConditions iconCode").textContent);
|
||||
|
||||
//
|
||||
// Capture the sunrise and sunset values from EC data
|
||||
//
|
||||
|
||||
const sunList = ECdoc.querySelectorAll("siteData riseSet dateTime");
|
||||
|
||||
currentWeather.sunrise = moment(sunList[1].querySelector("timeStamp").textContent, "YYYYMMDDhhmmss");
|
||||
currentWeather.sunset = moment(sunList[3].querySelector("timeStamp").textContent, "YYYYMMDDhhmmss");
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
//
|
||||
// Generate an array of WeatherObjects based on EC weather forecast
|
||||
//
|
||||
|
||||
generateWeatherObjectsFromForecast(ECdoc) {
|
||||
// Declare an array to hold each day's forecast object
|
||||
|
||||
const days = [];
|
||||
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
const foreBaseDates = ECdoc.querySelectorAll("siteData forecastGroup dateTime");
|
||||
const baseDate = foreBaseDates[1].querySelector("timeStamp").textContent;
|
||||
|
||||
weather.date = moment(baseDate, "YYYYMMDDhhmmss");
|
||||
|
||||
const foreGroup = ECdoc.querySelectorAll("siteData forecastGroup forecast");
|
||||
|
||||
// For simplicity, we will only accumulate precipitation and will not try to break out
|
||||
// rain vs snow accumulations
|
||||
|
||||
weather.rain = null;
|
||||
weather.snow = null;
|
||||
weather.precipitation = null;
|
||||
|
||||
//
|
||||
// The EC forecast is held in a 12-element array - Elements 0 to 11 - with each day encompassing
|
||||
// 2 elements. the first element for a day details the Today (daytime) forecast while the second
|
||||
// element details the Tonight (nightime) forecast. Element 0 is always for the current day.
|
||||
//
|
||||
// However... the forecast is somewhat 'rolling'.
|
||||
//
|
||||
// If the EC forecast is queried in the morning, then Element 0 will contain Current
|
||||
// Today and Element 1 will contain Current Tonight. From there, the next 5 days of forecast will be
|
||||
// contained in Elements 2/3, 4/5, 6/7, 8/9, and 10/11. This module will create a 6-day forecast using
|
||||
// all of these Elements.
|
||||
//
|
||||
// But, if the EC forecast is queried in late afternoon, the Current Today forecast will be rolled
|
||||
// off and Element 0 will contain Current Tonight. From there, the next 5 days will be contained in
|
||||
// Elements 1/2, 3/4, 5/6, 7/8, and 9/10. As well, Elelement 11 will contain a forecast for a 6th day,
|
||||
// but only for the Today portion (not Tonight). This module will create a 6-day forecast using
|
||||
// Elements 0 to 11, and will ignore the additional Todat forecast in Element 11.
|
||||
//
|
||||
// We need to determine if Element 0 is showing the forecast for Current Today or Current Tonight.
|
||||
// This is required to understand how Min and Max temperature will be determined, and to understand
|
||||
// where the next day's (aka Tomorrow's) forecast is located in the forecast array.
|
||||
//
|
||||
|
||||
let nextDay = 0;
|
||||
let lastDay = 0;
|
||||
const currentTemp = ECdoc.querySelector("siteData currentConditions temperature").textContent;
|
||||
|
||||
//
|
||||
// If the first Element is Current Today, look at Current Today and Current Tonight for the current day.
|
||||
//
|
||||
|
||||
if (foreGroup[0].querySelector("period[textForecastName='Today']")) {
|
||||
this.todaytempCacheMin = 0;
|
||||
this.todaytempCacheMax = 0;
|
||||
this.todayCached = true;
|
||||
|
||||
this.setMinMaxTemps(weather, foreGroup, 0, true, currentTemp);
|
||||
|
||||
this.setPrecipitation(weather, foreGroup, 0);
|
||||
|
||||
//
|
||||
// Set the Element number that will reflect where the next day's forecast is located. Also set
|
||||
// the Element number where the end of the forecast will be. This is important because of the
|
||||
// rolling nature of the EC forecast. In the current scenario (Today and Tonight are present
|
||||
// in elements 0 and 11, we know that we will have 6 full days of forecasts and we will use
|
||||
// them. We will set lastDay such that we iterate through all 12 elements of the forecast.
|
||||
//
|
||||
|
||||
nextDay = 2;
|
||||
lastDay = 12;
|
||||
}
|
||||
|
||||
//
|
||||
// If the first Element is Current Tonight, look at Tonight only for the current day.
|
||||
//
|
||||
if (foreGroup[0].querySelector("period[textForecastName='Tonight']")) {
|
||||
this.setMinMaxTemps(weather, foreGroup, 0, false, currentTemp);
|
||||
|
||||
this.setPrecipitation(weather, foreGroup, 0);
|
||||
|
||||
//
|
||||
// Set the Element number that will reflect where the next day's forecast is located. Also set
|
||||
// the Element number where the end of the forecast will be. This is important because of the
|
||||
// rolling nature of the EC forecast. In the current scenario (only Current Tonight is present
|
||||
// in Element 0, we know that we will have 6 full days of forecasts PLUS a half-day and
|
||||
// forecast in the final element. Because we will only use full day forecasts, we set the
|
||||
// lastDay number to ensure we ignore that final half-day (in forecast Element 11).
|
||||
//
|
||||
|
||||
nextDay = 1;
|
||||
lastDay = 11;
|
||||
}
|
||||
|
||||
//
|
||||
// Need to map EC weather icon to MM weatherType values. Always pick the first Element's icon to
|
||||
// reflect either Today or Tonight depending on what the forecast is showing in Element 0.
|
||||
//
|
||||
|
||||
weather.weatherType = this.convertWeatherType(foreGroup[0].querySelector("abbreviatedForecast iconCode").textContent);
|
||||
|
||||
// Push the weather object into the forecast array.
|
||||
|
||||
days.push(weather);
|
||||
|
||||
//
|
||||
// Now do the the rest of the forecast starting at nextDay. We will process each day using 2 EC
|
||||
// forecast Elements. This will address the fact that the EC forecast always includes Today and
|
||||
// Tonight for each day. This is why we iterate through the forecast by a a count of 2, with each
|
||||
// iteration looking at the current Element and the next Element.
|
||||
//
|
||||
|
||||
let lastDate = moment(baseDate, "YYYYMMDDhhmmss");
|
||||
|
||||
for (let stepDay = nextDay; stepDay < lastDay; stepDay += 2) {
|
||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
// Add 1 to the date to reflect the current forecast day we are building
|
||||
|
||||
lastDate = lastDate.add(1, "day");
|
||||
weather.date = moment(lastDate, "X");
|
||||
|
||||
// Capture the temperatures for the current Element and the next Element in order to set
|
||||
// the Min and Max temperatures for the forecast
|
||||
|
||||
this.setMinMaxTemps(weather, foreGroup, stepDay, true, currentTemp);
|
||||
|
||||
weather.rain = null;
|
||||
weather.snow = null;
|
||||
weather.precipitation = null;
|
||||
|
||||
this.setPrecipitation(weather, foreGroup, stepDay);
|
||||
|
||||
//
|
||||
// Need to map EC weather icon to MM weatherType values. Always pick the first Element icon.
|
||||
//
|
||||
|
||||
weather.weatherType = this.convertWeatherType(foreGroup[stepDay].querySelector("abbreviatedForecast iconCode").textContent);
|
||||
|
||||
// Push the weather object into the forecast array.
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
//
|
||||
// Generate an array of WeatherObjects based on EC hourly weather forecast
|
||||
//
|
||||
|
||||
generateWeatherObjectsFromHourly(ECdoc) {
|
||||
// Declare an array to hold each hour's forecast object
|
||||
|
||||
const hours = [];
|
||||
|
||||
// Get local timezone UTC offset so that each hourly time can be calculated properly
|
||||
|
||||
const baseHours = ECdoc.querySelectorAll("siteData hourlyForecastGroup dateTime");
|
||||
const hourOffset = baseHours[1].getAttribute("UTCOffset");
|
||||
|
||||
//
|
||||
// The EC hourly forecast is held in a 24-element array - Elements 0 to 23 - with Element 0 holding
|
||||
// the forecast for the next 'on the hour' timeslot. This means the array is a rolling 24 hours.
|
||||
//
|
||||
|
||||
const hourGroup = ECdoc.querySelectorAll("siteData hourlyForecastGroup hourlyForecast");
|
||||
|
||||
for (let stepHour = 0; stepHour < 24; stepHour += 1) {
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
// Determine local time by applying UTC offset to the forecast timestamp
|
||||
|
||||
const foreTime = moment(hourGroup[stepHour].getAttribute("dateTimeUTC"), "YYYYMMDDhhmmss");
|
||||
const currTime = foreTime.add(hourOffset, "hours");
|
||||
weather.date = moment(currTime, "X");
|
||||
|
||||
// Capture the temperature
|
||||
|
||||
weather.temperature = this.convertTemp(hourGroup[stepHour].querySelector("temperature").textContent);
|
||||
|
||||
// Capture Likelihood of Precipitation (LOP) and unit-of-measure values
|
||||
|
||||
const precipLOP = hourGroup[stepHour].querySelector("lop").textContent * 1.0;
|
||||
|
||||
if (precipLOP > 0) {
|
||||
weather.precipitation = precipLOP;
|
||||
weather.precipitationUnits = hourGroup[stepHour].querySelector("lop").getAttribute("units");
|
||||
}
|
||||
|
||||
//
|
||||
// Need to map EC weather icon to MM weatherType values. Always pick the first Element icon.
|
||||
//
|
||||
|
||||
weather.weatherType = this.convertWeatherType(hourGroup[stepHour].querySelector("iconCode").textContent);
|
||||
|
||||
// Push the weather object into the forecast array.
|
||||
|
||||
hours.push(weather);
|
||||
}
|
||||
|
||||
return hours;
|
||||
},
|
||||
//
|
||||
// Determine Min and Max temp based on a supplied Forecast Element index and a boolen that denotes if
|
||||
// the next Forecast element should be considered - i.e. look at Today *and* Tonight vs.Tonight-only
|
||||
//
|
||||
|
||||
setMinMaxTemps(weather, foreGroup, today, fullDay, currentTemp) {
|
||||
const todayTemp = foreGroup[today].querySelector("temperatures temperature").textContent;
|
||||
|
||||
const todayClass = foreGroup[today].querySelector("temperatures temperature").getAttribute("class");
|
||||
|
||||
//
|
||||
// The following logic is largely aimed at accommodating the Current day's forecast whereby we
|
||||
// can have either Current Today+Current Tonight or only Current Tonight.
|
||||
//
|
||||
// If fullDay is false, then we only have Tonight for the current day's forecast - meaning we have
|
||||
// lost a min or max temp value for the day. Therefore, we will see if we were able to cache the the
|
||||
// Today forecast for the current day. If we have, we will use them. If we do not have the cached values,
|
||||
// it means that MM or the Computer has been restarted since the time EC rolled off Today from the
|
||||
// forecast. In this scenario, we will simply default to the Current Conditions temperature and then
|
||||
// check the Tonight temperature.
|
||||
//
|
||||
|
||||
if (fullDay === false) {
|
||||
if (this.todayCached === true) {
|
||||
weather.minTemperature = this.todayTempCacheMin;
|
||||
weather.maxTemperature = this.todayTempCacheMax;
|
||||
} else {
|
||||
weather.minTemperature = this.convertTemp(currentTemp);
|
||||
weather.maxTemperature = weather.minTemperature;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// We will check to see if the current Element's temperature is Low or High and set weather values
|
||||
// accordingly. We will also check the condition where fullDay is true *and* we are looking at forecast
|
||||
// element 0. This is a special case where we will cache temperature values so that we have them later
|
||||
// in the current day when the Current Today element rolls off and we have Current Tonight only.
|
||||
//
|
||||
|
||||
if (todayClass === "low") {
|
||||
weather.minTemperature = this.convertTemp(todayTemp);
|
||||
if (today === 0 && fullDay === true) {
|
||||
this.todayTempCacheMin = weather.minTemperature;
|
||||
}
|
||||
}
|
||||
|
||||
if (todayClass === "high") {
|
||||
weather.maxTemperature = this.convertTemp(todayTemp);
|
||||
if (today === 0 && fullDay === true) {
|
||||
this.todayTempCacheMax = weather.maxTemperature;
|
||||
}
|
||||
}
|
||||
|
||||
const nextTemp = foreGroup[today + 1].querySelector("temperatures temperature").textContent;
|
||||
|
||||
const nextClass = foreGroup[today + 1].querySelector("temperatures temperature").getAttribute("class");
|
||||
|
||||
if (fullDay === true) {
|
||||
if (nextClass === "low") {
|
||||
weather.minTemperature = this.convertTemp(nextTemp);
|
||||
}
|
||||
|
||||
if (nextClass === "high") {
|
||||
weather.maxTemperature = this.convertTemp(nextTemp);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Check for a Precipitation forecast. EC can provide a forecast in 2 ways: either an accumulation figure
|
||||
// or a POP percentage. If there is a POP, then that is what the module will show. If there is an accumulation,
|
||||
// then it will be displayed ONLY if no POP is present.
|
||||
//
|
||||
// POP Logic: By default, we want to show the POP for 'daytime' since we are presuming that is what
|
||||
// people are more interested in seeing. While EC provides a separate POP for daytime and nightime portions
|
||||
// of each day, the weather module does not really allow for that view of a daily forecast. There we will
|
||||
// ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
|
||||
// the nightime forecast after a certain point in the afternoon. As such, we will be showing the nightime POP
|
||||
// (if one exists) in that specific scenario.
|
||||
//
|
||||
// Accumulation Logic: Similar to POP, we want to show accumulation for 'daytime' since we presume that is what
|
||||
// people are interested in seeing. While EC provides a separate accumulation for daytime and nightime portions
|
||||
// of each day, the weather module does not really allow for that view of a daily forecast. There we will
|
||||
// ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
|
||||
// the nightime forecast after a certain point in that specific scenario.
|
||||
//
|
||||
|
||||
setPrecipitation(weather, foreGroup, today) {
|
||||
if (foreGroup[today].querySelector("precipitation accumulation")) {
|
||||
weather.precipitation = foreGroup[today].querySelector("precipitation accumulation amount").textContent * 1.0;
|
||||
|
||||
weather.precipitationUnits = " " + foreGroup[today].querySelector("precipitation accumulation amount").getAttribute("units");
|
||||
|
||||
if (this.config.units === "imperial") {
|
||||
if (weather.precipitationUnits === " cm") {
|
||||
weather.precipitation = (weather.precipitation * 0.394).toFixed(2);
|
||||
weather.precipitationUnits = " in";
|
||||
}
|
||||
if (weather.precipitationUnits === " mm") {
|
||||
weather.precipitation = (weather.precipitation * 0.0394).toFixed(2);
|
||||
weather.precipitationUnits = " in";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check Today element for POP
|
||||
|
||||
if (foreGroup[today].querySelector("abbreviatedForecast pop").textContent > 0) {
|
||||
weather.precipitation = foreGroup[today].querySelector("abbreviatedForecast pop").textContent;
|
||||
weather.precipitationUnits = foreGroup[today].querySelector("abbreviatedForecast pop").getAttribute("units");
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Unit conversions
|
||||
//
|
||||
//
|
||||
// Convert C to F temps
|
||||
//
|
||||
convertTemp(temp) {
|
||||
if (this.config.tempUnits === "imperial") {
|
||||
return 1.8 * temp + 32;
|
||||
} else {
|
||||
return temp;
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Convert km/h to mph
|
||||
//
|
||||
convertWind(kilo) {
|
||||
if (this.config.windUnits === "imperial") {
|
||||
return kilo / 1.609344;
|
||||
} else {
|
||||
return kilo;
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Convert the icons to a more usable name.
|
||||
//
|
||||
convertWeatherType(weatherType) {
|
||||
const weatherTypes = {
|
||||
"00": "day-sunny",
|
||||
"01": "day-sunny",
|
||||
"02": "day-sunny-overcast",
|
||||
"03": "day-cloudy",
|
||||
"04": "day-cloudy",
|
||||
"05": "day-cloudy",
|
||||
"06": "day-sprinkle",
|
||||
"07": "day-showers",
|
||||
"08": "day-snow",
|
||||
"09": "day-thunderstorm",
|
||||
10: "cloud",
|
||||
11: "showers",
|
||||
12: "rain",
|
||||
13: "rain",
|
||||
14: "sleet",
|
||||
15: "sleet",
|
||||
16: "snow",
|
||||
17: "snow",
|
||||
18: "snow",
|
||||
19: "thunderstorm",
|
||||
20: "cloudy",
|
||||
21: "cloudy",
|
||||
22: "day-cloudy",
|
||||
23: "day-haze",
|
||||
24: "fog",
|
||||
25: "snow-wind",
|
||||
26: "sleet",
|
||||
27: "sleet",
|
||||
28: "rain",
|
||||
29: "na",
|
||||
30: "night-clear",
|
||||
31: "night-clear",
|
||||
32: "night-partly-cloudy",
|
||||
33: "night-alt-cloudy",
|
||||
34: "night-alt-cloudy",
|
||||
35: "night-partly-cloudy",
|
||||
36: "night-alt-showers",
|
||||
37: "night-rain-mix",
|
||||
38: "night-alt-snow",
|
||||
39: "night-thunderstorm",
|
||||
40: "snow-wind",
|
||||
41: "tornado",
|
||||
42: "tornado",
|
||||
43: "windy",
|
||||
44: "smoke",
|
||||
45: "sandstorm",
|
||||
46: "thunderstorm",
|
||||
47: "thunderstorm",
|
||||
48: "tornado"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
}
|
||||
});
|
@@ -18,10 +18,10 @@ WeatherProvider.register("openweathermap", {
|
||||
defaults: {
|
||||
apiVersion: "2.5",
|
||||
apiBase: "https://api.openweathermap.org/data/",
|
||||
weatherEndpoint: "",
|
||||
weatherEndpoint: "", // can be "onecall", "forecast" or "weather" (for current)
|
||||
locationID: false,
|
||||
location: false,
|
||||
lat: 0,
|
||||
lat: 0, // the onecall endpoint needs lat / lon values, it doesn'T support the locationId
|
||||
lon: 0,
|
||||
apiKey: ""
|
||||
},
|
||||
@@ -30,16 +30,14 @@ WeatherProvider.register("openweathermap", {
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data || !data.main || typeof data.main.temp === "undefined") {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
const weatherData = this.generateWeatherObjectsFromOnecall(data);
|
||||
this.setCurrentWeather(weatherData.current);
|
||||
this.setFetchedLocation(`${data.timezone}`);
|
||||
} else {
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
}
|
||||
|
||||
this.setFetchedLocation(`${data.name}, ${data.sys.country}`);
|
||||
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
@@ -51,16 +49,15 @@ WeatherProvider.register("openweathermap", {
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data || !data.list || !data.list.length) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
const weatherData = this.generateWeatherObjectsFromOnecall(data);
|
||||
this.setWeatherForecast(weatherData.days);
|
||||
this.setFetchedLocation(`${data.timezone}`);
|
||||
} else {
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data.list);
|
||||
this.setWeatherForecast(forecast);
|
||||
this.setFetchedLocation(`${data.city.name}, ${data.city.country}`);
|
||||
}
|
||||
|
||||
this.setFetchedLocation(`${data.city.name}, ${data.city.country}`);
|
||||
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data.list);
|
||||
this.setWeatherForecast(forecast);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
@@ -92,7 +89,7 @@ WeatherProvider.register("openweathermap", {
|
||||
/**
|
||||
* Overrides method for setting config to check if endpoint is correct for hourly
|
||||
*
|
||||
* @param config
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
setConfig(config) {
|
||||
this.config = config;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* global WeatherProvider, WeatherObject, SunCalc */
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
@@ -7,9 +7,8 @@
|
||||
* 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.
|
||||
* This class is a provider for SMHI (Sweden only). Metric system is the only
|
||||
* supported unit.
|
||||
*/
|
||||
WeatherProvider.register("smhi", {
|
||||
providerName: "SMHI",
|
||||
@@ -56,11 +55,11 @@ WeatherProvider.register("smhi", {
|
||||
/**
|
||||
* Overrides method for setting config with checks for the precipitationValue being unset or invalid
|
||||
*
|
||||
* @param config
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
setConfig(config) {
|
||||
this.config = config;
|
||||
if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) == -1) {
|
||||
if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) === -1) {
|
||||
console.log("invalid or not set: " + config.precipitationValue);
|
||||
config.precipitationValue = this.defaults.precipitationValue;
|
||||
}
|
||||
@@ -69,12 +68,13 @@ WeatherProvider.register("smhi", {
|
||||
/**
|
||||
* 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
|
||||
* @param {object[]} times Array of time objects
|
||||
* @returns {object} The weatherdata closest to the current time
|
||||
*/
|
||||
getClosestToCurrentTime(times) {
|
||||
let now = moment();
|
||||
let minDiff = undefined;
|
||||
for (time of times) {
|
||||
for (const time of times) {
|
||||
let diff = Math.abs(moment(time.validTime).diff(now));
|
||||
if (!minDiff || diff < Math.abs(moment(minDiff.validTime).diff(now))) {
|
||||
minDiff = time;
|
||||
@@ -85,6 +85,8 @@ WeatherProvider.register("smhi", {
|
||||
|
||||
/**
|
||||
* Get the forecast url for the configured coordinates
|
||||
*
|
||||
* @returns {string} the url for the specified coordinates
|
||||
*/
|
||||
getURL() {
|
||||
let lon = this.config.lon;
|
||||
@@ -97,25 +99,25 @@ WeatherProvider.register("smhi", {
|
||||
* 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
|
||||
* @param {object} weatherData Weatherdata to convert
|
||||
* @param {object} coordinates Coordinates of the locations of the weather
|
||||
* @returns {WeatherObject} The converted weatherdata at the specified location
|
||||
*/
|
||||
convertWeatherDataToObject(weatherData, coordinates) {
|
||||
let currentWeather = new WeatherObject("metric", "metric", "metric"); //Weather data is only for Sweden and nobody in Sweden would use imperial
|
||||
// Weather data is only for Sweden and nobody in Sweden would use imperial
|
||||
let currentWeather = new WeatherObject("metric", "metric", "metric");
|
||||
|
||||
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.updateSunTime(coordinates.lat, coordinates.lon);
|
||||
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));
|
||||
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), currentWeather.isDayTime());
|
||||
|
||||
//Determine the precipitation amount and category and update the weatherObject with it, the valuetype to use can be configured or uses median as default.
|
||||
// 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
|
||||
@@ -143,19 +145,18 @@ WeatherProvider.register("smhi", {
|
||||
/**
|
||||
* Takes all of the data points and converts it to one WeatherObject per day.
|
||||
*
|
||||
* @param allWeatherData
|
||||
* @param coordinates
|
||||
* @param allWeatherData
|
||||
* @param coordinates
|
||||
* @param {object[]} allWeatherData Array of weatherdata
|
||||
* @param {object} coordinates Coordinates of the locations of the weather
|
||||
* @returns {WeatherObject[]} Array of weatherobjects
|
||||
*/
|
||||
convertWeatherDataGroupedByDay(allWeatherData, coordinates) {
|
||||
var currentWeather;
|
||||
let currentWeather;
|
||||
let result = [];
|
||||
|
||||
let allWeatherObjects = this.fillInGaps(allWeatherData).map((weatherData) => this.convertWeatherDataToObject(weatherData, coordinates));
|
||||
var dayWeatherTypes = [];
|
||||
let dayWeatherTypes = [];
|
||||
|
||||
for (weatherObject of allWeatherObjects) {
|
||||
for (const 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);
|
||||
@@ -170,7 +171,7 @@ WeatherProvider.register("smhi", {
|
||||
}
|
||||
|
||||
//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)) {
|
||||
if (weatherObject.isDayTime()) {
|
||||
dayWeatherTypes.push(weatherObject.weatherType);
|
||||
}
|
||||
if (dayWeatherTypes.length > 0) {
|
||||
@@ -191,37 +192,31 @@ WeatherProvider.register("smhi", {
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolve coordinates from the response data (probably preferably to use this if it's not matching the config values exactly)
|
||||
* Resolve coordinates from the response data (probably preferably to use
|
||||
* this if it's not matching the config values exactly)
|
||||
*
|
||||
* @param data
|
||||
* @param {object} data Response data from the weather service
|
||||
* @returns {{lon, lat}} the lat/long coordinates of the 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
|
||||
* @param {object[]} data Response data from the weather service
|
||||
* @returns {object[]} Given data with filled gaps
|
||||
*/
|
||||
fillInGaps(data) {
|
||||
let result = [];
|
||||
for (var i = 1; i < data.length; i++) {
|
||||
for (let 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++) {
|
||||
for (let j = 0; j < hours; j++) {
|
||||
let current = Object.assign({}, data[i]);
|
||||
current.validTime = from.clone().add(j, "hours").toISOString();
|
||||
result.push(current);
|
||||
@@ -231,84 +226,81 @@ WeatherProvider.register("smhi", {
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method to fetch a property from the returned data set.
|
||||
* The returned values is an array with always one value in it.
|
||||
* Helper method to get a property from the returned data set.
|
||||
*
|
||||
* @param currentWeatherData
|
||||
* @param name
|
||||
* @param currentWeatherData
|
||||
* @param name
|
||||
* @param {object} currentWeatherData Weatherdata to get from
|
||||
* @param {string} name The name of the property
|
||||
* @returns {*} The value of the property in the weatherdata
|
||||
*/
|
||||
paramValue(currentWeatherData, name) {
|
||||
return currentWeatherData.parameters.filter((p) => p.name == name).flatMap((p) => p.values)[0];
|
||||
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.
|
||||
* Map the icon value from SMHI 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.
|
||||
* SMHI's description of what the numeric value means is the comment after the case.
|
||||
*
|
||||
* @param input
|
||||
* @param isDayTime
|
||||
* @param input
|
||||
* @param isDayTime
|
||||
* @param {number} input The SMHI icon value
|
||||
* @param {boolean} isDayTime True if the icon should be for daytime, false for nighttime
|
||||
* @returns {string} The icon name for the MagicMirror
|
||||
*/
|
||||
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
|
||||
return isDayTime ? "day-sunny-overcast" : "night-partly-cloudy"; // Nearly clear sky
|
||||
case 3:
|
||||
return isDayTime ? "day-cloudy" : "night-cloudy"; //Variable cloudiness
|
||||
return isDayTime ? "day-cloudy" : "night-cloudy"; // Variable cloudiness
|
||||
case 4:
|
||||
return isDayTime ? "day-cloudy" : "night-cloudy"; //Halfclear sky
|
||||
return isDayTime ? "day-cloudy" : "night-cloudy"; // Halfclear sky
|
||||
case 5:
|
||||
return "cloudy"; //Cloudy sky
|
||||
return "cloudy"; // Cloudy sky
|
||||
case 6:
|
||||
return "cloudy"; //Overcast
|
||||
return "cloudy"; // Overcast
|
||||
case 7:
|
||||
return "fog"; //Fog
|
||||
return "fog"; // Fog
|
||||
case 8:
|
||||
return "showers"; //Light rain showers
|
||||
return "showers"; // Light rain showers
|
||||
case 9:
|
||||
return "showers"; //Moderate rain showers
|
||||
return "showers"; // Moderate rain showers
|
||||
case 10:
|
||||
return "showers"; //Heavy rain showers
|
||||
return "showers"; // Heavy rain showers
|
||||
case 11:
|
||||
return "thunderstorm"; //Thunderstorm
|
||||
return "thunderstorm"; // Thunderstorm
|
||||
case 12:
|
||||
return "sleet"; //Light sleet showers
|
||||
return "sleet"; // Light sleet showers
|
||||
case 13:
|
||||
return "sleet"; //Moderate sleet showers
|
||||
return "sleet"; // Moderate sleet showers
|
||||
case 14:
|
||||
return "sleet"; //Heavy sleet showers
|
||||
return "sleet"; // Heavy sleet showers
|
||||
case 15:
|
||||
return "snow"; //Light snow showers
|
||||
return "snow"; // Light snow showers
|
||||
case 16:
|
||||
return "snow"; //Moderate snow showers
|
||||
return "snow"; // Moderate snow showers
|
||||
case 17:
|
||||
return "snow"; //Heavy snow showers
|
||||
return "snow"; // Heavy snow showers
|
||||
case 18:
|
||||
return "rain"; //Light rain
|
||||
return "rain"; // Light rain
|
||||
case 19:
|
||||
return "rain"; //Moderate rain
|
||||
return "rain"; // Moderate rain
|
||||
case 20:
|
||||
return "rain"; //Heavy rain
|
||||
return "rain"; // Heavy rain
|
||||
case 21:
|
||||
return "thunderstorm"; //Thunder
|
||||
return "thunderstorm"; // Thunder
|
||||
case 22:
|
||||
return "sleet"; // Light sleet
|
||||
case 23:
|
||||
return "sleet"; //Moderate sleet
|
||||
return "sleet"; // Moderate sleet
|
||||
case 24:
|
||||
return "sleet"; // Heavy sleet
|
||||
case 25:
|
||||
return "snow"; // Light snowfall
|
||||
case 26:
|
||||
return "snow"; //Moderate snowfall
|
||||
return "snow"; // Moderate snowfall
|
||||
case 27:
|
||||
return "snow"; //Heavy snowfall
|
||||
return "snow"; // Heavy snowfall
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* global WeatherProvider, WeatherObject, SunCalc */
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
@@ -81,6 +81,7 @@ WeatherProvider.register("ukmetoffice", {
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
const location = currentWeatherData.SiteRep.DV.Location;
|
||||
|
||||
// data times are always UTC
|
||||
let nowUtc = moment.utc();
|
||||
@@ -88,8 +89,8 @@ WeatherProvider.register("ukmetoffice", {
|
||||
let timeInMins = nowUtc.diff(midnightUtc, "minutes");
|
||||
|
||||
// loop round each of the (5) periods, look for today (the first period may be yesterday)
|
||||
for (var i in currentWeatherData.SiteRep.DV.Location.Period) {
|
||||
let periodDate = moment.utc(currentWeatherData.SiteRep.DV.Location.Period[i].value.substr(0, 10), "YYYY-MM-DD");
|
||||
for (const period of location.Period) {
|
||||
const periodDate = moment.utc(period.value.substr(0, 10), "YYYY-MM-DD");
|
||||
|
||||
// ignore if period is before today
|
||||
if (periodDate.isSameOrAfter(moment.utc().startOf("day"))) {
|
||||
@@ -97,17 +98,17 @@ WeatherProvider.register("ukmetoffice", {
|
||||
if (moment().diff(periodDate, "minutes") > 0) {
|
||||
// loop round the reports looking for the one we are in
|
||||
// $ value specifies the time in minutes-of-the-day: 0, 180, 360,...1260
|
||||
for (var j in currentWeatherData.SiteRep.DV.Location.Period[i].Rep) {
|
||||
let p = currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].$;
|
||||
for (const rep of period.Rep) {
|
||||
const p = rep.$;
|
||||
if (timeInMins >= p && timeInMins - 180 < p) {
|
||||
// finally got the one we want, so populate weather object
|
||||
currentWeather.humidity = currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].H;
|
||||
currentWeather.temperature = this.convertTemp(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].T);
|
||||
currentWeather.feelsLikeTemp = this.convertTemp(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].F);
|
||||
currentWeather.precipitation = parseInt(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].Pp);
|
||||
currentWeather.windSpeed = this.convertWindSpeed(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].S);
|
||||
currentWeather.windDirection = this.convertWindDirection(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].D);
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].W);
|
||||
currentWeather.humidity = rep.H;
|
||||
currentWeather.temperature = this.convertTemp(rep.T);
|
||||
currentWeather.feelsLikeTemp = this.convertTemp(rep.F);
|
||||
currentWeather.precipitation = parseInt(rep.Pp);
|
||||
currentWeather.windSpeed = this.convertWindSpeed(rep.S);
|
||||
currentWeather.windDirection = this.convertWindDirection(rep.D);
|
||||
currentWeather.weatherType = this.convertWeatherType(rep.W);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,9 +116,7 @@ WeatherProvider.register("ukmetoffice", {
|
||||
}
|
||||
|
||||
// determine the sunrise/sunset times - not supplied in UK Met Office data
|
||||
let times = this.calcAstroData(currentWeatherData.SiteRep.DV.Location);
|
||||
currentWeather.sunrise = times[0];
|
||||
currentWeather.sunset = times[1];
|
||||
currentWeather.updateSunTime(location.lat, location.lon);
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
@@ -130,21 +129,21 @@ 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) {
|
||||
for (const period of forecasts.SiteRep.DV.Location.Period) {
|
||||
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;
|
||||
const dateStr = period.value;
|
||||
let periodDate = moment.utc(dateStr.substr(0, 10), "YYYY-MM-DD");
|
||||
|
||||
// ignore if period is before today
|
||||
if (periodDate.isSameOrAfter(moment.utc().startOf("day"))) {
|
||||
// populate the weather object
|
||||
weather.date = moment.utc(dateStr.substr(0, 10), "YYYY-MM-DD");
|
||||
weather.minTemperature = this.convertTemp(forecasts.SiteRep.DV.Location.Period[j].Rep[1].Nm);
|
||||
weather.maxTemperature = this.convertTemp(forecasts.SiteRep.DV.Location.Period[j].Rep[0].Dm);
|
||||
weather.weatherType = this.convertWeatherType(forecasts.SiteRep.DV.Location.Period[j].Rep[0].W);
|
||||
weather.precipitation = parseInt(forecasts.SiteRep.DV.Location.Period[j].Rep[0].PPd);
|
||||
weather.minTemperature = this.convertTemp(period.Rep[1].Nm);
|
||||
weather.maxTemperature = this.convertTemp(period.Rep[0].Dm);
|
||||
weather.weatherType = this.convertWeatherType(period.Rep[0].W);
|
||||
weather.precipitation = parseInt(period.Rep[0].PPd);
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
@@ -153,20 +152,6 @@ WeatherProvider.register("ukmetoffice", {
|
||||
return days;
|
||||
},
|
||||
|
||||
/*
|
||||
* calculate the astronomical data
|
||||
*/
|
||||
calcAstroData(location) {
|
||||
const sunTimes = [];
|
||||
|
||||
// determine the sunrise/sunset times
|
||||
let times = SunCalc.getTimes(new Date(), location.lat, location.lon);
|
||||
sunTimes.push(moment(times.sunrise, "X"));
|
||||
sunTimes.push(moment(times.sunset, "X"));
|
||||
|
||||
return sunTimes;
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert the Met Office icons to a more usable name.
|
||||
*/
|
||||
@@ -247,16 +232,16 @@ WeatherProvider.register("ukmetoffice", {
|
||||
return windCardinals.hasOwnProperty(windDirection) ? windCardinals[windDirection] : null;
|
||||
},
|
||||
|
||||
/*
|
||||
/**
|
||||
* Generates an url with api parameters based on the config.
|
||||
*
|
||||
* return String - URL params.
|
||||
* @param {string} forecastType daily or 3hourly forecast
|
||||
* @returns {string} url
|
||||
*/
|
||||
getParams(forecastType) {
|
||||
let params = "?";
|
||||
params += "res=" + forecastType;
|
||||
params += "&key=" + this.config.apiKey;
|
||||
|
||||
return params;
|
||||
}
|
||||
});
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
*
|
||||
@@ -11,9 +13,8 @@
|
||||
* Hourly data for next 2 days ("hourly") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-hourly.pdf
|
||||
* 3-hourly data for the next 7 days ("3hourly") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-3-hourly.pdf
|
||||
* Daily data for the next 7 days ("daily") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-daily.pdf
|
||||
*/
|
||||
|
||||
/* NOTES
|
||||
*
|
||||
* NOTES
|
||||
* This provider requires longitude/latitude coordinates, rather than a location ID (as with the previous Met Office provider)
|
||||
* Provide the following in your config.js file:
|
||||
* weatherProvider: "ukmetofficedatahub",
|
||||
@@ -59,9 +60,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
let queryStrings = "?";
|
||||
queryStrings += "latitude=" + this.config.lat;
|
||||
queryStrings += "&longitude=" + this.config.lon;
|
||||
if (this.config.appendLocationNameToHeader) {
|
||||
queryStrings += "&includeLocationName=" + true;
|
||||
}
|
||||
queryStrings += "&includeLocationName=" + true;
|
||||
|
||||
// Return URL, making sure there is a trailing "/" in the base URL.
|
||||
return this.config.apiBase + (this.config.apiBase.endsWith("/") ? "" : "/") + forecastType + queryStrings;
|
||||
@@ -71,13 +70,11 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
// For DataHub requests, the API key/secret are sent in the headers rather than as query strings.
|
||||
// Headers defined according to Data Hub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
|
||||
getHeaders() {
|
||||
let headers = {
|
||||
return {
|
||||
accept: "application/json",
|
||||
"x-ibm-client-id": this.config.apiKey,
|
||||
"x-ibm-client-secret": this.config.apiSecret
|
||||
};
|
||||
|
||||
return headers;
|
||||
},
|
||||
|
||||
// Fetch data using supplied URL and request headers
|
||||
@@ -93,7 +90,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
this.fetchWeather(this.getUrl("hourly"), this.getHeaders())
|
||||
.then((data) => {
|
||||
// Check data is useable
|
||||
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length == 0) {
|
||||
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length === 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
Log.error("Possibly bad current/hourly data?");
|
||||
@@ -127,7 +124,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
let nowUtc = moment.utc();
|
||||
|
||||
// Find hour that contains the current time
|
||||
for (hour in forecastDataHours) {
|
||||
for (let hour in forecastDataHours) {
|
||||
let forecastTime = moment.utc(forecastDataHours[hour].time);
|
||||
if (nowUtc.isSameOrAfter(forecastTime) && nowUtc.isBefore(moment(forecastTime.add(1, "h")))) {
|
||||
currentWeather.date = forecastTime;
|
||||
@@ -150,11 +147,9 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
}
|
||||
|
||||
// Determine the sunrise/sunset times - (still) not supplied in UK Met Office data
|
||||
// Passes {longitude, latitude, height} to calcAstroData
|
||||
// Could just pass lat/long from this.config, but returned data from MO also contains elevation
|
||||
let times = this.calcAstroData(currentWeatherData.features[0].geometry.coordinates);
|
||||
currentWeather.sunrise = times[0];
|
||||
currentWeather.sunset = times[1];
|
||||
// Passes {longitude, latitude} to SunCalc, could pass height to, but
|
||||
// SunCalc.getTimes doesnt take that into account
|
||||
currentWeather.updateSunTime(this.config.lat, this.config.lon);
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
@@ -164,7 +159,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
this.fetchWeather(this.getUrl("daily"), this.getHeaders())
|
||||
.then((data) => {
|
||||
// Check data is useable
|
||||
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length == 0) {
|
||||
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length === 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
Log.error("Possibly bad forecast data?");
|
||||
@@ -198,7 +193,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
let today = moment.utc().startOf("date");
|
||||
|
||||
// Go through each day in the forecasts
|
||||
for (day in forecastDataDays) {
|
||||
for (let day in forecastDataDays) {
|
||||
const forecastWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
|
||||
// Get date of forecast
|
||||
@@ -223,7 +218,6 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
|
||||
// Pass on full details so they can be used in custom templates
|
||||
// Note the units of the supplied data when using this (see top of file)
|
||||
|
||||
forecastWeather.rawData = forecastDataDays[day];
|
||||
|
||||
dailyForecasts.push(forecastWeather);
|
||||
@@ -238,18 +232,6 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
this.fetchedLocationName = name;
|
||||
},
|
||||
|
||||
// Calculate sunrise/sunset times
|
||||
calcAstroData(location) {
|
||||
const sunTimes = [];
|
||||
|
||||
// Careful to pass values to SunCalc in correct order (latitude, longitude, elevation)
|
||||
let times = SunCalc.getTimes(new Date(), location[1], location[0], location[2]);
|
||||
sunTimes.push(moment(times.sunrise, "X"));
|
||||
sunTimes.push(moment(times.sunset, "X"));
|
||||
|
||||
return sunTimes;
|
||||
},
|
||||
|
||||
// Convert temperatures to Fahrenheit (from degrees C), if required
|
||||
convertTemp(tempInC) {
|
||||
return this.config.tempUnits === "imperial" ? (tempInC * 9) / 5 + 32 : tempInC;
|
||||
@@ -260,11 +242,11 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
// To use kilometres per hour, use "kph"
|
||||
// Else assumed imperial and the value is returned in miles per hour (a Met Office user is likely to be UK-based)
|
||||
convertWindSpeed(windInMpS) {
|
||||
if (this.config.windUnits == "mps") {
|
||||
if (this.config.windUnits === "mps") {
|
||||
return windInMpS;
|
||||
}
|
||||
|
||||
if (this.config.windUnits == "kph" || this.config.windUnits == "metric" || this.config.useKmh) {
|
||||
if (this.config.windUnits === "kph" || this.config.windUnits === "metric" || this.config.useKmh) {
|
||||
return windInMpS * 3.6;
|
||||
}
|
||||
|
||||
|
@@ -7,7 +7,8 @@
|
||||
* 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.
|
||||
* 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.
|
||||
@@ -89,7 +90,6 @@ WeatherProvider.register("weatherbit", {
|
||||
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");
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* global WeatherProvider, WeatherObject, SunCalc */
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
@@ -21,7 +21,7 @@ WeatherProvider.register("weathergov", {
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "https://api.weatherbit.io/v2.0",
|
||||
apiBase: "https://api.weather.gov/points/",
|
||||
weatherEndpoint: "/forecast",
|
||||
lat: 0,
|
||||
lon: 0
|
||||
@@ -129,7 +129,12 @@ WeatherProvider.register("weathergov", {
|
||||
.finally(() => {
|
||||
// excellent, let's fetch some actual wx data
|
||||
this.configURLs = true;
|
||||
this.fetchCurrentWeather();
|
||||
// handle 'forecast' config, fall back to 'current'
|
||||
if (config.type === "forecast") {
|
||||
this.fetchWeatherForecast();
|
||||
} else {
|
||||
this.fetchCurrentWeather();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -153,18 +158,11 @@ WeatherProvider.register("weathergov", {
|
||||
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);
|
||||
currentWeather.sunrise = times[0];
|
||||
currentWeather.sunset = times[1];
|
||||
currentWeather.updateSunTime(this.config.lat, this.config.lon);
|
||||
|
||||
// update weatherType
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.textDescription, currentWeather.isDayTime());
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
@@ -267,20 +265,6 @@ WeatherProvider.register("weathergov", {
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Calculate the astronomical data
|
||||
*/
|
||||
calcAstroData(lat, lon) {
|
||||
const sunTimes = [];
|
||||
|
||||
// determine the sunrise/sunset times
|
||||
let times = SunCalc.getTimes(new Date(), lat, lon);
|
||||
sunTimes.push(moment(times.sunrise, "X"));
|
||||
sunTimes.push(moment(times.sunset, "X"));
|
||||
|
||||
return sunTimes;
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert the icons to a more usable name.
|
||||
*/
|
||||
|
@@ -33,6 +33,7 @@ Module.register("weather", {
|
||||
showIndoorHumidity: false,
|
||||
maxNumberOfDays: 5,
|
||||
maxEntries: 5,
|
||||
ignoreToday: false,
|
||||
fade: true,
|
||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
||||
initialLoadDelay: 0, // 0 seconds delay
|
||||
@@ -48,6 +49,9 @@ Module.register("weather", {
|
||||
// Module properties.
|
||||
weatherProvider: null,
|
||||
|
||||
// Can be used by the provider to display location of event if nothing else is specified
|
||||
firstEvent: null,
|
||||
|
||||
// Define required scripts.
|
||||
getStyles: function () {
|
||||
return ["font-awesome.css", "weather-icons.css", "weather.css"];
|
||||
@@ -88,15 +92,13 @@ Module.register("weather", {
|
||||
// Override notification handler.
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
if (notification === "CALENDAR_EVENTS") {
|
||||
var senderClasses = sender.data.classes.toLowerCase().split(" ");
|
||||
const senderClasses = sender.data.classes.toLowerCase().split(" ");
|
||||
if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
|
||||
this.firstEvent = false;
|
||||
|
||||
for (var e in payload) {
|
||||
var event = payload[e];
|
||||
this.firstEvent = null;
|
||||
for (let event of payload) {
|
||||
if (event.location || event.geo) {
|
||||
this.firstEvent = event;
|
||||
//Log.log("First upcoming event with location: ", event);
|
||||
Log.debug("First upcoming event with location: ", event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -114,24 +116,26 @@ Module.register("weather", {
|
||||
getTemplate: function () {
|
||||
switch (this.config.type.toLowerCase()) {
|
||||
case "current":
|
||||
return `current.njk`;
|
||||
return "current.njk";
|
||||
case "hourly":
|
||||
return `hourly.njk`;
|
||||
return "hourly.njk";
|
||||
case "daily":
|
||||
case "forecast":
|
||||
return `forecast.njk`;
|
||||
return "forecast.njk";
|
||||
//Make the invalid values use the "Loading..." from forecast
|
||||
default:
|
||||
return `forecast.njk`;
|
||||
return "forecast.njk";
|
||||
}
|
||||
},
|
||||
|
||||
// Add all the data to the template.
|
||||
getTemplateData: function () {
|
||||
const forecast = this.weatherProvider.weatherForecast();
|
||||
|
||||
return {
|
||||
config: this.config,
|
||||
current: this.weatherProvider.currentWeather(),
|
||||
forecast: this.weatherProvider.weatherForecast(),
|
||||
forecast: forecast,
|
||||
hourly: this.weatherProvider.weatherHourly(),
|
||||
indoor: {
|
||||
humidity: this.indoorHumidity,
|
||||
@@ -152,7 +156,7 @@ Module.register("weather", {
|
||||
},
|
||||
|
||||
scheduleUpdate: function (delay = null) {
|
||||
var nextLoad = this.config.updateInterval;
|
||||
let nextLoad = this.config.updateInterval;
|
||||
if (delay !== null && delay >= 0) {
|
||||
nextLoad = delay;
|
||||
}
|
||||
@@ -176,8 +180,8 @@ Module.register("weather", {
|
||||
},
|
||||
|
||||
roundValue: function (temperature) {
|
||||
var decimals = this.config.roundTemp ? 0 : 1;
|
||||
var roundValue = parseFloat(temperature).toFixed(decimals);
|
||||
const decimals = this.config.roundTemp ? 0 : 1;
|
||||
const roundValue = parseFloat(temperature).toFixed(decimals);
|
||||
return roundValue === "-0" ? 0 : roundValue;
|
||||
},
|
||||
|
||||
@@ -272,8 +276,8 @@ Module.register("weather", {
|
||||
if (this.config.fadePoint < 0) {
|
||||
this.config.fadePoint = 0;
|
||||
}
|
||||
var startingPoint = numSteps * this.config.fadePoint;
|
||||
var numFadesteps = numSteps - startingPoint;
|
||||
const startingPoint = numSteps * this.config.fadePoint;
|
||||
const numFadesteps = numSteps - startingPoint;
|
||||
if (currentStep >= startingPoint) {
|
||||
return 1 - (currentStep - startingPoint) / numFadesteps;
|
||||
} else {
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* global SunCalc */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: Weather
|
||||
*
|
||||
@@ -10,6 +12,14 @@
|
||||
* As soon as we start implementing the forecast, mode properties will be added.
|
||||
*/
|
||||
class WeatherObject {
|
||||
/**
|
||||
* Constructor for a WeatherObject
|
||||
*
|
||||
* @param {string} units what units to use, "imperial" or "metric"
|
||||
* @param {string} tempUnits what tempunits to use
|
||||
* @param {string} windUnits what windunits to use
|
||||
* @param {boolean} useKmh use kmh if true, mps if false
|
||||
*/
|
||||
constructor(units, tempUnits, windUnits, useKmh) {
|
||||
this.units = units;
|
||||
this.tempUnits = tempUnits;
|
||||
@@ -28,6 +38,7 @@ class WeatherObject {
|
||||
this.rain = null;
|
||||
this.snow = null;
|
||||
this.precipitation = null;
|
||||
this.precipitationUnits = null;
|
||||
this.feelsLikeTemp = null;
|
||||
}
|
||||
|
||||
@@ -79,8 +90,7 @@ class WeatherObject {
|
||||
}
|
||||
|
||||
kmhWindSpeed() {
|
||||
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : (this.windSpeed * 60 * 60) / 1000;
|
||||
return windInKmh;
|
||||
return this.windUnits === "imperial" ? this.windSpeed * 1.609344 : (this.windSpeed * 60 * 60) / 1000;
|
||||
}
|
||||
|
||||
nextSunAction() {
|
||||
@@ -112,4 +122,33 @@ class WeatherObject {
|
||||
|
||||
return this.tempUnits === "imperial" ? feelsLike : ((feelsLike - 32) * 5) / 9;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the weatherObject is at dayTime.
|
||||
*
|
||||
* @returns {boolean} true if it is at dayTime
|
||||
*/
|
||||
isDayTime() {
|
||||
return this.date.isBetween(this.sunrise, this.sunset, undefined, "[]");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the sunrise / sunset time depending on the location. This can be
|
||||
* used if your provider doesnt provide that data by itself. Then SunCalc
|
||||
* is used here to calculate them according to the location.
|
||||
*
|
||||
* @param {number} lat latitude
|
||||
* @param {number} lon longitude
|
||||
*/
|
||||
updateSunTime(lat, lon) {
|
||||
let now = !this.date ? new Date() : this.date.toDate();
|
||||
let times = SunCalc.getTimes(now, lat, lon);
|
||||
this.sunrise = moment(times.sunrise, "X");
|
||||
this.sunset = moment(times.sunset, "X");
|
||||
}
|
||||
}
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = WeatherObject;
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@
|
||||
*
|
||||
* This class is the blueprint for a weather provider.
|
||||
*/
|
||||
var WeatherProvider = Class.extend({
|
||||
const WeatherProvider = Class.extend({
|
||||
// Weather Provider Properties
|
||||
providerName: null,
|
||||
defaults: {},
|
||||
@@ -114,7 +114,7 @@ var WeatherProvider = Class.extend({
|
||||
// A convenience function to make requests. It returns a promise.
|
||||
fetchData: function (url, method = "GET", data = null) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var request = new XMLHttpRequest();
|
||||
const request = new XMLHttpRequest();
|
||||
request.open(method, url, true);
|
||||
request.onreadystatechange = function () {
|
||||
if (this.readyState === 4) {
|
||||
|
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: WeatherForecast
|
||||
*
|
||||
@@ -339,7 +341,9 @@ Module.register("weatherforecast", {
|
||||
*
|
||||
* argument data object - Weather information received form openweather.org.
|
||||
*/
|
||||
processWeather: function (data) {
|
||||
processWeather: function (data, momenttz) {
|
||||
let mom = momenttz ? momenttz : moment; // Exception last.
|
||||
|
||||
// Forcast16 (paid) API endpoint provides this data. Onecall endpoint
|
||||
// does not.
|
||||
if (data.city) {
|
||||
@@ -357,8 +361,8 @@ Module.register("weatherforecast", {
|
||||
var dayEnds = 17;
|
||||
|
||||
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();
|
||||
dayStarts = new Date(mom.unix(data.city.sunrise).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours();
|
||||
dayEnds = new Date(mom.unix(data.city.sunset).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours();
|
||||
}
|
||||
|
||||
// Handle different structs between forecast16 and onecall endpoints
|
||||
@@ -379,11 +383,11 @@ Module.register("weatherforecast", {
|
||||
var day;
|
||||
var hour;
|
||||
if (forecast.dt_txt) {
|
||||
day = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd");
|
||||
hour = new Date(moment(forecast.dt_txt).locale("en").format("YYYY-MM-DD HH:mm:ss")).getHours();
|
||||
day = mom(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd");
|
||||
hour = new Date(mom(forecast.dt_txt).locale("en").format("YYYY-MM-DD HH:mm:ss")).getHours();
|
||||
} else {
|
||||
day = moment(forecast.dt, "X").format("ddd");
|
||||
hour = new Date(moment(forecast.dt, "X")).getHours();
|
||||
day = mom(forecast.dt, "X").format("ddd");
|
||||
hour = new Date(mom(forecast.dt, "X")).getHours();
|
||||
}
|
||||
|
||||
if (day !== lastDay) {
|
||||
@@ -392,7 +396,7 @@ Module.register("weatherforecast", {
|
||||
icon: this.config.iconTable[forecast.weather[0].icon],
|
||||
maxTemp: this.roundValue(forecast.temp.max),
|
||||
minTemp: this.roundValue(forecast.temp.min),
|
||||
rain: this.processRain(forecast, forecastList)
|
||||
rain: this.processRain(forecast, forecastList, mom)
|
||||
};
|
||||
this.forecast.push(forecastData);
|
||||
lastDay = day;
|
||||
@@ -482,16 +486,18 @@ Module.register("weatherforecast", {
|
||||
* That object has a property "3h" which contains the amount of rain since the previous forecast in the list.
|
||||
* This code finds all forecasts that is for the same day and sums the amount of rain and returns that.
|
||||
*/
|
||||
processRain: function (forecast, allForecasts) {
|
||||
processRain: function (forecast, allForecasts, momenttz) {
|
||||
let mom = momenttz ? momenttz : moment; // Exception last.
|
||||
|
||||
//If the amount of rain actually is a number, return it
|
||||
if (!isNaN(forecast.rain)) {
|
||||
return forecast.rain;
|
||||
}
|
||||
|
||||
//Find all forecasts that is for the same day
|
||||
var checkDateTime = forecast.dt_txt ? moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss") : moment(forecast.dt, "X");
|
||||
var checkDateTime = forecast.dt_txt ? mom(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss") : mom(forecast.dt, "X");
|
||||
var daysForecasts = allForecasts.filter(function (item) {
|
||||
var itemDateTime = item.dt_txt ? moment(item.dt_txt, "YYYY-MM-DD hh:mm:ss") : moment(item.dt, "X");
|
||||
var itemDateTime = item.dt_txt ? mom(item.dt_txt, "YYYY-MM-DD hh:mm:ss") : mom(item.dt, "X");
|
||||
return itemDateTime.isSame(checkDateTime, "day") && item.rain instanceof Object;
|
||||
});
|
||||
|
||||
|
15899
package-lock.json
generated
15899
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
129
package.json
129
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "magicmirror",
|
||||
"version": "2.15.0",
|
||||
"version": "2.17.0",
|
||||
"description": "The open source modular smart mirror platform.",
|
||||
"main": "js/electron.js",
|
||||
"scripts": {
|
||||
@@ -10,18 +10,21 @@
|
||||
"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 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 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": "NODE_ENV=test jest -i --forceExit",
|
||||
"test:coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text jest -i --forceExit",
|
||||
"test:electron": "NODE_ENV=test jest --selectProjects electron -i --forceExit",
|
||||
"test:e2e": "NODE_ENV=test jest --selectProjects e2e -i --forceExit",
|
||||
"test:unit": "NODE_ENV=test jest --selectProjects unit -i --forceExit",
|
||||
"test:prettier": "prettier . --check",
|
||||
"test:js": "eslint 'js/**/*.js' 'modules/default/**/*.js' 'clientonly/*.js' 'serveronly/*.js' 'translations/*.js' 'vendor/*.js' 'tests/**/*.js' 'config/*' --config .eslintrc.json",
|
||||
"test:css": "stylelint 'css/main.css' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.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 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"
|
||||
"lint:prettier": "prettier . --write",
|
||||
"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' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.css' --config .stylelintrc.json --fix",
|
||||
"lint:staged": "pretty-quick --staged",
|
||||
"prepare": "[ -f node_modules/.bin/husky ] && husky install || echo no husky installed."
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -43,60 +46,94 @@
|
||||
},
|
||||
"homepage": "https://magicmirror.builders",
|
||||
"devDependencies": {
|
||||
"chai": "^4.3.4",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-jsdoc": "^32.3.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-jest": "^24.4.2",
|
||||
"eslint-plugin-jsdoc": "^36.1.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"husky": "^4.3.8",
|
||||
"jsdom": "^16.5.1",
|
||||
"husky": "^7.0.2",
|
||||
"jest": "^27.2.2",
|
||||
"jsdom": "^17.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mocha": "^8.3.2",
|
||||
"mocha-each": "^2.0.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",
|
||||
"prettier": "^2.4.1",
|
||||
"pretty-quick": "^3.1.1",
|
||||
"sinon": "^11.1.2",
|
||||
"spectron": "^15.0.0",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-prettier": "^8.0.2",
|
||||
"stylelint-config-standard": "^21.0.0",
|
||||
"stylelint-prettier": "^1.2.0"
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"stylelint-prettier": "^1.2.0",
|
||||
"suncalc": "^1.8.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"electron": "^11.3.0"
|
||||
"electron": "^13.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"colors": "^1.4.0",
|
||||
"console-stamp": "^3.0.0-rc4.2",
|
||||
"digest-fetch": "^1.1.6",
|
||||
"eslint": "^7.23.0",
|
||||
"console-stamp": "^3.0.3",
|
||||
"digest-fetch": "^1.2.1",
|
||||
"eslint": "^7.32.0",
|
||||
"express": "^4.17.1",
|
||||
"express-ipfilter": "^1.1.2",
|
||||
"express-ipfilter": "^1.2.0",
|
||||
"feedme": "^2.0.2",
|
||||
"helmet": "^4.4.1",
|
||||
"iconv-lite": "^0.6.2",
|
||||
"helmet": "^4.6.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"module-alias": "^2.2.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": "^2.37.0",
|
||||
"socket.io": "^4.0.0"
|
||||
"node-fetch": "^2.6.5",
|
||||
"node-ical": "^0.13.0",
|
||||
"socket.io": "^4.2.0"
|
||||
},
|
||||
"_moduleAliases": {
|
||||
"node_helper": "js/node_helper.js",
|
||||
"logger": "js/logger.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=12"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "pretty-quick --staged"
|
||||
}
|
||||
"jest": {
|
||||
"verbose": true,
|
||||
"projects": [
|
||||
{
|
||||
"displayName": "unit",
|
||||
"moduleNameMapper": {
|
||||
"logger": "<rootDir>/js/logger.js"
|
||||
},
|
||||
"testMatch": [
|
||||
"**/tests/unit/**/*.[jt]s?(x)"
|
||||
],
|
||||
"testPathIgnorePatterns": [
|
||||
"<rootDir>/tests/unit/mocks"
|
||||
]
|
||||
},
|
||||
{
|
||||
"displayName": "electron",
|
||||
"testMatch": [
|
||||
"**/tests/electron/**/*.[jt]s?(x)"
|
||||
],
|
||||
"testPathIgnorePatterns": [
|
||||
"<rootDir>/tests/electron/modules/mocks",
|
||||
"<rootDir>/tests/electron/global-setup.js",
|
||||
"<rootDir>/tests/electron/modules/basic-auth.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"displayName": "e2e",
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/tests/e2e/mock-console.js"
|
||||
],
|
||||
"testMatch": [
|
||||
"**/tests/e2e/**/*.[jt]s?(x)"
|
||||
],
|
||||
"modulePaths": [
|
||||
"<rootDir>/js/"
|
||||
],
|
||||
"testPathIgnorePatterns": [
|
||||
"<rootDir>/tests/e2e/global-setup.js",
|
||||
"<rootDir>/tests/e2e/mock-console.js"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
const app = require("../js/app.js");
|
||||
const Log = require("logger");
|
||||
|
||||
app.start(function (config) {
|
||||
var bindAddress = config.address ? config.address : "localhost";
|
||||
var httpType = config.useHttps ? "https" : "http";
|
||||
app.start((config) => {
|
||||
const bindAddress = config.address ? config.address : "localhost";
|
||||
const httpType = config.useHttps ? "https" : "http";
|
||||
Log.log("\nReady to go! Please point your browser to: " + httpType + "://" + bindAddress + ":" + config.port);
|
||||
});
|
||||
|
37
tests/configs/data/calendar_test_recurring.ics
Normal file
37
tests/configs/data/calendar_test_recurring.ics
Normal file
@@ -0,0 +1,37 @@
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-WR-CALNAME:xxx@gmail.com
|
||||
X-WR-TIMEZONE:Europe/Zurich
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Etc/UTC
|
||||
X-LIC-LOCATION:Etc/UTC
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+0000
|
||||
TZOFFSETTO:+0000
|
||||
TZNAME:GMT
|
||||
DTSTART:19700101T00000--äüüßßß-0
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTART;VALUE=DATE:20210325
|
||||
DTEND;VALUE=DATE:20210326
|
||||
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1
|
||||
DTSTAMP:20210421T154106Z
|
||||
UID:zzz@google.com
|
||||
REATED:20200831T200244Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20200831T200244Z
|
||||
LOCATION:
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:Birthday
|
||||
TRANSP:OPAQUE
|
||||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:This is an event reminder
|
||||
TRIGGER:-P0DT7H0M0S
|
||||
END:VALARM
|
||||
END:VEVENT
|
@@ -1,44 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
|
||||
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
|
||||
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
|
||||
>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0"
|
||||
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
|
||||
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
|
||||
>
|
||||
<channel>
|
||||
<title>Rodrigo Ramírez Norambuena</title>
|
||||
<atom:link href="https://rodrigoramirez.com/feed/" rel="self" type="application/rss+xml"/>
|
||||
<link>https://rodrigoramirez.com</link>
|
||||
<description>Temas sobre Linux, VoIP, Open Source, tecnología y lo relacionado.</description>
|
||||
<lastBuildDate>Fri, 21 Oct 2016 21:30:22 +0000</lastBuildDate>
|
||||
<language>es-ES</language>
|
||||
<sy:updatePeriod>hourly</sy:updatePeriod>
|
||||
<sy:updateFrequency>1</sy:updateFrequency>
|
||||
<generator>https://wordpress.org/?v=4.7.3</generator>
|
||||
<item>
|
||||
<title>QPanel 0.13.0</title>
|
||||
<link>https://rodrigoramirez.com/qpanel-0-13-0/</link>
|
||||
<comments>https://rodrigoramirez.com/qpanel-0-13-0/#comments</comments>
|
||||
<pubDate>Tue, 20 Sep 2016 11:16:08 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Software]]></category>
|
||||
<category><![CDATA[app_queue]]></category>
|
||||
<category><![CDATA[asterisk]]></category>
|
||||
<category><![CDATA[FreeSWITCH]]></category>
|
||||
<category><![CDATA[qpanel]]></category>
|
||||
<category><![CDATA[queue]]></category>
|
||||
<category><![CDATA[spy]]></category>
|
||||
<category><![CDATA[supervision]]></category>
|
||||
<category><![CDATA[templates]]></category>
|
||||
<category><![CDATA[whisper]]></category>
|
||||
|
||||
<channel>
|
||||
<title>Rodrigo Ramírez Norambuena</title>
|
||||
<atom:link href="https://rodrigoramirez.com/feed/" rel="self" type="application/rss+xml" />
|
||||
<link>https://rodrigoramirez.com</link>
|
||||
<description>Temas sobre Linux, VoIP, Open Source, tecnología y lo relacionado.</description>
|
||||
<lastBuildDate>Fri, 21 Oct 2016 21:30:22 +0000</lastBuildDate>
|
||||
<language>es-ES</language>
|
||||
<sy:updatePeriod>hourly</sy:updatePeriod>
|
||||
<sy:updateFrequency>1</sy:updateFrequency>
|
||||
<generator>https://wordpress.org/?v=4.7.3</generator>
|
||||
<item>
|
||||
<title>QPanel 0.13.0</title>
|
||||
<link>https://rodrigoramirez.com/qpanel-0-13-0/</link>
|
||||
<comments>https://rodrigoramirez.com/qpanel-0-13-0/#comments</comments>
|
||||
<pubDate>Tue, 20 Sep 2016 11:16:08 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Software]]></category>
|
||||
<category><![CDATA[app_queue]]></category>
|
||||
<category><![CDATA[asterisk]]></category>
|
||||
<category><![CDATA[FreeSWITCH]]></category>
|
||||
<category><![CDATA[qpanel]]></category>
|
||||
<category><![CDATA[queue]]></category>
|
||||
<category><![CDATA[spy]]></category>
|
||||
<category><![CDATA[supervision]]></category>
|
||||
<category><![CDATA[templates]]></category>
|
||||
<category><![CDATA[whisper]]></category>
|
||||
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1299</guid>
|
||||
<description><![CDATA[<p>Ya está disponible la versión 0.13.0 de QPanel Para instalar esta nueva versión, la debes descargar de https://github.com/roramirez/qpanel/tree/0.13.0 En al README.md puedes encontrar las instrucciones para hacer que funcione en tu sistema. En esta nueva versión cuenta con los siguientes cambios: Se establece un limite para el reciclado del tiempo de conexión a la base […]</p>
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1299</guid>
|
||||
<description><![CDATA[<p>Ya está disponible la versión 0.13.0 de QPanel Para instalar esta nueva versión, la debes descargar de https://github.com/roramirez/qpanel/tree/0.13.0 En al README.md puedes encontrar las instrucciones para hacer que funcione en tu sistema. En esta nueva versión cuenta con los siguientes cambios: Se establece un limite para el reciclado del tiempo de conexión a la base […]</p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-13-0/">QPanel 0.13.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></description>
|
||||
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible la versión 0.13.0 de QPanel</p>
|
||||
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible la versión 0.13.0 de QPanel</p>
|
||||
<p>Para instalar esta nueva versión, la debes descargar de</p>
|
||||
<ul>
|
||||
<li><a href="https://github.com/roramirez/qpanel/tree/0.13.0">https://github.com/roramirez/qpanel/tree/0.13.0</a></li>
|
||||
@@ -57,25 +57,25 @@
|
||||
<p> </p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-13-0/">QPanel 0.13.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></content:encoded>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-13-0/feed/</wfw:commentRss>
|
||||
<slash:comments>3</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Problema VirtualBox “starting virtual machine” …</title>
|
||||
<link>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/</link>
|
||||
<comments>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/#respond</comments>
|
||||
<pubDate>Sat, 10 Sep 2016 22:50:13 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Linux]]></category>
|
||||
<category><![CDATA[no arranca]]></category>
|
||||
<category><![CDATA[Problema]]></category>
|
||||
<category><![CDATA[VirtualBox]]></category>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-13-0/feed/</wfw:commentRss>
|
||||
<slash:comments>3</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Problema VirtualBox “starting virtual machine” …</title>
|
||||
<link>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/</link>
|
||||
<comments>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/#respond</comments>
|
||||
<pubDate>Sat, 10 Sep 2016 22:50:13 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Linux]]></category>
|
||||
<category><![CDATA[no arranca]]></category>
|
||||
<category><![CDATA[Problema]]></category>
|
||||
<category><![CDATA[VirtualBox]]></category>
|
||||
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1284</guid>
|
||||
<description><![CDATA[<p>Después de una actualización de Debian, de la rama stretch/sid, tuve un problema con VirtualBox. La versión que se actualizó fue a la virtualbox 5.1.4-dfsg-1+b1. El gran problema era que ninguna maquina virtual quería arrancar, se quedaba en un largo limbo con el mensaje “starting virtual machine”, como el de la imagen de a continuación. […]</p>
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1284</guid>
|
||||
<description><![CDATA[<p>Después de una actualización de Debian, de la rama stretch/sid, tuve un problema con VirtualBox. La versión que se actualizó fue a la virtualbox 5.1.4-dfsg-1+b1. El gran problema era que ninguna maquina virtual quería arrancar, se quedaba en un largo limbo con el mensaje “starting virtual machine”, como el de la imagen de a continuación. […]</p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/">Problema VirtualBox “starting virtual machine” …</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></description>
|
||||
<content:encoded><![CDATA[<p>Después de una actualización de Debian, de la rama stretch/sid, tuve un problema con VirtualBox. La versión que se actualizó fue a la virtualbox 5.1.4-dfsg-1+b1. El gran problema era que ninguna maquina virtual quería arrancar, se quedaba en un largo limbo con el mensaje “starting virtual machine”, como el de la imagen de a continuación.</p>
|
||||
<content:encoded><![CDATA[<p>Después de una actualización de Debian, de la rama stretch/sid, tuve un problema con VirtualBox. La versión que se actualizó fue a la virtualbox 5.1.4-dfsg-1+b1. El gran problema era que ninguna maquina virtual quería arrancar, se quedaba en un largo limbo con el mensaje “starting virtual machine”, como el de la imagen de a continuación.</p>
|
||||
<p><a href="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09.png"><img class="aligncenter wp-image-1290 size-full" src="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09.png" alt="Starting virtual machine ... VirtualBox" width="648" height="554" srcset="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09.png 648w, https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09-300x256.png 300w" sizes="(max-width: 648px) 100vw, 648px" /></a></p>
|
||||
<p>Ninguna, pero ninguna maquina arrancó, se quedaban en ese mensaje. Fue de esos instantes en que sudas helado … <img src="https://s.w.org/images/core/emoji/2.2.1/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
|
||||
<p>Con un poco de investigación fue a parar al archivo<em> ~/.VirtualBox/VBoxSVC.log </em>que indicaba</p>
|
||||
@@ -85,7 +85,7 @@
|
||||
<p> </p>
|
||||
<p>Fui… algo de donde agarrarse. Mirando un poco mas se trataba de problemas con los permisos al vboxdrvu, mirando indicaba que tenía 0600.</p>
|
||||
<p> </p>
|
||||
<pre>$ ls -lh /dev/vboxdrvu
|
||||
<pre>$ ls -lh /dev/vboxdrvu
|
||||
crw------- 1 root root 10, 56 Sep 10 12:47 /dev/vboxdrvu</pre>
|
||||
<p> </p>
|
||||
<p>El tema es que deben estar en 0666, le cambias los permisos y eso soluciona el problema <img src="https://s.w.org/images/core/emoji/2.2.1/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
|
||||
@@ -95,24 +95,24 @@ $ ls -lh /dev/vboxdrvu
|
||||
crw-rw-rw- 1 root root 10, 56 Sep 10 12:47 /dev/vboxdrvu</pre>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/">Problema VirtualBox “starting virtual machine” …</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></content:encoded>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/feed/</wfw:commentRss>
|
||||
<slash:comments>0</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Mejorando la consola interactiva de Python</title>
|
||||
<link>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/</link>
|
||||
<comments>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/#comments</comments>
|
||||
<pubDate>Tue, 06 Sep 2016 04:24:43 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[desarrollo]]></category>
|
||||
<category><![CDATA[Desarrollo]]></category>
|
||||
<category><![CDATA[Python]]></category>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/feed/</wfw:commentRss>
|
||||
<slash:comments>0</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Mejorando la consola interactiva de Python</title>
|
||||
<link>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/</link>
|
||||
<comments>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/#comments</comments>
|
||||
<pubDate>Tue, 06 Sep 2016 04:24:43 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[desarrollo]]></category>
|
||||
<category><![CDATA[Desarrollo]]></category>
|
||||
<category><![CDATA[Python]]></category>
|
||||
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1247</guid>
|
||||
<description><![CDATA[<p>Cuando estás desarrollando en Python es muy cool estar utilizando la consola interactiva para ir probando cosas antes de ponerlas dentro del archivo de código fuente. La consola de Python funciona y cumple su cometido. Solo al tipear python te permite entrar en modo interactivo e ir probando cosas. El punto es que a veces […]</p>
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1247</guid>
|
||||
<description><![CDATA[<p>Cuando estás desarrollando en Python es muy cool estar utilizando la consola interactiva para ir probando cosas antes de ponerlas dentro del archivo de código fuente. La consola de Python funciona y cumple su cometido. Solo al tipear python te permite entrar en modo interactivo e ir probando cosas. El punto es que a veces […]</p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/">Mejorando la consola interactiva de Python</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></description>
|
||||
<content:encoded><![CDATA[<p>Cuando estás desarrollando en Python es muy <em>cool</em> estar utilizando la consola interactiva para ir probando cosas antes de ponerlas dentro del archivo de código fuente.</p>
|
||||
<content:encoded><![CDATA[<p>Cuando estás desarrollando en Python es muy <em>cool</em> estar utilizando la consola interactiva para ir probando cosas antes de ponerlas dentro del archivo de código fuente.</p>
|
||||
<p>La consola de Python funciona y cumple su cometido. Solo al tipear <em>python </em>te permite entrar en modo interactivo e ir probando cosas.</p>
|
||||
<p>El punto es que a veces uno necesita ir un poco más allá. Como autocomentado de código o resaltado de sintaxis, para eso tengo dos truco que utilizo generalmente.</p>
|
||||
<h2>Truco a)</h2>
|
||||
@@ -139,31 +139,31 @@ $ ls -lh /dev/vboxdrvu
|
||||
<p>O lo agregas a un bashrc, zshrc o la shell que ocupes.</p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/">Mejorando la consola interactiva de Python</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></content:encoded>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/feed/</wfw:commentRss>
|
||||
<slash:comments>4</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>QPanel 0.12.0 con estadísticas</title>
|
||||
<link>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/</link>
|
||||
<comments>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/#respond</comments>
|
||||
<pubDate>Mon, 22 Aug 2016 04:19:03 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Software]]></category>
|
||||
<category><![CDATA[app_queue]]></category>
|
||||
<category><![CDATA[asterisk]]></category>
|
||||
<category><![CDATA[FreeSWITCH]]></category>
|
||||
<category><![CDATA[qpanel]]></category>
|
||||
<category><![CDATA[queue]]></category>
|
||||
<category><![CDATA[spy]]></category>
|
||||
<category><![CDATA[supervision]]></category>
|
||||
<category><![CDATA[templates]]></category>
|
||||
<category><![CDATA[whisper]]></category>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/feed/</wfw:commentRss>
|
||||
<slash:comments>4</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>QPanel 0.12.0 con estadísticas</title>
|
||||
<link>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/</link>
|
||||
<comments>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/#respond</comments>
|
||||
<pubDate>Mon, 22 Aug 2016 04:19:03 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Software]]></category>
|
||||
<category><![CDATA[app_queue]]></category>
|
||||
<category><![CDATA[asterisk]]></category>
|
||||
<category><![CDATA[FreeSWITCH]]></category>
|
||||
<category><![CDATA[qpanel]]></category>
|
||||
<category><![CDATA[queue]]></category>
|
||||
<category><![CDATA[spy]]></category>
|
||||
<category><![CDATA[supervision]]></category>
|
||||
<category><![CDATA[templates]]></category>
|
||||
<category><![CDATA[whisper]]></category>
|
||||
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1268</guid>
|
||||
<description><![CDATA[<p>Ya está disponible una nueva versión de QPanel, esta es la 0.12.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.12.0 En esta nueva versión las funcionalidades agregadas son: Permite remover los agentes de las cola Posibilidad de cancelar llamadas que están en espera de atención Estadísticas por rango de fecha obtenidas desde […]</p>
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1268</guid>
|
||||
<description><![CDATA[<p>Ya está disponible una nueva versión de QPanel, esta es la 0.12.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.12.0 En esta nueva versión las funcionalidades agregadas son: Permite remover los agentes de las cola Posibilidad de cancelar llamadas que están en espera de atención Estadísticas por rango de fecha obtenidas desde […]</p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/">QPanel 0.12.0 con estadísticas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></description>
|
||||
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible una nueva versión de QPanel, esta es la 0.12.0</p>
|
||||
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible una nueva versión de QPanel, esta es la 0.12.0</p>
|
||||
<p>Para instalar esta nueva versión, debes visitar la siguiente URL</p>
|
||||
<ul>
|
||||
<li><a href="https://github.com/roramirez/qpanel/tree/0.12.0">https://github.com/roramirez/qpanel/tree/0.12.0</a></li>
|
||||
@@ -178,31 +178,31 @@ $ ls -lh /dev/vboxdrvu
|
||||
<p>Si deseas colaborar con el proyecto puedes agregar nuevas sugerencias mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a> ó colaborar mediante <a href="https://github.com/roramirez/qpanel/blob/dd42cf0f534408505f57b0d387dffee2f3688711/README.md#how-to-contribute">mediante un Pull Request</a></p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/">QPanel 0.12.0 con estadísticas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></content:encoded>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/feed/</wfw:commentRss>
|
||||
<slash:comments>0</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>QPanel 0.11.0 con Spy, Whisper y mas</title>
|
||||
<link>https://rodrigoramirez.com/qpanel-spy-supervisor/</link>
|
||||
<comments>https://rodrigoramirez.com/qpanel-spy-supervisor/#comments</comments>
|
||||
<pubDate>Thu, 21 Jul 2016 01:53:21 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Software]]></category>
|
||||
<category><![CDATA[app_queue]]></category>
|
||||
<category><![CDATA[asterisk]]></category>
|
||||
<category><![CDATA[FreeSWITCH]]></category>
|
||||
<category><![CDATA[qpanel]]></category>
|
||||
<category><![CDATA[queue]]></category>
|
||||
<category><![CDATA[spy]]></category>
|
||||
<category><![CDATA[supervision]]></category>
|
||||
<category><![CDATA[templates]]></category>
|
||||
<category><![CDATA[whisper]]></category>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/feed/</wfw:commentRss>
|
||||
<slash:comments>0</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>QPanel 0.11.0 con Spy, Whisper y mas</title>
|
||||
<link>https://rodrigoramirez.com/qpanel-spy-supervisor/</link>
|
||||
<comments>https://rodrigoramirez.com/qpanel-spy-supervisor/#comments</comments>
|
||||
<pubDate>Thu, 21 Jul 2016 01:53:21 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Software]]></category>
|
||||
<category><![CDATA[app_queue]]></category>
|
||||
<category><![CDATA[asterisk]]></category>
|
||||
<category><![CDATA[FreeSWITCH]]></category>
|
||||
<category><![CDATA[qpanel]]></category>
|
||||
<category><![CDATA[queue]]></category>
|
||||
<category><![CDATA[spy]]></category>
|
||||
<category><![CDATA[supervision]]></category>
|
||||
<category><![CDATA[templates]]></category>
|
||||
<category><![CDATA[whisper]]></category>
|
||||
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1245</guid>
|
||||
<description><![CDATA[<p>Ya está disponible una nueva versión de QPanel, esta es la 0.11.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.11.0 Esta versión hemos agregado algunas funcionalidades que los usuarios han ido solicitando. Para esta versión es posible realizar Spy, Whisper o Barge a un canal para la supervisión de los miembros que […]</p>
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1245</guid>
|
||||
<description><![CDATA[<p>Ya está disponible una nueva versión de QPanel, esta es la 0.11.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.11.0 Esta versión hemos agregado algunas funcionalidades que los usuarios han ido solicitando. Para esta versión es posible realizar Spy, Whisper o Barge a un canal para la supervisión de los miembros que […]</p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-spy-supervisor/">QPanel 0.11.0 con Spy, Whisper y mas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></description>
|
||||
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible una nueva versión de QPanel, esta es la 0.11.0</p>
|
||||
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible una nueva versión de QPanel, esta es la 0.11.0</p>
|
||||
<p>Para instalar esta nueva versión, debes visitar la siguiente URL</p>
|
||||
<ul>
|
||||
<li><a href="https://github.com/roramirez/qpanel/tree/0.11.0">https://github.com/roramirez/qpanel/tree/0.11.0</a></li>
|
||||
@@ -216,22 +216,22 @@ $ ls -lh /dev/vboxdrvu
|
||||
<p>El proyecto siempre está abierto a nuevas sugerencias las cuales puedes agregar mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a>.</p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-spy-supervisor/">QPanel 0.11.0 con Spy, Whisper y mas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></content:encoded>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/qpanel-spy-supervisor/feed/</wfw:commentRss>
|
||||
<slash:comments>4</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Añadir Swap a un sistema</title>
|
||||
<link>https://rodrigoramirez.com/crear-swap/</link>
|
||||
<comments>https://rodrigoramirez.com/crear-swap/#respond</comments>
|
||||
<pubDate>Fri, 15 Jul 2016 05:07:43 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Linux]]></category>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/qpanel-spy-supervisor/feed/</wfw:commentRss>
|
||||
<slash:comments>4</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Añadir Swap a un sistema</title>
|
||||
<link>https://rodrigoramirez.com/crear-swap/</link>
|
||||
<comments>https://rodrigoramirez.com/crear-swap/#respond</comments>
|
||||
<pubDate>Fri, 15 Jul 2016 05:07:43 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Linux]]></category>
|
||||
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1234</guid>
|
||||
<description><![CDATA[<p>Algo que me toma generalmente hacer es cuando trabajo con maquina virtuales es asignar una cantidad determinada de Swap. La memoria swap es un espacio de intercambio en disco para cuando el sistema ya no puede utilizar más memoria RAM. El problema para mi es que algunos sistemas de maquinas virtuales no asignan por defecto […]</p>
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1234</guid>
|
||||
<description><![CDATA[<p>Algo que me toma generalmente hacer es cuando trabajo con maquina virtuales es asignar una cantidad determinada de Swap. La memoria swap es un espacio de intercambio en disco para cuando el sistema ya no puede utilizar más memoria RAM. El problema para mi es que algunos sistemas de maquinas virtuales no asignan por defecto […]</p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/crear-swap/">Añadir Swap a un sistema</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></description>
|
||||
<content:encoded><![CDATA[<p>Algo que me toma generalmente hacer es cuando trabajo con maquina virtuales es asignar una cantidad determinada de Swap.</p>
|
||||
<content:encoded><![CDATA[<p>Algo que me toma generalmente hacer es cuando trabajo con maquina virtuales es asignar una cantidad determinada de Swap.</p>
|
||||
<p>La memoria swap es un espacio de intercambio en disco para cuando el sistema ya no puede utilizar más memoria RAM.</p>
|
||||
<p>El problema para mi es que algunos sistemas de maquinas virtuales no asignan por defecto un espacio para la Swap, lo que te lleva a que el sistema pueda tener crash durante la ejecución.</p>
|
||||
<p>Para comprobar la asignación de memoria, al ejecutar el comando <em>free</em> nos debería mostrar como algo similar a lo siguiente</p>
|
||||
@@ -271,27 +271,27 @@ Swap: 3071 0 3071</pre>
|
||||
<p> </p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/crear-swap/">Añadir Swap a un sistema</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></content:encoded>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/crear-swap/feed/</wfw:commentRss>
|
||||
<slash:comments>0</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>QPanel 0.10.0 con vista consolidada</title>
|
||||
<link>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/</link>
|
||||
<comments>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/#respond</comments>
|
||||
<pubDate>Mon, 20 Jun 2016 19:32:55 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Linux]]></category>
|
||||
<category><![CDATA[app_queue]]></category>
|
||||
<category><![CDATA[asterisk]]></category>
|
||||
<category><![CDATA[FreeSWITCH]]></category>
|
||||
<category><![CDATA[qpanel]]></category>
|
||||
<category><![CDATA[queue]]></category>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/crear-swap/feed/</wfw:commentRss>
|
||||
<slash:comments>0</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>QPanel 0.10.0 con vista consolidada</title>
|
||||
<link>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/</link>
|
||||
<comments>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/#respond</comments>
|
||||
<pubDate>Mon, 20 Jun 2016 19:32:55 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Linux]]></category>
|
||||
<category><![CDATA[app_queue]]></category>
|
||||
<category><![CDATA[asterisk]]></category>
|
||||
<category><![CDATA[FreeSWITCH]]></category>
|
||||
<category><![CDATA[qpanel]]></category>
|
||||
<category><![CDATA[queue]]></category>
|
||||
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1227</guid>
|
||||
<description><![CDATA[<p>Ya con la release numero 28 la nueva versión 0.10.0 de QPanel ya está disponible. Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.10.0 Esta versión versión nos preocupamos de realizar mejoras, refactorizaciones y agregamos una nueva funcionalidad. La nueva funcionalidad incluida es que ahora es posible contar con una vista consolidada para […]</p>
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1227</guid>
|
||||
<description><![CDATA[<p>Ya con la release numero 28 la nueva versión 0.10.0 de QPanel ya está disponible. Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.10.0 Esta versión versión nos preocupamos de realizar mejoras, refactorizaciones y agregamos una nueva funcionalidad. La nueva funcionalidad incluida es que ahora es posible contar con una vista consolidada para […]</p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/">QPanel 0.10.0 con vista consolidada</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></description>
|
||||
<content:encoded><![CDATA[<p><img class="alignleft" src="https://raw.githubusercontent.com/roramirez/qpanel/0.10.0/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="403" height="227" />Ya con la release numero 28 la nueva versión 0.10.0 de QPanel ya está disponible.</p>
|
||||
<content:encoded><![CDATA[<p><img class="alignleft" src="https://raw.githubusercontent.com/roramirez/qpanel/0.10.0/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="403" height="227" />Ya con la release numero 28 la nueva versión 0.10.0 de QPanel ya está disponible.</p>
|
||||
<p>Para instalar esta nueva versión, debes visitar la siguiente URL</p>
|
||||
<ul>
|
||||
<li><a href="https://github.com/roramirez/qpanel/tree/0.10.0">https://github.com/roramirez/qpanel/tree/0.10.0</a></li>
|
||||
@@ -301,29 +301,29 @@ Swap: 3071 0 3071</pre>
|
||||
<p>El proyecto siempre está abierto a nuevas sugerencias las cuales puedes agregar mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a>.</p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/">QPanel 0.10.0 con vista consolidada</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></content:encoded>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/feed/</wfw:commentRss>
|
||||
<slash:comments>0</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Nerdearla 2016, WebRTC Glue</title>
|
||||
<link>https://rodrigoramirez.com/nerdearla-2016/</link>
|
||||
<comments>https://rodrigoramirez.com/nerdearla-2016/#respond</comments>
|
||||
<pubDate>Wed, 15 Jun 2016 17:55:41 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Linux]]></category>
|
||||
<category><![CDATA[baires]]></category>
|
||||
<category><![CDATA[charla]]></category>
|
||||
<category><![CDATA[Computación]]></category>
|
||||
<category><![CDATA[informatica]]></category>
|
||||
<category><![CDATA[tech]]></category>
|
||||
<category><![CDATA[ti]]></category>
|
||||
<category><![CDATA[webrtc]]></category>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/feed/</wfw:commentRss>
|
||||
<slash:comments>0</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Nerdearla 2016, WebRTC Glue</title>
|
||||
<link>https://rodrigoramirez.com/nerdearla-2016/</link>
|
||||
<comments>https://rodrigoramirez.com/nerdearla-2016/#respond</comments>
|
||||
<pubDate>Wed, 15 Jun 2016 17:55:41 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Linux]]></category>
|
||||
<category><![CDATA[baires]]></category>
|
||||
<category><![CDATA[charla]]></category>
|
||||
<category><![CDATA[Computación]]></category>
|
||||
<category><![CDATA[informatica]]></category>
|
||||
<category><![CDATA[tech]]></category>
|
||||
<category><![CDATA[ti]]></category>
|
||||
<category><![CDATA[webrtc]]></category>
|
||||
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1218</guid>
|
||||
<description><![CDATA[<p>Días atrás estuve participando en el evento llamado Nerdearla en Buenos Aires. El ambiente era genial si eres de esas personas que desde niño sintio curiosidad por ver como funcionan las cosas, donde desarmabas para volver armar lo juguetes. Habían muchas cosas interesantes tanto en las presentaciones, co-working y workshop que se hubieron. Si te […]</p>
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1218</guid>
|
||||
<description><![CDATA[<p>Días atrás estuve participando en el evento llamado Nerdearla en Buenos Aires. El ambiente era genial si eres de esas personas que desde niño sintio curiosidad por ver como funcionan las cosas, donde desarmabas para volver armar lo juguetes. Habían muchas cosas interesantes tanto en las presentaciones, co-working y workshop que se hubieron. Si te […]</p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/nerdearla-2016/">Nerdearla 2016, WebRTC Glue</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></description>
|
||||
<content:encoded><![CDATA[<p>Días atrás estuve participando en el evento llamado <a href="https://nerdear.la/">Nerdearla</a> en Buenos Aires. El ambiente era genial si eres de esas personas que desde niño sintio curiosidad por ver como funcionan las cosas, donde desarmabas para volver armar lo juguetes.</p>
|
||||
<content:encoded><![CDATA[<p>Días atrás estuve participando en el evento llamado <a href="https://nerdear.la/">Nerdearla</a> en Buenos Aires. El ambiente era genial si eres de esas personas que desde niño sintio curiosidad por ver como funcionan las cosas, donde desarmabas para volver armar lo juguetes.</p>
|
||||
<p>Habían muchas cosas interesantes tanto en las presentaciones, co-working y workshop que se hubieron. Si te lo perdiste te recomiendo que estés pendiente para el proximo año.</p>
|
||||
<p> </p>
|
||||
<p>Te podias encontrar con una nuestra como esta<a href="https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS.jpg"><img class="aligncenter size-medium wp-image-1221" src="https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-300x169.jpg" alt="Kaypro II" width="300" height="169" srcset="https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-300x169.jpg 300w, https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-768x432.jpg 768w, https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-1024x576.jpg 1024w, https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS.jpg 1200w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>
|
||||
@@ -338,30 +338,30 @@ Swap: 3071 0 3071</pre>
|
||||
</p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/nerdearla-2016/">Nerdearla 2016, WebRTC Glue</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></content:encoded>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/nerdearla-2016/feed/</wfw:commentRss>
|
||||
<slash:comments>0</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>QPanel 0.9.0</title>
|
||||
<link>https://rodrigoramirez.com/qpanel-0-9-0/</link>
|
||||
<comments>https://rodrigoramirez.com/qpanel-0-9-0/#respond</comments>
|
||||
<pubDate>Mon, 09 May 2016 18:40:23 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Software]]></category>
|
||||
<category><![CDATA[asterisk]]></category>
|
||||
<category><![CDATA[callcenter]]></category>
|
||||
<category><![CDATA[colas]]></category>
|
||||
<category><![CDATA[monitor]]></category>
|
||||
<category><![CDATA[monitoreo]]></category>
|
||||
<category><![CDATA[panel]]></category>
|
||||
<category><![CDATA[qpanel]]></category>
|
||||
<category><![CDATA[queues]]></category>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/nerdearla-2016/feed/</wfw:commentRss>
|
||||
<slash:comments>0</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>QPanel 0.9.0</title>
|
||||
<link>https://rodrigoramirez.com/qpanel-0-9-0/</link>
|
||||
<comments>https://rodrigoramirez.com/qpanel-0-9-0/#respond</comments>
|
||||
<pubDate>Mon, 09 May 2016 18:40:23 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Software]]></category>
|
||||
<category><![CDATA[asterisk]]></category>
|
||||
<category><![CDATA[callcenter]]></category>
|
||||
<category><![CDATA[colas]]></category>
|
||||
<category><![CDATA[monitor]]></category>
|
||||
<category><![CDATA[monitoreo]]></category>
|
||||
<category><![CDATA[panel]]></category>
|
||||
<category><![CDATA[qpanel]]></category>
|
||||
<category><![CDATA[queues]]></category>
|
||||
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1206</guid>
|
||||
<description><![CDATA[<p>El Panel monitor callcenter para colas de Asterisk ya cuenta con una nueva versión, la 0.9.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.9.0 Esta versión versión nos preocupamos de realizar mejoras y refactorizaciones en el codigo para dar un mejor rendimiento, como también de la compatibilidad con la versión 11 de […]</p>
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1206</guid>
|
||||
<description><![CDATA[<p>El Panel monitor callcenter para colas de Asterisk ya cuenta con una nueva versión, la 0.9.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.9.0 Esta versión versión nos preocupamos de realizar mejoras y refactorizaciones en el codigo para dar un mejor rendimiento, como también de la compatibilidad con la versión 11 de […]</p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-9-0/">QPanel 0.9.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></description>
|
||||
<content:encoded><![CDATA[<p><img class="alignleft" src="https://raw.githubusercontent.com/roramirez/qpanel/0.9.0/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="403" height="227" />El Panel monitor callcenter para colas de Asterisk ya cuenta con una nueva versión, la 0.9.0</p>
|
||||
<content:encoded><![CDATA[<p><img class="alignleft" src="https://raw.githubusercontent.com/roramirez/qpanel/0.9.0/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="403" height="227" />El Panel monitor callcenter para colas de Asterisk ya cuenta con una nueva versión, la 0.9.0</p>
|
||||
<p>Para instalar esta nueva versión, debes visitar la siguiente URL</p>
|
||||
<ul>
|
||||
<li><a href="https://github.com/roramirez/qpanel/tree/0.9.0">https://github.com/roramirez/qpanel/tree/0.9.0</a></li>
|
||||
@@ -376,35 +376,35 @@ Swap: 3071 0 3071</pre>
|
||||
<p>El proyecto siempre está abierto a nuevas sugerencias las cuales puedes agregar mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a>.</p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-9-0/">QPanel 0.9.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></content:encoded>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-9-0/feed/</wfw:commentRss>
|
||||
<slash:comments>0</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Mandar un email desde la shell</title>
|
||||
<link>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/</link>
|
||||
<comments>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/#comments</comments>
|
||||
<pubDate>Wed, 13 Apr 2016 13:05:13 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Linux]]></category>
|
||||
<category><![CDATA[mini-tips]]></category>
|
||||
<category><![CDATA[bash]]></category>
|
||||
<category><![CDATA[cli]]></category>
|
||||
<category><![CDATA[Email]]></category>
|
||||
<category><![CDATA[mail]]></category>
|
||||
<category><![CDATA[sh]]></category>
|
||||
<category><![CDATA[shell]]></category>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-9-0/feed/</wfw:commentRss>
|
||||
<slash:comments>0</slash:comments>
|
||||
</item>
|
||||
<item>
|
||||
<title>Mandar un email desde la shell</title>
|
||||
<link>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/</link>
|
||||
<comments>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/#comments</comments>
|
||||
<pubDate>Wed, 13 Apr 2016 13:05:13 +0000</pubDate>
|
||||
<dc:creator><![CDATA[decipher]]></dc:creator>
|
||||
<category><![CDATA[Linux]]></category>
|
||||
<category><![CDATA[mini-tips]]></category>
|
||||
<category><![CDATA[bash]]></category>
|
||||
<category><![CDATA[cli]]></category>
|
||||
<category><![CDATA[Email]]></category>
|
||||
<category><![CDATA[mail]]></category>
|
||||
<category><![CDATA[sh]]></category>
|
||||
<category><![CDATA[shell]]></category>
|
||||
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1172</guid>
|
||||
<description><![CDATA[<p>Dejo esto por acá ya que es algo que siempre me olvido como es. El tema es enviar un email mediante el comando mail en un servidor con Linux. Si usas mail a secas te va pidiendo los datos para crear el correo, principalmente el body del correo. Para automatizar esto a través de un […]</p>
|
||||
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1172</guid>
|
||||
<description><![CDATA[<p>Dejo esto por acá ya que es algo que siempre me olvido como es. El tema es enviar un email mediante el comando mail en un servidor con Linux. Si usas mail a secas te va pidiendo los datos para crear el correo, principalmente el body del correo. Para automatizar esto a través de un […]</p>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mandar-un-email-desde-la-shell/">Mandar un email desde la shell</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></description>
|
||||
<content:encoded><![CDATA[<p>Dejo esto por acá ya que es algo que siempre me olvido como es. El tema es enviar un email mediante el comando <em>mail</em> en un servidor con Linux.</p>
|
||||
<content:encoded><![CDATA[<p>Dejo esto por acá ya que es algo que siempre me olvido como es. El tema es enviar un email mediante el comando <em>mail</em> en un servidor con Linux.</p>
|
||||
<p>Si usas mail a secas te va pidiendo los datos para crear el correo, principalmente el body del correo. Para automatizar esto a través de un <em>echo</em> le pasas por pipe a <em>mail</em></p>
|
||||
<pre>echo "Cuerpo del mensaje" | mail -s Asunto a@rodrigoramirez.com</pre>
|
||||
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mandar-un-email-desde-la-shell/">Mandar un email desde la shell</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
|
||||
]]></content:encoded>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/feed/</wfw:commentRss>
|
||||
<slash:comments>4</slash:comments>
|
||||
</item>
|
||||
</channel>
|
||||
<wfw:commentRss>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/feed/</wfw:commentRss>
|
||||
<slash:comments>4</slash:comments>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
|
21
tests/configs/default.js
Normal file
21
tests/configs/default.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/* Magic Mirror Test default config for modules
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
exports.configFactory = function (options) {
|
||||
return Object.assign(
|
||||
{
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
},
|
||||
|
||||
modules: []
|
||||
},
|
||||
options
|
||||
);
|
||||
};
|
@@ -3,23 +3,9 @@
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var config = {
|
||||
port: 8080,
|
||||
ipWhitelist: [],
|
||||
|
||||
language: "en",
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: []
|
||||
};
|
||||
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
|
||||
ipWhitelist: []
|
||||
});
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
|
@@ -3,23 +3,7 @@
|
||||
* 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: 24,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: []
|
||||
};
|
||||
let config = require(process.cwd() + "/tests/configs/default.js").configFactory();
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
|
21
tests/configs/modules/alert/default.js
Normal file
21
tests/configs/modules/alert/default.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/* Magic Mirror Test config sample module alert
|
||||
*
|
||||
* By rejas
|
||||
* MIT Licensed.
|
||||
*/
|
||||
let config = {
|
||||
modules: [
|
||||
{
|
||||
module: "alert",
|
||||
config: {
|
||||
display_time: 1000000,
|
||||
welcome_message: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
@@ -3,20 +3,8 @@
|
||||
* 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",
|
||||
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
@@ -36,7 +24,7 @@ var config = {
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
|
@@ -3,20 +3,8 @@
|
||||
* 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",
|
||||
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
@@ -37,7 +25,7 @@ var config = {
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
|
@@ -3,20 +3,8 @@
|
||||
* 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",
|
||||
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
@@ -36,7 +24,7 @@ var config = {
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
|
@@ -1,20 +1,10 @@
|
||||
/* Magic Mirror Test config custom calendar
|
||||
*
|
||||
* By Rejas
|
||||
* MIT Licensed.
|
||||
*/
|
||||
let config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
@@ -34,7 +24,7 @@ let config = {
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
|
@@ -3,20 +3,8 @@
|
||||
* 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",
|
||||
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
@@ -32,7 +20,7 @@ var config = {
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
|
@@ -5,20 +5,8 @@
|
||||
* 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",
|
||||
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
@@ -39,7 +27,7 @@ var config = {
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
|
@@ -3,20 +3,8 @@
|
||||
* 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",
|
||||
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
@@ -34,7 +22,7 @@ var config = {
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
|
29
tests/configs/modules/calendar/recurring.js
Normal file
29
tests/configs/modules/calendar/recurring.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/* Magic Mirror Test config custom calendar
|
||||
*
|
||||
* By Rejas
|
||||
* MIT Licensed.
|
||||
*/
|
||||
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
|
||||
timeFormat: 12,
|
||||
|
||||
modules: [
|
||||
{
|
||||
module: "calendar",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
calendars: [
|
||||
{
|
||||
maximumEntries: 6,
|
||||
maximumNumberOfDays: 3650,
|
||||
url: "http://localhost:8080/tests/configs/data/calendar_test_recurring.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
@@ -3,20 +3,8 @@
|
||||
* By Sergey Morozov
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
let config = {
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
|
@@ -3,21 +3,7 @@
|
||||
* By Sergey Morozov
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
let config = {
|
||||
modules: [
|
||||
{
|
||||
module: "clock",
|
||||
|
@@ -3,19 +3,6 @@
|
||||
* MIT Licensed.
|
||||
*/
|
||||
let config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
module: "clock",
|
||||
|
@@ -3,20 +3,8 @@
|
||||
* 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",
|
||||
let config = {
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
|
@@ -3,20 +3,8 @@
|
||||
* By Sergey Morozov
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
let config = {
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
|
23
tests/configs/modules/clock/clock_showTime.js
Normal file
23
tests/configs/modules/clock/clock_showTime.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/* Magic Mirror Test config for default clock module
|
||||
*
|
||||
* By Johan Hammar
|
||||
* MIT Licensed.
|
||||
*/
|
||||
let config = {
|
||||
timeFormat: 12,
|
||||
|
||||
modules: [
|
||||
{
|
||||
module: "clock",
|
||||
position: "middle_center",
|
||||
config: {
|
||||
showTime: false
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
@@ -3,20 +3,8 @@
|
||||
* By Johan Hammar
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var config = {
|
||||
port: 8080,
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
|
||||
|
||||
language: "en",
|
||||
let config = {
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
|
@@ -3,20 +3,9 @@
|
||||
* 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"],
|
||||
|
||||
let config = {
|
||||
language: "es",
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
|
@@ -3,20 +3,8 @@
|
||||
* 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"],
|
||||
|
||||
let config = {
|
||||
language: "es",
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
|
@@ -3,20 +3,9 @@
|
||||
* 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"],
|
||||
|
||||
let config = {
|
||||
language: "es",
|
||||
timeFormat: 12,
|
||||
units: "metric",
|
||||
electronOptions: {
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true
|
||||
}
|
||||
},
|
||||
|
||||
modules: [
|
||||
{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user