mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-08-21 12:55:22 +00:00
Compare commits
403 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0300ce05d5 | ||
|
9e0293047f | ||
|
298e585361 | ||
|
21a6d1bcea | ||
|
bbe17b9b01 | ||
|
4381fc6695 | ||
|
818fd7b490 | ||
|
4c6c6f9ed3 | ||
|
2338a90191 | ||
|
6cad0e191b | ||
|
f23e604ed4 | ||
|
0c1abad9df | ||
|
fb96cc3c72 | ||
|
e917f40542 | ||
|
29d467715f | ||
|
b791a3761f | ||
|
02201d9f15 | ||
|
b8dbf95497 | ||
|
65022f3ce1 | ||
|
44854d6a4f | ||
|
203014c654 | ||
|
d3e53586fd | ||
|
9dd343054e | ||
|
11d17dd2c0 | ||
|
4cc3e481cc | ||
|
174da38cc8 | ||
|
0a35505e8d | ||
|
032f7ac299 | ||
|
ca906c4b36 | ||
|
50f72f09ac | ||
|
02cf9b37e2 | ||
|
6f273d76b3 | ||
|
0a1067ec7d | ||
|
48756e8774 | ||
|
4915ad8fc7 | ||
|
ba128cbae9 | ||
|
2a6e51493e | ||
|
ef678f9f8a | ||
|
e997ee7071 | ||
|
1273390af3 | ||
|
0f6eb4a244 | ||
|
2c0c62c89b | ||
|
ec13c952d9 | ||
|
6fc71cf6f8 | ||
|
bd3137a3dc | ||
|
b26da4f97f | ||
|
cde0adc28e | ||
|
2813f101b8 | ||
|
4d8fb8c176 | ||
|
9ae62d60f7 | ||
|
17c14e137e | ||
|
bc239f6608 | ||
|
6493fad8a4 | ||
|
d539f459ca | ||
|
2cca6a2f39 | ||
|
80e07bae0d | ||
|
d4c4f6e1a5 | ||
|
d24fe4e983 | ||
|
aaa9042810 | ||
|
a4bb1cefb9 | ||
|
c3339b47bb | ||
|
0c1e5ea881 | ||
|
3fbd9006ad | ||
|
be9761146c | ||
|
5aa9e7e0f6 | ||
|
65e87aea52 | ||
|
66fffc932c | ||
|
1e934e16af | ||
|
82fbb7e32d | ||
|
8bf9b9bef9 | ||
|
2d15e4f976 | ||
|
055ce56a57 | ||
|
f1f2a61dc8 | ||
|
39c1b37726 | ||
|
5b1b25fa86 | ||
|
54ab0b1bf0 | ||
|
5507e9ffe9 | ||
|
30d5bfe59e | ||
|
b716ec33d9 | ||
|
e25209d9f9 | ||
|
1f4ac82495 | ||
|
0baf58f3fd | ||
|
9a769203e3 | ||
|
1435efaea7 | ||
|
c411ac821e | ||
|
da0489fc0c | ||
|
c82de1e314 | ||
|
604a555e14 | ||
|
87e011ff96 | ||
|
9b2051827c | ||
|
47aefb0c82 | ||
|
70da1d6f5c | ||
|
c0743ce9de | ||
|
fe7b4044e0 | ||
|
701ce8ad47 | ||
|
591f907134 | ||
|
4f1db749c0 | ||
|
ae72ed8c67 | ||
|
ab7934fa98 | ||
|
a4c77f0c9e | ||
|
0023c64d59 | ||
|
b55b3bd63b | ||
|
cbda20f67e | ||
|
e598dbb206 | ||
|
094881fc5c | ||
|
b13414cab8 | ||
|
3e65ac653b | ||
|
05621c9876 | ||
|
938619ce0c | ||
|
8e5267d44f | ||
|
3b55886c45 | ||
|
ed3aceb427 | ||
|
c3b2aaec69 | ||
|
bb1d3431cc | ||
|
fe83fe338a | ||
|
6504b5e818 | ||
|
8578900bfb | ||
|
d730dd04bf | ||
|
1d90c5e1fe | ||
|
0f39b7733c | ||
|
038b6765e7 | ||
|
58569a648c | ||
|
df0f048ecc | ||
|
288a008e72 | ||
|
439690b981 | ||
|
b4e75f6844 | ||
|
3f6a5a7772 | ||
|
a2d7cdcfb4 | ||
|
d995ce38ea | ||
|
0b21a22027 | ||
|
111d75b351 | ||
|
b54cac72ec | ||
|
69b2dd698c | ||
|
16bf19d115 | ||
|
c57c9c2a8f | ||
|
9f750e5980 | ||
|
8d296e563d | ||
|
88f7570caf | ||
|
00a7c6b5be | ||
|
612b06346d | ||
|
9620566fbb | ||
|
ed65c70a36 | ||
|
cdd87436be | ||
|
a143ebc8e0 | ||
|
93581ca4d9 | ||
|
c5e9c9a683 | ||
|
1bdab10872 | ||
|
eca339ad60 | ||
|
af5eb3447f | ||
|
b72cb52a71 | ||
|
d0565a15d3 | ||
|
468bf1d6a0 | ||
|
1d2637c980 | ||
|
a52c68d852 | ||
|
e12f57d872 | ||
|
5def02ff7f | ||
|
7cab6eb680 | ||
|
dcaa3526a4 | ||
|
2a989bc235 | ||
|
d588fab981 | ||
|
9cb006b871 | ||
|
9056abaf4a | ||
|
3c27fd10b6 | ||
|
4048d79fc5 | ||
|
212e60c12d | ||
|
45142bc1bc | ||
|
e791a663c8 | ||
|
81ae95eba7 | ||
|
7ca5d81123 | ||
|
e234d2379b | ||
|
d0838d53c2 | ||
|
880e2160a3 | ||
|
c214d776df | ||
|
0a8220ca52 | ||
|
b0c57272c2 | ||
|
fbdebcae8a | ||
|
8e376deaaa | ||
|
2d353ffa35 | ||
|
b70a9c36fa | ||
|
c98967cb1e | ||
|
05f0d1855c | ||
|
cee5043625 | ||
|
528358f3c3 | ||
|
da90412cea | ||
|
e794aab012 | ||
|
69daf14ffd | ||
|
afa6c4270d | ||
|
be22309e45 | ||
|
6f27e5ae07 | ||
|
7e79547973 | ||
|
a5668b1b99 | ||
|
f04dd6b6cd | ||
|
9f9c17ea8a | ||
|
48845ab60e | ||
|
59bc2318f8 | ||
|
c622db918b | ||
|
4b381f33b5 | ||
|
7cfc7b9d74 | ||
|
7f52f9bcf2 | ||
|
c4e7e42cdd | ||
|
d181365620 | ||
|
97b474665a | ||
|
284b01c524 | ||
|
e3857ca0f4 | ||
|
0aee42255f | ||
|
fcb0d33e29 | ||
|
3a761f2082 | ||
|
ed0ad7b988 | ||
|
c392b5a661 | ||
|
766848eea3 | ||
|
7ede537d1b | ||
|
ec38f90662 | ||
|
d793b82d51 | ||
|
aafeb1e875 | ||
|
480c10b239 | ||
|
82bb2544de | ||
|
b714abd9a0 | ||
|
c1be0180f9 | ||
|
2cfafe7bfe | ||
|
da8edbfdc1 | ||
|
54bcbbc1de | ||
|
5463183e01 | ||
|
42b80b18f8 | ||
|
65fada7ef1 | ||
|
9e4997aa81 | ||
|
e8c6ef945a | ||
|
14a22efae1 | ||
|
f7f0bc8681 | ||
|
9cb8a135e6 | ||
|
a02bce6517 | ||
|
9604c3a187 | ||
|
1ba4213910 | ||
|
6c63fac240 | ||
|
b3dd531abb | ||
|
4ce42d4f70 | ||
|
4325fcdca2 | ||
|
8cf9d5f3ed | ||
|
8cb6015930 | ||
|
f3cefde7cb | ||
|
92fcdde60d | ||
|
2dcf55ea89 | ||
|
8537f40f1d | ||
|
8417590893 | ||
|
4c43f5db15 | ||
|
eb32ec89b4 | ||
|
ad66c02735 | ||
|
e8c0611db4 | ||
|
7fa54642e1 | ||
|
0e6d40f96c | ||
|
cd1fe4e182 | ||
|
c56b601ab8 | ||
|
e78fa11fd4 | ||
|
1619dd29e9 | ||
|
793f3dd75c | ||
|
ce05f8e958 | ||
|
c8c4585946 | ||
|
990d1adfb2 | ||
|
404343de1d | ||
|
24bfaaca7e | ||
|
060ca43fc8 | ||
|
b87e802c8d | ||
|
0f596d5620 | ||
|
3ebdb67bc0 | ||
|
a5668ef729 | ||
|
24fccf6c44 | ||
|
e12692252e | ||
|
a6cbc9f0ef | ||
|
be073daaf2 | ||
|
4c919a7489 | ||
|
7a7ed48063 | ||
|
01010fe795 | ||
|
32382dc461 | ||
|
bf83341fb9 | ||
|
c4d2a7b409 | ||
|
4ad832dcdd | ||
|
b35d25eda4 | ||
|
da51a5512f | ||
|
710d51bf32 | ||
|
446bb229bc | ||
|
c605023bad | ||
|
28d866c001 | ||
|
95b587381b | ||
|
fc14431147 | ||
|
5d8f2f5da1 | ||
|
cf428dc1f7 | ||
|
c579989ded | ||
|
a954a62762 | ||
|
855860c00c | ||
|
58c48b1b21 | ||
|
512c392386 | ||
|
c5ed70dbfc | ||
|
f75ca0c399 | ||
|
75c2977daf | ||
|
b3ef4b40c5 | ||
|
bd5664cf8e | ||
|
272219eb61 | ||
|
acbcb47739 | ||
|
672575e427 | ||
|
be0b3b1d63 | ||
|
c88d25b4ee | ||
|
9b83862a96 | ||
|
21c7f0da6e | ||
|
dca5759569 | ||
|
0abf87bfa2 | ||
|
6b8b37843b | ||
|
fa0b83f056 | ||
|
a706fa3590 | ||
|
852aa3d260 | ||
|
c4edcaad87 | ||
|
9d0cab01d5 | ||
|
c3e507234d | ||
|
405ec82dd0 | ||
|
9c4c77fe84 | ||
|
831afdf9e7 | ||
|
1996efb183 | ||
|
19bb2a0238 | ||
|
af6cf70558 | ||
|
9b57e6049e | ||
|
f54c06fb94 | ||
|
2107e7c427 | ||
|
eae277f165 | ||
|
a9c4ad2895 | ||
|
e88ba1ab1c | ||
|
677dcb87ef | ||
|
7bb217636e | ||
|
abc16e98eb | ||
|
3b1405609e | ||
|
b0d3518c1d | ||
|
aed005618e | ||
|
559970c95d | ||
|
f6f3aa11ea | ||
|
472c91f022 | ||
|
4a7f498683 | ||
|
2b87d6ec01 | ||
|
1af282a7a1 | ||
|
46b63f52fe | ||
|
01977005fb | ||
|
b1a46b365b | ||
|
3a0a02d3ba | ||
|
1acbef5bfa | ||
|
66b2ba07bb | ||
|
4777538103 | ||
|
31d31fc3d3 | ||
|
21f606c6ba | ||
|
6508ec2d17 | ||
|
c623ca9fe0 | ||
|
90deaf564f | ||
|
e2d2cf67fa | ||
|
8ee73a11bb | ||
|
b5b01be373 | ||
|
730f2eb0b8 | ||
|
4576754de2 | ||
|
0fb9e0bc89 | ||
|
000c34ef73 | ||
|
0ec80a7791 | ||
|
e9650285bd | ||
|
d0cc0a4034 | ||
|
f9c4a3a9c0 | ||
|
15fd2021bb | ||
|
75cf1d610e | ||
|
8f5ee9466a | ||
|
467720f1c4 | ||
|
026e624e23 | ||
|
460a9fc5f7 | ||
|
3695e64ce9 | ||
|
b3bb829e4d | ||
|
1b9f8c23d2 | ||
|
5ee5fd7332 | ||
|
18d94b9a26 | ||
|
3ae9686b2b | ||
|
bbe4ef8497 | ||
|
a2fd354dc9 | ||
|
a7202078ce | ||
|
d8940a9cea | ||
|
96611333bf | ||
|
5074123f57 | ||
|
5bfbd3eaa0 | ||
|
7fdeed14f5 | ||
|
04a9ca92b5 | ||
|
016a1e9adb | ||
|
1d12e57606 | ||
|
cb753f5371 | ||
|
7a9f9b2705 | ||
|
6d4b4dd9fc | ||
|
3d9c92fb63 | ||
|
d4168f6b5d | ||
|
1751cabb9d | ||
|
b0e3b6414a | ||
|
51967ed9f5 | ||
|
fc06f13c30 | ||
|
b094707324 | ||
|
e4a0a517d5 | ||
|
222a5f3779 | ||
|
72f7106086 | ||
|
33387b60cc | ||
|
83cc18f648 | ||
|
1735ca57d5 | ||
|
a49459b253 | ||
|
332e429a41 | ||
|
7be25c21ed | ||
|
1dfa5d383c | ||
|
c2d2c278e0 | ||
|
75a57829c2 |
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2017,
|
||||
"ecmaVersion": 2020,
|
||||
"ecmaFeatures": {
|
||||
"globalReturn": true
|
||||
}
|
||||
|
18
.github/CONTRIBUTING.md
vendored
18
.github/CONTRIBUTING.md
vendored
@@ -4,13 +4,15 @@ Thanks for contributing to MagicMirror²!
|
||||
|
||||
We hold our code to standard, and these standards are documented below.
|
||||
|
||||
## Linters
|
||||
|
||||
If you wish to run our linters, use `npm run lint` without any arguments.
|
||||
|
||||
### JavaScript: Run ESLint
|
||||
|
||||
We use [ESLint](https://eslint.org) on our JavaScript files.
|
||||
|
||||
Our ESLint configuration is in our .eslintrc.json and .eslintignore files.
|
||||
Our ESLint configuration is in our `.eslintrc.json` and `.eslintignore` files.
|
||||
|
||||
To run ESLint, use `npm run lint:js`.
|
||||
|
||||
@@ -20,7 +22,15 @@ We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is i
|
||||
|
||||
To run StyleLint, use `npm run lint:css`.
|
||||
|
||||
### Submitting Issues
|
||||
## Testing
|
||||
|
||||
We use [Jest](https://jestjs.io) for JavaScript testing.
|
||||
|
||||
To run all tests, use `npm run test`.
|
||||
|
||||
The specific test commands are defined in `package.json`. So you can also run the specific tests with other commands, e.g. `npm run test:unit` or `npx jest tests/e2e/env_spec.js`.
|
||||
|
||||
## Submitting Issues
|
||||
|
||||
Please only submit reproducible issues.
|
||||
|
||||
@@ -32,9 +42,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 12 or later (recommended is 14).
|
||||
**Node Version**: Make sure it's version 14 or later (recommended is 16).
|
||||
|
||||
**MagicMirror Version**: Please let us know which version of MagicMirror you are running. It can be found in the `package.json` 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.
|
||||
|
||||
|
18
.github/ISSUE_TEMPLATE.md
vendored
18
.github/ISSUE_TEMPLATE.md
vendored
@@ -10,19 +10,17 @@ If you're not sure if it's a real bug or if it's just you, please open a topic o
|
||||
|
||||
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
|
||||
|
||||
A common problem is that your config file could be invalid. Please run in your MagicMirror directory: `npm run config:check` and see if it reports an error.
|
||||
A common problem is that your config file could be invalid. Please run in your MagicMirror² directory: `npm run config:check` and see if it reports an error.
|
||||
|
||||
## I found a bug in the MagicMirror installer
|
||||
## I found a bug in the MagicMirror² installer
|
||||
|
||||
If you are facing an issue or found a bug while trying to install MagicMirror via the installer please report it in the respective GitHub repository:
|
||||
If you are facing an issue or found a bug while trying to install MagicMirror² via the installer please report it in the respective GitHub repository:
|
||||
[https://github.com/sdetweil/MagicMirror_scripts](https://github.com/sdetweil/MagicMirror_scripts)
|
||||
|
||||
## I found a bug in the MagicMirror Docker image
|
||||
## 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 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)
|
||||
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:
|
||||
[https://gitlab.com/khassel/magicmirror](https://gitlab.com/khassel/magicmirror)
|
||||
|
||||
---
|
||||
|
||||
@@ -33,9 +31,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 12 or later (recommended is 14).
|
||||
**Node Version**: Make sure it's version 14 or later (recommended is 16).
|
||||
|
||||
**MagicMirror Version**: Please let us know which version of MagicMirror you are running. It can be found in the `package.json` 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.
|
||||
|
||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,4 +1,4 @@
|
||||
Hello and thank you for wanting to contribute to the MagicMirror project
|
||||
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:**
|
||||
|
||||
|
6
.github/dependabot.yaml
vendored
Normal file
6
.github/dependabot.yaml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
0
.github/stale.yml → .github/stale.yaml
vendored
0
.github/stale.yml → .github/stale.yaml
vendored
@@ -9,29 +9,31 @@ on:
|
||||
pull_request:
|
||||
branches: [master, develop]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x, 14.x, 16.x]
|
||||
node-version: [14.x, 16.x, 18.x]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "npm"
|
||||
- name: Install dependencies and run tests
|
||||
run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
npm install
|
||||
npm run install-mm:dev
|
||||
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
|
||||
npm run test
|
@@ -1,4 +1,5 @@
|
||||
# This workflow runs the automated test and uploads the coverage results to codecov.io
|
||||
# For more information see: https://github.com/codecov/codecov-action
|
||||
|
||||
name: "Run Codecov Tests"
|
||||
|
||||
@@ -8,13 +9,16 @@ on:
|
||||
pull_request:
|
||||
branches: [master, develop]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
run-and-upload-coverage-report:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Install dependencies and run coverage
|
||||
run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
@@ -23,7 +27,7 @@ jobs:
|
||||
touch css/custom.css
|
||||
npm run test:coverage
|
||||
- name: Upload coverage results to codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
files: ./coverage/lcov.info
|
||||
fail_ci_if_error: true
|
14
.github/workflows/depsreview.yaml
vendored
Normal file
14
.github/workflows/depsreview.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: "Dependency Review"
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v3
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v2
|
@@ -1,4 +1,5 @@
|
||||
# This workflow enforces the update of a changelog file on every pull request
|
||||
# For more information see: https://github.com/dangoslen/changelog-enforcer
|
||||
|
||||
name: "Enforce Changelog"
|
||||
|
||||
@@ -11,10 +12,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Enforce changelog️
|
||||
uses: dangoslen/changelog-enforcer@v1.6.1
|
||||
uses: dangoslen/changelog-enforcer@v3
|
||||
with:
|
||||
changeLogPath: "CHANGELOG.md"
|
||||
skipLabels: "Skip Changelog"
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,7 +16,6 @@ vendor/node_modules/**/*
|
||||
jspm_modules
|
||||
.npm
|
||||
.node_repl_history
|
||||
.nyc_output/
|
||||
|
||||
# Visual Studio Code ignoramuses.
|
||||
.vscode/
|
||||
|
@@ -1,4 +1,7 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint:staged
|
||||
[ -f "$(dirname "$0")/_/husky.sh" ] && . "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
if command -v npm &> /dev/null; then
|
||||
npm run lint:staged
|
||||
fi
|
||||
|
@@ -1,4 +1,3 @@
|
||||
/config
|
||||
/coverage
|
||||
.nyc_output
|
||||
package-lock.json
|
||||
|
261
CHANGELOG.md
261
CHANGELOG.md
@@ -3,7 +3,198 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
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²
|
||||
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror².
|
||||
|
||||
## [2.22.0] - 2023-01-01
|
||||
|
||||
Thanks to: @angeldeejay, @buxxi, @dariom, @dWoolridge, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @retroflex, @SkySails and @Tom.
|
||||
|
||||
Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you!
|
||||
|
||||
### Added
|
||||
|
||||
- Added test for remoteFile option in compliments module
|
||||
- Added hourlyWeather functionality to Weather.gov weather provider
|
||||
- Removed weatherEndpoint definition from weathergov.js (not used)
|
||||
- Added css class names "today" and "tomorrow" for default calendar
|
||||
- Added Collaboration.md
|
||||
- Added new github action for dependency review (#2862)
|
||||
- Added a WeatherProvider for Open-Meteo
|
||||
- Added Yr as a weather provider
|
||||
- Added config options "ignoreXOriginHeader" and "ignoreContentSecurityPolicy"
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed usage of internal fetch function of node until it is more stable
|
||||
|
||||
### Updated
|
||||
|
||||
- Cleaned up test directory (#2937) and jest config (#2959)
|
||||
- Wait for all modules to start before declaring the system ready (#2487)
|
||||
- Updated e2e tests (moved `done()` in helper functions) and use es6 syntax in all tests
|
||||
- Updated da translation
|
||||
- Rework weather module
|
||||
- Make sure smhi provider api only gets a maximum of 6 digits coordinates (#2955)
|
||||
- Use fetch instead of XMLHttpRequest in weatherprovider (#2935)
|
||||
- Reworked how weatherproviders handle units (#2849)
|
||||
- Use unix() method for parsing times, fix suntimes on the way (#2950)
|
||||
- Refactor conversion functions into utils class (#2958)
|
||||
- The `cors`-method in `server.js` now supports sending and recieving HTTP headers
|
||||
- Replace `…` by `…`
|
||||
- Cleanup compliments module
|
||||
- Updated dependencies including electron to v22 (#2903)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Correctly show apparent temperature in SMHI weather provider
|
||||
- Ensure updatenotification module isn't shown when local is _ahead_ of remote
|
||||
- Handle node_helper errors during startup (#2944)
|
||||
- Possibility to change FontAwesome class in calendar, so icons like `fab fa-facebook-square` works.
|
||||
- Fix cors problems with newsfeed articles (as far as possible), allow disabling cors per feed with option `useCorsProxy: false` (#2840)
|
||||
- Tests not waiting for the application to start and stop before starting the next test
|
||||
- Fix electron tests failing sometimes in github workflow
|
||||
- Fixed gap in clock module when displayed on the left side with displayType=digital
|
||||
- Fixed playwright issue by upgrading to v1.29.1 (#2969)
|
||||
|
||||
## [2.21.0] - 2022-10-01
|
||||
|
||||
Special thanks to: @BKeyport, @buxxi, @davide125, @khassel, @kolbyjack, @krukle, @MikeBishop, @rejas, @sdetweil, @SkySails and @veeck
|
||||
|
||||
### Added
|
||||
|
||||
- Added possibility to fetch calendars through socket notifications.
|
||||
- New scripts `install-mm` (and `install-mm:dev`) for simplifying mm installation (now: `npm run install-mm`) and adding params `--no-audit --no-fund --no-update-notifier` for less noise.
|
||||
- New `showTimeToday` option in calendar module shows time for current-day events even if `timeFormat` is `"relative"`.
|
||||
- Added hourly forecasts, apparent temperature & custom location name to SMHI weather provider.
|
||||
- Added new electron tests for calendar and moved some compliments tests from `e2e` to `electron` because of date mocking, removed mock stuff from compliments module.
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed old and deprecated weather modules `currentweather` and `weatherforecast`.
|
||||
- Removed `DAYAFTERTOMORROW` from English.
|
||||
|
||||
### Updated
|
||||
|
||||
- Updated dependencies.
|
||||
- Updated jsdoc.
|
||||
- Updated font tree to use variables consistently.
|
||||
- Removed deprecated Docker Repository from issue template.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Broadcast all calendar events while still honoring global and per-calendar maximumEntries.
|
||||
- Respect rss ttl provided by newsfeed (#2883).
|
||||
- Fix multi day calendar events always presented as "(1/X)" instead of the amount of days the event has progressed.
|
||||
- Fix weatherbit provider to use type config value instead of endpoint.
|
||||
- Fix calendar events which DO NOT specify rrule byday adjusted incorrectly (#2885).
|
||||
- Fix e2e tests not failing on errors (#2911).
|
||||
|
||||
## [2.20.0] - 2022-07-02
|
||||
|
||||
Special thanks to the following contributors: @eouia, @khassel, @kolbyjack, @KristjanESPERANTO, @nathannaveen, @naveensrinivasan, @rejas, @rohitdharavath and @sdetweil.
|
||||
|
||||
### Added
|
||||
|
||||
- Added a new config option `httpHeaders` used by helmet (see https://helmetjs.github.io/). You can now set own httpHeaders which will override the defaults in `js/defauls.js` which is useful e.g. if you want to embed MagicMirror into annother website (solves #2847).
|
||||
- Show endDate for calendar events when dateHeader is enabled and showEnd is set to true (#2192).
|
||||
- Added the notification emitting from the weather module on information updated.
|
||||
- Use recommended file extension for YAML files (#2864).
|
||||
|
||||
### Updated
|
||||
|
||||
- Use latest node 18 when running tests on github actions.
|
||||
- Updated `electron` to v19 and other dependencies.
|
||||
- Use internal fetch function of node instead external `node-fetch` library if used node version >= `v18`.
|
||||
- Include duplicate events in broadcasts.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix problems with non latin fonds caused by updating to fontsource (fixes #2835).
|
||||
|
||||
## [2.19.0] - 2022-04-01
|
||||
|
||||
Special thanks to the following contributors: @10bias, @CFenner, @JHWelch, @k1rd3rf, @khassel, @kolbyjack, @krekos, @KristjanESPERANTO, @Nerfzooka, @oraclesean, @oscarb, @philnagel, @rejas, @sdetweil, @shin10, @SiderealArt and @Tom-Hirschberger.
|
||||
|
||||
### Added
|
||||
|
||||
- Added a config option under the weather module, `absoluteDates`, providing an option to format weather forecast date output with either absolute or relative dates.
|
||||
- Added test for new weather forecast `absoluteDates` property.
|
||||
- The modules get a class hidden added/removed if they get hidden/shown which will also toggle pointer-events.
|
||||
- Added new config option `showTitleAsUrl` to newsfeed module. If set, the displayed title is a link to the article which is useful when running in a browser and you want to read this article.
|
||||
- Added internal cors proxy to get weather providers working without public proxies (fixes #2714). The new url `http(s)://address:port/cors?url=https://whatever-to-proxy` can be used in other modules too.
|
||||
- Added a WeatherProvider for Weatherflow.
|
||||
- Added new env var `ELECTRON_DISABLE_GPU` which disable gpu under electron if set (fixes #2831).
|
||||
- Added missing Czech translations.
|
||||
|
||||
### Updated
|
||||
|
||||
- Deprecated roboto fonts package `roboto-fontface-bower` replaced with `fontsource`.
|
||||
- Updated `electron` to v17, `helmet` to v5 (use defaults of v4) and other dependencies
|
||||
- Updated Font Awesome css class to new default style (fixes #2768)
|
||||
- Replaced deprecated modules `currentweather` and `weatherforecast` with dummy modules only displaying that they have to be replaced.
|
||||
- Include all calendar events from the configured date range when broadcasting.
|
||||
- Updated Danish and German translation.
|
||||
- Updated `node-ical` to v0.15 and added `luxon` as dependency for not breaking the "no-optional" install (see #2718 and #2824).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved and speedup e2e tests, artificial wait after mm start removed.
|
||||
- Improved husky setup not blocking `git commit` if `husky` or `npm` is not installed.
|
||||
- Using a consistent spelling of MagicMirror².
|
||||
- Fix minor console output issue for loading translations (#2814).
|
||||
- Don't adjust startDate for full day events if endDate is in the past.
|
||||
- Fix windspeed conversion error in openweathermap provider. (#2812)
|
||||
- Fix conflicting parms turning off showEnd for full day events. (#2629)
|
||||
- Fix regression, calendar.maximumEntries not used to filter calendar level entries (#2868)
|
||||
|
||||
## [2.18.0] - 2022-01-01
|
||||
|
||||
Special thanks to the following contributors: @AmpioRosso, @eouia, @fewieden, @jupadin, @khassel, @kolbyjack, @KristjanESPERANTO, @MariusVaice, @rejas, @rico24 and @sdetweil.
|
||||
|
||||
### Added
|
||||
|
||||
- Added test for calendar recurring event with checks the correct date displayed (related to #2752).
|
||||
|
||||
### Updated
|
||||
|
||||
- ESLint version supports now ECMAScript 2018.
|
||||
- Cleaned up `updatenotification` module and switched to nunjuck template.
|
||||
- Moved calendar tests from category `electron` to `e2e`.
|
||||
- Updated missed translations for Korean language (ko.json).
|
||||
- Updated missed translations for Dutch language (nl.json).
|
||||
- Cleaned up `alert` module and switched to nunjuck template.
|
||||
- Moved weather tests from category `electron` to `e2e`.
|
||||
- Updated github actions.
|
||||
- Replace spectron with playwright, update dependencies including electron update to v16.
|
||||
- Added lithuanian language to translations.js.
|
||||
- Show info message if newsfeed is empty (fixes #2731).
|
||||
- Added dangerouslyDisableAutoEscaping config option for newsfeed templates (fixes #2712).
|
||||
- Added missing shebang to `installers/mm.sh`.
|
||||
- Node versions in templates and github workflows.
|
||||
- Updated translations for Traditional Chinese (Taiwan) (zh-tw.json).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed wrong file `kr.json` to `ko.json`. Use language code 'ko' instead of 'kr' for Korean language.
|
||||
- Fixed `feels_like` data from openweathermaps current weather being ignored (#2678).
|
||||
- Fixed chaotic newsfeed display after network connection loss thanks to @jalibu (#2638).
|
||||
- Fixed incorrect time zone correction of recurring full day events (#2632 and #2634).
|
||||
- Fixed e2e tests by increasing testTimeout.
|
||||
- Revert node-ical update due to missing luxon package.
|
||||
- Fixed User-Agent-Header for newsfeed and calendar module (#2729).
|
||||
- Replace broken shields in Readme and use https for links.
|
||||
- Fixed electron tests with retry.
|
||||
- Fixed Calendar recurring cross timezone error (add/subtract a day, not just offset hours) (#2632).
|
||||
- Fixed Calendar showEnd and Full Date overlay (#2629).
|
||||
- Fixed regression on #2632, #2752.
|
||||
- Broadcast custom symbols in CALENDAR_EVENTS.
|
||||
|
||||
## [2.17.1] - 2021-10-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed error when accessing letsencrypt certificates
|
||||
- Fixed Calendar module enhancement: displaying full events without time (#2424)
|
||||
|
||||
## [2.17.0] - 2021-10-01
|
||||
|
||||
@@ -21,14 +212,14 @@ Special thanks to the following contributors: @apiontek, @eouia, @jupadin, @khas
|
||||
- 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.
|
||||
- Updated jsdocs and print warnings during testing too.
|
||||
- Updated 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.
|
||||
- Updated dependencies in package.json.
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -62,13 +253,13 @@ Special thanks to the following contributors: @210954, @B1gG, @codac, @Crazylegs
|
||||
- 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.
|
||||
- Updated dependencies in package.json, require node >= v12, remove `rrule-alt` and `rrule`.
|
||||
- Updated 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.
|
||||
- Updated documentation.
|
||||
- Updated jest tests: Reset changes on js/logger.js, mock logger.js in global_vars tests.
|
||||
- Updated dependencies in package.json.
|
||||
|
||||
### Removed
|
||||
|
||||
@@ -180,10 +371,10 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
|
||||
|
||||
- Merging .gitignore in the config-folder with the .gitignore in the root-folder.
|
||||
- Weather module - forecast now show TODAY and TOMORROW instead of weekday, to make it easier to understand.
|
||||
- Update dependencies to latest versions.
|
||||
- Update dependencies eslint, feedme, simple-git and socket.io to latest versions.
|
||||
- Update lithuanian translation.
|
||||
- Update config sample.
|
||||
- Updated dependencies to latest versions.
|
||||
- Updated dependencies eslint, feedme, simple-git and socket.io to latest versions.
|
||||
- Updated lithuanian translation.
|
||||
- Updated config sample.
|
||||
- Highlight required version mismatch.
|
||||
- No select Text for TouchScreen use.
|
||||
- Corrected logic for timeFormat "relative" and "absolute".
|
||||
@@ -204,19 +395,19 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
|
||||
- Rename Greek translation to correct ISO 639-1 alpha-2 code (gr > el). (#2155)
|
||||
- Add a space after icons of sunrise and sunset. (#2169)
|
||||
- Fix calendar when no DTEND record found in event, startDate overlay when endDate set. (#2177)
|
||||
- Fix windspeed convertion error in ukmetoffice weather provider. (#2189)
|
||||
- Fix windspeed conversion error in ukmetoffice weather provider. (#2189)
|
||||
- Fix console.debug not having timestamps. (#2199)
|
||||
- Fix calendar full day event east of UTC start time. (#2200)
|
||||
- Fix non-fullday recurring rule processing. (#2216)
|
||||
- Catch errors when parsing calendar data with ical. (#2022)
|
||||
- Fix Default Alert Module does not hide black overlay when alert is dismissed manually. (#2228)
|
||||
- Weather module - Always displays night icons when local is other than English. (#2221)
|
||||
- Update node-ical 0.12.4, fix invalid RRULE format in cal entries
|
||||
- Updated node-ical 0.12.4, fix invalid RRULE format in cal entries
|
||||
- Fix package.json for optional electron dependency (2378)
|
||||
- Update node-ical version again, 0.12.5, change RRULE fix (#2371, #2379)
|
||||
- Updated node-ical version again, 0.12.5, change RRULE fix (#2371, #2379)
|
||||
- Remove undefined objects from modules array (#2382)
|
||||
- Update node-ical version again, 0.12.7, change RRULE fix (#2371, #2379), node-ical now throws error (which we catch)
|
||||
- Update simple-git version to 2.31 unhandled promise rejection (#2383)
|
||||
- Updated node-ical version again, 0.12.7, change RRULE fix (#2371, #2379), node-ical now throws error (which we catch)
|
||||
- Updated simple-git version to 2.31 unhandled promise rejection (#2383)
|
||||
|
||||
## [2.13.0] - 2020-10-01
|
||||
|
||||
@@ -227,8 +418,8 @@ Special thanks to the following contributors: @bryanzzhu, @bugsounet, @chamakura
|
||||
### Added
|
||||
|
||||
- `--dry-run` Added option in fetch call within updatenotification node_helper. This is to prevent
|
||||
MagicMirror from consuming any fetch result. Causes conflict with MMPM when attempting to check
|
||||
for updates to MagicMirror and/or MagicMirror modules.
|
||||
MagicMirror² from consuming any fetch result. Causes conflict with MMPM when attempting to check
|
||||
for updates to MagicMirror² and/or MagicMirror² modules.
|
||||
- Test coverage with Istanbul, run it with `npm run test:coverage`.
|
||||
- Added lithuanian language.
|
||||
- Added support in weatherforecast for OpenWeather onecall API.
|
||||
@@ -301,7 +492,7 @@ Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryan
|
||||
|
||||
🚨 READ THIS BEFORE UPDATING 🚨
|
||||
|
||||
In the past years the project has grown a lot. This came with a huge downside: poor maintainability. If I let the project continue the way it was, it would eventually crash and burn. More important: I would completely lose the drive and interest to continue the project. Because of this the decision was made to simplify the core by removing all side features like automatic installers and support for exotic platforms. This release (2.11.0) is the first real release that will reflect (parts) of these changes. As a result of this, some things might break. So before you continue make sure to backup your installation. Your config, your modules or better yet: your full MagicMirror folder. In other words: update at your own risk.
|
||||
In the past years the project has grown a lot. This came with a huge downside: poor maintainability. If I let the project continue the way it was, it would eventually crash and burn. More important: I would completely lose the drive and interest to continue the project. Because of this the decision was made to simplify the core by removing all side features like automatic installers and support for exotic platforms. This release (2.11.0) is the first real release that will reflect (parts) of these changes. As a result of this, some things might break. So before you continue make sure to backup your installation. Your config, your modules or better yet: your full MagicMirror² folder. In other words: update at your own risk.
|
||||
|
||||
For more information regarding this major change, please check issue [#1860](https://github.com/MichMich/MagicMirror/issues/1860).
|
||||
|
||||
@@ -441,10 +632,10 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
|
||||
- English translation for "Feels" to "Feels like"
|
||||
- Fixed the example calendar url in `config.js.sample`
|
||||
- Update `ical.js` to solve various calendar issues.
|
||||
- Update weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676)
|
||||
- Updated `ical.js` to solve various calendar issues.
|
||||
- Updated 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.
|
||||
- Updated weatherprovider documentation.
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -464,7 +655,7 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
- use current username vs hardcoded 'pi' to support non-pi install
|
||||
- check for npm installed. node install doesn't do npm anymore
|
||||
- check for mac as part of PM2 install, add install option string
|
||||
- update pm2 config with current username instead of hard coded 'pi'
|
||||
- Updated pm2 config with current username instead of hard coded 'pi'
|
||||
- check for screen saver config, "/etc/xdg/lxsession", bypass if not setup
|
||||
|
||||
## [2.7.1] - 2019-04-02
|
||||
@@ -502,7 +693,7 @@ Fixed `package.json` version number.
|
||||
- Fixed unhandled error on bad git data in updatenotification module [#1285](https://github.com/MichMich/MagicMirror/issues/1285).
|
||||
- Weather forecast now works with openweathermap in new weather module. Daily data are displayed, see issue [#1504](https://github.com/MichMich/MagicMirror/issues/1504).
|
||||
- Fixed analogue clock border display issue where non-black backgrounds used (previous fix for issue 611)
|
||||
- Fixed compatibility issues caused when modules request different versions of Font Awesome, see issue [#1522](https://github.com/MichMich/MagicMirror/issues/1522). MagicMirror now uses [Font Awesome 5 with v4 shims included for backwards compatibility](https://fontawesome.com/how-to-use/on-the-web/setup/upgrading-from-version-4#shims).
|
||||
- Fixed compatibility issues caused when modules request different versions of Font Awesome, see issue [#1522](https://github.com/MichMich/MagicMirror/issues/1522). MagicMirror² now uses [Font Awesome 5 with v4 shims included for backwards compatibility](https://fontawesome.com/how-to-use/on-the-web/setup/upgrading-from-version-4#shims).
|
||||
- Installation script problems with raspbian
|
||||
- Calendar: only show repeating count if the event is actually repeating [#1534](https://github.com/MichMich/MagicMirror/pull/1534)
|
||||
- Calendar: Fix exdate handling when multiple values are specified (comma separated)
|
||||
@@ -614,7 +805,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
## [2.4.0] - 2018-07-01
|
||||
|
||||
⚠️ **Warning:** This release includes an updated version of Electron. This requires a Raspberry Pi configuration change to allow the best performance and prevent the CPU from overheating. Please read the information on the [MagicMirror Wiki](https://github.com/michmich/magicmirror/wiki/configuring-the-raspberry-pi#enable-the-open-gl-driver-to-decrease-electrons-cpu-usage).
|
||||
⚠️ **Warning:** This release includes an updated version of Electron. This requires a Raspberry Pi configuration change to allow the best performance and prevent the CPU from overheating. Please read the information on the [MagicMirror² Wiki](https://github.com/michmich/magicmirror/wiki/configuring-the-raspberry-pi#enable-the-open-gl-driver-to-decrease-electrons-cpu-usage).
|
||||
|
||||
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
|
||||
|
||||
@@ -672,7 +863,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Add types for module.
|
||||
- Implement Danger.js to notify contributors when CHANGELOG.md is missing in PR.
|
||||
- Allow scrolling in full page article view of default newsfeed module with gesture events from [MMM-Gestures](https://github.com/thobach/MMM-Gestures)
|
||||
- Changed 'compliments.js' - update DOM if remote compliments are loaded instead of waiting one updateInterval to show custom compliments
|
||||
- Changed 'compliments.js' - Updated DOM if remote compliments are loaded instead of waiting one updateInterval to show custom compliments
|
||||
- Automated unit tests utils, deprecated, translator, cloneObject(lockstrings)
|
||||
- Automated integration tests translations
|
||||
- Add advanced filtering to the excludedEvents configuration of the default calendar module
|
||||
@@ -684,7 +875,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
- Add link to GitHub repository which contains the respective Dockerfile.
|
||||
- Optimized automated unit tests cloneObject, cmpVersions
|
||||
- Update notifications use now translation templates instead of normal strings.
|
||||
- Updated notifications use now translation templates instead of normal strings.
|
||||
- Yarn can be used now as an installation tool
|
||||
- Changed Electron dependency to v1.7.13.
|
||||
|
||||
@@ -855,7 +1046,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Add use pm2 for manager process into Installer RaspberryPi script.
|
||||
- Russian Translation.
|
||||
- Afrikaans Translation.
|
||||
- Add postinstall script to notify user that MagicMirror installed successfully despite warnings from NPM.
|
||||
- Add postinstall script to notify user that MagicMirror² installed successfully despite warnings from NPM.
|
||||
- Init tests using mocha.
|
||||
- Option to use RegExp in Calendar's titleReplace.
|
||||
- Hungarian Translation.
|
||||
@@ -891,7 +1082,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
### Fixed
|
||||
|
||||
- Update .gitignore to not ignore default modules folder.
|
||||
- Updated .gitignore to not ignore default modules folder.
|
||||
- Remove white flash on boot up.
|
||||
- Added `update` in Raspberry Pi installation script.
|
||||
- Fix an issue where the analog clock looked scrambled. ([#611](https://github.com/MichMich/MagicMirror/issues/611))
|
||||
@@ -918,7 +1109,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Add VSCode IntelliSense support.
|
||||
- Module API: Add Visibility locking to module system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#visibility-locking) for more information.
|
||||
- Module API: Method to overwrite the module's header. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#getheader) for more information.
|
||||
- Module API: Option to define the minimum MagicMirror version to run a module. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#requiresversion) for more information.
|
||||
- Module API: Option to define the minimum MagicMirror² version to run a module. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#requiresversion) for more information.
|
||||
- Calendar module now broadcasts the event list to all other modules using the notification system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/calendar) for more information.
|
||||
- Possibility to use the calendar feed as the source for the weather (currentweather & weatherforecast) location data. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/weatherforecast) for more information.
|
||||
- Added option to show rain amount in the weatherforecast default module
|
||||
@@ -976,8 +1167,8 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
### Updated
|
||||
|
||||
- Force fullscreen when kioskmode is active.
|
||||
- Update the .github templates and information with more modern information.
|
||||
- Update the Gruntfile with a more functional StyleLint implementation.
|
||||
- Updated the .github templates and information with more modern information.
|
||||
- Updated the Gruntfile with a more functional StyleLint implementation.
|
||||
|
||||
## [2.0.4] - 2016-08-07
|
||||
|
||||
@@ -1065,6 +1256,6 @@ It includes (but is not limited to) the following features:
|
||||
|
||||
## [1.0.0] - 2014-02-16
|
||||
|
||||
### Initial release of MagicMirror.
|
||||
### Initial release of MagicMirror
|
||||
|
||||
This was part of the blogpost: [https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the](https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the)
|
||||
|
12
Collaboration.md
Normal file
12
Collaboration.md
Normal file
@@ -0,0 +1,12 @@
|
||||
This document describes how collaborators of this repository should work together.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- never merge your own PR's
|
||||
- never merge without someone having approved (approving and merging from same person is allowed)
|
||||
- wait for all approvals requested (or the author decides something different in the comments)
|
||||
|
||||
## Issues
|
||||
|
||||
- "real" Issues are closed if the problem is solved and the fix is released
|
||||
- unrelated Issues (e.g. related to a foreign module) are closed immediately with a comment to open an issue in the module repository or to discuss this further in the forum or discord
|
@@ -1,6 +1,6 @@
|
||||
# The MIT License (MIT)
|
||||
|
||||
Copyright © 2016-2021 Michael Teeuw
|
||||
Copyright © 2016-2022 Michael Teeuw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
|
19
README.md
19
README.md
@@ -1,12 +1,17 @@
|
||||

|
||||
|
||||
<p style="text-align: center">
|
||||
<a href="https://david-dm.org/MichMich/MagicMirror"><img src="https://david-dm.org/MichMich/MagicMirror.svg" alt="Dependency Status"></a>
|
||||
<a href="https://david-dm.org/MichMich/MagicMirror?type=dev"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a>
|
||||
<a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge" alt="CLI Best Practices"></a>
|
||||
<a href="https://codecov.io/gh/MichMich/MagicMirror"><img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg?token=LEG1KitZR6" alt="CodeCov Status"/></a>
|
||||
<a href="https://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
||||
<a href="https://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://choosealicense.com/licenses/mit">
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
|
||||
</a>
|
||||
<img src="https://img.shields.io/github/workflow/status/michmich/magicmirror/Run%20Automated%20Tests" alt="GitHub Actions">
|
||||
<img src="https://img.shields.io/github/checks-status/michmich/magicmirror/master" alt="Build Status">
|
||||
<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://github.com/MichMich/MagicMirror">
|
||||
<img src="https://img.shields.io/github/stars/michmich/magicmirror?style=social">
|
||||
</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).
|
||||
@@ -35,7 +40,7 @@ Contributions of all kinds are welcome, not only in the form of code but also wi
|
||||
- documentation
|
||||
- translations
|
||||
|
||||
For the full contribution guidelines, check out: [https://docs.magicmirror.builders/getting-started/contributing.html](https://docs.magicmirror.builders/getting-started/contributing.html)
|
||||
For the full contribution guidelines, check out: [https://docs.magicmirror.builders/about/contributing.html](https://docs.magicmirror.builders/about/contributing.html)
|
||||
|
||||
## Enjoying MagicMirror? Consider a donation!
|
||||
|
||||
|
@@ -1,10 +1,10 @@
|
||||
/* Magic Mirror Config Sample
|
||||
/* MagicMirror² Config Sample
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* For more information on how you can configure this file
|
||||
* see https://docs.magicmirror.builders/getting-started/configuration.html#general
|
||||
* see https://docs.magicmirror.builders/configuration/introduction.html
|
||||
* and https://docs.magicmirror.builders/modules/configuration.html
|
||||
*/
|
||||
let config = {
|
||||
@@ -14,7 +14,7 @@ let config = {
|
||||
// - "0.0.0.0", "::" to listen on any interface
|
||||
// Default, when address config is left out or empty, is "localhost"
|
||||
port: 8080,
|
||||
basePath: "/", // The URL path where MagicMirror is hosted. If you are using a Reverse proxy
|
||||
basePath: "/", // The URL path where MagicMirror² is hosted. If you are using a Reverse proxy
|
||||
// you must set the sub path here. basePath must end with a /
|
||||
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], // Set [] to allow all IP addresses
|
||||
// or add a specific IPv4 of 192.168.1.5 :
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror Custom CSS Sample
|
||||
/* MagicMirror² Custom CSS Sample
|
||||
*
|
||||
* Change color and fonts here.
|
||||
*
|
||||
|
30
css/main.css
30
css/main.css
@@ -8,7 +8,11 @@
|
||||
--font-secondary: "Roboto";
|
||||
|
||||
--font-size: 20px;
|
||||
--font-size-small: 0.75rem;
|
||||
--font-size-xsmall: 0.75rem;
|
||||
--font-size-small: 1rem;
|
||||
--font-size-medium: 1.5rem;
|
||||
--font-size-large: 3.25rem;
|
||||
--font-size-xlarge: 3.75rem;
|
||||
|
||||
--gap-body-top: 60px;
|
||||
--gap-body-right: 60px;
|
||||
@@ -60,27 +64,27 @@ body {
|
||||
}
|
||||
|
||||
.xsmall {
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-xsmall);
|
||||
line-height: 1.275;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 1rem;
|
||||
font-size: var(--font-size-small);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.medium {
|
||||
font-size: 1.5rem;
|
||||
font-size: var(--font-size-medium);
|
||||
line-height: 1.225;
|
||||
}
|
||||
|
||||
.large {
|
||||
font-size: 3.25rem;
|
||||
font-size: var(--font-size-large);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.xlarge {
|
||||
font-size: 3.75rem;
|
||||
font-size: var(--font-size-xlarge);
|
||||
line-height: 1;
|
||||
letter-spacing: -3px;
|
||||
}
|
||||
@@ -115,7 +119,7 @@ body {
|
||||
|
||||
header {
|
||||
text-transform: uppercase;
|
||||
font-size: var(--font-size-small);
|
||||
font-size: var(--font-size-xsmall);
|
||||
font-family: var(--font-primary), Arial, Helvetica, sans-serif;
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid var(--color-text-dimmed);
|
||||
@@ -138,6 +142,14 @@ sup {
|
||||
margin-bottom: var(--gap-modules);
|
||||
}
|
||||
|
||||
.module.hidden {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.module:not(.hidden) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.region.bottom .module {
|
||||
margin-top: var(--gap-modules);
|
||||
margin-bottom: 0;
|
||||
@@ -170,10 +182,6 @@ sup {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.region.fullscreen * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.region.right {
|
||||
right: 0;
|
||||
text-align: right;
|
||||
|
29
fonts/package-lock.json
generated
29
fonts/package-lock.json
generated
@@ -7,20 +7,31 @@
|
||||
"name": "magicmirror-fonts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"roboto-fontface": "^0.10.0"
|
||||
"@fontsource/roboto": "^4.5.8",
|
||||
"@fontsource/roboto-condensed": "^4.5.9"
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
"node_modules/@fontsource/roboto": {
|
||||
"version": "4.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz",
|
||||
"integrity": "sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA=="
|
||||
},
|
||||
"node_modules/@fontsource/roboto-condensed": {
|
||||
"version": "4.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-4.5.9.tgz",
|
||||
"integrity": "sha512-ql4sQq+h8puBVildZ5ssjYf8DWDONYDe3PD3Bu/p1ZW9GnRETRNPPcCTs/q62HIl3QimwwkiKWynn6wZhQaetg=="
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
"@fontsource/roboto": {
|
||||
"version": "4.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz",
|
||||
"integrity": "sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA=="
|
||||
},
|
||||
"@fontsource/roboto-condensed": {
|
||||
"version": "4.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-4.5.9.tgz",
|
||||
"integrity": "sha512-ql4sQq+h8puBVildZ5ssjYf8DWDONYDe3PD3Bu/p1ZW9GnRETRNPPcCTs/q62HIl3QimwwkiKWynn6wZhQaetg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "magicmirror-fonts",
|
||||
"description": "Package for fonts use by MagicMirror Core.",
|
||||
"description": "Package for fonts use by MagicMirror² Core.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/MichMich/MagicMirror.git"
|
||||
@@ -10,6 +10,7 @@
|
||||
"url": "https://github.com/MichMich/MagicMirror/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"roboto-fontface": "^0.10.0"
|
||||
"@fontsource/roboto": "^4.5.8",
|
||||
"@fontsource/roboto-condensed": "^4.5.9"
|
||||
}
|
||||
}
|
||||
|
@@ -2,57 +2,54 @@
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
src: local("Roboto Thin"), local("Roboto-Thin"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff") format("woff");
|
||||
src: local("Roboto Thin"), local("Roboto-Thin"), url("node_modules/@fontsource/roboto/files/roboto-all-100-normal.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local("Roboto Condensed Light"), local("RobotoCondensed-Light"), url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff2") format("woff2"),
|
||||
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff") format("woff");
|
||||
src: local("Roboto Condensed Light"), local("RobotoCondensed-Light"), url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-all-300-normal.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local("Roboto Condensed"), local("RobotoCondensed-Regular"), url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff2") format("woff2"),
|
||||
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff") format("woff");
|
||||
src: local("Roboto Condensed"), local("RobotoCondensed-Regular"), url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-all-400-normal.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local("Roboto Condensed Bold"), local("RobotoCondensed-Bold"), url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff2") format("woff2"),
|
||||
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff") format("woff");
|
||||
src: local("Roboto Condensed Bold"), local("RobotoCondensed-Bold"), url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-all-700-normal.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local("Roboto"), local("Roboto-Regular"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff") format("woff");
|
||||
src: local("Roboto"), local("Roboto-Regular"), url("node_modules/@fontsource/roboto/files/roboto-all-400-normal.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: local("Roboto Medium"), local("Roboto-Medium"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff") format("woff");
|
||||
src: local("Roboto Medium"), local("Roboto-Medium"), url("node_modules/@fontsource/roboto/files/roboto-all-500-normal.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local("Roboto Bold"), local("Roboto-Bold"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff") format("woff");
|
||||
src: local("Roboto Bold"), local("Roboto-Bold"), url("node_modules/@fontsource/roboto/files/roboto-all-700-normal.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local("Roboto Light"), local("Roboto-Light"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2") format("woff2"), url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff") format("woff");
|
||||
src: local("Roboto Light"), local("Roboto-Light"), url("node_modules/@fontsource/roboto/files/roboto-all-300-normal.woff") format("woff");
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
# This file is still here to keep PM2 working on older installations.
|
||||
cd ~/MagicMirror
|
||||
DISPLAY=:0 npm start
|
||||
|
32
jest.config.js
Normal file
32
jest.config.js
Normal file
@@ -0,0 +1,32 @@
|
||||
module.exports = async () => {
|
||||
return {
|
||||
verbose: true,
|
||||
testTimeout: 20000,
|
||||
testSequencer: "<rootDir>/tests/configs/test_sequencer.js",
|
||||
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/helpers/"]
|
||||
},
|
||||
{
|
||||
displayName: "e2e",
|
||||
setupFilesAfterEnv: ["<rootDir>/tests/e2e/helpers/mock-console.js"],
|
||||
testMatch: ["**/tests/e2e/**/*.[jt]s?(x)"],
|
||||
modulePaths: ["<rootDir>/js/"],
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers/", "<rootDir>/tests/e2e/mocks"]
|
||||
}
|
||||
],
|
||||
collectCoverageFrom: ["./clientonly/**/*.js", "./js/**/*.js", "./modules/**/*.js", "./serveronly/**/*.js"],
|
||||
coverageReporters: ["lcov", "text"],
|
||||
coverageProvider: "v8"
|
||||
};
|
||||
};
|
43
js/app.js
43
js/app.js
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* The Core App (Server)
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -37,7 +37,7 @@ if (process.env.MM_PORT) {
|
||||
process.on("uncaughtException", function (err) {
|
||||
Log.error("Whoops! There was an uncaught exception...");
|
||||
Log.error(err);
|
||||
Log.error("MagicMirror will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?");
|
||||
Log.error("MagicMirror² will not quit, but it might be a good idea to check why this happened. Maybe no internet connection?");
|
||||
Log.error("If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues");
|
||||
});
|
||||
|
||||
@@ -128,7 +128,7 @@ function App() {
|
||||
let m = new Module();
|
||||
|
||||
if (m.requiresVersion) {
|
||||
Log.log(`Check MagicMirror version for node helper '${moduleName}' - Minimum version: ${m.requiresVersion} - Current version: ${global.version}`);
|
||||
Log.log(`Check MagicMirror² version for node helper '${moduleName}' - Minimum version: ${m.requiresVersion} - Current version: ${global.version}`);
|
||||
if (cmpVersions(global.version, m.requiresVersion) >= 0) {
|
||||
Log.log("Version is ok!");
|
||||
} else {
|
||||
@@ -222,18 +222,33 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
loadModules(modules, function () {
|
||||
httpServer = new Server(config, function (app, io) {
|
||||
Log.log("Server started ...");
|
||||
loadModules(modules, async function () {
|
||||
httpServer = new Server(config);
|
||||
const { app, io } = await httpServer.open();
|
||||
Log.log("Server started ...");
|
||||
|
||||
for (let nodeHelper of nodeHelpers) {
|
||||
nodeHelper.setExpressApp(app);
|
||||
nodeHelper.setSocketIO(io);
|
||||
nodeHelper.start();
|
||||
const nodePromises = [];
|
||||
for (let nodeHelper of nodeHelpers) {
|
||||
nodeHelper.setExpressApp(app);
|
||||
nodeHelper.setSocketIO(io);
|
||||
|
||||
try {
|
||||
nodePromises.push(nodeHelper.start());
|
||||
} catch (error) {
|
||||
Log.error(`Error when starting node_helper for module ${nodeHelper.name}:`);
|
||||
Log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
Promise.allSettled(nodePromises).then((results) => {
|
||||
// Log errors that happened during async node_helper startup
|
||||
results.forEach((result) => {
|
||||
if (result.status === "rejected") {
|
||||
Log.error(result.reason);
|
||||
}
|
||||
});
|
||||
|
||||
Log.log("Sockets connected & modules started ...");
|
||||
|
||||
if (typeof callback === "function") {
|
||||
callback(config);
|
||||
}
|
||||
@@ -247,14 +262,16 @@ function App() {
|
||||
* exists.
|
||||
*
|
||||
* Added to fix #1056
|
||||
*
|
||||
* @param {Function} callback Function to be called after the app has stopped
|
||||
*/
|
||||
this.stop = function () {
|
||||
this.stop = function (callback) {
|
||||
for (const nodeHelper of nodeHelpers) {
|
||||
if (typeof nodeHelper.stop === "function") {
|
||||
nodeHelper.stop();
|
||||
}
|
||||
}
|
||||
httpServer.close();
|
||||
httpServer.close().then(callback);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
*
|
||||
* Check the configuration file for errors
|
||||
*
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/* global mmPort */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Config Defaults
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -25,6 +25,9 @@ const defaults = {
|
||||
units: "metric",
|
||||
zoom: 1,
|
||||
customCss: "css/custom.css",
|
||||
// httpHeaders used by helmet, see https://helmetjs.github.io/. You can add other/more object values by overriding this in config.js,
|
||||
// e.g. you need to add `frameguard: false` for embedding MagicMirror in another website, see https://github.com/MichMich/MagicMirror/issues/2847
|
||||
httpHeaders: { contentSecurityPolicy: false, crossOriginOpenerPolicy: false, crossOriginEmbedderPolicy: false, crossOriginResourcePolicy: false, originAgentCluster: false },
|
||||
|
||||
modules: [
|
||||
{
|
||||
@@ -36,7 +39,7 @@ const defaults = {
|
||||
position: "upper_third",
|
||||
classes: "large thin",
|
||||
config: {
|
||||
text: "Magic Mirror<sup>2</sup>"
|
||||
text: "MagicMirror²"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -59,7 +62,7 @@ const defaults = {
|
||||
position: "middle_center",
|
||||
classes: "xsmall",
|
||||
config: {
|
||||
text: "If you get this message while your config file is already created,<br>" + "it probably contains an error. To validate your config file run in your MagicMirror directory<br>" + "<pre>npm run config:check</pre>"
|
||||
text: "If you get this message while your config file is already created,<br>" + "it probably contains an error. To validate your config file run in your MagicMirror² directory<br>" + "<pre>npm run config:check</pre>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror Deprecated Config Options List
|
||||
/* MagicMirror² Deprecated Config Options List
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
|
@@ -8,6 +8,12 @@ const Log = require("logger");
|
||||
let config = process.env.config ? JSON.parse(process.env.config) : {};
|
||||
// Module to control application life.
|
||||
const app = electron.app;
|
||||
// If ELECTRON_DISABLE_GPU is set electron is started with --disable-gpu flag.
|
||||
// See https://www.electronjs.org/docs/latest/tutorial/offscreen-rendering for more info.
|
||||
if (process.env.ELECTRON_DISABLE_GPU !== undefined) {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
// Module to create native browser window.
|
||||
const BrowserWindow = electron.BrowserWindow;
|
||||
|
||||
@@ -53,7 +59,7 @@ function createWindow() {
|
||||
// If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
|
||||
|
||||
let prefix;
|
||||
if (config["tls"] !== null && config["tls"]) {
|
||||
if ((config["tls"] !== null && config["tls"]) || config.useHttps) {
|
||||
prefix = "https://";
|
||||
} else {
|
||||
prefix = "http://";
|
||||
@@ -97,6 +103,20 @@ function createWindow() {
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
//remove response headers that prevent sites of being embedded into iframes if configured
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
let curHeaders = details.responseHeaders;
|
||||
if (config["ignoreXOriginHeader"] || false) {
|
||||
curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !/x-frame-options/i.test(header[0])));
|
||||
}
|
||||
|
||||
if (config["ignoreContentSecurityPolicy"] || false) {
|
||||
curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !/content-security-policy/i.test(header[0])));
|
||||
}
|
||||
|
||||
callback({ responseHeaders: curHeaders });
|
||||
});
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
@@ -140,6 +160,13 @@ app.on("before-quit", (event) => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
/* handle errors from self signed certificates */
|
||||
|
||||
app.on("certificate-error", (event, webContents, url, error, certificate, callback) => {
|
||||
event.preventDefault();
|
||||
callback(true);
|
||||
});
|
||||
|
||||
// Start the core application if server is run on localhost
|
||||
// This starts all node helpers and starts the webserver.
|
||||
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) {
|
||||
|
28
js/fetch.js
Normal file
28
js/fetch.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Helper class to provide either third party fetch library or (if node >= 18)
|
||||
* return internal node fetch implementation.
|
||||
*
|
||||
* Attention: After some discussion we always return the third party
|
||||
* implementation until the node implementation is stable and more tested
|
||||
*
|
||||
* @see https://github.com/MichMich/MagicMirror/pull/2952
|
||||
* @see https://github.com/MichMich/MagicMirror/issues/2649
|
||||
* @param {string} url to be fetched
|
||||
* @param {object} options object e.g. for headers
|
||||
* @class
|
||||
*/
|
||||
async function fetch(url, options = {}) {
|
||||
// const nodeVersion = process.version.match(/^v(\d+)\.*/)[1];
|
||||
// if (nodeVersion >= 18) {
|
||||
// // node version >= 18
|
||||
// return global.fetch(url, options);
|
||||
// } else {
|
||||
// // node version < 18
|
||||
// const nodefetch = require("node-fetch");
|
||||
// return nodefetch(url, options);
|
||||
// }
|
||||
const nodefetch = require("node-fetch");
|
||||
return nodefetch(url, options);
|
||||
}
|
||||
|
||||
module.exports = fetch;
|
@@ -1,6 +1,6 @@
|
||||
/* global defaultModules, vendor */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module and File loaders.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Log
|
||||
*
|
||||
* This logger is very simple, but needs to be extended.
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/* global Loader, defaults, Translator */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Main System
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -70,7 +70,7 @@ const MM = (function () {
|
||||
* Select the wrapper dom object for a specific position.
|
||||
*
|
||||
* @param {string} position The name of the position.
|
||||
* @returns {HTMLElement} the wrapper element
|
||||
* @returns {HTMLElement | void} the wrapper element
|
||||
*/
|
||||
const selectWrapper = function (position) {
|
||||
const classes = position.replace("_", " ");
|
||||
@@ -245,6 +245,7 @@ const MM = (function () {
|
||||
if (moduleWrapper !== null) {
|
||||
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
|
||||
moduleWrapper.style.opacity = 0;
|
||||
moduleWrapper.classList.add("hidden");
|
||||
|
||||
clearTimeout(module.showHideTimer);
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
@@ -310,6 +311,7 @@ const MM = (function () {
|
||||
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
|
||||
// Restore the position. See hideModule() for more info.
|
||||
moduleWrapper.style.position = "static";
|
||||
moduleWrapper.classList.remove("hidden");
|
||||
|
||||
updateWrapperStates();
|
||||
|
||||
@@ -478,7 +480,7 @@ const MM = (function () {
|
||||
* Main init method.
|
||||
*/
|
||||
init: function () {
|
||||
Log.info("Initializing MagicMirror.");
|
||||
Log.info("Initializing MagicMirror².");
|
||||
loadConfig();
|
||||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
14
js/module.js
14
js/module.js
@@ -1,6 +1,6 @@
|
||||
/* global Class, cloneObject, Loader, MMSocket, nunjucks, Translator */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module Blueprint.
|
||||
* @typedef {Object} Module
|
||||
*
|
||||
@@ -12,7 +12,7 @@ const Module = Class.extend({
|
||||
* All methods (and properties) below can be subclassed. *
|
||||
*********************************************************/
|
||||
|
||||
// Set the minimum MagicMirror module version for this module.
|
||||
// Set the minimum MagicMirror² module version for this module.
|
||||
requiresVersion: "2.0.0",
|
||||
|
||||
// Module config defaults.
|
||||
@@ -74,7 +74,7 @@ const Module = Class.extend({
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates the dom which needs to be displayed. This method is called by the Magic Mirror core.
|
||||
* Generates the dom which needs to be displayed. This method is called by the MagicMirror² core.
|
||||
* This method can to be subclassed if the module wants to display info on the mirror.
|
||||
* Alternatively, the getTemplate method could be subclassed.
|
||||
*
|
||||
@@ -109,7 +109,7 @@ const Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Generates the header string which needs to be displayed if a user has a header configured for this module.
|
||||
* This method is called by the Magic Mirror core, but only if the user has configured a default header for the module.
|
||||
* This method is called by the MagicMirror² core, but only if the user has configured a default header for the module.
|
||||
* This method needs to be subclassed if the module wants to display modified headers on the mirror.
|
||||
*
|
||||
* @returns {string} The header to display above the header.
|
||||
@@ -141,7 +141,7 @@ const Module = Class.extend({
|
||||
},
|
||||
|
||||
/**
|
||||
* Called by the Magic Mirror core when a notification arrives.
|
||||
* Called by the MagicMirror² core when a notification arrives.
|
||||
*
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
@@ -434,7 +434,7 @@ const Module = Class.extend({
|
||||
});
|
||||
|
||||
/**
|
||||
* Merging MagicMirror (or other) default/config script by @bugsounet
|
||||
* Merging MagicMirror² (or other) default/config script by @bugsounet
|
||||
* Merge 2 objects or/with array
|
||||
*
|
||||
* Usage:
|
||||
@@ -498,7 +498,7 @@ Module.create = function (name) {
|
||||
|
||||
Module.register = function (name, moduleDefinition) {
|
||||
if (moduleDefinition.requiresVersion) {
|
||||
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.mmVersion);
|
||||
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 {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Node Helper Superclass
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -22,39 +22,38 @@ const NodeHelper = Class.extend({
|
||||
Log.log(`Starting module helper: ${this.name}`);
|
||||
},
|
||||
|
||||
/* stop()
|
||||
* Called when the MagicMirror server receives a `SIGINT`
|
||||
/**
|
||||
* Called when the MagicMirror² server receives a `SIGINT`
|
||||
* Close any open connections, stop any sub-processes and
|
||||
* gracefully exit the module.
|
||||
*
|
||||
*/
|
||||
stop() {
|
||||
Log.log(`Stopping module helper: ${this.name}`);
|
||||
},
|
||||
|
||||
/* socketNotificationReceived(notification, payload)
|
||||
/**
|
||||
* This method is called when a socket notification arrives.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
socketNotificationReceived(notification, payload) {
|
||||
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
|
||||
},
|
||||
|
||||
/* setName(name)
|
||||
/**
|
||||
* Set the module name.
|
||||
*
|
||||
* argument name string - Module name.
|
||||
* @param {string} name Module name.
|
||||
*/
|
||||
setName(name) {
|
||||
this.name = name;
|
||||
},
|
||||
|
||||
/* setPath(path)
|
||||
/**
|
||||
* Set the module path.
|
||||
*
|
||||
* argument path string - Module path.
|
||||
* @param {string} path Module path.
|
||||
*/
|
||||
setPath(path) {
|
||||
this.path = path;
|
||||
|
167
js/server.js
167
js/server.js
@@ -1,11 +1,10 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Server
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const express = require("express");
|
||||
const app = require("express")();
|
||||
const path = require("path");
|
||||
const ipfilter = require("express-ipfilter").IpFilter;
|
||||
const fs = require("fs");
|
||||
@@ -13,99 +12,107 @@ const helmet = require("helmet");
|
||||
|
||||
const Log = require("logger");
|
||||
const Utils = require("./utils.js");
|
||||
const { cors, getConfig, getHtml, getVersion } = require("./server_functions.js");
|
||||
|
||||
/**
|
||||
* Server
|
||||
*
|
||||
* @param {object} config The MM config
|
||||
* @param {Function} callback Function called when done.
|
||||
* @class
|
||||
*/
|
||||
function Server(config, callback) {
|
||||
function Server(config) {
|
||||
const app = express();
|
||||
const port = process.env.MM_PORT || config.port;
|
||||
const serverSockets = new Set();
|
||||
|
||||
let server = null;
|
||||
if (config.useHttps) {
|
||||
const options = {
|
||||
key: fs.readFileSync(config.httpsPrivateKey),
|
||||
cert: fs.readFileSync(config.httpsCertificate)
|
||||
};
|
||||
server = require("https").Server(options, app);
|
||||
} else {
|
||||
server = require("http").Server(app);
|
||||
}
|
||||
const io = require("socket.io")(server, {
|
||||
cors: {
|
||||
origin: /.*$/,
|
||||
credentials: true
|
||||
},
|
||||
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");
|
||||
|
||||
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
||||
Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
|
||||
}
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) {
|
||||
if (err === undefined) {
|
||||
return next();
|
||||
/**
|
||||
* Opens the server for incoming connections
|
||||
*
|
||||
* @returns {Promise} A promise that is resolved when the server listens to connections
|
||||
*/
|
||||
this.open = function () {
|
||||
return new Promise((resolve) => {
|
||||
if (config.useHttps) {
|
||||
const options = {
|
||||
key: fs.readFileSync(config.httpsPrivateKey),
|
||||
cert: fs.readFileSync(config.httpsCertificate)
|
||||
};
|
||||
server = require("https").Server(options, app);
|
||||
} else {
|
||||
server = require("http").Server(app);
|
||||
}
|
||||
Log.log(err.message);
|
||||
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
|
||||
const io = require("socket.io")(server, {
|
||||
cors: {
|
||||
origin: /.*$/,
|
||||
credentials: true
|
||||
},
|
||||
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");
|
||||
|
||||
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
||||
Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
|
||||
}
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) {
|
||||
if (err === undefined) {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
return next();
|
||||
}
|
||||
Log.log(err.message);
|
||||
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
|
||||
});
|
||||
});
|
||||
|
||||
app.use(helmet(config.httpHeaders));
|
||||
app.use("/js", express.static(__dirname));
|
||||
|
||||
// TODO add tests directory only when running tests?
|
||||
const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs", "/tests/mocks"];
|
||||
for (const directory of directories) {
|
||||
app.use(directory, express.static(path.resolve(global.root_path + directory)));
|
||||
}
|
||||
|
||||
app.get("/cors", async (req, res) => await cors(req, res));
|
||||
|
||||
app.get("/version", (req, res) => getVersion(req, res));
|
||||
|
||||
app.get("/config", (req, res) => getConfig(req, res));
|
||||
|
||||
app.get("/", (req, res) => getHtml(req, res));
|
||||
|
||||
server.on("listening", () => {
|
||||
resolve({
|
||||
app,
|
||||
io
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
app.use(helmet({ contentSecurityPolicy: false }));
|
||||
|
||||
app.use("/js", express.static(__dirname));
|
||||
|
||||
const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
|
||||
for (const directory of directories) {
|
||||
app.use(directory, express.static(path.resolve(global.root_path + directory)));
|
||||
}
|
||||
|
||||
app.get("/version", function (req, res) {
|
||||
res.send(global.version);
|
||||
});
|
||||
|
||||
app.get("/config", function (req, res) {
|
||||
res.send(config);
|
||||
});
|
||||
|
||||
app.get("/", function (req, res) {
|
||||
let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" });
|
||||
html = html.replace("#VERSION#", global.version);
|
||||
|
||||
let configFile = "config/config.js";
|
||||
if (typeof global.configuration_file !== "undefined") {
|
||||
configFile = global.configuration_file;
|
||||
}
|
||||
html = html.replace("#CONFIG_FILE#", configFile);
|
||||
|
||||
res.send(html);
|
||||
});
|
||||
|
||||
if (typeof callback === "function") {
|
||||
callback(app, io);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes the server and destroys all lingering connections to it.
|
||||
*
|
||||
* @returns {Promise} A promise that resolves when server has successfully shut down
|
||||
*/
|
||||
this.close = function () {
|
||||
for (const socket of serverSockets.values()) {
|
||||
socket.destroy();
|
||||
}
|
||||
server.close();
|
||||
return new Promise((resolve) => {
|
||||
for (const socket of serverSockets.values()) {
|
||||
socket.destroy();
|
||||
}
|
||||
server.close(resolve);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
127
js/server_functions.js
Normal file
127
js/server_functions.js
Normal file
@@ -0,0 +1,127 @@
|
||||
const fetch = require("./fetch");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Log = require("logger");
|
||||
|
||||
/**
|
||||
* Gets the config.
|
||||
*
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
function getConfig(req, res) {
|
||||
res.send(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* A method that forewards HTTP Get-methods to the internet to avoid CORS-errors.
|
||||
*
|
||||
* Example input request url: /cors?sendheaders=header1:value1,header2:value2&expectedheaders=header1,header2&url=http://www.test.com/path?param1=value1
|
||||
*
|
||||
* Only the url-param of the input request url is required. It must be the last parameter.
|
||||
*
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
async function cors(req, res) {
|
||||
try {
|
||||
const urlRegEx = "url=(.+?)$";
|
||||
let url = "";
|
||||
|
||||
const match = new RegExp(urlRegEx, "g").exec(req.url);
|
||||
if (!match) {
|
||||
url = "invalid url: " + req.url;
|
||||
Log.error(url);
|
||||
res.send(url);
|
||||
} else {
|
||||
url = match[1];
|
||||
|
||||
const headersToSend = getHeadersToSend(req.url);
|
||||
const expectedRecievedHeaders = geExpectedRecievedHeaders(req.url);
|
||||
|
||||
Log.log("cors url: " + url);
|
||||
const response = await fetch(url, { headers: headersToSend });
|
||||
|
||||
for (const header of expectedRecievedHeaders) {
|
||||
const headerValue = response.headers.get(header);
|
||||
if (header) res.set(header, headerValue);
|
||||
}
|
||||
const data = await response.text();
|
||||
res.send(data);
|
||||
}
|
||||
} catch (error) {
|
||||
Log.error(error);
|
||||
res.send(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets headers and values to attatch to the web request.
|
||||
*
|
||||
* @param {string} url - The url containing the headers and values to send.
|
||||
* @returns {object} An object specifying name and value of the headers.
|
||||
*/
|
||||
function getHeadersToSend(url) {
|
||||
const headersToSend = { "User-Agent": "Mozilla/5.0 MagicMirror/" + global.version };
|
||||
const headersToSendMatch = new RegExp("sendheaders=(.+?)(&|$)", "g").exec(url);
|
||||
if (headersToSendMatch) {
|
||||
const headers = headersToSendMatch[1].split(",");
|
||||
for (const header of headers) {
|
||||
const keyValue = header.split(":");
|
||||
if (keyValue.length !== 2) {
|
||||
throw new Error(`Invalid format for header ${header}`);
|
||||
}
|
||||
headersToSend[keyValue[0]] = decodeURIComponent(keyValue[1]);
|
||||
}
|
||||
}
|
||||
return headersToSend;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the headers expected from the response.
|
||||
*
|
||||
* @param {string} url - The url containing the expected headers from the response.
|
||||
* @returns {string[]} headers - The name of the expected headers.
|
||||
*/
|
||||
function geExpectedRecievedHeaders(url) {
|
||||
const expectedRecievedHeaders = ["Content-Type"];
|
||||
const expectedRecievedHeadersMatch = new RegExp("expectedheaders=(.+?)(&|$)", "g").exec(url);
|
||||
if (expectedRecievedHeadersMatch) {
|
||||
const headers = expectedRecievedHeadersMatch[1].split(",");
|
||||
for (const header of headers) {
|
||||
expectedRecievedHeaders.push(header);
|
||||
}
|
||||
}
|
||||
return expectedRecievedHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the HTML to display the magic mirror.
|
||||
*
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
function getHtml(req, res) {
|
||||
let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" });
|
||||
html = html.replace("#VERSION#", global.version);
|
||||
|
||||
let configFile = "config/config.js";
|
||||
if (typeof global.configuration_file !== "undefined") {
|
||||
configFile = global.configuration_file;
|
||||
}
|
||||
html = html.replace("#CONFIG_FILE#", configFile);
|
||||
|
||||
res.send(html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the MagicMirror version.
|
||||
*
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
function getVersion(req, res) {
|
||||
res.send(global.version);
|
||||
}
|
||||
|
||||
module.exports = { cors, getConfig, getHtml, getVersion };
|
@@ -1,6 +1,6 @@
|
||||
/* global io */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* TODO add description
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/* global translations */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Translator (l10n)
|
||||
*
|
||||
* By Christopher Fenner https://github.com/CFenner
|
||||
@@ -104,7 +104,7 @@ const Translator = (function () {
|
||||
* @param {Function} callback Function called when done.
|
||||
*/
|
||||
load(module, file, isFallback, callback) {
|
||||
Log.log(`${module.name} - Load translation${isFallback && " fallback"}: ${file}`);
|
||||
Log.log(`${module.name} - Load translation${isFallback ? " fallback" : ""}: ${file}`);
|
||||
|
||||
if (this.translationsFallback[module.name]) {
|
||||
callback();
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Utils
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# Module: Alert
|
||||
|
||||
The alert module is one of the default modules of the MagicMirror. This module displays notifications from other modules.
|
||||
The alert module is one of the default modules of the MagicMirror². This module displays notifications from other modules.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/alert.html).
|
||||
|
@@ -1,167 +1,146 @@
|
||||
/* global NotificationFx */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: alert
|
||||
*
|
||||
* By Paul-Vincent Roll https://paulvincentroll.com/
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("alert", {
|
||||
alerts: {},
|
||||
|
||||
defaults: {
|
||||
// scale|slide|genie|jelly|flip|bouncyflip|exploader
|
||||
effect: "slide",
|
||||
// scale|slide|genie|jelly|flip|bouncyflip|exploader
|
||||
alert_effect: "jelly",
|
||||
//time a notification is displayed in seconds
|
||||
display_time: 3500,
|
||||
//Position
|
||||
effect: "slide", // scale|slide|genie|jelly|flip|bouncyflip|exploader
|
||||
alert_effect: "jelly", // scale|slide|genie|jelly|flip|bouncyflip|exploader
|
||||
display_time: 3500, // time a notification is displayed in seconds
|
||||
position: "center",
|
||||
//shown at startup
|
||||
welcome_message: false
|
||||
welcome_message: false // shown at startup
|
||||
},
|
||||
getScripts: function () {
|
||||
|
||||
getScripts() {
|
||||
return ["notificationFx.js"];
|
||||
},
|
||||
getStyles: function () {
|
||||
return ["notificationFx.css", "font-awesome.css"];
|
||||
|
||||
getStyles() {
|
||||
return ["font-awesome.css", this.file(`./styles/notificationFx.css`), this.file(`./styles/${this.config.position}.css`)];
|
||||
},
|
||||
// Define required translations.
|
||||
getTranslations: function () {
|
||||
|
||||
getTranslations() {
|
||||
return {
|
||||
en: "translations/en.json",
|
||||
bg: "translations/bg.json",
|
||||
da: "translations/da.json",
|
||||
de: "translations/de.json",
|
||||
nl: "translations/nl.json"
|
||||
en: "translations/en.json",
|
||||
es: "translations/es.json",
|
||||
fr: "translations/fr.json",
|
||||
hu: "translations/hu.json",
|
||||
nl: "translations/nl.json",
|
||||
ru: "translations/ru.json"
|
||||
};
|
||||
},
|
||||
show_notification: function (message) {
|
||||
|
||||
getTemplate(type) {
|
||||
return `templates/${type}.njk`;
|
||||
},
|
||||
|
||||
start() {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
if (this.config.effect === "slide") {
|
||||
this.config.effect = this.config.effect + "-" + this.config.position;
|
||||
this.config.effect = `${this.config.effect}-${this.config.position}`;
|
||||
}
|
||||
let msg = "";
|
||||
if (message.title) {
|
||||
msg += "<span class='thin dimmed medium'>" + message.title + "</span>";
|
||||
|
||||
if (this.config.welcome_message) {
|
||||
const message = this.config.welcome_message === true ? this.translate("welcome") : this.config.welcome_message;
|
||||
this.showNotification({ title: this.translate("sysTitle"), message });
|
||||
}
|
||||
if (message.message) {
|
||||
if (msg !== "") {
|
||||
msg += "<br />";
|
||||
},
|
||||
|
||||
notificationReceived(notification, payload, sender) {
|
||||
if (notification === "SHOW_ALERT") {
|
||||
if (payload.type === "notification") {
|
||||
this.showNotification(payload);
|
||||
} else {
|
||||
this.showAlert(payload, sender);
|
||||
}
|
||||
msg += "<span class='light bright small'>" + message.message + "</span>";
|
||||
} else if (notification === "HIDE_ALERT") {
|
||||
this.hideAlert(sender);
|
||||
}
|
||||
},
|
||||
|
||||
async showNotification(notification) {
|
||||
const message = await this.renderMessage("notification", notification);
|
||||
|
||||
new NotificationFx({
|
||||
message: msg,
|
||||
message,
|
||||
layout: "growl",
|
||||
effect: this.config.effect,
|
||||
ttl: message.timer !== undefined ? message.timer : this.config.display_time
|
||||
ttl: notification.timer || this.config.display_time
|
||||
}).show();
|
||||
},
|
||||
show_alert: function (params, sender) {
|
||||
let image = "";
|
||||
//Set standard params if not provided by module
|
||||
if (typeof params.timer === "undefined") {
|
||||
params.timer = null;
|
||||
}
|
||||
if (typeof params.imageHeight === "undefined") {
|
||||
params.imageHeight = "80px";
|
||||
}
|
||||
if (typeof params.imageUrl === "undefined" && typeof params.imageFA === "undefined") {
|
||||
params.imageUrl = null;
|
||||
} else if (typeof params.imageFA === "undefined") {
|
||||
image = "<img src='" + params.imageUrl.toString() + "' height='" + params.imageHeight.toString() + "' style='margin-bottom: 10px;'/><br />";
|
||||
} else if (typeof params.imageUrl === "undefined") {
|
||||
image = "<span class='bright " + "fa fa-" + params.imageFA + "' style='margin-bottom: 10px;font-size:" + params.imageHeight.toString() + ";'/></span><br />";
|
||||
}
|
||||
//Create overlay
|
||||
const overlay = document.createElement("div");
|
||||
overlay.id = "overlay";
|
||||
overlay.innerHTML += '<div class="black_overlay"></div>';
|
||||
document.body.insertBefore(overlay, document.body.firstChild);
|
||||
|
||||
//If module already has an open alert close it
|
||||
async showAlert(alert, sender) {
|
||||
// If module already has an open alert close it
|
||||
if (this.alerts[sender.name]) {
|
||||
this.hide_alert(sender, false);
|
||||
this.hideAlert(sender, false);
|
||||
}
|
||||
|
||||
//Display title and message only if they are provided in notification parameters
|
||||
let message = "";
|
||||
if (params.title) {
|
||||
message += "<span class='light dimmed medium'>" + params.title + "</span>";
|
||||
}
|
||||
if (params.message) {
|
||||
if (message !== "") {
|
||||
message += "<br />";
|
||||
}
|
||||
|
||||
message += "<span class='thin bright small'>" + params.message + "</span>";
|
||||
// Add overlay
|
||||
if (!Object.keys(this.alerts).length) {
|
||||
this.toggleBlur(true);
|
||||
}
|
||||
|
||||
//Store alert in this.alerts
|
||||
const message = await this.renderMessage("alert", alert);
|
||||
|
||||
// Store alert in this.alerts
|
||||
this.alerts[sender.name] = new NotificationFx({
|
||||
message: image + message,
|
||||
message,
|
||||
effect: this.config.alert_effect,
|
||||
ttl: params.timer,
|
||||
onClose: () => this.hide_alert(sender),
|
||||
ttl: alert.timer,
|
||||
onClose: () => this.hideAlert(sender),
|
||||
al_no: "ns-alert"
|
||||
});
|
||||
|
||||
//Show alert
|
||||
// Show alert
|
||||
this.alerts[sender.name].show();
|
||||
|
||||
//Add timer to dismiss alert and overlay
|
||||
if (params.timer) {
|
||||
// Add timer to dismiss alert and overlay
|
||||
if (alert.timer) {
|
||||
setTimeout(() => {
|
||||
this.hide_alert(sender);
|
||||
}, params.timer);
|
||||
this.hideAlert(sender);
|
||||
}, alert.timer);
|
||||
}
|
||||
},
|
||||
hide_alert: function (sender, close = true) {
|
||||
//Dismiss alert and remove from this.alerts
|
||||
|
||||
hideAlert(sender, close = true) {
|
||||
// Dismiss alert and remove from this.alerts
|
||||
if (this.alerts[sender.name]) {
|
||||
this.alerts[sender.name].dismiss(close);
|
||||
this.alerts[sender.name] = null;
|
||||
//Remove overlay
|
||||
const overlay = document.getElementById("overlay");
|
||||
overlay.parentNode.removeChild(overlay);
|
||||
}
|
||||
},
|
||||
setPosition: function (pos) {
|
||||
//Add css to body depending on the set position for notifications
|
||||
const sheet = document.createElement("style");
|
||||
if (pos === "center") {
|
||||
sheet.innerHTML = ".ns-box {margin-left: auto; margin-right: auto;text-align: center;}";
|
||||
}
|
||||
if (pos === "right") {
|
||||
sheet.innerHTML = ".ns-box {margin-left: auto;text-align: right;}";
|
||||
}
|
||||
if (pos === "left") {
|
||||
sheet.innerHTML = ".ns-box {margin-right: auto;text-align: left;}";
|
||||
}
|
||||
document.body.appendChild(sheet);
|
||||
},
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
if (notification === "SHOW_ALERT") {
|
||||
if (typeof payload.type === "undefined") {
|
||||
payload.type = "alert";
|
||||
}
|
||||
if (payload.type === "alert") {
|
||||
this.show_alert(payload, sender);
|
||||
} else if (payload.type === "notification") {
|
||||
this.show_notification(payload);
|
||||
}
|
||||
} else if (notification === "HIDE_ALERT") {
|
||||
this.hide_alert(sender);
|
||||
}
|
||||
},
|
||||
start: function () {
|
||||
this.alerts = {};
|
||||
this.setPosition(this.config.position);
|
||||
if (this.config.welcome_message) {
|
||||
if (this.config.welcome_message === true) {
|
||||
this.show_notification({ title: this.translate("sysTitle"), message: this.translate("welcome") });
|
||||
} else {
|
||||
this.show_notification({ title: this.translate("sysTitle"), message: this.config.welcome_message });
|
||||
delete this.alerts[sender.name];
|
||||
// Remove overlay
|
||||
if (!Object.keys(this.alerts).length) {
|
||||
this.toggleBlur(false);
|
||||
}
|
||||
}
|
||||
Log.info("Starting module: " + this.name);
|
||||
},
|
||||
|
||||
renderMessage(type, data) {
|
||||
return new Promise((resolve) => {
|
||||
this.nunjucksEnvironment().render(this.getTemplate(type), data, function (err, res) {
|
||||
if (err) {
|
||||
Log.error("Failed to render alert", err);
|
||||
}
|
||||
|
||||
resolve(res);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
toggleBlur(add = false) {
|
||||
const method = add ? "add" : "remove";
|
||||
const modules = document.querySelectorAll(".module");
|
||||
for (const module of modules) {
|
||||
module.classList[method]("alert-blur");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -9,6 +9,8 @@
|
||||
*
|
||||
* Copyright 2014, Codrops
|
||||
* https://tympanus.net/codrops/
|
||||
*
|
||||
* @param {object} window The window object
|
||||
*/
|
||||
(function (window) {
|
||||
/**
|
||||
|
5
modules/default/alert/styles/center.css
Normal file
5
modules/default/alert/styles/center.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.ns-box {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
4
modules/default/alert/styles/left.css
Normal file
4
modules/default/alert/styles/left.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.ns-box {
|
||||
margin-right: auto;
|
||||
text-align: left;
|
||||
}
|
@@ -39,12 +39,8 @@
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.black_overlay {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
background-color: rgba(0, 0, 0, 0.93);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.alert-blur {
|
||||
filter: blur(2px) brightness(50%);
|
||||
}
|
||||
|
||||
[class^="ns-effect-"].ns-growl.ns-hide,
|
4
modules/default/alert/styles/right.css
Normal file
4
modules/default/alert/styles/right.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.ns-box {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
18
modules/default/alert/templates/alert.njk
Normal file
18
modules/default/alert/templates/alert.njk
Normal file
@@ -0,0 +1,18 @@
|
||||
{% if imageUrl or imageFA %}
|
||||
{% set imageHeight = imageHeight if imageHeight else "80px" %}
|
||||
{% if imageUrl %}
|
||||
<img src="{{ imageUrl }}" height="{{ imageHeight }}" style="margin-bottom: 10px;"/>
|
||||
{% else %}
|
||||
<span class="bright fas fa-{{ imageFA }}" style='margin-bottom: 10px; font-size: {{ imageHeight }};'/></span>
|
||||
{% endif %}
|
||||
<br/>
|
||||
{% endif %}
|
||||
{% if title %}
|
||||
<span class="thin dimmed medium">{{ title }}</span>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
{% if title %}
|
||||
<br/>
|
||||
{% endif %}
|
||||
<span class="light bright small">{{ message }}</span>
|
||||
{% endif %}
|
9
modules/default/alert/templates/notification.njk
Normal file
9
modules/default/alert/templates/notification.njk
Normal file
@@ -0,0 +1,9 @@
|
||||
{% if title %}
|
||||
<span class="thin dimmed medium">{{ title }}</span>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
{% if title %}
|
||||
<br/>
|
||||
{% endif %}
|
||||
<span class="light bright small">{{ message }}</span>
|
||||
{% endif %}
|
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror нотификация",
|
||||
"sysTitle": "MagicMirror² нотификация",
|
||||
"welcome": "Добре дошли, стартирането беше успешно"
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notifikation",
|
||||
"sysTitle": "MagicMirror² Notifikation",
|
||||
"welcome": "Velkommen, modulet er succesfuldt startet!"
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Benachrichtigung",
|
||||
"sysTitle": "MagicMirror² Benachrichtigung",
|
||||
"welcome": "Willkommen, Start war erfolgreich!"
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notification",
|
||||
"sysTitle": "MagicMirror² Notification",
|
||||
"welcome": "Welcome, start was successful!"
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notificaciones",
|
||||
"sysTitle": "MagicMirror² Notificaciones",
|
||||
"welcome": "Bienvenido, ¡se iniciado correctamente!"
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notification",
|
||||
"sysTitle": "MagicMirror² Notification",
|
||||
"welcome": "Bienvenue, le démarrage a été un succès!"
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror értesítés",
|
||||
"sysTitle": "MagicMirror² értesítés",
|
||||
"welcome": "Üdvözöljük, indulás sikeres!"
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Notificatie",
|
||||
"sysTitle": "MagicMirror² Notificatie",
|
||||
"welcome": "Welkom, Succesvol gestart!"
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror Уведомление",
|
||||
"sysTitle": "MagicMirror² Уведомление",
|
||||
"welcome": "Добро пожаловать, старт был успешным!"
|
||||
}
|
||||
|
2
modules/default/calendar/README.md
Executable file → Normal file
2
modules/default/calendar/README.md
Executable file → Normal file
@@ -1,6 +1,6 @@
|
||||
# Module: Calendar
|
||||
|
||||
The `calendar` module is one of the default modules of the MagicMirror.
|
||||
The `calendar` module is one of the default modules of the MagicMirror².
|
||||
This module displays events from a public .ical calendar. It can combine multiple calendars.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/calendar.html).
|
||||
|
179
modules/default/calendar/calendar.js
Executable file → Normal file
179
modules/default/calendar/calendar.js
Executable file → Normal file
@@ -1,6 +1,6 @@
|
||||
/* global cloneObject */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: Calendar
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -13,7 +13,8 @@ Module.register("calendar", {
|
||||
maximumNumberOfDays: 365,
|
||||
limitDays: 0, // Limit the number of days shown, 0 = no limit
|
||||
displaySymbol: true,
|
||||
defaultSymbol: "calendar", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
|
||||
defaultSymbol: "calendar-alt", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
|
||||
defaultSymbolClassName: "fas fa-fw fa-",
|
||||
showLocation: false,
|
||||
displayRepeatingCountTitle: false,
|
||||
defaultRepeatingCountTitle: "",
|
||||
@@ -37,13 +38,14 @@ Module.register("calendar", {
|
||||
hidePrivate: false,
|
||||
hideOngoing: false,
|
||||
hideTime: false,
|
||||
showTimeToday: false,
|
||||
colored: false,
|
||||
coloredSymbolOnly: false,
|
||||
customEvents: [], // Array of {keyword: "", symbol: "", color: ""} where Keyword is a regexp and symbol/color are to be applied for matched
|
||||
tableClass: "small",
|
||||
calendars: [
|
||||
{
|
||||
symbol: "calendar",
|
||||
symbol: "calendar-alt",
|
||||
url: "https://www.calendarlabs.com/templates/ical/US-Holidays.ics"
|
||||
}
|
||||
],
|
||||
@@ -133,6 +135,10 @@ Module.register("calendar", {
|
||||
|
||||
// Override socket notification handler.
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
if (notification === "FETCH_CALENDAR") {
|
||||
this.sendSocketNotification(notification, { url: payload.url, id: this.identifier });
|
||||
}
|
||||
|
||||
if (this.identifier !== payload.id) {
|
||||
return;
|
||||
}
|
||||
@@ -158,13 +164,12 @@ Module.register("calendar", {
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
// Define second, minute, hour, and day constants
|
||||
const oneSecond = 1000; // 1,000 milliseconds
|
||||
const oneMinute = oneSecond * 60;
|
||||
const oneHour = oneMinute * 60;
|
||||
const oneDay = oneHour * 24;
|
||||
const ONE_SECOND = 1000; // 1,000 milliseconds
|
||||
const ONE_MINUTE = ONE_SECOND * 60;
|
||||
const ONE_HOUR = ONE_MINUTE * 60;
|
||||
const ONE_DAY = ONE_HOUR * 24;
|
||||
|
||||
const events = this.createEventList();
|
||||
const events = this.createEventList(true);
|
||||
const wrapper = document.createElement("table");
|
||||
wrapper.className = this.config.tableClass;
|
||||
|
||||
@@ -200,6 +205,8 @@ Module.register("calendar", {
|
||||
if (lastSeenDate !== dateAsString) {
|
||||
const dateRow = document.createElement("tr");
|
||||
dateRow.className = "normal";
|
||||
if (event.today) dateRow.className += " today";
|
||||
else if (event.tomorrow) dateRow.className += " tomorrow";
|
||||
|
||||
const dateCell = document.createElement("td");
|
||||
dateCell.colSpan = "3";
|
||||
@@ -225,6 +232,8 @@ Module.register("calendar", {
|
||||
}
|
||||
|
||||
eventWrapper.className = "normal event";
|
||||
if (event.today) eventWrapper.className += " today";
|
||||
else if (event.tomorrow) eventWrapper.className += " tomorrow";
|
||||
|
||||
const symbolWrapper = document.createElement("td");
|
||||
|
||||
@@ -237,21 +246,9 @@ Module.register("calendar", {
|
||||
symbolWrapper.className = "symbol align-right " + symbolClass;
|
||||
|
||||
const symbols = this.symbolsForEvent(event);
|
||||
// If symbols are displayed and custom symbol is set, replace event symbol
|
||||
if (this.config.displaySymbol && this.config.customEvents.length > 0) {
|
||||
for (let ev in this.config.customEvents) {
|
||||
if (typeof this.config.customEvents[ev].symbol !== "undefined" && this.config.customEvents[ev].symbol !== "") {
|
||||
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||
if (needle.test(event.title)) {
|
||||
symbols[0] = this.config.customEvents[ev].symbol;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
symbols.forEach((s, index) => {
|
||||
const symbol = document.createElement("span");
|
||||
symbol.className = "fa fa-fw fa-" + s;
|
||||
symbol.className = s;
|
||||
if (index > 0) {
|
||||
symbol.style.paddingLeft = "5px";
|
||||
}
|
||||
@@ -317,6 +314,12 @@ Module.register("calendar", {
|
||||
timeWrapper.className = "time light align-left " + this.timeClassForUrl(event.url);
|
||||
timeWrapper.style.paddingLeft = "2px";
|
||||
timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
|
||||
|
||||
// Add endDate to dataheaders if showEnd is enabled
|
||||
if (this.config.showEnd) {
|
||||
timeWrapper.innerHTML += " - " + moment(event.endDate, "x").format("LT");
|
||||
}
|
||||
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
titleWrapper.classList.add("align-right");
|
||||
}
|
||||
@@ -339,10 +342,9 @@ Module.register("calendar", {
|
||||
// For full day events we use the fullDayEventDateFormat
|
||||
if (event.fullDayEvent) {
|
||||
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
||||
event.endDate -= oneSecond;
|
||||
event.endDate -= ONE_SECOND;
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
||||
}
|
||||
if (this.config.getRelative > 0 && event.startDate < now) {
|
||||
} else if (this.config.getRelative > 0 && event.startDate < now) {
|
||||
// Ongoing and getRelative is set
|
||||
timeWrapper.innerHTML = this.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
@@ -350,7 +352,7 @@ Module.register("calendar", {
|
||||
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
||||
})
|
||||
);
|
||||
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * oneDay) {
|
||||
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * ONE_DAY) {
|
||||
// Within urgency days
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
@@ -358,9 +360,9 @@ Module.register("calendar", {
|
||||
// Full days events within the next two days
|
||||
if (event.today) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
|
||||
} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
|
||||
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
|
||||
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
|
||||
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
}
|
||||
@@ -368,22 +370,33 @@ Module.register("calendar", {
|
||||
}
|
||||
} else {
|
||||
// Show relative times
|
||||
if (event.startDate >= now) {
|
||||
// Use relative time
|
||||
if (!this.config.hideTime) {
|
||||
if (event.startDate >= now || (event.fullDayEvent && event.today)) {
|
||||
// Use relative time
|
||||
if (!this.config.hideTime && !event.fullDayEvent) {
|
||||
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") + "]",
|
||||
sameDay: this.config.showTimeToday ? "LT" : "[" + this.translate("TODAY") + "]",
|
||||
nextDay: "[" + this.translate("TOMORROW") + "]",
|
||||
nextWeek: "dddd",
|
||||
sameElse: this.config.dateFormat
|
||||
sameElse: event.fullDayEvent ? this.config.fullDayEventDateFormat : this.config.dateFormat
|
||||
})
|
||||
);
|
||||
}
|
||||
if (event.startDate - now < this.config.getRelative * oneHour) {
|
||||
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
|
||||
if (event.fullDayEvent) {
|
||||
// Full days events within the next two days
|
||||
if (event.today) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
|
||||
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
|
||||
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
}
|
||||
}
|
||||
} else if (event.startDate - now < this.config.getRelative * ONE_HOUR) {
|
||||
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
|
||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||
}
|
||||
} else {
|
||||
@@ -412,6 +425,8 @@ Module.register("calendar", {
|
||||
if (event.location !== false) {
|
||||
const locationRow = document.createElement("tr");
|
||||
locationRow.className = "normal xsmall light";
|
||||
if (event.today) locationRow.className += " today";
|
||||
else if (event.tomorrow) locationRow.className += " tomorrow";
|
||||
|
||||
if (this.config.displaySymbol) {
|
||||
const symbolCell = document.createElement("td");
|
||||
@@ -478,9 +493,15 @@ Module.register("calendar", {
|
||||
/**
|
||||
* Creates the sorted list of all events.
|
||||
*
|
||||
* @param {boolean} limitNumberOfEntries Whether to filter returned events for display.
|
||||
* @returns {object[]} Array with events.
|
||||
*/
|
||||
createEventList: function () {
|
||||
createEventList: function (limitNumberOfEntries) {
|
||||
const ONE_SECOND = 1000; // 1,000 milliseconds
|
||||
const ONE_MINUTE = ONE_SECOND * 60;
|
||||
const ONE_HOUR = ONE_MINUTE * 60;
|
||||
const ONE_DAY = ONE_HOUR * 24;
|
||||
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day");
|
||||
const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
|
||||
@@ -488,40 +509,44 @@ Module.register("calendar", {
|
||||
|
||||
for (const calendarUrl in this.calendarData) {
|
||||
const calendar = this.calendarData[calendarUrl];
|
||||
let remainingEntries = this.maximumEntriesForUrl(calendarUrl);
|
||||
for (const e in calendar) {
|
||||
const event = JSON.parse(JSON.stringify(calendar[e])); // clone object
|
||||
|
||||
if (event.endDate < now) {
|
||||
if (this.config.hidePrivate && event.class === "PRIVATE") {
|
||||
// do not add the current event, skip it
|
||||
continue;
|
||||
}
|
||||
if (this.config.hidePrivate) {
|
||||
if (event.class === "PRIVATE") {
|
||||
// do not add the current event, skip it
|
||||
if (limitNumberOfEntries) {
|
||||
if (event.endDate < now) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (this.config.hideOngoing) {
|
||||
if (event.startDate < now) {
|
||||
if (this.config.hideOngoing && event.startDate < now) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (this.listContainsEvent(events, event)) {
|
||||
continue;
|
||||
if (this.listContainsEvent(events, event)) {
|
||||
continue;
|
||||
}
|
||||
if (--remainingEntries < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
event.url = calendarUrl;
|
||||
event.today = event.startDate >= today && event.startDate < today + 24 * 60 * 60 * 1000;
|
||||
event.today = event.startDate >= today && event.startDate < today + ONE_DAY;
|
||||
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
|
||||
|
||||
/* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
|
||||
* otherwise, esp. in dateheaders mode it is not clear how long these events are.
|
||||
*/
|
||||
const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / (1000 * 60 * 60 * 24)) + 1;
|
||||
const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / ONE_DAY) + 1;
|
||||
if (this.config.sliceMultiDayEvents && maxCount > 1) {
|
||||
const splitEvents = [];
|
||||
let midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
|
||||
let count = 1;
|
||||
while (event.endDate > midnight) {
|
||||
const thisEvent = JSON.parse(JSON.stringify(event)); // clone object
|
||||
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + 24 * 60 * 60 * 1000;
|
||||
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + ONE_DAY;
|
||||
thisEvent.tomorrow = !thisEvent.today && thisEvent.startDate >= today + ONE_DAY && thisEvent.startDate < today + 2 * ONE_DAY;
|
||||
thisEvent.endDate = midnight;
|
||||
thisEvent.title += " (" + count + "/" + maxCount + ")";
|
||||
splitEvents.push(thisEvent);
|
||||
@@ -532,6 +557,8 @@ Module.register("calendar", {
|
||||
}
|
||||
// Last day
|
||||
event.title += " (" + count + "/" + maxCount + ")";
|
||||
event.today += event.startDate >= today && event.startDate < today + ONE_DAY;
|
||||
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
|
||||
splitEvents.push(event);
|
||||
|
||||
for (let splitEvent of splitEvents) {
|
||||
@@ -549,6 +576,10 @@ Module.register("calendar", {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
if (!limitNumberOfEntries) {
|
||||
return events;
|
||||
}
|
||||
|
||||
// Limit the number of days displayed
|
||||
// If limitDays is set > 0, limit display to that number of days
|
||||
if (this.config.limitDays > 0) {
|
||||
@@ -629,6 +660,17 @@ Module.register("calendar", {
|
||||
symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "fullDaySymbol", this.config.defaultSymbol), symbols);
|
||||
}
|
||||
|
||||
// If custom symbol is set, replace event symbol
|
||||
for (let ev of this.config.customEvents) {
|
||||
if (typeof ev.symbol !== "undefined" && ev.symbol !== "") {
|
||||
let needle = new RegExp(ev.keyword, "gi");
|
||||
if (needle.test(event.title)) {
|
||||
symbols[0] = ev.symbol;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return symbols;
|
||||
},
|
||||
|
||||
@@ -700,6 +742,16 @@ Module.register("calendar", {
|
||||
return this.getCalendarProperty(url, "repeatingCountTitle", this.config.defaultRepeatingCountTitle);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the maximum entry count for a specific calendar url.
|
||||
*
|
||||
* @param {string} url The calendar url
|
||||
* @returns {number} The maximum entry count
|
||||
*/
|
||||
maximumEntriesForUrl: function (url) {
|
||||
return this.getCalendarProperty(url, "maximumEntries", this.config.maximumEntries);
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method to retrieve the property for a specific calendar url.
|
||||
*
|
||||
@@ -720,6 +772,11 @@ Module.register("calendar", {
|
||||
|
||||
getCalendarPropertyAsArray: function (url, property, defaultValue) {
|
||||
let p = this.getCalendarProperty(url, property, defaultValue);
|
||||
if (property === "symbol" || property === "recurringSymbol" || property === "fullDaySymbol") {
|
||||
const className = this.getCalendarProperty(url, "symbolClassName", this.config.defaultSymbolClassName);
|
||||
p = className + p;
|
||||
}
|
||||
|
||||
if (!(p instanceof Array)) p = [p];
|
||||
return p;
|
||||
},
|
||||
@@ -757,7 +814,7 @@ Module.register("calendar", {
|
||||
line++;
|
||||
if (line > maxTitleLines - 1) {
|
||||
if (i < words.length) {
|
||||
currentLine += "…";
|
||||
currentLine += "…";
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -774,7 +831,7 @@ Module.register("calendar", {
|
||||
return (temp + currentLine).trim();
|
||||
} else {
|
||||
if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
|
||||
return string.trim().slice(0, maxLength) + "…";
|
||||
return string.trim().slice(0, maxLength) + "…";
|
||||
} else {
|
||||
return string.trim();
|
||||
}
|
||||
@@ -825,22 +882,14 @@ Module.register("calendar", {
|
||||
* The all events available in one array, sorted on startdate.
|
||||
*/
|
||||
broadcastEvents: function () {
|
||||
const eventList = [];
|
||||
for (const url in this.calendarData) {
|
||||
for (const ev of this.calendarData[url]) {
|
||||
const event = cloneObject(ev);
|
||||
event.symbol = this.symbolsForEvent(event);
|
||||
event.calendarName = this.calendarNameForUrl(url);
|
||||
event.color = this.colorForUrl(url);
|
||||
delete event.url;
|
||||
eventList.push(event);
|
||||
}
|
||||
const eventList = this.createEventList(false);
|
||||
for (const event of eventList) {
|
||||
event.symbol = this.symbolsForEvent(event);
|
||||
event.calendarName = this.calendarNameForUrl(event.url);
|
||||
event.color = this.colorForUrl(event.url);
|
||||
delete event.url;
|
||||
}
|
||||
|
||||
eventList.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
this.sendNotification("CALENDAR_EVENTS", eventList);
|
||||
}
|
||||
});
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Node Helper: Calendar - CalendarFetcher
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -8,7 +8,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 fetch = require("fetch");
|
||||
const digest = require("digest-fetch");
|
||||
const https = require("https");
|
||||
|
||||
@@ -41,7 +41,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
let fetcher = null;
|
||||
let httpsAgent = null;
|
||||
let headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version
|
||||
};
|
||||
|
||||
if (selfSignedCert) {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Calendar Util Methods
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -98,7 +98,7 @@ const CalendarUtils = {
|
||||
if (h > 0 && h < Math.abs(current_offset) / 60) {
|
||||
// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
|
||||
// we need to fix that
|
||||
adjustHours = 24;
|
||||
//adjustHours = 24;
|
||||
// Log.debug("adjusting date")
|
||||
}
|
||||
//-300 > -240
|
||||
@@ -160,7 +160,7 @@ const CalendarUtils = {
|
||||
}
|
||||
|
||||
if (event.type === "VEVENT") {
|
||||
Log.debug("\nEvent: " + JSON.stringify(event));
|
||||
Log.debug("Event:\n" + JSON.stringify(event));
|
||||
let startDate = eventDate(event, "start");
|
||||
let endDate;
|
||||
|
||||
@@ -177,8 +177,8 @@ const CalendarUtils = {
|
||||
}
|
||||
}
|
||||
|
||||
Log.debug("startDate (local): " + startDate.toDate());
|
||||
Log.debug("endDate (local): " + endDate.toDate());
|
||||
Log.debug("start: " + startDate.toDate());
|
||||
Log.debug("end:: " + endDate.toDate());
|
||||
|
||||
// Calculate the duration of the event for use with recurring events.
|
||||
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
@@ -332,40 +332,47 @@ const CalendarUtils = {
|
||||
Log.debug("Fullday");
|
||||
// If the offset is negative (east of GMT), where the problem is
|
||||
if (dateoffset < 0) {
|
||||
// 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);
|
||||
if (dh < Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
// reduce the time by the offset
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// Apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date1 fulldate is " + date);
|
||||
}
|
||||
} else {
|
||||
// if the timezones are the same, correct date if needed
|
||||
if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh < Math.abs(dateoffset / 60)) {
|
||||
//if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh <= Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// apply the correction to the date/time back to right day
|
||||
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date2 is " + date);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date2 fulldate is " + date);
|
||||
}
|
||||
//}
|
||||
}
|
||||
} else {
|
||||
// not full day, but luxon can still screw up the date on the rule processing
|
||||
// we need to correct the date to get back to the right event for
|
||||
if (dateoffset < 0) {
|
||||
// if the date hour is less than the offset
|
||||
if (dh < Math.abs(dateoffset / 60)) {
|
||||
// Reduce the time by the offset:
|
||||
// Apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
|
||||
if (dh <= Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// Reduce the time by t:
|
||||
// Apply the correction to the date/time to get it UTC relative
|
||||
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
@@ -373,21 +380,24 @@ const CalendarUtils = {
|
||||
}
|
||||
} else {
|
||||
// if the timezones are the same, correct date if needed
|
||||
if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh < Math.abs(dateoffset / 60)) {
|
||||
//if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh <= Math.abs(dateoffset / 60)) {
|
||||
// if the rrule byweekday WAS explicitly set , correct it
|
||||
if (curEvent.rrule.origOptions.byweekday !== undefined) {
|
||||
// apply the correction to the date/time back to right day
|
||||
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date2 is " + date);
|
||||
}
|
||||
// the duration was calculated way back at the top before we could correct the start time..
|
||||
// fix it for this event entry
|
||||
//duration = 24 * 60 * 60 * 1000;
|
||||
Log.debug("new recurring date2 is " + date);
|
||||
}
|
||||
//}
|
||||
}
|
||||
}
|
||||
startDate = moment(date);
|
||||
Log.debug("Corrected startDate (local): " + startDate.toDate());
|
||||
Log.debug("Corrected startDate: " + startDate.toDate());
|
||||
|
||||
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date);
|
||||
|
||||
@@ -471,10 +481,6 @@ const CalendarUtils = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust start date so multiple day events will be displayed as happening today even though they started some days ago already
|
||||
if (fullDayEvent && startDate <= today) {
|
||||
startDate = moment(today);
|
||||
}
|
||||
// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
|
||||
if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
@@ -500,23 +506,7 @@ const CalendarUtils = {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
|
||||
// include up to maximumEntries current or upcoming events
|
||||
// If past events should be included, include all past events
|
||||
const now = moment();
|
||||
let entries = 0;
|
||||
let events = [];
|
||||
for (let ne of newEvents) {
|
||||
if (moment(ne.endDate, "x").isBefore(now)) {
|
||||
if (config.includePastEvents) events.push(ne);
|
||||
continue;
|
||||
}
|
||||
entries++;
|
||||
// If max events has been saved, skip the rest
|
||||
if (entries > config.maximumEntries) break;
|
||||
events.push(ne);
|
||||
}
|
||||
|
||||
return events;
|
||||
return newEvents;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/* CalendarFetcher Tester
|
||||
* use this script with `node debug.js` to test the fetcher without the need
|
||||
* of starting the MagicMirror core. Adjust the values below to your desire.
|
||||
* of starting the MagicMirror² core. Adjust the values below to your desire.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Node Helper: Calendar
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -19,6 +19,14 @@ module.exports = NodeHelper.create({
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
if (notification === "ADD_CALENDAR") {
|
||||
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.selfSignedCert, payload.id);
|
||||
} else if (notification === "FETCH_CALENDAR") {
|
||||
const key = payload.id + payload.url;
|
||||
if (typeof this.fetchers[key] === "undefined") {
|
||||
Log.error("Calendar Error. No fetcher exists with key: ", key);
|
||||
this.sendSocketNotification("CALENDAR_ERROR", { error_type: "MODULE_ERROR_UNSPECIFIED" });
|
||||
return;
|
||||
}
|
||||
this.fetchers[key].startFetch();
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Module: Clock
|
||||
|
||||
The `clock` module is one of the default modules of the MagicMirror.
|
||||
The `clock` module is one of the default modules of the MagicMirror².
|
||||
This module displays the current date and time. The information will be updated realtime.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/clock.html).
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/* global SunCalc */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: Clock
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -199,13 +199,13 @@ Module.register("clock", {
|
||||
sunWrapper.innerHTML =
|
||||
'<span class="' +
|
||||
(isVisible ? "bright" : "") +
|
||||
'"><i class="fa fa-sun-o" aria-hidden="true"></i> ' +
|
||||
'"><i class="fas fa-sun" aria-hidden="true"></i> ' +
|
||||
untilNextEventString +
|
||||
"</span>" +
|
||||
'<span><i class="fa fa-arrow-up" aria-hidden="true"></i> ' +
|
||||
'<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ' +
|
||||
formatTime(this.config, sunTimes.sunrise) +
|
||||
"</span>" +
|
||||
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i> ' +
|
||||
'<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ' +
|
||||
formatTime(this.config, sunTimes.sunset) +
|
||||
"</span>";
|
||||
digitalWrapper.appendChild(sunWrapper);
|
||||
@@ -230,13 +230,13 @@ Module.register("clock", {
|
||||
moonWrapper.innerHTML =
|
||||
'<span class="' +
|
||||
(isVisible ? "bright" : "") +
|
||||
'"><i class="fa fa-moon-o" aria-hidden="true"></i> ' +
|
||||
'"><i class="fas fa-moon" aria-hidden="true"></i> ' +
|
||||
illuminatedFractionString +
|
||||
"</span>" +
|
||||
'<span><i class="fa fa-arrow-up" aria-hidden="true"></i> ' +
|
||||
'<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ' +
|
||||
(moonRise ? formatTime(this.config, moonRise) : "...") +
|
||||
"</span>" +
|
||||
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i> ' +
|
||||
'<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ' +
|
||||
(moonSet ? formatTime(this.config, moonSet) : "...") +
|
||||
"</span>";
|
||||
digitalWrapper.appendChild(moonWrapper);
|
||||
@@ -303,7 +303,7 @@ Module.register("clock", {
|
||||
}
|
||||
|
||||
/*******************************************
|
||||
* Update placement, respect old analogShowDate even if its not needed anymore
|
||||
* Update placement, respect old analogShowDate even if it's not needed anymore
|
||||
*/
|
||||
if (this.config.displayType === "analog") {
|
||||
// Display only an analog clock
|
||||
@@ -311,16 +311,16 @@ Module.register("clock", {
|
||||
wrapper.classList.add("clockGrid--bottom");
|
||||
} else if (this.config.analogShowDate === "bottom") {
|
||||
wrapper.classList.add("clockGrid--top");
|
||||
} else {
|
||||
//analogWrapper.style.gridArea = "center";
|
||||
}
|
||||
wrapper.appendChild(analogWrapper);
|
||||
} else if (this.config.displayType === "digital") {
|
||||
wrapper.appendChild(digitalWrapper);
|
||||
} else if (this.config.displayType === "both") {
|
||||
wrapper.classList.add("clockGrid--" + this.config.analogPlacement);
|
||||
wrapper.appendChild(analogWrapper);
|
||||
wrapper.appendChild(digitalWrapper);
|
||||
}
|
||||
|
||||
wrapper.appendChild(analogWrapper);
|
||||
wrapper.appendChild(digitalWrapper);
|
||||
|
||||
// Return the wrapper to the dom.
|
||||
return wrapper;
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Module: Compliments
|
||||
|
||||
The `compliments` module is one of the default modules of the MagicMirror.
|
||||
The `compliments` module is one of the default modules of the MagicMirror².
|
||||
This module displays a random compliment.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/compliments.html).
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: Compliments
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -21,8 +21,7 @@ Module.register("compliments", {
|
||||
morningEndTime: 12,
|
||||
afternoonStartTime: 12,
|
||||
afternoonEndTime: 17,
|
||||
random: true,
|
||||
mockDate: null
|
||||
random: true
|
||||
},
|
||||
lastIndexUsed: -1,
|
||||
// Set currentweather from module
|
||||
@@ -40,7 +39,7 @@ Module.register("compliments", {
|
||||
this.lastComplimentIndex = -1;
|
||||
|
||||
if (this.config.remoteFile !== null) {
|
||||
this.complimentFile((response) => {
|
||||
this.loadComplimentFile().then((response) => {
|
||||
this.config.compliments = JSON.parse(response);
|
||||
this.updateDom();
|
||||
});
|
||||
@@ -85,30 +84,30 @@ Module.register("compliments", {
|
||||
*/
|
||||
complimentArray: function () {
|
||||
const hour = moment().hour();
|
||||
const date = this.config.mockDate ? this.config.mockDate : moment().format("YYYY-MM-DD");
|
||||
let compliments;
|
||||
const date = moment().format("YYYY-MM-DD");
|
||||
let compliments = [];
|
||||
|
||||
// Add time of day compliments
|
||||
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime && this.config.compliments.hasOwnProperty("morning")) {
|
||||
compliments = this.config.compliments.morning.slice(0);
|
||||
compliments = [...this.config.compliments.morning];
|
||||
} else if (hour >= this.config.afternoonStartTime && hour < this.config.afternoonEndTime && this.config.compliments.hasOwnProperty("afternoon")) {
|
||||
compliments = this.config.compliments.afternoon.slice(0);
|
||||
compliments = [...this.config.compliments.afternoon];
|
||||
} else if (this.config.compliments.hasOwnProperty("evening")) {
|
||||
compliments = this.config.compliments.evening.slice(0);
|
||||
}
|
||||
|
||||
if (typeof compliments === "undefined") {
|
||||
compliments = [];
|
||||
compliments = [...this.config.compliments.evening];
|
||||
}
|
||||
|
||||
// Add compliments based on weather
|
||||
if (this.currentWeatherType in this.config.compliments) {
|
||||
compliments.push.apply(compliments, this.config.compliments[this.currentWeatherType]);
|
||||
Array.prototype.push.apply(compliments, this.config.compliments[this.currentWeatherType]);
|
||||
}
|
||||
|
||||
compliments.push.apply(compliments, this.config.compliments.anytime);
|
||||
// Add compliments for anytime
|
||||
Array.prototype.push.apply(compliments, this.config.compliments.anytime);
|
||||
|
||||
// Add compliments for special days
|
||||
for (let entry in this.config.compliments) {
|
||||
if (new RegExp(entry).test(date)) {
|
||||
compliments.push.apply(compliments, this.config.compliments[entry]);
|
||||
Array.prototype.push.apply(compliments, this.config.compliments[entry]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,20 +117,13 @@ Module.register("compliments", {
|
||||
/**
|
||||
* Retrieve a file from the local filesystem
|
||||
*
|
||||
* @param {Function} callback Called when the file is retrieved.
|
||||
* @returns {Promise} Resolved when the file is loaded
|
||||
*/
|
||||
complimentFile: function (callback) {
|
||||
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");
|
||||
xobj.open("GET", path, true);
|
||||
xobj.onreadystatechange = function () {
|
||||
if (xobj.readyState === 4 && xobj.status === 200) {
|
||||
callback(xobj.responseText);
|
||||
}
|
||||
};
|
||||
xobj.send(null);
|
||||
loadComplimentFile: async function () {
|
||||
const isRemote = this.config.remoteFile.indexOf("http://") === 0 || this.config.remoteFile.indexOf("https://") === 0,
|
||||
url = isRemote ? this.config.remoteFile : this.file(this.config.remoteFile);
|
||||
const response = await fetch(url);
|
||||
return await response.text();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -139,7 +131,7 @@ Module.register("compliments", {
|
||||
*
|
||||
* @returns {string} a compliment
|
||||
*/
|
||||
randomCompliment: function () {
|
||||
getRandomCompliment: function () {
|
||||
// get the current time of day compliments list
|
||||
const compliments = this.complimentArray();
|
||||
// variable for index to next message to display
|
||||
@@ -162,34 +154,33 @@ Module.register("compliments", {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = this.config.classes ? this.config.classes : "thin xlarge bright pre-line";
|
||||
// get the compliment text
|
||||
const complimentText = this.randomCompliment();
|
||||
const complimentText = this.getRandomCompliment();
|
||||
// split it into parts on newline text
|
||||
const parts = complimentText.split("\n");
|
||||
// create a span to hold it all
|
||||
// create a span to hold the compliment
|
||||
const compliment = document.createElement("span");
|
||||
// process all the parts of the compliment text
|
||||
for (const part of parts) {
|
||||
// create a text element for each part
|
||||
compliment.appendChild(document.createTextNode(part));
|
||||
// add a break `
|
||||
compliment.appendChild(document.createElement("BR"));
|
||||
if (part !== "") {
|
||||
// create a text element for each part
|
||||
compliment.appendChild(document.createTextNode(part));
|
||||
// add a break
|
||||
compliment.appendChild(document.createElement("BR"));
|
||||
}
|
||||
}
|
||||
// only add compliment to wrapper if there is actual text in there
|
||||
if (compliment.children.length > 0) {
|
||||
// remove the last break
|
||||
compliment.lastElementChild.remove();
|
||||
wrapper.appendChild(compliment);
|
||||
}
|
||||
// remove the last break
|
||||
compliment.lastElementChild.remove();
|
||||
wrapper.appendChild(compliment);
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
// From data currentweather set weather type
|
||||
setCurrentWeatherType: function (type) {
|
||||
this.currentWeatherType = type;
|
||||
},
|
||||
|
||||
// Override notification handler.
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
if (notification === "CURRENTWEATHER_TYPE") {
|
||||
this.setCurrentWeatherType(payload.type);
|
||||
this.currentWeatherType = payload.type;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -1,8 +0,0 @@
|
||||
# Module: Current Weather
|
||||
|
||||
> :warning: **This module is deprecated in favor of the [weather](https://docs.magicmirror.builders/modules/weather.html) module.**
|
||||
|
||||
The `currentweather` module is one of the default modules of the MagicMirror.
|
||||
This module displays the current weather, including the windspeed, the sunset or sunrise time, the temperature and an icon to display the current conditions.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/currentweather.html).
|
@@ -1,15 +0,0 @@
|
||||
.currentweather .weathericon,
|
||||
.currentweather .fa-home {
|
||||
font-size: 75%;
|
||||
line-height: 65px;
|
||||
display: inline-block;
|
||||
transform: translate(0, -3px);
|
||||
}
|
||||
|
||||
.currentweather .humidityIcon {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.currentweather .humidity-padding {
|
||||
padding-bottom: 6px;
|
||||
}
|
@@ -1,600 +0,0 @@
|
||||
/* eslint-disable */
|
||||
|
||||
/* Magic Mirror
|
||||
* Module: CurrentWeather
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This module is deprecated. Any additional feature will no longer be merged.
|
||||
*/
|
||||
Module.register("currentweather", {
|
||||
// Default module config.
|
||||
defaults: {
|
||||
location: false,
|
||||
locationID: false,
|
||||
appid: "",
|
||||
units: config.units,
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
animationSpeed: 1000,
|
||||
timeFormat: config.timeFormat,
|
||||
showPeriod: true,
|
||||
showPeriodUpper: false,
|
||||
showWindDirection: true,
|
||||
showWindDirectionAsArrow: false,
|
||||
useBeaufort: true,
|
||||
useKMPHwind: false,
|
||||
lang: config.language,
|
||||
decimalSymbol: ".",
|
||||
showHumidity: false,
|
||||
showSun: true,
|
||||
degreeLabel: false,
|
||||
showIndoorTemperature: false,
|
||||
showIndoorHumidity: false,
|
||||
showFeelsLike: true,
|
||||
|
||||
initialLoadDelay: 0, // 0 seconds delay
|
||||
retryDelay: 2500,
|
||||
|
||||
apiVersion: "2.5",
|
||||
apiBase: "https://api.openweathermap.org/data/",
|
||||
weatherEndpoint: "weather",
|
||||
|
||||
appendLocationNameToHeader: true,
|
||||
useLocationAsHeader: false,
|
||||
|
||||
calendarClass: "calendar",
|
||||
tableClass: "large",
|
||||
|
||||
onlyTemp: false,
|
||||
hideTemp: false,
|
||||
roundTemp: false,
|
||||
|
||||
iconTable: {
|
||||
"01d": "day-sunny",
|
||||
"02d": "day-cloudy",
|
||||
"03d": "cloudy",
|
||||
"04d": "cloudy-windy",
|
||||
"09d": "showers",
|
||||
"10d": "rain",
|
||||
"11d": "thunderstorm",
|
||||
"13d": "snow",
|
||||
"50d": "fog",
|
||||
"01n": "night-clear",
|
||||
"02n": "night-cloudy",
|
||||
"03n": "night-cloudy",
|
||||
"04n": "night-cloudy",
|
||||
"09n": "night-showers",
|
||||
"10n": "night-rain",
|
||||
"11n": "night-thunderstorm",
|
||||
"13n": "night-snow",
|
||||
"50n": "night-alt-cloudy-windy"
|
||||
}
|
||||
},
|
||||
|
||||
// create a variable for the first upcoming calendar event. Used if no location is specified.
|
||||
firstEvent: false,
|
||||
|
||||
// create a variable to hold the location name based on the API result.
|
||||
fetchedLocationName: "",
|
||||
|
||||
// Define required scripts.
|
||||
getScripts: function () {
|
||||
return ["moment.js"];
|
||||
},
|
||||
|
||||
// Define required scripts.
|
||||
getStyles: function () {
|
||||
return ["weather-icons.css", "currentweather.css"];
|
||||
},
|
||||
|
||||
// Define required translations.
|
||||
getTranslations: function () {
|
||||
// The translations for the default modules are defined in the core translation files.
|
||||
// Therefor we can just return false. Otherwise we should have returned a dictionary.
|
||||
// If you're trying to build your own module including translations, check out the documentation.
|
||||
return false;
|
||||
},
|
||||
|
||||
// Define start sequence.
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
|
||||
// Set locale.
|
||||
moment.locale(config.language);
|
||||
|
||||
this.windSpeed = null;
|
||||
this.windDirection = null;
|
||||
this.windDeg = null;
|
||||
this.sunriseSunsetTime = null;
|
||||
this.sunriseSunsetIcon = null;
|
||||
this.temperature = null;
|
||||
this.indoorTemperature = null;
|
||||
this.indoorHumidity = null;
|
||||
this.weatherType = null;
|
||||
this.feelsLike = null;
|
||||
this.loaded = false;
|
||||
this.scheduleUpdate(this.config.initialLoadDelay);
|
||||
},
|
||||
|
||||
// add extra information of current weather
|
||||
// windDirection, humidity, sunrise and sunset
|
||||
addExtraInfoWeather: function (wrapper) {
|
||||
var small = document.createElement("div");
|
||||
small.className = "normal medium";
|
||||
|
||||
var windIcon = document.createElement("span");
|
||||
windIcon.className = "wi wi-strong-wind dimmed";
|
||||
small.appendChild(windIcon);
|
||||
|
||||
var windSpeed = document.createElement("span");
|
||||
windSpeed.innerHTML = " " + this.windSpeed;
|
||||
small.appendChild(windSpeed);
|
||||
|
||||
if (this.config.showWindDirection) {
|
||||
var windDirection = document.createElement("sup");
|
||||
if (this.config.showWindDirectionAsArrow) {
|
||||
if (this.windDeg !== null) {
|
||||
windDirection.innerHTML = ' <i class="fa fa-long-arrow-down" style="transform:rotate(' + this.windDeg + 'deg);"></i> ';
|
||||
}
|
||||
} else {
|
||||
windDirection.innerHTML = " " + this.translate(this.windDirection);
|
||||
}
|
||||
small.appendChild(windDirection);
|
||||
}
|
||||
var spacer = document.createElement("span");
|
||||
spacer.innerHTML = " ";
|
||||
small.appendChild(spacer);
|
||||
|
||||
if (this.config.showHumidity) {
|
||||
var humidity = document.createElement("span");
|
||||
humidity.innerHTML = this.humidity;
|
||||
|
||||
var supspacer = document.createElement("sup");
|
||||
supspacer.innerHTML = " ";
|
||||
|
||||
var humidityIcon = document.createElement("sup");
|
||||
humidityIcon.className = "wi wi-humidity humidityIcon";
|
||||
humidityIcon.innerHTML = " ";
|
||||
|
||||
small.appendChild(humidity);
|
||||
small.appendChild(supspacer);
|
||||
small.appendChild(humidityIcon);
|
||||
}
|
||||
|
||||
if (this.config.showSun) {
|
||||
var sunriseSunsetIcon = document.createElement("span");
|
||||
sunriseSunsetIcon.className = "wi dimmed " + this.sunriseSunsetIcon;
|
||||
small.appendChild(sunriseSunsetIcon);
|
||||
|
||||
var sunriseSunsetTime = document.createElement("span");
|
||||
sunriseSunsetTime.innerHTML = " " + this.sunriseSunsetTime;
|
||||
small.appendChild(sunriseSunsetTime);
|
||||
}
|
||||
|
||||
wrapper.appendChild(small);
|
||||
},
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
var wrapper = document.createElement("div");
|
||||
wrapper.className = this.config.tableClass;
|
||||
|
||||
if (this.config.appid === "") {
|
||||
wrapper.innerHTML = "Please set the correct openweather <i>appid</i> in the config for module: " + this.name + ".";
|
||||
wrapper.className = "dimmed light small";
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
if (!this.loaded) {
|
||||
wrapper.innerHTML = this.translate("LOADING");
|
||||
wrapper.className = "dimmed light small";
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
if (this.config.onlyTemp === false) {
|
||||
this.addExtraInfoWeather(wrapper);
|
||||
}
|
||||
|
||||
var large = document.createElement("div");
|
||||
large.className = "light";
|
||||
|
||||
var degreeLabel = "";
|
||||
if (this.config.units === "metric" || this.config.units === "imperial") {
|
||||
degreeLabel += "°";
|
||||
}
|
||||
if (this.config.degreeLabel) {
|
||||
switch (this.config.units) {
|
||||
case "metric":
|
||||
degreeLabel += "C";
|
||||
break;
|
||||
case "imperial":
|
||||
degreeLabel += "F";
|
||||
break;
|
||||
case "default":
|
||||
degreeLabel += "K";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.decimalSymbol === "") {
|
||||
this.config.decimalSymbol = ".";
|
||||
}
|
||||
|
||||
if (this.config.hideTemp === false) {
|
||||
var weatherIcon = document.createElement("span");
|
||||
weatherIcon.className = "wi weathericon wi-" + this.weatherType;
|
||||
large.appendChild(weatherIcon);
|
||||
|
||||
var temperature = document.createElement("span");
|
||||
temperature.className = "bright";
|
||||
temperature.innerHTML = " " + this.temperature.replace(".", this.config.decimalSymbol) + degreeLabel;
|
||||
large.appendChild(temperature);
|
||||
}
|
||||
|
||||
if (this.config.showIndoorTemperature && this.indoorTemperature) {
|
||||
var indoorIcon = document.createElement("span");
|
||||
indoorIcon.className = "fa fa-home";
|
||||
large.appendChild(indoorIcon);
|
||||
|
||||
var indoorTemperatureElem = document.createElement("span");
|
||||
indoorTemperatureElem.className = "bright";
|
||||
indoorTemperatureElem.innerHTML = " " + this.indoorTemperature.replace(".", this.config.decimalSymbol) + degreeLabel;
|
||||
large.appendChild(indoorTemperatureElem);
|
||||
}
|
||||
|
||||
if (this.config.showIndoorHumidity && this.indoorHumidity) {
|
||||
var indoorHumidityIcon = document.createElement("span");
|
||||
indoorHumidityIcon.className = "fa fa-tint";
|
||||
large.appendChild(indoorHumidityIcon);
|
||||
|
||||
var indoorHumidityElem = document.createElement("span");
|
||||
indoorHumidityElem.className = "bright";
|
||||
indoorHumidityElem.innerHTML = " " + this.indoorHumidity + "%";
|
||||
large.appendChild(indoorHumidityElem);
|
||||
}
|
||||
|
||||
wrapper.appendChild(large);
|
||||
|
||||
if (this.config.showFeelsLike && this.config.onlyTemp === false) {
|
||||
var small = document.createElement("div");
|
||||
small.className = "normal medium";
|
||||
|
||||
var feelsLike = document.createElement("span");
|
||||
feelsLike.className = "dimmed";
|
||||
feelsLike.innerHTML = this.translate("FEELS", {
|
||||
DEGREE: this.feelsLike + degreeLabel
|
||||
});
|
||||
small.appendChild(feelsLike);
|
||||
|
||||
wrapper.appendChild(small);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
// Override getHeader method.
|
||||
getHeader: function () {
|
||||
if (this.config.useLocationAsHeader && this.config.location !== false) {
|
||||
return this.config.location;
|
||||
}
|
||||
|
||||
if (this.config.appendLocationNameToHeader) {
|
||||
if (this.data.header) return this.data.header + " " + this.fetchedLocationName;
|
||||
else return this.fetchedLocationName;
|
||||
}
|
||||
|
||||
return this.data.header ? this.data.header : "";
|
||||
},
|
||||
|
||||
// Override notification handler.
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
if (notification === "DOM_OBJECTS_CREATED") {
|
||||
if (this.config.appendLocationNameToHeader) {
|
||||
this.hide(0, { lockString: this.identifier });
|
||||
}
|
||||
}
|
||||
if (notification === "CALENDAR_EVENTS") {
|
||||
var senderClasses = sender.data.classes.toLowerCase().split(" ");
|
||||
if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
|
||||
this.firstEvent = false;
|
||||
|
||||
for (var e in payload) {
|
||||
var event = payload[e];
|
||||
if (event.location || event.geo) {
|
||||
this.firstEvent = event;
|
||||
//Log.log("First upcoming event with location: ", event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (notification === "INDOOR_TEMPERATURE") {
|
||||
this.indoorTemperature = this.roundValue(payload);
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
}
|
||||
if (notification === "INDOOR_HUMIDITY") {
|
||||
this.indoorHumidity = this.roundValue(payload);
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
}
|
||||
},
|
||||
|
||||
/* updateWeather(compliments)
|
||||
* Requests new data from openweather.org.
|
||||
* Calls processWeather on succesfull response.
|
||||
*/
|
||||
updateWeather: function () {
|
||||
if (this.config.appid === "") {
|
||||
Log.error("CurrentWeather: APPID not set!");
|
||||
return;
|
||||
}
|
||||
|
||||
var url = this.config.apiBase + this.config.apiVersion + "/" + this.config.weatherEndpoint + this.getParams();
|
||||
var self = this;
|
||||
var retry = true;
|
||||
|
||||
var weatherRequest = new XMLHttpRequest();
|
||||
weatherRequest.open("GET", url, true);
|
||||
weatherRequest.onreadystatechange = function () {
|
||||
if (this.readyState === 4) {
|
||||
if (this.status === 200) {
|
||||
self.processWeather(JSON.parse(this.response));
|
||||
} else if (this.status === 401) {
|
||||
self.updateDom(self.config.animationSpeed);
|
||||
|
||||
Log.error(self.name + ": Incorrect APPID.");
|
||||
retry = true;
|
||||
} else {
|
||||
Log.error(self.name + ": Could not load weather.");
|
||||
}
|
||||
|
||||
if (retry) {
|
||||
self.scheduleUpdate(self.loaded ? -1 : self.config.retryDelay);
|
||||
}
|
||||
}
|
||||
};
|
||||
weatherRequest.send();
|
||||
},
|
||||
|
||||
/* getParams(compliments)
|
||||
* Generates an url with api parameters based on the config.
|
||||
*
|
||||
* return String - URL params.
|
||||
*/
|
||||
getParams: function () {
|
||||
var params = "?";
|
||||
if (this.config.locationID) {
|
||||
params += "id=" + this.config.locationID;
|
||||
} else if (this.config.location) {
|
||||
params += "q=" + this.config.location;
|
||||
} else if (this.firstEvent && this.firstEvent.geo) {
|
||||
params += "lat=" + this.firstEvent.geo.lat + "&lon=" + this.firstEvent.geo.lon;
|
||||
} else if (this.firstEvent && this.firstEvent.location) {
|
||||
params += "q=" + this.firstEvent.location;
|
||||
} else {
|
||||
this.hide(this.config.animationSpeed, { lockString: this.identifier });
|
||||
return;
|
||||
}
|
||||
|
||||
params += "&units=" + this.config.units;
|
||||
params += "&lang=" + this.config.lang;
|
||||
params += "&APPID=" + this.config.appid;
|
||||
|
||||
return params;
|
||||
},
|
||||
|
||||
/* processWeather(data)
|
||||
* Uses the received data to set the various values.
|
||||
*
|
||||
* argument data object - Weather information received form openweather.org.
|
||||
*/
|
||||
processWeather: function (data) {
|
||||
if (!data || !data.main || typeof data.main.temp === "undefined") {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
this.humidity = parseFloat(data.main.humidity);
|
||||
this.temperature = this.roundValue(data.main.temp);
|
||||
this.fetchedLocationName = data.name;
|
||||
this.feelsLike = 0;
|
||||
|
||||
if (this.config.useBeaufort) {
|
||||
this.windSpeed = this.ms2Beaufort(this.roundValue(data.wind.speed));
|
||||
} else if (this.config.useKMPHwind) {
|
||||
this.windSpeed = parseFloat((data.wind.speed * 60 * 60) / 1000).toFixed(0);
|
||||
} else {
|
||||
this.windSpeed = parseFloat(data.wind.speed).toFixed(0);
|
||||
}
|
||||
|
||||
// ONLY WORKS IF TEMP IN C //
|
||||
var windInMph = parseFloat(data.wind.speed * 2.23694);
|
||||
|
||||
var tempInF = 0;
|
||||
switch (this.config.units) {
|
||||
case "metric":
|
||||
tempInF = 1.8 * this.temperature + 32;
|
||||
break;
|
||||
case "imperial":
|
||||
tempInF = this.temperature;
|
||||
break;
|
||||
case "default":
|
||||
tempInF = 1.8 * (this.temperature - 273.15) + 32;
|
||||
break;
|
||||
}
|
||||
|
||||
if (windInMph > 3 && tempInF < 50) {
|
||||
// windchill
|
||||
var windChillInF = Math.round(35.74 + 0.6215 * tempInF - 35.75 * Math.pow(windInMph, 0.16) + 0.4275 * tempInF * Math.pow(windInMph, 0.16));
|
||||
var windChillInC = (windChillInF - 32) * (5 / 9);
|
||||
// this.feelsLike = windChillInC.toFixed(0);
|
||||
|
||||
switch (this.config.units) {
|
||||
case "metric":
|
||||
this.feelsLike = windChillInC.toFixed(0);
|
||||
break;
|
||||
case "imperial":
|
||||
this.feelsLike = windChillInF.toFixed(0);
|
||||
break;
|
||||
case "default":
|
||||
this.feelsLike = (windChillInC + 273.15).toFixed(0);
|
||||
break;
|
||||
}
|
||||
} else if (tempInF > 80 && this.humidity > 40) {
|
||||
// heat index
|
||||
var Hindex =
|
||||
-42.379 +
|
||||
2.04901523 * tempInF +
|
||||
10.14333127 * this.humidity -
|
||||
0.22475541 * tempInF * this.humidity -
|
||||
6.83783 * Math.pow(10, -3) * tempInF * tempInF -
|
||||
5.481717 * Math.pow(10, -2) * this.humidity * this.humidity +
|
||||
1.22874 * Math.pow(10, -3) * tempInF * tempInF * this.humidity +
|
||||
8.5282 * Math.pow(10, -4) * tempInF * this.humidity * this.humidity -
|
||||
1.99 * Math.pow(10, -6) * tempInF * tempInF * this.humidity * this.humidity;
|
||||
|
||||
switch (this.config.units) {
|
||||
case "metric":
|
||||
this.feelsLike = parseFloat((Hindex - 32) / 1.8).toFixed(0);
|
||||
break;
|
||||
case "imperial":
|
||||
this.feelsLike = Hindex.toFixed(0);
|
||||
break;
|
||||
case "default":
|
||||
var tc = parseFloat((Hindex - 32) / 1.8) + 273.15;
|
||||
this.feelsLike = tc.toFixed(0);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this.feelsLike = parseFloat(this.temperature).toFixed(0);
|
||||
}
|
||||
|
||||
this.windDirection = this.deg2Cardinal(data.wind.deg);
|
||||
this.windDeg = data.wind.deg;
|
||||
this.weatherType = this.config.iconTable[data.weather[0].icon];
|
||||
|
||||
var now = new Date();
|
||||
var sunrise = new Date(data.sys.sunrise * 1000);
|
||||
var sunset = new Date(data.sys.sunset * 1000);
|
||||
|
||||
// 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 sunriseSunsetDateObject = sunrise < now && sunset > now ? sunset : sunrise;
|
||||
var timeString = moment(sunriseSunsetDateObject).format("HH:mm");
|
||||
if (this.config.timeFormat !== 24) {
|
||||
//var hours = sunriseSunsetDateObject.getHours() % 12 || 12;
|
||||
if (this.config.showPeriod) {
|
||||
if (this.config.showPeriodUpper) {
|
||||
//timeString = hours + moment(sunriseSunsetDateObject).format(':mm A');
|
||||
timeString = moment(sunriseSunsetDateObject).format("h:mm A");
|
||||
} else {
|
||||
//timeString = hours + moment(sunriseSunsetDateObject).format(':mm a');
|
||||
timeString = moment(sunriseSunsetDateObject).format("h:mm a");
|
||||
}
|
||||
} else {
|
||||
//timeString = hours + moment(sunriseSunsetDateObject).format(':mm');
|
||||
timeString = moment(sunriseSunsetDateObject).format("h:mm");
|
||||
}
|
||||
}
|
||||
|
||||
this.sunriseSunsetTime = timeString;
|
||||
this.sunriseSunsetIcon = sunrise < now && sunset > now ? "wi-sunset" : "wi-sunrise";
|
||||
|
||||
this.show(this.config.animationSpeed, { lockString: this.identifier });
|
||||
this.loaded = true;
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
this.sendNotification("CURRENTWEATHER_DATA", { data: data });
|
||||
this.sendNotification("CURRENTWEATHER_TYPE", { type: this.config.iconTable[data.weather[0].icon].replace("-", "_") });
|
||||
},
|
||||
|
||||
/* scheduleUpdate()
|
||||
* Schedule next update.
|
||||
*
|
||||
* argument delay number - Milliseconds before next update. If empty, this.config.updateInterval is used.
|
||||
*/
|
||||
scheduleUpdate: function (delay) {
|
||||
var nextLoad = this.config.updateInterval;
|
||||
if (typeof delay !== "undefined" && delay >= 0) {
|
||||
nextLoad = delay;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
setTimeout(function () {
|
||||
self.updateWeather();
|
||||
}, nextLoad);
|
||||
},
|
||||
|
||||
/* ms2Beaufort(ms)
|
||||
* Converts m2 to beaufort (windspeed).
|
||||
*
|
||||
* see:
|
||||
* https://www.spc.noaa.gov/faq/tornado/beaufort.html
|
||||
* https://en.wikipedia.org/wiki/Beaufort_scale#Modern_scale
|
||||
*
|
||||
* argument ms number - Windspeed in m/s.
|
||||
*
|
||||
* return number - Windspeed in beaufort.
|
||||
*/
|
||||
ms2Beaufort: function (ms) {
|
||||
var kmh = (ms * 60 * 60) / 1000;
|
||||
var speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
|
||||
for (var beaufort in speeds) {
|
||||
var speed = speeds[beaufort];
|
||||
if (speed > kmh) {
|
||||
return beaufort;
|
||||
}
|
||||
}
|
||||
return 12;
|
||||
},
|
||||
|
||||
deg2Cardinal: function (deg) {
|
||||
if (deg > 11.25 && deg <= 33.75) {
|
||||
return "NNE";
|
||||
} else if (deg > 33.75 && deg <= 56.25) {
|
||||
return "NE";
|
||||
} else if (deg > 56.25 && deg <= 78.75) {
|
||||
return "ENE";
|
||||
} else if (deg > 78.75 && deg <= 101.25) {
|
||||
return "E";
|
||||
} else if (deg > 101.25 && deg <= 123.75) {
|
||||
return "ESE";
|
||||
} else if (deg > 123.75 && deg <= 146.25) {
|
||||
return "SE";
|
||||
} else if (deg > 146.25 && deg <= 168.75) {
|
||||
return "SSE";
|
||||
} else if (deg > 168.75 && deg <= 191.25) {
|
||||
return "S";
|
||||
} else if (deg > 191.25 && deg <= 213.75) {
|
||||
return "SSW";
|
||||
} else if (deg > 213.75 && deg <= 236.25) {
|
||||
return "SW";
|
||||
} else if (deg > 236.25 && deg <= 258.75) {
|
||||
return "WSW";
|
||||
} else if (deg > 258.75 && deg <= 281.25) {
|
||||
return "W";
|
||||
} else if (deg > 281.25 && deg <= 303.75) {
|
||||
return "WNW";
|
||||
} else if (deg > 303.75 && deg <= 326.25) {
|
||||
return "NW";
|
||||
} else if (deg > 326.25 && deg <= 348.75) {
|
||||
return "NNW";
|
||||
} else {
|
||||
return "N";
|
||||
}
|
||||
},
|
||||
|
||||
/* function(temperature)
|
||||
* Rounds a temperature to 1 decimal or integer (depending on config.roundTemp).
|
||||
*
|
||||
* argument temperature number - Temperature.
|
||||
*
|
||||
* return string - Rounded Temperature.
|
||||
*/
|
||||
roundValue: function (temperature) {
|
||||
var decimals = this.config.roundTemp ? 0 : 1;
|
||||
var roundValue = parseFloat(temperature).toFixed(decimals);
|
||||
return roundValue === "-0" ? 0 : roundValue;
|
||||
}
|
||||
});
|
@@ -1,9 +0,0 @@
|
||||
const NodeHelper = require("node_helper");
|
||||
const Log = require("logger");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
start: function () {
|
||||
Log.warn(`The module '${this.name}' is deprecated in favor of the 'weather'-module, please refer to the documentation for a migration path`);
|
||||
}
|
||||
});
|
@@ -1,10 +1,10 @@
|
||||
/* Magic Mirror Default Modules List
|
||||
/* MagicMirror² 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.
|
||||
*/
|
||||
const defaultModules = ["alert", "calendar", "clock", "compliments", "currentweather", "helloworld", "newsfeed", "weatherforecast", "updatenotification", "weather"];
|
||||
const defaultModules = ["alert", "calendar", "clock", "compliments", "helloworld", "newsfeed", "updatenotification", "weather"];
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# Module: Hello World
|
||||
|
||||
The `helloworld` module is one of the default modules of the MagicMirror. It is a simple way to display a static text on the mirror.
|
||||
The `helloworld` module is one of the default modules of the MagicMirror². It is a simple way to display a static text on the mirror.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/helloworld.html).
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: HelloWorld
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Module: News Feed
|
||||
|
||||
The `newsfeed` module is one of the default modules of the MagicMirror.
|
||||
The `newsfeed` module is one of the default modules of the MagicMirror².
|
||||
This module displays news headlines based on an RSS feed. Scrolling through news headlines happens time-based (`updateInterval`), but can also be controlled by sending news feed specific notifications to the module.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/newsfeed.html).
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: NewsFeed
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -20,6 +20,7 @@ Module.register("newsfeed", {
|
||||
broadcastNewsFeeds: true,
|
||||
broadcastNewsUpdates: true,
|
||||
showDescription: false,
|
||||
showTitleAsUrl: false,
|
||||
wrapTitle: true,
|
||||
wrapDescription: true,
|
||||
truncDescription: true,
|
||||
@@ -37,7 +38,16 @@ Module.register("newsfeed", {
|
||||
endTags: [],
|
||||
prohibitedWords: [],
|
||||
scrollLength: 500,
|
||||
logFeedWarnings: false
|
||||
logFeedWarnings: false,
|
||||
dangerouslyDisableAutoEscaping: false
|
||||
},
|
||||
|
||||
getUrlPrefix: function (item) {
|
||||
if (item.useCorsProxy) {
|
||||
return location.protocol + "//" + location.host + "/cors?url=";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
// Define required scripts.
|
||||
@@ -121,7 +131,7 @@ Module.register("newsfeed", {
|
||||
}
|
||||
if (this.newsItems.length === 0) {
|
||||
return {
|
||||
loaded: false
|
||||
empty: true
|
||||
};
|
||||
}
|
||||
if (this.activeItem >= this.newsItems.length) {
|
||||
@@ -140,13 +150,19 @@ Module.register("newsfeed", {
|
||||
sourceTitle: item.sourceTitle,
|
||||
publishDate: moment(new Date(item.pubdate)).fromNow(),
|
||||
title: item.title,
|
||||
url: this.getUrlPrefix(item) + item.url,
|
||||
description: item.description,
|
||||
items: items
|
||||
};
|
||||
},
|
||||
|
||||
getActiveItemURL: function () {
|
||||
return typeof this.newsItems[this.activeItem].url === "string" ? this.newsItems[this.activeItem].url : this.newsItems[this.activeItem].url.href;
|
||||
const item = this.newsItems[this.activeItem];
|
||||
if (item) {
|
||||
return typeof item.url === "string" ? this.getUrlPrefix(item) + item.url : this.getUrlPrefix(item) + item.url.href;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -184,6 +200,7 @@ Module.register("newsfeed", {
|
||||
const dateB = new Date(b.pubdate);
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
if (this.config.maxNewsItems > 0) {
|
||||
newsItems = newsItems.slice(0, this.config.maxNewsItems);
|
||||
}
|
||||
@@ -219,7 +236,6 @@ Module.register("newsfeed", {
|
||||
}
|
||||
|
||||
//Remove selected tags from the end of rss feed items (title or description)
|
||||
|
||||
if (this.config.removeEndTags) {
|
||||
for (let endTag of this.config.endTags) {
|
||||
if (item.title.slice(-endTag.length) === endTag) {
|
||||
@@ -295,6 +311,9 @@ Module.register("newsfeed", {
|
||||
this.sendNotification("NEWS_FEED", { items: this.newsItems });
|
||||
}
|
||||
|
||||
// #2638 Clear timer if it already exists
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
this.activeItem++;
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
|
@@ -1,3 +1,27 @@
|
||||
{% macro escapeText(text, dangerouslyDisableAutoEscaping=false) %}
|
||||
{% if dangerouslyDisableAutoEscaping %}
|
||||
{{ text | safe}}
|
||||
{% else %}
|
||||
{{ text }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro escapeTitle(title, url, dangerouslyDisableAutoEscaping=false, showTitleAsUrl=false) %}
|
||||
{% if dangerouslyDisableAutoEscaping %}
|
||||
{% if showTitleAsUrl %}
|
||||
<a href="{{ url }}" style="text-decoration:none;color:#ffffff" target="_blank">{{ title | safe }}</a>
|
||||
{% else %}
|
||||
{{ title | safe}}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if showTitleAsUrl %}
|
||||
<a href="{{ url }}" style="text-decoration:none;color:#ffffff" target="_blank">{{ title }}</a>
|
||||
{% else %}
|
||||
{{ title }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% if loaded %}
|
||||
{% if config.showAsList %}
|
||||
<ul class="newsfeed-list">
|
||||
@@ -14,14 +38,14 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
||||
{{ item.title }}
|
||||
{{ escapeTitle(item.title, item.url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }}
|
||||
</div>
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ item.description | truncate(config.lengthDescription) }}
|
||||
{{ escapeText(item.description | truncate(config.lengthDescription), config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ item.description }}
|
||||
{{ escapeText(item.description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -33,7 +57,7 @@
|
||||
{% 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 %}
|
||||
{{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %}, {% else %}: {% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}
|
||||
{{ publishDate }}:
|
||||
@@ -41,19 +65,23 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
||||
{{ title }}
|
||||
{{ escapeTitle(title, url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }}
|
||||
</div>
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ description | truncate(config.lengthDescription) }}
|
||||
{{ escapeText(description | truncate(config.lengthDescription), config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ description }}
|
||||
{{ escapeText(description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elseif empty %}
|
||||
<div class="small dimmed">
|
||||
{{ "NEWSFEED_NO_ITEMS" | translate | safe }}
|
||||
</div>
|
||||
{% elseif error %}
|
||||
<div class="small dimmed">
|
||||
{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Node Helper: Newsfeed - NewsfeedFetcher
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -7,8 +7,9 @@
|
||||
const Log = require("logger");
|
||||
const FeedMe = require("feedme");
|
||||
const NodeHelper = require("node_helper");
|
||||
const fetch = require("node-fetch");
|
||||
const fetch = require("fetch");
|
||||
const iconv = require("iconv-lite");
|
||||
const stream = require("stream");
|
||||
|
||||
/**
|
||||
* Responsible for requesting an update on the set interval and broadcasting the data.
|
||||
@@ -17,9 +18,10 @@ const iconv = require("iconv-lite");
|
||||
* @param {number} reloadInterval Reload interval in milliseconds.
|
||||
* @param {string} encoding Encoding of the feed.
|
||||
* @param {boolean} logFeedWarnings If true log warnings when there is an error parsing a news article.
|
||||
* @param {boolean} useCorsProxy If true cors proxy is used for article url's.
|
||||
* @class
|
||||
*/
|
||||
const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
||||
const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings, useCorsProxy) {
|
||||
let reloadTimer = null;
|
||||
let items = [];
|
||||
|
||||
@@ -56,7 +58,8 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
title: title,
|
||||
description: description,
|
||||
pubdate: pubdate,
|
||||
url: url
|
||||
url: url,
|
||||
useCorsProxy: useCorsProxy
|
||||
});
|
||||
} else if (logFeedWarnings) {
|
||||
Log.warn("Can't parse feed item:");
|
||||
@@ -77,9 +80,22 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
scheduleTimer();
|
||||
});
|
||||
|
||||
parser.on("ttl", (minutes) => {
|
||||
try {
|
||||
// 86400000 = 24 hours is mentioned in the docs as maximum value:
|
||||
const ttlms = Math.min(minutes * 60 * 1000, 86400000);
|
||||
if (ttlms > reloadInterval) {
|
||||
reloadInterval = ttlms;
|
||||
Log.info("Newsfeed-Fetcher: reloadInterval set to ttl=" + reloadInterval + " for url " + url);
|
||||
}
|
||||
} catch (error) {
|
||||
Log.warn("Newsfeed-Fetcher: feed ttl is no valid integer=" + minutes + " for url " + url);
|
||||
}
|
||||
});
|
||||
|
||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
const headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)",
|
||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version,
|
||||
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
||||
Pragma: "no-cache"
|
||||
};
|
||||
@@ -87,7 +103,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);
|
||||
let nodeStream;
|
||||
if (response.body instanceof stream.Readable) {
|
||||
nodeStream = response.body;
|
||||
} else {
|
||||
nodeStream = stream.Readable.fromWeb(response.body);
|
||||
}
|
||||
nodeStream.pipe(iconv.decodeStream(encoding)).pipe(parser);
|
||||
})
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Node Helper: Newsfeed
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -34,6 +34,8 @@ module.exports = NodeHelper.create({
|
||||
const url = feed.url || "";
|
||||
const encoding = feed.encoding || "UTF-8";
|
||||
const reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
|
||||
let useCorsProxy = feed.useCorsProxy;
|
||||
if (useCorsProxy === undefined) useCorsProxy = true;
|
||||
|
||||
try {
|
||||
new URL(url);
|
||||
@@ -46,7 +48,7 @@ module.exports = NodeHelper.create({
|
||||
let fetcher;
|
||||
if (typeof this.fetchers[url] === "undefined") {
|
||||
Log.log("Create new newsfetcher for url: " + url + " - Interval: " + reloadInterval);
|
||||
fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings);
|
||||
fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings, useCorsProxy);
|
||||
|
||||
fetcher.onReceive(() => {
|
||||
this.broadcastFeeds();
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Module: Update Notification
|
||||
|
||||
The `updatenotification` module is one of the default modules of the MagicMirror.
|
||||
This will display a message whenever a new version of the MagicMirror application is available.
|
||||
The `updatenotification` module is one of the default modules of the MagicMirror².
|
||||
This will display a message whenever a new version of the MagicMirror² application is available.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/updatenotification.html).
|
||||
|
@@ -4,48 +4,53 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const Log = require("logger");
|
||||
|
||||
class gitHelper {
|
||||
const BASE_DIR = path.normalize(`${__dirname}/../../../`);
|
||||
|
||||
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");
|
||||
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);
|
||||
const { stdout = "", stderr = "" } = await exec(command);
|
||||
|
||||
res.stdout = stdout;
|
||||
res.stderr = stderr;
|
||||
return res;
|
||||
return { stdout, stderr };
|
||||
}
|
||||
|
||||
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);
|
||||
const { stderr } = await this.execShell(`cd ${moduleFolder} && git remote -v`);
|
||||
|
||||
if (stderr) {
|
||||
Log.error(`Failed to fetch git data for ${moduleFolder}: ${stderr}`);
|
||||
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
add(moduleName) {
|
||||
let moduleFolder = this.baseDir;
|
||||
async add(moduleName) {
|
||||
let moduleFolder = BASE_DIR;
|
||||
|
||||
if (moduleName !== "default") {
|
||||
moduleFolder = moduleFolder + "modules/" + moduleName;
|
||||
moduleFolder = `${moduleFolder}modules/${moduleName}`;
|
||||
}
|
||||
|
||||
try {
|
||||
Log.info("Checking git for module: " + moduleName);
|
||||
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)) {
|
||||
const isGitRepo = await this.isGitRepo(moduleFolder);
|
||||
|
||||
if (isGitRepo) {
|
||||
// Folder has .git and has at least one git remote, watch this folder
|
||||
this.gitRepos.unshift({ module: moduleName, folder: moduleFolder });
|
||||
this.gitRepos.push({ module: moduleName, folder: moduleFolder });
|
||||
}
|
||||
} catch (err) {
|
||||
// Error when directory .git doesn't exist or doesn't have any remotes
|
||||
@@ -56,117 +61,102 @@ class gitHelper {
|
||||
async getStatusInfo(repo) {
|
||||
let gitInfo = {
|
||||
module: repo.module,
|
||||
// commits behind:
|
||||
behind: 0,
|
||||
// branch name:
|
||||
current: "",
|
||||
// current hash:
|
||||
hash: "",
|
||||
// remote branch:
|
||||
tracking: "",
|
||||
behind: 0, // commits behind
|
||||
current: "", // branch name
|
||||
hash: "", // current hash
|
||||
tracking: "", // remote branch
|
||||
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);
|
||||
const { stderr, stdout } = await this.execShell(`cd ${repo.folder} && git rev-parse HEAD`);
|
||||
|
||||
if (stderr) {
|
||||
Log.error(`Failed to get current commit hash for ${repo.module}: ${stderr}`);
|
||||
}
|
||||
gitInfo.hash = res.stdout;
|
||||
|
||||
gitInfo.hash = 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);
|
||||
|
||||
const { stderr, stdout } = await this.execShell(`cd ${repo.folder} && git status -sb`);
|
||||
|
||||
if (stderr) {
|
||||
Log.error(`Failed to get git status for ${repo.module}: ${stderr}`);
|
||||
// exit without git status info
|
||||
return;
|
||||
}
|
||||
|
||||
// only the first line of stdout is evaluated
|
||||
let status = res.stdout.split("\n")[0];
|
||||
let status = stdout.split("\n")[0];
|
||||
// examples for status:
|
||||
// ## develop...origin/develop
|
||||
// ## master...origin/master [behind 8]
|
||||
status = status.match(/(?![.#])([^.]*)/g);
|
||||
// ## master...origin/master [ahead 8, behind 1]
|
||||
status = status.match(/## (.*)\.\.\.([^ ]*)(?: .*behind (\d+))?/);
|
||||
// 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]) {
|
||||
// [ '## develop...origin/develop', 'develop', 'origin/develop' ]
|
||||
// [ '## master...origin/master [behind 8]', 'master', 'origin/master', '8' ]
|
||||
// [ '## master...origin/master [ahead 8, behind 1]', 'master', 'origin/master', '1' ]
|
||||
gitInfo.current = status[1];
|
||||
gitInfo.tracking = status[2];
|
||||
|
||||
if (status[3]) {
|
||||
// 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.behind = parseInt(status[3]);
|
||||
gitInfo.isBehindInStatus = true;
|
||||
}
|
||||
|
||||
return gitInfo;
|
||||
}
|
||||
|
||||
async getRepoInfo(repo) {
|
||||
let gitInfo;
|
||||
if (repo.gitInfo) {
|
||||
// mocking
|
||||
gitInfo = repo.gitInfo;
|
||||
} else {
|
||||
gitInfo = await this.getStatusInfo(repo);
|
||||
}
|
||||
const 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");
|
||||
}
|
||||
|
||||
const { stderr } = 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));
|
||||
const matches = 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);
|
||||
const { stdout } = await this.execShell(`cd ${repo.folder} && git rev-list --ancestry-path --count ${matches[0]}`);
|
||||
gitInfo.behind = parseInt(stdout);
|
||||
|
||||
return gitInfo;
|
||||
} catch (err) {
|
||||
Log.error("Failed to get git revisions for " + repo.module + ": " + 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);
|
||||
|
||||
for (const repo of this.gitRepos) {
|
||||
try {
|
||||
const gitInfo = await this.getRepoInfo(repo);
|
||||
|
||||
if (gitInfo) {
|
||||
gitResultList.push(gitInfo);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error(`Failed to retrieve repo info for ${repo.module}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,4 +164,4 @@ class gitHelper {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.gitHelper = gitHelper;
|
||||
module.exports = GitHelper;
|
||||
|
@@ -1,70 +1,66 @@
|
||||
const GitHelper = require(__dirname + "/git_helper.js");
|
||||
const defaultModules = require(__dirname + "/../defaultmodules.js");
|
||||
const GitHelper = require("./git_helper");
|
||||
const defaultModules = require("../defaultmodules");
|
||||
const NodeHelper = require("node_helper");
|
||||
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
config: {},
|
||||
|
||||
updateTimer: null,
|
||||
updateProcessStarted: false,
|
||||
|
||||
gitHelper: new GitHelper.gitHelper(),
|
||||
gitHelper: new 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
|
||||
this.gitHelper.add("default");
|
||||
|
||||
for (let moduleName in modules) {
|
||||
async configureModules(modules) {
|
||||
for (const moduleName of modules) {
|
||||
if (!this.ignoreUpdateChecking(moduleName)) {
|
||||
this.gitHelper.add(moduleName);
|
||||
await this.gitHelper.add(moduleName);
|
||||
}
|
||||
}
|
||||
|
||||
await this.gitHelper.add("default");
|
||||
},
|
||||
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
async socketNotificationReceived(notification, payload) {
|
||||
if (notification === "CONFIG") {
|
||||
this.config = payload;
|
||||
} else if (notification === "MODULES") {
|
||||
// if this is the 1st time thru the update check process
|
||||
if (!this.updateProcessStarted) {
|
||||
this.updateProcessStarted = true;
|
||||
this.configureModules(payload).then(() => this.performFetch());
|
||||
await this.configureModules(payload);
|
||||
await this.performFetch();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
performFetch: async function () {
|
||||
for (let gitInfo of await this.gitHelper.getRepos()) {
|
||||
this.sendSocketNotification("STATUS", gitInfo);
|
||||
async performFetch() {
|
||||
const repos = await this.gitHelper.getRepos();
|
||||
|
||||
for (const repo of repos) {
|
||||
this.sendSocketNotification("STATUS", repo);
|
||||
}
|
||||
|
||||
this.scheduleNextFetch(this.config.updateInterval);
|
||||
},
|
||||
|
||||
scheduleNextFetch: function (delay) {
|
||||
if (delay < 60 * 1000) {
|
||||
delay = 60 * 1000;
|
||||
}
|
||||
|
||||
let self = this;
|
||||
scheduleNextFetch(delay) {
|
||||
clearTimeout(this.updateTimer);
|
||||
this.updateTimer = setTimeout(function () {
|
||||
self.performFetch();
|
||||
}, delay);
|
||||
|
||||
this.updateTimer = setTimeout(() => {
|
||||
this.performFetch();
|
||||
}, Math.max(delay, ONE_MINUTE));
|
||||
},
|
||||
|
||||
ignoreUpdateChecking: function (moduleName) {
|
||||
ignoreUpdateChecking(moduleName) {
|
||||
// Should not check for updates for default modules
|
||||
if (defaultModules.indexOf(moduleName) >= 0) {
|
||||
if (defaultModules.includes(moduleName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Should not check for updates for ignored modules
|
||||
if (this.config.ignoreModules.indexOf(moduleName) >= 0) {
|
||||
if (this.config.ignoreModules.includes(moduleName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,3 @@
|
||||
.module.updatenotification a.difflink {
|
||||
text-decoration: none;
|
||||
}
|
@@ -1,46 +1,63 @@
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: UpdateNotification
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* 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: 5000
|
||||
ignoreModules: []
|
||||
},
|
||||
|
||||
suspended: false,
|
||||
moduleList: {},
|
||||
|
||||
// Override start method.
|
||||
start: function () {
|
||||
Log.info("Starting module: " + this.name);
|
||||
start() {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
this.addFilters();
|
||||
setInterval(() => {
|
||||
this.moduleList = {};
|
||||
this.updateDom(2);
|
||||
}, this.config.refreshInterval);
|
||||
},
|
||||
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
suspend() {
|
||||
this.suspended = true;
|
||||
},
|
||||
|
||||
resume() {
|
||||
this.suspended = false;
|
||||
this.updateDom(2);
|
||||
},
|
||||
|
||||
notificationReceived(notification) {
|
||||
if (notification === "DOM_OBJECTS_CREATED") {
|
||||
this.sendSocketNotification("CONFIG", this.config);
|
||||
this.sendSocketNotification("MODULES", Module.definitions);
|
||||
//this.hide(0, { lockString: this.identifier });
|
||||
this.sendSocketNotification("MODULES", Object.keys(Module.definitions));
|
||||
}
|
||||
},
|
||||
|
||||
// Override socket notification handler.
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
socketNotificationReceived(notification, payload) {
|
||||
if (notification === "STATUS") {
|
||||
this.updateUI(payload);
|
||||
}
|
||||
},
|
||||
|
||||
updateUI: function (payload) {
|
||||
getStyles() {
|
||||
return [`${this.name}.css`];
|
||||
},
|
||||
|
||||
getTemplate() {
|
||||
return `${this.name}.njk`;
|
||||
},
|
||||
|
||||
getTemplateData() {
|
||||
return { moduleList: this.moduleList, suspended: this.suspended };
|
||||
},
|
||||
|
||||
updateUI(payload) {
|
||||
if (payload && payload.behind > 0) {
|
||||
// if we haven't seen info for this module
|
||||
if (this.moduleList[payload.module] === undefined) {
|
||||
@@ -48,7 +65,6 @@ Module.register("updatenotification", {
|
||||
this.moduleList[payload.module] = payload;
|
||||
this.updateDom(2);
|
||||
}
|
||||
//self.show(1000, { lockString: self.identifier });
|
||||
} else if (payload && payload.behind === 0) {
|
||||
// if the module WAS in the list, but shouldn't be
|
||||
if (this.moduleList[payload.module] !== undefined) {
|
||||
@@ -59,62 +75,15 @@ Module.register("updatenotification", {
|
||||
}
|
||||
},
|
||||
|
||||
diffLink: function (module, text) {
|
||||
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 () {
|
||||
const wrapper = document.createElement("div");
|
||||
if (this.suspended === false) {
|
||||
// process the hash of module info found
|
||||
for (const key of Object.keys(this.moduleList)) {
|
||||
let m = this.moduleList[key];
|
||||
|
||||
const message = document.createElement("div");
|
||||
message.className = "small bright";
|
||||
|
||||
const icon = document.createElement("i");
|
||||
icon.className = "fa fa-exclamation-circle";
|
||||
icon.innerHTML = " ";
|
||||
message.appendChild(icon);
|
||||
|
||||
const updateInfoKeyName = m.behind === 1 ? "UPDATE_INFO_SINGLE" : "UPDATE_INFO_MULTIPLE";
|
||||
|
||||
let subtextHtml = this.translate(updateInfoKeyName, {
|
||||
COMMIT_COUNT: m.behind,
|
||||
BRANCH_NAME: m.current
|
||||
});
|
||||
|
||||
const text = document.createElement("span");
|
||||
if (m.module === "default") {
|
||||
text.innerHTML = this.translate("UPDATE_NOTIFICATION");
|
||||
subtextHtml = this.diffLink(m, subtextHtml);
|
||||
} else {
|
||||
text.innerHTML = this.translate("UPDATE_NOTIFICATION_MODULE", {
|
||||
MODULE_NAME: m.module
|
||||
});
|
||||
}
|
||||
message.appendChild(text);
|
||||
|
||||
wrapper.appendChild(message);
|
||||
|
||||
const subtext = document.createElement("div");
|
||||
subtext.innerHTML = subtextHtml;
|
||||
subtext.className = "xsmall dimmed";
|
||||
wrapper.appendChild(subtext);
|
||||
addFilters() {
|
||||
this.nunjucksEnvironment().addFilter("diffLink", (text, status) => {
|
||||
if (status.module !== "default") {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
suspend: function () {
|
||||
this.suspended = true;
|
||||
},
|
||||
resume: function () {
|
||||
this.suspended = false;
|
||||
this.updateDom(2);
|
||||
const localRef = status.hash;
|
||||
const remoteRef = status.tracking.replace(/.*\//, "");
|
||||
return `<a href="https://github.com/MichMich/MagicMirror/compare/${localRef}...${remoteRef}" class="xsmall dimmed difflink" target="_blank">${text}</a>`;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
15
modules/default/updatenotification/updatenotification.njk
Normal file
15
modules/default/updatenotification/updatenotification.njk
Normal file
@@ -0,0 +1,15 @@
|
||||
{% if not suspended %}
|
||||
{% for name, status in moduleList %}
|
||||
<div class="small bright">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<span>
|
||||
{% set mainTextLabel = "UPDATE_NOTIFICATION" if name === "default" else "UPDATE_NOTIFICATION_MODULE" %}
|
||||
{{ mainTextLabel | translate({MODULE_NAME: name}) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="xsmall dimmed">
|
||||
{% set subTextLabel = "UPDATE_INFO_SINGLE" if status.behind === 1 else "UPDATE_INFO_MULTIPLE" %}
|
||||
{{ subTextLabel | translate({COMMIT_COUNT: status.behind, BRANCH_NAME: status.current}) | diffLink(status) | safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
147
modules/default/utils.js
Normal file
147
modules/default/utils.js
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* A function to make HTTP requests via the server to avoid CORS-errors.
|
||||
*
|
||||
* @param {string} url the url to fetch from
|
||||
* @param {string} type what contenttype to expect in the response, can be "json" or "xml"
|
||||
* @param {boolean} useCorsProxy A flag to indicate
|
||||
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||
* @returns {Promise} resolved when the fetch is done. The response headers is placed in a headers-property (provided the response does not allready contain a headers-property).
|
||||
*/
|
||||
async function performWebRequest(url, type = "json", useCorsProxy = false, requestHeaders = undefined, expectedResponseHeaders = undefined) {
|
||||
const request = {};
|
||||
if (useCorsProxy) {
|
||||
url = getCorsUrl(url, requestHeaders, expectedResponseHeaders);
|
||||
} else {
|
||||
request.headers = getHeadersToSend(requestHeaders);
|
||||
}
|
||||
const response = await fetch(url, request);
|
||||
const data = await response.text();
|
||||
|
||||
if (type === "xml") {
|
||||
return new DOMParser().parseFromString(data, "text/html");
|
||||
} else {
|
||||
if (!data || !data.length > 0) return undefined;
|
||||
|
||||
const dataResponse = JSON.parse(data);
|
||||
if (!dataResponse.headers) {
|
||||
dataResponse.headers = getHeadersFromResponse(expectedResponseHeaders, response);
|
||||
}
|
||||
return dataResponse;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a URL that will be used when calling the CORS-method on the server.
|
||||
*
|
||||
* @param {string} url the url to fetch from
|
||||
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||
* @returns {string} to be used as URL when calling CORS-method on server.
|
||||
*/
|
||||
const getCorsUrl = function (url, requestHeaders, expectedResponseHeaders) {
|
||||
if (!url || url.length < 1) {
|
||||
throw new Error(`Invalid URL: ${url}`);
|
||||
} else {
|
||||
let corsUrl = `${location.protocol}//${location.host}/cors?`;
|
||||
|
||||
const requestHeaderString = getRequestHeaderString(requestHeaders);
|
||||
if (requestHeaderString) corsUrl = `${corsUrl}sendheaders=${requestHeaderString}`;
|
||||
|
||||
const expectedResponseHeadersString = getExpectedResponseHeadersString(expectedResponseHeaders);
|
||||
if (requestHeaderString && expectedResponseHeadersString) {
|
||||
corsUrl = `${corsUrl}&expectedheaders=${expectedResponseHeadersString}`;
|
||||
} else if (expectedResponseHeadersString) {
|
||||
corsUrl = `${corsUrl}expectedheaders=${expectedResponseHeadersString}`;
|
||||
}
|
||||
|
||||
if (requestHeaderString || expectedResponseHeadersString) {
|
||||
return `${corsUrl}&url=${url}`;
|
||||
}
|
||||
return `${corsUrl}url=${url}`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the part of the CORS URL that represents the HTTP headers to send.
|
||||
*
|
||||
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||
* @returns {string} to be used as request-headers component in CORS URL.
|
||||
*/
|
||||
const getRequestHeaderString = function (requestHeaders) {
|
||||
let requestHeaderString = "";
|
||||
if (requestHeaders) {
|
||||
for (const header of requestHeaders) {
|
||||
if (requestHeaderString.length === 0) {
|
||||
requestHeaderString = `${header.name}:${encodeURIComponent(header.value)}`;
|
||||
} else {
|
||||
requestHeaderString = `${requestHeaderString},${header.name}:${encodeURIComponent(header.value)}`;
|
||||
}
|
||||
}
|
||||
return requestHeaderString;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets headers and values to attatch to the web request.
|
||||
*
|
||||
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||
* @returns {object} An object specifying name and value of the headers.
|
||||
*/
|
||||
const getHeadersToSend = (requestHeaders) => {
|
||||
const headersToSend = {};
|
||||
if (requestHeaders) {
|
||||
for (const header of requestHeaders) {
|
||||
headersToSend[header.name] = header.value;
|
||||
}
|
||||
}
|
||||
|
||||
return headersToSend;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the part of the CORS URL that represents the expected HTTP headers to recieve.
|
||||
*
|
||||
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||
* @returns {string} to be used as the expected HTTP-headers component in CORS URL.
|
||||
*/
|
||||
const getExpectedResponseHeadersString = function (expectedResponseHeaders) {
|
||||
let expectedResponseHeadersString = "";
|
||||
if (expectedResponseHeaders) {
|
||||
for (const header of expectedResponseHeaders) {
|
||||
if (expectedResponseHeadersString.length === 0) {
|
||||
expectedResponseHeadersString = `${header}`;
|
||||
} else {
|
||||
expectedResponseHeadersString = `${expectedResponseHeadersString},${header}`;
|
||||
}
|
||||
}
|
||||
return expectedResponseHeaders;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the values for the expected headers from the response.
|
||||
*
|
||||
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||
* @param {Response} response the HTTP response
|
||||
* @returns {string} to be used as the expected HTTP-headers component in CORS URL.
|
||||
*/
|
||||
const getHeadersFromResponse = (expectedResponseHeaders, response) => {
|
||||
const responseHeaders = [];
|
||||
|
||||
if (expectedResponseHeaders) {
|
||||
for (const header of expectedResponseHeaders) {
|
||||
const headerValue = response.headers.get(header);
|
||||
responseHeaders.push({ name: header, value: headerValue });
|
||||
}
|
||||
}
|
||||
|
||||
return responseHeaders;
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined")
|
||||
module.exports = {
|
||||
performWebRequest
|
||||
};
|
2
modules/default/weather/README.md
Executable file → Normal file
2
modules/default/weather/README.md
Executable file → Normal file
@@ -1,5 +1,5 @@
|
||||
# Weather Module
|
||||
|
||||
This module aims to be the replacement for the current `currentweather` and `weatherforcast` modules. The module will be configurable to be used as a current weather view, or to show the forecast. This way the module can be used twice to fulfill both purposes.
|
||||
This module will be configurable to be used as a current weather view, or to show the forecast. This way the module can be used twice to fulfill both purposes.
|
||||
|
||||
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/weather.html).
|
||||
|
16
modules/default/weather/current.njk
Executable file → Normal file
16
modules/default/weather/current.njk
Executable file → Normal file
@@ -3,19 +3,11 @@
|
||||
<div class="normal medium">
|
||||
<span class="wi wi-strong-wind dimmed"></span>
|
||||
<span>
|
||||
{% if config.useBeaufort %}
|
||||
{{ current.beaufortWindSpeed() | round }}
|
||||
{% else %}
|
||||
{% if config.useKmh %}
|
||||
{{ current.kmhWindSpeed() | round }}
|
||||
{% else %}
|
||||
{{ current.windSpeed | round }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ current.windSpeed | unit("wind") | round }}
|
||||
{% if config.showWindDirection %}
|
||||
<sup>
|
||||
{% if config.showWindDirectionAsArrow %}
|
||||
<i class="fa fa-long-arrow-up" style="transform:rotate({{ current.windDirection }}deg);"></i>
|
||||
<i class="fas fa-long-arrow-alt-up" style="transform:rotate({{ current.windDirection }}deg);"></i>
|
||||
{% else %}
|
||||
{{ current.cardinalWindDirection() | translate }}
|
||||
{% endif %}
|
||||
@@ -47,7 +39,7 @@
|
||||
<div class="normal light indoor">
|
||||
{% if config.showIndoorTemperature and indoor.temperature %}
|
||||
<div>
|
||||
<span class="fa fa-home"></span>
|
||||
<span class="fas fa-home"></span>
|
||||
<span class="bright">
|
||||
{{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</span>
|
||||
@@ -55,7 +47,7 @@
|
||||
{% endif %}
|
||||
{% if config.showIndoorHumidity and indoor.humidity %}
|
||||
<div>
|
||||
<span class="fa fa-tint"></span>
|
||||
<span class="fas fa-tint"></span>
|
||||
<span class="bright">
|
||||
{{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }}
|
||||
</span>
|
||||
|
@@ -8,9 +8,9 @@
|
||||
{% 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) and config.ignoreToday == false %}
|
||||
{% if (currentStep == 0) and config.ignoreToday == false and config.absoluteDates == false %}
|
||||
<td class="day">{{ "TODAY" | translate }}</td>
|
||||
{% elif (currentStep == 1) and config.ignoreToday == false %}
|
||||
{% elif (currentStep == 1) and config.ignoreToday == false and config.absoluteDates == false %}
|
||||
<td class="day">{{ "TOMORROW" | translate }}</td>
|
||||
{% else %}
|
||||
<td class="day">{{ f.date.format('ddd') }}</td>
|
||||
|
0
modules/default/weather/providers/README.md
Executable file → Normal file
0
modules/default/weather/providers/README.md
Executable file → Normal file
23
modules/default/weather/providers/darksky.js
Executable file → Normal file
23
modules/default/weather/providers/darksky.js
Executable file → Normal file
@@ -1,6 +1,6 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: Dark Sky
|
||||
*
|
||||
@@ -18,18 +18,14 @@ WeatherProvider.register("darksky", {
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "https://cors-anywhere.herokuapp.com/https://api.darksky.net",
|
||||
useCorsProxy: true,
|
||||
apiBase: "https://api.darksky.net",
|
||||
weatherEndpoint: "/forecast",
|
||||
apiKey: "",
|
||||
lat: 0,
|
||||
lon: 0
|
||||
},
|
||||
|
||||
units: {
|
||||
imperial: "us",
|
||||
metric: "si"
|
||||
},
|
||||
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
@@ -66,13 +62,12 @@ WeatherProvider.register("darksky", {
|
||||
|
||||
// Create a URL from the config and base URL.
|
||||
getUrl() {
|
||||
const units = this.units[this.config.units] || "auto";
|
||||
return `${this.config.apiBase}${this.config.weatherEndpoint}/${this.config.apiKey}/${this.config.lat},${this.config.lon}?units=${units}&lang=${this.config.lang}`;
|
||||
return `${this.config.apiBase}${this.config.weatherEndpoint}/${this.config.apiKey}/${this.config.lat},${this.config.lon}?units=si&lang=${this.config.lang}`;
|
||||
},
|
||||
|
||||
// Implement WeatherDay generator.
|
||||
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
currentWeather.date = moment();
|
||||
currentWeather.humidity = parseFloat(currentWeatherData.currently.humidity);
|
||||
@@ -80,8 +75,8 @@ WeatherProvider.register("darksky", {
|
||||
currentWeather.windSpeed = parseFloat(currentWeatherData.currently.windSpeed);
|
||||
currentWeather.windDirection = currentWeatherData.currently.windBearing;
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.currently.icon);
|
||||
currentWeather.sunrise = moment(currentWeatherData.daily.data[0].sunriseTime, "X");
|
||||
currentWeather.sunset = moment(currentWeatherData.daily.data[0].sunsetTime, "X");
|
||||
currentWeather.sunrise = moment.unix(currentWeatherData.daily.data[0].sunriseTime);
|
||||
currentWeather.sunset = moment.unix(currentWeatherData.daily.data[0].sunsetTime);
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
@@ -90,9 +85,9 @@ WeatherProvider.register("darksky", {
|
||||
const days = [];
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
const weather = new WeatherObject();
|
||||
|
||||
weather.date = moment(forecast.time, "X");
|
||||
weather.date = moment.unix(forecast.time);
|
||||
weather.minTemperature = forecast.temperatureMin;
|
||||
weather.maxTemperature = forecast.temperatureMax;
|
||||
weather.weatherType = this.convertWeatherType(forecast.icon);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: Environment Canada (EC)
|
||||
*
|
||||
@@ -11,13 +11,13 @@
|
||||
* 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:
|
||||
* This module supports Canadian locations only and requires 2 additional config parameters:
|
||||
*
|
||||
* 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
|
||||
* Example: for Toronto, Ontario, the following parameters would be used
|
||||
*
|
||||
* siteCode: 's0000458',
|
||||
* provCode: 'ON'
|
||||
@@ -40,6 +40,7 @@ WeatherProvider.register("envcanada", {
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
useCorsProxy: true,
|
||||
siteCode: "s1234567",
|
||||
provCode: "ON"
|
||||
},
|
||||
@@ -63,17 +64,13 @@ WeatherProvider.register("envcanada", {
|
||||
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")
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
@@ -93,7 +90,7 @@ WeatherProvider.register("envcanada", {
|
||||
// Override the fetchWeatherForecast method to query EC and construct Forecast weather objects
|
||||
//
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl(), "GET")
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
@@ -113,7 +110,7 @@ WeatherProvider.register("envcanada", {
|
||||
// Override the fetchWeatherHourly method to query EC and construct Forecast weather objects
|
||||
//
|
||||
fetchWeatherHourly() {
|
||||
this.fetchData(this.getUrl(), "GET")
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
@@ -129,26 +126,6 @@ WeatherProvider.register("envcanada", {
|
||||
.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
|
||||
@@ -156,15 +133,12 @@ WeatherProvider.register("envcanada", {
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
//
|
||||
// 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
|
||||
// Build the EC URL based on the Site Code and Province Code specified in the config params. Note that the
|
||||
// URL defaults to the English version simply because there is no language dependency 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";
|
||||
return "https://dd.weather.gc.ca/citypage_weather/xml/" + this.config.provCode + "/" + this.config.siteCode + "_e.xml";
|
||||
},
|
||||
|
||||
//
|
||||
@@ -172,7 +146,7 @@ WeatherProvider.register("envcanada", {
|
||||
//
|
||||
|
||||
generateWeatherObjectFromCurrentWeather(ECdoc) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
// 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
|
||||
@@ -183,13 +157,13 @@ WeatherProvider.register("envcanada", {
|
||||
// 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);
|
||||
currentWeather.temperature = 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.windSpeed = WeatherUtils.convertWindToMs(ECdoc.querySelector("siteData currentConditions wind speed").textContent);
|
||||
|
||||
currentWeather.windDirection = ECdoc.querySelector("siteData currentConditions wind bearing").textContent;
|
||||
|
||||
@@ -212,11 +186,11 @@ WeatherProvider.register("envcanada", {
|
||||
currentWeather.feelsLikeTemp = currentWeather.temperature;
|
||||
|
||||
if (ECdoc.querySelector("siteData currentConditions windChill")) {
|
||||
currentWeather.feelsLikeTemp = this.convertTemp(ECdoc.querySelector("siteData currentConditions windChill").textContent);
|
||||
currentWeather.feelsLikeTemp = ECdoc.querySelector("siteData currentConditions windChill").textContent;
|
||||
}
|
||||
|
||||
if (ECdoc.querySelector("siteData currentConditions humidex")) {
|
||||
currentWeather.feelsLikeTemp = this.convertTemp(ECdoc.querySelector("siteData currentConditions humidex").textContent);
|
||||
currentWeather.feelsLikeTemp = ECdoc.querySelector("siteData currentConditions humidex").textContent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +221,7 @@ WeatherProvider.register("envcanada", {
|
||||
|
||||
const days = [];
|
||||
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const weather = new WeatherObject();
|
||||
|
||||
const foreBaseDates = ECdoc.querySelectorAll("siteData forecastGroup dateTime");
|
||||
const baseDate = foreBaseDates[1].querySelector("timeStamp").textContent;
|
||||
@@ -348,7 +322,7 @@ WeatherProvider.register("envcanada", {
|
||||
days.push(weather);
|
||||
|
||||
//
|
||||
// Now do the the rest of the forecast starting at nextDay. We will process each day using 2 EC
|
||||
// Now do 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.
|
||||
@@ -357,12 +331,12 @@ WeatherProvider.register("envcanada", {
|
||||
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);
|
||||
let weather = new WeatherObject();
|
||||
|
||||
// Add 1 to the date to reflect the current forecast day we are building
|
||||
|
||||
lastDate = lastDate.add(1, "day");
|
||||
weather.date = moment(lastDate, "X");
|
||||
weather.date = moment.unix(lastDate);
|
||||
|
||||
// Capture the temperatures for the current Element and the next Element in order to set
|
||||
// the Min and Max temperatures for the forecast
|
||||
@@ -411,17 +385,17 @@ WeatherProvider.register("envcanada", {
|
||||
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);
|
||||
const weather = new WeatherObject();
|
||||
|
||||
// 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");
|
||||
weather.date = moment.unix(currTime);
|
||||
|
||||
// Capture the temperature
|
||||
|
||||
weather.temperature = this.convertTemp(hourGroup[stepHour].querySelector("temperature").textContent);
|
||||
weather.temperature = hourGroup[stepHour].querySelector("temperature").textContent;
|
||||
|
||||
// Capture Likelihood of Precipitation (LOP) and unit-of-measure values
|
||||
|
||||
@@ -472,7 +446,7 @@ WeatherProvider.register("envcanada", {
|
||||
weather.minTemperature = this.todayTempCacheMin;
|
||||
weather.maxTemperature = this.todayTempCacheMax;
|
||||
} else {
|
||||
weather.minTemperature = this.convertTemp(currentTemp);
|
||||
weather.minTemperature = currentTemp;
|
||||
weather.maxTemperature = weather.minTemperature;
|
||||
}
|
||||
}
|
||||
@@ -485,14 +459,14 @@ WeatherProvider.register("envcanada", {
|
||||
//
|
||||
|
||||
if (todayClass === "low") {
|
||||
weather.minTemperature = this.convertTemp(todayTemp);
|
||||
weather.minTemperature = todayTemp;
|
||||
if (today === 0 && fullDay === true) {
|
||||
this.todayTempCacheMin = weather.minTemperature;
|
||||
}
|
||||
}
|
||||
|
||||
if (todayClass === "high") {
|
||||
weather.maxTemperature = this.convertTemp(todayTemp);
|
||||
weather.maxTemperature = todayTemp;
|
||||
if (today === 0 && fullDay === true) {
|
||||
this.todayTempCacheMax = weather.maxTemperature;
|
||||
}
|
||||
@@ -504,11 +478,11 @@ WeatherProvider.register("envcanada", {
|
||||
|
||||
if (fullDay === true) {
|
||||
if (nextClass === "low") {
|
||||
weather.minTemperature = this.convertTemp(nextTemp);
|
||||
weather.minTemperature = nextTemp;
|
||||
}
|
||||
|
||||
if (nextClass === "high") {
|
||||
weather.maxTemperature = this.convertTemp(nextTemp);
|
||||
weather.maxTemperature = nextTemp;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -558,31 +532,6 @@ WeatherProvider.register("envcanada", {
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
537
modules/default/weather/providers/openmeteo.js
Normal file
537
modules/default/weather/providers/openmeteo.js
Normal file
@@ -0,0 +1,537 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: Open-Meteo
|
||||
*
|
||||
* By Andrés Vanegas
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Open-Meteo, based on Andrew Pometti's class
|
||||
* for Weatherbit.
|
||||
*/
|
||||
// https://www.bigdatacloud.com/docs/api/free-reverse-geocode-to-city-api
|
||||
const GEOCODE_BASE = "https://api.bigdatacloud.net/data/reverse-geocode-client";
|
||||
const OPEN_METEO_BASE = "https://api.open-meteo.com/v1";
|
||||
|
||||
WeatherProvider.register("openmeteo", {
|
||||
// Set the name of the provider.
|
||||
// Not strictly required, but helps for debugging.
|
||||
providerName: "Open-Meteo",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: OPEN_METEO_BASE,
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
past_days: 0,
|
||||
type: "current"
|
||||
},
|
||||
|
||||
// https://open-meteo.com/en/docs
|
||||
hourlyParams: [
|
||||
// Air temperature at 2 meters above ground
|
||||
"temperature_2m",
|
||||
// Relative humidity at 2 meters above ground
|
||||
"relativehumidity_2m",
|
||||
// Dew point temperature at 2 meters above ground
|
||||
"dewpoint_2m",
|
||||
// Apparent temperature is the perceived feels-like temperature combining wind chill factor, relative humidity and solar radiation
|
||||
"apparent_temperature",
|
||||
// Atmospheric air pressure reduced to mean sea level (msl) or pressure at surface. Typically pressure on mean sea level is used in meteorology. Surface pressure gets lower with increasing elevation.
|
||||
"pressure_msl",
|
||||
"surface_pressure",
|
||||
// Total cloud cover as an area fraction
|
||||
"cloudcover",
|
||||
// Low level clouds and fog up to 3 km altitude
|
||||
"cloudcover_low",
|
||||
// Mid level clouds from 3 to 8 km altitude
|
||||
"cloudcover_mid",
|
||||
// High level clouds from 8 km altitude
|
||||
"cloudcover_high",
|
||||
// Wind speed at 10, 80, 120 or 180 meters above ground. Wind speed on 10 meters is the standard level.
|
||||
"windspeed_10m",
|
||||
"windspeed_80m",
|
||||
"windspeed_120m",
|
||||
"windspeed_180m",
|
||||
// Wind direction at 10, 80, 120 or 180 meters above ground
|
||||
"winddirection_10m",
|
||||
"winddirection_80m",
|
||||
"winddirection_120m",
|
||||
"winddirection_180m",
|
||||
// Gusts at 10 meters above ground as a maximum of the preceding hour
|
||||
"windgusts_10m",
|
||||
// Shortwave solar radiation as average of the preceding hour. This is equal to the total global horizontal irradiation
|
||||
"shortwave_radiation",
|
||||
// Direct solar radiation as average of the preceding hour on the horizontal plane and the normal plane (perpendicular to the sun)
|
||||
"direct_radiation",
|
||||
"direct_normal_irradiance",
|
||||
// Diffuse solar radiation as average of the preceding hour
|
||||
"diffuse_radiation",
|
||||
// Vapor Pressure Deificit (VPD) in kilopascal (kPa). For high VPD (>1.6), water transpiration of plants increases. For low VPD (<0.4), transpiration decreases
|
||||
"vapor_pressure_deficit",
|
||||
// Evapotranspration from land surface and plants that weather models assumes for this location. Available soil water is considered. 1 mm evapotranspiration per hour equals 1 liter of water per spare meter.
|
||||
"evapotranspiration",
|
||||
// ET₀ Reference Evapotranspiration of a well watered grass field. Based on FAO-56 Penman-Monteith equations ET₀ is calculated from temperature, wind speed, humidity and solar radiation. Unlimited soil water is assumed. ET₀ is commonly used to estimate the required irrigation for plants.
|
||||
"et0_fao_evapotranspiration",
|
||||
// Total precipitation (rain, showers, snow) sum of the preceding hour
|
||||
"precipitation",
|
||||
// Snowfall amount of the preceding hour in centimeters. For the water equivalent in millimeter, divide by 7. E.g. 7 cm snow = 10 mm precipitation water equivalent
|
||||
"snowfall",
|
||||
// Rain from large scale weather systems of the preceding hour in millimeter
|
||||
"rain",
|
||||
// Showers from convective precipitation in millimeters from the preceding hour
|
||||
"showers",
|
||||
// Weather condition as a numeric code. Follow WMO weather interpretation codes.
|
||||
"weathercode",
|
||||
// Snow depth on the ground
|
||||
"snow_depth",
|
||||
// Altitude above sea level of the 0°C level
|
||||
"freezinglevel_height",
|
||||
// Temperature in the soil at 0, 6, 18 and 54 cm depths. 0 cm is the surface temperature on land or water surface temperature on water.
|
||||
"soil_temperature_0cm",
|
||||
"soil_temperature_6cm",
|
||||
"soil_temperature_18cm",
|
||||
"soil_temperature_54cm",
|
||||
// Average soil water content as volumetric mixing ratio at 0-1, 1-3, 3-9, 9-27 and 27-81 cm depths.
|
||||
"soil_moisture_0_1cm",
|
||||
"soil_moisture_1_3cm",
|
||||
"soil_moisture_3_9cm",
|
||||
"soil_moisture_9_27cm",
|
||||
"soil_moisture_27_81cm"
|
||||
],
|
||||
|
||||
dailyParams: [
|
||||
// Maximum and minimum daily air temperature at 2 meters above ground
|
||||
"temperature_2m_max",
|
||||
"temperature_2m_min",
|
||||
// Maximum and minimum daily apparent temperature
|
||||
"apparent_temperature_min",
|
||||
"apparent_temperature_max",
|
||||
// Sum of daily precipitation (including rain, showers and snowfall)
|
||||
"precipitation_sum",
|
||||
// Sum of daily rain
|
||||
"rain_sum",
|
||||
// Sum of daily showers
|
||||
"showers_sum",
|
||||
// Sum of daily snowfall
|
||||
"snowfall_sum",
|
||||
// The number of hours with rain
|
||||
"precipitation_hours",
|
||||
// The most severe weather condition on a given day
|
||||
"weathercode",
|
||||
// Sun rise and set times
|
||||
"sunrise",
|
||||
"sunset",
|
||||
// Maximum wind speed and gusts on a day
|
||||
"windspeed_10m_max",
|
||||
"windgusts_10m_max",
|
||||
// Dominant wind direction
|
||||
"winddirection_10m_dominant",
|
||||
// The sum of solar radiation on a given day in Megajoules
|
||||
"shortwave_radiation_sum",
|
||||
// Daily sum of ET₀ Reference Evapotranspiration of a well watered grass field
|
||||
"et0_fao_evapotranspiration"
|
||||
],
|
||||
|
||||
fetchedLocation: function () {
|
||||
return this.fetchedLocationName || "";
|
||||
},
|
||||
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => this.parseWeatherApiResponse(data))
|
||||
.then((parsedData) => {
|
||||
if (!parsedData) {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWeather = this.generateWeatherDayFromCurrentWeather(parsedData);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => this.parseWeatherApiResponse(data))
|
||||
.then((parsedData) => {
|
||||
if (!parsedData) {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const dailyForecast = this.generateWeatherObjectsFromForecast(parsedData);
|
||||
this.setWeatherForecast(dailyForecast);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
fetchWeatherHourly() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => this.parseWeatherApiResponse(data))
|
||||
.then((parsedData) => {
|
||||
if (!parsedData) {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const hourlyForecast = this.generateWeatherObjectsFromHourly(parsedData);
|
||||
this.setWeatherHourly(hourlyForecast);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides method for setting config to check if endpoint is correct for hourly
|
||||
*
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
setConfig(config) {
|
||||
this.config = {
|
||||
lang: config.lang ?? "en",
|
||||
...this.defaults,
|
||||
...config
|
||||
};
|
||||
|
||||
// Set properly maxNumberOfDays and max Entries properties according to config and value ranges allowed in the documentation
|
||||
const maxEntriesLimit = ["daily", "forecast"].includes(this.config.type) ? 7 : this.config.type === "hourly" ? 48 : 0;
|
||||
if (this.config.hasOwnProperty("maxNumberOfDays") && !isNaN(parseFloat(this.config.maxNumberOfDays))) {
|
||||
const daysFactor = ["daily", "forecast"].includes(this.config.type) ? 1 : this.config.type === "hourly" ? 24 : 0;
|
||||
this.config.maxEntries = Math.max(1, Math.min(Math.round(parseFloat(this.config.maxNumberOfDays)) * daysFactor, maxEntriesLimit));
|
||||
this.config.maxNumberOfDays = Math.ceil(this.config.maxEntries / Math.max(1, daysFactor));
|
||||
}
|
||||
this.config.maxEntries = Math.max(1, Math.min(this.config.maxEntries, maxEntriesLimit));
|
||||
|
||||
if (!this.config.type) {
|
||||
Log.error("type not configured and could not resolve it");
|
||||
}
|
||||
|
||||
this.fetchLocation();
|
||||
},
|
||||
|
||||
// Generate valid query params to perform the request
|
||||
getQueryParameters() {
|
||||
let params = {
|
||||
latitude: this.config.lat,
|
||||
longitude: this.config.lon,
|
||||
timeformat: "unixtime",
|
||||
timezone: "auto",
|
||||
past_days: this.config.past_days ?? 0,
|
||||
daily: this.dailyParams,
|
||||
hourly: this.hourlyParams,
|
||||
// Fixed units as metric
|
||||
temperature_unit: "celsius",
|
||||
windspeed_unit: "kmh",
|
||||
precipitation_unit: "mm"
|
||||
};
|
||||
|
||||
const startDate = moment().startOf("day");
|
||||
const endDate = moment(startDate)
|
||||
.add(Math.max(0, Math.min(7, this.config.maxNumberOfDays)), "days")
|
||||
.endOf("day");
|
||||
|
||||
params["start_date"] = startDate.format("YYYY-MM-DD");
|
||||
|
||||
switch (this.config.type) {
|
||||
case "hourly":
|
||||
case "daily":
|
||||
case "forecast":
|
||||
params["end_date"] = endDate.format("YYYY-MM-DD");
|
||||
break;
|
||||
case "current":
|
||||
params["current_weather"] = true;
|
||||
params["end_date"] = params["start_date"];
|
||||
break;
|
||||
default:
|
||||
// Failsafe
|
||||
return "";
|
||||
}
|
||||
|
||||
return Object.keys(params)
|
||||
.filter((key) => (params[key] ? true : false))
|
||||
.map((key) => {
|
||||
switch (key) {
|
||||
case "hourly":
|
||||
case "daily":
|
||||
return encodeURIComponent(key) + "=" + params[key].join(",");
|
||||
default:
|
||||
return encodeURIComponent(key) + "=" + encodeURIComponent(params[key]);
|
||||
}
|
||||
})
|
||||
.join("&");
|
||||
},
|
||||
|
||||
// Create a URL from the config and base URL.
|
||||
getUrl() {
|
||||
return `${this.config.apiBase}/forecast?${this.getQueryParameters()}`;
|
||||
},
|
||||
|
||||
// Transpose hourly and daily data matrices
|
||||
transposeDataMatrix(data) {
|
||||
return data.time.map((_, index) =>
|
||||
Object.keys(data).reduce((row, key) => {
|
||||
return {
|
||||
...row,
|
||||
// Parse time values as momentjs instances
|
||||
[key]: ["time", "sunrise", "sunset"].includes(key) ? moment.unix(data[key][index]) : data[key][index]
|
||||
};
|
||||
}, {})
|
||||
);
|
||||
},
|
||||
|
||||
// Sanitize and validate API response
|
||||
parseWeatherApiResponse(data) {
|
||||
const validByType = {
|
||||
current: data.current_weather && data.current_weather.time,
|
||||
hourly: data.hourly && data.hourly.time && Array.isArray(data.hourly.time) && data.hourly.time.length > 0,
|
||||
daily: data.daily && data.daily.time && Array.isArray(data.daily.time) && data.daily.time.length > 0
|
||||
};
|
||||
// backwards compatibility
|
||||
const type = ["daily", "forecast"].includes(this.config.type) ? "daily" : this.config.type;
|
||||
|
||||
if (!validByType[type]) return;
|
||||
|
||||
switch (type) {
|
||||
case "current":
|
||||
if (!validByType.daily && !validByType.hourly) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "hourly":
|
||||
case "daily":
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of ["hourly", "daily"]) {
|
||||
if (typeof data[key] === "object") {
|
||||
data[key] = this.transposeDataMatrix(data[key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.current_weather) {
|
||||
data.current_weather.time = moment.unix(data.current_weather.time);
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
// Reverse geocoding from latitude and longitude provided
|
||||
fetchLocation() {
|
||||
this.fetchData(`${GEOCODE_BASE}?latitude=${this.config.lat}&longitude=${this.config.lon}&localityLanguage=${this.config.lang}`)
|
||||
.then((data) => {
|
||||
if (!data || !data.city) {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
this.fetchedLocationName = `${data.city}, ${data.principalSubdivisionCode}`;
|
||||
})
|
||||
.catch((request) => {
|
||||
Log.error("Could not load data ... ", request);
|
||||
});
|
||||
},
|
||||
|
||||
// Implement WeatherDay generator.
|
||||
generateWeatherDayFromCurrentWeather(weather) {
|
||||
/**
|
||||
* Since some units comes from API response "splitted" into daily, hourly and current_weather
|
||||
* every time you request it, you have to ensure to get the data from the right place every time.
|
||||
* For the current weather case, the response have the following structure (after transposing):
|
||||
* ```
|
||||
* {
|
||||
* current_weather: { ...<some current weather here> },
|
||||
* hourly: [
|
||||
* 0: {...<data for hour zero here> },
|
||||
* 1: {...<data for hour one here> },
|
||||
* ...
|
||||
* ],
|
||||
* daily: [
|
||||
* {...<summary data for current day here> },
|
||||
* ]
|
||||
* }
|
||||
* ```
|
||||
* Some data should be returned from `hourly` array data when the index matches the current hour,
|
||||
* some data from the first and only one object received in `daily` array and some from the
|
||||
* `current_weather` object.
|
||||
*/
|
||||
const h = moment().hour();
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
currentWeather.date = weather.current_weather.time;
|
||||
currentWeather.windSpeed = weather.current_weather.windspeed;
|
||||
currentWeather.windDirection = weather.current_weather.winddirection;
|
||||
currentWeather.sunrise = weather.daily[0].sunrise;
|
||||
currentWeather.sunset = weather.daily[0].sunset;
|
||||
currentWeather.temperature = parseFloat(weather.current_weather.temperature);
|
||||
currentWeather.minTemperature = parseFloat(weather.daily[0].temperature_2m_min);
|
||||
currentWeather.maxTemperature = parseFloat(weather.daily[0].temperature_2m_max);
|
||||
currentWeather.weatherType = this.convertWeatherType(weather.current_weather.weathercode, currentWeather.isDayTime());
|
||||
currentWeather.humidity = parseFloat(weather.hourly[h].relativehumidity_2m);
|
||||
currentWeather.rain = parseFloat(weather.hourly[h].rain);
|
||||
currentWeather.snow = parseFloat(weather.hourly[h].snowfall * 10);
|
||||
currentWeather.precipitation = parseFloat(weather.hourly[h].precipitation);
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
// Implement WeatherForecast generator.
|
||||
generateWeatherObjectsFromForecast(weathers) {
|
||||
const days = [];
|
||||
|
||||
weathers.daily.forEach((weather, i) => {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
|
||||
currentWeather.date = weather.time;
|
||||
currentWeather.windSpeed = weather.windspeed_10m_max;
|
||||
currentWeather.windDirection = weather.winddirection_10m_dominant;
|
||||
currentWeather.sunrise = weather.sunrise;
|
||||
currentWeather.sunset = weather.sunset;
|
||||
currentWeather.temperature = parseFloat((weather.apparent_temperature_max + weather.apparent_temperature_min) / 2);
|
||||
currentWeather.minTemperature = parseFloat(weather.apparent_temperature_min);
|
||||
currentWeather.maxTemperature = parseFloat(weather.apparent_temperature_max);
|
||||
currentWeather.weatherType = this.convertWeatherType(weather.weathercode, currentWeather.isDayTime());
|
||||
currentWeather.rain = parseFloat(weather.rain_sum);
|
||||
currentWeather.snow = parseFloat(weather.snowfall_sum * 10);
|
||||
currentWeather.precipitation = parseFloat(weather.precipitation_sum);
|
||||
|
||||
days.push(currentWeather);
|
||||
});
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
// Implement WeatherHourly generator.
|
||||
generateWeatherObjectsFromHourly(weathers) {
|
||||
const hours = [];
|
||||
const now = moment();
|
||||
|
||||
weathers.hourly.forEach((weather, i) => {
|
||||
if ((hours.length === 0 && weather.time.hour() <= now.hour()) || hours.length >= this.config.maxEntries) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||
const h = Math.ceil((i + 1) / 24) - 1;
|
||||
|
||||
currentWeather.date = weather.time;
|
||||
currentWeather.windSpeed = weather.windspeed_10m;
|
||||
currentWeather.windDirection = weather.winddirection_10m;
|
||||
currentWeather.sunrise = weathers.daily[h].sunrise;
|
||||
currentWeather.sunset = weathers.daily[h].sunset;
|
||||
currentWeather.temperature = parseFloat(weather.apparent_temperature);
|
||||
currentWeather.minTemperature = parseFloat(weathers.daily[h].apparent_temperature_min);
|
||||
currentWeather.maxTemperature = parseFloat(weathers.daily[h].apparent_temperature_max);
|
||||
currentWeather.weatherType = this.convertWeatherType(weather.weathercode, currentWeather.isDayTime());
|
||||
currentWeather.humidity = parseFloat(weather.relativehumidity_2m);
|
||||
currentWeather.rain = parseFloat(weather.rain);
|
||||
currentWeather.snow = parseFloat(weather.snowfall * 10);
|
||||
currentWeather.precipitation = parseFloat(weather.precipitation);
|
||||
|
||||
hours.push(currentWeather);
|
||||
});
|
||||
|
||||
return hours;
|
||||
},
|
||||
|
||||
// Map icons from Dark Sky to our icons.
|
||||
convertWeatherType(weathercode, isDayTime) {
|
||||
const weatherConditions = {
|
||||
0: "clear",
|
||||
1: "mainly-clear",
|
||||
2: "partly-cloudy",
|
||||
3: "overcast",
|
||||
45: "fog",
|
||||
48: "depositing-rime-fog",
|
||||
51: "drizzle-light-intensity",
|
||||
53: "drizzle-moderate-intensity",
|
||||
55: "drizzle-dense-intensity",
|
||||
56: "freezing-drizzle-light-intensity",
|
||||
57: "freezing-drizzle-dense-intensity",
|
||||
61: "rain-slight-intensity",
|
||||
63: "rain-moderate-intensity",
|
||||
65: "rain-heavy-intensity",
|
||||
66: "freezing-rain-light-heavy-intensity",
|
||||
67: "freezing-rain-heavy-intensity",
|
||||
71: "snow-fall-slight-intensity",
|
||||
73: "snow-fall-moderate-intensity",
|
||||
75: "snow-fall-heavy-intensity",
|
||||
77: "snow-grains",
|
||||
80: "rain-showers-slight",
|
||||
81: "rain-showers-moderate",
|
||||
82: "rain-showers-violent",
|
||||
85: "snow-showers-slight",
|
||||
86: "snow-showers-heavy",
|
||||
95: "thunderstorm",
|
||||
96: "thunderstorm-slight-hail",
|
||||
99: "thunderstorm-heavy-hail"
|
||||
};
|
||||
|
||||
if (!Object.keys(weatherConditions).includes(`${weathercode}`)) return null;
|
||||
|
||||
switch (weatherConditions[`${weathercode}`]) {
|
||||
case "clear":
|
||||
return isDayTime ? "day-sunny" : "night-clear";
|
||||
case "mainly-clear":
|
||||
case "partly-cloudy":
|
||||
return isDayTime ? "day-cloudy" : "night-alt-cloudy";
|
||||
case "overcast":
|
||||
return isDayTime ? "day-sunny-overcast" : "night-alt-partly-cloudy";
|
||||
case "fog":
|
||||
case "depositing-rime-fog":
|
||||
return isDayTime ? "day-fog" : "night-fog";
|
||||
case "drizzle-light-intensity":
|
||||
case "rain-slight-intensity":
|
||||
case "rain-showers-slight":
|
||||
return isDayTime ? "day-sprinkle" : "night-sprinkle";
|
||||
case "drizzle-moderate-intensity":
|
||||
case "rain-moderate-intensity":
|
||||
case "rain-showers-moderate":
|
||||
return isDayTime ? "day-showers" : "night-showers";
|
||||
case "drizzle-dense-intensity":
|
||||
case "rain-heavy-intensity":
|
||||
case "rain-showers-violent":
|
||||
return isDayTime ? "day-thunderstorm" : "night-thunderstorm";
|
||||
case "freezing-rain-light-intensity":
|
||||
return isDayTime ? "day-rain-mix" : "night-rain-mix";
|
||||
case "freezing-drizzle-light-intensity":
|
||||
case "freezing-drizzle-dense-intensity":
|
||||
return "snowflake-cold";
|
||||
case "snow-grains":
|
||||
return isDayTime ? "day-sleet" : "night-sleet";
|
||||
case "snow-fall-slight-intensity":
|
||||
case "snow-fall-moderate-intensity":
|
||||
return isDayTime ? "day-snow-wind" : "night-snow-wind";
|
||||
case "snow-fall-heavy-intensity":
|
||||
case "freezing-rain-heavy-intensity":
|
||||
return isDayTime ? "day-snow-thunderstorm" : "night-snow-thunderstorm";
|
||||
case "snow-showers-slight":
|
||||
case "snow-showers-heavy":
|
||||
return isDayTime ? "day-rain-mix" : "night-rain-mix";
|
||||
case "thunderstorm":
|
||||
return isDayTime ? "day-thunderstorm" : "night-thunderstorm";
|
||||
case "thunderstorm-slight-hail":
|
||||
return isDayTime ? "day-sleet" : "night-sleet";
|
||||
case "thunderstorm-heavy-hail":
|
||||
return isDayTime ? "day-sleet-storm" : "night-sleet-storm";
|
||||
default:
|
||||
return "na";
|
||||
}
|
||||
},
|
||||
|
||||
// Define required scripts.
|
||||
getScripts: function () {
|
||||
return ["moment.js"];
|
||||
}
|
||||
});
|
87
modules/default/weather/providers/openweathermap.js
Executable file → Normal file
87
modules/default/weather/providers/openweathermap.js
Executable file → Normal file
@@ -1,6 +1,6 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
@@ -21,7 +21,7 @@ WeatherProvider.register("openweathermap", {
|
||||
weatherEndpoint: "", // can be "onecall", "forecast" or "weather" (for current)
|
||||
locationID: false,
|
||||
location: false,
|
||||
lat: 0, // the onecall endpoint needs lat / lon values, it doesn'T support the locationId
|
||||
lat: 0, // the onecall endpoint needs lat / lon values, it doesn't support the locationId
|
||||
lon: 0,
|
||||
apiKey: ""
|
||||
},
|
||||
@@ -30,14 +30,14 @@ WeatherProvider.register("openweathermap", {
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
let currentWeather;
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
const weatherData = this.generateWeatherObjectsFromOnecall(data);
|
||||
this.setCurrentWeather(weatherData.current);
|
||||
currentWeather = this.generateWeatherObjectsFromOnecall(data).current;
|
||||
this.setFetchedLocation(`${data.timezone}`);
|
||||
} else {
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
||||
}
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
@@ -49,15 +49,17 @@ WeatherProvider.register("openweathermap", {
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
let forecast;
|
||||
let location;
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
const weatherData = this.generateWeatherObjectsFromOnecall(data);
|
||||
this.setWeatherForecast(weatherData.days);
|
||||
this.setFetchedLocation(`${data.timezone}`);
|
||||
forecast = this.generateWeatherObjectsFromOnecall(data).days;
|
||||
location = `${data.timezone}`;
|
||||
} else {
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data.list);
|
||||
this.setWeatherForecast(forecast);
|
||||
this.setFetchedLocation(`${data.city.name}, ${data.city.country}`);
|
||||
forecast = this.generateWeatherObjectsFromForecast(data.list);
|
||||
location = `${data.city.name}, ${data.city.country}`;
|
||||
}
|
||||
this.setWeatherForecast(forecast);
|
||||
this.setFetchedLocation(location);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
@@ -123,19 +125,17 @@ WeatherProvider.register("openweathermap", {
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
currentWeather.date = moment.unix(currentWeatherData.dt);
|
||||
currentWeather.humidity = currentWeatherData.main.humidity;
|
||||
currentWeather.temperature = currentWeatherData.main.temp;
|
||||
if (this.config.windUnits === "metric") {
|
||||
currentWeather.windSpeed = this.config.useKmh ? currentWeatherData.wind.speed * 3.6 : currentWeatherData.wind.speed;
|
||||
} else {
|
||||
currentWeather.windSpeed = currentWeatherData.wind.speed;
|
||||
}
|
||||
currentWeather.feelsLikeTemp = currentWeatherData.main.feels_like;
|
||||
currentWeather.windSpeed = currentWeatherData.wind.speed;
|
||||
currentWeather.windDirection = currentWeatherData.wind.deg;
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon);
|
||||
currentWeather.sunrise = moment(currentWeatherData.sys.sunrise, "X");
|
||||
currentWeather.sunset = moment(currentWeatherData.sys.sunset, "X");
|
||||
currentWeather.sunrise = moment.unix(currentWeatherData.sys.sunrise);
|
||||
currentWeather.sunset = moment.unix(currentWeatherData.sys.sunset);
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
@@ -150,8 +150,7 @@ WeatherProvider.register("openweathermap", {
|
||||
return this.fetchForecastDaily(forecasts);
|
||||
}
|
||||
// if weatherEndpoint does not match forecast or forecast/daily, what should be returned?
|
||||
const days = [new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh)];
|
||||
return days;
|
||||
return [new WeatherObject()];
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -162,8 +161,7 @@ WeatherProvider.register("openweathermap", {
|
||||
return this.fetchOnecall(data);
|
||||
}
|
||||
// if weatherEndpoint does not match onecall, what should be returned?
|
||||
const weatherData = { current: new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh), hours: [], days: [] };
|
||||
return weatherData;
|
||||
return { current: new WeatherObject(), hours: [], days: [] };
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -179,10 +177,10 @@ WeatherProvider.register("openweathermap", {
|
||||
let snow = 0;
|
||||
// variable for date
|
||||
let date = "";
|
||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
let weather = new WeatherObject();
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
if (date !== moment(forecast.dt, "X").format("YYYY-MM-DD")) {
|
||||
if (date !== moment.unix(forecast.dt).format("YYYY-MM-DD")) {
|
||||
// calculate minimum/maximum temperature, specify rain amount
|
||||
weather.minTemperature = Math.min.apply(null, minTemp);
|
||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||
@@ -192,7 +190,7 @@ WeatherProvider.register("openweathermap", {
|
||||
// push weather information to days array
|
||||
days.push(weather);
|
||||
// create new weather-object
|
||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
weather = new WeatherObject();
|
||||
|
||||
minTemp = [];
|
||||
maxTemp = [];
|
||||
@@ -200,16 +198,16 @@ WeatherProvider.register("openweathermap", {
|
||||
snow = 0;
|
||||
|
||||
// set new date
|
||||
date = moment(forecast.dt, "X").format("YYYY-MM-DD");
|
||||
date = moment.unix(forecast.dt).format("YYYY-MM-DD");
|
||||
|
||||
// specify date
|
||||
weather.date = moment(forecast.dt, "X");
|
||||
weather.date = moment.unix(forecast.dt);
|
||||
|
||||
// If the first value of today is later than 17:00, we have an icon at least!
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||
}
|
||||
|
||||
if (moment(forecast.dt, "X").format("H") >= 8 && moment(forecast.dt, "X").format("H") <= 17) {
|
||||
if (moment.unix(forecast.dt).format("H") >= 8 && moment.unix(forecast.dt).format("H") <= 17) {
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||
}
|
||||
|
||||
@@ -255,9 +253,9 @@ WeatherProvider.register("openweathermap", {
|
||||
const days = [];
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
const weather = new WeatherObject();
|
||||
|
||||
weather.date = moment(forecast.dt, "X");
|
||||
weather.date = moment.unix(forecast.dt);
|
||||
weather.minTemperature = forecast.temp.min;
|
||||
weather.maxTemperature = forecast.temp.max;
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||
@@ -301,13 +299,13 @@ WeatherProvider.register("openweathermap", {
|
||||
let precip = false;
|
||||
|
||||
// get current weather, if requested
|
||||
const current = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
const current = new WeatherObject();
|
||||
if (data.hasOwnProperty("current")) {
|
||||
current.date = moment(data.current.dt, "X").utcOffset(data.timezone_offset / 60);
|
||||
current.date = moment.unix(data.current.dt).utcOffset(data.timezone_offset / 60);
|
||||
current.windSpeed = data.current.wind_speed;
|
||||
current.windDirection = data.current.wind_deg;
|
||||
current.sunrise = moment(data.current.sunrise, "X").utcOffset(data.timezone_offset / 60);
|
||||
current.sunset = moment(data.current.sunset, "X").utcOffset(data.timezone_offset / 60);
|
||||
current.sunrise = moment.unix(data.current.sunrise).utcOffset(data.timezone_offset / 60);
|
||||
current.sunset = moment.unix(data.current.sunset).utcOffset(data.timezone_offset / 60);
|
||||
current.temperature = data.current.temp;
|
||||
current.weatherType = this.convertWeatherType(data.current.weather[0].icon);
|
||||
current.humidity = data.current.humidity;
|
||||
@@ -333,14 +331,13 @@ WeatherProvider.register("openweathermap", {
|
||||
current.feelsLikeTemp = data.current.feels_like;
|
||||
}
|
||||
|
||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
let weather = new WeatherObject();
|
||||
|
||||
// get hourly weather, if requested
|
||||
const hours = [];
|
||||
if (data.hasOwnProperty("hourly")) {
|
||||
for (const hour of data.hourly) {
|
||||
weather.date = moment(hour.dt, "X").utcOffset(data.timezone_offset / 60);
|
||||
// weather.date = moment(hour.dt, "X").utcOffset(data.timezone_offset/60).format(onecallDailyFormat+","+onecallHourlyFormat);
|
||||
weather.date = moment.unix(hour.dt).utcOffset(data.timezone_offset / 60);
|
||||
weather.temperature = hour.temp;
|
||||
weather.feelsLikeTemp = hour.feels_like;
|
||||
weather.humidity = hour.humidity;
|
||||
@@ -369,7 +366,7 @@ WeatherProvider.register("openweathermap", {
|
||||
}
|
||||
|
||||
hours.push(weather);
|
||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
weather = new WeatherObject();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,9 +374,9 @@ WeatherProvider.register("openweathermap", {
|
||||
const days = [];
|
||||
if (data.hasOwnProperty("daily")) {
|
||||
for (const day of data.daily) {
|
||||
weather.date = moment(day.dt, "X").utcOffset(data.timezone_offset / 60);
|
||||
weather.sunrise = moment(day.sunrise, "X").utcOffset(data.timezone_offset / 60);
|
||||
weather.sunset = moment(day.sunset, "X").utcOffset(data.timezone_offset / 60);
|
||||
weather.date = moment.unix(day.dt).utcOffset(data.timezone_offset / 60);
|
||||
weather.sunrise = moment.unix(day.sunrise).utcOffset(data.timezone_offset / 60);
|
||||
weather.sunset = moment.unix(day.sunset).utcOffset(data.timezone_offset / 60);
|
||||
weather.minTemperature = day.temp.min;
|
||||
weather.maxTemperature = day.temp.max;
|
||||
weather.humidity = day.humidity;
|
||||
@@ -408,7 +405,7 @@ WeatherProvider.register("openweathermap", {
|
||||
}
|
||||
|
||||
days.push(weather);
|
||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
weather = new WeatherObject();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,7 +474,7 @@ WeatherProvider.register("openweathermap", {
|
||||
return;
|
||||
}
|
||||
|
||||
params += "&units=" + this.config.units;
|
||||
params += "&units=metric"; // WeatherProviders should use metric internally and use the units only for when displaying data
|
||||
params += "&lang=" + this.config.lang;
|
||||
params += "&APPID=" + this.config.apiKey;
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: SMHI
|
||||
*
|
||||
@@ -15,21 +15,22 @@ WeatherProvider.register("smhi", {
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
precipitationValue: "pmedian"
|
||||
lat: 0, // Cant have more than 6 digits
|
||||
lon: 0, // Cant have more than 6 digits
|
||||
precipitationValue: "pmedian",
|
||||
location: false
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements method in interface for fetching current weather
|
||||
* Implements method in interface for fetching current weather.
|
||||
*/
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getURL())
|
||||
.then((data) => {
|
||||
let closest = this.getClosestToCurrentTime(data.timeSeries);
|
||||
let coordinates = this.resolveCoordinates(data);
|
||||
let weatherObject = this.convertWeatherDataToObject(closest, coordinates);
|
||||
this.setFetchedLocation(`(${coordinates.lat},${coordinates.lon})`);
|
||||
const closest = this.getClosestToCurrentTime(data.timeSeries);
|
||||
const coordinates = this.resolveCoordinates(data);
|
||||
const weatherObject = this.convertWeatherDataToObject(closest, coordinates);
|
||||
this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`);
|
||||
this.setCurrentWeather(weatherObject);
|
||||
})
|
||||
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||
@@ -37,21 +38,35 @@ WeatherProvider.register("smhi", {
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements method in interface for fetching a forecast.
|
||||
* Handling hourly forecast would be easy as not grouping by day but it seems really specific for one weather provider for now.
|
||||
* Implements method in interface for fetching a multi-day forecast.
|
||||
*/
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getURL())
|
||||
.then((data) => {
|
||||
let coordinates = this.resolveCoordinates(data);
|
||||
let weatherObjects = this.convertWeatherDataGroupedByDay(data.timeSeries, coordinates);
|
||||
this.setFetchedLocation(`(${coordinates.lat},${coordinates.lon})`);
|
||||
const coordinates = this.resolveCoordinates(data);
|
||||
const weatherObjects = this.convertWeatherDataGroupedBy(data.timeSeries, coordinates);
|
||||
this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`);
|
||||
this.setWeatherForecast(weatherObjects);
|
||||
})
|
||||
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements method in interface for fetching hourly forecasts.
|
||||
*/
|
||||
fetchWeatherHourly() {
|
||||
this.fetchData(this.getURL())
|
||||
.then((data) => {
|
||||
const coordinates = this.resolveCoordinates(data);
|
||||
const weatherObjects = this.convertWeatherDataGroupedBy(data.timeSeries, coordinates, "hour");
|
||||
this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`);
|
||||
this.setWeatherHourly(weatherObjects);
|
||||
})
|
||||
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides method for setting config with checks for the precipitationValue being unset or invalid
|
||||
*
|
||||
@@ -60,7 +75,7 @@ WeatherProvider.register("smhi", {
|
||||
setConfig(config) {
|
||||
this.config = config;
|
||||
if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) === -1) {
|
||||
console.log("invalid or not set: " + config.precipitationValue);
|
||||
Log.log("invalid or not set: " + config.precipitationValue);
|
||||
config.precipitationValue = this.defaults.precipitationValue;
|
||||
}
|
||||
},
|
||||
@@ -89,11 +104,30 @@ WeatherProvider.register("smhi", {
|
||||
* @returns {string} the url for the specified coordinates
|
||||
*/
|
||||
getURL() {
|
||||
let lon = this.config.lon;
|
||||
let lat = this.config.lat;
|
||||
const formatter = new Intl.NumberFormat("en-US", {
|
||||
minimumFractionDigits: 6,
|
||||
maximumFractionDigits: 6
|
||||
});
|
||||
const lon = formatter.format(this.config.lon);
|
||||
const lat = formatter.format(this.config.lat);
|
||||
return `https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/${lon}/lat/${lat}/data.json`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculates the apparent temperature based on known atmospheric data.
|
||||
*
|
||||
* @param {object} weatherData Weatherdata to use for the calculation
|
||||
* @returns {number} The apparent temperature
|
||||
*/
|
||||
calculateApparentTemperature(weatherData) {
|
||||
const Ta = this.paramValue(weatherData, "t");
|
||||
const rh = this.paramValue(weatherData, "r");
|
||||
const ws = this.paramValue(weatherData, "ws");
|
||||
const p = (rh / 100) * 6.105 * Math.E * ((17.27 * Ta) / (237.7 + Ta));
|
||||
|
||||
return Ta + 0.33 * p - 0.7 * ws - 4;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts the returned data into a WeatherObject with required properties set for both current weather and forecast.
|
||||
* The returned units is always in metric system.
|
||||
@@ -104,8 +138,7 @@ WeatherProvider.register("smhi", {
|
||||
* @returns {WeatherObject} The converted weatherdata at the specified location
|
||||
*/
|
||||
convertWeatherDataToObject(weatherData, coordinates) {
|
||||
// Weather data is only for Sweden and nobody in Sweden would use imperial
|
||||
let currentWeather = new WeatherObject("metric", "metric", "metric");
|
||||
let currentWeather = new WeatherObject();
|
||||
|
||||
currentWeather.date = moment(weatherData.validTime);
|
||||
currentWeather.updateSunTime(coordinates.lat, coordinates.lon);
|
||||
@@ -114,6 +147,7 @@ WeatherProvider.register("smhi", {
|
||||
currentWeather.windSpeed = this.paramValue(weatherData, "ws");
|
||||
currentWeather.windDirection = this.paramValue(weatherData, "wd");
|
||||
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), currentWeather.isDayTime());
|
||||
currentWeather.feelsLikeTemp = this.calculateApparentTemperature(weatherData);
|
||||
|
||||
// Determine the precipitation amount and category and update the
|
||||
// weatherObject with it, the valuetype to use can be configured or uses
|
||||
@@ -143,13 +177,14 @@ WeatherProvider.register("smhi", {
|
||||
},
|
||||
|
||||
/**
|
||||
* Takes all of the data points and converts it to one WeatherObject per day.
|
||||
* Takes all the data points and converts it to one WeatherObject per day.
|
||||
*
|
||||
* @param {object[]} allWeatherData Array of weatherdata
|
||||
* @param {object} coordinates Coordinates of the locations of the weather
|
||||
* @param {string} groupBy The interval to use for grouping the data (day, hour)
|
||||
* @returns {WeatherObject[]} Array of weatherobjects
|
||||
*/
|
||||
convertWeatherDataGroupedByDay(allWeatherData, coordinates) {
|
||||
convertWeatherDataGroupedBy(allWeatherData, coordinates, groupBy = "day") {
|
||||
let currentWeather;
|
||||
let result = [];
|
||||
|
||||
@@ -157,10 +192,11 @@ WeatherProvider.register("smhi", {
|
||||
let dayWeatherTypes = [];
|
||||
|
||||
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);
|
||||
//If its the first object or if a day/hour change we need to reset the summary object
|
||||
if (!currentWeather || !currentWeather.date.isSame(weatherObject.date, groupBy)) {
|
||||
currentWeather = new WeatherObject();
|
||||
dayWeatherTypes = [];
|
||||
currentWeather.temperature = weatherObject.temperature;
|
||||
currentWeather.date = weatherObject.date;
|
||||
currentWeather.minTemperature = Infinity;
|
||||
currentWeather.maxTemperature = -Infinity;
|
||||
@@ -170,7 +206,7 @@ WeatherProvider.register("smhi", {
|
||||
result.push(currentWeather);
|
||||
}
|
||||
|
||||
//Keep track of what icons has been used for each hour of daytime and use the middle one for the forecast
|
||||
//Keep track of what icons have been used for each hour of daytime and use the middle one for the forecast
|
||||
if (weatherObject.isDayTime()) {
|
||||
dayWeatherTypes.push(weatherObject.weatherType);
|
||||
}
|
||||
@@ -237,8 +273,8 @@ WeatherProvider.register("smhi", {
|
||||
},
|
||||
|
||||
/**
|
||||
* Map the icon value from SMHI to an icon that MagicMirror understands.
|
||||
* Uses different icons depending if its daytime or nighttime.
|
||||
* Map the icon value from SMHI to an icon that MagicMirror² understands.
|
||||
* Uses different icons depending on if its daytime or nighttime.
|
||||
* SMHI's description of what the numeric value means is the comment after the case.
|
||||
*
|
||||
* @param {number} input The SMHI icon value
|
||||
|
65
modules/default/weather/providers/ukmetoffice.js
Executable file → Normal file
65
modules/default/weather/providers/ukmetoffice.js
Executable file → Normal file
@@ -1,6 +1,6 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||
|
||||
/* Magic Mirror
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
*
|
||||
* By Malcolm Oakes https://github.com/maloakes
|
||||
@@ -21,11 +21,6 @@ WeatherProvider.register("ukmetoffice", {
|
||||
apiKey: ""
|
||||
},
|
||||
|
||||
units: {
|
||||
imperial: "us",
|
||||
metric: "si"
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl("3hourly"))
|
||||
@@ -80,7 +75,7 @@ WeatherProvider.register("ukmetoffice", {
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||
const currentWeather = new WeatherObject();
|
||||
const location = currentWeatherData.SiteRep.DV.Location;
|
||||
|
||||
// data times are always UTC
|
||||
@@ -103,11 +98,11 @@ WeatherProvider.register("ukmetoffice", {
|
||||
if (timeInMins >= p && timeInMins - 180 < p) {
|
||||
// finally got the one we want, so populate weather object
|
||||
currentWeather.humidity = rep.H;
|
||||
currentWeather.temperature = this.convertTemp(rep.T);
|
||||
currentWeather.feelsLikeTemp = this.convertTemp(rep.F);
|
||||
currentWeather.temperature = rep.T;
|
||||
currentWeather.feelsLikeTemp = rep.F;
|
||||
currentWeather.precipitation = parseInt(rep.Pp);
|
||||
currentWeather.windSpeed = this.convertWindSpeed(rep.S);
|
||||
currentWeather.windDirection = this.convertWindDirection(rep.D);
|
||||
currentWeather.windSpeed = WeatherUtils.convertWindToMetric(rep.S);
|
||||
currentWeather.windDirection = WeatherUtils.convertWindDirection(rep.D);
|
||||
currentWeather.weatherType = this.convertWeatherType(rep.W);
|
||||
}
|
||||
}
|
||||
@@ -130,7 +125,7 @@ WeatherProvider.register("ukmetoffice", {
|
||||
// loop round the (5) periods getting the data
|
||||
// for each period array, Day is [0], Night is [1]
|
||||
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);
|
||||
const weather = new WeatherObject();
|
||||
|
||||
// data times are always UTC
|
||||
const dateStr = period.value;
|
||||
@@ -140,8 +135,8 @@ WeatherProvider.register("ukmetoffice", {
|
||||
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(period.Rep[1].Nm);
|
||||
weather.maxTemperature = this.convertTemp(period.Rep[0].Dm);
|
||||
weather.minTemperature = period.Rep[1].Nm;
|
||||
weather.maxTemperature = period.Rep[0].Dm;
|
||||
weather.weatherType = this.convertWeatherType(period.Rep[0].W);
|
||||
weather.precipitation = parseInt(period.Rep[0].PPd);
|
||||
|
||||
@@ -192,46 +187,6 @@ WeatherProvider.register("ukmetoffice", {
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert temp (from degrees C) if required
|
||||
*/
|
||||
convertTemp(tempInC) {
|
||||
return this.tempUnits === "imperial" ? (tempInC * 9) / 5 + 32 : tempInC;
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert wind speed (from mph to m/s or km/h) if required
|
||||
*/
|
||||
convertWindSpeed(windInMph) {
|
||||
return this.windUnits === "metric" ? (this.useKmh ? windInMph * 1.60934 : windInMph / 2.23694) : windInMph;
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert the wind direction cardinal to value
|
||||
*/
|
||||
convertWindDirection(windDirection) {
|
||||
const windCardinals = {
|
||||
N: 0,
|
||||
NNE: 22,
|
||||
NE: 45,
|
||||
ENE: 67,
|
||||
E: 90,
|
||||
ESE: 112,
|
||||
SE: 135,
|
||||
SSE: 157,
|
||||
S: 180,
|
||||
SSW: 202,
|
||||
SW: 225,
|
||||
WSW: 247,
|
||||
W: 270,
|
||||
WNW: 292,
|
||||
NW: 315,
|
||||
NNW: 337
|
||||
};
|
||||
|
||||
return windCardinals.hasOwnProperty(windDirection) ? windCardinals[windDirection] : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates an url with api parameters based on the config.
|
||||
*
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user