mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-10-15 08:34:46 +00:00
Release 2.33.0 (#3903)
This commit is contained in:
committed by
GitHub
parent
62b0f7f26e
commit
b0c5924019
10
.github/workflows/automated-tests.yaml
vendored
10
.github/workflows/automated-tests.yaml
vendored
@@ -18,9 +18,9 @@ jobs:
|
|||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
- name: "Use Node.js"
|
- name: "Use Node.js"
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: lts/*
|
node-version: lts/*
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
@@ -38,16 +38,16 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [22.14.0, 22.x, 24.x]
|
node-version: [22.18.0, 22.x, 24.x]
|
||||||
steps:
|
steps:
|
||||||
- name: Install electron dependencies and labwc
|
- name: Install electron dependencies and labwc
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libnss3 libasound2t64 labwc
|
sudo apt-get install -y libnss3 libasound2t64 labwc
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
- name: "Use Node.js ${{ matrix.node-version }}"
|
- name: "Use Node.js ${{ matrix.node-version }}"
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
2
.github/workflows/dep-review.yaml
vendored
2
.github/workflows/dep-review.yaml
vendored
@@ -13,6 +13,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
- name: "Dependency Review"
|
- name: "Dependency Review"
|
||||||
uses: actions/dependency-review-action@v4
|
uses: actions/dependency-review-action@v4
|
||||||
|
6
.github/workflows/electron-rebuild.yaml
vendored
6
.github/workflows/electron-rebuild.yaml
vendored
@@ -8,12 +8,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [22.14.0, 22.x, 24.x]
|
node-version: [22.18.0, 22.x, 24.x]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
- name: "Use Node.js ${{ matrix.node-version }}"
|
- name: "Use Node.js ${{ matrix.node-version }}"
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
4
.github/workflows/spellcheck.yaml
vendored
4
.github/workflows/spellcheck.yaml
vendored
@@ -15,11 +15,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: develop
|
ref: develop
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
node-version: lts/*
|
node-version: lts/*
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
2
.github/workflows/stale.yaml
vendored
2
.github/workflows/stale.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v9
|
- uses: actions/stale@v10
|
||||||
with:
|
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."
|
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-stale: 60
|
||||||
|
64
CHANGELOG.md
64
CHANGELOG.md
@@ -7,6 +7,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/#donate) With your help we can continue to improve the MagicMirror².
|
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/#donate) With your help we can continue to improve the MagicMirror².
|
||||||
|
|
||||||
|
## [2.33.0] - 2025-10-01
|
||||||
|
|
||||||
|
Thanks to: @Crazylegstoo, @dathbe, @m-idler, @plebcity, @khassel, @KristjanESPERANTO, @rejas and @sdetweil!
|
||||||
|
|
||||||
|
> ⚠️ This release needs nodejs version `v22.18.0 or higher`
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add configuration option for `User-Agent`, used by calendar & news module (#3255)
|
||||||
|
- [linter] Add prettier plugin for nunjuck templates (#3887)
|
||||||
|
- [core] Add clear log for occupied port at startup (#3890)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- [clock] Add CSS to prevent line breaking of sunset/sunrise time display (#3816)
|
||||||
|
- [core] Enhance system information logging format and include additional env and RAM details (#3839, #3843)
|
||||||
|
- [refactor] Add new file `js/module_functions.js` to move code used in several modules to one place (#3837)
|
||||||
|
- [refactor] Use global.root_path where possible and add tests for config:check (#3883, #3885, #3886, #3889)
|
||||||
|
- [tests] refactor: simplify jest config file (#3844)
|
||||||
|
- [tests] refactor: extract constants for weather electron tests (#3845)
|
||||||
|
- [tests] refactor: add `setupDOMEnvironment` helper function to eliminate repetitive JSDOM setup code (#3860)
|
||||||
|
- [tests] replace `console` with `Log` in calendar `debug.js` to avoid exception in eslint config (#3846)
|
||||||
|
- [tests] speed up e2e tests, cleanup and stabilize weather e2e tests, update snapshot url (#3847, #3848, #3861)
|
||||||
|
- [tests] refactor translation tests (#3866)
|
||||||
|
- Remove `sinon` dependency in favor of Jest native mocking
|
||||||
|
- Unify test helper functions across translation test suites
|
||||||
|
- Rename `setupDOMEnvironment` to `createTranslationTestEnvironment` for consistency
|
||||||
|
- Simplify DOM setup by removing unnecessary Promise/async patterns
|
||||||
|
- Avoid potential port conflicts by using port 3001 for translator unit tests
|
||||||
|
- Improve test reliability and maintainability
|
||||||
|
- [tests] add alert module tests for different welcome_message configurations (#3867)
|
||||||
|
- [lint-staged] use `prettier --write --ignore-unknown` in `lint-staged` to avoid errors on unsupported files (#3888)
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
|
||||||
|
- [calendar] Update defaultSymbol name and also the link to the icon search site (#3879)
|
||||||
|
- [core] Update dependencies including electron to v38 as well as github actions (#3831, #3849, #3857, #3858, #3872, #3876, #3882, #3891, #3896)
|
||||||
|
- [weather] Update feels_like temperature calculation formula (#3869)
|
||||||
|
- [weather] Update null value handling for weather type (#3892)
|
||||||
|
- [layout] Update styles for weather and calendar (#3894)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- [calendar] Fixed broken unittest that only broke on the 1st of July and 1st of january (#3830)
|
||||||
|
- [clock] Fixed missing icons when no other modules with icons is loaded (#3834)
|
||||||
|
- [weather] Fixed handling of empty values in weathergov providers handling of precipitationAmount (#3859)
|
||||||
|
- [calendar] Fix regression handling of limit days (#3840)
|
||||||
|
- [calendar] Fixed regression of calendarfetcherutils.shouldEventBeExcluded (#3841)
|
||||||
|
- [core] Fixed socket.io timeout when server is slow to send notification, notification lost at client (#3380)
|
||||||
|
- [tests] refactor AnimateCSS tests after jsdom 27 upgrade (#3891)
|
||||||
|
- [weather] Use `apparent_temperature` data from openmeteo's hourly weather for current feelsLikeTemp (#3868).
|
||||||
|
- [weather] Updated envcanada Provider to use new database/URL schema for accessing weather data (#3878).
|
||||||
|
|
||||||
## [2.32.0] - 2025-07-01
|
## [2.32.0] - 2025-07-01
|
||||||
|
|
||||||
Thanks to: @bughaver, @bugsounet, @khassel, @KristjanESPERANTO, @plebcity, @rejas, @sdetweil.
|
Thanks to: @bughaver, @bugsounet, @khassel, @KristjanESPERANTO, @plebcity, @rejas, @sdetweil.
|
||||||
@@ -260,7 +313,7 @@ For more info, please read the following post: [A New Chapter for MagicMirror: T
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Output of system information to the console for troubleshooting (#3328 and #3337), ignore errors under aarch64 (#3349)
|
- 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)
|
- [linter] 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)
|
- [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)
|
- electron-rebuild test suite for electron and 3rd party modules compatibility (#3392)
|
||||||
- Create MM² icon and attach it to electron process (#3407)
|
- Create MM² icon and attach it to electron process (#3407)
|
||||||
@@ -277,7 +330,7 @@ For more info, please read the following post: [A New Chapter for MagicMirror: T
|
|||||||
- Update translations for estonian (#3371)
|
- Update translations for estonian (#3371)
|
||||||
- Update electron to v29 and update other dependencies
|
- 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
|
- [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
|
- [weather] Update layout of current weather indoor values
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
@@ -422,7 +475,7 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al
|
|||||||
- Added UV Index to hourly and current Weather, with support for Openmeteo
|
- Added UV Index to hourly and current Weather, with support for Openmeteo
|
||||||
- Added tests for serveronly
|
- Added tests for serveronly
|
||||||
- Set Timezone `Europe/Berlin` in unit tests (needed for new formatTime tests)
|
- Set Timezone `Europe/Berlin` in unit tests (needed for new formatTime tests)
|
||||||
- Added no-param-reassign eslint rule and fix warnings
|
- [linter] Added no-param-reassign eslint rule and fix warnings
|
||||||
- [updatenotification] Added `sendUpdatesNotifications` feature. Broadcast update with `UPDATES` notification to 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
|
- [updatenotification] Allow force scanning with `SCAN_UPDATES` notification from other modules
|
||||||
- Added per-calendar fetchInterval
|
- Added per-calendar fetchInterval
|
||||||
@@ -687,7 +740,7 @@ Special thanks to the following contributors: @AmpioRosso, @eouia, @fewieden, @j
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed wrong file `kr.json` to `ko.json`. Use language code 'ko' instead of 'kr' for Korean language.
|
- Fixed wrong file `kr.json` to `ko.json`. Use language code 'ko' instead of 'kr' for Korean language.
|
||||||
- Fixed `feels_like` data from openweathermap's current weather being ignored (#2678).
|
- [weather] 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 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 incorrect time zone correction of recurring full day events (#2632 and #2634).
|
||||||
- Fixed e2e tests by increasing testTimeout.
|
- Fixed e2e tests by increasing testTimeout.
|
||||||
@@ -725,7 +778,7 @@ Special thanks to the following contributors: @apiontek, @eouia, @jupadin, @khas
|
|||||||
- Actually test all js and css files when lint script is run.
|
- Actually test all js and css files when lint script is run.
|
||||||
- Updated jsdocs and print warnings during testing too.
|
- Updated jsdocs and print warnings during testing too.
|
||||||
- Updated weathergov provider to try fetching not just current, but also forecast, when API URLs available.
|
- Updated weathergov provider to try fetching not just current, but also forecast, when API URLs available.
|
||||||
- Refactored clock layout.
|
- [clock] Refactored clock layout.
|
||||||
- Refactored methods from weather-providers into weatherobject (isDaytime, updateSunTime).
|
- Refactored methods from weather-providers into weatherobject (isDaytime, updateSunTime).
|
||||||
- Use of `logger.js` in jest tests.
|
- Use of `logger.js` in jest tests.
|
||||||
- Run prettier over all relevant files.
|
- Run prettier over all relevant files.
|
||||||
@@ -1771,6 +1824,7 @@ It includes (but is not limited to) the following features:
|
|||||||
|
|
||||||
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)
|
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.33.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.32.0...v2.33.0
|
||||||
[2.32.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.31.0...v2.32.0
|
[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.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.30.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.29.0...v2.30.0
|
||||||
|
@@ -35,7 +35,7 @@ Are done by
|
|||||||
- [ ] test `prep-release` branch
|
- [ ] test `prep-release` branch
|
||||||
- [ ] update `CHANGELOG.md`
|
- [ ] update `CHANGELOG.md`
|
||||||
- [ ] add all contributor names: `...`
|
- [ ] add all contributor names: `...`
|
||||||
- [ ] add min. node version: > ⚠️ This release needs nodejs version `v22.14.0` or higher
|
- [ ] add min. node version: > ⚠️ This release needs nodejs version `v22.18.0` or higher
|
||||||
- [ ] check release link at the bottom of the file
|
- [ ] check release link at the bottom of the file
|
||||||
- [ ] commit and push all changes
|
- [ ] commit and push all changes
|
||||||
- [ ] create pull request from `prep-release` to `develop` branch with title `Prepare Release 2.xx.0`
|
- [ ] create pull request from `prep-release` to `develop` branch with title `Prepare Release 2.xx.0`
|
||||||
|
@@ -21,6 +21,7 @@
|
|||||||
"browserwindow",
|
"browserwindow",
|
||||||
"bryanzzhu",
|
"bryanzzhu",
|
||||||
"btoconnor",
|
"btoconnor",
|
||||||
|
"bughaver",
|
||||||
"bugsounet",
|
"bugsounet",
|
||||||
"buxxi",
|
"buxxi",
|
||||||
"byday",
|
"byday",
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
"darksky",
|
"darksky",
|
||||||
"dateheader",
|
"dateheader",
|
||||||
"dateheaders",
|
"dateheaders",
|
||||||
|
"dathbe",
|
||||||
"davide",
|
"davide",
|
||||||
"DAYAFTERTOMORROW",
|
"DAYAFTERTOMORROW",
|
||||||
"DAYBEFOREYESTERDAY",
|
"DAYBEFOREYESTERDAY",
|
||||||
@@ -175,6 +177,7 @@
|
|||||||
"oraclesean",
|
"oraclesean",
|
||||||
"oscarb",
|
"oscarb",
|
||||||
"philnagel",
|
"philnagel",
|
||||||
|
"plebcity",
|
||||||
"Português",
|
"Português",
|
||||||
"PRECIP",
|
"PRECIP",
|
||||||
"Problema",
|
"Problema",
|
||||||
@@ -233,6 +236,7 @@
|
|||||||
"Weatherflow",
|
"Weatherflow",
|
||||||
"weatherforecast",
|
"weatherforecast",
|
||||||
"weathergov",
|
"weathergov",
|
||||||
|
"weathericon",
|
||||||
"weathericons",
|
"weathericons",
|
||||||
"weatherobject",
|
"weatherobject",
|
||||||
"weatherutils",
|
"weatherutils",
|
||||||
|
12
css/main.css
12
css/main.css
@@ -252,3 +252,15 @@ sup {
|
|||||||
.region .container.hidden {
|
.region .container.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.region.left .flex {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region.center .flex {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region.right .flex {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
@@ -3,7 +3,7 @@ import globals from "globals";
|
|||||||
import {flatConfigs as importX} from "eslint-plugin-import-x";
|
import {flatConfigs as importX} from "eslint-plugin-import-x";
|
||||||
import jest from "eslint-plugin-jest";
|
import jest from "eslint-plugin-jest";
|
||||||
import js from "@eslint/js";
|
import js from "@eslint/js";
|
||||||
import jsdoc from "eslint-plugin-jsdoc";
|
import jsdocPlugin from "eslint-plugin-jsdoc";
|
||||||
import packageJson from "eslint-plugin-package-json";
|
import packageJson from "eslint-plugin-package-json";
|
||||||
import stylistic from "@stylistic/eslint-plugin";
|
import stylistic from "@stylistic/eslint-plugin";
|
||||||
|
|
||||||
@@ -23,8 +23,8 @@ export default defineConfig([
|
|||||||
moment: "readonly"
|
moment: "readonly"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: {js, jsdoc, stylistic},
|
plugins: {js, stylistic},
|
||||||
extends: [importX.recommended, jest.configs["flat/recommended"], "js/recommended", jsdoc.configs["flat/recommended"], "stylistic/all"],
|
extends: [importX.recommended, jest.configs["flat/recommended"], "js/recommended", jsdocPlugin.configs["flat/recommended"], "stylistic/all"],
|
||||||
rules: {
|
rules: {
|
||||||
"@stylistic/array-element-newline": ["error", "consistent"],
|
"@stylistic/array-element-newline": ["error", "consistent"],
|
||||||
"@stylistic/arrow-parens": ["error", "always"],
|
"@stylistic/arrow-parens": ["error", "always"],
|
||||||
@@ -88,7 +88,6 @@ export default defineConfig([
|
|||||||
files: ["**/*.js"],
|
files: ["**/*.js"],
|
||||||
ignores: [
|
ignores: [
|
||||||
"clientonly/index.js",
|
"clientonly/index.js",
|
||||||
"modules/default/calendar/debug.js",
|
|
||||||
"js/logger.js",
|
"js/logger.js",
|
||||||
"tests/**/*.js"
|
"tests/**/*.js"
|
||||||
],
|
],
|
||||||
|
@@ -1,32 +1,37 @@
|
|||||||
module.exports = async () => {
|
const config = {
|
||||||
return {
|
verbose: true,
|
||||||
verbose: true,
|
testTimeout: 20000,
|
||||||
testTimeout: 20000,
|
testSequencer: "<rootDir>/tests/utils/test_sequencer.js",
|
||||||
testSequencer: "<rootDir>/tests/utils/test_sequencer.js",
|
projects: [
|
||||||
projects: [
|
{
|
||||||
{
|
displayName: "unit",
|
||||||
displayName: "unit",
|
globalSetup: "<rootDir>/tests/unit/helpers/global-setup.js",
|
||||||
globalSetup: "<rootDir>/tests/unit/helpers/global-setup.js",
|
moduleNameMapper: {
|
||||||
moduleNameMapper: {
|
logger: "<rootDir>/js/logger.js"
|
||||||
logger: "<rootDir>/js/logger.js"
|
|
||||||
},
|
|
||||||
testMatch: ["**/tests/unit/**/*.[jt]s?(x)"],
|
|
||||||
testPathIgnorePatterns: ["<rootDir>/tests/unit/mocks", "<rootDir>/tests/unit/helpers"]
|
|
||||||
},
|
},
|
||||||
{
|
testMatch: ["**/tests/unit/**/*.[jt]s?(x)"],
|
||||||
displayName: "electron",
|
testPathIgnorePatterns: ["<rootDir>/tests/unit/mocks", "<rootDir>/tests/unit/helpers"]
|
||||||
testMatch: ["**/tests/electron/**/*.[jt]s?(x)"],
|
},
|
||||||
testPathIgnorePatterns: ["<rootDir>/tests/electron/helpers"]
|
{
|
||||||
},
|
displayName: "electron",
|
||||||
{
|
testMatch: ["**/tests/electron/**/*.[jt]s?(x)"],
|
||||||
displayName: "e2e",
|
testPathIgnorePatterns: ["<rootDir>/tests/electron/helpers"]
|
||||||
testMatch: ["**/tests/e2e/**/*.[jt]s?(x)"],
|
},
|
||||||
modulePaths: ["<rootDir>/js/"],
|
{
|
||||||
testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers", "<rootDir>/tests/e2e/mocks"]
|
displayName: "e2e",
|
||||||
}
|
testMatch: ["**/tests/e2e/**/*.[jt]s?(x)"],
|
||||||
],
|
modulePaths: ["<rootDir>/js/"],
|
||||||
collectCoverageFrom: ["./clientonly/**/*.js", "./js/**/*.js", "./modules/default/**/*.js", "./serveronly/**/*.js"],
|
testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers", "<rootDir>/tests/e2e/mocks"]
|
||||||
coverageReporters: ["lcov", "text"],
|
}
|
||||||
coverageProvider: "v8"
|
],
|
||||||
};
|
collectCoverageFrom: [
|
||||||
|
"<rootDir>/clientonly/**/*.js",
|
||||||
|
"<rootDir>/js/**/*.js",
|
||||||
|
"<rootDir>/modules/default/**/*.js",
|
||||||
|
"<rootDir>/serveronly/**/*.js"
|
||||||
|
],
|
||||||
|
coverageReporters: ["lcov", "text"],
|
||||||
|
coverageProvider: "v8"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
|
18
js/app.js
18
js/app.js
@@ -6,26 +6,26 @@ const path = require("node:path");
|
|||||||
const envsub = require("envsub");
|
const envsub = require("envsub");
|
||||||
const Log = require("logger");
|
const Log = require("logger");
|
||||||
|
|
||||||
|
// global absolute root path
|
||||||
|
global.root_path = path.resolve(`${__dirname}/../`);
|
||||||
|
|
||||||
const Server = require(`${__dirname}/server`);
|
const Server = require(`${__dirname}/server`);
|
||||||
const Utils = require(`${__dirname}/utils`);
|
const Utils = require(`${__dirname}/utils`);
|
||||||
const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`);
|
|
||||||
const { getEnvVarsAsObj } = require(`${__dirname}/server_functions`);
|
|
||||||
|
|
||||||
|
const defaultModules = require(`${global.root_path}/modules/default/defaultmodules`);
|
||||||
// used to control fetch timeout for node_helpers
|
// used to control fetch timeout for node_helpers
|
||||||
const { setGlobalDispatcher, Agent } = require("undici");
|
const { setGlobalDispatcher, Agent } = require("undici");
|
||||||
|
const { getEnvVarsAsObj } = require("#server_functions");
|
||||||
// common timeout value, provide environment override in case
|
// common timeout value, provide environment override in case
|
||||||
const fetch_timeout = process.env.mmFetchTimeout !== undefined ? process.env.mmFetchTimeout : 30000;
|
const fetch_timeout = process.env.mmFetchTimeout !== undefined ? process.env.mmFetchTimeout : 30000;
|
||||||
|
|
||||||
// Get version number.
|
// Get version number.
|
||||||
global.version = require(`${__dirname}/../package.json`).version;
|
global.version = require(`${global.root_path}/package.json`).version;
|
||||||
global.mmTestMode = process.env.mmTestMode === "true";
|
global.mmTestMode = process.env.mmTestMode === "true";
|
||||||
Log.log(`Starting MagicMirror: v${global.version}`);
|
Log.log(`Starting MagicMirror: v${global.version}`);
|
||||||
|
|
||||||
// Log system information.
|
// Log system information.
|
||||||
Utils.logSystemInformation();
|
Utils.logSystemInformation(global.version);
|
||||||
|
|
||||||
// global absolute root path
|
|
||||||
global.root_path = path.resolve(`${__dirname}/../`);
|
|
||||||
|
|
||||||
if (process.env.MM_CONFIG_FILE) {
|
if (process.env.MM_CONFIG_FILE) {
|
||||||
global.configuration_file = process.env.MM_CONFIG_FILE.replace(`${global.root_path}/`, "");
|
global.configuration_file = process.env.MM_CONFIG_FILE.replace(`${global.root_path}/`, "");
|
||||||
@@ -181,10 +181,10 @@ function App () {
|
|||||||
const elements = module.split("/");
|
const elements = module.split("/");
|
||||||
const moduleName = elements[elements.length - 1];
|
const moduleName = elements[elements.length - 1];
|
||||||
const env = getEnvVarsAsObj();
|
const env = getEnvVarsAsObj();
|
||||||
let moduleFolder = path.resolve(`${__dirname}/../${env.modulesDir}`, module);
|
let moduleFolder = path.resolve(`${global.root_path}/${env.modulesDir}`, module);
|
||||||
|
|
||||||
if (defaultModules.includes(moduleName)) {
|
if (defaultModules.includes(moduleName)) {
|
||||||
const defaultModuleFolder = path.resolve(`${__dirname}/../modules/default/`, module);
|
const defaultModuleFolder = path.resolve(`${global.root_path}/modules/default/`, module);
|
||||||
if (process.env.JEST_WORKER_ID === undefined) {
|
if (process.env.JEST_WORKER_ID === undefined) {
|
||||||
moduleFolder = defaultModuleFolder;
|
moduleFolder = defaultModuleFolder;
|
||||||
} else {
|
} else {
|
||||||
|
14
js/main.js
14
js/main.js
@@ -90,7 +90,7 @@ const MM = (function () {
|
|||||||
/**
|
/**
|
||||||
* Send a notification to all modules.
|
* Send a notification to all modules.
|
||||||
* @param {string} notification The identifier of the notification.
|
* @param {string} notification The identifier of the notification.
|
||||||
* @param {*} payload The payload of the notification.
|
* @param {object} payload The payload of the notification.
|
||||||
* @param {Module} sender The module that sent the notification.
|
* @param {Module} sender The module that sent the notification.
|
||||||
* @param {Module} [sendTo] The (optional) module to send the notification to.
|
* @param {Module} [sendTo] The (optional) module to send the notification to.
|
||||||
*/
|
*/
|
||||||
@@ -262,7 +262,7 @@ const MM = (function () {
|
|||||||
* Hide the module.
|
* Hide the module.
|
||||||
* @param {Module} module The module to hide.
|
* @param {Module} module The module to hide.
|
||||||
* @param {number} speed The speed of the hide animation.
|
* @param {number} speed The speed of the hide animation.
|
||||||
* @param {Function} callback Called when the animation is done.
|
* @param {Promise} callback Called when the animation is done.
|
||||||
* @param {object} [options] Optional settings for the hide method.
|
* @param {object} [options] Optional settings for the hide method.
|
||||||
*/
|
*/
|
||||||
const hideModule = function (module, speed, callback, options = {}) {
|
const hideModule = function (module, speed, callback, options = {}) {
|
||||||
@@ -347,7 +347,7 @@ const MM = (function () {
|
|||||||
* Show the module.
|
* Show the module.
|
||||||
* @param {Module} module The module to show.
|
* @param {Module} module The module to show.
|
||||||
* @param {number} speed The speed of the show animation.
|
* @param {number} speed The speed of the show animation.
|
||||||
* @param {Function} callback Called when the animation is done.
|
* @param {Promise} callback Called when the animation is done.
|
||||||
* @param {object} [options] Optional settings for the show method.
|
* @param {object} [options] Optional settings for the show method.
|
||||||
*/
|
*/
|
||||||
const showModule = function (module, speed, callback, options = {}) {
|
const showModule = function (module, speed, callback, options = {}) {
|
||||||
@@ -552,7 +552,7 @@ const MM = (function () {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Walks thru a collection of modules and executes the callback with the module as an argument.
|
* Walks thru a collection of modules and executes the callback with the module as an argument.
|
||||||
* @param {Function} callback The function to execute with the module as an argument.
|
* @param {module} callback The function to execute with the module as an argument.
|
||||||
*/
|
*/
|
||||||
const enumerate = function (callback) {
|
const enumerate = function (callback) {
|
||||||
modules.map(function (module) {
|
modules.map(function (module) {
|
||||||
@@ -629,7 +629,7 @@ const MM = (function () {
|
|||||||
/**
|
/**
|
||||||
* Send a notification to all modules.
|
* Send a notification to all modules.
|
||||||
* @param {string} notification The identifier of the notification.
|
* @param {string} notification The identifier of the notification.
|
||||||
* @param {*} payload The payload of the notification.
|
* @param {object} payload The payload of the notification.
|
||||||
* @param {Module} sender The module that sent the notification.
|
* @param {Module} sender The module that sent the notification.
|
||||||
*/
|
*/
|
||||||
sendNotification (notification, payload, sender) {
|
sendNotification (notification, payload, sender) {
|
||||||
@@ -688,7 +688,7 @@ const MM = (function () {
|
|||||||
* Hide the module.
|
* Hide the module.
|
||||||
* @param {Module} module The module to hide.
|
* @param {Module} module The module to hide.
|
||||||
* @param {number} speed The speed of the hide animation.
|
* @param {number} speed The speed of the hide animation.
|
||||||
* @param {Function} callback Called when the animation is done.
|
* @param {Promise} callback Called when the animation is done.
|
||||||
* @param {object} [options] Optional settings for the hide method.
|
* @param {object} [options] Optional settings for the hide method.
|
||||||
*/
|
*/
|
||||||
hideModule (module, speed, callback, options) {
|
hideModule (module, speed, callback, options) {
|
||||||
@@ -700,7 +700,7 @@ const MM = (function () {
|
|||||||
* Show the module.
|
* Show the module.
|
||||||
* @param {Module} module The module to show.
|
* @param {Module} module The module to show.
|
||||||
* @param {number} speed The speed of the show animation.
|
* @param {number} speed The speed of the show animation.
|
||||||
* @param {Function} callback Called when the animation is done.
|
* @param {Promise} callback Called when the animation is done.
|
||||||
* @param {object} [options] Optional settings for the show method.
|
* @param {object} [options] Optional settings for the show method.
|
||||||
*/
|
*/
|
||||||
showModule (module, speed, callback, options) {
|
showModule (module, speed, callback, options) {
|
||||||
|
14
js/module.js
14
js/module.js
@@ -68,7 +68,7 @@ const Module = Class.extend({
|
|||||||
* Returns a map of translation files the module requires to be loaded.
|
* Returns a map of translation files the module requires to be loaded.
|
||||||
*
|
*
|
||||||
* return Map<String, String> -
|
* return Map<String, String> -
|
||||||
* @returns {*} A map with langKeys and filenames.
|
* @returns {Map} A map with langKeys and filenames.
|
||||||
*/
|
*/
|
||||||
getTranslations () {
|
getTranslations () {
|
||||||
return false;
|
return false;
|
||||||
@@ -140,7 +140,7 @@ const Module = Class.extend({
|
|||||||
/**
|
/**
|
||||||
* Called by the MagicMirror² core when a notification arrives.
|
* Called by the MagicMirror² core when a notification arrives.
|
||||||
* @param {string} notification The identifier of the notification.
|
* @param {string} notification The identifier of the notification.
|
||||||
* @param {*} payload The payload of the notification.
|
* @param {object} payload The payload of the notification.
|
||||||
* @param {Module} sender The module that sent the notification.
|
* @param {Module} sender The module that sent the notification.
|
||||||
*/
|
*/
|
||||||
notificationReceived (notification, payload, sender) {
|
notificationReceived (notification, payload, sender) {
|
||||||
@@ -176,7 +176,7 @@ const Module = Class.extend({
|
|||||||
/**
|
/**
|
||||||
* Called when a socket notification arrives.
|
* Called when a socket notification arrives.
|
||||||
* @param {string} notification The identifier of the notification.
|
* @param {string} notification The identifier of the notification.
|
||||||
* @param {*} payload The payload of the notification.
|
* @param {object} payload The payload of the notification.
|
||||||
*/
|
*/
|
||||||
socketNotificationReceived (notification, payload) {
|
socketNotificationReceived (notification, payload) {
|
||||||
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
|
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
|
||||||
@@ -344,7 +344,7 @@ const Module = Class.extend({
|
|||||||
/**
|
/**
|
||||||
* Send a notification to all modules.
|
* Send a notification to all modules.
|
||||||
* @param {string} notification The identifier of the notification.
|
* @param {string} notification The identifier of the notification.
|
||||||
* @param {*} payload The payload of the notification.
|
* @param {object} payload The payload of the notification.
|
||||||
*/
|
*/
|
||||||
sendNotification (notification, payload) {
|
sendNotification (notification, payload) {
|
||||||
MM.sendNotification(notification, payload, this);
|
MM.sendNotification(notification, payload, this);
|
||||||
@@ -353,7 +353,7 @@ const Module = Class.extend({
|
|||||||
/**
|
/**
|
||||||
* Send a socket notification to the node helper.
|
* Send a socket notification to the node helper.
|
||||||
* @param {string} notification The identifier of the notification.
|
* @param {string} notification The identifier of the notification.
|
||||||
* @param {*} payload The payload of the notification.
|
* @param {object} payload The payload of the notification.
|
||||||
*/
|
*/
|
||||||
sendSocketNotification (notification, payload) {
|
sendSocketNotification (notification, payload) {
|
||||||
this.socket().sendNotification(notification, payload);
|
this.socket().sendNotification(notification, payload);
|
||||||
@@ -362,7 +362,7 @@ const Module = Class.extend({
|
|||||||
/**
|
/**
|
||||||
* Hide this module.
|
* Hide this module.
|
||||||
* @param {number} speed The speed of the hide animation.
|
* @param {number} speed The speed of the hide animation.
|
||||||
* @param {Function} callback Called when the animation is done.
|
* @param {Promise} callback Called when the animation is done.
|
||||||
* @param {object} [options] Optional settings for the hide method.
|
* @param {object} [options] Optional settings for the hide method.
|
||||||
*/
|
*/
|
||||||
hide (speed, callback, options = {}) {
|
hide (speed, callback, options = {}) {
|
||||||
@@ -389,7 +389,7 @@ const Module = Class.extend({
|
|||||||
/**
|
/**
|
||||||
* Show this module.
|
* Show this module.
|
||||||
* @param {number} speed The speed of the show animation.
|
* @param {number} speed The speed of the show animation.
|
||||||
* @param {Function} callback Called when the animation is done.
|
* @param {Promise} callback Called when the animation is done.
|
||||||
* @param {object} [options] Optional settings for the show method.
|
* @param {object} [options] Optional settings for the show method.
|
||||||
*/
|
*/
|
||||||
show (speed, callback, options) {
|
show (speed, callback, options) {
|
||||||
|
18
js/module_functions.js
Normal file
18
js/module_functions.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Schedule the timer for the next update
|
||||||
|
* @param {object} timer The timer of the module
|
||||||
|
* @param {bigint} intervalMS interval in milliseconds
|
||||||
|
* @param {Promise} callback function to call when the timer expires
|
||||||
|
*/
|
||||||
|
const scheduleTimer = function (timer, intervalMS, callback) {
|
||||||
|
if (process.env.JEST_WORKER_ID === undefined) {
|
||||||
|
// only set timer when not running in jest
|
||||||
|
let tmr = timer;
|
||||||
|
clearTimeout(tmr);
|
||||||
|
tmr = setTimeout(function () {
|
||||||
|
callback();
|
||||||
|
}, intervalMS);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { scheduleTimer };
|
@@ -27,7 +27,7 @@ const NodeHelper = Class.extend({
|
|||||||
/**
|
/**
|
||||||
* This method is called when a socket notification arrives.
|
* This method is called when a socket notification arrives.
|
||||||
* @param {string} notification The identifier of the notification.
|
* @param {string} notification The identifier of the notification.
|
||||||
* @param {*} payload The payload of the notification.
|
* @param {object} payload The payload of the notification.
|
||||||
*/
|
*/
|
||||||
socketNotificationReceived (notification, payload) {
|
socketNotificationReceived (notification, payload) {
|
||||||
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
|
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
|
||||||
|
29
js/server.js
29
js/server.js
@@ -7,7 +7,7 @@ const ipfilter = require("express-ipfilter").IpFilter;
|
|||||||
const helmet = require("helmet");
|
const helmet = require("helmet");
|
||||||
const socketio = require("socket.io");
|
const socketio = require("socket.io");
|
||||||
const Log = require("logger");
|
const Log = require("logger");
|
||||||
const { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars } = require("./server_functions");
|
const { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars } = require("#server_functions");
|
||||||
|
|
||||||
const vendor = require(`${__dirname}/vendor`);
|
const vendor = require(`${__dirname}/vendor`);
|
||||||
|
|
||||||
@@ -42,7 +42,9 @@ function Server (config) {
|
|||||||
origin: /.*$/,
|
origin: /.*$/,
|
||||||
credentials: true
|
credentials: true
|
||||||
},
|
},
|
||||||
allowEIO3: true
|
allowEIO3: true,
|
||||||
|
pingInterval: 120000, // server → client ping every 2 mins
|
||||||
|
pingTimeout: 120000 // wait up to 2 mins for client pong
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on("connection", (socket) => {
|
server.on("connection", (socket) => {
|
||||||
@@ -53,6 +55,29 @@ function Server (config) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Log.log(`Starting server on port ${port} ... `);
|
Log.log(`Starting server on port ${port} ... `);
|
||||||
|
|
||||||
|
// Add explicit error handling BEFORE calling listen so we can give user-friendly feedback
|
||||||
|
server.once("error", (err) => {
|
||||||
|
if (err && err.code === "EADDRINUSE") {
|
||||||
|
const bindAddr = config.address || "localhost";
|
||||||
|
const portInUseMessage = [
|
||||||
|
"",
|
||||||
|
"────────────────────────────────────────────────────────────────",
|
||||||
|
` PORT IN USE: ${bindAddr}:${port}`,
|
||||||
|
"",
|
||||||
|
" Another process (most likely another MagicMirror instance)",
|
||||||
|
" is already using this port.",
|
||||||
|
"",
|
||||||
|
" Stop the other process (free the port) or use a different port.",
|
||||||
|
"────────────────────────────────────────────────────────────────"
|
||||||
|
].join("\n");
|
||||||
|
Log.error(portInUseMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.error("Failed to start server:", err);
|
||||||
|
});
|
||||||
|
|
||||||
server.listen(port, config.address || "localhost");
|
server.listen(port, config.address || "localhost");
|
||||||
|
|
||||||
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
||||||
|
@@ -69,7 +69,7 @@ async function cors (req, res) {
|
|||||||
* @returns {object} An object specifying name and value of the headers.
|
* @returns {object} An object specifying name and value of the headers.
|
||||||
*/
|
*/
|
||||||
function getHeadersToSend (url) {
|
function getHeadersToSend (url) {
|
||||||
const headersToSend = { "User-Agent": `Mozilla/5.0 MagicMirror/${global.version}` };
|
const headersToSend = { "User-Agent": getUserAgent() };
|
||||||
const headersToSendMatch = new RegExp("sendheaders=(.+?)(&|$)", "g").exec(url);
|
const headersToSendMatch = new RegExp("sendheaders=(.+?)(&|$)", "g").exec(url);
|
||||||
if (headersToSendMatch) {
|
if (headersToSendMatch) {
|
||||||
const headers = headersToSendMatch[1].split(",");
|
const headers = headersToSendMatch[1].split(",");
|
||||||
@@ -129,6 +129,27 @@ function getVersion (req, res) {
|
|||||||
res.send(global.version);
|
res.send(global.version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the preferred `User-Agent`
|
||||||
|
* @returns {string} `User-Agent` to be used
|
||||||
|
*/
|
||||||
|
function getUserAgent () {
|
||||||
|
const defaultUserAgent = `Mozilla/5.0 (Node.js ${Number(process.version.match(/^v(\d+\.\d+)/)[1])}) MagicMirror/${global.version}`;
|
||||||
|
|
||||||
|
if (typeof config === "undefined") {
|
||||||
|
return defaultUserAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (typeof config.userAgent) {
|
||||||
|
case "function":
|
||||||
|
return config.userAgent();
|
||||||
|
case "string":
|
||||||
|
return config.userAgent;
|
||||||
|
default:
|
||||||
|
return defaultUserAgent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets environment variables needed in the browser.
|
* Gets environment variables needed in the browser.
|
||||||
* @returns {object} environment variables key: values
|
* @returns {object} environment variables key: values
|
||||||
@@ -155,4 +176,4 @@ function getEnvVars (req, res) {
|
|||||||
res.send(obj);
|
res.send(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars, getEnvVarsAsObj };
|
module.exports = { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars, getEnvVarsAsObj, getUserAgent };
|
||||||
|
@@ -13,7 +13,9 @@ const MMSocket = function (moduleName) {
|
|||||||
base = config.basePath;
|
base = config.basePath;
|
||||||
}
|
}
|
||||||
this.socket = io(`/${this.moduleName}`, {
|
this.socket = io(`/${this.moduleName}`, {
|
||||||
path: `${base}socket.io`
|
path: `${base}socket.io`,
|
||||||
|
pingInterval: 120000, // send pings every 2 mins
|
||||||
|
pingTimeout: 120000 // wait up to 2 mins for a pong
|
||||||
});
|
});
|
||||||
|
|
||||||
let notificationCallback = function () {};
|
let notificationCallback = function () {};
|
||||||
|
38
js/utils.js
38
js/utils.js
@@ -1,4 +1,3 @@
|
|||||||
const execSync = require("node:child_process").execSync;
|
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
|
|
||||||
const rootPath = path.resolve(`${__dirname}/../`);
|
const rootPath = path.resolve(`${__dirname}/../`);
|
||||||
@@ -14,27 +13,34 @@ const discoveredPositionsJSFilename = "js/positions.js";
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
||||||
async logSystemInformation () {
|
async logSystemInformation (mirrorVersion) {
|
||||||
try {
|
try {
|
||||||
let installedNodeVersion = execSync("node -v", { encoding: "utf-8" }).replace("v", "").replace(/(?:\r\n|\r|\n)/g, "");
|
const system = await si.system();
|
||||||
|
const osInfo = await si.osInfo();
|
||||||
|
const versions = await si.versions();
|
||||||
|
|
||||||
const staticData = await si.get({
|
const usedNodeVersion = process.version.replace("v", "");
|
||||||
system: "manufacturer, model, virtual",
|
const installedNodeVersion = versions.node;
|
||||||
osInfo: "platform, distro, release, arch",
|
const totalRam = (os.totalmem() / 1024 / 1024).toFixed(2);
|
||||||
versions: "kernel, node, npm, pm2"
|
const freeRam = (os.freemem() / 1024 / 1024).toFixed(2);
|
||||||
});
|
const usedRam = ((os.totalmem() - os.freemem()) / 1024 / 1024).toFixed(2);
|
||||||
let systemDataString = `System information:
|
|
||||||
### SYSTEM: manufacturer: ${staticData.system.manufacturer}; model: ${staticData.system.model}; virtual: ${staticData.system.virtual}
|
let systemDataString = [
|
||||||
### OS: platform: ${staticData.osInfo.platform}; distro: ${staticData.osInfo.distro}; release: ${staticData.osInfo.release}; arch: ${staticData.osInfo.arch}; kernel: ${staticData.versions.kernel}
|
"\n#### System Information ####",
|
||||||
### VERSIONS: electron: ${process.versions.electron}; used node: ${staticData.versions.node}; installed node: ${installedNodeVersion}; npm: ${staticData.versions.npm}; pm2: ${staticData.versions.pm2}
|
`- SYSTEM: manufacturer: ${system.manufacturer}; model: ${system.model}; virtual: ${system.virtual}; MM: ${mirrorVersion}`,
|
||||||
### OTHER: timeZone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}; ELECTRON_ENABLE_GPU: ${process.env.ELECTRON_ENABLE_GPU}`
|
`- OS: platform: ${osInfo.platform}; distro: ${osInfo.distro}; release: ${osInfo.release}; arch: ${osInfo.arch}; kernel: ${versions.kernel}`,
|
||||||
.replace(/\t/g, "");
|
`- VERSIONS: electron: ${process.versions.electron}; used node: ${usedNodeVersion}; installed node: ${installedNodeVersion}; npm: ${versions.npm}; pm2: ${versions.pm2}`,
|
||||||
|
`- ENV: XDG_SESSION_TYPE: ${process.env.XDG_SESSION_TYPE}; MM_CONFIG_FILE: ${process.env.MM_CONFIG_FILE}`,
|
||||||
|
` WAYLAND_DISPLAY: ${process.env.WAYLAND_DISPLAY}; DISPLAY: ${process.env.DISPLAY}; ELECTRON_ENABLE_GPU: ${process.env.ELECTRON_ENABLE_GPU}`,
|
||||||
|
`- RAM: total: ${totalRam} MB; free: ${freeRam} MB; used: ${usedRam} MB`,
|
||||||
|
`- OTHERS: uptime: ${Math.floor(os.uptime() / 60)} minutes; timeZone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}`
|
||||||
|
].join("\n");
|
||||||
Log.info(systemDataString);
|
Log.info(systemDataString);
|
||||||
|
|
||||||
// Return is currently only for jest
|
// Return is currently only for jest
|
||||||
return systemDataString;
|
return systemDataString;
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
Log.error(e);
|
Log.error(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
font-size: 70%;
|
font-size: 70%;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: table;
|
display: table;
|
||||||
word-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
top: 40%;
|
top: 40%;
|
||||||
width: 40%;
|
width: 40%;
|
||||||
height: auto;
|
height: auto;
|
||||||
word-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,20 +1,20 @@
|
|||||||
{% if imageUrl or imageFA %}
|
{% if imageUrl or imageFA %}
|
||||||
{% set imageHeight = imageHeight if imageHeight else "80px" %}
|
{% set imageHeight = imageHeight if imageHeight else "80px" %}
|
||||||
{% if imageUrl %}
|
{% if imageUrl %}
|
||||||
<img src="{{ imageUrl }}"
|
<img src="{{ imageUrl }}" height="{{ imageHeight }}" style="margin-bottom: 10px" />
|
||||||
height="{{ imageHeight }}"
|
{% else %}
|
||||||
style="margin-bottom: 10px" />
|
<span
|
||||||
{% else %}
|
class="bright fas fa-{{ imageFA }}"
|
||||||
<span class="bright fas fa-{{ imageFA }}"
|
style="margin-bottom: 10px;
|
||||||
style="margin-bottom: 10px;
|
font-size: {{ imageHeight }}"
|
||||||
font-size: {{ imageHeight }}"></span>
|
></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br />
|
<br />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if title %}
|
{% if title %}
|
||||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if message %}
|
{% if message %}
|
||||||
{% if title %}<br />{% endif %}
|
{% if title %}<br />{% endif %}
|
||||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{% if title %}
|
{% if title %}
|
||||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if message %}
|
{% if message %}
|
||||||
{% if title %}<br />{% endif %}
|
{% if title %}<br />{% endif %}
|
||||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@@ -2,23 +2,14 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
padding-left: 0;
|
gap: 5px;
|
||||||
padding-right: 10px;
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar .symbol span {
|
|
||||||
padding-top: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar .title {
|
.calendar .title {
|
||||||
padding-left: 0;
|
padding: 0 10px;
|
||||||
padding-right: 0;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar .time {
|
.calendar .time {
|
||||||
padding-left: 30px;
|
padding-left: 20px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
vertical-align: top;
|
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ Module.register("calendar", {
|
|||||||
limitDays: 0, // Limit the number of days shown, 0 = no limit
|
limitDays: 0, // Limit the number of days shown, 0 = no limit
|
||||||
pastDaysCount: 0,
|
pastDaysCount: 0,
|
||||||
displaySymbol: true,
|
displaySymbol: true,
|
||||||
defaultSymbol: "calendar-alt", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
|
defaultSymbol: "calendar-days", // Fontawesome Symbol see https://fontawesome.com/search?ic=free&o=r
|
||||||
defaultSymbolClassName: "fas fa-fw fa-",
|
defaultSymbolClassName: "fas fa-fw fa-",
|
||||||
showLocation: false,
|
showLocation: false,
|
||||||
displayRepeatingCountTitle: false,
|
displayRepeatingCountTitle: false,
|
||||||
@@ -168,8 +168,8 @@ Module.register("calendar", {
|
|||||||
|
|
||||||
this.selfUpdate();
|
this.selfUpdate();
|
||||||
},
|
},
|
||||||
notificationReceived (notification, payload, sender) {
|
|
||||||
|
|
||||||
|
notificationReceived (notification, payload, sender) {
|
||||||
if (notification === "FETCH_CALENDAR") {
|
if (notification === "FETCH_CALENDAR") {
|
||||||
if (this.hasCalendarURL(payload.url)) {
|
if (this.hasCalendarURL(payload.url)) {
|
||||||
this.sendSocketNotification(notification, { url: payload.url, id: this.identifier });
|
this.sendSocketNotification(notification, { url: payload.url, id: this.identifier });
|
||||||
@@ -217,7 +217,6 @@ Module.register("calendar", {
|
|||||||
|
|
||||||
// Override dom generator.
|
// Override dom generator.
|
||||||
getDom () {
|
getDom () {
|
||||||
const ONE_SECOND = 1000; // 1,000 milliseconds
|
|
||||||
const events = this.createEventList(true);
|
const events = this.createEventList(true);
|
||||||
const wrapper = document.createElement("table");
|
const wrapper = document.createElement("table");
|
||||||
wrapper.className = this.config.tableClass;
|
wrapper.className = this.config.tableClass;
|
||||||
@@ -308,15 +307,12 @@ Module.register("calendar", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const symbolClass = this.symbolClassForUrl(event.url);
|
const symbolClass = this.symbolClassForUrl(event.url);
|
||||||
symbolWrapper.className = `symbol align-right ${symbolClass}`;
|
symbolWrapper.className = `symbol ${symbolClass}`;
|
||||||
|
|
||||||
const symbols = this.symbolsForEvent(event);
|
const symbols = this.symbolsForEvent(event);
|
||||||
symbols.forEach((s, index) => {
|
symbols.forEach((s) => {
|
||||||
const symbol = document.createElement("span");
|
const symbol = document.createElement("span");
|
||||||
symbol.className = s;
|
symbol.className = s;
|
||||||
if (index > 0) {
|
|
||||||
symbol.style.paddingLeft = "5px";
|
|
||||||
}
|
|
||||||
symbolWrapper.appendChild(symbol);
|
symbolWrapper.appendChild(symbol);
|
||||||
});
|
});
|
||||||
eventWrapper.appendChild(symbolWrapper);
|
eventWrapper.appendChild(symbolWrapper);
|
||||||
@@ -601,7 +597,6 @@ Module.register("calendar", {
|
|||||||
*/
|
*/
|
||||||
createEventList (limitNumberOfEntries) {
|
createEventList (limitNumberOfEntries) {
|
||||||
let now = moment();
|
let now = moment();
|
||||||
let today = now.clone().startOf("day");
|
|
||||||
let future = now.clone().startOf("day").add(this.config.maximumNumberOfDays, "days");
|
let future = now.clone().startOf("day").add(this.config.maximumNumberOfDays, "days");
|
||||||
|
|
||||||
let events = [];
|
let events = [];
|
||||||
@@ -705,30 +700,24 @@ Module.register("calendar", {
|
|||||||
* Limit the number of days displayed
|
* Limit the number of days displayed
|
||||||
* If limitDays is set > 0, limit display to that number of days
|
* If limitDays is set > 0, limit display to that number of days
|
||||||
*/
|
*/
|
||||||
if (this.config.limitDays > 0) {
|
if (this.config.limitDays > 0 && events.length > 0) { // watch out for initial display before events arrive from helper
|
||||||
let newEvents = [];
|
// Group all events by date, events on the same date will be in a list with the key being the date.
|
||||||
let lastDate = today.clone().subtract(1, "days");
|
const eventsByDate = Object.groupBy(events, (ev) => this.timestampToMoment(ev.startDate).format("YYYY-MM-DD"));
|
||||||
let days = 0;
|
const newEvents = [];
|
||||||
for (const ev of events) {
|
let currentDate = moment();
|
||||||
let eventDate = this.timestampToMoment(ev.startDate);
|
let daysCollected = 0;
|
||||||
|
|
||||||
/*
|
while (daysCollected < this.config.limitDays) {
|
||||||
* if date of event is later than lastdate
|
const dateStr = currentDate.format("YYYY-MM-DD");
|
||||||
* check if we already are showing max unique days
|
// Check if there are events on the currentDate
|
||||||
*/
|
if (eventsByDate[dateStr] && eventsByDate[dateStr].length > 0) {
|
||||||
if (eventDate.isAfter(lastDate)) {
|
// If there are any events today then get all those events and select the currently active events and the events that are starting later in the day.
|
||||||
// if the only entry in the first day is a full day event that day is not counted as unique
|
newEvents.push(...eventsByDate[dateStr].filter((ev) => this.timestampToMoment(ev.endDate).isAfter(moment())));
|
||||||
if (!this.config.limitDaysNeverSkip && newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
|
// Since we found a day with events, increase the daysCollected by 1
|
||||||
days--;
|
daysCollected++;
|
||||||
}
|
|
||||||
days++;
|
|
||||||
if (days > this.config.limitDays) {
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
lastDate = eventDate;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
newEvents.push(ev);
|
// Search for the next day
|
||||||
|
currentDate.add(1, "day");
|
||||||
}
|
}
|
||||||
events = newEvents;
|
events = newEvents;
|
||||||
}
|
}
|
||||||
@@ -887,7 +876,7 @@ Module.register("calendar", {
|
|||||||
* @param {string} url The calendar url
|
* @param {string} url The calendar url
|
||||||
* @param {string} property The property to look for
|
* @param {string} property The property to look for
|
||||||
* @param {string} defaultValue The value if the property is not found
|
* @param {string} defaultValue The value if the property is not found
|
||||||
* @returns {*} The property
|
* @returns {property} The property
|
||||||
*/
|
*/
|
||||||
getCalendarProperty (url, property, defaultValue) {
|
getCalendarProperty (url, property, defaultValue) {
|
||||||
for (const calendar of this.config.calendars) {
|
for (const calendar of this.config.calendars) {
|
||||||
|
@@ -3,6 +3,8 @@ const ical = require("node-ical");
|
|||||||
const Log = require("logger");
|
const Log = require("logger");
|
||||||
const NodeHelper = require("node_helper");
|
const NodeHelper = require("node_helper");
|
||||||
const CalendarFetcherUtils = require("./calendarfetcherutils");
|
const CalendarFetcherUtils = require("./calendarfetcherutils");
|
||||||
|
const { getUserAgent } = require("#server_functions");
|
||||||
|
const { scheduleTimer } = require("#module_functions");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -29,10 +31,9 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
const fetchCalendar = () => {
|
const fetchCalendar = () => {
|
||||||
clearTimeout(reloadTimer);
|
clearTimeout(reloadTimer);
|
||||||
reloadTimer = null;
|
reloadTimer = null;
|
||||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
|
||||||
let httpsAgent = null;
|
let httpsAgent = null;
|
||||||
let headers = {
|
let headers = {
|
||||||
"User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`
|
"User-Agent": getUserAgent()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selfSignedCert) {
|
if (selfSignedCert) {
|
||||||
@@ -65,31 +66,18 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
fetchFailedCallback(this, error);
|
fetchFailedCallback(this, error);
|
||||||
scheduleTimer();
|
scheduleTimer(reloadTimer, reloadInterval, fetchCalendar);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.broadcastEvents();
|
this.broadcastEvents();
|
||||||
scheduleTimer();
|
scheduleTimer(reloadTimer, reloadInterval, fetchCalendar);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
fetchFailedCallback(this, error);
|
fetchFailedCallback(this, error);
|
||||||
scheduleTimer();
|
scheduleTimer(reloadTimer, reloadInterval, fetchCalendar);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Schedule the timer for the next update.
|
|
||||||
*/
|
|
||||||
const scheduleTimer = function () {
|
|
||||||
if (process.env.JEST_WORKER_ID === undefined) {
|
|
||||||
// only set timer when not running in jest
|
|
||||||
clearTimeout(reloadTimer);
|
|
||||||
reloadTimer = setTimeout(function () {
|
|
||||||
fetchCalendar();
|
|
||||||
}, reloadInterval);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/* public methods */
|
/* public methods */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,7 +97,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the on success callback
|
* Sets the on success callback
|
||||||
* @param {Function} callback The on success callback.
|
* @param {eventsReceivedCallback} callback The on success callback.
|
||||||
*/
|
*/
|
||||||
this.onReceive = function (callback) {
|
this.onReceive = function (callback) {
|
||||||
eventsReceivedCallback = callback;
|
eventsReceivedCallback = callback;
|
||||||
@@ -117,7 +105,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the on error callback
|
* Sets the on error callback
|
||||||
* @param {Function} callback The on error callback.
|
* @param {fetchFailedCallback} callback The on error callback.
|
||||||
*/
|
*/
|
||||||
this.onError = function (callback) {
|
this.onError = function (callback) {
|
||||||
fetchFailedCallback = callback;
|
fetchFailedCallback = callback;
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
const moment = require("moment-timezone");
|
const moment = require("moment-timezone");
|
||||||
|
|
||||||
const Log = require("../../../js/logger");
|
const Log = require("logger");
|
||||||
|
|
||||||
const CalendarFetcherUtils = {
|
const CalendarFetcherUtils = {
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ const CalendarFetcherUtils = {
|
|||||||
* until: the date until the event should be excluded.
|
* until: the date until the event should be excluded.
|
||||||
*/
|
*/
|
||||||
shouldEventBeExcluded (config, title) {
|
shouldEventBeExcluded (config, title) {
|
||||||
let filter = {
|
let result = {
|
||||||
excluded: false,
|
excluded: false,
|
||||||
until: null
|
until: null
|
||||||
};
|
};
|
||||||
@@ -55,14 +55,14 @@ const CalendarFetcherUtils = {
|
|||||||
|
|
||||||
if (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
|
if (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
|
||||||
if (until) {
|
if (until) {
|
||||||
filter.until = until;
|
result.until = until;
|
||||||
} else {
|
} else {
|
||||||
filter.excluded = true;
|
result.excluded = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return filter;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
// Alias modules mentioned in package.js under _moduleAliases.
|
// Alias modules mentioned in package.js under _moduleAliases.
|
||||||
require("module-alias/register");
|
require("module-alias/register");
|
||||||
|
const Log = require("logger");
|
||||||
|
|
||||||
const CalendarFetcher = require("./calendarfetcher");
|
const CalendarFetcher = require("./calendarfetcher");
|
||||||
|
|
||||||
@@ -20,22 +21,22 @@ const auth = {
|
|||||||
pass: pass
|
pass: pass
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Create fetcher ...");
|
Log.log("Create fetcher ...");
|
||||||
|
|
||||||
const fetcher = new CalendarFetcher(url, fetchInterval, [], maximumEntries, maximumNumberOfDays, auth);
|
const fetcher = new CalendarFetcher(url, fetchInterval, [], maximumEntries, maximumNumberOfDays, auth);
|
||||||
|
|
||||||
fetcher.onReceive(function (fetcher) {
|
fetcher.onReceive(function (fetcher) {
|
||||||
console.log(fetcher.events());
|
Log.log(fetcher.events());
|
||||||
console.log("------------------------------------------------------------");
|
Log.log("------------------------------------------------------------");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
fetcher.onError(function (fetcher, error) {
|
fetcher.onError(function (fetcher, error) {
|
||||||
console.log("Fetcher error:");
|
Log.log("Fetcher error:");
|
||||||
console.log(error);
|
Log.log(error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
fetcher.startFetch();
|
fetcher.startFetch();
|
||||||
|
|
||||||
console.log("Create fetcher done! ");
|
Log.log("Create fetcher done! ");
|
||||||
|
@@ -36,7 +36,7 @@ Module.register("clock", {
|
|||||||
},
|
},
|
||||||
// Define styles.
|
// Define styles.
|
||||||
getStyles () {
|
getStyles () {
|
||||||
return ["clock_styles.css"];
|
return ["clock_styles.css", "font-awesome.css"];
|
||||||
},
|
},
|
||||||
// Define start sequence.
|
// Define start sequence.
|
||||||
start () {
|
start () {
|
||||||
|
@@ -87,9 +87,17 @@
|
|||||||
transform-origin: 50% 100%;
|
transform-origin: 50% 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module.clock .digital {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.module.clock .sun,
|
.module.clock .sun,
|
||||||
.module.clock .moon {
|
.module.clock .moon {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
white-space: nowrap;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module.clock .sun > *,
|
.module.clock .sun > *,
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
<div>
|
<div>
|
||||||
<iframe class="newsfeed-fullarticle" src="{{ url }}"></iframe>
|
<iframe class="newsfeed-fullarticle" src="{{ url }}"></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -181,7 +181,7 @@ Module.register("newsfeed", {
|
|||||||
* Gets a feed property by name
|
* Gets a feed property by name
|
||||||
* @param {object} feed A feed object.
|
* @param {object} feed A feed object.
|
||||||
* @param {string} property The name of the property.
|
* @param {string} property The name of the property.
|
||||||
* @returns {*} The value of the specified property for the feed.
|
* @returns {property} The value of the specified property for the feed.
|
||||||
*/
|
*/
|
||||||
getFeedProperty (feed, property) {
|
getFeedProperty (feed, property) {
|
||||||
let res = this.config[property];
|
let res = this.config[property];
|
||||||
|
@@ -1,89 +1,89 @@
|
|||||||
{% macro escapeText(text, dangerouslyDisableAutoEscaping=false) %}
|
{% macro escapeText(text, dangerouslyDisableAutoEscaping=false) %}
|
||||||
{% if dangerouslyDisableAutoEscaping -%}
|
{% if dangerouslyDisableAutoEscaping -%}
|
||||||
{{ text | safe }}
|
{{ text | safe }}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{{ text }}
|
{{ text }}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
{% macro escapeTitle(title, url, dangerouslyDisableAutoEscaping=false, showTitleAsUrl=false) %}
|
{% macro escapeTitle(title, url, dangerouslyDisableAutoEscaping=false, showTitleAsUrl=false) %}
|
||||||
{% if dangerouslyDisableAutoEscaping %}
|
{% if dangerouslyDisableAutoEscaping %}
|
||||||
{% if showTitleAsUrl %}
|
{% if showTitleAsUrl %}
|
||||||
<a href="{{ url }}"
|
<a
|
||||||
style="text-decoration:none;
|
href="{{ url }}"
|
||||||
|
style="text-decoration:none;
|
||||||
color:#ffffff"
|
color:#ffffff"
|
||||||
target="_blank">{{ title | safe }}</a>
|
target="_blank"
|
||||||
{% else %}
|
>{{ title | safe }}</a
|
||||||
{{ title | safe }}
|
>
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if showTitleAsUrl %}
|
{{ title | safe }}
|
||||||
<a href="{{ url }}"
|
|
||||||
style="text-decoration:none;
|
|
||||||
color:#ffffff"
|
|
||||||
target="_blank">{{ title }}</a>
|
|
||||||
{% else %}
|
|
||||||
{{ title }}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if showTitleAsUrl %}
|
||||||
|
<a
|
||||||
|
href="{{ url }}"
|
||||||
|
style="text-decoration:none;
|
||||||
|
color:#ffffff"
|
||||||
|
target="_blank"
|
||||||
|
>{{ title }}</a
|
||||||
|
>
|
||||||
|
{% else %}
|
||||||
|
{{ title }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
{% if loaded %}
|
{% if loaded %}
|
||||||
{% if config.showAsList %}
|
{% if config.showAsList %}
|
||||||
<ul class="newsfeed-list">
|
<ul class="newsfeed-list">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<li>
|
<li>
|
||||||
{% if (config.showSourceTitle and item.sourceTitle) or config.showPublishDate %}
|
{% if (config.showSourceTitle and item.sourceTitle) or config.showPublishDate %}
|
||||||
<div class="newsfeed-source light small dimmed">
|
<div class="newsfeed-source light small dimmed">
|
||||||
{% if item.sourceTitle and config.showSourceTitle %}
|
{% if item.sourceTitle and config.showSourceTitle %}
|
||||||
{{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}:{% endif %}
|
{{ item.sourceTitle }}{% if config.showPublishDate %},{% else %}:{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if config.showPublishDate %}{{ item.publishDate }}:{% endif %}
|
{% if config.showPublishDate %}{{ item.publishDate }}:{% endif %}
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
|
||||||
{{ escapeTitle(item.title, item.url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }}
|
|
||||||
</div>
|
|
||||||
{% if config.showDescription %}
|
|
||||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
|
||||||
{% if config.truncDescription %}
|
|
||||||
{{ escapeText(item.description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
|
||||||
{% else %}
|
|
||||||
{{ escapeText(item.description, config.dangerouslyDisableAutoEscaping) }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% else %}
|
|
||||||
<div>
|
|
||||||
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
|
|
||||||
<div class="newsfeed-source light small dimmed">
|
|
||||||
{% if sourceTitle and config.showSourceTitle %}
|
|
||||||
{{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %}, {% else %}:{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% if config.showPublishDate %}{{ publishDate }}:{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
|
||||||
{{ escapeTitle(title, url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }}
|
|
||||||
</div>
|
</div>
|
||||||
{% if config.showDescription %}
|
{% endif %}
|
||||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">{{ escapeTitle(item.title, item.url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }}</div>
|
||||||
{% if config.truncDescription %}
|
{% if config.showDescription %}
|
||||||
{{ escapeText(description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||||
{% else %}
|
{% if config.truncDescription %}
|
||||||
{{ escapeText(description, config.dangerouslyDisableAutoEscaping) }}
|
{{ escapeText(item.description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
||||||
{% endif %}
|
{% else %}
|
||||||
</div>
|
{{ escapeText(item.description, config.dangerouslyDisableAutoEscaping) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<div>
|
||||||
|
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
|
||||||
|
<div class="newsfeed-source light small dimmed">
|
||||||
|
{% if sourceTitle and config.showSourceTitle %}
|
||||||
|
{{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %},{% else %}:{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if config.showPublishDate %}{{ publishDate }}:{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elseif empty %}
|
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">{{ escapeTitle(title, url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }}</div>
|
||||||
<div class="small dimmed">{{ "NEWSFEED_NO_ITEMS" | translate | safe }}</div>
|
{% if config.showDescription %}
|
||||||
{% elseif error %}
|
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||||
<div class="small dimmed">
|
{% if config.truncDescription %}
|
||||||
{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}
|
{{ escapeText(description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
||||||
|
{% else %}
|
||||||
|
{{ escapeText(description, config.dangerouslyDisableAutoEscaping) }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% elseif empty %}
|
||||||
|
<div class="small dimmed">{{ "NEWSFEED_NO_ITEMS" | translate | safe }}</div>
|
||||||
|
{% elseif error %}
|
||||||
|
<div class="small dimmed">{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="small dimmed">{{ "LOADING" | translate | safe }}</div>
|
<div class="small dimmed">{{ "LOADING" | translate | safe }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@@ -5,6 +5,8 @@ const iconv = require("iconv-lite");
|
|||||||
const { htmlToText } = require("html-to-text");
|
const { htmlToText } = require("html-to-text");
|
||||||
const Log = require("logger");
|
const Log = require("logger");
|
||||||
const NodeHelper = require("node_helper");
|
const NodeHelper = require("node_helper");
|
||||||
|
const { getUserAgent } = require("#server_functions");
|
||||||
|
const { scheduleTimer } = require("#module_functions");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responsible for requesting an update on the set interval and broadcasting the data.
|
* Responsible for requesting an update on the set interval and broadcasting the data.
|
||||||
@@ -79,12 +81,12 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
|||||||
|
|
||||||
parser.on("error", (error) => {
|
parser.on("error", (error) => {
|
||||||
fetchFailedCallback(this, error);
|
fetchFailedCallback(this, error);
|
||||||
scheduleTimer();
|
scheduleTimer(reloadTimer, reloadIntervalMS, fetchNews);
|
||||||
});
|
});
|
||||||
|
|
||||||
//"end" event is not broadcast if the feed is empty but "finish" is used for both
|
//"end" event is not broadcast if the feed is empty but "finish" is used for both
|
||||||
parser.on("finish", () => {
|
parser.on("finish", () => {
|
||||||
scheduleTimer();
|
scheduleTimer(reloadTimer, reloadIntervalMS, fetchNews);
|
||||||
});
|
});
|
||||||
|
|
||||||
parser.on("ttl", (minutes) => {
|
parser.on("ttl", (minutes) => {
|
||||||
@@ -100,9 +102,8 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
|
||||||
const headers = {
|
const headers = {
|
||||||
"User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`,
|
"User-Agent": getUserAgent(),
|
||||||
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
||||||
Pragma: "no-cache"
|
Pragma: "no-cache"
|
||||||
};
|
};
|
||||||
@@ -120,23 +121,10 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
fetchFailedCallback(this, error);
|
fetchFailedCallback(this, error);
|
||||||
scheduleTimer();
|
scheduleTimer(reloadTimer, reloadIntervalMS, fetchNews);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Schedule the timer for the next update.
|
|
||||||
*/
|
|
||||||
const scheduleTimer = function () {
|
|
||||||
if (process.env.JEST_WORKER_ID === undefined) {
|
|
||||||
// only set timer when not running in jest
|
|
||||||
clearTimeout(reloadTimer);
|
|
||||||
reloadTimer = setTimeout(function () {
|
|
||||||
fetchNews();
|
|
||||||
}, reloadIntervalMS);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/* public methods */
|
/* public methods */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,3 +1 @@
|
|||||||
<div class="small bright">
|
<div class="small bright">{{ "MODULE_CONFIG_CHANGED" | translate({MODULE_NAME: "Newsfeed"}) | safe }}</div>
|
||||||
{{ "MODULE_CONFIG_CHANGED" | translate({MODULE_NAME: "Newsfeed"}) | safe }}
|
|
||||||
</div>
|
|
||||||
|
@@ -4,8 +4,6 @@ const fs = require("node:fs");
|
|||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const Log = require("logger");
|
const Log = require("logger");
|
||||||
|
|
||||||
const BASE_DIR = path.normalize(`${__dirname}/../../../`);
|
|
||||||
|
|
||||||
class GitHelper {
|
class GitHelper {
|
||||||
constructor () {
|
constructor () {
|
||||||
this.gitRepos = [];
|
this.gitRepos = [];
|
||||||
@@ -35,10 +33,10 @@ class GitHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async add (moduleName) {
|
async add (moduleName) {
|
||||||
let moduleFolder = BASE_DIR;
|
let moduleFolder = `${global.root_path}`;
|
||||||
|
|
||||||
if (moduleName !== "MagicMirror") {
|
if (moduleName !== "MagicMirror") {
|
||||||
moduleFolder = `${moduleFolder}modules/${moduleName}`;
|
moduleFolder = `${moduleFolder}/modules/${moduleName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
const fs = require("node:fs");
|
const fs = require("node:fs");
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const NodeHelper = require("node_helper");
|
const NodeHelper = require("node_helper");
|
||||||
const defaultModules = require("../defaultmodules");
|
|
||||||
|
const defaultModules = require(`${global.root_path}/modules/default/defaultmodules`);
|
||||||
const GitHelper = require("./git_helper");
|
const GitHelper = require("./git_helper");
|
||||||
const UpdateHelper = require("./update_helper");
|
const UpdateHelper = require("./update_helper");
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ module.exports = NodeHelper.create({
|
|||||||
return modules;
|
return modules;
|
||||||
} else {
|
} else {
|
||||||
// get modules from modules-directory
|
// get modules from modules-directory
|
||||||
const moduleDir = path.normalize(`${__dirname}/../../`);
|
const moduleDir = path.normalize(`${global.root_path}/modules`);
|
||||||
const getDirectories = (source) => {
|
const getDirectories = (source) => {
|
||||||
return fs.readdirSync(source, { withFileTypes: true })
|
return fs.readdirSync(source, { withFileTypes: true })
|
||||||
.filter((dirent) => dirent.isDirectory() && dirent.name !== "default")
|
.filter((dirent) => dirent.isDirectory() && dirent.name !== "default")
|
||||||
|
@@ -1,41 +1,41 @@
|
|||||||
{% if not suspended %}
|
{% if not suspended %}
|
||||||
{% if needRestart %}
|
{% if needRestart %}
|
||||||
<div class="small bright">
|
<div class="small bright">
|
||||||
<i class="fas fa-rotate"></i>
|
<i class="fas fa-rotate"></i>
|
||||||
<span>
|
<span>
|
||||||
{% set restartTextLabel = "UPDATE_NOTIFICATION_NEED-RESTART" %}
|
{% set restartTextLabel = "UPDATE_NOTIFICATION_NEED-RESTART" %}
|
||||||
{{ restartTextLabel | translate() | safe }}
|
{{ restartTextLabel | translate() | safe }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for name, status in moduleList %}
|
{% for name, status in moduleList %}
|
||||||
<div class="small bright">
|
<div class="small bright">
|
||||||
<i class="fas fa-exclamation-circle"></i>
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
<span>
|
<span>
|
||||||
{% set mainTextLabel = "UPDATE_NOTIFICATION" if name === "MagicMirror" else "UPDATE_NOTIFICATION_MODULE" %}
|
{% set mainTextLabel = "UPDATE_NOTIFICATION" if name === "MagicMirror" else "UPDATE_NOTIFICATION_MODULE" %}
|
||||||
{{ mainTextLabel | translate({MODULE_NAME: name}) }}
|
{{ mainTextLabel | translate({MODULE_NAME: name}) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="xsmall dimmed">
|
<div class="xsmall dimmed">
|
||||||
{% set subTextLabel = "UPDATE_INFO_SINGLE" if status.behind === 1 else "UPDATE_INFO_MULTIPLE" %}
|
{% set subTextLabel = "UPDATE_INFO_SINGLE" if status.behind === 1 else "UPDATE_INFO_MULTIPLE" %}
|
||||||
{{ subTextLabel | translate({COMMIT_COUNT: status.behind, BRANCH_NAME: status.current}) | diffLink(status) | safe }}
|
{{ subTextLabel | translate({COMMIT_COUNT: status.behind, BRANCH_NAME: status.current}) | diffLink(status) | safe }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for name, status in updatesList %}
|
{% for name, status in updatesList %}
|
||||||
<div class="small bright">
|
<div class="small bright">
|
||||||
{% if status.done %}
|
{% if status.done %}
|
||||||
<i class="fas fa-check" style="color: lightgreen;"></i>
|
<i class="fas fa-check" style="color: lightgreen;"></i>
|
||||||
<span>
|
<span>
|
||||||
{% set updateTextLabel = "UPDATE_NOTIFICATION_DONE" %}
|
{% set updateTextLabel = "UPDATE_NOTIFICATION_DONE" %}
|
||||||
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
|
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="fas fa-xmark" style="color: red;"></i>
|
<i class="fas fa-xmark" style="color: red;"></i>
|
||||||
<span>
|
<span>
|
||||||
{% set updateTextLabel = "UPDATE_NOTIFICATION_ERROR" %}
|
{% set updateTextLabel = "UPDATE_NOTIFICATION_ERROR" %}
|
||||||
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
|
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@@ -1,101 +1,97 @@
|
|||||||
{% macro humidity() %}
|
{% macro humidity() %}
|
||||||
{% if current.humidity %}
|
{% if current.humidity %}
|
||||||
<span class="humidity"><span>{{ current.humidity | decimalSymbol }}</span><sup> <i class="wi wi-humidity humidity-icon"></i></sup></span>
|
<span class="humidity"
|
||||||
{% endif %}
|
><span>{{ current.humidity | decimalSymbol }}</span><sup> <i class="wi wi-humidity humidity-icon"></i></sup
|
||||||
|
></span>
|
||||||
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
{% if current %}
|
{% if current %}
|
||||||
{% if not config.onlyTemp %}
|
{% if not config.onlyTemp %}
|
||||||
<div class="normal medium">
|
<div class="normal medium">
|
||||||
<span class="wi wi-strong-wind dimmed"></span>
|
<span class="wi wi-strong-wind dimmed"></span>
|
||||||
<span>
|
<span>
|
||||||
{{ current.windSpeed | unit("wind") | round }}
|
{{ current.windSpeed | unit("wind") | round }}
|
||||||
{% if config.showWindDirection %}
|
{% if config.showWindDirection %}
|
||||||
<sup>
|
<sup>
|
||||||
{% if config.showWindDirectionAsArrow %}
|
{% 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 %}
|
{% else %}
|
||||||
{{ current.cardinalWindDirection() | translate }}
|
{{ current.cardinalWindDirection() | translate }}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</sup>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
{% if config.showHumidity === "wind" %}
|
|
||||||
{{ humidity() }}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if config.showSun %}
|
|
||||||
<span class="wi dimmed wi-{{ current.nextSunAction() }}"></span>
|
</sup>
|
||||||
<span>
|
|
||||||
{% if current.nextSunAction() === "sunset" %}
|
|
||||||
{{ current.sunset | formatTime }}
|
|
||||||
{% else %}
|
|
||||||
{{ current.sunrise | formatTime }}
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if config.showUVIndex %}
|
|
||||||
<td class="align-right bright uv-index">
|
|
||||||
<div class="wi dimmed wi-hot"></div>
|
|
||||||
{{ current.uv_index }}
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<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 %}
|
|
||||||
<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 %}
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
{% if config.showHumidity === "wind" %}
|
||||||
|
{{ humidity() }}
|
||||||
|
{% endif %}
|
||||||
|
{% if config.showSun %}
|
||||||
|
<span class="wi dimmed wi-{{ current.nextSunAction() }}"></span>
|
||||||
|
<span>
|
||||||
|
{% if current.nextSunAction() === "sunset" %}
|
||||||
|
{{ current.sunset | formatTime }}
|
||||||
|
{% else %}
|
||||||
|
{{ current.sunrise | formatTime }}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if config.showUVIndex %}
|
||||||
|
<td class="align-right bright uv-index">
|
||||||
|
<div class="wi dimmed wi-hot"></div>
|
||||||
|
{{ current.uv_index }}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if (config.showFeelsLike or config.showPrecipitationAmount or config.showPrecipitationProbability) and not config.onlyTemp %}
|
{% endif %}
|
||||||
<div class="normal medium feelslike">
|
<div class="flex large type-temp">
|
||||||
{% if config.showFeelsLike %}
|
{% if config.showIndoorTemperature and indoor.temperature or config.showIndoorHumidity and indoor.humidity %}
|
||||||
<span class="dimmed">
|
<span class="medium fas fa-home"></span>
|
||||||
{% if config.showHumidity === "feelslike" %}
|
<span style="display: inline-block">
|
||||||
{{ humidity() }}
|
{% if config.showIndoorTemperature and indoor.temperature %}
|
||||||
{% endif %}
|
<sup class="small" style="position: relative; display: block; text-align: left;">
|
||||||
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
|
<span> {{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }} </span>
|
||||||
</span>
|
</sup>
|
||||||
<br />
|
{% endif %}
|
||||||
{% endif %}
|
{% if config.showIndoorHumidity and indoor.humidity %}
|
||||||
{% if config.showPrecipitationAmount and current.precipitationAmount %}
|
<sub class="small" style="position: relative; display: block; text-align: left;">
|
||||||
<span class="dimmed">
|
<span> {{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }} </span>
|
||||||
<span class="precipitationLeadText">{{ "PRECIP_AMOUNT" | translate }}</span> {{ current.precipitationAmount | unit("precip", current.precipitationUnits) }}
|
</sub>
|
||||||
</span>
|
{% endif %}
|
||||||
<br />
|
</span>
|
||||||
{% endif %}
|
|
||||||
{% if config.showPrecipitationProbability and current.precipitationProbability %}
|
|
||||||
<span class="dimmed">
|
|
||||||
<span class="precipitationLeadText">{{ "PRECIP_POP" | translate }}</span> {{ current.precipitationProbability }}%
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if config.showHumidity === "below" %}
|
{% if current.weatherType %}
|
||||||
<span class="medium dimmed">{{ humidity() }}</span>
|
<span class="light wi weathericon wi-{{ current.weatherType }}"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<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 />
|
||||||
|
{% endif %}
|
||||||
|
{% if config.showPrecipitationAmount and current.precipitationAmount %}
|
||||||
|
<span class="dimmed"> <span class="precipitationLeadText">{{ "PRECIP_AMOUNT" | translate }}</span> {{ current.precipitationAmount | unit("precip", current.precipitationUnits) }} </span>
|
||||||
|
<br />
|
||||||
|
{% endif %}
|
||||||
|
{% if config.showPrecipitationProbability and current.precipitationProbability %}
|
||||||
|
<span class="dimmed"> <span class="precipitationLeadText">{{ "PRECIP_POP" | translate }}</span> {{ current.precipitationProbability }}% </span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if config.showHumidity === "below" %}
|
||||||
|
<span class="medium dimmed">{{ humidity() }}</span>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Uncomment the line below to see the contents of the `current` object. -->
|
<!-- Uncomment the line below to see the contents of the `current` object. -->
|
||||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{current | dump}}</div> -->
|
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{ current | dump }}</div> -->
|
||||||
|
@@ -1,52 +1,46 @@
|
|||||||
{% if forecast %}
|
{% if forecast %}
|
||||||
{% set numSteps = forecast | calcNumSteps %}
|
{% set numSteps = forecast | calcNumSteps %}
|
||||||
{% set currentStep = 0 %}
|
{% set currentStep = 0 %}
|
||||||
<table class="{{ config.tableClass }}">
|
<table class="{{ config.tableClass }}">
|
||||||
{% if config.ignoreToday %}
|
{% if config.ignoreToday %}
|
||||||
{% set forecast = forecast.splice(1) %}
|
{% set forecast = forecast.splice(1) %}
|
||||||
|
{% endif %}
|
||||||
|
{% set forecast = forecast.slice(0, numSteps) %}
|
||||||
|
{% for f in forecast %}
|
||||||
|
<tr
|
||||||
|
{% if config.colored %}class="colored"{% endif %}
|
||||||
|
{% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}
|
||||||
|
>
|
||||||
|
{% if (currentStep == 0) and config.ignoreToday == false and config.absoluteDates == false %}
|
||||||
|
<td class="day">{{ "TODAY" | translate }}</td>
|
||||||
|
{% elif (currentStep == 1) and config.ignoreToday == false and config.absoluteDates == false %}
|
||||||
|
<td class="day">{{ "TOMORROW" | translate }}</td>
|
||||||
|
{% else %}
|
||||||
|
<td class="day">{{ f.date.format("ddd") }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% set forecast = forecast.slice(0, numSteps) %}
|
<td class="bright weather-icon">
|
||||||
{% for f in forecast %}
|
<span class="wi weathericon wi-{{ f.weatherType }}"></span>
|
||||||
<tr {% if config.colored %}class="colored"{% endif %}
|
</td>
|
||||||
{% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
|
<td class="align-right bright max-temp">{{ f.maxTemperature | roundValue | unit("temperature") | decimalSymbol }}</td>
|
||||||
{% if (currentStep == 0) and config.ignoreToday == false and config.absoluteDates == false %}
|
<td class="align-right min-temp">{{ f.minTemperature | roundValue | unit("temperature") | decimalSymbol }}</td>
|
||||||
<td class="day">{{ "TODAY" | translate }}</td>
|
{% if config.showPrecipitationAmount %}
|
||||||
{% elif (currentStep == 1) and config.ignoreToday == false and config.absoluteDates == false %}
|
<td class="align-right bright precipitation-amount">{{ f.precipitationAmount | unit("precip", f.precipitationUnits) }}</td>
|
||||||
<td class="day">{{ "TOMORROW" | translate }}</td>
|
{% endif %}
|
||||||
{% else %}
|
{% if config.showPrecipitationProbability %}
|
||||||
<td class="day">{{ f.date.format("ddd") }}</td>
|
<td class="align-right bright precipitation-prob">{{ f.precipitationProbability | unit('precip', '%') }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="bright weather-icon">
|
{% if config.showUVIndex %}
|
||||||
<span class="wi weathericon wi-{{ f.weatherType }}"></span>
|
<td class="align-right dimmed uv-index">
|
||||||
</td>
|
{{ f.uv_index }}
|
||||||
<td class="align-right bright max-temp">
|
<span class="wi dimmed weathericon wi-hot"></span>
|
||||||
{{ f.maxTemperature | roundValue | unit("temperature") | decimalSymbol }}
|
</td>
|
||||||
</td>
|
{% endif %}
|
||||||
<td class="align-right min-temp">
|
</tr>
|
||||||
{{ f.minTemperature | roundValue | unit("temperature") | decimalSymbol }}
|
{% set currentStep = currentStep + 1 %}
|
||||||
</td>
|
{% endfor %}
|
||||||
{% if config.showPrecipitationAmount %}
|
</table>
|
||||||
<td class="align-right bright precipitation-amount">
|
|
||||||
{{ f.precipitationAmount | unit("precip", f.precipitationUnits) }}
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
|
||||||
{% if config.showPrecipitationProbability %}
|
|
||||||
<td class="align-right bright precipitation-prob">
|
|
||||||
{{ f.precipitationProbability | unit('precip', '%') }}
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
|
||||||
{% if config.showUVIndex %}
|
|
||||||
<td class="align-right dimmed uv-index">
|
|
||||||
{{ f.uv_index }}
|
|
||||||
<span class="wi dimmed weathericon wi-hot"></span>
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% set currentStep = currentStep + 1 %}
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Uncomment the line below to see the contents of the `forecast` object. -->
|
<!-- Uncomment the line below to see the contents of the `forecast` object. -->
|
||||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{forecast | dump}}</div> -->
|
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{ forecast | dump }}</div> -->
|
||||||
|
@@ -1,52 +1,48 @@
|
|||||||
{% if hourly %}
|
{% if hourly %}
|
||||||
{% set numSteps = hourly | calcNumEntries %}
|
{% set numSteps = hourly | calcNumEntries %}
|
||||||
{% set currentStep = 0 %}
|
{% set currentStep = 0 %}
|
||||||
<table class="{{ config.tableClass }}">
|
<table class="{{ config.tableClass }}">
|
||||||
{% set hours = hourly.slice(0, numSteps) %}
|
{% set hours = hourly.slice(0, numSteps) %}
|
||||||
{% for hour in hours %}
|
{% for hour in hours %}
|
||||||
<tr {% if config.colored %}class="colored"{% endif %}
|
<tr
|
||||||
{% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
|
{% if config.colored %}class="colored"{% endif %}
|
||||||
<td class="day">{{ hour.date | formatTime }}</td>
|
{% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}
|
||||||
<td class="bright weather-icon">
|
>
|
||||||
<span class="wi weathericon wi-{{ hour.weatherType }}"></span>
|
<td class="day">{{ hour.date | formatTime }}</td>
|
||||||
</td>
|
<td class="bright weather-icon">
|
||||||
<td class="align-right bright">
|
<span class="wi weathericon wi-{{ hour.weatherType }}"></span>
|
||||||
{{ hour.temperature | roundValue | unit("temperature") }}
|
</td>
|
||||||
</td>
|
<td class="align-right bright">{{ hour.temperature | roundValue | unit("temperature") }}</td>
|
||||||
{% if config.showUVIndex %}
|
{% if config.showUVIndex %}
|
||||||
<td class="align-right bright uv-index">
|
<td class="align-right bright uv-index">
|
||||||
{% if hour.uv_index!=0 %}
|
{% if hour.uv_index!=0 %}
|
||||||
{{ hour.uv_index }}
|
{{ hour.uv_index }}
|
||||||
<span class="wi weathericon wi-hot"></span>
|
<span class="wi weathericon wi-hot"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if config.showHumidity != "none" %}
|
{% if config.showHumidity != "none" %}
|
||||||
<td class="align-left bright humidity-hourly">
|
<td class="align-left bright humidity-hourly">
|
||||||
{{ hour.humidity }}
|
{{ hour.humidity }}
|
||||||
<span class="wi wi-humidity humidity-icon"></span>
|
<span class="wi wi-humidity humidity-icon"></span>
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if config.showPrecipitationAmount %}
|
{% if config.showPrecipitationAmount %}
|
||||||
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
|
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
|
||||||
<td class="align-right bright precipitation-amount">
|
<td class="align-right bright precipitation-amount">{{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }}</td>
|
||||||
{{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }}
|
{% endif %}
|
||||||
</td>
|
{% endif %}
|
||||||
{% endif %}
|
{% if config.showPrecipitationProbability %}
|
||||||
{% endif %}
|
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
|
||||||
{% if config.showPrecipitationProbability %}
|
<td class="align-right bright precipitation-prob">{{ hour.precipitationProbability | unit('precip', '%') }}</td>
|
||||||
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
|
{% endif %}
|
||||||
<td class="align-right bright precipitation-prob">
|
{% endif %}
|
||||||
{{ hour.precipitationProbability | unit('precip', '%') }}
|
</tr>
|
||||||
</td>
|
{% set currentStep = currentStep + 1 %}
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
{% endif %}
|
</table>
|
||||||
</tr>
|
|
||||||
{% set currentStep = currentStep + 1 %}
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Uncomment the line below to see the contents of the `hourly` object. -->
|
<!-- Uncomment the line below to see the contents of the `hourly` object. -->
|
||||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{hourly | dump}}</div> -->
|
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{ hourly | dump }}</div> -->
|
||||||
|
@@ -24,6 +24,8 @@
|
|||||||
* with locations you can search under column B (English Names), with the corresponding siteCode under
|
* with locations you can search under column B (English Names), with the corresponding siteCode under
|
||||||
* column A (Codes) and provCode under column C (Province).
|
* column A (Codes) and provCode under column C (Province).
|
||||||
*
|
*
|
||||||
|
* Acknowledgement: Some logic and code for parsing Environment Canada web pages is based on material from MMM-EnvCanada
|
||||||
|
*
|
||||||
* License to use Environment Canada (EC) data is detailed here:
|
* License to use Environment Canada (EC) data is detailed here:
|
||||||
* https://eccc-msc.github.io/open-data/licence/readme_en/
|
* https://eccc-msc.github.io/open-data/licence/readme_en/
|
||||||
*/
|
*/
|
||||||
@@ -49,6 +51,9 @@ WeatherProvider.register("envcanada", {
|
|||||||
this.todayTempCacheMax = 0;
|
this.todayTempCacheMax = 0;
|
||||||
this.todayCached = false;
|
this.todayCached = false;
|
||||||
this.cacheCurrentTemp = 999;
|
this.cacheCurrentTemp = 999;
|
||||||
|
this.lastCityPageCurrent = " ";
|
||||||
|
this.lastCityPageForecast = " ";
|
||||||
|
this.lastCityPageHourly = " ";
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -63,69 +68,158 @@ WeatherProvider.register("envcanada", {
|
|||||||
* 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 () {
|
fetchCurrentWeather () {
|
||||||
this.fetchData(this.getUrl(), "xml")
|
this.fetchCommon("Current");
|
||||||
.then((data) => {
|
|
||||||
if (!data) {
|
|
||||||
// Did not receive usable new data.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
|
||||||
|
|
||||||
this.setCurrentWeather(currentWeather);
|
|
||||||
})
|
|
||||||
.catch(function (request) {
|
|
||||||
Log.error("Could not load EnvCanada site data ... ", request);
|
|
||||||
})
|
|
||||||
.finally(() => this.updateAvailable());
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Override the fetchWeatherForecast method to query EC and construct Forecast weather objects
|
* Override the fetchWeatherForecast method to query EC and construct Forecast/Daily weather objects
|
||||||
*/
|
*/
|
||||||
fetchWeatherForecast () {
|
fetchWeatherForecast () {
|
||||||
this.fetchData(this.getUrl(), "xml")
|
|
||||||
.then((data) => {
|
|
||||||
if (!data) {
|
|
||||||
// Did not receive usable new data.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const forecastWeather = this.generateWeatherObjectsFromForecast(data);
|
|
||||||
|
|
||||||
this.setWeatherForecast(forecastWeather);
|
this.fetchCommon("Forecast");
|
||||||
})
|
|
||||||
.catch(function (request) {
|
|
||||||
Log.error("Could not load EnvCanada forecast data ... ", request);
|
|
||||||
})
|
|
||||||
.finally(() => this.updateAvailable());
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Override the fetchWeatherHourly method to query EC and construct Forecast weather objects
|
* Override the fetchWeatherHourly method to query EC and construct Hourly weather objects
|
||||||
*/
|
*/
|
||||||
fetchWeatherHourly () {
|
fetchWeatherHourly () {
|
||||||
this.fetchData(this.getUrl(), "xml")
|
this.fetchCommon("Hourly");
|
||||||
.then((data) => {
|
|
||||||
if (!data) {
|
|
||||||
// Did not receive usable new data.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const hourlyWeather = this.generateWeatherObjectsFromHourly(data);
|
|
||||||
|
|
||||||
this.setWeatherHourly(hourlyWeather);
|
|
||||||
})
|
|
||||||
.catch(function (request) {
|
|
||||||
Log.error("Could not load EnvCanada hourly data ... ", request);
|
|
||||||
})
|
|
||||||
.finally(() => this.updateAvailable());
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Build the EC URL based on the Site Code and Province Code specified in the config params. Note that the
|
* Because the process to fetch weather data is virtually the same for Current, Forecast/Daily, and Hourly weather,
|
||||||
* URL defaults to the English version simply because there is no language dependency in the data
|
* a common module is used to access the EC weather data. The only customization (based on the caller of this routine)
|
||||||
* being accessed. This is only pertinent when using the EC data elements that contain a textual forecast.
|
* is how the data will be parsed to satisfy the Weather module config in Config.js
|
||||||
|
*
|
||||||
|
* Accessing EC weather data is accomplished in 2 steps:
|
||||||
|
*
|
||||||
|
* 1. Query the MSC Datamart Index page, which returns a list of all the filenames for all the cities that have
|
||||||
|
* weather data currently available.
|
||||||
|
*
|
||||||
|
* 2. With the city filename identified, build the appropriate URL and get the weather data (XML document) for the
|
||||||
|
* city specified in the Weather module Config information
|
||||||
|
*/
|
||||||
|
fetchCommon (target) {
|
||||||
|
const forecastURL = this.getUrl(); // Get the approriate URL for the MSC Datamart Index page
|
||||||
|
|
||||||
|
Log.debug(`[weather.envcanada] ${target} Index url: ${forecastURL}`);
|
||||||
|
|
||||||
|
this.fetchData(forecastURL, "xml") // Query the Index page URL
|
||||||
|
.then((indexData) => {
|
||||||
|
if (!indexData) {
|
||||||
|
// Did not receive usable new data.
|
||||||
|
Log.info(`weather.envcanada ${target} - did not receive usable index data`);
|
||||||
|
this.updateAvailable(); // If there were issues, update anyways to reset timer
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* With the Index page read, we must locate the filename/link for the specified city (aka Sitecode).
|
||||||
|
* This is done by building the city filename and searching for it on the Index page. Once found,
|
||||||
|
* extract the full filename (a unique name that includes dat/time, filename, etc.) and then add it
|
||||||
|
* to the Index page URL to create the proper URL pointing to the city's weather data. Finally, read the
|
||||||
|
* URL to pull in the city's XML document so that weather data can be parsed and displayed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let forecastFile = "";
|
||||||
|
let forecastFileURL = "";
|
||||||
|
const fileSuffix = `${this.config.siteCode}_en.xml`; // Build city filename
|
||||||
|
const nextFile = indexData.body.innerHTML.split(fileSuffix); // Find filename on Index page
|
||||||
|
|
||||||
|
if (nextFile.length > 1) { // Parse out the full unqiue file city filename
|
||||||
|
// Find the last occurrence
|
||||||
|
forecastFile = nextFile[nextFile.length - 2].slice(-41) + fileSuffix;
|
||||||
|
forecastFileURL = forecastURL + forecastFile; // Create full URL to the city's weather data
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.debug(`[weather.envcanada] ${target} Citypage url: ${forecastFileURL}`);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If the Citypage filename has not changed since the last Weather refresh, the forecast has not changed and
|
||||||
|
* and therefore we can skip reading the Citypage URL.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (target === "Current" && this.lastCityPageCurrent === forecastFileURL) {
|
||||||
|
Log.debug(`[weather.envcanada] ${target} - Newest Citypage has already been seen - skipping!`);
|
||||||
|
this.updateAvailable(); // Update anyways to reset refresh timer
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target === "Forecast" && this.lastCityPageForecast === forecastFileURL) {
|
||||||
|
Log.debug(`[weather.envcanada] ${target} - Newest Citypage has already been seen - skipping!`);
|
||||||
|
this.updateAvailable(); // Update anyways to reset refresh timer
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target === "Hourly" && this.lastCityPageHourly === forecastFileURL) {
|
||||||
|
Log.debug(`[weather.envcanada] ${target} - Newest Citypage has already been seen - skipping!`);
|
||||||
|
this.updateAvailable(); // Update anyways to reset refresh timer
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchData(forecastFileURL, "xml") // Read city's URL to get weather data
|
||||||
|
.then((cityData) => {
|
||||||
|
if (!cityData) {
|
||||||
|
// Did not receive usable new data.
|
||||||
|
Log.info(`weather.envcanada ${target} - did not receive usable citypage data`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* With the city's weather data read, parse the resulting XML document for the appropriate weather data
|
||||||
|
* elements to create a weather object. Next, set Weather modules details from that object.
|
||||||
|
*/
|
||||||
|
Log.debug(`[weather.envcanada] ${target} - Citypage has been read and will be processed for updates`);
|
||||||
|
|
||||||
|
if (target === "Current") {
|
||||||
|
const currentWeather = this.generateWeatherObjectFromCurrentWeather(cityData);
|
||||||
|
this.setCurrentWeather(currentWeather);
|
||||||
|
this.lastCityPageCurrent = forecastFileURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target === "Forecast") {
|
||||||
|
const forecastWeather = this.generateWeatherObjectsFromForecast(cityData);
|
||||||
|
this.setWeatherForecast(forecastWeather);
|
||||||
|
this.lastCityPageForecast = forecastFileURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target === "Hourly") {
|
||||||
|
const hourlyWeather = this.generateWeatherObjectsFromHourly(cityData);
|
||||||
|
this.setWeatherHourly(hourlyWeather);
|
||||||
|
this.lastCityPageHourly = forecastFileURL;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (cityRequest) {
|
||||||
|
Log.info(`weather.envcanada ${target} - could not load citypage data from: ${forecastFileURL}`);
|
||||||
|
})
|
||||||
|
.finally(() => this.updateAvailable()); // Update no matter what to reset weather refresh timer
|
||||||
|
})
|
||||||
|
.catch(function (indexRequest) {
|
||||||
|
Log.error(`weather.envcanada ${target} - could not load index data ... `, indexRequest);
|
||||||
|
this.updateAvailable(); // If there were issues, update anyways to reset timer
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Build the EC Index page URL based on current GMT hour. The Index page will provide a list of links for each city
|
||||||
|
* that will, in turn, provide actual weather data. The URL is comprised of 3 parts:
|
||||||
|
*
|
||||||
|
* Fixed value + Prov code specified in Weather module Config.js + current hour as GMT
|
||||||
*/
|
*/
|
||||||
getUrl () {
|
getUrl () {
|
||||||
return `https://dd.weather.gc.ca/citypage_weather/xml/${this.config.provCode}/${this.config.siteCode}_e.xml`;
|
let forecastURL = `https://dd.weather.gc.ca/citypage_weather/${this.config.provCode}`;
|
||||||
|
const hour = this.getCurrentHourGMT();
|
||||||
|
forecastURL += `/${hour}/`;
|
||||||
|
return forecastURL;
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Get current hour-of-day in GMT context
|
||||||
|
*/
|
||||||
|
getCurrentHourGMT () {
|
||||||
|
const now = new Date();
|
||||||
|
return now.toISOString().substring(11, 13); // "HH" in GMT
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -151,7 +245,6 @@ WeatherProvider.register("envcanada", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentWeather.windSpeed = WeatherUtils.convertWindToMs(ECdoc.querySelector("siteData currentConditions wind speed").textContent);
|
currentWeather.windSpeed = WeatherUtils.convertWindToMs(ECdoc.querySelector("siteData currentConditions wind speed").textContent);
|
||||||
|
|
||||||
currentWeather.windFromDirection = ECdoc.querySelector("siteData currentConditions wind bearing").textContent;
|
currentWeather.windFromDirection = ECdoc.querySelector("siteData currentConditions wind bearing").textContent;
|
||||||
|
|
||||||
currentWeather.humidity = ECdoc.querySelector("siteData currentConditions relativeHumidity").textContent;
|
currentWeather.humidity = ECdoc.querySelector("siteData currentConditions relativeHumidity").textContent;
|
||||||
@@ -214,7 +307,7 @@ WeatherProvider.register("envcanada", {
|
|||||||
/*
|
/*
|
||||||
* The EC forecast is held in a 12-element array - Elements 0 to 11 - with each day encompassing
|
* 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
|
* 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.
|
* element details the Tonight (nighttime) forecast. Element 0 is always for the current day.
|
||||||
*
|
*
|
||||||
* However... the forecast is somewhat 'rolling'.
|
* However... the forecast is somewhat 'rolling'.
|
||||||
*
|
*
|
||||||
@@ -225,7 +318,7 @@ WeatherProvider.register("envcanada", {
|
|||||||
*
|
*
|
||||||
* But, if the EC forecast is queried in late afternoon, the Current Today forecast will be rolled
|
* 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
|
* 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,
|
* Elements 1/2, 3/4, 5/6, 7/8, and 9/10. As well, Element 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
|
* 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.
|
* Elements 0 to 11, and will ignore the additional Todat forecast in Element 11.
|
||||||
*
|
*
|
||||||
@@ -436,17 +529,17 @@ WeatherProvider.register("envcanada", {
|
|||||||
* then it will be displayed ONLY if no POP is present.
|
* 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
|
* 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
|
* people are more interested in seeing. While EC provides a separate POP for daytime and nighttime portions
|
||||||
* of each day, the weather module does not really allow for that view of a daily forecast. There we will
|
* 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
|
* ignore any nighttime 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
|
* the nighttime forecast after a certain point in the afternoon. As such, we will be showing the nighttime POP
|
||||||
* (if one exists) in that specific scenario.
|
* (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
|
* 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
|
* people are interested in seeing. While EC provides a separate accumulation for daytime and nighttime portions
|
||||||
* of each day, the weather module does not really allow for that view of a daily forecast. There we will
|
* 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
|
* ignore any nighttime 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.
|
* the nighttime forecast after a certain point in that specific scenario.
|
||||||
*/
|
*/
|
||||||
setPrecipitation (weather, foreGroup, today) {
|
setPrecipitation (weather, foreGroup, today) {
|
||||||
if (foreGroup[today].querySelector("precipitation accumulation")) {
|
if (foreGroup[today].querySelector("precipitation accumulation")) {
|
||||||
|
@@ -13,7 +13,7 @@ WeatherProvider.register("openmeteo", {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Set the name of the provider.
|
* Set the name of the provider.
|
||||||
* Not strictly required, but helps for debugging.
|
* Not strictly required but helps for debugging.
|
||||||
*/
|
*/
|
||||||
providerName: "Open-Meteo",
|
providerName: "Open-Meteo",
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ WeatherProvider.register("openmeteo", {
|
|||||||
generateWeatherDayFromCurrentWeather (weather) {
|
generateWeatherDayFromCurrentWeather (weather) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Since some units comes from API response "splitted" into daily, hourly and current_weather
|
* Since some units come from API response "splitted" into daily, hourly and current_weather
|
||||||
* every time you request it, you have to ensure to get the data from the right place every time.
|
* every time you request it, you have to ensure to get the data from the right place every time.
|
||||||
* For the current weather case, the response have the following structure (after transposing):
|
* For the current weather case, the response have the following structure (after transposing):
|
||||||
* ```
|
* ```
|
||||||
@@ -381,6 +381,7 @@ WeatherProvider.register("openmeteo", {
|
|||||||
currentWeather.maxTemperature = parseFloat(weather.daily[0].temperature_2m_max);
|
currentWeather.maxTemperature = parseFloat(weather.daily[0].temperature_2m_max);
|
||||||
currentWeather.weatherType = this.convertWeatherType(weather.current_weather.weathercode, currentWeather.isDayTime());
|
currentWeather.weatherType = this.convertWeatherType(weather.current_weather.weathercode, currentWeather.isDayTime());
|
||||||
currentWeather.humidity = parseFloat(weather.hourly[h].relativehumidity_2m);
|
currentWeather.humidity = parseFloat(weather.hourly[h].relativehumidity_2m);
|
||||||
|
currentWeather.feelsLikeTemp = parseFloat(weather.hourly[h].apparent_temperature);
|
||||||
currentWeather.rain = parseFloat(weather.hourly[h].rain);
|
currentWeather.rain = parseFloat(weather.hourly[h].rain);
|
||||||
currentWeather.snow = parseFloat(weather.hourly[h].snowfall * 10);
|
currentWeather.snow = parseFloat(weather.hourly[h].snowfall * 10);
|
||||||
currentWeather.precipitationAmount = parseFloat(weather.hourly[h].precipitation);
|
currentWeather.precipitationAmount = parseFloat(weather.hourly[h].precipitation);
|
||||||
|
@@ -254,7 +254,7 @@ WeatherProvider.register("smhi", {
|
|||||||
* Helper method to get a property from the returned data set.
|
* Helper method to get a property from the returned data set.
|
||||||
* @param {object} currentWeatherData Weatherdata to get from
|
* @param {object} currentWeatherData Weatherdata to get from
|
||||||
* @param {string} name The name of the property
|
* @param {string} name The name of the property
|
||||||
* @returns {*} The value of the property in the weatherdata
|
* @returns {string} The value of the property in the weatherdata
|
||||||
*/
|
*/
|
||||||
paramValue (currentWeatherData, name) {
|
paramValue (currentWeatherData, name) {
|
||||||
return currentWeatherData.parameters.filter((p) => p.name === name).flatMap((p) => p.values)[0];
|
return currentWeatherData.parameters.filter((p) => p.name === name).flatMap((p) => p.values)[0];
|
||||||
|
@@ -218,7 +218,7 @@ WeatherProvider.register("weathergov", {
|
|||||||
currentWeather.minTemperature = currentWeatherData.minTemperatureLast24Hours.value;
|
currentWeather.minTemperature = currentWeatherData.minTemperatureLast24Hours.value;
|
||||||
currentWeather.maxTemperature = currentWeatherData.maxTemperatureLast24Hours.value;
|
currentWeather.maxTemperature = currentWeatherData.maxTemperatureLast24Hours.value;
|
||||||
currentWeather.humidity = Math.round(currentWeatherData.relativeHumidity.value);
|
currentWeather.humidity = Math.round(currentWeatherData.relativeHumidity.value);
|
||||||
currentWeather.precipitationAmount = currentWeatherData.precipitationLastHour.value ? currentWeatherData.precipitationLastHour.value : currentWeatherData.precipitationLast3Hours.value;
|
currentWeather.precipitationAmount = currentWeatherData.precipitationLastHour?.value ?? currentWeatherData.precipitationLast3Hours?.value;
|
||||||
if (currentWeatherData.heatIndex.value !== null) {
|
if (currentWeatherData.heatIndex.value !== null) {
|
||||||
currentWeather.feelsLikeTemp = currentWeatherData.heatIndex.value;
|
currentWeather.feelsLikeTemp = currentWeatherData.heatIndex.value;
|
||||||
} else if (currentWeatherData.windChill.value !== null) {
|
} else if (currentWeatherData.windChill.value !== null) {
|
||||||
|
@@ -1,9 +1,6 @@
|
|||||||
.weather .weathericon,
|
.weather .weathericon,
|
||||||
.weather .fa-home {
|
.weather .fa-home {
|
||||||
font-size: 75%;
|
font-size: 75%;
|
||||||
line-height: 65px;
|
|
||||||
display: inline-block;
|
|
||||||
transform: translate(0, -3px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.weather .humidity-icon {
|
.weather .humidity-icon {
|
||||||
@@ -37,10 +34,6 @@
|
|||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.weather tr .weathericon {
|
|
||||||
line-height: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.weather tr.colored .min-temp {
|
.weather tr.colored .min-temp {
|
||||||
color: #bcddff;
|
color: #bcddff;
|
||||||
}
|
}
|
||||||
@@ -48,3 +41,9 @@
|
|||||||
.weather tr.colored .max-temp {
|
.weather tr.colored .max-temp {
|
||||||
color: #ff8e99;
|
color: #ff8e99;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.weather .type-temp {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
@@ -163,11 +163,12 @@ Module.register("weather", {
|
|||||||
// What to do when the weather provider has new information available?
|
// What to do when the weather provider has new information available?
|
||||||
updateAvailable () {
|
updateAvailable () {
|
||||||
Log.log("New weather information available.");
|
Log.log("New weather information available.");
|
||||||
this.updateDom(0);
|
// this value was changed from 0 to 300 to stabilize weather tests:
|
||||||
|
this.updateDom(300);
|
||||||
this.scheduleUpdate();
|
this.scheduleUpdate();
|
||||||
|
|
||||||
if (this.weatherProvider.currentWeather()) {
|
if (this.weatherProvider.currentWeather()) {
|
||||||
this.sendNotification("CURRENTWEATHER_TYPE", { type: this.weatherProvider.currentWeather().weatherType.replace("-", "_") });
|
this.sendNotification("CURRENTWEATHER_TYPE", { type: this.weatherProvider.currentWeather().weatherType?.replace("-", "_") });
|
||||||
}
|
}
|
||||||
|
|
||||||
const notificationPayload = {
|
const notificationPayload = {
|
||||||
|
@@ -53,7 +53,7 @@ const WeatherUtils = {
|
|||||||
/**
|
/**
|
||||||
* Convert temp (from degrees C) into imperial or metric unit depending on
|
* Convert temp (from degrees C) into imperial or metric unit depending on
|
||||||
* your config
|
* your config
|
||||||
* @param {number} tempInC the temperature in celsius you want to convert
|
* @param {number} tempInC the temperature in Celsius you want to convert
|
||||||
* @param {string} unit can be 'imperial' or 'metric'
|
* @param {string} unit can be 'imperial' or 'metric'
|
||||||
* @returns {number} the converted temperature
|
* @returns {number} the converted temperature
|
||||||
*/
|
*/
|
||||||
@@ -61,6 +61,15 @@ const WeatherUtils = {
|
|||||||
return unit === "imperial" ? tempInC * 1.8 + 32 : tempInC;
|
return unit === "imperial" ? tempInC * 1.8 + 32 : tempInC;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert temp (from degrees C) into metric unit
|
||||||
|
* @param {number} tempInF the temperature in Fahrenheit you want to convert
|
||||||
|
* @returns {number} the converted temperature
|
||||||
|
*/
|
||||||
|
convertTempToMetric (tempInF) {
|
||||||
|
return ((tempInF - 32) * 5) / 9;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert wind speed into another unit.
|
* Convert wind speed into another unit.
|
||||||
* @param {number} windInMS the windspeed in meter/sec you want to convert
|
* @param {number} windInMS the windspeed in meter/sec you want to convert
|
||||||
@@ -118,27 +127,51 @@ const WeatherUtils = {
|
|||||||
return kmh * 0.27777777777778;
|
return kmh * 0.27777777777778;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Taken from https://community.home-assistant.io/t/calculating-apparent-feels-like-temperature/370834/18
|
||||||
|
* @param {number} temperature temperature in degrees Celsius
|
||||||
|
* @param {number} windSpeed wind speed in meter/second
|
||||||
|
* @param {number} humidity relative humidity in percent
|
||||||
|
* @returns {number} the feels like temperature in degrees Celsius
|
||||||
|
*/
|
||||||
calculateFeelsLike (temperature, windSpeed, humidity) {
|
calculateFeelsLike (temperature, windSpeed, humidity) {
|
||||||
const windInMph = this.convertWind(windSpeed, "imperial");
|
const windInMph = this.convertWind(windSpeed, "imperial");
|
||||||
const tempInF = this.convertTemp(temperature, "imperial");
|
const tempInF = this.convertTemp(temperature, "imperial");
|
||||||
let feelsLike = tempInF;
|
|
||||||
|
|
||||||
if (windInMph > 3 && tempInF < 50) {
|
let HI;
|
||||||
feelsLike = Math.round(35.74 + 0.6215 * tempInF - 35.75 * Math.pow(windInMph, 0.16) + 0.4275 * tempInF * Math.pow(windInMph, 0.16));
|
let WC = tempInF;
|
||||||
} else if (tempInF > 80 && humidity > 40) {
|
|
||||||
feelsLike
|
// Calculate wind chill for certain conditions
|
||||||
= -42.379
|
if (tempInF <= 70 && windInMph >= 3) {
|
||||||
+ 2.04901523 * tempInF
|
WC = 35.74 + (0.6215 * tempInF) - 35.75 * Math.pow(windInMph, 0.16) + ((0.4275 * tempInF) * Math.pow(windInMph, 0.16));
|
||||||
+ 10.14333127 * humidity
|
|
||||||
- 0.22475541 * tempInF * humidity
|
|
||||||
- 6.83783 * Math.pow(10, -3) * tempInF * tempInF
|
|
||||||
- 5.481717 * Math.pow(10, -2) * humidity * humidity
|
|
||||||
+ 1.22874 * Math.pow(10, -3) * tempInF * tempInF * humidity
|
|
||||||
+ 8.5282 * Math.pow(10, -4) * tempInF * humidity * humidity
|
|
||||||
- 1.99 * Math.pow(10, -6) * tempInF * tempInF * humidity * humidity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ((feelsLike - 32) * 5) / 9;
|
// Steadman Heat Index Vorberechnung
|
||||||
|
const STEADMAN_HI = 0.5 * (tempInF + 61.0 + ((tempInF - 68.0) * 1.2) + (humidity * 0.094));
|
||||||
|
|
||||||
|
if (STEADMAN_HI >= 80) {
|
||||||
|
// Rothfusz-Komplex
|
||||||
|
const ROTHFUSZ_HI = -42.379 + 2.04901523 * tempInF + 10.14333127 * humidity - 0.22475541 * tempInF * humidity - 0.00683783 * tempInF * tempInF - 0.05481717 * humidity * humidity + 0.00122874 * tempInF * tempInF * humidity + 0.00085282 * tempInF * humidity * humidity - 0.00000199 * tempInF * tempInF * humidity * humidity;
|
||||||
|
|
||||||
|
HI = ROTHFUSZ_HI;
|
||||||
|
|
||||||
|
if (humidity < 13 && tempInF > 80 && tempInF < 112) {
|
||||||
|
const ADJUSTMENT = ((13 - humidity) / 4) * Math.pow(Math.abs(17 - (tempInF - 95)), 0.5) / 17; // sqrt Teil
|
||||||
|
HI = HI - ADJUSTMENT;
|
||||||
|
} else if (humidity > 85 && tempInF > 80 && tempInF < 87) {
|
||||||
|
const ADJUSTMENT = ((humidity - 85) / 10) * ((87 - tempInF) / 5);
|
||||||
|
HI = HI + ADJUSTMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else { HI = STEADMAN_HI; }
|
||||||
|
|
||||||
|
// Feuchte Lastberechnung FL
|
||||||
|
let FL;
|
||||||
|
if (tempInF < 50) { FL = WC; }
|
||||||
|
else if (tempInF >= 50 && tempInF < 70) { FL = ((70 - tempInF) / 20) * WC + ((tempInF - 50) / 20) * HI; }
|
||||||
|
else if (tempInF >= 70) { FL = HI; }
|
||||||
|
|
||||||
|
return this.convertTempToMetric(FL);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
3367
package-lock.json
generated
3367
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
58
package.json
58
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "magicmirror",
|
"name": "magicmirror",
|
||||||
"version": "2.32.0",
|
"version": "2.33.0",
|
||||||
"description": "The open source modular smart mirror platform.",
|
"description": "The open source modular smart mirror platform.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"magic mirror",
|
"magic mirror",
|
||||||
@@ -23,16 +23,24 @@
|
|||||||
"https://github.com/MagicMirrorOrg/MagicMirror/graphs/contributors"
|
"https://github.com/MagicMirrorOrg/MagicMirror/graphs/contributors"
|
||||||
],
|
],
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
|
"imports": {
|
||||||
|
"#module_functions": {
|
||||||
|
"default": "./js/module_functions.js"
|
||||||
|
},
|
||||||
|
"#server_functions": {
|
||||||
|
"default": "./js/server_functions.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"main": "js/electron.js",
|
"main": "js/electron.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"config:check": "node js/check_config.js",
|
"config:check": "node js/check_config.js",
|
||||||
|
"postinstall": "git clean -df fonts vendor",
|
||||||
"install-mm": "npm install --no-audit --no-fund --no-update-notifier --only=prod --omit=dev",
|
"install-mm": "npm install --no-audit --no-fund --no-update-notifier --only=prod --omit=dev",
|
||||||
"install-mm:dev": "npm install --no-audit --no-fund --no-update-notifier",
|
"install-mm:dev": "npm install --no-audit --no-fund --no-update-notifier",
|
||||||
"lint:css": "stylelint 'css/main.css' 'css/roboto.css' 'css/font-awesome.css' 'modules/default/**/*.css' --fix",
|
"lint:css": "stylelint 'css/main.css' 'css/roboto.css' 'css/font-awesome.css' 'modules/default/**/*.css' --fix",
|
||||||
"lint:js": "eslint --fix",
|
"lint:js": "eslint --fix",
|
||||||
"lint:markdown": "markdownlint-cli2 . --fix",
|
"lint:markdown": "markdownlint-cli2 . --fix",
|
||||||
"lint:prettier": "prettier . --write",
|
"lint:prettier": "prettier . --write",
|
||||||
"postinstall": "git clean -df fonts vendor",
|
|
||||||
"prepare": "[ -f node_modules/.bin/husky ] && husky || echo no husky installed.",
|
"prepare": "[ -f node_modules/.bin/husky ] && husky || echo no husky installed.",
|
||||||
"server": "node ./serveronly",
|
"server": "node ./serveronly",
|
||||||
"start": "node --run start:x11",
|
"start": "node --run start:x11",
|
||||||
@@ -56,63 +64,63 @@
|
|||||||
"test:unit": "NODE_ENV=test jest --selectProjects unit"
|
"test:unit": "NODE_ENV=test jest --selectProjects unit"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*": "prettier --write",
|
"*": "prettier --ignore-unknown --write",
|
||||||
"*.js": "eslint --fix",
|
"*.js": "eslint --fix",
|
||||||
"*.css": "stylelint --fix"
|
"*.css": "stylelint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/roboto": "^5.2.6",
|
"@fontsource/roboto": "^5.2.8",
|
||||||
"@fontsource/roboto-condensed": "^5.2.6",
|
"@fontsource/roboto-condensed": "^5.2.8",
|
||||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
"@fortawesome/fontawesome-free": "^7.0.1",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"console-stamp": "^3.1.2",
|
"console-stamp": "^3.1.2",
|
||||||
"croner": "^9.1.0",
|
"croner": "^9.1.0",
|
||||||
"envsub": "^4.1.0",
|
"envsub": "^4.1.0",
|
||||||
"eslint": "^9.30.0",
|
"eslint": "^9.36.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-ipfilter": "^1.3.2",
|
"express-ipfilter": "^1.3.2",
|
||||||
"feedme": "^2.0.2",
|
"feedme": "^2.0.2",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.7.0",
|
||||||
"module-alias": "^2.2.3",
|
"module-alias": "^2.2.3",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.6.0",
|
"moment-timezone": "^0.6.0",
|
||||||
"node-ical": "^0.20.1",
|
"node-ical": "^0.21.0",
|
||||||
"nunjucks": "^3.2.4",
|
"nunjucks": "^3.2.4",
|
||||||
"pm2": "^6.0.8",
|
"pm2": "^6.0.13",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"suncalc": "^1.9.0",
|
"suncalc": "^1.9.0",
|
||||||
"systeminformation": "^5.27.7",
|
"systeminformation": "^5.27.10",
|
||||||
"undici": "^7.11.0",
|
"undici": "^7.16.0",
|
||||||
"weathericons": "^2.1.0"
|
"weathericons": "^2.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/eslint-plugin": "^5.1.0",
|
"@stylistic/eslint-plugin": "^5.4.0",
|
||||||
"cspell": "^9.1.2",
|
"cspell": "^9.2.1",
|
||||||
"eslint-plugin-import-x": "^4.16.1",
|
"eslint-plugin-import-x": "^4.16.1",
|
||||||
"eslint-plugin-jest": "^29.0.1",
|
"eslint-plugin-jest": "^29.0.1",
|
||||||
"eslint-plugin-jsdoc": "^51.2.3",
|
"eslint-plugin-jsdoc": "^60.1.1",
|
||||||
"eslint-plugin-package-json": "^0.42.0",
|
"eslint-plugin-package-json": "^0.56.3",
|
||||||
"express-basic-auth": "^1.2.1",
|
"express-basic-auth": "^1.2.1",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jest": "^30.0.3",
|
"jest": "^30.1.3",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^27.0.0",
|
||||||
"lint-staged": "^16.1.2",
|
"lint-staged": "^16.2.0",
|
||||||
"markdownlint-cli2": "^0.18.1",
|
"markdownlint-cli2": "^0.18.1",
|
||||||
"playwright": "^1.53.1",
|
"playwright": "^1.55.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"sinon": "^21.0.0",
|
"prettier-plugin-jinja-template": "^2.1.0",
|
||||||
"stylelint": "^16.21.0",
|
"stylelint": "^16.24.0",
|
||||||
"stylelint-config-standard": "^38.0.0",
|
"stylelint-config-standard": "^39.0.0",
|
||||||
"stylelint-prettier": "^5.0.3"
|
"stylelint-prettier": "^5.0.3"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"electron": "^36.6.0"
|
"electron": "^38.1.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.14.0"
|
"node": ">=22.18.0"
|
||||||
},
|
},
|
||||||
"_moduleAliases": {
|
"_moduleAliases": {
|
||||||
"node_helper": "js/node_helper.js",
|
"node_helper": "js/node_helper.js",
|
||||||
|
@@ -1,10 +1,17 @@
|
|||||||
const config = {
|
const config = {
|
||||||
|
plugins: ["prettier-plugin-jinja-template"],
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: "*.md",
|
files: "*.md",
|
||||||
options: {
|
options: {
|
||||||
parser: "markdown"
|
parser: "markdown"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["*.njk"],
|
||||||
|
options: {
|
||||||
|
parser: "jinja-template"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
trailingComma: "none"
|
trailingComma: "none"
|
||||||
|
18
tests/configs/modules/alert/welcome_false.js
Normal file
18
tests/configs/modules/alert/welcome_false.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
let config = {
|
||||||
|
address: "0.0.0.0",
|
||||||
|
ipWhitelist: [],
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
module: "alert",
|
||||||
|
config: {
|
||||||
|
display_time: 1000000,
|
||||||
|
welcome_message: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||||
|
if (typeof module !== "undefined") {
|
||||||
|
module.exports = config;
|
||||||
|
}
|
18
tests/configs/modules/alert/welcome_string.js
Normal file
18
tests/configs/modules/alert/welcome_string.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
let config = {
|
||||||
|
address: "0.0.0.0",
|
||||||
|
ipWhitelist: [],
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
module: "alert",
|
||||||
|
config: {
|
||||||
|
display_time: 1000000,
|
||||||
|
welcome_message: "Custom welcome message!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||||
|
if (typeof module !== "undefined") {
|
||||||
|
module.exports = config;
|
||||||
|
}
|
@@ -1,43 +1,79 @@
|
|||||||
const helpers = require("./helpers/global-setup");
|
const helpers = require("./helpers/global-setup");
|
||||||
|
|
||||||
|
// Validate Animate.css integration for compliments module using class toggling.
|
||||||
|
// We intentionally ignore computed animation styles (jsdom doesn't simulate real animations).
|
||||||
describe("AnimateCSS integration Test", () => {
|
describe("AnimateCSS integration Test", () => {
|
||||||
// define config file for testing
|
// Config variants under test
|
||||||
let testConfigFile = "tests/configs/modules/compliments/compliments_animateCSS.js";
|
const TEST_CONFIG_ANIM = "tests/configs/modules/compliments/compliments_animateCSS.js";
|
||||||
// define config file to fallback to default: wrong animation name (must return no animation)
|
const TEST_CONFIG_FALLBACK = "tests/configs/modules/compliments/compliments_animateCSS_fallbackToDefault.js"; // invalid animation names
|
||||||
let testConfigFileFallbackToDefault = "tests/configs/modules/compliments/compliments_animateCSS_fallbackToDefault.js";
|
const TEST_CONFIG_INVERTED = "tests/configs/modules/compliments/compliments_animateCSS_invertedAnimationName.js"; // in/out swapped
|
||||||
// define config file with an inverted name animation : in for out and vice versa (must return no animation)
|
const TEST_CONFIG_NONE = "tests/configs/modules/compliments/compliments_anytime.js"; // no animations defined
|
||||||
let testConfigFileInvertedAnimationName = "tests/configs/modules/compliments/compliments_animateCSS_invertedAnimationName.js";
|
|
||||||
// define config file with no animation defined
|
|
||||||
let testConfigByDefault = "tests/configs/modules/compliments/compliments_anytime.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* move similar tests in function doTest
|
* Get the compliments container element (waits until available).
|
||||||
* @param {string} [animationIn] animation in name of AnimateCSS to test.
|
* @returns {Promise<HTMLElement>} compliments root element
|
||||||
* @param {string} [animationOut] animation out name of AnimateCSS to test.
|
|
||||||
* @returns {boolean} result
|
|
||||||
*/
|
*/
|
||||||
const doTest = async (animationIn, animationOut) => {
|
async function getComplimentsElement () {
|
||||||
await helpers.getDocument();
|
await helpers.getDocument();
|
||||||
let elem = await helpers.waitForElement(".compliments");
|
const el = await helpers.waitForElement(".compliments");
|
||||||
expect(elem).not.toBeNull();
|
expect(el).not.toBeNull();
|
||||||
let styles = window.getComputedStyle(elem);
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
if (animationIn && animationIn !== "") {
|
/**
|
||||||
expect(styles._values.get("animation-name")).toBe(animationIn);
|
* Wait for an Animate.css class to appear and persist briefly.
|
||||||
} else {
|
* @param {string} cls Animation class name without leading dot (e.g. animate__flipInX)
|
||||||
expect(styles._values.get("animation-name")).toBeUndefined();
|
* @param {{timeout?: number}} [options] Poll timeout in ms (default 6000)
|
||||||
|
* @returns {Promise<boolean>} true if class detected in time
|
||||||
|
*/
|
||||||
|
async function waitForAnimationClass (cls, { timeout = 6000 } = {}) {
|
||||||
|
const start = Date.now();
|
||||||
|
while (Date.now() - start < timeout) {
|
||||||
|
if (document.querySelector(`.compliments.animate__animated.${cls}`)) {
|
||||||
|
// small stability wait
|
||||||
|
await new Promise((r) => setTimeout(r, 50));
|
||||||
|
if (document.querySelector(`.compliments.animate__animated.${cls}`)) return true;
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
}
|
}
|
||||||
|
throw new Error(`Timeout waiting for class ${cls}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (animationOut && animationOut !== "") {
|
/**
|
||||||
elem = await helpers.waitForElement(`.compliments.animate__animated.animate__${animationOut}`);
|
* Assert that no Animate.css animation class is applied within a time window.
|
||||||
expect(elem).not.toBeNull();
|
* @param {number} [ms] Observation period in ms (default 2000)
|
||||||
styles = window.getComputedStyle(elem);
|
* @returns {Promise<void>}
|
||||||
expect(styles._values.get("animation-name")).toBe(animationOut);
|
*/
|
||||||
} else {
|
async function assertNoAnimationWithin (ms = 2000) {
|
||||||
expect(styles._values.get("animation-name")).toBeUndefined();
|
const start = Date.now();
|
||||||
|
while (Date.now() - start < ms) {
|
||||||
|
if (document.querySelector(".compliments.animate__animated")) {
|
||||||
|
throw new Error("Unexpected animate__animated class present in non-animation scenario");
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run one animation test scenario.
|
||||||
|
* @param {string} [animationIn] Expected animate-in name
|
||||||
|
* @param {string} [animationOut] Expected animate-out name
|
||||||
|
* @returns {Promise<boolean>} true when scenario assertions pass
|
||||||
|
*/
|
||||||
|
async function runAnimationTest (animationIn, animationOut) {
|
||||||
|
await getComplimentsElement();
|
||||||
|
if (!animationIn && !animationOut) {
|
||||||
|
await assertNoAnimationWithin(2000);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (animationIn) await waitForAnimationClass(`animate__${animationIn}`);
|
||||||
|
if (animationOut) {
|
||||||
|
// Wait just beyond one update cycle (updateInterval=2000ms) before expecting animateOut.
|
||||||
|
await new Promise((r) => setTimeout(r, 2100));
|
||||||
|
await waitForAnimationClass(`animate__${animationOut}`);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
}
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await helpers.stopApplication();
|
await helpers.stopApplication();
|
||||||
@@ -45,29 +81,29 @@ describe("AnimateCSS integration Test", () => {
|
|||||||
|
|
||||||
describe("animateIn and animateOut Test", () => {
|
describe("animateIn and animateOut Test", () => {
|
||||||
it("with flipInX and flipOutX animation", async () => {
|
it("with flipInX and flipOutX animation", async () => {
|
||||||
await helpers.startApplication(testConfigFile);
|
await helpers.startApplication(TEST_CONFIG_ANIM);
|
||||||
await expect(doTest("flipInX", "flipOutX")).resolves.toBe(true);
|
await expect(runAnimationTest("flipInX", "flipOutX")).resolves.toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("use animateOut name for animateIn (vice versa) Test", () => {
|
describe("use animateOut name for animateIn (vice versa) Test", () => {
|
||||||
it("without animation", async () => {
|
it("without animation (inverted names)", async () => {
|
||||||
await helpers.startApplication(testConfigFileInvertedAnimationName);
|
await helpers.startApplication(TEST_CONFIG_INVERTED);
|
||||||
await expect(doTest()).resolves.toBe(true);
|
await expect(runAnimationTest()).resolves.toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("false Animation name test", () => {
|
describe("false Animation name test", () => {
|
||||||
it("without animation", async () => {
|
it("without animation (invalid names)", async () => {
|
||||||
await helpers.startApplication(testConfigFileFallbackToDefault);
|
await helpers.startApplication(TEST_CONFIG_FALLBACK);
|
||||||
await expect(doTest()).resolves.toBe(true);
|
await expect(runAnimationTest()).resolves.toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("no Animation defined test", () => {
|
describe("no Animation defined test", () => {
|
||||||
it("without animation", async () => {
|
it("without animation (no config)", async () => {
|
||||||
await helpers.startApplication(testConfigByDefault);
|
await helpers.startApplication(TEST_CONFIG_NONE);
|
||||||
await expect(doTest()).resolves.toBe(true);
|
await expect(runAnimationTest()).resolves.toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -3,7 +3,7 @@ const helpers = require("./helpers/global-setup");
|
|||||||
describe("All font files from roboto.css should be downloadable", () => {
|
describe("All font files from roboto.css should be downloadable", () => {
|
||||||
const fontFiles = [];
|
const fontFiles = [];
|
||||||
// Statements below filters out all 'url' lines in the CSS file
|
// Statements below filters out all 'url' lines in the CSS file
|
||||||
const fileContent = require("node:fs").readFileSync(`${__dirname}/../../css/roboto.css`, "utf8");
|
const fileContent = require("node:fs").readFileSync(`${global.root_path}/css/roboto.css`, "utf8");
|
||||||
const regex = /\burl\(['"]([^'"]+)['"]\)/g;
|
const regex = /\burl\(['"]([^'"]+)['"]\)/g;
|
||||||
let match = regex.exec(fileContent);
|
let match = regex.exec(fileContent);
|
||||||
while (match !== null) {
|
while (match !== null) {
|
||||||
|
@@ -13,10 +13,9 @@ app.use(basicAuth);
|
|||||||
|
|
||||||
// Set available directories
|
// Set available directories
|
||||||
const directories = ["/tests/configs", "/tests/mocks"];
|
const directories = ["/tests/configs", "/tests/mocks"];
|
||||||
const rootPath = path.resolve(`${__dirname}/../../../`);
|
|
||||||
|
|
||||||
for (let directory of directories) {
|
for (let directory of directories) {
|
||||||
app.use(directory, express.static(path.resolve(rootPath + directory)));
|
app.use(directory, express.static(path.resolve(`${global.root_path}/${directory}`)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let server;
|
let server;
|
||||||
|
@@ -1,9 +1,13 @@
|
|||||||
|
const path = require("node:path");
|
||||||
const os = require("node:os");
|
const os = require("node:os");
|
||||||
const fs = require("node:fs");
|
const fs = require("node:fs");
|
||||||
const jsdom = require("jsdom");
|
const jsdom = require("jsdom");
|
||||||
|
|
||||||
const indexFile = `${__dirname}/../../../index.html`;
|
// global absolute root path
|
||||||
const cssFile = `${__dirname}/../../../css/custom.css`;
|
global.root_path = path.resolve(`${__dirname}/../../../`);
|
||||||
|
|
||||||
|
const indexFile = `${global.root_path}/index.html`;
|
||||||
|
const cssFile = `${global.root_path}/css/custom.css`;
|
||||||
const sampleCss = [
|
const sampleCss = [
|
||||||
".region.row3 {",
|
".region.row3 {",
|
||||||
" top: 0;",
|
" top: 0;",
|
||||||
@@ -29,12 +33,12 @@ exports.startApplication = async (configFilename, exec) => {
|
|||||||
process.env.mmTestMode = "true";
|
process.env.mmTestMode = "true";
|
||||||
process.setMaxListeners(0);
|
process.setMaxListeners(0);
|
||||||
if (exec) exec;
|
if (exec) exec;
|
||||||
global.app = require("../../../js/app");
|
global.app = require(`${global.root_path}/js/app`);
|
||||||
|
|
||||||
return global.app.start();
|
return global.app.start();
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.stopApplication = async (waitTime = 1000) => {
|
exports.stopApplication = async (waitTime = 10) => {
|
||||||
if (global.window) {
|
if (global.window) {
|
||||||
// no closing causes jest errors and memory leaks
|
// no closing causes jest errors and memory leaks
|
||||||
global.window.close();
|
global.window.close();
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
const { injectMockData } = require("../../utils/weather_mocker");
|
const { injectMockData, cleanupMockData } = require("../../utils/weather_mocker");
|
||||||
const helpers = require("./global-setup");
|
const helpers = require("./global-setup");
|
||||||
|
|
||||||
exports.getText = async (element, result) => {
|
exports.getText = async (element, result) => {
|
||||||
@@ -13,7 +13,12 @@ exports.getText = async (element, result) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.startApp = async (configFileName, additionalMockData) => {
|
exports.startApplication = async (configFileName, additionalMockData) => {
|
||||||
await helpers.startApplication(injectMockData(configFileName, additionalMockData));
|
await helpers.startApplication(injectMockData(configFileName, additionalMockData));
|
||||||
await helpers.getDocument();
|
await helpers.getDocument();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.stopApplication = async () => {
|
||||||
|
await helpers.stopApplication();
|
||||||
|
cleanupMockData();
|
||||||
|
};
|
||||||
|
@@ -1,17 +1,52 @@
|
|||||||
const helpers = require("../helpers/global-setup");
|
const helpers = require("../helpers/global-setup");
|
||||||
|
|
||||||
describe("Alert module", () => {
|
describe("Alert module", () => {
|
||||||
beforeAll(async () => {
|
|
||||||
await helpers.startApplication("tests/configs/modules/alert/default.js");
|
|
||||||
await helpers.getDocument();
|
|
||||||
});
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await helpers.stopApplication();
|
await helpers.stopApplication();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the welcome message", async () => {
|
describe("with welcome_message set to false", () => {
|
||||||
const elem = await helpers.waitForElement(".ns-box .ns-box-inner .light.bright.small");
|
beforeAll(async () => {
|
||||||
expect(elem).not.toBeNull();
|
await helpers.startApplication("tests/configs/modules/alert/welcome_false.js");
|
||||||
expect(elem.textContent).toContain("Welcome, start was successful!");
|
await helpers.getDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not show any welcome message", async () => {
|
||||||
|
// Wait a bit to ensure no message appears
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// Check that no alert/notification elements are present
|
||||||
|
const alertElements = document.querySelectorAll(".ns-box .ns-box-inner .light.bright.small");
|
||||||
|
expect(alertElements).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("with welcome_message set to true", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await helpers.startApplication("tests/configs/modules/alert/welcome_true.js");
|
||||||
|
await helpers.getDocument();
|
||||||
|
|
||||||
|
// Wait for the application to initialize
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the translated welcome message", async () => {
|
||||||
|
const elem = await helpers.waitForElement(".ns-box .ns-box-inner .light.bright.small");
|
||||||
|
expect(elem).not.toBeNull();
|
||||||
|
expect(elem.textContent).toContain("Welcome, start was successful!");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("with welcome_message set to custom string", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await helpers.startApplication("tests/configs/modules/alert/welcome_string.js");
|
||||||
|
await helpers.getDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the custom welcome message", async () => {
|
||||||
|
const elem = await helpers.waitForElement(".ns-box .ns-box-inner .light.bright.small");
|
||||||
|
expect(elem).not.toBeNull();
|
||||||
|
expect(elem.textContent).toContain("Custom welcome message!");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -42,7 +42,7 @@ describe("Calendar module", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should show the default calendar symbol in each event", async () => {
|
it("should show the default calendar symbol in each event", async () => {
|
||||||
await expect(testElementLength(".calendar .event .fa-calendar-alt", 0, "not")).resolves.toBe(true);
|
await expect(testElementLength(".calendar .event .fa-calendar-days", 0, "not")).resolves.toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -83,8 +83,7 @@ describe("Newsfeed module", () => {
|
|||||||
|
|
||||||
describe("Newsfeed module located in config directory", () => {
|
describe("Newsfeed module located in config directory", () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
const baseDir = `${__dirname}/../../..`;
|
fs.cpSync(`${global.root_path}/modules/default/newsfeed`, `${global.root_path}/config/newsfeed`, { recursive: true });
|
||||||
fs.cpSync(`${baseDir}/modules/default/newsfeed`, `${baseDir}/config/newsfeed`, { recursive: true });
|
|
||||||
process.env.MM_MODULES_DIR = "config";
|
process.env.MM_MODULES_DIR = "config";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,17 +1,15 @@
|
|||||||
const helpers = require("../helpers/global-setup");
|
const helpers = require("../helpers/global-setup");
|
||||||
const weatherFunc = require("../helpers/weather-functions");
|
const weatherFunc = require("../helpers/weather-functions");
|
||||||
const { cleanupMockData } = require("../../utils/weather_mocker");
|
|
||||||
|
|
||||||
describe("Weather module", () => {
|
describe("Weather module", () => {
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await helpers.stopApplication();
|
await weatherFunc.stopApplication();
|
||||||
await cleanupMockData();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Current weather", () => {
|
describe("Current weather", () => {
|
||||||
describe("Default configuration", () => {
|
describe("Default configuration", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await weatherFunc.startApp("tests/configs/modules/weather/currentweather_default.js", {});
|
await weatherFunc.startApplication("tests/configs/modules/weather/currentweather_default.js", {});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render wind speed and wind direction", async () => {
|
it("should render wind speed and wind direction", async () => {
|
||||||
@@ -20,12 +18,16 @@ describe("Weather module", () => {
|
|||||||
|
|
||||||
it("should render temperature with icon", async () => {
|
it("should render temperature with icon", async () => {
|
||||||
await expect(weatherFunc.getText(".weather .large span.light.bright", "1.5°")).resolves.toBe(true);
|
await expect(weatherFunc.getText(".weather .large span.light.bright", "1.5°")).resolves.toBe(true);
|
||||||
|
|
||||||
|
const elem = await helpers.waitForElement(".weather .large span.weathericon");
|
||||||
|
expect(elem).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render feels like temperature", async () => {
|
it("should render feels like temperature", async () => {
|
||||||
// Template contains which renders as \xa0
|
// Template contains which renders as \xa0
|
||||||
await expect(weatherFunc.getText(".weather .normal.medium.feelslike span.dimmed", "93.7\xa0 Feels like -5.6°")).resolves.toBe(true);
|
await expect(weatherFunc.getText(".weather .normal.medium.feelslike span.dimmed", "93.7\xa0 Feels like -5.6°")).resolves.toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render humidity next to feels-like", async () => {
|
it("should render humidity next to feels-like", async () => {
|
||||||
await expect(weatherFunc.getText(".weather .normal.medium.feelslike span.dimmed .humidity", "93.7")).resolves.toBe(true);
|
await expect(weatherFunc.getText(".weather .normal.medium.feelslike span.dimmed .humidity", "93.7")).resolves.toBe(true);
|
||||||
});
|
});
|
||||||
@@ -34,7 +36,7 @@ describe("Weather module", () => {
|
|||||||
|
|
||||||
describe("Compliments Integration", () => {
|
describe("Compliments Integration", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await weatherFunc.startApp("tests/configs/modules/weather/currentweather_compliments.js", {});
|
await weatherFunc.startApplication("tests/configs/modules/weather/currentweather_compliments.js", {});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render a compliment based on the current weather", async () => {
|
it("should render a compliment based on the current weather", async () => {
|
||||||
@@ -44,7 +46,7 @@ describe("Weather module", () => {
|
|||||||
|
|
||||||
describe("Configuration Options", () => {
|
describe("Configuration Options", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await weatherFunc.startApp("tests/configs/modules/weather/currentweather_options.js", {});
|
await weatherFunc.startApplication("tests/configs/modules/weather/currentweather_options.js", {});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render windUnits in beaufort", async () => {
|
it("should render windUnits in beaufort", async () => {
|
||||||
@@ -72,7 +74,7 @@ describe("Weather module", () => {
|
|||||||
|
|
||||||
describe("Current weather with imperial units", () => {
|
describe("Current weather with imperial units", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await weatherFunc.startApp("tests/configs/modules/weather/currentweather_units.js", {});
|
await weatherFunc.startApplication("tests/configs/modules/weather/currentweather_units.js", {});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render wind in imperial units", async () => {
|
it("should render wind in imperial units", async () => {
|
||||||
|
@@ -1,16 +1,14 @@
|
|||||||
const helpers = require("../helpers/global-setup");
|
const helpers = require("../helpers/global-setup");
|
||||||
const weatherFunc = require("../helpers/weather-functions");
|
const weatherFunc = require("../helpers/weather-functions");
|
||||||
const { cleanupMockData } = require("../../utils/weather_mocker");
|
|
||||||
|
|
||||||
describe("Weather module: Weather Forecast", () => {
|
describe("Weather module: Weather Forecast", () => {
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await helpers.stopApplication();
|
await weatherFunc.stopApplication();
|
||||||
await cleanupMockData();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Default configuration", () => {
|
describe("Default configuration", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await weatherFunc.startApp("tests/configs/modules/weather/forecastweather_default.js", {});
|
await weatherFunc.startApplication("tests/configs/modules/weather/forecastweather_default.js", {});
|
||||||
});
|
});
|
||||||
|
|
||||||
const days = ["Today", "Tomorrow", "Sun", "Mon", "Tue"];
|
const days = ["Today", "Tomorrow", "Sun", "Mon", "Tue"];
|
||||||
@@ -54,7 +52,7 @@ describe("Weather module: Weather Forecast", () => {
|
|||||||
|
|
||||||
describe("Absolute configuration", () => {
|
describe("Absolute configuration", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await weatherFunc.startApp("tests/configs/modules/weather/forecastweather_absolute.js", {});
|
await weatherFunc.startApplication("tests/configs/modules/weather/forecastweather_absolute.js", {});
|
||||||
});
|
});
|
||||||
|
|
||||||
const days = ["Fri", "Sat", "Sun", "Mon", "Tue"];
|
const days = ["Fri", "Sat", "Sun", "Mon", "Tue"];
|
||||||
@@ -67,7 +65,7 @@ describe("Weather module: Weather Forecast", () => {
|
|||||||
|
|
||||||
describe("Configuration Options", () => {
|
describe("Configuration Options", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await weatherFunc.startApp("tests/configs/modules/weather/forecastweather_options.js", {});
|
await weatherFunc.startApplication("tests/configs/modules/weather/forecastweather_options.js", {});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render custom table class", async () => {
|
it("should render custom table class", async () => {
|
||||||
@@ -94,7 +92,7 @@ describe("Weather module: Weather Forecast", () => {
|
|||||||
|
|
||||||
describe("Forecast weather with imperial units", () => {
|
describe("Forecast weather with imperial units", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await weatherFunc.startApp("tests/configs/modules/weather/forecastweather_units.js", {});
|
await weatherFunc.startApplication("tests/configs/modules/weather/forecastweather_units.js", {});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Temperature units", () => {
|
describe("Temperature units", () => {
|
||||||
|
@@ -1,16 +1,13 @@
|
|||||||
const helpers = require("../helpers/global-setup");
|
|
||||||
const weatherFunc = require("../helpers/weather-functions");
|
const weatherFunc = require("../helpers/weather-functions");
|
||||||
const { cleanupMockData } = require("../../utils/weather_mocker");
|
|
||||||
|
|
||||||
describe("Weather module: Weather Hourly Forecast", () => {
|
describe("Weather module: Weather Hourly Forecast", () => {
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await helpers.stopApplication();
|
await weatherFunc.stopApplication();
|
||||||
await cleanupMockData();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Default configuration", () => {
|
describe("Default configuration", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await weatherFunc.startApp("tests/configs/modules/weather/hourlyweather_default.js", {});
|
await weatherFunc.startApplication("tests/configs/modules/weather/hourlyweather_default.js", {});
|
||||||
});
|
});
|
||||||
|
|
||||||
const minTemps = ["7:00 pm", "8:00 pm", "9:00 pm", "10:00 pm", "11:00 pm"];
|
const minTemps = ["7:00 pm", "8:00 pm", "9:00 pm", "10:00 pm", "11:00 pm"];
|
||||||
@@ -23,7 +20,7 @@ describe("Weather module: Weather Hourly Forecast", () => {
|
|||||||
|
|
||||||
describe("Hourly weather options", () => {
|
describe("Hourly weather options", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await weatherFunc.startApp("tests/configs/modules/weather/hourlyweather_options.js", {});
|
await weatherFunc.startApplication("tests/configs/modules/weather/hourlyweather_options.js", {});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Hourly increments of 2", () => {
|
describe("Hourly increments of 2", () => {
|
||||||
@@ -38,7 +35,7 @@ describe("Weather module: Weather Hourly Forecast", () => {
|
|||||||
|
|
||||||
describe("Show precipitations", () => {
|
describe("Show precipitations", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await weatherFunc.startApp("tests/configs/modules/weather/hourlyweather_showPrecipitation.js", {});
|
await weatherFunc.startApplication("tests/configs/modules/weather/hourlyweather_showPrecipitation.js", {});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Shows precipitation amount", () => {
|
describe("Shows precipitation amount", () => {
|
||||||
|
@@ -2,11 +2,17 @@ const delay = (time) => {
|
|||||||
return new Promise((resolve) => setTimeout(resolve, time));
|
return new Promise((resolve) => setTimeout(resolve, time));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const runConfigCheck = async () => {
|
||||||
|
const serverProcess = await require("node:child_process").spawnSync("node", ["--run", "config:check"], { env: process.env });
|
||||||
|
expect(serverProcess.stderr.toString()).toBe("");
|
||||||
|
return await serverProcess.status;
|
||||||
|
};
|
||||||
|
|
||||||
describe("App environment", () => {
|
describe("App environment", () => {
|
||||||
let serverProcess;
|
let serverProcess;
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
process.env.MM_CONFIG_FILE = "tests/configs/default.js";
|
process.env.MM_CONFIG_FILE = "tests/configs/default.js";
|
||||||
serverProcess = await require("node:child_process").spawn("npm", ["run", "server"], { env: process.env, detached: true });
|
serverProcess = await require("node:child_process").spawn("node", ["--run", "server"], { env: process.env, detached: true });
|
||||||
// we have to wait until the server is started
|
// we have to wait until the server is started
|
||||||
await delay(2000);
|
await delay(2000);
|
||||||
});
|
});
|
||||||
@@ -24,3 +30,15 @@ describe("App environment", () => {
|
|||||||
expect(res.status).toBe(404);
|
expect(res.status).toBe(404);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Check config", () => {
|
||||||
|
it("config check should return without errors", async () => {
|
||||||
|
process.env.MM_CONFIG_FILE = "tests/configs/default.js";
|
||||||
|
await expect(runConfigCheck()).resolves.toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("config check should fail with non existent config file", async () => {
|
||||||
|
process.env.MM_CONFIG_FILE = "tests/configs/not_exists.js";
|
||||||
|
await expect(runConfigCheck()).resolves.toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@@ -3,9 +3,26 @@ const path = require("node:path");
|
|||||||
const helmet = require("helmet");
|
const helmet = require("helmet");
|
||||||
const { JSDOM } = require("jsdom");
|
const { JSDOM } = require("jsdom");
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const sinon = require("sinon");
|
|
||||||
const translations = require("../../translations/translations");
|
const translations = require("../../translations/translations");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to create a fresh Translator instance with DOM environment.
|
||||||
|
* @returns {object} Object containing window and Translator
|
||||||
|
*/
|
||||||
|
function createTranslationTestEnvironment () {
|
||||||
|
// Setup DOM environment with Translator
|
||||||
|
const translatorJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "translator.js"), "utf-8");
|
||||||
|
const dom = new JSDOM("", { url: "http://localhost:3000", runScripts: "outside-only" });
|
||||||
|
|
||||||
|
dom.window.Log = { log: jest.fn(), error: jest.fn() };
|
||||||
|
dom.window.translations = translations;
|
||||||
|
dom.window.eval(translatorJs);
|
||||||
|
|
||||||
|
const window = dom.window;
|
||||||
|
|
||||||
|
return { window, Translator: window.Translator };
|
||||||
|
}
|
||||||
|
|
||||||
describe("translations", () => {
|
describe("translations", () => {
|
||||||
let server;
|
let server;
|
||||||
|
|
||||||
@@ -37,91 +54,76 @@ describe("translations", () => {
|
|||||||
let dom;
|
let dom;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Create a new JSDOM instance for each test
|
// Create a new translation test environment for each test
|
||||||
dom = new JSDOM("", { runScripts: "dangerously", resources: "usable" });
|
const env = createTranslationTestEnvironment();
|
||||||
|
const window = env.window;
|
||||||
|
|
||||||
// Mock the necessary global objects
|
// Load class.js and module.js content directly for loadTranslations tests
|
||||||
dom.window.Log = { log: jest.fn(), error: jest.fn() };
|
|
||||||
dom.window.Translator = {};
|
|
||||||
dom.window.config = { language: "de" };
|
|
||||||
|
|
||||||
// Load class.js and module.js content directly
|
|
||||||
const classJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "class.js"), "utf-8");
|
const classJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "class.js"), "utf-8");
|
||||||
const moduleJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "module.js"), "utf-8");
|
const moduleJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "module.js"), "utf-8");
|
||||||
|
|
||||||
// Execute the scripts in the JSDOM context
|
// Execute the scripts in the JSDOM context
|
||||||
dom.window.eval(classJs);
|
window.eval(classJs);
|
||||||
dom.window.eval(moduleJs);
|
window.eval(moduleJs);
|
||||||
|
|
||||||
|
// Additional setup for loadTranslations tests
|
||||||
|
window.config = { language: "de" };
|
||||||
|
|
||||||
|
dom = { window };
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should load translation file", async () => {
|
it("should load translation file", async () => {
|
||||||
await new Promise((resolve) => {
|
|
||||||
dom.window.onload = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { Translator, Module, config } = dom.window;
|
const { Translator, Module, config } = dom.window;
|
||||||
config.language = "en";
|
config.language = "en";
|
||||||
Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
|
Translator.load = jest.fn().mockImplementation((_m, _f, _fb) => null);
|
||||||
|
|
||||||
Module.register("name", { getTranslations: () => translations });
|
Module.register("name", { getTranslations: () => translations });
|
||||||
const MMM = Module.create("name");
|
const MMM = Module.create("name");
|
||||||
|
|
||||||
await MMM.loadTranslations();
|
await MMM.loadTranslations();
|
||||||
|
|
||||||
expect(Translator.load.args).toHaveLength(1);
|
expect(Translator.load.mock.calls).toHaveLength(1);
|
||||||
expect(Translator.load.calledWith(MMM, "translations/en.json", false)).toBe(true);
|
expect(Translator.load).toHaveBeenCalledWith(MMM, "translations/en.json", false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should load translation + fallback file", async () => {
|
it("should load translation + fallback file", async () => {
|
||||||
await new Promise((resolve) => {
|
|
||||||
dom.window.onload = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { Translator, Module } = dom.window;
|
const { Translator, Module } = dom.window;
|
||||||
Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
|
Translator.load = jest.fn().mockImplementation((_m, _f, _fb) => null);
|
||||||
|
|
||||||
Module.register("name", { getTranslations: () => translations });
|
Module.register("name", { getTranslations: () => translations });
|
||||||
const MMM = Module.create("name");
|
const MMM = Module.create("name");
|
||||||
|
|
||||||
await MMM.loadTranslations();
|
await MMM.loadTranslations();
|
||||||
|
|
||||||
expect(Translator.load.args).toHaveLength(2);
|
expect(Translator.load.mock.calls).toHaveLength(2);
|
||||||
expect(Translator.load.calledWith(MMM, "translations/de.json", false)).toBe(true);
|
expect(Translator.load).toHaveBeenCalledWith(MMM, "translations/de.json", false);
|
||||||
expect(Translator.load.calledWith(MMM, "translations/en.json", true)).toBe(true);
|
expect(Translator.load).toHaveBeenCalledWith(MMM, "translations/en.json", true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should load translation fallback file", async () => {
|
it("should load translation fallback file", async () => {
|
||||||
await new Promise((resolve) => {
|
|
||||||
dom.window.onload = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { Translator, Module, config } = dom.window;
|
const { Translator, Module, config } = dom.window;
|
||||||
config.language = "--";
|
config.language = "--";
|
||||||
Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
|
Translator.load = jest.fn().mockImplementation((_m, _f, _fb) => null);
|
||||||
|
|
||||||
Module.register("name", { getTranslations: () => translations });
|
Module.register("name", { getTranslations: () => translations });
|
||||||
const MMM = Module.create("name");
|
const MMM = Module.create("name");
|
||||||
|
|
||||||
await MMM.loadTranslations();
|
await MMM.loadTranslations();
|
||||||
|
|
||||||
expect(Translator.load.args).toHaveLength(1);
|
expect(Translator.load.mock.calls).toHaveLength(1);
|
||||||
expect(Translator.load.calledWith(MMM, "translations/en.json", true)).toBe(true);
|
expect(Translator.load).toHaveBeenCalledWith(MMM, "translations/en.json", true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should load no file", async () => {
|
it("should load no file", async () => {
|
||||||
await new Promise((resolve) => {
|
|
||||||
dom.window.onload = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { Translator, Module } = dom.window;
|
const { Translator, Module } = dom.window;
|
||||||
Translator.load = sinon.stub();
|
Translator.load = jest.fn();
|
||||||
|
|
||||||
Module.register("name", {});
|
Module.register("name", {});
|
||||||
const MMM = Module.create("name");
|
const MMM = Module.create("name");
|
||||||
|
|
||||||
await MMM.loadTranslations();
|
await MMM.loadTranslations();
|
||||||
|
|
||||||
expect(Translator.load.callCount).toBe(0);
|
expect(Translator.load.mock.calls).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,21 +134,10 @@ describe("translations", () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const translatorJs = fs.readFileSync(path.join(__dirname, "..", "..", "js", "translator.js"), "utf-8");
|
|
||||||
|
|
||||||
describe("parsing language files through the Translator class", () => {
|
describe("parsing language files through the Translator class", () => {
|
||||||
for (const language in translations) {
|
for (const language in translations) {
|
||||||
it(`should parse ${language}`, async () => {
|
it(`should parse ${language}`, async () => {
|
||||||
const dom = new JSDOM("", { runScripts: "dangerously", resources: "usable" });
|
const { Translator } = createTranslationTestEnvironment();
|
||||||
dom.window.Log = { log: jest.fn() };
|
|
||||||
dom.window.translations = translations;
|
|
||||||
dom.window.eval(translatorJs);
|
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
dom.window.onload = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { Translator } = dom.window;
|
|
||||||
await Translator.load(mmm, translations[language], false);
|
await Translator.load(mmm, translations[language], false);
|
||||||
|
|
||||||
expect(typeof Translator.translations[mmm.name]).toBe("object");
|
expect(typeof Translator.translations[mmm.name]).toBe("object");
|
||||||
@@ -177,19 +168,10 @@ describe("translations", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Function to initialize JSDOM and load translations
|
// Function to initialize JSDOM and load translations
|
||||||
const initializeTranslationDOM = (language) => {
|
const initializeTranslationDOM = async (language) => {
|
||||||
const dom = new JSDOM("", { runScripts: "dangerously", resources: "usable" });
|
const { Translator } = createTranslationTestEnvironment();
|
||||||
dom.window.Log = { log: jest.fn() };
|
await Translator.load(mmm, translations[language], false);
|
||||||
dom.window.translations = translations;
|
return Translator.translations[mmm.name];
|
||||||
dom.window.eval(translatorJs);
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
dom.window.onload = async () => {
|
|
||||||
const { Translator } = dom.window;
|
|
||||||
await Translator.load(mmm, translations[language], false);
|
|
||||||
resolve(Translator.translations[mmm.name]);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
@@ -9,7 +9,7 @@ describe("Vendors", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Get list vendors", () => {
|
describe("Get list vendors", () => {
|
||||||
const vendors = require(`${__dirname}/../../js/vendor.js`);
|
const vendors = require(`${global.root_path}/js/vendor.js`);
|
||||||
|
|
||||||
Object.keys(vendors).forEach((vendor) => {
|
Object.keys(vendors).forEach((vendor) => {
|
||||||
it(`should return 200 HTTP code for vendor "${vendor}"`, async () => {
|
it(`should return 200 HTTP code for vendor "${vendor}"`, async () => {
|
||||||
|
@@ -2,29 +2,38 @@ const helpers = require("../helpers/global-setup");
|
|||||||
const weatherHelper = require("../helpers/weather-setup");
|
const weatherHelper = require("../helpers/weather-setup");
|
||||||
const { cleanupMockData } = require("../../utils/weather_mocker");
|
const { cleanupMockData } = require("../../utils/weather_mocker");
|
||||||
|
|
||||||
|
const CURRENT_WEATHER_CONFIG = "tests/configs/modules/weather/currentweather_default.js";
|
||||||
|
const SUNRISE_DATE = "13 Jan 2019 00:30:00 GMT";
|
||||||
|
const SUNSET_DATE = "13 Jan 2019 12:30:00 GMT";
|
||||||
|
const SUN_EVENT_SELECTOR = ".weather .normal.medium span:nth-child(4)";
|
||||||
|
const EXPECTED_SUNRISE_TEXT = "7:00 am";
|
||||||
|
const EXPECTED_SUNSET_TEXT = "3:45 pm";
|
||||||
|
|
||||||
describe("Weather module", () => {
|
describe("Weather module", () => {
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await helpers.stopApplication();
|
await helpers.stopApplication();
|
||||||
await cleanupMockData();
|
cleanupMockData();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Current weather with sunrise", () => {
|
describe("Current weather with sunrise", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await weatherHelper.startApp("tests/configs/modules/weather/currentweather_default.js", "13 Jan 2019 00:30:00 GMT");
|
await weatherHelper.startApp(CURRENT_WEATHER_CONFIG, SUNRISE_DATE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render sunrise", async () => {
|
it("should render sunrise", async () => {
|
||||||
await expect(weatherHelper.getText(".weather .normal.medium span:nth-child(4)", "7:00 am")).resolves.toBe(true);
|
const isSunriseRendered = await weatherHelper.getText(SUN_EVENT_SELECTOR, EXPECTED_SUNRISE_TEXT);
|
||||||
|
expect(isSunriseRendered).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Current weather with sunset", () => {
|
describe("Current weather with sunset", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await weatherHelper.startApp("tests/configs/modules/weather/currentweather_default.js", "13 Jan 2019 12:30:00 GMT");
|
await weatherHelper.startApp(CURRENT_WEATHER_CONFIG, SUNSET_DATE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render sunset", async () => {
|
it("should render sunset", async () => {
|
||||||
await expect(weatherHelper.getText(".weather .normal.medium span:nth-child(4)", "3:45 pm")).resolves.toBe(true);
|
const isSunsetRendered = await weatherHelper.getText(SUN_EVENT_SELECTOR, EXPECTED_SUNSET_TEXT);
|
||||||
|
expect(isSunsetRendered).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -4,11 +4,23 @@ const helmet = require("helmet");
|
|||||||
const { JSDOM } = require("jsdom");
|
const { JSDOM } = require("jsdom");
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to create a fresh Translator instance with DOM environment.
|
||||||
|
* @returns {object} Object containing window and Translator
|
||||||
|
*/
|
||||||
|
function createTranslationTestEnvironment () {
|
||||||
|
const translatorJs = fs.readFileSync(path.join(__dirname, "..", "..", "..", "js", "translator.js"), "utf-8");
|
||||||
|
const dom = new JSDOM("", { url: "http://localhost:3001", runScripts: "outside-only" });
|
||||||
|
|
||||||
|
dom.window.Log = { log: jest.fn(), error: jest.fn() };
|
||||||
|
dom.window.eval(translatorJs);
|
||||||
|
|
||||||
|
return { window: dom.window, Translator: dom.window.Translator };
|
||||||
|
}
|
||||||
|
|
||||||
describe("Translator", () => {
|
describe("Translator", () => {
|
||||||
let server;
|
let server;
|
||||||
const sockets = new Set();
|
const sockets = new Set();
|
||||||
const translatorJsPath = path.join(__dirname, "..", "..", "..", "js", "translator.js");
|
|
||||||
const translatorJsScriptContent = fs.readFileSync(translatorJsPath, "utf8");
|
|
||||||
const translationTestData = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "..", "..", "tests", "mocks", "translation_test.json"), "utf8"));
|
const translationTestData = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "..", "..", "tests", "mocks", "translation_test.json"), "utf8"));
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@@ -20,7 +32,7 @@ describe("Translator", () => {
|
|||||||
});
|
});
|
||||||
app.use("/translations", express.static(path.join(__dirname, "..", "..", "..", "tests", "mocks")));
|
app.use("/translations", express.static(path.join(__dirname, "..", "..", "..", "tests", "mocks")));
|
||||||
|
|
||||||
server = app.listen(3000);
|
server = app.listen(3001);
|
||||||
|
|
||||||
server.on("connection", (socket) => {
|
server.on("connection", (socket) => {
|
||||||
sockets.add(socket);
|
sockets.add(socket);
|
||||||
@@ -81,12 +93,7 @@ describe("Translator", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it("should return custom module translation", async () => {
|
it("should return custom module translation", async () => {
|
||||||
const dom = new JSDOM("", { runScripts: "outside-only" });
|
const { Translator } = createTranslationTestEnvironment();
|
||||||
dom.window.eval(translatorJsScriptContent);
|
|
||||||
|
|
||||||
await new Promise((resolve) => dom.window.onload = resolve);
|
|
||||||
|
|
||||||
const { Translator } = dom.window;
|
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
|
|
||||||
let translation = Translator.translate({ name: "MMM-Module" }, "Hello");
|
let translation = Translator.translate({ name: "MMM-Module" }, "Hello");
|
||||||
@@ -97,12 +104,7 @@ describe("Translator", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return core translation", async () => {
|
it("should return core translation", async () => {
|
||||||
const dom = new JSDOM("", { runScripts: "outside-only" });
|
const { Translator } = createTranslationTestEnvironment();
|
||||||
dom.window.eval(translatorJsScriptContent);
|
|
||||||
|
|
||||||
await new Promise((resolve) => dom.window.onload = resolve);
|
|
||||||
|
|
||||||
const { Translator } = dom.window;
|
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
let translation = Translator.translate({ name: "MMM-Module" }, "FOO");
|
let translation = Translator.translate({ name: "MMM-Module" }, "FOO");
|
||||||
expect(translation).toBe("Foo");
|
expect(translation).toBe("Foo");
|
||||||
@@ -111,48 +113,28 @@ describe("Translator", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return custom module translation fallback", async () => {
|
it("should return custom module translation fallback", async () => {
|
||||||
const dom = new JSDOM("", { runScripts: "outside-only" });
|
const { Translator } = createTranslationTestEnvironment();
|
||||||
dom.window.eval(translatorJsScriptContent);
|
|
||||||
|
|
||||||
await new Promise((resolve) => dom.window.onload = resolve);
|
|
||||||
|
|
||||||
const { Translator } = dom.window;
|
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
const translation = Translator.translate({ name: "MMM-Module" }, "A key");
|
const translation = Translator.translate({ name: "MMM-Module" }, "A key");
|
||||||
expect(translation).toBe("A translation");
|
expect(translation).toBe("A translation");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return core translation fallback", async () => {
|
it("should return core translation fallback", async () => {
|
||||||
const dom = new JSDOM("", { runScripts: "outside-only" });
|
const { Translator } = createTranslationTestEnvironment();
|
||||||
dom.window.eval(translatorJsScriptContent);
|
|
||||||
|
|
||||||
await new Promise((resolve) => dom.window.onload = resolve);
|
|
||||||
|
|
||||||
const { Translator } = dom.window;
|
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
const translation = Translator.translate({ name: "MMM-Module" }, "Fallback");
|
const translation = Translator.translate({ name: "MMM-Module" }, "Fallback");
|
||||||
expect(translation).toBe("core fallback");
|
expect(translation).toBe("core fallback");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return translation with placeholder for missing variables", async () => {
|
it("should return translation with placeholder for missing variables", async () => {
|
||||||
const dom = new JSDOM("", { runScripts: "outside-only" });
|
const { Translator } = createTranslationTestEnvironment();
|
||||||
dom.window.eval(translatorJsScriptContent);
|
|
||||||
|
|
||||||
await new Promise((resolve) => dom.window.onload = resolve);
|
|
||||||
|
|
||||||
const { Translator } = dom.window;
|
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
const translation = Translator.translate({ name: "MMM-Module" }, "Hello {username}");
|
const translation = Translator.translate({ name: "MMM-Module" }, "Hello {username}");
|
||||||
expect(translation).toBe("Hallo {username}");
|
expect(translation).toBe("Hallo {username}");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return key if no translation was found", async () => {
|
it("should return key if no translation was found", async () => {
|
||||||
const dom = new JSDOM("", { runScripts: "outside-only" });
|
const { Translator } = createTranslationTestEnvironment();
|
||||||
dom.window.eval(translatorJsScriptContent);
|
|
||||||
|
|
||||||
await new Promise((resolve) => dom.window.onload = resolve);
|
|
||||||
|
|
||||||
const { Translator } = dom.window;
|
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
const translation = Translator.translate({ name: "MMM-Module" }, "MISSING");
|
const translation = Translator.translate({ name: "MMM-Module" }, "MISSING");
|
||||||
expect(translation).toBe("MISSING");
|
expect(translation).toBe("MISSING");
|
||||||
@@ -163,17 +145,12 @@ describe("Translator", () => {
|
|||||||
const mmm = {
|
const mmm = {
|
||||||
name: "TranslationTest",
|
name: "TranslationTest",
|
||||||
file (file) {
|
file (file) {
|
||||||
return `http://localhost:3000/translations/${file}`;
|
return `http://localhost:3001/translations/${file}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should load translations", async () => {
|
it("should load translations", async () => {
|
||||||
const dom = new JSDOM("", { runScripts: "outside-only" });
|
const { Translator } = createTranslationTestEnvironment();
|
||||||
dom.window.eval(translatorJsScriptContent);
|
|
||||||
dom.window.Log = { log: jest.fn() };
|
|
||||||
await new Promise((resolve) => dom.window.onload = resolve);
|
|
||||||
|
|
||||||
const { Translator } = dom.window;
|
|
||||||
const file = "translation_test.json";
|
const file = "translation_test.json";
|
||||||
|
|
||||||
await Translator.load(mmm, file, false);
|
await Translator.load(mmm, file, false);
|
||||||
@@ -182,30 +159,18 @@ describe("Translator", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should load translation fallbacks", async () => {
|
it("should load translation fallbacks", async () => {
|
||||||
const dom = new JSDOM("", { runScripts: "outside-only" });
|
const { Translator } = createTranslationTestEnvironment();
|
||||||
dom.window.eval(translatorJsScriptContent);
|
|
||||||
|
|
||||||
await new Promise((resolve) => dom.window.onload = resolve);
|
|
||||||
|
|
||||||
const { Translator } = dom.window;
|
|
||||||
const file = "translation_test.json";
|
const file = "translation_test.json";
|
||||||
|
|
||||||
dom.window.Log = { log: jest.fn() };
|
|
||||||
await Translator.load(mmm, file, true);
|
await Translator.load(mmm, file, true);
|
||||||
const json = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "..", "..", "tests", "mocks", file), "utf8"));
|
const json = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "..", "..", "tests", "mocks", file), "utf8"));
|
||||||
expect(Translator.translationsFallback[mmm.name]).toEqual(json);
|
expect(Translator.translationsFallback[mmm.name]).toEqual(json);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not load translations, if module fallback exists", async () => {
|
it("should not load translations, if module fallback exists", async () => {
|
||||||
const dom = new JSDOM("", { runScripts: "outside-only" });
|
const { Translator } = createTranslationTestEnvironment();
|
||||||
dom.window.eval(translatorJsScriptContent);
|
|
||||||
await new Promise((resolve) => dom.window.onload = resolve);
|
|
||||||
|
|
||||||
const { Translator } = dom.window;
|
|
||||||
const file = "translation_test.json";
|
const file = "translation_test.json";
|
||||||
|
|
||||||
|
|
||||||
dom.window.Log = { log: jest.fn() };
|
|
||||||
Translator.translationsFallback[mmm.name] = {
|
Translator.translationsFallback[mmm.name] = {
|
||||||
Hello: "Hallo"
|
Hello: "Hallo"
|
||||||
};
|
};
|
||||||
@@ -220,37 +185,23 @@ describe("Translator", () => {
|
|||||||
|
|
||||||
describe("loadCoreTranslations", () => {
|
describe("loadCoreTranslations", () => {
|
||||||
it("should load core translations and fallback", async () => {
|
it("should load core translations and fallback", async () => {
|
||||||
const dom = new JSDOM("", { runScripts: "outside-only" });
|
const { window, Translator } = createTranslationTestEnvironment();
|
||||||
dom.window.eval(translatorJsScriptContent);
|
window.translations = { en: "http://localhost:3001/translations/translation_test.json" };
|
||||||
dom.window.translations = { en: "http://localhost:3000/translations/translation_test.json" };
|
|
||||||
dom.window.Log = { log: jest.fn() };
|
|
||||||
await new Promise((resolve) => dom.window.onload = resolve);
|
|
||||||
|
|
||||||
const { Translator } = dom.window;
|
|
||||||
await Translator.loadCoreTranslations("en");
|
await Translator.loadCoreTranslations("en");
|
||||||
|
|
||||||
const en = translationTestData;
|
const en = translationTestData;
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
expect(Translator.coreTranslations).toEqual(en);
|
expect(Translator.coreTranslations).toEqual(en);
|
||||||
expect(Translator.coreTranslationsFallback).toEqual(en);
|
expect(Translator.coreTranslationsFallback).toEqual(en);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should load core fallback if language cannot be found", async () => {
|
it("should load core fallback if language cannot be found", async () => {
|
||||||
const dom = new JSDOM("", { runScripts: "outside-only" });
|
const { window, Translator } = createTranslationTestEnvironment();
|
||||||
dom.window.eval(translatorJsScriptContent);
|
window.translations = { en: "http://localhost:3001/translations/translation_test.json" };
|
||||||
dom.window.translations = { en: "http://localhost:3000/translations/translation_test.json" };
|
|
||||||
dom.window.Log = { log: jest.fn() };
|
|
||||||
await new Promise((resolve) => dom.window.onload = resolve);
|
|
||||||
|
|
||||||
const { Translator } = dom.window;
|
|
||||||
await Translator.loadCoreTranslations("MISSINGLANG");
|
await Translator.loadCoreTranslations("MISSINGLANG");
|
||||||
|
|
||||||
const en = translationTestData;
|
const en = translationTestData;
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
expect(Translator.coreTranslations).toEqual({});
|
expect(Translator.coreTranslations).toEqual({});
|
||||||
expect(Translator.coreTranslationsFallback).toEqual(en);
|
expect(Translator.coreTranslationsFallback).toEqual(en);
|
||||||
});
|
});
|
||||||
@@ -258,35 +209,20 @@ describe("Translator", () => {
|
|||||||
|
|
||||||
describe("loadCoreTranslationsFallback", () => {
|
describe("loadCoreTranslationsFallback", () => {
|
||||||
it("should load core translations fallback", async () => {
|
it("should load core translations fallback", async () => {
|
||||||
const dom = new JSDOM("", { runScripts: "outside-only" });
|
const { window, Translator } = createTranslationTestEnvironment();
|
||||||
dom.window.eval(translatorJsScriptContent);
|
window.translations = { en: "http://localhost:3001/translations/translation_test.json" };
|
||||||
dom.window.translations = { en: "http://localhost:3000/translations/translation_test.json" };
|
|
||||||
dom.window.Log = { log: jest.fn() };
|
|
||||||
await new Promise((resolve) => dom.window.onload = resolve);
|
|
||||||
|
|
||||||
const { Translator } = dom.window;
|
|
||||||
await Translator.loadCoreTranslationsFallback();
|
await Translator.loadCoreTranslationsFallback();
|
||||||
|
|
||||||
const en = translationTestData;
|
const en = translationTestData;
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
expect(Translator.coreTranslationsFallback).toEqual(en);
|
expect(Translator.coreTranslationsFallback).toEqual(en);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should load core fallback if language cannot be found", async () => {
|
it("should load core fallback if language cannot be found", async () => {
|
||||||
const dom = new JSDOM("", { runScripts: "outside-only" });
|
const { window, Translator } = createTranslationTestEnvironment();
|
||||||
dom.window.eval(translatorJsScriptContent);
|
window.translations = {};
|
||||||
dom.window.translations = {};
|
|
||||||
dom.window.Log = { log: jest.fn() };
|
|
||||||
|
|
||||||
await new Promise((resolve) => dom.window.onload = resolve);
|
|
||||||
|
|
||||||
const { Translator } = dom.window;
|
|
||||||
await Translator.loadCoreTranslations();
|
await Translator.loadCoreTranslations();
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
expect(Translator.coreTranslationsFallback).toEqual({});
|
expect(Translator.coreTranslationsFallback).toEqual({});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||||
|
|
||||||
exports[`Updatenotification custom module returns status information without hash 1`] = `
|
exports[`Updatenotification custom module returns status information without hash 1`] = `
|
||||||
{
|
{
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
const { cors } = require("../../../js/server_functions");
|
const { expect } = require("playwright/test");
|
||||||
|
const { cors, getUserAgent } = require("#server_functions");
|
||||||
|
|
||||||
describe("server_functions tests", () => {
|
describe("server_functions tests", () => {
|
||||||
describe("The cors method", () => {
|
describe("The cors method", () => {
|
||||||
@@ -142,5 +143,21 @@ describe("server_functions tests", () => {
|
|||||||
expect(corsResponse.set.mock.calls[2][0]).toBe("header2");
|
expect(corsResponse.set.mock.calls[2][0]).toBe("header2");
|
||||||
expect(corsResponse.set.mock.calls[2][1]).toBe("value2");
|
expect(corsResponse.set.mock.calls[2][1]).toBe("value2");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Gets User-Agent from configuration", async () => {
|
||||||
|
config = {};
|
||||||
|
let userAgent;
|
||||||
|
|
||||||
|
userAgent = getUserAgent();
|
||||||
|
expect(userAgent).toContain("Mozilla/5.0 (Node.js ");
|
||||||
|
|
||||||
|
config.userAgent = "Mozilla/5.0 (Foo)";
|
||||||
|
userAgent = getUserAgent();
|
||||||
|
expect(userAgent).toBe("Mozilla/5.0 (Foo)");
|
||||||
|
|
||||||
|
config.userAgent = () => "Mozilla/5.0 (Bar)";
|
||||||
|
userAgent = getUserAgent();
|
||||||
|
expect(userAgent).toBe("Mozilla/5.0 (Bar)");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -4,7 +4,7 @@ const path = require("node:path");
|
|||||||
const root_path = path.join(__dirname, "../../..");
|
const root_path = path.join(__dirname, "../../..");
|
||||||
|
|
||||||
describe("Default modules set in modules/default/defaultmodules.js", () => {
|
describe("Default modules set in modules/default/defaultmodules.js", () => {
|
||||||
const expectedDefaultModules = require("../../../modules/default/defaultmodules");
|
const expectedDefaultModules = require(`${root_path}/modules/default/defaultmodules`);
|
||||||
|
|
||||||
for (const defaultModule of expectedDefaultModules) {
|
for (const defaultModule of expectedDefaultModules) {
|
||||||
it(`contains a folder for modules/default/${defaultModule}"`, () => {
|
it(`contains a folder for modules/default/${defaultModule}"`, () => {
|
||||||
|
@@ -2,7 +2,7 @@ const fs = require("node:fs");
|
|||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
|
|
||||||
const root_path = path.join(__dirname, "../../..");
|
const root_path = path.join(__dirname, "../../..");
|
||||||
const version = require(`${__dirname}/../../../package.json`).version;
|
const version = require(`${root_path}/package.json`).version;
|
||||||
|
|
||||||
describe("'global.root_path' set in js/app.js", () => {
|
describe("'global.root_path' set in js/app.js", () => {
|
||||||
const expectedSubPaths = ["modules", "serveronly", "js", "js/app.js", "js/main.js", "js/electron.js", "config"];
|
const expectedSubPaths = ["modules", "serveronly", "js", "js/app.js", "js/main.js", "js/electron.js", "config"];
|
||||||
|
@@ -10,7 +10,7 @@ describe("Calendar fetcher utils test", () => {
|
|||||||
excludedEvents: [],
|
excludedEvents: [],
|
||||||
includePastEvents: false,
|
includePastEvents: false,
|
||||||
maximumEntries: 10,
|
maximumEntries: 10,
|
||||||
maximumNumberOfDays: 365
|
maximumNumberOfDays: 367
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("filterEvents", () => {
|
describe("filterEvents", () => {
|
||||||
|
@@ -2,10 +2,18 @@ const weather = require("../../../../../modules/default/weather/weatherutils");
|
|||||||
const WeatherUtils = require("../../../../../modules/default/weather/weatherutils");
|
const WeatherUtils = require("../../../../../modules/default/weather/weatherutils");
|
||||||
|
|
||||||
describe("Weather utils tests", () => {
|
describe("Weather utils tests", () => {
|
||||||
describe("windspeed conversion to imperial", () => {
|
describe("temperature conversion to imperial", () => {
|
||||||
|
it("should convert temp correctly from Celsius to Celsius", () => {
|
||||||
|
expect(Math.round(WeatherUtils.convertTemp(10, "metric"))).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
it("should convert temp correctly from Celsius to Fahrenheit", () => {
|
it("should convert temp correctly from Celsius to Fahrenheit", () => {
|
||||||
expect(Math.round(WeatherUtils.convertTemp(10, "imperial"))).toBe(50);
|
expect(Math.round(WeatherUtils.convertTemp(10, "imperial"))).toBe(50);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should convert temp correctly from Fahrenheit to Celsius", () => {
|
||||||
|
expect(Math.round(WeatherUtils.convertTempToMetric(10))).toBe(-12);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("windspeed conversion to beaufort", () => {
|
describe("windspeed conversion to beaufort", () => {
|
||||||
@@ -44,11 +52,11 @@ describe("Weather utils tests", () => {
|
|||||||
|
|
||||||
describe("feelsLike calculation", () => {
|
describe("feelsLike calculation", () => {
|
||||||
it("should return a calculated feelsLike info (negative value)", () => {
|
it("should return a calculated feelsLike info (negative value)", () => {
|
||||||
expect(WeatherUtils.calculateFeelsLike(0, 20, 40)).toBe(-9.444444444444445);
|
expect(WeatherUtils.calculateFeelsLike(0, 20, 40)).toBe(-9.397005931555448);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return a calculated feelsLike info (positive value)", () => {
|
it("should return a calculated feelsLike info (positive value)", () => {
|
||||||
expect(WeatherUtils.calculateFeelsLike(30, 0, 60)).toBe(32.8320322777777);
|
expect(WeatherUtils.calculateFeelsLike(30, 0, 60)).toBe(32.832032277777756);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
const fs = require("node:fs");
|
const fs = require("node:fs");
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const util = require("node:util");
|
const exec = require("node:child_process").execSync;
|
||||||
const exec = util.promisify(require("node:child_process").exec);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} type what data to read, can be "current" "forecast" or "hourly
|
* @param {string} type what data to read, can be "current" "forecast" or "hourly
|
||||||
@@ -45,9 +44,9 @@ const injectMockData = (configFileName, extendedData = {}) => {
|
|||||||
return tempFile;
|
return tempFile;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanupMockData = async () => {
|
const cleanupMockData = () => {
|
||||||
const tempDir = path.resolve(`${__dirname}/../configs`).toString();
|
const tempDir = path.resolve(`${__dirname}/../configs`).toString();
|
||||||
await exec(`find ${tempDir} -type f -name *_temp.js -delete`);
|
exec(`find ${tempDir} -type f -name *_temp.js -delete`);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { injectMockData, cleanupMockData };
|
module.exports = { injectMockData, cleanupMockData };
|
||||||
|
Reference in New Issue
Block a user