mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 17:01:08 -04:00
Release 2.29.0 (#3568)
## [2.29.0] - 2024-10-01 Thanks to: @bugsounet, @dkallen78, @jargordon, @khassel, @KristjanESPERANTO, @MarcLandis, @rejas, @ryan-d-williams, @sdetweil, @skpanagiotis. > ⚠️ This release needs nodejs version `v20` or `v22`, minimum version is `v20.9.0` ### Added - [compliments] Added support for cron type date/time format entries mm hh DD MM dow (minutes/hours/days/months and day of week) see https://crontab.cronhub.io for construction (#3481) - [core] Check config at every start of MagicMirror² (#3450) - [core] Add spelling check (cspell): `npm run test:spelling` and handle spelling issues (#3544) - [core] removed `config.paths.vendor` (could not work because `vendor` is hardcoded in `index.html`), renamed `config.paths.modules` to `config.foreignModulesDir`, added variable `MM_CUSTOMCSS_FILE` which - if set - overrides `config.customCss`, added variable `MM_MODULES_DIR` which - if set - overrides `config.foreignModulesDir`, added test for `MM_MODULES_DIR` (#3530) - [core] elements are now removed from `index.html` when loading script or stylesheet files fails - [core] Added `MODULE_DOM_UPDATED` notification each time the DOM is re-rendered via `updateDom` (#3534) - [tests] added minimal needed node version to tests (currently v20.9.0) to avoid releases with wrong node version info - [tests] Added `node-libgpiod` library to electron-rebuild tests (#3563) ### Removed - [core] removed installer only files (#3492) - [core] removed raspberry object from systeminformation (#3505) - [linter] removed `eslint-plugin-import`, because it doesn't support ESLint v9. We will reenter it later when it does. - [tests] removed `onoff` library from electron-rebuild tests (#3563) ### Updated - [weather] Updated `apiVersion` default from 2.5 to 3.0 (#3424) - [core] Updated dependencies including stylistic-eslint - [core] nail down `node-ical` version to `0.18.0` with exception `allow-ghsas: GHSA-8hc4-vh64-cxmj` in `dep-review.yaml` (which should removed after next `node-ical` update) - [core] Updated SocketIO catch all to new API - [core] Allow custom modules positions by scanning index.html for the defined regions, instead of hard coded (PR #3518 fixes issue #3504) - [core] Detail optimizations in `config_check.js` - [core] Updated minimal needed node version in `package.json` (currently v20.9.0) (#3559) and except for v21 (no security updates) (#3561) - [linter] Switch to ESLint v9 and flat config and replace `eslint-plugin-unicorn` by `@eslint/js` - [core] fix discovering module positions twice after #3450 ### Fixed - Fixed `checks` badge in README.md - [weather] Fixed issue with the UK Met Office provider following a change in their API paths and header info. - [core] add check for node_helper loading for multiple instances of same module (#3502) - [weather] Fixed issue for respecting unit config on broadcasted notifications - [tests] Fixes calendar test by moving it from e2e to electron with fixed date (#3532) - [calendar] fixed sliceMultiDayEvents getting wrong count and displaying incorrect entries, Europe/Berlin (#3542) - [tests] ignore `js/positions.js` when linting (this file is created at runtime) - [calendar] fixed sliceMultiDayEvents showing previous day without config enabled --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Michael Teeuw <michael@xonaymedia.nl> Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ross Younger <crazyscot@gmail.com> Co-authored-by: Veeck <github@veeck.de> Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr> Co-authored-by: jkriegshauser <joshuakr@nvidia.com> Co-authored-by: illimarkangur <116028111+illimarkangur@users.noreply.github.com> Co-authored-by: sam detweiler <sdetweil@gmail.com> Co-authored-by: vppencilsharpener <tim.pray@gmail.com> Co-authored-by: veeck <michael.veeck@nebenan.de> Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com> Co-authored-by: Brian O'Connor <btoconnor@users.noreply.github.com> Co-authored-by: WallysWellies <59727507+WallysWellies@users.noreply.github.com> Co-authored-by: Jason Stieber <jrstieber@gmail.com> Co-authored-by: jargordon <50050429+jargordon@users.noreply.github.com> Co-authored-by: Daniel <32464403+dkallen78@users.noreply.github.com> Co-authored-by: Ryan Williams <65094007+ryan-d-williams@users.noreply.github.com> Co-authored-by: Panagiotis Skias <panagiotis.skias@gmail.com> Co-authored-by: Marc Landis <dirk.rettschlag@gmail.com>
This commit is contained in:
parent
53fc814ff8
commit
94c3c699e8
84 changed files with 3899 additions and 2830 deletions
|
@ -1,2 +0,0 @@
|
|||
modules/*
|
||||
!modules/default/
|
|
@ -1,93 +0,0 @@
|
|||
{
|
||||
"extends": ["eslint:recommended", "plugin:@stylistic/all-extends", "plugin:import/recommended", "plugin:jest/recommended", "plugin:jsdoc/recommended"],
|
||||
"plugins": ["unicorn"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2024": true,
|
||||
"jest/globals": true,
|
||||
"node": true
|
||||
},
|
||||
"globals": {
|
||||
"config": true,
|
||||
"Log": true,
|
||||
"MM": true,
|
||||
"Module": true,
|
||||
"moment": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": "latest",
|
||||
"ecmaFeatures": {
|
||||
"globalReturn": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"eqeqeq": "error",
|
||||
"import/order": "error",
|
||||
"import/extensions": "error",
|
||||
"import/newline-after-import": "error",
|
||||
"jest/consistent-test-it": "warn",
|
||||
"jest/expect-expect": "warn",
|
||||
"jest/no-done-callback": "warn",
|
||||
"jest/prefer-expect-resolves": "warn",
|
||||
"jest/prefer-mock-promise-shorthand": "warn",
|
||||
"jest/prefer-to-be": "warn",
|
||||
"jest/prefer-to-have-length": "warn",
|
||||
"no-param-reassign": "error",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-throw-literal": "error",
|
||||
"no-unused-vars": "off",
|
||||
"no-useless-return": "error",
|
||||
"object-shorthand": ["error", "methods"],
|
||||
"prefer-template": "error",
|
||||
"@stylistic/array-element-newline": ["error", "consistent"],
|
||||
"@stylistic/arrow-parens": ["error", "always"],
|
||||
"@stylistic/brace-style": "off",
|
||||
"@stylistic/comma-dangle": ["error", "never"],
|
||||
"@stylistic/dot-location": ["error", "property"],
|
||||
"@stylistic/function-call-argument-newline": ["error", "consistent"],
|
||||
"@stylistic/function-paren-newline": ["error", "consistent"],
|
||||
"@stylistic/implicit-arrow-linebreak": ["error", "beside"],
|
||||
"@stylistic/max-statements-per-line": ["error", { "max": 2 }],
|
||||
"@stylistic/multiline-ternary": ["error", "always-multiline"],
|
||||
"@stylistic/newline-per-chained-call": ["error", { "ignoreChainWithDepth": 4 }],
|
||||
"@stylistic/no-extra-parens": "off",
|
||||
"@stylistic/no-tabs": "off",
|
||||
"@stylistic/object-curly-spacing": ["error", "always"],
|
||||
"@stylistic/object-property-newline": ["error", { "allowAllPropertiesOnSameLine": true }],
|
||||
"@stylistic/operator-linebreak": ["error", "before"],
|
||||
"@stylistic/padded-blocks": "off",
|
||||
"@stylistic/quote-props": ["error", "as-needed"],
|
||||
"@stylistic/quotes": ["error", "double"],
|
||||
"@stylistic/indent": ["error", "tab"],
|
||||
"@stylistic/semi": ["error", "always"],
|
||||
"@stylistic/space-before-function-paren": ["error", "always"],
|
||||
"@stylistic/spaced-comment": "off",
|
||||
"unicorn/prefer-node-protocol": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["config/config.js*"],
|
||||
"rules": {
|
||||
"@stylistic/comma-dangle": "off",
|
||||
"@stylistic/indent": "off",
|
||||
"@stylistic/no-multi-spaces": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["tests/configs/modules/weather/*.js"],
|
||||
"rules": {
|
||||
"@stylistic/quotes": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"extends": ["plugin:package-json/recommended"],
|
||||
"files": ["package.json"],
|
||||
"parser": "jsonc-eslint-parser",
|
||||
"plugins": ["package-json"],
|
||||
"rules": {
|
||||
"package-json/sort-collections": ["error", ["devDependencies", "dependencies", "peerDependencies", "config"]]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
|
@ -12,7 +12,7 @@ We use prettier for automatic linting of all our files: `npm run lint:prettier`.
|
|||
|
||||
We use [ESLint](https://eslint.org) on our JavaScript files.
|
||||
|
||||
Our ESLint configuration is in our `.eslintrc.json` and `.eslintignore` files.
|
||||
The ESLint configuration is in our `eslint.config.mjs` file.
|
||||
|
||||
To run ESLint, use `npm run lint:js`.
|
||||
|
||||
|
|
2
.github/workflows/automated-tests.yaml
vendored
2
.github/workflows/automated-tests.yaml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
|||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x, 22.x]
|
||||
node-version: [20.9.0, 20.x, 22.x]
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
|
|
|
@ -16,3 +16,5 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
allow-ghsas: GHSA-8hc4-vh64-cxmj
|
|
@ -8,7 +8,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x, 22.x]
|
||||
node-version: [20.9.0, 20.x, 22.x]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
@ -21,8 +21,10 @@ jobs:
|
|||
run: npm run install-mm
|
||||
- name: Install @electron/rebuild
|
||||
run: npm install @electron/rebuild
|
||||
- name: Install node-libgpiod deps
|
||||
run: sudo apt-get install gpiod libgpiod2 libgpiod-dev
|
||||
- name: Install some test library to be rebuilded
|
||||
run: npm install onoff node-pty drivelist
|
||||
run: npm install node-libgpiod node-pty drivelist
|
||||
- name: Run electron-rebuild
|
||||
run: npx electron-rebuild
|
||||
continue-on-error: false
|
|
@ -5,7 +5,7 @@
|
|||
name: "Enforce Pull-Request Rules"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||
|
||||
jobs:
|
||||
|
@ -25,4 +25,4 @@ jobs:
|
|||
echo "Please don't do this. Switch the branch to 'develop'."
|
||||
exit 1
|
||||
env:
|
||||
BASE_BRANCH: ${{ github.base_ref }}
|
||||
BASE_BRANCH: ${{ github.event.pull_request.base.ref }}
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -81,3 +81,6 @@ Temporary Items
|
|||
*.orig
|
||||
*.rej
|
||||
*.bak
|
||||
|
||||
# Ignore positions file (#3518)
|
||||
js/positions.js
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
*.js
|
||||
.eslintignore
|
||||
*.mjs
|
||||
.husky/pre-commit
|
||||
.prettierignore
|
||||
/config
|
||||
|
|
75
CHANGELOG.md
75
CHANGELOG.md
|
@ -5,11 +5,58 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|||
|
||||
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/#donate) With your help we can continue to improve the MagicMirror².
|
||||
|
||||
## [2.29.0] - 2024-10-01
|
||||
|
||||
Thanks to: @bugsounet, @dkallen78, @jargordon, @khassel, @KristjanESPERANTO, @MarcLandis, @rejas, @ryan-d-williams, @sdetweil, @skpanagiotis.
|
||||
|
||||
> ⚠️ This release needs nodejs version `v20` or `v22`, minimum version is `v20.9.0`
|
||||
|
||||
### Added
|
||||
|
||||
- [compliments] Added support for cron type date/time format entries mm hh DD MM dow (minutes/hours/days/months and day of week) see https://crontab.cronhub.io for construction (#3481)
|
||||
- [core] Check config at every start of MagicMirror² (#3450)
|
||||
- [core] Add spelling check (cspell): `npm run test:spelling` and handle spelling issues (#3544)
|
||||
- [core] removed `config.paths.vendor` (could not work because `vendor` is hardcoded in `index.html`), renamed `config.paths.modules` to `config.foreignModulesDir`, added variable `MM_CUSTOMCSS_FILE` which - if set - overrides `config.customCss`, added variable `MM_MODULES_DIR` which - if set - overrides `config.foreignModulesDir`, added test for `MM_MODULES_DIR` (#3530)
|
||||
- [core] elements are now removed from `index.html` when loading script or stylesheet files fails
|
||||
- [core] Added `MODULE_DOM_UPDATED` notification each time the DOM is re-rendered via `updateDom` (#3534)
|
||||
- [tests] added minimal needed node version to tests (currently v20.9.0) to avoid releases with wrong node version info
|
||||
- [tests] Added `node-libgpiod` library to electron-rebuild tests (#3563)
|
||||
|
||||
### Removed
|
||||
|
||||
- [core] removed installer only files (#3492)
|
||||
- [core] removed raspberry object from systeminformation (#3505)
|
||||
- [linter] removed `eslint-plugin-import`, because it doesn't support ESLint v9. We will reenter it later when it does.
|
||||
- [tests] removed `onoff` library from electron-rebuild tests (#3563)
|
||||
|
||||
### Updated
|
||||
|
||||
- [weather] Updated `apiVersion` default from 2.5 to 3.0 (#3424)
|
||||
- [core] Updated dependencies including stylistic-eslint
|
||||
- [core] nail down `node-ical` version to `0.18.0` with exception `allow-ghsas: GHSA-8hc4-vh64-cxmj` in `dep-review.yaml` (which should removed after next `node-ical` update)
|
||||
- [core] Updated SocketIO catch all to new API
|
||||
- [core] Allow custom modules positions by scanning index.html for the defined regions, instead of hard coded (PR #3518 fixes issue #3504)
|
||||
- [core] Detail optimizations in `config_check.js`
|
||||
- [core] Updated minimal needed node version in `package.json` (currently v20.9.0) (#3559) and except for v21 (no security updates) (#3561)
|
||||
- [linter] Switch to ESLint v9 and flat config and replace `eslint-plugin-unicorn` by `@eslint/js`
|
||||
- [core] fix discovering module positions twice after #3450
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `checks` badge in README.md
|
||||
- [weather] Fixed issue with the UK Met Office provider following a change in their API paths and header info.
|
||||
- [core] add check for node_helper loading for multiple instances of same module (#3502)
|
||||
- [weather] Fixed issue for respecting unit config on broadcasted notifications
|
||||
- [tests] Fixes calendar test by moving it from e2e to electron with fixed date (#3532)
|
||||
- [calendar] fixed sliceMultiDayEvents getting wrong count and displaying incorrect entries, Europe/Berlin (#3542)
|
||||
- [tests] ignore `js/positions.js` when linting (this file is created at runtime)
|
||||
- [calendar] fixed sliceMultiDayEvents showing previous day without config enabled
|
||||
|
||||
## [2.28.0] - 2024-07-01
|
||||
|
||||
Thanks to: @btoconnor, @bugsounet, @JasonStieber, @khassel, @kleinmantara and @WallysWellies.
|
||||
|
||||
> ⚠️ This release needs nodejs version >= v20
|
||||
> ⚠️ This release needs nodejs version >= v20.9.0
|
||||
|
||||
### Added
|
||||
|
||||
|
@ -31,7 +78,7 @@ Thanks to: @btoconnor, @bugsounet, @JasonStieber, @khassel, @kleinmantara and @W
|
|||
|
||||
### Fixed
|
||||
|
||||
- [core] Fixed crash possibility if `module: <name>` is not defined and on `postion: <positon>` mistake (#3445)
|
||||
- [core] Fixed crash possibility if `module: <name>` is not defined and on `position: <position>` mistake (#3445)
|
||||
- [weather] Fixed precipitationProbability in forecast for provider openmeteo (#3446)
|
||||
- [weather] Fixed type=daily for provider openmeteo having no data when running after 23:00 (#3449)
|
||||
- [weather] Fixed type=daily for provider openmeteo showing nightly icons in forecast when current time is "nightly" (#3458)
|
||||
|
@ -70,7 +117,7 @@ For more info, please read the following post: [A New Chapter for MagicMirror: T
|
|||
|
||||
### Fixed
|
||||
|
||||
- Correct apibase of weathergov weatherprovider to match documentation (#2926)
|
||||
- Correct apiBase of weathergov weatherProvider to match documentation (#2926)
|
||||
- Worked around several issues in the RRULE library that were causing deleted calender events to still show, some
|
||||
initial and recurring events to not show, and some event times to be off an hour. (#3291)
|
||||
- Skip changelog requirement when running tests for dependency updates (#3320)
|
||||
|
@ -136,8 +183,8 @@ This release also marks the latest release by Michael Teeuw. For more info, plea
|
|||
- Fix issue template (#3167)
|
||||
- Fix #3256 filter out bad results from rrule.between
|
||||
- Fix calendar events sometimes not respecting deleted events (#3250)
|
||||
- Fix electron loadurl locally on Windows when address "0.0.0.0" (#2550)
|
||||
- Fix updatanotification (update_helper.js): catch error if response is not an JSON format (check PM2)
|
||||
- Fix electron loadURL locally on Windows when address "0.0.0.0" (#2550)
|
||||
- Fix updatenotification (update_helper.js): catch error if response is not an JSON format (check PM2)
|
||||
- Fix missing typeof in calendar module
|
||||
- Fix style issues after prettier update
|
||||
- Fix calendar test (#3291) by moving "Exdate check" from e2e to electron to run on a Thursday
|
||||
|
@ -395,7 +442,7 @@ Special thanks to the following contributors: @eouia, @khassel, @kolbyjack, @Kri
|
|||
|
||||
### Added
|
||||
|
||||
- Added a new config option `httpHeaders` used by helmet (see https://helmetjs.github.io/). You can now set own httpHeaders which will override the defaults in `js/defauls.js` which is useful e.g. if you want to embed MagicMirror into annother website (solves #2847).
|
||||
- Added a new config option `httpHeaders` used by helmet (see https://helmetjs.github.io/). You can now set own httpHeaders which will override the defaults in `js/defaults.js` which is useful e.g. if you want to embed MagicMirror into another website (solves #2847).
|
||||
- Show endDate for calendar events when dateHeader is enabled and showEnd is set to true (#2192).
|
||||
- Added the notification emitting from the weather module on information updated.
|
||||
- Use recommended file extension for YAML files (#2864).
|
||||
|
@ -444,7 +491,7 @@ Special thanks to the following contributors: @10bias, @CFenner, @JHWelch, @k1rd
|
|||
- Fix minor console output issue for loading translations (#2814).
|
||||
- Don't adjust startDate for full day events if endDate is in the past.
|
||||
- Fix windspeed conversion error in openweathermap provider. (#2812)
|
||||
- Fix conflicting parms turning off showEnd for full day events. (#2629)
|
||||
- Fix conflicting parameter turning off showEnd for full day events. (#2629)
|
||||
- Fix regression, calendar.maximumEntries not used to filter calendar level entries (#2868)
|
||||
|
||||
## [2.18.0] - 2022-01-01
|
||||
|
@ -476,7 +523,7 @@ Special thanks to the following contributors: @AmpioRosso, @eouia, @fewieden, @j
|
|||
### Fixed
|
||||
|
||||
- Fixed wrong file `kr.json` to `ko.json`. Use language code 'ko' instead of 'kr' for Korean language.
|
||||
- Fixed `feels_like` data from openweathermaps current weather being ignored (#2678).
|
||||
- Fixed `feels_like` data from openweathermap's current weather being ignored (#2678).
|
||||
- Fixed chaotic newsfeed display after network connection loss thanks to @jalibu (#2638).
|
||||
- Fixed incorrect time zone correction of recurring full day events (#2632 and #2634).
|
||||
- Fixed e2e tests by increasing testTimeout.
|
||||
|
@ -513,7 +560,7 @@ Special thanks to the following contributors: @apiontek, @eouia, @jupadin, @khas
|
|||
- Updated github templates.
|
||||
- Actually test all js and css files when lint script is run.
|
||||
- Updated jsdocs and print warnings during testing too.
|
||||
- Updated weathergov provider to try fetching not just current, but also foreacst, when API URLs available.
|
||||
- Updated weathergov provider to try fetching not just current, but also forecast, when API URLs available.
|
||||
- Refactored clock layout.
|
||||
- Refactored methods from weather-providers into weatherobject (isDaytime, updateSunTime).
|
||||
- Use of `logger.js` in jest tests.
|
||||
|
@ -1084,7 +1131,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
|||
- Fixed close dates to be absolute, if no configured in the config.js - module Calendar
|
||||
- Fixed the updatenotification module message about new commits in the repository, so they can be correctly localized in singular and plural form.
|
||||
- Fix for weatherforecast rainfall rounding [#1374](https://github.com/MagicMirrorOrg/MagicMirror/issues/1374)
|
||||
- Fix calendar parsing issue for Midori on RasperryPi Zero w, related to issue #694.
|
||||
- Fix calendar parsing issue for Midori on Raspberry Pi Zero w, related to issue #694.
|
||||
- Fix weather city ID link in sample config
|
||||
- Fixed issue with clientonly not updating with IP address and port provided on command line.
|
||||
|
||||
|
@ -1139,7 +1186,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
|||
- Fixed weatherforecast to use dt_txt field instead of dt to handle timezones better
|
||||
- Newsfeed now remembers to show the description when `"ARTICLE_LESS_DETAILS"` is called if the user wants to always show the description. [#1282](https://github.com/MagicMirrorOrg/MagicMirror/issues/1282)
|
||||
- `clientonly/*.js` is now linted, and one linting error is fixed
|
||||
- Fix issue #1196 by changing underscore to hyphen in locale id, in align with momentjs.
|
||||
- Fix issue #1196 by changing underscore to hyphen in locale id, in align with moment.js.
|
||||
- Fixed issue where heat index and wind chill were reporting incorrect values in Kelvin. [#1263](https://github.com/MagicMirrorOrg/MagicMirror/issues/1263)
|
||||
|
||||
### Updated
|
||||
|
@ -1164,7 +1211,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
|||
- Implement Danger.js to notify contributors when CHANGELOG.md is missing in PR.
|
||||
- Allow scrolling in full page article view of default newsfeed module with gesture events from [MMM-Gestures](https://github.com/thobach/MMM-Gestures)
|
||||
- Changed 'compliments.js' - Updated DOM if remote compliments are loaded instead of waiting one updateInterval to show custom compliments
|
||||
- Automated unit tests utils, deprecated, translator, cloneObject(lockstrings)
|
||||
- Automated unit tests utils, deprecated, translator, cloneObject(lockStrings)
|
||||
- Automated integration tests translations
|
||||
- Add advanced filtering to the excludedEvents configuration of the default calendar module
|
||||
- New currentweather module config option: `showFeelsLike`: Shows how it actually feels like. (wind chill or heat index)
|
||||
|
@ -1268,7 +1315,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
|||
### Fixed
|
||||
|
||||
- Fixed issue with incorrect alignment of analog clock when displayed in the center column of the MM.
|
||||
- Fixed ipWhitelist behaviour to make empty whitelist ([]) allow any and all hosts access to the MM.
|
||||
- Fixed ipWhitelist behavior to make empty whitelist ([]) allow any and all hosts access to the MM.
|
||||
- Fixed issue with calendar module where 'excludedEvents' count towards 'maximumEntries'.
|
||||
- Fixed issue with calendar module where global configuration of maximumEntries was not overridden by calendar specific config (see module doc).
|
||||
- Fixed issue where `this.file(filename)` returns a path with two hashes.
|
||||
|
@ -1373,7 +1420,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
|
|||
- Added multiple calendar icon support.
|
||||
- Added tests for Translations, dev argument, version, dev console.
|
||||
- Added test anytime feature compliments module.
|
||||
- Added test ipwhitelist configuration directive.
|
||||
- Added test ipWhitelist configuration directive.
|
||||
- Added test for calendar module: default, basic-auth, backward compatibility, fail-basic-auth.
|
||||
- Added meta tags to support fullscreen mode on iOS (for server mode)
|
||||
- Added `ignoreOldItems` and `ignoreOlderThan` options to the News Feed module
|
||||
|
|
|
@ -15,24 +15,44 @@ This document describes how collaborators of this repository should work togethe
|
|||
|
||||
## Releases
|
||||
|
||||
Are done by @rejas or @khassel.
|
||||
Are done by
|
||||
|
||||
- [ ] @rejas
|
||||
- [ ] @sdetweil
|
||||
- [ ] @khassel
|
||||
|
||||
### Pre-Deployment steps
|
||||
|
||||
- [ ] update dependencies (a few days before)
|
||||
|
||||
### Deployment steps
|
||||
|
||||
- pull latest `develop` branch
|
||||
- update `package.json` to reflect correct version number
|
||||
- run `npm install` to generate new `package-lock.json`
|
||||
- test `develop` branch
|
||||
- update `CHANGELOG.md` (don't forget to add all contributor names)
|
||||
- commit and push all changes
|
||||
- after successful test run via github actions: create pull request to `master` branch
|
||||
- after PR tests run without issues, merge PR
|
||||
- create new release with corresponding version tag
|
||||
- publish release notes with link to github release on forum in new locked topic
|
||||
- [ ] pull latest `develop` branch
|
||||
- [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0`
|
||||
- [ ] test `develop` branch
|
||||
- [ ] update `CHANGELOG.md`
|
||||
- [ ] add all contributor names: `...`
|
||||
- [ ] add min. node version: > ⚠️ This release needs nodejs version `v20` or `v22`, minimum version is `v20.9.0`
|
||||
- [ ] commit and push all changes
|
||||
- [ ] after successful test run via github actions: create pull request from `develop` to `master` branch
|
||||
- [ ] add label `mastermerge`
|
||||
- [ ] title of the PR is `Release 2.xx.0`
|
||||
- [ ] description of the PR is the section of the `CHANGELOG.md`
|
||||
- [ ] after PR tests run without issues, merge PR
|
||||
- [ ] create new release with
|
||||
- [ ] corresponding version tag `v2.xx.0`
|
||||
- [ ] a release name: `...`
|
||||
- [ ] description of the PR is the section of the `CHANGELOG.md`
|
||||
|
||||
### Draft new development release
|
||||
|
||||
- checkout `develop` branch
|
||||
- update `package.json` to reflect correct version number `2.xx.0-develop`
|
||||
- draft new section in `CHANGELOG.md`
|
||||
- commit and publish `develop` branch
|
||||
- [ ] checkout `develop` branch
|
||||
- [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0-develop`
|
||||
- [ ] draft new section in `CHANGELOG.md`
|
||||
- [ ] commit and publish `develop` branch
|
||||
|
||||
### After release
|
||||
|
||||
- [ ] publish release notes with link to github release on forum in new locked topic
|
||||
- [ ] close all issues with label `ready (coming with next release)`
|
||||
- [ ] release new documentation by merging `develop` on `master` in documentation repository
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
|
||||
</a>
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/magicmirrororg/magicmirror/automated-tests.yaml" alt="GitHub Actions">
|
||||
<img src="https://img.shields.io/github/checks-status/magicmirrororg/magicmirror/master" alt="Build Status">
|
||||
<img src="https://img.shields.io/github/check-runs/magicmirrororg/magicmirror/master" alt="Build Status">
|
||||
<a href="https://github.com/MagicMirrorOrg/MagicMirror">
|
||||
<img src="https://img.shields.io/github/stars/magicmirrororg/magicmirror?style=social">
|
||||
</a>
|
||||
|
@ -41,7 +41,7 @@ For the full contribution guidelines, check out: [https://docs.magicmirror.build
|
|||
|
||||
## Enjoying MagicMirror? Consider a donation!
|
||||
|
||||
MagicMirror² is opensource and free. That doesn't mean we don't need any money.
|
||||
MagicMirror² is Open Source and free. That doesn't mean we don't need any money.
|
||||
|
||||
Please consider a donation to help us cover the ongoing costs like webservers and email services.
|
||||
If we receive enough donations we might even be able to free up some working hours and spend some extra time improving the MagicMirror² core.
|
||||
|
|
227
cspell.config.json
Normal file
227
cspell.config.json
Normal file
|
@ -0,0 +1,227 @@
|
|||
{
|
||||
"version": "0.2",
|
||||
"language": "en",
|
||||
"words": [
|
||||
"aarch",
|
||||
"Alvinger",
|
||||
"Ampio",
|
||||
"andrezibaia",
|
||||
"angeldeejay",
|
||||
"apiontek",
|
||||
"armv",
|
||||
"ashishtank",
|
||||
"autoplay",
|
||||
"beada",
|
||||
"Binney",
|
||||
"bluemanos",
|
||||
"bnitkin",
|
||||
"bokmål",
|
||||
"Brasileiro",
|
||||
"Brento",
|
||||
"browserwindow",
|
||||
"bryanzzhu",
|
||||
"btoconnor",
|
||||
"bugsounet",
|
||||
"buxxi",
|
||||
"byday",
|
||||
"calendarfetcherutils",
|
||||
"calendarutils",
|
||||
"chamakura",
|
||||
"cjbrunner",
|
||||
"clientonly",
|
||||
"clockfaces",
|
||||
"cmdline",
|
||||
"codac",
|
||||
"Crazylegstoo",
|
||||
"crazyscot",
|
||||
"Creepin",
|
||||
"currentweather",
|
||||
"CUSTOMCSS",
|
||||
"customregions",
|
||||
"Cymraeg",
|
||||
"dariom",
|
||||
"darksky",
|
||||
"dateheader",
|
||||
"dateheaders",
|
||||
"davide",
|
||||
"DAYAFTERTOMORROW",
|
||||
"DAYBEFOREYESTERDAY",
|
||||
"defaultmodules",
|
||||
"dgoth",
|
||||
"DTEND",
|
||||
"Duffman",
|
||||
"earlman",
|
||||
"easyas",
|
||||
"eddiehung",
|
||||
"Edgardos",
|
||||
"Ekristoffe",
|
||||
"envcanada",
|
||||
"envsub",
|
||||
"envsubst",
|
||||
"eouia",
|
||||
"exdate",
|
||||
"expectedheaders",
|
||||
"ezeholz",
|
||||
"Faizan",
|
||||
"feedme",
|
||||
"feelslike",
|
||||
"Fenner",
|
||||
"fewieden",
|
||||
"fixuppm",
|
||||
"flopp",
|
||||
"fontawesome",
|
||||
"fontface",
|
||||
"forecastweather",
|
||||
"fortawesome",
|
||||
"frameguard",
|
||||
"Frysk",
|
||||
"fulldate",
|
||||
"fullday",
|
||||
"fullscreen",
|
||||
"Gevoelstemperatuur",
|
||||
"ghsas",
|
||||
"grenagit",
|
||||
"Hirschberger",
|
||||
"hourlyweather",
|
||||
"Hwind",
|
||||
"ical",
|
||||
"illimarkangur",
|
||||
"Ingan",
|
||||
"ipfilter",
|
||||
"ismarslomic",
|
||||
"jakemulley",
|
||||
"jakobsarwary",
|
||||
"jalibu",
|
||||
"jetson",
|
||||
"jkriegshauser",
|
||||
"jsdocs",
|
||||
"jsonlint",
|
||||
"jupadin",
|
||||
"kaennchenstruggle",
|
||||
"kenzal",
|
||||
"Keyport",
|
||||
"khassel",
|
||||
"Kingdon",
|
||||
"kioskmode",
|
||||
"klaernie",
|
||||
"kleinmantara",
|
||||
"Kmph",
|
||||
"Knapoc",
|
||||
"Koepke",
|
||||
"kolbyjack",
|
||||
"krekos",
|
||||
"Kristjan",
|
||||
"krukle",
|
||||
"larryare",
|
||||
"letsencrypt",
|
||||
"Lightspeed",
|
||||
"locationforecast",
|
||||
"lockstring",
|
||||
"lstrip",
|
||||
"Luciella",
|
||||
"luxon",
|
||||
"lxsession",
|
||||
"magicmirror",
|
||||
"martingron",
|
||||
"marvai",
|
||||
"mastermerge",
|
||||
"Meteo",
|
||||
"michaelteeuw",
|
||||
"michmich",
|
||||
"Midori",
|
||||
"mirontoli",
|
||||
"MISSINGLANG",
|
||||
"MMPM",
|
||||
"modernizr",
|
||||
"modulename",
|
||||
"multiday",
|
||||
"Mystara",
|
||||
"Ñandú",
|
||||
"nathannaveen",
|
||||
"naveensrinivasan",
|
||||
"ndom",
|
||||
"Nerfzooka",
|
||||
"NEWSFEED",
|
||||
"newsitems",
|
||||
"nfogal",
|
||||
"njwilliams",
|
||||
"Norsk",
|
||||
"nunjuck",
|
||||
"odroid",
|
||||
"oemel",
|
||||
"onecall",
|
||||
"onevent",
|
||||
"openmeteo",
|
||||
"openweathermap",
|
||||
"oraclesean",
|
||||
"oscarb",
|
||||
"philnagel",
|
||||
"Português",
|
||||
"PRECIP",
|
||||
"Problema",
|
||||
"psieg",
|
||||
"radokristof",
|
||||
"rajniszp",
|
||||
"Reis",
|
||||
"rejas",
|
||||
"Resig",
|
||||
"roboto",
|
||||
"rohitdharavath",
|
||||
"Rosso",
|
||||
"rrule",
|
||||
"sdetweil",
|
||||
"sendheaders",
|
||||
"serveronly",
|
||||
"SMHI",
|
||||
"Snille",
|
||||
"socketclient",
|
||||
"socketio",
|
||||
"spectron",
|
||||
"Starinvest",
|
||||
"sthuber",
|
||||
"Stieber",
|
||||
"stylelintrc",
|
||||
"subclassing",
|
||||
"sunaction",
|
||||
"suncalc",
|
||||
"suntimes",
|
||||
"systeminformation",
|
||||
"tada",
|
||||
"taglist",
|
||||
"Teeuw",
|
||||
"thomasrockhu",
|
||||
"tomzt",
|
||||
"ukmetoffice",
|
||||
"ukmetofficedatahub",
|
||||
"unitless",
|
||||
"unparseable",
|
||||
"updatenotification",
|
||||
"Vaice",
|
||||
"veeck",
|
||||
"VEVENT",
|
||||
"vgtu",
|
||||
"Voelt",
|
||||
"vppencilsharpener",
|
||||
"Wallys",
|
||||
"Weatherbit",
|
||||
"WEATHERDATA",
|
||||
"Weatherflow",
|
||||
"weatherforecast",
|
||||
"weathergov",
|
||||
"weathericons",
|
||||
"weatherobject",
|
||||
"weatherutils",
|
||||
"windspeed",
|
||||
"Woolridge",
|
||||
"worktree",
|
||||
"xlarge",
|
||||
"xrandr",
|
||||
"xsmall",
|
||||
"xwindows",
|
||||
"xxxe",
|
||||
"Ybbet",
|
||||
"yearmatchgroup"
|
||||
],
|
||||
"ignorePaths": ["node_modules/**", "modules/**", "vendor/node_modules/**", "translations/**", "tests/mocks/**", "tests/e2e/modules/clock_es_spec.js", "fonts/roboto.css"],
|
||||
"dictionaries": ["node"]
|
||||
}
|
121
eslint.config.mjs
Normal file
121
eslint.config.mjs
Normal file
|
@ -0,0 +1,121 @@
|
|||
import eslintPluginJest from "eslint-plugin-jest";
|
||||
import eslintPluginJs from "@eslint/js";
|
||||
import eslintPluginStylistic from "@stylistic/eslint-plugin";
|
||||
import globals from "globals";
|
||||
|
||||
const config = [
|
||||
eslintPluginJs.configs.recommended,
|
||||
{
|
||||
files: ["**/*.js"],
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
Log: "readonly",
|
||||
MM: "readonly",
|
||||
Module: "readonly",
|
||||
config: "readonly",
|
||||
moment: "readonly"
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
...eslintPluginStylistic.configs["all-flat"].plugins,
|
||||
...eslintPluginJest.configs["flat/recommended"].plugins
|
||||
},
|
||||
rules: {
|
||||
...eslintPluginStylistic.configs["all-flat"].rules,
|
||||
...eslintPluginJest.configs["flat/recommended"].rules,
|
||||
"@stylistic/array-element-newline": ["error", "consistent"],
|
||||
"@stylistic/arrow-parens": ["error", "always"],
|
||||
"@stylistic/brace-style": "off",
|
||||
"@stylistic/comma-dangle": ["error", "never"],
|
||||
"@stylistic/dot-location": ["error", "property"],
|
||||
"@stylistic/function-call-argument-newline": ["error", "consistent"],
|
||||
"@stylistic/function-paren-newline": ["error", "consistent"],
|
||||
"@stylistic/implicit-arrow-linebreak": ["error", "beside"],
|
||||
"@stylistic/indent": ["error", "tab"],
|
||||
"@stylistic/max-statements-per-line": ["error", {max: 2}],
|
||||
"@stylistic/multiline-comment-style": "off",
|
||||
"@stylistic/multiline-ternary": ["error", "always-multiline"],
|
||||
"@stylistic/newline-per-chained-call": ["error", {ignoreChainWithDepth: 4}],
|
||||
"@stylistic/no-extra-parens": "off",
|
||||
"@stylistic/no-tabs": "off",
|
||||
"@stylistic/object-curly-spacing": ["error", "always"],
|
||||
"@stylistic/object-property-newline": ["error", {allowAllPropertiesOnSameLine: true}],
|
||||
"@stylistic/operator-linebreak": ["error", "before"],
|
||||
"@stylistic/padded-blocks": "off",
|
||||
"@stylistic/quote-props": ["error", "as-needed"],
|
||||
"@stylistic/quotes": ["error", "double"],
|
||||
"@stylistic/semi": ["error", "always"],
|
||||
"@stylistic/space-before-function-paren": ["error", "always"],
|
||||
"@stylistic/spaced-comment": "off",
|
||||
eqeqeq: "error",
|
||||
"id-length": "off",
|
||||
"init-declarations": "off",
|
||||
"jest/consistent-test-it": "warn",
|
||||
"jest/no-done-callback": "warn",
|
||||
"jest/prefer-expect-resolves": "warn",
|
||||
"jest/prefer-mock-promise-shorthand": "warn",
|
||||
"jest/prefer-to-be": "warn",
|
||||
"jest/prefer-to-have-length": "warn",
|
||||
"max-lines-per-function": ["warn", 350],
|
||||
"max-statements": "off",
|
||||
"no-global-assign": "off",
|
||||
"no-inline-comments": "off",
|
||||
"no-magic-numbers": "off",
|
||||
"no-param-reassign": "error",
|
||||
"no-plusplus": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-ternary": "off",
|
||||
"no-throw-literal": "error",
|
||||
"no-undefined": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-useless-return": "error",
|
||||
"no-warning-comments": "off",
|
||||
"object-shorthand": ["error", "methods"],
|
||||
"one-var": "off",
|
||||
"prefer-destructuring": "off",
|
||||
"prefer-template": "error",
|
||||
"sort-keys": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["**/*.mjs"],
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
globals: {
|
||||
...globals.node
|
||||
},
|
||||
sourceType: "module"
|
||||
},
|
||||
plugins: {
|
||||
...eslintPluginStylistic.configs["all-flat"].plugins
|
||||
},
|
||||
rules: {
|
||||
...eslintPluginStylistic.configs["all-flat"].rules,
|
||||
"@stylistic/array-element-newline": "off",
|
||||
"@stylistic/indent": ["error", "tab"],
|
||||
"@stylistic/padded-blocks": ["error", "never"],
|
||||
"@stylistic/quote-props": ["error", "as-needed"],
|
||||
"func-style": "off",
|
||||
"import/namespace": "off",
|
||||
"max-lines-per-function": ["error", 100],
|
||||
"no-magic-numbers": "off",
|
||||
"one-var": "off",
|
||||
"prefer-destructuring": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["tests/configs/modules/weather/*.js"],
|
||||
rules: {
|
||||
"@stylistic/quotes": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: ["config/**", "modules/**", "!modules/default/**", "js/positions.js"]
|
||||
}
|
||||
];
|
||||
|
||||
export default config;
|
18
fonts/package-lock.json
generated
18
fonts/package-lock.json
generated
|
@ -9,19 +9,21 @@
|
|||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@fontsource/roboto-condensed": "^5.0.16"
|
||||
"@fontsource/roboto": "^5.1.0",
|
||||
"@fontsource/roboto-condensed": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/roboto": {
|
||||
"version": "5.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.13.tgz",
|
||||
"integrity": "sha512-j61DHjsdUCKMXSdNLTOxcG701FWnF0jcqNNQi2iPCDxU8seN/sMxeh62dC++UiagCWq9ghTypX+Pcy7kX+QOeQ=="
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.1.0.tgz",
|
||||
"integrity": "sha512-cFRRC1s6RqPygeZ8Uw/acwVHqih8Czjt6Q0MwoUoDe9U3m4dH1HmNDRBZyqlMSFwgNAUKgFImncKdmDHyKpwdg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@fontsource/roboto-condensed": {
|
||||
"version": "5.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-5.0.16.tgz",
|
||||
"integrity": "sha512-pjO80g5x/hkqzWCIafvkS3JrkBDxSiTjEy4LdqQKJYrmoGx8x2AlhSUMgzIzG/ge4kT98bA7+gmm7yquzrrZ/w=="
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-5.1.0.tgz",
|
||||
"integrity": "sha512-cTS62X9bgR6H+3qRtaDwt0I+3ocitMPalyr2OrzJtilIcuEo4my8UA4VVhOgr0OI2Sk9JNrNYcSxkv0k4XuKtQ==",
|
||||
"license": "OFL-1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@fontsource/roboto-condensed": "^5.0.16"
|
||||
"@fontsource/roboto": "^5.1.0",
|
||||
"@fontsource/roboto-condensed": "^5.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
<script type="text/javascript" src="js/loader.js"></script>
|
||||
<script type="text/javascript" src="js/socketclient.js"></script>
|
||||
<script type="text/javascript" src="js/animateCSS.js"></script>
|
||||
<script type="text/javascript" src="js/positions.js"></script>
|
||||
<script type="text/javascript" src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/bash
|
||||
# This file is still here to keep PM2 working on older installations.
|
||||
cd ~/MagicMirror
|
||||
DISPLAY=:0 npm start
|
29
js/app.js
29
js/app.js
|
@ -9,6 +9,7 @@ const Log = require("logger");
|
|||
const Server = require(`${__dirname}/server`);
|
||||
const Utils = require(`${__dirname}/utils`);
|
||||
const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`);
|
||||
const { getEnvVarsAsObj } = require(`${__dirname}/server_functions`);
|
||||
|
||||
// Get version number.
|
||||
global.version = require(`${__dirname}/../package.json`).version;
|
||||
|
@ -116,6 +117,8 @@ function App () {
|
|||
}
|
||||
}
|
||||
|
||||
require(`${global.root_path}/js/check_config.js`);
|
||||
|
||||
try {
|
||||
fs.accessSync(configFilename, fs.F_OK);
|
||||
const c = require(configFilename);
|
||||
|
@ -159,10 +162,19 @@ function App () {
|
|||
function loadModule (module) {
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = `${__dirname}/../modules/${module}`;
|
||||
const env = getEnvVarsAsObj();
|
||||
let moduleFolder = `${__dirname}/../${env.modulesDir}/${module}`;
|
||||
|
||||
if (defaultModules.includes(moduleName)) {
|
||||
moduleFolder = `${__dirname}/../modules/default/${module}`;
|
||||
const defaultModuleFolder = `${__dirname}/../modules/default/${module}`;
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
} else {
|
||||
// running in Jest, allow defaultModules placed under moduleDir for testing
|
||||
if (env.modulesDir === "modules") {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const moduleFile = `${moduleFolder}/${module}.js`;
|
||||
|
@ -183,6 +195,7 @@ function App () {
|
|||
Log.log(`No helper found for module: ${moduleName}.`);
|
||||
}
|
||||
|
||||
// if the helper was found
|
||||
if (loadHelper) {
|
||||
const Module = require(helperPath);
|
||||
let m = new Module();
|
||||
|
@ -255,17 +268,23 @@ function App () {
|
|||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
// get the used module positions
|
||||
Utils.getModulePositions();
|
||||
|
||||
let modules = [];
|
||||
for (const module of config.modules) {
|
||||
if (module.disabled) continue;
|
||||
if (module.module) {
|
||||
if (Utils.moduleHasValidPosition(module.position) || typeof (module.position) === "undefined") {
|
||||
// Only add this module to be loaded if it is not a duplicate (repeated instance of the same module)
|
||||
if (!modules.includes(module.module)) {
|
||||
modules.push(module.module);
|
||||
} else {
|
||||
Log.warn("Invalid module position found for this configuration:", module);
|
||||
}
|
||||
} else {
|
||||
Log.warn("No module name found for this configuration:", module);
|
||||
Log.warn("Invalid module position found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`);
|
||||
}
|
||||
} else {
|
||||
Log.warn("No module name found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
const colors = require("ansis");
|
||||
const { Linter } = require("eslint");
|
||||
|
||||
const linter = new Linter();
|
||||
|
||||
const Ajv = require("ajv");
|
||||
|
||||
const ajv = new Ajv();
|
||||
const colors = require("ansis");
|
||||
const globals = require("globals");
|
||||
const { Linter } = require("eslint");
|
||||
|
||||
const rootPath = path.resolve(`${__dirname}/../`);
|
||||
const Log = require(`${rootPath}/js/logger.js`);
|
||||
const Utils = require(`${rootPath}/js/utils.js`);
|
||||
|
||||
const linter = new Linter({ configType: "flat" });
|
||||
const ajv = new Ajv();
|
||||
|
||||
/**
|
||||
* Returns a string with path of configuration file.
|
||||
|
@ -30,46 +30,55 @@ function checkConfigFile () {
|
|||
|
||||
// Check if file is present
|
||||
if (fs.existsSync(configFileName) === false) {
|
||||
Log.error(`File not found: ${configFileName}`);
|
||||
throw new Error("No config file present!");
|
||||
throw new Error(`File not found: ${configFileName}\nNo config file present!`);
|
||||
}
|
||||
|
||||
// Check permission
|
||||
try {
|
||||
fs.accessSync(configFileName, fs.F_OK);
|
||||
} catch (e) {
|
||||
Log.error(e);
|
||||
throw new Error("No permission to access config file!");
|
||||
} catch (error) {
|
||||
throw new Error(`${error}\nNo permission to access config file!`);
|
||||
}
|
||||
|
||||
// Validate syntax of the configuration file.
|
||||
Log.info("Checking file... ", configFileName);
|
||||
Log.info(`Checking config file ${configFileName} ...`);
|
||||
|
||||
// I'm not sure if all ever is utf-8
|
||||
const configFile = fs.readFileSync(configFileName, "utf-8");
|
||||
|
||||
// Explicitly tell linter that he might encounter es6 syntax ("let config = {...}")
|
||||
const errors = linter.verify(configFile, {
|
||||
env: {
|
||||
es6: true
|
||||
const errors = linter.verify(
|
||||
configFile,
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
configFileName
|
||||
);
|
||||
|
||||
if (errors.length === 0) {
|
||||
Log.info(colors.green("Your configuration file doesn't contain syntax errors :)"));
|
||||
validateModulePositions(configFileName);
|
||||
} else {
|
||||
Log.error(colors.red("Your configuration file contains syntax errors :("));
|
||||
let errorMessage = "Your configuration file contains syntax errors :(";
|
||||
|
||||
for (const error of errors) {
|
||||
Log.error(`Line ${error.line} column ${error.column}: ${error.message}`);
|
||||
errorMessage += `\nLine ${error.line} column ${error.column}: ${error.message}`;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
function validateModulePositions (configFileName) {
|
||||
Log.info("Checking modules structure configuration ...");
|
||||
|
||||
// Make Ajv schema confguration of modules config
|
||||
// only scan "module" and "position"
|
||||
const positionList = Utils.getModulePositions();
|
||||
|
||||
// Make Ajv schema configuration of modules config
|
||||
// Only scan "module" and "position"
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
@ -83,21 +92,7 @@ function checkConfigFile () {
|
|||
},
|
||||
position: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"top_bar",
|
||||
"top_left",
|
||||
"top_center",
|
||||
"top_right",
|
||||
"upper_third",
|
||||
"middle_center",
|
||||
"lower_third",
|
||||
"bottom_left",
|
||||
"bottom_center",
|
||||
"bottom_right",
|
||||
"bottom_bar",
|
||||
"fullscreen_above",
|
||||
"fullscreen_below"
|
||||
]
|
||||
enum: positionList
|
||||
}
|
||||
},
|
||||
required: ["module"]
|
||||
|
@ -106,26 +101,31 @@ function checkConfigFile () {
|
|||
}
|
||||
};
|
||||
|
||||
// scan all modules
|
||||
// Scan all modules
|
||||
const validate = ajv.compile(schema);
|
||||
const data = require(configFileName);
|
||||
|
||||
const valid = validate(data);
|
||||
if (!valid) {
|
||||
let module = validate.errors[0].instancePath.split("/")[2];
|
||||
let position = validate.errors[0].instancePath.split("/")[3];
|
||||
|
||||
Log.error(colors.red("This module configuration contains errors:"));
|
||||
Log.error(data.modules[module]);
|
||||
if (position) {
|
||||
Log.error(colors.red(`${position}: ${validate.errors[0].message}`));
|
||||
Log.error(validate.errors[0].params.allowedValues);
|
||||
} else {
|
||||
Log.error(colors.red(validate.errors[0].message));
|
||||
}
|
||||
} else {
|
||||
if (valid) {
|
||||
Log.info(colors.green("Your modules structure configuration doesn't contain errors :)"));
|
||||
} else {
|
||||
const module = validate.errors[0].instancePath.split("/")[2];
|
||||
const position = validate.errors[0].instancePath.split("/")[3];
|
||||
let errorMessage = "This module configuration contains errors:";
|
||||
errorMessage += `\n${JSON.stringify(data.modules[module], null, 2)}`;
|
||||
if (position) {
|
||||
errorMessage += `\n${position}: ${validate.errors[0].message}`;
|
||||
errorMessage += `\n${JSON.stringify(validate.errors[0].params.allowedValues, null, 2).slice(1, -1)}`;
|
||||
} else {
|
||||
errorMessage += validate.errors[0].message;
|
||||
}
|
||||
Log.error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
checkConfigFile();
|
||||
} catch (error) {
|
||||
Log.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
21
js/class.js
21
js/class.js
|
@ -1,6 +1,7 @@
|
|||
/* global Class, xyz */
|
||||
|
||||
/* Simple JavaScript Inheritance
|
||||
/*
|
||||
* Simple JavaScript Inheritance
|
||||
* By John Resig https://johnresig.com/
|
||||
*
|
||||
* Inspired by base2 and Prototype
|
||||
|
@ -22,8 +23,10 @@
|
|||
Class.extend = function (prop) {
|
||||
let _super = this.prototype;
|
||||
|
||||
// Instantiate a base class (but only create the instance,
|
||||
// don't run the init constructor)
|
||||
/*
|
||||
* Instantiate a base class (but only create the instance,
|
||||
* don't run the init constructor)
|
||||
*/
|
||||
initializing = true;
|
||||
const prototype = new this();
|
||||
initializing = false;
|
||||
|
@ -42,12 +45,16 @@
|
|||
return function () {
|
||||
const tmp = this._super;
|
||||
|
||||
// Add a new ._super() method that is the same method
|
||||
// but on the super-class
|
||||
/*
|
||||
* Add a new ._super() method that is the same method
|
||||
* but on the super-class
|
||||
*/
|
||||
this._super = _super[name];
|
||||
|
||||
// The method only need to be bound temporarily, so we
|
||||
// remove it when we're done executing
|
||||
/*
|
||||
* The method only need to be bound temporarily, so we
|
||||
* remove it when we're done executing
|
||||
*/
|
||||
const ret = fn.apply(this, arguments);
|
||||
this._super = tmp;
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ const defaults = {
|
|||
units: "metric",
|
||||
zoom: 1,
|
||||
customCss: "css/custom.css",
|
||||
foreignModulesDir: "modules",
|
||||
// httpHeaders used by helmet, see https://helmetjs.github.io/. You can add other/more object values by overriding this in config.js,
|
||||
// e.g. you need to add `frameguard: false` for embedding MagicMirror in another website, see https://github.com/MagicMirrorOrg/MagicMirror/issues/2847
|
||||
httpHeaders: { contentSecurityPolicy: false, crossOriginOpenerPolicy: false, crossOriginEmbedderPolicy: false, crossOriginResourcePolicy: false, originAgentCluster: false },
|
||||
|
@ -72,12 +73,7 @@ const defaults = {
|
|||
text: "www.michaelteeuw.nl"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
paths: {
|
||||
modules: "modules",
|
||||
vendor: "vendor"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
|
|
|
@ -8,9 +8,12 @@ const Log = require("./logger");
|
|||
let config = process.env.config ? JSON.parse(process.env.config) : {};
|
||||
// Module to control application life.
|
||||
const app = electron.app;
|
||||
// Per default electron is started with --disable-gpu flag, if you want the gpu enabled,
|
||||
// you must set the env var ELECTRON_ENABLE_GPU=1 on startup.
|
||||
// See https://www.electronjs.org/docs/latest/tutorial/offscreen-rendering for more info.
|
||||
|
||||
/*
|
||||
* Per default electron is started with --disable-gpu flag, if you want the gpu enabled,
|
||||
* you must set the env var ELECTRON_ENABLE_GPU=1 on startup.
|
||||
* See https://www.electronjs.org/docs/latest/tutorial/offscreen-rendering for more info.
|
||||
*/
|
||||
if (process.env.ELECTRON_ENABLE_GPU !== "1") {
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
@ -18,16 +21,21 @@ if (process.env.ELECTRON_ENABLE_GPU !== "1") {
|
|||
// Module to create native browser window.
|
||||
const BrowserWindow = electron.BrowserWindow;
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
/*
|
||||
* Keep a global reference of the window object, if you don't, the window will
|
||||
* be closed automatically when the JavaScript object is garbage collected.
|
||||
*/
|
||||
let mainWindow;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function createWindow () {
|
||||
// see https://www.electronjs.org/docs/latest/api/screen
|
||||
// Create a window that fills the screen's available work area.
|
||||
|
||||
/*
|
||||
* see https://www.electronjs.org/docs/latest/api/screen
|
||||
* Create a window that fills the screen's available work area.
|
||||
*/
|
||||
let electronSize = (800, 600);
|
||||
try {
|
||||
electronSize = electron.screen.getPrimaryDisplay().workAreaSize;
|
||||
|
@ -52,8 +60,10 @@ function createWindow () {
|
|||
backgroundColor: "#000000"
|
||||
};
|
||||
|
||||
// DEPRECATED: "kioskmode" backwards compatibility, to be removed
|
||||
// settings these options directly instead provides cleaner interface
|
||||
/*
|
||||
* DEPRECATED: "kioskmode" backwards compatibility, to be removed
|
||||
* settings these options directly instead provides cleaner interface
|
||||
*/
|
||||
if (config.kioskmode) {
|
||||
electronOptionsDefaults.kiosk = true;
|
||||
} else {
|
||||
|
@ -69,8 +79,10 @@ function createWindow () {
|
|||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow(electronOptions);
|
||||
|
||||
// and load the index.html of the app.
|
||||
// If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
|
||||
/*
|
||||
* and load the index.html of the app.
|
||||
* If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
|
||||
*/
|
||||
|
||||
let prefix;
|
||||
if ((config["tls"] !== null && config["tls"]) || config.useHttps) {
|
||||
|
@ -149,14 +161,18 @@ app.on("window-all-closed", function () {
|
|||
});
|
||||
|
||||
app.on("activate", function () {
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
|
||||
/*
|
||||
* On OS X it's common to re-create a window in the app when the
|
||||
* dock icon is clicked and there are no other windows open.
|
||||
*/
|
||||
if (mainWindow === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
/* This method will be called when SIGINT is received and will call
|
||||
/*
|
||||
* This method will be called when SIGINT is received and will call
|
||||
* each node_helper's stop function if it exists. Added to fix #1056
|
||||
*
|
||||
* Note: this is only used if running Electron. Otherwise
|
||||
|
@ -187,8 +203,10 @@ if (process.env.clientonly) {
|
|||
});
|
||||
}
|
||||
|
||||
// Start the core application if server is run on localhost
|
||||
// This starts all node helpers and starts the webserver.
|
||||
/*
|
||||
* Start the core application if server is run on localhost
|
||||
* This starts all node helpers and starts the webserver.
|
||||
*/
|
||||
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) {
|
||||
core.start().then((c) => {
|
||||
config = c;
|
||||
|
|
34
js/loader.js
34
js/loader.js
|
@ -10,6 +10,15 @@ const Loader = (function () {
|
|||
|
||||
/* Private Methods */
|
||||
|
||||
/**
|
||||
* Retrieve object of env variables.
|
||||
* @returns {object} with key: values as assembled in js/server_functions.js
|
||||
*/
|
||||
const getEnvVars = async function () {
|
||||
const res = await fetch(`${location.protocol}//${location.host}/env`);
|
||||
return JSON.parse(await res.text());
|
||||
};
|
||||
|
||||
/**
|
||||
* Loops through all modules and requests start for every module.
|
||||
*/
|
||||
|
@ -58,19 +67,28 @@ const Loader = (function () {
|
|||
* Generate array with module information including module paths.
|
||||
* @returns {object[]} Module information.
|
||||
*/
|
||||
const getModuleData = function () {
|
||||
const getModuleData = async function () {
|
||||
const modules = getAllModules();
|
||||
const moduleFiles = [];
|
||||
const envVars = await getEnvVars();
|
||||
|
||||
modules.forEach(function (moduleData, index) {
|
||||
const module = moduleData.module;
|
||||
|
||||
const elements = module.split("/");
|
||||
const moduleName = elements[elements.length - 1];
|
||||
let moduleFolder = `${config.paths.modules}/${module}`;
|
||||
let moduleFolder = `${envVars.modulesDir}/${module}`;
|
||||
|
||||
if (defaultModules.indexOf(moduleName) !== -1) {
|
||||
moduleFolder = `${config.paths.modules}/default/${module}`;
|
||||
const defaultModuleFolder = `modules/default/${module}`;
|
||||
if (window.name !== "jsdom") {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
} else {
|
||||
// running in Jest, allow defaultModules placed under moduleDir for testing
|
||||
if (envVars.modulesDir === "modules") {
|
||||
moduleFolder = defaultModuleFolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleData.disabled === true) {
|
||||
|
@ -166,6 +184,7 @@ const Loader = (function () {
|
|||
};
|
||||
script.onerror = function () {
|
||||
Log.error("Error on loading script:", fileName);
|
||||
script.remove();
|
||||
resolve();
|
||||
};
|
||||
document.getElementsByTagName("body")[0].appendChild(script);
|
||||
|
@ -183,6 +202,7 @@ const Loader = (function () {
|
|||
};
|
||||
stylesheet.onerror = function () {
|
||||
Log.error("Error on loading stylesheet:", fileName);
|
||||
stylesheet.remove();
|
||||
resolve();
|
||||
};
|
||||
document.getElementsByTagName("head")[0].appendChild(stylesheet);
|
||||
|
@ -197,7 +217,9 @@ const Loader = (function () {
|
|||
* Load all modules as defined in the config.
|
||||
*/
|
||||
async loadModules () {
|
||||
let moduleData = getModuleData();
|
||||
let moduleData = await getModuleData();
|
||||
const envVars = await getEnvVars();
|
||||
const customCss = envVars.customCss;
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>} when all modules are loaded
|
||||
|
@ -212,7 +234,7 @@ const Loader = (function () {
|
|||
// All modules loaded. Load custom.css
|
||||
// This is done after all the modules so we can
|
||||
// overwrite all the defined styles.
|
||||
await loadFile(config.customCss);
|
||||
await loadFile(customCss);
|
||||
// custom.css loaded. Start all modules.
|
||||
await startModules();
|
||||
}
|
||||
|
@ -244,7 +266,7 @@ const Loader = (function () {
|
|||
// This file is available in the vendor folder.
|
||||
// Load it from this vendor folder.
|
||||
loadedFiles.push(fileName.toLowerCase());
|
||||
return loadFile(`${config.paths.vendor}/${vendor[fileName]}`);
|
||||
return loadFile(`vendor/${vendor[fileName]}`);
|
||||
}
|
||||
|
||||
// File not loaded yet.
|
||||
|
|
22
js/main.js
22
js/main.js
|
@ -1,4 +1,4 @@
|
|||
/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut */
|
||||
/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut, modulePositions */
|
||||
|
||||
const MM = (function () {
|
||||
let modules = [];
|
||||
|
@ -286,9 +286,9 @@ const MM = (function () {
|
|||
Log.debug(`${module.identifier} Force remove animateIn (in hide): ${module.hasAnimateIn}`);
|
||||
module.hasAnimateIn = false;
|
||||
}
|
||||
// haveAnimateName for verify if we are using AninateCSS library
|
||||
// haveAnimateName for verify if we are using AnimateCSS library
|
||||
// we check AnimateCSSOut Array for validate it
|
||||
// and finaly return the animate name or `null` (for default MM² animation)
|
||||
// and finally return the animate name or `null` (for default MM² animation)
|
||||
let haveAnimateName = null;
|
||||
// check if have valid animateOut in module definition (module.data.animateOut)
|
||||
if (module.data.animateOut && AnimateCSSOut.indexOf(module.data.animateOut) !== -1) haveAnimateName = module.data.animateOut;
|
||||
|
@ -357,7 +357,7 @@ const MM = (function () {
|
|||
}
|
||||
}
|
||||
|
||||
// Check if there are no more lockstrings set, or the force option is set.
|
||||
// Check if there are no more lockStrings set, or the force option is set.
|
||||
// Otherwise cancel show action.
|
||||
if (module.lockStrings.length !== 0 && options.force !== true) {
|
||||
Log.log(`Will not show ${module.name}. LockStrings active: ${module.lockStrings.join(",")}`);
|
||||
|
@ -380,7 +380,7 @@ const MM = (function () {
|
|||
|
||||
module.hidden = false;
|
||||
|
||||
// If forced show, clean current lockstrings.
|
||||
// If forced show, clean current lockStrings.
|
||||
if (module.lockStrings.length !== 0 && options.force === true) {
|
||||
Log.log(`Force show of module: ${module.name}`);
|
||||
module.lockStrings = [];
|
||||
|
@ -390,9 +390,9 @@ const MM = (function () {
|
|||
if (moduleWrapper !== null) {
|
||||
clearTimeout(module.showHideTimer);
|
||||
|
||||
// haveAnimateName for verify if we are using AninateCSS library
|
||||
// haveAnimateName for verify if we are using AnimateCSS library
|
||||
// we check AnimateCSSIn Array for validate it
|
||||
// and finaly return the animate name or `null` (for default MM² animation)
|
||||
// and finally return the animate name or `null` (for default MM² animation)
|
||||
let haveAnimateName = null;
|
||||
// check if have valid animateOut in module definition (module.data.animateIn)
|
||||
if (module.data.animateIn && AnimateCSSIn.indexOf(module.data.animateIn) !== -1) haveAnimateName = module.data.animateIn;
|
||||
|
@ -450,7 +450,6 @@ const MM = (function () {
|
|||
* an ugly top margin. By using this function, the top bar will be hidden if the
|
||||
* update notification is not visible.
|
||||
*/
|
||||
const modulePositions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||
|
||||
const updateWrapperStates = function () {
|
||||
modulePositions.forEach(function (position) {
|
||||
|
@ -667,7 +666,10 @@ const MM = (function () {
|
|||
}
|
||||
|
||||
// Further implementation is done in the private method.
|
||||
updateDom(module, updateOptions);
|
||||
updateDom(module, updateOptions).then(function () {
|
||||
// Once the update is complete and rendered, send a notification to the module that the DOM has been updated
|
||||
sendNotification("MODULE_DOM_UPDATED", null, null, module);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -703,7 +705,7 @@ const MM = (function () {
|
|||
showModule(module, speed, callback, options);
|
||||
},
|
||||
|
||||
// return all available module postions.
|
||||
// Return all available module positions.
|
||||
getAvailableModulePositions: modulePositions
|
||||
};
|
||||
}());
|
||||
|
|
29
js/module.js
29
js/module.js
|
@ -1,13 +1,16 @@
|
|||
/* global Class, cloneObject, Loader, MMSocket, nunjucks, Translator */
|
||||
|
||||
/* Module Blueprint.
|
||||
/*
|
||||
* Module Blueprint.
|
||||
* @typedef {Object} Module
|
||||
*/
|
||||
const Module = Class.extend({
|
||||
|
||||
/*********************************************************
|
||||
/**
|
||||
********************************************************
|
||||
* All methods (and properties) below can be subclassed. *
|
||||
*********************************************************/
|
||||
********************************************************
|
||||
*/
|
||||
|
||||
// Set the minimum MagicMirror² module version for this module.
|
||||
requiresVersion: "2.0.0",
|
||||
|
@ -18,13 +21,17 @@ const Module = Class.extend({
|
|||
// Timer reference used for showHide animation callbacks.
|
||||
showHideTimer: null,
|
||||
|
||||
// Array to store lockStrings. These strings are used to lock
|
||||
// visibility when hiding and showing module.
|
||||
/*
|
||||
* Array to store lockStrings. These strings are used to lock
|
||||
* visibility when hiding and showing module.
|
||||
*/
|
||||
lockStrings: [],
|
||||
|
||||
// Storage of the nunjucks Environment,
|
||||
// This should not be referenced directly.
|
||||
// Use the nunjucksEnvironment() to get it.
|
||||
/*
|
||||
* Storage of the nunjucks Environment,
|
||||
* This should not be referenced directly.
|
||||
* Use the nunjucksEnvironment() to get it.
|
||||
*/
|
||||
_nunjucksEnvironment: null,
|
||||
|
||||
/**
|
||||
|
@ -189,9 +196,11 @@ const Module = Class.extend({
|
|||
Log.log(`${this.name} is resumed.`);
|
||||
},
|
||||
|
||||
/*********************************************
|
||||
/**
|
||||
********************************************
|
||||
* The methods below don't need subclassing. *
|
||||
*********************************************/
|
||||
********************************************
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set the module data.
|
||||
|
|
|
@ -49,7 +49,8 @@ const NodeHelper = Class.extend({
|
|||
this.path = path;
|
||||
},
|
||||
|
||||
/* sendSocketNotification(notification, payload)
|
||||
/*
|
||||
* sendSocketNotification(notification, payload)
|
||||
* Send a socket notification to the node helper.
|
||||
*
|
||||
* argument notification string - The identifier of the notification.
|
||||
|
@ -59,7 +60,8 @@ const NodeHelper = Class.extend({
|
|||
this.io.of(this.name).emit(notification, payload);
|
||||
},
|
||||
|
||||
/* setExpressApp(app)
|
||||
/*
|
||||
* setExpressApp(app)
|
||||
* Sets the express app object for this module.
|
||||
* This allows you to host files from the created webserver.
|
||||
*
|
||||
|
@ -71,7 +73,8 @@ const NodeHelper = Class.extend({
|
|||
app.use(`/${this.name}`, express.static(`${this.path}/public`));
|
||||
},
|
||||
|
||||
/* setSocketIO(io)
|
||||
/*
|
||||
* setSocketIO(io)
|
||||
* Sets the socket io object for this module.
|
||||
* Binds message receiver.
|
||||
*
|
||||
|
@ -83,20 +86,9 @@ const NodeHelper = Class.extend({
|
|||
Log.log(`Connecting socket for: ${this.name}`);
|
||||
|
||||
io.of(this.name).on("connection", (socket) => {
|
||||
// add a catch all event.
|
||||
const onevent = socket.onevent;
|
||||
socket.onevent = function (packet) {
|
||||
const args = packet.data || [];
|
||||
onevent.call(this, packet); // original call
|
||||
packet.data = ["*"].concat(args);
|
||||
onevent.call(this, packet); // additional call to catch-all
|
||||
};
|
||||
|
||||
// register catch all.
|
||||
socket.on("*", (notification, payload) => {
|
||||
if (notification !== "*") {
|
||||
socket.onAny((notification, payload) => {
|
||||
this.socketNotificationReceived(notification, payload);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
12
js/server.js
12
js/server.js
|
@ -8,8 +8,7 @@ const helmet = require("helmet");
|
|||
const socketio = require("socket.io");
|
||||
|
||||
const Log = require("logger");
|
||||
const Utils = require("./utils");
|
||||
const { cors, getConfig, getHtml, getVersion, getStartup } = require("./server_functions");
|
||||
const { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars } = require("./server_functions");
|
||||
|
||||
/**
|
||||
* Server
|
||||
|
@ -73,8 +72,11 @@ function Server (config) {
|
|||
app.use(helmet(config.httpHeaders));
|
||||
app.use("/js", express.static(__dirname));
|
||||
|
||||
// TODO add tests directory only when running tests?
|
||||
const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs", "/tests/mocks"];
|
||||
let directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations"];
|
||||
if (process.env.JEST_WORKER_ID !== undefined) {
|
||||
// add tests directories only when running tests
|
||||
directories.push("/tests/configs", "/tests/mocks");
|
||||
}
|
||||
for (const directory of directories) {
|
||||
app.use(directory, express.static(path.resolve(global.root_path + directory)));
|
||||
}
|
||||
|
@ -87,6 +89,8 @@ function Server (config) {
|
|||
|
||||
app.get("/startup", (req, res) => getStartup(req, res));
|
||||
|
||||
app.get("/env", (req, res) => getEnvVars(req, res));
|
||||
|
||||
app.get("/", (req, res) => getHtml(req, res));
|
||||
|
||||
server.on("listening", () => {
|
||||
|
|
|
@ -45,12 +45,12 @@ async function cors (req, res) {
|
|||
url = match[1];
|
||||
|
||||
const headersToSend = getHeadersToSend(req.url);
|
||||
const expectedRecievedHeaders = geExpectedRecievedHeaders(req.url);
|
||||
const expectedReceivedHeaders = geExpectedReceivedHeaders(req.url);
|
||||
|
||||
Log.log(`cors url: ${url}`);
|
||||
const response = await fetch(url, { headers: headersToSend });
|
||||
|
||||
for (const header of expectedRecievedHeaders) {
|
||||
for (const header of expectedReceivedHeaders) {
|
||||
const headerValue = response.headers.get(header);
|
||||
if (header) res.set(header, headerValue);
|
||||
}
|
||||
|
@ -89,16 +89,16 @@ function getHeadersToSend (url) {
|
|||
* @param {string} url - The url containing the expected headers from the response.
|
||||
* @returns {string[]} headers - The name of the expected headers.
|
||||
*/
|
||||
function geExpectedRecievedHeaders (url) {
|
||||
const expectedRecievedHeaders = ["Content-Type"];
|
||||
const expectedRecievedHeadersMatch = new RegExp("expectedheaders=(.+?)(&|$)", "g").exec(url);
|
||||
if (expectedRecievedHeadersMatch) {
|
||||
const headers = expectedRecievedHeadersMatch[1].split(",");
|
||||
function geExpectedReceivedHeaders (url) {
|
||||
const expectedReceivedHeaders = ["Content-Type"];
|
||||
const expectedReceivedHeadersMatch = new RegExp("expectedheaders=(.+?)(&|$)", "g").exec(url);
|
||||
if (expectedReceivedHeadersMatch) {
|
||||
const headers = expectedReceivedHeadersMatch[1].split(",");
|
||||
for (const header of headers) {
|
||||
expectedRecievedHeaders.push(header);
|
||||
expectedReceivedHeaders.push(header);
|
||||
}
|
||||
}
|
||||
return expectedRecievedHeaders;
|
||||
return expectedReceivedHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -128,4 +128,30 @@ function getVersion (req, res) {
|
|||
res.send(global.version);
|
||||
}
|
||||
|
||||
module.exports = { cors, getConfig, getHtml, getVersion, getStartup };
|
||||
/**
|
||||
* Gets environment variables needed in the browser.
|
||||
* @returns {object} environment variables key: values
|
||||
*/
|
||||
function getEnvVarsAsObj () {
|
||||
const obj = { modulesDir: `${config.foreignModulesDir}`, customCss: `${config.customCss}` };
|
||||
if (process.env.MM_MODULES_DIR) {
|
||||
obj.modulesDir = process.env.MM_MODULES_DIR.replace(`${global.root_path}/`, "");
|
||||
}
|
||||
if (process.env.MM_CUSTOMCSS_FILE) {
|
||||
obj.customCss = process.env.MM_CUSTOMCSS_FILE.replace(`${global.root_path}/`, "");
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets environment variables needed in the browser.
|
||||
* @param {Request} req - the request
|
||||
* @param {Response} res - the result
|
||||
*/
|
||||
function getEnvVars (req, res) {
|
||||
const obj = getEnvVarsAsObj();
|
||||
res.send(obj);
|
||||
}
|
||||
|
||||
module.exports = { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars, getEnvVarsAsObj };
|
||||
|
|
|
@ -15,14 +15,14 @@ const Translator = (function () {
|
|||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
// needs error handler try/catch at least
|
||||
let fileinfo = null;
|
||||
let fileInfo = null;
|
||||
try {
|
||||
fileinfo = JSON.parse(xhr.responseText);
|
||||
fileInfo = JSON.parse(xhr.responseText);
|
||||
} catch (exception) {
|
||||
// nothing here, but don't die
|
||||
Log.error(` loading json file =${file} failed`);
|
||||
}
|
||||
resolve(fileinfo);
|
||||
resolve(fileInfo);
|
||||
}
|
||||
};
|
||||
xhr.send(null);
|
||||
|
|
41
js/utils.js
41
js/utils.js
|
@ -1,7 +1,17 @@
|
|||
const execSync = require("node:child_process").execSync;
|
||||
const Log = require("logger");
|
||||
const path = require("node:path");
|
||||
|
||||
const rootPath = path.resolve(`${__dirname}/../`);
|
||||
const Log = require(`${rootPath}/js/logger.js`);
|
||||
const os = require("node:os");
|
||||
const fs = require("node:fs");
|
||||
const si = require("systeminformation");
|
||||
|
||||
const modulePositions = []; // will get list from index.html
|
||||
const regionRegEx = /"region ([^"]*)/i;
|
||||
const indexFileName = "index.html";
|
||||
const discoveredPositionsJSFilename = "js/positions.js";
|
||||
|
||||
module.exports = {
|
||||
|
||||
async logSystemInformation () {
|
||||
|
@ -14,7 +24,7 @@ module.exports = {
|
|||
versions: "kernel, node, npm, pm2"
|
||||
});
|
||||
let systemDataString = "System information:";
|
||||
systemDataString += `\n### SYSTEM: manufacturer: ${staticData["system"]["manufacturer"]}; model: ${staticData["system"]["model"]}; raspberry: ${staticData["system"]["raspberry"]}; virtual: ${staticData["system"]["virtual"]}`;
|
||||
systemDataString += `\n### SYSTEM: manufacturer: ${staticData["system"]["manufacturer"]}; model: ${staticData["system"]["model"]}; virtual: ${staticData["system"]["virtual"]}`;
|
||||
systemDataString += `\n### OS: platform: ${staticData["osInfo"]["platform"]}; distro: ${staticData["osInfo"]["distro"]}; release: ${staticData["osInfo"]["release"]}; arch: ${staticData["osInfo"]["arch"]}; kernel: ${staticData["versions"]["kernel"]}`;
|
||||
systemDataString += `\n### VERSIONS: electron: ${process.versions.electron}; used node: ${staticData["versions"]["node"]}; installed node: ${installedNodeVersion}; npm: ${staticData["versions"]["npm"]}; pm2: ${staticData["versions"]["pm2"]}`;
|
||||
systemDataString += `\n### OTHER: timeZone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}; ELECTRON_ENABLE_GPU: ${process.env.ELECTRON_ENABLE_GPU}`;
|
||||
|
@ -29,12 +39,35 @@ module.exports = {
|
|||
|
||||
// return all available module positions
|
||||
getAvailableModulePositions () {
|
||||
return ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||
return modulePositions;
|
||||
},
|
||||
|
||||
// return if postion is on modulePositions Array (true/false)
|
||||
// return if position is on modulePositions Array (true/false)
|
||||
moduleHasValidPosition (position) {
|
||||
if (this.getAvailableModulePositions().indexOf(position) === -1) return false;
|
||||
return true;
|
||||
},
|
||||
|
||||
getModulePositions () {
|
||||
// if not already discovered
|
||||
if (modulePositions.length === 0) {
|
||||
// get the lines of the index.html
|
||||
const lines = fs.readFileSync(indexFileName).toString().split(os.EOL);
|
||||
// loop thru the lines
|
||||
lines.forEach((line) => {
|
||||
// run the regex on each line
|
||||
const results = regionRegEx.exec(line);
|
||||
// if the regex returned something
|
||||
if (results && results.length > 0) {
|
||||
// get the position parts and replace space with underscore
|
||||
const positionName = results[1].replace(" ", "_");
|
||||
// add it to the list
|
||||
modulePositions.push(positionName);
|
||||
}
|
||||
});
|
||||
fs.writeFileSync(discoveredPositionsJSFilename, `const modulePositions=${JSON.stringify(modulePositions)}`);
|
||||
}
|
||||
// return the list to the caller
|
||||
return modulePositions;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -82,9 +82,12 @@ Module.register("calendar", {
|
|||
|
||||
// Define required translations.
|
||||
getTranslations () {
|
||||
// The translations for the default modules are defined in the core translation files.
|
||||
// Therefore we can just return false. Otherwise we should have returned a dictionary.
|
||||
// If you're trying to build your own module including translations, check out the documentation.
|
||||
|
||||
/*
|
||||
* The translations for the default modules are defined in the core translation files.
|
||||
* Therefore we can just return false. Otherwise we should have returned a dictionary.
|
||||
* If you're trying to build your own module including translations, check out the documentation.
|
||||
*/
|
||||
return false;
|
||||
},
|
||||
|
||||
|
@ -148,8 +151,10 @@ Module.register("calendar", {
|
|||
};
|
||||
}
|
||||
|
||||
// tell helper to start a fetcher for this calendar
|
||||
// fetcher till cycle
|
||||
/*
|
||||
* tell helper to start a fetcher for this calendar
|
||||
* fetcher till cycle
|
||||
*/
|
||||
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
||||
});
|
||||
|
||||
|
@ -627,10 +632,11 @@ Module.register("calendar", {
|
|||
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
|
||||
event.dayAfterTomorrow = !event.tomorrow && event.startDate >= today + ONE_DAY * 2 && event.startDate < today + 3 * ONE_DAY;
|
||||
|
||||
/* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
|
||||
/*
|
||||
* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
|
||||
* otherwise, esp. in dateheaders mode it is not clear how long these events are.
|
||||
*/
|
||||
const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / ONE_DAY) + 1;
|
||||
const maxCount = Math.round((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / ONE_DAY) + 1;
|
||||
if (this.config.sliceMultiDayEvents && maxCount > 1) {
|
||||
const splitEvents = [];
|
||||
let midnight
|
||||
|
@ -638,19 +644,20 @@ Module.register("calendar", {
|
|||
.clone()
|
||||
.startOf("day")
|
||||
.add(1, "day")
|
||||
.endOf("day")
|
||||
.format("x");
|
||||
let count = 1;
|
||||
while (event.endDate > midnight) {
|
||||
const thisEvent = JSON.parse(JSON.stringify(event)); // clone object
|
||||
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + ONE_DAY;
|
||||
thisEvent.tomorrow = !thisEvent.today && thisEvent.startDate >= today + ONE_DAY && thisEvent.startDate < today + 2 * ONE_DAY;
|
||||
thisEvent.endDate = midnight;
|
||||
thisEvent.endDate = moment(midnight, "x").clone().subtract(1, "day").format("x");
|
||||
thisEvent.title += ` (${count}/${maxCount})`;
|
||||
splitEvents.push(thisEvent);
|
||||
|
||||
event.startDate = midnight;
|
||||
count += 1;
|
||||
midnight = moment(midnight, "x").add(1, "day").format("x"); // next day
|
||||
midnight = moment(midnight, "x").add(1, "day").endOf("day").format("x"); // next day
|
||||
}
|
||||
// Last day
|
||||
event.title += ` (${count}/${maxCount})`;
|
||||
|
@ -677,16 +684,21 @@ Module.register("calendar", {
|
|||
return events;
|
||||
}
|
||||
|
||||
// Limit the number of days displayed
|
||||
// If limitDays is set > 0, limit display to that number of days
|
||||
/*
|
||||
* Limit the number of days displayed
|
||||
* If limitDays is set > 0, limit display to that number of days
|
||||
*/
|
||||
if (this.config.limitDays > 0) {
|
||||
let newEvents = [];
|
||||
let lastDate = today.clone().subtract(1, "days").format("YYYYMMDD");
|
||||
let days = 0;
|
||||
for (const ev of events) {
|
||||
let eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
|
||||
// if date of event is later than lastdate
|
||||
// check if we already are showing max unique days
|
||||
|
||||
/*
|
||||
* if date of event is later than lastdate
|
||||
* check if we already are showing max unique days
|
||||
*/
|
||||
if (eventDate > lastDate) {
|
||||
// if the only entry in the first day is a full day event that day is not counted as unique
|
||||
if (!this.config.limitDaysNeverSkip && newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* CalendarFetcher Tester
|
||||
/*
|
||||
* CalendarFetcher Tester
|
||||
* use this script with `node debug.js` to test the fetcher without the need
|
||||
* of starting the MagicMirror² core. Adjust the values below to your desire.
|
||||
*/
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* global Cron */
|
||||
|
||||
Module.register("compliments", {
|
||||
// Module config defaults.
|
||||
defaults: {
|
||||
|
@ -21,10 +23,12 @@ Module.register("compliments", {
|
|||
lastIndexUsed: -1,
|
||||
// Set currentweather from module
|
||||
currentWeatherType: "",
|
||||
|
||||
cron_regex: /^(((\d+,)+\d+|((\d+|[*])[/]\d+|((JAN|FEB|APR|MA[RY]|JU[LN]|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|APR|MA[RY]|JU[LN]|AUG|SEP|OCT|NOV|DEC))?))|(\d+-\d+)|\d+(-\d+)?[/]\d+(-\d+)?|\d+|[*]|(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?) ?){5}$/i,
|
||||
date_regex: "[1-9.][0-9.][0-9.]{2}-([0][1-9]|[1][0-2])-([1-2][0-9]|[0][1-9]|[3][0-1])",
|
||||
pre_defined_types: ["anytime", "morning", "afternoon", "evening"],
|
||||
// Define required scripts.
|
||||
getScripts () {
|
||||
return ["moment.js"];
|
||||
return ["croner.js", "moment.js"];
|
||||
},
|
||||
|
||||
// Define start sequence.
|
||||
|
@ -38,12 +42,46 @@ Module.register("compliments", {
|
|||
this.config.compliments = JSON.parse(response);
|
||||
this.updateDom();
|
||||
}
|
||||
|
||||
// Schedule update timer.
|
||||
let minute_sync_delay = 1;
|
||||
// loop thru all the configured when events
|
||||
for (let m of Object.keys(this.config.compliments)) {
|
||||
// if it is a cron entry
|
||||
if (this.isCronEntry(m)) {
|
||||
// we need to synch our interval cycle to the minute
|
||||
minute_sync_delay = (60 - (moment().second())) * 1000;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Schedule update timer. sync to the minute start (if needed), so minute based events happen on the minute start
|
||||
setTimeout(() => {
|
||||
setInterval(() => {
|
||||
this.updateDom(this.config.fadeSpeed);
|
||||
}, this.config.updateInterval);
|
||||
},
|
||||
minute_sync_delay);
|
||||
},
|
||||
|
||||
// check to see if this entry could be a cron entry wich contains spaces
|
||||
isCronEntry (entry) {
|
||||
return entry.includes(" ");
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} cronExpression The cron expression. See https://croner.56k.guru/usage/pattern/
|
||||
* @param {Date} [timestamp] The timestamp to check. Defaults to the current time.
|
||||
* @returns {number} The number of seconds until the next cron run.
|
||||
*/
|
||||
getSecondsUntilNextCronRun (cronExpression, timestamp = new Date()) {
|
||||
// Required for seconds precision
|
||||
const adjustedTimestamp = new Date(timestamp.getTime() - 1000);
|
||||
|
||||
// https://www.npmjs.com/package/croner
|
||||
const cronJob = new Cron(cronExpression);
|
||||
const nextRunTime = cronJob.nextRun(adjustedTimestamp);
|
||||
|
||||
const secondsDelta = (nextRunTime - adjustedTimestamp) / 1000;
|
||||
return secondsDelta;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate a random index for a list of compliments.
|
||||
|
@ -75,8 +113,9 @@ Module.register("compliments", {
|
|||
* @returns {string[]} array with compliments for the time of the day.
|
||||
*/
|
||||
complimentArray () {
|
||||
const hour = moment().hour();
|
||||
const date = moment().format("YYYY-MM-DD");
|
||||
const now = moment();
|
||||
const hour = now.hour();
|
||||
const date = now.format("YYYY-MM-DD");
|
||||
let compliments = [];
|
||||
|
||||
// Add time of day compliments
|
||||
|
@ -91,20 +130,49 @@ Module.register("compliments", {
|
|||
// Add compliments based on weather
|
||||
if (this.currentWeatherType in this.config.compliments) {
|
||||
Array.prototype.push.apply(compliments, this.config.compliments[this.currentWeatherType]);
|
||||
// if the predefine list doesn't include it (yet)
|
||||
if (!this.pre_defined_types.includes(this.currentWeatherType)) {
|
||||
// add it
|
||||
this.pre_defined_types.push(this.currentWeatherType);
|
||||
}
|
||||
}
|
||||
|
||||
// Add compliments for anytime
|
||||
Array.prototype.push.apply(compliments, this.config.compliments.anytime);
|
||||
|
||||
// Add compliments for special days
|
||||
for (let entry in this.config.compliments) {
|
||||
if (new RegExp(entry).test(date)) {
|
||||
// Only display compliments configured for the day if specialDayUnique is set to true
|
||||
// get the list of just date entry keys
|
||||
let temp_list = Object.keys(this.config.compliments).filter((k) => {
|
||||
if (this.pre_defined_types.includes(k)) return false;
|
||||
else return true;
|
||||
});
|
||||
|
||||
let date_compliments = [];
|
||||
// Add compliments for special day/times
|
||||
for (let entry of temp_list) {
|
||||
// check if this could be a cron type entry
|
||||
if (this.isCronEntry(entry)) {
|
||||
// make sure the regex is valid
|
||||
if (new RegExp(this.cron_regex).test(entry)) {
|
||||
// check if we are in the time range for the cron entry
|
||||
if (this.getSecondsUntilNextCronRun(entry, now.set("seconds", 0).toDate()) <= 1) {
|
||||
// if so, use its notice entries
|
||||
Array.prototype.push.apply(date_compliments, this.config.compliments[entry]);
|
||||
}
|
||||
} else Log.error(`compliments cron syntax invalid=${JSON.stringify(entry)}`);
|
||||
} else if (new RegExp(entry).test(date)) {
|
||||
Array.prototype.push.apply(date_compliments, this.config.compliments[entry]);
|
||||
}
|
||||
}
|
||||
|
||||
// if we found any date compliments
|
||||
if (date_compliments.length) {
|
||||
// and the special flag is true
|
||||
if (this.config.specialDayUnique) {
|
||||
// clear the non-date compliments if any
|
||||
compliments.length = 0;
|
||||
}
|
||||
Array.prototype.push.apply(compliments, this.config.compliments[entry]);
|
||||
}
|
||||
// put the date based compliments on the list
|
||||
Array.prototype.push.apply(compliments, date_compliments);
|
||||
}
|
||||
|
||||
return compliments;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* Default Modules List
|
||||
/*
|
||||
* Default Modules List
|
||||
* Modules listed below can be loaded without the 'default/' prefix. Omitting the default folder name.
|
||||
*/
|
||||
const defaultModules = ["alert", "calendar", "clock", "compliments", "helloworld", "newsfeed", "updatenotification", "weather"];
|
||||
|
|
|
@ -4,7 +4,8 @@ const fs = require("node:fs");
|
|||
|
||||
const Log = require("logger");
|
||||
|
||||
/* class Updater
|
||||
/*
|
||||
* class Updater
|
||||
* Allow to self updating 3rd party modules from command defined in config
|
||||
*
|
||||
* [constructor] read value in config:
|
||||
|
@ -84,13 +85,15 @@ class Updater {
|
|||
return updater;
|
||||
}
|
||||
|
||||
// module updater with his proper command
|
||||
// return object as result
|
||||
//{
|
||||
// error: <boolean>, // if error detected
|
||||
// updated: <boolean>, // if updated successfully
|
||||
// needRestart: <boolean> // if magicmirror restart required
|
||||
//};
|
||||
/*
|
||||
* module updater with his proper command
|
||||
* return object as result
|
||||
* {
|
||||
* error: <boolean>, // if error detected
|
||||
* updated: <boolean>, // if updated successfully
|
||||
* needRestart: <boolean> // if magicmirror restart required
|
||||
* };
|
||||
*/
|
||||
updateProcess (module) {
|
||||
let Result = {
|
||||
error: false,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||
|
||||
/* This class is a provider for Environment Canada MSC Datamart
|
||||
/*
|
||||
* This class is a provider for Environment Canada MSC Datamart
|
||||
* Note that this is only for Canadian locations and does not require an API key (access is anonymous)
|
||||
*
|
||||
* EC Documentation at following links:
|
||||
|
@ -25,9 +26,7 @@
|
|||
*
|
||||
* License to use Environment Canada (EC) data is detailed here:
|
||||
* https://eccc-msc.github.io/open-data/licence/readme_en/
|
||||
*
|
||||
*/
|
||||
|
||||
WeatherProvider.register("envcanada", {
|
||||
// Set the name of the provider for debugging and alerting purposes (eg. provide eye-catcher)
|
||||
providerName: "Environment Canada",
|
||||
|
@ -39,10 +38,10 @@ WeatherProvider.register("envcanada", {
|
|||
provCode: "ON"
|
||||
},
|
||||
|
||||
//
|
||||
// Set config values (equates to weather module config values). Also set values pertaining to caching of
|
||||
// Today's temperature forecast (for use in the Forecast functions below)
|
||||
//
|
||||
/*
|
||||
* Set config values (equates to weather module config values). Also set values pertaining to caching of
|
||||
* Today's temperature forecast (for use in the Forecast functions below)
|
||||
*/
|
||||
setConfig (config) {
|
||||
this.config = config;
|
||||
|
||||
|
@ -52,17 +51,17 @@ WeatherProvider.register("envcanada", {
|
|||
this.cacheCurrentTemp = 999;
|
||||
},
|
||||
|
||||
//
|
||||
// Called when the weather provider is started
|
||||
//
|
||||
/*
|
||||
* Called when the weather provider is started
|
||||
*/
|
||||
start () {
|
||||
Log.info(`Weather provider: ${this.providerName} started.`);
|
||||
this.setFetchedLocation(this.config.location);
|
||||
},
|
||||
|
||||
//
|
||||
// Override the fetchCurrentWeather method to query EC and construct a Current weather object
|
||||
//
|
||||
/*
|
||||
* Override the fetchCurrentWeather method to query EC and construct a Current weather object
|
||||
*/
|
||||
fetchCurrentWeather () {
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
|
@ -80,9 +79,9 @@ WeatherProvider.register("envcanada", {
|
|||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
//
|
||||
// Override the fetchWeatherForecast method to query EC and construct Forecast weather objects
|
||||
//
|
||||
/*
|
||||
* Override the fetchWeatherForecast method to query EC and construct Forecast weather objects
|
||||
*/
|
||||
fetchWeatherForecast () {
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
|
@ -100,9 +99,9 @@ WeatherProvider.register("envcanada", {
|
|||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
//
|
||||
// Override the fetchWeatherHourly method to query EC and construct Forecast weather objects
|
||||
//
|
||||
/*
|
||||
* Override the fetchWeatherHourly method to query EC and construct Forecast weather objects
|
||||
*/
|
||||
fetchWeatherHourly () {
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
|
@ -120,36 +119,30 @@ WeatherProvider.register("envcanada", {
|
|||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Environment Canada methods - not part of the standard Provider methods
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
//
|
||||
// Build the EC URL based on the Site Code and Province Code specified in the config params. Note that the
|
||||
// URL defaults to the English version simply because there is no language dependency in the data
|
||||
// being accessed. This is only pertinent when using the EC data elements that contain a textual forecast.
|
||||
//
|
||||
/*
|
||||
* Build the EC URL based on the Site Code and Province Code specified in the config params. Note that the
|
||||
* URL defaults to the English version simply because there is no language dependency in the data
|
||||
* being accessed. This is only pertinent when using the EC data elements that contain a textual forecast.
|
||||
*/
|
||||
getUrl () {
|
||||
return `https://dd.weather.gc.ca/citypage_weather/xml/${this.config.provCode}/${this.config.siteCode}_e.xml`;
|
||||
},
|
||||
|
||||
//
|
||||
// Generate a WeatherObject based on current EC weather conditions
|
||||
//
|
||||
|
||||
/*
|
||||
* Generate a WeatherObject based on current EC weather conditions
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather (ECdoc) {
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
// There are instances where EC will update weather data and current temperature will not be
|
||||
// provided. While this is a defect in the EC systems, we need to accommodate to avoid a current temp
|
||||
// of NaN being displayed. Therefore... whenever we get a valid current temp from EC, we will cache
|
||||
// the value. Whenever EC data is missing current temp, we will provide the cached value
|
||||
// instead. This is reasonable since the cached value will typically be accurate within the previous
|
||||
// hour. The only time this does not work as expected is when MM is restarted and the first query to
|
||||
// EC finds no current temp. In this scenario, MM will end up displaying a current temp of null;
|
||||
|
||||
/*
|
||||
* There are instances where EC will update weather data and current temperature will not be
|
||||
* provided. While this is a defect in the EC systems, we need to accommodate to avoid a current temp
|
||||
* of NaN being displayed. Therefore... whenever we get a valid current temp from EC, we will cache
|
||||
* the value. Whenever EC data is missing current temp, we will provide the cached value
|
||||
* instead. This is reasonable since the cached value will typically be accurate within the previous
|
||||
* hour. The only time this does not work as expected is when MM is restarted and the first query to
|
||||
* EC finds no current temp. In this scenario, MM will end up displaying a current temp of null;
|
||||
*/
|
||||
if (ECdoc.querySelector("siteData currentConditions temperature").textContent) {
|
||||
currentWeather.temperature = ECdoc.querySelector("siteData currentConditions temperature").textContent;
|
||||
this.cacheCurrentTemp = currentWeather.temperature;
|
||||
|
@ -163,19 +156,19 @@ WeatherProvider.register("envcanada", {
|
|||
|
||||
currentWeather.humidity = ECdoc.querySelector("siteData currentConditions relativeHumidity").textContent;
|
||||
|
||||
// Ensure showPrecipitationAmount is forced to false. EC does not really provide POP for current day
|
||||
// and this feature for the weather module (current only) is sort of broken in that it wants
|
||||
// to say POP but will display precip as an accumulated amount vs. a percentage.
|
||||
|
||||
/*
|
||||
* Ensure showPrecipitationAmount is forced to false. EC does not really provide POP for current day
|
||||
* and this feature for the weather module (current only) is sort of broken in that it wants
|
||||
* to say POP but will display precip as an accumulated amount vs. a percentage.
|
||||
*/
|
||||
this.config.showPrecipitationAmount = false;
|
||||
|
||||
//
|
||||
// If the module config wants to showFeelsLike... default to the current temperature.
|
||||
// Check for EC wind chill and humidex values and overwrite the feelsLikeTemp value.
|
||||
// This assumes that the EC current conditions will never contain both a wind chill
|
||||
// and humidex temperature.
|
||||
//
|
||||
|
||||
/*
|
||||
* If the module config wants to showFeelsLike... default to the current temperature.
|
||||
* Check for EC wind chill and humidex values and overwrite the feelsLikeTemp value.
|
||||
* This assumes that the EC current conditions will never contain both a wind chill
|
||||
* and humidex temperature.
|
||||
*/
|
||||
if (this.config.showFeelsLike) {
|
||||
currentWeather.feelsLikeTemp = currentWeather.temperature;
|
||||
|
||||
|
@ -188,16 +181,10 @@ WeatherProvider.register("envcanada", {
|
|||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Need to map EC weather icon to MM weatherType values
|
||||
//
|
||||
|
||||
currentWeather.weatherType = this.convertWeatherType(ECdoc.querySelector("siteData currentConditions iconCode").textContent);
|
||||
|
||||
//
|
||||
// Capture the sunrise and sunset values from EC data
|
||||
//
|
||||
|
||||
const sunList = ECdoc.querySelectorAll("siteData riseSet dateTime");
|
||||
|
||||
currentWeather.sunrise = moment(sunList[1].querySelector("timeStamp").textContent, "YYYYMMDDhhmmss");
|
||||
|
@ -206,13 +193,11 @@ WeatherProvider.register("envcanada", {
|
|||
return currentWeather;
|
||||
},
|
||||
|
||||
//
|
||||
// Generate an array of WeatherObjects based on EC weather forecast
|
||||
//
|
||||
|
||||
/*
|
||||
* Generate an array of WeatherObjects based on EC weather forecast
|
||||
*/
|
||||
generateWeatherObjectsFromForecast (ECdoc) {
|
||||
// Declare an array to hold each day's forecast object
|
||||
|
||||
const days = [];
|
||||
|
||||
const weather = new WeatherObject();
|
||||
|
@ -226,37 +211,33 @@ WeatherProvider.register("envcanada", {
|
|||
|
||||
weather.precipitationAmount = null;
|
||||
|
||||
//
|
||||
// The EC forecast is held in a 12-element array - Elements 0 to 11 - with each day encompassing
|
||||
// 2 elements. the first element for a day details the Today (daytime) forecast while the second
|
||||
// element details the Tonight (nightime) forecast. Element 0 is always for the current day.
|
||||
//
|
||||
// However... the forecast is somewhat 'rolling'.
|
||||
//
|
||||
// If the EC forecast is queried in the morning, then Element 0 will contain Current
|
||||
// Today and Element 1 will contain Current Tonight. From there, the next 5 days of forecast will be
|
||||
// contained in Elements 2/3, 4/5, 6/7, 8/9, and 10/11. This module will create a 6-day forecast using
|
||||
// all of these Elements.
|
||||
//
|
||||
// But, if the EC forecast is queried in late afternoon, the Current Today forecast will be rolled
|
||||
// off and Element 0 will contain Current Tonight. From there, the next 5 days will be contained in
|
||||
// Elements 1/2, 3/4, 5/6, 7/8, and 9/10. As well, Elelement 11 will contain a forecast for a 6th day,
|
||||
// but only for the Today portion (not Tonight). This module will create a 6-day forecast using
|
||||
// Elements 0 to 11, and will ignore the additional Todat forecast in Element 11.
|
||||
//
|
||||
// We need to determine if Element 0 is showing the forecast for Current Today or Current Tonight.
|
||||
// This is required to understand how Min and Max temperature will be determined, and to understand
|
||||
// where the next day's (aka Tomorrow's) forecast is located in the forecast array.
|
||||
//
|
||||
|
||||
/*
|
||||
* The EC forecast is held in a 12-element array - Elements 0 to 11 - with each day encompassing
|
||||
* 2 elements. the first element for a day details the Today (daytime) forecast while the second
|
||||
* element details the Tonight (nightime) forecast. Element 0 is always for the current day.
|
||||
*
|
||||
* However... the forecast is somewhat 'rolling'.
|
||||
*
|
||||
* If the EC forecast is queried in the morning, then Element 0 will contain Current
|
||||
* Today and Element 1 will contain Current Tonight. From there, the next 5 days of forecast will be
|
||||
* contained in Elements 2/3, 4/5, 6/7, 8/9, and 10/11. This module will create a 6-day forecast using
|
||||
* all of these Elements.
|
||||
*
|
||||
* But, if the EC forecast is queried in late afternoon, the Current Today forecast will be rolled
|
||||
* off and Element 0 will contain Current Tonight. From there, the next 5 days will be contained in
|
||||
* Elements 1/2, 3/4, 5/6, 7/8, and 9/10. As well, Elelement 11 will contain a forecast for a 6th day,
|
||||
* but only for the Today portion (not Tonight). This module will create a 6-day forecast using
|
||||
* Elements 0 to 11, and will ignore the additional Todat forecast in Element 11.
|
||||
*
|
||||
* We need to determine if Element 0 is showing the forecast for Current Today or Current Tonight.
|
||||
* This is required to understand how Min and Max temperature will be determined, and to understand
|
||||
* where the next day's (aka Tomorrow's) forecast is located in the forecast array.
|
||||
*/
|
||||
let nextDay = 0;
|
||||
let lastDay = 0;
|
||||
const currentTemp = ECdoc.querySelector("siteData currentConditions temperature").textContent;
|
||||
|
||||
//
|
||||
// If the first Element is Current Today, look at Current Today and Current Tonight for the current day.
|
||||
//
|
||||
|
||||
if (foreGroup[0].querySelector("period[textForecastName='Today']")) {
|
||||
this.todaytempCacheMin = 0;
|
||||
this.todaytempCacheMax = 0;
|
||||
|
@ -266,167 +247,144 @@ WeatherProvider.register("envcanada", {
|
|||
|
||||
this.setPrecipitation(weather, foreGroup, 0);
|
||||
|
||||
//
|
||||
// Set the Element number that will reflect where the next day's forecast is located. Also set
|
||||
// the Element number where the end of the forecast will be. This is important because of the
|
||||
// rolling nature of the EC forecast. In the current scenario (Today and Tonight are present
|
||||
// in elements 0 and 11, we know that we will have 6 full days of forecasts and we will use
|
||||
// them. We will set lastDay such that we iterate through all 12 elements of the forecast.
|
||||
//
|
||||
|
||||
/*
|
||||
* Set the Element number that will reflect where the next day's forecast is located. Also set
|
||||
* the Element number where the end of the forecast will be. This is important because of the
|
||||
* rolling nature of the EC forecast. In the current scenario (Today and Tonight are present
|
||||
* in elements 0 and 11, we know that we will have 6 full days of forecasts and we will use
|
||||
* them. We will set lastDay such that we iterate through all 12 elements of the forecast.
|
||||
*/
|
||||
nextDay = 2;
|
||||
lastDay = 12;
|
||||
}
|
||||
|
||||
//
|
||||
// If the first Element is Current Tonight, look at Tonight only for the current day.
|
||||
//
|
||||
if (foreGroup[0].querySelector("period[textForecastName='Tonight']")) {
|
||||
this.setMinMaxTemps(weather, foreGroup, 0, false, currentTemp);
|
||||
|
||||
this.setPrecipitation(weather, foreGroup, 0);
|
||||
|
||||
//
|
||||
// Set the Element number that will reflect where the next day's forecast is located. Also set
|
||||
// the Element number where the end of the forecast will be. This is important because of the
|
||||
// rolling nature of the EC forecast. In the current scenario (only Current Tonight is present
|
||||
// in Element 0, we know that we will have 6 full days of forecasts PLUS a half-day and
|
||||
// forecast in the final element. Because we will only use full day forecasts, we set the
|
||||
// lastDay number to ensure we ignore that final half-day (in forecast Element 11).
|
||||
//
|
||||
|
||||
/*
|
||||
* Set the Element number that will reflect where the next day's forecast is located. Also set
|
||||
* the Element number where the end of the forecast will be. This is important because of the
|
||||
* rolling nature of the EC forecast. In the current scenario (only Current Tonight is present
|
||||
* in Element 0, we know that we will have 6 full days of forecasts PLUS a half-day and
|
||||
* forecast in the final element. Because we will only use full day forecasts, we set the
|
||||
* lastDay number to ensure we ignore that final half-day (in forecast Element 11).
|
||||
*/
|
||||
nextDay = 1;
|
||||
lastDay = 11;
|
||||
}
|
||||
|
||||
//
|
||||
// Need to map EC weather icon to MM weatherType values. Always pick the first Element's icon to
|
||||
// reflect either Today or Tonight depending on what the forecast is showing in Element 0.
|
||||
//
|
||||
|
||||
/*
|
||||
* Need to map EC weather icon to MM weatherType values. Always pick the first Element's icon to
|
||||
* reflect either Today or Tonight depending on what the forecast is showing in Element 0.
|
||||
*/
|
||||
weather.weatherType = this.convertWeatherType(foreGroup[0].querySelector("abbreviatedForecast iconCode").textContent);
|
||||
|
||||
// Push the weather object into the forecast array.
|
||||
|
||||
days.push(weather);
|
||||
|
||||
//
|
||||
// Now do the rest of the forecast starting at nextDay. We will process each day using 2 EC
|
||||
// forecast Elements. This will address the fact that the EC forecast always includes Today and
|
||||
// Tonight for each day. This is why we iterate through the forecast by a a count of 2, with each
|
||||
// iteration looking at the current Element and the next Element.
|
||||
//
|
||||
|
||||
/*
|
||||
* Now do the rest of the forecast starting at nextDay. We will process each day using 2 EC
|
||||
* forecast Elements. This will address the fact that the EC forecast always includes Today and
|
||||
* Tonight for each day. This is why we iterate through the forecast by a a count of 2, with each
|
||||
* iteration looking at the current Element and the next Element.
|
||||
*/
|
||||
let lastDate = moment(baseDate, "YYYYMMDDhhmmss");
|
||||
|
||||
for (let stepDay = nextDay; stepDay < lastDay; stepDay += 2) {
|
||||
let weather = new WeatherObject();
|
||||
|
||||
// Add 1 to the date to reflect the current forecast day we are building
|
||||
|
||||
lastDate = lastDate.add(1, "day");
|
||||
weather.date = moment(lastDate);
|
||||
|
||||
// Capture the temperatures for the current Element and the next Element in order to set
|
||||
// the Min and Max temperatures for the forecast
|
||||
|
||||
/*
|
||||
* Capture the temperatures for the current Element and the next Element in order to set
|
||||
* the Min and Max temperatures for the forecast
|
||||
*/
|
||||
this.setMinMaxTemps(weather, foreGroup, stepDay, true, currentTemp);
|
||||
|
||||
weather.precipitationAmount = null;
|
||||
|
||||
this.setPrecipitation(weather, foreGroup, stepDay);
|
||||
|
||||
//
|
||||
// Need to map EC weather icon to MM weatherType values. Always pick the first Element icon.
|
||||
//
|
||||
|
||||
weather.weatherType = this.convertWeatherType(foreGroup[stepDay].querySelector("abbreviatedForecast iconCode").textContent);
|
||||
|
||||
// Push the weather object into the forecast array.
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
//
|
||||
// Generate an array of WeatherObjects based on EC hourly weather forecast
|
||||
//
|
||||
|
||||
/*
|
||||
* Generate an array of WeatherObjects based on EC hourly weather forecast
|
||||
*/
|
||||
generateWeatherObjectsFromHourly (ECdoc) {
|
||||
// Declare an array to hold each hour's forecast object
|
||||
|
||||
const hours = [];
|
||||
|
||||
// Get local timezone UTC offset so that each hourly time can be calculated properly
|
||||
|
||||
const baseHours = ECdoc.querySelectorAll("siteData hourlyForecastGroup dateTime");
|
||||
const hourOffset = baseHours[1].getAttribute("UTCOffset");
|
||||
|
||||
//
|
||||
// The EC hourly forecast is held in a 24-element array - Elements 0 to 23 - with Element 0 holding
|
||||
// the forecast for the next 'on the hour' timeslot. This means the array is a rolling 24 hours.
|
||||
//
|
||||
|
||||
/*
|
||||
* The EC hourly forecast is held in a 24-element array - Elements 0 to 23 - with Element 0 holding
|
||||
* the forecast for the next 'on the hour' timeslot. This means the array is a rolling 24 hours.
|
||||
*/
|
||||
const hourGroup = ECdoc.querySelectorAll("siteData hourlyForecastGroup hourlyForecast");
|
||||
|
||||
for (let stepHour = 0; stepHour < 24; stepHour += 1) {
|
||||
const weather = new WeatherObject();
|
||||
|
||||
// Determine local time by applying UTC offset to the forecast timestamp
|
||||
|
||||
const foreTime = moment(hourGroup[stepHour].getAttribute("dateTimeUTC"), "YYYYMMDDhhmmss");
|
||||
const currTime = foreTime.add(hourOffset, "hours");
|
||||
weather.date = moment(currTime);
|
||||
|
||||
// Capture the temperature
|
||||
|
||||
weather.temperature = hourGroup[stepHour].querySelector("temperature").textContent;
|
||||
|
||||
// Capture Likelihood of Precipitation (LOP) and unit-of-measure values
|
||||
|
||||
const precipLOP = hourGroup[stepHour].querySelector("lop").textContent * 1.0;
|
||||
|
||||
if (precipLOP > 0) {
|
||||
weather.precipitationProbability = precipLOP;
|
||||
}
|
||||
|
||||
//
|
||||
// Need to map EC weather icon to MM weatherType values. Always pick the first Element icon.
|
||||
//
|
||||
|
||||
weather.weatherType = this.convertWeatherType(hourGroup[stepHour].querySelector("iconCode").textContent);
|
||||
|
||||
// Push the weather object into the forecast array.
|
||||
|
||||
hours.push(weather);
|
||||
}
|
||||
|
||||
return hours;
|
||||
},
|
||||
//
|
||||
// Determine Min and Max temp based on a supplied Forecast Element index and a boolen that denotes if
|
||||
// the next Forecast element should be considered - i.e. look at Today *and* Tonight vs.Tonight-only
|
||||
//
|
||||
|
||||
/*
|
||||
* Determine Min and Max temp based on a supplied Forecast Element index and a boolen that denotes if
|
||||
* the next Forecast element should be considered - i.e. look at Today *and* Tonight vs.Tonight-only
|
||||
*/
|
||||
setMinMaxTemps (weather, foreGroup, today, fullDay, currentTemp) {
|
||||
const todayTemp = foreGroup[today].querySelector("temperatures temperature").textContent;
|
||||
|
||||
const todayClass = foreGroup[today].querySelector("temperatures temperature").getAttribute("class");
|
||||
|
||||
//
|
||||
// The following logic is largely aimed at accommodating the Current day's forecast whereby we
|
||||
// can have either Current Today+Current Tonight or only Current Tonight.
|
||||
//
|
||||
// If fullDay is false, then we only have Tonight for the current day's forecast - meaning we have
|
||||
// lost a min or max temp value for the day. Therefore, we will see if we were able to cache the the
|
||||
// Today forecast for the current day. If we have, we will use them. If we do not have the cached values,
|
||||
// it means that MM or the Computer has been restarted since the time EC rolled off Today from the
|
||||
// forecast. In this scenario, we will simply default to the Current Conditions temperature and then
|
||||
// check the Tonight temperature.
|
||||
//
|
||||
|
||||
/*
|
||||
* The following logic is largely aimed at accommodating the Current day's forecast whereby we
|
||||
* can have either Current Today+Current Tonight or only Current Tonight.
|
||||
*
|
||||
* If fullDay is false, then we only have Tonight for the current day's forecast - meaning we have
|
||||
* lost a min or max temp value for the day. Therefore, we will see if we were able to cache the the
|
||||
* Today forecast for the current day. If we have, we will use them. If we do not have the cached values,
|
||||
* it means that MM or the Computer has been restarted since the time EC rolled off Today from the
|
||||
* forecast. In this scenario, we will simply default to the Current Conditions temperature and then
|
||||
* check the Tonight temperature.x
|
||||
*/
|
||||
if (fullDay === false) {
|
||||
if (this.todayCached === true) {
|
||||
weather.minTemperature = this.todayTempCacheMin;
|
||||
|
@ -437,13 +395,12 @@ WeatherProvider.register("envcanada", {
|
|||
}
|
||||
}
|
||||
|
||||
//
|
||||
// We will check to see if the current Element's temperature is Low or High and set weather values
|
||||
// accordingly. We will also check the condition where fullDay is true *and* we are looking at forecast
|
||||
// element 0. This is a special case where we will cache temperature values so that we have them later
|
||||
// in the current day when the Current Today element rolls off and we have Current Tonight only.
|
||||
//
|
||||
|
||||
/*
|
||||
* We will check to see if the current Element's temperature is Low or High and set weather values
|
||||
* accordingly. We will also check the condition where fullDay is true *and* we are looking at forecast
|
||||
* element 0. This is a special case where we will cache temperature values so that we have them later
|
||||
* in the current day when the Current Today element rolls off and we have Current Tonight only.
|
||||
*/
|
||||
if (todayClass === "low") {
|
||||
weather.minTemperature = todayTemp;
|
||||
if (today === 0 && fullDay === true) {
|
||||
|
@ -473,25 +430,24 @@ WeatherProvider.register("envcanada", {
|
|||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Check for a Precipitation forecast. EC can provide a forecast in 2 ways: either an accumulation figure
|
||||
// or a POP percentage. If there is a POP, then that is what the module will show. If there is an accumulation,
|
||||
// then it will be displayed ONLY if no POP is present.
|
||||
//
|
||||
// POP Logic: By default, we want to show the POP for 'daytime' since we are presuming that is what
|
||||
// people are more interested in seeing. While EC provides a separate POP for daytime and nightime portions
|
||||
// of each day, the weather module does not really allow for that view of a daily forecast. There we will
|
||||
// ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
|
||||
// the nightime forecast after a certain point in the afternoon. As such, we will be showing the nightime POP
|
||||
// (if one exists) in that specific scenario.
|
||||
//
|
||||
// Accumulation Logic: Similar to POP, we want to show accumulation for 'daytime' since we presume that is what
|
||||
// people are interested in seeing. While EC provides a separate accumulation for daytime and nightime portions
|
||||
// of each day, the weather module does not really allow for that view of a daily forecast. There we will
|
||||
// ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
|
||||
// the nightime forecast after a certain point in that specific scenario.
|
||||
//
|
||||
|
||||
/*
|
||||
* Check for a Precipitation forecast. EC can provide a forecast in 2 ways: either an accumulation figure
|
||||
* or a POP percentage. If there is a POP, then that is what the module will show. If there is an accumulation,
|
||||
* then it will be displayed ONLY if no POP is present.
|
||||
*
|
||||
* POP Logic: By default, we want to show the POP for 'daytime' since we are presuming that is what
|
||||
* people are more interested in seeing. While EC provides a separate POP for daytime and nightime portions
|
||||
* of each day, the weather module does not really allow for that view of a daily forecast. There we will
|
||||
* ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
|
||||
* the nightime forecast after a certain point in the afternoon. As such, we will be showing the nightime POP
|
||||
* (if one exists) in that specific scenario.
|
||||
*
|
||||
* Accumulation Logic: Similar to POP, we want to show accumulation for 'daytime' since we presume that is what
|
||||
* people are interested in seeing. While EC provides a separate accumulation for daytime and nightime portions
|
||||
* of each day, the weather module does not really allow for that view of a daily forecast. There we will
|
||||
* ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
|
||||
* the nightime forecast after a certain point in that specific scenario.
|
||||
*/
|
||||
setPrecipitation (weather, foreGroup, today) {
|
||||
if (foreGroup[today].querySelector("precipitation accumulation")) {
|
||||
weather.precipitationAmount = foreGroup[today].querySelector("precipitation accumulation amount").textContent * 1.0;
|
||||
|
@ -505,9 +461,9 @@ WeatherProvider.register("envcanada", {
|
|||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Convert the icons to a more usable name.
|
||||
//
|
||||
/*
|
||||
* Convert the icons to a more usable name.
|
||||
*/
|
||||
convertWeatherType (weatherType) {
|
||||
const weatherTypes = {
|
||||
"00": "day-sunny",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* This class is a provider for Open-Meteo,
|
||||
/*
|
||||
* This class is a provider for Open-Meteo,
|
||||
* see https://open-meteo.com/
|
||||
*/
|
||||
|
||||
|
@ -9,8 +10,11 @@ const GEOCODE_BASE = "https://api.bigdatacloud.net/data/reverse-geocode-client";
|
|||
const OPEN_METEO_BASE = "https://api.open-meteo.com/v1";
|
||||
|
||||
WeatherProvider.register("openmeteo", {
|
||||
// Set the name of the provider.
|
||||
// Not strictly required, but helps for debugging.
|
||||
|
||||
/*
|
||||
* Set the name of the provider.
|
||||
* Not strictly required, but helps for debugging.
|
||||
*/
|
||||
providerName: "Open-Meteo",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* This class is a provider for Openweathermap,
|
||||
/*
|
||||
* This class is a provider for Openweathermap,
|
||||
* see https://openweathermap.org/
|
||||
*/
|
||||
WeatherProvider.register("openweathermap", {
|
||||
// Set the name of the provider.
|
||||
// This isn't strictly necessary, since it will fallback to the provider identifier
|
||||
// But for debugging (and future alerts) it would be nice to have the real name.
|
||||
|
||||
/*
|
||||
* Set the name of the provider.
|
||||
* This isn't strictly necessary, since it will fallback to the provider identifier
|
||||
* But for debugging (and future alerts) it would be nice to have the real name.
|
||||
*/
|
||||
providerName: "OpenWeatherMap",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiVersion: "2.5",
|
||||
apiVersion: "3.0",
|
||||
apiBase: "https://api.openweathermap.org/data/",
|
||||
weatherEndpoint: "", // can be "onecall", "forecast" or "weather" (for current)
|
||||
locationID: false,
|
||||
|
@ -67,8 +71,11 @@ WeatherProvider.register("openweathermap", {
|
|||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
|
||||
/*
|
||||
* Did not receive usable new data.
|
||||
* Maybe this needs a better check?
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -206,8 +213,10 @@ WeatherProvider.register("openweathermap", {
|
|||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||
}
|
||||
|
||||
// the same day as before
|
||||
// add values from forecast to corresponding variables
|
||||
/*
|
||||
* the same day as before
|
||||
* add values from forecast to corresponding variables
|
||||
*/
|
||||
minTemp.push(forecast.main.temp_min);
|
||||
maxTemp.push(forecast.main.temp_max);
|
||||
|
||||
|
@ -220,8 +229,10 @@ WeatherProvider.register("openweathermap", {
|
|||
}
|
||||
}
|
||||
|
||||
// last day
|
||||
// calculate minimum/maximum temperature, specify rain amount
|
||||
/*
|
||||
* last day
|
||||
* calculate minimum/maximum temperature, specify rain amount
|
||||
*/
|
||||
weather.minTemperature = Math.min.apply(null, minTemp);
|
||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||
weather.rain = rain;
|
||||
|
@ -250,14 +261,18 @@ WeatherProvider.register("openweathermap", {
|
|||
weather.rain = 0;
|
||||
weather.snow = 0;
|
||||
|
||||
// forecast.rain not available if amount is zero
|
||||
// The API always returns in millimeters
|
||||
/*
|
||||
* forecast.rain not available if amount is zero
|
||||
* The API always returns in millimeters
|
||||
*/
|
||||
if (forecast.hasOwnProperty("rain") && !isNaN(forecast.rain)) {
|
||||
weather.rain = forecast.rain;
|
||||
}
|
||||
|
||||
// forecast.snow not available if amount is zero
|
||||
// The API always returns in millimeters
|
||||
/*
|
||||
* forecast.snow not available if amount is zero
|
||||
* The API always returns in millimeters
|
||||
*/
|
||||
if (forecast.hasOwnProperty("snow") && !isNaN(forecast.snow)) {
|
||||
weather.snow = forecast.snow;
|
||||
}
|
||||
|
@ -402,7 +417,8 @@ WeatherProvider.register("openweathermap", {
|
|||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
},
|
||||
|
||||
/* getParams(compliments)
|
||||
/*
|
||||
* getParams(compliments)
|
||||
* Generates an url with api parameters based on the config.
|
||||
*
|
||||
* return String - URL params.
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* This class is a provider for Pirate Weather, it is a replacement for Dark Sky (same api),
|
||||
/*
|
||||
* This class is a provider for Pirate Weather, it is a replacement for Dark Sky (same api),
|
||||
* see http://pirateweather.net/en/latest/
|
||||
*/
|
||||
WeatherProvider.register("pirateweather", {
|
||||
// Set the name of the provider.
|
||||
// Not strictly required, but helps for debugging.
|
||||
|
||||
/*
|
||||
* Set the name of the provider.
|
||||
* Not strictly required, but helps for debugging.
|
||||
*/
|
||||
providerName: "pirateweather",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* This class is a provider for SMHI (Sweden only).
|
||||
/*
|
||||
* This class is a provider for SMHI (Sweden only).
|
||||
* Metric system is the only supported unit,
|
||||
* see https://www.smhi.se/
|
||||
*/
|
||||
|
@ -138,9 +139,11 @@ WeatherProvider.register("smhi", {
|
|||
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), currentWeather.isDayTime());
|
||||
currentWeather.feelsLikeTemp = this.calculateApparentTemperature(weatherData);
|
||||
|
||||
// Determine the precipitation amount and category and update the
|
||||
// weatherObject with it, the valuetype to use can be configured or uses
|
||||
// median as default.
|
||||
/*
|
||||
* Determine the precipitation amount and category and update the
|
||||
* weatherObject with it, the valuetype to use can be configured or uses
|
||||
* median as default.
|
||||
*/
|
||||
let precipitationValue = this.paramValue(weatherData, this.config.precipitationValue);
|
||||
switch (this.paramValue(weatherData, "pcat")) {
|
||||
// 0 = No precipitation
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||
|
||||
/* This class is a provider for UK Met Office Datapoint,
|
||||
/*
|
||||
* This class is a provider for UK Met Office Datapoint,
|
||||
* see https://www.metoffice.gov.uk/
|
||||
*/
|
||||
WeatherProvider.register("ukmetoffice", {
|
||||
// Set the name of the provider.
|
||||
// This isn't strictly necessary, since it will fallback to the provider identifier
|
||||
// But for debugging (and future alerts) it would be nice to have the real name.
|
||||
|
||||
/*
|
||||
* Set the name of the provider.
|
||||
* This isn't strictly necessary, since it will fallback to the provider identifier
|
||||
* But for debugging (and future alerts) it would be nice to have the real name.
|
||||
*/
|
||||
providerName: "UK Met Office",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
|
@ -21,8 +25,11 @@ WeatherProvider.register("ukmetoffice", {
|
|||
this.fetchData(this.getUrl("3hourly"))
|
||||
.then((data) => {
|
||||
if (!data || !data.SiteRep || !data.SiteRep.DV || !data.SiteRep.DV.Location || !data.SiteRep.DV.Location.Period || data.SiteRep.DV.Location.Period.length === 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
|
||||
/*
|
||||
* Did not receive usable new data.
|
||||
* Maybe this needs a better check?
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -42,8 +49,11 @@ WeatherProvider.register("ukmetoffice", {
|
|||
this.fetchData(this.getUrl("daily"))
|
||||
.then((data) => {
|
||||
if (!data || !data.SiteRep || !data.SiteRep.DV || !data.SiteRep.DV.Location || !data.SiteRep.DV.Location.Period || data.SiteRep.DV.Location.Period.length === 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
|
||||
/*
|
||||
* Did not receive usable new data.
|
||||
* Maybe this needs a better check?
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -86,8 +96,11 @@ WeatherProvider.register("ukmetoffice", {
|
|||
if (periodDate.isSameOrAfter(moment.utc().startOf("day"))) {
|
||||
// check this is the period we want, after today the diff will be -ve
|
||||
if (moment().diff(periodDate, "minutes") > 0) {
|
||||
// loop round the reports looking for the one we are in
|
||||
// $ value specifies the time in minutes-of-the-day: 0, 180, 360,...1260
|
||||
|
||||
/*
|
||||
* loop round the reports looking for the one we are in
|
||||
* $ value specifies the time in minutes-of-the-day: 0, 180, 360,...1260
|
||||
*/
|
||||
for (const rep of period.Rep) {
|
||||
const p = rep.$;
|
||||
if (timeInMins >= p && timeInMins - 180 < p) {
|
||||
|
@ -117,8 +130,10 @@ WeatherProvider.register("ukmetoffice", {
|
|||
generateWeatherObjectsFromForecast (forecasts) {
|
||||
const days = [];
|
||||
|
||||
// loop round the (5) periods getting the data
|
||||
// for each period array, Day is [0], Night is [1]
|
||||
/*
|
||||
* loop round the (5) periods getting the data
|
||||
* for each period array, Day is [0], Night is [1]
|
||||
*/
|
||||
for (const period of forecasts.SiteRep.DV.Location.Period) {
|
||||
const weather = new WeatherObject();
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* This class is a provider for UK Met Office Data Hub (the replacement for their Data Point services).
|
||||
/*
|
||||
* This class is a provider for UK Met Office Data Hub (the replacement for their Data Point services).
|
||||
* For more information on Data Hub, see https://www.metoffice.gov.uk/services/data/datapoint/notifications/weather-datahub
|
||||
* Data available:
|
||||
* Hourly data for next 2 days ("hourly") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-hourly.pdf
|
||||
|
@ -11,9 +12,8 @@
|
|||
* This provider requires longitude/latitude coordinates, rather than a location ID (as with the previous Met Office provider)
|
||||
* Provide the following in your config.js file:
|
||||
* weatherProvider: "ukmetofficedatahub",
|
||||
* apiBase: "https://api-metoffice.apiconnect.ibmcloud.com/metoffice/production/v0/forecasts/point/",
|
||||
* apiBase: "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/",
|
||||
* apiKey: "[YOUR API KEY]",
|
||||
* apiSecret: "[YOUR API SECRET]",
|
||||
* lat: [LATITUDE (DECIMAL)],
|
||||
* lon: [LONGITUDE (DECIMAL)]
|
||||
*
|
||||
|
@ -38,14 +38,13 @@ WeatherProvider.register("ukmetofficedatahub", {
|
|||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "https://api-metoffice.apiconnect.ibmcloud.com/metoffice/production/v0/forecasts/point/",
|
||||
apiBase: "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/",
|
||||
apiKey: "",
|
||||
apiSecret: "",
|
||||
lat: 0,
|
||||
lon: 0
|
||||
},
|
||||
|
||||
// Build URL with query strings according to DataHub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
|
||||
// Build URL with query strings according to DataHub API (https://datahub.metoffice.gov.uk/docs/f/category/site-specific/type/site-specific/api-documentation#get-/point/hourly)
|
||||
getUrl (forecastType) {
|
||||
let queryStrings = "?";
|
||||
queryStrings += `latitude=${this.config.lat}`;
|
||||
|
@ -56,14 +55,15 @@ WeatherProvider.register("ukmetofficedatahub", {
|
|||
return this.config.apiBase + (this.config.apiBase.endsWith("/") ? "" : "/") + forecastType + queryStrings;
|
||||
},
|
||||
|
||||
// Build the list of headers for the request
|
||||
// For DataHub requests, the API key/secret are sent in the headers rather than as query strings.
|
||||
// Headers defined according to Data Hub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
|
||||
/*
|
||||
* Build the list of headers for the request
|
||||
* For DataHub requests, the API key/secret are sent in the headers rather than as query strings.
|
||||
* Headers defined according to Data Hub API (https://datahub.metoffice.gov.uk/docs/f/category/site-specific/type/site-specific/api-documentation#get-/point/hourly)
|
||||
*/
|
||||
getHeaders () {
|
||||
return {
|
||||
accept: "application/json",
|
||||
"x-ibm-client-id": this.config.apiKey,
|
||||
"x-ibm-client-secret": this.config.apiSecret
|
||||
apikey: this.config.apiKey
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -81,8 +81,11 @@ WeatherProvider.register("ukmetofficedatahub", {
|
|||
.then((data) => {
|
||||
// Check data is usable
|
||||
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length === 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
|
||||
/*
|
||||
* Did not receive usable new data.
|
||||
* Maybe this needs a better check?
|
||||
*/
|
||||
Log.error("Possibly bad current/hourly data?");
|
||||
Log.error(data);
|
||||
return;
|
||||
|
@ -130,15 +133,19 @@ WeatherProvider.register("ukmetofficedatahub", {
|
|||
currentWeather.precipitationProbability = forecastDataHours[hour].probOfPrecipitation;
|
||||
currentWeather.feelsLikeTemp = forecastDataHours[hour].feelsLikeTemperature;
|
||||
|
||||
// Pass on full details, so they can be used in custom templates
|
||||
// Note the units of the supplied data when using this (see top of file)
|
||||
/*
|
||||
* Pass on full details, so they can be used in custom templates
|
||||
* Note the units of the supplied data when using this (see top of file)
|
||||
*/
|
||||
currentWeather.rawData = forecastDataHours[hour];
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the sunrise/sunset times - (still) not supplied in UK Met Office data
|
||||
// Passes {longitude, latitude} to SunCalc, could pass height to, but
|
||||
// SunCalc.getTimes doesn't take that into account
|
||||
/*
|
||||
* Determine the sunrise/sunset times - (still) not supplied in UK Met Office data
|
||||
* Passes {longitude, latitude} to SunCalc, could pass height to, but
|
||||
* SunCalc.getTimes doesn't take that into account
|
||||
*/
|
||||
currentWeather.updateSunTime(this.config.lat, this.config.lon);
|
||||
|
||||
return currentWeather;
|
||||
|
@ -150,8 +157,11 @@ WeatherProvider.register("ukmetofficedatahub", {
|
|||
.then((data) => {
|
||||
// Check data is usable
|
||||
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length === 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
|
||||
/*
|
||||
* Did not receive usable new data.
|
||||
* Maybe this needs a better check?
|
||||
*/
|
||||
Log.error("Possibly bad forecast data?");
|
||||
Log.error(data);
|
||||
return;
|
||||
|
@ -206,8 +216,10 @@ WeatherProvider.register("ukmetofficedatahub", {
|
|||
forecastWeather.snow = forecastDataDays[day].dayProbabilityOfSnow;
|
||||
forecastWeather.feelsLikeTemp = forecastDataDays[day].dayMaxFeelsLikeTemp;
|
||||
|
||||
// Pass on full details, so they can be used in custom templates
|
||||
// Note the units of the supplied data when using this (see top of file)
|
||||
/*
|
||||
* Pass on full details, so they can be used in custom templates
|
||||
* Note the units of the supplied data when using this (see top of file)
|
||||
*/
|
||||
forecastWeather.rawData = forecastDataDays[day];
|
||||
|
||||
dailyForecasts.push(forecastWeather);
|
||||
|
@ -222,9 +234,11 @@ WeatherProvider.register("ukmetofficedatahub", {
|
|||
this.fetchedLocationName = name;
|
||||
},
|
||||
|
||||
// Match the Met Office "significant weather code" to a weathericons.css icon
|
||||
// Use: https://metoffice.apiconnect.ibmcloud.com/metoffice/production/node/264
|
||||
// and: https://erikflowers.github.io/weather-icons/
|
||||
/*
|
||||
* Match the Met Office "significant weather code" to a weathericons.css icon
|
||||
* Use: https://metoffice.apiconnect.ibmcloud.com/metoffice/production/node/264
|
||||
* and: https://erikflowers.github.io/weather-icons/
|
||||
*/
|
||||
convertWeatherType (weatherType) {
|
||||
const weatherTypes = {
|
||||
0: "night-clear",
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* This class is a provider for Weatherbit,
|
||||
/*
|
||||
* This class is a provider for Weatherbit,
|
||||
* see https://www.weatherbit.io/
|
||||
*/
|
||||
WeatherProvider.register("weatherbit", {
|
||||
// Set the name of the provider.
|
||||
// Not strictly required, but helps for debugging.
|
||||
|
||||
/*
|
||||
* Set the name of the provider.
|
||||
* Not strictly required, but helps for debugging.
|
||||
*/
|
||||
providerName: "Weatherbit",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||
|
||||
/* This class is a provider for Weatherflow.
|
||||
/*
|
||||
* This class is a provider for Weatherflow.
|
||||
* Note that the Weatherflow API does not provide snowfall.
|
||||
*/
|
||||
WeatherProvider.register("weatherflow", {
|
||||
// Set the name of the provider.
|
||||
// Not strictly required, but helps for debugging
|
||||
|
||||
/*
|
||||
* Set the name of the provider.
|
||||
* Not strictly required, but helps for debugging
|
||||
*/
|
||||
providerName: "WeatherFlow",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||
|
||||
/* Provider: weather.gov
|
||||
/*
|
||||
* Provider: weather.gov
|
||||
* https://weather-gov.github.io/api/general-faqs
|
||||
*
|
||||
* This class is a provider for weather.gov.
|
||||
|
@ -9,9 +10,12 @@
|
|||
*/
|
||||
|
||||
WeatherProvider.register("weathergov", {
|
||||
// Set the name of the provider.
|
||||
// This isn't strictly necessary, since it will fallback to the provider identifier
|
||||
// But for debugging (and future alerts) it would be nice to have the real name.
|
||||
|
||||
/*
|
||||
* Set the name of the provider.
|
||||
* This isn't strictly necessary, since it will fallback to the provider identifier
|
||||
* But for debugging (and future alerts) it would be nice to have the real name.
|
||||
*/
|
||||
providerName: "Weather.gov",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
|
@ -98,8 +102,11 @@ WeatherProvider.register("weathergov", {
|
|||
this.fetchData(this.forecastHourlyURL)
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
|
||||
/*
|
||||
* Did not receive usable new data.
|
||||
* Maybe this needs a better check?
|
||||
*/
|
||||
return;
|
||||
}
|
||||
const hourly = this.generateWeatherObjectsFromHourly(data.properties.periods);
|
||||
|
@ -282,14 +289,18 @@ WeatherProvider.register("weathergov", {
|
|||
weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime);
|
||||
}
|
||||
|
||||
// the same day as before
|
||||
// add values from forecast to corresponding variables
|
||||
/*
|
||||
* the same day as before
|
||||
* add values from forecast to corresponding variables
|
||||
*/
|
||||
minTemp.push(forecast.temperature);
|
||||
maxTemp.push(forecast.temperature);
|
||||
}
|
||||
|
||||
// last day
|
||||
// calculate minimum/maximum temperature
|
||||
/*
|
||||
* last day
|
||||
* calculate minimum/maximum temperature
|
||||
*/
|
||||
weather.minTemperature = Math.min.apply(null, minTemp);
|
||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||
|
||||
|
@ -302,8 +313,11 @@ WeatherProvider.register("weathergov", {
|
|||
* Convert the icons to a more usable name.
|
||||
*/
|
||||
convertWeatherType (weatherType, isDaytime) {
|
||||
//https://w1.weather.gov/xml/current_obs/weather.php
|
||||
// There are way too many types to create, so lets just look for certain strings
|
||||
|
||||
/*
|
||||
* https://w1.weather.gov/xml/current_obs/weather.php
|
||||
* There are way too many types to create, so lets just look for certain strings
|
||||
*/
|
||||
|
||||
if (weatherType.includes("Cloudy") || weatherType.includes("Partly")) {
|
||||
if (isDaytime) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* This class is a provider for Yr.no, a norwegian weather service.
|
||||
/*
|
||||
* This class is a provider for Yr.no, a norwegian weather service.
|
||||
* Terms of service: https://developer.yr.no/doc/TermsOfService/
|
||||
*/
|
||||
WeatherProvider.register("yr", {
|
||||
|
@ -67,8 +68,11 @@ WeatherProvider.register("yr", {
|
|||
|
||||
getWeatherData () {
|
||||
return new Promise((resolve, reject) => {
|
||||
// If a user has several Yr-modules, for instance one current and one forecast, the API calls must be synchronized across classes.
|
||||
// This is to avoid multiple similar calls to the API.
|
||||
|
||||
/*
|
||||
* If a user has several Yr-modules, for instance one current and one forecast, the API calls must be synchronized across classes.
|
||||
* This is to avoid multiple similar calls to the API.
|
||||
*/
|
||||
let shouldWait = localStorage.getItem("yrIsFetchingWeatherData");
|
||||
if (shouldWait) {
|
||||
const checkForGo = setInterval(function () {
|
||||
|
@ -201,8 +205,11 @@ WeatherProvider.register("yr", {
|
|||
},
|
||||
|
||||
getStellarData () {
|
||||
// If a user has several Yr-modules, for instance one current and one forecast, the API calls must be synchronized across classes.
|
||||
// This is to avoid multiple similar calls to the API.
|
||||
|
||||
/*
|
||||
* If a user has several Yr-modules, for instance one current and one forecast, the API calls must be synchronized across classes.
|
||||
* This is to avoid multiple similar calls to the API.
|
||||
*/
|
||||
return new Promise((resolve, reject) => {
|
||||
let shouldWait = localStorage.getItem("yrIsFetchingStellarData");
|
||||
if (shouldWait) {
|
||||
|
|
|
@ -170,12 +170,19 @@ Module.register("weather", {
|
|||
}
|
||||
|
||||
const notificationPayload = {
|
||||
currentWeather: this.weatherProvider?.currentWeatherObject?.simpleClone() ?? null,
|
||||
forecastArray: this.weatherProvider?.weatherForecastArray?.map((ar) => ar.simpleClone()) ?? [],
|
||||
hourlyArray: this.weatherProvider?.weatherHourlyArray?.map((ar) => ar.simpleClone()) ?? [],
|
||||
currentWeather: this.config.units === "imperial"
|
||||
? WeatherUtils.convertWeatherObjectToImperial(this.weatherProvider?.currentWeatherObject?.simpleClone()) ?? null
|
||||
: this.weatherProvider?.currentWeatherObject?.simpleClone() ?? null,
|
||||
forecastArray: this.config.units === "imperial"
|
||||
? this.weatherProvider?.weatherForecastArray?.map((ar) => WeatherUtils.convertWeatherObjectToImperial(ar.simpleClone())) ?? []
|
||||
: this.weatherProvider?.weatherForecastArray?.map((ar) => ar.simpleClone()) ?? [],
|
||||
hourlyArray: this.config.units === "imperial"
|
||||
? this.weatherProvider?.weatherHourlyArray?.map((ar) => WeatherUtils.convertWeatherObjectToImperial(ar.simpleClone())) ?? []
|
||||
: this.weatherProvider?.weatherHourlyArray?.map((ar) => ar.simpleClone()) ?? [],
|
||||
locationName: this.weatherProvider?.fetchedLocationName,
|
||||
providerName: this.weatherProvider.providerName
|
||||
};
|
||||
|
||||
this.sendNotification("WEATHER_UPDATED", notificationPayload);
|
||||
},
|
||||
|
||||
|
|
|
@ -30,8 +30,7 @@ const WeatherUtils = {
|
|||
let convertedValue = value;
|
||||
let conversionUnit = valueUnit;
|
||||
if (outputUnit === "imperial") {
|
||||
if (valueUnit && valueUnit.toLowerCase() === "cm") convertedValue = convertedValue * 0.3937007874;
|
||||
else convertedValue = convertedValue * 0.03937007874;
|
||||
convertedValue = this.convertPrecipitationToInch(value, valueUnit);
|
||||
conversionUnit = "in";
|
||||
} else {
|
||||
conversionUnit = valueUnit ? valueUnit : "mm";
|
||||
|
@ -40,6 +39,17 @@ const WeatherUtils = {
|
|||
return `${convertedValue.toFixed(2)} ${conversionUnit}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert precipitation value into inch
|
||||
* @param {number} value the precipitation value for convert
|
||||
* @param {string} valueUnit can be 'mm' or 'cm'
|
||||
* @returns {number} the converted precipitation value
|
||||
*/
|
||||
convertPrecipitationToInch (value, valueUnit) {
|
||||
if (valueUnit && valueUnit.toLowerCase() === "cm") return value * 0.3937007874;
|
||||
else return value * 0.03937007874;
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert temp (from degrees C) into imperial or metric unit depending on
|
||||
* your config
|
||||
|
@ -129,6 +139,28 @@ const WeatherUtils = {
|
|||
}
|
||||
|
||||
return ((feelsLike - 32) * 5) / 9;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts the Weather Object's values into imperial unit
|
||||
* @param {object} weatherObject the weather object
|
||||
* @returns {object} the weather object with converted values to imperial
|
||||
*/
|
||||
convertWeatherObjectToImperial (weatherObject) {
|
||||
if (!weatherObject || Object.keys(weatherObject).length === 0) return null;
|
||||
|
||||
let imperialWeatherObject = { ...weatherObject };
|
||||
|
||||
if (imperialWeatherObject) {
|
||||
if (imperialWeatherObject.feelsLikeTemp) imperialWeatherObject.feelsLikeTemp = this.convertTemp(imperialWeatherObject.feelsLikeTemp, "imperial");
|
||||
if (imperialWeatherObject.maxTemperature) imperialWeatherObject.maxTemperature = this.convertTemp(imperialWeatherObject.maxTemperature, "imperial");
|
||||
if (imperialWeatherObject.minTemperature) imperialWeatherObject.minTemperature = this.convertTemp(imperialWeatherObject.minTemperature, "imperial");
|
||||
if (imperialWeatherObject.precipitationAmount) imperialWeatherObject.precipitationAmount = this.convertPrecipitationToInch(imperialWeatherObject.precipitationAmount, imperialWeatherObject.precipitationUnits);
|
||||
if (imperialWeatherObject.temperature) imperialWeatherObject.temperature = this.convertTemp(imperialWeatherObject.temperature, "imperial");
|
||||
if (imperialWeatherObject.windSpeed) imperialWeatherObject.windSpeed = this.convertWind(imperialWeatherObject.windSpeed, "imperial");
|
||||
}
|
||||
|
||||
return imperialWeatherObject;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
4214
package-lock.json
generated
4214
package-lock.json
generated
File diff suppressed because it is too large
Load diff
51
package.json
51
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "magicmirror",
|
||||
"version": "2.28.0",
|
||||
"version": "2.29.0",
|
||||
"description": "The open source modular smart mirror platform.",
|
||||
"keywords": [
|
||||
"magic mirror",
|
||||
|
@ -41,6 +41,7 @@
|
|||
"test:js": "eslint .",
|
||||
"test:css": "stylelint 'css/main.css' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.css' --config .stylelintrc.json",
|
||||
"test:calendar": "node ./modules/default/calendar/debug.js",
|
||||
"test:spelling": "cspell . --gitignore",
|
||||
"config:check": "node js/check_config.js",
|
||||
"lint:prettier": "prettier . --write",
|
||||
"lint:js": "eslint . --fix",
|
||||
|
@ -54,12 +55,12 @@
|
|||
"*.css": "stylelint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "^8.16.0",
|
||||
"ansis": "^3.2.0",
|
||||
"ajv": "^8.17.1",
|
||||
"ansis": "^3.3.2",
|
||||
"console-stamp": "^3.1.2",
|
||||
"envsub": "^4.1.0",
|
||||
"eslint": "^8.57.0",
|
||||
"express": "^4.19.2",
|
||||
"eslint": "^9.11.1",
|
||||
"express": "^4.21.0",
|
||||
"express-ipfilter": "^1.3.2",
|
||||
"feedme": "^2.0.2",
|
||||
"helmet": "^7.1.0",
|
||||
|
@ -67,36 +68,36 @@
|
|||
"iconv-lite": "^0.6.3",
|
||||
"module-alias": "^2.2.3",
|
||||
"moment": "^2.30.1",
|
||||
"node-ical": "^0.18.0",
|
||||
"pm2": "^5.4.1",
|
||||
"socket.io": "^4.7.5",
|
||||
"node-ical": "0.18.0",
|
||||
"pm2": "^5.4.2",
|
||||
"socket.io": "^4.8.0",
|
||||
"suncalc": "^1.9.0",
|
||||
"systeminformation": "^5.22.11"
|
||||
"systeminformation": "^5.23.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@stylistic/eslint-plugin": "^1.8.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jest": "^28.6.0",
|
||||
"eslint-plugin-jsdoc": "^48.5.0",
|
||||
"eslint-plugin-package-json": "^0.15.0",
|
||||
"eslint-plugin-unicorn": "^54.0.0",
|
||||
"@eslint/js": "^9.11.1",
|
||||
"@stylistic/eslint-plugin": "^2.8.0",
|
||||
"cspell": "^8.14.4",
|
||||
"eslint-plugin-jest": "^28.8.3",
|
||||
"eslint-plugin-jsdoc": "^50.3.0",
|
||||
"eslint-plugin-package-json": "^0.15.3",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"husky": "^9.0.11",
|
||||
"husky": "^9.1.6",
|
||||
"jest": "^29.7.0",
|
||||
"jsdom": "^24.1.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"playwright": "^1.45.0",
|
||||
"prettier": "^3.3.2",
|
||||
"sinon": "^18.0.0",
|
||||
"stylelint": "^16.6.1",
|
||||
"jsdom": "^25.0.1",
|
||||
"lint-staged": "^15.2.10",
|
||||
"playwright": "^1.47.2",
|
||||
"prettier": "^3.3.3",
|
||||
"sinon": "^19.0.2",
|
||||
"stylelint": "^16.9.0",
|
||||
"stylelint-config-standard": "^36.0.1",
|
||||
"stylelint-prettier": "^5.0.0"
|
||||
"stylelint-prettier": "^5.0.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"electron": "^31.1.0"
|
||||
"electron": "^31.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=20.9.0 <21 || 22"
|
||||
},
|
||||
"_moduleAliases": {
|
||||
"node_helper": "js/node_helper.js",
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
[Plymouth Theme]
|
||||
Name=MagicMirror
|
||||
Description=Mirror Splash
|
||||
ModuleName=script
|
||||
|
||||
[script]
|
||||
ImageDir=/usr/share/plymouth/themes/MagicMirror
|
||||
ScriptFile=/usr/share/plymouth/themes/MagicMirror/MagicMirror.script
|
|
@ -1,53 +0,0 @@
|
|||
screen_width = Window.GetWidth();
|
||||
screen_height = Window.GetHeight();
|
||||
|
||||
if (Plymouth.GetMode() != "shutdown")
|
||||
{
|
||||
theme_image = Image("splash.png");
|
||||
}
|
||||
else
|
||||
{
|
||||
theme_image = Image("splash_halt.png");
|
||||
}
|
||||
|
||||
image_width = theme_image.GetWidth();
|
||||
image_height = theme_image.GetHeight();
|
||||
|
||||
scale_x = image_width / screen_width;
|
||||
scale_y = image_height / screen_height;
|
||||
|
||||
if (scale_x > 1 || scale_y > 1)
|
||||
{
|
||||
if (scale_x > scale_y)
|
||||
{
|
||||
resized_image = theme_image.Scale (screen_width, image_height / scale_x);
|
||||
image_x = 0;
|
||||
image_y = (screen_height - ((image_height * screen_width) / image_width)) / 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
resized_image = theme_image.Scale (image_width / scale_y, screen_height);
|
||||
image_x = (screen_width - ((image_width * screen_height) / image_height)) / 2;
|
||||
image_y = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resized_image = theme_image.Scale (image_width, image_height);
|
||||
image_x = (screen_width - image_width) / 2;
|
||||
image_y = (screen_height - image_height) / 2;
|
||||
}
|
||||
|
||||
sprite = Sprite (resized_image);
|
||||
sprite.SetPosition (image_x, image_y, -100);
|
||||
|
||||
message_sprite = Sprite();
|
||||
message_sprite.SetPosition(screen_width * 0.1, screen_height * 0.9, 10000);
|
||||
|
||||
fun message_callback (text) {
|
||||
my_image = Image.Text(text, 1, 1, 1);
|
||||
message_sprite.SetImage(my_image);
|
||||
sprite.SetImage (resized_image);
|
||||
}
|
||||
|
||||
Plymouth.SetUpdateStatusFunction(message_callback);
|
Binary file not shown.
Before Width: | Height: | Size: 36 KiB |
Binary file not shown.
Before Width: | Height: | Size: 22 KiB |
23
tests/configs/customregions.js
Normal file
23
tests/configs/customregions.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
let config = {
|
||||
modules:
|
||||
// Using exotic content. This is why don't accept go to JSON configuration file
|
||||
(() => {
|
||||
let positions = ["row3_left", "top3_left1"];
|
||||
let modules = Array();
|
||||
for (let idx in positions) {
|
||||
modules.push({
|
||||
module: "helloworld",
|
||||
position: positions[idx],
|
||||
config: {
|
||||
text: `Text in ${positions[idx]}`
|
||||
}
|
||||
});
|
||||
}
|
||||
return modules;
|
||||
})()
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
/* MagicMirror² Test calendar exdate
|
||||
/*
|
||||
* MagicMirror² Test calendar exdate
|
||||
*
|
||||
* By jkriegshauser
|
||||
* MIT Licensed.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* MagicMirror² Test calendar exdate
|
||||
/*
|
||||
* MagicMirror² Test calendar exdate
|
||||
*
|
||||
* By jkriegshauser
|
||||
* MIT Licensed.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* MagicMirror² Test calendar exdate
|
||||
/*
|
||||
* MagicMirror² Test calendar exdate
|
||||
*
|
||||
* By jkriegshauser
|
||||
* MIT Licensed.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* MagicMirror² Test calendar exdate
|
||||
/*
|
||||
* MagicMirror² Test calendar exdate
|
||||
*
|
||||
* By jkriegshauser
|
||||
* MIT Licensed.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* MagicMirror² Test calendar exdate
|
||||
/*
|
||||
* MagicMirror² Test calendar exdate
|
||||
*
|
||||
* By jkriegshauser
|
||||
* MIT Licensed.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* MagicMirror² Test calendar exdate
|
||||
/*
|
||||
* MagicMirror² Test calendar exdate
|
||||
*
|
||||
* By jkriegshauser
|
||||
* MIT Licensed.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* MagicMirror² Test config for fullday calendar entries over multiple days
|
||||
/*
|
||||
* MagicMirror² Test config for fullday calendar entries over multiple days
|
||||
*
|
||||
* By Paranoid93 https://github.com/Paranoid93/
|
||||
* MIT Licensed.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* MagicMirror² Test config for fullday calendar entries over multiple days
|
||||
/*
|
||||
* MagicMirror² Test config for fullday calendar entries over multiple days
|
||||
*
|
||||
* By Paranoid93 https://github.com/Paranoid93/
|
||||
* MIT Licensed.
|
||||
|
|
30
tests/configs/modules/calendar/sliceMultiDayEvents.js
Normal file
30
tests/configs/modules/calendar/sliceMultiDayEvents.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
let config = {
|
||||
timeFormat: 12,
|
||||
|
||||
modules: [
|
||||
{
|
||||
module: "calendar",
|
||||
position: "bottom_bar",
|
||||
config: {
|
||||
hideDuplicates: false,
|
||||
maximumEntries: 100,
|
||||
sliceMultiDayEvents: true,
|
||||
calendars: [
|
||||
{
|
||||
maximumEntries: 100,
|
||||
url: "http://localhost:8080/tests/mocks/sliceMultiDayEvents.ics"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
Date.now = () => {
|
||||
return new Date("01 Sept 2024 10:38:00 GMT+2:00").valueOf();
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
18
tests/configs/modules/compliments/compliments_cron_entry.js
Normal file
18
tests/configs/modules/compliments/compliments_cron_entry.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
let config = {
|
||||
modules: [
|
||||
{
|
||||
module: "compliments",
|
||||
position: "middle_center",
|
||||
config: {
|
||||
specialDayUnique: true,
|
||||
compliments: {
|
||||
anytime: ["just a test"],
|
||||
"00-10 16-19 * * fri": ["just pub time"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") { module.exports = config; }
|
|
@ -0,0 +1,18 @@
|
|||
let config = {
|
||||
modules: [
|
||||
{
|
||||
module: "compliments",
|
||||
position: "middle_center",
|
||||
config: {
|
||||
specialDayUnique: true,
|
||||
compliments: {
|
||||
anytime: ["just a test"],
|
||||
"* * * * *": ["anytime cron"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") { module.exports = config; }
|
|
@ -5,7 +5,7 @@ describe("AnimateCSS integration Test", () => {
|
|||
let testConfigFile = "tests/configs/modules/compliments/compliments_animateCSS.js";
|
||||
// define config file to fallback to default: wrong animation name (must return no animation)
|
||||
let testConfigFileFallbackToDefault = "tests/configs/modules/compliments/compliments_animateCSS_fallbackToDefault.js";
|
||||
// define config file with an inversed name animation : in for out and vice versa (must return no animation)
|
||||
// define config file with an inverted name animation : in for out and vice versa (must return no animation)
|
||||
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";
|
||||
|
|
30
tests/e2e/custom_module_regions_spec.js
Normal file
30
tests/e2e/custom_module_regions_spec.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
const helpers = require("./helpers/global-setup");
|
||||
|
||||
describe("Custom Position of modules", () => {
|
||||
beforeAll(async () => {
|
||||
await helpers.fixupIndex();
|
||||
await helpers.startApplication("tests/configs/customregions.js");
|
||||
await helpers.getDocument();
|
||||
});
|
||||
afterAll(async () => {
|
||||
await helpers.stopApplication();
|
||||
await helpers.restoreIndex();
|
||||
});
|
||||
|
||||
const positions = ["row3_left", "top3_left1"];
|
||||
let i = 0;
|
||||
const className1 = positions[i].replace("_", ".");
|
||||
let message1 = positions[i];
|
||||
it(`should show text in ${message1}`, async () => {
|
||||
const elem = await helpers.waitForElement(`.${className1}`);
|
||||
expect(elem).not.toBeNull();
|
||||
expect(elem.textContent).toContain(`Text in ${message1}`);
|
||||
});
|
||||
i = 1;
|
||||
const className2 = positions[i].replace("_", ".");
|
||||
let message2 = positions[i];
|
||||
it(`should NOT show text in ${message2}`, async () => {
|
||||
const elem = await helpers.waitForElement(`.${className2}`, "", 1500);
|
||||
expect(elem).toBeNull();
|
||||
}, 1510);
|
||||
});
|
|
@ -1,5 +1,20 @@
|
|||
const os = require("node:os");
|
||||
const fs = require("node:fs");
|
||||
const jsdom = require("jsdom");
|
||||
|
||||
const indexFile = `${__dirname}/../../../index.html`;
|
||||
const cssFile = `${__dirname}/../../../css/custom.css`;
|
||||
const sampleCss = [
|
||||
".region.row3 {",
|
||||
" top: 0;",
|
||||
"}",
|
||||
".region.row3.left {",
|
||||
" top: 100%;",
|
||||
"}"
|
||||
];
|
||||
var indexData = [];
|
||||
var cssData = [];
|
||||
|
||||
exports.startApplication = async (configFilename, exec) => {
|
||||
jest.resetModules();
|
||||
if (global.app) {
|
||||
|
@ -45,11 +60,12 @@ exports.getDocument = () => {
|
|||
});
|
||||
};
|
||||
|
||||
exports.waitForElement = (selector, ignoreValue = "") => {
|
||||
exports.waitForElement = (selector, ignoreValue = "", timeout = 0) => {
|
||||
return new Promise((resolve) => {
|
||||
let oldVal = "dummy12345";
|
||||
let element = null;
|
||||
const interval = setInterval(() => {
|
||||
const element = document.querySelector(selector);
|
||||
element = document.querySelector(selector);
|
||||
if (element) {
|
||||
let newVal = element.textContent;
|
||||
if (newVal === oldVal) {
|
||||
|
@ -64,6 +80,12 @@ exports.waitForElement = (selector, ignoreValue = "") => {
|
|||
}
|
||||
}
|
||||
}, 100);
|
||||
if (timeout !== 0) {
|
||||
setTimeout(() => {
|
||||
if (interval) clearInterval(interval);
|
||||
resolve(null);
|
||||
}, timeout);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -91,3 +113,34 @@ exports.testMatch = async (element, regex) => {
|
|||
expect(elem.textContent).toMatch(regex);
|
||||
return true;
|
||||
};
|
||||
|
||||
exports.fixupIndex = async () => {
|
||||
// read and save the git level index file
|
||||
indexData = (await fs.promises.readFile(indexFile)).toString();
|
||||
// make lines of the content
|
||||
let workIndexLines = indexData.split(os.EOL);
|
||||
// loop thru the lines to find place to insert new region
|
||||
for (let l in workIndexLines) {
|
||||
if (workIndexLines[l].includes("region top right")) {
|
||||
// insert a new line with new region definition
|
||||
workIndexLines.splice(l, 0, " <div class=\"region row3 left\"><div class=\"container\"></div></div>");
|
||||
break;
|
||||
}
|
||||
}
|
||||
// write out the new index.html file, not append
|
||||
await fs.promises.writeFile(indexFile, workIndexLines.join(os.EOL), { flush: true });
|
||||
// read in the current custom.css
|
||||
cssData = (await fs.promises.readFile(cssFile)).toString();
|
||||
// write out the custom.css for this testcase, matching the new region name
|
||||
await fs.promises.writeFile(cssFile, sampleCss.join(os.EOL), { flush: true });
|
||||
};
|
||||
|
||||
exports.restoreIndex = async () => {
|
||||
// if we read in data
|
||||
if (indexData.length > 1) {
|
||||
//write out saved index.html
|
||||
await fs.promises.writeFile(indexFile, indexData, { flush: true });
|
||||
// write out saved custom.css
|
||||
await fs.promises.writeFile(cssFile, cssData, { flush: true });
|
||||
}
|
||||
};
|
||||
|
|
|
@ -88,17 +88,6 @@ describe("Calendar module", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("Events from multiple calendars", () => {
|
||||
beforeAll(async () => {
|
||||
await helpers.startApplication("tests/configs/modules/calendar/show-duplicates-in-calendar.js");
|
||||
await helpers.getDocument();
|
||||
});
|
||||
|
||||
it("should show multiple events with the same title and start time from different calendars", async () => {
|
||||
await expect(testElementLength(".calendar .event", 22)).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
//Will contain everyday an fullDayEvent that starts today and ends tomorrow, and one starting tomorrow and ending the day after tomorrow
|
||||
describe("FullDayEvent over several days should show how many days are left from the from the starting date on", () => {
|
||||
beforeAll(async () => {
|
||||
|
|
|
@ -77,5 +77,16 @@ describe("Compliments module", () => {
|
|||
await expect(doTest(["Special day message"])).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cron type key", () => {
|
||||
beforeAll(async () => {
|
||||
await helpers.startApplication("tests/configs/modules/compliments/compliments_e2e_cron_entry.js");
|
||||
await helpers.getDocument();
|
||||
});
|
||||
|
||||
it("compliments array contains only special value", async () => {
|
||||
await expect(doTest(["anytime cron"])).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
const fs = require("node:fs");
|
||||
const helpers = require("../helpers/global-setup");
|
||||
|
||||
describe("Newsfeed module", () => {
|
||||
afterAll(async () => {
|
||||
await helpers.stopApplication();
|
||||
});
|
||||
|
||||
const runTests = async () => {
|
||||
describe("Default configuration", () => {
|
||||
beforeAll(async () => {
|
||||
await helpers.startApplication("tests/configs/modules/newsfeed/default.js");
|
||||
|
@ -74,4 +71,28 @@ describe("Newsfeed module", () => {
|
|||
expect(elem.textContent).toContain("No news at the moment.");
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe("Newsfeed module", () => {
|
||||
afterAll(async () => {
|
||||
await helpers.stopApplication();
|
||||
});
|
||||
|
||||
runTests();
|
||||
});
|
||||
|
||||
describe("Newsfeed module located in config directory", () => {
|
||||
beforeAll(() => {
|
||||
const baseDir = `${__dirname}/../../..`;
|
||||
if (!fs.existsSync(`${baseDir}/config/newsfeed`)) {
|
||||
fs.cpSync(`${baseDir}/modules/default/newsfeed`, `${baseDir}/config/newsfeed`, { recursive: true });
|
||||
}
|
||||
process.env.MM_MODULES_DIR = "config";
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await helpers.stopApplication();
|
||||
});
|
||||
|
||||
runTests();
|
||||
});
|
||||
|
|
|
@ -53,8 +53,8 @@ describe("Weather module: Weather Hourly Forecast", () => {
|
|||
});
|
||||
|
||||
describe("Shows precipitation probability", () => {
|
||||
const propabilities = [undefined, undefined, "12 %", "36 %", "44 %"];
|
||||
for (const [index, pop] of propabilities.entries()) {
|
||||
const probabilities = [undefined, undefined, "12 %", "36 %", "44 %"];
|
||||
for (const [index, pop] of probabilities.entries()) {
|
||||
if (pop) {
|
||||
it(`should render probability ${pop}`, async () => {
|
||||
await expect(weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-prob`, pop)).resolves.toBe(true);
|
||||
|
|
|
@ -12,7 +12,7 @@ describe("Display of modules", () => {
|
|||
it("should show the test header", async () => {
|
||||
const elem = await helpers.waitForElement("#module_0_helloworld .module-header");
|
||||
expect(elem).not.toBeNull();
|
||||
// textContent gibt hier lowercase zurück, das uppercase wird durch css realisiert, was daher nicht in textContent landet
|
||||
// textContent returns lowercase here, the uppercase is realized by CSS, which therefore does not end up in textContent
|
||||
expect(elem.textContent).toBe("test_header");
|
||||
});
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ describe("App environment", () => {
|
|||
beforeAll(async () => {
|
||||
process.env.MM_CONFIG_FILE = "tests/configs/default.js";
|
||||
serverProcess = await require("node:child_process").spawn("npm", ["run", "server"], { env: process.env, detached: true });
|
||||
// we have to wait until the server is startet
|
||||
// we have to wait until the server is started
|
||||
await delay(2000);
|
||||
});
|
||||
afterAll(async () => {
|
||||
|
|
|
@ -13,6 +13,15 @@ describe("Calendar module", () => {
|
|||
return true;
|
||||
};
|
||||
|
||||
const doTestCount = async () => {
|
||||
expect(global.page).not.toBeNull();
|
||||
const loc = await global.page.locator(".calendar .event");
|
||||
const elem = loc.first();
|
||||
await elem.waitFor();
|
||||
expect(elem).not.toBeNull();
|
||||
return await loc.count();
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
await helpers.stopApplication();
|
||||
});
|
||||
|
@ -44,112 +53,98 @@ describe("Calendar module", () => {
|
|||
});
|
||||
});
|
||||
|
||||
/****************************/
|
||||
// RRULE TESTS:
|
||||
// Add any tests that check rrule functionality here.
|
||||
describe("rrule", () => {
|
||||
it("Issue #3393 recurrence dates past rrule until date", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/calendar/rrule_until.js", "07 Mar 2024 10:38:00 GMT-07:00", ["js/electron.js"], "America/Los_Angeles");
|
||||
expect(global.page).not.toBeNull();
|
||||
const loc = await global.page.locator(".calendar .event");
|
||||
const elem = loc.first();
|
||||
await elem.waitFor();
|
||||
expect(elem).not.toBeNull();
|
||||
const cnt = await loc.count();
|
||||
expect(cnt).toBe(1);
|
||||
describe("Events from multiple calendars", () => {
|
||||
it("should show multiple events with the same title and start time from different calendars", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/calendar/show-duplicates-in-calendar.js", "15 Sep 2024 12:30:00 GMT");
|
||||
await expect(doTestCount()).resolves.toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
/****************************/
|
||||
// LOS ANGELES TESTS:
|
||||
// In 2023, DST (GMT-7) was until 5 Nov, after which is standard (STD) (GMT-8) time.
|
||||
// Test takes place on Thu 19 Oct, recurring event on a Wednesday. maximumNumberOfDays=28, so there should be
|
||||
// 4 events (25 Oct, 1 Nov, (switch to STD), 8 Nov, Nov 15), but 1 Nov and 8 Nov are excluded.
|
||||
// There are three separate tests:
|
||||
// * before midnight GMT (3pm local time)
|
||||
// * at midnight GMT in STD time (4pm local time)
|
||||
// * at midnight GMT in DST time (5pm local time)
|
||||
/*
|
||||
* RRULE TESTS:
|
||||
* Add any tests that check rrule functionality here.
|
||||
*/
|
||||
describe("rrule", () => {
|
||||
it("Issue #3393 recurrence dates past rrule until date", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/calendar/rrule_until.js", "07 Mar 2024 10:38:00 GMT-07:00", ["js/electron.js"], "America/Los_Angeles");
|
||||
await expect(doTestCount()).resolves.toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* LOS ANGELES TESTS:
|
||||
* In 2023, DST (GMT-7) was until 5 Nov, after which is standard (STD) (GMT-8) time.
|
||||
* Test takes place on Thu 19 Oct, recurring event on a Wednesday. maximumNumberOfDays=28, so there should be
|
||||
* 4 events (25 Oct, 1 Nov, (switch to STD), 8 Nov, Nov 15), but 1 Nov and 8 Nov are excluded.
|
||||
* There are three separate tests:
|
||||
* * before midnight GMT (3pm local time)
|
||||
* * at midnight GMT in STD time (4pm local time)
|
||||
* * at midnight GMT in DST time (5pm local time)
|
||||
*/
|
||||
describe("Exdate: LA crossover DST before midnight GMT", () => {
|
||||
it("LA crossover DST before midnight GMT should have 2 events", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/calendar/exdate_la_before_midnight.js", "19 Oct 2023 12:30:00 GMT-07:00", ["js/electron.js"], "America/Los_Angeles");
|
||||
expect(global.page).not.toBeNull();
|
||||
const loc = await global.page.locator(".calendar .event");
|
||||
const elem = loc.first();
|
||||
await elem.waitFor();
|
||||
expect(elem).not.toBeNull();
|
||||
const cnt = await loc.count();
|
||||
expect(cnt).toBe(2);
|
||||
await expect(doTestCount()).resolves.toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Exdate: LA crossover DST at midnight GMT local STD", () => {
|
||||
it("LA crossover DST before midnight GMT should have 2 events", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/calendar/exdate_la_at_midnight_std.js", "19 Oct 2023 12:30:00 GMT-07:00", ["js/electron.js"], "America/Los_Angeles");
|
||||
expect(global.page).not.toBeNull();
|
||||
const loc = await global.page.locator(".calendar .event");
|
||||
const elem = loc.first();
|
||||
await elem.waitFor();
|
||||
expect(elem).not.toBeNull();
|
||||
const cnt = await loc.count();
|
||||
expect(cnt).toBe(2);
|
||||
await expect(doTestCount()).resolves.toBe(2);
|
||||
});
|
||||
});
|
||||
describe("Exdate: LA crossover DST at midnight GMT local DST", () => {
|
||||
it("LA crossover DST before midnight GMT should have 2 events", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/calendar/exdate_la_at_midnight_dst.js", "19 Oct 2023 12:30:00 GMT-07:00", ["js/electron.js"], "America/Los_Angeles");
|
||||
expect(global.page).not.toBeNull();
|
||||
const loc = await global.page.locator(".calendar .event");
|
||||
const elem = loc.first();
|
||||
await elem.waitFor();
|
||||
expect(elem).not.toBeNull();
|
||||
const cnt = await loc.count();
|
||||
expect(cnt).toBe(2);
|
||||
await expect(doTestCount()).resolves.toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
/****************************/
|
||||
// SYDNEY TESTS:
|
||||
// In 2023, standard time (STD) (GMT+10) was until 1 Oct, after which is DST (GMT+11).
|
||||
// Test takes place on Thu 14 Sep, recurring event on a Wednesday. maximumNumberOfDays=28, so there should be
|
||||
// 4 events (20 Sep, 27 Sep, (switch to DST), 4 Oct, 11 Oct), but 27 Sep and 4 Oct are excluded.
|
||||
// There are three separate tests:
|
||||
// * before midnight GMT (9am local time)
|
||||
// * at midnight GMT in STD time (10am local time)
|
||||
// * at midnight GMT in DST time (11am local time)
|
||||
/*
|
||||
* SYDNEY TESTS:
|
||||
* In 2023, standard time (STD) (GMT+10) was until 1 Oct, after which is DST (GMT+11).
|
||||
* Test takes place on Thu 14 Sep, recurring event on a Wednesday. maximumNumberOfDays=28, so there should be
|
||||
* 4 events (20 Sep, 27 Sep, (switch to DST), 4 Oct, 11 Oct), but 27 Sep and 4 Oct are excluded.
|
||||
* There are three separate tests:
|
||||
* * before midnight GMT (9am local time)
|
||||
* * at midnight GMT in STD time (10am local time)
|
||||
* * at midnight GMT in DST time (11am local time)
|
||||
*/
|
||||
describe("Exdate: SYD crossover DST before midnight GMT", () => {
|
||||
it("LA crossover DST before midnight GMT should have 2 events", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/calendar/exdate_syd_before_midnight.js", "14 Sep 2023 12:30:00 GMT+10:00", ["js/electron.js"], "Australia/Sydney");
|
||||
expect(global.page).not.toBeNull();
|
||||
const loc = await global.page.locator(".calendar .event");
|
||||
const elem = loc.first();
|
||||
await elem.waitFor();
|
||||
expect(elem).not.toBeNull();
|
||||
const cnt = await loc.count();
|
||||
expect(cnt).toBe(2);
|
||||
await expect(doTestCount()).resolves.toBe(2);
|
||||
});
|
||||
});
|
||||
describe("Exdate: SYD crossover DST at midnight GMT local STD", () => {
|
||||
it("LA crossover DST before midnight GMT should have 2 events", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/calendar/exdate_syd_at_midnight_std.js", "14 Sep 2023 12:30:00 GMT+10:00", ["js/electron.js"], "Australia/Sydney");
|
||||
expect(global.page).not.toBeNull();
|
||||
const loc = await global.page.locator(".calendar .event");
|
||||
const elem = loc.first();
|
||||
await elem.waitFor();
|
||||
expect(elem).not.toBeNull();
|
||||
const cnt = await loc.count();
|
||||
expect(cnt).toBe(2);
|
||||
await expect(doTestCount()).resolves.toBe(2);
|
||||
});
|
||||
});
|
||||
describe("Exdate: SYD crossover DST at midnight GMT local DST", () => {
|
||||
it("SYD crossover DST at midnight GMT local DST should have 2 events", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/calendar/exdate_syd_at_midnight_dst.js", "14 Sep 2023 12:30:00 GMT+10:00", ["js/electron.js"], "Australia/Sydney");
|
||||
await expect(doTestCount()).resolves.toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* RRULE TESTS:
|
||||
* Add any tests that check rrule functionality here.
|
||||
*/
|
||||
describe("sliceMultiDayEvents", () => {
|
||||
it("Issue #3452 split multiday in Europe", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/calendar/sliceMultiDayEvents.js", "01 Sept 2024 10:38:00 GMT+02:00", ["js/electron.js"], "Europe/Berlin");
|
||||
expect(global.page).not.toBeNull();
|
||||
const loc = await global.page.locator(".calendar .event");
|
||||
const elem = loc.first();
|
||||
await elem.waitFor();
|
||||
expect(elem).not.toBeNull();
|
||||
const cnt = await loc.count();
|
||||
expect(cnt).toBe(2);
|
||||
expect(cnt).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -43,5 +43,41 @@ describe("Compliments module", () => {
|
|||
await expect(doTest(["Happy new year!"])).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test only custom date events shown with new property", () => {
|
||||
it("shows 'Special day message' on May 6", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/compliments/compliments_specialDayUnique_true.js", "06 May 2022 10:00:00 GMT");
|
||||
await expect(doTest(["Special day message"])).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test all date events shown without new property", () => {
|
||||
it("shows 'any message' on May 6", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/compliments/compliments_specialDayUnique_false.js", "06 May 2022 10:00:00 GMT");
|
||||
await expect(doTest(["Special day message", "Typical message 1", "Typical message 2", "Typical message 3"])).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test only custom cron date event shown with new property", () => {
|
||||
it("shows 'any message' on May 6", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/compliments/compliments_cron_entry.js", "06 May 2022 17:03:00 GMT");
|
||||
await expect(doTest(["just pub time"])).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test any event shows after time window", () => {
|
||||
it("shows 'any message' on May 6", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/compliments/compliments_cron_entry.js", "06 May 2022 17:11:00 GMT");
|
||||
await expect(doTest(["just a test"])).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test any event shows different day", () => {
|
||||
it("shows 'any message' on May 5", async () => {
|
||||
await helpers.startApplication("tests/configs/modules/compliments/compliments_cron_entry.js", "05 May 2022 17:00:00 GMT");
|
||||
await expect(doTest(["just a test"])).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
58
tests/mocks/sliceMultiDayEvents.ics
Normal file
58
tests/mocks/sliceMultiDayEvents.ics
Normal file
|
@ -0,0 +1,58 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-WR-CALNAME:Dirk Test
|
||||
X-WR-TIMEZONE:Europe/Berlin
|
||||
BEGIN:VEVENT
|
||||
DTSTART;VALUE=DATE:20240918
|
||||
DTEND;VALUE=DATE:20240919
|
||||
DTSTAMP:20240916T084410Z
|
||||
UID:2crbv1ijljc2kt9jclkgu5hqa0@google.com
|
||||
CREATED:20240916T083831Z
|
||||
LAST-MODIFIED:20240916T083831Z
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:1 day single
|
||||
TRANSP:TRANSPARENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTART;VALUE=DATE:20240919
|
||||
DTEND;VALUE=DATE:20240920
|
||||
RRULE:FREQ=YEARLY
|
||||
DTSTAMP:20240916T084410Z
|
||||
UID:6gb19havnq6vp2qput51e5rmml@google.com
|
||||
CREATED:20240916T083850Z
|
||||
LAST-MODIFIED:20240916T083850Z
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:1 day repeat
|
||||
TRANSP:TRANSPARENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTART;VALUE=DATE:20240920
|
||||
DTEND;VALUE=DATE:20240922
|
||||
DTSTAMP:20240916T084410Z
|
||||
UID:06e9u1trbqi3jbvstvq4qqutau@google.com
|
||||
CREATED:20240916T083902Z
|
||||
LAST-MODIFIED:20240916T083902Z
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:2 day single
|
||||
TRANSP:TRANSPARENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTART;VALUE=DATE:20240923
|
||||
DTEND;VALUE=DATE:20240925
|
||||
RRULE:FREQ=YEARLY
|
||||
DTSTAMP:20240916T084410Z
|
||||
UID:0ui78rk6hpcv8rmbb6nuonhmgg@google.com
|
||||
CREATED:20240916T083919Z
|
||||
LAST-MODIFIED:20240916T083919Z
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:2 day repeat
|
||||
TRANSP:TRANSPARENT
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -19,7 +19,7 @@ describe("server_functions tests", () => {
|
|||
},
|
||||
text: fetchResponseHeadersText
|
||||
};
|
||||
// eslint-disable-next-line
|
||||
|
||||
fetch = jest.fn();
|
||||
fetch.mockImplementation(() => fetchResponse);
|
||||
|
||||
|
@ -45,7 +45,7 @@ describe("server_functions tests", () => {
|
|||
expect(fetchMock.mock.calls[0][0]).toBe(urlToCall);
|
||||
});
|
||||
|
||||
it("Forewards Content-Type if json", async () => {
|
||||
it("Forwards Content-Type if json", async () => {
|
||||
fetchResponseHeadersGet.mockImplementation(() => "json");
|
||||
|
||||
await cors(request, corsResponse);
|
||||
|
@ -58,7 +58,7 @@ describe("server_functions tests", () => {
|
|||
expect(corsResponse.set.mock.calls[0][1]).toBe("json");
|
||||
});
|
||||
|
||||
it("Forewards Content-Type if xml", async () => {
|
||||
it("Forwards Content-Type if xml", async () => {
|
||||
fetchResponseHeadersGet.mockImplementation(() => "xml");
|
||||
|
||||
await cors(request, corsResponse);
|
||||
|
|
|
@ -47,7 +47,7 @@ describe("Weather utils tests", () => {
|
|||
expect(WeatherUtils.calculateFeelsLike(0, 20, 40)).toBe(-9.444444444444445);
|
||||
});
|
||||
|
||||
it("should return a calculated feelsLike info (positiv value)", () => {
|
||||
it("should return a calculated feelsLike info (positive value)", () => {
|
||||
expect(WeatherUtils.calculateFeelsLike(30, 0, 60)).toBe(32.8320322777777);
|
||||
});
|
||||
});
|
||||
|
|
36
vendor/package-lock.json
generated
vendored
36
vendor/package-lock.json
generated
vendored
|
@ -9,8 +9,9 @@
|
|||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"croner": "^8.1.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"nunjucks": "^3.2.4",
|
||||
|
@ -19,10 +20,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-free": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.2.tgz",
|
||||
"integrity": "sha512-hRILoInAx8GNT5IMkrtIt9blOdrqHOnPBH+k70aWUAqPZPgopb9G5EQJFpaBx/S8zp2fC+mPW349Bziuk1o28Q==",
|
||||
"hasInstallScript": true,
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz",
|
||||
"integrity": "sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow==",
|
||||
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
|
@ -30,30 +31,44 @@
|
|||
"node_modules/a-sync-waterfall": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz",
|
||||
"integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA=="
|
||||
"integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/animate.css": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
|
||||
"integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ=="
|
||||
"integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
|
||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
|
||||
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/croner": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/croner/-/croner-8.1.2.tgz",
|
||||
"integrity": "sha512-ypfPFcAXHuAZRCzo3vJL6ltENzniTjwe/qsLleH1V2/7SRDjgvRQyrLmumFTLmjFax4IuSxfGXEn79fozXcJog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
|
@ -62,6 +77,7 @@
|
|||
"version": "0.5.45",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz",
|
||||
"integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"moment": "^2.29.4"
|
||||
},
|
||||
|
@ -73,6 +89,7 @@
|
|||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz",
|
||||
"integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"a-sync-waterfall": "^1.0.0",
|
||||
"asap": "^2.0.3",
|
||||
|
@ -101,7 +118,8 @@
|
|||
"node_modules/weathericons": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/weathericons/-/weathericons-2.1.0.tgz",
|
||||
"integrity": "sha512-V45viuyuQQOuoePTqzxvP/wBpYALWkD695fkFvqpn+BiMyo64fFlyDbP2A8umZyFyz1cXFPNw1pWqeaSaQqJlQ=="
|
||||
"integrity": "sha512-V45viuyuQQOuoePTqzxvP/wBpYALWkD695fkFvqpn+BiMyo64fFlyDbP2A8umZyFyz1cXFPNw1pWqeaSaQqJlQ==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
3
vendor/package.json
vendored
3
vendor/package.json
vendored
|
@ -11,8 +11,9 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"croner": "^8.1.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"nunjucks": "^3.2.4",
|
||||
|
|
3
vendor/vendor.js
vendored
3
vendor/vendor.js
vendored
|
@ -5,7 +5,8 @@ const vendor = {
|
|||
"weather-icons-wind.css": "node_modules/weathericons/css/weather-icons-wind.css",
|
||||
"font-awesome.css": "css/font-awesome.css",
|
||||
"nunjucks.js": "node_modules/nunjucks/browser/nunjucks.min.js",
|
||||
"suncalc.js": "node_modules/suncalc/suncalc.js"
|
||||
"suncalc.js": "node_modules/suncalc/suncalc.js",
|
||||
"croner.js": "node_modules/croner/dist/croner.umd.min.js"
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue