mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-08-16 10:55:33 +00:00
Compare commits
218 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
62b0f7f26e | ||
|
8e0b8468d3 | ||
|
39a614e0de | ||
|
9c9a5359dd | ||
|
c24de64d77 | ||
|
94c3c699e8 | ||
|
53fc814ff8 | ||
|
5ea8a3469a | ||
|
118e21238c | ||
|
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 | ||
|
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 +0,0 @@
|
||||
modules/default/calendar/vendor/*
|
@@ -1,83 +0,0 @@
|
||||
{
|
||||
"extends": ["eslint:recommended", "plugin:@stylistic/all-extends", "plugin:import/recommended", "plugin:jest/recommended", "plugin:jsdoc/recommended"],
|
||||
"plugins": [],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2023": true,
|
||||
"jest/globals": true,
|
||||
"node": true
|
||||
},
|
||||
"globals": {
|
||||
"config": true,
|
||||
"Log": true,
|
||||
"MM": true,
|
||||
"Module": true,
|
||||
"moment": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2023,
|
||||
"ecmaFeatures": {
|
||||
"globalReturn": true
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
137
.github/CODE_OF_CONDUCT.md
vendored
Normal file
137
.github/CODE_OF_CONDUCT.md
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement:
|
||||
Contact [Rejas](https://forum.magicmirror.builders/user/rejas),
|
||||
[Karsten](https://forum.magicmirror.builders/user/karsten13),
|
||||
[Sam](https://forum.magicmirror.builders/user/sdetweil) or
|
||||
[Kristjan](https://forum.magicmirror.builders/user/kristjanesperanto)
|
||||
via private message in the forum.
|
||||
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
52
.github/CONTRIBUTING.md
vendored
52
.github/CONTRIBUTING.md
vendored
@@ -6,55 +6,33 @@ We hold our code to standard, and these standards are documented below.
|
||||
|
||||
## Linters
|
||||
|
||||
We use prettier for automatic linting of all our files: `npm run lint:prettier`.
|
||||
We use [prettier](https://prettier.io/) for automatic formatting a lot all our files. The configuration is in our `prettier.config.mjs` file.
|
||||
|
||||
To run prettier, use `node --run lint:prettier`.
|
||||
|
||||
### JavaScript: Run ESLint
|
||||
|
||||
We use [ESLint](https://eslint.org) on our JavaScript files.
|
||||
We use [ESLint](https://eslint.org) to lint our JavaScript files. The configuration is in our `eslint.config.mjs` file.
|
||||
|
||||
Our ESLint configuration is in our `.eslintrc.json` and `.eslintignore` files.
|
||||
|
||||
To run ESLint, use `npm run lint:js`.
|
||||
To run ESLint, use `node --run lint:js`.
|
||||
|
||||
### CSS: Run StyleLint
|
||||
|
||||
We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is in our `.stylelintrc` file.
|
||||
We use [StyleLint](https://stylelint.io) to lint our CSS. The configuration is in our `stylelint.config.mjs` file.
|
||||
|
||||
To run StyleLint, use `npm run lint:css`.
|
||||
To run StyleLint, use `node --run lint:css`.
|
||||
|
||||
### Markdown: Run markdownlint
|
||||
|
||||
We use [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) to lint our markdown files. The configuration is in our `.markdownlint.json` file.
|
||||
|
||||
To run markdownlint, use `node --run lint:markdown`.
|
||||
|
||||
## Testing
|
||||
|
||||
We use [Jest](https://jestjs.io) for JavaScript testing.
|
||||
|
||||
To run all tests, use `npm run test`.
|
||||
To run all tests, use `node --run test`.
|
||||
|
||||
The specific test commands are defined in `package.json`.
|
||||
So you can also run the specific tests with other commands, e.g. `npm run test:unit` or `npx jest tests/e2e/env_spec.js`.
|
||||
|
||||
## Submitting Issues
|
||||
|
||||
Please only submit reproducible issues.
|
||||
|
||||
If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt)
|
||||
|
||||
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
|
||||
|
||||
When submitting a new issue, please supply the following information:
|
||||
|
||||
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX).
|
||||
|
||||
**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.
|
||||
|
||||
**Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem.
|
||||
|
||||
**Steps to Reproduce**: List the step by step process to reproduce the issue.
|
||||
|
||||
**Expected Results**: Describe what you expected to see.
|
||||
|
||||
**Actual Results**: Describe what you actually saw.
|
||||
|
||||
**Configuration**: What does the used config.js file look like? Don't forget to remove any sensitive information!
|
||||
|
||||
**Additional Notes**: Provide any other relevant notes not previously mentioned. This is optional.
|
||||
So you can also run the specific tests with other commands, e.g. `node --run test:unit` or `npx jest tests/e2e/env_spec.js`.
|
||||
|
52
.github/ISSUE_TEMPLATE.md
vendored
52
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,52 +0,0 @@
|
||||
Hello and thank you for opening an issue.
|
||||
|
||||
**⚠️ Please make sure that you have read the following lines before submitting your Issue:**
|
||||
|
||||
## I'm not sure if this is a bug
|
||||
|
||||
If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt)
|
||||
|
||||
## I'm having troubles installing or configuring MagicMirror
|
||||
|
||||
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
|
||||
|
||||
A common problem is that your config file could be invalid. Please run in your MagicMirror² directory: `npm run config:check` and see if it reports an error.
|
||||
|
||||
## I found a bug in the MagicMirror² installer
|
||||
|
||||
If you are facing an issue or found a bug while trying to install MagicMirror² via the installer please report it in the respective GitHub repository:
|
||||
[https://github.com/sdetweil/MagicMirror_scripts](https://github.com/sdetweil/MagicMirror_scripts)
|
||||
|
||||
## I found a bug in the MagicMirror² Docker image
|
||||
|
||||
If you are facing an issue or found a bug while running MagicMirror² inside a Docker container please create an issue in the corresponding repository:
|
||||
[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
|
||||
|
||||
Please make sure to only submit reproducible issues. You can safely remove everything above the dividing line.
|
||||
When submitting a new issue, please supply the following information:
|
||||
|
||||
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX).
|
||||
|
||||
**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.
|
||||
|
||||
**Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem.
|
||||
|
||||
**Steps to Reproduce**: List the step by step process to reproduce the issue.
|
||||
|
||||
**Expected Results**: Describe what you expected to see.
|
||||
|
||||
**Actual Results**: Describe what you actually saw.
|
||||
|
||||
**Configuration**: What does the used config.js file look like? Don't forget to remove any sensitive information!
|
||||
|
||||
**Additional Notes**: Provide any other relevant notes not previously mentioned. This is optional.
|
154
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
154
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
name: 🐛 Report a problem
|
||||
description: Report an issue with MagicMirror² 🚨
|
||||
title: "[Bug] {{ brief description }}"
|
||||
labels:
|
||||
- bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for reporting a bug! Please fill in the following template to help us reproduce the issue.
|
||||
Please only submit reproducible issues. If you're not sure if it's a real bug or if it's just you, please open a topic on the forum.
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
Please tell us about how your MagicMirror² is set up.
|
||||
|
||||
Optimal would be the systeminformation from the logs, which looks like this:
|
||||
```bash
|
||||
[2025-01-14 20:05:03.529] [INFO] System information:
|
||||
### SYSTEM: manufacturer: Raspberry Pi Foundation; model: Raspberry Pi 4 Model B Rev 1.5; virtual: false
|
||||
### OS: platform: linux; distro: Debian GNU/Linux; release: 12; arch: arm64; kernel: 6.1.21-v8+
|
||||
### VERSIONS: electron: 31.2.1; used node: 20.15.0; installed node: 22.4.1; npm: 10.8.1; pm2:
|
||||
### OTHER: timeZone: Europe/Berlin; ELECTRON_ENABLE_GPU: undefined
|
||||
```
|
||||
|
||||
If you can't provide this information, please provide the following:
|
||||
- MagicMirror² version: Can be found in the `package.json` file. Please use the latest version before reporting a bug.
|
||||
- Node version: Run `node -v` to find out. Make sure it's version 20 or later (recommended is 22).
|
||||
- npm version: Run `npm -v` to find out.
|
||||
- Platform: Are you using a Raspberry Pi (2/3/4/5), Windows, Mac, Linux, Docker, or something else?
|
||||
value: |
|
||||
MagicMirror² version:
|
||||
Node version:
|
||||
npm version:
|
||||
Platform:
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: start-option
|
||||
attributes:
|
||||
label: Which start option are you using?
|
||||
description: |
|
||||
Please keep in mind that some problems are specific to certain start options.
|
||||
options:
|
||||
- "node --run start"
|
||||
- "node --run start:wayland"
|
||||
- "node --run start:windows"
|
||||
- "node --run start:x11"
|
||||
- "node --run server"
|
||||
- "node clientonly --address ... --port ..."
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: pm2
|
||||
attributes:
|
||||
label: Are you using PM2?
|
||||
options:
|
||||
- "No"
|
||||
- "Yes"
|
||||
- "I don't know"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: module
|
||||
attributes:
|
||||
label: Module
|
||||
description: |
|
||||
If the issue is related to a specific module, please provide the name of the module.
|
||||
Note: Please don't report issues with 3rd party modules here. Report them on the module's repository.
|
||||
options:
|
||||
- "alert"
|
||||
- "calendar"
|
||||
- "clock"
|
||||
- "compliments"
|
||||
- "helloworld"
|
||||
- "newsfeed"
|
||||
- "updatenotification"
|
||||
- "weather"
|
||||
- type: checkboxes
|
||||
id: module-disabled
|
||||
attributes:
|
||||
label: Have you tried disabling other modules?
|
||||
options:
|
||||
- label: "Yes"
|
||||
- label: "No"
|
||||
- type: checkboxes
|
||||
id: search
|
||||
attributes:
|
||||
label: Have you searched if someone else has already reported the issue on the forum or in the issues?
|
||||
options:
|
||||
- label: "Yes"
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: What did you do?
|
||||
description: |
|
||||
Please include a *minimal* reproduction case. List the step by step process to reproduce the issue.
|
||||
You can use Markdown in this field.
|
||||
value: |
|
||||
<details>
|
||||
<summary>Configuration</summary>
|
||||
|
||||
```
|
||||
<!-- Paste your configuration here. Don't forget to remove any sensitive information! -->
|
||||
```
|
||||
</details>
|
||||
|
||||
```js
|
||||
<!-- Paste relevant code here -->
|
||||
```
|
||||
|
||||
Steps to reproduce the issue:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expectation
|
||||
attributes:
|
||||
label: What did you expect to happen?
|
||||
description: |
|
||||
You can use Markdown in this field.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: lint-output
|
||||
attributes:
|
||||
label: What actually happened?
|
||||
description: |
|
||||
Please copy-paste relevant log output or error messages.
|
||||
You can use Markdown in this field.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: comments
|
||||
attributes:
|
||||
label: Additional comments
|
||||
description: |
|
||||
Is there anything else that's important for the team to know?
|
||||
Fill out all fields and provide as much information as possible.
|
||||
Adding screenshots might help us understand your problem better.
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Participation
|
||||
options:
|
||||
- label: "I am willing to submit a pull request for this change."
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please **do not** open a pull request until this issue has been accepted by the team.
|
41
.github/ISSUE_TEMPLATE/change_request.yml
vendored
Normal file
41
.github/ISSUE_TEMPLATE/change_request.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: 🔀 Request a change
|
||||
description: Request a change that is not a bug fix, a feature request or a support request.
|
||||
title: "[Change Request] {{ brief description }}"
|
||||
labels:
|
||||
- enhancement
|
||||
- core
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thanks for requesting a change! Please fill in the following template to help us understand your request.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What problem do you want to solve with this change?
|
||||
description: |
|
||||
Please explain your use case in as much detail as possible.
|
||||
placeholder: |
|
||||
Currently...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What do you think is the correct solution?
|
||||
description: |
|
||||
Please explain how you'd like to change MagicMirror² to address the problem.
|
||||
placeholder: |
|
||||
I'd like MagicMirror² to...
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Participation
|
||||
options:
|
||||
- label: I am willing to submit a pull request for this change.
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please **do not** open a pull request until this issue has been accepted by the team.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional comments
|
||||
description: Is there anything else that's important for the team to know?
|
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 📚 Documentation
|
||||
url: https://github.com/MagicMirrorOrg/MagicMirror-Documentation/issues
|
||||
about: This issue tracker is not for documentation issues. Please file documentation issues on the docs repo.
|
||||
- name: 🤔 Support Question
|
||||
url: https://forum.magicmirror.builders/
|
||||
about: Problems installing or configuring your MagicMirror? Please post your question on the MagicMirror² Forum.
|
||||
- name: 💬 Exchange of ideas
|
||||
url: https://discord.gg/AmGBBwPph5
|
||||
about: This issue tracker is not for general discussion. Please use the Discord channel.
|
||||
- name: 📦 Issues with a 3rd-party module
|
||||
url: https://kristjanesperanto.github.io/MagicMirror-3rd-Party-Modules/
|
||||
about: This issue tracker is not for 3rd-party module issues. Please file 3rd-party module issues on the module's repo.
|
67
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
67
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: 🚀 Feature Request
|
||||
description: Suggest a new feature for MagicMirror² 💡
|
||||
title: "[Feature Request] {{ brief description }}"
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: prerequisites
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
description: Please ensure you have completed all of the following.
|
||||
options:
|
||||
- label: I am running the latest version of MagicMirror², and know that this feature is not available now.
|
||||
required: true
|
||||
- label: I know my issue is not related to a third-party module.
|
||||
required: true
|
||||
- label: I have searched for [existing issues](https://github.com/MagicMirrorOrg/MagicMirror/issues) that already include this feature request, without success.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the Feature Request
|
||||
description: A clear and concise description of what the feature does.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: use-case
|
||||
attributes:
|
||||
label: Describe the Use Case
|
||||
description: A clear and concise use case for what problem this feature would solve.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposed-solution
|
||||
attributes:
|
||||
label: Describe Preferred Solution
|
||||
description: A clear and concise description of how you want this feature to be added to MagicMirror².
|
||||
|
||||
- type: textarea
|
||||
id: alternatives-considered
|
||||
attributes:
|
||||
label: Describe Alternatives
|
||||
description: A clear and concise description of any alternative solutions or features you have considered.
|
||||
|
||||
- type: textarea
|
||||
id: related-code
|
||||
attributes:
|
||||
label: Related Code
|
||||
description: If you are able to illustrate the feature request with an example, please provide a sample here.
|
||||
|
||||
- type: textarea
|
||||
id: additional-information
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: List any other information that is relevant to your issue. Related issues, suggestions on how to implement, Stack Overflow links, forum links, etc.
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Participation
|
||||
options:
|
||||
- label: I am willing to submit a pull request for this change.
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please **do not** open a pull request until this issue has been accepted by the team.
|
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,4 +1,4 @@
|
||||
Hello and thank you for wanting to contribute to the MagicMirror² project
|
||||
Hello and thank you for wanting to contribute to the MagicMirror² project!
|
||||
|
||||
**Please make sure that you have followed these 4 rules before submitting your Pull Request:**
|
||||
|
||||
@@ -10,7 +10,7 @@ Hello and thank you for wanting to contribute to the MagicMirror² project
|
||||
> - What does the pull request accomplish? Use a list if needed.
|
||||
> - If it includes major visual changes please add screenshots.
|
||||
>
|
||||
> 3. Please run `npm run lint:prettier` before submitting so that
|
||||
> 3. Please run `node --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
|
19
.github/dependabot.yaml
vendored
19
.github/dependabot.yaml
vendored
@@ -5,21 +5,16 @@ updates:
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "develop"
|
||||
labels:
|
||||
- "Skip Changelog"
|
||||
- "dependencies"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
target-branch: "develop"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/vendor"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
target-branch: "develop"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fonts"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
target-branch: "develop"
|
||||
labels:
|
||||
- "Skip Changelog"
|
||||
- "dependencies"
|
||||
- "javascript"
|
||||
|
19
.github/stale.yaml
vendored
19
.github/stale.yaml
vendored
@@ -1,19 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- under investigation
|
||||
- pr welcome
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
50
.github/workflows/automated-tests.yaml
vendored
50
.github/workflows/automated-tests.yaml
vendored
@@ -13,29 +13,57 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
code-style-check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Use Node.js"
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: "npm"
|
||||
- name: "Install dependencies"
|
||||
run: |
|
||||
node --run install-mm:dev
|
||||
- name: "Run linter tests"
|
||||
run: |
|
||||
node --run test:prettier
|
||||
node --run test:js
|
||||
node --run test:css
|
||||
node --run test:markdown
|
||||
test:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x, 20.x, 21.x]
|
||||
node-version: [22.14.0, 22.x, 24.x]
|
||||
steps:
|
||||
- name: Install electron dependencies and labwc
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libnss3 libasound2t64 labwc
|
||||
- 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
|
||||
cache: "npm"
|
||||
- name: "Install dependencies"
|
||||
- name: "Install MagicMirror²"
|
||||
run: |
|
||||
npm run install-mm:dev
|
||||
node --run install-mm:dev
|
||||
- name: "Prepare environment for tests"
|
||||
run: |
|
||||
# Fix chrome-sandbox permissions:
|
||||
sudo chown root:root ./node_modules/electron/dist/chrome-sandbox
|
||||
sudo chmod 4755 ./node_modules/electron/dist/chrome-sandbox
|
||||
# Start labwc
|
||||
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 WLR_RENDERER=pixman labwc &
|
||||
touch css/custom.css
|
||||
- name: "Run tests"
|
||||
run: |
|
||||
Xvfb :99 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:99
|
||||
touch css/custom.css
|
||||
npm run test:prettier
|
||||
npm run test:js
|
||||
npm run test:css
|
||||
npm run test
|
||||
export WAYLAND_DISPLAY=wayland-0
|
||||
node --run test
|
||||
|
@@ -15,4 +15,4 @@ jobs:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
32
.github/workflows/electron-rebuild.yaml
vendored
Normal file
32
.github/workflows/electron-rebuild.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: "Electron Rebuild Testing"
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
rebuild:
|
||||
name: Run electron-rebuild
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [22.14.0, 22.x, 24.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: node --run install-mm
|
||||
- name: Install @electron/rebuild
|
||||
run: npm install @electron/rebuild
|
||||
- name: Install node-libgpiod deps
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gpiod libgpiod2 libgpiod-dev
|
||||
- name: Install test library (node-libgpiod) to be rebuilded
|
||||
run: npm install node-libgpiod
|
||||
- name: Run electron-rebuild
|
||||
run: npx electron-rebuild
|
||||
continue-on-error: false
|
@@ -5,7 +5,7 @@
|
||||
name: "Enforce Pull-Request Rules"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||
|
||||
jobs:
|
||||
@@ -19,10 +19,10 @@ 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'."
|
||||
exit 1
|
||||
env:
|
||||
BASE_BRANCH: ${{ github.base_ref }}
|
||||
BASE_BRANCH: ${{ github.event.pull_request.base.ref }}
|
||||
|
31
.github/workflows/spellcheck.yaml
vendored
Normal file
31
.github/workflows/spellcheck.yaml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# This workflow will run a spellcheck on the codebase.
|
||||
# It runs a few days before each release. At 00:00 on day-of-month 27 in March, June, September, and December.
|
||||
|
||||
name: Run Spellcheck
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 27 3,6,9,12 *"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
spellcheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: develop
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
check-latest: true
|
||||
cache: "npm"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
node --run install-mm:dev
|
||||
- name: Run Spellcheck
|
||||
run: node --run test:spelling
|
22
.github/workflows/stale.yaml
vendored
Normal file
22
.github/workflows/stale.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: "Close stale issues and PRs"
|
||||
|
||||
on:
|
||||
workflow_dispatch: # needed for manually running this workflow
|
||||
schedule:
|
||||
- cron: "30 1 * * 6" # every Saturday at 1:30
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-issue-message: "This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions."
|
||||
days-before-issue-stale: 60
|
||||
days-before-issue-close: 7
|
||||
operations-per-run: 100
|
||||
stale-issue-label: "wontfix"
|
||||
exempt-issue-labels: "pinned,security,under investigation,pr welcome,ready (coming with next release)"
|
17
.gitignore
vendored
17
.gitignore
vendored
@@ -10,8 +10,6 @@ coverage
|
||||
.lock-wscript
|
||||
build/Release
|
||||
/node_modules/**/*
|
||||
fonts/node_modules/**/*
|
||||
vendor/node_modules/**/*
|
||||
!/tests/node_modules/**/*
|
||||
jspm_modules
|
||||
.npm
|
||||
@@ -63,8 +61,12 @@ 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
|
||||
!/css/roboto.css
|
||||
!/css/font-awesome.css
|
||||
|
||||
# Ignore users config file but keep the sample.
|
||||
/config/*
|
||||
@@ -79,3 +81,10 @@ Temporary Items
|
||||
*.orig
|
||||
*.rej
|
||||
*.bak
|
||||
|
||||
# Ignore positions file (#3518)
|
||||
js/positions.js
|
||||
|
||||
# Ignore lock files other than package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
@@ -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
|
||||
if command -v npx &> /dev/null; then
|
||||
npx lint-staged
|
||||
fi
|
||||
|
6
.markdownlint.json
Normal file
6
.markdownlint.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"line_length": false,
|
||||
"no-duplicate-heading": false,
|
||||
"no-inline-html": false,
|
||||
"no-trailing-punctuation": false
|
||||
}
|
@@ -1,4 +1,6 @@
|
||||
*.js
|
||||
*.mjs
|
||||
.husky/pre-commit
|
||||
.prettierignore
|
||||
/config
|
||||
/coverage
|
||||
|
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"trailingComma": "none"
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": ["stylelint-config-standard"],
|
||||
"plugins": ["stylelint-prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": true
|
||||
}
|
||||
}
|
524
CHANGELOG.md
524
CHANGELOG.md
@@ -1,11 +1,310 @@
|
||||
# MagicMirror² Change Log
|
||||
# Changelog
|
||||
|
||||
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².
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.26.0] - 01-01-2024
|
||||
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/#donate) With your help we can continue to improve the MagicMirror².
|
||||
|
||||
## [2.32.0] - 2025-07-01
|
||||
|
||||
Thanks to: @bughaver, @bugsounet, @khassel, @KristjanESPERANTO, @plebcity, @rejas, @sdetweil.
|
||||
|
||||
> ⚠️ This release needs nodejs version `v22.14.0 or higher`
|
||||
|
||||
### Added
|
||||
|
||||
- [config] Allow to change module order for final renderer (or dynamically with CSS): Feature `order` in config (#3762)
|
||||
- [clock] Added option 'disableNextEvent' to hide next sun event (#3769)
|
||||
- [clock] Implement short syntax for clock week (#3775)
|
||||
|
||||
### Changed
|
||||
|
||||
- [refactor] Simplify module loading process (#3766)
|
||||
- Use `node --run` instead of `npm run` (#3764) and adapt `start:dev` script (#3773)
|
||||
- [workflow] Run linter and spellcheck with LTS node version (#3767)
|
||||
- [workflow] Split "Run test" step into two steps for more clarity (#3767)
|
||||
- [linter] Review linter setup (#3783)
|
||||
- Fix command to lint markdown in `CONTRIBUTING.md`
|
||||
- Re-activate JSDoc linting and fix linting issues
|
||||
- Refactor ESLint config to use `defineConfig` and `globalIgnores`
|
||||
- Replace `eslint-plugin-import` with `eslint-plugin-import-x`
|
||||
- Switch Stylelint config to flat format and simplify Stylelint scripts
|
||||
- [workflow] Replace Node.js version v23 with v24 (#3770)
|
||||
- [refactor] Replace deprecated constants `fs.F_OK` and `fs.R_OK` (#3789)
|
||||
- [refactor] Replace `ansis` with built-in function `util.styleText` (#3793)
|
||||
- [core] Integrate stuff from `vendor` and `fonts` folders into main `package.json`, simplifies install and maintaining dependencies (#3795, #3805)
|
||||
- [l10n] Complete translations (with the help of translation tools) (#3794)
|
||||
- [refactor] Refactored `calendarfetcherutils` in Calendar module to handle timezones better (#3806)
|
||||
- Removed as many of the date conversions as possible
|
||||
- Use `moment-timezone` when calculating recurring events, this will fix problems from the past with offsets and DST not being handled properly
|
||||
- Added some tests to test the behavior of the refactored methods to make sure the correct event dates are returned
|
||||
- [linter] Enable ESLint rule `no-console` and replace `console` with `Log` in some files (#3810)
|
||||
- [tests] Review and refactor translation tests (#3792)
|
||||
|
||||
### Fixed
|
||||
|
||||
- [fix] Handle spellcheck issues (#3783)
|
||||
- [calendar] fix fullday event rrule until with timezone offset (#3781)
|
||||
- [feat] Add rule `no-undef` in config file validation to fix #3785 (#3786)
|
||||
- [fonts] Fix `roboto.css` to avoid error message `Unknown descriptor 'var(' in @font-face rule.` in firefox console (#3787)
|
||||
- [tests] Fix and refactor e2e test `Same keys` in `translations_spec.js` (#3809)
|
||||
- [tests] Fix e2e tests newsfeed and calendar to exit without open handles (#3817)
|
||||
|
||||
### Updated
|
||||
|
||||
- [core] Update dependencies including electron to v36 (#3774, #3788, #3811, #3804, #3815, #3823)
|
||||
- [core] Update package type to `commonjs`
|
||||
- [logger] Review factory code part: use `switch/case` instead of `if/else if` (#3812)
|
||||
|
||||
## [2.31.0] - 2025-04-01
|
||||
|
||||
Thanks to: @Developer-Incoming, @eltociear, @geraki, @khassel, @KristjanESPERANTO, @MagMar94, @mixasgr, @n8many, @OWL4C, @rejas, @savvadam, @sdetweil.
|
||||
|
||||
> ⚠️ This release needs nodejs version `v22.14.0 or higher`
|
||||
|
||||
### Added
|
||||
|
||||
- Add CSS support to the digital clock hour/minute/second through the use of the classes `clock-hour-digital`, `clock-minute-digital`, and `clock-second-digital`.
|
||||
- Add Arabic (#3719) and Esperanto translation (#3740)
|
||||
- Mark option `secondsColor` as deprecated in clock module.
|
||||
- Add Greek translation to Alerts module.
|
||||
- [newsfeed] Add specific ignoreOlderThan value (override) per feed (#3360)
|
||||
- [weather] Added option Humidity to hourly View
|
||||
- [weather] Added option to hide hourly entries that are Zero, hiding the entire column if empty.
|
||||
- [updatenotification] Added option to iterate over modules directory instead using modules defined in `config.js` (#3739)
|
||||
|
||||
### Changed
|
||||
|
||||
- [core] Starting clientonly now checks for needed env var `WAYLAND_DISPLAY` or `DISPLAY` and starts electron with needed parameters (if both are set Wayland is used) (#3677)
|
||||
- [core] Optimize systeminformation calls and output (#3689)
|
||||
- [core] Add issue templates for feature requests and bug reports (#3695)
|
||||
- [core] Adapt `start:x11:dev` script
|
||||
- [weather/yr] The Yr weather provider now enforces a minimum `updateInterval` of 600 000 ms (10 minutes) to comply with the terms of service. If a lower value is set, it will be automatically increased to this minimum.
|
||||
- [weather/weatherflow] Fixed icons and added hourly support as well as UV, precipitation, and location name support.
|
||||
- [workflow] Run `sudo apt-get update` before installing packages to avoid install errors
|
||||
- [workflow] Exclude issues with label `ready (coming with next release)` from stale job
|
||||
|
||||
### Removed
|
||||
|
||||
### Updated
|
||||
|
||||
- [core] Update requirements and dependencies including electron to v35 and formatting (#3593, #3693, #3717)
|
||||
- [core] Update prettier, ESLint and simplify config
|
||||
- Update Greek translation
|
||||
|
||||
### Fixed
|
||||
|
||||
- [calendar] Fix clipping events being broadcast (#3678)
|
||||
- [tests] Fix Electron tests by running them under new github image ubuntu-24.04, replace xserver with labwc, running under xserver and labwc depending on env variable WAYLAND_DISPLAY is set (#3676)
|
||||
- [calendar] Fix arrayed symbols, #3267, again, add testcase, add testcase for #3678
|
||||
- [weather] Fix wrong weatherCondition name in openmeteo provider which lead to n/a icon (#3691)
|
||||
- [core] Fix wrong port in log message when starting server only (#3696)
|
||||
- [calendar] Fix NewYork event processed on system in Central timezone shows wrong time #3701
|
||||
- [weather/yr] The Yr weather provider is now able to recover from bad API responses instead of freezing (#3296)
|
||||
- [compliments] Fix evening events being shown during the day (#3727)
|
||||
- [weather] Fixed minor spacing issues when using UV Index in Hourly
|
||||
- [workflow] Fix command to run spellcheck
|
||||
|
||||
## [2.30.0] - 2025-01-01
|
||||
|
||||
Thanks to: @xsorifc28, @HeikoGr, @bugsounet, @khassel, @KristjanESPERANTO, @rejas, @sdetweil.
|
||||
|
||||
> ⚠️ This release needs nodejs version `v20` or `v22 or higher`, minimum version is `v20.18.1`
|
||||
|
||||
### Added
|
||||
|
||||
- [core] Add Wayland and Windows start options to `package.json` (#3594)
|
||||
- [docs] Add step for npm publishing in release process (#3595)
|
||||
- [core] Add GitHub workflow to run spellcheck a few days before each release (#3623)
|
||||
- [core] Add test flag to `index.html` to pass to module js for test mode detection (needed by #3630)
|
||||
- [core] Add export on animation names (#3644)
|
||||
- [compliments] Add support for refreshing remote compliments file, and test cases (#3630)
|
||||
- [linter] Re-add `eslint-plugin-import`now that it supports ESLint v9 (#3586)
|
||||
- [linter] Re-activate `eslint-plugin-package-json` to lint `package.json` (#3643)
|
||||
- [linter] Add linting for markdown files (#3646)
|
||||
- [linter] Add some handy ESLint rules (#3665)
|
||||
- [calendar] Add ability to display end date for full date events, where end is not same day (showEnd=true) (#3650)
|
||||
- [core] Add text to the config.js.sample file about the locale variable (#3654, #3655)
|
||||
- [core] Add fetch timeout for all node_helpers (thru undici, forces node 20.18.1 minimum) to help on slower systems. (#3660) (3661)
|
||||
|
||||
### Changed
|
||||
|
||||
- [core] Run code style checks in workflow only once (#3648)
|
||||
- [core] Fix animations export #3644 only on server side (#3649)
|
||||
- [core] Use project URL in fallback config (#3656)
|
||||
- [core] Fix Access Denied crash writing js/positions.js (on synology nas) #3651. new message, MM starts, but no modules showing (#3652)
|
||||
- [linter] Switch to 'npx' for lint-staged in pre-commit hook (#3658)
|
||||
|
||||
### Removed
|
||||
|
||||
- [tests] Remove `node-pty` and `drivelist` from rebuilded test (#3575)
|
||||
- [deps] Remove `@eslint/js` dependency. Already installed with `eslint` in deep (#3636)
|
||||
|
||||
### Updated
|
||||
|
||||
- [repo] Reactivate `stale.yaml` as GitHub action to mark issues as stale after 60 days and close them 7 days later (if no activity) (#3577, #3580, #3581)
|
||||
- [core] Update electron dependency to v32 (test electron rebuild) and all other dependencies too (#3657)
|
||||
- [tests] All test configs have been updated to allow full external access, allowing for easier debugging (especially when running as a container)
|
||||
- [core] Run and test with node 23 (#3588)
|
||||
- [workflow] delete exception `allow-ghsas: GHSA-8hc4-vh64-cxmj` in `dep-review.yaml` (#3659)
|
||||
|
||||
### Fixed
|
||||
|
||||
- [updatenotification] Fix pm2 using detection when pm2 script is inside or outside MagicMirror root folder (#3576) (#3605) (#3626) (#3628)
|
||||
- [core] Fix loading node_helper of modules: avoid black screen, display errors and continue loading with next module (#3578)
|
||||
- [weather] Change default value for weatherEndpoint of provider openweathermap to "/onecall" (#3574)
|
||||
- [tests] Fix electron tests with mock dates, the mock on server side was missing (#3597)
|
||||
- [tests] Fix testcases with hard coded Date.now (#3597)
|
||||
- [core] Fix missing `basePath` where `location.host` is used (#3613)
|
||||
- [compliments] croner library changed filenames used in latest version (#3624)
|
||||
- [linter] Fix ESLint ignore pattern which caused that default modules not to be linted (#3632)
|
||||
- [core] Fix module path in case of sub/sub folder is used and use path.resolve for resolve `moduleFolder` and `defaultModuleFolder` in app.js (#3653)
|
||||
- [calendar] Update to resolve issues #3098 #3144 #3351 #3422 #3443 #3467 #3537 related to timezone changes
|
||||
- [calendar] Fix #3267 (styles array), also fixes event with both exdate AND recurrence(and testcase)
|
||||
- [calendar] Fix showEndsOnlyWithDuration not working, #3598, applies ONLY to full day events
|
||||
- [calendar] Fix showEnd for Full Day events (#3602)
|
||||
- [tests] Suppress "module is not defined" in e2e tests (#3647)
|
||||
- [calendar] Fix #3267 (styles array, really this time!)
|
||||
- [core] Fix #3662 js/positions.js created incorrectly
|
||||
|
||||
## [2.29.0] - 2024-10-01
|
||||
|
||||
Thanks to: @bugsounet, @dkallen78, @jargordon, @khassel, @KristjanESPERANTO, @MarcLandis, @rejas, @ryan-d-williams, @sdetweil, @skpanagiotis.
|
||||
|
||||
> ⚠️ This release needs nodejs version `v20` or `v22`, minimum version is `v20.9.0`
|
||||
|
||||
### Added
|
||||
|
||||
- [compliments] Added support for cron type date/time format entries mm hh DD MM dow (minutes/hours/days/months and day of week) see <https://crontab.cronhub.io> for construction (#3481)
|
||||
- [core] Check config at every start of MagicMirror² (#3450)
|
||||
- [core] Add spelling check (cspell): `npm run test:spelling` and handle spelling issues (#3544)
|
||||
- [core] removed `config.paths.vendor` (could not work because `vendor` is hardcoded in `index.html`), renamed `config.paths.modules` to `config.foreignModulesDir`, added variable `MM_CUSTOMCSS_FILE` which - if set - overrides `config.customCss`, added variable `MM_MODULES_DIR` which - if set - overrides `config.foreignModulesDir`, added test for `MM_MODULES_DIR` (#3530)
|
||||
- [core] elements are now removed from `index.html` when loading script or stylesheet files fails
|
||||
- [core] Added `MODULE_DOM_UPDATED` notification each time the DOM is re-rendered via `updateDom` (#3534)
|
||||
- [tests] added minimal needed node version to tests (currently v20.9.0) to avoid releases with wrong node version info
|
||||
- [tests] Added `node-libgpiod` library to electron-rebuild tests (#3563)
|
||||
|
||||
### Removed
|
||||
|
||||
- [core] removed installer only files (#3492)
|
||||
- [core] removed raspberry object from systeminformation (#3505)
|
||||
- [linter] removed `eslint-plugin-import`, because it doesn't support ESLint v9. We will reenter it later when it does.
|
||||
- [tests] removed `onoff` library from electron-rebuild tests (#3563)
|
||||
|
||||
### Updated
|
||||
|
||||
- [weather] Updated `apiVersion` default from 2.5 to 3.0 (#3424)
|
||||
- [core] Updated dependencies including stylistic-eslint
|
||||
- [core] nail down `node-ical` version to `0.18.0` with exception `allow-ghsas: GHSA-8hc4-vh64-cxmj` in `dep-review.yaml` (which should removed after next `node-ical` update)
|
||||
- [core] Updated SocketIO catch all to new API
|
||||
- [core] Allow custom modules positions by scanning index.html for the defined regions, instead of hard coded (PR #3518 fixes issue #3504)
|
||||
- [core] Detail optimizations in `config_check.js`
|
||||
- [core] Updated minimal needed node version in `package.json` (currently v20.9.0) (#3559) and except for v21 (no security updates) (#3561)
|
||||
- [linter] Switch to ESLint v9 and flat config and replace `eslint-plugin-unicorn` by `@eslint/js`
|
||||
- [core] Fix discovering module positions twice after #3450
|
||||
|
||||
### Fixed
|
||||
|
||||
- [docs] Fixed `checks` badge in README.md
|
||||
- [weather] Fixed issue with the UK Met Office provider following a change in their API paths and header info.
|
||||
- [core] Add check for node_helper loading for multiple instances of same module (#3502)
|
||||
- [weather] Fixed issue for respecting unit config on broadcasted notifications
|
||||
- [tests] Fixes calendar test by moving it from e2e to electron with fixed date (#3532)
|
||||
- [calendar] fixed sliceMultiDayEvents getting wrong count and displaying incorrect entries, Europe/Berlin (#3542)
|
||||
- [tests] ignore `js/positions.js` when linting (this file is created at runtime)
|
||||
- [calendar] fixed sliceMultiDayEvents showing previous day without config enabled
|
||||
|
||||
## [2.28.0] - 2024-07-01
|
||||
|
||||
Thanks to: @btoconnor, @bugsounet, @JasonStieber, @khassel, @kleinmantara and @WallysWellies.
|
||||
|
||||
> ⚠️ This release needs nodejs version >= v20.9.0
|
||||
|
||||
### 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 `position: <position>` 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
|
||||
|
||||
- [updatenotification] Recode update_helper.js 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
|
||||
|
||||
- [weather] 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.
|
||||
|
||||
@@ -32,7 +331,7 @@ This release also marks the latest release by Michael Teeuw. For more info, plea
|
||||
- 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)
|
||||
- Updated dependencies
|
||||
- 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)
|
||||
@@ -48,8 +347,8 @@ This release also marks the latest release by Michael Teeuw. For more info, plea
|
||||
- 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 reponse is not an JSON format (check PM2)
|
||||
- Fix electron loadURL locally on Windows when address "0.0.0.0" (#2550)
|
||||
- Fix updatenotification (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
|
||||
@@ -124,8 +423,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
|
||||
@@ -134,7 +433,7 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al
|
||||
|
||||
### Updated
|
||||
|
||||
- Added support for precipitation probability with openmeteo weather-provider
|
||||
- [weather] Added support for precipitation probability with openmeteo weather-provider
|
||||
- Update electron to v25.2 and other dependencies
|
||||
- Use node v20 in github workflow (replacing v14)
|
||||
- Refactor formatTime into common util function for default modules
|
||||
@@ -307,7 +606,7 @@ Special thanks to the following contributors: @eouia, @khassel, @kolbyjack, @Kri
|
||||
|
||||
### Added
|
||||
|
||||
- Added a new config option `httpHeaders` used by helmet (see https://helmetjs.github.io/). You can now set own httpHeaders which will override the defaults in `js/defauls.js` which is useful e.g. if you want to embed MagicMirror into annother website (solves #2847).
|
||||
- Added a new config option `httpHeaders` used by helmet (see <https://helmetjs.github.io/>). You can now set own httpHeaders which will override the defaults in `js/defaults.js` which is useful e.g. if you want to embed MagicMirror into another website (solves #2847).
|
||||
- Show endDate for calendar events when dateHeader is enabled and showEnd is set to true (#2192).
|
||||
- Added the notification emitting from the weather module on information updated.
|
||||
- Use recommended file extension for YAML files (#2864).
|
||||
@@ -356,7 +655,7 @@ Special thanks to the following contributors: @10bias, @CFenner, @JHWelch, @k1rd
|
||||
- Fix minor console output issue for loading translations (#2814).
|
||||
- Don't adjust startDate for full day events if endDate is in the past.
|
||||
- Fix windspeed conversion error in openweathermap provider. (#2812)
|
||||
- Fix conflicting parms turning off showEnd for full day events. (#2629)
|
||||
- Fix conflicting parameter turning off showEnd for full day events. (#2629)
|
||||
- Fix regression, calendar.maximumEntries not used to filter calendar level entries (#2868)
|
||||
|
||||
## [2.18.0] - 2022-01-01
|
||||
@@ -388,7 +687,7 @@ Special thanks to the following contributors: @AmpioRosso, @eouia, @fewieden, @j
|
||||
### Fixed
|
||||
|
||||
- Fixed wrong file `kr.json` to `ko.json`. Use language code 'ko' instead of 'kr' for Korean language.
|
||||
- Fixed `feels_like` data from openweathermaps current weather being ignored (#2678).
|
||||
- Fixed `feels_like` data from openweathermap's current weather being ignored (#2678).
|
||||
- Fixed chaotic newsfeed display after network connection loss thanks to @jalibu (#2638).
|
||||
- Fixed incorrect time zone correction of recurring full day events (#2632 and #2634).
|
||||
- Fixed e2e tests by increasing testTimeout.
|
||||
@@ -425,7 +724,7 @@ Special thanks to the following contributors: @apiontek, @eouia, @jupadin, @khas
|
||||
- Updated github templates.
|
||||
- Actually test all js and css files when lint script is run.
|
||||
- Updated jsdocs and print warnings during testing too.
|
||||
- Updated weathergov provider to try fetching not just current, but also foreacst, when API URLs available.
|
||||
- Updated weathergov provider to try fetching not just current, but also forecast, when API URLs available.
|
||||
- Refactored clock layout.
|
||||
- Refactored methods from weather-providers into weatherobject (isDaytime, updateSunTime).
|
||||
- Use of `logger.js` in jest tests.
|
||||
@@ -652,12 +951,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
|
||||
@@ -691,14 +990,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
|
||||
|
||||
@@ -706,7 +1005,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
|
||||
|
||||
@@ -736,8 +1035,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
|
||||
@@ -775,7 +1074,7 @@ Special thanks to @sdetweil for all his great contributions!
|
||||
### Updated
|
||||
|
||||
- Updated lower bound of `lodash` and `helmet` dependencies for security patches.
|
||||
- Updated compliments.js to handle newline in text, as textfields to not interpolate contents.
|
||||
- Updated compliments.js to handle newline in text, as text fields to not interpolate contents.
|
||||
- Updated raspberry.sh installer script to handle new platform issues, split node/npm, pm2, and screen saver changes.
|
||||
- Improve handling for armv6l devices, where electron support has gone away, add optional serveronly config option.
|
||||
- Improved run-start.sh to handle for serveronly mode, by choice, or when electron not available.
|
||||
@@ -805,14 +1104,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.
|
||||
|
||||
@@ -827,7 +1126,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
|
||||
@@ -836,7 +1135,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
|
||||
|
||||
@@ -845,18 +1144,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
|
||||
@@ -894,41 +1193,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
|
||||
@@ -953,7 +1252,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
|
||||
@@ -962,7 +1261,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
|
||||
|
||||
@@ -995,8 +1294,8 @@ 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 calendar parsing issue for Midori on RasperryPi Zero w, related to issue #694.
|
||||
- Fix for weatherforecast rainfall rounding [#1374](https://github.com/MagicMirrorOrg/MagicMirror/issues/1374)
|
||||
- Fix calendar parsing issue for Midori on Raspberry Pi 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.
|
||||
|
||||
@@ -1017,7 +1316,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`
|
||||
|
||||
@@ -1039,20 +1338,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)
|
||||
- Fix issue #1196 by changing underscore to hyphen in locale id, in align with moment.js.
|
||||
- Fixed issue where heat index and wind chill were reporting incorrect values in Kelvin. [#1263](https://github.com/MagicMirrorOrg/MagicMirror/issues/1263)
|
||||
|
||||
### Updated
|
||||
|
||||
@@ -1064,7 +1363,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
|
||||
|
||||
@@ -1076,7 +1375,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Implement Danger.js to notify contributors when CHANGELOG.md is missing in PR.
|
||||
- Allow scrolling in full page article view of default newsfeed module with gesture events from [MMM-Gestures](https://github.com/thobach/MMM-Gestures)
|
||||
- Changed 'compliments.js' - Updated DOM if remote compliments are loaded instead of waiting one updateInterval to show custom compliments
|
||||
- Automated unit tests utils, deprecated, translator, cloneObject(lockstrings)
|
||||
- Automated unit tests utils, deprecated, translator, cloneObject(lockStrings)
|
||||
- Automated integration tests translations
|
||||
- Add advanced filtering to the excludedEvents configuration of the default calendar module
|
||||
- New currentweather module config option: `showFeelsLike`: Shows how it actually feels like. (wind chill or heat index)
|
||||
@@ -1180,7 +1479,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
### Fixed
|
||||
|
||||
- Fixed issue with incorrect alignment of analog clock when displayed in the center column of the MM.
|
||||
- Fixed ipWhitelist behaviour to make empty whitelist ([]) allow any and all hosts access to the MM.
|
||||
- Fixed ipWhitelist behavior to make empty whitelist ([]) allow any and all hosts access to the MM.
|
||||
- Fixed issue with calendar module where 'excludedEvents' count towards 'maximumEntries'.
|
||||
- Fixed issue with calendar module where global configuration of maximumEntries was not overridden by calendar specific config (see module doc).
|
||||
- Fixed issue where `this.file(filename)` returns a path with two hashes.
|
||||
@@ -1190,8 +1489,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
|
||||
@@ -1222,7 +1521,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
### Fixed
|
||||
|
||||
- Fix instruction in README for using automatically installer script.
|
||||
- Bug of duplicated compliments as described in [here](https://forum.magicmirror.builders/topic/2381/compliments-module-stops-cycling-compliments).
|
||||
- Bug of [duplicated compliments](https://forum.magicmirror.builders/topic/2381/compliments-module-stops-cycling-compliments).
|
||||
- Fix double message about port when server is starting
|
||||
- Corrected Swedish translations for TODAY/TOMORROW/DAYAFTERTOMORROW.
|
||||
- Removed unused import from js/electron.js
|
||||
@@ -1238,7 +1537,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.
|
||||
@@ -1251,7 +1550,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)
|
||||
@@ -1285,7 +1584,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
||||
- Added multiple calendar icon support.
|
||||
- Added tests for Translations, dev argument, version, dev console.
|
||||
- Added test anytime feature compliments module.
|
||||
- Added test ipwhitelist configuration directive.
|
||||
- Added test ipWhitelist configuration directive.
|
||||
- Added test for calendar module: default, basic-auth, backward compatibility, fail-basic-auth.
|
||||
- Added meta tags to support fullscreen mode on iOS (for server mode)
|
||||
- Added `ignoreOldItems` and `ignoreOlderThan` options to the News Feed module
|
||||
@@ -1297,7 +1596,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.
|
||||
@@ -1314,18 +1613,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
|
||||
@@ -1395,11 +1694,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
|
||||
|
||||
@@ -1419,8 +1718,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
|
||||
|
||||
@@ -1437,7 +1736,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
|
||||
@@ -1471,3 +1770,52 @@ It includes (but is not limited to) the following features:
|
||||
### Initial release of MagicMirror
|
||||
|
||||
This was part of the blogpost: [https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the](https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the)
|
||||
|
||||
[2.32.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.31.0...v2.32.0
|
||||
[2.31.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.30.0...v2.31.0
|
||||
[2.30.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.29.0...v2.30.0
|
||||
[2.29.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.28.0...v2.29.0
|
||||
[2.28.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.27.0...v2.28.0
|
||||
[2.27.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.26.0...v2.27.0
|
||||
[2.26.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.25.0...v2.26.0
|
||||
[2.25.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.24.0...v2.25.0
|
||||
[2.24.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.23.0...v2.24.0
|
||||
[2.23.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.22.0...v2.23.0
|
||||
[2.22.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.21.0...v2.22.0
|
||||
[2.21.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.20.0...v2.21.0
|
||||
[2.20.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.19.0...v2.20.0
|
||||
[2.19.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.18.0...v2.19.0
|
||||
[2.18.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.17.1...v2.18.0
|
||||
[2.17.1]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.17.0...v2.17.1
|
||||
[2.17.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.16.0...v2.17.0
|
||||
[2.16.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.15.0...v2.16.0
|
||||
[2.15.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.14.0...v2.15.0
|
||||
[2.14.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.13.0...v2.14.0
|
||||
[2.13.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.12.0...v2.13.0
|
||||
[2.12.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.11.0...v2.12.0
|
||||
[2.11.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.10.1...v2.11.0
|
||||
[2.10.1]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.10.0...v2.10.1
|
||||
[2.10.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.9.0...v2.10.0
|
||||
[2.9.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.8.0...v2.9.0
|
||||
[2.8.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.7.1...v2.8.0
|
||||
[2.7.1]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.7.0...v2.7.1
|
||||
[2.7.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.6.0...v2.7.0
|
||||
[2.6.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.5.0...v2.6.0
|
||||
[2.5.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.4.1...v2.5.0
|
||||
[2.4.1]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.4.0...v2.4.1
|
||||
[2.4.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.3.1...v2.4.0
|
||||
[2.3.1]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.3.0...v2.3.1
|
||||
[2.3.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.2.2...v2.3.0
|
||||
[2.2.2]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.2.1...v2.2.2
|
||||
[2.2.1]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.2.0...v2.2.1
|
||||
[2.2.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.1.3...v2.2.0
|
||||
[2.1.3]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.1.2...v2.1.3
|
||||
[2.1.2]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.1.1...v2.1.2
|
||||
[2.1.1]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.1.0...v2.1.1
|
||||
[2.1.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.0.5...v2.1.0
|
||||
[2.0.5]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.0.4...v2.0.5
|
||||
[2.0.4]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.0.3...v2.0.4
|
||||
[2.0.3]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.0.2...v2.0.3
|
||||
[2.0.2]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.0.1...v2.0.2
|
||||
[2.0.1]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.0.0...v2.0.1
|
||||
[2.0.0]: https://github.com/MagicMirrorOrg/MagicMirror/releases/tag/v2.0.0
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# Collaboration
|
||||
|
||||
This document describes how collaborators of this repository should work together.
|
||||
|
||||
## Pull Requests
|
||||
@@ -5,7 +7,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 +17,51 @@ This document describes how collaborators of this repository should work togethe
|
||||
|
||||
## Releases
|
||||
|
||||
- are done by @MichMich only
|
||||
Are done by
|
||||
|
||||
- [ ] @rejas
|
||||
- [ ] @sdetweil
|
||||
- [ ] @khassel
|
||||
|
||||
### Pre-Deployment steps
|
||||
|
||||
- [ ] update dependencies (a few days before)
|
||||
|
||||
### Deployment steps
|
||||
|
||||
- [ ] pull latest `develop` branch
|
||||
- [ ] create `prep-release` branch from `develop`
|
||||
- [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0`
|
||||
- [ ] test `prep-release` branch
|
||||
- [ ] update `CHANGELOG.md`
|
||||
- [ ] add all contributor names: `...`
|
||||
- [ ] add min. node version: > ⚠️ This release needs nodejs version `v22.14.0` or higher
|
||||
- [ ] check release link at the bottom of the file
|
||||
- [ ] commit and push all changes
|
||||
- [ ] create pull request from `prep-release` to `develop` branch with title `Prepare Release 2.xx.0`
|
||||
- [ ] after successful test run via github actions: merge pull request to `develop`
|
||||
- [ ] after successful test run via github actions: create pull request from `develop` to `master` branch
|
||||
- [ ] add label `mastermerge`
|
||||
- [ ] title of the PR is `Release 2.xx.0`
|
||||
- [ ] description of the PR is the section of the `CHANGELOG.md`
|
||||
- [ ] after PR tests run without issues, merge PR
|
||||
- [ ] create new release with
|
||||
- [ ] corresponding version tag `v2.xx.0`
|
||||
- [ ] a release name: `...`
|
||||
- [ ] description of the release is the section of the `CHANGELOG.md`
|
||||
|
||||
### Draft new development release
|
||||
|
||||
- [ ] checkout `develop` branch
|
||||
- [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0-develop`
|
||||
- [ ] draft new section in `CHANGELOG.md`
|
||||
- [ ] create new release link at the bottom of the file
|
||||
- [ ] commit and push `develop` branch
|
||||
- [ ] if new release will be in January, update the year in LICENSE.md
|
||||
|
||||
### After release
|
||||
|
||||
- [ ] publish release notes with link to github release on forum in new locked topic
|
||||
- [ ] close all issues with label `ready (coming with next release)`
|
||||
- [ ] release new documentation by merging `develop` on `master` in documentation repository
|
||||
- [ ] publish new version on [npm](https://www.npmjs.com/package/magicmirror)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# The MIT License (MIT)
|
||||
|
||||
Copyright © 2016-2022 Michael Teeuw
|
||||
Copyright © 2016-2025 Michael Teeuw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
|
24
README.md
24
README.md
@@ -1,17 +1,17 @@
|
||||

|
||||
# 
|
||||
|
||||
<p style="text-align: center">
|
||||
<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://github.com/MichMich/MagicMirror">
|
||||
<img src="https://img.shields.io/github/stars/michmich/magicmirror?style=social">
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
|
||||
</a>
|
||||
<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/check-runs/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" alt="GitHub Stars">
|
||||
</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!
|
||||
|
||||
@@ -24,7 +24,7 @@ For the full documentation including **[installation instructions](https://docs.
|
||||
- Website: [https://magicmirror.builders](https://magicmirror.builders)
|
||||
- Documentation: [https://docs.magicmirror.builders](https://docs.magicmirror.builders)
|
||||
- Forum: [https://forum.magicmirror.builders](https://forum.magicmirror.builders)
|
||||
- Technical discussions: https://forum.magicmirror.builders/category/11/core-system
|
||||
- Technical discussions: <https://forum.magicmirror.builders/category/11/core-system>
|
||||
- Discord: [https://discord.gg/J5BAtvx](https://discord.gg/J5BAtvx)
|
||||
- Blog: [https://michaelteeuw.nl/tagged/magicmirror](https://michaelteeuw.nl/tagged/magicmirror)
|
||||
- Donations: [https://magicmirror.builders/#donate](https://magicmirror.builders/#donate)
|
||||
@@ -41,7 +41,7 @@ For the full contribution guidelines, check out: [https://docs.magicmirror.build
|
||||
|
||||
## Enjoying MagicMirror? Consider a donation!
|
||||
|
||||
MagicMirror² is opensource and free. That doesn't mean we don't need any money.
|
||||
MagicMirror² is Open Source and free. That doesn't mean we don't need any money.
|
||||
|
||||
Please consider a donation to help us cover the ongoing costs like webservers and email services.
|
||||
If we receive enough donations we might even be able to free up some working hours and spend some extra time improving the MagicMirror² core.
|
||||
@@ -49,5 +49,5 @@ If we receive enough donations we might even be able to free up some working hou
|
||||
To donate, please follow [this](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G5D8E9MR5DTD2&source=url) link.
|
||||
|
||||
<p style="text-align: center">
|
||||
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
|
||||
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
|
||||
</p>
|
||||
|
@@ -28,7 +28,7 @@
|
||||
});
|
||||
|
||||
// determine if "--use-tls"-flag was provided
|
||||
config["tls"] = process.argv.indexOf("--use-tls") > 0;
|
||||
config.tls = process.argv.indexOf("--use-tls") > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,7 +40,7 @@
|
||||
// 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 = "";
|
||||
|
||||
@@ -83,6 +83,17 @@
|
||||
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) === -1) {
|
||||
getServerConfig(`${prefix}${config.address}:${config.port}/config/`)
|
||||
.then(function (configReturn) {
|
||||
// check environment for DISPLAY or WAYLAND_DISPLAY
|
||||
const elecParams = ["js/electron.js"];
|
||||
if (process.env.WAYLAND_DISPLAY) {
|
||||
console.log(`Client: Using WAYLAND_DISPLAY=${process.env.WAYLAND_DISPLAY}`);
|
||||
elecParams.push("--enable-features=UseOzonePlatform");
|
||||
elecParams.push("--ozone-platform=wayland");
|
||||
} else if (process.env.DISPLAY) {
|
||||
console.log(`Client: Using DISPLAY=${process.env.DISPLAY}`);
|
||||
} else {
|
||||
fail("Error: Requires environment variable WAYLAND_DISPLAY or DISPLAY, none is provided.");
|
||||
}
|
||||
// Pass along the server config via an environment variable
|
||||
const env = Object.create(process.env);
|
||||
env.clientonly = true; // set to pass to electron.js
|
||||
@@ -94,7 +105,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, elecParams, options);
|
||||
|
||||
// Pipe all child process output to current stdout
|
||||
child.stdout.on("data", function (buf) {
|
||||
|
@@ -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
|
||||
@@ -31,7 +28,11 @@ let config = {
|
||||
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
|
||||
|
||||
language: "en",
|
||||
locale: "en-US",
|
||||
locale: "en-US", // this variable is provided as a consistent location
|
||||
// it is currently only used by 3rd party modules. no MagicMirror code uses this value
|
||||
// as we have no usage, we have no constraints on what this field holds
|
||||
// see https://en.wikipedia.org/wiki/Locale_(computer_software) for the possibilities
|
||||
|
||||
logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging
|
||||
timeFormat: 24,
|
||||
units: "metric",
|
||||
@@ -70,11 +71,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 +82,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
|
||||
}
|
||||
},
|
||||
{
|
||||
|
253
cspell.config.json
Normal file
253
cspell.config.json
Normal file
@@ -0,0 +1,253 @@
|
||||
{
|
||||
"version": "0.2",
|
||||
"language": "en",
|
||||
"words": [
|
||||
"aarch",
|
||||
"Alvinger",
|
||||
"Ampio",
|
||||
"andrezibaia",
|
||||
"angeldeejay",
|
||||
"apiontek",
|
||||
"armv",
|
||||
"ashishtank",
|
||||
"autoplay",
|
||||
"beada",
|
||||
"Binney",
|
||||
"bluemanos",
|
||||
"bnitkin",
|
||||
"bokmål",
|
||||
"Brasileiro",
|
||||
"Brento",
|
||||
"browserwindow",
|
||||
"bryanzzhu",
|
||||
"btoconnor",
|
||||
"bugsounet",
|
||||
"buxxi",
|
||||
"byday",
|
||||
"calendarfetcherutils",
|
||||
"calendarutils",
|
||||
"chamakura",
|
||||
"cjbrunner",
|
||||
"clientonly",
|
||||
"clockfaces",
|
||||
"cmdline",
|
||||
"codac",
|
||||
"Crazylegstoo",
|
||||
"crazyscot",
|
||||
"Creepin",
|
||||
"currentweather",
|
||||
"CUSTOMCSS",
|
||||
"customregions",
|
||||
"cxmj",
|
||||
"Cymraeg",
|
||||
"dariom",
|
||||
"darksky",
|
||||
"dateheader",
|
||||
"dateheaders",
|
||||
"davide",
|
||||
"DAYAFTERTOMORROW",
|
||||
"DAYBEFOREYESTERDAY",
|
||||
"defaultmodules",
|
||||
"dgoth",
|
||||
"dkallen",
|
||||
"drivelist",
|
||||
"DTEND",
|
||||
"DTSTAMP",
|
||||
"DTSTART",
|
||||
"Duffman",
|
||||
"earlman",
|
||||
"easyas",
|
||||
"eddiehung",
|
||||
"Edgardos",
|
||||
"Ekristoffe",
|
||||
"elec",
|
||||
"eltociear",
|
||||
"envcanada",
|
||||
"envsub",
|
||||
"envsubst",
|
||||
"eouia",
|
||||
"exdate",
|
||||
"expectedheaders",
|
||||
"ezeholz",
|
||||
"Faizan",
|
||||
"feedme",
|
||||
"feelslike",
|
||||
"Fenner",
|
||||
"fewieden",
|
||||
"fixuppm",
|
||||
"flopp",
|
||||
"fontawesome",
|
||||
"fontface",
|
||||
"forecastweather",
|
||||
"fortawesome",
|
||||
"frameguard",
|
||||
"Frysk",
|
||||
"fulldate",
|
||||
"fullday",
|
||||
"fullscreen",
|
||||
"geraki",
|
||||
"Gevoelstemperatuur",
|
||||
"GHSA",
|
||||
"ghsas",
|
||||
"grenagit",
|
||||
"Heiko",
|
||||
"Hirschberger",
|
||||
"hourlyweather",
|
||||
"Hwind",
|
||||
"ical",
|
||||
"illimarkangur",
|
||||
"Ingan",
|
||||
"ipfilter",
|
||||
"ismarslomic",
|
||||
"jakemulley",
|
||||
"jakobsarwary",
|
||||
"jalibu",
|
||||
"jargordon",
|
||||
"jetson",
|
||||
"jkriegshauser",
|
||||
"jsdocs",
|
||||
"jsonlint",
|
||||
"jupadin",
|
||||
"kaennchenstruggle",
|
||||
"Kalenderwoche",
|
||||
"kenzal",
|
||||
"Keyport",
|
||||
"khassel",
|
||||
"Kingdon",
|
||||
"kioskmode",
|
||||
"klaernie",
|
||||
"kleinmantara",
|
||||
"Kmph",
|
||||
"Knapoc",
|
||||
"Koepke",
|
||||
"kolbyjack",
|
||||
"krekos",
|
||||
"Kristjan",
|
||||
"krukle",
|
||||
"labwc",
|
||||
"Landis",
|
||||
"larryare",
|
||||
"letsencrypt",
|
||||
"libgpiod",
|
||||
"Lightspeed",
|
||||
"locationforecast",
|
||||
"lockstring",
|
||||
"lstrip",
|
||||
"Luciella",
|
||||
"luxon",
|
||||
"lxsession",
|
||||
"magicmirror",
|
||||
"martingron",
|
||||
"marvai",
|
||||
"mastermerge",
|
||||
"matchtype",
|
||||
"maxentries",
|
||||
"Meteo",
|
||||
"michaelteeuw",
|
||||
"michmich",
|
||||
"Midori",
|
||||
"mirontoli",
|
||||
"MISSINGLANG",
|
||||
"mixasgr",
|
||||
"MMPM",
|
||||
"modernizr",
|
||||
"modulename",
|
||||
"multiday",
|
||||
"Mystara",
|
||||
"Ñandú",
|
||||
"nathannaveen",
|
||||
"naveensrinivasan",
|
||||
"ndom",
|
||||
"Nerfzooka",
|
||||
"NEWSFEED",
|
||||
"newsitems",
|
||||
"nfogal",
|
||||
"njwilliams",
|
||||
"nonrepeating",
|
||||
"Norsk",
|
||||
"nunjuck",
|
||||
"odroid",
|
||||
"oemel",
|
||||
"onecall",
|
||||
"onevent",
|
||||
"openmeteo",
|
||||
"openweathermap",
|
||||
"oraclesean",
|
||||
"oscarb",
|
||||
"philnagel",
|
||||
"Português",
|
||||
"PRECIP",
|
||||
"Problema",
|
||||
"psieg",
|
||||
"radokristof",
|
||||
"rajniszp",
|
||||
"rebuilded",
|
||||
"Reis",
|
||||
"rejas",
|
||||
"Resig",
|
||||
"roboto",
|
||||
"rohitdharavath",
|
||||
"Rosso",
|
||||
"rrule",
|
||||
"savvadam",
|
||||
"sdetweil",
|
||||
"sendheaders",
|
||||
"serveronly",
|
||||
"sexualized",
|
||||
"skpanagiotis",
|
||||
"SMHI",
|
||||
"Snille",
|
||||
"socketclient",
|
||||
"socketio",
|
||||
"spectron",
|
||||
"Starinvest",
|
||||
"sthuber",
|
||||
"Stieber",
|
||||
"stylelintrc",
|
||||
"subclassing",
|
||||
"sunaction",
|
||||
"suncalc",
|
||||
"suntimes",
|
||||
"symboltest",
|
||||
"systeminformation",
|
||||
"tada",
|
||||
"taglist",
|
||||
"Teeuw",
|
||||
"TESTMODE",
|
||||
"thomasrockhu",
|
||||
"tomzt",
|
||||
"ukmetoffice",
|
||||
"ukmetofficedatahub",
|
||||
"unitless",
|
||||
"unparseable",
|
||||
"updatenotification",
|
||||
"Vaice",
|
||||
"veeck",
|
||||
"VEVENT",
|
||||
"vgtu",
|
||||
"Voelt",
|
||||
"vppencilsharpener",
|
||||
"Wallys",
|
||||
"Weatherbit",
|
||||
"WEATHERDATA",
|
||||
"Weatherflow",
|
||||
"weatherforecast",
|
||||
"weathergov",
|
||||
"weathericons",
|
||||
"weatherobject",
|
||||
"weatherutils",
|
||||
"windspeed",
|
||||
"Woolridge",
|
||||
"worktree",
|
||||
"xlarge",
|
||||
"xrandr",
|
||||
"xsmall",
|
||||
"xsorifc",
|
||||
"xwindows",
|
||||
"xxxe",
|
||||
"Ybbet",
|
||||
"yearmatchgroup"
|
||||
],
|
||||
"ignorePaths": ["node_modules/**", "modules/**", "translations/**", "tests/mocks/**", "tests/e2e/modules/clock_es_spec.js", "css/roboto.css"],
|
||||
"dictionaries": ["node"]
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
13
css/main.css
13
css/main.css
@@ -239,3 +239,16 @@ sup {
|
||||
border-spacing: 0;
|
||||
border-collapse: separate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Container Definitions.
|
||||
*/
|
||||
|
||||
.region .container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.region .container.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
@@ -2,11 +2,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-display: swap;
|
||||
font-weight: 100;
|
||||
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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -38,11 +38,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -50,11 +50,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -62,11 +62,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -74,11 +74,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -86,11 +86,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-display: swap;
|
||||
font-weight: 300;
|
||||
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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -98,11 +98,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -110,11 +110,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -122,11 +122,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -134,11 +134,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -146,11 +146,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -158,11 +158,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -170,11 +170,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -182,11 +182,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -194,11 +194,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -206,11 +206,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -218,11 +218,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -230,11 +230,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -242,11 +242,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -254,11 +254,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-display: swap;
|
||||
font-weight: 500;
|
||||
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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -266,11 +266,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -278,11 +278,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -290,11 +290,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -302,11 +302,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -314,11 +314,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -326,11 +326,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -338,11 +338,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-display: swap;
|
||||
font-weight: 700;
|
||||
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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -350,11 +350,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -362,11 +362,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -374,11 +374,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -386,11 +386,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -398,11 +398,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -410,11 +410,11 @@
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -422,11 +422,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -434,11 +434,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -446,11 +446,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -458,11 +458,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -470,11 +470,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -482,11 +482,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -494,11 +494,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -506,11 +506,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -518,11 +518,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -530,11 +530,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -542,11 +542,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -554,11 +554,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -566,11 +566,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -578,11 +578,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -590,11 +590,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -602,11 +602,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -614,11 +614,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -626,11 +626,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -638,11 +638,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -650,11 +650,11 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -662,10 +662,10 @@
|
||||
@font-face {
|
||||
font-family: "Roboto Condensed";
|
||||
font-style: normal;
|
||||
font-display: var(--fontsource-display, swap);
|
||||
font-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");
|
||||
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;
|
||||
}
|
132
eslint.config.mjs
Normal file
132
eslint.config.mjs
Normal file
@@ -0,0 +1,132 @@
|
||||
import {defineConfig, globalIgnores} from "eslint/config";
|
||||
import globals from "globals";
|
||||
import {flatConfigs as importX} from "eslint-plugin-import-x";
|
||||
import jest from "eslint-plugin-jest";
|
||||
import js from "@eslint/js";
|
||||
import jsdoc from "eslint-plugin-jsdoc";
|
||||
import packageJson from "eslint-plugin-package-json";
|
||||
import stylistic from "@stylistic/eslint-plugin";
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(["config/**", "modules/**/*", "!modules/default/**", "js/positions.js"]),
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
Log: "readonly",
|
||||
MM: "readonly",
|
||||
Module: "readonly",
|
||||
config: "readonly",
|
||||
moment: "readonly"
|
||||
}
|
||||
},
|
||||
plugins: {js, jsdoc, stylistic},
|
||||
extends: [importX.recommended, jest.configs["flat/recommended"], "js/recommended", jsdoc.configs["flat/recommended"], "stylistic/all"],
|
||||
rules: {
|
||||
"@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/indent": ["error", "tab"],
|
||||
"@stylistic/max-statements-per-line": ["error", {max: 2}],
|
||||
"@stylistic/multiline-comment-style": "off",
|
||||
"@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/semi": ["error", "always"],
|
||||
"@stylistic/space-before-function-paren": ["error", "always"],
|
||||
"@stylistic/spaced-comment": "off",
|
||||
"dot-notation": "error",
|
||||
eqeqeq: "error",
|
||||
"id-length": "off",
|
||||
"import-x/extensions": "error",
|
||||
"import-x/newline-after-import": "error",
|
||||
"import-x/order": "error",
|
||||
"init-declarations": "off",
|
||||
"jest/consistent-test-it": "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",
|
||||
"max-lines-per-function": ["warn", 400],
|
||||
"max-statements": "off",
|
||||
"no-global-assign": "off",
|
||||
"no-inline-comments": "off",
|
||||
"no-magic-numbers": "off",
|
||||
"no-param-reassign": "error",
|
||||
"no-plusplus": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-ternary": "off",
|
||||
"no-throw-literal": "error",
|
||||
"no-undefined": "off",
|
||||
"no-unneeded-ternary": "error",
|
||||
"no-unused-vars": "off",
|
||||
"no-useless-return": "error",
|
||||
"no-warning-comments": "off",
|
||||
"object-shorthand": ["error", "methods"],
|
||||
"one-var": "off",
|
||||
"prefer-template": "error",
|
||||
"sort-keys": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
ignores: [
|
||||
"clientonly/index.js",
|
||||
"modules/default/calendar/debug.js",
|
||||
"js/logger.js",
|
||||
"tests/**/*.js"
|
||||
],
|
||||
rules: {"no-console": "error"}
|
||||
},
|
||||
{
|
||||
files: ["**/package.json"],
|
||||
plugins: {packageJson},
|
||||
extends: ["packageJson/recommended"]
|
||||
},
|
||||
{
|
||||
files: ["**/*.mjs"],
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
globals: {
|
||||
...globals.node
|
||||
},
|
||||
sourceType: "module"
|
||||
},
|
||||
plugins: {js, stylistic},
|
||||
extends: [importX.recommended, "js/all", "stylistic/all"],
|
||||
rules: {
|
||||
"@stylistic/array-element-newline": "off",
|
||||
"@stylistic/indent": ["error", "tab"],
|
||||
"@stylistic/object-property-newline": ["error", {allowAllPropertiesOnSameLine: true}],
|
||||
"@stylistic/padded-blocks": ["error", "never"],
|
||||
"@stylistic/quote-props": ["error", "as-needed"],
|
||||
"import-x/no-unresolved": ["error", {ignore: ["eslint/config"]}],
|
||||
"max-lines-per-function": ["error", 100],
|
||||
"no-magic-numbers": "off",
|
||||
"one-var": ["error", "never"],
|
||||
"sort-keys": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["tests/configs/modules/weather/*.js"],
|
||||
rules: {
|
||||
"@stylistic/quotes": "off"
|
||||
}
|
||||
}
|
||||
]);
|
25
fonts/package-lock.json
generated
25
fonts/package-lock.json
generated
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "magicmirror-fonts",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "magicmirror-fonts",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"@fontsource/roboto-condensed": "^5.0.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/roboto": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.8.tgz",
|
||||
"integrity": "sha512-XxPltXs5R31D6UZeLIV1td3wTXU3jzd3f2DLsXI8tytMGBkIsGcc9sIyiupRtA8y73HAhuSCeweOoBqf6DbWCA=="
|
||||
},
|
||||
"node_modules/@fontsource/roboto-condensed": {
|
||||
"version": "5.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-5.0.14.tgz",
|
||||
"integrity": "sha512-ZNBHUhE5/3z9efMIjpBblFsfLHgGotJjlzKd5Q8DODbkRRWy6Yh+JnbiaJZ8zwQyHyYBNOolk57BG4BcjSzrRg=="
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "magicmirror-fonts",
|
||||
"description": "Package for fonts use by MagicMirror² Core.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/MichMich/MagicMirror.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/MichMich/MagicMirror/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"@fontsource/roboto-condensed": "^5.0.14"
|
||||
}
|
||||
}
|
10
index.html
10
index.html
@@ -12,12 +12,13 @@
|
||||
|
||||
<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" />
|
||||
<link rel="stylesheet" type="text/css" href="css/roboto.css" />
|
||||
<link rel="stylesheet" type="text/css" href="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">
|
||||
window.mmVersion = "#VERSION#";
|
||||
window.mmTestMode = "#TESTMODE#";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -41,10 +42,10 @@
|
||||
</div>
|
||||
<div class="region fullscreen above"><div class="container"></div></div>
|
||||
<script type="text/javascript" src="socket.io/socket.io.js"></script>
|
||||
<script type="text/javascript" src="vendor/node_modules/nunjucks/browser/nunjucks.min.js"></script>
|
||||
<script type="text/javascript" src="node_modules/nunjucks/browser/nunjucks.min.js"></script>
|
||||
<script type="text/javascript" src="js/defaults.js"></script>
|
||||
<script type="text/javascript" src="#CONFIG_FILE#"></script>
|
||||
<script type="text/javascript" src="vendor/vendor.js"></script>
|
||||
<script type="text/javascript" src="js/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>
|
||||
@@ -55,6 +56,7 @@
|
||||
<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/positions.js"></script>
|
||||
<script type="text/javascript" src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
# This file is still here to keep PM2 working on older installations.
|
||||
cd ~/MagicMirror
|
||||
DISPLAY=:0 npm start
|
@@ -20,7 +20,6 @@ module.exports = async () => {
|
||||
},
|
||||
{
|
||||
displayName: "e2e",
|
||||
setupFilesAfterEnv: ["<rootDir>/tests/e2e/helpers/mock-console.js"],
|
||||
testMatch: ["**/tests/e2e/**/*.[jt]s?(x)"],
|
||||
modulePaths: ["<rootDir>/js/"],
|
||||
testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers", "<rootDir>/tests/e2e/mocks"]
|
||||
|
@@ -1,10 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* AnimateCSS System from https://animate.style/
|
||||
* by @bugsounet
|
||||
* for Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
/* enumeration of animations in Array **/
|
||||
const AnimateCSSIn = [
|
||||
// Attention seekers
|
||||
@@ -162,3 +155,4 @@ function removeAnimateCSS (element, animation) {
|
||||
node.classList.remove("animate__animated", animationName);
|
||||
node.style.removeProperty("--animate-duration");
|
||||
}
|
||||
if (typeof window === "undefined") module.exports = { AnimateCSSIn, AnimateCSSOut };
|
||||
|
122
js/app.js
122
js/app.js
@@ -1,26 +1,29 @@
|
||||
/* 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`);
|
||||
const { getEnvVarsAsObj } = require(`${__dirname}/server_functions`);
|
||||
|
||||
// used to control fetch timeout for node_helpers
|
||||
const { setGlobalDispatcher, Agent } = require("undici");
|
||||
// common timeout value, provide environment override in case
|
||||
const fetch_timeout = process.env.mmFetchTimeout !== undefined ? process.env.mmFetchTimeout : 30000;
|
||||
|
||||
// Get version number.
|
||||
global.version = require(`${__dirname}/../package.json`).version;
|
||||
global.mmTestMode = process.env.mmTestMode === "true";
|
||||
Log.log(`Starting MagicMirror: v${global.version}`);
|
||||
|
||||
// Log system information.
|
||||
Utils.logSystemInformation();
|
||||
|
||||
// global absolute root path
|
||||
global.root_path = path.resolve(`${__dirname}/../`);
|
||||
|
||||
@@ -29,7 +32,7 @@ if (process.env.MM_CONFIG_FILE) {
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -37,10 +40,13 @@ 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");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -59,6 +65,10 @@ function App () {
|
||||
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
|
||||
@@ -67,10 +77,10 @@ function App () {
|
||||
|
||||
// check if templateFile exists
|
||||
try {
|
||||
fs.accessSync(templateFile, fs.F_OK);
|
||||
fs.accessSync(templateFile, fs.constants.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) {
|
||||
@@ -91,7 +101,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 = {
|
||||
@@ -113,18 +123,23 @@ function App () {
|
||||
}
|
||||
}
|
||||
|
||||
require(`${global.root_path}/js/check_config.js`);
|
||||
|
||||
try {
|
||||
fs.accessSync(configFilename, fs.F_OK);
|
||||
fs.accessSync(configFilename, fs.constants.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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,11 +153,23 @@ function App () {
|
||||
*/
|
||||
function checkDeprecatedOptions (userConfig) {
|
||||
const deprecated = require(`${global.root_path}/js/deprecated`);
|
||||
const deprecatedOptions = deprecated.configs;
|
||||
|
||||
// check for deprecated core options
|
||||
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 option(s): ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`);
|
||||
}
|
||||
|
||||
// check for deprecated module options
|
||||
for (const element of userConfig.modules) {
|
||||
if (deprecated[element.module] !== undefined && element.config !== undefined) {
|
||||
const deprecatedModuleOptions = deprecated[element.module];
|
||||
const usedDeprecatedModuleOptions = deprecatedModuleOptions.filter((option) => element.config.hasOwnProperty(option));
|
||||
if (usedDeprecatedModuleOptions.length > 0) {
|
||||
Log.warn(`WARNING! Your config for module ${element.module} is using deprecated option(s): ${usedDeprecatedModuleOptions.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,16 +180,25 @@ function App () {
|
||||
function loadModule (module) {
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = `${__dirname}/../modules/${module}`;
|
||||
const env = getEnvVarsAsObj();
|
||||
let moduleFolder = path.resolve(`${__dirname}/../${env.modulesDir}`, module);
|
||||
|
||||
if (defaultModules.includes(moduleName)) {
|
||||
moduleFolder = `${__dirname}/../modules/default/${module}`;
|
||||
const defaultModuleFolder = path.resolve(`${__dirname}/../modules/default/`, module);
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
} else {
|
||||
// running in Jest, allow defaultModules placed under moduleDir for testing
|
||||
if (env.modulesDir === "modules" || env.modulesDir === "tests/mocks") {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const moduleFile = `${moduleFolder}/${module}.js`;
|
||||
const moduleFile = `${moduleFolder}/${moduleName}.js`;
|
||||
|
||||
try {
|
||||
fs.accessSync(moduleFile, fs.R_OK);
|
||||
fs.accessSync(moduleFile, fs.constants.R_OK);
|
||||
} catch (e) {
|
||||
Log.warn(`No ${moduleFile} found for module: ${moduleName}.`);
|
||||
}
|
||||
@@ -171,14 +207,21 @@ function App () {
|
||||
|
||||
let loadHelper = true;
|
||||
try {
|
||||
fs.accessSync(helperPath, fs.R_OK);
|
||||
fs.accessSync(helperPath, fs.constants.R_OK);
|
||||
} catch (e) {
|
||||
loadHelper = false;
|
||||
Log.log(`No helper found for module: ${moduleName}.`);
|
||||
}
|
||||
|
||||
// if the helper was found
|
||||
if (loadHelper) {
|
||||
const Module = require(helperPath);
|
||||
let Module;
|
||||
try {
|
||||
Module = require(helperPath);
|
||||
} catch (e) {
|
||||
Log.error(`Error when loading ${moduleName}:`, e.message);
|
||||
return;
|
||||
}
|
||||
let m = new Module();
|
||||
|
||||
if (m.requiresVersion) {
|
||||
@@ -249,13 +292,28 @@ function App () {
|
||||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
// get the used module positions
|
||||
Utils.getModulePositions();
|
||||
|
||||
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") {
|
||||
// Only add this module to be loaded if it is not a duplicate (repeated instance of the same module)
|
||||
if (!modules.includes(module.module)) {
|
||||
modules.push(module.module);
|
||||
}
|
||||
} else {
|
||||
Log.warn("Invalid module position found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`);
|
||||
}
|
||||
} else {
|
||||
Log.warn("No module name found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
setGlobalDispatcher(new Agent({ connect: { timeout: fetch_timeout } }));
|
||||
|
||||
await loadModules(modules);
|
||||
|
||||
httpServer = new Server(config);
|
||||
@@ -306,7 +364,7 @@ function App () {
|
||||
}
|
||||
} catch (error) {
|
||||
Log.error(`Error when stopping node_helper for module ${nodeHelper.name}:`);
|
||||
console.error(error);
|
||||
Log.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,20 +1,17 @@
|
||||
/* 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 { styleText } = require("node:util");
|
||||
const Ajv = require("ajv");
|
||||
const globals = require("globals");
|
||||
const { Linter } = require("eslint");
|
||||
|
||||
const linter = new Linter();
|
||||
|
||||
const rootPath = path.resolve(`${__dirname}/../`);
|
||||
const Log = require(`${rootPath}/js/logger.js`);
|
||||
const Utils = require(`${rootPath}/js/utils.js`);
|
||||
|
||||
const linter = new Linter({ configType: "flat" });
|
||||
const ajv = new Ajv();
|
||||
|
||||
/**
|
||||
* Returns a string with path of configuration file.
|
||||
* Check if set by environment variable MM_CONFIG_FILE
|
||||
@@ -33,40 +30,107 @@ function checkConfigFile () {
|
||||
|
||||
// Check if file is present
|
||||
if (fs.existsSync(configFileName) === false) {
|
||||
Log.error(Utils.colors.error("File not found: "), configFileName);
|
||||
throw new Error("No config file present!");
|
||||
throw new Error(`File not found: ${configFileName}\nNo config file present!`);
|
||||
}
|
||||
|
||||
// Check permission
|
||||
try {
|
||||
fs.accessSync(configFileName, fs.F_OK);
|
||||
} catch (e) {
|
||||
Log.error(Utils.colors.error(e));
|
||||
throw new Error("No permission to access config file!");
|
||||
fs.accessSync(configFileName, fs.constants.F_OK);
|
||||
} catch (error) {
|
||||
throw new Error(`${error}\nNo permission to access config file!`);
|
||||
}
|
||||
|
||||
// Validate syntax of the configuration file.
|
||||
Log.info(Utils.colors.info("Checking file... "), configFileName);
|
||||
Log.info(`Checking config file ${configFileName} ...`);
|
||||
|
||||
// I'm not sure if all ever is utf-8
|
||||
const configFile = fs.readFileSync(configFileName, "utf-8");
|
||||
|
||||
// Explicitly tell linter that he might encounter es6 syntax ("let config = {...}")
|
||||
const errors = linter.verify(configFile, {
|
||||
env: {
|
||||
es6: true
|
||||
}
|
||||
});
|
||||
const errors = linter.verify(
|
||||
configFile,
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
rules: { "no-undef": "error" }
|
||||
},
|
||||
configFileName
|
||||
);
|
||||
|
||||
if (errors.length === 0) {
|
||||
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
|
||||
Log.info(styleText("green", "Your configuration file doesn't contain syntax errors :)"));
|
||||
validateModulePositions(configFileName);
|
||||
} else {
|
||||
Log.error(Utils.colors.error("Your configuration file contains syntax errors :("));
|
||||
let errorMessage = "Your configuration file contains syntax errors :(";
|
||||
|
||||
for (const error of errors) {
|
||||
Log.error(`Line ${error.line} column ${error.column}: ${error.message}`);
|
||||
errorMessage += `\nLine ${error.line} column ${error.column}: ${error.message}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
checkConfigFile();
|
||||
/**
|
||||
*
|
||||
* @param {string} configFileName - The path and filename of the configuration file to validate.
|
||||
*/
|
||||
function validateModulePositions (configFileName) {
|
||||
Log.info("Checking modules structure configuration ...");
|
||||
|
||||
const positionList = Utils.getModulePositions();
|
||||
|
||||
// Make Ajv schema configuration 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: positionList
|
||||
}
|
||||
},
|
||||
required: ["module"]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Scan all modules
|
||||
const validate = ajv.compile(schema);
|
||||
const data = require(configFileName);
|
||||
|
||||
const valid = validate(data);
|
||||
if (valid) {
|
||||
Log.info(styleText("green", "Your modules structure configuration doesn't contain errors :)"));
|
||||
} else {
|
||||
const module = validate.errors[0].instancePath.split("/")[2];
|
||||
const position = validate.errors[0].instancePath.split("/")[3];
|
||||
let errorMessage = "This module configuration contains errors:";
|
||||
errorMessage += `\n${JSON.stringify(data.modules[module], null, 2)}`;
|
||||
if (position) {
|
||||
errorMessage += `\n${position}: ${validate.errors[0].message}`;
|
||||
errorMessage += `\n${JSON.stringify(validate.errors[0].params.allowedValues, null, 2).slice(1, -1)}`;
|
||||
} else {
|
||||
errorMessage += validate.errors[0].message;
|
||||
}
|
||||
Log.error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
checkConfigFile();
|
||||
} catch (error) {
|
||||
Log.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
21
js/class.js
21
js/class.js
@@ -1,6 +1,7 @@
|
||||
/* global Class, xyz */
|
||||
|
||||
/* Simple JavaScript Inheritance
|
||||
/*
|
||||
* Simple JavaScript Inheritance
|
||||
* By John Resig https://johnresig.com/
|
||||
*
|
||||
* Inspired by base2 and Prototype
|
||||
@@ -22,8 +23,10 @@
|
||||
Class.extend = function (prop) {
|
||||
let _super = this.prototype;
|
||||
|
||||
// Instantiate a base class (but only create the instance,
|
||||
// don't run the init constructor)
|
||||
/*
|
||||
* Instantiate a base class (but only create the instance,
|
||||
* don't run the init constructor)
|
||||
*/
|
||||
initializing = true;
|
||||
const prototype = new this();
|
||||
initializing = false;
|
||||
@@ -42,12 +45,16 @@
|
||||
return function () {
|
||||
const tmp = this._super;
|
||||
|
||||
// Add a new ._super() method that is the same method
|
||||
// but on the super-class
|
||||
/*
|
||||
* 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
|
||||
/*
|
||||
* 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;
|
||||
|
||||
|
@@ -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") {
|
||||
@@ -25,8 +19,9 @@ const defaults = {
|
||||
units: "metric",
|
||||
zoom: 1,
|
||||
customCss: "css/custom.css",
|
||||
foreignModulesDir: "modules",
|
||||
// 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
|
||||
@@ -67,7 +62,7 @@ const defaults = {
|
||||
position: "middle_center",
|
||||
classes: "xsmall",
|
||||
config: {
|
||||
text: "If you get this message while your config file is already created,<br>" + "it probably contains an error. To validate your config file run in your MagicMirror² directory<br>" + "<pre>npm run config:check</pre>"
|
||||
text: "If you get this message while your config file is already created,<br>" + "it probably contains an error. To validate your config file run in your MagicMirror² directory<br>" + "<pre>node --run config:check</pre>"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -75,15 +70,10 @@ const defaults = {
|
||||
position: "bottom_bar",
|
||||
classes: "xsmall dimmed",
|
||||
config: {
|
||||
text: "www.michaelteeuw.nl"
|
||||
text: "https://magicmirror.builders/"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
paths: {
|
||||
modules: "modules",
|
||||
vendor: "vendor"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
|
@@ -1,11 +1,4 @@
|
||||
/* MagicMirror² Deprecated Config Options List
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* Olex S. original idea this deprecated option
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
configs: ["kioskmode"]
|
||||
configs: ["kioskmode"],
|
||||
clock: ["secondsColor"]
|
||||
};
|
||||
|
@@ -8,9 +8,12 @@ const Log = require("./logger");
|
||||
let config = process.env.config ? JSON.parse(process.env.config) : {};
|
||||
// Module to control application life.
|
||||
const app = electron.app;
|
||||
// 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.
|
||||
|
||||
/*
|
||||
* 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_ENABLE_GPU !== "1") {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
@@ -18,16 +21,21 @@ if (process.env.ELECTRON_ENABLE_GPU !== "1") {
|
||||
// Module to create native browser window.
|
||||
const BrowserWindow = electron.BrowserWindow;
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
/*
|
||||
* Keep a global reference of the window object, if you don't, the window will
|
||||
* be closed automatically when the JavaScript object is garbage collected.
|
||||
*/
|
||||
let mainWindow;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function createWindow () {
|
||||
// see https://www.electronjs.org/docs/latest/api/screen
|
||||
// Create a window that fills the screen's available work area.
|
||||
|
||||
/*
|
||||
* 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;
|
||||
@@ -40,6 +48,7 @@ function createWindow () {
|
||||
let electronOptionsDefaults = {
|
||||
width: electronSize.width,
|
||||
height: electronSize.height,
|
||||
icon: "mm2.png",
|
||||
x: 0,
|
||||
y: 0,
|
||||
darkTheme: true,
|
||||
@@ -51,8 +60,10 @@ function createWindow () {
|
||||
backgroundColor: "#000000"
|
||||
};
|
||||
|
||||
// DEPRECATED: "kioskmode" backwards compatibility, to be removed
|
||||
// settings these options directly instead provides cleaner interface
|
||||
/*
|
||||
* DEPRECATED: "kioskmode" backwards compatibility, to be removed
|
||||
* settings these options directly instead provides cleaner interface
|
||||
*/
|
||||
if (config.kioskmode) {
|
||||
electronOptionsDefaults.kiosk = true;
|
||||
} else {
|
||||
@@ -65,14 +76,33 @@ function createWindow () {
|
||||
|
||||
const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
|
||||
|
||||
if (process.env.JEST_WORKER_ID !== undefined && process.env.MOCK_DATE !== undefined) {
|
||||
// if we are running with jest and we want to mock the current date
|
||||
const fakeNow = new Date(process.env.MOCK_DATE).valueOf();
|
||||
Date = class extends Date {
|
||||
constructor (...args) {
|
||||
if (args.length === 0) {
|
||||
super(fakeNow);
|
||||
} else {
|
||||
super(...args);
|
||||
}
|
||||
}
|
||||
};
|
||||
const __DateNowOffset = fakeNow - Date.now();
|
||||
const __DateNow = Date.now;
|
||||
Date.now = () => __DateNow() + __DateNowOffset;
|
||||
}
|
||||
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow(electronOptions);
|
||||
|
||||
// and load the index.html of the app.
|
||||
// If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
|
||||
/*
|
||||
* and load the index.html of the app.
|
||||
* If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
|
||||
*/
|
||||
|
||||
let prefix;
|
||||
if ((config["tls"] !== null && config["tls"]) || config.useHttps) {
|
||||
if ((config.tls !== null && config.tls) || config.useHttps) {
|
||||
prefix = "https://";
|
||||
} else {
|
||||
prefix = "http://";
|
||||
@@ -82,7 +112,7 @@ function createWindow () {
|
||||
const port = process.env.MM_PORT || config.port;
|
||||
mainWindow.loadURL(`${prefix}${address}:${port}`);
|
||||
|
||||
// Open the DevTools if run with "npm start dev"
|
||||
// Open the DevTools if run with "node --run start:dev"
|
||||
if (process.argv.includes("dev")) {
|
||||
if (process.env.JEST_WORKER_ID !== undefined) {
|
||||
// if we are running with jest
|
||||
@@ -121,11 +151,11 @@ function createWindow () {
|
||||
//remove response headers that prevent sites of being embedded into iframes if configured
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
let curHeaders = details.responseHeaders;
|
||||
if (config["ignoreXOriginHeader"] || false) {
|
||||
if (config.ignoreXOriginHeader || false) {
|
||||
curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !(/x-frame-options/i).test(header[0])));
|
||||
}
|
||||
|
||||
if (config["ignoreContentSecurityPolicy"] || false) {
|
||||
if (config.ignoreContentSecurityPolicy || false) {
|
||||
curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !(/content-security-policy/i).test(header[0])));
|
||||
}
|
||||
|
||||
@@ -148,14 +178,18 @@ app.on("window-all-closed", function () {
|
||||
});
|
||||
|
||||
app.on("activate", function () {
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
|
||||
/*
|
||||
* On OS X it's common to re-create a window in the app when the
|
||||
* dock icon is clicked and there are no other windows open.
|
||||
*/
|
||||
if (mainWindow === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
/* This method will be called when SIGINT is received and will call
|
||||
/*
|
||||
* This method will be called when SIGINT is received and will call
|
||||
* each node_helper's stop function if it exists. Added to fix #1056
|
||||
*
|
||||
* Note: this is only used if running Electron. Otherwise
|
||||
@@ -186,8 +220,10 @@ if (process.env.clientonly) {
|
||||
});
|
||||
}
|
||||
|
||||
// Start the core application if server is run on localhost
|
||||
// This starts all node helpers and starts the webserver.
|
||||
/*
|
||||
* 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)) {
|
||||
core.start().then((c) => {
|
||||
config = c;
|
||||
|
75
js/loader.js
75
js/loader.js
@@ -1,11 +1,5 @@
|
||||
/* global defaultModules, vendor */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module and File loaders.
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Loader = (function () {
|
||||
|
||||
/* Create helper variables */
|
||||
@@ -16,6 +10,15 @@ const Loader = (function () {
|
||||
|
||||
/* Private Methods */
|
||||
|
||||
/**
|
||||
* Retrieve object of env variables.
|
||||
* @returns {object} with key: values as assembled in js/server_functions.js
|
||||
*/
|
||||
const getEnvVars = async function () {
|
||||
const res = await fetch(`${location.protocol}//${location.host}${config.basePath}env`);
|
||||
return JSON.parse(await res.text());
|
||||
};
|
||||
|
||||
/**
|
||||
* Loops through all modules and requests start for every module.
|
||||
*/
|
||||
@@ -56,26 +59,36 @@ 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate array with module information including module paths.
|
||||
* @returns {object[]} Module information.
|
||||
*/
|
||||
const getModuleData = function () {
|
||||
const getModuleData = async function () {
|
||||
const modules = getAllModules();
|
||||
const moduleFiles = [];
|
||||
const envVars = await getEnvVars();
|
||||
|
||||
modules.forEach(function (moduleData, index) {
|
||||
const module = moduleData.module;
|
||||
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = `${config.paths.modules}/${module}`;
|
||||
let moduleFolder = `${envVars.modulesDir}/${module}`;
|
||||
|
||||
if (defaultModules.indexOf(moduleName) !== -1) {
|
||||
moduleFolder = `${config.paths.modules}/default/${module}`;
|
||||
const defaultModuleFolder = `modules/default/${module}`;
|
||||
if (window.name !== "jsdom") {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
} else {
|
||||
// running in Jest, allow defaultModules placed under moduleDir for testing
|
||||
if (envVars.modulesDir === "modules") {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleData.disabled === true) {
|
||||
@@ -95,7 +108,8 @@ const Loader = (function () {
|
||||
header: moduleData.header,
|
||||
configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false,
|
||||
config: moduleData.config,
|
||||
classes: typeof moduleData.classes !== "undefined" ? `${moduleData.classes} ${module}` : module
|
||||
classes: typeof moduleData.classes !== "undefined" ? `${moduleData.classes} ${module}` : module,
|
||||
order: (typeof moduleData.order === "number" && Number.isInteger(moduleData.order)) ? moduleData.order : 0
|
||||
});
|
||||
});
|
||||
|
||||
@@ -171,6 +185,7 @@ const Loader = (function () {
|
||||
};
|
||||
script.onerror = function () {
|
||||
Log.error("Error on loading script:", fileName);
|
||||
script.remove();
|
||||
resolve();
|
||||
};
|
||||
document.getElementsByTagName("body")[0].appendChild(script);
|
||||
@@ -188,6 +203,7 @@ const Loader = (function () {
|
||||
};
|
||||
stylesheet.onerror = function () {
|
||||
Log.error("Error on loading stylesheet:", fileName);
|
||||
stylesheet.remove();
|
||||
resolve();
|
||||
};
|
||||
document.getElementsByTagName("head")[0].appendChild(stylesheet);
|
||||
@@ -202,27 +218,22 @@ const Loader = (function () {
|
||||
* Load all modules as defined in the config.
|
||||
*/
|
||||
async loadModules () {
|
||||
let moduleData = getModuleData();
|
||||
const moduleData = await getModuleData();
|
||||
const envVars = await getEnvVars();
|
||||
const customCss = envVars.customCss;
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>} when all modules are loaded
|
||||
*/
|
||||
const loadNextModule = async function () {
|
||||
if (moduleData.length > 0) {
|
||||
const nextModule = moduleData[0];
|
||||
await loadModule(nextModule);
|
||||
moduleData = moduleData.slice(1);
|
||||
await loadNextModule();
|
||||
} else {
|
||||
// All modules loaded. Load custom.css
|
||||
// This is done after all the modules so we can
|
||||
// overwrite all the defined styles.
|
||||
await loadFile(config.customCss);
|
||||
// custom.css loaded. Start all modules.
|
||||
await startModules();
|
||||
}
|
||||
};
|
||||
await loadNextModule();
|
||||
// Load all modules
|
||||
for (const module of moduleData) {
|
||||
await loadModule(module);
|
||||
}
|
||||
|
||||
// Load custom.css
|
||||
// Since this happens after loading the modules,
|
||||
// it overwrites the default styles.
|
||||
await loadFile(customCss);
|
||||
|
||||
// Start all modules.
|
||||
await startModules();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -249,7 +260,7 @@ const Loader = (function () {
|
||||
// This file is available in the vendor folder.
|
||||
// Load it from this vendor folder.
|
||||
loadedFiles.push(fileName.toLowerCase());
|
||||
return loadFile(`${config.paths.vendor}/${vendor[fileName]}`);
|
||||
return loadFile(`${vendor[fileName]}`);
|
||||
}
|
||||
|
||||
// File not loaded yet.
|
||||
|
52
js/logger.js
52
js/logger.js
@@ -1,19 +1,49 @@
|
||||
/* 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 { styleText } = require("node:util");
|
||||
|
||||
// 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);
|
||||
switch (method) {
|
||||
case "error":
|
||||
label = styleText("red", label);
|
||||
break;
|
||||
case "warn":
|
||||
label = styleText("yellow", label);
|
||||
break;
|
||||
case "debug":
|
||||
label = styleText("bgBlue", label);
|
||||
break;
|
||||
case "info":
|
||||
label = styleText("blue", label);
|
||||
break;
|
||||
}
|
||||
return label;
|
||||
},
|
||||
msg: (arg) => {
|
||||
const { method, defaultTokens } = arg;
|
||||
let msg = defaultTokens.msg(arg);
|
||||
switch (method) {
|
||||
case "error":
|
||||
msg = styleText("red", msg);
|
||||
break;
|
||||
case "warn":
|
||||
msg = styleText("yellow", msg);
|
||||
break;
|
||||
case "info":
|
||||
msg = styleText("blue", msg);
|
||||
break;
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Node, CommonJS-like
|
||||
|
44
js/main.js
44
js/main.js
@@ -1,11 +1,5 @@
|
||||
/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut */
|
||||
/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut, modulePositions */
|
||||
|
||||
/* MagicMirror²
|
||||
* Main System
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const MM = (function () {
|
||||
let modules = [];
|
||||
|
||||
@@ -36,6 +30,8 @@ const MM = (function () {
|
||||
dom.className = `module ${dom.className} ${module.data.classes}`;
|
||||
}
|
||||
|
||||
dom.style.order = (typeof module.data.order === "number" && Number.isInteger(module.data.order)) ? module.data.order : 0;
|
||||
|
||||
dom.opacity = 0;
|
||||
wrapper.appendChild(dom);
|
||||
|
||||
@@ -292,9 +288,9 @@ const MM = (function () {
|
||||
Log.debug(`${module.identifier} Force remove animateIn (in hide): ${module.hasAnimateIn}`);
|
||||
module.hasAnimateIn = false;
|
||||
}
|
||||
// haveAnimateName for verify if we are using AninateCSS library
|
||||
// haveAnimateName for verify if we are using AnimateCSS library
|
||||
// we check AnimateCSSOut Array for validate it
|
||||
// and finaly return the animate name or `null` (for default MM² animation)
|
||||
// and finally 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;
|
||||
@@ -363,7 +359,7 @@ const MM = (function () {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are no more lockstrings set, or the force option is set.
|
||||
// Check if there are no more lockStrings set, or the force option is set.
|
||||
// Otherwise cancel show action.
|
||||
if (module.lockStrings.length !== 0 && options.force !== true) {
|
||||
Log.log(`Will not show ${module.name}. LockStrings active: ${module.lockStrings.join(",")}`);
|
||||
@@ -386,7 +382,7 @@ const MM = (function () {
|
||||
|
||||
module.hidden = false;
|
||||
|
||||
// If forced show, clean current lockstrings.
|
||||
// If forced show, clean current lockStrings.
|
||||
if (module.lockStrings.length !== 0 && options.force === true) {
|
||||
Log.log(`Force show of module: ${module.name}`);
|
||||
module.lockStrings = [];
|
||||
@@ -396,9 +392,9 @@ const MM = (function () {
|
||||
if (moduleWrapper !== null) {
|
||||
clearTimeout(module.showHideTimer);
|
||||
|
||||
// haveAnimateName for verify if we are using AninateCSS library
|
||||
// haveAnimateName for verify if we are using AnimateCSS library
|
||||
// we check AnimateCSSIn Array for validate it
|
||||
// and finaly return the animate name or `null` (for default MM² animation)
|
||||
// and finally 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;
|
||||
@@ -456,10 +452,9 @@ 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"];
|
||||
|
||||
positions.forEach(function (position) {
|
||||
const updateWrapperStates = function () {
|
||||
modulePositions.forEach(function (position) {
|
||||
const wrapper = selectWrapper(position);
|
||||
const moduleWrappers = wrapper.getElementsByClassName("module");
|
||||
|
||||
@@ -470,7 +465,8 @@ const MM = (function () {
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.style.display = showWrapper ? "block" : "none";
|
||||
// move container definitions to main CSS
|
||||
wrapper.className = showWrapper ? "container" : "container hidden";
|
||||
});
|
||||
};
|
||||
|
||||
@@ -615,13 +611,13 @@ const MM = (function () {
|
||||
// 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 res = await fetch(`${location.protocol}//${location.host}${config.basePath}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");
|
||||
Log.warn("Refreshing Website because server was restarted");
|
||||
}
|
||||
} catch (err) {
|
||||
Log.error(`MagicMirror not reachable: ${err}`);
|
||||
@@ -673,7 +669,10 @@ const MM = (function () {
|
||||
}
|
||||
|
||||
// Further implementation is done in the private method.
|
||||
updateDom(module, updateOptions);
|
||||
updateDom(module, updateOptions).then(function () {
|
||||
// Once the update is complete and rendered, send a notification to the module that the DOM has been updated
|
||||
sendNotification("MODULE_DOM_UPDATED", null, null, module);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -707,7 +706,10 @@ const MM = (function () {
|
||||
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 positions.
|
||||
getAvailableModulePositions: modulePositions
|
||||
};
|
||||
}());
|
||||
|
||||
|
31
js/module.js
31
js/module.js
@@ -1,17 +1,16 @@
|
||||
/* global Class, cloneObject, Loader, MMSocket, nunjucks, Translator */
|
||||
|
||||
/* MagicMirror²
|
||||
/*
|
||||
* 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. *
|
||||
*********************************************************/
|
||||
********************************************************
|
||||
*/
|
||||
|
||||
// Set the minimum MagicMirror² module version for this module.
|
||||
requiresVersion: "2.0.0",
|
||||
@@ -22,13 +21,17 @@ const Module = Class.extend({
|
||||
// Timer reference used for showHide animation callbacks.
|
||||
showHideTimer: null,
|
||||
|
||||
// Array to store lockStrings. These strings are used to lock
|
||||
// visibility when hiding and showing module.
|
||||
/*
|
||||
* Array to store lockStrings. These strings are used to lock
|
||||
* visibility when hiding and showing module.
|
||||
*/
|
||||
lockStrings: [],
|
||||
|
||||
// Storage of the nunjucks Environment,
|
||||
// This should not be referenced directly.
|
||||
// Use the nunjucksEnvironment() to get it.
|
||||
/*
|
||||
* Storage of the nunjucks Environment,
|
||||
* This should not be referenced directly.
|
||||
* Use the nunjucksEnvironment() to get it.
|
||||
*/
|
||||
_nunjucksEnvironment: null,
|
||||
|
||||
/**
|
||||
@@ -193,9 +196,11 @@ const Module = Class.extend({
|
||||
Log.log(`${this.name} is resumed.`);
|
||||
},
|
||||
|
||||
/*********************************************
|
||||
/**
|
||||
********************************************
|
||||
* The methods below don't need subclassing. *
|
||||
*********************************************/
|
||||
********************************************
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set the module data.
|
||||
|
@@ -1,9 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* Node Helper Superclass
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const express = require("express");
|
||||
const Log = require("logger");
|
||||
const Class = require("./class");
|
||||
@@ -55,7 +49,8 @@ const NodeHelper = Class.extend({
|
||||
this.path = path;
|
||||
},
|
||||
|
||||
/* sendSocketNotification(notification, payload)
|
||||
/*
|
||||
* sendSocketNotification(notification, payload)
|
||||
* Send a socket notification to the node helper.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
@@ -65,7 +60,8 @@ const NodeHelper = Class.extend({
|
||||
this.io.of(this.name).emit(notification, payload);
|
||||
},
|
||||
|
||||
/* setExpressApp(app)
|
||||
/*
|
||||
* setExpressApp(app)
|
||||
* Sets the express app object for this module.
|
||||
* This allows you to host files from the created webserver.
|
||||
*
|
||||
@@ -77,7 +73,8 @@ const NodeHelper = Class.extend({
|
||||
app.use(`/${this.name}`, express.static(`${this.path}/public`));
|
||||
},
|
||||
|
||||
/* setSocketIO(io)
|
||||
/*
|
||||
* setSocketIO(io)
|
||||
* Sets the socket io object for this module.
|
||||
* Binds message receiver.
|
||||
*
|
||||
@@ -89,20 +86,9 @@ const NodeHelper = Class.extend({
|
||||
Log.log(`Connecting socket for: ${this.name}`);
|
||||
|
||||
io.of(this.name).on("connection", (socket) => {
|
||||
// add a catch all event.
|
||||
const onevent = socket.onevent;
|
||||
socket.onevent = function (packet) {
|
||||
const args = packet.data || [];
|
||||
onevent.call(this, packet); // original call
|
||||
packet.data = ["*"].concat(args);
|
||||
onevent.call(this, packet); // additional call to catch-all
|
||||
};
|
||||
|
||||
// register catch all.
|
||||
socket.on("*", (notification, payload) => {
|
||||
if (notification !== "*") {
|
||||
this.socketNotificationReceived(notification, payload);
|
||||
}
|
||||
socket.onAny((notification, payload) => {
|
||||
this.socketNotificationReceived(notification, payload);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
34
js/server.js
34
js/server.js
@@ -1,21 +1,15 @@
|
||||
/* 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");
|
||||
const socketio = require("socket.io");
|
||||
|
||||
const Log = require("logger");
|
||||
const Utils = require("./utils");
|
||||
const { cors, getConfig, getHtml, getVersion, getStartup } = require("./server_functions");
|
||||
const { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars } = require("./server_functions");
|
||||
|
||||
const vendor = require(`${__dirname}/vendor`);
|
||||
|
||||
/**
|
||||
* Server
|
||||
@@ -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) {
|
||||
@@ -79,9 +73,13 @@ function Server (config) {
|
||||
app.use(helmet(config.httpHeaders));
|
||||
app.use("/js", express.static(__dirname));
|
||||
|
||||
// TODO add tests directory only when running tests?
|
||||
const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs", "/tests/mocks"];
|
||||
for (const directory of directories) {
|
||||
let directories = ["/config", "/css", "/modules", "/node_modules/animate.css", "/node_modules/@fontsource", "/node_modules/@fortawesome", "/translations", "/tests/configs", "/tests/mocks"];
|
||||
for (const [key, value] of Object.entries(vendor)) {
|
||||
const dirArr = value.split("/");
|
||||
if (dirArr[0] === "node_modules") directories.push(`/${dirArr[0]}/${dirArr[1]}`);
|
||||
}
|
||||
const uniqDirs = [...new Set(directories)];
|
||||
for (const directory of uniqDirs) {
|
||||
app.use(directory, express.static(path.resolve(global.root_path + directory)));
|
||||
}
|
||||
|
||||
@@ -93,6 +91,8 @@ function Server (config) {
|
||||
|
||||
app.get("/startup", (req, res) => getStartup(req, res));
|
||||
|
||||
app.get("/env", (req, res) => getEnvVars(req, res));
|
||||
|
||||
app.get("/", (req, res) => getHtml(req, res));
|
||||
|
||||
server.on("listening", () => {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const Log = require("logger");
|
||||
|
||||
const startUp = new Date();
|
||||
@@ -45,12 +45,12 @@ async function cors (req, res) {
|
||||
url = match[1];
|
||||
|
||||
const headersToSend = getHeadersToSend(req.url);
|
||||
const expectedRecievedHeaders = geExpectedRecievedHeaders(req.url);
|
||||
const expectedReceivedHeaders = geExpectedReceivedHeaders(req.url);
|
||||
|
||||
Log.log(`cors url: ${url}`);
|
||||
const response = await fetch(url, { headers: headersToSend });
|
||||
|
||||
for (const header of expectedRecievedHeaders) {
|
||||
for (const header of expectedReceivedHeaders) {
|
||||
const headerValue = response.headers.get(header);
|
||||
if (header) res.set(header, headerValue);
|
||||
}
|
||||
@@ -89,16 +89,16 @@ 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) {
|
||||
const expectedRecievedHeaders = ["Content-Type"];
|
||||
const expectedRecievedHeadersMatch = new RegExp("expectedheaders=(.+?)(&|$)", "g").exec(url);
|
||||
if (expectedRecievedHeadersMatch) {
|
||||
const headers = expectedRecievedHeadersMatch[1].split(",");
|
||||
function geExpectedReceivedHeaders (url) {
|
||||
const expectedReceivedHeaders = ["Content-Type"];
|
||||
const expectedReceivedHeadersMatch = new RegExp("expectedheaders=(.+?)(&|$)", "g").exec(url);
|
||||
if (expectedReceivedHeadersMatch) {
|
||||
const headers = expectedReceivedHeadersMatch[1].split(",");
|
||||
for (const header of headers) {
|
||||
expectedRecievedHeaders.push(header);
|
||||
expectedReceivedHeaders.push(header);
|
||||
}
|
||||
}
|
||||
return expectedRecievedHeaders;
|
||||
return expectedReceivedHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,6 +109,7 @@ function geExpectedRecievedHeaders (url) {
|
||||
function getHtml (req, res) {
|
||||
let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" });
|
||||
html = html.replace("#VERSION#", global.version);
|
||||
html = html.replace("#TESTMODE#", global.mmTestMode);
|
||||
|
||||
let configFile = "config/config.js";
|
||||
if (typeof global.configuration_file !== "undefined") {
|
||||
@@ -128,4 +129,30 @@ function getVersion (req, res) {
|
||||
res.send(global.version);
|
||||
}
|
||||
|
||||
module.exports = { cors, getConfig, getHtml, getVersion, getStartup };
|
||||
/**
|
||||
* Gets environment variables needed in the browser.
|
||||
* @returns {object} environment variables key: values
|
||||
*/
|
||||
function getEnvVarsAsObj () {
|
||||
const obj = { modulesDir: `${config.foreignModulesDir}`, customCss: `${config.customCss}` };
|
||||
if (process.env.MM_MODULES_DIR) {
|
||||
obj.modulesDir = process.env.MM_MODULES_DIR.replace(`${global.root_path}/`, "");
|
||||
}
|
||||
if (process.env.MM_CUSTOMCSS_FILE) {
|
||||
obj.customCss = process.env.MM_CUSTOMCSS_FILE.replace(`${global.root_path}/`, "");
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets environment variables needed in the browser.
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
function getEnvVars (req, res) {
|
||||
const obj = getEnvVarsAsObj();
|
||||
res.send(obj);
|
||||
}
|
||||
|
||||
module.exports = { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars, getEnvVarsAsObj };
|
||||
|
@@ -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,11 +1,5 @@
|
||||
/* global translations */
|
||||
|
||||
/* MagicMirror²
|
||||
* Translator (l10n)
|
||||
*
|
||||
* By Christopher Fenner https://github.com/CFenner
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const Translator = (function () {
|
||||
|
||||
/**
|
||||
@@ -21,14 +15,14 @@ const Translator = (function () {
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
// needs error handler try/catch at least
|
||||
let fileinfo = null;
|
||||
let fileInfo = null;
|
||||
try {
|
||||
fileinfo = JSON.parse(xhr.responseText);
|
||||
fileInfo = JSON.parse(xhr.responseText);
|
||||
} catch (exception) {
|
||||
// nothing here, but don't die
|
||||
Log.error(` loading json file =${file} failed`);
|
||||
}
|
||||
resolve(fileinfo);
|
||||
resolve(fileInfo);
|
||||
}
|
||||
};
|
||||
xhr.send(null);
|
||||
|
87
js/utils.js
87
js/utils.js
@@ -1,16 +1,79 @@
|
||||
/* 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 path = require("node:path");
|
||||
|
||||
const rootPath = path.resolve(`${__dirname}/../`);
|
||||
const Log = require(`${rootPath}/js/logger.js`);
|
||||
const os = require("node:os");
|
||||
const fs = require("node:fs");
|
||||
const si = require("systeminformation");
|
||||
|
||||
const modulePositions = []; // will get list from index.html
|
||||
const regionRegEx = /"region ([^"]*)/i;
|
||||
const indexFileName = "index.html";
|
||||
const discoveredPositionsJSFilename = "js/positions.js";
|
||||
|
||||
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, virtual",
|
||||
osInfo: "platform, distro, release, arch",
|
||||
versions: "kernel, node, npm, pm2"
|
||||
});
|
||||
let systemDataString = `System information:
|
||||
### SYSTEM: manufacturer: ${staticData.system.manufacturer}; model: ${staticData.system.model}; virtual: ${staticData.system.virtual}
|
||||
### OS: platform: ${staticData.osInfo.platform}; distro: ${staticData.osInfo.distro}; release: ${staticData.osInfo.release}; arch: ${staticData.osInfo.arch}; kernel: ${staticData.versions.kernel}
|
||||
### VERSIONS: electron: ${process.versions.electron}; used node: ${staticData.versions.node}; installed node: ${installedNodeVersion}; npm: ${staticData.versions.npm}; pm2: ${staticData.versions.pm2}
|
||||
### OTHER: timeZone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}; ELECTRON_ENABLE_GPU: ${process.env.ELECTRON_ENABLE_GPU}`
|
||||
.replace(/\t/g, "");
|
||||
Log.info(systemDataString);
|
||||
|
||||
// Return is currently only for jest
|
||||
return systemDataString;
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
}
|
||||
},
|
||||
|
||||
// return all available module positions
|
||||
getAvailableModulePositions () {
|
||||
return modulePositions;
|
||||
},
|
||||
|
||||
// return if position is on modulePositions Array (true/false)
|
||||
moduleHasValidPosition (position) {
|
||||
if (this.getAvailableModulePositions().indexOf(position) === -1) return false;
|
||||
return true;
|
||||
},
|
||||
|
||||
getModulePositions () {
|
||||
// if not already discovered
|
||||
if (modulePositions.length === 0) {
|
||||
// get the lines of the index.html
|
||||
const lines = fs.readFileSync(indexFileName).toString().split("\n");
|
||||
// loop thru the lines
|
||||
lines.forEach((line) => {
|
||||
// run the regex on each line
|
||||
const results = regionRegEx.exec(line);
|
||||
// if the regex returned something
|
||||
if (results && results.length > 0) {
|
||||
// get the position parts and replace space with underscore
|
||||
const positionName = results[1].replace(" ", "_");
|
||||
// add it to the list
|
||||
modulePositions.push(positionName);
|
||||
}
|
||||
});
|
||||
try {
|
||||
fs.writeFileSync(discoveredPositionsJSFilename, `const modulePositions=${JSON.stringify(modulePositions)}`);
|
||||
}
|
||||
catch (error) {
|
||||
Log.error("unable to write js/positions.js with the discovered module positions\nmake the MagicMirror/js folder writeable by the user starting MagicMirror");
|
||||
}
|
||||
}
|
||||
// return the list to the caller
|
||||
return modulePositions;
|
||||
}
|
||||
};
|
||||
|
@@ -1,9 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* Vendor File Definition
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const vendor = {
|
||||
"moment.js": "node_modules/moment/min/moment-with-locales.js",
|
||||
"moment-timezone.js": "node_modules/moment-timezone/builds/moment-timezone-with-data.js",
|
||||
@@ -11,7 +5,8 @@ const vendor = {
|
||||
"weather-icons-wind.css": "node_modules/weathericons/css/weather-icons-wind.css",
|
||||
"font-awesome.css": "css/font-awesome.css",
|
||||
"nunjucks.js": "node_modules/nunjucks/browser/nunjucks.min.js",
|
||||
"suncalc.js": "node_modules/suncalc/suncalc.js"
|
||||
"suncalc.js": "node_modules/suncalc/suncalc.js",
|
||||
"croner.js": "node_modules/croner/dist/croner.umd.js"
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") {
|
@@ -1,11 +1,5 @@
|
||||
/* global NotificationFx */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: alert
|
||||
*
|
||||
* By Paul-Vincent Roll https://paulvincentroll.com/
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("alert", {
|
||||
alerts: {},
|
||||
|
||||
@@ -31,6 +25,7 @@ Module.register("alert", {
|
||||
da: "translations/da.json",
|
||||
de: "translations/de.json",
|
||||
en: "translations/en.json",
|
||||
eo: "translations/eo.json",
|
||||
es: "translations/es.json",
|
||||
fr: "translations/fr.json",
|
||||
hu: "translations/hu.json",
|
||||
|
4
modules/default/alert/translations/el.json
Normal file
4
modules/default/alert/translations/el.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror² Ειδοποίηση",
|
||||
"welcome": "Καλώς ήρθατε, η εκκίνηση ήταν επιτυχής!"
|
||||
}
|
4
modules/default/alert/translations/eo.json
Normal file
4
modules/default/alert/translations/eo.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sysTitle": "MagicMirror²-sciigo",
|
||||
"welcome": "Bonvenon, lanĉo sukcesis!"
|
||||
}
|
@@ -1,11 +1,5 @@
|
||||
/* global CalendarUtils */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Calendar
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("calendar", {
|
||||
// Define module defaults
|
||||
defaults: {
|
||||
@@ -35,6 +29,7 @@ Module.register("calendar", {
|
||||
dateEndFormat: "LT",
|
||||
fullDayEventDateFormat: "MMM Do",
|
||||
showEnd: false,
|
||||
showEndsOnlyWithDuration: false,
|
||||
getRelative: 6,
|
||||
hidePrivate: false,
|
||||
hideOngoing: false,
|
||||
@@ -42,6 +37,7 @@ Module.register("calendar", {
|
||||
hideDuplicates: true,
|
||||
showTimeToday: false,
|
||||
colored: false,
|
||||
forceUseCurrentTime: false,
|
||||
tableClass: "small",
|
||||
calendars: [
|
||||
{
|
||||
@@ -81,14 +77,17 @@ Module.register("calendar", {
|
||||
|
||||
// Define required scripts.
|
||||
getScripts () {
|
||||
return ["calendarutils.js", "moment.js"];
|
||||
return ["calendarutils.js", "moment.js", "moment-timezone.js"];
|
||||
},
|
||||
|
||||
// Define required translations.
|
||||
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.
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
return false;
|
||||
},
|
||||
|
||||
@@ -97,12 +96,12 @@ Module.register("calendar", {
|
||||
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;
|
||||
}
|
||||
@@ -152,8 +151,10 @@ Module.register("calendar", {
|
||||
};
|
||||
}
|
||||
|
||||
// tell helper to start a fetcher for this calendar
|
||||
// fetcher till cycle
|
||||
/*
|
||||
* tell helper to start a fetcher for this calendar
|
||||
* fetcher till cycle
|
||||
*/
|
||||
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
||||
});
|
||||
|
||||
@@ -167,12 +168,17 @@ Module.register("calendar", {
|
||||
|
||||
this.selfUpdate();
|
||||
},
|
||||
notificationReceived (notification, payload, sender) {
|
||||
|
||||
if (notification === "FETCH_CALENDAR") {
|
||||
if (this.hasCalendarURL(payload.url)) {
|
||||
this.sendSocketNotification(notification, { url: payload.url, id: this.identifier });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Override socket notification handler.
|
||||
socketNotificationReceived (notification, payload) {
|
||||
if (notification === "FETCH_CALENDAR") {
|
||||
this.sendSocketNotification(notification, { url: payload.url, id: this.identifier });
|
||||
}
|
||||
|
||||
if (this.identifier !== payload.id) {
|
||||
return;
|
||||
@@ -212,10 +218,6 @@ Module.register("calendar", {
|
||||
// Override dom generator.
|
||||
getDom () {
|
||||
const ONE_SECOND = 1000; // 1,000 milliseconds
|
||||
const ONE_MINUTE = ONE_SECOND * 60;
|
||||
const ONE_HOUR = ONE_MINUTE * 60;
|
||||
const ONE_DAY = ONE_HOUR * 24;
|
||||
|
||||
const events = this.createEventList(true);
|
||||
const wrapper = document.createElement("table");
|
||||
wrapper.className = this.config.tableClass;
|
||||
@@ -247,7 +249,9 @@ Module.register("calendar", {
|
||||
let lastSeenDate = "";
|
||||
|
||||
events.forEach((event, index) => {
|
||||
const dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
|
||||
const eventStartDateMoment = this.timestampToMoment(event.startDate);
|
||||
const eventEndDateMoment = this.timestampToMoment(event.endDate);
|
||||
const dateAsString = eventStartDateMoment.format(this.config.dateFormat);
|
||||
if (this.config.timeFormat === "dateheaders") {
|
||||
if (lastSeenDate !== dateAsString) {
|
||||
const dateRow = document.createElement("tr");
|
||||
@@ -329,7 +333,7 @@ Module.register("calendar", {
|
||||
repeatingCountTitle = this.countTitleForUrl(event.url);
|
||||
|
||||
if (repeatingCountTitle !== "") {
|
||||
const thisYear = new Date(parseInt(event.startDate)).getFullYear(),
|
||||
const thisYear = eventStartDateMoment.year(),
|
||||
yearDiff = thisYear - event.firstYear;
|
||||
|
||||
repeatingCountTitle = `, ${yearDiff} ${repeatingCountTitle}`;
|
||||
@@ -384,11 +388,15 @@ Module.register("calendar", {
|
||||
timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(event.url)}`;
|
||||
timeWrapper.style.paddingLeft = "2px";
|
||||
timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left";
|
||||
timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
|
||||
timeWrapper.innerHTML = eventStartDateMoment.format("LT");
|
||||
|
||||
// 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(eventEndDateMoment.format("LT"))}`;
|
||||
}
|
||||
}
|
||||
|
||||
eventWrapper.appendChild(timeWrapper);
|
||||
@@ -400,32 +408,43 @@ Module.register("calendar", {
|
||||
const timeWrapper = document.createElement("td");
|
||||
|
||||
eventWrapper.appendChild(titleWrapper);
|
||||
const now = new Date();
|
||||
const now = moment();
|
||||
|
||||
if (this.config.timeFormat === "absolute") {
|
||||
// Use dateFormat
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.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));
|
||||
// and has a duation
|
||||
if (event.startDate !== event.endDate) {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += CalendarUtils.capFirst(eventEndDateMoment.format(this.config.dateEndFormat));
|
||||
}
|
||||
}
|
||||
|
||||
// For full day events we use the fullDayEventDateFormat
|
||||
if (event.fullDayEvent) {
|
||||
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
||||
event.endDate -= ONE_SECOND;
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
||||
} else if (this.config.getRelative > 0 && event.startDate < now) {
|
||||
eventEndDateMoment.subtract(1, "second");
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.format(this.config.fullDayEventDateFormat));
|
||||
// only show end if requested and allowed and the dates are different
|
||||
if (this.config.showEnd && !this.config.showEndsOnlyWithDuration && !eventStartDateMoment.isSame(eventEndDateMoment, "d")) {
|
||||
timeWrapper.innerHTML += "-";
|
||||
timeWrapper.innerHTML += CalendarUtils.capFirst(eventEndDateMoment.format(this.config.fullDayEventDateFormat));
|
||||
} else if (!eventStartDateMoment.isSame(eventEndDateMoment, "d") && eventStartDateMoment.isBefore(now)) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(now.format(this.config.fullDayEventDateFormat));
|
||||
}
|
||||
} else if (this.config.getRelative > 0 && eventStartDateMoment.isBefore(now)) {
|
||||
// Ongoing and getRelative is set
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
|
||||
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
||||
timeUntilEnd: eventEndDateMoment.fromNow(true)
|
||||
})
|
||||
);
|
||||
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * ONE_DAY) {
|
||||
} else if (this.config.urgency > 0 && eventStartDateMoment.diff(now, "d") < this.config.urgency) {
|
||||
// Within urgency days
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow());
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(eventStartDateMoment.fromNow());
|
||||
}
|
||||
if (event.fullDayEvent && this.config.nextDaysRelative) {
|
||||
// Full days events within the next two days
|
||||
@@ -433,9 +452,9 @@ Module.register("calendar", {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
|
||||
} else if (event.yesterday) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
|
||||
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
|
||||
} else if (event.tomorrow) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
|
||||
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
|
||||
} else if (event.dayAfterTomorrow) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
}
|
||||
@@ -443,23 +462,25 @@ Module.register("calendar", {
|
||||
}
|
||||
} else {
|
||||
// Show relative times
|
||||
if (event.startDate >= now || (event.fullDayEvent && event.today)) {
|
||||
if (eventStartDateMoment.isSameOrAfter(now) || (event.fullDayEvent && eventEndDateMoment.diff(now, "days") === 0)) {
|
||||
// Use relative time
|
||||
if (!this.config.hideTime && !event.fullDayEvent) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
|
||||
Log.debug("event not hidden and not fullday");
|
||||
timeWrapper.innerHTML = `${CalendarUtils.capFirst(eventStartDateMoment.calendar(null, { sameElse: this.config.dateFormat }))}`;
|
||||
} else {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(
|
||||
moment(event.startDate, "x").calendar(null, {
|
||||
Log.debug("event full day or hidden");
|
||||
timeWrapper.innerHTML = `${CalendarUtils.capFirst(
|
||||
eventStartDateMoment.calendar(null, {
|
||||
sameDay: this.config.showTimeToday ? "LT" : `[${this.translate("TODAY")}]`,
|
||||
nextDay: `[${this.translate("TOMORROW")}]`,
|
||||
nextWeek: "dddd",
|
||||
sameElse: event.fullDayEvent ? this.config.fullDayEventDateFormat : this.config.dateFormat
|
||||
})
|
||||
);
|
||||
)}`;
|
||||
}
|
||||
if (event.fullDayEvent) {
|
||||
// Full days events within the next two days
|
||||
if (event.today) {
|
||||
if (event.today || (event.fullDayEvent && eventEndDateMoment.diff(now, "days") === 0)) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
|
||||
} else if (event.dayBeforeYesterday) {
|
||||
if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") {
|
||||
@@ -467,23 +488,25 @@ Module.register("calendar", {
|
||||
}
|
||||
} else if (event.yesterday) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
|
||||
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
|
||||
} else if (event.tomorrow) {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
|
||||
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
|
||||
} else if (event.dayAfterTomorrow) {
|
||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||
}
|
||||
}
|
||||
} else if (event.startDate - now < this.config.getRelative * ONE_HOUR) {
|
||||
Log.info("event fullday");
|
||||
} else if (eventStartDateMoment.diff(now, "h") < this.config.getRelative) {
|
||||
Log.info("not full day but within getrelative size");
|
||||
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow());
|
||||
timeWrapper.innerHTML = `${CalendarUtils.capFirst(eventStartDateMoment.fromNow())}`;
|
||||
}
|
||||
} else {
|
||||
// Ongoing event
|
||||
timeWrapper.innerHTML = CalendarUtils.capFirst(
|
||||
this.translate("RUNNING", {
|
||||
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
|
||||
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
||||
timeUntilEnd: eventEndDateMoment.fromNow(true)
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -562,97 +585,114 @@ Module.register("calendar", {
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* converts the given timestamp to a moment with a timezone
|
||||
* @param {number} timestamp timestamp from an event
|
||||
* @returns {moment.Moment} moment with a timezone
|
||||
*/
|
||||
timestampToMoment (timestamp) {
|
||||
return moment(timestamp, "x").tz(moment.tz.guess());
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates the sorted list of all events.
|
||||
* @param {boolean} limitNumberOfEntries Whether to filter returned events for display.
|
||||
* @returns {object[]} Array with events.
|
||||
*/
|
||||
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;
|
||||
let now = moment();
|
||||
let today = now.clone().startOf("day");
|
||||
let future = now.clone().startOf("day").add(this.config.maximumNumberOfDays, "days");
|
||||
|
||||
const now = new Date();
|
||||
const today = moment().startOf("day");
|
||||
const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
|
||||
let events = [];
|
||||
|
||||
for (const calendarUrl in this.calendarData) {
|
||||
const calendar = this.calendarData[calendarUrl];
|
||||
let remainingEntries = this.maximumEntriesForUrl(calendarUrl);
|
||||
let maxPastDaysCompare = now - this.maximumPastDaysForUrl(calendarUrl) * ONE_DAY;
|
||||
let maxPastDaysCompare = now.clone().subtract(this.maximumPastDaysForUrl(calendarUrl), "days");
|
||||
let by_url_calevents = [];
|
||||
for (const e in calendar) {
|
||||
const event = JSON.parse(JSON.stringify(calendar[e])); // clone object
|
||||
const eventStartDateMoment = this.timestampToMoment(event.startDate);
|
||||
const eventEndDateMoment = this.timestampToMoment(event.endDate);
|
||||
|
||||
if (this.config.hidePrivate && event.class === "PRIVATE") {
|
||||
// do not add the current event, skip it
|
||||
continue;
|
||||
}
|
||||
if (limitNumberOfEntries) {
|
||||
if (event.endDate < maxPastDaysCompare) {
|
||||
if (eventEndDateMoment.isBefore(maxPastDaysCompare)) {
|
||||
continue;
|
||||
}
|
||||
if (this.config.hideOngoing && event.startDate < now) {
|
||||
if (this.config.hideOngoing && eventStartDateMoment.isBefore(now)) {
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
event.yesterday = event.startDate >= today - ONE_DAY && event.startDate < today;
|
||||
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
|
||||
event.dayAfterTomorrow = !event.tomorrow && event.startDate >= today + ONE_DAY * 2 && event.startDate < today + 3 * ONE_DAY;
|
||||
event.today = eventStartDateMoment.isSame(now, "d");
|
||||
event.dayBeforeYesterday = eventStartDateMoment.isSame(now.clone().subtract(2, "days"), "d");
|
||||
event.yesterday = eventStartDateMoment.isSame(now.clone().subtract(1, "days"), "d");
|
||||
event.tomorrow = eventStartDateMoment.isSame(now.clone().add(1, "days"), "d");
|
||||
event.dayAfterTomorrow = eventStartDateMoment.isSame(now.clone().add(2, "days"), "d");
|
||||
|
||||
/* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
|
||||
/*
|
||||
* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
|
||||
* otherwise, esp. in dateheaders mode it is not clear how long these events are.
|
||||
*/
|
||||
const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / ONE_DAY) + 1;
|
||||
const maxCount = eventEndDateMoment.diff(eventStartDateMoment, "days");
|
||||
if (this.config.sliceMultiDayEvents && maxCount > 1) {
|
||||
const splitEvents = [];
|
||||
let midnight
|
||||
= moment(event.startDate, "x")
|
||||
= eventStartDateMoment
|
||||
.clone()
|
||||
.startOf("day")
|
||||
.add(1, "day")
|
||||
.format("x");
|
||||
.endOf("day");
|
||||
let count = 1;
|
||||
while (event.endDate > midnight) {
|
||||
while (eventEndDateMoment.isAfter(midnight)) {
|
||||
const thisEvent = JSON.parse(JSON.stringify(event)); // clone object
|
||||
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + ONE_DAY;
|
||||
thisEvent.tomorrow = !thisEvent.today && thisEvent.startDate >= today + ONE_DAY && thisEvent.startDate < today + 2 * ONE_DAY;
|
||||
thisEvent.endDate = midnight;
|
||||
thisEvent.today = this.timestampToMoment(thisEvent.startDate).isSame(now, "d");
|
||||
thisEvent.tomorrow = this.timestampToMoment(thisEvent.startDate).isSame(now.clone().add(1, "days"), "d");
|
||||
thisEvent.endDate = midnight.clone().subtract(1, "day").format("x");
|
||||
thisEvent.title += ` (${count}/${maxCount})`;
|
||||
splitEvents.push(thisEvent);
|
||||
|
||||
event.startDate = midnight;
|
||||
event.startDate = midnight.format("x");
|
||||
count += 1;
|
||||
midnight = moment(midnight, "x").add(1, "day").format("x"); // next day
|
||||
midnight = midnight.clone().add(1, "day").endOf("day"); // next day
|
||||
}
|
||||
// Last day
|
||||
event.title += ` (${count}/${maxCount})`;
|
||||
event.today += event.startDate >= today && event.startDate < today + ONE_DAY;
|
||||
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
|
||||
event.today += this.timestampToMoment(event.startDate).isSame(now, "d");
|
||||
event.tomorrow = this.timestampToMoment(event.startDate).isSame(now.clone().add(1, "days"), "d");
|
||||
splitEvents.push(event);
|
||||
|
||||
for (let splitEvent of splitEvents) {
|
||||
if (splitEvent.endDate > now && splitEvent.endDate <= future) {
|
||||
events.push(splitEvent);
|
||||
if (this.timestampToMoment(splitEvent.endDate).isAfter(now) && this.timestampToMoment(splitEvent.endDate).isSameOrBefore(future)) {
|
||||
by_url_calevents.push(splitEvent);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
events.push(event);
|
||||
by_url_calevents.push(event);
|
||||
}
|
||||
}
|
||||
if (limitNumberOfEntries) {
|
||||
// sort entries before clipping
|
||||
by_url_calevents.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
Log.debug(`pushing ${by_url_calevents.length} events to total with room for ${remainingEntries}`);
|
||||
events = events.concat(by_url_calevents.slice(0, remainingEntries));
|
||||
Log.debug(`events for calendar=${events.length}`);
|
||||
} else {
|
||||
events = events.concat(by_url_calevents);
|
||||
}
|
||||
}
|
||||
|
||||
Log.info(`sorting events count=${events.length}`);
|
||||
events.sort(function (a, b) {
|
||||
return a.startDate - b.startDate;
|
||||
});
|
||||
@@ -661,17 +701,22 @@ Module.register("calendar", {
|
||||
return events;
|
||||
}
|
||||
|
||||
// Limit the number of days displayed
|
||||
// If limitDays is set > 0, limit display to that number of days
|
||||
/*
|
||||
* Limit the number of days displayed
|
||||
* If limitDays is set > 0, limit display to that number of days
|
||||
*/
|
||||
if (this.config.limitDays > 0) {
|
||||
let newEvents = [];
|
||||
let lastDate = today.clone().subtract(1, "days").format("YYYYMMDD");
|
||||
let lastDate = today.clone().subtract(1, "days");
|
||||
let days = 0;
|
||||
for (const ev of events) {
|
||||
let eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
|
||||
// if date of event is later than lastdate
|
||||
// check if we already are showing max unique days
|
||||
if (eventDate > lastDate) {
|
||||
let eventDate = this.timestampToMoment(ev.startDate);
|
||||
|
||||
/*
|
||||
* if date of event is later than lastdate
|
||||
* check if we already are showing max unique days
|
||||
*/
|
||||
if (eventDate.isAfter(lastDate)) {
|
||||
// if the only entry in the first day is a full day event that day is not counted as unique
|
||||
if (!this.config.limitDaysNeverSkip && newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
|
||||
days--;
|
||||
@@ -687,7 +732,7 @@ Module.register("calendar", {
|
||||
}
|
||||
events = newEvents;
|
||||
}
|
||||
|
||||
Log.info(`slicing events total maxcount=${this.config.maximumEntries}`);
|
||||
return events.slice(0, this.config.maximumEntries);
|
||||
},
|
||||
|
||||
@@ -858,9 +903,13 @@ Module.register("calendar", {
|
||||
let p = this.getCalendarProperty(url, property, defaultValue);
|
||||
if (property === "symbol" || property === "recurringSymbol" || property === "fullDaySymbol") {
|
||||
const className = this.getCalendarProperty(url, "symbolClassName", this.config.defaultSymbolClassName);
|
||||
p = className + p;
|
||||
if (p instanceof Array) {
|
||||
let t = [];
|
||||
p.forEach((n) => { t.push(className + n); });
|
||||
p = t;
|
||||
}
|
||||
else p = className + p;
|
||||
}
|
||||
|
||||
if (!(p instanceof Array)) p = [p];
|
||||
return p;
|
||||
},
|
||||
|
@@ -1,11 +1,4 @@
|
||||
/* MagicMirror²
|
||||
* Node Helper: Calendar - CalendarFetcher
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
const https = require("https");
|
||||
const https = require("node:https");
|
||||
const ical = require("node-ical");
|
||||
const Log = require("logger");
|
||||
const NodeHelper = require("node_helper");
|
||||
@@ -63,7 +56,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
|
||||
try {
|
||||
data = ical.parseICS(responseData);
|
||||
Log.debug(`parsed data=${JSON.stringify(data)}`);
|
||||
Log.debug(`parsed data=${JSON.stringify(data, null, 2)}`);
|
||||
events = CalendarFetcherUtils.filterEvents(data, {
|
||||
excludedEvents,
|
||||
includePastEvents,
|
||||
@@ -88,10 +81,13 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
* Schedule the timer for the next update.
|
||||
*/
|
||||
const scheduleTimer = function () {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = setTimeout(function () {
|
||||
fetchCalendar();
|
||||
}, reloadInterval);
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
// only set timer when not running in jest
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = setTimeout(function () {
|
||||
fetchCalendar();
|
||||
}, reloadInterval);
|
||||
}
|
||||
};
|
||||
|
||||
/* public methods */
|
||||
|
@@ -1,121 +1,130 @@
|
||||
/* MagicMirror²
|
||||
* Calendar Fetcher Util Methods
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @external Moment
|
||||
*/
|
||||
const path = require("path");
|
||||
const moment = require("moment");
|
||||
const moment = require("moment-timezone");
|
||||
|
||||
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
|
||||
* @param {object} event the event which needs adjustment
|
||||
* @param {Date} date the date on which this event happens
|
||||
* @returns {number} the necessary adjustment in hours
|
||||
* Determine based on the title of an event if it should be excluded from the list of events
|
||||
* TODO This seems like an overly complicated way to exclude events based on the title.
|
||||
* @param {object} config the global config
|
||||
* @param {string} title the title of the event
|
||||
* @returns {object} excluded: true if the event should be excluded, false otherwise
|
||||
* until: the date until the event should be excluded.
|
||||
*/
|
||||
calculateTimezoneAdjustment (event, date) {
|
||||
let adjustHours = 0;
|
||||
// if a timezone was specified
|
||||
if (!event.start.tz) {
|
||||
Log.debug(" if no tz, guess based on now");
|
||||
event.start.tz = moment.tz.guess();
|
||||
}
|
||||
Log.debug(`initial tz=${event.start.tz}`);
|
||||
shouldEventBeExcluded (config, title) {
|
||||
let filter = {
|
||||
excluded: false,
|
||||
until: null
|
||||
};
|
||||
for (let f in config.excludedEvents) {
|
||||
let filter = config.excludedEvents[f],
|
||||
testTitle = title.toLowerCase(),
|
||||
until = null,
|
||||
useRegex = false,
|
||||
regexFlags = "g";
|
||||
|
||||
// if there is a start date specified
|
||||
if (event.start.tz) {
|
||||
// if this is a windows timezone
|
||||
if (event.start.tz.includes(" ")) {
|
||||
// use the lookup table to get theIANA name as moment and date don't know MS timezones
|
||||
let tz = CalendarFetcherUtils.getIanaTZFromMS(event.start.tz);
|
||||
Log.debug(`corrected TZ=${tz}`);
|
||||
// watch out for unregistered windows timezone names
|
||||
// if we had a successful lookup
|
||||
if (tz) {
|
||||
// change the timezone to the IANA name
|
||||
event.start.tz = tz;
|
||||
// Log.debug("corrected timezone="+event.start.tz)
|
||||
if (filter instanceof Object) {
|
||||
if (typeof filter.until !== "undefined") {
|
||||
until = filter.until;
|
||||
}
|
||||
}
|
||||
Log.debug(`corrected tz=${event.start.tz}`);
|
||||
let current_offset = 0; // offset from TZ string or calculated
|
||||
let mm = 0; // date with tz or offset
|
||||
let start_offset = 0; // utc offset of created with tz
|
||||
// if there is still an offset, lookup failed, use it
|
||||
if (event.start.tz.startsWith("(")) {
|
||||
const regex = /[+|-]\d*:\d*/;
|
||||
const start_offsetString = event.start.tz.match(regex).toString().split(":");
|
||||
let start_offset = parseInt(start_offsetString[0]);
|
||||
start_offset *= event.start.tz[1] === "-" ? -1 : 1;
|
||||
adjustHours = start_offset;
|
||||
Log.debug(`defined offset=${start_offset} hours`);
|
||||
current_offset = start_offset;
|
||||
event.start.tz = "";
|
||||
Log.debug(`ical offset=${current_offset} date=${date}`);
|
||||
mm = moment(date);
|
||||
let x = parseInt(moment(new Date()).utcOffset());
|
||||
Log.debug(`net mins=${current_offset * 60 - x}`);
|
||||
|
||||
mm = mm.add(x - current_offset * 60, "minutes");
|
||||
adjustHours = (current_offset * 60 - x) / 60;
|
||||
event.start = mm.toDate();
|
||||
Log.debug(`adjusted date=${event.start}`);
|
||||
if (typeof filter.regex !== "undefined") {
|
||||
useRegex = filter.regex;
|
||||
}
|
||||
|
||||
// If additional advanced filtering is added in, this section
|
||||
// must remain last as we overwrite the filter object with the
|
||||
// filterBy string
|
||||
if (filter.caseSensitive) {
|
||||
filter = filter.filterBy;
|
||||
testTitle = title;
|
||||
} else if (useRegex) {
|
||||
filter = filter.filterBy;
|
||||
testTitle = title;
|
||||
regexFlags += "i";
|
||||
} else {
|
||||
filter = filter.filterBy.toLowerCase();
|
||||
}
|
||||
} else {
|
||||
// get the start time in that timezone
|
||||
let es = moment(event.start);
|
||||
// check for start date prior to start of daylight changing date
|
||||
if (es.format("YYYY") < 2007) {
|
||||
es.set("year", 2013); // if so, use a closer date
|
||||
}
|
||||
Log.debug(`start date/time=${es.toDate()}`);
|
||||
start_offset = moment.tz(es, event.start.tz).utcOffset();
|
||||
Log.debug(`start offset=${start_offset}`);
|
||||
|
||||
Log.debug(`start date/time w tz =${moment.tz(moment(event.start), event.start.tz).toDate()}`);
|
||||
|
||||
// get the specified date in that timezone
|
||||
mm = moment.tz(moment(date), event.start.tz);
|
||||
Log.debug(`event date=${mm.toDate()}`);
|
||||
current_offset = mm.utcOffset();
|
||||
filter = filter.toLowerCase();
|
||||
}
|
||||
Log.debug(`event offset=${current_offset} hour=${mm.format("H")} event date=${mm.toDate()}`);
|
||||
|
||||
// if the offset is greater than 0, east of london
|
||||
if (current_offset !== start_offset) {
|
||||
// big offset
|
||||
Log.debug("offset");
|
||||
let h = parseInt(mm.format("H"));
|
||||
// check if the event time is less than the offset
|
||||
if (h > 0 && h < Math.abs(current_offset) / 60) {
|
||||
// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
|
||||
// we need to fix that
|
||||
//adjustHours = 24;
|
||||
// Log.debug("adjusting date")
|
||||
}
|
||||
//-300 > -240
|
||||
//if (Math.abs(current_offset) > Math.abs(start_offset)){
|
||||
if (current_offset > start_offset) {
|
||||
adjustHours -= 1;
|
||||
Log.debug("adjust down 1 hour dst change");
|
||||
//} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
|
||||
} else if (current_offset < start_offset) {
|
||||
adjustHours += 1;
|
||||
Log.debug("adjust up 1 hour dst change");
|
||||
if (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
|
||||
if (until) {
|
||||
filter.until = until;
|
||||
} else {
|
||||
filter.excluded = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
Log.debug(`adjustHours=${adjustHours}`);
|
||||
return adjustHours;
|
||||
return filter;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get local timezone.
|
||||
* This method makes it easier to test if different timezones cause problems by changing this implementation.
|
||||
* @returns {string} timezone
|
||||
*/
|
||||
getLocalTimezone () {
|
||||
return moment.tz.guess();
|
||||
},
|
||||
|
||||
/**
|
||||
* This function returns a list of moments for a recurring event.
|
||||
* @param {object} event the current event which is a recurring event
|
||||
* @param {moment.Moment} pastLocalMoment The past date to search for recurring events
|
||||
* @param {moment.Moment} futureLocalMoment The future date to search for recurring events
|
||||
* @param {number} durationInMs the duration of the event, this is used to take into account currently running events
|
||||
* @returns {moment.Moment[]} All moments for the recurring event
|
||||
*/
|
||||
getMomentsFromRecurringEvent (event, pastLocalMoment, futureLocalMoment, durationInMs) {
|
||||
const rule = event.rrule;
|
||||
|
||||
// can cause problems with e.g. birthdays before 1900
|
||||
if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
|
||||
rule.origOptions.dtstart.setYear(1900);
|
||||
rule.options.dtstart.setYear(1900);
|
||||
}
|
||||
|
||||
// subtract the max of the duration of this event or 1 day to find events in the past that are currently still running and should therefor be displayed.
|
||||
const oneDayInMs = 24 * 60 * 60000;
|
||||
let searchFromDate = pastLocalMoment.clone().subtract(Math.max(durationInMs, oneDayInMs), "milliseconds").toDate();
|
||||
let searchToDate = futureLocalMoment.clone().add(1, "days").toDate();
|
||||
Log.debug(`Search for recurring events between: ${searchFromDate} and ${searchToDate}`);
|
||||
|
||||
// if until is set, and its a full day event, force the time to midnight. rrule gets confused with non-00 offset
|
||||
// looks like MS Outlook sets the until time incorrectly for fullday events
|
||||
if ((rule.options.until !== undefined) && CalendarFetcherUtils.isFullDayEvent(event)) {
|
||||
Log.debug("fixup rrule until");
|
||||
rule.options.until = moment(rule.options.until).clone().startOf("day").add(1, "day")
|
||||
.toDate();
|
||||
}
|
||||
|
||||
Log.debug("fix rrule start=", rule.options.dtstart);
|
||||
Log.debug("event before rrule.between=", JSON.stringify(event, null, 2), "exdates=", event.exdate);
|
||||
|
||||
Log.debug(`RRule: ${rule.toString()}`);
|
||||
rule.options.tzid = null; // RRule gets *very* confused with timezones
|
||||
|
||||
let dates = rule.between(searchFromDate, searchToDate, true, () => {
|
||||
return true;
|
||||
});
|
||||
|
||||
Log.debug(`Title: ${event.summary}, with dates: \n\n${JSON.stringify(dates)}\n`);
|
||||
|
||||
// shouldn't need this anymore, as RRULE not passed junk
|
||||
dates = dates.filter((d) => {
|
||||
return JSON.stringify(d) !== "null";
|
||||
});
|
||||
|
||||
// Dates are returned in UTC timezone but with localdatetime because tzid is null.
|
||||
// So we map the date to a moment using the original timezone of the event.
|
||||
return dates.map((d) => (event.start.tz ? moment.tz(d, "UTC").tz(event.start.tz, true) : moment.tz(d, "UTC").tz(CalendarFetcherUtils.getLocalTimezone(), true)));
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -127,32 +136,33 @@ const CalendarFetcherUtils = {
|
||||
filterEvents (data, config) {
|
||||
const newEvents = [];
|
||||
|
||||
// limitFunction doesn't do much limiting, see comment re: the dates
|
||||
// array in rrule section below as to why we need to do the filtering
|
||||
// ourselves
|
||||
const limitFunction = function (date, i) {
|
||||
return true;
|
||||
};
|
||||
|
||||
const eventDate = function (event, time) {
|
||||
return CalendarFetcherUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||
const startMoment = event[time].tz ? moment.tz(event[time], event[time].tz) : moment.tz(event[time], CalendarFetcherUtils.getLocalTimezone());
|
||||
return CalendarFetcherUtils.isFullDayEvent(event) ? startMoment.startOf("day") : startMoment;
|
||||
};
|
||||
|
||||
Log.debug(`There are ${Object.entries(data).length} calendar entries.`);
|
||||
|
||||
const now = moment();
|
||||
const pastLocalMoment = config.includePastEvents ? now.clone().startOf("day").subtract(config.maximumNumberOfDays, "days") : now;
|
||||
const futureLocalMoment
|
||||
= now
|
||||
.clone()
|
||||
.startOf("day")
|
||||
.add(config.maximumNumberOfDays, "days")
|
||||
// Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||
.subtract(1, "seconds");
|
||||
|
||||
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") // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||
.toDate();
|
||||
let past = today;
|
||||
|
||||
if (config.includePastEvents) {
|
||||
past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
|
||||
const title = CalendarFetcherUtils.getTitleFromEvent(event);
|
||||
Log.debug(`title: ${title}`);
|
||||
|
||||
// Return quickly if event should be excluded.
|
||||
let { excluded, eventFilterUntil } = this.shouldEventBeExcluded(config, title);
|
||||
if (excluded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: Ugly fix to solve the facebook birthday issue.
|
||||
@@ -165,292 +175,103 @@ const CalendarFetcherUtils = {
|
||||
}
|
||||
|
||||
if (event.type === "VEVENT") {
|
||||
Log.debug(`Event:\n${JSON.stringify(event)}`);
|
||||
let startDate = eventDate(event, "start");
|
||||
let endDate;
|
||||
Log.debug(`Event:\n${JSON.stringify(event, null, 2)}`);
|
||||
let eventStartMoment = eventDate(event, "start");
|
||||
let eventEndMoment;
|
||||
|
||||
if (typeof event.end !== "undefined") {
|
||||
endDate = eventDate(event, "end");
|
||||
eventEndMoment = eventDate(event, "end");
|
||||
} else if (typeof event.duration !== "undefined") {
|
||||
endDate = startDate.clone().add(moment.duration(event.duration));
|
||||
eventEndMoment = eventStartMoment.clone().add(moment.duration(event.duration));
|
||||
} else {
|
||||
if (!isFacebookBirthday) {
|
||||
// make copy of start date, separate storage area
|
||||
endDate = moment(startDate.format("x"), "x");
|
||||
eventEndMoment = eventStartMoment.clone();
|
||||
} else {
|
||||
endDate = moment(startDate).add(1, "days");
|
||||
eventEndMoment = eventStartMoment.clone().add(1, "days");
|
||||
}
|
||||
}
|
||||
|
||||
Log.debug(`start: ${startDate.toDate()}`);
|
||||
Log.debug(`end:: ${endDate.toDate()}`);
|
||||
Log.debug(`start: ${eventStartMoment.toDate()}`);
|
||||
Log.debug(`end:: ${eventEndMoment.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}`);
|
||||
|
||||
// FIXME: Since the parsed json object from node-ical comes with time information
|
||||
// this check could be removed (?)
|
||||
if (event.start.length === 8) {
|
||||
startDate = startDate.startOf("day");
|
||||
}
|
||||
|
||||
const title = CalendarFetcherUtils.getTitleFromEvent(event);
|
||||
Log.debug(`title: ${title}`);
|
||||
|
||||
let excluded = false,
|
||||
dateFilter = null;
|
||||
|
||||
for (let f in config.excludedEvents) {
|
||||
let filter = config.excludedEvents[f],
|
||||
testTitle = title.toLowerCase(),
|
||||
until = null,
|
||||
useRegex = false,
|
||||
regexFlags = "g";
|
||||
|
||||
if (filter instanceof Object) {
|
||||
if (typeof filter.until !== "undefined") {
|
||||
until = filter.until;
|
||||
}
|
||||
|
||||
if (typeof filter.regex !== "undefined") {
|
||||
useRegex = filter.regex;
|
||||
}
|
||||
|
||||
// If additional advanced filtering is added in, this section
|
||||
// must remain last as we overwrite the filter object with the
|
||||
// filterBy string
|
||||
if (filter.caseSensitive) {
|
||||
filter = filter.filterBy;
|
||||
testTitle = title;
|
||||
} else if (useRegex) {
|
||||
filter = filter.filterBy;
|
||||
testTitle = title;
|
||||
regexFlags += "i";
|
||||
} else {
|
||||
filter = filter.filterBy.toLowerCase();
|
||||
}
|
||||
} else {
|
||||
filter = filter.toLowerCase();
|
||||
}
|
||||
|
||||
if (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
|
||||
if (until) {
|
||||
dateFilter = until;
|
||||
} else {
|
||||
excluded = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (excluded) {
|
||||
return;
|
||||
}
|
||||
const durationMs = eventEndMoment.valueOf() - eventStartMoment.valueOf();
|
||||
Log.debug(`duration: ${durationMs}`);
|
||||
|
||||
const location = event.location || false;
|
||||
const geo = event.geo || false;
|
||||
const description = event.description || false;
|
||||
|
||||
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
|
||||
const rule = event.rrule;
|
||||
// TODO This should be a seperate function.
|
||||
if (event.rrule && typeof event.rrule !== "undefined" && !isFacebookBirthday) {
|
||||
// Recurring event.
|
||||
let moments = CalendarFetcherUtils.getMomentsFromRecurringEvent(event, pastLocalMoment, futureLocalMoment, durationMs);
|
||||
|
||||
const pastMoment = moment(past);
|
||||
const futureMoment = moment(future);
|
||||
|
||||
// can cause problems with e.g. birthdays before 1900
|
||||
if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
|
||||
rule.origOptions.dtstart.setYear(1900);
|
||||
rule.options.dtstart.setYear(1900);
|
||||
}
|
||||
|
||||
// For recurring events, get the set of start dates that fall within the range
|
||||
// of dates we're looking for.
|
||||
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
|
||||
let pastLocal = 0;
|
||||
let futureLocal = 0;
|
||||
if (CalendarFetcherUtils.isFullDayEvent(event)) {
|
||||
Log.debug("fullday");
|
||||
// if full day event, only use the date part of the ranges
|
||||
pastLocal = pastMoment.toDate();
|
||||
futureLocal = futureMoment.toDate();
|
||||
|
||||
Log.debug(`pastLocal: ${pastLocal}`);
|
||||
Log.debug(`futureLocal: ${futureLocal}`);
|
||||
} else {
|
||||
// if we want past events
|
||||
if (config.includePastEvents) {
|
||||
// use the calculated past time for the between from
|
||||
pastLocal = pastMoment.toDate();
|
||||
} else {
|
||||
// otherwise use NOW.. cause we shouldn't use any before now
|
||||
pastLocal = moment().toDate(); //now
|
||||
}
|
||||
futureLocal = futureMoment.toDate(); // future
|
||||
}
|
||||
Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`);
|
||||
let dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||
Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`);
|
||||
dates = dates.filter((d) => {
|
||||
if (JSON.stringify(d) === "null") return false;
|
||||
else return true;
|
||||
});
|
||||
// The "dates" array contains the set of dates within our desired date range range that are valid
|
||||
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
|
||||
// had its date changed from outside the range to inside the range. For the time being,
|
||||
// we'll handle this by adding *all* recurrence entries into the set of dates that we check,
|
||||
// because the logic below will filter out any recurrences that don't actually belong within
|
||||
// our display range.
|
||||
// Would be great if there was a better way to handle this.
|
||||
Log.debug(`event.recurrences: ${event.recurrences}`);
|
||||
if (event.recurrences !== undefined) {
|
||||
for (let r in event.recurrences) {
|
||||
// Only add dates that weren't already in the range we added from the rrule so that
|
||||
// we don"t double-add those events.
|
||||
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
|
||||
dates.push(new Date(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Loop through the set of date entries to see which recurrences should be added to our event list.
|
||||
for (let d in dates) {
|
||||
let date = dates[d];
|
||||
// Loop through the set of moment entries to see which recurrences should be added to our event list.
|
||||
// TODO This should create an event per moment so we can change anything we want.
|
||||
for (let m in moments) {
|
||||
let curEvent = event;
|
||||
let showRecurrence = true;
|
||||
let recurringEventStartMoment = moments[m].tz(CalendarFetcherUtils.getLocalTimezone()).clone();
|
||||
let recurringEventEndMoment = recurringEventStartMoment.clone().add(durationMs, "ms");
|
||||
|
||||
// set the time information in the date to equal the time information in the event
|
||||
date.setUTCHours(curEvent.start.getUTCHours(), curEvent.start.getUTCMinutes(), curEvent.start.getUTCSeconds(), curEvent.start.getUTCMilliseconds());
|
||||
|
||||
// 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);
|
||||
|
||||
// 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 dateKey = recurringEventStartMoment.tz("UTC").format("YYYY-MM-DD");
|
||||
|
||||
Log.debug("event date dateKey=", dateKey);
|
||||
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
|
||||
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
|
||||
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||
curEvent = curEvent.recurrences[dateKey];
|
||||
startDate = moment(curEvent.start);
|
||||
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
|
||||
if (curEvent.recurrences !== undefined) {
|
||||
Log.debug("have recurrences=", curEvent.recurrences);
|
||||
if (curEvent.recurrences[dateKey] !== undefined) {
|
||||
Log.debug("have a recurrence match for dateKey=", dateKey);
|
||||
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||
curEvent = curEvent.recurrences[dateKey];
|
||||
// Some event start/end dates don't have timezones
|
||||
if (curEvent.start.tz) {
|
||||
recurringEventStartMoment = moment(curEvent.start).tz(curEvent.start.tz).tz(CalendarFetcherUtils.getLocalTimezone());
|
||||
} else {
|
||||
recurringEventStartMoment = moment(curEvent.start).tz(CalendarFetcherUtils.getLocalTimezone());
|
||||
}
|
||||
if (curEvent.end.tz) {
|
||||
recurringEventEndMoment = moment(curEvent.end).tz(curEvent.end.tz).tz(CalendarFetcherUtils.getLocalTimezone());
|
||||
} else {
|
||||
recurringEventEndMoment = moment(curEvent.end).tz(CalendarFetcherUtils.getLocalTimezone());
|
||||
}
|
||||
} else {
|
||||
Log.debug("recurrence key ", dateKey, " doesn't match");
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
if (curEvent.exdate !== undefined) {
|
||||
Log.debug("have datekey=", dateKey, " exdates=", curEvent.exdate);
|
||||
if (curEvent.exdate[dateKey] !== undefined) {
|
||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||
showRecurrence = false;
|
||||
}
|
||||
}
|
||||
Log.debug(`duration: ${duration}`);
|
||||
|
||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
||||
if (startDate.format("x") === endDate.format("x")) {
|
||||
endDate = endDate.endOf("day");
|
||||
if (recurringEventStartMoment.valueOf() === recurringEventEndMoment.valueOf()) {
|
||||
recurringEventEndMoment = recurringEventEndMoment.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 (recurringEventEndMoment.isBefore(pastLocalMoment) || recurringEventStartMoment.isAfter(futureLocalMoment)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) {
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, recurringEventEndMoment, eventFilterUntil)) {
|
||||
showRecurrence = false;
|
||||
}
|
||||
|
||||
if (showRecurrence === true) {
|
||||
Log.debug(`saving event: ${description}`);
|
||||
Log.debug(`saving event: ${recurrenceTitle}`);
|
||||
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: recurringEventStartMoment.format("x"),
|
||||
endDate: recurringEventEndMoment.format("x"),
|
||||
fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event),
|
||||
recurringEvent: true,
|
||||
class: event.class,
|
||||
@@ -459,7 +280,10 @@ const CalendarFetcherUtils = {
|
||||
geo: geo,
|
||||
description: description
|
||||
});
|
||||
} else {
|
||||
Log.debug("not saving event ", recurrenceTitle, eventStartMoment);
|
||||
}
|
||||
Log.debug(" ");
|
||||
}
|
||||
// End recurring event parsing.
|
||||
} else {
|
||||
@@ -468,45 +292,45 @@ 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 && eventStartMoment.valueOf() === eventEndMoment.valueOf()) {
|
||||
eventEndMoment = eventEndMoment.endOf("day");
|
||||
}
|
||||
|
||||
if (config.includePastEvents) {
|
||||
// Past event is too far in the past, so skip.
|
||||
if (endDate < past) {
|
||||
if (eventEndMoment < pastLocalMoment) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// It's not a fullday event, and it is in the past, so skip.
|
||||
if (!fullDayEvent && endDate < new Date()) {
|
||||
if (!fullDayEvent && eventEndMoment < now) {
|
||||
return;
|
||||
}
|
||||
|
||||
// It's a fullday event, and it is before today, So skip.
|
||||
if (fullDayEvent && endDate <= today) {
|
||||
if (fullDayEvent && eventEndMoment <= now.startOf("day")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// It exceeds the maximumNumberOfDays limit, so skip.
|
||||
if (startDate > future) {
|
||||
if (eventStartMoment > futureLocalMoment) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) {
|
||||
if (CalendarFetcherUtils.timeFilterApplies(now, eventEndMoment, eventFilterUntil)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get correction for date saving and dst change between now and then
|
||||
let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, startDate.toDate());
|
||||
// Every thing is good. Add it to the list.
|
||||
newEvents.push({
|
||||
title: title,
|
||||
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||
startDate: eventStartMoment.format("x"),
|
||||
endDate: eventEndMoment.format("x"),
|
||||
fullDayEvent: fullDayEvent,
|
||||
recurringEvent: false,
|
||||
class: event.class,
|
||||
firstYear: event.start.getFullYear(),
|
||||
location: location,
|
||||
geo: geo,
|
||||
description: description
|
||||
@@ -522,18 +346,6 @@ const CalendarFetcherUtils = {
|
||||
return newEvents;
|
||||
},
|
||||
|
||||
/**
|
||||
* Lookup iana tz from windows
|
||||
* @param {string} msTZName the timezone name to lookup
|
||||
* @returns {string|null} the iana name or null of none is found
|
||||
*/
|
||||
getIanaTZFromMS (msTZName) {
|
||||
// Get hash entry
|
||||
const he = zoneTable[msTZName];
|
||||
// If found return iana name, else null
|
||||
return he ? he.iana[0] : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the title from the event.
|
||||
* @param {object} event The event object to check.
|
||||
@@ -573,8 +385,8 @@ const CalendarFetcherUtils = {
|
||||
|
||||
/**
|
||||
* Determines if the user defined time filter should apply
|
||||
* @param {Date} now Date object using previously created object for consistency
|
||||
* @param {Moment} endDate Moment object representing the event end date
|
||||
* @param {moment.Moment} now Date object using previously created object for consistency
|
||||
* @param {moment.Moment} endDate Moment object representing the event end date
|
||||
* @param {string} filter The time to subtract from the end date to determine if an event should be shown
|
||||
* @returns {boolean} True if the event should be filtered out, false otherwise
|
||||
*/
|
||||
@@ -585,7 +397,7 @@ const CalendarFetcherUtils = {
|
||||
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;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@@ -1,9 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* Calendar Util Methods
|
||||
*
|
||||
* By Rejas
|
||||
* MIT Licensed.
|
||||
*/
|
||||
const CalendarUtils = {
|
||||
|
||||
/**
|
||||
|
@@ -1,9 +1,7 @@
|
||||
/* CalendarFetcher Tester
|
||||
/*
|
||||
* 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,9 +1,3 @@
|
||||
/* 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");
|
||||
@@ -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);
|
||||
|
@@ -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: {
|
||||
@@ -20,7 +14,7 @@ Module.register("clock", {
|
||||
clockBold: false,
|
||||
showDate: true,
|
||||
showTime: true,
|
||||
showWeek: false,
|
||||
showWeek: false, // options: true, false, 'short'
|
||||
dateFormat: "dddd, LL",
|
||||
sendNotifications: false,
|
||||
|
||||
@@ -29,9 +23,9 @@ Module.register("clock", {
|
||||
analogFace: "simple", // options: 'none', 'simple', 'face-###' (where ### is 001 to 012 inclusive)
|
||||
analogPlacement: "bottom", // options: 'top', 'bottom', 'left', 'right'
|
||||
analogShowDate: "top", // OBSOLETE, can be replaced with analogPlacement and showTime, options: false, 'top', or 'bottom'
|
||||
secondsColor: "#888888",
|
||||
secondsColor: "#888888", // DEPRECATED, use CSS instead. Class "clock-second-digital" for digital clock, "clock-second" for analog clock.
|
||||
|
||||
showSunTimes: false,
|
||||
showSunTimes: false, // options: true, false, 'disableNextEvent'
|
||||
showMoonTimes: false, // options: false, 'times' (rise/set), 'percent' (lit percent), 'phase' (current phase), or 'both' (percent & phase)
|
||||
lat: 47.630539,
|
||||
lon: -122.344147
|
||||
@@ -105,13 +99,14 @@ Module.register("clock", {
|
||||
analogWrapper.className = "clock-circle";
|
||||
const digitalWrapper = document.createElement("div");
|
||||
digitalWrapper.className = "digital";
|
||||
digitalWrapper.style.gridArea = "center";
|
||||
|
||||
/************************************
|
||||
* Create wrappers for DIGITAL clock
|
||||
*/
|
||||
const dateWrapper = document.createElement("div");
|
||||
const timeWrapper = document.createElement("div");
|
||||
const hoursWrapper = document.createElement("span");
|
||||
const minutesWrapper = document.createElement("span");
|
||||
const secondsWrapper = document.createElement("sup");
|
||||
const periodWrapper = document.createElement("span");
|
||||
const sunWrapper = document.createElement("div");
|
||||
@@ -121,39 +116,40 @@ Module.register("clock", {
|
||||
// Style Wrappers
|
||||
dateWrapper.className = "date normal medium";
|
||||
timeWrapper.className = "time bright large light";
|
||||
secondsWrapper.className = "seconds dimmed";
|
||||
hoursWrapper.className = "clock-hour-digital";
|
||||
minutesWrapper.className = "clock-minute-digital";
|
||||
secondsWrapper.className = "clock-second-digital dimmed";
|
||||
sunWrapper.className = "sun dimmed small";
|
||||
moonWrapper.className = "moon dimmed small";
|
||||
weekWrapper.className = "week dimmed medium";
|
||||
|
||||
// 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
|
||||
let timeString;
|
||||
const now = moment();
|
||||
if (this.config.timezone) {
|
||||
now.tz(this.config.timezone);
|
||||
}
|
||||
|
||||
let hourSymbol = "HH";
|
||||
if (this.config.timeFormat !== 24) {
|
||||
hourSymbol = "h";
|
||||
}
|
||||
|
||||
if (this.config.clockBold) {
|
||||
timeString = now.format(`${hourSymbol}[<span class="bold">]mm[</span>]`);
|
||||
} else {
|
||||
timeString = now.format(`${hourSymbol}:mm`);
|
||||
}
|
||||
|
||||
if (this.config.showDate) {
|
||||
dateWrapper.innerHTML = now.format(this.config.dateFormat);
|
||||
digitalWrapper.appendChild(dateWrapper);
|
||||
}
|
||||
|
||||
if (this.config.displayType !== "analog" && this.config.showTime) {
|
||||
timeWrapper.innerHTML = timeString;
|
||||
let hourSymbol = "HH";
|
||||
if (this.config.timeFormat !== 24) {
|
||||
hourSymbol = "h";
|
||||
}
|
||||
|
||||
hoursWrapper.innerHTML = now.format(hourSymbol);
|
||||
minutesWrapper.innerHTML = now.format("mm");
|
||||
|
||||
timeWrapper.appendChild(hoursWrapper);
|
||||
if (this.config.clockBold) {
|
||||
minutesWrapper.classList.add("bold");
|
||||
} else {
|
||||
timeWrapper.innerHTML += ":";
|
||||
}
|
||||
timeWrapper.appendChild(minutesWrapper);
|
||||
secondsWrapper.innerHTML = now.format("ss");
|
||||
if (this.config.showPeriodUpper) {
|
||||
periodWrapper.innerHTML = now.format("A");
|
||||
@@ -175,21 +171,28 @@ Module.register("clock", {
|
||||
if (this.config.showSunTimes) {
|
||||
const sunTimes = SunCalc.getTimes(now, this.config.lat, this.config.lon);
|
||||
const isVisible = now.isBetween(sunTimes.sunrise, sunTimes.sunset);
|
||||
let nextEvent;
|
||||
if (now.isBefore(sunTimes.sunrise)) {
|
||||
nextEvent = sunTimes.sunrise;
|
||||
} else if (now.isBefore(sunTimes.sunset)) {
|
||||
nextEvent = sunTimes.sunset;
|
||||
} else {
|
||||
const tomorrowSunTimes = SunCalc.getTimes(now.clone().add(1, "day"), this.config.lat, this.config.lon);
|
||||
nextEvent = tomorrowSunTimes.sunrise;
|
||||
let sunWrapperInnerHTML = "";
|
||||
|
||||
if (this.config.showSunTimes !== "disableNextEvent") {
|
||||
let nextEvent;
|
||||
if (now.isBefore(sunTimes.sunrise)) {
|
||||
nextEvent = sunTimes.sunrise;
|
||||
} else if (now.isBefore(sunTimes.sunset)) {
|
||||
nextEvent = sunTimes.sunset;
|
||||
} else {
|
||||
const tomorrowSunTimes = SunCalc.getTimes(now.clone().add(1, "day"), this.config.lat, this.config.lon);
|
||||
nextEvent = tomorrowSunTimes.sunrise;
|
||||
}
|
||||
const untilNextEvent = moment.duration(moment(nextEvent).diff(now));
|
||||
const untilNextEventString = `${untilNextEvent.hours()}h ${untilNextEvent.minutes()}m`;
|
||||
|
||||
sunWrapperInnerHTML = `<span class="${isVisible ? "bright" : ""}"><i class="fas fa-sun" aria-hidden="true"></i> ${untilNextEventString}</span>`;
|
||||
}
|
||||
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>`;
|
||||
|
||||
sunWrapperInnerHTML += `<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 = sunWrapperInnerHTML;
|
||||
digitalWrapper.appendChild(sunWrapper);
|
||||
}
|
||||
|
||||
@@ -215,13 +218,18 @@ Module.register("clock", {
|
||||
|
||||
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>`;
|
||||
+ `<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);
|
||||
}
|
||||
|
||||
if (this.config.showWeek) {
|
||||
weekWrapper.innerHTML = this.translate("WEEK", { weekNumber: now.week() });
|
||||
if (this.config.showWeek === "short") {
|
||||
weekWrapper.innerHTML = this.translate("WEEK_SHORT", { weekNumber: now.week() });
|
||||
} else {
|
||||
weekWrapper.innerHTML = this.translate("WEEK", { weekNumber: now.week() });
|
||||
}
|
||||
|
||||
digitalWrapper.appendChild(weekWrapper);
|
||||
}
|
||||
|
||||
@@ -247,7 +255,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") {
|
||||
@@ -274,7 +282,7 @@ Module.register("clock", {
|
||||
clockSecond.id = "clock-second";
|
||||
clockSecond.style.transform = `rotate(${second}deg)`;
|
||||
clockSecond.className = "clock-second";
|
||||
clockSecond.style.backgroundColor = this.config.secondsColor;
|
||||
clockSecond.style.backgroundColor = this.config.secondsColor; /* DEPRECATED, to be removed in a future version , use CSS instead */
|
||||
clockFace.appendChild(clockSecond);
|
||||
}
|
||||
analogWrapper.appendChild(clockFace);
|
||||
|
@@ -78,7 +78,12 @@
|
||||
left: 50%;
|
||||
margin: -38% -1px 0 0; /* numbers must match negative length & thickness */
|
||||
padding: 38% 1px 0 0; /* indicator length & thickness */
|
||||
background: var(--color-text);
|
||||
|
||||
/* background: #888888 !important; */
|
||||
|
||||
/* use this instead of secondsColor */
|
||||
|
||||
/* have to use !important, because the code explicitly sets the color currently */
|
||||
transform-origin: 50% 100%;
|
||||
}
|
||||
|
||||
@@ -91,3 +96,15 @@
|
||||
.module.clock .moon > * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.module.clock .clock-hour-digital {
|
||||
color: var(--color-text-bright);
|
||||
}
|
||||
|
||||
.module.clock .clock-minute-digital {
|
||||
color: var(--color-text-bright);
|
||||
}
|
||||
|
||||
.module.clock .clock-second-digital {
|
||||
color: var(--color-text-dimmed);
|
||||
}
|
||||
|
@@ -1,9 +1,5 @@
|
||||
/* MagicMirror²
|
||||
* Module: Compliments
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
/* global Cron */
|
||||
|
||||
Module.register("compliments", {
|
||||
// Module config defaults.
|
||||
defaults: {
|
||||
@@ -16,20 +12,27 @@ Module.register("compliments", {
|
||||
},
|
||||
updateInterval: 30000,
|
||||
remoteFile: null,
|
||||
remoteFileRefreshInterval: 0,
|
||||
fadeSpeed: 4000,
|
||||
morningStartTime: 3,
|
||||
morningEndTime: 12,
|
||||
afternoonStartTime: 12,
|
||||
afternoonEndTime: 17,
|
||||
random: true
|
||||
random: true,
|
||||
specialDayUnique: false
|
||||
},
|
||||
urlSuffix: "",
|
||||
compliments_new: null,
|
||||
refreshMinimumDelay: 15 * 60 * 60 * 1000, // 15 minutes
|
||||
lastIndexUsed: -1,
|
||||
// Set currentweather from module
|
||||
currentWeatherType: "",
|
||||
|
||||
cron_regex: /^(((\d+,)+\d+|((\d+|[*])[/]\d+|((JAN|FEB|APR|MA[RY]|JU[LN]|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|APR|MA[RY]|JU[LN]|AUG|SEP|OCT|NOV|DEC))?))|(\d+-\d+)|\d+(-\d+)?[/]\d+(-\d+)?|\d+|[*]|(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?) ?){5}$/i,
|
||||
date_regex: "[1-9.][0-9.][0-9.]{2}-([0][1-9]|[1][0-2])-([1-2][0-9]|[0][1-9]|[3][0-1])",
|
||||
pre_defined_types: ["anytime", "morning", "afternoon", "evening"],
|
||||
// Define required scripts.
|
||||
getScripts () {
|
||||
return ["moment.js"];
|
||||
return ["croner.js", "moment.js"];
|
||||
},
|
||||
|
||||
// Define start sequence.
|
||||
@@ -42,12 +45,62 @@ Module.register("compliments", {
|
||||
const response = await this.loadComplimentFile();
|
||||
this.config.compliments = JSON.parse(response);
|
||||
this.updateDom();
|
||||
if (this.config.remoteFileRefreshInterval !== 0) {
|
||||
if ((this.config.remoteFileRefreshInterval >= this.refreshMinimumDelay) || window.mmTestMode === "true") {
|
||||
setInterval(async () => {
|
||||
const response = await this.loadComplimentFile();
|
||||
if (response) {
|
||||
this.compliments_new = JSON.parse(response);
|
||||
}
|
||||
else {
|
||||
Log.error(`${this.name} remoteFile refresh failed`);
|
||||
}
|
||||
},
|
||||
this.config.remoteFileRefreshInterval);
|
||||
} else {
|
||||
Log.error(`${this.name} remoteFileRefreshInterval less than minimum`);
|
||||
}
|
||||
}
|
||||
}
|
||||
let minute_sync_delay = 1;
|
||||
// loop thru all the configured when events
|
||||
for (let m of Object.keys(this.config.compliments)) {
|
||||
// if it is a cron entry
|
||||
if (this.isCronEntry(m)) {
|
||||
// we need to synch our interval cycle to the minute
|
||||
minute_sync_delay = (60 - (moment().second())) * 1000;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Schedule update timer. sync to the minute start (if needed), so minute based events happen on the minute start
|
||||
setTimeout(() => {
|
||||
setInterval(() => {
|
||||
this.updateDom(this.config.fadeSpeed);
|
||||
}, this.config.updateInterval);
|
||||
},
|
||||
minute_sync_delay);
|
||||
},
|
||||
|
||||
// Schedule update timer.
|
||||
setInterval(() => {
|
||||
this.updateDom(this.config.fadeSpeed);
|
||||
}, this.config.updateInterval);
|
||||
// check to see if this entry could be a cron entry wich contains spaces
|
||||
isCronEntry (entry) {
|
||||
return entry.includes(" ");
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} cronExpression The cron expression. See https://croner.56k.guru/usage/pattern/
|
||||
* @param {Date} [timestamp] The timestamp to check. Defaults to the current time.
|
||||
* @returns {number} The number of seconds until the next cron run.
|
||||
*/
|
||||
getSecondsUntilNextCronRun (cronExpression, timestamp = new Date()) {
|
||||
// Required for seconds precision
|
||||
const adjustedTimestamp = new Date(timestamp.getTime() - 1000);
|
||||
|
||||
// https://www.npmjs.com/package/croner
|
||||
const cronJob = new Cron(cronExpression);
|
||||
const nextRunTime = cronJob.nextRun(adjustedTimestamp);
|
||||
|
||||
const secondsDelta = (nextRunTime - adjustedTimestamp) / 1000;
|
||||
return secondsDelta;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -56,7 +109,7 @@ Module.register("compliments", {
|
||||
* @returns {number} a random index of given array
|
||||
*/
|
||||
randomIndex (compliments) {
|
||||
if (compliments.length === 1) {
|
||||
if (compliments.length <= 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -80,34 +133,73 @@ Module.register("compliments", {
|
||||
* @returns {string[]} array with compliments for the time of the day.
|
||||
*/
|
||||
complimentArray () {
|
||||
const hour = moment().hour();
|
||||
const date = moment().format("YYYY-MM-DD");
|
||||
const now = moment();
|
||||
const hour = now.hour();
|
||||
const date = now.format("YYYY-MM-DD");
|
||||
let compliments = [];
|
||||
|
||||
// Add time of day compliments
|
||||
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime && this.config.compliments.hasOwnProperty("morning")) {
|
||||
compliments = [...this.config.compliments.morning];
|
||||
} else if (hour >= this.config.afternoonStartTime && hour < this.config.afternoonEndTime && this.config.compliments.hasOwnProperty("afternoon")) {
|
||||
compliments = [...this.config.compliments.afternoon];
|
||||
} else if (this.config.compliments.hasOwnProperty("evening")) {
|
||||
compliments = [...this.config.compliments.evening];
|
||||
let timeOfDay;
|
||||
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime) {
|
||||
timeOfDay = "morning";
|
||||
} else if (hour >= this.config.afternoonStartTime && hour < this.config.afternoonEndTime) {
|
||||
timeOfDay = "afternoon";
|
||||
} else {
|
||||
timeOfDay = "evening";
|
||||
}
|
||||
|
||||
if (timeOfDay && this.config.compliments.hasOwnProperty(timeOfDay)) {
|
||||
compliments = [...this.config.compliments[timeOfDay]];
|
||||
}
|
||||
|
||||
// Add compliments based on weather
|
||||
if (this.currentWeatherType in this.config.compliments) {
|
||||
Array.prototype.push.apply(compliments, this.config.compliments[this.currentWeatherType]);
|
||||
// if the predefine list doesn't include it (yet)
|
||||
if (!this.pre_defined_types.includes(this.currentWeatherType)) {
|
||||
// add it
|
||||
this.pre_defined_types.push(this.currentWeatherType);
|
||||
}
|
||||
}
|
||||
|
||||
// Add compliments for anytime
|
||||
Array.prototype.push.apply(compliments, this.config.compliments.anytime);
|
||||
|
||||
// Add compliments for special days
|
||||
for (let entry in this.config.compliments) {
|
||||
if (new RegExp(entry).test(date)) {
|
||||
Array.prototype.push.apply(compliments, this.config.compliments[entry]);
|
||||
// get the list of just date entry keys
|
||||
let temp_list = Object.keys(this.config.compliments).filter((k) => {
|
||||
if (this.pre_defined_types.includes(k)) return false;
|
||||
else return true;
|
||||
});
|
||||
|
||||
let date_compliments = [];
|
||||
// Add compliments for special day/times
|
||||
for (let entry of temp_list) {
|
||||
// check if this could be a cron type entry
|
||||
if (this.isCronEntry(entry)) {
|
||||
// make sure the regex is valid
|
||||
if (new RegExp(this.cron_regex).test(entry)) {
|
||||
// check if we are in the time range for the cron entry
|
||||
if (this.getSecondsUntilNextCronRun(entry, now.set("seconds", 0).toDate()) <= 1) {
|
||||
// if so, use its notice entries
|
||||
Array.prototype.push.apply(date_compliments, this.config.compliments[entry]);
|
||||
}
|
||||
} else Log.error(`compliments cron syntax invalid=${JSON.stringify(entry)}`);
|
||||
} else if (new RegExp(entry).test(date)) {
|
||||
Array.prototype.push.apply(date_compliments, this.config.compliments[entry]);
|
||||
}
|
||||
}
|
||||
|
||||
// if we found any date compliments
|
||||
if (date_compliments.length) {
|
||||
// and the special flag is true
|
||||
if (this.config.specialDayUnique) {
|
||||
// clear the non-date compliments if any
|
||||
compliments.length = 0;
|
||||
}
|
||||
// put the date based compliments on the list
|
||||
Array.prototype.push.apply(compliments, date_compliments);
|
||||
}
|
||||
|
||||
return compliments;
|
||||
},
|
||||
|
||||
@@ -118,8 +210,18 @@ Module.register("compliments", {
|
||||
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);
|
||||
return await response.text();
|
||||
// because we may be fetching the same url,
|
||||
// we need to force the server to not give us the cached result
|
||||
// create an extra property (ignored by the server handler) just so the url string is different
|
||||
// that will never be the same, using the ms value of date
|
||||
if (isRemote && this.config.remoteFileRefreshInterval !== 0) this.urlSuffix = `?dummy=${Date.now()}`;
|
||||
//
|
||||
try {
|
||||
const response = await fetch(url + this.urlSuffix);
|
||||
return await response.text();
|
||||
} catch (error) {
|
||||
Log.info(`${this.name} fetch failed error=`, error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -169,6 +271,27 @@ Module.register("compliments", {
|
||||
compliment.lastElementChild.remove();
|
||||
wrapper.appendChild(compliment);
|
||||
}
|
||||
// if a new set of compliments was loaded from the refresh task
|
||||
// we do this here to make sure no other function is using the compliments list
|
||||
if (this.compliments_new) {
|
||||
// use them
|
||||
if (JSON.stringify(this.config.compliments) !== JSON.stringify(this.compliments_new)) {
|
||||
// only reset if the contents changes
|
||||
this.config.compliments = this.compliments_new;
|
||||
// reset the index
|
||||
this.lastIndexUsed = -1;
|
||||
}
|
||||
// clear new file list so we don't waste cycles comparing between refreshes
|
||||
this.compliments_new = null;
|
||||
}
|
||||
// only in test mode
|
||||
if (window.mmTestMode === "true") {
|
||||
// check for (undocumented) remoteFile2 to test new file load
|
||||
if (this.config.remoteFile2 !== null && this.config.remoteFileRefreshInterval !== 0) {
|
||||
// switch the file so that next time it will be loaded from a changed file
|
||||
this.config.remoteFile = this.config.remoteFile2;
|
||||
}
|
||||
}
|
||||
return wrapper;
|
||||
},
|
||||
|
||||
|
@@ -1,8 +1,6 @@
|
||||
/* 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,9 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* Module: HelloWorld
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("helloworld", {
|
||||
// Default module config.
|
||||
defaults: {
|
||||
|
@@ -1,9 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* Module: NewsFeed
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("newsfeed", {
|
||||
// Default module config.
|
||||
defaults: {
|
||||
@@ -44,7 +38,7 @@ Module.register("newsfeed", {
|
||||
|
||||
getUrlPrefix (item) {
|
||||
if (item.useCorsProxy) {
|
||||
return `${location.protocol}//${location.host}/cors?url=`;
|
||||
return `${location.protocol}//${location.host}${config.basePath}cors?url=`;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
@@ -63,7 +57,7 @@ Module.register("newsfeed", {
|
||||
// Define required translations.
|
||||
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.
|
||||
// Therefore we can just return false. Otherwise we should have returned a dictionary.
|
||||
// If you're trying to build your own module including translations, check out the documentation.
|
||||
return false;
|
||||
},
|
||||
@@ -118,27 +112,33 @@ Module.register("newsfeed", {
|
||||
|
||||
//Override template data and return whats used for the current template
|
||||
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,7 +150,7 @@ 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
|
||||
};
|
||||
@@ -177,6 +177,19 @@ Module.register("newsfeed", {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a feed property by name
|
||||
* @param {object} feed A feed object.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {*} The value of the specified property for the feed.
|
||||
*/
|
||||
getFeedProperty (feed, property) {
|
||||
let res = this.config[property];
|
||||
const f = this.config.feeds.find((feedItem) => feedItem.url === feed);
|
||||
if (f && f[property]) res = f[property];
|
||||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate an ordered list of items for this configured module.
|
||||
* @param {object} feeds An object with feeds returned by the node helper.
|
||||
@@ -188,7 +201,7 @@ Module.register("newsfeed", {
|
||||
if (this.subscribedToFeed(feed)) {
|
||||
for (let item of feedItems) {
|
||||
item.sourceTitle = this.titleForFeed(feed);
|
||||
if (!(this.config.ignoreOldItems && Date.now() - new Date(item.pubdate) > this.config.ignoreOlderThan)) {
|
||||
if (!(this.getFeedProperty(feed, "ignoreOldItems") && Date.now() - new Date(item.pubdate) > this.getFeedProperty(feed, "ignoreOlderThan"))) {
|
||||
newsItems.push(item);
|
||||
}
|
||||
}
|
||||
@@ -312,8 +325,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) {
|
||||
|
@@ -1,11 +1,5 @@
|
||||
/* 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 { htmlToText } = require("html-to-text");
|
||||
@@ -67,7 +61,8 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
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:");
|
||||
@@ -133,10 +128,13 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
* Schedule the timer for the next update.
|
||||
*/
|
||||
const scheduleTimer = function () {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = setTimeout(function () {
|
||||
fetchNews();
|
||||
}, reloadIntervalMS);
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
// only set timer when not running in jest
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = setTimeout(function () {
|
||||
fetchNews();
|
||||
}, reloadIntervalMS);
|
||||
}
|
||||
};
|
||||
|
||||
/* public methods */
|
||||
|
@@ -1,10 +1,3 @@
|
||||
/* 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");
|
||||
|
@@ -1,7 +1,7 @@
|
||||
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}/../../../`);
|
||||
@@ -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));
|
||||
|
@@ -1,3 +1,5 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const NodeHelper = require("node_helper");
|
||||
const defaultModules = require("../defaultmodules");
|
||||
const GitHelper = require("./git_helper");
|
||||
@@ -14,8 +16,23 @@ module.exports = NodeHelper.create({
|
||||
gitHelper: new GitHelper(),
|
||||
updateHelper: null,
|
||||
|
||||
getModules (modules) {
|
||||
if (this.config.useModulesFromConfig) {
|
||||
return modules;
|
||||
} else {
|
||||
// get modules from modules-directory
|
||||
const moduleDir = path.normalize(`${__dirname}/../../`);
|
||||
const getDirectories = (source) => {
|
||||
return fs.readdirSync(source, { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory() && dirent.name !== "default")
|
||||
.map((dirent) => dirent.name);
|
||||
};
|
||||
return getDirectories(moduleDir);
|
||||
}
|
||||
},
|
||||
|
||||
async configureModules (modules) {
|
||||
for (const moduleName of modules) {
|
||||
for (const moduleName of this.getModules(modules)) {
|
||||
if (!this.ignoreUpdateChecking(moduleName)) {
|
||||
await this.gitHelper.add(moduleName);
|
||||
}
|
||||
|
@@ -1,9 +1,11 @@
|
||||
const Exec = require("child_process").exec;
|
||||
const Spawn = require("child_process").spawn;
|
||||
const commandExists = require("command-exists");
|
||||
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
|
||||
/*
|
||||
* class Updater
|
||||
* Allow to self updating 3rd party modules from command defined in config
|
||||
*
|
||||
* [constructor] read value in config:
|
||||
@@ -45,8 +47,8 @@ class Updater {
|
||||
this.autoRestart = config.updateAutorestart;
|
||||
this.moduleList = {};
|
||||
this.updating = false;
|
||||
this.usePM2 = false;
|
||||
this.PM2 = null;
|
||||
this.usePM2 = false; // don't use pm2 by default
|
||||
this.PM2Id = null; // pm2 process number
|
||||
this.version = global.version;
|
||||
this.root_path = global.root_path;
|
||||
Log.info("updatenotification: Updater Class Loaded!");
|
||||
@@ -83,13 +85,15 @@ class 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
|
||||
//};
|
||||
/*
|
||||
* 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,
|
||||
@@ -129,29 +133,30 @@ class Updater {
|
||||
});
|
||||
}
|
||||
|
||||
// restart rules (pm2 or npm start)
|
||||
// restart rules (pm2 or node --run start)
|
||||
restart () {
|
||||
if (this.usePM2) this.pm2Restart();
|
||||
else this.npmRestart();
|
||||
else this.nodeRestart();
|
||||
}
|
||||
|
||||
// restart MagicMiror with "pm2"
|
||||
// restart MagicMiror with "pm2": use PM2Id for restart it
|
||||
pm2Restart () {
|
||||
Log.info("updatenotification: PM2 will restarting MagicMirror...");
|
||||
Exec(`pm2 restart ${this.PM2}`, (err, std, sde) => {
|
||||
const pm2 = require("pm2");
|
||||
pm2.restart(this.PM2Id, (err, proc) => {
|
||||
if (err) {
|
||||
Log.error("updatenotification:[PM2] restart Error", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// restart MagicMiror with "npm start"
|
||||
npmRestart () {
|
||||
// restart MagicMiror with "node --run start"
|
||||
nodeRestart () {
|
||||
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();
|
||||
const subprocess = Spawn("node --run start", { cwd: this.root_path, shell: true, detached: true, stdio: ["ignore", out, err] });
|
||||
subprocess.unref(); // detach the newly launched process from the master process
|
||||
process.exit();
|
||||
}
|
||||
|
||||
@@ -159,53 +164,50 @@ class Updater {
|
||||
check_PM2_Process () {
|
||||
Log.info("updatenotification: Checking PM2 using...");
|
||||
return new Promise((resolve) => {
|
||||
commandExists("pm2")
|
||||
.then(async () => {
|
||||
var PM2_List = await this.PM2_GetList();
|
||||
if (!PM2_List) {
|
||||
if (fs.existsSync("/.dockerenv")) {
|
||||
Log.info("updatenotification: Running in docker container, not using PM2 ...");
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.unique_id === undefined) {
|
||||
Log.info("updatenotification: [PM2] You are not using pm2");
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.debug(`updatenotification: [PM2] Search for pm2 id: ${process.env.pm_id} -- name: ${process.env.name} -- unique_id: ${process.env.unique_id}`);
|
||||
|
||||
const pm2 = require("pm2");
|
||||
pm2.connect((err) => {
|
||||
if (err) {
|
||||
Log.error("updatenotification: [PM2]", err);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
pm2.list((err, list) => {
|
||||
if (err) {
|
||||
Log.error("updatenotification: [PM2] Can't get process List!");
|
||||
this.usePM2 = false;
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
PM2_List.forEach((pm) => {
|
||||
if (pm.pm2_env.version === this.version && pm.pm2_env.status === "online" && pm.pm2_env.PWD.includes(this.root_path)) {
|
||||
this.PM2 = pm.name;
|
||||
list.forEach((pm) => {
|
||||
Log.debug(`updatenotification: [PM2] found pm2 process id: ${pm.pm_id} -- name: ${pm.name} -- unique_id: ${pm.pm2_env.unique_id}`);
|
||||
if (pm.pm2_env.status === "online" && process.env.name === pm.name && +process.env.pm_id === +pm.pm_id && process.env.unique_id === pm.pm2_env.unique_id) {
|
||||
this.PM2Id = pm.pm_id;
|
||||
this.usePM2 = true;
|
||||
Log.info("updatenotification: You are using pm2 with", this.PM2);
|
||||
Log.info(`updatenotification: [PM2] You are using pm2 with id: ${this.PM2Id} (${pm.name})`);
|
||||
resolve(true);
|
||||
} else {
|
||||
Log.debug(`updatenotification: [PM2] pm2 process id: ${pm.pm_id} don't match...`);
|
||||
}
|
||||
});
|
||||
if (!this.PM2) {
|
||||
Log.info("updatenotification: You are not using pm2");
|
||||
this.usePM2 = false;
|
||||
pm2.disconnect();
|
||||
if (!this.usePM2) {
|
||||
Log.info("updatenotification: [PM2] You are not using pm2");
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
Log.info("updatenotification: You are not using pm2");
|
||||
this.usePM2 = false;
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get the list of pm2 process
|
||||
PM2_GetList () {
|
||||
return new Promise((resolve) => {
|
||||
Exec("pm2 jlist", (err, std, sde) => {
|
||||
if (err) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let result = JSON.parse(std);
|
||||
resolve(result);
|
||||
} catch (e) {
|
||||
Log.error("updatenotification: [PM2] can't GetList!");
|
||||
Log.debug("updatenotification: [PM2] GetList is not an JSON format", e);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -218,7 +220,7 @@ class Updater {
|
||||
|
||||
// search update module command
|
||||
applyCommand (module) {
|
||||
if (this.isMagicMirror(module.module)) return null;
|
||||
if (this.isMagicMirror(module.module) || !this.updates.length) return null;
|
||||
let command = null;
|
||||
this.updates.forEach((updater) => {
|
||||
if (updater[module]) command = updater[module];
|
||||
|
@@ -1,9 +1,3 @@
|
||||
/* MagicMirror²
|
||||
* Module: UpdateNotification
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*/
|
||||
Module.register("updatenotification", {
|
||||
defaults: {
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
@@ -12,13 +6,14 @@ Module.register("updatenotification", {
|
||||
sendUpdatesNotifications: false,
|
||||
updates: [],
|
||||
updateTimeout: 2 * 60 * 1000, // max update duration
|
||||
updateAutorestart: false // autoRestart MM when update done ?
|
||||
updateAutorestart: false, // autoRestart MM when update done ?
|
||||
useModulesFromConfig: true // if `false` iterate over modules directory
|
||||
},
|
||||
|
||||
suspended: false,
|
||||
moduleList: {},
|
||||
needRestart: false,
|
||||
updates: {},
|
||||
updates: [],
|
||||
|
||||
start () {
|
||||
Log.info(`Starting module: ${this.name}`);
|
||||
@@ -102,7 +97,7 @@ 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>`;
|
||||
});
|
||||
},
|
||||
|
||||
|
@@ -5,13 +5,14 @@
|
||||
* @param {boolean} useCorsProxy A flag to indicate
|
||||
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
|
||||
* @param {string} basePath The base path, default is "/"
|
||||
* @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, basePath = "/") {
|
||||
const request = {};
|
||||
let requestUrl;
|
||||
if (useCorsProxy) {
|
||||
requestUrl = getCorsUrl(url, requestHeaders, expectedResponseHeaders);
|
||||
requestUrl = getCorsUrl(url, requestHeaders, expectedResponseHeaders, basePath);
|
||||
} else {
|
||||
requestUrl = url;
|
||||
request.headers = getHeadersToSend(requestHeaders);
|
||||
@@ -37,13 +38,14 @@ async function performWebRequest (url, type = "json", useCorsProxy = false, requ
|
||||
* @param {string} url the url to fetch from
|
||||
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
|
||||
* @param {string} basePath The base path, default is "/"
|
||||
* @returns {string} to be used as URL when calling CORS-method on server.
|
||||
*/
|
||||
const getCorsUrl = function (url, requestHeaders, expectedResponseHeaders) {
|
||||
const getCorsUrl = function (url, requestHeaders, expectedResponseHeaders, basePath = "/") {
|
||||
if (!url || url.length < 1) {
|
||||
throw new Error(`Invalid URL: ${url}`);
|
||||
} else {
|
||||
let corsUrl = `${location.protocol}//${location.host}/cors?`;
|
||||
let corsUrl = `${location.protocol}//${location.host}${basePath}cors?`;
|
||||
|
||||
const requestHeaderString = getRequestHeaderString(requestHeaders);
|
||||
if (requestHeaderString) corsUrl = `${corsUrl}sendheaders=${requestHeaderString}`;
|
||||
|
@@ -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,8 +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 %}
|
||||
@@ -16,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>
|
||||
@@ -37,28 +41,39 @@
|
||||
{% 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 />
|
||||
@@ -76,6 +91,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if config.showHumidity === "below" %}
|
||||
<span class="medium dimmed">{{ humidity() }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
||||
{% endif %}
|
||||
|
@@ -21,15 +21,25 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if config.showHumidity != "none" %}
|
||||
<td class="align-left bright humidity-hourly">
|
||||
{{ hour.humidity }}
|
||||
<span class="wi wi-humidity humidity-icon"></span>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationAmount %}
|
||||
<td class="align-right bright precipitation-amount">
|
||||
{{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }}
|
||||
</td>
|
||||
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
|
||||
<td class="align-right bright precipitation-amount">
|
||||
{{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationProbability %}
|
||||
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
|
||||
<td class="align-right bright precipitation-prob">
|
||||
{{ hour.precipitationProbability | unit('precip', '%') }}
|
||||
{{ hour.precipitationProbability | unit('precip', '%') }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% set currentStep = currentStep + 1 %}
|
||||
|
@@ -1,9 +1,6 @@
|
||||
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: Environment Canada (EC)
|
||||
*
|
||||
/*
|
||||
* This class is a provider for Environment Canada MSC Datamart
|
||||
* Note that this is only for Canadian locations and does not require an API key (access is anonymous)
|
||||
*
|
||||
@@ -27,13 +24,9 @@
|
||||
* with locations you can search under column B (English Names), with the corresponding siteCode under
|
||||
* column A (Codes) and provCode under column C (Province).
|
||||
*
|
||||
* Original by Kevin Godin
|
||||
*
|
||||
* License to use Environment Canada (EC) data is detailed here:
|
||||
* https://eccc-msc.github.io/open-data/licence/readme_en/
|
||||
*
|
||||
*/
|
||||
|
||||
WeatherProvider.register("envcanada", {
|
||||
// Set the name of the provider for debugging and alerting purposes (eg. provide eye-catcher)
|
||||
providerName: "Environment Canada",
|
||||
@@ -45,10 +38,10 @@ WeatherProvider.register("envcanada", {
|
||||
provCode: "ON"
|
||||
},
|
||||
|
||||
//
|
||||
// Set config values (equates to weather module config values). Also set values pertaining to caching of
|
||||
// Today's temperature forecast (for use in the Forecast functions below)
|
||||
//
|
||||
/*
|
||||
* 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 (config) {
|
||||
this.config = config;
|
||||
|
||||
@@ -58,17 +51,17 @@ WeatherProvider.register("envcanada", {
|
||||
this.cacheCurrentTemp = 999;
|
||||
},
|
||||
|
||||
//
|
||||
// Called when the weather provider is started
|
||||
//
|
||||
/*
|
||||
* Called when the weather provider is started
|
||||
*/
|
||||
start () {
|
||||
Log.info(`Weather provider: ${this.providerName} started.`);
|
||||
this.setFetchedLocation(this.config.location);
|
||||
},
|
||||
|
||||
//
|
||||
// Override the fetchCurrentWeather method to query EC and construct a Current weather object
|
||||
//
|
||||
/*
|
||||
* Override the fetchCurrentWeather method to query EC and construct a Current weather object
|
||||
*/
|
||||
fetchCurrentWeather () {
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
@@ -86,9 +79,9 @@ WeatherProvider.register("envcanada", {
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
//
|
||||
// Override the fetchWeatherForecast method to query EC and construct Forecast weather objects
|
||||
//
|
||||
/*
|
||||
* Override the fetchWeatherForecast method to query EC and construct Forecast weather objects
|
||||
*/
|
||||
fetchWeatherForecast () {
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
@@ -106,9 +99,9 @@ WeatherProvider.register("envcanada", {
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
//
|
||||
// Override the fetchWeatherHourly method to query EC and construct Forecast weather objects
|
||||
//
|
||||
/*
|
||||
* Override the fetchWeatherHourly method to query EC and construct Forecast weather objects
|
||||
*/
|
||||
fetchWeatherHourly () {
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
@@ -126,36 +119,30 @@ WeatherProvider.register("envcanada", {
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Environment Canada methods - not part of the standard Provider methods
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
//
|
||||
// Build the EC URL based on the Site Code and Province Code specified in the config params. Note that the
|
||||
// URL defaults to the English version simply because there is no language dependency in the data
|
||||
// being accessed. This is only pertinent when using the EC data elements that contain a textual forecast.
|
||||
//
|
||||
/*
|
||||
* Build the EC URL based on the Site Code and Province Code specified in the config params. Note that the
|
||||
* URL defaults to the English version simply because there is no language dependency in the data
|
||||
* being accessed. This is only pertinent when using the EC data elements that contain a textual forecast.
|
||||
*/
|
||||
getUrl () {
|
||||
return `https://dd.weather.gc.ca/citypage_weather/xml/${this.config.provCode}/${this.config.siteCode}_e.xml`;
|
||||
},
|
||||
|
||||
//
|
||||
// Generate a WeatherObject based on current EC weather conditions
|
||||
//
|
||||
|
||||
/*
|
||||
* Generate a WeatherObject based on current EC weather conditions
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather (ECdoc) {
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
// There are instances where EC will update weather data and current temperature will not be
|
||||
// provided. While this is a defect in the EC systems, we need to accommodate to avoid a current temp
|
||||
// of NaN being displayed. Therefore... whenever we get a valid current temp from EC, we will cache
|
||||
// the value. Whenever EC data is missing current temp, we will provide the cached value
|
||||
// instead. This is reasonable since the cached value will typically be accurate within the previous
|
||||
// hour. The only time this does not work as expected is when MM is restarted and the first query to
|
||||
// EC finds no current temp. In this scenario, MM will end up displaying a current temp of null;
|
||||
|
||||
/*
|
||||
* There are instances where EC will update weather data and current temperature will not be
|
||||
* provided. While this is a defect in the EC systems, we need to accommodate to avoid a current temp
|
||||
* of NaN being displayed. Therefore... whenever we get a valid current temp from EC, we will cache
|
||||
* the value. Whenever EC data is missing current temp, we will provide the cached value
|
||||
* instead. This is reasonable since the cached value will typically be accurate within the previous
|
||||
* hour. The only time this does not work as expected is when MM is restarted and the first query to
|
||||
* EC finds no current temp. In this scenario, MM will end up displaying a current temp of null;
|
||||
*/
|
||||
if (ECdoc.querySelector("siteData currentConditions temperature").textContent) {
|
||||
currentWeather.temperature = ECdoc.querySelector("siteData currentConditions temperature").textContent;
|
||||
this.cacheCurrentTemp = currentWeather.temperature;
|
||||
@@ -169,19 +156,19 @@ WeatherProvider.register("envcanada", {
|
||||
|
||||
currentWeather.humidity = ECdoc.querySelector("siteData currentConditions relativeHumidity").textContent;
|
||||
|
||||
// Ensure showPrecipitationAmount is forced to false. EC does not really provide POP for current day
|
||||
// and this feature for the weather module (current only) is sort of broken in that it wants
|
||||
// to say POP but will display precip as an accumulated amount vs. a percentage.
|
||||
|
||||
/*
|
||||
* Ensure showPrecipitationAmount is forced to false. EC does not really provide POP for current day
|
||||
* and this feature for the weather module (current only) is sort of broken in that it wants
|
||||
* to say POP but will display precip as an accumulated amount vs. a percentage.
|
||||
*/
|
||||
this.config.showPrecipitationAmount = false;
|
||||
|
||||
//
|
||||
// If the module config wants to showFeelsLike... default to the current temperature.
|
||||
// Check for EC wind chill and humidex values and overwrite the feelsLikeTemp value.
|
||||
// This assumes that the EC current conditions will never contain both a wind chill
|
||||
// and humidex temperature.
|
||||
//
|
||||
|
||||
/*
|
||||
* If the module config wants to showFeelsLike... default to the current temperature.
|
||||
* Check for EC wind chill and humidex values and overwrite the feelsLikeTemp value.
|
||||
* This assumes that the EC current conditions will never contain both a wind chill
|
||||
* and humidex temperature.
|
||||
*/
|
||||
if (this.config.showFeelsLike) {
|
||||
currentWeather.feelsLikeTemp = currentWeather.temperature;
|
||||
|
||||
@@ -194,16 +181,10 @@ WeatherProvider.register("envcanada", {
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Need to map EC weather icon to MM weatherType values
|
||||
//
|
||||
|
||||
currentWeather.weatherType = this.convertWeatherType(ECdoc.querySelector("siteData currentConditions iconCode").textContent);
|
||||
|
||||
//
|
||||
// Capture the sunrise and sunset values from EC data
|
||||
//
|
||||
|
||||
const sunList = ECdoc.querySelectorAll("siteData riseSet dateTime");
|
||||
|
||||
currentWeather.sunrise = moment(sunList[1].querySelector("timeStamp").textContent, "YYYYMMDDhhmmss");
|
||||
@@ -212,13 +193,11 @@ WeatherProvider.register("envcanada", {
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
//
|
||||
// Generate an array of WeatherObjects based on EC weather forecast
|
||||
//
|
||||
|
||||
/*
|
||||
* Generate an array of WeatherObjects based on EC weather forecast
|
||||
*/
|
||||
generateWeatherObjectsFromForecast (ECdoc) {
|
||||
// Declare an array to hold each day's forecast object
|
||||
|
||||
const days = [];
|
||||
|
||||
const weather = new WeatherObject();
|
||||
@@ -232,37 +211,33 @@ WeatherProvider.register("envcanada", {
|
||||
|
||||
weather.precipitationAmount = null;
|
||||
|
||||
//
|
||||
// The EC forecast is held in a 12-element array - Elements 0 to 11 - with each day encompassing
|
||||
// 2 elements. the first element for a day details the Today (daytime) forecast while the second
|
||||
// element details the Tonight (nightime) forecast. Element 0 is always for the current day.
|
||||
//
|
||||
// However... the forecast is somewhat 'rolling'.
|
||||
//
|
||||
// If the EC forecast is queried in the morning, then Element 0 will contain Current
|
||||
// Today and Element 1 will contain Current Tonight. From there, the next 5 days of forecast will be
|
||||
// contained in Elements 2/3, 4/5, 6/7, 8/9, and 10/11. This module will create a 6-day forecast using
|
||||
// all of these Elements.
|
||||
//
|
||||
// But, if the EC forecast is queried in late afternoon, the Current Today forecast will be rolled
|
||||
// off and Element 0 will contain Current Tonight. From there, the next 5 days will be contained in
|
||||
// Elements 1/2, 3/4, 5/6, 7/8, and 9/10. As well, Elelement 11 will contain a forecast for a 6th day,
|
||||
// but only for the Today portion (not Tonight). This module will create a 6-day forecast using
|
||||
// Elements 0 to 11, and will ignore the additional Todat forecast in Element 11.
|
||||
//
|
||||
// We need to determine if Element 0 is showing the forecast for Current Today or Current Tonight.
|
||||
// This is required to understand how Min and Max temperature will be determined, and to understand
|
||||
// where the next day's (aka Tomorrow's) forecast is located in the forecast array.
|
||||
//
|
||||
|
||||
/*
|
||||
* The EC forecast is held in a 12-element array - Elements 0 to 11 - with each day encompassing
|
||||
* 2 elements. the first element for a day details the Today (daytime) forecast while the second
|
||||
* element details the Tonight (nightime) forecast. Element 0 is always for the current day.
|
||||
*
|
||||
* However... the forecast is somewhat 'rolling'.
|
||||
*
|
||||
* If the EC forecast is queried in the morning, then Element 0 will contain Current
|
||||
* Today and Element 1 will contain Current Tonight. From there, the next 5 days of forecast will be
|
||||
* contained in Elements 2/3, 4/5, 6/7, 8/9, and 10/11. This module will create a 6-day forecast using
|
||||
* all of these Elements.
|
||||
*
|
||||
* But, if the EC forecast is queried in late afternoon, the Current Today forecast will be rolled
|
||||
* off and Element 0 will contain Current Tonight. From there, the next 5 days will be contained in
|
||||
* Elements 1/2, 3/4, 5/6, 7/8, and 9/10. As well, Elelement 11 will contain a forecast for a 6th day,
|
||||
* but only for the Today portion (not Tonight). This module will create a 6-day forecast using
|
||||
* Elements 0 to 11, and will ignore the additional Todat forecast in Element 11.
|
||||
*
|
||||
* We need to determine if Element 0 is showing the forecast for Current Today or Current Tonight.
|
||||
* This is required to understand how Min and Max temperature will be determined, and to understand
|
||||
* where the next day's (aka Tomorrow's) forecast is located in the forecast array.
|
||||
*/
|
||||
let nextDay = 0;
|
||||
let lastDay = 0;
|
||||
const currentTemp = ECdoc.querySelector("siteData currentConditions temperature").textContent;
|
||||
|
||||
//
|
||||
// If the first Element is Current Today, look at Current Today and Current Tonight for the current day.
|
||||
//
|
||||
|
||||
if (foreGroup[0].querySelector("period[textForecastName='Today']")) {
|
||||
this.todaytempCacheMin = 0;
|
||||
this.todaytempCacheMax = 0;
|
||||
@@ -272,167 +247,144 @@ WeatherProvider.register("envcanada", {
|
||||
|
||||
this.setPrecipitation(weather, foreGroup, 0);
|
||||
|
||||
//
|
||||
// Set the Element number that will reflect where the next day's forecast is located. Also set
|
||||
// the Element number where the end of the forecast will be. This is important because of the
|
||||
// rolling nature of the EC forecast. In the current scenario (Today and Tonight are present
|
||||
// in elements 0 and 11, we know that we will have 6 full days of forecasts and we will use
|
||||
// them. We will set lastDay such that we iterate through all 12 elements of the forecast.
|
||||
//
|
||||
|
||||
/*
|
||||
* Set the Element number that will reflect where the next day's forecast is located. Also set
|
||||
* the Element number where the end of the forecast will be. This is important because of the
|
||||
* rolling nature of the EC forecast. In the current scenario (Today and Tonight are present
|
||||
* in elements 0 and 11, we know that we will have 6 full days of forecasts and we will use
|
||||
* them. We will set lastDay such that we iterate through all 12 elements of the forecast.
|
||||
*/
|
||||
nextDay = 2;
|
||||
lastDay = 12;
|
||||
}
|
||||
|
||||
//
|
||||
// If the first Element is Current Tonight, look at Tonight only for the current day.
|
||||
//
|
||||
if (foreGroup[0].querySelector("period[textForecastName='Tonight']")) {
|
||||
this.setMinMaxTemps(weather, foreGroup, 0, false, currentTemp);
|
||||
|
||||
this.setPrecipitation(weather, foreGroup, 0);
|
||||
|
||||
//
|
||||
// Set the Element number that will reflect where the next day's forecast is located. Also set
|
||||
// the Element number where the end of the forecast will be. This is important because of the
|
||||
// rolling nature of the EC forecast. In the current scenario (only Current Tonight is present
|
||||
// in Element 0, we know that we will have 6 full days of forecasts PLUS a half-day and
|
||||
// forecast in the final element. Because we will only use full day forecasts, we set the
|
||||
// lastDay number to ensure we ignore that final half-day (in forecast Element 11).
|
||||
//
|
||||
|
||||
/*
|
||||
* Set the Element number that will reflect where the next day's forecast is located. Also set
|
||||
* the Element number where the end of the forecast will be. This is important because of the
|
||||
* rolling nature of the EC forecast. In the current scenario (only Current Tonight is present
|
||||
* in Element 0, we know that we will have 6 full days of forecasts PLUS a half-day and
|
||||
* forecast in the final element. Because we will only use full day forecasts, we set the
|
||||
* lastDay number to ensure we ignore that final half-day (in forecast Element 11).
|
||||
*/
|
||||
nextDay = 1;
|
||||
lastDay = 11;
|
||||
}
|
||||
|
||||
//
|
||||
// Need to map EC weather icon to MM weatherType values. Always pick the first Element's icon to
|
||||
// reflect either Today or Tonight depending on what the forecast is showing in Element 0.
|
||||
//
|
||||
|
||||
/*
|
||||
* Need to map EC weather icon to MM weatherType values. Always pick the first Element's icon to
|
||||
* reflect either Today or Tonight depending on what the forecast is showing in Element 0.
|
||||
*/
|
||||
weather.weatherType = this.convertWeatherType(foreGroup[0].querySelector("abbreviatedForecast iconCode").textContent);
|
||||
|
||||
// Push the weather object into the forecast array.
|
||||
|
||||
days.push(weather);
|
||||
|
||||
//
|
||||
// Now do the rest of the forecast starting at nextDay. We will process each day using 2 EC
|
||||
// forecast Elements. This will address the fact that the EC forecast always includes Today and
|
||||
// Tonight for each day. This is why we iterate through the forecast by a a count of 2, with each
|
||||
// iteration looking at the current Element and the next Element.
|
||||
//
|
||||
|
||||
/*
|
||||
* Now do the rest of the forecast starting at nextDay. We will process each day using 2 EC
|
||||
* forecast Elements. This will address the fact that the EC forecast always includes Today and
|
||||
* Tonight for each day. This is why we iterate through the forecast by a a count of 2, with each
|
||||
* iteration looking at the current Element and the next Element.
|
||||
*/
|
||||
let lastDate = moment(baseDate, "YYYYMMDDhhmmss");
|
||||
|
||||
for (let stepDay = nextDay; stepDay < lastDay; stepDay += 2) {
|
||||
let weather = new WeatherObject();
|
||||
|
||||
// Add 1 to the date to reflect the current forecast day we are building
|
||||
|
||||
lastDate = lastDate.add(1, "day");
|
||||
weather.date = moment(lastDate);
|
||||
|
||||
// Capture the temperatures for the current Element and the next Element in order to set
|
||||
// the Min and Max temperatures for the forecast
|
||||
|
||||
/*
|
||||
* Capture the temperatures for the current Element and the next Element in order to set
|
||||
* the Min and Max temperatures for the forecast
|
||||
*/
|
||||
this.setMinMaxTemps(weather, foreGroup, stepDay, true, currentTemp);
|
||||
|
||||
weather.precipitationAmount = null;
|
||||
|
||||
this.setPrecipitation(weather, foreGroup, stepDay);
|
||||
|
||||
//
|
||||
// Need to map EC weather icon to MM weatherType values. Always pick the first Element icon.
|
||||
//
|
||||
|
||||
weather.weatherType = this.convertWeatherType(foreGroup[stepDay].querySelector("abbreviatedForecast iconCode").textContent);
|
||||
|
||||
// Push the weather object into the forecast array.
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
//
|
||||
// Generate an array of WeatherObjects based on EC hourly weather forecast
|
||||
//
|
||||
|
||||
/*
|
||||
* Generate an array of WeatherObjects based on EC hourly weather forecast
|
||||
*/
|
||||
generateWeatherObjectsFromHourly (ECdoc) {
|
||||
// Declare an array to hold each hour's forecast object
|
||||
|
||||
const hours = [];
|
||||
|
||||
// Get local timezone UTC offset so that each hourly time can be calculated properly
|
||||
|
||||
const baseHours = ECdoc.querySelectorAll("siteData hourlyForecastGroup dateTime");
|
||||
const hourOffset = baseHours[1].getAttribute("UTCOffset");
|
||||
|
||||
//
|
||||
// The EC hourly forecast is held in a 24-element array - Elements 0 to 23 - with Element 0 holding
|
||||
// the forecast for the next 'on the hour' timeslot. This means the array is a rolling 24 hours.
|
||||
//
|
||||
|
||||
/*
|
||||
* The EC hourly forecast is held in a 24-element array - Elements 0 to 23 - with Element 0 holding
|
||||
* the forecast for the next 'on the hour' timeslot. This means the array is a rolling 24 hours.
|
||||
*/
|
||||
const hourGroup = ECdoc.querySelectorAll("siteData hourlyForecastGroup hourlyForecast");
|
||||
|
||||
for (let stepHour = 0; stepHour < 24; stepHour += 1) {
|
||||
const weather = new WeatherObject();
|
||||
|
||||
// Determine local time by applying UTC offset to the forecast timestamp
|
||||
|
||||
const foreTime = moment(hourGroup[stepHour].getAttribute("dateTimeUTC"), "YYYYMMDDhhmmss");
|
||||
const currTime = foreTime.add(hourOffset, "hours");
|
||||
weather.date = moment(currTime);
|
||||
|
||||
// Capture the temperature
|
||||
|
||||
weather.temperature = hourGroup[stepHour].querySelector("temperature").textContent;
|
||||
|
||||
// Capture Likelihood of Precipitation (LOP) and unit-of-measure values
|
||||
|
||||
const precipLOP = hourGroup[stepHour].querySelector("lop").textContent * 1.0;
|
||||
|
||||
if (precipLOP > 0) {
|
||||
weather.precipitationProbability = precipLOP;
|
||||
}
|
||||
|
||||
//
|
||||
// Need to map EC weather icon to MM weatherType values. Always pick the first Element icon.
|
||||
//
|
||||
|
||||
weather.weatherType = this.convertWeatherType(hourGroup[stepHour].querySelector("iconCode").textContent);
|
||||
|
||||
// Push the weather object into the forecast array.
|
||||
|
||||
hours.push(weather);
|
||||
}
|
||||
|
||||
return hours;
|
||||
},
|
||||
//
|
||||
// Determine Min and Max temp based on a supplied Forecast Element index and a boolen that denotes if
|
||||
// the next Forecast element should be considered - i.e. look at Today *and* Tonight vs.Tonight-only
|
||||
//
|
||||
|
||||
/*
|
||||
* Determine Min and Max temp based on a supplied Forecast Element index and a boolen that denotes if
|
||||
* the next Forecast element should be considered - i.e. look at Today *and* Tonight vs.Tonight-only
|
||||
*/
|
||||
setMinMaxTemps (weather, foreGroup, today, fullDay, currentTemp) {
|
||||
const todayTemp = foreGroup[today].querySelector("temperatures temperature").textContent;
|
||||
|
||||
const todayClass = foreGroup[today].querySelector("temperatures temperature").getAttribute("class");
|
||||
|
||||
//
|
||||
// The following logic is largely aimed at accommodating the Current day's forecast whereby we
|
||||
// can have either Current Today+Current Tonight or only Current Tonight.
|
||||
//
|
||||
// If fullDay is false, then we only have Tonight for the current day's forecast - meaning we have
|
||||
// lost a min or max temp value for the day. Therefore, we will see if we were able to cache the the
|
||||
// Today forecast for the current day. If we have, we will use them. If we do not have the cached values,
|
||||
// it means that MM or the Computer has been restarted since the time EC rolled off Today from the
|
||||
// forecast. In this scenario, we will simply default to the Current Conditions temperature and then
|
||||
// check the Tonight temperature.
|
||||
//
|
||||
|
||||
/*
|
||||
* The following logic is largely aimed at accommodating the Current day's forecast whereby we
|
||||
* can have either Current Today+Current Tonight or only Current Tonight.
|
||||
*
|
||||
* If fullDay is false, then we only have Tonight for the current day's forecast - meaning we have
|
||||
* lost a min or max temp value for the day. Therefore, we will see if we were able to cache the the
|
||||
* Today forecast for the current day. If we have, we will use them. If we do not have the cached values,
|
||||
* it means that MM or the Computer has been restarted since the time EC rolled off Today from the
|
||||
* forecast. In this scenario, we will simply default to the Current Conditions temperature and then
|
||||
* check the Tonight temperature.x
|
||||
*/
|
||||
if (fullDay === false) {
|
||||
if (this.todayCached === true) {
|
||||
weather.minTemperature = this.todayTempCacheMin;
|
||||
@@ -443,13 +395,12 @@ WeatherProvider.register("envcanada", {
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// We will check to see if the current Element's temperature is Low or High and set weather values
|
||||
// accordingly. We will also check the condition where fullDay is true *and* we are looking at forecast
|
||||
// element 0. This is a special case where we will cache temperature values so that we have them later
|
||||
// in the current day when the Current Today element rolls off and we have Current Tonight only.
|
||||
//
|
||||
|
||||
/*
|
||||
* We will check to see if the current Element's temperature is Low or High and set weather values
|
||||
* accordingly. We will also check the condition where fullDay is true *and* we are looking at forecast
|
||||
* element 0. This is a special case where we will cache temperature values so that we have them later
|
||||
* in the current day when the Current Today element rolls off and we have Current Tonight only.
|
||||
*/
|
||||
if (todayClass === "low") {
|
||||
weather.minTemperature = todayTemp;
|
||||
if (today === 0 && fullDay === true) {
|
||||
@@ -479,25 +430,24 @@ WeatherProvider.register("envcanada", {
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Check for a Precipitation forecast. EC can provide a forecast in 2 ways: either an accumulation figure
|
||||
// or a POP percentage. If there is a POP, then that is what the module will show. If there is an accumulation,
|
||||
// then it will be displayed ONLY if no POP is present.
|
||||
//
|
||||
// POP Logic: By default, we want to show the POP for 'daytime' since we are presuming that is what
|
||||
// people are more interested in seeing. While EC provides a separate POP for daytime and nightime portions
|
||||
// of each day, the weather module does not really allow for that view of a daily forecast. There we will
|
||||
// ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
|
||||
// the nightime forecast after a certain point in the afternoon. As such, we will be showing the nightime POP
|
||||
// (if one exists) in that specific scenario.
|
||||
//
|
||||
// Accumulation Logic: Similar to POP, we want to show accumulation for 'daytime' since we presume that is what
|
||||
// people are interested in seeing. While EC provides a separate accumulation for daytime and nightime portions
|
||||
// of each day, the weather module does not really allow for that view of a daily forecast. There we will
|
||||
// ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
|
||||
// the nightime forecast after a certain point in that specific scenario.
|
||||
//
|
||||
|
||||
/*
|
||||
* Check for a Precipitation forecast. EC can provide a forecast in 2 ways: either an accumulation figure
|
||||
* or a POP percentage. If there is a POP, then that is what the module will show. If there is an accumulation,
|
||||
* then it will be displayed ONLY if no POP is present.
|
||||
*
|
||||
* POP Logic: By default, we want to show the POP for 'daytime' since we are presuming that is what
|
||||
* people are more interested in seeing. While EC provides a separate POP for daytime and nightime portions
|
||||
* of each day, the weather module does not really allow for that view of a daily forecast. There we will
|
||||
* ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
|
||||
* the nightime forecast after a certain point in the afternoon. As such, we will be showing the nightime POP
|
||||
* (if one exists) in that specific scenario.
|
||||
*
|
||||
* Accumulation Logic: Similar to POP, we want to show accumulation for 'daytime' since we presume that is what
|
||||
* people are interested in seeing. While EC provides a separate accumulation for daytime and nightime portions
|
||||
* of each day, the weather module does not really allow for that view of a daily forecast. There we will
|
||||
* ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
|
||||
* the nightime forecast after a certain point in that specific scenario.
|
||||
*/
|
||||
setPrecipitation (weather, foreGroup, today) {
|
||||
if (foreGroup[today].querySelector("precipitation accumulation")) {
|
||||
weather.precipitationAmount = foreGroup[today].querySelector("precipitation accumulation amount").textContent * 1.0;
|
||||
@@ -511,9 +461,9 @@ WeatherProvider.register("envcanada", {
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Convert the icons to a more usable name.
|
||||
//
|
||||
/*
|
||||
* Convert the icons to a more usable name.
|
||||
*/
|
||||
convertWeatherType (weatherType) {
|
||||
const weatherTypes = {
|
||||
"00": "day-sunny",
|
||||
|
@@ -1,22 +1,20 @@
|
||||
/* 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";
|
||||
|
||||
WeatherProvider.register("openmeteo", {
|
||||
// Set the name of the provider.
|
||||
// Not strictly required, but helps for debugging.
|
||||
|
||||
/*
|
||||
* Set the name of the provider.
|
||||
* Not strictly required, but helps for debugging.
|
||||
*/
|
||||
providerName: "Open-Meteo",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
@@ -246,17 +244,17 @@ WeatherProvider.register("openmeteo", {
|
||||
.add(Math.max(0, Math.min(7, this.config.maxNumberOfDays)), "days")
|
||||
.endOf("day");
|
||||
|
||||
params["start_date"] = startDate.format("YYYY-MM-DD");
|
||||
params.start_date = startDate.format("YYYY-MM-DD");
|
||||
|
||||
switch (this.config.type) {
|
||||
case "hourly":
|
||||
case "daily":
|
||||
case "forecast":
|
||||
params["end_date"] = endDate.format("YYYY-MM-DD");
|
||||
params.end_date = endDate.format("YYYY-MM-DD");
|
||||
break;
|
||||
case "current":
|
||||
params["current_weather"] = true;
|
||||
params["end_date"] = params["start_date"];
|
||||
params.current_weather = true;
|
||||
params.end_date = params.start_date;
|
||||
break;
|
||||
default:
|
||||
// Failsafe
|
||||
@@ -264,7 +262,7 @@ WeatherProvider.register("openmeteo", {
|
||||
}
|
||||
|
||||
return Object.keys(params)
|
||||
.filter((key) => (params[key] ? true : false))
|
||||
.filter((key) => (!!params[key]))
|
||||
.map((key) => {
|
||||
switch (key) {
|
||||
case "hourly":
|
||||
@@ -404,14 +402,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);
|
||||
@@ -426,7 +424,7 @@ WeatherProvider.register("openmeteo", {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -438,9 +436,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);
|
||||
@@ -472,7 +470,7 @@ WeatherProvider.register("openmeteo", {
|
||||
61: "rain-slight-intensity",
|
||||
63: "rain-moderate-intensity",
|
||||
65: "rain-heavy-intensity",
|
||||
66: "freezing-rain-light-heavy-intensity",
|
||||
66: "freezing-rain-light-intensity",
|
||||
67: "freezing-rain-heavy-intensity",
|
||||
71: "snow-fall-slight-intensity",
|
||||
73: "snow-fall-moderate-intensity",
|
||||
|
@@ -1,27 +1,29 @@
|
||||
/* 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.
|
||||
// This isn't strictly necessary, since it will fallback to the provider identifier
|
||||
// But for debugging (and future alerts) it would be nice to have the real name.
|
||||
|
||||
/*
|
||||
* Set the name of the provider.
|
||||
* This isn't strictly necessary, since it will fallback to the provider identifier
|
||||
* But for debugging (and future alerts) it would be nice to have the real name.
|
||||
*/
|
||||
providerName: "OpenWeatherMap",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiVersion: "2.5",
|
||||
apiVersion: "3.0",
|
||||
apiBase: "https://api.openweathermap.org/data/",
|
||||
weatherEndpoint: "", // can be "onecall", "forecast" or "weather" (for current)
|
||||
// weatherEndpoint is "/onecall" since API 3.0
|
||||
// "/onecall", "/forecast" or "/weather" only for pro customers
|
||||
weatherEndpoint: "/onecall",
|
||||
locationID: false,
|
||||
location: false,
|
||||
lat: 0, // the onecall endpoint needs lat / lon values, it doesn't support the locationId
|
||||
// the /onecall endpoint needs lat / lon values, it doesn't support the locationId
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
apiKey: ""
|
||||
},
|
||||
@@ -72,8 +74,11 @@ WeatherProvider.register("openweathermap", {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
|
||||
/*
|
||||
* Did not receive usable new data.
|
||||
* Maybe this needs a better check?
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -88,30 +93,6 @@ WeatherProvider.register("openweathermap", {
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides method for setting config to check if endpoint is correct for hourly
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
setConfig (config) {
|
||||
this.config = config;
|
||||
if (!this.config.weatherEndpoint) {
|
||||
switch (this.config.type) {
|
||||
case "hourly":
|
||||
this.config.weatherEndpoint = "/onecall";
|
||||
break;
|
||||
case "daily":
|
||||
case "forecast":
|
||||
this.config.weatherEndpoint = "/forecast";
|
||||
break;
|
||||
case "current":
|
||||
this.config.weatherEndpoint = "/weather";
|
||||
break;
|
||||
default:
|
||||
Log.error("weatherEndpoint not configured and could not resolve it based on type");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** OpenWeatherMap Specific Methods - These are not part of the default provider methods */
|
||||
/*
|
||||
* Gets the complete url for the request
|
||||
@@ -211,8 +192,10 @@ WeatherProvider.register("openweathermap", {
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||
}
|
||||
|
||||
// the same day as before
|
||||
// add values from forecast to corresponding variables
|
||||
/*
|
||||
* the same day as before
|
||||
* add values from forecast to corresponding variables
|
||||
*/
|
||||
minTemp.push(forecast.main.temp_min);
|
||||
maxTemp.push(forecast.main.temp_max);
|
||||
|
||||
@@ -225,8 +208,10 @@ WeatherProvider.register("openweathermap", {
|
||||
}
|
||||
}
|
||||
|
||||
// last day
|
||||
// calculate minimum/maximum temperature, specify rain amount
|
||||
/*
|
||||
* last day
|
||||
* calculate minimum/maximum temperature, specify rain amount
|
||||
*/
|
||||
weather.minTemperature = Math.min.apply(null, minTemp);
|
||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||
weather.rain = rain;
|
||||
@@ -255,14 +240,18 @@ WeatherProvider.register("openweathermap", {
|
||||
weather.rain = 0;
|
||||
weather.snow = 0;
|
||||
|
||||
// forecast.rain not available if amount is zero
|
||||
// The API always returns in millimeters
|
||||
/*
|
||||
* forecast.rain not available if amount is zero
|
||||
* The API always returns in millimeters
|
||||
*/
|
||||
if (forecast.hasOwnProperty("rain") && !isNaN(forecast.rain)) {
|
||||
weather.rain = forecast.rain;
|
||||
}
|
||||
|
||||
// forecast.snow not available if amount is zero
|
||||
// The API always returns in millimeters
|
||||
/*
|
||||
* forecast.snow not available if amount is zero
|
||||
* The API always returns in millimeters
|
||||
*/
|
||||
if (forecast.hasOwnProperty("snow") && !isNaN(forecast.snow)) {
|
||||
weather.snow = forecast.snow;
|
||||
}
|
||||
@@ -296,12 +285,12 @@ WeatherProvider.register("openweathermap", {
|
||||
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"];
|
||||
if (data.current.hasOwnProperty("rain") && !isNaN(data.current.rain["1h"])) {
|
||||
current.rain = data.current.rain["1h"];
|
||||
precip = true;
|
||||
}
|
||||
if (data.current.hasOwnProperty("snow") && !isNaN(data.current["snow"]["1h"])) {
|
||||
current.snow = data.current["snow"]["1h"];
|
||||
if (data.current.hasOwnProperty("snow") && !isNaN(data.current.snow["1h"])) {
|
||||
current.snow = data.current.snow["1h"];
|
||||
precip = true;
|
||||
}
|
||||
if (precip) {
|
||||
@@ -407,7 +396,8 @@ WeatherProvider.register("openweathermap", {
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
},
|
||||
|
||||
/* getParams(compliments)
|
||||
/*
|
||||
* getParams(compliments)
|
||||
* Generates an url with api parameters based on the config.
|
||||
*
|
||||
* return String - URL params.
|
||||
|
@@ -1,18 +1,15 @@
|
||||
/* 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.
|
||||
// Not strictly required, but helps for debugging.
|
||||
|
||||
/*
|
||||
* Set the name of the provider.
|
||||
* Not strictly required, but helps for debugging.
|
||||
*/
|
||||
providerName: "pirateweather",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
|
@@ -1,14 +1,9 @@
|
||||
/* 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",
|
||||
@@ -144,9 +139,11 @@ WeatherProvider.register("smhi", {
|
||||
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), currentWeather.isDayTime());
|
||||
currentWeather.feelsLikeTemp = this.calculateApparentTemperature(weatherData);
|
||||
|
||||
// Determine the precipitation amount and category and update the
|
||||
// weatherObject with it, the valuetype to use can be configured or uses
|
||||
// median as default.
|
||||
/*
|
||||
* Determine the precipitation amount and category and update the
|
||||
* weatherObject with it, the valuetype to use can be configured or uses
|
||||
* median as default.
|
||||
*/
|
||||
let precipitationValue = this.paramValue(weatherData, this.config.precipitationValue);
|
||||
switch (this.paramValue(weatherData, "pcat")) {
|
||||
// 0 = No precipitation
|
||||
|
@@ -1,17 +1,16 @@
|
||||
/* 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.
|
||||
// This isn't strictly necessary, since it will fallback to the provider identifier
|
||||
// But for debugging (and future alerts) it would be nice to have the real name.
|
||||
|
||||
/*
|
||||
* Set the name of the provider.
|
||||
* This isn't strictly necessary, since it will fallback to the provider identifier
|
||||
* But for debugging (and future alerts) it would be nice to have the real name.
|
||||
*/
|
||||
providerName: "UK Met Office",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
@@ -26,8 +25,11 @@ WeatherProvider.register("ukmetoffice", {
|
||||
this.fetchData(this.getUrl("3hourly"))
|
||||
.then((data) => {
|
||||
if (!data || !data.SiteRep || !data.SiteRep.DV || !data.SiteRep.DV.Location || !data.SiteRep.DV.Location.Period || data.SiteRep.DV.Location.Period.length === 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
|
||||
/*
|
||||
* Did not receive usable new data.
|
||||
* Maybe this needs a better check?
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,8 +49,11 @@ WeatherProvider.register("ukmetoffice", {
|
||||
this.fetchData(this.getUrl("daily"))
|
||||
.then((data) => {
|
||||
if (!data || !data.SiteRep || !data.SiteRep.DV || !data.SiteRep.DV.Location || !data.SiteRep.DV.Location.Period || data.SiteRep.DV.Location.Period.length === 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
|
||||
/*
|
||||
* Did not receive usable new data.
|
||||
* Maybe this needs a better check?
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,8 +96,11 @@ WeatherProvider.register("ukmetoffice", {
|
||||
if (periodDate.isSameOrAfter(moment.utc().startOf("day"))) {
|
||||
// check this is the period we want, after today the diff will be -ve
|
||||
if (moment().diff(periodDate, "minutes") > 0) {
|
||||
// loop round the reports looking for the one we are in
|
||||
// $ value specifies the time in minutes-of-the-day: 0, 180, 360,...1260
|
||||
|
||||
/*
|
||||
* loop round the reports looking for the one we are in
|
||||
* $ value specifies the time in minutes-of-the-day: 0, 180, 360,...1260
|
||||
*/
|
||||
for (const rep of period.Rep) {
|
||||
const p = rep.$;
|
||||
if (timeInMins >= p && timeInMins - 180 < p) {
|
||||
@@ -122,8 +130,10 @@ WeatherProvider.register("ukmetoffice", {
|
||||
generateWeatherObjectsFromForecast (forecasts) {
|
||||
const days = [];
|
||||
|
||||
// loop round the (5) periods getting the data
|
||||
// for each period array, Day is [0], Night is [1]
|
||||
/*
|
||||
* loop round the (5) periods getting the data
|
||||
* for each period array, Day is [0], Night is [1]
|
||||
*/
|
||||
for (const period of forecasts.SiteRep.DV.Location.Period) {
|
||||
const weather = new WeatherObject();
|
||||
|
||||
|
@@ -1,12 +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).
|
||||
* For more information on Data Hub, see https://www.metoffice.gov.uk/services/data/datapoint/notifications/weather-datahub
|
||||
* Data available:
|
||||
@@ -18,9 +12,8 @@
|
||||
* This provider requires longitude/latitude coordinates, rather than a location ID (as with the previous Met Office provider)
|
||||
* Provide the following in your config.js file:
|
||||
* weatherProvider: "ukmetofficedatahub",
|
||||
* apiBase: "https://api-metoffice.apiconnect.ibmcloud.com/metoffice/production/v0/forecasts/point/",
|
||||
* apiBase: "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/",
|
||||
* apiKey: "[YOUR API KEY]",
|
||||
* apiSecret: "[YOUR API SECRET]",
|
||||
* lat: [LATITUDE (DECIMAL)],
|
||||
* lon: [LONGITUDE (DECIMAL)]
|
||||
*
|
||||
@@ -45,14 +38,13 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "https://api-metoffice.apiconnect.ibmcloud.com/metoffice/production/v0/forecasts/point/",
|
||||
apiBase: "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/",
|
||||
apiKey: "",
|
||||
apiSecret: "",
|
||||
lat: 0,
|
||||
lon: 0
|
||||
},
|
||||
|
||||
// Build URL with query strings according to DataHub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
|
||||
// Build URL with query strings according to DataHub API (https://datahub.metoffice.gov.uk/docs/f/category/site-specific/type/site-specific/api-documentation#get-/point/hourly)
|
||||
getUrl (forecastType) {
|
||||
let queryStrings = "?";
|
||||
queryStrings += `latitude=${this.config.lat}`;
|
||||
@@ -63,14 +55,15 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
return this.config.apiBase + (this.config.apiBase.endsWith("/") ? "" : "/") + forecastType + queryStrings;
|
||||
},
|
||||
|
||||
// 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)
|
||||
/*
|
||||
* 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://datahub.metoffice.gov.uk/docs/f/category/site-specific/type/site-specific/api-documentation#get-/point/hourly)
|
||||
*/
|
||||
getHeaders () {
|
||||
return {
|
||||
accept: "application/json",
|
||||
"x-ibm-client-id": this.config.apiKey,
|
||||
"x-ibm-client-secret": this.config.apiSecret
|
||||
apikey: this.config.apiKey
|
||||
};
|
||||
},
|
||||
|
||||
@@ -88,8 +81,11 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
.then((data) => {
|
||||
// Check data is usable
|
||||
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length === 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
|
||||
/*
|
||||
* Did not receive usable new data.
|
||||
* Maybe this needs a better check?
|
||||
*/
|
||||
Log.error("Possibly bad current/hourly data?");
|
||||
Log.error(data);
|
||||
return;
|
||||
@@ -137,15 +133,19 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
currentWeather.precipitationProbability = forecastDataHours[hour].probOfPrecipitation;
|
||||
currentWeather.feelsLikeTemp = forecastDataHours[hour].feelsLikeTemperature;
|
||||
|
||||
// Pass on full details, so they can be used in custom templates
|
||||
// Note the units of the supplied data when using this (see top of file)
|
||||
/*
|
||||
* Pass on full details, so they can be used in custom templates
|
||||
* Note the units of the supplied data when using this (see top of file)
|
||||
*/
|
||||
currentWeather.rawData = forecastDataHours[hour];
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the sunrise/sunset times - (still) not supplied in UK Met Office data
|
||||
// Passes {longitude, latitude} to SunCalc, could pass height to, but
|
||||
// SunCalc.getTimes doesn't take that into account
|
||||
/*
|
||||
* Determine the sunrise/sunset times - (still) not supplied in UK Met Office data
|
||||
* Passes {longitude, latitude} to SunCalc, could pass height to, but
|
||||
* SunCalc.getTimes doesn't take that into account
|
||||
*/
|
||||
currentWeather.updateSunTime(this.config.lat, this.config.lon);
|
||||
|
||||
return currentWeather;
|
||||
@@ -157,8 +157,11 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
.then((data) => {
|
||||
// Check data is usable
|
||||
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length === 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
|
||||
/*
|
||||
* Did not receive usable new data.
|
||||
* Maybe this needs a better check?
|
||||
*/
|
||||
Log.error("Possibly bad forecast data?");
|
||||
Log.error(data);
|
||||
return;
|
||||
@@ -213,8 +216,10 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
forecastWeather.snow = forecastDataDays[day].dayProbabilityOfSnow;
|
||||
forecastWeather.feelsLikeTemp = forecastDataDays[day].dayMaxFeelsLikeTemp;
|
||||
|
||||
// Pass on full details, so they can be used in custom templates
|
||||
// Note the units of the supplied data when using this (see top of file)
|
||||
/*
|
||||
* Pass on full details, so they can be used in custom templates
|
||||
* Note the units of the supplied data when using this (see top of file)
|
||||
*/
|
||||
forecastWeather.rawData = forecastDataDays[day];
|
||||
|
||||
dailyForecasts.push(forecastWeather);
|
||||
@@ -229,9 +234,11 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||
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/
|
||||
/*
|
||||
* 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) {
|
||||
const weatherTypes = {
|
||||
0: "night-clear",
|
||||
|
@@ -1,18 +1,15 @@
|
||||
/* 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.
|
||||
// Not strictly required, but helps for debugging.
|
||||
|
||||
/*
|
||||
* Set the name of the provider.
|
||||
* Not strictly required, but helps for debugging.
|
||||
*/
|
||||
providerName: "Weatherbit",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
|
@@ -1,19 +1,15 @@
|
||||
/* 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.
|
||||
* 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
|
||||
|
||||
/*
|
||||
* Set the name of the provider.
|
||||
* Not strictly required, but helps for debugging
|
||||
*/
|
||||
providerName: "WeatherFlow",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
@@ -29,14 +25,20 @@ WeatherProvider.register("weatherflow", {
|
||||
const currentWeather = new WeatherObject();
|
||||
currentWeather.date = moment();
|
||||
|
||||
// Other available values: air_density, brightness, delta_t, dew_point,
|
||||
// pressure_trend (i.e. rising/falling), sea_level_pressure, wind gust, and more.
|
||||
|
||||
currentWeather.humidity = data.current_conditions.relative_humidity;
|
||||
currentWeather.temperature = data.current_conditions.air_temperature;
|
||||
currentWeather.feelsLikeTemp = data.current_conditions.feels_like;
|
||||
currentWeather.windSpeed = WeatherUtils.convertWindToMs(data.current_conditions.wind_avg);
|
||||
currentWeather.windFromDirection = data.current_conditions.wind_direction;
|
||||
currentWeather.weatherType = data.forecast.daily[0].icon;
|
||||
currentWeather.weatherType = this.convertWeatherType(data.current_conditions.icon);
|
||||
currentWeather.uv_index = data.current_conditions.uv;
|
||||
currentWeather.sunrise = moment.unix(data.forecast.daily[0].sunrise);
|
||||
currentWeather.sunset = moment.unix(data.forecast.daily[0].sunset);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
this.fetchedLocationName = data.location_name;
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
@@ -56,13 +58,27 @@ WeatherProvider.register("weatherflow", {
|
||||
weather.minTemperature = forecast.air_temp_low;
|
||||
weather.maxTemperature = forecast.air_temp_high;
|
||||
weather.precipitationProbability = forecast.precip_probability;
|
||||
weather.weatherType = forecast.icon;
|
||||
weather.snow = 0;
|
||||
weather.weatherType = this.convertWeatherType(forecast.icon);
|
||||
|
||||
// Must manually build UV and Precipitation from hourly
|
||||
weather.precipitationAmount = 0.0; // This will sum up rain and snow
|
||||
weather.precipitationUnits = "mm";
|
||||
weather.uv_index = 0;
|
||||
|
||||
for (const hour of data.forecast.hourly) {
|
||||
const hour_time = moment.unix(hour.time);
|
||||
if (hour_time.day() === weather.date.day()) { // Iterate though until day is reached
|
||||
// Get data from today
|
||||
weather.uv_index = Math.max(weather.uv_index, hour.uv);
|
||||
weather.precipitationAmount += (hour.precip ?? 0);
|
||||
} else if (hour_time.diff(weather.date) >= 86400) {
|
||||
break; // No more data to be found
|
||||
}
|
||||
}
|
||||
days.push(weather);
|
||||
}
|
||||
|
||||
this.setWeatherForecast(days);
|
||||
this.fetchedLocationName = data.location_name;
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
@@ -70,6 +86,63 @@ WeatherProvider.register("weatherflow", {
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
fetchWeatherHourly () {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
const hours = [];
|
||||
for (const hour of data.forecast.hourly) {
|
||||
const weather = new WeatherObject();
|
||||
|
||||
weather.date = moment.unix(hour.time);
|
||||
weather.temperature = hour.air_temperature;
|
||||
weather.feelsLikeTemp = hour.feels_like;
|
||||
weather.humidity = hour.relative_humidity;
|
||||
weather.windSpeed = hour.wind_avg;
|
||||
weather.windFromDirection = hour.wind_direction;
|
||||
weather.weatherType = this.convertWeatherType(hour.icon);
|
||||
weather.precipitationProbability = hour.precip_probability;
|
||||
weather.precipitationAmount = hour.precip; // NOTE: precipitation type is available
|
||||
weather.precipitationUnits = "mm"; // Hardcoded via request, TODO: Add conversion
|
||||
weather.uv_index = hour.uv;
|
||||
|
||||
hours.push(weather);
|
||||
if (hours.length >= 48) break; // 10 days of hours are available, best to trim down.
|
||||
}
|
||||
this.setWeatherHourly(hours);
|
||||
this.fetchedLocationName = data.location_name;
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
convertWeatherType (weatherType) {
|
||||
const weatherTypes = {
|
||||
"clear-day": "day-sunny",
|
||||
"clear-night": "night-clear",
|
||||
cloudy: "cloudy",
|
||||
foggy: "fog",
|
||||
"partly-cloudy-day": "day-cloudy",
|
||||
"partly-cloudy-night": "night-alt-cloudy",
|
||||
"possibly-rainy-day": "day-rain",
|
||||
"possibly-rainy-night": "night-alt-rain",
|
||||
"possibly-sleet-day": "day-sleet",
|
||||
"possibly-sleet-night": "night-alt-sleet",
|
||||
"possibly-snow-day": "day-snow",
|
||||
"possibly-snow-night": "night-alt-snow",
|
||||
"possibly-thunderstorm-day": "day-thunderstorm",
|
||||
"possibly-thunderstorm-night": "night-alt-thunderstorm",
|
||||
rainy: "rain",
|
||||
sleet: "sleet",
|
||||
snow: "snow",
|
||||
thunderstorm: "thunderstorm",
|
||||
windy: "strong-wind"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
},
|
||||
|
||||
// Create a URL from the config and base URL.
|
||||
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,22 +1,21 @@
|
||||
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
WeatherProvider.register("weathergov", {
|
||||
// Set the name of the provider.
|
||||
// This isn't strictly necessary, since it will fallback to the provider identifier
|
||||
// But for debugging (and future alerts) it would be nice to have the real name.
|
||||
|
||||
/*
|
||||
* Set the name of the provider.
|
||||
* This isn't strictly necessary, since it will fallback to the provider identifier
|
||||
* But for debugging (and future alerts) it would be nice to have the real name.
|
||||
*/
|
||||
providerName: "Weather.gov",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
@@ -39,7 +38,6 @@ WeatherProvider.register("weathergov", {
|
||||
// Called to set the config, this config is the same as the weather module's config.
|
||||
setConfig (config) {
|
||||
this.config = config;
|
||||
this.config.apiBase = "https://api.weather.gov";
|
||||
this.fetchWxGovURLs(this.config);
|
||||
},
|
||||
|
||||
@@ -104,8 +102,11 @@ WeatherProvider.register("weathergov", {
|
||||
this.fetchData(this.forecastHourlyURL)
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
|
||||
/*
|
||||
* Did not receive usable new data.
|
||||
* Maybe this needs a better check?
|
||||
*/
|
||||
return;
|
||||
}
|
||||
const hourly = this.generateWeatherObjectsFromHourly(data.properties.periods);
|
||||
@@ -123,7 +124,7 @@ WeatherProvider.register("weathergov", {
|
||||
* Get specific URLs
|
||||
*/
|
||||
fetchWxGovURLs (config) {
|
||||
this.fetchData(`${config.apiBase}/points/${config.lat},${config.lon}`)
|
||||
this.fetchData(`${config.apiBase}/${config.lat},${config.lon}`)
|
||||
.then((data) => {
|
||||
if (!data || !data.properties) {
|
||||
// points URL did not respond with usable data.
|
||||
@@ -217,7 +218,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) {
|
||||
@@ -288,14 +289,18 @@ WeatherProvider.register("weathergov", {
|
||||
weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime);
|
||||
}
|
||||
|
||||
// the same day as before
|
||||
// add values from forecast to corresponding variables
|
||||
/*
|
||||
* the same day as before
|
||||
* add values from forecast to corresponding variables
|
||||
*/
|
||||
minTemp.push(forecast.temperature);
|
||||
maxTemp.push(forecast.temperature);
|
||||
}
|
||||
|
||||
// last day
|
||||
// calculate minimum/maximum temperature
|
||||
/*
|
||||
* last day
|
||||
* calculate minimum/maximum temperature
|
||||
*/
|
||||
weather.minTemperature = Math.min.apply(null, minTemp);
|
||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||
|
||||
@@ -308,8 +313,11 @@ WeatherProvider.register("weathergov", {
|
||||
* Convert the icons to a more usable name.
|
||||
*/
|
||||
convertWeatherType (weatherType, isDaytime) {
|
||||
//https://w1.weather.gov/xml/current_obs/weather.php
|
||||
// There are way too many types to create, so lets just look for certain strings
|
||||
|
||||
/*
|
||||
* https://w1.weather.gov/xml/current_obs/weather.php
|
||||
* There are way too many types to create, so lets just look for certain strings
|
||||
*/
|
||||
|
||||
if (weatherType.includes("Cloudy") || weatherType.includes("Partly")) {
|
||||
if (isDaytime) {
|
||||
|
@@ -1,14 +1,7 @@
|
||||
/* 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.
|
||||
*
|
||||
* Terms of service: https://developer.yr.no/doc/TermsOfService/
|
||||
*/
|
||||
WeatherProvider.register("yr", {
|
||||
@@ -30,6 +23,10 @@ WeatherProvider.register("yr", {
|
||||
Log.error("The Yr weather provider requires local storage.");
|
||||
throw new Error("Local storage not available");
|
||||
}
|
||||
if (this.config.updateInterval < 600000) {
|
||||
Log.warn("The Yr weather provider requires a minimum update interval of 10 minutes (600 000 ms). The configuration has been adjusted to meet this requirement.");
|
||||
this.delegate.config.updateInterval = 600000;
|
||||
}
|
||||
Log.info(`Weather provider: ${this.providerName} started.`);
|
||||
},
|
||||
|
||||
@@ -41,7 +38,7 @@ WeatherProvider.register("yr", {
|
||||
})
|
||||
.catch((error) => {
|
||||
Log.error(error);
|
||||
throw new Error(error);
|
||||
this.updateAvailable();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -75,8 +72,11 @@ WeatherProvider.register("yr", {
|
||||
|
||||
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.
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
let shouldWait = localStorage.getItem("yrIsFetchingWeatherData");
|
||||
if (shouldWait) {
|
||||
const checkForGo = setInterval(function () {
|
||||
@@ -123,7 +123,12 @@ WeatherProvider.register("yr", {
|
||||
})
|
||||
.catch((err) => {
|
||||
Log.error(err);
|
||||
reject("Unable to get weather data from Yr.");
|
||||
if (weatherData) {
|
||||
Log.warn("Using outdated cached weather data.");
|
||||
resolve(weatherData);
|
||||
} else {
|
||||
reject("Unable to get weather data from Yr.");
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
localStorage.removeItem("yrIsFetchingWeatherData");
|
||||
@@ -209,8 +214,11 @@ WeatherProvider.register("yr", {
|
||||
},
|
||||
|
||||
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.
|
||||
|
||||
/*
|
||||
* 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) => {
|
||||
let shouldWait = localStorage.getItem("yrIsFetchingStellarData");
|
||||
if (shouldWait) {
|
||||
@@ -498,7 +506,7 @@ WeatherProvider.register("yr", {
|
||||
})
|
||||
.catch((error) => {
|
||||
Log.error(error);
|
||||
throw new Error(error);
|
||||
this.updateAvailable();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -531,7 +539,15 @@ WeatherProvider.register("yr", {
|
||||
getHourlyForecastFrom (weatherData) {
|
||||
const series = [];
|
||||
|
||||
const now = moment({
|
||||
year: moment().year(),
|
||||
month: moment().month(),
|
||||
day: moment().date(),
|
||||
hour: moment().hour()
|
||||
});
|
||||
for (const forecast of weatherData.properties.timeseries) {
|
||||
if (now.isAfter(moment(forecast.time))) continue;
|
||||
|
||||
forecast.symbol = forecast.data.next_1_hours?.summary?.symbol_code;
|
||||
forecast.precipitationAmount = forecast.data.next_1_hours?.details?.precipitation_amount;
|
||||
forecast.precipitationProbability = forecast.data.next_1_hours?.details?.probability_of_precipitation;
|
||||
@@ -601,7 +617,7 @@ WeatherProvider.register("yr", {
|
||||
})
|
||||
.catch((error) => {
|
||||
Log.error(error);
|
||||
throw new Error(error);
|
||||
this.updateAvailable();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -31,6 +31,7 @@
|
||||
|
||||
.weather .precipitation-amount,
|
||||
.weather .precipitation-prob,
|
||||
.weather .humidity-hourly,
|
||||
.weather .uv-index {
|
||||
padding-left: 20px;
|
||||
padding-right: 0;
|
||||
|
@@ -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,7 +14,8 @@ Module.register("weather", {
|
||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||
animationSpeed: 1000,
|
||||
showFeelsLike: true,
|
||||
showHumidity: false,
|
||||
showHumidity: "none", // possible options for "current" weather are "none", "wind", "temp", "feelslike" or "below", for "hourly" weather "none" or "true"
|
||||
hideZeroes: false, // hide zeroes (and empty columns) in hourly, currently only for precipitation
|
||||
showIndoorHumidity: false,
|
||||
showIndoorTemperature: false,
|
||||
allowOverrideNotification: false,
|
||||
@@ -86,6 +81,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);
|
||||
@@ -172,12 +171,19 @@ Module.register("weather", {
|
||||
}
|
||||
|
||||
const notificationPayload = {
|
||||
currentWeather: this.weatherProvider?.currentWeatherObject?.simpleClone() ?? null,
|
||||
forecastArray: this.weatherProvider?.weatherForecastArray?.map((ar) => ar.simpleClone()) ?? [],
|
||||
hourlyArray: this.weatherProvider?.weatherHourlyArray?.map((ar) => ar.simpleClone()) ?? [],
|
||||
currentWeather: this.config.units === "imperial"
|
||||
? WeatherUtils.convertWeatherObjectToImperial(this.weatherProvider?.currentWeatherObject?.simpleClone()) ?? null
|
||||
: this.weatherProvider?.currentWeatherObject?.simpleClone() ?? null,
|
||||
forecastArray: this.config.units === "imperial"
|
||||
? this.weatherProvider?.weatherForecastArray?.map((ar) => WeatherUtils.convertWeatherObjectToImperial(ar.simpleClone())) ?? []
|
||||
: this.weatherProvider?.weatherForecastArray?.map((ar) => ar.simpleClone()) ?? [],
|
||||
hourlyArray: this.config.units === "imperial"
|
||||
? this.weatherProvider?.weatherHourlyArray?.map((ar) => WeatherUtils.convertWeatherObjectToImperial(ar.simpleClone())) ?? []
|
||||
: this.weatherProvider?.weatherHourlyArray?.map((ar) => ar.simpleClone()) ?? [],
|
||||
locationName: this.weatherProvider?.fetchedLocationName,
|
||||
providerName: this.weatherProvider.providerName
|
||||
};
|
||||
|
||||
this.sendNotification("WEATHER_UPDATED", notificationPayload);
|
||||
},
|
||||
|
||||
@@ -235,7 +241,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,17 +1,5 @@
|
||||
/* 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
|
||||
*/
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user