mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-08-16 19:05:44 +00:00
Compare commits
214 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
53fc814ff8 | ||
|
5ea8a3469a | ||
|
118e21238c | ||
|
8c0e7db494 | ||
|
d20d9a7ef8 | ||
|
b8e0e2a791 | ||
|
a927eb20d9 | ||
|
aad3eefc62 | ||
|
ee1960ced0 | ||
|
0b70274a1a | ||
|
4e7b68a69d | ||
|
786ea86e0e | ||
|
d397568062 | ||
|
a7af76b619 | ||
|
319a921f75 | ||
|
d5406f4900 | ||
|
55cd03576f | ||
|
9d97724401 | ||
|
74854387cd | ||
|
e77f10b86d | ||
|
6ffdc7b55b | ||
|
7098f1e41f | ||
|
679a413788 | ||
|
247115d2e4 | ||
|
70ddd80632 | ||
|
203e8647d4 | ||
|
3fe5ad4b3d | ||
|
b300191609 | ||
|
296df06c21 | ||
|
fe882bf92a | ||
|
2a6e2aacdc | ||
|
3a01acd389 | ||
|
a8d06ae74e | ||
|
04f0df269a | ||
|
f80889d953 | ||
|
6815dfa02b | ||
|
bbc27f5ae2 | ||
|
f46b226940 | ||
|
764ca3ac5c | ||
|
0e2da630d5 | ||
|
a0b444d6c4 | ||
|
5d2ddbd3dd | ||
|
b067711ede | ||
|
66b29ec26e | ||
|
343e7de7bd | ||
|
6ea94e4512 | ||
|
290b350856 | ||
|
9566d6c9a0 | ||
|
6b204cda25 | ||
|
e530c783f8 | ||
|
a3c2e7b816 | ||
|
ad665a7a33 | ||
|
95ec3096e0 | ||
|
a67a0b677c | ||
|
8b1c279c07 | ||
|
4eccce3f77 | ||
|
af0fe37f70 | ||
|
e5adbea49c | ||
|
7127979c6f | ||
|
fa7c7fc8cf | ||
|
91fd931a58 | ||
|
7a1591b2d6 | ||
|
f2957f90df | ||
|
ffdf321e23 | ||
|
79e99e18ea | ||
|
a92b3d3f71 | ||
|
5cbdd28db3 | ||
|
9d49196e69 | ||
|
ef20fe2d11 | ||
|
c0a5f35a00 | ||
|
2ad463b6c7 | ||
|
200db181d5 | ||
|
7ba96aeb98 | ||
|
7c64d8fce6 | ||
|
7dcea98e99 | ||
|
59e9d765e2 | ||
|
156db32c76 | ||
|
58cdfa3cb1 | ||
|
49c72d8dfc | ||
|
1bd146f52e | ||
|
948910d304 | ||
|
0b97639341 | ||
|
f802c85a38 | ||
|
62eb23ba6a | ||
|
4b0e0aa48f | ||
|
e9f1bd9d7a | ||
|
46bca1bc6d | ||
|
2b6720e6e5 | ||
|
ea818bf899 | ||
|
0e00e64493 | ||
|
3c35d346ee | ||
|
675e4d4f67 | ||
|
c1850f2577 | ||
|
e985e99036 | ||
|
b7371538bc | ||
|
a56b92990d | ||
|
c7405b76b3 | ||
|
eceec8285d | ||
|
0573d6e772 | ||
|
babd22b04f | ||
|
432d900ecd | ||
|
83315f1fed | ||
|
e09d60d1d1 | ||
|
d832d795df | ||
|
a41aa48dd1 | ||
|
b80485b52f | ||
|
7e58b38ddf | ||
|
979f4ec664 | ||
|
4e3369062e | ||
|
77f9c86774 | ||
|
dee3cd3da7 | ||
|
09f117c3d9 | ||
|
32192d1698 | ||
|
2c7beeaaaf | ||
|
0d3ad9812c | ||
|
b7eb21e48f | ||
|
9703226c73 | ||
|
cc11b77f24 | ||
|
c5a8b85f4e | ||
|
fa40a3e8e8 | ||
|
6223584392 | ||
|
b5a22bc09b | ||
|
4ef030af5f | ||
|
5f38c53260 | ||
|
d5395ee3f8 | ||
|
ab0876f07a | ||
|
d276a7ddb9 | ||
|
8f8945d418 | ||
|
6d779235cf | ||
|
beea754514 | ||
|
c6db22524a | ||
|
23ee155ded | ||
|
1b2785cc56 | ||
|
b5b61246e6 | ||
|
498b440174 | ||
|
fe0b915a5d | ||
|
2b792cdbb8 | ||
|
a23769156e | ||
|
6d86ffade4 | ||
|
390e5d6490 | ||
|
b08a4737af | ||
|
bf28e63709 | ||
|
fb22a76796 | ||
|
81244d961e | ||
|
65aa1b0ddc | ||
|
88c7e42368 | ||
|
e24dfa6b1a | ||
|
a65ee86501 | ||
|
4b478a5a5e | ||
|
1dc0a0d5b5 | ||
|
bf279d9a57 | ||
|
42d42ef452 | ||
|
ed90f0546f | ||
|
a8dc563a31 | ||
|
58b9ddcd9f | ||
|
7198ae5eae | ||
|
f6dcfb5ca3 | ||
|
157e74ce7c | ||
|
67e4dbaacd | ||
|
2e2962d492 | ||
|
cd4ba428da | ||
|
ee8695637b | ||
|
4244c05764 | ||
|
c714399b4d | ||
|
8d9f132666 | ||
|
d2327d3d6f | ||
|
29e3ec06cb | ||
|
877f8ad380 | ||
|
6e80e5a295 | ||
|
7bc91a742f | ||
|
fc303146a5 | ||
|
2908c15ea6 | ||
|
a975b44fbb | ||
|
4fc38bd5bb | ||
|
c99f660d98 | ||
|
cd739b6912 | ||
|
0ebedd0fb8 | ||
|
e9be668d1b | ||
|
76d9042e60 | ||
|
2fec314ff5 | ||
|
3124b0a9c5 | ||
|
a2624442cc | ||
|
eee289aee8 | ||
|
abbae90a8f | ||
|
bd0b3c00ad | ||
|
b9b7d2c95d | ||
|
0b01e9dbe0 | ||
|
4fecffc3df | ||
|
4d47c0837f | ||
|
3879949f58 | ||
|
f25abfd2f8 | ||
|
7058fc5fd8 | ||
|
00bc6eb28c | ||
|
f79d3f007d | ||
|
c191ff0032 | ||
|
dde88601a6 | ||
|
2d3940a4ff | ||
|
64ed5a54cb | ||
|
7bd944391e | ||
|
ad4dbd786a | ||
|
fc59ed20e3 | ||
|
835c893205 | ||
|
7bbf8c19db | ||
|
1eb2965b2b | ||
|
85a9f14178 | ||
|
a328ce537f | ||
|
21ae79b386 | ||
|
d5e855dd6d | ||
|
a86e27a12c | ||
|
f434be3d44 | ||
|
ce4906d13b | ||
|
8212d30c4c | ||
|
f04d578704 | ||
|
7694d6fa86 |
@@ -1 +1,2 @@
|
||||
modules/default/calendar/vendor/*
|
||||
modules/*
|
||||
!modules/default/
|
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"],
|
||||
"plugins": ["prettier", "import", "jsdoc", "jest"],
|
||||
"extends": ["eslint:recommended", "plugin:@stylistic/all-extends", "plugin:import/recommended", "plugin:jest/recommended", "plugin:jsdoc/recommended"],
|
||||
"plugins": ["unicorn"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2022": true,
|
||||
"es2024": true,
|
||||
"jest/globals": true,
|
||||
"node": true
|
||||
},
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2022,
|
||||
"ecmaVersion": "latest",
|
||||
"ecmaFeatures": {
|
||||
"globalReturn": true
|
||||
}
|
||||
@@ -24,11 +24,70 @@
|
||||
"rules": {
|
||||
"eqeqeq": "error",
|
||||
"import/order": "error",
|
||||
"import/extensions": "error",
|
||||
"import/newline-after-import": "error",
|
||||
"jest/consistent-test-it": "warn",
|
||||
"jest/expect-expect": "warn",
|
||||
"jest/no-done-callback": "warn",
|
||||
"jest/prefer-expect-resolves": "warn",
|
||||
"jest/prefer-mock-promise-shorthand": "warn",
|
||||
"jest/prefer-to-be": "warn",
|
||||
"jest/prefer-to-have-length": "warn",
|
||||
"no-param-reassign": "error",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-throw-literal": "error",
|
||||
"no-unused-vars": "off",
|
||||
"no-useless-return": "error",
|
||||
"prefer-template": "error"
|
||||
}
|
||||
"object-shorthand": ["error", "methods"],
|
||||
"prefer-template": "error",
|
||||
"@stylistic/array-element-newline": ["error", "consistent"],
|
||||
"@stylistic/arrow-parens": ["error", "always"],
|
||||
"@stylistic/brace-style": "off",
|
||||
"@stylistic/comma-dangle": ["error", "never"],
|
||||
"@stylistic/dot-location": ["error", "property"],
|
||||
"@stylistic/function-call-argument-newline": ["error", "consistent"],
|
||||
"@stylistic/function-paren-newline": ["error", "consistent"],
|
||||
"@stylistic/implicit-arrow-linebreak": ["error", "beside"],
|
||||
"@stylistic/max-statements-per-line": ["error", { "max": 2 }],
|
||||
"@stylistic/multiline-ternary": ["error", "always-multiline"],
|
||||
"@stylistic/newline-per-chained-call": ["error", { "ignoreChainWithDepth": 4 }],
|
||||
"@stylistic/no-extra-parens": "off",
|
||||
"@stylistic/no-tabs": "off",
|
||||
"@stylistic/object-curly-spacing": ["error", "always"],
|
||||
"@stylistic/object-property-newline": ["error", { "allowAllPropertiesOnSameLine": true }],
|
||||
"@stylistic/operator-linebreak": ["error", "before"],
|
||||
"@stylistic/padded-blocks": "off",
|
||||
"@stylistic/quote-props": ["error", "as-needed"],
|
||||
"@stylistic/quotes": ["error", "double"],
|
||||
"@stylistic/indent": ["error", "tab"],
|
||||
"@stylistic/semi": ["error", "always"],
|
||||
"@stylistic/space-before-function-paren": ["error", "always"],
|
||||
"@stylistic/spaced-comment": "off",
|
||||
"unicorn/prefer-node-protocol": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["config/config.js*"],
|
||||
"rules": {
|
||||
"@stylistic/comma-dangle": "off",
|
||||
"@stylistic/indent": "off",
|
||||
"@stylistic/no-multi-spaces": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/configs/modules/weather/*.js"],
|
||||
"rules": {
|
||||
"@stylistic/quotes": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"extends": ["plugin:package-json/recommended"],
|
||||
"files": ["package.json"],
|
||||
"parser": "jsonc-eslint-parser",
|
||||
"plugins": ["package-json"],
|
||||
"rules": {
|
||||
"package-json/sort-collections": ["error", ["devDependencies", "dependencies", "peerDependencies", "config"]]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -43,7 +43,7 @@ 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 16 or later (recommended is 18).
|
||||
**Node Version**: Make sure it's version 18 or later (recommended is 20).
|
||||
|
||||
**MagicMirror² Version**: Please let us know which version of MagicMirror² you are running. It can be found in the `package.json` file.
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
Hello and thank you for opening an issue.
|
||||
|
||||
**Please make sure that you have read the following lines before submitting your Issue:**
|
||||
**⚠️ Please make sure that you have read the following lines before submitting your Issue:**
|
||||
|
||||
## I'm not sure if this is a bug
|
||||
|
||||
@@ -22,6 +22,10 @@ If you are facing an issue or found a bug while trying to install 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)
|
||||
|
||||
## I'm having troubles installing or configuring foreign modules
|
||||
|
||||
Please open an issue in the module repository or ask for help in the [forum](https://forum.magicmirror.builders/)
|
||||
|
||||
---
|
||||
|
||||
## I found a bug in MagicMirror
|
||||
@@ -31,7 +35,7 @@ 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 16 or later (recommended is 18).
|
||||
**Node Version**: Make sure it's version 18 or later (recommended is 20).
|
||||
|
||||
**MagicMirror² Version**: Please let us know which version of MagicMirror² you are running. It can be found in the `package.json` file.
|
||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -3,7 +3,6 @@ Hello and thank you for wanting to contribute to the MagicMirror² project
|
||||
**Please make sure that you have followed these 4 rules before submitting your Pull Request:**
|
||||
|
||||
> 1. Base your pull requests against the `develop` branch.
|
||||
>
|
||||
> 2. Include these infos in the description:
|
||||
>
|
||||
> - Does the pull request solve a **related** issue?
|
||||
@@ -13,7 +12,6 @@ Hello and thank you for wanting to contribute to the MagicMirror² project
|
||||
>
|
||||
> 3. Please run `npm run lint:prettier` before submitting so that
|
||||
> style issues are fixed.
|
||||
>
|
||||
> 4. Don't forget to add an entry about your changes to
|
||||
> the CHANGELOG.md file.
|
||||
|
||||
|
10
.github/codecov.yaml
vendored
10
.github/codecov.yaml
vendored
@@ -1,10 +0,0 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# advanced settings
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
threshold: 0%
|
||||
target: 0
|
33
.github/dependabot.yaml
vendored
33
.github/dependabot.yaml
vendored
@@ -5,3 +5,36 @@ updates:
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "develop"
|
||||
labels:
|
||||
- "Skip Changelog"
|
||||
- "dependencies"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
target-branch: "develop"
|
||||
labels:
|
||||
- "Skip Changelog"
|
||||
- "dependencies"
|
||||
- "javascript"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/vendor"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
target-branch: "develop"
|
||||
labels:
|
||||
- "Skip Changelog"
|
||||
- "dependencies"
|
||||
- "javascript"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fonts"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
target-branch: "develop"
|
||||
labels:
|
||||
- "Skip Changelog"
|
||||
- "dependencies"
|
||||
- "javascript"
|
||||
|
7
.github/workflows/automated-tests.yaml
vendored
7
.github/workflows/automated-tests.yaml
vendored
@@ -18,14 +18,15 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x, 18.x, 20.x]
|
||||
node-version: [20.x, 22.x]
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: "Use Node.js ${{ matrix.node-version }}"
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
check-latest: true
|
||||
cache: "npm"
|
||||
- name: "Install dependencies"
|
||||
run: |
|
||||
|
35
.github/workflows/codecov-test-suites.yaml
vendored
35
.github/workflows/codecov-test-suites.yaml
vendored
@@ -1,35 +0,0 @@
|
||||
# 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"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, develop]
|
||||
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@v3
|
||||
- name: "Install dependencies"
|
||||
run: |
|
||||
npm ci
|
||||
- name: "Run coverage"
|
||||
run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
touch css/custom.css
|
||||
npm run test:coverage
|
||||
- name: "Upload coverage results to codecov"
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./coverage/lcov.info
|
||||
fail_ci_if_error: true
|
4
.github/workflows/depsreview.yaml
vendored
4
.github/workflows/depsreview.yaml
vendored
@@ -13,6 +13,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
||||
|
28
.github/workflows/electronRebuild.yaml
vendored
Normal file
28
.github/workflows/electronRebuild.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: "Electron Rebuild Testing"
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
rebuild:
|
||||
name: Run electron-rebuild
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x, 22.x]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: "Use Node.js ${{ matrix.node-version }}"
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
check-latest: true
|
||||
- name: Install MagicMirror
|
||||
run: npm run install-mm
|
||||
- name: Install @electron/rebuild
|
||||
run: npm install @electron/rebuild
|
||||
- name: Install some test library to be rebuilded
|
||||
run: npm install onoff node-pty drivelist
|
||||
- name: Run electron-rebuild
|
||||
run: npx electron-rebuild
|
||||
continue-on-error: false
|
@@ -19,7 +19,7 @@ jobs:
|
||||
changeLogPath: "CHANGELOG.md"
|
||||
skipLabels: "Skip Changelog"
|
||||
- name: "Enforce develop branch"
|
||||
if: ${{ github.base_ref == 'master' && !contains(github.event.pull_request.labels.*.name, 'mastermerge') }}
|
||||
if: ${{ github.event.pull_request.base.ref == 'master' && !contains(github.event.pull_request.labels.*.name, 'mastermerge') }}
|
||||
run: |
|
||||
echo "This PR is based against the master branch and not a release or hotfix."
|
||||
echo "Please don't do this. Switch the branch to 'develop'."
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -63,8 +63,10 @@ Temporary Items
|
||||
!/modules/default/**
|
||||
!/modules/README.md**
|
||||
|
||||
# Ignore changes to the custom css files.
|
||||
/css/custom.css
|
||||
# Ignore changes to the custom css files but keep the sample and main.
|
||||
/css/*
|
||||
!/css/custom.css.sample
|
||||
!/css/main.css
|
||||
|
||||
# Ignore users config file but keep the sample.
|
||||
/config/*
|
||||
|
@@ -1,7 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ -f "$(dirname "$0")/_/husky.sh" ] && . "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
if command -v npm &> /dev/null; then
|
||||
npm run lint:staged
|
||||
fi
|
||||
|
@@ -1,3 +1,8 @@
|
||||
*.js
|
||||
.eslintignore
|
||||
.husky/pre-commit
|
||||
.prettierignore
|
||||
/config
|
||||
/coverage
|
||||
package-lock.json
|
||||
**.ics
|
||||
|
335
CHANGELOG.md
335
CHANGELOG.md
@@ -3,7 +3,202 @@
|
||||
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.28.0] - 2024-07-01
|
||||
|
||||
Thanks to: @btoconnor, @bugsounet, @JasonStieber, @khassel, @kleinmantara and @WallysWellies.
|
||||
|
||||
> ⚠️ This release needs nodejs version >= v20
|
||||
|
||||
### Added
|
||||
|
||||
- [calendar] Added config option "showEndsOnlyWithDuration" for default calendar
|
||||
- [compliments] Added `specialDayUnique` config option, defaults to `false` (#3465)
|
||||
- [weather] Provider weathergov: Use `precipitationLast3Hours` if `precipitationLastHour` is `null` (#3124)
|
||||
|
||||
### Removed
|
||||
|
||||
- [tests] delete node v18 support (#3462)
|
||||
|
||||
### Updated
|
||||
|
||||
- [core] Update dependencies including electron to v31
|
||||
- [core] use node >= v20 (#3462)
|
||||
- [core] Update `config.js.sample` to use openmeteo as weather provider which needs no api key
|
||||
- [tests] Use latest@version of node for `automated-tests.yaml` (#3483)
|
||||
- [updatenotification] Avoid using pm2 when running in docker container
|
||||
|
||||
### Fixed
|
||||
|
||||
- [core] Fixed crash possibility if `module: <name>` is not defined and on `postion: <positon>` mistake (#3445)
|
||||
- [weather] Fixed precipitationProbability in forecast for provider openmeteo (#3446)
|
||||
- [weather] Fixed type=daily for provider openmeteo having no data when running after 23:00 (#3449)
|
||||
- [weather] Fixed type=daily for provider openmeteo showing nightly icons in forecast when current time is "nightly" (#3458)
|
||||
- [weather] Fixed forecast and hourly weather for provider openmeteo to use real temperatures, not apparent temperatures (#3466)
|
||||
- [tests] Fixed e2e tests running in docker container which needs `address: "0.0.0.0"` (#3479)
|
||||
|
||||
## [2.27.0] - 2024-04-01
|
||||
|
||||
Thanks to: @bugsounet, @crazyscot, @illimarkangur, @jkriegshauser, @khassel, @KristjanESPERANTO, @Paranoid93, @rejas, @sdetweil and @vppencilsharpener.
|
||||
|
||||
This release marks the first release without Michael Teeuw (@michmich). A very special thanks to him for creating MagicMirror and leading the project for so many years.
|
||||
|
||||
For more info, please read the following post: [A New Chapter for MagicMirror: The Community Takes the Lead](https://forum.magicmirror.builders/topic/18329/a-new-chapter-for-magicmirror-the-community-takes-the-lead).
|
||||
|
||||
### Added
|
||||
|
||||
- Output of system information to the console for troubleshooting (#3328 and #3337), ignore errors under aarch64 (#3349)
|
||||
- [core] Add `eslint-plugin-package-json` to lint the `package.json` files (#3368)
|
||||
- [weather] `showHumidity` config is now a string describing where to show this element. Supported values: "wind", "temp", "feelslike", "below", "none". (#3330)
|
||||
- electron-rebuild test suite for electron and 3rd party modules compatibility (#3392)
|
||||
- Create MM² icon and attach it to electron process (#3407)
|
||||
|
||||
### Updated
|
||||
|
||||
- Update updatenotification (update_helper.js): Recode with pm2 library (#3332)
|
||||
- Removing lodash dependency by replacing merge by spread operator (#3339)
|
||||
- Use node prefix for build-in modules (#3340)
|
||||
- Rework logging colors (#3350)
|
||||
- Update pm2 to v5.3.1 with no allow-ghsas (#3364)
|
||||
- [core] Update husky and let lint-staged fix ESLint issues
|
||||
- [core] Update dependencies including electron to v29 (#3357) and node-ical
|
||||
- Update translations for estonian (#3371)
|
||||
- Update electron to v29 and update other dependencies
|
||||
- [calendar] fullDay events over several days now show the left days from the first day on and 'today' on the last day
|
||||
- Update layout of current weather indoor values
|
||||
|
||||
### Fixed
|
||||
|
||||
- Correct apibase of weathergov weatherprovider to match documentation (#2926)
|
||||
- Worked around several issues in the RRULE library that were causing deleted calender events to still show, some
|
||||
initial and recurring events to not show, and some event times to be off an hour. (#3291)
|
||||
- Skip changelog requirement when running tests for dependency updates (#3320)
|
||||
- Display precipitation probability when it is 0% instead of blank/empty (#3345)
|
||||
- [newsfeed] Suppress unsightly animation cases when there are 0 or 1 active news items (#3336)
|
||||
- [newsfeed] Always compute the feed item URL using the same helper function (#3336)
|
||||
- Ignore all custom css files (#3359)
|
||||
- [newsfeed] Fix newsfeed stall issue introduced by #3336 (#3361)
|
||||
- Changed `log.debug` to `log.log` in `app.js` where logLevel is not set because config is not loaded at this time (#3353)
|
||||
- [calendar] deny fetch interval < 60000 and set 60000 in this case (prevent fetch loop failed) (#3382)
|
||||
- added message in case where config.js is missing the module.export line PR #3383
|
||||
- Fixed an issue where recurring events could extend past their recurrence end date (#3393)
|
||||
- Don't display any `npm WARN <....>` on install (#3399)
|
||||
- [core] Moved suncalc dependency to production from dev, as it is used by clock module
|
||||
- [compliments] Fix mirror not responding anymore when no compliments are to be shown (#3385)
|
||||
- [core] Fixed mastermerge workflow (#3415)
|
||||
|
||||
### Deleted
|
||||
|
||||
- Unneeded file headers (#3358)
|
||||
- Removed codecov.yaml
|
||||
|
||||
## [2.26.0] - 2024-01-01
|
||||
|
||||
Thanks to: @bnitkin, @bugsounet, @dependabot, @jkriegshauser, @kaennchenstruggle, @KristjanESPERANTO and @Ybbet.
|
||||
|
||||
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 guys! You are awesome!
|
||||
|
||||
This release also marks the latest release by Michael Teeuw. For more info, please read the following post: [A New Chapter for MagicMirror: The Community Takes the Lead](https://forum.magicmirror.builders/topic/18329/a-new-chapter-for-magicmirror-the-community-takes-the-lead).
|
||||
|
||||
### Added
|
||||
|
||||
- Added update notification updater (for 3rd party modules)
|
||||
- Added node 21 to the test matrix
|
||||
- Added transform object to calendar:customEvents
|
||||
- Added ESLint rules for jest (including jest/expect-expect and jest/no-done-callback)
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed Codecov workflow (not working anymore, other workflow required) (#3107)
|
||||
- Removed titleReplace from calendar, replaced + extended by customEvents (backward compatibility included) (#3249)
|
||||
- Removed failing unit test (#3254)
|
||||
- Removed some unused variables
|
||||
|
||||
### Updated
|
||||
|
||||
- Update electron to v27 and update other dependencies as well as github actions
|
||||
- Update newsfeed: Use `html-to-text` instead of regex for transform description
|
||||
- Review ESLint config (#3269)
|
||||
- Update dependencies
|
||||
- Clock module: optionally display current moon phase in addition to rise/set times
|
||||
- electron is now per default started without gpu, if needed it must be enabled with new env var `ELECTRON_ENABLE_GPU=1` on startup (#3226)
|
||||
- Replace prettier by stylistic in ESLint config to lint JavaScript (and disable some rules for `config/config.js*` files)
|
||||
- Update node-ical to v0.17.1 and fix tests
|
||||
|
||||
### Fixed
|
||||
|
||||
- Avoid fade out/in on updateDom when many calendars are used
|
||||
- Fix the option eventClass on customEvents.
|
||||
- Fix yr API version in locationforecast and sunrise call (#3227)
|
||||
- Fix cloneObject() function to respect RegExp (#3237)
|
||||
- Fix newsfeed module for feeds using "a10:updated" tag (#3238)
|
||||
- Fix issue template (#3167)
|
||||
- Fix #3256 filter out bad results from rrule.between
|
||||
- Fix calendar events sometimes not respecting deleted events (#3250)
|
||||
- Fix electron loadurl locally on Windows when address "0.0.0.0" (#2550)
|
||||
- Fix updatanotification (update_helper.js): catch error if response is not an JSON format (check PM2)
|
||||
- Fix missing typeof in calendar module
|
||||
- Fix style issues after prettier update
|
||||
- Fix calendar test (#3291) by moving "Exdate check" from e2e to electron to run on a Thursday
|
||||
- Fix calendar config params `fetchInterval` and `excludedEvents` were never used from single calendar config (#3297)
|
||||
- Fix MM_PORT variable not used in electron and allow full path for MM_CONFIG_FILE variable (#3302)
|
||||
|
||||
## [2.25.0] - 2023-10-01
|
||||
|
||||
Thanks to: @bugsounet, @dgoth, @dependabot, @kenzal, @Knapoc, @KristjanESPERANTO, @martingron, @NolanKingdon, @Paranoid93, @TeddyStarinvest and @Ybbet.
|
||||
|
||||
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 guys! You are awesome!
|
||||
|
||||
> ⚠️ This release needs nodejs version >= `v18`, older releases have reached end of life and will not work!
|
||||
|
||||
### Added
|
||||
|
||||
- Added UV Index support to OpenWeatherMap
|
||||
- Added 'hideDuplicates' flag to the calendar module
|
||||
- Added `allowOverrideNotification` to weather module to enable sending current weather objects with the `CURRENT_WEATHER_OVERRIDE` notification to supplement/replace the current weather displayed
|
||||
- Added optional AnimateCSS animate for `hide()`, `show()`, `updateDom()`
|
||||
- Added AnimateIn and animateOut in module config definition
|
||||
- Apply AnimateIn rules on the first start
|
||||
- Added automatic client page reload when server was restarted by setting `reloadAfterServerRestart: true` in `config.js`, per default `false` (#3105)
|
||||
- Added eventClass option for customEvents on the default calendar
|
||||
- Added AnimateCSS integration in tests suite (#3206)
|
||||
- Added npm dependabot [Reserved to developer] (#3210)
|
||||
- Added improved logging for calendar (#3110)
|
||||
|
||||
### Removed
|
||||
|
||||
- **Breaking Change**: Removed `digest` authentication method from calendar module (which was already broken since release `2.15.0`)
|
||||
|
||||
### Updated
|
||||
|
||||
- Update roboto fonts to version v5
|
||||
- Update issue template
|
||||
- Update dev/dependencies incl. electron to v26
|
||||
- Replace pretty-quick by lint-staged (<https://github.com/azz/pretty-quick/issues/164>)
|
||||
- Update engine node >=18. v16 reached its end of life. (#3170)
|
||||
- Update typescript definition for modules
|
||||
- Cleaned up nunjuck templates
|
||||
- Replace `node-fetch` with internal fetch (#2649) and remove `digest-fetch`
|
||||
- Update the French translation according to the English file.
|
||||
- Update dependabot incl. vendor/fonts (monthly check)
|
||||
- Renew `package-lock.json` for release
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix engine check on npm install (#3135)
|
||||
- Fix undefined formatTime method in clock module (#3143)
|
||||
- Fix clientonly startup fails after async added (#3151)
|
||||
- Fix electron width/height when using xrandr under bullseye
|
||||
- Fix time issue with certain recurring events in calendar module
|
||||
- Fix ipWhiteList test (#3179)
|
||||
- Fix newsfeed: Convert HTML entities, codes and tag in description (#3191)
|
||||
- Respect width/height (no fullscreen) if set in electronOptions (together with `fullscreen: false`) in `config.js` (#3174)
|
||||
- Fix: AnimateCSS merge hide() and show() animated css class when we do multiple call
|
||||
- Fix `Uncaught SyntaxError: Identifier 'getCorsUrl' has already been declared (at utils.js:1:1)` when using `clock` and `weather` module (#3204)
|
||||
- Fix overriding `config.js` when running tests (#3201)
|
||||
- Fix issue in weathergov provider with probability of precipitation not showing up on hourly or daily forecast
|
||||
- Fix yr weather provider after changes in yr API (#3189)
|
||||
|
||||
## [2.24.0] - 2023-07-01
|
||||
|
||||
@@ -17,8 +212,8 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al
|
||||
- Added tests for serveronly
|
||||
- Set Timezone `Europe/Berlin` in unit tests (needed for new formatTime tests)
|
||||
- Added no-param-reassign eslint rule and fix warnings
|
||||
- updatenotification: Added `sendUpdatesNotifications` feature. Broadcast update with `UPDATES` notification to other modules
|
||||
- updatenotification: allow force scanning with `SCAN_UPDATES` notification from other modules
|
||||
- [updatenotification] Added `sendUpdatesNotifications` feature. Broadcast update with `UPDATES` notification to other modules
|
||||
- [updatenotification] Allow force scanning with `SCAN_UPDATES` notification from other modules
|
||||
- Added per-calendar fetchInterval
|
||||
|
||||
### Removed
|
||||
@@ -545,12 +740,12 @@ Special thanks to the following contributors: @bryanzzhu, @bugsounet, @chamakura
|
||||
### Fixed
|
||||
|
||||
- Fix backward compatibility issues for Safari < 11.
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast depending on the endpoint (forecast/daily or forecast)". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
|
||||
- Fix calendar display. Account for current timezone. [#2068](https://github.com/MichMich/MagicMirror/issues/2068)
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast depending on the endpoint (forecast/daily or forecast)". [#2018](https://github.com/MagicMirrorOrg/MagicMirror/issues/2018)
|
||||
- Fix calendar display. Account for current timezone. [#2068](https://github.com/MagicMirrorOrg/MagicMirror/issues/2068)
|
||||
- Fix logLevel being set before loading config.
|
||||
- Fix incorrect namespace links in svg clockfaces. [#2072](https://github.com/MichMich/MagicMirror/issues/2072)
|
||||
- Fix weather/providers/weathergov for API guidelines. [#2045](https://github.com/MichMich/MagicMirror/issues/2045)
|
||||
- Fix "undefined" in weather modules header. [#1985](https://github.com/MichMich/MagicMirror/issues/1985)
|
||||
- Fix incorrect namespace links in svg clockfaces. [#2072](https://github.com/MagicMirrorOrg/MagicMirror/issues/2072)
|
||||
- Fix weather/providers/weathergov for API guidelines. [#2045](https://github.com/MagicMirrorOrg/MagicMirror/issues/2045)
|
||||
- Fix "undefined" in weather modules header. [#1985](https://github.com/MagicMirrorOrg/MagicMirror/issues/1985)
|
||||
- Fix #2110, #2111, #2118: Recurring full day events should not use timezone adjustment. Just compare month/day.
|
||||
|
||||
## [2.12.0] - 2020-07-01
|
||||
@@ -584,14 +779,14 @@ Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryan
|
||||
|
||||
### Fixed
|
||||
|
||||
- The broken modules due to Socket.io change from last release. [#1973](https://github.com/MichMich/MagicMirror/issues/1973)
|
||||
- Add backward compatibility for old module code in socketclient.js. [#1973](https://github.com/MichMich/MagicMirror/issues/1973)
|
||||
- Support multiple instances of calendar module with different config. [#1109](https://github.com/MichMich/MagicMirror/issues/1109)
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
|
||||
- Throw error when check_config fails. [#1928](https://github.com/MichMich/MagicMirror/issues/1928)
|
||||
- Bug fix related to 'maxEntries' not displaying Calendar events. [#2050](https://github.com/MichMich/MagicMirror/issues/2050)
|
||||
- Updated ical library to the latest version. [#1926](https://github.com/MichMich/MagicMirror/issues/1926)
|
||||
- Fix config check after merge of prettier [#2109](https://github.com/MichMich/MagicMirror/issues/2109)
|
||||
- The broken modules due to Socket.io change from last release. [#1973](https://github.com/MagicMirrorOrg/MagicMirror/issues/1973)
|
||||
- Add backward compatibility for old module code in socketclient.js. [#1973](https://github.com/MagicMirrorOrg/MagicMirror/issues/1973)
|
||||
- Support multiple instances of calendar module with different config. [#1109](https://github.com/MagicMirrorOrg/MagicMirror/issues/1109)
|
||||
- Fix the use of "maxNumberOfDays" in the module "weatherforecast". [#2018](https://github.com/MagicMirrorOrg/MagicMirror/issues/2018)
|
||||
- Throw error when check_config fails. [#1928](https://github.com/MagicMirrorOrg/MagicMirror/issues/1928)
|
||||
- Bug fix related to 'maxEntries' not displaying Calendar events. [#2050](https://github.com/MagicMirrorOrg/MagicMirror/issues/2050)
|
||||
- Updated ical library to the latest version. [#1926](https://github.com/MagicMirrorOrg/MagicMirror/issues/1926)
|
||||
- Fix config check after merge of prettier [#2109](https://github.com/MagicMirrorOrg/MagicMirror/issues/2109)
|
||||
|
||||
## [2.11.0] - 2020-04-01
|
||||
|
||||
@@ -599,7 +794,7 @@ Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryan
|
||||
|
||||
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).
|
||||
For more information regarding this major change, please check issue [#1860](https://github.com/MagicMirrorOrg/MagicMirror/issues/1860).
|
||||
|
||||
### Deleted
|
||||
|
||||
@@ -629,8 +824,8 @@ For more information regarding this major change, please check issue [#1860](htt
|
||||
- Fix calendar time offset for recurring events crossing Daylight Savings Time (ISSUE #1798)
|
||||
- Fix regression in currentweather module causing 'undefined' to show up when config.hideTemp is false
|
||||
- Fix FEELS translation for Croatian
|
||||
- Fixed weather tests [#1840](https://github.com/MichMich/MagicMirror/issues/1840)
|
||||
- Fixed Socket.io can't be used with Reverse Proxy in serveronly mode [#1934](https://github.com/MichMich/MagicMirror/issues/1934)
|
||||
- Fixed weather tests [#1840](https://github.com/MagicMirrorOrg/MagicMirror/issues/1840)
|
||||
- Fixed Socket.io can't be used with Reverse Proxy in serveronly mode [#1934](https://github.com/MagicMirrorOrg/MagicMirror/issues/1934)
|
||||
- Fix update checking skipping 3rd party modules the first time
|
||||
|
||||
### Changed
|
||||
@@ -698,14 +893,14 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
|
||||
- Updatenotification module: Display update notification for a limited (configurable) time.
|
||||
- Enabled e2e/vendor_spec.js tests.
|
||||
- The css/custom.css will be renamed after the next release. We've added into `run-start.sh` an instruction by GIT to ignore with `--skip-worktree` and `rm --cached`. [#1540](https://github.com/MichMich/MagicMirror/issues/1540)
|
||||
- The css/custom.css will be renamed after the next release. We've added into `run-start.sh` an instruction by GIT to ignore with `--skip-worktree` and `rm --cached`. [#1540](https://github.com/MagicMirrorOrg/MagicMirror/issues/1540)
|
||||
- Disable sending of notification CLOCK_SECOND when displaySeconds is false.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Updatenotification module: Properly handle race conditions, prevent crash.
|
||||
- Send `NEWS_FEED` notification also for the first news messages which are shown.
|
||||
- Fixed issue where weather module would not refresh data after a network or API outage. [#1722](https://github.com/MichMich/MagicMirror/issues/1722)
|
||||
- Fixed issue where weather module would not refresh data after a network or API outage. [#1722](https://github.com/MagicMirrorOrg/MagicMirror/issues/1722)
|
||||
- Fixed weatherforecast module not displaying rain amount on fallback endpoint.
|
||||
- Notifications CLOCK_SECOND & CLOCK_MINUTE being from startup instead of matched against the clock and avoid drifting.
|
||||
|
||||
@@ -720,7 +915,7 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
- Russian translation for “Feels”
|
||||
- Calendar module: added `nextDaysRelative` config option
|
||||
- Add `broadcastPastEvents` config option for calendars to include events from the past `maximumNumberOfDays` in event broadcasts
|
||||
- Added feature to broadcast news feed items `NEWS_FEED` and updated news items `NEWS_FEED_UPDATED` in default [newsfeed](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/newsfeed) module (when news is updated) with documented default and `config.js` options in [README.md](https://github.com/MichMich/MagicMirror/blob/develop/modules/default/newsfeed/README.md)
|
||||
- Added feature to broadcast news feed items `NEWS_FEED` and updated news items `NEWS_FEED_UPDATED` in default [newsfeed](https://github.com/MagicMirrorOrg/MagicMirror/tree/develop/modules/default/newsfeed) module (when news is updated) with documented default and `config.js` options in [README.md](https://github.com/MagicMirrorOrg/MagicMirror/blob/develop/modules/default/newsfeed/README.md)
|
||||
- Added notifications to default `clock` module broadcasting `CLOCK_SECOND` and `CLOCK_MINUTE` for the respective time elapsed.
|
||||
- Added UK Met Office Datapoint feed as a provider in the default weather module.
|
||||
- Added new provider class
|
||||
@@ -729,7 +924,7 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
- Use Feels Like temp from feed if present
|
||||
- Optionally display probability of precipitation (PoP) in current weather (UK Met Office data)
|
||||
- Automatically try to fix eslint errors by passing `--fix` option to it
|
||||
- Added sunrise and sunset times to weathergov weather-provider [#1705](https://github.com/MichMich/MagicMirror/issues/1705)
|
||||
- Added sunrise and sunset times to weathergov weather-provider [#1705](https://github.com/MagicMirrorOrg/MagicMirror/issues/1705)
|
||||
- Added "useLocationAsHeader" to display "location" in `config.js` as header when location name is not returned
|
||||
- Added to `newsfeed.js`: in order to design the news article better with css, three more class-names were introduced: newsfeed-desc, newsfeed-desc, newsfeed-desc
|
||||
|
||||
@@ -738,18 +933,18 @@ 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`
|
||||
- Updated `ical.js` to solve various calendar issues.
|
||||
- Updated weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676)
|
||||
- Updated weather city list url [#1676](https://github.com/MagicMirrorOrg/MagicMirror/issues/1676)
|
||||
- Only update clock once per minute when seconds aren't shown
|
||||
- Updated weather-provider documentation.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed uncaught exception, race condition on module update
|
||||
- Fixed issue [#1696](https://github.com/MichMich/MagicMirror/issues/1696), some ical files start date to not parse to date type
|
||||
- Fixed issue [#1696](https://github.com/MagicMirrorOrg/MagicMirror/issues/1696), some ical files start date to not parse to date type
|
||||
- Allowance HTML5 autoplay-policy (policy is changed from Chrome 66 updates)
|
||||
- Handle SIGTERM messages
|
||||
- Fixes sliceMultiDayEvents so it respects maximumNumberOfDays
|
||||
- Minor types in default NewsFeed [README.md](https://github.com/MichMich/MagicMirror/blob/develop/modules/default/newsfeed/README.md)
|
||||
- Minor types in default NewsFeed [README.md](https://github.com/MagicMirrorOrg/MagicMirror/blob/develop/modules/default/newsfeed/README.md)
|
||||
- Fix typos and small syntax errors, cleanup dependencies, remove multiple-empty-lines, add semi-rule
|
||||
- Fixed issues with calendar not displaying one-time changes to repeating events
|
||||
- Updated the fetchedLocationName variable in currentweather.js so that city shows up in the header
|
||||
@@ -787,41 +982,41 @@ Fixed `package.json` version number.
|
||||
|
||||
### Updated
|
||||
|
||||
- Bumped the Electron dependency to v3.0.13 to support the most recent Raspbian. [#1500](https://github.com/MichMich/MagicMirror/issues/1500)
|
||||
- Bumped the Electron dependency to v3.0.13 to support the most recent Raspbian. [#1500](https://github.com/MagicMirrorOrg/MagicMirror/issues/1500)
|
||||
- Updated modernizr code in alert module, fixed a small typo there too
|
||||
- More verbose error message on console if the config is malformed
|
||||
- Updated installer script to install Node.js version 10.x
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed temperature displays in currentweather and weatherforecast modules [#1503](https://github.com/MichMich/MagicMirror/issues/1503), [#1511](https://github.com/MichMich/MagicMirror/issues/1511).
|
||||
- Fixed unhandled error on bad git data in updatenotification module [#1285](https://github.com/MichMich/MagicMirror/issues/1285).
|
||||
- Weather forecast now works with openweathermap in new weather module. Daily data are displayed, see issue [#1504](https://github.com/MichMich/MagicMirror/issues/1504).
|
||||
- Fixed temperature displays in currentweather and weatherforecast modules [#1503](https://github.com/MagicMirrorOrg/MagicMirror/issues/1503), [#1511](https://github.com/MagicMirrorOrg/MagicMirror/issues/1511).
|
||||
- Fixed unhandled error on bad git data in updatenotification module [#1285](https://github.com/MagicMirrorOrg/MagicMirror/issues/1285).
|
||||
- Weather forecast now works with openweathermap in new weather module. Daily data are displayed, see issue [#1504](https://github.com/MagicMirrorOrg/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/MagicMirrorOrg/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: only show repeating count if the event is actually repeating [#1534](https://github.com/MagicMirrorOrg/MagicMirror/pull/1534)
|
||||
- Calendar: Fix exdate handling when multiple values are specified (comma separated)
|
||||
- Calendar: Fix relative date handling for fulldate events, calculate difference always from start of day [#1572](https://github.com/MichMich/MagicMirror/issues/1572)
|
||||
- Calendar: Fix relative date handling for fulldate events, calculate difference always from start of day [#1572](https://github.com/MagicMirrorOrg/MagicMirror/issues/1572)
|
||||
- Fix null dereference in moduleNeedsUpdate when the module isn't visible
|
||||
- Calendar: Fixed event end times by setting default calendarEndTime to "LT" (Local time format). [#1479]
|
||||
- Calendar: Fixed missing calendar fetchers after server process restarts [#1589](https://github.com/MichMich/MagicMirror/issues/1589)
|
||||
- Calendar: Fixed missing calendar fetchers after server process restarts [#1589](https://github.com/MagicMirrorOrg/MagicMirror/issues/1589)
|
||||
- Notification: fixed background color (was white text on white background)
|
||||
- Use getHeader instead of data.header when creating the DOM so overwriting the function also propagates into it
|
||||
- Fix documentation of `useKMPHwind` option in currentweather
|
||||
|
||||
### New weather module
|
||||
|
||||
- Fixed weather forecast table display [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||
- Fixed weather forecast table display [#1499](https://github.com/MagicMirrorOrg/MagicMirror/issues/1499).
|
||||
- Dimmed loading indicator for weather forecast.
|
||||
- Implemented config option `decimalSymbol` [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||
- Aligned indoor values in current weather vertical [#1499](https://github.com/MichMich/MagicMirror/issues/1499).
|
||||
- Implemented config option `decimalSymbol` [#1499](https://github.com/MagicMirrorOrg/MagicMirror/issues/1499).
|
||||
- Aligned indoor values in current weather vertical [#1499](https://github.com/MagicMirrorOrg/MagicMirror/issues/1499).
|
||||
- Added humidity support to nunjuck unit filter.
|
||||
- Do not display degree symbol for temperature in Kelvin [#1503](https://github.com/MichMich/MagicMirror/issues/1503).
|
||||
- Weather forecast now works with openweathermap for both, `/forecast` and `/forecast/daily`, in new weather module. If you use the `/forecast`-weatherEndpoint, the hourly data are converted to daily data, see issues [#1504](https://github.com/MichMich/MagicMirror/issues/1504), [#1513](https://github.com/MichMich/MagicMirror/issues/1513).
|
||||
- Added fade, fadePoint and maxNumberOfDays properties to the forecast mode [#1516](https://github.com/MichMich/MagicMirror/issues/1516)
|
||||
- Fixed Loading string and decimalSymbol string replace [#1538](https://github.com/MichMich/MagicMirror/issues/1538)
|
||||
- Show Snow amounts in new weather module [#1545](https://github.com/MichMich/MagicMirror/issues/1545)
|
||||
- Do not display degree symbol for temperature in Kelvin [#1503](https://github.com/MagicMirrorOrg/MagicMirror/issues/1503).
|
||||
- Weather forecast now works with openweathermap for both, `/forecast` and `/forecast/daily`, in new weather module. If you use the `/forecast`-weatherEndpoint, the hourly data are converted to daily data, see issues [#1504](https://github.com/MagicMirrorOrg/MagicMirror/issues/1504), [#1513](https://github.com/MagicMirrorOrg/MagicMirror/issues/1513).
|
||||
- Added fade, fadePoint and maxNumberOfDays properties to the forecast mode [#1516](https://github.com/MagicMirrorOrg/MagicMirror/issues/1516)
|
||||
- Fixed Loading string and decimalSymbol string replace [#1538](https://github.com/MagicMirrorOrg/MagicMirror/issues/1538)
|
||||
- Show Snow amounts in new weather module [#1545](https://github.com/MagicMirrorOrg/MagicMirror/issues/1545)
|
||||
- Added weather.gov as a new weather-provider for US locations
|
||||
|
||||
## [2.6.0] - 2019-01-01
|
||||
@@ -846,7 +1041,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Screenshot for the weather forecast module
|
||||
- Portuguese translation for "Feels"
|
||||
- Croatian translation
|
||||
- Fading for dateheaders timeFormat in Calendar [#1464](https://github.com/MichMich/MagicMirror/issues/1464)
|
||||
- Fading for dateheaders timeFormat in Calendar [#1464](https://github.com/MagicMirrorOrg/MagicMirror/issues/1464)
|
||||
- Documentation for the existing `scale` option in the Weather Forecast module.
|
||||
|
||||
### Fixed
|
||||
@@ -855,7 +1050,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Fixed Polish translation for Single Update Info
|
||||
- Ignore entries with unparseable details in the calendar module
|
||||
- Bug showing FullDayEvents one day too long in calendar fixed
|
||||
- Bug in newsfeed when `removeStartTags` is used on the description [#1478](https://github.com/MichMich/MagicMirror/issues/1478)
|
||||
- Bug in newsfeed when `removeStartTags` is used on the description [#1478](https://github.com/MagicMirrorOrg/MagicMirror/issues/1478)
|
||||
|
||||
### Updated
|
||||
|
||||
@@ -888,7 +1083,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Fixed mixup between german and spanish translation for newsfeed.
|
||||
- Fixed close dates to be absolute, if no configured in the config.js - module Calendar
|
||||
- Fixed the updatenotification module message about new commits in the repository, so they can be correctly localized in singular and plural form.
|
||||
- Fix for weatherforecast rainfall rounding [#1374](https://github.com/MichMich/MagicMirror/issues/1374)
|
||||
- Fix for weatherforecast rainfall rounding [#1374](https://github.com/MagicMirrorOrg/MagicMirror/issues/1374)
|
||||
- Fix calendar parsing issue for Midori on RasperryPi Zero w, related to issue #694.
|
||||
- Fix weather city ID link in sample config
|
||||
- Fixed issue with clientonly not updating with IP address and port provided on command line.
|
||||
@@ -910,7 +1105,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/MagicMirrorOrg/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`
|
||||
|
||||
@@ -932,20 +1127,20 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
- Upgrade to Electron 2.0.0.
|
||||
- Remove yarn-or-npm which breaks production builds.
|
||||
- Invoke module suspend even if no dom content. [#1308](https://github.com/MichMich/MagicMirror/issues/1308)
|
||||
- Invoke module suspend even if no dom content. [#1308](https://github.com/MagicMirrorOrg/MagicMirror/issues/1308)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue where wind chill could not be displayed in Fahrenheit. [#1247](https://github.com/MichMich/MagicMirror/issues/1247)
|
||||
- Fixed issues where a module crashes when it tries to dismiss a non existing alert. [#1240](https://github.com/MichMich/MagicMirror/issues/1240)
|
||||
- Fixed issue where wind chill could not be displayed in Fahrenheit. [#1247](https://github.com/MagicMirrorOrg/MagicMirror/issues/1247)
|
||||
- Fixed issues where a module crashes when it tries to dismiss a non existing alert. [#1240](https://github.com/MagicMirrorOrg/MagicMirror/issues/1240)
|
||||
- In default module currentWeather/currentWeather.js line 296, 300, self.config.animationSpeed can not be found because the notificationReceived function does not have "self" variable.
|
||||
- Fixed browser-side code to work on the Midori browser.
|
||||
- Fixed issue where heat index was reporting incorrect values in Celsius and Fahrenheit. [#1263](https://github.com/MichMich/MagicMirror/issues/1263)
|
||||
- Fixed issue where heat index was reporting incorrect values in Celsius and Fahrenheit. [#1263](https://github.com/MagicMirrorOrg/MagicMirror/issues/1263)
|
||||
- Fixed weatherforecast to use dt_txt field instead of dt to handle timezones better
|
||||
- Newsfeed now remembers to show the description when `"ARTICLE_LESS_DETAILS"` is called if the user wants to always show the description. [#1282](https://github.com/MichMich/MagicMirror/issues/1282)
|
||||
- Newsfeed now remembers to show the description when `"ARTICLE_LESS_DETAILS"` is called if the user wants to always show the description. [#1282](https://github.com/MagicMirrorOrg/MagicMirror/issues/1282)
|
||||
- `clientonly/*.js` is now linted, and one linting error is fixed
|
||||
- Fix issue #1196 by changing underscore to hyphen in locale id, in align with momentjs.
|
||||
- Fixed issue where heat index and wind chill were reporting incorrect values in Kelvin. [#1263](https://github.com/MichMich/MagicMirror/issues/1263)
|
||||
- Fixed issue where heat index and wind chill were reporting incorrect values in Kelvin. [#1263](https://github.com/MagicMirrorOrg/MagicMirror/issues/1263)
|
||||
|
||||
### Updated
|
||||
|
||||
@@ -957,7 +1152,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
### Fixed
|
||||
|
||||
- Downgrade electron to 1.4.15 to solve the black screen issue.[#1243](https://github.com/MichMich/MagicMirror/issues/1243)
|
||||
- Downgrade electron to 1.4.15 to solve the black screen issue.[#1243](https://github.com/MagicMirrorOrg/MagicMirror/issues/1243)
|
||||
|
||||
## [2.3.0] - 2018-04-01
|
||||
|
||||
@@ -1083,8 +1278,8 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
### Changed
|
||||
|
||||
- Revert Docker related changes in favor of [docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror). All Docker images are outsourced. ([#856](https://github.com/MichMich/MagicMirror/pull/856))
|
||||
- Change Docker base image (Debian + Node) to an arm based distro (AlpineARM + Node) ([#846](https://github.com/MichMich/MagicMirror/pull/846))
|
||||
- Revert Docker related changes in favor of [docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror). All Docker images are outsourced. ([#856](https://github.com/MagicMirrorOrg/MagicMirror/pull/856))
|
||||
- Change Docker base image (Debian + Node) to an arm based distro (AlpineARM + Node) ([#846](https://github.com/MagicMirrorOrg/MagicMirror/pull/846))
|
||||
- Fix the dockerfile to have it running from the first time.
|
||||
|
||||
### Added
|
||||
@@ -1131,7 +1326,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Add `anytime` group for Compliments module.
|
||||
- Compliments module can use remoteFile without default daytime arrays defined.
|
||||
- Installer: Use init config.js from config.js.sample.
|
||||
- Switched out `rrule` package for `rrule-alt` and fixes in `ical.js` in order to fix calendar issues. ([#565](https://github.com/MichMich/MagicMirror/issues/565))
|
||||
- Switched out `rrule` package for `rrule-alt` and fixes in `ical.js` in order to fix calendar issues. ([#565](https://github.com/MagicMirrorOrg/MagicMirror/issues/565))
|
||||
- Make mouse events pass through the region fullscreen_above to modules below.
|
||||
- Scaled the splash screen down to make it a bit more subtle.
|
||||
- Replace HTML tables with markdown tables in README files.
|
||||
@@ -1144,7 +1339,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
### Added
|
||||
|
||||
- Added Docker support (Pull Request [#673](https://github.com/MichMich/MagicMirror/pull/673)).
|
||||
- Added Docker support (Pull Request [#673](https://github.com/MagicMirrorOrg/MagicMirror/pull/673)).
|
||||
- Calendar-specific support for `maximumEntries`, and `maximumNumberOfDays`.
|
||||
- Add loaded function to modules, providing an async callback.
|
||||
- Made default newsfeed module aware of gesture events from [MMM-Gestures](https://github.com/thobach/MMM-Gestures)
|
||||
@@ -1190,7 +1385,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- 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))
|
||||
- Fix an issue where the analog clock looked scrambled. ([#611](https://github.com/MagicMirrorOrg/MagicMirror/issues/611))
|
||||
- If units are set to imperial, the showRainAmount option of weatherforecast will show the correct unit.
|
||||
- Module currentWeather: check if temperature received from api is defined.
|
||||
- Fix an issue with module hidden status changing to `true` although lock string prevented showing it.
|
||||
@@ -1207,18 +1402,18 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Finnish translation.
|
||||
- Danish translation.
|
||||
- Turkish translation.
|
||||
- Option to limit access to certain IP addresses based on the value of `ipWhitelist` in the `config.js`, default is access from localhost only (Issue [#456](https://github.com/MichMich/MagicMirror/issues/456)).
|
||||
- Option to limit access to certain IP addresses based on the value of `ipWhitelist` in the `config.js`, default is access from localhost only (Issue [#456](https://github.com/MagicMirrorOrg/MagicMirror/issues/456)).
|
||||
- Added ability to change the point of time when calendar events get relative.
|
||||
- Add Splash screen on boot.
|
||||
- Add option to show humidity in currentWeather module.
|
||||
- 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.
|
||||
- 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.
|
||||
- Module API: Add Visibility locking to module system. [See documentation](https://github.com/MagicMirrorOrg/MagicMirror/tree/develop/modules#visibility-locking) for more information.
|
||||
- Module API: Method to overwrite the module's header. [See documentation](https://github.com/MagicMirrorOrg/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/MagicMirrorOrg/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/MagicMirrorOrg/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/MagicMirrorOrg/MagicMirror/tree/develop/modules/default/weatherforecast) for more information.
|
||||
- Added option to show rain amount in the weatherforecast default module
|
||||
- Add module `updatenotification` to get an update whenever a new version is available. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/updatenotification) for more information.
|
||||
- Add module `updatenotification` to get an update whenever a new version is available. [See documentation](https://github.com/MagicMirrorOrg/MagicMirror/tree/develop/modules/default/updatenotification) for more information.
|
||||
- Add the ability to set timezone on the date display in the Clock Module
|
||||
- Ability to set date format in calendar module
|
||||
- Possibility to use currentweather for the compliments
|
||||
@@ -1288,11 +1483,11 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
### Fixed
|
||||
|
||||
- Prevent `getModules()` selectors from returning duplicate entries.
|
||||
- Append endpoints of weather modules with `/` to retrieve the correct data. (Issue [#337](https://github.com/MichMich/MagicMirror/issues/337))
|
||||
- Append endpoints of weather modules with `/` to retrieve the correct data. (Issue [#337](https://github.com/MagicMirrorOrg/MagicMirror/issues/337))
|
||||
- Corrected grammar in `module.js` from 'suspend' to 'suspended'.
|
||||
- Fixed openweathermap.org URL in config sample.
|
||||
- Prevent currentweather module from crashing when received data object is incorrect.
|
||||
- Fix issue where translation loading prevented the UI start-up when the language was set to 'en'. (Issue [#388](https://github.com/MichMich/MagicMirror/issues/388))
|
||||
- Fix issue where translation loading prevented the UI start-up when the language was set to 'en'. (Issue [#388](https://github.com/MagicMirrorOrg/MagicMirror/issues/388))
|
||||
|
||||
### Updated
|
||||
|
||||
@@ -1312,8 +1507,8 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
|
||||
### Fixed
|
||||
|
||||
- Edit Alert Module to display title & message if they are provided in the notification (Issue [#300](https://github.com/MichMich/MagicMirror/issues/300))
|
||||
- Removed 'null' reference from updateModuleContent(). This fixes recent Edge and Internet Explorer browser displays (Issue [#319](https://github.com/MichMich/MagicMirror/issues/319))
|
||||
- Edit Alert Module to display title & message if they are provided in the notification (Issue [#300](https://github.com/MagicMirrorOrg/MagicMirror/issues/300))
|
||||
- Removed 'null' reference from updateModuleContent(). This fixes recent Edge and Internet Explorer browser displays (Issue [#319](https://github.com/MagicMirrorOrg/MagicMirror/issues/319))
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -1330,7 +1525,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
### Fixed
|
||||
|
||||
- Added reference to Italian Translation.
|
||||
- Added the missing NE translation to all languages. [#344](https://github.com/MichMich/MagicMirror/issues/344)
|
||||
- Added the missing NE translation to all languages. [#344](https://github.com/MagicMirrorOrg/MagicMirror/issues/344)
|
||||
- Added proper User-Agent string to calendar call.
|
||||
|
||||
### Changed
|
||||
|
@@ -5,7 +5,7 @@ This document describes how collaborators of this repository should work togethe
|
||||
- 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)
|
||||
- never merge to `master`, except for releases (because of update notification)
|
||||
- merge to `master` only for releases or other urgent issues (update notification is only triggered by tags)
|
||||
- merges to master should be tagged with the "mastermerge" label so that the test runs through
|
||||
|
||||
## Issues
|
||||
@@ -15,4 +15,24 @@ This document describes how collaborators of this repository should work togethe
|
||||
|
||||
## Releases
|
||||
|
||||
- are done by @MichMich only
|
||||
Are done by @rejas or @khassel.
|
||||
|
||||
### Deployment steps
|
||||
|
||||
- pull latest `develop` branch
|
||||
- update `package.json` to reflect correct version number
|
||||
- run `npm install` to generate new `package-lock.json`
|
||||
- test `develop` branch
|
||||
- update `CHANGELOG.md` (don't forget to add all contributor names)
|
||||
- commit and push all changes
|
||||
- after successful test run via github actions: create pull request to `master` branch
|
||||
- after PR tests run without issues, merge PR
|
||||
- create new release with corresponding version tag
|
||||
- publish release notes with link to github release on forum in new locked topic
|
||||
|
||||
### Draft new development release
|
||||
|
||||
- checkout `develop` branch
|
||||
- update `package.json` to reflect correct version number `2.xx.0-develop`
|
||||
- draft new section in `CHANGELOG.md`
|
||||
- commit and publish `develop` branch
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# The MIT License (MIT)
|
||||
|
||||
Copyright © 2016-2022 Michael Teeuw
|
||||
Copyright © 2016-2024 Michael Teeuw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
|
13
README.md
13
README.md
@@ -4,17 +4,14 @@
|
||||
<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/actions/workflow/status/michmich/magicmirror/automated-tests.yaml" 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">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/magicmirrororg/magicmirror/automated-tests.yaml" alt="GitHub Actions">
|
||||
<img src="https://img.shields.io/github/checks-status/magicmirrororg/magicmirror/master" alt="Build Status">
|
||||
<a href="https://github.com/MagicMirrorOrg/MagicMirror">
|
||||
<img src="https://img.shields.io/github/stars/magicmirrororg/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).
|
||||
**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/MagicMirrorOrg/MagicMirror/graphs/contributors).
|
||||
|
||||
MagicMirror² focuses on a modular plugin system and uses [Electron](https://www.electronjs.org/) as an application wrapper. So no more web server or browser installs necessary!
|
||||
|
||||
|
@@ -7,7 +7,8 @@
|
||||
/**
|
||||
* Helper function to get server address/hostname from either the commandline or env
|
||||
*/
|
||||
function getServerAddress() {
|
||||
function getServerAddress () {
|
||||
|
||||
/**
|
||||
* Get command line parameters
|
||||
* Assumes that a cmdline parameter is defined with `--key [value]`
|
||||
@@ -15,7 +16,7 @@
|
||||
* @param {string} defaultValue value if no key is given at the command line
|
||||
* @returns {string} the value of the parameter
|
||||
*/
|
||||
function getCommandLineParameter(key, defaultValue = undefined) {
|
||||
function getCommandLineParameter (key, defaultValue = undefined) {
|
||||
const index = process.argv.indexOf(`--${key}`);
|
||||
const value = index > -1 ? process.argv[index + 1] : undefined;
|
||||
return value !== undefined ? String(value) : defaultValue;
|
||||
@@ -35,11 +36,11 @@
|
||||
* @param {string} url location where the server is running.
|
||||
* @returns {Promise} the config
|
||||
*/
|
||||
function getServerConfig(url) {
|
||||
function getServerConfig (url) {
|
||||
// Return new pending promise
|
||||
return new Promise((resolve, reject) => {
|
||||
// Select http or https module, depending on requested url
|
||||
const lib = url.startsWith("https") ? require("https") : require("http");
|
||||
const lib = url.startsWith("https") ? require("node:https") : require("node:http");
|
||||
const request = lib.get(url, (response) => {
|
||||
let configData = "";
|
||||
|
||||
@@ -64,7 +65,7 @@
|
||||
* @param {string} message error message to print
|
||||
* @param {number} code error code for the exit call
|
||||
*/
|
||||
function fail(message, code = 1) {
|
||||
function fail (message, code = 1) {
|
||||
if (message !== undefined && typeof message === "string") {
|
||||
console.log(message);
|
||||
} else {
|
||||
@@ -84,6 +85,7 @@
|
||||
.then(function (configReturn) {
|
||||
// Pass along the server config via an environment variable
|
||||
const env = Object.create(process.env);
|
||||
env.clientonly = true; // set to pass to electron.js
|
||||
const options = { env: env };
|
||||
configReturn.address = config.address;
|
||||
configReturn.port = config.port;
|
||||
@@ -92,7 +94,7 @@
|
||||
|
||||
// Spawn electron application
|
||||
const electron = require("electron");
|
||||
const child = require("child_process").spawn(electron, ["js/electron.js"], options);
|
||||
const child = require("node:child_process").spawn(electron, ["js/electron.js"], options);
|
||||
|
||||
// Pipe all child process output to current stdout
|
||||
child.stdout.on("data", function (buf) {
|
||||
@@ -120,4 +122,4 @@
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
})();
|
||||
}());
|
||||
|
@@ -1,7 +1,4 @@
|
||||
/* MagicMirror² Config Sample
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
/* Config Sample
|
||||
*
|
||||
* For more information on how you can configure this file
|
||||
* see https://docs.magicmirror.builders/configuration/introduction.html
|
||||
@@ -18,17 +15,17 @@ 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
|
||||
// you must set the sub path here. basePath must end with a /
|
||||
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 :
|
||||
// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.1.5"],
|
||||
// or IPv4 range of 192.168.3.0 --> 192.168.3.15 use CIDR format :
|
||||
// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.3.0/28"],
|
||||
// or add a specific IPv4 of 192.168.1.5 :
|
||||
// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.1.5"],
|
||||
// or IPv4 range of 192.168.3.0 --> 192.168.3.15 use CIDR format :
|
||||
// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.3.0/28"],
|
||||
|
||||
useHttps: false, // Support HTTPS or not, default "false" will use HTTP
|
||||
httpsPrivateKey: "", // HTTPS private key path, only require when useHttps is true
|
||||
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
|
||||
useHttps: false, // Support HTTPS or not, default "false" will use HTTP
|
||||
httpsPrivateKey: "", // HTTPS private key path, only require when useHttps is true
|
||||
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
|
||||
|
||||
language: "en",
|
||||
locale: "en-US",
|
||||
@@ -70,11 +67,10 @@ let config = {
|
||||
module: "weather",
|
||||
position: "top_right",
|
||||
config: {
|
||||
weatherProvider: "openweathermap",
|
||||
weatherProvider: "openmeteo",
|
||||
type: "current",
|
||||
location: "New York",
|
||||
locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
||||
apiKey: "YOUR_OPENWEATHER_API_KEY"
|
||||
lat: 40.776676,
|
||||
lon: -73.971321
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -82,11 +78,10 @@ let config = {
|
||||
position: "top_right",
|
||||
header: "Weather Forecast",
|
||||
config: {
|
||||
weatherProvider: "openweathermap",
|
||||
weatherProvider: "openmeteo",
|
||||
type: "forecast",
|
||||
location: "New York",
|
||||
locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
||||
apiKey: "YOUR_OPENWEATHER_API_KEY"
|
||||
lat: 40.776676,
|
||||
lon: -73.971321
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -109,4 +104,4 @@ let config = {
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {module.exports = config;}
|
||||
if (typeof module !== "undefined") { module.exports = config; }
|
||||
|
@@ -1,10 +1,8 @@
|
||||
/* MagicMirror² Custom CSS Sample
|
||||
/* Custom CSS Sample
|
||||
*
|
||||
* Change color and fonts here.
|
||||
*
|
||||
* Beware that properties cannot be unitless, so for example write '--gap-body: 0px;' instead of just '--gap-body: 0;'
|
||||
*
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
/* Uncomment and adjust accordingly if you want to import another font from the google-fonts-api: */
|
||||
@@ -18,7 +16,7 @@
|
||||
|
||||
--font-primary: "Roboto Condensed";
|
||||
--font-secondary: "Roboto";
|
||||
|
||||
|
||||
--font-size: 20px;
|
||||
--font-size-small: 0.75rem;
|
||||
|
||||
@@ -26,6 +24,6 @@
|
||||
--gap-body-right: 60px;
|
||||
--gap-body-bottom: 60px;
|
||||
--gap-body-left: 60px;
|
||||
|
||||
|
||||
--gap-modules: 30px;
|
||||
}
|
||||
|
32
fonts/package-lock.json
generated
32
fonts/package-lock.json
generated
@@ -1,37 +1,27 @@
|
||||
{
|
||||
"name": "magicmirror-fonts",
|
||||
"lockfileVersion": 2,
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "magicmirror-fonts",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^4.5.8",
|
||||
"@fontsource/roboto-condensed": "^4.5.9"
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@fontsource/roboto-condensed": "^5.0.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/roboto": {
|
||||
"version": "4.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz",
|
||||
"integrity": "sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA=="
|
||||
"version": "5.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.13.tgz",
|
||||
"integrity": "sha512-j61DHjsdUCKMXSdNLTOxcG701FWnF0jcqNNQi2iPCDxU8seN/sMxeh62dC++UiagCWq9ghTypX+Pcy7kX+QOeQ=="
|
||||
},
|
||||
"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": {
|
||||
"@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=="
|
||||
"version": "5.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-5.0.16.tgz",
|
||||
"integrity": "sha512-pjO80g5x/hkqzWCIafvkS3JrkBDxSiTjEy4LdqQKJYrmoGx8x2AlhSUMgzIzG/ge4kT98bA7+gmm7yquzrrZ/w=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"name": "magicmirror-fonts",
|
||||
"description": "Package for fonts use by MagicMirror² Core.",
|
||||
"version": "1.0.0",
|
||||
"description": "Package for fonts use by MagicMirror² core.",
|
||||
"bugs": {
|
||||
"url": "https://github.com/MagicMirrorOrg/MagicMirror/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/MichMich/MagicMirror.git"
|
||||
"url": "https://github.com/MagicMirrorOrg/MagicMirror"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/MichMich/MagicMirror/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^4.5.8",
|
||||
"@fontsource/roboto-condensed": "^4.5.9"
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@fontsource/roboto-condensed": "^5.0.16"
|
||||
}
|
||||
}
|
||||
|
662
fonts/roboto.css
662
fonts/roboto.css
@@ -1,55 +1,671 @@
|
||||
/* roboto-cyrillic-ext-100-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 100;
|
||||
src: local("Roboto Thin"), local("Roboto-Thin"), url("node_modules/@fontsource/roboto/files/roboto-all-100-normal.woff") format("woff");
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-100-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-100-normal.woff") format("woff");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-100-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 100;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-100-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-100-normal.woff") format("woff");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* roboto-greek-ext-100-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 100;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-100-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-100-normal.woff") format("woff");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* roboto-greek-100-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 100;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-100-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-100-normal.woff") format("woff");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* roboto-vietnamese-100-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 100;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-100-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-100-normal.woff") format("woff");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* roboto-latin-ext-100-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 100;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-100-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-100-normal.woff") format("woff");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* roboto-latin-100-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 100;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-100-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-100-normal.woff") format("woff");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-ext-300-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
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/@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/@fontsource/roboto-condensed/files/roboto-condensed-all-700-normal.woff") format("woff");
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-300-normal.woff") format("woff");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-300-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local("Roboto"), local("Roboto-Regular"), url("node_modules/@fontsource/roboto/files/roboto-all-400-normal.woff") format("woff");
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-300-normal.woff") format("woff");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* roboto-greek-ext-300-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-300-normal.woff") format("woff");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* roboto-greek-300-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-300-normal.woff") format("woff");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* roboto-vietnamese-300-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-300-normal.woff") format("woff");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* roboto-latin-ext-300-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-300-normal.woff") format("woff");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* roboto-latin-300-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-300-normal.woff") format("woff");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-ext-400-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-400-normal.woff") format("woff");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-400-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-400-normal.woff") format("woff");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* roboto-greek-ext-400-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-400-normal.woff") format("woff");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* roboto-greek-400-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-400-normal.woff") format("woff");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* roboto-vietnamese-400-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-400-normal.woff") format("woff");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* roboto-latin-ext-400-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-400-normal.woff") format("woff");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* roboto-latin-400-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-400-normal.woff") format("woff");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-ext-500-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 500;
|
||||
src: local("Roboto Medium"), local("Roboto-Medium"), url("node_modules/@fontsource/roboto/files/roboto-all-500-normal.woff") format("woff");
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-500-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-500-normal.woff") format("woff");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-500-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 500;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-500-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-500-normal.woff") format("woff");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* roboto-greek-ext-500-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 500;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-500-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-500-normal.woff") format("woff");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* roboto-greek-500-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 500;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-500-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-500-normal.woff") format("woff");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* roboto-vietnamese-500-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 500;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-500-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-500-normal.woff") format("woff");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* roboto-latin-ext-500-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 500;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-500-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-500-normal.woff") format("woff");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* roboto-latin-500-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 500;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-500-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-500-normal.woff") format("woff");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-ext-700-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src: local("Roboto Bold"), local("Roboto-Bold"), url("node_modules/@fontsource/roboto/files/roboto-all-700-normal.woff") format("woff");
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-ext-700-normal.woff") format("woff");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* roboto-cyrillic-700-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local("Roboto Light"), local("Roboto-Light"), url("node_modules/@fontsource/roboto/files/roboto-all-300-normal.woff") format("woff");
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-cyrillic-700-normal.woff") format("woff");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* roboto-greek-ext-700-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-ext-700-normal.woff") format("woff");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* roboto-greek-700-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-greek-700-normal.woff") format("woff");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* roboto-vietnamese-700-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-vietnamese-700-normal.woff") format("woff");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* roboto-latin-ext-700-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-ext-700-normal.woff") format("woff");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* roboto-latin-700-normal */
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto/files/roboto-latin-700-normal.woff") format("woff");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* roboto-condensed-cyrillic-ext-300-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-300-normal.woff") format("woff");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* roboto-condensed-cyrillic-300-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-300-normal.woff") format("woff");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* roboto-condensed-greek-ext-300-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-300-normal.woff") format("woff");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-greek-300-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-300-normal.woff") format("woff");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-vietnamese-300-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-300-normal.woff") format("woff");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* roboto-condensed-latin-ext-300-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-300-normal.woff") format("woff");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-latin-300-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-300-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-300-normal.woff") format("woff");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* roboto-condensed-cyrillic-ext-400-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-400-normal.woff") format("woff");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* roboto-condensed-cyrillic-400-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-400-normal.woff") format("woff");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* roboto-condensed-greek-ext-400-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-400-normal.woff") format("woff");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-greek-400-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-400-normal.woff") format("woff");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-vietnamese-400-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-400-normal.woff") format("woff");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* roboto-condensed-latin-ext-400-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-400-normal.woff") format("woff");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-latin-400-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-400-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-400-normal.woff") format("woff");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* roboto-condensed-cyrillic-ext-700-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-ext-700-normal.woff") format("woff");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* roboto-condensed-cyrillic-700-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-cyrillic-700-normal.woff") format("woff");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* roboto-condensed-greek-ext-700-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-ext-700-normal.woff") format("woff");
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-greek-700-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-greek-700-normal.woff") format("woff");
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-vietnamese-700-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-vietnamese-700-normal.woff") format("woff");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
|
||||
/* roboto-condensed-latin-ext-700-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-ext-700-normal.woff") format("woff");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* roboto-condensed-latin-700-normal */
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-700-normal.woff2") format("woff2"),
|
||||
url("node_modules/@fontsource/roboto-condensed/files/roboto-condensed-latin-700-normal.woff") format("woff");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>MagicMirror²</title>
|
||||
@@ -13,6 +13,7 @@
|
||||
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
|
||||
<link rel="stylesheet" type="text/css" href="css/main.css" />
|
||||
<link rel="stylesheet" type="text/css" href="fonts/roboto.css" />
|
||||
<link rel="stylesheet" type="text/css" href="vendor/node_modules/animate.css/animate.min.css" />
|
||||
<!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. -->
|
||||
|
||||
<script type="text/javascript">
|
||||
@@ -45,6 +46,7 @@
|
||||
<script type="text/javascript" src="#CONFIG_FILE#"></script>
|
||||
<script type="text/javascript" src="vendor/vendor.js"></script>
|
||||
<script type="text/javascript" src="modules/default/defaultmodules.js"></script>
|
||||
<script type="text/javascript" src="modules/default/utils.js"></script>
|
||||
<script type="text/javascript" src="js/logger.js"></script>
|
||||
<script type="text/javascript" src="translations/translations.js"></script>
|
||||
<script type="text/javascript" src="js/translator.js"></script>
|
||||
@@ -52,6 +54,7 @@
|
||||
<script type="text/javascript" src="js/module.js"></script>
|
||||
<script type="text/javascript" src="js/loader.js"></script>
|
||||
<script type="text/javascript" src="js/socketclient.js"></script>
|
||||
<script type="text/javascript" src="js/animateCSS.js"></script>
|
||||
<script type="text/javascript" src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
157
js/animateCSS.js
Normal file
157
js/animateCSS.js
Normal file
@@ -0,0 +1,157 @@
|
||||
/* enumeration of animations in Array **/
|
||||
const AnimateCSSIn = [
|
||||
// Attention seekers
|
||||
"bounce",
|
||||
"flash",
|
||||
"pulse",
|
||||
"rubberBand",
|
||||
"shakeX",
|
||||
"shakeY",
|
||||
"headShake",
|
||||
"swing",
|
||||
"tada",
|
||||
"wobble",
|
||||
"jello",
|
||||
"heartBeat",
|
||||
// Back entrances
|
||||
"backInDown",
|
||||
"backInLeft",
|
||||
"backInRight",
|
||||
"backInUp",
|
||||
// Bouncing entrances
|
||||
"bounceIn",
|
||||
"bounceInDown",
|
||||
"bounceInLeft",
|
||||
"bounceInRight",
|
||||
"bounceInUp",
|
||||
// Fading entrances
|
||||
"fadeIn",
|
||||
"fadeInDown",
|
||||
"fadeInDownBig",
|
||||
"fadeInLeft",
|
||||
"fadeInLeftBig",
|
||||
"fadeInRight",
|
||||
"fadeInRightBig",
|
||||
"fadeInUp",
|
||||
"fadeInUpBig",
|
||||
"fadeInTopLeft",
|
||||
"fadeInTopRight",
|
||||
"fadeInBottomLeft",
|
||||
"fadeInBottomRight",
|
||||
// Flippers
|
||||
"flip",
|
||||
"flipInX",
|
||||
"flipInY",
|
||||
// Lightspeed
|
||||
"lightSpeedInRight",
|
||||
"lightSpeedInLeft",
|
||||
// Rotating entrances
|
||||
"rotateIn",
|
||||
"rotateInDownLeft",
|
||||
"rotateInDownRight",
|
||||
"rotateInUpLeft",
|
||||
"rotateInUpRight",
|
||||
// Specials
|
||||
"jackInTheBox",
|
||||
"rollIn",
|
||||
// Zooming entrances
|
||||
"zoomIn",
|
||||
"zoomInDown",
|
||||
"zoomInLeft",
|
||||
"zoomInRight",
|
||||
"zoomInUp",
|
||||
// Sliding entrances
|
||||
"slideInDown",
|
||||
"slideInLeft",
|
||||
"slideInRight",
|
||||
"slideInUp"
|
||||
];
|
||||
|
||||
const AnimateCSSOut = [
|
||||
// Back exits
|
||||
"backOutDown",
|
||||
"backOutLeft",
|
||||
"backOutRight",
|
||||
"backOutUp",
|
||||
// Bouncing exits
|
||||
"bounceOut",
|
||||
"bounceOutDown",
|
||||
"bounceOutLeft",
|
||||
"bounceOutRight",
|
||||
"bounceOutUp",
|
||||
// Fading exits
|
||||
"fadeOut",
|
||||
"fadeOutDown",
|
||||
"fadeOutDownBig",
|
||||
"fadeOutLeft",
|
||||
"fadeOutLeftBig",
|
||||
"fadeOutRight",
|
||||
"fadeOutRightBig",
|
||||
"fadeOutUp",
|
||||
"fadeOutUpBig",
|
||||
"fadeOutTopLeft",
|
||||
"fadeOutTopRight",
|
||||
"fadeOutBottomRight",
|
||||
"fadeOutBottomLeft",
|
||||
// Flippers
|
||||
"flipOutX",
|
||||
"flipOutY",
|
||||
// Lightspeed
|
||||
"lightSpeedOutRight",
|
||||
"lightSpeedOutLeft",
|
||||
// Rotating exits
|
||||
"rotateOut",
|
||||
"rotateOutDownLeft",
|
||||
"rotateOutDownRight",
|
||||
"rotateOutUpLeft",
|
||||
"rotateOutUpRight",
|
||||
// Specials
|
||||
"hinge",
|
||||
"rollOut",
|
||||
// Zooming exits
|
||||
"zoomOut",
|
||||
"zoomOutDown",
|
||||
"zoomOutLeft",
|
||||
"zoomOutRight",
|
||||
"zoomOutUp",
|
||||
// Sliding exits
|
||||
"slideOutDown",
|
||||
"slideOutLeft",
|
||||
"slideOutRight",
|
||||
"slideOutUp"
|
||||
];
|
||||
|
||||
/**
|
||||
* Create an animation with Animate CSS
|
||||
* @param {string} [element] div element to animate.
|
||||
* @param {string} [animation] animation name.
|
||||
* @param {number} [animationTime] animation duration.
|
||||
*/
|
||||
function addAnimateCSS (element, animation, animationTime) {
|
||||
const animationName = `animate__${animation}`;
|
||||
const node = document.getElementById(element);
|
||||
if (!node) {
|
||||
// don't execute animate: we don't find div
|
||||
Log.warn("addAnimateCSS: node not found for", element);
|
||||
return;
|
||||
}
|
||||
node.style.setProperty("--animate-duration", `${animationTime}s`);
|
||||
node.classList.add("animate__animated", animationName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an animation with Animate CSS
|
||||
* @param {string} [element] div element to animate.
|
||||
* @param {string} [animation] animation name.
|
||||
*/
|
||||
function removeAnimateCSS (element, animation) {
|
||||
const animationName = `animate__${animation}`;
|
||||
const node = document.getElementById(element);
|
||||
if (!node) {
|
||||
// don't execute animate: we don't find div
|
||||
Log.warn("removeAnimateCSS: node not found for", element);
|
||||
return;
|
||||
}
|
||||
node.classList.remove("animate__animated", animationName);
|
||||
node.style.removeProperty("--animate-duration");
|
||||
}
|
72
js/app.js
72
js/app.js
@@ -1,17 +1,11 @@
|
||||
/* MagicMirror²
|
||||
* The Core App (Server)
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
// Alias modules mentioned in package.js under _moduleAliases.
|
||||
require("module-alias/register");
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const envsub = require("envsub");
|
||||
const Log = require("logger");
|
||||
|
||||
const Server = require(`${__dirname}/server`);
|
||||
const Utils = require(`${__dirname}/utils`);
|
||||
const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`);
|
||||
@@ -20,15 +14,18 @@ const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`)
|
||||
global.version = require(`${__dirname}/../package.json`).version;
|
||||
Log.log(`Starting MagicMirror: v${global.version}`);
|
||||
|
||||
// Log system information.
|
||||
Utils.logSystemInformation();
|
||||
|
||||
// global absolute root path
|
||||
global.root_path = path.resolve(`${__dirname}/../`);
|
||||
|
||||
if (process.env.MM_CONFIG_FILE) {
|
||||
global.configuration_file = process.env.MM_CONFIG_FILE;
|
||||
global.configuration_file = process.env.MM_CONFIG_FILE.replace(`${global.root_path}/`, "");
|
||||
}
|
||||
|
||||
// FIXME: Hotfix Pull Request
|
||||
// https://github.com/MichMich/MagicMirror/pull/673
|
||||
// https://github.com/MagicMirrorOrg/MagicMirror/pull/673
|
||||
if (process.env.MM_PORT) {
|
||||
global.mmPort = process.env.MM_PORT;
|
||||
}
|
||||
@@ -36,17 +33,20 @@ if (process.env.MM_PORT) {
|
||||
// The next part is here to prevent a major exception when there
|
||||
// is no internet connection. This could probable be solved better.
|
||||
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("If you think this really is an issue, please open an issue on GitHub: https://github.com/MichMich/MagicMirror/issues");
|
||||
// ignore strange exceptions under aarch64 coming from systeminformation:
|
||||
if (!err.stack.includes("node_modules/systeminformation")) {
|
||||
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("If you think this really is an issue, please open an issue on GitHub: https://github.com/MagicMirrorOrg/MagicMirror/issues");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The core app.
|
||||
* @class
|
||||
*/
|
||||
function App() {
|
||||
function App () {
|
||||
let nodeHelpers = [];
|
||||
let httpServer;
|
||||
|
||||
@@ -55,9 +55,13 @@ function App() {
|
||||
* @async
|
||||
* @returns {Promise<object>} the loaded config or the defaults if something goes wrong
|
||||
*/
|
||||
async function loadConfig() {
|
||||
async function loadConfig () {
|
||||
Log.log("Loading config ...");
|
||||
const defaults = require(`${__dirname}/defaults`);
|
||||
if (process.env.JEST_WORKER_ID !== undefined) {
|
||||
// if we are running with jest
|
||||
defaults.address = "0.0.0.0";
|
||||
}
|
||||
|
||||
// For this check proposed to TestSuite
|
||||
// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8
|
||||
@@ -69,7 +73,7 @@ function App() {
|
||||
fs.accessSync(templateFile, fs.F_OK);
|
||||
} catch (err) {
|
||||
templateFile = null;
|
||||
Log.debug("config template file not exists, no envsubst");
|
||||
Log.log("config template file not exists, no envsubst");
|
||||
}
|
||||
|
||||
if (templateFile) {
|
||||
@@ -90,7 +94,7 @@ function App() {
|
||||
envFiles.push(configEnvFile);
|
||||
}
|
||||
} catch (err) {
|
||||
Log.debug(`${configEnvFile} does not exist. ${err.message}`);
|
||||
Log.log(`${configEnvFile} does not exist. ${err.message}`);
|
||||
}
|
||||
|
||||
let options = {
|
||||
@@ -115,15 +119,18 @@ function App() {
|
||||
try {
|
||||
fs.accessSync(configFilename, fs.F_OK);
|
||||
const c = require(configFilename);
|
||||
if (Object.keys(c).length === 0) {
|
||||
Log.error("WARNING! Config file appears empty, maybe missing module.exports last line?");
|
||||
}
|
||||
checkDeprecatedOptions(c);
|
||||
return Object.assign(defaults, c);
|
||||
} catch (e) {
|
||||
if (e.code === "ENOENT") {
|
||||
Log.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
|
||||
Log.error("WARNING! Could not find config file. Please create one. Starting with default configuration.");
|
||||
} else if (e instanceof ReferenceError || e instanceof SyntaxError) {
|
||||
Log.error(Utils.colors.error(`WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: ${e.stack}`));
|
||||
Log.error(`WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: ${e.stack}`);
|
||||
} else {
|
||||
Log.error(Utils.colors.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`));
|
||||
Log.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,13 +142,13 @@ function App() {
|
||||
* if it encounters one option from the deprecated.js list
|
||||
* @param {object} userConfig The user config
|
||||
*/
|
||||
function checkDeprecatedOptions(userConfig) {
|
||||
function checkDeprecatedOptions (userConfig) {
|
||||
const deprecated = require(`${global.root_path}/js/deprecated`);
|
||||
const deprecatedOptions = deprecated.configs;
|
||||
|
||||
const usedDeprecated = deprecatedOptions.filter((option) => userConfig.hasOwnProperty(option));
|
||||
if (usedDeprecated.length > 0) {
|
||||
Log.warn(Utils.colors.warn(`WARNING! Your config is using deprecated options: ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`));
|
||||
Log.warn(`WARNING! Your config is using deprecated options: ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +156,7 @@ function App() {
|
||||
* Loads a specific module.
|
||||
* @param {string} module The name of the module (including subpath).
|
||||
*/
|
||||
function loadModule(module) {
|
||||
function loadModule (module) {
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = `${__dirname}/../modules/${module}`;
|
||||
@@ -203,7 +210,7 @@ function App() {
|
||||
* @param {Module[]} modules All modules to be loaded
|
||||
* @returns {Promise} A promise that is resolved when all modules been loaded
|
||||
*/
|
||||
async function loadModules(modules) {
|
||||
async function loadModules (modules) {
|
||||
Log.log("Loading module helpers ...");
|
||||
|
||||
for (let module of modules) {
|
||||
@@ -220,7 +227,7 @@ function App() {
|
||||
* @returns {number} A positive number if a is larger than b, a negative
|
||||
* number if a is smaller and 0 if they are the same
|
||||
*/
|
||||
function cmpVersions(a, b) {
|
||||
function cmpVersions (a, b) {
|
||||
let i, diff;
|
||||
const regExStrip0 = /(\.0+)+$/;
|
||||
const segmentsA = a.replace(regExStrip0, "").split(".");
|
||||
@@ -250,8 +257,15 @@ function App() {
|
||||
|
||||
let modules = [];
|
||||
for (const module of config.modules) {
|
||||
if (!modules.includes(module.module) && !module.disabled) {
|
||||
modules.push(module.module);
|
||||
if (module.disabled) continue;
|
||||
if (module.module) {
|
||||
if (Utils.moduleHasValidPosition(module.position) || typeof (module.position) === "undefined") {
|
||||
modules.push(module.module);
|
||||
} else {
|
||||
Log.warn("Invalid module position found for this configuration:", module);
|
||||
}
|
||||
} else {
|
||||
Log.warn("No module name found for this configuration:", module);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,26 +1,23 @@
|
||||
/* MagicMirror²
|
||||
*
|
||||
* Check the configuration file for errors
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
const colors = require("ansis");
|
||||
const { Linter } = require("eslint");
|
||||
|
||||
const linter = new Linter();
|
||||
|
||||
const Ajv = require("ajv");
|
||||
|
||||
const ajv = new Ajv();
|
||||
|
||||
const rootPath = path.resolve(`${__dirname}/../`);
|
||||
const Log = require(`${rootPath}/js/logger.js`);
|
||||
const Utils = require(`${rootPath}/js/utils.js`);
|
||||
|
||||
/**
|
||||
* Returns a string with path of configuration file.
|
||||
* Check if set by environment variable MM_CONFIG_FILE
|
||||
* @returns {string} path and filename of the config file
|
||||
*/
|
||||
function getConfigFile() {
|
||||
function getConfigFile () {
|
||||
// FIXME: This function should be in core. Do you want refactor me ;) ?, be good!
|
||||
return path.resolve(process.env.MM_CONFIG_FILE || `${rootPath}/config/config.js`);
|
||||
}
|
||||
@@ -28,12 +25,12 @@ function getConfigFile() {
|
||||
/**
|
||||
* Checks the config file using eslint.
|
||||
*/
|
||||
function checkConfigFile() {
|
||||
function checkConfigFile () {
|
||||
const configFileName = getConfigFile();
|
||||
|
||||
// Check if file is present
|
||||
if (fs.existsSync(configFileName) === false) {
|
||||
Log.error(Utils.colors.error("File not found: "), configFileName);
|
||||
Log.error(`File not found: ${configFileName}`);
|
||||
throw new Error("No config file present!");
|
||||
}
|
||||
|
||||
@@ -41,12 +38,12 @@ function checkConfigFile() {
|
||||
try {
|
||||
fs.accessSync(configFileName, fs.F_OK);
|
||||
} catch (e) {
|
||||
Log.error(Utils.colors.error(e));
|
||||
Log.error(e);
|
||||
throw new Error("No permission to access config file!");
|
||||
}
|
||||
|
||||
// Validate syntax of the configuration file.
|
||||
Log.info(Utils.colors.info("Checking file... "), configFileName);
|
||||
Log.info("Checking file... ", configFileName);
|
||||
|
||||
// I'm not sure if all ever is utf-8
|
||||
const configFile = fs.readFileSync(configFileName, "utf-8");
|
||||
@@ -59,13 +56,75 @@ function checkConfigFile() {
|
||||
});
|
||||
|
||||
if (errors.length === 0) {
|
||||
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
|
||||
Log.info(colors.green("Your configuration file doesn't contain syntax errors :)"));
|
||||
} else {
|
||||
Log.error(Utils.colors.error("Your configuration file contains syntax errors :("));
|
||||
Log.error(colors.red("Your configuration file contains syntax errors :("));
|
||||
|
||||
for (const error of errors) {
|
||||
Log.error(`Line ${error.line} column ${error.column}: ${error.message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Log.info("Checking modules structure configuration... ");
|
||||
|
||||
// Make Ajv schema confguration of modules config
|
||||
// only scan "module" and "position"
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
modules: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
module: {
|
||||
type: "string"
|
||||
},
|
||||
position: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"top_bar",
|
||||
"top_left",
|
||||
"top_center",
|
||||
"top_right",
|
||||
"upper_third",
|
||||
"middle_center",
|
||||
"lower_third",
|
||||
"bottom_left",
|
||||
"bottom_center",
|
||||
"bottom_right",
|
||||
"bottom_bar",
|
||||
"fullscreen_above",
|
||||
"fullscreen_below"
|
||||
]
|
||||
}
|
||||
},
|
||||
required: ["module"]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// scan all modules
|
||||
const validate = ajv.compile(schema);
|
||||
const data = require(configFileName);
|
||||
|
||||
const valid = validate(data);
|
||||
if (!valid) {
|
||||
let module = validate.errors[0].instancePath.split("/")[2];
|
||||
let position = validate.errors[0].instancePath.split("/")[3];
|
||||
|
||||
Log.error(colors.red("This module configuration contains errors:"));
|
||||
Log.error(data.modules[module]);
|
||||
if (position) {
|
||||
Log.error(colors.red(`${position}: ${validate.errors[0].message}`));
|
||||
Log.error(validate.errors[0].params.allowedValues);
|
||||
} else {
|
||||
Log.error(colors.red(validate.errors[0].message));
|
||||
}
|
||||
} else {
|
||||
Log.info(colors.green("Your modules structure configuration doesn't contain errors :)"));
|
||||
}
|
||||
}
|
||||
|
||||
|
40
js/class.js
40
js/class.js
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
(function () {
|
||||
let initializing = false;
|
||||
const fnTest = /xyz/.test(function () {
|
||||
const fnTest = (/xyz/).test(function () {
|
||||
xyz;
|
||||
})
|
||||
? /\b_super\b/
|
||||
@@ -36,31 +36,31 @@
|
||||
// Copy the properties over onto the new prototype
|
||||
for (const name in prop) {
|
||||
// Check if we're overwriting an existing function
|
||||
prototype[name] =
|
||||
typeof prop[name] === "function" && typeof _super[name] === "function" && fnTest.test(prop[name])
|
||||
prototype[name]
|
||||
= typeof prop[name] === "function" && typeof _super[name] === "function" && fnTest.test(prop[name])
|
||||
? (function (name, fn) {
|
||||
return function () {
|
||||
const tmp = this._super;
|
||||
return function () {
|
||||
const tmp = this._super;
|
||||
|
||||
// Add a new ._super() method that is the same method
|
||||
// but on the super-class
|
||||
this._super = _super[name];
|
||||
// Add a new ._super() method that is the same method
|
||||
// but on the super-class
|
||||
this._super = _super[name];
|
||||
|
||||
// The method only need to be bound temporarily, so we
|
||||
// remove it when we're done executing
|
||||
const ret = fn.apply(this, arguments);
|
||||
this._super = tmp;
|
||||
// The method only need to be bound temporarily, so we
|
||||
// remove it when we're done executing
|
||||
const ret = fn.apply(this, arguments);
|
||||
this._super = tmp;
|
||||
|
||||
return ret;
|
||||
};
|
||||
})(name, prop[name])
|
||||
return ret;
|
||||
};
|
||||
}(name, prop[name]))
|
||||
: prop[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* The dummy class constructor
|
||||
*/
|
||||
function Class() {
|
||||
function Class () {
|
||||
// All construction is actually done in the init method
|
||||
if (!initializing && this.init) {
|
||||
this.init.apply(this, arguments);
|
||||
@@ -78,18 +78,22 @@
|
||||
|
||||
return Class;
|
||||
};
|
||||
})();
|
||||
}());
|
||||
|
||||
/**
|
||||
* Define the clone method for later use. Helper Method.
|
||||
* @param {object} obj Object to be cloned
|
||||
* @returns {object} the cloned object
|
||||
*/
|
||||
function cloneObject(obj) {
|
||||
function cloneObject (obj) {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (obj.constructor.name === "RegExp") {
|
||||
return new RegExp(obj);
|
||||
}
|
||||
|
||||
const temp = obj.constructor(); // give temp the original obj's constructor
|
||||
for (const key in obj) {
|
||||
temp[key] = cloneObject(obj[key]);
|
||||
|
@@ -1,11 +1,5 @@
|
||||
/* global mmPort */
|
||||
|
||||
/* MagicMirror²
|
||||
* Config Defaults
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const address = "localhost";
|
||||
let port = 8080;
|
||||
if (typeof mmPort !== "undefined") {
|
||||
@@ -26,9 +20,14 @@ const defaults = {
|
||||
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
|
||||
// e.g. you need to add `frameguard: false` for embedding MagicMirror in another website, see https://github.com/MagicMirrorOrg/MagicMirror/issues/2847
|
||||
httpHeaders: { contentSecurityPolicy: false, crossOriginOpenerPolicy: false, crossOriginEmbedderPolicy: false, crossOriginResourcePolicy: false, originAgentCluster: false },
|
||||
|
||||
// properties for checking if server is alive and has same startup-timestamp, the check is per default enabled
|
||||
// (interval 30 seconds). If startup-timestamp has changed the client reloads the magicmirror webpage.
|
||||
checkServerInterval: 30 * 1000,
|
||||
reloadAfterServerRestart: false,
|
||||
|
||||
modules: [
|
||||
{
|
||||
module: "updatenotification",
|
||||
|
@@ -1,11 +1,3 @@
|
||||
/* MagicMirror² Deprecated Config Options List
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* Olex S. original idea this deprecated option
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
configs: ["kioskmode"]
|
||||
};
|
||||
|
@@ -8,9 +8,10 @@ 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.
|
||||
// Per default electron is started with --disable-gpu flag, if you want the gpu enabled,
|
||||
// you must set the env var ELECTRON_ENABLE_GPU=1 on startup.
|
||||
// See https://www.electronjs.org/docs/latest/tutorial/offscreen-rendering for more info.
|
||||
if (process.env.ELECTRON_DISABLE_GPU !== undefined) {
|
||||
if (process.env.ELECTRON_ENABLE_GPU !== "1") {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
@@ -24,12 +25,22 @@ let mainWindow;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function createWindow() {
|
||||
function createWindow () {
|
||||
// see https://www.electronjs.org/docs/latest/api/screen
|
||||
// Create a window that fills the screen's available work area.
|
||||
let electronSize = (800, 600);
|
||||
try {
|
||||
electronSize = electron.screen.getPrimaryDisplay().workAreaSize;
|
||||
} catch {
|
||||
Log.warn("Could not get display size, using defaults ...");
|
||||
}
|
||||
|
||||
let electronSwitchesDefaults = ["autoplay-policy", "no-user-gesture-required"];
|
||||
app.commandLine.appendSwitch(...new Set(electronSwitchesDefaults, config.electronSwitches));
|
||||
let electronOptionsDefaults = {
|
||||
width: 800,
|
||||
height: 600,
|
||||
width: electronSize.width,
|
||||
height: electronSize.height,
|
||||
icon: "mm2.png",
|
||||
x: 0,
|
||||
y: 0,
|
||||
darkTheme: true,
|
||||
@@ -50,6 +61,7 @@ function createWindow() {
|
||||
electronOptionsDefaults.frame = false;
|
||||
electronOptionsDefaults.transparent = true;
|
||||
electronOptionsDefaults.hasShadow = false;
|
||||
electronOptionsDefaults.fullscreen = true;
|
||||
}
|
||||
|
||||
const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
|
||||
@@ -67,8 +79,9 @@ function createWindow() {
|
||||
prefix = "http://";
|
||||
}
|
||||
|
||||
let address = (config.address === void 0) | (config.address === "") ? (config.address = "localhost") : config.address;
|
||||
mainWindow.loadURL(`${prefix}${address}:${config.port}`);
|
||||
let address = (config.address === void 0) | (config.address === "") | (config.address === "0.0.0.0") ? (config.address = "localhost") : config.address;
|
||||
const port = process.env.MM_PORT || config.port;
|
||||
mainWindow.loadURL(`${prefix}${address}:${port}`);
|
||||
|
||||
// Open the DevTools if run with "npm start dev"
|
||||
if (process.argv.includes("dev")) {
|
||||
@@ -110,18 +123,17 @@ function createWindow() {
|
||||
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])));
|
||||
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])));
|
||||
curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !(/content-security-policy/i).test(header[0])));
|
||||
}
|
||||
|
||||
callback({ responseHeaders: curHeaders });
|
||||
});
|
||||
|
||||
mainWindow.once("ready-to-show", () => {
|
||||
mainWindow.setFullScreen(true);
|
||||
mainWindow.show();
|
||||
});
|
||||
}
|
||||
@@ -168,6 +180,13 @@ app.on("certificate-error", (event, webContents, url, error, certificate, callba
|
||||
callback(true);
|
||||
});
|
||||
|
||||
if (process.env.clientonly) {
|
||||
app.whenReady().then(() => {
|
||||
Log.log("Launching client viewer application.");
|
||||
createWindow();
|
||||
});
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
|
27
js/fetch.js
27
js/fetch.js
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* 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;
|
19
js/loader.js
19
js/loader.js
@@ -1,12 +1,7 @@
|
||||
/* global defaultModules, vendor */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module and File loaders.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Loader = (function () {
|
||||
|
||||
/* Create helper variables */
|
||||
|
||||
const loadedModuleFiles = [];
|
||||
@@ -55,7 +50,8 @@ const Loader = (function () {
|
||||
* @returns {object[]} module data as configured in config
|
||||
*/
|
||||
const getAllModules = function () {
|
||||
return config.modules;
|
||||
const AllModules = config.modules.filter((module) => (module.module !== undefined) && (MM.getAvailableModulePositions.indexOf(module.position) > -1 || typeof (module.position) === "undefined"));
|
||||
return AllModules;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -88,6 +84,8 @@ const Loader = (function () {
|
||||
path: `${moduleFolder}/`,
|
||||
file: `${moduleName}.js`,
|
||||
position: moduleData.position,
|
||||
animateIn: moduleData.animateIn,
|
||||
animateOut: moduleData.animateOut,
|
||||
hiddenOnStartup: moduleData.hiddenOnStartup,
|
||||
header: moduleData.header,
|
||||
configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false,
|
||||
@@ -194,10 +192,11 @@ const Loader = (function () {
|
||||
|
||||
/* Public Methods */
|
||||
return {
|
||||
|
||||
/**
|
||||
* Load all modules as defined in the config.
|
||||
*/
|
||||
loadModules: async function () {
|
||||
async loadModules () {
|
||||
let moduleData = getModuleData();
|
||||
|
||||
/**
|
||||
@@ -228,7 +227,7 @@ const Loader = (function () {
|
||||
* @param {Module} module The module that calls the loadFile function.
|
||||
* @returns {Promise} resolved when the file is loaded
|
||||
*/
|
||||
loadFileForModule: async function (fileName, module) {
|
||||
async loadFileForModule (fileName, module) {
|
||||
if (loadedFiles.indexOf(fileName.toLowerCase()) !== -1) {
|
||||
Log.log(`File already loaded: ${fileName}`);
|
||||
return;
|
||||
@@ -254,4 +253,4 @@ const Loader = (function () {
|
||||
return loadFile(module.file(fileName));
|
||||
}
|
||||
};
|
||||
})();
|
||||
}());
|
||||
|
71
js/logger.js
71
js/logger.js
@@ -1,19 +1,40 @@
|
||||
/* MagicMirror²
|
||||
* Log
|
||||
*
|
||||
* This logger is very simple, but needs to be extended.
|
||||
* This system can eventually be used to push the log messages to an external target.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
// This logger is very simple, but needs to be extended.
|
||||
(function (root, factory) {
|
||||
if (typeof exports === "object") {
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
const colors = require("ansis");
|
||||
|
||||
// add timestamps in front of log messages
|
||||
require("console-stamp")(console, {
|
||||
pattern: "yyyy-mm-dd HH:MM:ss.l",
|
||||
include: ["debug", "log", "info", "warn", "error"]
|
||||
format: ":date(yyyy-mm-dd HH:MM:ss.l) :label(7) :msg",
|
||||
tokens: {
|
||||
label: (arg) => {
|
||||
const { method, defaultTokens } = arg;
|
||||
let label = defaultTokens.label(arg);
|
||||
if (method === "error") {
|
||||
label = colors.red(label);
|
||||
} else if (method === "warn") {
|
||||
label = colors.yellow(label);
|
||||
} else if (method === "debug") {
|
||||
label = colors.bgBlue(label);
|
||||
} else if (method === "info") {
|
||||
label = colors.blue(label);
|
||||
}
|
||||
return label;
|
||||
},
|
||||
msg: (arg) => {
|
||||
const { method, defaultTokens } = arg;
|
||||
let msg = defaultTokens.msg(arg);
|
||||
if (method === "error") {
|
||||
msg = colors.red(msg);
|
||||
} else if (method === "warn") {
|
||||
msg = colors.yellow(msg);
|
||||
} else if (method === "info") {
|
||||
msg = colors.blue(msg);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Node, CommonJS-like
|
||||
@@ -22,7 +43,7 @@
|
||||
// Browser globals (root is window)
|
||||
root.Log = factory(root.config);
|
||||
}
|
||||
})(this, function (config) {
|
||||
}(this, function (config) {
|
||||
let logLevel;
|
||||
let enableLog;
|
||||
if (typeof exports === "object") {
|
||||
@@ -50,7 +71,7 @@
|
||||
|
||||
logLevel.setLogLevel = function (newLevel) {
|
||||
if (newLevel) {
|
||||
Object.keys(logLevel).forEach(function (key, index) {
|
||||
Object.keys(logLevel).forEach(function (key) {
|
||||
if (!newLevel.includes(key.toLocaleUpperCase())) {
|
||||
logLevel[key] = function () {};
|
||||
}
|
||||
@@ -59,21 +80,21 @@
|
||||
};
|
||||
} else {
|
||||
logLevel = {
|
||||
debug: function () {},
|
||||
log: function () {},
|
||||
info: function () {},
|
||||
warn: function () {},
|
||||
error: function () {},
|
||||
group: function () {},
|
||||
groupCollapsed: function () {},
|
||||
groupEnd: function () {},
|
||||
time: function () {},
|
||||
timeEnd: function () {},
|
||||
timeStamp: function () {}
|
||||
debug () {},
|
||||
log () {},
|
||||
info () {},
|
||||
warn () {},
|
||||
error () {},
|
||||
group () {},
|
||||
groupCollapsed () {},
|
||||
groupEnd () {},
|
||||
time () {},
|
||||
timeEnd () {},
|
||||
timeStamp () {}
|
||||
};
|
||||
|
||||
logLevel.setLogLevel = function () {};
|
||||
}
|
||||
|
||||
return logLevel;
|
||||
});
|
||||
}));
|
||||
|
250
js/main.js
250
js/main.js
@@ -1,11 +1,5 @@
|
||||
/* global Loader, defaults, Translator */
|
||||
/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut */
|
||||
|
||||
/* MagicMirror²
|
||||
* Main System
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const MM = (function () {
|
||||
let modules = [];
|
||||
|
||||
@@ -22,6 +16,10 @@ const MM = (function () {
|
||||
return;
|
||||
}
|
||||
|
||||
let haveAnimateIn = null;
|
||||
// check if have valid animateIn in module definition (module.data.animateIn)
|
||||
if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateIn = module.data.animateIn;
|
||||
|
||||
const wrapper = selectWrapper(module.data.position);
|
||||
|
||||
const dom = document.createElement("div");
|
||||
@@ -50,7 +48,12 @@ const MM = (function () {
|
||||
moduleContent.className = "module-content";
|
||||
dom.appendChild(moduleContent);
|
||||
|
||||
const domCreationPromise = updateDom(module, 0);
|
||||
// create the domCreationPromise with AnimateCSS (with animateIn of module definition)
|
||||
// or just display it
|
||||
var domCreationPromise;
|
||||
if (haveAnimateIn) domCreationPromise = updateDom(module, { options: { speed: 1000, animate: { in: haveAnimateIn } } }, true);
|
||||
else domCreationPromise = updateDom(module, 0);
|
||||
|
||||
domCreationPromises.push(domCreationPromise);
|
||||
domCreationPromise
|
||||
.then(function () {
|
||||
@@ -101,11 +104,30 @@ const MM = (function () {
|
||||
/**
|
||||
* Update the dom for a specific module.
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The (optional) number of microseconds for the animation.
|
||||
* @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates)
|
||||
* @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start of MagicMirror)
|
||||
* @returns {Promise} Resolved when the dom is fully updated.
|
||||
*/
|
||||
const updateDom = function (module, speed) {
|
||||
const updateDom = function (module, updateOptions, createAnimatedDom = false) {
|
||||
return new Promise(function (resolve) {
|
||||
let speed = updateOptions;
|
||||
let animateOut = null;
|
||||
let animateIn = null;
|
||||
if (typeof updateOptions === "object") {
|
||||
if (typeof updateOptions.options === "object" && updateOptions.options.speed !== undefined) {
|
||||
speed = updateOptions.options.speed;
|
||||
Log.debug(`updateDom: ${module.identifier} Has speed in object: ${speed}`);
|
||||
if (typeof updateOptions.options.animate === "object") {
|
||||
animateOut = updateOptions.options.animate.out;
|
||||
animateIn = updateOptions.options.animate.in;
|
||||
Log.debug(`updateDom: ${module.identifier} Has animate in object: out->${animateOut}, in->${animateIn}`);
|
||||
}
|
||||
} else {
|
||||
Log.debug(`updateDom: ${module.identifier} Has no speed in object`);
|
||||
speed = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const newHeader = module.getHeader();
|
||||
let newContentPromise = module.getDom();
|
||||
|
||||
@@ -116,7 +138,7 @@ const MM = (function () {
|
||||
|
||||
newContentPromise
|
||||
.then(function (newContent) {
|
||||
const updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
|
||||
const updatePromise = updateDomWithContent(module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom);
|
||||
|
||||
updatePromise.then(resolve).catch(Log.error);
|
||||
})
|
||||
@@ -130,9 +152,12 @@ const MM = (function () {
|
||||
* @param {number} [speed] The (optional) number of microseconds for the animation.
|
||||
* @param {string} newHeader The new header that is generated.
|
||||
* @param {HTMLElement} newContent The new content that is generated.
|
||||
* @param {string} [animateOut] AnimateCss animation name before hidden
|
||||
* @param {string} [animateIn] AnimateCss animation name on show
|
||||
* @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start)
|
||||
* @returns {Promise} Resolved when the module dom has been updated.
|
||||
*/
|
||||
const updateDomWithContent = function (module, speed, newHeader, newContent) {
|
||||
const updateDomWithContent = function (module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom = false) {
|
||||
return new Promise(function (resolve) {
|
||||
if (module.hidden || !speed) {
|
||||
updateModuleContent(module, newHeader, newContent);
|
||||
@@ -151,13 +176,28 @@ const MM = (function () {
|
||||
return;
|
||||
}
|
||||
|
||||
hideModule(module, speed / 2, function () {
|
||||
if (createAnimatedDom && animateIn !== null) {
|
||||
Log.debug(`${module.identifier} createAnimatedDom (${animateIn})`);
|
||||
updateModuleContent(module, newHeader, newContent);
|
||||
if (!module.hidden) {
|
||||
showModule(module, speed / 2);
|
||||
showModule(module, speed, null, { animate: animateIn });
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
hideModule(
|
||||
module,
|
||||
speed / 2,
|
||||
function () {
|
||||
updateModuleContent(module, newHeader, newContent);
|
||||
if (!module.hidden) {
|
||||
showModule(module, speed / 2, null, { animate: animateIn });
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
{ animate: animateOut }
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -234,24 +274,65 @@ const MM = (function () {
|
||||
|
||||
const moduleWrapper = document.getElementById(module.identifier);
|
||||
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 () {
|
||||
// To not take up any space, we just make the position absolute.
|
||||
// since it's fade out anyway, we can see it lay above or
|
||||
// below other modules. This works way better than adjusting
|
||||
// the .display property.
|
||||
moduleWrapper.style.position = "fixed";
|
||||
// reset all animations if needed
|
||||
if (module.hasAnimateOut) {
|
||||
removeAnimateCSS(module.identifier, module.hasAnimateOut);
|
||||
Log.debug(`${module.identifier} Force remove animateOut (in hide): ${module.hasAnimateOut}`);
|
||||
module.hasAnimateOut = false;
|
||||
}
|
||||
if (module.hasAnimateIn) {
|
||||
removeAnimateCSS(module.identifier, module.hasAnimateIn);
|
||||
Log.debug(`${module.identifier} Force remove animateIn (in hide): ${module.hasAnimateIn}`);
|
||||
module.hasAnimateIn = false;
|
||||
}
|
||||
// haveAnimateName for verify if we are using AninateCSS library
|
||||
// we check AnimateCSSOut Array for validate it
|
||||
// and finaly return the animate name or `null` (for default MM² animation)
|
||||
let haveAnimateName = null;
|
||||
// check if have valid animateOut in module definition (module.data.animateOut)
|
||||
if (module.data.animateOut && AnimateCSSOut.indexOf(module.data.animateOut) !== -1) haveAnimateName = module.data.animateOut;
|
||||
// can't be override with options.animate
|
||||
else if (options.animate && AnimateCSSOut.indexOf(options.animate) !== -1) haveAnimateName = options.animate;
|
||||
|
||||
updateWrapperStates();
|
||||
if (haveAnimateName) {
|
||||
// with AnimateCSS
|
||||
Log.debug(`${module.identifier} Has animateOut: ${haveAnimateName}`);
|
||||
module.hasAnimateOut = haveAnimateName;
|
||||
addAnimateCSS(module.identifier, haveAnimateName, speed / 1000);
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
removeAnimateCSS(module.identifier, haveAnimateName);
|
||||
Log.debug(`${module.identifier} Remove animateOut: ${module.hasAnimateOut}`);
|
||||
// AnimateCSS is now done
|
||||
moduleWrapper.style.opacity = 0;
|
||||
moduleWrapper.classList.add("hidden");
|
||||
moduleWrapper.style.position = "fixed";
|
||||
module.hasAnimateOut = false;
|
||||
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
updateWrapperStates();
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
} else {
|
||||
// default MM² Animate
|
||||
moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
|
||||
moduleWrapper.style.opacity = 0;
|
||||
moduleWrapper.classList.add("hidden");
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
// To not take up any space, we just make the position absolute.
|
||||
// since it's fade out anyway, we can see it lay above or
|
||||
// below other modules. This works way better than adjusting
|
||||
// the .display property.
|
||||
moduleWrapper.style.position = "fixed";
|
||||
|
||||
updateWrapperStates();
|
||||
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
}
|
||||
} else {
|
||||
// invoke callback even if no content, issue 1308
|
||||
if (typeof callback === "function") {
|
||||
@@ -285,6 +366,17 @@ const MM = (function () {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// reset all animations if needed
|
||||
if (module.hasAnimateOut) {
|
||||
removeAnimateCSS(module.identifier, module.hasAnimateOut);
|
||||
Log.debug(`${module.identifier} Force remove animateOut (in show): ${module.hasAnimateOut}`);
|
||||
module.hasAnimateOut = false;
|
||||
}
|
||||
if (module.hasAnimateIn) {
|
||||
removeAnimateCSS(module.identifier, module.hasAnimateIn);
|
||||
Log.debug(`${module.identifier} Force remove animateIn (in show): ${module.hasAnimateIn}`);
|
||||
module.hasAnimateIn = false;
|
||||
}
|
||||
|
||||
module.hidden = false;
|
||||
|
||||
@@ -296,7 +388,18 @@ const MM = (function () {
|
||||
|
||||
const moduleWrapper = document.getElementById(module.identifier);
|
||||
if (moduleWrapper !== null) {
|
||||
moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
|
||||
clearTimeout(module.showHideTimer);
|
||||
|
||||
// haveAnimateName for verify if we are using AninateCSS library
|
||||
// we check AnimateCSSIn Array for validate it
|
||||
// and finaly return the animate name or `null` (for default MM² animation)
|
||||
let haveAnimateName = null;
|
||||
// check if have valid animateOut in module definition (module.data.animateIn)
|
||||
if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateName = module.data.animateIn;
|
||||
// can't be override with options.animate
|
||||
else if (options.animate && AnimateCSSIn.indexOf(options.animate) !== -1) haveAnimateName = options.animate;
|
||||
|
||||
if (!haveAnimateName) moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
|
||||
// Restore the position. See hideModule() for more info.
|
||||
moduleWrapper.style.position = "static";
|
||||
moduleWrapper.classList.remove("hidden");
|
||||
@@ -307,12 +410,27 @@ const MM = (function () {
|
||||
const dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
|
||||
moduleWrapper.style.opacity = 1;
|
||||
|
||||
clearTimeout(module.showHideTimer);
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
if (haveAnimateName) {
|
||||
// with AnimateCSS
|
||||
Log.debug(`${module.identifier} Has animateIn: ${haveAnimateName}`);
|
||||
module.hasAnimateIn = haveAnimateName;
|
||||
addAnimateCSS(module.identifier, haveAnimateName, speed / 1000);
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
removeAnimateCSS(module.identifier, haveAnimateName);
|
||||
Log.debug(`${module.identifier} Remove animateIn: ${haveAnimateName}`);
|
||||
module.hasAnimateIn = false;
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
} else {
|
||||
// default MM² Animate
|
||||
module.showHideTimer = setTimeout(function () {
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}, speed);
|
||||
}
|
||||
} else {
|
||||
// invoke callback
|
||||
if (typeof callback === "function") {
|
||||
@@ -332,10 +450,10 @@ const MM = (function () {
|
||||
* an ugly top margin. By using this function, the top bar will be hidden if the
|
||||
* update notification is not visible.
|
||||
*/
|
||||
const updateWrapperStates = function () {
|
||||
const positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||
const modulePositions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||
|
||||
positions.forEach(function (position) {
|
||||
const updateWrapperStates = function () {
|
||||
modulePositions.forEach(function (position) {
|
||||
const wrapper = selectWrapper(position);
|
||||
const moduleWrappers = wrapper.getElementsByClassName("module");
|
||||
|
||||
@@ -355,7 +473,6 @@ const MM = (function () {
|
||||
*/
|
||||
const loadConfig = function () {
|
||||
// FIXME: Think about how to pass config around without breaking tests
|
||||
/* eslint-disable */
|
||||
if (typeof config === "undefined") {
|
||||
config = defaults;
|
||||
Log.error("Config file is missing! Please create a config file.");
|
||||
@@ -363,7 +480,6 @@ const MM = (function () {
|
||||
}
|
||||
|
||||
config = Object.assign({}, defaults, config);
|
||||
/* eslint-enable */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -371,6 +487,7 @@ const MM = (function () {
|
||||
* @param {Module[]} modules Array of modules.
|
||||
*/
|
||||
const setSelectionMethodsForModules = function (modules) {
|
||||
|
||||
/**
|
||||
* Filter modules with the specified classes.
|
||||
* @param {string|string[]} className one or multiple classnames (array or space divided).
|
||||
@@ -456,12 +573,13 @@ const MM = (function () {
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
/* Public Methods */
|
||||
|
||||
/**
|
||||
* Main init method.
|
||||
*/
|
||||
init: async function () {
|
||||
async init () {
|
||||
Log.info("Initializing MagicMirror².");
|
||||
loadConfig();
|
||||
|
||||
@@ -475,14 +593,35 @@ const MM = (function () {
|
||||
* Gets called when all modules are started.
|
||||
* @param {Module[]} moduleObjects All module instances.
|
||||
*/
|
||||
modulesStarted: function (moduleObjects) {
|
||||
modulesStarted (moduleObjects) {
|
||||
modules = [];
|
||||
let startUp = "";
|
||||
|
||||
moduleObjects.forEach((module) => modules.push(module));
|
||||
|
||||
Log.info("All modules started!");
|
||||
sendNotification("ALL_MODULES_STARTED");
|
||||
|
||||
createDomObjects();
|
||||
|
||||
if (config.reloadAfterServerRestart) {
|
||||
setInterval(async () => {
|
||||
// if server startup time has changed (which means server was restarted)
|
||||
// the client reloads the mm page
|
||||
try {
|
||||
const res = await fetch(`${location.protocol}//${location.host}/startup`);
|
||||
const curr = await res.text();
|
||||
if (startUp === "") startUp = curr;
|
||||
if (startUp !== curr) {
|
||||
startUp = "";
|
||||
window.location.reload(true);
|
||||
console.warn("Refreshing Website because server was restarted");
|
||||
}
|
||||
} catch (err) {
|
||||
Log.error(`MagicMirror not reachable: ${err}`);
|
||||
}
|
||||
}, config.checkServerInterval);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -491,7 +630,7 @@ const MM = (function () {
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
*/
|
||||
sendNotification: function (notification, payload, sender) {
|
||||
sendNotification (notification, payload, sender) {
|
||||
if (arguments.length < 3) {
|
||||
Log.error("sendNotification: Missing arguments.");
|
||||
return;
|
||||
@@ -514,9 +653,9 @@ const MM = (function () {
|
||||
/**
|
||||
* Update the dom for a specific module.
|
||||
* @param {Module} module The module that needs an update.
|
||||
* @param {number} [speed] The number of microseconds for the animation.
|
||||
* @param {object|number} [updateOptions] The (optional) number of microseconds for the animation or object with updateOptions (speed/animates)
|
||||
*/
|
||||
updateDom: function (module, speed) {
|
||||
updateDom (module, updateOptions) {
|
||||
if (!(module instanceof Module)) {
|
||||
Log.error("updateDom: Sender should be a module.");
|
||||
return;
|
||||
@@ -528,14 +667,14 @@ const MM = (function () {
|
||||
}
|
||||
|
||||
// Further implementation is done in the private method.
|
||||
updateDom(module, speed);
|
||||
updateDom(module, updateOptions);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a collection of all modules currently active.
|
||||
* @returns {Module[]} A collection of all modules currently active.
|
||||
*/
|
||||
getModules: function () {
|
||||
getModules () {
|
||||
setSelectionMethodsForModules(modules);
|
||||
return modules;
|
||||
},
|
||||
@@ -547,7 +686,7 @@ const MM = (function () {
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the hide method.
|
||||
*/
|
||||
hideModule: function (module, speed, callback, options) {
|
||||
hideModule (module, speed, callback, options) {
|
||||
module.hidden = true;
|
||||
hideModule(module, speed, callback, options);
|
||||
},
|
||||
@@ -559,12 +698,15 @@ const MM = (function () {
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the show method.
|
||||
*/
|
||||
showModule: function (module, speed, callback, options) {
|
||||
showModule (module, speed, callback, options) {
|
||||
// do not change module.hidden yet, only if we really show it later
|
||||
showModule(module, speed, callback, options);
|
||||
}
|
||||
},
|
||||
|
||||
// return all available module postions.
|
||||
getAvailableModulePositions: modulePositions
|
||||
};
|
||||
})();
|
||||
}());
|
||||
|
||||
// Add polyfill for Object.assign.
|
||||
if (typeof Object.assign !== "function") {
|
||||
@@ -587,7 +729,7 @@ if (typeof Object.assign !== "function") {
|
||||
}
|
||||
return output;
|
||||
};
|
||||
})();
|
||||
}());
|
||||
}
|
||||
|
||||
MM.init();
|
||||
|
77
js/module.js
77
js/module.js
@@ -1,13 +1,10 @@
|
||||
/* global Class, cloneObject, Loader, MMSocket, nunjucks, Translator */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module Blueprint.
|
||||
/* Module Blueprint.
|
||||
* @typedef {Object} Module
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Module = Class.extend({
|
||||
|
||||
/*********************************************************
|
||||
* All methods (and properties) below can be subclassed. *
|
||||
*********************************************************/
|
||||
@@ -33,14 +30,14 @@ const Module = Class.extend({
|
||||
/**
|
||||
* Called when the module is instantiated.
|
||||
*/
|
||||
init: function () {
|
||||
init () {
|
||||
//Log.log(this.defaults);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the module is started.
|
||||
*/
|
||||
start: async function () {
|
||||
async start () {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
},
|
||||
|
||||
@@ -48,7 +45,7 @@ const Module = Class.extend({
|
||||
* Returns a list of scripts the module requires to be loaded.
|
||||
* @returns {string[]} An array with filenames.
|
||||
*/
|
||||
getScripts: function () {
|
||||
getScripts () {
|
||||
return [];
|
||||
},
|
||||
|
||||
@@ -56,7 +53,7 @@ const Module = Class.extend({
|
||||
* Returns a list of stylesheets the module requires to be loaded.
|
||||
* @returns {string[]} An array with filenames.
|
||||
*/
|
||||
getStyles: function () {
|
||||
getStyles () {
|
||||
return [];
|
||||
},
|
||||
|
||||
@@ -66,7 +63,7 @@ const Module = Class.extend({
|
||||
* return Map<String, String> -
|
||||
* @returns {*} A map with langKeys and filenames.
|
||||
*/
|
||||
getTranslations: function () {
|
||||
getTranslations () {
|
||||
return false;
|
||||
},
|
||||
|
||||
@@ -76,14 +73,14 @@ const Module = Class.extend({
|
||||
* Alternatively, the getTemplate method could be subclassed.
|
||||
* @returns {HTMLElement|Promise} The dom or a promise with the dom to display.
|
||||
*/
|
||||
getDom: function () {
|
||||
getDom () {
|
||||
return new Promise((resolve) => {
|
||||
const div = document.createElement("div");
|
||||
const template = this.getTemplate();
|
||||
const templateData = this.getTemplateData();
|
||||
|
||||
// Check to see if we need to render a template string or a file.
|
||||
if (/^.*((\.html)|(\.njk))$/.test(template)) {
|
||||
if ((/^.*((\.html)|(\.njk))$/).test(template)) {
|
||||
// the template is a filename
|
||||
this.nunjucksEnvironment().render(template, templateData, function (err, res) {
|
||||
if (err) {
|
||||
@@ -109,7 +106,7 @@ const Module = Class.extend({
|
||||
* 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.
|
||||
*/
|
||||
getHeader: function () {
|
||||
getHeader () {
|
||||
return this.data.header;
|
||||
},
|
||||
|
||||
@@ -120,7 +117,7 @@ const Module = Class.extend({
|
||||
* If the string ends with '.html' it's considered a file from within the module's folder.
|
||||
* @returns {string} The template string of filename.
|
||||
*/
|
||||
getTemplate: function () {
|
||||
getTemplate () {
|
||||
return `<div class="normal">${this.name}</div><div class="small dimmed">${this.identifier}</div>`;
|
||||
},
|
||||
|
||||
@@ -129,7 +126,7 @@ const Module = Class.extend({
|
||||
* This method needs to be subclassed if the module wants to use a custom data.
|
||||
* @returns {object} The data for the template
|
||||
*/
|
||||
getTemplateData: function () {
|
||||
getTemplateData () {
|
||||
return {};
|
||||
},
|
||||
|
||||
@@ -139,7 +136,7 @@ const Module = Class.extend({
|
||||
* @param {*} payload The payload of the notification.
|
||||
* @param {Module} sender The module that sent the notification.
|
||||
*/
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
notificationReceived (notification, payload, sender) {
|
||||
if (sender) {
|
||||
// Log.log(this.name + " received a module notification: " + notification + " from sender: " + sender.name);
|
||||
} else {
|
||||
@@ -152,7 +149,7 @@ const Module = Class.extend({
|
||||
* The environment is checked in the _nunjucksEnvironment instance variable.
|
||||
* @returns {object} The Nunjucks Environment
|
||||
*/
|
||||
nunjucksEnvironment: function () {
|
||||
nunjucksEnvironment () {
|
||||
if (this._nunjucksEnvironment !== null) {
|
||||
return this._nunjucksEnvironment;
|
||||
}
|
||||
@@ -174,37 +171,39 @@ const Module = Class.extend({
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
socketNotificationReceived (notification, payload) {
|
||||
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the module is hidden.
|
||||
*/
|
||||
suspend: function () {
|
||||
suspend () {
|
||||
Log.log(`${this.name} is suspended.`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the module is shown.
|
||||
*/
|
||||
resume: function () {
|
||||
resume () {
|
||||
Log.log(`${this.name} is resumed.`);
|
||||
},
|
||||
|
||||
/*********************************************
|
||||
* The methods below don"t need subclassing. *
|
||||
* The methods below don't need subclassing. *
|
||||
*********************************************/
|
||||
|
||||
/**
|
||||
* Set the module data.
|
||||
* @param {object} data The module data
|
||||
*/
|
||||
setData: function (data) {
|
||||
setData (data) {
|
||||
this.data = data;
|
||||
this.name = data.name;
|
||||
this.identifier = data.identifier;
|
||||
this.hidden = false;
|
||||
this.hasAnimateIn = false;
|
||||
this.hasAnimateOut = false;
|
||||
|
||||
this.setConfig(data.config, data.configDeepMerge);
|
||||
},
|
||||
@@ -214,7 +213,7 @@ const Module = Class.extend({
|
||||
* @param {object} config The combined module config.
|
||||
* @param {boolean} deep Merge module config in deep.
|
||||
*/
|
||||
setConfig: function (config, deep) {
|
||||
setConfig (config, deep) {
|
||||
this.config = deep ? configMerge({}, this.defaults, config) : Object.assign({}, this.defaults, config);
|
||||
},
|
||||
|
||||
@@ -223,7 +222,7 @@ const Module = Class.extend({
|
||||
* It also registers the notification callback.
|
||||
* @returns {MMSocket} a socket object
|
||||
*/
|
||||
socket: function () {
|
||||
socket () {
|
||||
if (typeof this._socket === "undefined") {
|
||||
this._socket = new MMSocket(this.name);
|
||||
}
|
||||
@@ -240,7 +239,7 @@ const Module = Class.extend({
|
||||
* @param {string} file Filename
|
||||
* @returns {string} the file path
|
||||
*/
|
||||
file: function (file) {
|
||||
file (file) {
|
||||
return `${this.data.path}/${file}`.replace("//", "/");
|
||||
},
|
||||
|
||||
@@ -248,7 +247,7 @@ const Module = Class.extend({
|
||||
* Load all required stylesheets by requesting the MM object to load the files.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadStyles: function () {
|
||||
loadStyles () {
|
||||
return this.loadDependencies("getStyles");
|
||||
},
|
||||
|
||||
@@ -256,7 +255,7 @@ const Module = Class.extend({
|
||||
* Load all required scripts by requesting the MM object to load the files.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadScripts: function () {
|
||||
loadScripts () {
|
||||
return this.loadDependencies("getScripts");
|
||||
},
|
||||
|
||||
@@ -265,7 +264,7 @@ const Module = Class.extend({
|
||||
* @param {string} funcName Function name to call to get scripts or styles.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadDependencies: async function (funcName) {
|
||||
async loadDependencies (funcName) {
|
||||
let dependencies = this[funcName]();
|
||||
|
||||
const loadNextDependency = async () => {
|
||||
@@ -286,7 +285,7 @@ const Module = Class.extend({
|
||||
* Load all translations.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadTranslations: async function () {
|
||||
async loadTranslations () {
|
||||
const translations = this.getTranslations() || {};
|
||||
const language = config.language.toLowerCase();
|
||||
|
||||
@@ -318,7 +317,7 @@ const Module = Class.extend({
|
||||
* @param {string} [defaultValue] The default value with variables.
|
||||
* @returns {string} the translated key
|
||||
*/
|
||||
translate: function (key, defaultValueOrVariables, defaultValue) {
|
||||
translate (key, defaultValueOrVariables, defaultValue) {
|
||||
if (typeof defaultValueOrVariables === "object") {
|
||||
return Translator.translate(this, key, defaultValueOrVariables) || defaultValue || "";
|
||||
}
|
||||
@@ -327,10 +326,10 @@ const Module = Class.extend({
|
||||
|
||||
/**
|
||||
* Request an (animated) update of the module.
|
||||
* @param {number} [speed] The speed of the animation.
|
||||
* @param {number|object} [updateOptions] The speed of the animation or object with for updateOptions (speed/animates)
|
||||
*/
|
||||
updateDom: function (speed) {
|
||||
MM.updateDom(this, speed);
|
||||
updateDom (updateOptions) {
|
||||
MM.updateDom(this, updateOptions);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -338,7 +337,7 @@ const Module = Class.extend({
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
sendNotification: function (notification, payload) {
|
||||
sendNotification (notification, payload) {
|
||||
MM.sendNotification(notification, payload, this);
|
||||
},
|
||||
|
||||
@@ -347,7 +346,7 @@ const Module = Class.extend({
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
sendSocketNotification: function (notification, payload) {
|
||||
sendSocketNotification (notification, payload) {
|
||||
this.socket().sendNotification(notification, payload);
|
||||
},
|
||||
|
||||
@@ -357,7 +356,7 @@ const Module = Class.extend({
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the hide method.
|
||||
*/
|
||||
hide: function (speed, callback, options = {}) {
|
||||
hide (speed, callback, options = {}) {
|
||||
let usedCallback = callback || function () {};
|
||||
let usedOptions = options;
|
||||
|
||||
@@ -384,7 +383,7 @@ const Module = Class.extend({
|
||||
* @param {Function} callback Called when the animation is done.
|
||||
* @param {object} [options] Optional settings for the show method.
|
||||
*/
|
||||
show: function (speed, callback, options) {
|
||||
show (speed, callback, options) {
|
||||
let usedCallback = callback || function () {};
|
||||
let usedOptions = options;
|
||||
|
||||
@@ -428,7 +427,7 @@ const Module = Class.extend({
|
||||
* @param {object} result the initial object
|
||||
* @returns {object} the merged config
|
||||
*/
|
||||
function configMerge(result) {
|
||||
function configMerge (result) {
|
||||
const stack = Array.prototype.slice.call(arguments, 1);
|
||||
let item, key;
|
||||
|
||||
@@ -491,7 +490,7 @@ window.Module = Module;
|
||||
* @returns {number} A positive number if a is larger than b, a negative
|
||||
* number if a is smaller and 0 if they are the same
|
||||
*/
|
||||
function cmpVersions(a, b) {
|
||||
function cmpVersions (a, b) {
|
||||
const regExStrip0 = /(\.0+)+$/;
|
||||
const segmentsA = a.replace(regExStrip0, "").split(".");
|
||||
const segmentsB = b.replace(regExStrip0, "").split(".");
|
||||
|
@@ -1,23 +1,17 @@
|
||||
/* MagicMirror²
|
||||
* Node Helper Superclass
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const express = require("express");
|
||||
const Log = require("logger");
|
||||
const Class = require("./class");
|
||||
|
||||
const NodeHelper = Class.extend({
|
||||
init() {
|
||||
init () {
|
||||
Log.log("Initializing new module helper ...");
|
||||
},
|
||||
|
||||
loaded() {
|
||||
loaded () {
|
||||
Log.log(`Module helper loaded: ${this.name}`);
|
||||
},
|
||||
|
||||
start() {
|
||||
start () {
|
||||
Log.log(`Starting module helper: ${this.name}`);
|
||||
},
|
||||
|
||||
@@ -26,7 +20,7 @@ const NodeHelper = Class.extend({
|
||||
* Close any open connections, stop any sub-processes and
|
||||
* gracefully exit the module.
|
||||
*/
|
||||
stop() {
|
||||
stop () {
|
||||
Log.log(`Stopping module helper: ${this.name}`);
|
||||
},
|
||||
|
||||
@@ -35,7 +29,7 @@ const NodeHelper = Class.extend({
|
||||
* @param {string} notification The identifier of the notification.
|
||||
* @param {*} payload The payload of the notification.
|
||||
*/
|
||||
socketNotificationReceived(notification, payload) {
|
||||
socketNotificationReceived (notification, payload) {
|
||||
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
|
||||
},
|
||||
|
||||
@@ -43,7 +37,7 @@ const NodeHelper = Class.extend({
|
||||
* Set the module name.
|
||||
* @param {string} name Module name.
|
||||
*/
|
||||
setName(name) {
|
||||
setName (name) {
|
||||
this.name = name;
|
||||
},
|
||||
|
||||
@@ -51,7 +45,7 @@ const NodeHelper = Class.extend({
|
||||
* Set the module path.
|
||||
* @param {string} path Module path.
|
||||
*/
|
||||
setPath(path) {
|
||||
setPath (path) {
|
||||
this.path = path;
|
||||
},
|
||||
|
||||
@@ -61,7 +55,7 @@ const NodeHelper = Class.extend({
|
||||
* argument notification string - The identifier of the notification.
|
||||
* argument payload mixed - The payload of the notification.
|
||||
*/
|
||||
sendSocketNotification(notification, payload) {
|
||||
sendSocketNotification (notification, payload) {
|
||||
this.io.of(this.name).emit(notification, payload);
|
||||
},
|
||||
|
||||
@@ -71,7 +65,7 @@ const NodeHelper = Class.extend({
|
||||
*
|
||||
* argument app Express app - The Express app object.
|
||||
*/
|
||||
setExpressApp(app) {
|
||||
setExpressApp (app) {
|
||||
this.expressApp = app;
|
||||
|
||||
app.use(`/${this.name}`, express.static(`${this.path}/public`));
|
||||
@@ -83,7 +77,7 @@ const NodeHelper = Class.extend({
|
||||
*
|
||||
* argument io Socket.io - The Socket io object.
|
||||
*/
|
||||
setSocketIO(io) {
|
||||
setSocketIO (io) {
|
||||
this.io = io;
|
||||
|
||||
Log.log(`Connecting socket for: ${this.name}`);
|
||||
|
22
js/server.js
22
js/server.js
@@ -1,13 +1,7 @@
|
||||
/* MagicMirror²
|
||||
* Server
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
const path = require("path");
|
||||
const fs = require("node:fs");
|
||||
const http = require("node:http");
|
||||
const https = require("node:https");
|
||||
const path = require("node:path");
|
||||
const express = require("express");
|
||||
const ipfilter = require("express-ipfilter").IpFilter;
|
||||
const helmet = require("helmet");
|
||||
@@ -15,14 +9,14 @@ const socketio = require("socket.io");
|
||||
|
||||
const Log = require("logger");
|
||||
const Utils = require("./utils");
|
||||
const { cors, getConfig, getHtml, getVersion } = require("./server_functions");
|
||||
const { cors, getConfig, getHtml, getVersion, getStartup } = require("./server_functions");
|
||||
|
||||
/**
|
||||
* Server
|
||||
* @param {object} config The MM config
|
||||
* @class
|
||||
*/
|
||||
function Server(config) {
|
||||
function Server (config) {
|
||||
const app = express();
|
||||
const port = process.env.MM_PORT || config.port;
|
||||
const serverSockets = new Set();
|
||||
@@ -62,7 +56,7 @@ function Server(config) {
|
||||
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"));
|
||||
Log.warn("You're using a full whitelist configuration to allow for all IPs");
|
||||
}
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
@@ -91,6 +85,8 @@ function Server(config) {
|
||||
|
||||
app.get("/config", (req, res) => getConfig(req, res));
|
||||
|
||||
app.get("/startup", (req, res) => getStartup(req, res));
|
||||
|
||||
app.get("/", (req, res) => getHtml(req, res));
|
||||
|
||||
server.on("listening", () => {
|
||||
|
@@ -1,17 +1,27 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const Log = require("logger");
|
||||
const fetch = require("./fetch");
|
||||
|
||||
const startUp = new Date();
|
||||
|
||||
/**
|
||||
* Gets the config.
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
function getConfig(req, res) {
|
||||
function getConfig (req, res) {
|
||||
res.send(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the startup time.
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
function getStartup (req, res) {
|
||||
res.send(startUp);
|
||||
}
|
||||
|
||||
/**
|
||||
* A method that forwards HTTP Get-methods to the internet to avoid CORS-errors.
|
||||
*
|
||||
@@ -21,7 +31,7 @@ function getConfig(req, res) {
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
async function cors(req, res) {
|
||||
async function cors (req, res) {
|
||||
try {
|
||||
const urlRegEx = "url=(.+?)$";
|
||||
let url;
|
||||
@@ -58,7 +68,7 @@ async function cors(req, res) {
|
||||
* @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) {
|
||||
function getHeadersToSend (url) {
|
||||
const headersToSend = { "User-Agent": `Mozilla/5.0 MagicMirror/${global.version}` };
|
||||
const headersToSendMatch = new RegExp("sendheaders=(.+?)(&|$)", "g").exec(url);
|
||||
if (headersToSendMatch) {
|
||||
@@ -79,7 +89,7 @@ function getHeadersToSend(url) {
|
||||
* @param {string} url - The url containing the expected headers from the response.
|
||||
* @returns {string[]} headers - The name of the expected headers.
|
||||
*/
|
||||
function geExpectedRecievedHeaders(url) {
|
||||
function geExpectedRecievedHeaders (url) {
|
||||
const expectedRecievedHeaders = ["Content-Type"];
|
||||
const expectedRecievedHeadersMatch = new RegExp("expectedheaders=(.+?)(&|$)", "g").exec(url);
|
||||
if (expectedRecievedHeadersMatch) {
|
||||
@@ -96,7 +106,7 @@ function geExpectedRecievedHeaders(url) {
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
function getHtml(req, res) {
|
||||
function getHtml (req, res) {
|
||||
let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" });
|
||||
html = html.replace("#VERSION#", global.version);
|
||||
|
||||
@@ -114,8 +124,8 @@ function getHtml(req, res) {
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
function getVersion(req, res) {
|
||||
function getVersion (req, res) {
|
||||
res.send(global.version);
|
||||
}
|
||||
|
||||
module.exports = { cors, getConfig, getHtml, getVersion };
|
||||
module.exports = { cors, getConfig, getHtml, getVersion, getStartup };
|
||||
|
@@ -1,11 +1,5 @@
|
||||
/* global io */
|
||||
|
||||
/* MagicMirror²
|
||||
* TODO add description
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const MMSocket = function (moduleName) {
|
||||
if (typeof moduleName !== "string") {
|
||||
throw new Error("Please set the module name for the MMSocket.");
|
||||
|
@@ -1,18 +1,13 @@
|
||||
/* global translations */
|
||||
|
||||
/* MagicMirror²
|
||||
* Translator (l10n)
|
||||
*
|
||||
* By Christopher Fenner https://github.com/CFenner
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Translator = (function () {
|
||||
|
||||
/**
|
||||
* Load a JSON file via XHR.
|
||||
* @param {string} file Path of the file we want to load.
|
||||
* @returns {Promise<object>} the translations in the specified file
|
||||
*/
|
||||
async function loadJSON(file) {
|
||||
async function loadJSON (file) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
return new Promise(function (resolve) {
|
||||
xhr.overrideMimeType("application/json");
|
||||
@@ -47,7 +42,8 @@ const Translator = (function () {
|
||||
* @param {object} variables The variables to use within the translation template (optional)
|
||||
* @returns {string} the translated key
|
||||
*/
|
||||
translate: function (module, key, variables = {}) {
|
||||
translate (module, key, variables = {}) {
|
||||
|
||||
/**
|
||||
* Combines template and variables like:
|
||||
* template: "Please wait for {timeToWait} before continuing with {work}."
|
||||
@@ -57,7 +53,7 @@ const Translator = (function () {
|
||||
* @param {object} variables Variables for the placeholder
|
||||
* @returns {string} the template filled with the variables
|
||||
*/
|
||||
function createStringFromTemplate(template, variables) {
|
||||
function createStringFromTemplate (template, variables) {
|
||||
if (Object.prototype.toString.call(template) !== "[object String]") {
|
||||
return template;
|
||||
}
|
||||
@@ -99,7 +95,7 @@ const Translator = (function () {
|
||||
* @param {string} file Path of the file we want to load.
|
||||
* @param {boolean} isFallback Flag to indicate fallback translations.
|
||||
*/
|
||||
async load(module, file, isFallback) {
|
||||
async load (module, file, isFallback) {
|
||||
Log.log(`${module.name} - Load translation${isFallback ? " fallback" : ""}: ${file}`);
|
||||
|
||||
if (this.translationsFallback[module.name]) {
|
||||
@@ -115,7 +111,7 @@ const Translator = (function () {
|
||||
* Load the core translations.
|
||||
* @param {string} lang The language identifier of the core language.
|
||||
*/
|
||||
loadCoreTranslations: async function (lang) {
|
||||
async loadCoreTranslations (lang) {
|
||||
if (lang in translations) {
|
||||
Log.log(`Loading core translation file: ${translations[lang]}`);
|
||||
this.coreTranslations = await loadJSON(translations[lang]);
|
||||
@@ -130,7 +126,7 @@ const Translator = (function () {
|
||||
* Load the core translations' fallback.
|
||||
* The first language defined in translations.js will be used.
|
||||
*/
|
||||
loadCoreTranslationsFallback: async function () {
|
||||
async loadCoreTranslationsFallback () {
|
||||
let first = Object.keys(translations)[0];
|
||||
if (first) {
|
||||
Log.log(`Loading core translation fallback file: ${translations[first]}`);
|
||||
@@ -138,6 +134,6 @@ const Translator = (function () {
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
}());
|
||||
|
||||
window.Translator = Translator;
|
||||
|
48
js/utils.js
48
js/utils.js
@@ -1,16 +1,40 @@
|
||||
/* MagicMirror²
|
||||
* Utils
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const colors = require("colors/safe");
|
||||
const execSync = require("node:child_process").execSync;
|
||||
const Log = require("logger");
|
||||
const si = require("systeminformation");
|
||||
|
||||
module.exports = {
|
||||
colors: {
|
||||
warn: colors.yellow,
|
||||
error: colors.red,
|
||||
info: colors.blue,
|
||||
pass: colors.green
|
||||
|
||||
async logSystemInformation () {
|
||||
try {
|
||||
let installedNodeVersion = execSync("node -v", { encoding: "utf-8" }).replace("v", "").replace(/(?:\r\n|\r|\n)/g, "");
|
||||
|
||||
const staticData = await si.get({
|
||||
system: "manufacturer, model, raspberry, virtual",
|
||||
osInfo: "platform, distro, release, arch",
|
||||
versions: "kernel, node, npm, pm2"
|
||||
});
|
||||
let systemDataString = "System information:";
|
||||
systemDataString += `\n### SYSTEM: manufacturer: ${staticData["system"]["manufacturer"]}; model: ${staticData["system"]["model"]}; raspberry: ${staticData["system"]["raspberry"]}; virtual: ${staticData["system"]["virtual"]}`;
|
||||
systemDataString += `\n### OS: platform: ${staticData["osInfo"]["platform"]}; distro: ${staticData["osInfo"]["distro"]}; release: ${staticData["osInfo"]["release"]}; arch: ${staticData["osInfo"]["arch"]}; kernel: ${staticData["versions"]["kernel"]}`;
|
||||
systemDataString += `\n### VERSIONS: electron: ${process.versions.electron}; used node: ${staticData["versions"]["node"]}; installed node: ${installedNodeVersion}; npm: ${staticData["versions"]["npm"]}; pm2: ${staticData["versions"]["pm2"]}`;
|
||||
systemDataString += `\n### OTHER: timeZone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}; ELECTRON_ENABLE_GPU: ${process.env.ELECTRON_ENABLE_GPU}`;
|
||||
Log.info(systemDataString);
|
||||
|
||||
// Return is currently only for jest
|
||||
return systemDataString;
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
},
|
||||
|
||||
// return all available module positions
|
||||
getAvailableModulePositions () {
|
||||
return ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||
},
|
||||
|
||||
// return if postion is on modulePositions Array (true/false)
|
||||
moduleHasValidPosition (position) {
|
||||
if (this.getAvailableModulePositions().indexOf(position) === -1) return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
@@ -1,16 +1,19 @@
|
||||
type ModuleProperties = {
|
||||
defaults?: object;
|
||||
[key: string]: any;
|
||||
start?(): void;
|
||||
getScripts?(): string[];
|
||||
getStyles?(): string[];
|
||||
getTranslations?(): object;
|
||||
getDom?(): HTMLElement;
|
||||
getHeader?(): string;
|
||||
getTemplate?(): string;
|
||||
getTemplateData?(): object;
|
||||
notificationReceived?(notification: string, payload: any, sender: object): void;
|
||||
nunjucksEnvironment?(): void;
|
||||
socketNotificationReceived?(notification: string, payload: any): void;
|
||||
suspend?(): void;
|
||||
resume?(): void;
|
||||
getDom?(): HTMLElement;
|
||||
getStyles?(): string[];
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export declare const Module: {
|
||||
|
@@ -1,11 +1,5 @@
|
||||
/* global NotificationFx */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: alert
|
||||
*
|
||||
* By Paul-Vincent Roll https://paulvincentroll.com/
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("alert", {
|
||||
alerts: {},
|
||||
|
||||
@@ -17,15 +11,15 @@ Module.register("alert", {
|
||||
welcome_message: false // shown at startup
|
||||
},
|
||||
|
||||
getScripts() {
|
||||
getScripts () {
|
||||
return ["notificationFx.js"];
|
||||
},
|
||||
|
||||
getStyles() {
|
||||
return ["font-awesome.css", this.file(`./styles/notificationFx.css`), this.file(`./styles/${this.config.position}.css`)];
|
||||
getStyles () {
|
||||
return ["font-awesome.css", this.file("./styles/notificationFx.css"), this.file(`./styles/${this.config.position}.css`)];
|
||||
},
|
||||
|
||||
getTranslations() {
|
||||
getTranslations () {
|
||||
return {
|
||||
bg: "translations/bg.json",
|
||||
da: "translations/da.json",
|
||||
@@ -40,11 +34,11 @@ Module.register("alert", {
|
||||
};
|
||||
},
|
||||
|
||||
getTemplate(type) {
|
||||
getTemplate (type) {
|
||||
return `templates/${type}.njk`;
|
||||
},
|
||||
|
||||
async start() {
|
||||
async start () {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
if (this.config.effect === "slide") {
|
||||
@@ -57,7 +51,7 @@ Module.register("alert", {
|
||||
}
|
||||
},
|
||||
|
||||
notificationReceived(notification, payload, sender) {
|
||||
notificationReceived (notification, payload, sender) {
|
||||
if (notification === "SHOW_ALERT") {
|
||||
if (payload.type === "notification") {
|
||||
this.showNotification(payload);
|
||||
@@ -69,7 +63,7 @@ Module.register("alert", {
|
||||
}
|
||||
},
|
||||
|
||||
async showNotification(notification) {
|
||||
async showNotification (notification) {
|
||||
const message = await this.renderMessage(notification.templateName || "notification", notification);
|
||||
|
||||
new NotificationFx({
|
||||
@@ -80,7 +74,7 @@ Module.register("alert", {
|
||||
}).show();
|
||||
},
|
||||
|
||||
async showAlert(alert, sender) {
|
||||
async showAlert (alert, sender) {
|
||||
// If module already has an open alert close it
|
||||
if (this.alerts[sender.name]) {
|
||||
this.hideAlert(sender, false);
|
||||
@@ -113,7 +107,7 @@ Module.register("alert", {
|
||||
}
|
||||
},
|
||||
|
||||
hideAlert(sender, close = true) {
|
||||
hideAlert (sender, close = true) {
|
||||
// Dismiss alert and remove from this.alerts
|
||||
if (this.alerts[sender.name]) {
|
||||
this.alerts[sender.name].dismiss(close);
|
||||
@@ -125,7 +119,7 @@ Module.register("alert", {
|
||||
}
|
||||
},
|
||||
|
||||
renderMessage(type, data) {
|
||||
renderMessage (type, data) {
|
||||
return new Promise((resolve) => {
|
||||
this.nunjucksEnvironment().render(this.getTemplate(type), data, function (err, res) {
|
||||
if (err) {
|
||||
@@ -137,7 +131,7 @@ Module.register("alert", {
|
||||
});
|
||||
},
|
||||
|
||||
toggleBlur(add = false) {
|
||||
toggleBlur (add = false) {
|
||||
const method = add ? "add" : "remove";
|
||||
const modules = document.querySelectorAll(".module");
|
||||
for (const module of modules) {
|
||||
|
@@ -12,13 +12,14 @@
|
||||
* @param {object} window The window object
|
||||
*/
|
||||
(function (window) {
|
||||
|
||||
/**
|
||||
* Extend one object with another one
|
||||
* @param {object} a The object to extend
|
||||
* @param {object} b The object which extends the other, overwrites existing keys
|
||||
* @returns {object} The merged object
|
||||
*/
|
||||
function extend(a, b) {
|
||||
function extend (a, b) {
|
||||
for (let key in b) {
|
||||
if (b.hasOwnProperty(key)) {
|
||||
a[key] = b[key];
|
||||
@@ -32,7 +33,7 @@
|
||||
* @param {object} options The configuration options
|
||||
* @class
|
||||
*/
|
||||
function NotificationFx(options) {
|
||||
function NotificationFx (options) {
|
||||
this.options = extend({}, this.options);
|
||||
extend(this.options, options);
|
||||
this._init();
|
||||
@@ -63,10 +64,10 @@
|
||||
ttl: 6000,
|
||||
al_no: "ns-box",
|
||||
// callbacks
|
||||
onClose: function () {
|
||||
onClose () {
|
||||
return false;
|
||||
},
|
||||
onOpen: function () {
|
||||
onOpen () {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -78,7 +79,7 @@
|
||||
// create HTML structure
|
||||
this.ntf = document.createElement("div");
|
||||
this.ntf.className = `${this.options.al_no} ns-${this.options.layout} ns-effect-${this.options.effect} ns-type-${this.options.type}`;
|
||||
let strinner = '<div class="ns-box-inner">';
|
||||
let strinner = "<div class=\"ns-box-inner\">";
|
||||
strinner += this.options.message;
|
||||
strinner += "</div>";
|
||||
this.ntf.innerHTML = strinner;
|
||||
@@ -153,4 +154,4 @@
|
||||
* Add to global namespace
|
||||
*/
|
||||
window.NotificationFx = NotificationFx;
|
||||
})(window);
|
||||
}(window));
|
||||
|
@@ -1,18 +1,20 @@
|
||||
{% 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/>
|
||||
{% 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 if titleType == 'text' else title | safe }}</span>
|
||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
{% if title %}
|
||||
<br/>
|
||||
{% endif %}
|
||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||
{% if title %}<br />{% endif %}
|
||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||
{% endif %}
|
||||
|
@@ -1,9 +1,7 @@
|
||||
{% if title %}
|
||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
{% if title %}
|
||||
<br/>
|
||||
{% endif %}
|
||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||
{% if title %}<br />{% endif %}
|
||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||
{% endif %}
|
||||
|
@@ -1,11 +1,5 @@
|
||||
/* global CalendarUtils, cloneObject */
|
||||
/* global CalendarUtils */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Calendar
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("calendar", {
|
||||
// Define module defaults
|
||||
defaults: {
|
||||
@@ -28,20 +22,22 @@ Module.register("calendar", {
|
||||
fetchInterval: 60 * 60 * 1000, // Update every hour
|
||||
animationSpeed: 2000,
|
||||
fade: true,
|
||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
||||
urgency: 7,
|
||||
timeFormat: "relative",
|
||||
dateFormat: "MMM Do",
|
||||
dateEndFormat: "LT",
|
||||
fullDayEventDateFormat: "MMM Do",
|
||||
showEnd: false,
|
||||
showEndsOnlyWithDuration: false,
|
||||
getRelative: 6,
|
||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
||||
hidePrivate: false,
|
||||
hideOngoing: false,
|
||||
hideTime: false,
|
||||
hideDuplicates: true,
|
||||
showTimeToday: false,
|
||||
colored: false,
|
||||
customEvents: [], // Array of {keyword: "", symbol: "", color: ""} where Keyword is a regexp and symbol/color are to be applied for matched
|
||||
forceUseCurrentTime: false,
|
||||
tableClass: "small",
|
||||
calendars: [
|
||||
{
|
||||
@@ -49,10 +45,11 @@ Module.register("calendar", {
|
||||
url: "https://www.calendarlabs.com/templates/ical/US-Holidays.ics"
|
||||
}
|
||||
],
|
||||
titleReplace: {
|
||||
"De verjaardag van ": "",
|
||||
"'s birthday": ""
|
||||
},
|
||||
customEvents: [
|
||||
// Array of {keyword: "", symbol: "", color: "", eventClass: ""} where Keyword is a regexp and symbol/color/eventClass are to be applied for matched
|
||||
{ keyword: ".*", transform: { search: "De verjaardag van ", replace: "" } },
|
||||
{ keyword: ".*", transform: { search: "'s birthday", replace: "" } }
|
||||
],
|
||||
locationTitleReplace: {
|
||||
"street ": ""
|
||||
},
|
||||
@@ -67,23 +64,24 @@ Module.register("calendar", {
|
||||
coloredSymbol: false,
|
||||
coloredBackground: false,
|
||||
limitDaysNeverSkip: false,
|
||||
flipDateHeaderTitle: false
|
||||
flipDateHeaderTitle: false,
|
||||
updateOnFetch: true
|
||||
},
|
||||
|
||||
requiresVersion: "2.1.0",
|
||||
|
||||
// Define required scripts.
|
||||
getStyles: function () {
|
||||
getStyles () {
|
||||
return ["calendar.css", "font-awesome.css"];
|
||||
},
|
||||
|
||||
// Define required scripts.
|
||||
getScripts: function () {
|
||||
getScripts () {
|
||||
return ["calendarutils.js", "moment.js"];
|
||||
},
|
||||
|
||||
// Define required translations.
|
||||
getTranslations: function () {
|
||||
getTranslations () {
|
||||
// The translations for the default modules are defined in the core translation files.
|
||||
// Therefore we can just return false. Otherwise we should have returned a dictionary.
|
||||
// If you're trying to build your own module including translations, check out the documentation.
|
||||
@@ -91,18 +89,16 @@ Module.register("calendar", {
|
||||
},
|
||||
|
||||
// Override start method.
|
||||
start: function () {
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
|
||||
start () {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
if (this.config.colored) {
|
||||
Log.warn("Your are using the deprecated config values 'colored'. Please switch to 'coloredSymbol' & 'coloredText'!");
|
||||
Log.warn("Your are using the deprecated config values 'colored'. Please switch to 'coloredSymbol' & 'coloredText'!");
|
||||
this.config.coloredText = true;
|
||||
this.config.coloredSymbol = true;
|
||||
}
|
||||
if (this.config.coloredSymbolOnly) {
|
||||
Log.warn("Your are using the deprecated config values 'coloredSymbolOnly'. Please switch to 'coloredSymbol' & 'coloredText'!");
|
||||
Log.warn("Your are using the deprecated config values 'coloredSymbolOnly'. Please switch to 'coloredSymbol' & 'coloredText'!");
|
||||
this.config.coloredText = false;
|
||||
this.config.coloredSymbol = true;
|
||||
}
|
||||
@@ -116,6 +112,9 @@ Module.register("calendar", {
|
||||
// indicate no data available yet
|
||||
this.loaded = false;
|
||||
|
||||
// data holder of calendar url. Avoid fade out/in on updateDom (one for each calendar update)
|
||||
this.calendarDisplayer = {};
|
||||
|
||||
this.config.calendars.forEach((calendar) => {
|
||||
calendar.url = calendar.url.replace("webcal://", "http://");
|
||||
|
||||
@@ -124,23 +123,25 @@ Module.register("calendar", {
|
||||
maximumNumberOfDays: calendar.maximumNumberOfDays,
|
||||
pastDaysCount: calendar.pastDaysCount,
|
||||
broadcastPastEvents: calendar.broadcastPastEvents,
|
||||
selfSignedCert: calendar.selfSignedCert
|
||||
selfSignedCert: calendar.selfSignedCert,
|
||||
excludedEvents: calendar.excludedEvents,
|
||||
fetchInterval: calendar.fetchInterval
|
||||
};
|
||||
|
||||
if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
|
||||
if (typeof calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
|
||||
calendarConfig.symbolClass = "";
|
||||
}
|
||||
if (calendar.titleClass === "undefined" || calendar.titleClass === null) {
|
||||
if (typeof calendar.titleClass === "undefined" || calendar.titleClass === null) {
|
||||
calendarConfig.titleClass = "";
|
||||
}
|
||||
if (calendar.timeClass === "undefined" || calendar.timeClass === null) {
|
||||
if (typeof calendar.timeClass === "undefined" || calendar.timeClass === null) {
|
||||
calendarConfig.timeClass = "";
|
||||
}
|
||||
|
||||
// we check user and password here for backwards compatibility with old configs
|
||||
if (calendar.user && calendar.pass) {
|
||||
Log.warn("Deprecation warning: Please update your calendar authentication configuration.");
|
||||
Log.warn("https://github.com/MichMich/MagicMirror/tree/v2.1.2/modules/default/calendar#calendar-authentication-options");
|
||||
Log.warn("https://docs.magicmirror.builders/modules/calendar.html#configuration-options");
|
||||
calendar.auth = {
|
||||
user: calendar.user,
|
||||
pass: calendar.pass
|
||||
@@ -152,17 +153,19 @@ Module.register("calendar", {
|
||||
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
||||
});
|
||||
|
||||
// Refresh the DOM every minute if needed: When using relative date format for events that start
|
||||
// or end in less than an hour, the date shows minute granularity and we want to keep that accurate.
|
||||
setTimeout(() => {
|
||||
setInterval(() => {
|
||||
this.updateDom(1);
|
||||
}, ONE_MINUTE);
|
||||
}, ONE_MINUTE - (new Date() % ONE_MINUTE));
|
||||
// for backward compatibility titleReplace
|
||||
if (typeof this.config.titleReplace !== "undefined") {
|
||||
Log.warn("Deprecation warning: Please consider upgrading your calendar titleReplace configuration to customEvents.");
|
||||
for (const [titlesearchstr, titlereplacestr] of Object.entries(this.config.titleReplace)) {
|
||||
this.config.customEvents.push({ keyword: ".*", transform: { search: titlesearchstr, replace: titlereplacestr } });
|
||||
}
|
||||
}
|
||||
|
||||
this.selfUpdate();
|
||||
},
|
||||
|
||||
// Override socket notification handler.
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
socketNotificationReceived (notification, payload) {
|
||||
if (notification === "FETCH_CALENDAR") {
|
||||
this.sendSocketNotification(notification, { url: payload.url, id: this.identifier });
|
||||
}
|
||||
@@ -180,6 +183,18 @@ Module.register("calendar", {
|
||||
if (this.config.broadcastEvents) {
|
||||
this.broadcastEvents();
|
||||
}
|
||||
|
||||
if (!this.config.updateOnFetch) {
|
||||
if (this.calendarDisplayer[payload.url] === undefined) {
|
||||
// calendar will never displayed, so display it
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
// set this calendar as displayed
|
||||
this.calendarDisplayer[payload.url] = true;
|
||||
} else {
|
||||
Log.debug("[Calendar] DOM not updated waiting self update()");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (notification === "CALENDAR_ERROR") {
|
||||
let error_message = this.translate(payload.error_type);
|
||||
@@ -190,8 +205,13 @@ Module.register("calendar", {
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
},
|
||||
|
||||
eventEndingWithinNextFullTimeUnit (event, ONE_DAY) {
|
||||
const now = new Date();
|
||||
return event.endDate - now <= ONE_DAY;
|
||||
},
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
getDom () {
|
||||
const ONE_SECOND = 1000; // 1,000 milliseconds
|
||||
const ONE_MINUTE = ONE_SECOND * 60;
|
||||
const ONE_HOUR = ONE_MINUTE * 60;
|
||||
@@ -317,12 +337,17 @@ Module.register("calendar", {
|
||||
}
|
||||
}
|
||||
|
||||
// Color events if custom color is specified
|
||||
var transformedTitle = event.title;
|
||||
|
||||
// Color events if custom color or eventClass are specified, transform title if required
|
||||
if (this.config.customEvents.length > 0) {
|
||||
for (let ev in this.config.customEvents) {
|
||||
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
|
||||
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||
if (needle.test(event.title)) {
|
||||
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||
if (needle.test(event.title)) {
|
||||
if (typeof this.config.customEvents[ev].transform === "object") {
|
||||
transformedTitle = CalendarUtils.titleTransform(transformedTitle, [this.config.customEvents[ev].transform]);
|
||||
}
|
||||
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
|
||||
// Respect parameter ColoredSymbolOnly also for custom events
|
||||
if (this.config.coloredText) {
|
||||
eventWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
|
||||
@@ -331,13 +356,14 @@ Module.register("calendar", {
|
||||
if (this.config.displaySymbol && this.config.coloredSymbol) {
|
||||
symbolWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (typeof this.config.customEvents[ev].eventClass !== "undefined" && this.config.customEvents[ev].eventClass !== "") {
|
||||
eventWrapper.className += ` ${this.config.customEvents[ev].eventClass}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const transformedTitle = CalendarUtils.titleTransform(event.title, this.config.titleReplace);
|
||||
titleWrapper.innerHTML = CalendarUtils.shorten(transformedTitle, this.config.maxTitleLength, this.config.wrapEvents, this.config.maxTitleLines) + repeatingCountTitle;
|
||||
|
||||
const titleClass = this.titleClassForUrl(event.url);
|
||||
@@ -363,7 +389,11 @@ Module.register("calendar", {
|
||||
|
||||
// Add endDate to dataheaders if showEnd is enabled
|
||||
if (this.config.showEnd) {
|
||||
timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(moment(event.endDate, "x").format("LT"))}`;
|
||||
if (this.config.showEndsOnlyWithDuration && event.startDate === event.endDate) {
|
||||
// no duration here, don't display end
|
||||
} else {
|
||||
timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(moment(event.endDate, "x").format("LT"))}`;
|
||||
}
|
||||
}
|
||||
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
@@ -382,8 +412,12 @@ Module.register("calendar", {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||
// Add end time if showEnd
|
||||
if (this.config.showEnd) {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += CalendarUtils.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
||||
if (this.config.showEndsOnlyWithDuration && event.startDate === event.endDate) {
|
||||
// no duration here, don't display end
|
||||
} else {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += CalendarUtils.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
||||
}
|
||||
}
|
||||
// For full day events we use the fullDayEventDateFormat
|
||||
if (event.fullDayEvent) {
|
||||
@@ -418,7 +452,7 @@ Module.register("calendar", {
|
||||
}
|
||||
} else {
|
||||
// Show relative times
|
||||
if (event.startDate >= now || (event.fullDayEvent && event.today)) {
|
||||
if (event.startDate >= now || (event.fullDayEvent && this.eventEndingWithinNextFullTimeUnit(event, ONE_DAY))) {
|
||||
// Use relative time
|
||||
if (!this.config.hideTime && !event.fullDayEvent) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
|
||||
@@ -434,7 +468,7 @@ Module.register("calendar", {
|
||||
}
|
||||
if (event.fullDayEvent) {
|
||||
// Full days events within the next two days
|
||||
if (event.today) {
|
||||
if (event.today || (event.fullDayEvent && this.eventEndingWithinNextFullTimeUnit(event, ONE_DAY))) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
|
||||
} else if (event.dayBeforeYesterday) {
|
||||
if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") {
|
||||
@@ -527,7 +561,7 @@ Module.register("calendar", {
|
||||
* @param {string} url The calendar url
|
||||
* @returns {boolean} True if the calendar config contains the url, False otherwise
|
||||
*/
|
||||
hasCalendarURL: function (url) {
|
||||
hasCalendarURL (url) {
|
||||
for (const calendar of this.config.calendars) {
|
||||
if (calendar.url === url) {
|
||||
return true;
|
||||
@@ -542,15 +576,22 @@ Module.register("calendar", {
|
||||
* @param {boolean} limitNumberOfEntries Whether to filter returned events for display.
|
||||
* @returns {object[]} Array with events.
|
||||
*/
|
||||
createEventList: function (limitNumberOfEntries) {
|
||||
createEventList (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();
|
||||
let now, today, future;
|
||||
if (this.config.forceUseCurrentTime || this.defaults.forceUseCurrentTime) {
|
||||
now = new Date();
|
||||
today = moment().startOf("day");
|
||||
future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
|
||||
} else {
|
||||
now = new Date(Date.now()); // Can use overridden time
|
||||
today = moment(now).startOf("day");
|
||||
future = moment(now).startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
|
||||
}
|
||||
let events = [];
|
||||
|
||||
for (const calendarUrl in this.calendarData) {
|
||||
@@ -571,13 +612,14 @@ Module.register("calendar", {
|
||||
if (this.config.hideOngoing && event.startDate < now) {
|
||||
continue;
|
||||
}
|
||||
if (this.listContainsEvent(events, event)) {
|
||||
if (this.config.hideDuplicates && this.listContainsEvent(events, event)) {
|
||||
continue;
|
||||
}
|
||||
if (--remainingEntries < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
event.url = calendarUrl;
|
||||
event.today = event.startDate >= today && event.startDate < today + ONE_DAY;
|
||||
event.dayBeforeYesterday = event.startDate >= today - ONE_DAY * 2 && event.startDate < today - ONE_DAY;
|
||||
@@ -591,7 +633,12 @@ Module.register("calendar", {
|
||||
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 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
|
||||
@@ -660,9 +707,9 @@ Module.register("calendar", {
|
||||
return events.slice(0, this.config.maximumEntries);
|
||||
},
|
||||
|
||||
listContainsEvent: function (eventList, event) {
|
||||
listContainsEvent (eventList, event) {
|
||||
for (const evt of eventList) {
|
||||
if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)) {
|
||||
if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate) && parseInt(evt.endDate) === parseInt(event.endDate)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -675,7 +722,7 @@ Module.register("calendar", {
|
||||
* @param {object} auth The authentication method and credentials
|
||||
* @param {object} calendarConfig The config of the specific calendar
|
||||
*/
|
||||
addCalendar: function (url, auth, calendarConfig) {
|
||||
addCalendar (url, auth, calendarConfig) {
|
||||
this.sendSocketNotification("ADD_CALENDAR", {
|
||||
id: this.identifier,
|
||||
url: url,
|
||||
@@ -698,7 +745,7 @@ Module.register("calendar", {
|
||||
* @param {object} event Event to look for.
|
||||
* @returns {string[]} The symbols
|
||||
*/
|
||||
symbolsForEvent: function (event) {
|
||||
symbolsForEvent (event) {
|
||||
let symbols = this.getCalendarPropertyAsArray(event.url, "symbol", this.config.defaultSymbol);
|
||||
|
||||
if (event.recurringEvent === true && this.hasCalendarProperty(event.url, "recurringSymbol")) {
|
||||
@@ -725,7 +772,7 @@ Module.register("calendar", {
|
||||
return symbols;
|
||||
},
|
||||
|
||||
mergeUnique: function (arr1, arr2) {
|
||||
mergeUnique (arr1, arr2) {
|
||||
return arr1.concat(
|
||||
arr2.filter(function (item) {
|
||||
return arr1.indexOf(item) === -1;
|
||||
@@ -738,7 +785,7 @@ Module.register("calendar", {
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The class to be used for the symbols of the calendar
|
||||
*/
|
||||
symbolClassForUrl: function (url) {
|
||||
symbolClassForUrl (url) {
|
||||
return this.getCalendarProperty(url, "symbolClass", "");
|
||||
},
|
||||
|
||||
@@ -747,7 +794,7 @@ Module.register("calendar", {
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The class to be used for the title of the calendar
|
||||
*/
|
||||
titleClassForUrl: function (url) {
|
||||
titleClassForUrl (url) {
|
||||
return this.getCalendarProperty(url, "titleClass", "");
|
||||
},
|
||||
|
||||
@@ -756,7 +803,7 @@ Module.register("calendar", {
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The class to be used for the time of the calendar
|
||||
*/
|
||||
timeClassForUrl: function (url) {
|
||||
timeClassForUrl (url) {
|
||||
return this.getCalendarProperty(url, "timeClass", "");
|
||||
},
|
||||
|
||||
@@ -765,7 +812,7 @@ Module.register("calendar", {
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The name of the calendar
|
||||
*/
|
||||
calendarNameForUrl: function (url) {
|
||||
calendarNameForUrl (url) {
|
||||
return this.getCalendarProperty(url, "name", "");
|
||||
},
|
||||
|
||||
@@ -775,7 +822,7 @@ Module.register("calendar", {
|
||||
* @param {boolean} isBg Determines if we fetch the bgColor or not
|
||||
* @returns {string} The color
|
||||
*/
|
||||
colorForUrl: function (url, isBg) {
|
||||
colorForUrl (url, isBg) {
|
||||
return this.getCalendarProperty(url, isBg ? "bgColor" : "color", "#fff");
|
||||
},
|
||||
|
||||
@@ -784,7 +831,7 @@ Module.register("calendar", {
|
||||
* @param {string} url The calendar url
|
||||
* @returns {string} The title
|
||||
*/
|
||||
countTitleForUrl: function (url) {
|
||||
countTitleForUrl (url) {
|
||||
return this.getCalendarProperty(url, "repeatingCountTitle", this.config.defaultRepeatingCountTitle);
|
||||
},
|
||||
|
||||
@@ -793,7 +840,7 @@ Module.register("calendar", {
|
||||
* @param {string} url The calendar url
|
||||
* @returns {number} The maximum entry count
|
||||
*/
|
||||
maximumEntriesForUrl: function (url) {
|
||||
maximumEntriesForUrl (url) {
|
||||
return this.getCalendarProperty(url, "maximumEntries", this.config.maximumEntries);
|
||||
},
|
||||
|
||||
@@ -802,7 +849,7 @@ Module.register("calendar", {
|
||||
* @param {string} url The calendar url
|
||||
* @returns {number} The maximum past days count
|
||||
*/
|
||||
maximumPastDaysForUrl: function (url) {
|
||||
maximumPastDaysForUrl (url) {
|
||||
return this.getCalendarProperty(url, "pastDaysCount", this.config.pastDaysCount);
|
||||
},
|
||||
|
||||
@@ -813,7 +860,7 @@ Module.register("calendar", {
|
||||
* @param {string} defaultValue The value if the property is not found
|
||||
* @returns {*} The property
|
||||
*/
|
||||
getCalendarProperty: function (url, property, defaultValue) {
|
||||
getCalendarProperty (url, property, defaultValue) {
|
||||
for (const calendar of this.config.calendars) {
|
||||
if (calendar.url === url && calendar.hasOwnProperty(property)) {
|
||||
return calendar[property];
|
||||
@@ -823,7 +870,7 @@ Module.register("calendar", {
|
||||
return defaultValue;
|
||||
},
|
||||
|
||||
getCalendarPropertyAsArray: function (url, property, defaultValue) {
|
||||
getCalendarPropertyAsArray (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);
|
||||
@@ -834,7 +881,7 @@ Module.register("calendar", {
|
||||
return p;
|
||||
},
|
||||
|
||||
hasCalendarProperty: function (url, property) {
|
||||
hasCalendarProperty (url, property) {
|
||||
return !!this.getCalendarProperty(url, property, undefined);
|
||||
},
|
||||
|
||||
@@ -842,7 +889,7 @@ Module.register("calendar", {
|
||||
* Broadcasts the events to all other modules for reuse.
|
||||
* The all events available in one array, sorted on startdate.
|
||||
*/
|
||||
broadcastEvents: function () {
|
||||
broadcastEvents () {
|
||||
const eventList = this.createEventList(false);
|
||||
for (const event of eventList) {
|
||||
event.symbol = this.symbolsForEvent(event);
|
||||
@@ -852,5 +899,30 @@ Module.register("calendar", {
|
||||
}
|
||||
|
||||
this.sendNotification("CALENDAR_EVENTS", eventList);
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh the DOM every minute if needed: When using relative date format for events that start
|
||||
* or end in less than an hour, the date shows minute granularity and we want to keep that accurate.
|
||||
* --
|
||||
* When updateOnFetch is not set, it will Avoid fade out/in on updateDom when many calendars are used
|
||||
* and it's allow to refresh The DOM every minute with animation speed too
|
||||
* (because updateDom is not set in CALENDAR_EVENTS for this case)
|
||||
*/
|
||||
selfUpdate () {
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
setTimeout(
|
||||
() => {
|
||||
setInterval(() => {
|
||||
Log.debug("[Calendar] self update");
|
||||
if (this.config.updateOnFetch) {
|
||||
this.updateDom(1);
|
||||
} else {
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
}
|
||||
}, ONE_MINUTE);
|
||||
},
|
||||
ONE_MINUTE - (new Date() % ONE_MINUTE)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@@ -1,14 +1,5 @@
|
||||
/* MagicMirror²
|
||||
* Node Helper: Calendar - CalendarFetcher
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
const https = require("https");
|
||||
const digest = require("digest-fetch");
|
||||
const https = require("node:https");
|
||||
const ical = require("node-ical");
|
||||
const fetch = require("fetch");
|
||||
const Log = require("logger");
|
||||
const NodeHelper = require("node_helper");
|
||||
const CalendarFetcherUtils = require("./calendarfetcherutils");
|
||||
@@ -39,7 +30,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = null;
|
||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
let fetcher = null;
|
||||
let httpsAgent = null;
|
||||
let headers = {
|
||||
"User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`
|
||||
@@ -53,17 +43,12 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
if (auth) {
|
||||
if (auth.method === "bearer") {
|
||||
headers.Authorization = `Bearer ${auth.pass}`;
|
||||
} else if (auth.method === "digest") {
|
||||
fetcher = new digest(auth.user, auth.pass).fetch(url, { headers: headers, agent: httpsAgent });
|
||||
} else {
|
||||
headers.Authorization = `Basic ${Buffer.from(`${auth.user}:${auth.pass}`).toString("base64")}`;
|
||||
}
|
||||
}
|
||||
if (fetcher === null) {
|
||||
fetcher = fetch(url, { headers: headers, agent: httpsAgent });
|
||||
}
|
||||
|
||||
fetcher
|
||||
fetch(url, { headers: headers, agent: httpsAgent })
|
||||
.then(NodeHelper.checkFetchStatus)
|
||||
.then((response) => response.text())
|
||||
.then((responseData) => {
|
||||
@@ -115,7 +100,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
* Broadcast the existing events.
|
||||
*/
|
||||
this.broadcastEvents = function () {
|
||||
Log.info(`Calendar-Fetcher: Broadcasting ${events.length} events.`);
|
||||
Log.info(`Calendar-Fetcher: Broadcasting ${events.length} events from ${url}.`);
|
||||
eventsReceivedCallback(this);
|
||||
};
|
||||
|
||||
|
@@ -1,19 +1,14 @@
|
||||
/* MagicMirror²
|
||||
* Calendar Fetcher Util Methods
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external Moment
|
||||
*/
|
||||
const path = require("path");
|
||||
const path = require("node:path");
|
||||
const moment = require("moment");
|
||||
|
||||
const zoneTable = require(path.join(__dirname, "windowsZones.json"));
|
||||
const Log = require("../../../js/logger");
|
||||
|
||||
const CalendarFetcherUtils = {
|
||||
|
||||
/**
|
||||
* Calculate the time correction, either dst/std or full day in cases where
|
||||
* utc time is day before plus offset
|
||||
@@ -21,7 +16,7 @@ const CalendarFetcherUtils = {
|
||||
* @param {Date} date the date on which this event happens
|
||||
* @returns {number} the necessary adjustment in hours
|
||||
*/
|
||||
calculateTimezoneAdjustment: function (event, date) {
|
||||
calculateTimezoneAdjustment (event, date) {
|
||||
let adjustHours = 0;
|
||||
// if a timezone was specified
|
||||
if (!event.start.tz) {
|
||||
@@ -61,7 +56,7 @@ const CalendarFetcherUtils = {
|
||||
event.start.tz = "";
|
||||
Log.debug(`ical offset=${current_offset} date=${date}`);
|
||||
mm = moment(date);
|
||||
let x = parseInt(moment(new Date()).utcOffset());
|
||||
let x = moment(new Date()).utcOffset();
|
||||
Log.debug(`net mins=${current_offset * 60 - x}`);
|
||||
|
||||
mm = mm.add(x - current_offset * 60, "minutes");
|
||||
@@ -122,7 +117,7 @@ const CalendarFetcherUtils = {
|
||||
* @param {object} config The configuration object
|
||||
* @returns {string[]} the filtered events
|
||||
*/
|
||||
filterEvents: function (data, config) {
|
||||
filterEvents (data, config) {
|
||||
const newEvents = [];
|
||||
|
||||
// limitFunction doesn't do much limiting, see comment re: the dates
|
||||
@@ -133,19 +128,26 @@ const CalendarFetcherUtils = {
|
||||
};
|
||||
|
||||
const eventDate = function (event, time) {
|
||||
return CalendarFetcherUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||
return CalendarFetcherUtils.isFullDayEvent(event) ? moment(event[time]).startOf("day") : moment(event[time]);
|
||||
};
|
||||
|
||||
Log.debug(`There are ${Object.entries(data).length} calendar entries.`);
|
||||
|
||||
const now = new Date(Date.now());
|
||||
const todayLocal = moment(now).startOf("day").toDate();
|
||||
const futureLocalDate
|
||||
= moment(now)
|
||||
.startOf("day")
|
||||
.add(config.maximumNumberOfDays, "days")
|
||||
.subtract(1, "seconds") // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||
.toDate();
|
||||
|
||||
Object.entries(data).forEach(([key, event]) => {
|
||||
Log.debug("Processing entry...");
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day").toDate();
|
||||
const future = moment().startOf("day").add(config.maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||
let past = today;
|
||||
let pastLocalDate = todayLocal;
|
||||
|
||||
if (config.includePastEvents) {
|
||||
past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
|
||||
pastLocalDate = moment(now).startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
|
||||
}
|
||||
|
||||
// FIXME: Ugly fix to solve the facebook birthday issue.
|
||||
@@ -159,33 +161,33 @@ const CalendarFetcherUtils = {
|
||||
|
||||
if (event.type === "VEVENT") {
|
||||
Log.debug(`Event:\n${JSON.stringify(event)}`);
|
||||
let startDate = eventDate(event, "start");
|
||||
let endDate;
|
||||
let startMoment = eventDate(event, "start");
|
||||
let endMoment;
|
||||
|
||||
if (typeof event.end !== "undefined") {
|
||||
endDate = eventDate(event, "end");
|
||||
endMoment = eventDate(event, "end");
|
||||
} else if (typeof event.duration !== "undefined") {
|
||||
endDate = startDate.clone().add(moment.duration(event.duration));
|
||||
endMoment = startMoment.clone().add(moment.duration(event.duration));
|
||||
} else {
|
||||
if (!isFacebookBirthday) {
|
||||
// make copy of start date, separate storage area
|
||||
endDate = moment(startDate.format("x"), "x");
|
||||
endMoment = moment(startMoment.valueOf());
|
||||
} else {
|
||||
endDate = moment(startDate).add(1, "days");
|
||||
endMoment = moment(startMoment).add(1, "days");
|
||||
}
|
||||
}
|
||||
|
||||
Log.debug(`start: ${startDate.toDate()}`);
|
||||
Log.debug(`end:: ${endDate.toDate()}`);
|
||||
Log.debug(`start: ${startMoment.toDate()}`);
|
||||
Log.debug(`end:: ${endMoment.toDate()}`);
|
||||
|
||||
// Calculate the duration of the event for use with recurring events.
|
||||
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||
Log.debug(`duration: ${duration}`);
|
||||
const durationMs = endMoment.valueOf() - startMoment.valueOf();
|
||||
Log.debug(`duration: ${durationMs}`);
|
||||
|
||||
// FIXME: Since the parsed json object from node-ical comes with time information
|
||||
// this check could be removed (?)
|
||||
if (event.start.length === 8) {
|
||||
startDate = startDate.startOf("day");
|
||||
startMoment = startMoment.startOf("day");
|
||||
}
|
||||
|
||||
const title = CalendarFetcherUtils.getTitleFromEvent(event);
|
||||
@@ -245,12 +247,11 @@ const CalendarFetcherUtils = {
|
||||
const geo = event.geo || false;
|
||||
const description = event.description || false;
|
||||
|
||||
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
|
||||
if (event.rrule && typeof event.rrule !== "undefined" && !isFacebookBirthday) {
|
||||
const rule = event.rrule;
|
||||
let addedEvents = 0;
|
||||
|
||||
const pastMoment = moment(past);
|
||||
const futureMoment = moment(future);
|
||||
const pastMoment = moment(pastLocalDate);
|
||||
const futureMoment = moment(futureLocalDate);
|
||||
|
||||
// can cause problems with e.g. birthdays before 1900
|
||||
if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
|
||||
@@ -261,8 +262,8 @@ const CalendarFetcherUtils = {
|
||||
// For recurring events, get the set of start dates that fall within the range
|
||||
// of dates we're looking for.
|
||||
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
|
||||
let pastLocal = 0;
|
||||
let futureLocal = 0;
|
||||
let pastLocal;
|
||||
let futureLocal;
|
||||
if (CalendarFetcherUtils.isFullDayEvent(event)) {
|
||||
Log.debug("fullday");
|
||||
// if full day event, only use the date part of the ranges
|
||||
@@ -278,13 +279,57 @@ const CalendarFetcherUtils = {
|
||||
pastLocal = pastMoment.toDate();
|
||||
} else {
|
||||
// otherwise use NOW.. cause we shouldn't use any before now
|
||||
pastLocal = moment().toDate(); //now
|
||||
pastLocal = moment(now).toDate(); //now
|
||||
}
|
||||
futureLocal = futureMoment.toDate(); // future
|
||||
}
|
||||
Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`);
|
||||
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
const hasByWeekdayRule = rule.options.byweekday !== undefined && rule.options.byweekday !== null;
|
||||
const oneDayInMs = 24 * 60 * 60 * 1000;
|
||||
Log.debug(`RRule: ${rule.toString()}`);
|
||||
rule.options.tzid = null; // RRule gets *very* confused with timezones
|
||||
let dates = rule.between(new Date(pastLocal.valueOf() - oneDayInMs), new Date(futureLocal.valueOf() + oneDayInMs), true, () => { return true; });
|
||||
Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`);
|
||||
dates = dates.filter((d) => {
|
||||
if (JSON.stringify(d) === "null") return false;
|
||||
else return true;
|
||||
});
|
||||
|
||||
// RRule can generate dates with an incorrect recurrence date. Process the array here and apply date correction.
|
||||
if (hasByWeekdayRule) {
|
||||
Log.debug("Rule has byweekday, checking for correction");
|
||||
dates.forEach((date, index, arr) => {
|
||||
// NOTE: getTimezoneOffset() is negative of the expected value. For America/Los_Angeles under DST (GMT-7),
|
||||
// this value is +420. For Australia/Sydney under DST (GMT+11), this value is -660.
|
||||
const tzOffset = date.getTimezoneOffset() / 60;
|
||||
const hour = date.getHours();
|
||||
if ((tzOffset < 0) && (hour < -tzOffset)) { // east of GMT
|
||||
Log.debug(`East of GMT (tzOffset: ${tzOffset}) and hour=${hour} < ${-tzOffset}, Subtracting 1 day from ${date}`);
|
||||
arr[index] = new Date(date.valueOf() - oneDayInMs);
|
||||
} else if ((tzOffset > 0) && (hour >= (24 - tzOffset))) { // west of GMT
|
||||
Log.debug(`West of GMT (tzOffset: ${tzOffset}) and hour=${hour} >= 24-${tzOffset}, Adding 1 day to ${date}`);
|
||||
arr[index] = new Date(date.valueOf() + oneDayInMs);
|
||||
}
|
||||
});
|
||||
// Adjusting the dates could push it beyond the 'until' date, so filter those out here.
|
||||
if (rule.options.until !== null) {
|
||||
dates = dates.filter((date) => {
|
||||
return date.valueOf() <= rule.options.until.valueOf();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// The dates array from rrule can be confused by DST. If the event was created during DST and we
|
||||
// are querying a date range during non-DST, rrule can have the incorrect time for the date range.
|
||||
// Reprocess the array here computing and applying the time offset.
|
||||
dates.forEach((date, index, arr) => {
|
||||
let adjustHours = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date);
|
||||
if (adjustHours !== 0) {
|
||||
Log.debug(`Applying timezone adjustment hours=${adjustHours} to ${date}`);
|
||||
arr[index] = new Date(date.valueOf() + (adjustHours * 60 * 60 * 1000));
|
||||
}
|
||||
});
|
||||
|
||||
// The "dates" array contains the set of dates within our desired date range range that are valid
|
||||
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
|
||||
// had its date changed from outside the range to inside the range. For the time being,
|
||||
@@ -294,149 +339,80 @@ const CalendarFetcherUtils = {
|
||||
// Would be great if there was a better way to handle this.
|
||||
Log.debug(`event.recurrences: ${event.recurrences}`);
|
||||
if (event.recurrences !== undefined) {
|
||||
for (let r in event.recurrences) {
|
||||
for (let dateKey in event.recurrences) {
|
||||
// Only add dates that weren't already in the range we added from the rrule so that
|
||||
// we don"t double-add those events.
|
||||
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
|
||||
dates.push(new Date(r));
|
||||
// we don't double-add those events.
|
||||
let d = new Date(dateKey);
|
||||
if (!moment(d).isBetween(pastMoment, futureMoment)) {
|
||||
dates.push(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lastly, sometimes rrule doesn't include the event.start even if it is in the requested range. Ensure
|
||||
// inclusion here. Unfortunately dates.includes() doesn't find it so we have to do forEach().
|
||||
{
|
||||
let found = false;
|
||||
dates.forEach((d) => { if (d.valueOf() === event.start.valueOf()) found = true; });
|
||||
if (!found) {
|
||||
Log.debug(`event.start=${event.start} was not included in results from rrule; adding`);
|
||||
dates.splice(0, 0, event.start);
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through the set of date entries to see which recurrences should be added to our event list.
|
||||
for (let d in dates) {
|
||||
let date = dates[d];
|
||||
let curEvent = event;
|
||||
let curDurationMs = durationMs;
|
||||
let showRecurrence = true;
|
||||
|
||||
startMoment = moment(date);
|
||||
|
||||
// Remove the time information of each date by using its substring, using the following method:
|
||||
// .toISOString().substring(0,10).
|
||||
// since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
|
||||
// (see https://momentjs.com/docs/#/displaying/as-iso-string/).
|
||||
// This must be done after `date` is adjusted
|
||||
const dateKey = date.toISOString().substring(0, 10);
|
||||
let curEvent = event;
|
||||
let showRecurrence = true;
|
||||
|
||||
// Get the offset of today where we are processing
|
||||
// This will be the correction, we need to apply.
|
||||
let nowOffset = new Date().getTimezoneOffset();
|
||||
// For full day events, the time might be off from RRULE/Luxon problem
|
||||
// Get time zone offset of the rule calculated event
|
||||
let dateoffset = date.getTimezoneOffset();
|
||||
|
||||
// Reduce the time by the following offset.
|
||||
Log.debug(` recurring date is ${date} offset is ${dateoffset}`);
|
||||
|
||||
let dh = moment(date).format("HH");
|
||||
Log.debug(` recurring date is ${date} offset is ${dateoffset / 60} Hour is ${dh}`);
|
||||
|
||||
if (CalendarFetcherUtils.isFullDayEvent(event)) {
|
||||
Log.debug("Fullday");
|
||||
// If the offset is negative (east of GMT), where the problem is
|
||||
if (dateoffset < 0) {
|
||||
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 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 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)) {
|
||||
// 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;
|
||||
Log.debug(`new recurring date1 is ${date}`);
|
||||
}
|
||||
} else {
|
||||
// if the timezones are the same, correct date if needed
|
||||
//if (event.start.tz === moment.tz.guess()) {
|
||||
// if the date hour is less than the offset
|
||||
if (24 - dh <= Math.abs(dateoffset / 60)) {
|
||||
// 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}`);
|
||||
}
|
||||
//}
|
||||
}
|
||||
}
|
||||
startDate = moment(date);
|
||||
Log.debug(`Corrected startDate: ${startDate.toDate()}`);
|
||||
|
||||
let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date);
|
||||
|
||||
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
|
||||
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
|
||||
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||
curEvent = curEvent.recurrences[dateKey];
|
||||
startDate = moment(curEvent.start);
|
||||
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
|
||||
startMoment = moment(curEvent.start);
|
||||
curDurationMs = curEvent.end.valueOf() - startMoment.valueOf();
|
||||
}
|
||||
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
|
||||
else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
|
||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||
showRecurrence = false;
|
||||
}
|
||||
Log.debug(`duration: ${duration}`);
|
||||
Log.debug(`duration: ${curDurationMs}`);
|
||||
|
||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
||||
if (startDate.format("x") === endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
endMoment = moment(startMoment.valueOf() + curDurationMs);
|
||||
if (startMoment.valueOf() === endMoment.valueOf()) {
|
||||
endMoment = endMoment.endOf("day");
|
||||
}
|
||||
|
||||
const recurrenceTitle = CalendarFetcherUtils.getTitleFromEvent(curEvent);
|
||||
|
||||
// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
|
||||
// it to the event list.
|
||||
if (endDate.isBefore(past) || startDate.isAfter(future)) {
|
||||
if (endMoment.isBefore(pastLocal) || startMoment.isAfter(futureLocal)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) {
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, endMoment, dateFilter)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (showRecurrence === true) {
|
||||
Log.debug(`saving event: ${description}`);
|
||||
addedEvents++;
|
||||
newEvents.push({
|
||||
title: recurrenceTitle,
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
startDate: startMoment.format("x"),
|
||||
endDate: endMoment.format("x"),
|
||||
fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event),
|
||||
recurringEvent: true,
|
||||
class: event.class,
|
||||
@@ -454,43 +430,47 @@ const CalendarFetcherUtils = {
|
||||
// Log.debug("full day event")
|
||||
|
||||
// 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");
|
||||
if (fullDayEvent && startMoment.valueOf() === endMoment.valueOf()) {
|
||||
endMoment = endMoment.endOf("day");
|
||||
}
|
||||
|
||||
if (config.includePastEvents) {
|
||||
// Past event is too far in the past, so skip.
|
||||
if (endDate < past) {
|
||||
if (endMoment < pastLocalDate) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// It's not a fullday event, and it is in the past, so skip.
|
||||
if (!fullDayEvent && endDate < new Date()) {
|
||||
if (!fullDayEvent && endMoment < now) {
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a fullday event, and it is before today, So skip.
|
||||
if (fullDayEvent && endDate <= today) {
|
||||
if (fullDayEvent && endMoment <= todayLocal) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// It exceeds the maximumNumberOfDays limit, so skip.
|
||||
if (startDate > future) {
|
||||
if (startMoment > futureLocalDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) {
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, endMoment, dateFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get correction for date saving and dst change between now and then
|
||||
let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, startDate.toDate());
|
||||
let adjustHours = CalendarFetcherUtils.calculateTimezoneAdjustment(event, startMoment.toDate());
|
||||
// This shouldn't happen
|
||||
if (adjustHours) {
|
||||
Log.warn(`Unexpected timezone adjustment of ${adjustHours} hours on non-recurring event`);
|
||||
}
|
||||
// Every thing is good. Add it to the list.
|
||||
newEvents.push({
|
||||
title: title,
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
startDate: startMoment.add(adjustHours, "hours").format("x"),
|
||||
endDate: endMoment.add(adjustHours, "hours").format("x"),
|
||||
fullDayEvent: fullDayEvent,
|
||||
class: event.class,
|
||||
location: location,
|
||||
@@ -513,7 +493,7 @@ const CalendarFetcherUtils = {
|
||||
* @param {string} msTZName the timezone name to lookup
|
||||
* @returns {string|null} the iana name or null of none is found
|
||||
*/
|
||||
getIanaTZFromMS: function (msTZName) {
|
||||
getIanaTZFromMS (msTZName) {
|
||||
// Get hash entry
|
||||
const he = zoneTable[msTZName];
|
||||
// If found return iana name, else null
|
||||
@@ -525,7 +505,7 @@ const CalendarFetcherUtils = {
|
||||
* @param {object} event The event object to check.
|
||||
* @returns {string} The title of the event, or "Event" if no title is found.
|
||||
*/
|
||||
getTitleFromEvent: function (event) {
|
||||
getTitleFromEvent (event) {
|
||||
let title = "Event";
|
||||
if (event.summary) {
|
||||
title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
|
||||
@@ -541,7 +521,7 @@ const CalendarFetcherUtils = {
|
||||
* @param {object} event The event object to check.
|
||||
* @returns {boolean} True if the event is a fullday event, false otherwise
|
||||
*/
|
||||
isFullDayEvent: function (event) {
|
||||
isFullDayEvent (event) {
|
||||
if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
|
||||
return true;
|
||||
}
|
||||
@@ -564,14 +544,14 @@ const CalendarFetcherUtils = {
|
||||
* @param {string} filter The time to subtract from the end date to determine if an event should be shown
|
||||
* @returns {boolean} True if the event should be filtered out, false otherwise
|
||||
*/
|
||||
timeFilterApplies: function (now, endDate, filter) {
|
||||
timeFilterApplies (now, endDate, filter) {
|
||||
if (filter) {
|
||||
const until = filter.split(" "),
|
||||
value = parseInt(until[0]),
|
||||
increment = until[1].slice(-1) === "s" ? until[1] : `${until[1]}s`, // Massage the data for moment js
|
||||
filterUntil = moment(endDate.format()).subtract(value, increment);
|
||||
|
||||
return now < filterUntil.format("x");
|
||||
return now < filterUntil.toDate();
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -585,7 +565,7 @@ const CalendarFetcherUtils = {
|
||||
* @param {string} regexFlags flags that should be applied to the regex
|
||||
* @returns {boolean} True if the title should be filtered out, false otherwise
|
||||
*/
|
||||
titleFilterApplies: function (title, filter, useRegex, regexFlags) {
|
||||
titleFilterApplies (title, filter, useRegex, regexFlags) {
|
||||
if (useRegex) {
|
||||
let regexFilter = filter;
|
||||
// Assume if leading slash, there is also trailing slash
|
||||
|
@@ -1,16 +1,11 @@
|
||||
/* MagicMirror²
|
||||
* Calendar Util Methods
|
||||
*
|
||||
* By Rejas
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const CalendarUtils = {
|
||||
|
||||
/**
|
||||
* Capitalize the first letter of a string
|
||||
* @param {string} string The string to capitalize
|
||||
* @returns {string} The capitalized string
|
||||
*/
|
||||
capFirst: function (string) {
|
||||
capFirst (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
},
|
||||
|
||||
@@ -21,7 +16,7 @@ const CalendarUtils = {
|
||||
* @param {number} timeFormat Specifies either 12 or 24-hour time format
|
||||
* @returns {moment.LocaleSpecification} formatted time
|
||||
*/
|
||||
getLocaleSpecification: function (timeFormat) {
|
||||
getLocaleSpecification (timeFormat) {
|
||||
switch (timeFormat) {
|
||||
case 12: {
|
||||
return { longDateFormat: { LT: "h:mm A" } };
|
||||
@@ -43,7 +38,7 @@ const CalendarUtils = {
|
||||
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
|
||||
* @returns {string} The shortened string
|
||||
*/
|
||||
shorten: function (string, maxLength, wrapEvents, maxTitleLines) {
|
||||
shorten (string, maxLength, wrapEvents, maxTitleLines) {
|
||||
if (typeof string !== "string") {
|
||||
return "";
|
||||
}
|
||||
@@ -90,23 +85,39 @@ const CalendarUtils = {
|
||||
/**
|
||||
* Transforms the title of an event for usage.
|
||||
* Replaces parts of the text as defined in config.titleReplace.
|
||||
* Shortens title based on config.maxTitleLength and config.wrapEvents
|
||||
* @param {string} title The title to transform.
|
||||
* @param {object} titleReplace Pairs of strings to be replaced in the title
|
||||
* @param {object} titleReplace object definition of parts to be replaced in the title
|
||||
* object definition:
|
||||
* search: {string,required} RegEx in format //x or simple string to be searched. For (birthday) year calcluation, the element matching the year must be in a RegEx group
|
||||
* replace: {string,required} Replacement string, may contain match group references (latter is required for year calculation)
|
||||
* yearmatchgroup: {number,optional} match group for year element
|
||||
* @returns {string} The transformed title.
|
||||
*/
|
||||
titleTransform: function (title, titleReplace) {
|
||||
titleTransform (title, titleReplace) {
|
||||
let transformedTitle = title;
|
||||
for (let needle in titleReplace) {
|
||||
const replacement = titleReplace[needle];
|
||||
for (let tr in titleReplace) {
|
||||
let transform = titleReplace[tr];
|
||||
if (typeof transform === "object") {
|
||||
if (typeof transform.search !== "undefined" && transform.search !== "" && typeof transform.replace !== "undefined") {
|
||||
let regParts = transform.search.match(/^\/(.+)\/([gim]*)$/);
|
||||
let needle = new RegExp(transform.search, "g");
|
||||
if (regParts) {
|
||||
// the parsed pattern is a regexp with flags.
|
||||
needle = new RegExp(regParts[1], regParts[2]);
|
||||
}
|
||||
|
||||
const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
|
||||
if (regParts) {
|
||||
// the parsed pattern is a regexp.
|
||||
needle = new RegExp(regParts[1], regParts[2]);
|
||||
let replacement = transform.replace;
|
||||
if (typeof transform.yearmatchgroup !== "undefined" && transform.yearmatchgroup !== "") {
|
||||
const yearmatch = [...title.matchAll(needle)];
|
||||
if (yearmatch[0].length >= transform.yearmatchgroup + 1 && yearmatch[0][transform.yearmatchgroup] * 1 >= 1900) {
|
||||
let calcage = new Date().getFullYear() - yearmatch[0][transform.yearmatchgroup] * 1;
|
||||
let searchstr = `$${transform.yearmatchgroup}`;
|
||||
replacement = replacement.replace(searchstr, calcage);
|
||||
}
|
||||
}
|
||||
transformedTitle = transformedTitle.replace(needle, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
transformedTitle = transformedTitle.replace(needle, replacement);
|
||||
}
|
||||
return transformedTitle;
|
||||
}
|
||||
|
@@ -1,9 +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.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
// Alias modules mentioned in package.js under _moduleAliases.
|
||||
require("module-alias/register");
|
||||
|
@@ -1,22 +1,16 @@
|
||||
/* MagicMirror²
|
||||
* Node Helper: Calendar
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const NodeHelper = require("node_helper");
|
||||
const Log = require("logger");
|
||||
const CalendarFetcher = require("./calendarfetcher");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
start: function () {
|
||||
start () {
|
||||
Log.log(`Starting node helper for: ${this.name}`);
|
||||
this.fetchers = [];
|
||||
},
|
||||
|
||||
// Override socketNotificationReceived method.
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
socketNotificationReceived (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") {
|
||||
@@ -43,7 +37,7 @@ module.exports = NodeHelper.create({
|
||||
* @param {boolean} selfSignedCert If true, the server certificate is not verified against the list of supplied CAs.
|
||||
* @param {string} identifier ID of the module
|
||||
*/
|
||||
createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
|
||||
createFetcher (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (error) {
|
||||
@@ -53,9 +47,14 @@ module.exports = NodeHelper.create({
|
||||
}
|
||||
|
||||
let fetcher;
|
||||
let fetchIntervalCorrected;
|
||||
if (typeof this.fetchers[identifier + url] === "undefined") {
|
||||
Log.log(`Create new calendarfetcher for url: ${url} - Interval: ${fetchInterval}`);
|
||||
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
|
||||
if (fetchInterval < 60000) {
|
||||
Log.warn(`fetchInterval for url ${url} must be >= 60000`);
|
||||
fetchIntervalCorrected = 60000;
|
||||
}
|
||||
Log.log(`Create new calendarfetcher for url: ${url} - Interval: ${fetchIntervalCorrected || fetchInterval}`);
|
||||
fetcher = new CalendarFetcher(url, fetchIntervalCorrected || fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
|
||||
|
||||
fetcher.onReceive((fetcher) => {
|
||||
this.broadcastEvents(fetcher, identifier);
|
||||
@@ -85,7 +84,7 @@ module.exports = NodeHelper.create({
|
||||
* @param {object} fetcher the fetcher associated with the calendar
|
||||
* @param {string} identifier the identifier of the calendar
|
||||
*/
|
||||
broadcastEvents: function (fetcher, identifier) {
|
||||
broadcastEvents (fetcher, identifier) {
|
||||
this.sendSocketNotification("CALENDAR_EVENTS", {
|
||||
id: identifier,
|
||||
url: fetcher.url(),
|
||||
|
@@ -1,11 +1,5 @@
|
||||
/* global SunCalc, formatTime */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Clock
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("clock", {
|
||||
// Module config defaults.
|
||||
defaults: {
|
||||
@@ -32,20 +26,20 @@ Module.register("clock", {
|
||||
secondsColor: "#888888",
|
||||
|
||||
showSunTimes: false,
|
||||
showMoonTimes: false,
|
||||
showMoonTimes: false, // options: false, 'times' (rise/set), 'percent' (lit percent), 'phase' (current phase), or 'both' (percent & phase)
|
||||
lat: 47.630539,
|
||||
lon: -122.344147
|
||||
},
|
||||
// Define required scripts.
|
||||
getScripts: function () {
|
||||
getScripts () {
|
||||
return ["moment.js", "moment-timezone.js", "suncalc.js"];
|
||||
},
|
||||
// Define styles.
|
||||
getStyles: function () {
|
||||
getStyles () {
|
||||
return ["clock_styles.css"];
|
||||
},
|
||||
// Define start sequence.
|
||||
start: function () {
|
||||
start () {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
// Schedule update interval.
|
||||
@@ -94,7 +88,7 @@ Module.register("clock", {
|
||||
moment.locale(config.language);
|
||||
},
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
getDom () {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.classList.add("clock-grid");
|
||||
|
||||
@@ -105,7 +99,6 @@ Module.register("clock", {
|
||||
analogWrapper.className = "clock-circle";
|
||||
const digitalWrapper = document.createElement("div");
|
||||
digitalWrapper.className = "digital";
|
||||
digitalWrapper.style.gridArea = "center";
|
||||
|
||||
/************************************
|
||||
* Create wrappers for DIGITAL clock
|
||||
@@ -129,7 +122,7 @@ Module.register("clock", {
|
||||
// Set content of wrappers.
|
||||
// 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
|
||||
// See issue: https://github.com/MagicMirrorOrg/MagicMirror/issues/181
|
||||
let timeString;
|
||||
const now = moment();
|
||||
if (this.config.timezone) {
|
||||
@@ -186,10 +179,10 @@ Module.register("clock", {
|
||||
}
|
||||
const untilNextEvent = moment.duration(moment(nextEvent).diff(now));
|
||||
const untilNextEventString = `${untilNextEvent.hours()}h ${untilNextEvent.minutes()}m`;
|
||||
sunWrapper.innerHTML =
|
||||
`<span class="${isVisible ? "bright" : ""}"><i class="fas fa-sun" aria-hidden="true"></i> ${untilNextEventString}</span>` +
|
||||
`<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ${formatTime(this.config, sunTimes.sunrise)}</span>` +
|
||||
`<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ${formatTime(this.config, sunTimes.sunset)}</span>`;
|
||||
sunWrapper.innerHTML
|
||||
= `<span class="${isVisible ? "bright" : ""}"><i class="fas fa-sun" aria-hidden="true"></i> ${untilNextEventString}</span>`
|
||||
+ `<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ${formatTime(this.config, sunTimes.sunrise)}</span>`
|
||||
+ `<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ${formatTime(this.config, sunTimes.sunset)}</span>`;
|
||||
digitalWrapper.appendChild(sunWrapper);
|
||||
}
|
||||
|
||||
@@ -208,11 +201,15 @@ Module.register("clock", {
|
||||
moonSet = nextMoonTimes.set;
|
||||
}
|
||||
const isVisible = now.isBetween(moonRise, moonSet) || moonTimes.alwaysUp === true;
|
||||
const showFraction = ["both", "percent"].includes(this.config.showMoonTimes);
|
||||
const showUnicode = ["both", "phase"].includes(this.config.showMoonTimes);
|
||||
const illuminatedFractionString = `${Math.round(moonIllumination.fraction * 100)}%`;
|
||||
moonWrapper.innerHTML =
|
||||
`<span class="${isVisible ? "bright" : ""}"><i class="fas fa-moon" aria-hidden="true"></i> ${illuminatedFractionString}</span>` +
|
||||
`<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ${moonRise ? formatTime(this.config, moonRise) : "..."}</span>` +
|
||||
`<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ${moonSet ? formatTime(this.config, moonSet) : "..."}</span>`;
|
||||
const image = showUnicode ? [..."🌑🌒🌓🌔🌕🌖🌗🌘"][Math.floor(moonIllumination.phase * 8)] : "<i class=\"fas fa-moon\" aria-hidden=\"true\"></i>";
|
||||
|
||||
moonWrapper.innerHTML
|
||||
= `<span class="${isVisible ? "bright" : ""}">${image} ${showFraction ? illuminatedFractionString : ""}</span>`
|
||||
+ `<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ${moonRise ? formatTime(this.config, moonRise) : "..."}</span>`
|
||||
+ `<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ${moonSet ? formatTime(this.config, moonSet) : "..."}</span>`;
|
||||
digitalWrapper.appendChild(moonWrapper);
|
||||
}
|
||||
|
||||
@@ -243,7 +240,7 @@ Module.register("clock", {
|
||||
analogWrapper.style.background = `url(${this.data.path}faces/${this.config.analogFace}.svg)`;
|
||||
analogWrapper.style.backgroundSize = "100%";
|
||||
|
||||
// The following line solves issue: https://github.com/MichMich/MagicMirror/issues/611
|
||||
// The following line solves issue: https://github.com/MagicMirrorOrg/MagicMirror/issues/611
|
||||
// analogWrapper.style.border = "1px solid black";
|
||||
analogWrapper.style.border = "rgba(0, 0, 0, 0.1)"; //Updated fix for Issue 611 where non-black backgrounds are used
|
||||
} else if (this.config.analogFace !== "none") {
|
||||
|
@@ -1,9 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* Module: Compliments
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("compliments", {
|
||||
// Module config defaults.
|
||||
defaults: {
|
||||
@@ -21,19 +15,20 @@ Module.register("compliments", {
|
||||
morningEndTime: 12,
|
||||
afternoonStartTime: 12,
|
||||
afternoonEndTime: 17,
|
||||
random: true
|
||||
random: true,
|
||||
specialDayUnique: false
|
||||
},
|
||||
lastIndexUsed: -1,
|
||||
// Set currentweather from module
|
||||
currentWeatherType: "",
|
||||
|
||||
// Define required scripts.
|
||||
getScripts: function () {
|
||||
getScripts () {
|
||||
return ["moment.js"];
|
||||
},
|
||||
|
||||
// Define start sequence.
|
||||
start: async function () {
|
||||
async start () {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
this.lastComplimentIndex = -1;
|
||||
@@ -55,8 +50,8 @@ Module.register("compliments", {
|
||||
* @param {string[]} compliments Array with compliments.
|
||||
* @returns {number} a random index of given array
|
||||
*/
|
||||
randomIndex: function (compliments) {
|
||||
if (compliments.length === 1) {
|
||||
randomIndex (compliments) {
|
||||
if (compliments.length <= 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -79,7 +74,7 @@ Module.register("compliments", {
|
||||
* Retrieve an array of compliments for the time of the day.
|
||||
* @returns {string[]} array with compliments for the time of the day.
|
||||
*/
|
||||
complimentArray: function () {
|
||||
complimentArray () {
|
||||
const hour = moment().hour();
|
||||
const date = moment().format("YYYY-MM-DD");
|
||||
let compliments = [];
|
||||
@@ -104,6 +99,10 @@ Module.register("compliments", {
|
||||
// Add compliments for special days
|
||||
for (let entry in this.config.compliments) {
|
||||
if (new RegExp(entry).test(date)) {
|
||||
// Only display compliments configured for the day if specialDayUnique is set to true
|
||||
if (this.config.specialDayUnique) {
|
||||
compliments.length = 0;
|
||||
}
|
||||
Array.prototype.push.apply(compliments, this.config.compliments[entry]);
|
||||
}
|
||||
}
|
||||
@@ -115,7 +114,7 @@ Module.register("compliments", {
|
||||
* Retrieve a file from the local filesystem
|
||||
* @returns {Promise} Resolved when the file is loaded
|
||||
*/
|
||||
loadComplimentFile: async function () {
|
||||
async loadComplimentFile () {
|
||||
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);
|
||||
@@ -126,7 +125,7 @@ Module.register("compliments", {
|
||||
* Retrieve a random compliment.
|
||||
* @returns {string} a compliment
|
||||
*/
|
||||
getRandomCompliment: function () {
|
||||
getRandomCompliment () {
|
||||
// get the current time of day compliments list
|
||||
const compliments = this.complimentArray();
|
||||
// variable for index to next message to display
|
||||
@@ -145,7 +144,7 @@ Module.register("compliments", {
|
||||
},
|
||||
|
||||
// Override dom generator.
|
||||
getDom: function () {
|
||||
getDom () {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = this.config.classes ? this.config.classes : "thin xlarge bright pre-line";
|
||||
// get the compliment text
|
||||
@@ -173,7 +172,7 @@ Module.register("compliments", {
|
||||
},
|
||||
|
||||
// Override notification handler.
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
notificationReceived (notification, payload, sender) {
|
||||
if (notification === "CURRENTWEATHER_TYPE") {
|
||||
this.currentWeatherType = payload.type;
|
||||
}
|
||||
|
@@ -1,8 +1,5 @@
|
||||
/* MagicMirror² Default Modules List
|
||||
/* 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", "helloworld", "newsfeed", "updatenotification", "weather"];
|
||||
|
||||
|
@@ -1,20 +1,14 @@
|
||||
/* MagicMirror²
|
||||
* Module: HelloWorld
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("helloworld", {
|
||||
// Default module config.
|
||||
defaults: {
|
||||
text: "Hello World!"
|
||||
},
|
||||
|
||||
getTemplate: function () {
|
||||
getTemplate () {
|
||||
return "helloworld.njk";
|
||||
},
|
||||
|
||||
getTemplateData: function () {
|
||||
getTemplateData () {
|
||||
return this.config;
|
||||
}
|
||||
});
|
||||
|
@@ -2,4 +2,4 @@
|
||||
Use ` | safe` to allow html tages within the text string.
|
||||
https://mozilla.github.io/nunjucks/templating.html#autoescaping
|
||||
-->
|
||||
<div>{{text | safe}}</div>
|
||||
<div>{{ text | safe }}</div>
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<div>
|
||||
<iframe class="newsfeed-fullarticle" src="{{ url }}"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,9 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* Module: NewsFeed
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("newsfeed", {
|
||||
// Default module config.
|
||||
defaults: {
|
||||
@@ -42,7 +36,7 @@ Module.register("newsfeed", {
|
||||
dangerouslyDisableAutoEscaping: false
|
||||
},
|
||||
|
||||
getUrlPrefix: function (item) {
|
||||
getUrlPrefix (item) {
|
||||
if (item.useCorsProxy) {
|
||||
return `${location.protocol}//${location.host}/cors?url=`;
|
||||
} else {
|
||||
@@ -51,17 +45,17 @@ Module.register("newsfeed", {
|
||||
},
|
||||
|
||||
// Define required scripts.
|
||||
getScripts: function () {
|
||||
getScripts () {
|
||||
return ["moment.js"];
|
||||
},
|
||||
|
||||
//Define required styles.
|
||||
getStyles: function () {
|
||||
getStyles () {
|
||||
return ["newsfeed.css"];
|
||||
},
|
||||
|
||||
// Define required translations.
|
||||
getTranslations: function () {
|
||||
getTranslations () {
|
||||
// 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.
|
||||
@@ -69,7 +63,7 @@ Module.register("newsfeed", {
|
||||
},
|
||||
|
||||
// Define start sequence.
|
||||
start: function () {
|
||||
start () {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
|
||||
// Set locale.
|
||||
@@ -87,7 +81,7 @@ Module.register("newsfeed", {
|
||||
},
|
||||
|
||||
// Override socket notification handler.
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
socketNotificationReceived (notification, payload) {
|
||||
if (notification === "NEWS_ITEMS") {
|
||||
this.generateFeed(payload);
|
||||
|
||||
@@ -107,7 +101,7 @@ Module.register("newsfeed", {
|
||||
},
|
||||
|
||||
//Override fetching of template name
|
||||
getTemplate: function () {
|
||||
getTemplate () {
|
||||
if (this.config.feedUrl) {
|
||||
return "oldconfig.njk";
|
||||
} else if (this.config.showFullArticle) {
|
||||
@@ -117,28 +111,34 @@ Module.register("newsfeed", {
|
||||
},
|
||||
|
||||
//Override template data and return whats used for the current template
|
||||
getTemplateData: function () {
|
||||
getTemplateData () {
|
||||
if (this.activeItem >= this.newsItems.length) {
|
||||
this.activeItem = 0;
|
||||
}
|
||||
this.activeItemCount = this.newsItems.length;
|
||||
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications
|
||||
if (this.config.showFullArticle) {
|
||||
this.activeItemHash = this.newsItems[this.activeItem]?.hash;
|
||||
return {
|
||||
url: this.getActiveItemURL()
|
||||
};
|
||||
}
|
||||
if (this.error) {
|
||||
this.activeItemHash = undefined;
|
||||
return {
|
||||
error: this.error
|
||||
};
|
||||
}
|
||||
if (this.newsItems.length === 0) {
|
||||
this.activeItemHash = undefined;
|
||||
return {
|
||||
empty: true
|
||||
};
|
||||
}
|
||||
if (this.activeItem >= this.newsItems.length) {
|
||||
this.activeItem = 0;
|
||||
}
|
||||
|
||||
const item = this.newsItems[this.activeItem];
|
||||
this.activeItemHash = item.hash;
|
||||
|
||||
const items = this.newsItems.map(function (item) {
|
||||
item.publishDate = moment(new Date(item.pubdate)).fromNow();
|
||||
return item;
|
||||
@@ -150,13 +150,13 @@ Module.register("newsfeed", {
|
||||
sourceTitle: item.sourceTitle,
|
||||
publishDate: moment(new Date(item.pubdate)).fromNow(),
|
||||
title: item.title,
|
||||
url: this.getUrlPrefix(item) + item.url,
|
||||
url: this.getActiveItemURL(),
|
||||
description: item.description,
|
||||
items: items
|
||||
};
|
||||
},
|
||||
|
||||
getActiveItemURL: function () {
|
||||
getActiveItemURL () {
|
||||
const item = this.newsItems[this.activeItem];
|
||||
if (item) {
|
||||
return typeof item.url === "string" ? this.getUrlPrefix(item) + item.url : this.getUrlPrefix(item) + item.url.href;
|
||||
@@ -168,7 +168,7 @@ Module.register("newsfeed", {
|
||||
/**
|
||||
* Registers the feeds to be used by the backend.
|
||||
*/
|
||||
registerFeeds: function () {
|
||||
registerFeeds () {
|
||||
for (let feed of this.config.feeds) {
|
||||
this.sendSocketNotification("ADD_FEED", {
|
||||
feed: feed,
|
||||
@@ -181,7 +181,7 @@ Module.register("newsfeed", {
|
||||
* Generate an ordered list of items for this configured module.
|
||||
* @param {object} feeds An object with feeds returned by the node helper.
|
||||
*/
|
||||
generateFeed: function (feeds) {
|
||||
generateFeed (feeds) {
|
||||
let newsItems = [];
|
||||
for (let feed in feeds) {
|
||||
const feedItems = feeds[feed];
|
||||
@@ -274,7 +274,7 @@ Module.register("newsfeed", {
|
||||
* @param {string} feedUrl Url of the feed to check.
|
||||
* @returns {boolean} True if it is subscribed, false otherwise
|
||||
*/
|
||||
subscribedToFeed: function (feedUrl) {
|
||||
subscribedToFeed (feedUrl) {
|
||||
for (let feed of this.config.feeds) {
|
||||
if (feed.url === feedUrl) {
|
||||
return true;
|
||||
@@ -288,7 +288,7 @@ Module.register("newsfeed", {
|
||||
* @param {string} feedUrl Url of the feed
|
||||
* @returns {string} The title of the feed
|
||||
*/
|
||||
titleForFeed: function (feedUrl) {
|
||||
titleForFeed (feedUrl) {
|
||||
for (let feed of this.config.feeds) {
|
||||
if (feed.url === feedUrl) {
|
||||
return feed.title || "";
|
||||
@@ -300,7 +300,7 @@ Module.register("newsfeed", {
|
||||
/**
|
||||
* Schedule visual update.
|
||||
*/
|
||||
scheduleUpdateInterval: function () {
|
||||
scheduleUpdateInterval () {
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
|
||||
// Broadcast NewsFeed if needed
|
||||
@@ -312,8 +312,27 @@ Module.register("newsfeed", {
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
this.activeItem++;
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
|
||||
/*
|
||||
* When animations are enabled, don't update the DOM unless we are actually changing what we are displaying.
|
||||
* (Animating from a headline to itself is unsightly.)
|
||||
* Cases:
|
||||
*
|
||||
* Number of items | Number of items | Display
|
||||
* at last update | right now | Behaviour
|
||||
* ----------------------------------------------------
|
||||
* 0 | 0 | do not update
|
||||
* 0 | >0 | update
|
||||
* 1 | 0 or >1 | update
|
||||
* 1 | 1 | update only if item details (hash value) changed
|
||||
* >1 | any | update
|
||||
*
|
||||
* (N.B. We set activeItemCount and activeItemHash in getTemplateData().)
|
||||
*/
|
||||
if (this.newsItems.length > 1 || this.newsItems.length !== this.activeItemCount || this.activeItemHash !== this.newsItems[0]?.hash) {
|
||||
this.activeItem++; // this is OK if newsItems.Length==1; getTemplateData will wrap it around
|
||||
this.updateDom(this.config.animationSpeed);
|
||||
}
|
||||
|
||||
// Broadcast NewsFeed if needed
|
||||
if (this.config.broadcastNewsFeeds) {
|
||||
@@ -322,7 +341,7 @@ Module.register("newsfeed", {
|
||||
}, this.config.updateInterval);
|
||||
},
|
||||
|
||||
resetDescrOrFullArticleAndTimer: function () {
|
||||
resetDescrOrFullArticleAndTimer () {
|
||||
this.isShowingDescription = this.config.showDescription;
|
||||
this.config.showFullArticle = false;
|
||||
this.scrollPosition = 0;
|
||||
@@ -333,7 +352,7 @@ Module.register("newsfeed", {
|
||||
}
|
||||
},
|
||||
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
notificationReceived (notification, payload, sender) {
|
||||
const before = this.activeItem;
|
||||
if (notification === "MODULE_DOM_CREATED" && this.config.hideLoading) {
|
||||
this.hide();
|
||||
@@ -394,7 +413,7 @@ Module.register("newsfeed", {
|
||||
}
|
||||
},
|
||||
|
||||
showFullArticle: function () {
|
||||
showFullArticle () {
|
||||
this.isShowingDescription = !this.isShowingDescription;
|
||||
this.config.showFullArticle = !this.isShowingDescription;
|
||||
// make bottom bar align to top to allow scrolling
|
||||
|
@@ -1,27 +1,31 @@
|
||||
{% macro escapeText(text, dangerouslyDisableAutoEscaping=false) %}
|
||||
{% if dangerouslyDisableAutoEscaping %}
|
||||
{{ text | safe}}
|
||||
{% else %}
|
||||
{{ text }}
|
||||
{% endif %}
|
||||
{% 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>
|
||||
{% if dangerouslyDisableAutoEscaping %}
|
||||
{% if showTitleAsUrl %}
|
||||
<a href="{{ url }}"
|
||||
style="text-decoration:none;
|
||||
color:#ffffff"
|
||||
target="_blank">{{ title | safe }}</a>
|
||||
{% else %}
|
||||
{{ title | safe }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ title | safe}}
|
||||
{% if showTitleAsUrl %}
|
||||
<a href="{{ url }}"
|
||||
style="text-decoration:none;
|
||||
color:#ffffff"
|
||||
target="_blank">{{ title }}</a>
|
||||
{% else %}
|
||||
{{ title }}
|
||||
{% endif %}
|
||||
{% 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">
|
||||
@@ -30,11 +34,9 @@
|
||||
{% if (config.showSourceTitle and item.sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if item.sourceTitle and config.showSourceTitle %}
|
||||
{{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}: {% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}
|
||||
{{ item.publishDate }}:
|
||||
{{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}:{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}{{ item.publishDate }}:{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
||||
@@ -43,7 +45,7 @@
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ escapeText(item.description | truncate(config.lengthDescription), config.dangerouslyDisableAutoEscaping) }}
|
||||
{{ escapeText(item.description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ escapeText(item.description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
@@ -57,11 +59,9 @@
|
||||
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if sourceTitle and config.showSourceTitle %}
|
||||
{{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %}, {% else %}: {% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}
|
||||
{{ publishDate }}:
|
||||
{{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %}, {% else %}:{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}{{ publishDate }}:{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
||||
@@ -70,7 +70,7 @@
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ escapeText(description | truncate(config.lengthDescription), config.dangerouslyDisableAutoEscaping) }}
|
||||
{{ escapeText(description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ escapeText(description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
@@ -79,15 +79,11 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elseif empty %}
|
||||
<div class="small dimmed">
|
||||
{{ "NEWSFEED_NO_ITEMS" | translate | safe }}
|
||||
</div>
|
||||
<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 }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="small dimmed">
|
||||
{{ "LOADING" | translate | safe }}
|
||||
</div>
|
||||
<div class="small dimmed">{{ "LOADING" | translate | safe }}</div>
|
||||
{% endif %}
|
||||
|
@@ -1,14 +1,8 @@
|
||||
/* MagicMirror²
|
||||
* Node Helper: Newsfeed - NewsfeedFetcher
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
const stream = require("stream");
|
||||
const crypto = require("node:crypto");
|
||||
const stream = require("node:stream");
|
||||
const FeedMe = require("feedme");
|
||||
const iconv = require("iconv-lite");
|
||||
const fetch = require("fetch");
|
||||
const { htmlToText } = require("html-to-text");
|
||||
const Log = require("logger");
|
||||
const NodeHelper = require("node_helper");
|
||||
|
||||
@@ -48,19 +42,27 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
parser.on("item", (item) => {
|
||||
const title = item.title;
|
||||
let description = item.description || item.summary || item.content || "";
|
||||
const pubdate = item.pubdate || item.published || item.updated || item["dc:date"];
|
||||
const pubdate = item.pubdate || item.published || item.updated || item["dc:date"] || item["a10:updated"];
|
||||
const url = item.url || item.link || "";
|
||||
|
||||
if (title && pubdate) {
|
||||
const regex = /(<([^>]+)>)/gi;
|
||||
description = description.toString().replace(regex, "");
|
||||
// Convert HTML entities, codes and tag
|
||||
description = htmlToText(description, {
|
||||
wordwrap: false,
|
||||
selectors: [
|
||||
{ selector: "a", options: { ignoreHref: true, noAnchorUrl: true } },
|
||||
{ selector: "br", format: "inlineSurround", options: { prefix: " " } },
|
||||
{ selector: "img", format: "skip" }
|
||||
]
|
||||
});
|
||||
|
||||
items.push({
|
||||
title: title,
|
||||
description: description,
|
||||
pubdate: pubdate,
|
||||
url: url,
|
||||
useCorsProxy: useCorsProxy
|
||||
useCorsProxy: useCorsProxy,
|
||||
hash: crypto.createHash("sha256").update(`${pubdate} :: ${title} :: ${url}`).digest("hex")
|
||||
});
|
||||
} else if (logFeedWarnings) {
|
||||
Log.warn("Can't parse feed item:");
|
||||
|
@@ -1,23 +1,16 @@
|
||||
/* MagicMirror²
|
||||
* Node Helper: Newsfeed
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
const NodeHelper = require("node_helper");
|
||||
const Log = require("logger");
|
||||
const NewsfeedFetcher = require("./newsfeedfetcher");
|
||||
|
||||
module.exports = NodeHelper.create({
|
||||
// Override start method.
|
||||
start: function () {
|
||||
start () {
|
||||
Log.log(`Starting node helper for: ${this.name}`);
|
||||
this.fetchers = [];
|
||||
},
|
||||
|
||||
// Override socketNotificationReceived received.
|
||||
socketNotificationReceived: function (notification, payload) {
|
||||
socketNotificationReceived (notification, payload) {
|
||||
if (notification === "ADD_FEED") {
|
||||
this.createFetcher(payload.feed, payload.config);
|
||||
}
|
||||
@@ -29,7 +22,7 @@ module.exports = NodeHelper.create({
|
||||
* @param {object} feed The feed object
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
createFetcher: function (feed, config) {
|
||||
createFetcher (feed, config) {
|
||||
const url = feed.url || "";
|
||||
const encoding = feed.encoding || "UTF-8";
|
||||
const reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
|
||||
@@ -76,7 +69,7 @@ module.exports = NodeHelper.create({
|
||||
* Creates an object with all feed items of the different registered feeds,
|
||||
* and broadcasts these using sendSocketNotification.
|
||||
*/
|
||||
broadcastFeeds: function () {
|
||||
broadcastFeeds () {
|
||||
const feeds = {};
|
||||
for (let f in this.fetchers) {
|
||||
feeds[f] = this.fetchers[f].items();
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<div class="small bright">
|
||||
{{ "MODULE_CONFIG_CHANGED" | translate({MODULE_NAME: "Newsfeed"}) | safe }}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,28 +1,28 @@
|
||||
const util = require("util");
|
||||
const exec = util.promisify(require("child_process").exec);
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const util = require("node:util");
|
||||
const exec = util.promisify(require("node:child_process").exec);
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const Log = require("logger");
|
||||
|
||||
const BASE_DIR = path.normalize(`${__dirname}/../../../`);
|
||||
|
||||
class GitHelper {
|
||||
constructor() {
|
||||
constructor () {
|
||||
this.gitRepos = [];
|
||||
this.gitResultList = [];
|
||||
}
|
||||
|
||||
getRefRegex(branch) {
|
||||
getRefRegex (branch) {
|
||||
return new RegExp(`s*([a-z,0-9]+[.][.][a-z,0-9]+) ${branch}`, "g");
|
||||
}
|
||||
|
||||
async execShell(command) {
|
||||
async execShell (command) {
|
||||
const { stdout = "", stderr = "" } = await exec(command);
|
||||
|
||||
return { stdout, stderr };
|
||||
}
|
||||
|
||||
async isGitRepo(moduleFolder) {
|
||||
async isGitRepo (moduleFolder) {
|
||||
const { stderr } = await this.execShell(`cd ${moduleFolder} && git remote -v`);
|
||||
|
||||
if (stderr) {
|
||||
@@ -34,7 +34,7 @@ class GitHelper {
|
||||
return true;
|
||||
}
|
||||
|
||||
async add(moduleName) {
|
||||
async add (moduleName) {
|
||||
let moduleFolder = BASE_DIR;
|
||||
|
||||
if (moduleName !== "MagicMirror") {
|
||||
@@ -59,7 +59,7 @@ class GitHelper {
|
||||
}
|
||||
}
|
||||
|
||||
async getStatusInfo(repo) {
|
||||
async getStatusInfo (repo) {
|
||||
let gitInfo = {
|
||||
module: repo.module,
|
||||
behind: 0, // commits behind
|
||||
@@ -114,7 +114,7 @@ class GitHelper {
|
||||
return gitInfo;
|
||||
}
|
||||
|
||||
async getRepoInfo(repo) {
|
||||
async getRepoInfo (repo) {
|
||||
const gitInfo = await this.getStatusInfo(repo);
|
||||
|
||||
if (!gitInfo || !gitInfo.current) {
|
||||
@@ -128,7 +128,7 @@ class GitHelper {
|
||||
const { stderr } = await this.execShell(`cd ${repo.folder} && git fetch -n --dry-run`);
|
||||
|
||||
// example output:
|
||||
// From https://github.com/MichMich/MagicMirror
|
||||
// From https://github.com/MagicMirrorOrg/MagicMirror
|
||||
// e40ddd4..06389e3 develop -> origin/develop
|
||||
// here the result is in stderr (this is a git default, don't ask why ...)
|
||||
const matches = stderr.match(this.getRefRegex(gitInfo.current));
|
||||
@@ -174,7 +174,7 @@ class GitHelper {
|
||||
}
|
||||
}
|
||||
|
||||
async getRepos() {
|
||||
async getRepos () {
|
||||
this.gitResultList = [];
|
||||
|
||||
for (const repo of this.gitRepos) {
|
||||
@@ -192,7 +192,7 @@ class GitHelper {
|
||||
return this.gitResultList;
|
||||
}
|
||||
|
||||
async checkUpdates() {
|
||||
async checkUpdates () {
|
||||
var updates = [];
|
||||
|
||||
const allRepos = await this.gitResultList.map((module) => {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
const NodeHelper = require("node_helper");
|
||||
const defaultModules = require("../defaultmodules");
|
||||
const GitHelper = require("./git_helper");
|
||||
const UpdateHelper = require("./update_helper");
|
||||
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
|
||||
@@ -11,8 +12,9 @@ module.exports = NodeHelper.create({
|
||||
updateProcessStarted: false,
|
||||
|
||||
gitHelper: new GitHelper(),
|
||||
updateHelper: null,
|
||||
|
||||
async configureModules(modules) {
|
||||
async configureModules (modules) {
|
||||
for (const moduleName of modules) {
|
||||
if (!this.ignoreUpdateChecking(moduleName)) {
|
||||
await this.gitHelper.add(moduleName);
|
||||
@@ -24,10 +26,12 @@ module.exports = NodeHelper.create({
|
||||
}
|
||||
},
|
||||
|
||||
async socketNotificationReceived(notification, payload) {
|
||||
async socketNotificationReceived (notification, payload) {
|
||||
switch (notification) {
|
||||
case "CONFIG":
|
||||
this.config = payload;
|
||||
this.updateHelper = new UpdateHelper(this.config);
|
||||
await this.updateHelper.check_PM2_Process();
|
||||
break;
|
||||
case "MODULES":
|
||||
// if this is the 1st time thru the update check process
|
||||
@@ -47,30 +51,43 @@ module.exports = NodeHelper.create({
|
||||
}
|
||||
},
|
||||
|
||||
async performFetch() {
|
||||
async performFetch () {
|
||||
const repos = await this.gitHelper.getRepos();
|
||||
|
||||
for (const repo of repos) {
|
||||
this.sendSocketNotification("STATUS", repo);
|
||||
this.sendSocketNotification("REPO_STATUS", repo);
|
||||
}
|
||||
|
||||
if (this.config.sendUpdatesNotifications) {
|
||||
const updates = await this.gitHelper.checkUpdates();
|
||||
if (updates.length) this.sendSocketNotification("UPDATES", updates);
|
||||
const updates = await this.gitHelper.checkUpdates();
|
||||
|
||||
if (this.config.sendUpdatesNotifications && updates.length) {
|
||||
this.sendSocketNotification("UPDATES", updates);
|
||||
}
|
||||
|
||||
if (updates.length) {
|
||||
const updateResult = await this.updateHelper.parse(updates);
|
||||
for (const update of updateResult) {
|
||||
if (update.inProgress) {
|
||||
this.sendSocketNotification("UPDATE_STATUS", update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.scheduleNextFetch(this.config.updateInterval);
|
||||
},
|
||||
|
||||
scheduleNextFetch(delay) {
|
||||
scheduleNextFetch (delay) {
|
||||
clearTimeout(this.updateTimer);
|
||||
|
||||
this.updateTimer = setTimeout(() => {
|
||||
this.performFetch();
|
||||
}, Math.max(delay, ONE_MINUTE));
|
||||
this.updateTimer = setTimeout(
|
||||
() => {
|
||||
this.performFetch();
|
||||
},
|
||||
Math.max(delay, ONE_MINUTE)
|
||||
);
|
||||
},
|
||||
|
||||
ignoreUpdateChecking(moduleName) {
|
||||
ignoreUpdateChecking (moduleName) {
|
||||
// Should not check for updates for default modules
|
||||
if (defaultModules.includes(moduleName)) {
|
||||
return true;
|
||||
|
222
modules/default/updatenotification/update_helper.js
Normal file
222
modules/default/updatenotification/update_helper.js
Normal file
@@ -0,0 +1,222 @@
|
||||
const Exec = require("node:child_process").exec;
|
||||
const Spawn = require("node:child_process").spawn;
|
||||
const fs = require("node:fs");
|
||||
|
||||
const Log = require("logger");
|
||||
|
||||
/* class Updater
|
||||
* Allow to self updating 3rd party modules from command defined in config
|
||||
*
|
||||
* [constructor] read value in config:
|
||||
* updates: [ // array of modules update commands
|
||||
* {
|
||||
* <module name>: <update command>
|
||||
* },
|
||||
* {
|
||||
* ...
|
||||
* }
|
||||
* ],
|
||||
* updateTimeout: 2 * 60 * 1000, // max update duration
|
||||
* updateAutorestart: false // autoRestart MM when update done ?
|
||||
*
|
||||
* [main command]: parse(<Array of modules>):
|
||||
* parse if module update is needed
|
||||
* --> Apply ONLY one update (first of the module list)
|
||||
* --> auto-restart MagicMirror or wait manual restart by user
|
||||
* return array with modules update state information for `updatenotification` module displayer information
|
||||
* [
|
||||
* {
|
||||
* name = <module-name>, // name of the module
|
||||
* updateCommand = <update command>, // update command (if found)
|
||||
* inProgress = <boolean>, // an update if in progress for this module
|
||||
* error = <boolean>, // an error if detected when updating
|
||||
* updated = <boolean>, // updated successfully
|
||||
* needRestart = <boolean> // manual restart of MagicMirror is required by user
|
||||
* },
|
||||
* {
|
||||
* ...
|
||||
* }
|
||||
* ]
|
||||
*/
|
||||
|
||||
class Updater {
|
||||
constructor (config) {
|
||||
this.updates = config.updates;
|
||||
this.timeout = config.updateTimeout;
|
||||
this.autoRestart = config.updateAutorestart;
|
||||
this.moduleList = {};
|
||||
this.updating = false;
|
||||
this.usePM2 = false;
|
||||
this.PM2 = null;
|
||||
this.version = global.version;
|
||||
this.root_path = global.root_path;
|
||||
Log.info("updatenotification: Updater Class Loaded!");
|
||||
}
|
||||
|
||||
// [main command] parse if module update is needed
|
||||
async parse (modules) {
|
||||
var parser = modules.map(async (module) => {
|
||||
if (this.moduleList[module.module] === undefined) {
|
||||
this.moduleList[module.module] = {};
|
||||
this.moduleList[module.module].name = module.module;
|
||||
this.moduleList[module.module].updateCommand = await this.applyCommand(module.module);
|
||||
this.moduleList[module.module].inProgress = false;
|
||||
this.moduleList[module.module].error = null;
|
||||
this.moduleList[module.module].updated = false;
|
||||
this.moduleList[module.module].needRestart = false;
|
||||
}
|
||||
if (!this.moduleList[module.module].inProgress) {
|
||||
if (!this.updating) {
|
||||
if (!this.moduleList[module.module].updateCommand) {
|
||||
this.updating = false;
|
||||
} else {
|
||||
this.updating = true;
|
||||
this.moduleList[module.module].inProgress = true;
|
||||
Object.assign(this.moduleList[module.module], await this.updateProcess(this.moduleList[module.module]));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(parser);
|
||||
let updater = Object.values(this.moduleList);
|
||||
Log.debug("updatenotification Update Result:", updater);
|
||||
return updater;
|
||||
}
|
||||
|
||||
// module updater with his proper command
|
||||
// return object as result
|
||||
//{
|
||||
// error: <boolean>, // if error detected
|
||||
// updated: <boolean>, // if updated successfully
|
||||
// needRestart: <boolean> // if magicmirror restart required
|
||||
//};
|
||||
updateProcess (module) {
|
||||
let Result = {
|
||||
error: false,
|
||||
updated: false,
|
||||
needRestart: false
|
||||
};
|
||||
let Command = null;
|
||||
const Path = `${this.root_path}/modules/`;
|
||||
const modulePath = Path + module.name;
|
||||
|
||||
if (module.updateCommand) {
|
||||
Command = module.updateCommand;
|
||||
} else {
|
||||
Log.warn(`updatenotification: Update of ${module.name} is not supported.`);
|
||||
return Result;
|
||||
}
|
||||
Log.info(`updatenotification: Updating ${module.name}...`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
Exec(Command, { cwd: modulePath, timeout: this.timeout }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
Log.error(`updatenotification: exec error: ${error}`);
|
||||
Result.error = true;
|
||||
} else {
|
||||
Log.info(`updatenotification: Update logs of ${module.name}: ${stdout}`);
|
||||
Result.updated = true;
|
||||
if (this.autoRestart) {
|
||||
Log.info("updatenotification: Update done");
|
||||
setTimeout(() => this.restart(), 3000);
|
||||
} else {
|
||||
Log.info("updatenotification: Update done, don't forget to restart MagicMirror!");
|
||||
Result.needRestart = true;
|
||||
}
|
||||
}
|
||||
resolve(Result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// restart rules (pm2 or npm start)
|
||||
restart () {
|
||||
if (this.usePM2) this.pm2Restart();
|
||||
else this.npmRestart();
|
||||
}
|
||||
|
||||
// restart MagicMiror with "pm2"
|
||||
pm2Restart () {
|
||||
Log.info("updatenotification: PM2 will restarting MagicMirror...");
|
||||
const pm2 = require("pm2");
|
||||
pm2.restart(this.PM2, (err, proc) => {
|
||||
if (err) {
|
||||
Log.error("updatenotification:[PM2] restart Error", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// restart MagicMiror with "npm start"
|
||||
npmRestart () {
|
||||
Log.info("updatenotification: Restarting MagicMirror...");
|
||||
const out = process.stdout;
|
||||
const err = process.stderr;
|
||||
const subprocess = Spawn("npm start", { cwd: this.root_path, shell: true, detached: true, stdio: ["ignore", out, err] });
|
||||
subprocess.unref();
|
||||
process.exit();
|
||||
}
|
||||
|
||||
// Check using pm2
|
||||
check_PM2_Process () {
|
||||
Log.info("updatenotification: Checking PM2 using...");
|
||||
return new Promise((resolve) => {
|
||||
if (fs.existsSync("/.dockerenv")) {
|
||||
Log.info("updatenotification: Running in docker container, not using PM2 ...");
|
||||
this.usePM2 = false;
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const pm2 = require("pm2");
|
||||
pm2.connect((err) => {
|
||||
if (err) {
|
||||
Log.error("updatenotification: [PM2]", err);
|
||||
this.usePM2 = false;
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
pm2.list((err, list) => {
|
||||
if (err) {
|
||||
Log.error("updatenotification: [PM2] Can't get process List!");
|
||||
this.usePM2 = false;
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
list.forEach((pm) => {
|
||||
if (pm.pm2_env.version === this.version && pm.pm2_env.status === "online" && pm.pm2_env.pm_cwd.includes(`${this.root_path}/`)) {
|
||||
this.PM2 = pm.name;
|
||||
this.usePM2 = true;
|
||||
Log.info("updatenotification: [PM2] You are using pm2 with", this.PM2);
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
pm2.disconnect();
|
||||
if (!this.PM2) {
|
||||
Log.info("updatenotification: [PM2] You are not using pm2");
|
||||
this.usePM2 = false;
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// check if module is MagicMirror
|
||||
isMagicMirror (module) {
|
||||
if (module === "MagicMirror") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// search update module command
|
||||
applyCommand (module) {
|
||||
if (this.isMagicMirror(module.module) || !this.updates.length) return null;
|
||||
let command = null;
|
||||
this.updates.forEach((updater) => {
|
||||
if (updater[module]) command = updater[module];
|
||||
});
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Updater;
|
@@ -1,21 +1,20 @@
|
||||
/* MagicMirror²
|
||||
* Module: UpdateNotification
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("updatenotification", {
|
||||
defaults: {
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
refreshInterval: 24 * 60 * 60 * 1000, // one day
|
||||
ignoreModules: [],
|
||||
sendUpdatesNotifications: false
|
||||
sendUpdatesNotifications: false,
|
||||
updates: [],
|
||||
updateTimeout: 2 * 60 * 1000, // max update duration
|
||||
updateAutorestart: false // autoRestart MM when update done ?
|
||||
},
|
||||
|
||||
suspended: false,
|
||||
moduleList: {},
|
||||
needRestart: false,
|
||||
updates: [],
|
||||
|
||||
start() {
|
||||
start () {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
this.addFilters();
|
||||
setInterval(() => {
|
||||
@@ -24,16 +23,16 @@ Module.register("updatenotification", {
|
||||
}, this.config.refreshInterval);
|
||||
},
|
||||
|
||||
suspend() {
|
||||
suspend () {
|
||||
this.suspended = true;
|
||||
},
|
||||
|
||||
resume() {
|
||||
resume () {
|
||||
this.suspended = false;
|
||||
this.updateDom(2);
|
||||
},
|
||||
|
||||
notificationReceived(notification) {
|
||||
notificationReceived (notification) {
|
||||
switch (notification) {
|
||||
case "DOM_OBJECTS_CREATED":
|
||||
this.sendSocketNotification("CONFIG", this.config);
|
||||
@@ -45,30 +44,33 @@ Module.register("updatenotification", {
|
||||
}
|
||||
},
|
||||
|
||||
socketNotificationReceived(notification, payload) {
|
||||
socketNotificationReceived (notification, payload) {
|
||||
switch (notification) {
|
||||
case "STATUS":
|
||||
case "REPO_STATUS":
|
||||
this.updateUI(payload);
|
||||
break;
|
||||
case "UPDATES":
|
||||
this.sendNotification("UPDATES", payload);
|
||||
break;
|
||||
case "UPDATE_STATUS":
|
||||
this.updatesNotifier(payload);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
getStyles() {
|
||||
getStyles () {
|
||||
return [`${this.name}.css`];
|
||||
},
|
||||
|
||||
getTemplate() {
|
||||
getTemplate () {
|
||||
return `${this.name}.njk`;
|
||||
},
|
||||
|
||||
getTemplateData() {
|
||||
return { moduleList: this.moduleList, suspended: this.suspended };
|
||||
getTemplateData () {
|
||||
return { moduleList: this.moduleList, updatesList: this.updates, suspended: this.suspended, needRestart: this.needRestart };
|
||||
},
|
||||
|
||||
updateUI(payload) {
|
||||
updateUI (payload) {
|
||||
if (payload && payload.behind > 0) {
|
||||
// if we haven't seen info for this module
|
||||
if (this.moduleList[payload.module] === undefined) {
|
||||
@@ -86,7 +88,7 @@ Module.register("updatenotification", {
|
||||
}
|
||||
},
|
||||
|
||||
addFilters() {
|
||||
addFilters () {
|
||||
this.nunjucksEnvironment().addFilter("diffLink", (text, status) => {
|
||||
if (status.module !== "MagicMirror") {
|
||||
return text;
|
||||
@@ -94,7 +96,31 @@ Module.register("updatenotification", {
|
||||
|
||||
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>`;
|
||||
return `<a href="https://github.com/MagicMirrorOrg/MagicMirror/compare/${localRef}...${remoteRef}" class="xsmall dimmed difflink" target="_blank">${text}</a>`;
|
||||
});
|
||||
},
|
||||
|
||||
updatesNotifier (payload, done = true) {
|
||||
if (this.updates[payload.name] === undefined) {
|
||||
this.updates[payload.name] = {
|
||||
name: payload.name,
|
||||
done: done
|
||||
};
|
||||
|
||||
if (payload.error) {
|
||||
this.sendSocketNotification("UPDATE_ERROR", payload.name);
|
||||
this.updates[payload.name].done = false;
|
||||
} else {
|
||||
if (payload.updated) {
|
||||
delete this.moduleList[payload.name];
|
||||
this.updates[payload.name].done = true;
|
||||
}
|
||||
if (payload.needRestart) {
|
||||
this.needRestart = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateDom(2);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -1,15 +1,41 @@
|
||||
{% 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 === "MagicMirror" 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 %}
|
||||
{% if needRestart %}
|
||||
<div class="small bright">
|
||||
<i class="fas fa-rotate"></i>
|
||||
<span>
|
||||
{% set restartTextLabel = "UPDATE_NOTIFICATION_NEED-RESTART" %}
|
||||
{{ restartTextLabel | translate() | safe }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for name, status in moduleList %}
|
||||
<div class="small bright">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<span>
|
||||
{% set mainTextLabel = "UPDATE_NOTIFICATION" if name === "MagicMirror" 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 %}
|
||||
{% for name, status in updatesList %}
|
||||
<div class="small bright">
|
||||
{% if status.done %}
|
||||
<i class="fas fa-check" style="color: lightgreen;"></i>
|
||||
<span>
|
||||
{% set updateTextLabel = "UPDATE_NOTIFICATION_DONE" %}
|
||||
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
|
||||
</span>
|
||||
{% else %}
|
||||
<i class="fas fa-xmark" style="color: red;"></i>
|
||||
<span>
|
||||
{% set updateTextLabel = "UPDATE_NOTIFICATION_ERROR" %}
|
||||
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
|
||||
* @returns {Promise} resolved when the fetch is done. The response headers is placed in a headers-property (provided the response does not already contain a headers-property).
|
||||
*/
|
||||
async function performWebRequest(url, type = "json", useCorsProxy = false, requestHeaders = undefined, expectedResponseHeaders = undefined) {
|
||||
async function performWebRequest (url, type = "json", useCorsProxy = false, requestHeaders = undefined, expectedResponseHeaders = undefined) {
|
||||
const request = {};
|
||||
let requestUrl;
|
||||
if (useCorsProxy) {
|
||||
@@ -165,8 +165,7 @@ const formatTime = (config, time) => {
|
||||
return date.format("HH:mm");
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined")
|
||||
module.exports = {
|
||||
performWebRequest,
|
||||
formatTime
|
||||
};
|
||||
if (typeof module !== "undefined") module.exports = {
|
||||
performWebRequest,
|
||||
formatTime
|
||||
};
|
||||
|
@@ -1,3 +1,8 @@
|
||||
{% macro humidity() %}
|
||||
{% if current.humidity %}
|
||||
<span class="humidity"><span>{{ current.humidity | decimalSymbol }}</span><sup> <i class="wi wi-humidity humidity-icon"></i></sup></span>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{% if current %}
|
||||
{% if not config.onlyTemp %}
|
||||
<div class="normal medium">
|
||||
@@ -7,7 +12,7 @@
|
||||
{% if config.showWindDirection %}
|
||||
<sup>
|
||||
{% if config.showWindDirectionAsArrow %}
|
||||
<i class="fas fa-long-arrow-alt-down" style="transform:rotate({{ current.windFromDirection }}deg);"></i>
|
||||
<i class="fas fa-long-arrow-alt-down" style="transform:rotate({{ current.windFromDirection }}deg)"></i>
|
||||
{% else %}
|
||||
{{ current.cardinalWindDirection() | translate }}
|
||||
{% endif %}
|
||||
@@ -15,8 +20,8 @@
|
||||
</sup>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if config.showHumidity and current.humidity %}
|
||||
<span>{{ current.humidity | decimalSymbol }}</span><sup> <i class="wi wi-humidity humidity-icon"></i></sup>
|
||||
{% if config.showHumidity === "wind" %}
|
||||
{{ humidity() }}
|
||||
{% endif %}
|
||||
{% if config.showSun %}
|
||||
<span class="wi dimmed wi-{{ current.nextSunAction() }}"></span>
|
||||
@@ -29,61 +34,68 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if config.showUVIndex %}
|
||||
<td class="align-right bright uv-index">
|
||||
<td class="align-right bright uv-index">
|
||||
<div class="wi dimmed wi-hot"></div>
|
||||
{{ current.uv_index }}
|
||||
</td>
|
||||
{{ current.uv_index }}
|
||||
</td>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="large light">
|
||||
<span class="wi weathericon wi-{{current.weatherType}}"></span>
|
||||
<span class="bright">
|
||||
{{ current.temperature | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="normal light indoor">
|
||||
{% if config.showIndoorTemperature and indoor.temperature %}
|
||||
<div>
|
||||
<span class="fas fa-home"></span>
|
||||
<span class="bright">
|
||||
{{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="large">
|
||||
{% if config.showIndoorTemperature and indoor.temperature or config.showIndoorHumidity and indoor.humidity %}
|
||||
<span class="medium fas fa-home"></span>
|
||||
<span style="display: inline-block">
|
||||
{% if config.showIndoorTemperature and indoor.temperature %}
|
||||
<sup class="small" style="position: relative; display: block; text-align: left;">
|
||||
<span>
|
||||
{{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</span>
|
||||
</sup>
|
||||
{% endif %}
|
||||
{% if config.showIndoorHumidity and indoor.humidity %}
|
||||
<sub class="small" style="position: relative; display: block; text-align: left;">
|
||||
<span>
|
||||
{{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }}
|
||||
</span>
|
||||
</sub>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if config.showIndoorHumidity and indoor.humidity %}
|
||||
<div>
|
||||
<span class="fas fa-tint"></span>
|
||||
<span class="bright">
|
||||
{{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="light wi weathericon wi-{{ current.weatherType }}"></span>
|
||||
<span class="light bright">{{ current.temperature | roundValue | unit("temperature") | decimalSymbol }}</span>
|
||||
{% if config.showHumidity === "temp" %}
|
||||
<span class="medium bright">{{ humidity() }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if (config.showFeelsLike or config.showPrecipitationAmount or config.showPrecipitationProbability) and not config.onlyTemp %}
|
||||
<div class="normal medium feelslike">
|
||||
{% if config.showFeelsLike %}
|
||||
<span class="dimmed">
|
||||
{% if config.showHumidity === "feelslike" %}
|
||||
{{ humidity() }}
|
||||
{% endif %}
|
||||
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
|
||||
</span><br/>
|
||||
</span>
|
||||
<br />
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationAmount and current.precipitationAmount %}
|
||||
<span class="dimmed">
|
||||
<span class="precipitationLeadText">{{ "PRECIP_AMOUNT" | translate }}</span> {{ current.precipitationAmount | unit("precip", current.precipitationUnits) }}
|
||||
</span><br/>
|
||||
</span>
|
||||
<br />
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationProbability and current.precipitationProbability %}
|
||||
{% if config.showPrecipitationProbability and current.precipitationProbability %}
|
||||
<span class="dimmed">
|
||||
<span class="precipitationLeadText">{{ "PRECIP_POP" | translate }}</span> {{ current.precipitationProbability }}%
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if config.showHumidity === "below" %}
|
||||
<span class="medium dimmed">{{ humidity() }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="dimmed light small">
|
||||
{{ "LOADING" | translate }}
|
||||
</div>
|
||||
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Uncomment the line below to see the contents of the `current` object. -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{current | dump}}</div> -->
|
||||
|
@@ -7,15 +7,18 @@
|
||||
{% endif %}
|
||||
{% set forecast = forecast.slice(0, numSteps) %}
|
||||
{% for f in forecast %}
|
||||
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
|
||||
<tr {% if config.colored %}class="colored"{% endif %}
|
||||
{% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
|
||||
{% if (currentStep == 0) and config.ignoreToday == false and config.absoluteDates == false %}
|
||||
<td class="day">{{ "TODAY" | translate }}</td>
|
||||
{% 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>
|
||||
<td class="day">{{ f.date.format("ddd") }}</td>
|
||||
{% endif %}
|
||||
<td class="bright weather-icon"><span class="wi weathericon wi-{{ f.weatherType }}"></span></td>
|
||||
<td class="bright weather-icon">
|
||||
<span class="wi weathericon wi-{{ f.weatherType }}"></span>
|
||||
</td>
|
||||
<td class="align-right bright max-temp">
|
||||
{{ f.maxTemperature | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</td>
|
||||
@@ -29,7 +32,7 @@
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationProbability %}
|
||||
<td class="align-right bright precipitation-prob">
|
||||
{{ f.precipitationProbability | unit("precip", "%") }}
|
||||
{{ f.precipitationProbability | unit('precip', '%') }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if config.showUVIndex %}
|
||||
@@ -43,10 +46,7 @@
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="dimmed light small">
|
||||
{{ "LOADING" | translate }}
|
||||
</div>
|
||||
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Uncomment the line below to see the contents of the `forecast` object. -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{forecast | dump}}</div> -->
|
||||
|
@@ -4,9 +4,12 @@
|
||||
<table class="{{ config.tableClass }}">
|
||||
{% set hours = hourly.slice(0, numSteps) %}
|
||||
{% for hour in hours %}
|
||||
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
|
||||
<tr {% if config.colored %}class="colored"{% endif %}
|
||||
{% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
|
||||
<td class="day">{{ hour.date | formatTime }}</td>
|
||||
<td class="bright weather-icon"><span class="wi weathericon wi-{{ hour.weatherType }}"></span></td>
|
||||
<td class="bright weather-icon">
|
||||
<span class="wi weathericon wi-{{ hour.weatherType }}"></span>
|
||||
</td>
|
||||
<td class="align-right bright">
|
||||
{{ hour.temperature | roundValue | unit("temperature") }}
|
||||
</td>
|
||||
@@ -25,7 +28,7 @@
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationProbability %}
|
||||
<td class="align-right bright precipitation-prob">
|
||||
{{ hour.precipitationProbability | unit("precip", "%") }}
|
||||
{{ hour.precipitationProbability | unit('precip', '%') }}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
@@ -33,10 +36,7 @@
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="dimmed light small">
|
||||
{{ "LOADING" | translate }}
|
||||
</div>
|
||||
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Uncomment the line below to see the contents of the `hourly` object. -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{hourly | dump}}</div> -->
|
||||
|
@@ -1,10 +1,6 @@
|
||||
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: Environment Canada (EC)
|
||||
*
|
||||
* This class is a provider for Environment Canada MSC Datamart
|
||||
/* This class is a provider for Environment Canada MSC Datamart
|
||||
* Note that this is only for Canadian locations and does not require an API key (access is anonymous)
|
||||
*
|
||||
* EC Documentation at following links:
|
||||
@@ -27,8 +23,6 @@
|
||||
* with locations you can search under column B (English Names), with the corresponding siteCode under
|
||||
* column A (Codes) and provCode under column C (Province).
|
||||
*
|
||||
* Original by Kevin Godin
|
||||
*
|
||||
* License to use Environment Canada (EC) data is detailed here:
|
||||
* https://eccc-msc.github.io/open-data/licence/readme_en/
|
||||
*
|
||||
@@ -49,7 +43,7 @@ WeatherProvider.register("envcanada", {
|
||||
// Set config values (equates to weather module config values). Also set values pertaining to caching of
|
||||
// Today's temperature forecast (for use in the Forecast functions below)
|
||||
//
|
||||
setConfig: function (config) {
|
||||
setConfig (config) {
|
||||
this.config = config;
|
||||
|
||||
this.todayTempCacheMin = 0;
|
||||
@@ -61,7 +55,7 @@ WeatherProvider.register("envcanada", {
|
||||
//
|
||||
// Called when the weather provider is started
|
||||
//
|
||||
start: function () {
|
||||
start () {
|
||||
Log.info(`Weather provider: ${this.providerName} started.`);
|
||||
this.setFetchedLocation(this.config.location);
|
||||
},
|
||||
@@ -69,7 +63,7 @@ WeatherProvider.register("envcanada", {
|
||||
//
|
||||
// Override the fetchCurrentWeather method to query EC and construct a Current weather object
|
||||
//
|
||||
fetchCurrentWeather() {
|
||||
fetchCurrentWeather () {
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
@@ -89,7 +83,7 @@ WeatherProvider.register("envcanada", {
|
||||
//
|
||||
// Override the fetchWeatherForecast method to query EC and construct Forecast weather objects
|
||||
//
|
||||
fetchWeatherForecast() {
|
||||
fetchWeatherForecast () {
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
@@ -109,7 +103,7 @@ WeatherProvider.register("envcanada", {
|
||||
//
|
||||
// Override the fetchWeatherHourly method to query EC and construct Forecast weather objects
|
||||
//
|
||||
fetchWeatherHourly() {
|
||||
fetchWeatherHourly () {
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
@@ -137,7 +131,7 @@ WeatherProvider.register("envcanada", {
|
||||
// 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.
|
||||
//
|
||||
getUrl() {
|
||||
getUrl () {
|
||||
return `https://dd.weather.gc.ca/citypage_weather/xml/${this.config.provCode}/${this.config.siteCode}_e.xml`;
|
||||
},
|
||||
|
||||
@@ -145,7 +139,7 @@ WeatherProvider.register("envcanada", {
|
||||
// Generate a WeatherObject based on current EC weather conditions
|
||||
//
|
||||
|
||||
generateWeatherObjectFromCurrentWeather(ECdoc) {
|
||||
generateWeatherObjectFromCurrentWeather (ECdoc) {
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
// There are instances where EC will update weather data and current temperature will not be
|
||||
@@ -216,7 +210,7 @@ WeatherProvider.register("envcanada", {
|
||||
// Generate an array of WeatherObjects based on EC weather forecast
|
||||
//
|
||||
|
||||
generateWeatherObjectsFromForecast(ECdoc) {
|
||||
generateWeatherObjectsFromForecast (ECdoc) {
|
||||
// Declare an array to hold each day's forecast object
|
||||
|
||||
const days = [];
|
||||
@@ -360,7 +354,7 @@ WeatherProvider.register("envcanada", {
|
||||
// Generate an array of WeatherObjects based on EC hourly weather forecast
|
||||
//
|
||||
|
||||
generateWeatherObjectsFromHourly(ECdoc) {
|
||||
generateWeatherObjectsFromHourly (ECdoc) {
|
||||
// Declare an array to hold each hour's forecast object
|
||||
|
||||
const hours = [];
|
||||
@@ -416,7 +410,7 @@ WeatherProvider.register("envcanada", {
|
||||
// the next Forecast element should be considered - i.e. look at Today *and* Tonight vs.Tonight-only
|
||||
//
|
||||
|
||||
setMinMaxTemps(weather, foreGroup, today, fullDay, currentTemp) {
|
||||
setMinMaxTemps (weather, foreGroup, today, fullDay, currentTemp) {
|
||||
const todayTemp = foreGroup[today].querySelector("temperatures temperature").textContent;
|
||||
|
||||
const todayClass = foreGroup[today].querySelector("temperatures temperature").getAttribute("class");
|
||||
@@ -498,7 +492,7 @@ WeatherProvider.register("envcanada", {
|
||||
// the nightime forecast after a certain point in that specific scenario.
|
||||
//
|
||||
|
||||
setPrecipitation(weather, foreGroup, today) {
|
||||
setPrecipitation (weather, foreGroup, today) {
|
||||
if (foreGroup[today].querySelector("precipitation accumulation")) {
|
||||
weather.precipitationAmount = foreGroup[today].querySelector("precipitation accumulation amount").textContent * 1.0;
|
||||
weather.precipitationUnits = foreGroup[today].querySelector("precipitation accumulation amount").getAttribute("units");
|
||||
@@ -514,7 +508,7 @@ WeatherProvider.register("envcanada", {
|
||||
//
|
||||
// Convert the icons to a more usable name.
|
||||
//
|
||||
convertWeatherType(weatherType) {
|
||||
convertWeatherType (weatherType) {
|
||||
const weatherTypes = {
|
||||
"00": "day-sunny",
|
||||
"01": "day-sunny",
|
||||
|
@@ -1,15 +1,9 @@
|
||||
/* 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.
|
||||
/* This class is a provider for Open-Meteo,
|
||||
* see https://open-meteo.com/
|
||||
*/
|
||||
|
||||
// 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";
|
||||
@@ -140,11 +134,11 @@ WeatherProvider.register("openmeteo", {
|
||||
"et0_fao_evapotranspiration"
|
||||
],
|
||||
|
||||
fetchedLocation: function () {
|
||||
fetchedLocation () {
|
||||
return this.fetchedLocationName || "";
|
||||
},
|
||||
|
||||
fetchCurrentWeather() {
|
||||
fetchCurrentWeather () {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => this.parseWeatherApiResponse(data))
|
||||
.then((parsedData) => {
|
||||
@@ -162,7 +156,7 @@ WeatherProvider.register("openmeteo", {
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
fetchWeatherForecast() {
|
||||
fetchWeatherForecast () {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => this.parseWeatherApiResponse(data))
|
||||
.then((parsedData) => {
|
||||
@@ -180,7 +174,7 @@ WeatherProvider.register("openmeteo", {
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
fetchWeatherHourly() {
|
||||
fetchWeatherHourly () {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => this.parseWeatherApiResponse(data))
|
||||
.then((parsedData) => {
|
||||
@@ -202,7 +196,7 @@ WeatherProvider.register("openmeteo", {
|
||||
* Overrides method for setting config to check if endpoint is correct for hourly
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
setConfig(config) {
|
||||
setConfig (config) {
|
||||
this.config = {
|
||||
lang: config.lang ?? "en",
|
||||
...this.defaults,
|
||||
@@ -226,7 +220,7 @@ WeatherProvider.register("openmeteo", {
|
||||
},
|
||||
|
||||
// Generate valid query params to perform the request
|
||||
getQueryParameters() {
|
||||
getQueryParameters () {
|
||||
let params = {
|
||||
latitude: this.config.lat,
|
||||
longitude: this.config.lon,
|
||||
@@ -278,25 +272,23 @@ WeatherProvider.register("openmeteo", {
|
||||
},
|
||||
|
||||
// Create a URL from the config and base URL.
|
||||
getUrl() {
|
||||
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]
|
||||
};
|
||||
}, {})
|
||||
);
|
||||
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) {
|
||||
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,
|
||||
@@ -334,7 +326,7 @@ WeatherProvider.register("openmeteo", {
|
||||
},
|
||||
|
||||
// Reverse geocoding from latitude and longitude provided
|
||||
fetchLocation() {
|
||||
fetchLocation () {
|
||||
this.fetchData(`${GEOCODE_BASE}?latitude=${this.config.lat}&longitude=${this.config.lon}&localityLanguage=${this.config.lang}`)
|
||||
.then((data) => {
|
||||
if (!data || !data.city) {
|
||||
@@ -349,7 +341,8 @@ WeatherProvider.register("openmeteo", {
|
||||
},
|
||||
|
||||
// Implement WeatherDay generator.
|
||||
generateWeatherDayFromCurrentWeather(weather) {
|
||||
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.
|
||||
@@ -394,10 +387,10 @@ WeatherProvider.register("openmeteo", {
|
||||
},
|
||||
|
||||
// Implement WeatherForecast generator.
|
||||
generateWeatherObjectsFromForecast(weathers) {
|
||||
generateWeatherObjectsFromForecast (weathers) {
|
||||
const days = [];
|
||||
|
||||
weathers.daily.forEach((weather, i) => {
|
||||
weathers.daily.forEach((weather) => {
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
currentWeather.date = weather.time;
|
||||
@@ -405,14 +398,14 @@ WeatherProvider.register("openmeteo", {
|
||||
currentWeather.windFromDirection = 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.temperature = parseFloat((weather.temperature_2m_max + weather.temperature_2m_min) / 2);
|
||||
currentWeather.minTemperature = parseFloat(weather.temperature_2m_min);
|
||||
currentWeather.maxTemperature = parseFloat(weather.temperature_2m_max);
|
||||
currentWeather.weatherType = this.convertWeatherType(weather.weathercode, true);
|
||||
currentWeather.rain = parseFloat(weather.rain_sum);
|
||||
currentWeather.snow = parseFloat(weather.snowfall_sum * 10);
|
||||
currentWeather.precipitationAmount = parseFloat(weather.precipitation_sum);
|
||||
currentWeather.precipitationProbability = parseFloat(weather.precipitation_probability);
|
||||
currentWeather.precipitationProbability = parseFloat(weather.precipitation_hours * 100 / 24);
|
||||
currentWeather.uv_index = parseFloat(weather.uv_index_max);
|
||||
|
||||
days.push(currentWeather);
|
||||
@@ -422,12 +415,12 @@ WeatherProvider.register("openmeteo", {
|
||||
},
|
||||
|
||||
// Implement WeatherHourly generator.
|
||||
generateWeatherObjectsFromHourly(weathers) {
|
||||
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) {
|
||||
if ((hours.length === 0 && weather.time <= now) || hours.length >= this.config.maxEntries) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -439,9 +432,9 @@ WeatherProvider.register("openmeteo", {
|
||||
currentWeather.windFromDirection = 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.temperature = parseFloat(weather.temperature_2m);
|
||||
currentWeather.minTemperature = parseFloat(weathers.daily[h].temperature_2m_min);
|
||||
currentWeather.maxTemperature = parseFloat(weathers.daily[h].temperature_2m_max);
|
||||
currentWeather.weatherType = this.convertWeatherType(weather.weathercode, currentWeather.isDayTime());
|
||||
currentWeather.humidity = parseFloat(weather.relativehumidity_2m);
|
||||
currentWeather.rain = parseFloat(weather.rain);
|
||||
@@ -457,7 +450,7 @@ WeatherProvider.register("openmeteo", {
|
||||
},
|
||||
|
||||
// Map icons from Dark Sky to our icons.
|
||||
convertWeatherType(weathercode, isDayTime) {
|
||||
convertWeatherType (weathercode, isDayTime) {
|
||||
const weatherConditions = {
|
||||
0: "clear",
|
||||
1: "mainly-clear",
|
||||
@@ -542,7 +535,7 @@ WeatherProvider.register("openmeteo", {
|
||||
},
|
||||
|
||||
// Define required scripts.
|
||||
getScripts: function () {
|
||||
getScripts () {
|
||||
return ["moment.js"];
|
||||
}
|
||||
});
|
||||
|
@@ -1,12 +1,7 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is the blueprint for a weather provider.
|
||||
/* This class is a provider for Openweathermap,
|
||||
* see https://openweathermap.org/
|
||||
*/
|
||||
WeatherProvider.register("openweathermap", {
|
||||
// Set the name of the provider.
|
||||
@@ -27,7 +22,7 @@ WeatherProvider.register("openweathermap", {
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchCurrentWeather() {
|
||||
fetchCurrentWeather () {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
let currentWeather;
|
||||
@@ -46,7 +41,7 @@ WeatherProvider.register("openweathermap", {
|
||||
},
|
||||
|
||||
// Overwrite the fetchWeatherForecast method.
|
||||
fetchWeatherForecast() {
|
||||
fetchWeatherForecast () {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
let forecast;
|
||||
@@ -68,7 +63,7 @@ WeatherProvider.register("openweathermap", {
|
||||
},
|
||||
|
||||
// Overwrite the fetchWeatherHourly method.
|
||||
fetchWeatherHourly() {
|
||||
fetchWeatherHourly () {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
@@ -92,7 +87,7 @@ WeatherProvider.register("openweathermap", {
|
||||
* Overrides method for setting config to check if endpoint is correct for hourly
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
setConfig(config) {
|
||||
setConfig (config) {
|
||||
this.config = config;
|
||||
if (!this.config.weatherEndpoint) {
|
||||
switch (this.config.type) {
|
||||
@@ -116,14 +111,14 @@ WeatherProvider.register("openweathermap", {
|
||||
/*
|
||||
* Gets the complete url for the request
|
||||
*/
|
||||
getUrl() {
|
||||
getUrl () {
|
||||
return this.config.apiBase + this.config.apiVersion + this.config.weatherEndpoint + this.getParams();
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
generateWeatherObjectFromCurrentWeather (currentWeatherData) {
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
currentWeather.date = moment.unix(currentWeatherData.dt);
|
||||
@@ -142,7 +137,7 @@ WeatherProvider.register("openweathermap", {
|
||||
/*
|
||||
* Generate WeatherObjects based on forecast information
|
||||
*/
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
generateWeatherObjectsFromForecast (forecasts) {
|
||||
if (this.config.weatherEndpoint === "/forecast") {
|
||||
return this.generateForecastHourly(forecasts);
|
||||
} else if (this.config.weatherEndpoint === "/forecast/daily") {
|
||||
@@ -155,7 +150,7 @@ WeatherProvider.register("openweathermap", {
|
||||
/*
|
||||
* Generate WeatherObjects based on One Call forecast information
|
||||
*/
|
||||
generateWeatherObjectsFromOnecall(data) {
|
||||
generateWeatherObjectsFromOnecall (data) {
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
return this.fetchOnecall(data);
|
||||
}
|
||||
@@ -167,7 +162,7 @@ WeatherProvider.register("openweathermap", {
|
||||
* Generate forecast information for 3-hourly forecast (available for free
|
||||
* subscription).
|
||||
*/
|
||||
generateForecastHourly(forecasts) {
|
||||
generateForecastHourly (forecasts) {
|
||||
// initial variable declaration
|
||||
const days = [];
|
||||
// variables for temperature range and rain
|
||||
@@ -241,7 +236,7 @@ WeatherProvider.register("openweathermap", {
|
||||
* Generate forecast information for daily forecast (available for paid
|
||||
* subscription or old apiKey).
|
||||
*/
|
||||
generateForecastDaily(forecasts) {
|
||||
generateForecastDaily (forecasts) {
|
||||
// initial variable declaration
|
||||
const days = [];
|
||||
|
||||
@@ -281,7 +276,7 @@ WeatherProvider.register("openweathermap", {
|
||||
* Factors in timezone offsets.
|
||||
* Minutely forecasts are excluded for the moment, see getParams().
|
||||
*/
|
||||
fetchOnecall(data) {
|
||||
fetchOnecall (data) {
|
||||
let precip = false;
|
||||
|
||||
// get current weather, if requested
|
||||
@@ -295,6 +290,7 @@ WeatherProvider.register("openweathermap", {
|
||||
current.temperature = data.current.temp;
|
||||
current.weatherType = this.convertWeatherType(data.current.weather[0].icon);
|
||||
current.humidity = data.current.humidity;
|
||||
current.uv_index = data.current.uvi;
|
||||
if (data.current.hasOwnProperty("rain") && !isNaN(data.current["rain"]["1h"])) {
|
||||
current.rain = data.current["rain"]["1h"];
|
||||
precip = true;
|
||||
@@ -323,6 +319,7 @@ WeatherProvider.register("openweathermap", {
|
||||
weather.windFromDirection = hour.wind_deg;
|
||||
weather.weatherType = this.convertWeatherType(hour.weather[0].icon);
|
||||
weather.precipitationProbability = hour.pop ? hour.pop * 100 : undefined;
|
||||
weather.uv_index = hour.uvi;
|
||||
precip = false;
|
||||
if (hour.hasOwnProperty("rain") && !isNaN(hour.rain["1h"])) {
|
||||
weather.rain = hour.rain["1h"];
|
||||
@@ -355,6 +352,7 @@ WeatherProvider.register("openweathermap", {
|
||||
weather.windFromDirection = day.wind_deg;
|
||||
weather.weatherType = this.convertWeatherType(day.weather[0].icon);
|
||||
weather.precipitationProbability = day.pop ? day.pop * 100 : undefined;
|
||||
weather.uv_index = day.uvi;
|
||||
precip = false;
|
||||
if (!isNaN(day.rain)) {
|
||||
weather.rain = day.rain;
|
||||
@@ -379,7 +377,7 @@ WeatherProvider.register("openweathermap", {
|
||||
/*
|
||||
* Convert the OpenWeatherMap icons to a more usable name.
|
||||
*/
|
||||
convertWeatherType(weatherType) {
|
||||
convertWeatherType (weatherType) {
|
||||
const weatherTypes = {
|
||||
"01d": "day-sunny",
|
||||
"02d": "day-cloudy",
|
||||
@@ -409,7 +407,7 @@ WeatherProvider.register("openweathermap", {
|
||||
*
|
||||
* return String - URL params.
|
||||
*/
|
||||
getParams() {
|
||||
getParams () {
|
||||
let params = "?";
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
params += `lat=${this.config.lat}`;
|
||||
|
112
modules/default/weather/providers/overrideWrapper.js
Normal file
112
modules/default/weather/providers/overrideWrapper.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/* global Class, WeatherObject */
|
||||
|
||||
/*
|
||||
* Wrapper class to enable overrides of currentOverrideWeatherObject.
|
||||
*
|
||||
* Sits between the weather.js module and the provider implementations to allow us to
|
||||
* combine the incoming data from the CURRENT_WEATHER_OVERRIDE notification with the
|
||||
* existing data received from the current api provider. If no notifications have
|
||||
* been received then the api provider's data is used.
|
||||
*
|
||||
* The intent is to allow partial WeatherObjects from local sensors to augment or
|
||||
* replace the WeatherObjects from the api providers.
|
||||
*
|
||||
* This class shares the signature of WeatherProvider, and passes any methods not
|
||||
* concerning the current weather directly to the api provider implementation that
|
||||
* is currently in use.
|
||||
*/
|
||||
const OverrideWrapper = Class.extend({
|
||||
baseProvider: null,
|
||||
providerName: "localWrapper",
|
||||
notificationWeatherObject: null,
|
||||
currentOverrideWeatherObject: null,
|
||||
|
||||
init (baseProvider) {
|
||||
this.baseProvider = baseProvider;
|
||||
|
||||
// Binding the scope of current weather functions so any fetchData calls with
|
||||
// setCurrentWeather nested in them call this classes implementation instead
|
||||
// of the provider's default
|
||||
this.baseProvider.setCurrentWeather = this.setCurrentWeather.bind(this);
|
||||
this.baseProvider.currentWeather = this.currentWeather.bind(this);
|
||||
},
|
||||
|
||||
/* Unchanged Api Provider Methods */
|
||||
|
||||
setConfig (config) {
|
||||
this.baseProvider.setConfig(config);
|
||||
},
|
||||
start () {
|
||||
this.baseProvider.start();
|
||||
},
|
||||
fetchCurrentWeather () {
|
||||
this.baseProvider.fetchCurrentWeather();
|
||||
},
|
||||
fetchWeatherForecast () {
|
||||
this.baseProvider.fetchWeatherForecast();
|
||||
},
|
||||
fetchWeatherHourly () {
|
||||
this.baseProvider.fetchEatherHourly();
|
||||
},
|
||||
weatherForecast () {
|
||||
this.baseProvider.weatherForecast();
|
||||
},
|
||||
weatherHourly () {
|
||||
this.baseProvider.weatherHourly();
|
||||
},
|
||||
fetchedLocation () {
|
||||
this.baseProvider.fetchedLocation();
|
||||
},
|
||||
setWeatherForecast (weatherForecastArray) {
|
||||
this.baseProvider.setWeatherForecast(weatherForecastArray);
|
||||
},
|
||||
setWeatherHourly (weatherHourlyArray) {
|
||||
this.baseProvider.setWeatherHourly(weatherHourlyArray);
|
||||
},
|
||||
setFetchedLocation (name) {
|
||||
this.baseProvider.setFetchedLocation(name);
|
||||
},
|
||||
updateAvailable () {
|
||||
this.baseProvider.updateAvailable();
|
||||
},
|
||||
async fetchData (url, type = "json", requestHeaders = undefined, expectedResponseHeaders = undefined) {
|
||||
this.baseProvider.fetchData(url, type, requestHeaders, expectedResponseHeaders);
|
||||
},
|
||||
|
||||
/* Override Methods */
|
||||
|
||||
/**
|
||||
* Override to return this scope's
|
||||
* @returns {WeatherObject} The current weather object. May or may not contain overridden data.
|
||||
*/
|
||||
currentWeather () {
|
||||
return this.currentOverrideWeatherObject;
|
||||
},
|
||||
|
||||
/**
|
||||
* Override to combine the overrideWeatherObejct provided in the
|
||||
* notificationReceived method with the currentOverrideWeatherObject provided by the
|
||||
* api provider fetchData implementation.
|
||||
* @param {WeatherObject} currentWeatherObject - the api provider weather object
|
||||
*/
|
||||
setCurrentWeather (currentWeatherObject) {
|
||||
this.currentOverrideWeatherObject = Object.assign(currentWeatherObject, this.notificationWeatherObject);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the overrideWeatherObject, calls setCurrentWeather to combine it with
|
||||
* the existing current weather object provided by the base provider, and signals
|
||||
* that an update is ready.
|
||||
* @param {WeatherObject} payload - the weather object received from the CURRENT_WEATHER_OVERRIDE
|
||||
* notification. Represents information to augment the
|
||||
* existing currentOverrideWeatherObject with.
|
||||
*/
|
||||
notificationReceived (payload) {
|
||||
this.notificationWeatherObject = payload;
|
||||
|
||||
// setCurrentWeather combines the newly received notification weather with
|
||||
// the existing weather object we return for current weather
|
||||
this.setCurrentWeather(this.currentOverrideWeatherObject);
|
||||
this.updateAvailable();
|
||||
}
|
||||
});
|
@@ -1,14 +1,7 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: Pirate Weather
|
||||
*
|
||||
* Written by Nicholas Hubbard https://github.com/nhubbard for formerly Dark Sky Provider
|
||||
* Modified by Karsten Hassel for Pirate Weather
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Pirate Weather, it is a replacement for Dark Sky (same api).
|
||||
/* This class is a provider for Pirate Weather, it is a replacement for Dark Sky (same api),
|
||||
* see http://pirateweather.net/en/latest/
|
||||
*/
|
||||
WeatherProvider.register("pirateweather", {
|
||||
// Set the name of the provider.
|
||||
@@ -25,7 +18,7 @@ WeatherProvider.register("pirateweather", {
|
||||
lon: 0
|
||||
},
|
||||
|
||||
fetchCurrentWeather() {
|
||||
fetchCurrentWeather () {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data || !data.currently || typeof data.currently.temperature === "undefined") {
|
||||
@@ -42,7 +35,7 @@ WeatherProvider.register("pirateweather", {
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
fetchWeatherForecast() {
|
||||
fetchWeatherForecast () {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data || !data.daily || !data.daily.data.length) {
|
||||
@@ -60,12 +53,12 @@ WeatherProvider.register("pirateweather", {
|
||||
},
|
||||
|
||||
// Create a URL from the config and base URL.
|
||||
getUrl() {
|
||||
getUrl () {
|
||||
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) {
|
||||
generateWeatherDayFromCurrentWeather (currentWeatherData) {
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
currentWeather.date = moment();
|
||||
@@ -80,7 +73,7 @@ WeatherProvider.register("pirateweather", {
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
generateWeatherObjectsFromForecast (forecasts) {
|
||||
const days = [];
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
@@ -114,7 +107,7 @@ WeatherProvider.register("pirateweather", {
|
||||
},
|
||||
|
||||
// Map icons from Pirate Weather to our icons.
|
||||
convertWeatherType(weatherType) {
|
||||
convertWeatherType (weatherType) {
|
||||
const weatherTypes = {
|
||||
"clear-day": "day-sunny",
|
||||
"clear-night": "night-clear",
|
||||
|
@@ -1,14 +1,8 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: SMHI
|
||||
*
|
||||
* By BuXXi https://github.com/buxxi
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for SMHI (Sweden only). Metric system is the only
|
||||
* supported unit.
|
||||
/* This class is a provider for SMHI (Sweden only).
|
||||
* Metric system is the only supported unit,
|
||||
* see https://www.smhi.se/
|
||||
*/
|
||||
WeatherProvider.register("smhi", {
|
||||
providerName: "SMHI",
|
||||
@@ -24,7 +18,7 @@ WeatherProvider.register("smhi", {
|
||||
/**
|
||||
* Implements method in interface for fetching current weather.
|
||||
*/
|
||||
fetchCurrentWeather() {
|
||||
fetchCurrentWeather () {
|
||||
this.fetchData(this.getURL())
|
||||
.then((data) => {
|
||||
const closest = this.getClosestToCurrentTime(data.timeSeries);
|
||||
@@ -40,7 +34,7 @@ WeatherProvider.register("smhi", {
|
||||
/**
|
||||
* Implements method in interface for fetching a multi-day forecast.
|
||||
*/
|
||||
fetchWeatherForecast() {
|
||||
fetchWeatherForecast () {
|
||||
this.fetchData(this.getURL())
|
||||
.then((data) => {
|
||||
const coordinates = this.resolveCoordinates(data);
|
||||
@@ -55,7 +49,7 @@ WeatherProvider.register("smhi", {
|
||||
/**
|
||||
* Implements method in interface for fetching hourly forecasts.
|
||||
*/
|
||||
fetchWeatherHourly() {
|
||||
fetchWeatherHourly () {
|
||||
this.fetchData(this.getURL())
|
||||
.then((data) => {
|
||||
const coordinates = this.resolveCoordinates(data);
|
||||
@@ -71,7 +65,7 @@ WeatherProvider.register("smhi", {
|
||||
* Overrides method for setting config with checks for the precipitationValue being unset or invalid
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
setConfig(config) {
|
||||
setConfig (config) {
|
||||
this.config = config;
|
||||
if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) === -1) {
|
||||
Log.log(`invalid or not set: ${config.precipitationValue}`);
|
||||
@@ -84,7 +78,7 @@ WeatherProvider.register("smhi", {
|
||||
* @param {object[]} times Array of time objects
|
||||
* @returns {object} The weatherdata closest to the current time
|
||||
*/
|
||||
getClosestToCurrentTime(times) {
|
||||
getClosestToCurrentTime (times) {
|
||||
let now = moment();
|
||||
let minDiff = undefined;
|
||||
for (const time of times) {
|
||||
@@ -100,7 +94,7 @@ WeatherProvider.register("smhi", {
|
||||
* Get the forecast url for the configured coordinates
|
||||
* @returns {string} the url for the specified coordinates
|
||||
*/
|
||||
getURL() {
|
||||
getURL () {
|
||||
const formatter = new Intl.NumberFormat("en-US", {
|
||||
minimumFractionDigits: 6,
|
||||
maximumFractionDigits: 6
|
||||
@@ -115,7 +109,7 @@ WeatherProvider.register("smhi", {
|
||||
* @param {object} weatherData Weatherdata to use for the calculation
|
||||
* @returns {number} The apparent temperature
|
||||
*/
|
||||
calculateApparentTemperature(weatherData) {
|
||||
calculateApparentTemperature (weatherData) {
|
||||
const Ta = this.paramValue(weatherData, "t");
|
||||
const rh = this.paramValue(weatherData, "r");
|
||||
const ws = this.paramValue(weatherData, "ws");
|
||||
@@ -132,7 +126,7 @@ WeatherProvider.register("smhi", {
|
||||
* @param {object} coordinates Coordinates of the locations of the weather
|
||||
* @returns {WeatherObject} The converted weatherdata at the specified location
|
||||
*/
|
||||
convertWeatherDataToObject(weatherData, coordinates) {
|
||||
convertWeatherDataToObject (weatherData, coordinates) {
|
||||
let currentWeather = new WeatherObject();
|
||||
|
||||
currentWeather.date = moment(weatherData.validTime);
|
||||
@@ -178,7 +172,7 @@ WeatherProvider.register("smhi", {
|
||||
* @param {string} groupBy The interval to use for grouping the data (day, hour)
|
||||
* @returns {WeatherObject[]} Array of weatherobjects
|
||||
*/
|
||||
convertWeatherDataGroupedBy(allWeatherData, coordinates, groupBy = "day") {
|
||||
convertWeatherDataGroupedBy (allWeatherData, coordinates, groupBy = "day") {
|
||||
let currentWeather;
|
||||
let result = [];
|
||||
|
||||
@@ -227,7 +221,7 @@ WeatherProvider.register("smhi", {
|
||||
* @param {object} data Response data from the weather service
|
||||
* @returns {{lon, lat}} the lat/long coordinates of the data
|
||||
*/
|
||||
resolveCoordinates(data) {
|
||||
resolveCoordinates (data) {
|
||||
return { lat: data.geometry.coordinates[0][1], lon: data.geometry.coordinates[0][0] };
|
||||
},
|
||||
|
||||
@@ -237,7 +231,7 @@ WeatherProvider.register("smhi", {
|
||||
* @param {object[]} data Response data from the weather service
|
||||
* @returns {object[]} Given data with filled gaps
|
||||
*/
|
||||
fillInGaps(data) {
|
||||
fillInGaps (data) {
|
||||
let result = [];
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
let to = moment(data[i].validTime);
|
||||
@@ -259,7 +253,7 @@ WeatherProvider.register("smhi", {
|
||||
* @param {string} name The name of the property
|
||||
* @returns {*} The value of the property in the weatherdata
|
||||
*/
|
||||
paramValue(currentWeatherData, name) {
|
||||
paramValue (currentWeatherData, name) {
|
||||
return currentWeatherData.parameters.filter((p) => p.name === name).flatMap((p) => p.values)[0];
|
||||
},
|
||||
|
||||
@@ -271,7 +265,7 @@ WeatherProvider.register("smhi", {
|
||||
* @param {boolean} isDayTime True if the icon should be for daytime, false for nighttime
|
||||
* @returns {string} The icon name for the MagicMirror
|
||||
*/
|
||||
convertWeatherType(input, isDayTime) {
|
||||
convertWeatherType (input, isDayTime) {
|
||||
switch (input) {
|
||||
case 1:
|
||||
return isDayTime ? "day-sunny" : "night-clear"; // Clear sky
|
||||
|
@@ -1,12 +1,7 @@
|
||||
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
*
|
||||
* By Malcolm Oakes https://github.com/maloakes
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is a provider for UK Met Office Datapoint.
|
||||
/* This class is a provider for UK Met Office Datapoint,
|
||||
* see https://www.metoffice.gov.uk/
|
||||
*/
|
||||
WeatherProvider.register("ukmetoffice", {
|
||||
// Set the name of the provider.
|
||||
@@ -22,7 +17,7 @@ WeatherProvider.register("ukmetoffice", {
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchCurrentWeather() {
|
||||
fetchCurrentWeather () {
|
||||
this.fetchData(this.getUrl("3hourly"))
|
||||
.then((data) => {
|
||||
if (!data || !data.SiteRep || !data.SiteRep.DV || !data.SiteRep.DV.Location || !data.SiteRep.DV.Location.Period || data.SiteRep.DV.Location.Period.length === 0) {
|
||||
@@ -43,7 +38,7 @@ WeatherProvider.register("ukmetoffice", {
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchWeatherForecast() {
|
||||
fetchWeatherForecast () {
|
||||
this.fetchData(this.getUrl("daily"))
|
||||
.then((data) => {
|
||||
if (!data || !data.SiteRep || !data.SiteRep.DV || !data.SiteRep.DV.Location || !data.SiteRep.DV.Location.Period || data.SiteRep.DV.Location.Period.length === 0) {
|
||||
@@ -67,14 +62,14 @@ WeatherProvider.register("ukmetoffice", {
|
||||
/*
|
||||
* Gets the complete url for the request
|
||||
*/
|
||||
getUrl(forecastType) {
|
||||
getUrl (forecastType) {
|
||||
return this.config.apiBase + this.config.locationID + this.getParams(forecastType);
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
generateWeatherObjectFromCurrentWeather (currentWeatherData) {
|
||||
const currentWeather = new WeatherObject();
|
||||
const location = currentWeatherData.SiteRep.DV.Location;
|
||||
|
||||
@@ -119,7 +114,7 @@ WeatherProvider.register("ukmetoffice", {
|
||||
/*
|
||||
* Generate WeatherObjects based on forecast information
|
||||
*/
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
generateWeatherObjectsFromForecast (forecasts) {
|
||||
const days = [];
|
||||
|
||||
// loop round the (5) periods getting the data
|
||||
@@ -150,7 +145,7 @@ WeatherProvider.register("ukmetoffice", {
|
||||
/*
|
||||
* Convert the Met Office icons to a more usable name.
|
||||
*/
|
||||
convertWeatherType(weatherType) {
|
||||
convertWeatherType (weatherType) {
|
||||
const weatherTypes = {
|
||||
0: "night-clear",
|
||||
1: "day-sunny",
|
||||
@@ -192,7 +187,7 @@ WeatherProvider.register("ukmetoffice", {
|
||||
* @param {string} forecastType daily or 3hourly forecast
|
||||
* @returns {string} url
|
||||
*/
|
||||
getParams(forecastType) {
|
||||
getParams (forecastType) {
|
||||
let params = "?";
|
||||
params += `res=${forecastType}`;
|
||||
params += `&key=${this.config.apiKey}`;
|
||||
|
@@ -1,13 +1,6 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
*
|
||||
* By Malcolm Oakes https://github.com/maloakes
|
||||
* Existing Met Office provider edited for new MetOffice Data Hub by CreepinJesus http://github.com/XBCreepinJesus
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is a provider for UK Met Office Data Hub (the replacement for their Data Point services).
|
||||
/* This class is a provider for UK Met Office Data Hub (the replacement for their Data Point services).
|
||||
* For more information on Data Hub, see https://www.metoffice.gov.uk/services/data/datapoint/notifications/weather-datahub
|
||||
* Data available:
|
||||
* Hourly data for next 2 days ("hourly") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-hourly.pdf
|
||||
@@ -53,7 +46,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
},
|
||||
|
||||
// Build URL with query strings according to DataHub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
|
||||
getUrl(forecastType) {
|
||||
getUrl (forecastType) {
|
||||
let queryStrings = "?";
|
||||
queryStrings += `latitude=${this.config.lat}`;
|
||||
queryStrings += `&longitude=${this.config.lon}`;
|
||||
@@ -66,7 +59,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
// Build the list of headers for the request
|
||||
// For DataHub requests, the API key/secret are sent in the headers rather than as query strings.
|
||||
// Headers defined according to Data Hub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
|
||||
getHeaders() {
|
||||
getHeaders () {
|
||||
return {
|
||||
accept: "application/json",
|
||||
"x-ibm-client-id": this.config.apiKey,
|
||||
@@ -75,7 +68,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
},
|
||||
|
||||
// Fetch data using supplied URL and request headers
|
||||
async fetchWeather(url, headers) {
|
||||
async fetchWeather (url, headers) {
|
||||
const response = await fetch(url, { headers: headers });
|
||||
|
||||
// Return JSON data
|
||||
@@ -83,7 +76,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
},
|
||||
|
||||
// Fetch hourly forecast data (to use for current weather)
|
||||
fetchCurrentWeather() {
|
||||
fetchCurrentWeather () {
|
||||
this.fetchWeather(this.getUrl("hourly"), this.getHeaders())
|
||||
.then((data) => {
|
||||
// Check data is usable
|
||||
@@ -111,7 +104,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
},
|
||||
|
||||
// Create a WeatherObject using current weather data (data for the current hour)
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
generateWeatherObjectFromCurrentWeather (currentWeatherData) {
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
// Extract the actual forecasts
|
||||
@@ -152,7 +145,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
},
|
||||
|
||||
// Fetch daily forecast data
|
||||
fetchWeatherForecast() {
|
||||
fetchWeatherForecast () {
|
||||
this.fetchWeather(this.getUrl("daily"), this.getHeaders())
|
||||
.then((data) => {
|
||||
// Check data is usable
|
||||
@@ -180,7 +173,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
},
|
||||
|
||||
// Create a WeatherObject for each day using daily forecast data
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
generateWeatherObjectsFromForecast (forecasts) {
|
||||
const dailyForecasts = [];
|
||||
|
||||
// Extract the actual forecasts
|
||||
@@ -225,14 +218,14 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
},
|
||||
|
||||
// Set the fetched location name.
|
||||
setFetchedLocation: function (name) {
|
||||
setFetchedLocation (name) {
|
||||
this.fetchedLocationName = name;
|
||||
},
|
||||
|
||||
// Match the Met Office "significant weather code" to a weathericons.css icon
|
||||
// Use: https://metoffice.apiconnect.ibmcloud.com/metoffice/production/node/264
|
||||
// and: https://erikflowers.github.io/weather-icons/
|
||||
convertWeatherType(weatherType) {
|
||||
convertWeatherType (weatherType) {
|
||||
const weatherTypes = {
|
||||
0: "night-clear",
|
||||
1: "day-sunny",
|
||||
|
@@ -1,14 +1,7 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: Weatherbit
|
||||
*
|
||||
* By Andrew Pometti
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Weatherbit, based on Nicholas Hubbard's class
|
||||
* for Dark Sky & Vince Peri's class for Weather.gov.
|
||||
/* This class is a provider for Weatherbit,
|
||||
* see https://www.weatherbit.io/
|
||||
*/
|
||||
WeatherProvider.register("weatherbit", {
|
||||
// Set the name of the provider.
|
||||
@@ -23,11 +16,11 @@ WeatherProvider.register("weatherbit", {
|
||||
lon: 0
|
||||
},
|
||||
|
||||
fetchedLocation: function () {
|
||||
fetchedLocation () {
|
||||
return this.fetchedLocationName || "";
|
||||
},
|
||||
|
||||
fetchCurrentWeather() {
|
||||
fetchCurrentWeather () {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data || !data.data[0] || typeof data.data[0].temp === "undefined") {
|
||||
@@ -44,7 +37,7 @@ WeatherProvider.register("weatherbit", {
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
fetchWeatherForecast() {
|
||||
fetchWeatherForecast () {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data || !data.data) {
|
||||
@@ -67,7 +60,7 @@ WeatherProvider.register("weatherbit", {
|
||||
* Overrides method for setting config to check if endpoint is correct for hourly
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
setConfig(config) {
|
||||
setConfig (config) {
|
||||
this.config = config;
|
||||
if (!this.config.weatherEndpoint) {
|
||||
switch (this.config.type) {
|
||||
@@ -88,12 +81,12 @@ WeatherProvider.register("weatherbit", {
|
||||
},
|
||||
|
||||
// Create a URL from the config and base URL.
|
||||
getUrl() {
|
||||
getUrl () {
|
||||
return `${this.config.apiBase}${this.config.weatherEndpoint}?lat=${this.config.lat}&lon=${this.config.lon}&units=M&key=${this.config.apiKey}`;
|
||||
},
|
||||
|
||||
// Implement WeatherDay generator.
|
||||
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
||||
generateWeatherDayFromCurrentWeather (currentWeatherData) {
|
||||
//Calculate TZ Offset and invert to convert Sunrise/Sunset times to Local
|
||||
const d = new Date();
|
||||
let tzOffset = d.getTimezoneOffset();
|
||||
@@ -115,7 +108,7 @@ WeatherProvider.register("weatherbit", {
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
generateWeatherObjectsFromForecast (forecasts) {
|
||||
const days = [];
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
@@ -135,7 +128,7 @@ WeatherProvider.register("weatherbit", {
|
||||
},
|
||||
|
||||
// Map icons from Dark Sky to our icons.
|
||||
convertWeatherType(weatherType) {
|
||||
convertWeatherType (weatherType) {
|
||||
const weatherTypes = {
|
||||
t01d: "day-thunderstorm",
|
||||
t01n: "night-alt-thunderstorm",
|
||||
|
@@ -1,16 +1,8 @@
|
||||
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: Weatherflow
|
||||
*
|
||||
* By Tobias Dreyem https://github.com/10bias
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Weatherflow.
|
||||
/* This class is a provider for Weatherflow.
|
||||
* Note that the Weatherflow API does not provide snowfall.
|
||||
*/
|
||||
|
||||
WeatherProvider.register("weatherflow", {
|
||||
// Set the name of the provider.
|
||||
// Not strictly required, but helps for debugging
|
||||
@@ -23,7 +15,7 @@ WeatherProvider.register("weatherflow", {
|
||||
stationid: ""
|
||||
},
|
||||
|
||||
fetchCurrentWeather() {
|
||||
fetchCurrentWeather () {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
const currentWeather = new WeatherObject();
|
||||
@@ -44,7 +36,7 @@ WeatherProvider.register("weatherflow", {
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
fetchWeatherForecast() {
|
||||
fetchWeatherForecast () {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
const days = [];
|
||||
@@ -71,7 +63,7 @@ WeatherProvider.register("weatherflow", {
|
||||
},
|
||||
|
||||
// Create a URL from the config and base URL.
|
||||
getUrl() {
|
||||
getUrl () {
|
||||
return `${this.config.apiBase}better_forecast?station_id=${this.config.stationid}&units_temp=c&units_wind=kph&units_pressure=mb&units_precip=mm&units_distance=km&token=${this.config.token}`;
|
||||
}
|
||||
});
|
||||
|
@@ -1,13 +1,8 @@
|
||||
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: weather.gov
|
||||
/* Provider: weather.gov
|
||||
* https://weather-gov.github.io/api/general-faqs
|
||||
*
|
||||
* Original by Vince Peri
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is a provider for weather.gov.
|
||||
* Note that this is only for US locations (lat and lon) and does not require an API key
|
||||
* Since it is free, there are some items missing - like sunrise, sunset
|
||||
@@ -37,24 +32,23 @@ WeatherProvider.register("weathergov", {
|
||||
stationObsURL: "tbd",
|
||||
|
||||
// Called to set the config, this config is the same as the weather module's config.
|
||||
setConfig: function (config) {
|
||||
setConfig (config) {
|
||||
this.config = config;
|
||||
this.config.apiBase = "https://api.weather.gov";
|
||||
this.fetchWxGovURLs(this.config);
|
||||
},
|
||||
|
||||
// Called when the weather provider is about to start.
|
||||
start: function () {
|
||||
start () {
|
||||
Log.info(`Weather provider: ${this.providerName} started.`);
|
||||
},
|
||||
|
||||
// This returns the name of the fetched location or an empty string.
|
||||
fetchedLocation: function () {
|
||||
fetchedLocation () {
|
||||
return this.fetchedLocationName || "";
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchCurrentWeather() {
|
||||
fetchCurrentWeather () {
|
||||
if (!this.configURLs) {
|
||||
Log.info("fetchCurrentWeather: fetch wx waiting on config URLs");
|
||||
return;
|
||||
@@ -75,7 +69,7 @@ WeatherProvider.register("weathergov", {
|
||||
},
|
||||
|
||||
// Overwrite the fetchWeatherForecast method.
|
||||
fetchWeatherForecast() {
|
||||
fetchWeatherForecast () {
|
||||
if (!this.configURLs) {
|
||||
Log.info("fetchWeatherForecast: fetch wx waiting on config URLs");
|
||||
return;
|
||||
@@ -96,7 +90,7 @@ WeatherProvider.register("weathergov", {
|
||||
},
|
||||
|
||||
// Overwrite the fetchWeatherHourly method.
|
||||
fetchWeatherHourly() {
|
||||
fetchWeatherHourly () {
|
||||
if (!this.configURLs) {
|
||||
Log.info("fetchWeatherHourly: fetch wx waiting on config URLs");
|
||||
return;
|
||||
@@ -122,8 +116,8 @@ WeatherProvider.register("weathergov", {
|
||||
/*
|
||||
* Get specific URLs
|
||||
*/
|
||||
fetchWxGovURLs(config) {
|
||||
this.fetchData(`${config.apiBase}/points/${config.lat},${config.lon}`)
|
||||
fetchWxGovURLs (config) {
|
||||
this.fetchData(`${config.apiBase}/${config.lat},${config.lon}`)
|
||||
.then((data) => {
|
||||
if (!data || !data.properties) {
|
||||
// points URL did not respond with usable data.
|
||||
@@ -162,12 +156,13 @@ WeatherProvider.register("weathergov", {
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate a WeatherObject based on hourlyWeatherInformation
|
||||
* Weather.gov API uses specific units; API does not include choice of units
|
||||
* ... object needs data in units based on config!
|
||||
*/
|
||||
generateWeatherObjectsFromHourly(forecasts) {
|
||||
generateWeatherObjectsFromHourly (forecasts) {
|
||||
const days = [];
|
||||
|
||||
// variable for date
|
||||
@@ -182,6 +177,12 @@ WeatherProvider.register("weathergov", {
|
||||
weather.windSpeed = WeatherUtils.convertWindToMs(weather.windSpeed);
|
||||
weather.windFromDirection = forecast.windDirection;
|
||||
weather.temperature = forecast.temperature;
|
||||
//assign probability of precipitation
|
||||
if (forecast.probabilityOfPrecipitation.value === null) {
|
||||
weather.precipitationProbability = 0;
|
||||
} else {
|
||||
weather.precipitationProbability = forecast.probabilityOfPrecipitation.value;
|
||||
}
|
||||
// use the forecast isDayTime attribute to help build the weatherType label
|
||||
weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime);
|
||||
|
||||
@@ -200,7 +201,7 @@ WeatherProvider.register("weathergov", {
|
||||
* Weather.gov API uses specific units; API does not include choice of units
|
||||
* ... object needs data in units based on config!
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
generateWeatherObjectFromCurrentWeather (currentWeatherData) {
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
currentWeather.date = moment(currentWeatherData.timestamp);
|
||||
@@ -210,7 +211,7 @@ WeatherProvider.register("weathergov", {
|
||||
currentWeather.minTemperature = currentWeatherData.minTemperatureLast24Hours.value;
|
||||
currentWeather.maxTemperature = currentWeatherData.maxTemperatureLast24Hours.value;
|
||||
currentWeather.humidity = Math.round(currentWeatherData.relativeHumidity.value);
|
||||
currentWeather.precipitationAmount = currentWeatherData.precipitationLastHour.value;
|
||||
currentWeather.precipitationAmount = currentWeatherData.precipitationLastHour.value ? currentWeatherData.precipitationLastHour.value : currentWeatherData.precipitationLast3Hours.value;
|
||||
if (currentWeatherData.heatIndex.value !== null) {
|
||||
currentWeather.feelsLikeTemp = currentWeatherData.heatIndex.value;
|
||||
} else if (currentWeatherData.windChill.value !== null) {
|
||||
@@ -230,16 +231,14 @@ WeatherProvider.register("weathergov", {
|
||||
/*
|
||||
* Generate WeatherObjects based on forecast information
|
||||
*/
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
generateWeatherObjectsFromForecast (forecasts) {
|
||||
return this.fetchForecastDaily(forecasts);
|
||||
},
|
||||
|
||||
/*
|
||||
* fetch forecast information for daily forecast.
|
||||
*/
|
||||
fetchForecastDaily(forecasts) {
|
||||
const precipitationProbabilityRegEx = "Chance of precipitation is ([0-9]+?)%";
|
||||
|
||||
fetchForecastDaily (forecasts) {
|
||||
// initial variable declaration
|
||||
const days = [];
|
||||
// variables for temperature range and rain
|
||||
@@ -262,8 +261,12 @@ WeatherProvider.register("weathergov", {
|
||||
|
||||
minTemp = [];
|
||||
maxTemp = [];
|
||||
const precipitation = new RegExp(precipitationProbabilityRegEx, "g").exec(forecast.detailedForecast);
|
||||
if (precipitation) weather.precipitationProbability = precipitation[1];
|
||||
//assign probability of precipitation
|
||||
if (forecast.probabilityOfPrecipitation.value === null) {
|
||||
weather.precipitationProbability = 0;
|
||||
} else {
|
||||
weather.precipitationProbability = forecast.probabilityOfPrecipitation.value;
|
||||
}
|
||||
|
||||
// set new date
|
||||
date = moment(forecast.startTime).format("YYYY-MM-DD");
|
||||
@@ -298,7 +301,7 @@ WeatherProvider.register("weathergov", {
|
||||
/*
|
||||
* Convert the icons to a more usable name.
|
||||
*/
|
||||
convertWeatherType(weatherType, isDaytime) {
|
||||
convertWeatherType (weatherType, isDaytime) {
|
||||
//https://w1.weather.gov/xml/current_obs/weather.php
|
||||
// There are way too many types to create, so lets just look for certain strings
|
||||
|
||||
|
@@ -1,14 +1,6 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: Yr.no
|
||||
*
|
||||
* By Magnus Marthinsen
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Yr.no, a norwegian weather service.
|
||||
*
|
||||
/* This class is a provider for Yr.no, a norwegian weather service.
|
||||
* Terms of service: https://developer.yr.no/doc/TermsOfService/
|
||||
*/
|
||||
WeatherProvider.register("yr", {
|
||||
@@ -18,11 +10,13 @@ WeatherProvider.register("yr", {
|
||||
defaults: {
|
||||
useCorsProxy: true,
|
||||
apiBase: "https://api.met.no/weatherapi",
|
||||
forecastApiVersion: "2.0",
|
||||
sunriseApiVersion: "3.0",
|
||||
altitude: 0,
|
||||
currentForecastHours: 1 //1, 6 or 12
|
||||
},
|
||||
|
||||
start() {
|
||||
start () {
|
||||
if (typeof Storage === "undefined") {
|
||||
//local storage unavailable
|
||||
Log.error("The Yr weather provider requires local storage.");
|
||||
@@ -31,7 +25,7 @@ WeatherProvider.register("yr", {
|
||||
Log.info(`Weather provider: ${this.providerName} started.`);
|
||||
},
|
||||
|
||||
fetchCurrentWeather() {
|
||||
fetchCurrentWeather () {
|
||||
this.getCurrentWeather()
|
||||
.then((currentWeather) => {
|
||||
this.setCurrentWeather(currentWeather);
|
||||
@@ -43,9 +37,8 @@ WeatherProvider.register("yr", {
|
||||
});
|
||||
},
|
||||
|
||||
async getCurrentWeather() {
|
||||
const getRequests = [this.getWeatherData(), this.getStellarData()];
|
||||
const [weatherData, stellarData] = await Promise.all(getRequests);
|
||||
async getCurrentWeather () {
|
||||
const [weatherData, stellarData] = await Promise.all([this.getWeatherData(), this.getStellarData()]);
|
||||
if (!stellarData) {
|
||||
Log.warn("No stellar data available.");
|
||||
}
|
||||
@@ -72,7 +65,7 @@ WeatherProvider.register("yr", {
|
||||
return this.getWeatherDataFrom(forecast, stellarData, weatherData.properties.meta.units);
|
||||
},
|
||||
|
||||
getWeatherData() {
|
||||
getWeatherData () {
|
||||
return new Promise((resolve, reject) => {
|
||||
// If a user has several Yr-modules, for instance one current and one forecast, the API calls must be synchronized across classes.
|
||||
// This is to avoid multiple similar calls to the API.
|
||||
@@ -98,7 +91,7 @@ WeatherProvider.register("yr", {
|
||||
});
|
||||
},
|
||||
|
||||
getWeatherDataFromYrOrCache(resolve, reject) {
|
||||
getWeatherDataFromYrOrCache (resolve, reject) {
|
||||
localStorage.setItem("yrIsFetchingWeatherData", "true");
|
||||
|
||||
let weatherData = this.getWeatherDataFromCache();
|
||||
@@ -130,16 +123,16 @@ WeatherProvider.register("yr", {
|
||||
}
|
||||
},
|
||||
|
||||
weatherDataIsValid(weatherData) {
|
||||
weatherDataIsValid (weatherData) {
|
||||
return (
|
||||
weatherData &&
|
||||
weatherData.timeout &&
|
||||
0 < moment(weatherData.timeout).diff(moment()) &&
|
||||
(!weatherData.geometry || !weatherData.geometry.coordinates || !weatherData.geometry.coordinates.length < 2 || (weatherData.geometry.coordinates[0] === this.config.lat && weatherData.geometry.coordinates[1] === this.config.lon))
|
||||
weatherData
|
||||
&& weatherData.timeout
|
||||
&& 0 < moment(weatherData.timeout).diff(moment())
|
||||
&& (!weatherData.geometry || !weatherData.geometry.coordinates || !weatherData.geometry.coordinates.length < 2 || (weatherData.geometry.coordinates[0] === this.config.lat && weatherData.geometry.coordinates[1] === this.config.lon))
|
||||
);
|
||||
},
|
||||
|
||||
getWeatherDataFromCache() {
|
||||
getWeatherDataFromCache () {
|
||||
const weatherData = localStorage.getItem("weatherData");
|
||||
if (weatherData) {
|
||||
return JSON.parse(weatherData);
|
||||
@@ -148,7 +141,7 @@ WeatherProvider.register("yr", {
|
||||
}
|
||||
},
|
||||
|
||||
getWeatherDataFromYr(currentDataFetchedAt) {
|
||||
getWeatherDataFromYr (currentDataFetchedAt) {
|
||||
const requestHeaders = [{ name: "Accept", value: "application/json" }];
|
||||
if (currentDataFetchedAt) {
|
||||
requestHeaders.push({ name: "If-Modified-Since", value: currentDataFetchedAt });
|
||||
@@ -170,7 +163,7 @@ WeatherProvider.register("yr", {
|
||||
});
|
||||
},
|
||||
|
||||
getForecastUrl() {
|
||||
getConfigOptions () {
|
||||
if (!this.config.lat) {
|
||||
Log.error("Latitude not provided.");
|
||||
throw new Error("Latitude not provided.");
|
||||
@@ -183,6 +176,11 @@ WeatherProvider.register("yr", {
|
||||
let lat = this.config.lat.toString();
|
||||
let lon = this.config.lon.toString();
|
||||
const altitude = this.config.altitude ?? 0;
|
||||
return { lat, lon, altitude };
|
||||
},
|
||||
|
||||
getForecastUrl () {
|
||||
let { lat, lon, altitude } = this.getConfigOptions();
|
||||
|
||||
if (lat.includes(".") && lat.split(".")[1].length > 4) {
|
||||
Log.warn("Latitude is too specific for weather data. Do not use more than four decimals. Trimming to maximum length.");
|
||||
@@ -195,19 +193,14 @@ WeatherProvider.register("yr", {
|
||||
lon = `${lonParts[0]}.${lonParts[1].substring(0, 4)}`;
|
||||
}
|
||||
|
||||
return `${this.config.apiBase}/locationforecast/2.0/complete?&altitude=${altitude}&lat=${lat}&lon=${lon}`;
|
||||
return `${this.config.apiBase}/locationforecast/${this.config.forecastApiVersion}/complete?&altitude=${altitude}&lat=${lat}&lon=${lon}`;
|
||||
},
|
||||
|
||||
cacheWeatherData(weatherData) {
|
||||
cacheWeatherData (weatherData) {
|
||||
localStorage.setItem("weatherData", JSON.stringify(weatherData));
|
||||
},
|
||||
|
||||
getAuthenticationString() {
|
||||
if (!this.config.authenticationEmail) throw new Error("Authentication email not provided.");
|
||||
return `${this.config.applicaitionName} ${this.config.authenticationEmail}`;
|
||||
},
|
||||
|
||||
getStellarData() {
|
||||
getStellarData () {
|
||||
// If a user has several Yr-modules, for instance one current and one forecast, the API calls must be synchronized across classes.
|
||||
// This is to avoid multiple similar calls to the API.
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -233,7 +226,7 @@ WeatherProvider.register("yr", {
|
||||
});
|
||||
},
|
||||
|
||||
getStellarDataFromYrOrCache(resolve, reject) {
|
||||
getStellarDataFromYrOrCache (resolve, reject) {
|
||||
localStorage.setItem("yrIsFetchingStellarData", "true");
|
||||
|
||||
let stellarData = this.getStellarDataFromCache();
|
||||
@@ -291,7 +284,7 @@ WeatherProvider.register("yr", {
|
||||
}
|
||||
},
|
||||
|
||||
getStellarDataFromCache() {
|
||||
getStellarDataFromCache () {
|
||||
const stellarData = localStorage.getItem("stellarData");
|
||||
if (stellarData) {
|
||||
return JSON.parse(stellarData);
|
||||
@@ -300,9 +293,9 @@ WeatherProvider.register("yr", {
|
||||
}
|
||||
},
|
||||
|
||||
getStellarDataFromYr(date, days = 1) {
|
||||
getStellarDataFromYr (date, days = 1) {
|
||||
const requestHeaders = [{ name: "Accept", value: "application/json" }];
|
||||
return this.fetchData(this.getStellarDatatUrl(date, days), "json", requestHeaders)
|
||||
return this.fetchData(this.getStellarDataUrl(date, days), "json", requestHeaders)
|
||||
.then((data) => {
|
||||
Log.debug("Got stellar data from yr.");
|
||||
return data;
|
||||
@@ -313,19 +306,8 @@ WeatherProvider.register("yr", {
|
||||
});
|
||||
},
|
||||
|
||||
getStellarDatatUrl(date, days) {
|
||||
if (!this.config.lat) {
|
||||
Log.error("Latitude not provided.");
|
||||
throw new Error("Latitude not provided.");
|
||||
}
|
||||
if (!this.config.lon) {
|
||||
Log.error("Longitude not provided.");
|
||||
throw new Error("Longitude not provided.");
|
||||
}
|
||||
|
||||
let lat = this.config.lat.toString();
|
||||
let lon = this.config.lon.toString();
|
||||
const altitude = this.config.altitude ?? 0;
|
||||
getStellarDataUrl (date, days) {
|
||||
let { lat, lon, altitude } = this.getConfigOptions();
|
||||
|
||||
if (lat.includes(".") && lat.split(".")[1].length > 4) {
|
||||
Log.warn("Latitude is too specific for stellar data. Do not use more than four decimals. Trimming to maximum length.");
|
||||
@@ -352,18 +334,15 @@ WeatherProvider.register("yr", {
|
||||
if (hours.length < 2) {
|
||||
hours = `0${hours}`;
|
||||
}
|
||||
|
||||
return `${this.config.apiBase}/sunrise/2.0/.json?date=${date}&days=${days}&height=${altitude}&lat=${lat}&lon=${lon}&offset=${utcOffsetPrefix}${hours}%3A${minutes}`;
|
||||
return `${this.config.apiBase}/sunrise/${this.config.sunriseApiVersion}/sun?lat=${lat}&lon=${lon}&date=${date}&offset=${utcOffsetPrefix}${hours}%3A${minutes}`;
|
||||
},
|
||||
|
||||
cacheStellarData(data) {
|
||||
cacheStellarData (data) {
|
||||
localStorage.setItem("stellarData", JSON.stringify(data));
|
||||
},
|
||||
|
||||
getWeatherDataFrom(forecast, stellarData, units) {
|
||||
getWeatherDataFrom (forecast, stellarData, units) {
|
||||
const weather = new WeatherObject();
|
||||
const stellarTimesToday = stellarData?.today ? this.getStellarTimesFrom(stellarData.today, moment().format("YYYY-MM-DD")) : undefined;
|
||||
const stellarTimesTomorrow = stellarData?.tomorrow ? this.getStellarTimesFrom(stellarData.tomorrow, moment().add(1, "days").format("YYYY-MM-DD")) : undefined;
|
||||
|
||||
weather.date = moment(forecast.time);
|
||||
weather.windSpeed = forecast.data.instant.details.wind_speed;
|
||||
@@ -377,15 +356,13 @@ WeatherProvider.register("yr", {
|
||||
weather.precipitationProbability = forecast.precipitationProbability;
|
||||
weather.precipitationUnits = units.precipitation_amount;
|
||||
|
||||
if (stellarTimesToday) {
|
||||
weather.sunset = moment(stellarTimesToday.sunset.time);
|
||||
weather.sunrise = weather.sunset < moment() && stellarTimesTomorrow ? moment(stellarTimesTomorrow.sunrise.time) : moment(stellarTimesToday.sunrise.time);
|
||||
}
|
||||
weather.sunrise = stellarData?.today?.properties?.sunrise?.time;
|
||||
weather.sunset = stellarData?.today?.properties?.sunset?.time;
|
||||
|
||||
return weather;
|
||||
},
|
||||
|
||||
convertWeatherType(weatherType, weatherTime) {
|
||||
convertWeatherType (weatherType, weatherTime) {
|
||||
const weatherHour = moment(weatherTime).format("HH");
|
||||
|
||||
const weatherTypes = {
|
||||
@@ -477,16 +454,7 @@ WeatherProvider.register("yr", {
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
},
|
||||
|
||||
getStellarTimesFrom(stellarData, date) {
|
||||
for (const time of stellarData.location.time) {
|
||||
if (time.date === date) {
|
||||
return time;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
getForecastForXHoursFrom(weather) {
|
||||
getForecastForXHoursFrom (weather) {
|
||||
if (this.config.currentForecastHours === 1) {
|
||||
if (weather.next_1_hours) {
|
||||
return weather.next_1_hours;
|
||||
@@ -514,7 +482,7 @@ WeatherProvider.register("yr", {
|
||||
}
|
||||
},
|
||||
|
||||
fetchWeatherHourly() {
|
||||
fetchWeatherHourly () {
|
||||
this.getWeatherForecast("hourly")
|
||||
.then((forecast) => {
|
||||
this.setWeatherHourly(forecast);
|
||||
@@ -526,9 +494,8 @@ WeatherProvider.register("yr", {
|
||||
});
|
||||
},
|
||||
|
||||
async getWeatherForecast(type) {
|
||||
const getRequests = [this.getWeatherData(), this.getStellarData()];
|
||||
const [weatherData, stellarData] = await Promise.all(getRequests);
|
||||
async getWeatherForecast (type) {
|
||||
const [weatherData, stellarData] = await Promise.all([this.getWeatherData(), this.getStellarData()]);
|
||||
if (!weatherData.properties.timeseries || !weatherData.properties.timeseries[0]) {
|
||||
Log.error("No weather data available.");
|
||||
return;
|
||||
@@ -553,7 +520,7 @@ WeatherProvider.register("yr", {
|
||||
return series;
|
||||
},
|
||||
|
||||
getHourlyForecastFrom(weatherData) {
|
||||
getHourlyForecastFrom (weatherData) {
|
||||
const series = [];
|
||||
|
||||
for (const forecast of weatherData.properties.timeseries) {
|
||||
@@ -568,7 +535,7 @@ WeatherProvider.register("yr", {
|
||||
return series;
|
||||
},
|
||||
|
||||
getDailyForecastFrom(weatherData) {
|
||||
getDailyForecastFrom (weatherData) {
|
||||
const series = [];
|
||||
|
||||
const days = weatherData.properties.timeseries.reduce(function (days, forecast) {
|
||||
@@ -578,7 +545,7 @@ WeatherProvider.register("yr", {
|
||||
return days;
|
||||
}, Object.create(null));
|
||||
|
||||
Object.keys(days).forEach(function (time, index) {
|
||||
Object.keys(days).forEach(function (time) {
|
||||
let minTemperature = undefined;
|
||||
let maxTemperature = undefined;
|
||||
|
||||
@@ -618,7 +585,7 @@ WeatherProvider.register("yr", {
|
||||
return series;
|
||||
},
|
||||
|
||||
fetchWeatherForecast() {
|
||||
fetchWeatherForecast () {
|
||||
this.getWeatherForecast("daily")
|
||||
.then((forecast) => {
|
||||
this.setWeatherForecast(forecast);
|
||||
|
@@ -1,11 +1,5 @@
|
||||
/* global WeatherProvider, WeatherUtils, formatTime */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("weather", {
|
||||
// Default module config.
|
||||
defaults: {
|
||||
@@ -20,9 +14,10 @@ Module.register("weather", {
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
animationSpeed: 1000,
|
||||
showFeelsLike: true,
|
||||
showHumidity: false,
|
||||
showHumidity: "none", // this is now a string; see current.njk
|
||||
showIndoorHumidity: false,
|
||||
showIndoorTemperature: false,
|
||||
allowOverrideNotification: false,
|
||||
showPeriod: true,
|
||||
showPeriodUpper: false,
|
||||
showPrecipitationAmount: false,
|
||||
@@ -55,17 +50,17 @@ Module.register("weather", {
|
||||
firstEvent: null,
|
||||
|
||||
// Define required scripts.
|
||||
getStyles: function () {
|
||||
getStyles () {
|
||||
return ["font-awesome.css", "weather-icons.css", "weather.css"];
|
||||
},
|
||||
|
||||
// Return the scripts that are necessary for the weather module.
|
||||
getScripts: function () {
|
||||
return ["moment.js", this.file("../utils.js"), "weatherutils.js", "weatherprovider.js", "weatherobject.js", "suncalc.js", this.file(`providers/${this.config.weatherProvider.toLowerCase()}.js`)];
|
||||
getScripts () {
|
||||
return ["moment.js", "weatherutils.js", "weatherobject.js", this.file("providers/overrideWrapper.js"), "weatherprovider.js", "suncalc.js", this.file(`providers/${this.config.weatherProvider.toLowerCase()}.js`)];
|
||||
},
|
||||
|
||||
// Override getHeader method.
|
||||
getHeader: function () {
|
||||
getHeader () {
|
||||
if (this.config.appendLocationNameToHeader && this.weatherProvider) {
|
||||
if (this.data.header) return `${this.data.header} ${this.weatherProvider.fetchedLocation()}`;
|
||||
else return this.weatherProvider.fetchedLocation();
|
||||
@@ -75,7 +70,7 @@ Module.register("weather", {
|
||||
},
|
||||
|
||||
// Start the weather module.
|
||||
start: function () {
|
||||
start () {
|
||||
moment.locale(this.config.lang);
|
||||
|
||||
if (this.config.useKmh) {
|
||||
@@ -85,6 +80,10 @@ Module.register("weather", {
|
||||
Log.warn("Your are using the deprecated config values 'useBeaufort'. Please switch to windUnits!");
|
||||
this.windUnits = "beaufort";
|
||||
}
|
||||
if (typeof this.config.showHumidity === "boolean") {
|
||||
Log.warn("[weather] Deprecation warning: Please consider updating showHumidity to the new style (config string).");
|
||||
this.config.showHumidity = this.config.showHumidity ? "wind" : "none";
|
||||
}
|
||||
|
||||
// Initialize the weather provider.
|
||||
this.weatherProvider = WeatherProvider.initialize(this.config.weatherProvider, this);
|
||||
@@ -100,7 +99,7 @@ Module.register("weather", {
|
||||
},
|
||||
|
||||
// Override notification handler.
|
||||
notificationReceived: function (notification, payload, sender) {
|
||||
notificationReceived (notification, payload, sender) {
|
||||
if (notification === "CALENDAR_EVENTS") {
|
||||
const senderClasses = sender.data.classes.toLowerCase().split(" ");
|
||||
if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
|
||||
@@ -119,11 +118,13 @@ Module.register("weather", {
|
||||
} else if (notification === "INDOOR_HUMIDITY") {
|
||||
this.indoorHumidity = this.roundValue(payload);
|
||||
this.updateDom(300);
|
||||
} else if (notification === "CURRENT_WEATHER_OVERRIDE" && this.config.allowOverrideNotification) {
|
||||
this.weatherProvider.notificationReceived(payload);
|
||||
}
|
||||
},
|
||||
|
||||
// Select the template depending on the display type.
|
||||
getTemplate: function () {
|
||||
getTemplate () {
|
||||
switch (this.config.type.toLowerCase()) {
|
||||
case "current":
|
||||
return "current.njk";
|
||||
@@ -139,7 +140,7 @@ Module.register("weather", {
|
||||
},
|
||||
|
||||
// Add all the data to the template.
|
||||
getTemplateData: function () {
|
||||
getTemplateData () {
|
||||
const currentData = this.weatherProvider.currentWeather();
|
||||
const forecastData = this.weatherProvider.weatherForecast();
|
||||
|
||||
@@ -159,7 +160,7 @@ Module.register("weather", {
|
||||
},
|
||||
|
||||
// What to do when the weather provider has new information available?
|
||||
updateAvailable: function () {
|
||||
updateAvailable () {
|
||||
Log.log("New weather information available.");
|
||||
this.updateDom(0);
|
||||
this.scheduleUpdate();
|
||||
@@ -178,7 +179,7 @@ Module.register("weather", {
|
||||
this.sendNotification("WEATHER_UPDATED", notificationPayload);
|
||||
},
|
||||
|
||||
scheduleUpdate: function (delay = null) {
|
||||
scheduleUpdate (delay = null) {
|
||||
let nextLoad = this.config.updateInterval;
|
||||
if (delay !== null && delay >= 0) {
|
||||
nextLoad = delay;
|
||||
@@ -202,13 +203,13 @@ Module.register("weather", {
|
||||
}, nextLoad);
|
||||
},
|
||||
|
||||
roundValue: function (temperature) {
|
||||
roundValue (temperature) {
|
||||
const decimals = this.config.roundTemp ? 0 : 1;
|
||||
const roundValue = parseFloat(temperature).toFixed(decimals);
|
||||
return roundValue === "-0" ? 0 : roundValue;
|
||||
},
|
||||
|
||||
addFilters() {
|
||||
addFilters () {
|
||||
this.nunjucksEnvironment().addFilter(
|
||||
"formatTime",
|
||||
function (date) {
|
||||
@@ -232,7 +233,7 @@ Module.register("weather", {
|
||||
}
|
||||
}
|
||||
} else if (type === "precip") {
|
||||
if (value === null || isNaN(value) || value === 0 || value.toFixed(2) === "0.00") {
|
||||
if (value === null || isNaN(value)) {
|
||||
formattedValue = "";
|
||||
} else {
|
||||
formattedValue = WeatherUtils.convertPrecipitationUnit(value, valueUnit, this.config.units);
|
||||
|
@@ -1,25 +1,14 @@
|
||||
/* global SunCalc, WeatherUtils */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is the blueprint for a day which includes weather information.
|
||||
*
|
||||
* Currently this is focused on the information which is necessary for the current weather.
|
||||
* As soon as we start implementing the forecast, mode properties will be added.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external Moment
|
||||
*/
|
||||
class WeatherObject {
|
||||
|
||||
/**
|
||||
* Constructor for a WeatherObject
|
||||
*/
|
||||
constructor() {
|
||||
constructor () {
|
||||
this.date = null;
|
||||
this.windSpeed = null;
|
||||
this.windFromDirection = null;
|
||||
@@ -36,7 +25,7 @@ class WeatherObject {
|
||||
this.feelsLikeTemp = null;
|
||||
}
|
||||
|
||||
cardinalWindDirection() {
|
||||
cardinalWindDirection () {
|
||||
if (this.windFromDirection > 11.25 && this.windFromDirection <= 33.75) {
|
||||
return "NNE";
|
||||
} else if (this.windFromDirection > 33.75 && this.windFromDirection <= 56.25) {
|
||||
@@ -79,11 +68,11 @@ class WeatherObject {
|
||||
* action for. Useful only in tests, defaults to the current time.
|
||||
* @returns {string} "sunset" or "sunrise"
|
||||
*/
|
||||
nextSunAction(date = moment()) {
|
||||
nextSunAction (date = moment()) {
|
||||
return date.isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise";
|
||||
}
|
||||
|
||||
feelsLike() {
|
||||
feelsLike () {
|
||||
if (this.feelsLikeTemp) {
|
||||
return this.feelsLikeTemp;
|
||||
}
|
||||
@@ -94,7 +83,7 @@ class WeatherObject {
|
||||
* Checks if the weatherObject is at dayTime.
|
||||
* @returns {boolean} true if it is at dayTime
|
||||
*/
|
||||
isDayTime() {
|
||||
isDayTime () {
|
||||
const now = !this.date ? moment() : this.date;
|
||||
return now.isBetween(this.sunrise, this.sunset, undefined, "[]");
|
||||
}
|
||||
@@ -106,7 +95,7 @@ class WeatherObject {
|
||||
* @param {number} lat latitude
|
||||
* @param {number} lon longitude
|
||||
*/
|
||||
updateSunTime(lat, lon) {
|
||||
updateSunTime (lat, lon) {
|
||||
const now = !this.date ? new Date() : this.date.toDate();
|
||||
const times = SunCalc.getTimes(now, lat, lon);
|
||||
this.sunrise = moment(times.sunrise);
|
||||
@@ -120,7 +109,7 @@ class WeatherObject {
|
||||
* Especially 'moment' object is not immutable, so original 'date', 'sunrise', 'sunset' could be corrupted or changed by other modules.
|
||||
* @returns {object} plained object clone of original weatherObject
|
||||
*/
|
||||
simpleClone() {
|
||||
simpleClone () {
|
||||
const toFlat = ["date", "sunrise", "sunset"];
|
||||
let clone = { ...this };
|
||||
for (const prop of toFlat) {
|
||||
|
@@ -1,13 +1,6 @@
|
||||
/* global Class, performWebRequest */
|
||||
/* global Class, performWebRequest, OverrideWrapper */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is the blueprint for a weather provider.
|
||||
*/
|
||||
// This class is the blueprint for a weather provider.
|
||||
const WeatherProvider = Class.extend({
|
||||
// Weather Provider Properties
|
||||
providerName: null,
|
||||
@@ -30,84 +23,84 @@ const WeatherProvider = Class.extend({
|
||||
// All the following methods can be overwritten, although most are good as they are.
|
||||
|
||||
// Called when a weather provider is initialized.
|
||||
init: function (config) {
|
||||
init (config) {
|
||||
this.config = config;
|
||||
Log.info(`Weather provider: ${this.providerName} initialized.`);
|
||||
},
|
||||
|
||||
// Called to set the config, this config is the same as the weather module's config.
|
||||
setConfig: function (config) {
|
||||
setConfig (config) {
|
||||
this.config = config;
|
||||
Log.info(`Weather provider: ${this.providerName} config set.`, this.config);
|
||||
},
|
||||
|
||||
// Called when the weather provider is about to start.
|
||||
start: function () {
|
||||
start () {
|
||||
Log.info(`Weather provider: ${this.providerName} started.`);
|
||||
},
|
||||
|
||||
// This method should start the API request to fetch the current weather.
|
||||
// This method should definitely be overwritten in the provider.
|
||||
fetchCurrentWeather: function () {
|
||||
fetchCurrentWeather () {
|
||||
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchCurrentWeather method.`);
|
||||
},
|
||||
|
||||
// This method should start the API request to fetch the weather forecast.
|
||||
// This method should definitely be overwritten in the provider.
|
||||
fetchWeatherForecast: function () {
|
||||
fetchWeatherForecast () {
|
||||
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherForecast method.`);
|
||||
},
|
||||
|
||||
// This method should start the API request to fetch the weather hourly.
|
||||
// This method should definitely be overwritten in the provider.
|
||||
fetchWeatherHourly: function () {
|
||||
fetchWeatherHourly () {
|
||||
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherHourly method.`);
|
||||
},
|
||||
|
||||
// This returns a WeatherDay object for the current weather.
|
||||
currentWeather: function () {
|
||||
currentWeather () {
|
||||
return this.currentWeatherObject;
|
||||
},
|
||||
|
||||
// This returns an array of WeatherDay objects for the weather forecast.
|
||||
weatherForecast: function () {
|
||||
weatherForecast () {
|
||||
return this.weatherForecastArray;
|
||||
},
|
||||
|
||||
// This returns an object containing WeatherDay object(s) depending on the type of call.
|
||||
weatherHourly: function () {
|
||||
weatherHourly () {
|
||||
return this.weatherHourlyArray;
|
||||
},
|
||||
|
||||
// This returns the name of the fetched location or an empty string.
|
||||
fetchedLocation: function () {
|
||||
fetchedLocation () {
|
||||
return this.fetchedLocationName || "";
|
||||
},
|
||||
|
||||
// Set the currentWeather and notify the delegate that new information is available.
|
||||
setCurrentWeather: function (currentWeatherObject) {
|
||||
setCurrentWeather (currentWeatherObject) {
|
||||
// We should check here if we are passing a WeatherDay
|
||||
this.currentWeatherObject = currentWeatherObject;
|
||||
},
|
||||
|
||||
// Set the weatherForecastArray and notify the delegate that new information is available.
|
||||
setWeatherForecast: function (weatherForecastArray) {
|
||||
setWeatherForecast (weatherForecastArray) {
|
||||
// We should check here if we are passing a WeatherDay
|
||||
this.weatherForecastArray = weatherForecastArray;
|
||||
},
|
||||
|
||||
// Set the weatherHourlyArray and notify the delegate that new information is available.
|
||||
setWeatherHourly: function (weatherHourlyArray) {
|
||||
setWeatherHourly (weatherHourlyArray) {
|
||||
this.weatherHourlyArray = weatherHourlyArray;
|
||||
},
|
||||
|
||||
// Set the fetched location name.
|
||||
setFetchedLocation: function (name) {
|
||||
setFetchedLocation (name) {
|
||||
this.fetchedLocationName = name;
|
||||
},
|
||||
|
||||
// Notify the delegate that new weather is available.
|
||||
updateAvailable: function () {
|
||||
updateAvailable () {
|
||||
this.delegate.updateAvailable(this);
|
||||
},
|
||||
|
||||
@@ -119,7 +112,7 @@ const WeatherProvider = Class.extend({
|
||||
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||
* @returns {Promise} resolved when the fetch is done
|
||||
*/
|
||||
fetchData: async function (url, type = "json", requestHeaders = undefined, expectedResponseHeaders = undefined) {
|
||||
async fetchData (url, type = "json", requestHeaders = undefined, expectedResponseHeaders = undefined) {
|
||||
const mockData = this.config.mockData;
|
||||
if (mockData) {
|
||||
const data = mockData.substring(1, mockData.length - 1);
|
||||
@@ -164,5 +157,9 @@ WeatherProvider.initialize = function (providerIdentifier, delegate) {
|
||||
provider.providerName = pi;
|
||||
}
|
||||
|
||||
if (config.allowOverrideNotification) {
|
||||
return new OverrideWrapper(provider);
|
||||
}
|
||||
|
||||
return provider;
|
||||
};
|
||||
|
@@ -1,16 +1,11 @@
|
||||
/* MagicMirror²
|
||||
* Weather Util Methods
|
||||
*
|
||||
* By Rejas
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const WeatherUtils = {
|
||||
|
||||
/**
|
||||
* Convert wind (from m/s) to beaufort scale
|
||||
* @param {number} speedInMS the windspeed you want to convert
|
||||
* @returns {number} the speed in beaufort
|
||||
*/
|
||||
beaufortWindSpeed(speedInMS) {
|
||||
beaufortWindSpeed (speedInMS) {
|
||||
const windInKmh = this.convertWind(speedInMS, "kmh");
|
||||
const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
|
||||
for (const [index, speed] of speeds.entries()) {
|
||||
@@ -29,7 +24,7 @@ const WeatherUtils = {
|
||||
* @param {string} outputUnit - The unit system (imperial/metric) the return value should have.
|
||||
* @returns {string} - A string with tha value and a unit postfix.
|
||||
*/
|
||||
convertPrecipitationUnit(value, valueUnit, outputUnit) {
|
||||
convertPrecipitationUnit (value, valueUnit, outputUnit) {
|
||||
if (valueUnit === "%") return `${value.toFixed(0)} ${valueUnit}`;
|
||||
|
||||
let convertedValue = value;
|
||||
@@ -52,7 +47,7 @@ const WeatherUtils = {
|
||||
* @param {string} unit can be 'imperial' or 'metric'
|
||||
* @returns {number} the converted temperature
|
||||
*/
|
||||
convertTemp(tempInC, unit) {
|
||||
convertTemp (tempInC, unit) {
|
||||
return unit === "imperial" ? tempInC * 1.8 + 32 : tempInC;
|
||||
},
|
||||
|
||||
@@ -63,7 +58,7 @@ const WeatherUtils = {
|
||||
* or 'metric' (mps)
|
||||
* @returns {number} the converted windspeed
|
||||
*/
|
||||
convertWind(windInMS, unit) {
|
||||
convertWind (windInMS, unit) {
|
||||
switch (unit) {
|
||||
case "beaufort":
|
||||
return this.beaufortWindSpeed(windInMS);
|
||||
@@ -82,7 +77,7 @@ const WeatherUtils = {
|
||||
/*
|
||||
* Convert the wind direction cardinal to value
|
||||
*/
|
||||
convertWindDirection(windDirection) {
|
||||
convertWindDirection (windDirection) {
|
||||
const windCardinals = {
|
||||
N: 0,
|
||||
NNE: 22,
|
||||
@@ -105,15 +100,15 @@ const WeatherUtils = {
|
||||
return windCardinals.hasOwnProperty(windDirection) ? windCardinals[windDirection] : null;
|
||||
},
|
||||
|
||||
convertWindToMetric(mph) {
|
||||
convertWindToMetric (mph) {
|
||||
return mph / 2.2369362920544;
|
||||
},
|
||||
|
||||
convertWindToMs(kmh) {
|
||||
convertWindToMs (kmh) {
|
||||
return kmh * 0.27777777777778;
|
||||
},
|
||||
|
||||
calculateFeelsLike(temperature, windSpeed, humidity) {
|
||||
calculateFeelsLike (temperature, windSpeed, humidity) {
|
||||
const windInMph = this.convertWind(windSpeed, "imperial");
|
||||
const tempInF = this.convertTemp(temperature, "imperial");
|
||||
let feelsLike = tempInF;
|
||||
@@ -121,16 +116,16 @@ const WeatherUtils = {
|
||||
if (windInMph > 3 && tempInF < 50) {
|
||||
feelsLike = Math.round(35.74 + 0.6215 * tempInF - 35.75 * Math.pow(windInMph, 0.16) + 0.4275 * tempInF * Math.pow(windInMph, 0.16));
|
||||
} else if (tempInF > 80 && humidity > 40) {
|
||||
feelsLike =
|
||||
-42.379 +
|
||||
2.04901523 * tempInF +
|
||||
10.14333127 * humidity -
|
||||
0.22475541 * tempInF * humidity -
|
||||
6.83783 * Math.pow(10, -3) * tempInF * tempInF -
|
||||
5.481717 * Math.pow(10, -2) * humidity * humidity +
|
||||
1.22874 * Math.pow(10, -3) * tempInF * tempInF * humidity +
|
||||
8.5282 * Math.pow(10, -4) * tempInF * humidity * humidity -
|
||||
1.99 * Math.pow(10, -6) * tempInF * tempInF * humidity * humidity;
|
||||
feelsLike
|
||||
= -42.379
|
||||
+ 2.04901523 * tempInF
|
||||
+ 10.14333127 * humidity
|
||||
- 0.22475541 * tempInF * humidity
|
||||
- 6.83783 * Math.pow(10, -3) * tempInF * tempInF
|
||||
- 5.481717 * Math.pow(10, -2) * humidity * humidity
|
||||
+ 1.22874 * Math.pow(10, -3) * tempInF * tempInF * humidity
|
||||
+ 8.5282 * Math.pow(10, -4) * tempInF * humidity * humidity
|
||||
- 1.99 * Math.pow(10, -6) * tempInF * tempInF * humidity * humidity;
|
||||
}
|
||||
|
||||
return ((feelsLike - 32) * 5) / 9;
|
||||
|
15127
package-lock.json
generated
15127
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
133
package.json
133
package.json
@@ -1,7 +1,27 @@
|
||||
{
|
||||
"name": "magicmirror",
|
||||
"version": "2.24.0",
|
||||
"version": "2.28.0",
|
||||
"description": "The open source modular smart mirror platform.",
|
||||
"keywords": [
|
||||
"magic mirror",
|
||||
"magicmirror",
|
||||
"smart mirror",
|
||||
"mirror UI",
|
||||
"modular"
|
||||
],
|
||||
"homepage": "https://magicmirror.builders",
|
||||
"bugs": {
|
||||
"url": "https://github.com/MagicMirrorOrg/MagicMirror/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MagicMirrorOrg/MagicMirror"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Michael Teeuw",
|
||||
"contributors": [
|
||||
"https://github.com/MagicMirrorOrg/MagicMirror/graphs/contributors"
|
||||
],
|
||||
"main": "js/electron.js",
|
||||
"scripts": {
|
||||
"start": "DISPLAY=\"${DISPLAY:=:0}\" ./node_modules/.bin/electron js/electron.js",
|
||||
@@ -18,83 +38,68 @@
|
||||
"test:e2e": "NODE_ENV=test jest --selectProjects e2e -i --forceExit",
|
||||
"test:unit": "NODE_ENV=test jest --selectProjects unit",
|
||||
"test:prettier": "prettier . --check",
|
||||
"test:js": "eslint 'js/**/*.js' 'modules/default/**/*.js' 'clientonly/*.js' 'serveronly/*.js' 'translations/*.js' 'vendor/*.js' 'tests/**/*.js' 'config/*' --config .eslintrc.json",
|
||||
"test:js": "eslint .",
|
||||
"test:css": "stylelint 'css/main.css' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.css' --config .stylelintrc.json",
|
||||
"test:calendar": "node ./modules/default/calendar/debug.js",
|
||||
"config:check": "node js/check_config.js",
|
||||
"lint:prettier": "prettier . --write",
|
||||
"lint:js": "eslint 'js/**/*.js' 'modules/default/**/*.js' 'clientonly/*.js' 'serveronly/*.js' 'translations/*.js' 'vendor/*.js' 'tests/**/*.js' 'config/*' --config .eslintrc.json --fix",
|
||||
"lint:js": "eslint . --fix",
|
||||
"lint:css": "stylelint 'css/main.css' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.css' --config .stylelintrc.json --fix",
|
||||
"lint:staged": "pretty-quick --staged",
|
||||
"prepare": "[ -f node_modules/.bin/husky ] && husky install || echo no husky installed."
|
||||
"lint:staged": "lint-staged",
|
||||
"prepare": "[ -f node_modules/.bin/husky ] && husky || echo no husky installed."
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/MichMich/MagicMirror.git"
|
||||
},
|
||||
"keywords": [
|
||||
"magic mirror",
|
||||
"magicmirror",
|
||||
"smart mirror",
|
||||
"mirror UI",
|
||||
"modular"
|
||||
],
|
||||
"author": "Michael Teeuw",
|
||||
"contributors": [
|
||||
"https://github.com/MichMich/MagicMirror/graphs/contributors"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/MichMich/MagicMirror/issues"
|
||||
},
|
||||
"homepage": "https://magicmirror.builders",
|
||||
"devDependencies": {
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jest": "^27.2.2",
|
||||
"eslint-plugin-jsdoc": "^46.4.2",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.5.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"playwright": "^1.35.1",
|
||||
"prettier": "^2.8.8",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"sinon": "^15.2.0",
|
||||
"stylelint": "^15.9.0",
|
||||
"stylelint-config-standard": "^33.0.0",
|
||||
"stylelint-prettier": "^3.0.0",
|
||||
"suncalc": "^1.9.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"electron": "^25.2.0"
|
||||
"lint-staged": {
|
||||
"*": "prettier --write",
|
||||
"*.js": "eslint --fix",
|
||||
"*.css": "stylelint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"colors": "^1.4.0",
|
||||
"console-stamp": "^3.1.1",
|
||||
"digest-fetch": "^2.0.3",
|
||||
"ajv": "^8.16.0",
|
||||
"ansis": "^3.2.0",
|
||||
"console-stamp": "^3.1.2",
|
||||
"envsub": "^4.1.0",
|
||||
"eslint": "^8.43.0",
|
||||
"express": "^4.18.2",
|
||||
"express-ipfilter": "^1.3.1",
|
||||
"eslint": "^8.57.0",
|
||||
"express": "^4.19.2",
|
||||
"express-ipfilter": "^1.3.2",
|
||||
"feedme": "^2.0.2",
|
||||
"helmet": "^7.0.0",
|
||||
"helmet": "^7.1.0",
|
||||
"html-to-text": "^9.0.5",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"luxon": "^1.28.1",
|
||||
"module-alias": "^2.2.3",
|
||||
"moment": "^2.29.4",
|
||||
"node-fetch": "^2.6.12",
|
||||
"node-ical": "^0.16.1",
|
||||
"socket.io": "^4.7.1"
|
||||
"moment": "^2.30.1",
|
||||
"node-ical": "^0.18.0",
|
||||
"pm2": "^5.4.1",
|
||||
"socket.io": "^4.7.5",
|
||||
"suncalc": "^1.9.0",
|
||||
"systeminformation": "^5.22.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@stylistic/eslint-plugin": "^1.8.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jest": "^28.6.0",
|
||||
"eslint-plugin-jsdoc": "^48.5.0",
|
||||
"eslint-plugin-package-json": "^0.15.0",
|
||||
"eslint-plugin-unicorn": "^54.0.0",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"husky": "^9.0.11",
|
||||
"jest": "^29.7.0",
|
||||
"jsdom": "^24.1.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"playwright": "^1.45.0",
|
||||
"prettier": "^3.3.2",
|
||||
"sinon": "^18.0.0",
|
||||
"stylelint": "^16.6.1",
|
||||
"stylelint-config-standard": "^36.0.1",
|
||||
"stylelint-prettier": "^5.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"electron": "^31.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"_moduleAliases": {
|
||||
"node_helper": "js/node_helper.js",
|
||||
"logger": "js/logger.js",
|
||||
"fetch": "js/fetch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"logger": "js/logger.js"
|
||||
}
|
||||
}
|
||||
|
@@ -4,5 +4,5 @@ const Log = require("../js/logger");
|
||||
app.start().then((config) => {
|
||||
const bindAddress = config.address ? config.address : "localhost";
|
||||
const httpType = config.useHttps ? "https" : "http";
|
||||
Log.log(`\nReady to go! Please point your browser to: ${httpType}://${bindAddress}:${config.port}`);
|
||||
Log.info(`\n>>> Ready to go! Please point your browser to: ${httpType}://${bindAddress}:${config.port} <<<`);
|
||||
});
|
||||
|
@@ -1,8 +1,3 @@
|
||||
/* MagicMirror² Test default config for modules
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
exports.configFactory = (options) => {
|
||||
return Object.assign(
|
||||
{
|
||||
|
@@ -1,10 +1,6 @@
|
||||
/* MagicMirror² Test config sample ipWhitelist
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
|
||||
ipWhitelist: []
|
||||
ipWhitelist: [],
|
||||
port: 8282
|
||||
});
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
|
@@ -1,8 +1,3 @@
|
||||
/* MagicMirror² Test config sample module alert
|
||||
*
|
||||
* By rejas
|
||||
* MIT Licensed.
|
||||
*/
|
||||
let config = {
|
||||
modules: [
|
||||
{
|
||||
|
@@ -1,8 +1,3 @@
|
||||
/* MagicMirror² Test config default calendar with auth by default
|
||||
*
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
let config = {
|
||||
timeFormat: 12,
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user