Release 2.31.0 (#3758)

Thanks to: @Developer-Incoming, @eltociear, @geraki, @khassel,
@KristjanESPERANTO, @MagMar94, @mixasgr, @n8many, @OWL4C, @rejas,
@savvadam, @sdetweil.

> ⚠️ This release needs nodejs version `v22.14.0 or higher`

### Added

- Add CSS support to the digital clock hour/minute/second through the
use of the classes `clock-hour-digital`, `clock-minute-digital`, and
`clock-second-digital`.
- Add Arabic (#3719) and Esperanto translation.
- Mark option `secondsColor` as deprecated in clock module.
- Add Greek translation to Alerts module.
- [newsfeed] Add specific ignoreOlderThan value (override) per feed
(#3360)
- [weather] Added option Humidity to hourly View
- [weather] Added option to hide hourly entries that are Zero, hiding
the entire column if empty.
- [updatenotification] Added option to iterate over modules directory
instead using modules defined in `config.js` (#3739)

### Changed

- [core] starting clientonly now checks for needed env var
`WAYLAND_DISPLAY` or `DISPLAY` and starts electron with needed
parameters (if both are set wayland is used) (#3677)
- [core] Optimize systeminformation calls and output (#3689)
- [core] Add issue templates for feature requests and bug reports
(#3695)
- [core] Adapt `start:x11:dev` script
- [weather/yr] The Yr weather provider now enforces a minimum
`updateInterval` of 600 000 ms (10 minutes) to comply with the terms of
service. If a lower value is set, it will be automatically increased to
this minimum.
- [weather/weatherflow] Fixed icons and added hourly support as well as
UV, precipitation, and location name support.
- [workflow] Run `sudo apt-get update` before installing packages to
avoid install errors
- [workflow] Exclude issues with label `ready (coming with next
release)` from stale job

### Removed

### Updated

- [core] Update requirements and dependencies including electron to v35
and formatting (#3593, #3693, #3717)
- [core] Update prettier, ESLint and simplify config
- Update Greek translation

### Fixed

- [calendar] Fix clipping events being broadcast (#3678)
- [tests] Fix Electron tests by running them under new github image
ubuntu-24.04, replace xserver with labwc, running under xserver and
labwc depending on env variable WAYLAND_DISPLAY is set (#3676)
- [calendar] Fix arrayed symbols, #3267, again, add testcase, add
testcase for #3678
- [weather] Fix wrong weatherCondition name in openmeteo provider which
lead to n/a icon (#3691)
- [core] Fix wrong port in log message when starting server only (#3696)
- [calendar] Fix NewYork event processed on system in Central timezone
shows wrong time #3701
- [weather/yr] The Yr weather provider is now able to recover from bad
API responses instead of freezing (#3296)
- [compliments] Fix evening events being shown during the day (#3727)
- [weather] Fixed minor spacing issues when using UV Index in Hourly
- [workflow] Fix command to run spellcheck

---------

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: Karsten Hassel <hassel@gmx.de>
Co-authored-by: Ross Younger <crazyscot@gmail.com>
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: 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>
Co-authored-by: HeikoGr <20295490+HeikoGr@users.noreply.github.com>
Co-authored-by: Pedro Lamas <pedrolamas@gmail.com>
Co-authored-by: veeck <gitkraken@veeck.de>
Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com>
Co-authored-by: Ikko Eltociear Ashimine <eltociear@gmail.com>
Co-authored-by: DevIncomin <56730075+Developer-Incoming@users.noreply.github.com>
Co-authored-by: Nathan <n8nyoung@gmail.com>
Co-authored-by: mixasgr <mixasgr@users.noreply.github.com>
Co-authored-by: Savvas Adamtziloglou <savvas-gr@greeklug.gr>
Co-authored-by: Konstantinos <geraki@gmail.com>
Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com>
This commit is contained in:
Veeck 2025-04-01 20:11:02 +02:00 committed by GitHub
parent 9c9a5359dd
commit 39a614e0de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 2252 additions and 1093 deletions

View File

@ -34,12 +34,16 @@ jobs:
npm run test:css npm run test:css
npm run test:markdown npm run test:markdown
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-24.04
timeout-minutes: 30 timeout-minutes: 30
strategy: strategy:
matrix: matrix:
node-version: [20.18.1, 20.x, 22.x, 23.x] node-version: [22.14.0, 22.x, 23.x]
steps: steps:
- name: Install electron dependencies and labwc
run: |
sudo apt-get update
sudo apt-get install -y libnss3 libasound2t64 labwc
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: "Use Node.js ${{ matrix.node-version }}" - name: "Use Node.js ${{ matrix.node-version }}"
@ -48,12 +52,16 @@ jobs:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
check-latest: true check-latest: true
cache: "npm" cache: "npm"
- name: "Install dependencies" - name: "Install MagicMirror²"
run: | run: |
npm run install-mm:dev npm run install-mm:dev
- name: "Run tests" - name: "Run tests"
run: | run: |
Xvfb :99 -screen 0 1024x768x16 & # Fix chrome-sandbox permissions:
export DISPLAY=:99 sudo chown root:root ./node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 ./node_modules/electron/dist/chrome-sandbox
# Start labwc
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 WLR_RENDERER=pixman labwc &
export WAYLAND_DISPLAY=wayland-0
touch css/custom.css touch css/custom.css
npm run test npm run test

View File

@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [20.18.1, 20.x, 22.x, 23.x] node-version: [22.14.0, 22.x, 23.x]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -22,7 +22,9 @@ jobs:
- name: Install @electron/rebuild - name: Install @electron/rebuild
run: npm install @electron/rebuild run: npm install @electron/rebuild
- name: Install node-libgpiod deps - name: Install node-libgpiod deps
run: sudo apt-get install gpiod libgpiod2 libgpiod-dev run: |
sudo apt-get update
sudo apt-get install gpiod libgpiod2 libgpiod-dev
- name: Install test library (node-libgpiod) to be rebuilded - name: Install test library (node-libgpiod) to be rebuilded
run: npm install node-libgpiod run: npm install node-libgpiod
- name: Run electron-rebuild - name: Run electron-rebuild

View File

@ -28,4 +28,4 @@ jobs:
run: | run: |
npm run install-mm:dev npm run install-mm:dev
- name: Run Spellcheck - name: Run Spellcheck
run: npm run test:spellcheck run: npm run test:spelling

View File

@ -19,4 +19,4 @@ jobs:
days-before-issue-close: 7 days-before-issue-close: 7
operations-per-run: 100 operations-per-run: 100
stale-issue-label: "wontfix" stale-issue-label: "wontfix"
exempt-issue-labels: "pinned,security,under investigation,pr welcome" exempt-issue-labels: "pinned,security,under investigation,pr welcome,ready (coming with next release)"

View File

@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/#donate) With your help we can continue to improve the MagicMirror². ❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/#donate) With your help we can continue to improve the MagicMirror².
## [2.31.0] - 2025-04-01
Thanks to: @Developer-Incoming, @eltociear, @geraki, @khassel, @KristjanESPERANTO, @MagMar94, @mixasgr, @n8many, @OWL4C, @rejas, @savvadam, @sdetweil.
> ⚠️ This release needs nodejs version `v22.14.0 or higher`
### Added
- Add CSS support to the digital clock hour/minute/second through the use of the classes `clock-hour-digital`, `clock-minute-digital`, and `clock-second-digital`.
- Add Arabic (#3719) and Esperanto translation.
- Mark option `secondsColor` as deprecated in clock module.
- Add Greek translation to Alerts module.
- [newsfeed] Add specific ignoreOlderThan value (override) per feed (#3360)
- [weather] Added option Humidity to hourly View
- [weather] Added option to hide hourly entries that are Zero, hiding the entire column if empty.
- [updatenotification] Added option to iterate over modules directory instead using modules defined in `config.js` (#3739)
### Changed
- [core] starting clientonly now checks for needed env var `WAYLAND_DISPLAY` or `DISPLAY` and starts electron with needed parameters (if both are set wayland is used) (#3677)
- [core] Optimize systeminformation calls and output (#3689)
- [core] Add issue templates for feature requests and bug reports (#3695)
- [core] Adapt `start:x11:dev` script
- [weather/yr] The Yr weather provider now enforces a minimum `updateInterval` of 600 000 ms (10 minutes) to comply with the terms of service. If a lower value is set, it will be automatically increased to this minimum.
- [weather/weatherflow] Fixed icons and added hourly support as well as UV, precipitation, and location name support.
- [workflow] Run `sudo apt-get update` before installing packages to avoid install errors
- [workflow] Exclude issues with label `ready (coming with next release)` from stale job
### Removed
### Updated
- [core] Update requirements and dependencies including electron to v35 and formatting (#3593, #3693, #3717)
- [core] Update prettier, ESLint and simplify config
- Update Greek translation
### Fixed
- [calendar] Fix clipping events being broadcast (#3678)
- [tests] Fix Electron tests by running them under new github image ubuntu-24.04, replace xserver with labwc, running under xserver and labwc depending on env variable WAYLAND_DISPLAY is set (#3676)
- [calendar] Fix arrayed symbols, #3267, again, add testcase, add testcase for #3678
- [weather] Fix wrong weatherCondition name in openmeteo provider which lead to n/a icon (#3691)
- [core] Fix wrong port in log message when starting server only (#3696)
- [calendar] Fix NewYork event processed on system in Central timezone shows wrong time #3701
- [weather/yr] The Yr weather provider is now able to recover from bad API responses instead of freezing (#3296)
- [compliments] Fix evening events being shown during the day (#3727)
- [weather] Fixed minor spacing issues when using UV Index in Hourly
- [workflow] Fix command to run spellcheck
## [2.30.0] - 2025-01-01 ## [2.30.0] - 2025-01-01
Thanks to: @xsorifc28, @HeikoGr, @bugsounet, @khassel, @KristjanESPERANTO, @rejas, @sdetweil. Thanks to: @xsorifc28, @HeikoGr, @bugsounet, @khassel, @KristjanESPERANTO, @rejas, @sdetweil.
@ -24,7 +73,7 @@ Thanks to: @xsorifc28, @HeikoGr, @bugsounet, @khassel, @KristjanESPERANTO, @reja
- [linter] Re-add `eslint-plugin-import`now that it supports ESLint v9 (#3586) - [linter] Re-add `eslint-plugin-import`now that it supports ESLint v9 (#3586)
- [linter] Re-activate `eslint-plugin-package-json` to lint `package.json` (#3643) - [linter] Re-activate `eslint-plugin-package-json` to lint `package.json` (#3643)
- [linter] Add linting for markdown files (#3646) - [linter] Add linting for markdown files (#3646)
- [linter] Add some handy ESLint rules. - [linter] Add some handy ESLint rules (#3665)
- [calendar] Add ability to display end date for full date events, where end is not same day (showEnd=true) (#3650) - [calendar] Add ability to display end date for full date events, where end is not same day (showEnd=true) (#3650)
- [core] Add text to the config.js.sample file about the locale variable (#3654, #3655) - [core] Add text to the config.js.sample file about the locale variable (#3654, #3655)
- [core] Add fetch timeout for all node_helpers (thru undici, forces node 20.18.1 minimum) to help on slower systems. (#3660) (3661) - [core] Add fetch timeout for all node_helpers (thru undici, forces node 20.18.1 minimum) to help on slower systems. (#3660) (3661)
@ -103,7 +152,7 @@ Thanks to: @bugsounet, @dkallen78, @jargordon, @khassel, @KristjanESPERANTO, @Ma
- [core] Detail optimizations in `config_check.js` - [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) - [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` - [linter] Switch to ESLint v9 and flat config and replace `eslint-plugin-unicorn` by `@eslint/js`
- [core] fix discovering module positions twice after #3450 - [core] Fix discovering module positions twice after #3450
### Fixed ### Fixed
@ -167,7 +216,7 @@ For more info, please read the following post: [A New Chapter for MagicMirror: T
### Updated ### Updated
- Update updatenotification (update_helper.js): Recode with pm2 library (#3332) - [updatenotification] Recode update_helper.js with pm2 library (#3332)
- Removing lodash dependency by replacing merge by spread operator (#3339) - Removing lodash dependency by replacing merge by spread operator (#3339)
- Use node prefix for build-in modules (#3340) - Use node prefix for build-in modules (#3340)
- Rework logging colors (#3350) - Rework logging colors (#3350)
@ -1671,6 +1720,7 @@ It includes (but is not limited to) the following features:
This was part of the blogpost: [https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the](https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the) This was part of the blogpost: [https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the](https://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the)
[2.31.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.30.0...v2.31.0
[2.30.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.29.0...v2.30.0 [2.30.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.29.0...v2.30.0
[2.29.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.28.0...v2.29.0 [2.29.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.28.0...v2.29.0
[2.28.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.27.0...v2.28.0 [2.28.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.27.0...v2.28.0

View File

@ -30,13 +30,16 @@ Are done by
### Deployment steps ### Deployment steps
- [ ] pull latest `develop` branch - [ ] pull latest `develop` branch
- [ ] create `prep-release` branch from `develop`
- [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0` - [ ] update `package.json` and `package-lock.json` to reflect correct version number `2.xx.0`
- [ ] test `develop` branch - [ ] test `prep-release` branch
- [ ] update `CHANGELOG.md` - [ ] update `CHANGELOG.md`
- [ ] add all contributor names: `...` - [ ] add all contributor names: `...`
- [ ] add min. node version: > ⚠️ This release needs nodejs version `v20` or `v22`, minimum version is `v20.9.0` - [ ] add min. node version: > ⚠️ This release needs nodejs version `v22.14.0` or higher
- [ ] check release link at the bottom of the file - [ ] check release link at the bottom of the file
- [ ] commit and push all changes - [ ] commit and push all changes
- [ ] create pull request from `prep-release` to `develop` branch with title `Prepare Release 2.xx.0`
- [ ] after successful test run via github actions: merge pull request to `develop`
- [ ] after successful test run via github actions: create pull request from `develop` to `master` branch - [ ] after successful test run via github actions: create pull request from `develop` to `master` branch
- [ ] add label `mastermerge` - [ ] add label `mastermerge`
- [ ] title of the PR is `Release 2.xx.0` - [ ] title of the PR is `Release 2.xx.0`
@ -54,6 +57,7 @@ Are done by
- [ ] draft new section in `CHANGELOG.md` - [ ] draft new section in `CHANGELOG.md`
- [ ] create new release link at the bottom of the file - [ ] create new release link at the bottom of the file
- [ ] commit and publish `develop` branch - [ ] commit and publish `develop` branch
- [ ] if new release will be in January, update the year in LICENSE.md
### After release ### After release

View File

@ -1,6 +1,6 @@
# The MIT License (MIT) # The MIT License (MIT)
Copyright © 2016-2024 Michael Teeuw Copyright © 2016-2025 Michael Teeuw
Permission is hereby granted, free of charge, to any person Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation obtaining a copy of this software and associated documentation

View File

@ -83,6 +83,17 @@
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) === -1) { if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) === -1) {
getServerConfig(`${prefix}${config.address}:${config.port}/config/`) getServerConfig(`${prefix}${config.address}:${config.port}/config/`)
.then(function (configReturn) { .then(function (configReturn) {
// check environment for DISPLAY or WAYLAND_DISPLAY
const elecParams = ["js/electron.js"];
if (process.env.WAYLAND_DISPLAY) {
console.log(`Client: Using WAYLAND_DISPLAY=${process.env.WAYLAND_DISPLAY}`);
elecParams.push("--enable-features=UseOzonePlatform");
elecParams.push("--ozone-platform=wayland");
} else if (process.env.DISPLAY) {
console.log(`Client: Using DISPLAY=${process.env.DISPLAY}`);
} else {
fail("Error: Requires environment variable WAYLAND_DISPLAY or DISPLAY, none is provided.");
}
// Pass along the server config via an environment variable // Pass along the server config via an environment variable
const env = Object.create(process.env); const env = Object.create(process.env);
env.clientonly = true; // set to pass to electron.js env.clientonly = true; // set to pass to electron.js
@ -94,7 +105,7 @@
// Spawn electron application // Spawn electron application
const electron = require("electron"); const electron = require("electron");
const child = require("node:child_process").spawn(electron, ["js/electron.js"], options); const child = require("node:child_process").spawn(electron, elecParams, options);
// Pipe all child process output to current stdout // Pipe all child process output to current stdout
child.stdout.on("data", function (buf) { child.stdout.on("data", function (buf) {

View File

@ -58,6 +58,7 @@
"eddiehung", "eddiehung",
"Edgardos", "Edgardos",
"Ekristoffe", "Ekristoffe",
"elec",
"envcanada", "envcanada",
"envsub", "envsub",
"envsubst", "envsubst",
@ -85,6 +86,7 @@
"GHSA", "GHSA",
"ghsas", "ghsas",
"grenagit", "grenagit",
"Heiko",
"Hirschberger", "Hirschberger",
"hourlyweather", "hourlyweather",
"Hwind", "Hwind",
@ -117,6 +119,7 @@
"krekos", "krekos",
"Kristjan", "Kristjan",
"krukle", "krukle",
"labwc",
"Landis", "Landis",
"larryare", "larryare",
"letsencrypt", "letsencrypt",
@ -132,6 +135,8 @@
"martingron", "martingron",
"marvai", "marvai",
"mastermerge", "mastermerge",
"matchtype",
"maxentries",
"Meteo", "Meteo",
"michaelteeuw", "michaelteeuw",
"michmich", "michmich",
@ -195,6 +200,7 @@
"sunaction", "sunaction",
"suncalc", "suncalc",
"suntimes", "suntimes",
"symboltest",
"systeminformation", "systeminformation",
"tada", "tada",
"taglist", "taglist",
@ -228,6 +234,7 @@
"xlarge", "xlarge",
"xrandr", "xrandr",
"xsmall", "xsmall",
"xsorifc",
"xwindows", "xwindows",
"xxxe", "xxxe",
"Ybbet", "Ybbet",

View File

@ -1,14 +1,16 @@
import eslintPluginImport from "eslint-plugin-import"; import eslintPluginImport from "eslint-plugin-import";
import eslintPluginJest from "eslint-plugin-jest"; import eslintPluginJest from "eslint-plugin-jest";
import eslintPluginJs from "@eslint/js"; import eslintPluginJs from "@eslint/js";
import eslintPluginPackageJson from "eslint-plugin-package-json/configs/recommended"; import eslintPluginPackageJson from "eslint-plugin-package-json";
import eslintPluginStylistic from "@stylistic/eslint-plugin"; import eslintPluginStylistic from "@stylistic/eslint-plugin";
import globals from "globals"; import globals from "globals";
const config = [ const config = [
eslintPluginJs.configs.recommended,
eslintPluginImport.flatConfigs.recommended, eslintPluginImport.flatConfigs.recommended,
eslintPluginPackageJson, eslintPluginJest.configs["flat/recommended"],
eslintPluginJs.configs.recommended,
eslintPluginPackageJson.configs.recommended,
eslintPluginStylistic.configs.all,
{ {
files: ["**/*.js"], files: ["**/*.js"],
languageOptions: { languageOptions: {
@ -24,13 +26,7 @@ const config = [
moment: "readonly" moment: "readonly"
} }
}, },
plugins: {
...eslintPluginStylistic.configs["all-flat"].plugins,
...eslintPluginJest.configs["flat/recommended"].plugins
},
rules: { rules: {
...eslintPluginStylistic.configs["all-flat"].rules,
...eslintPluginJest.configs["flat/recommended"].rules,
"@stylistic/array-element-newline": ["error", "consistent"], "@stylistic/array-element-newline": ["error", "consistent"],
"@stylistic/arrow-parens": ["error", "always"], "@stylistic/arrow-parens": ["error", "always"],
"@stylistic/brace-style": "off", "@stylistic/brace-style": "off",
@ -99,11 +95,7 @@ const config = [
}, },
sourceType: "module" sourceType: "module"
}, },
plugins: {
...eslintPluginStylistic.configs["all-flat"].plugins
},
rules: { rules: {
...eslintPluginStylistic.configs["all-flat"].rules,
"@stylistic/array-element-newline": "off", "@stylistic/array-element-newline": "off",
"@stylistic/indent": ["error", "tab"], "@stylistic/indent": ["error", "tab"],
"@stylistic/padded-blocks": ["error", "never"], "@stylistic/padded-blocks": ["error", "never"],

View File

@ -9,19 +9,27 @@
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fontsource/roboto": "^5.1.1", "@fontsource/roboto": "^5.2.5",
"@fontsource/roboto-condensed": "^5.1.1" "@fontsource/roboto-condensed": "^5.2.5"
} }
}, },
"node_modules/@fontsource/roboto": { "node_modules/@fontsource/roboto": {
"version": "5.1.1", "version": "5.2.5",
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.1.1.tgz", "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.5.tgz",
"integrity": "sha512-XwVVXtERDQIM7HPUIbyDe0FP4SRovpjF7zMI8M7pbqFp3ahLJsJTd18h+E6pkar6UbV3btbwkKjYARr5M+SQow==" "integrity": "sha512-70r2UZ0raqLn5W+sPeKhqlf8wGvUXFWlofaDlcbt/S3d06+17gXKr3VNqDODB0I1ASme3dGT5OJj9NABt7OTZQ==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
}, },
"node_modules/@fontsource/roboto-condensed": { "node_modules/@fontsource/roboto-condensed": {
"version": "5.1.1", "version": "5.2.5",
"resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-5.1.1.tgz", "resolved": "https://registry.npmjs.org/@fontsource/roboto-condensed/-/roboto-condensed-5.2.5.tgz",
"integrity": "sha512-0SYkGnWPsvyCI3TAqBYAglfVUqVu/fsdgsyl5u396oK8ZgyamWHdQMFHDqCWrb4H4hNiewJT1l2ShDCA/cu6Ug==" "integrity": "sha512-FVubmVJpZ2js2+nCBEA3IOHhAgWmZ2/YKvTae0X25jlxbd85umOOvUIY6FL6OMpUvIgvwOImS9l0GJjzEPk+mg==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
} }
} }
} }

View File

@ -11,7 +11,7 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fontsource/roboto": "^5.1.1", "@fontsource/roboto": "^5.2.5",
"@fontsource/roboto-condensed": "^5.1.1" "@fontsource/roboto-condensed": "^5.2.5"
} }
} }

View File

@ -153,11 +153,23 @@ function App () {
*/ */
function checkDeprecatedOptions (userConfig) { function checkDeprecatedOptions (userConfig) {
const deprecated = require(`${global.root_path}/js/deprecated`); const deprecated = require(`${global.root_path}/js/deprecated`);
const deprecatedOptions = deprecated.configs;
// check for deprecated core options
const deprecatedOptions = deprecated.configs;
const usedDeprecated = deprecatedOptions.filter((option) => userConfig.hasOwnProperty(option)); const usedDeprecated = deprecatedOptions.filter((option) => userConfig.hasOwnProperty(option));
if (usedDeprecated.length > 0) { if (usedDeprecated.length > 0) {
Log.warn(`WARNING! Your config is using deprecated options: ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`); Log.warn(`WARNING! Your config is using deprecated option(s): ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`);
}
// check for deprecated module options
for (const element of userConfig.modules) {
if (deprecated[element.module] !== undefined && element.config !== undefined) {
const deprecatedModuleOptions = deprecated[element.module];
const usedDeprecatedModuleOptions = deprecatedModuleOptions.filter((option) => element.config.hasOwnProperty(option));
if (usedDeprecatedModuleOptions.length > 0) {
Log.warn(`WARNING! Your config for module ${element.module} is using deprecated option(s): ${usedDeprecatedModuleOptions.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`);
}
}
} }
} }
@ -177,7 +189,7 @@ function App () {
moduleFolder = defaultModuleFolder; moduleFolder = defaultModuleFolder;
} else { } else {
// running in Jest, allow defaultModules placed under moduleDir for testing // running in Jest, allow defaultModules placed under moduleDir for testing
if (env.modulesDir === "modules") { if (env.modulesDir === "modules" || env.modulesDir === "tests/mocks") {
moduleFolder = defaultModuleFolder; moduleFolder = defaultModuleFolder;
} }
} }

View File

@ -1,3 +1,4 @@
module.exports = { module.exports = {
configs: ["kioskmode"] configs: ["kioskmode"],
clock: ["secondsColor"]
}; };

View File

@ -19,15 +19,16 @@ module.exports = {
let installedNodeVersion = execSync("node -v", { encoding: "utf-8" }).replace("v", "").replace(/(?:\r\n|\r|\n)/g, ""); let installedNodeVersion = execSync("node -v", { encoding: "utf-8" }).replace("v", "").replace(/(?:\r\n|\r|\n)/g, "");
const staticData = await si.get({ const staticData = await si.get({
system: "manufacturer, model, raspberry, virtual", system: "manufacturer, model, virtual",
osInfo: "platform, distro, release, arch", osInfo: "platform, distro, release, arch",
versions: "kernel, node, npm, pm2" versions: "kernel, node, npm, pm2"
}); });
let systemDataString = "System information:"; let systemDataString = `System information:
systemDataString += `\n### SYSTEM: manufacturer: ${staticData.system.manufacturer}; model: ${staticData.system.model}; virtual: ${staticData.system.virtual}`; ### 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}`; ### 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}`; ### 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}`; ### OTHER: timeZone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}; ELECTRON_ENABLE_GPU: ${process.env.ELECTRON_ENABLE_GPU}`
.replace(/\t/g, "");
Log.info(systemDataString); Log.info(systemDataString);
// Return is currently only for jest // Return is currently only for jest

View File

@ -25,6 +25,7 @@ Module.register("alert", {
da: "translations/da.json", da: "translations/da.json",
de: "translations/de.json", de: "translations/de.json",
en: "translations/en.json", en: "translations/en.json",
eo: "translations/eo.json",
es: "translations/es.json", es: "translations/es.json",
fr: "translations/fr.json", fr: "translations/fr.json",
hu: "translations/hu.json", hu: "translations/hu.json",

View File

@ -0,0 +1,4 @@
{
"sysTitle": "MagicMirror² Ειδοποίηση",
"welcome": "Καλώς ήρθατε, η εκκίνηση ήταν επιτυχής!"
}

View File

@ -0,0 +1,4 @@
{
"sysTitle": "MagicMirror²-sciigo",
"welcome": "Bonvenon, lanĉo sukcesis!"
}

View File

@ -689,12 +689,17 @@ Module.register("calendar", {
by_url_calevents.push(event); by_url_calevents.push(event);
} }
} }
if (limitNumberOfEntries) {
// sort entries before clipping
by_url_calevents.sort(function (a, b) { by_url_calevents.sort(function (a, b) {
return a.startDate - b.startDate; return a.startDate - b.startDate;
}); });
Log.debug(`pushing ${by_url_calevents.length} events to total with room for ${remainingEntries}`); Log.debug(`pushing ${by_url_calevents.length} events to total with room for ${remainingEntries}`);
events = events.concat(by_url_calevents.slice(0, remainingEntries)); events = events.concat(by_url_calevents.slice(0, remainingEntries));
Log.debug(`events for calendar=${events.length}`); Log.debug(`events for calendar=${events.length}`);
} else {
events = events.concat(by_url_calevents);
}
} }
Log.info(`sorting events count=${events.length}`); Log.info(`sorting events count=${events.length}`);
events.sort(function (a, b) { events.sort(function (a, b) {
@ -907,7 +912,11 @@ Module.register("calendar", {
let p = this.getCalendarProperty(url, property, defaultValue); let p = this.getCalendarProperty(url, property, defaultValue);
if (property === "symbol" || property === "recurringSymbol" || property === "fullDaySymbol") { if (property === "symbol" || property === "recurringSymbol" || property === "fullDaySymbol") {
const className = this.getCalendarProperty(url, "symbolClassName", this.config.defaultSymbolClassName); const className = this.getCalendarProperty(url, "symbolClassName", this.config.defaultSymbolClassName);
if (p instanceof Array) p.push(className); if (p instanceof Array) {
let t = [];
p.forEach((n) => { t.push(className + n); });
p = t;
}
else p = className + p; else p = className + p;
} }
if (!(p instanceof Array)) p = [p]; if (!(p instanceof Array)) p = [p];

View File

@ -662,9 +662,11 @@ const CalendarFetcherUtils = {
Log.debug("signs are the same"); Log.debug("signs are the same");
if (Math.sign(eventDiff) === -1) { if (Math.sign(eventDiff) === -1) {
//if west, looking at more west //if west, looking at more west
// -350 <-300
if (nowDiff < eventDiff) { if (nowDiff < eventDiff) {
//-600 -420 //-600 -420
eventDiff = -(eventDiff - (nowDiff - eventDiff)); //-180 //300 -300 -360 +300
eventDiff = nowDiff - eventDiff; //-180
Log.debug("now looking back east delta diff=", eventDiff); Log.debug("now looking back east delta diff=", eventDiff);
} }
else { else {

View File

@ -23,7 +23,7 @@ Module.register("clock", {
analogFace: "simple", // options: 'none', 'simple', 'face-###' (where ### is 001 to 012 inclusive) analogFace: "simple", // options: 'none', 'simple', 'face-###' (where ### is 001 to 012 inclusive)
analogPlacement: "bottom", // options: 'top', 'bottom', 'left', 'right' analogPlacement: "bottom", // options: 'top', 'bottom', 'left', 'right'
analogShowDate: "top", // OBSOLETE, can be replaced with analogPlacement and showTime, options: false, 'top', or 'bottom' analogShowDate: "top", // OBSOLETE, can be replaced with analogPlacement and showTime, options: false, 'top', or 'bottom'
secondsColor: "#888888", secondsColor: "#888888", // DEPRECATED, use CSS instead. Class "clock-second-digital" for digital clock, "clock-second" for analog clock.
showSunTimes: false, showSunTimes: false,
showMoonTimes: false, // options: false, 'times' (rise/set), 'percent' (lit percent), 'phase' (current phase), or 'both' (percent & phase) showMoonTimes: false, // options: false, 'times' (rise/set), 'percent' (lit percent), 'phase' (current phase), or 'both' (percent & phase)
@ -105,6 +105,8 @@ Module.register("clock", {
*/ */
const dateWrapper = document.createElement("div"); const dateWrapper = document.createElement("div");
const timeWrapper = document.createElement("div"); const timeWrapper = document.createElement("div");
const hoursWrapper = document.createElement("span");
const minutesWrapper = document.createElement("span");
const secondsWrapper = document.createElement("sup"); const secondsWrapper = document.createElement("sup");
const periodWrapper = document.createElement("span"); const periodWrapper = document.createElement("span");
const sunWrapper = document.createElement("div"); const sunWrapper = document.createElement("div");
@ -114,39 +116,40 @@ Module.register("clock", {
// Style Wrappers // Style Wrappers
dateWrapper.className = "date normal medium"; dateWrapper.className = "date normal medium";
timeWrapper.className = "time bright large light"; timeWrapper.className = "time bright large light";
secondsWrapper.className = "seconds dimmed"; hoursWrapper.className = "clock-hour-digital";
minutesWrapper.className = "clock-minute-digital";
secondsWrapper.className = "clock-second-digital dimmed";
sunWrapper.className = "sun dimmed small"; sunWrapper.className = "sun dimmed small";
moonWrapper.className = "moon dimmed small"; moonWrapper.className = "moon dimmed small";
weekWrapper.className = "week dimmed medium"; weekWrapper.className = "week dimmed medium";
// Set content of wrappers. // Set content of wrappers.
// The moment().format("h") method has a bug on the Raspberry Pi.
// So we need to generate the timestring manually.
// See issue: https://github.com/MagicMirrorOrg/MagicMirror/issues/181
let timeString;
const now = moment(); const now = moment();
if (this.config.timezone) { if (this.config.timezone) {
now.tz(this.config.timezone); now.tz(this.config.timezone);
} }
let hourSymbol = "HH";
if (this.config.timeFormat !== 24) {
hourSymbol = "h";
}
if (this.config.clockBold) {
timeString = now.format(`${hourSymbol}[<span class="bold">]mm[</span>]`);
} else {
timeString = now.format(`${hourSymbol}:mm`);
}
if (this.config.showDate) { if (this.config.showDate) {
dateWrapper.innerHTML = now.format(this.config.dateFormat); dateWrapper.innerHTML = now.format(this.config.dateFormat);
digitalWrapper.appendChild(dateWrapper); digitalWrapper.appendChild(dateWrapper);
} }
if (this.config.displayType !== "analog" && this.config.showTime) { if (this.config.displayType !== "analog" && this.config.showTime) {
timeWrapper.innerHTML = timeString; let hourSymbol = "HH";
if (this.config.timeFormat !== 24) {
hourSymbol = "h";
}
hoursWrapper.innerHTML = now.format(hourSymbol);
minutesWrapper.innerHTML = now.format("mm");
timeWrapper.appendChild(hoursWrapper);
if (this.config.clockBold) {
minutesWrapper.classList.add("bold");
} else {
timeWrapper.innerHTML += ":";
}
timeWrapper.appendChild(minutesWrapper);
secondsWrapper.innerHTML = now.format("ss"); secondsWrapper.innerHTML = now.format("ss");
if (this.config.showPeriodUpper) { if (this.config.showPeriodUpper) {
periodWrapper.innerHTML = now.format("A"); periodWrapper.innerHTML = now.format("A");
@ -267,7 +270,7 @@ Module.register("clock", {
clockSecond.id = "clock-second"; clockSecond.id = "clock-second";
clockSecond.style.transform = `rotate(${second}deg)`; clockSecond.style.transform = `rotate(${second}deg)`;
clockSecond.className = "clock-second"; clockSecond.className = "clock-second";
clockSecond.style.backgroundColor = this.config.secondsColor; clockSecond.style.backgroundColor = this.config.secondsColor; /* DEPRECATED, to be removed in a future version , use CSS instead */
clockFace.appendChild(clockSecond); clockFace.appendChild(clockSecond);
} }
analogWrapper.appendChild(clockFace); analogWrapper.appendChild(clockFace);

View File

@ -78,7 +78,12 @@
left: 50%; left: 50%;
margin: -38% -1px 0 0; /* numbers must match negative length & thickness */ margin: -38% -1px 0 0; /* numbers must match negative length & thickness */
padding: 38% 1px 0 0; /* indicator length & thickness */ padding: 38% 1px 0 0; /* indicator length & thickness */
background: var(--color-text);
/* background: #888888 !important; */
/* use this instead of secondsColor */
/* have to use !important, because the code explicitly sets the color currently */
transform-origin: 50% 100%; transform-origin: 50% 100%;
} }
@ -91,3 +96,15 @@
.module.clock .moon > * { .module.clock .moon > * {
flex: 1; flex: 1;
} }
.module.clock .clock-hour-digital {
color: var(--color-text-bright);
}
.module.clock .clock-minute-digital {
color: var(--color-text-bright);
}
.module.clock .clock-second-digital {
color: var(--color-text-dimmed);
}

View File

@ -139,12 +139,17 @@ Module.register("compliments", {
let compliments = []; let compliments = [];
// Add time of day compliments // Add time of day compliments
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime && this.config.compliments.hasOwnProperty("morning")) { let timeOfDay;
compliments = [...this.config.compliments.morning]; if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime) {
} else if (hour >= this.config.afternoonStartTime && hour < this.config.afternoonEndTime && this.config.compliments.hasOwnProperty("afternoon")) { timeOfDay = "morning";
compliments = [...this.config.compliments.afternoon]; } else if (hour >= this.config.afternoonStartTime && hour < this.config.afternoonEndTime) {
} else if (this.config.compliments.hasOwnProperty("evening")) { timeOfDay = "afternoon";
compliments = [...this.config.compliments.evening]; } else {
timeOfDay = "evening";
}
if (timeOfDay && this.config.compliments.hasOwnProperty(timeOfDay)) {
compliments = [...this.config.compliments[timeOfDay]];
} }
// Add compliments based on weather // Add compliments based on weather

View File

@ -57,7 +57,7 @@ Module.register("newsfeed", {
// Define required translations. // Define required translations.
getTranslations () { getTranslations () {
// The translations for the default modules are defined in the core translation files. // The translations for the default modules are defined in the core translation files.
// Therefor we can just return false. Otherwise we should have returned a dictionary. // Therefore we can just return false. Otherwise we should have returned a dictionary.
// If you're trying to build your own module including translations, check out the documentation. // If you're trying to build your own module including translations, check out the documentation.
return false; return false;
}, },
@ -177,6 +177,18 @@ Module.register("newsfeed", {
} }
}, },
/**
* Gets a feed property by name
* @param {object} feed A feed object.
* @param {string} property The name of the property.
*/
getFeedProperty (feed, property) {
let res = this.config[property];
const f = this.config.feeds.find((feedItem) => feedItem.url === feed);
if (f && f[property]) res = f[property];
return res;
},
/** /**
* Generate an ordered list of items for this configured module. * Generate an ordered list of items for this configured module.
* @param {object} feeds An object with feeds returned by the node helper. * @param {object} feeds An object with feeds returned by the node helper.
@ -188,7 +200,7 @@ Module.register("newsfeed", {
if (this.subscribedToFeed(feed)) { if (this.subscribedToFeed(feed)) {
for (let item of feedItems) { for (let item of feedItems) {
item.sourceTitle = this.titleForFeed(feed); item.sourceTitle = this.titleForFeed(feed);
if (!(this.config.ignoreOldItems && Date.now() - new Date(item.pubdate) > this.config.ignoreOlderThan)) { if (!(this.getFeedProperty(feed, "ignoreOldItems") && Date.now() - new Date(item.pubdate) > this.getFeedProperty(feed, "ignoreOlderThan"))) {
newsItems.push(item); newsItems.push(item);
} }
} }

View File

@ -1,3 +1,5 @@
const fs = require("node:fs");
const path = require("node:path");
const NodeHelper = require("node_helper"); const NodeHelper = require("node_helper");
const defaultModules = require("../defaultmodules"); const defaultModules = require("../defaultmodules");
const GitHelper = require("./git_helper"); const GitHelper = require("./git_helper");
@ -14,8 +16,23 @@ module.exports = NodeHelper.create({
gitHelper: new GitHelper(), gitHelper: new GitHelper(),
updateHelper: null, updateHelper: null,
getModules (modules) {
if (this.config.useModulesFromConfig) {
return modules;
} else {
// get modules from modules-directory
const moduleDir = path.normalize(`${__dirname}/../../`);
const getDirectories = (source) => {
return fs.readdirSync(source, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory() && dirent.name !== "default")
.map((dirent) => dirent.name);
};
return getDirectories(moduleDir);
}
},
async configureModules (modules) { async configureModules (modules) {
for (const moduleName of modules) { for (const moduleName of this.getModules(modules)) {
if (!this.ignoreUpdateChecking(moduleName)) { if (!this.ignoreUpdateChecking(moduleName)) {
await this.gitHelper.add(moduleName); await this.gitHelper.add(moduleName);
} }

View File

@ -6,7 +6,8 @@ Module.register("updatenotification", {
sendUpdatesNotifications: false, sendUpdatesNotifications: false,
updates: [], updates: [],
updateTimeout: 2 * 60 * 1000, // max update duration updateTimeout: 2 * 60 * 1000, // max update duration
updateAutorestart: false // autoRestart MM when update done ? updateAutorestart: false, // autoRestart MM when update done ?
useModulesFromConfig: true // if `false` iterate over modules directory
}, },
suspended: false, suspended: false,

View File

@ -21,16 +21,26 @@
{% endif %} {% endif %}
</td> </td>
{% endif %} {% endif %}
{% if config.showHumidity != "none" %}
<td class="align-left bright humidity-hourly">
{{ hour.humidity }}
<span class="wi wi-humidity humidity-icon"></span>
</td>
{% endif %}
{% if config.showPrecipitationAmount %} {% if config.showPrecipitationAmount %}
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
<td class="align-right bright precipitation-amount"> <td class="align-right bright precipitation-amount">
{{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }} {{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }}
</td> </td>
{% endif %} {% endif %}
{% endif %}
{% if config.showPrecipitationProbability %} {% if config.showPrecipitationProbability %}
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
<td class="align-right bright precipitation-prob"> <td class="align-right bright precipitation-prob">
{{ hour.precipitationProbability | unit('precip', '%') }} {{ hour.precipitationProbability | unit('precip', '%') }}
</td> </td>
{% endif %} {% endif %}
{% endif %}
</tr> </tr>
{% set currentStep = currentStep + 1 %} {% set currentStep = currentStep + 1 %}
{% endfor %} {% endfor %}

View File

@ -470,7 +470,7 @@ WeatherProvider.register("openmeteo", {
61: "rain-slight-intensity", 61: "rain-slight-intensity",
63: "rain-moderate-intensity", 63: "rain-moderate-intensity",
65: "rain-heavy-intensity", 65: "rain-heavy-intensity",
66: "freezing-rain-light-heavy-intensity", 66: "freezing-rain-light-intensity",
67: "freezing-rain-heavy-intensity", 67: "freezing-rain-heavy-intensity",
71: "snow-fall-slight-intensity", 71: "snow-fall-slight-intensity",
73: "snow-fall-moderate-intensity", 73: "snow-fall-moderate-intensity",

View File

@ -25,14 +25,20 @@ WeatherProvider.register("weatherflow", {
const currentWeather = new WeatherObject(); const currentWeather = new WeatherObject();
currentWeather.date = moment(); currentWeather.date = moment();
// Other available values: air_density, brightness, delta_t, dew_point,
// pressure_trend (i.e. rising/falling), sea_level_pressure, wind gust, and more.
currentWeather.humidity = data.current_conditions.relative_humidity; currentWeather.humidity = data.current_conditions.relative_humidity;
currentWeather.temperature = data.current_conditions.air_temperature; currentWeather.temperature = data.current_conditions.air_temperature;
currentWeather.feelsLikeTemp = data.current_conditions.feels_like;
currentWeather.windSpeed = WeatherUtils.convertWindToMs(data.current_conditions.wind_avg); currentWeather.windSpeed = WeatherUtils.convertWindToMs(data.current_conditions.wind_avg);
currentWeather.windFromDirection = data.current_conditions.wind_direction; currentWeather.windFromDirection = data.current_conditions.wind_direction;
currentWeather.weatherType = data.forecast.daily[0].icon; currentWeather.weatherType = this.convertWeatherType(data.current_conditions.icon);
currentWeather.uv_index = data.current_conditions.uv;
currentWeather.sunrise = moment.unix(data.forecast.daily[0].sunrise); currentWeather.sunrise = moment.unix(data.forecast.daily[0].sunrise);
currentWeather.sunset = moment.unix(data.forecast.daily[0].sunset); currentWeather.sunset = moment.unix(data.forecast.daily[0].sunset);
this.setCurrentWeather(currentWeather); this.setCurrentWeather(currentWeather);
this.fetchedLocationName = data.location_name;
}) })
.catch(function (request) { .catch(function (request) {
Log.error("Could not load data ... ", request); Log.error("Could not load data ... ", request);
@ -52,13 +58,27 @@ WeatherProvider.register("weatherflow", {
weather.minTemperature = forecast.air_temp_low; weather.minTemperature = forecast.air_temp_low;
weather.maxTemperature = forecast.air_temp_high; weather.maxTemperature = forecast.air_temp_high;
weather.precipitationProbability = forecast.precip_probability; weather.precipitationProbability = forecast.precip_probability;
weather.weatherType = forecast.icon; weather.weatherType = this.convertWeatherType(forecast.icon);
weather.snow = 0;
// Must manually build UV and Precipitation from hourly
weather.precipitationAmount = 0.0; // This will sum up rain and snow
weather.precipitationUnits = "mm";
weather.uv_index = 0;
for (const hour of data.forecast.hourly) {
const hour_time = moment.unix(hour.time);
if (hour_time.day() === weather.date.day()) { // Iterate though until day is reached
// Get data from today
weather.uv_index = Math.max(weather.uv_index, hour.uv);
weather.precipitationAmount += (hour.precip ?? 0);
} else if (hour_time.diff(weather.date) >= 86400) {
break; // No more data to be found
}
}
days.push(weather); days.push(weather);
} }
this.setWeatherForecast(days); this.setWeatherForecast(days);
this.fetchedLocationName = data.location_name;
}) })
.catch(function (request) { .catch(function (request) {
Log.error("Could not load data ... ", request); Log.error("Could not load data ... ", request);
@ -66,6 +86,63 @@ WeatherProvider.register("weatherflow", {
.finally(() => this.updateAvailable()); .finally(() => this.updateAvailable());
}, },
fetchWeatherHourly () {
this.fetchData(this.getUrl())
.then((data) => {
const hours = [];
for (const hour of data.forecast.hourly) {
const weather = new WeatherObject();
weather.date = moment.unix(hour.time);
weather.temperature = hour.air_temperature;
weather.feelsLikeTemp = hour.feels_like;
weather.humidity = hour.relative_humidity;
weather.windSpeed = hour.wind_avg;
weather.windFromDirection = hour.wind_direction;
weather.weatherType = this.convertWeatherType(hour.icon);
weather.precipitationProbability = hour.precip_probability;
weather.precipitationAmount = hour.precip; // NOTE: precipitation type is available
weather.precipitationUnits = "mm"; // Hardcoded via request, TODO: Add conversion
weather.uv_index = hour.uv;
hours.push(weather);
if (hours.length >= 48) break; // 10 days of hours are available, best to trim down.
}
this.setWeatherHourly(hours);
this.fetchedLocationName = data.location_name;
})
.catch(function (request) {
Log.error("Could not load data ... ", request);
})
.finally(() => this.updateAvailable());
},
convertWeatherType (weatherType) {
const weatherTypes = {
"clear-day": "day-sunny",
"clear-night": "night-clear",
cloudy: "cloudy",
foggy: "fog",
"partly-cloudy-day": "day-cloudy",
"partly-cloudy-night": "night-alt-cloudy",
"possibly-rainy-day": "day-rain",
"possibly-rainy-night": "night-alt-rain",
"possibly-sleet-day": "day-sleet",
"possibly-sleet-night": "night-alt-sleet",
"possibly-snow-day": "day-snow",
"possibly-snow-night": "night-alt-snow",
"possibly-thunderstorm-day": "day-thunderstorm",
"possibly-thunderstorm-night": "night-alt-thunderstorm",
rainy: "rain",
sleet: "sleet",
snow: "snow",
thunderstorm: "thunderstorm",
windy: "strong-wind"
};
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
},
// Create a URL from the config and base URL. // Create a URL from the config and base URL.
getUrl () { getUrl () {
return `${this.config.apiBase}better_forecast?station_id=${this.config.stationid}&units_temp=c&units_wind=kph&units_pressure=mb&units_precip=mm&units_distance=km&token=${this.config.token}`; return `${this.config.apiBase}better_forecast?station_id=${this.config.stationid}&units_temp=c&units_wind=kph&units_pressure=mb&units_precip=mm&units_distance=km&token=${this.config.token}`;

View File

@ -23,6 +23,10 @@ WeatherProvider.register("yr", {
Log.error("The Yr weather provider requires local storage."); Log.error("The Yr weather provider requires local storage.");
throw new Error("Local storage not available"); throw new Error("Local storage not available");
} }
if (this.config.updateInterval < 600000) {
Log.warn("The Yr weather provider requires a minimum update interval of 10 minutes (600 000 ms). The configuration has been adjusted to meet this requirement.");
this.delegate.config.updateInterval = 600000;
}
Log.info(`Weather provider: ${this.providerName} started.`); Log.info(`Weather provider: ${this.providerName} started.`);
}, },
@ -34,7 +38,7 @@ WeatherProvider.register("yr", {
}) })
.catch((error) => { .catch((error) => {
Log.error(error); Log.error(error);
throw new Error(error); this.updateAvailable();
}); });
}, },
@ -119,7 +123,12 @@ WeatherProvider.register("yr", {
}) })
.catch((err) => { .catch((err) => {
Log.error(err); Log.error(err);
if (weatherData) {
Log.warn("Using outdated cached weather data.");
resolve(weatherData);
} else {
reject("Unable to get weather data from Yr."); reject("Unable to get weather data from Yr.");
}
}) })
.finally(() => { .finally(() => {
localStorage.removeItem("yrIsFetchingWeatherData"); localStorage.removeItem("yrIsFetchingWeatherData");
@ -497,7 +506,7 @@ WeatherProvider.register("yr", {
}) })
.catch((error) => { .catch((error) => {
Log.error(error); Log.error(error);
throw new Error(error); this.updateAvailable();
}); });
}, },
@ -530,7 +539,15 @@ WeatherProvider.register("yr", {
getHourlyForecastFrom (weatherData) { getHourlyForecastFrom (weatherData) {
const series = []; const series = [];
const now = moment({
year: moment().year(),
month: moment().month(),
day: moment().date(),
hour: moment().hour()
});
for (const forecast of weatherData.properties.timeseries) { for (const forecast of weatherData.properties.timeseries) {
if (now.isAfter(moment(forecast.time))) continue;
forecast.symbol = forecast.data.next_1_hours?.summary?.symbol_code; forecast.symbol = forecast.data.next_1_hours?.summary?.symbol_code;
forecast.precipitationAmount = forecast.data.next_1_hours?.details?.precipitation_amount; forecast.precipitationAmount = forecast.data.next_1_hours?.details?.precipitation_amount;
forecast.precipitationProbability = forecast.data.next_1_hours?.details?.probability_of_precipitation; forecast.precipitationProbability = forecast.data.next_1_hours?.details?.probability_of_precipitation;
@ -600,7 +617,7 @@ WeatherProvider.register("yr", {
}) })
.catch((error) => { .catch((error) => {
Log.error(error); Log.error(error);
throw new Error(error); this.updateAvailable();
}); });
} }
}); });

View File

@ -31,6 +31,7 @@
.weather .precipitation-amount, .weather .precipitation-amount,
.weather .precipitation-prob, .weather .precipitation-prob,
.weather .humidity-hourly,
.weather .uv-index { .weather .uv-index {
padding-left: 20px; padding-left: 20px;
padding-right: 0; padding-right: 0;

View File

@ -14,7 +14,8 @@ Module.register("weather", {
updateInterval: 10 * 60 * 1000, // every 10 minutes updateInterval: 10 * 60 * 1000, // every 10 minutes
animationSpeed: 1000, animationSpeed: 1000,
showFeelsLike: true, showFeelsLike: true,
showHumidity: "none", // this is now a string; see current.njk showHumidity: "none", // possible options for "current" weather are "none", "wind", "temp", "feelslike" or "below", for "hourly" weather "none" or "true"
hideZeroes: false, // hide zeroes (and empty columns) in hourly, currently only for precipitation
showIndoorHumidity: false, showIndoorHumidity: false,
showIndoorTemperature: false, showIndoorTemperature: false,
allowOverrideNotification: false, allowOverrideNotification: false,

2173
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "magicmirror", "name": "magicmirror",
"version": "2.30.0", "version": "2.31.0",
"description": "The open source modular smart mirror platform.", "description": "The open source modular smart mirror platform.",
"keywords": [ "keywords": [
"magic mirror", "magic mirror",
@ -30,7 +30,7 @@
"install-mm:dev": "npm install --no-audit --no-fund --no-update-notifier", "install-mm:dev": "npm install --no-audit --no-fund --no-update-notifier",
"install-vendor": "echo \"Installing vendor files ...\n\" && cd vendor && npm install --loglevel=error --no-audit --no-fund --no-update-notifier", "install-vendor": "echo \"Installing vendor files ...\n\" && cd vendor && npm install --loglevel=error --no-audit --no-fund --no-update-notifier",
"lint:css": "stylelint 'css/main.css' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.css' --config .stylelintrc.json --fix", "lint:css": "stylelint 'css/main.css' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.css' --config .stylelintrc.json --fix",
"lint:js": "eslint . --fix", "lint:js": "eslint --fix",
"lint:markdown": "markdownlint-cli2 . --fix", "lint:markdown": "markdownlint-cli2 . --fix",
"lint:prettier": "prettier . --write", "lint:prettier": "prettier . --write",
"postinstall": "npm run install-vendor && npm run install-fonts && echo \"MagicMirror² installation finished successfully! \n\"", "postinstall": "npm run install-vendor && npm run install-fonts && echo \"MagicMirror² installation finished successfully! \n\"",
@ -43,14 +43,14 @@
"start:windows": ".\\node_modules\\.bin\\electron js\\electron.js", "start:windows": ".\\node_modules\\.bin\\electron js\\electron.js",
"start:windows:dev": "npm run start:windows -- dev", "start:windows:dev": "npm run start:windows -- dev",
"start:x11": "DISPLAY=\"${DISPLAY:=:0}\" ./node_modules/.bin/electron js/electron.js", "start:x11": "DISPLAY=\"${DISPLAY:=:0}\" ./node_modules/.bin/electron js/electron.js",
"start:x11:dev": "npm run start -- dev", "start:x11:dev": "npm run start:x11 -- dev",
"test": "NODE_ENV=test jest -i --forceExit", "test": "NODE_ENV=test jest -i --forceExit",
"test:calendar": "node ./modules/default/calendar/debug.js", "test:calendar": "node ./modules/default/calendar/debug.js",
"test:coverage": "NODE_ENV=test jest --coverage -i --verbose false --forceExit", "test:coverage": "NODE_ENV=test jest --coverage -i --verbose false --forceExit",
"test:css": "stylelint 'css/main.css' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.css' --config .stylelintrc.json", "test:css": "stylelint 'css/main.css' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.css' --config .stylelintrc.json",
"test:e2e": "NODE_ENV=test jest --selectProjects e2e -i --forceExit", "test:e2e": "NODE_ENV=test jest --selectProjects e2e -i --forceExit",
"test:electron": "NODE_ENV=test jest --selectProjects electron -i --forceExit", "test:electron": "NODE_ENV=test jest --selectProjects electron -i --forceExit",
"test:js": "eslint .", "test:js": "eslint",
"test:markdown": "markdownlint-cli2 .", "test:markdown": "markdownlint-cli2 .",
"test:prettier": "prettier . --check", "test:prettier": "prettier . --check",
"test:spelling": "cspell . --gitignore", "test:spelling": "cspell . --gitignore",
@ -63,14 +63,14 @@
}, },
"dependencies": { "dependencies": {
"ajv": "^8.17.1", "ajv": "^8.17.1",
"ansis": "^3.5.2", "ansis": "^3.17.0",
"console-stamp": "^3.1.2", "console-stamp": "^3.1.2",
"envsub": "^4.1.0", "envsub": "^4.1.0",
"eslint": "^9.17.0", "eslint": "^9.23.0",
"express": "^4.21.2", "express": "^4.21.2",
"express-ipfilter": "^1.3.2", "express-ipfilter": "^1.3.2",
"feedme": "^2.0.2", "feedme": "^2.0.2",
"helmet": "^8.0.0", "helmet": "^8.1.0",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"module-alias": "^2.2.3", "module-alias": "^2.2.3",
@ -79,34 +79,34 @@
"pm2": "^5.4.3", "pm2": "^5.4.3",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"suncalc": "^1.9.0", "suncalc": "^1.9.0",
"systeminformation": "^5.24.3", "systeminformation": "^5.25.11",
"undici": "^7.2.0" "undici": "^7.6.0"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/eslint-plugin": "^2.12.1", "@stylistic/eslint-plugin": "^4.2.0",
"cspell": "^8.17.1", "cspell": "^8.18.1",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.10.0", "eslint-plugin-jest": "^28.11.0",
"eslint-plugin-jsdoc": "^50.6.1", "eslint-plugin-jsdoc": "^50.6.9",
"eslint-plugin-package-json": "^0.19.0", "eslint-plugin-package-json": "^0.29.0",
"express-basic-auth": "^1.2.1", "express-basic-auth": "^1.2.1",
"husky": "^9.1.7", "husky": "^9.1.7",
"jest": "^29.7.0", "jest": "^29.7.0",
"jsdom": "^25.0.1", "jsdom": "^26.0.0",
"lint-staged": "^15.3.0", "lint-staged": "^15.5.0",
"markdownlint-cli2": "^0.17.1", "markdownlint-cli2": "^0.17.2",
"playwright": "^1.49.1", "playwright": "^1.51.1",
"prettier": "^3.4.2", "prettier": "^3.5.3",
"sinon": "^19.0.2", "sinon": "^20.0.0",
"stylelint": "^16.12.0", "stylelint": "^16.17.0",
"stylelint-config-standard": "^36.0.1", "stylelint-config-standard": "^37.0.0",
"stylelint-prettier": "^5.0.2" "stylelint-prettier": "^5.0.3"
}, },
"optionalDependencies": { "optionalDependencies": {
"electron": "^32.2.7" "electron": "^35.1.2"
}, },
"engines": { "engines": {
"node": ">=20.18.1 <21 || >=22" "node": ">=22.14.0"
}, },
"_moduleAliases": { "_moduleAliases": {
"node_helper": "js/node_helper.js", "node_helper": "js/node_helper.js",

View File

@ -4,5 +4,5 @@ const Log = require("../js/logger");
app.start().then((config) => { app.start().then((config) => {
const bindAddress = config.address ? config.address : "localhost"; const bindAddress = config.address ? config.address : "localhost";
const httpType = config.useHttps ? "https" : "http"; const httpType = config.useHttps ? "https" : "http";
Log.info(`\n>>> Ready to go! Please point your browser to: ${httpType}://${bindAddress}:${config.port} <<<`); Log.info(`\n>>> Ready to go! Please point your browser to: ${httpType}://${bindAddress}:${global.mmPort || config.port} <<<`);
}); });

View File

@ -0,0 +1,33 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 24,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
fade: false,
urgency: 0,
dateFormat: "Do.MMM, HH:mm",
fullDayEventDateFormat: "Do.MMM",
timeFormat: "absolute",
getRelative: 0,
maximumNumberOfDays: 28,
showEnd: true,
calendars: [
{
maximumEntries: 100,
url: "http://localhost:8080/tests/mocks/chicago-nyedge.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@ -0,0 +1,39 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 12,
foreignModulesDir: "tests/mocks",
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
maximumEntries: 1,
calendars: [
{
fetchInterval: 10000, //7 * 24 * 60 * 60 * 1000,
symbol: ["calendar-check", "google"],
url: "http://localhost:8080/tests/mocks/12_events.ics"
}
]
}
},
{
module: "testNotification",
position: "bottom_bar",
config: {
debug: true,
match: {
matchtype: "count",
notificationID: "CALENDAR_EVENTS"
}
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@ -0,0 +1,28 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 12,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
maximumEntries: 1,
calendars: [
{
fetchInterval: 7 * 24 * 60 * 60 * 1000,
symbol: ["calendar-check", "google"],
url: "https://ics.calendarlabs.com/76/mm3137/US_Holidays.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@ -0,0 +1,22 @@
let config = {
address: "0.0.0.0",
ipWhitelist: [],
timeFormat: 12,
modules: [
{
module: "compliments",
position: "middle_center",
config: {
compliments: {
evening: ["Evening here"]
}
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@ -38,6 +38,13 @@ describe("Clock module", () => {
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/; const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/;
await expect(helpers.testMatch(".clock .time", timeRegex)).resolves.toBe(true); await expect(helpers.testMatch(".clock .time", timeRegex)).resolves.toBe(true);
}); });
it("check for discreet elements of clock", async () => {
let elemClock = helpers.waitForElement(".clock-hour-digital");
await expect(elemClock).not.toBeNull();
elemClock = helpers.waitForElement(".clock-minute-digital");
await expect(elemClock).not.toBeNull();
});
}); });
describe("with showPeriodUpper config enabled", () => { describe("with showPeriodUpper config enabled", () => {

View File

@ -19,7 +19,7 @@ describe("Electron app environment", () => {
describe("Development console tests", () => { describe("Development console tests", () => {
beforeEach(async () => { beforeEach(async () => {
await helpers.startApplication("tests/configs/modules/display.js", null, ["js/electron.js", "dev"]); await helpers.startApplication("tests/configs/modules/display.js", null, ["dev"]);
}); });
afterEach(async () => { afterEach(async () => {

View File

@ -3,7 +3,7 @@
// https://www.anycodings.com/1questions/958135/can-i-set-the-date-for-playwright-browser // https://www.anycodings.com/1questions/958135/can-i-set-the-date-for-playwright-browser
const { _electron: electron } = require("playwright"); const { _electron: electron } = require("playwright");
exports.startApplication = async (configFilename, systemDate = null, electronParams = ["js/electron.js"], timezone = "GMT") => { exports.startApplication = async (configFilename, systemDate = null, electronParams = [], timezone = "GMT") => {
global.electronApp = null; global.electronApp = null;
global.page = null; global.page = null;
process.env.MM_CONFIG_FILE = configFilename; process.env.MM_CONFIG_FILE = configFilename;
@ -13,6 +13,13 @@ exports.startApplication = async (configFilename, systemDate = null, electronPar
} }
process.env.mmTestMode = "true"; process.env.mmTestMode = "true";
// check environment for DISPLAY or WAYLAND_DISPLAY
if (process.env.WAYLAND_DISPLAY) {
electronParams.unshift("js/electron.js", "--enable-features=UseOzonePlatform", "--ozone-platform=wayland");
} else {
electronParams.unshift("js/electron.js");
}
global.electronApp = await electron.launch({ args: electronParams }); global.electronApp = await electron.launch({ args: electronParams });
await global.electronApp.firstWindow(); await global.electronApp.firstWindow();
@ -42,10 +49,10 @@ exports.stopApplication = async () => {
process.env.MOCK_DATE = undefined; process.env.MOCK_DATE = undefined;
}; };
exports.getElement = async (selector) => { exports.getElement = async (selector, state = "visible") => {
expect(global.page).not.toBeNull(); expect(global.page).not.toBeNull();
let elem = global.page.locator(selector); const elem = global.page.locator(selector);
await elem.waitFor(); await elem.waitFor({ state: state });
expect(elem).not.toBeNull(); expect(elem).not.toBeNull();
return elem; return elem;
}; };

View File

@ -13,9 +13,9 @@ describe("Calendar module", () => {
return true; return true;
}; };
const doTestCount = async () => { const doTestCount = async (locator = ".calendar .event") => {
expect(global.page).not.toBeNull(); expect(global.page).not.toBeNull();
const loc = await global.page.locator(".calendar .event"); const loc = await global.page.locator(locator);
const elem = loc.first(); const elem = loc.first();
await elem.waitFor(); await elem.waitFor();
expect(elem).not.toBeNull(); expect(elem).not.toBeNull();
@ -32,8 +32,8 @@ describe("Calendar module", () => {
// uses playwright nth locator syntax // uses playwright nth locator syntax
const doTestTableContent = async (table_row, table_column, content, row = first) => { const doTestTableContent = async (table_row, table_column, content, row = first) => {
const elem = await global.page.locator(table_row); const elem = await global.page.locator(table_row);
const date = await global.page.locator(table_column).locator(`nth=${row}`); const column = await elem.locator(table_column).locator(`nth=${row}`);
await expect(date.textContent()).resolves.toContain(content); await expect(column.textContent()).resolves.toContain(content);
return true; return true;
}; };
@ -81,7 +81,7 @@ describe("Calendar module", () => {
*/ */
describe("rrule", () => { describe("rrule", () => {
it("Issue #3393 recurrence dates past rrule until date", async () => { 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 helpers.startApplication("tests/configs/modules/calendar/rrule_until.js", "07 Mar 2024 10:38:00 GMT-07:00", [], "America/Los_Angeles");
await expect(doTestCount()).resolves.toBe(1); await expect(doTestCount()).resolves.toBe(1);
}); });
}); });
@ -98,20 +98,20 @@ describe("Calendar module", () => {
*/ */
describe("Exdate: LA crossover DST before midnight GMT", () => { describe("Exdate: LA crossover DST before midnight GMT", () => {
it("LA crossover DST before midnight GMT should have 2 events", async () => { 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"); await helpers.startApplication("tests/configs/modules/calendar/exdate_la_before_midnight.js", "19 Oct 2023 12:30:00 GMT-07:00", [], "America/Los_Angeles");
await expect(doTestCount()).resolves.toBe(2); await expect(doTestCount()).resolves.toBe(2);
}); });
}); });
describe("Exdate: LA crossover DST at midnight GMT local STD", () => { describe("Exdate: LA crossover DST at midnight GMT local STD", () => {
it("LA crossover DST before midnight GMT should have 2 events", async () => { 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"); await helpers.startApplication("tests/configs/modules/calendar/exdate_la_at_midnight_std.js", "19 Oct 2023 12:30:00 GMT-07:00", [], "America/Los_Angeles");
await expect(doTestCount()).resolves.toBe(2); await expect(doTestCount()).resolves.toBe(2);
}); });
}); });
describe("Exdate: LA crossover DST at midnight GMT local DST", () => { describe("Exdate: LA crossover DST at midnight GMT local DST", () => {
it("LA crossover DST before midnight GMT should have 2 events", async () => { 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"); await helpers.startApplication("tests/configs/modules/calendar/exdate_la_at_midnight_dst.js", "19 Oct 2023 12:30:00 GMT-07:00", [], "America/Los_Angeles");
await expect(doTestCount()).resolves.toBe(2); await expect(doTestCount()).resolves.toBe(2);
}); });
}); });
@ -128,19 +128,19 @@ describe("Calendar module", () => {
*/ */
describe("Exdate: SYD crossover DST before midnight GMT", () => { describe("Exdate: SYD crossover DST before midnight GMT", () => {
it("LA crossover DST before midnight GMT should have 2 events", async () => { 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"); await helpers.startApplication("tests/configs/modules/calendar/exdate_syd_before_midnight.js", "14 Sep 2023 12:30:00 GMT+10:00", [], "Australia/Sydney");
await expect(doTestCount()).resolves.toBe(2); await expect(doTestCount()).resolves.toBe(2);
}); });
}); });
describe("Exdate: SYD crossover DST at midnight GMT local STD", () => { describe("Exdate: SYD crossover DST at midnight GMT local STD", () => {
it("LA crossover DST before midnight GMT should have 2 events", async () => { 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"); await helpers.startApplication("tests/configs/modules/calendar/exdate_syd_at_midnight_std.js", "14 Sep 2023 12:30:00 GMT+10:00", [], "Australia/Sydney");
await expect(doTestCount()).resolves.toBe(2); await expect(doTestCount()).resolves.toBe(2);
}); });
}); });
describe("Exdate: SYD crossover DST at midnight GMT local DST", () => { describe("Exdate: SYD crossover DST at midnight GMT local DST", () => {
it("SYD crossover DST at midnight GMT local DST should have 2 events", async () => { 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 helpers.startApplication("tests/configs/modules/calendar/exdate_syd_at_midnight_dst.js", "14 Sep 2023 12:30:00 GMT+10:00", [], "Australia/Sydney");
await expect(doTestCount()).resolves.toBe(2); await expect(doTestCount()).resolves.toBe(2);
}); });
}); });
@ -151,7 +151,7 @@ describe("Calendar module", () => {
*/ */
describe("sliceMultiDayEvents", () => { describe("sliceMultiDayEvents", () => {
it("Issue #3452 split multiday in Europe", async () => { 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"); await helpers.startApplication("tests/configs/modules/calendar/sliceMultiDayEvents.js", "01 Sept 2024 10:38:00 GMT+02:00", [], "Europe/Berlin");
expect(global.page).not.toBeNull(); expect(global.page).not.toBeNull();
const loc = await global.page.locator(".calendar .event"); const loc = await global.page.locator(".calendar .event");
const elem = loc.first(); const elem = loc.first();
@ -164,56 +164,56 @@ describe("Calendar module", () => {
describe("sliceMultiDayEvents direct count", () => { describe("sliceMultiDayEvents direct count", () => {
it("Issue #3452 split multiday in Europe", async () => { 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"); await helpers.startApplication("tests/configs/modules/calendar/sliceMultiDayEvents.js", "01 Sept 2024 10:38:00 GMT+02:00", [], "Europe/Berlin");
await expect(doTestCount()).resolves.toBe(6); await expect(doTestCount()).resolves.toBe(6);
}); });
}); });
describe("germany timezone", () => { describe("germany timezone", () => {
it("Issue #unknown fullday timezone East of UTC edge", async () => { it("Issue #unknown fullday timezone East of UTC edge", async () => {
await helpers.startApplication("tests/configs/modules/calendar/germany_at_end_of_day_repeating.js", "01 Oct 2024 10:38:00 GMT+02:00", ["js/electron.js"], "Europe/Berlin"); await helpers.startApplication("tests/configs/modules/calendar/germany_at_end_of_day_repeating.js", "01 Oct 2024 10:38:00 GMT+02:00", [], "Europe/Berlin");
await expect(doTestTableContent(".calendar .event", ".time", "Oct 22nd, 23:00", first)).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "Oct 22nd, 23:00", first)).resolves.toBe(true);
}); });
}); });
describe("germany all day repeating moved (recurrence and exdate)", () => { describe("germany all day repeating moved (recurrence and exdate)", () => {
it("Issue #unknown fullday timezone East of UTC event moved", async () => { it("Issue #unknown fullday timezone East of UTC event moved", async () => {
await helpers.startApplication("tests/configs/modules/calendar/3_move_first_allday_repeating_event.js", "01 Oct 2024 10:38:00 GMT+02:00", ["js/electron.js"], "Europe/Berlin"); await helpers.startApplication("tests/configs/modules/calendar/3_move_first_allday_repeating_event.js", "01 Oct 2024 10:38:00 GMT+02:00", [], "Europe/Berlin");
await expect(doTestTableContent(".calendar .event", ".time", "12th.Oct")).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "12th.Oct")).resolves.toBe(true);
}); });
}); });
describe("chicago late in timezone", () => { describe("chicago late in timezone", () => {
it("Issue #unknown rrule US close to timezone edge", async () => { it("Issue #unknown rrule US close to timezone edge", async () => {
await helpers.startApplication("tests/configs/modules/calendar/chicago_late_in_timezone.js", "01 Sept 2024 10:38:00 GMT-5:00", ["js/electron.js"], "America/Chicago"); await helpers.startApplication("tests/configs/modules/calendar/chicago_late_in_timezone.js", "01 Sept 2024 10:38:00 GMT-5:00", [], "America/Chicago");
await expect(doTestTableContent(".calendar .event", ".time", "10th.Sep, 20:15")).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "10th.Sep, 20:15")).resolves.toBe(true);
}); });
}); });
describe("berlin late in day event moved, viewed from berlin", () => { describe("berlin late in day event moved, viewed from berlin", () => {
it("Issue #unknown rrule ETC+2 close to timezone edge", async () => { it("Issue #unknown rrule ETC+2 close to timezone edge", async () => {
await helpers.startApplication("tests/configs/modules/calendar/end_of_day_berlin_moved.js", "08 Oct 2024 12:30:00 GMT+02:00", ["js/electron.js"], "Europe/Berlin"); await helpers.startApplication("tests/configs/modules/calendar/end_of_day_berlin_moved.js", "08 Oct 2024 12:30:00 GMT+02:00", [], "Europe/Berlin");
await expect(doTestTableContent(".calendar .event", ".time", "24th.Oct, 23:00-00:00", last)).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "24th.Oct, 23:00-00:00", last)).resolves.toBe(true);
}); });
}); });
describe("berlin late in day event moved, viewed from sydney", () => { describe("berlin late in day event moved, viewed from sydney", () => {
it("Issue #unknown rrule ETC+2 close to timezone edge", async () => { it("Issue #unknown rrule ETC+2 close to timezone edge", async () => {
await helpers.startApplication("tests/configs/modules/calendar/end_of_day_berlin_moved.js", "08 Oct 2024 12:30:00 GMT+02:00", ["js/electron.js"], "Australia/Sydney"); await helpers.startApplication("tests/configs/modules/calendar/end_of_day_berlin_moved.js", "08 Oct 2024 12:30:00 GMT+02:00", [], "Australia/Sydney");
await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 01:00-02:00", last)).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 01:00-02:00", last)).resolves.toBe(true);
}); });
}); });
describe("berlin late in day event moved, viewed from chicago", () => { describe("berlin late in day event moved, viewed from chicago", () => {
it("Issue #unknown rrule ETC+2 close to timezone edge", async () => { it("Issue #unknown rrule ETC+2 close to timezone edge", async () => {
await helpers.startApplication("tests/configs/modules/calendar/end_of_day_berlin_moved.js", "08 Oct 2024 12:30:00 GMT+02:00", ["js/electron.js"], "America/Chicago"); await helpers.startApplication("tests/configs/modules/calendar/end_of_day_berlin_moved.js", "08 Oct 2024 12:30:00 GMT+02:00", [], "America/Chicago");
await expect(doTestTableContent(".calendar .event", ".time", "24th.Oct, 16:00-17:00", last)).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "24th.Oct, 16:00-17:00", last)).resolves.toBe(true);
}); });
}); });
describe("berlin multi-events inside offset", () => { describe("berlin multi-events inside offset", () => {
it("some events before DST. some after midnight", async () => { it("some events before DST. some after midnight", async () => {
await helpers.startApplication("tests/configs/modules/calendar/berlin_multi.js", "08 Oct 2024 12:30:00 GMT+02:00", ["js/electron.js"], "Europe/Berlin"); await helpers.startApplication("tests/configs/modules/calendar/berlin_multi.js", "08 Oct 2024 12:30:00 GMT+02:00", [], "Europe/Berlin");
await expect(doTestTableContent(".calendar .event", ".time", "30th.Oct, 00:00-01:00", last)).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "30th.Oct, 00:00-01:00", last)).resolves.toBe(true);
await expect(doTestTableContent(".calendar .event", ".time", "21st.Oct, 00:00-01:00", first)).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "21st.Oct, 00:00-01:00", first)).resolves.toBe(true);
}); });
@ -221,7 +221,7 @@ describe("Calendar module", () => {
describe("berlin whole day repeating, start moved after end", () => { describe("berlin whole day repeating, start moved after end", () => {
it("some events before DST. some after", async () => { it("some events before DST. some after", async () => {
await helpers.startApplication("tests/configs/modules/calendar/berlin_whole_day_event_moved_over_dst_change.js", "08 Oct 2024 12:30:00 GMT+02:00", ["js/electron.js"], "Europe/Berlin"); await helpers.startApplication("tests/configs/modules/calendar/berlin_whole_day_event_moved_over_dst_change.js", "08 Oct 2024 12:30:00 GMT+02:00", [], "Europe/Berlin");
await expect(doTestTableContent(".calendar .event", ".time", "30th.Oct", last)).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "30th.Oct", last)).resolves.toBe(true);
await expect(doTestTableContent(".calendar .event", ".time", "27th.Oct", first)).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "27th.Oct", first)).resolves.toBe(true);
}); });
@ -229,7 +229,7 @@ describe("Calendar module", () => {
describe("berlin 11pm-midnight", () => { describe("berlin 11pm-midnight", () => {
it("right inside the offset, before midnight", async () => { it("right inside the offset, before midnight", async () => {
await helpers.startApplication("tests/configs/modules/calendar/berlin_end_of_day_repeating.js", "08 Oct 2024 12:30:00 GMT+02:00", ["js/electron.js"], "Europe/Berlin"); await helpers.startApplication("tests/configs/modules/calendar/berlin_end_of_day_repeating.js", "08 Oct 2024 12:30:00 GMT+02:00", [], "Europe/Berlin");
await expect(doTestTableContent(".calendar .event", ".time", "24th.Oct, 23:00-00:00", last)).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "24th.Oct, 23:00-00:00", last)).resolves.toBe(true);
await expect(doTestTableContent(".calendar .event", ".time", "22nd.Oct, 23:00-00:00", first)).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "22nd.Oct, 23:00-00:00", first)).resolves.toBe(true);
}); });
@ -237,7 +237,7 @@ describe("Calendar module", () => {
describe("both moved and delete events in recurring list", () => { describe("both moved and delete events in recurring list", () => {
it("with moved before and after original", async () => { it("with moved before and after original", async () => {
await helpers.startApplication("tests/configs/modules/calendar/exdate_and_recurrence_together.js", "08 Oct 2024 12:30:00 GMT-07:00", ["js/electron.js"], "America/Los_Angeles"); await helpers.startApplication("tests/configs/modules/calendar/exdate_and_recurrence_together.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Los_Angeles");
// moved after end at oct 26 // moved after end at oct 26
await expect(doTestTableContent(".calendar .event", ".time", "27th.Oct, 14:30-15:30", last)).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "27th.Oct, 14:30-15:30", last)).resolves.toBe(true);
// moved before start at oct 23 // moved before start at oct 23
@ -249,15 +249,20 @@ describe("Calendar module", () => {
describe("one event diff tz", () => { describe("one event diff tz", () => {
it("start/end in diff timezones", async () => { it("start/end in diff timezones", async () => {
await helpers.startApplication("tests/configs/modules/calendar/diff_tz_start_end.js", "08 Oct 2024 12:30:00 GMT-07:00", ["js/electron.js"], "America/Chicago"); await helpers.startApplication("tests/configs/modules/calendar/diff_tz_start_end.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago");
// just // just
await expect(doTestTableContent(".calendar .event", ".time", "29th.Oct, 05:00-30th.Oct, 18:00", first)).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "29th.Oct, 05:00-30th.Oct, 18:00", first)).resolves.toBe(true);
}); });
it("viewing from further west in diff timezones", async () => {
await helpers.startApplication("tests/configs/modules/calendar/chicago-looking-at-ny-recurring.js", "22 Jan 2025 14:30:00 GMT-06:00", [], "America/Chicago");
// just
await expect(doTestTableContent(".calendar .event", ".time", "22nd.Jan, 17:30-19:30", first)).resolves.toBe(true);
});
}); });
describe("one event non repeating", () => { describe("one event non repeating", () => {
it("fullday non-repeating", async () => { it("fullday non-repeating", async () => {
await helpers.startApplication("tests/configs/modules/calendar/fullday_event_over_multiple_days_nonrepeating.js", "08 Oct 2024 12:30:00 GMT-07:00", ["js/electron.js"], "America/Chicago"); await helpers.startApplication("tests/configs/modules/calendar/fullday_event_over_multiple_days_nonrepeating.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago");
// just // just
await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct-30th.Oct", first)).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct-30th.Oct", first)).resolves.toBe(true);
}); });
@ -265,7 +270,7 @@ describe("Calendar module", () => {
describe("one event no end display", () => { describe("one event no end display", () => {
it("don't display end", async () => { it("don't display end", async () => {
await helpers.startApplication("tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_no_display_end.js", "08 Oct 2024 12:30:00 GMT-07:00", ["js/electron.js"], "America/Chicago"); await helpers.startApplication("tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_no_display_end.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago");
// just // just
await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00", first)).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00", first)).resolves.toBe(true);
}); });
@ -273,10 +278,27 @@ describe("Calendar module", () => {
describe("display end display end", () => { describe("display end display end", () => {
it("display end", async () => { it("display end", async () => {
await helpers.startApplication("tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end.js", "08 Oct 2024 12:30:00 GMT-07:00", ["js/electron.js"], "America/Chicago"); await helpers.startApplication("tests/configs/modules/calendar/event_with_time_over_multiple_days_non_repeating_display_end.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago");
// just // just
await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-26th.Oct, 06:00", first)).resolves.toBe(true); await expect(doTestTableContent(".calendar .event", ".time", "25th.Oct, 20:00-26th.Oct, 06:00", first)).resolves.toBe(true);
}); });
}); });
describe("count and check symbols", () => {
it("in array", async () => {
await helpers.startApplication("tests/configs/modules/calendar/symboltest.js", "08 Oct 2024 12:30:00 GMT-07:00", [], "America/Chicago");
// just
await expect(doTestCount(".calendar .event .symbol .fa-fw")).resolves.toBe(2);
await expect(doTestCount(".calendar .event .symbol .fa-calendar-check")).resolves.toBe(1);
await expect(doTestCount(".calendar .event .symbol .fa-google")).resolves.toBe(1);
});
});
describe("count events broadcast", () => {
it("get 12 with maxentries set to 1", async () => {
await helpers.startApplication("tests/configs/modules/calendar/countCalendarEvents.js", "01 Jan 2024 12:30:00 GMT-076:00", [], "America/Chicago");
await expect(doTestTableContent(".testNotification", ".elementCount", "12", first)).resolves.toBe(true);
});
});
}); });

View File

@ -7,9 +7,9 @@ describe("Compliments module", () => {
* @param {Array} complimentsArray The array of compliments. * @param {Array} complimentsArray The array of compliments.
* @returns {boolean} result * @returns {boolean} result
*/ */
const doTest = async (complimentsArray) => { const doTest = async (complimentsArray, state = "visible") => {
await helpers.getElement(".compliments"); await helpers.getElement(".compliments", state);
const elem = await helpers.getElement(".module-content"); const elem = await helpers.getElement(".module-content", state);
expect(elem).not.toBeNull(); expect(elem).not.toBeNull();
expect(complimentsArray).toContain(await elem.textContent()); expect(complimentsArray).toContain(await elem.textContent());
return true; return true;
@ -34,6 +34,11 @@ describe("Compliments module", () => {
await helpers.startApplication("tests/configs/modules/compliments/compliments_parts_day.js", "01 Oct 2022 20:00:00 GMT"); await helpers.startApplication("tests/configs/modules/compliments/compliments_parts_day.js", "01 Oct 2022 20:00:00 GMT");
await expect(doTest(["Hello There", "Good Evening", "Evening test"])).resolves.toBe(true); await expect(doTest(["Hello There", "Good Evening", "Evening test"])).resolves.toBe(true);
}); });
it("doesnt show evening compliments during the day when the other parts of day are not set", async () => {
await helpers.startApplication("tests/configs/modules/compliments/compliments_evening.js", "01 Oct 2022 08:00:00 GMT");
await expect(doTest([""], "attached")).resolves.toBe(true);
});
}); });
describe("Feature date in compliments module", () => { describe("Feature date in compliments module", () => {

164
tests/mocks/12_events.ics Normal file
View File

@ -0,0 +1,164 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Calendar Labs//Calendar 1.0//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:US Holidays
X-WR-TIMEZONE:Etc/GMT
BEGIN:VEVENT
SUMMARY:Start of Month 1
DTSTART:20190101
DTEND:20190101
RRULE:FREQ=YEARLY;WKST=SU;INTERVAL=1
LOCATION:United States
DESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. \n\n Like us on Facebook: http://fb.com/calendarlabs to get updates
UID:5e52949sada28d231582470298@calendarlabs.com
DTSTAMP:20200223T150458Z
STATUS:CONFIRMED
TRANSP:TRANSPARENT
SEQUENCE:0
END:VEVENT
BEGIN:VEVENT
SUMMARY:Start of Month 2
DTSTART:20190201
DTEND:20190201
RRULE:FREQ=YEARLY;WKST=SU;INTERVAL=1
LOCATION:United States
DESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. \n\n Like us on Facebook: http://fb.com/calendarlabs to get updates
UID:5e52949a2wds8d231582470298@calendarlabs.com
DTSTAMP:20200223T150458Z
STATUS:CONFIRMED
TRANSP:TRANSPARENT
SEQUENCE:0
END:VEVENT
BEGIN:VEVENT
SUMMARY:Start of Month 3
DTSTART:20190301
DTEND:20190301
RRULE:FREQ=YEARLY;WKST=SU;INTERVAL=1
LOCATION:United States
DESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. \n\n Like us on Facebook: http://fb.com/calendarlabs to get updates
UID:5e52949a2SDD8d231582470298@calendarlabs.com
DTSTAMP:20200223T150458Z
STATUS:CONFIRMED
TRANSP:TRANSPARENT
SEQUENCE:0
END:VEVENT
BEGIN:VEVENT
SUMMARY:Start of Month 4
DTSTART:20190401
DTEND:20190401
RRULE:FREQ=YEARLY;WKST=SU;INTERVAL=1
LOCATION:United States
DESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. \n\n Like us on Facebook: http://fb.com/calendarlabs to get updates
UID:5e52949a2SDD8d231582FDSFD470298@calendarlabs.com
DTSTAMP:20200223T150458Z
STATUS:CONFIRMED
TRANSP:TRANSPARENT
SEQUENCE:0
END:VEVENT
BEGIN:VEVENT
SUMMARY:Start of Month 5
DTSTART:20190501
DTEND:20190501
RRULE:FREQ=YEARLY;WKST=SU;INTERVAL=1
LOCATION:United States
DESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. \n\n Like us on Facebook: http://fb.com/calendarlabs to get updates
UID:5e52949a2SDD8d2DD315824702598@calendarlabs.com
DTSTAMP:20200223T150458Z
STATUS:CONFIRMED
TRANSP:TRANSPARENT
SEQUENCE:0
END:VEVENT
BEGIN:VEVENT
SUMMARY:Start of Month 6
DTSTART:20190601
DTEND:20190601
RRULE:FREQ=YEARLY;WKST=SU;INTERVAL=1
LOCATION:United States
DESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. \n\n Like us on Facebook: http://fb.com/calendarlabs to get updates
UID:5e52949a2SDD8d2DD31582470298@calendarlabs.com
DTSTAMP:20200223T150458Z
STATUS:CONFIRMED
TRANSP:TRANSPARENT
SEQUENCE:0
END:VEVENT
BEGIN:VEVENT
SUMMARY:Start of Month 7
DTSTART:20190701
DTEND:20190701
RRULE:FREQ=YEARLY;WKST=SU;INTERVAL=1
LOCATION:United States
DESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. \n\n Like us on Facebook: http://fb.com/calendarlabs to get updates
UID:5e52942SDD8d2DD31582470298@calendarlabs.com
DTSTAMP:20200223T150458Z
STATUS:CONFIRMED
TRANSP:TRANSPARENT
SEQUENCE:0
END:VEVENT
BEGIN:VEVENT
SUMMARY:Start of Month 8
DTSTART:20190801
DTEND:20190801
RRULE:FREQ=YEARLY;WKST=SU;INTERVAL=1
LOCATION:United States
DESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. \n\n Like us on Facebook: http://fb.com/calendarlabs to get updates
UID:5e52949a2SDD8d2DDt31582470298@calendarlabs.com
DTSTAMP:20200223T150458Z
STATUS:CONFIRMED
TRANSP:TRANSPARENT
SEQUENCE:0
END:VEVENT
BEGIN:VEVENT
SUMMARY:Start of Month 9
DTSTART:20190901
DTEND:20190901
RRULE:FREQ=YEARLY;WKST=SU;INTERVAL=1
LOCATION:United States
DESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. \n\n Like us on Facebook: http://fb.com/calendarlabs to get updates
UID:5e529449a2SDD8d2DDt315824702798@calendarlabs.com
DTSTAMP:20200223T150458Z
STATUS:CONFIRMED
TRANSP:TRANSPARENT
SEQUENCE:0
END:VEVENT
BEGIN:VEVENT
SUMMARY:Start of Month 10
DTSTART:20191001
DTEND:20191001
RRULE:FREQ=YEARLY;WKST=SU;INTERVAL=1
LOCATION:United States
DESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. \n\n Like us on Facebook: http://fb.com/calendarlabs to get updates
UID:5e529449a2SDD8d2DDt31582470298@calendarlabs.com
DTSTAMP:20200223T150458Z
STATUS:CONFIRMED
TRANSP:TRANSPARENT
SEQUENCE:0
END:VEVENT
BEGIN:VEVENT
SUMMARY:Start of Month 11
DTSTART:20191101
DTEND:20191101
RRULE:FREQ=YEARLY;WKST=SU;INTERVAL=1
LOCATION:United States
DESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. \n\n Like us on Facebook: http://fb.com/calendarlabs to get updates
UID:5e5294449a2SDD8d2DDt31582470298@calendarlabs.com
DTSTAMP:20200223T150458Z
STATUS:CONFIRMED
TRANSP:TRANSPARENT
SEQUENCE:0
END:VEVENT
BEGIN:VEVENT
SUMMARY:Start of Month 12
DTSTART:20191201
DTEND:20191201
RRULE:FREQ=YEARLY;WKST=SU;INTERVAL=1
LOCATION:United States
DESCRIPTION:Visit https://calendarlabs.com/holidays/us/new-years-day.php to know more about New Year's Day. \n\n Like us on Facebook: http://fb.com/calendarlabs to get updates
UID:5e5294a2SDD8d2DDt31582470298@calendarlabs.com
DTSTAMP:20200223T150458Z
STATUS:CONFIRMED
TRANSP:TRANSPARENT
SEQUENCE:0
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,15 @@
BEGIN:VEVENT
DTSTART;TZID=America/New_York:20240918T183000
DTEND;TZID=America/New_York:20240918T203000
RRULE:FREQ=WEEKLY;BYDAY=WE
EXDATE;TZID=America/New_York:20241127T183000
EXDATE;TZID=America/New_York:20241225T183000
DTSTAMP:20250122T045443Z
UID:_@google.com
CREATED:20240916T131843Z
LAST-MODIFIED:20241222T235014Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Derby
TRANSP:OPAQUE
END:VEVENT

View File

@ -0,0 +1,59 @@
Module.register("testNotification", {
defaults: {
debug: false,
match: {
notificationID: "",
matchtype: "count"
//or
// type: 'contents' // look for item in field of content
}
},
count: 0,
table: null,
notificationReceived (notification, payload) {
if (notification === this.config.match.notificationID) {
if (this.config.match.matchtype === "count") {
this.count = payload.length;
if (this.count) {
this.table = document.createElement("table");
this.addTableRow(this.table, null, `${this.count}:elementCount`);
if (this.config.debug) {
payload.forEach((e, i) => {
this.addTableRow(this.table, i, e.title);
});
}
}
this.updateDom();
}
}
},
maketd (row, info) {
let td = document.createElement("td");
row.appendChild(td);
if (info !== null) {
let colinfo = info.toString().split(":");
if (colinfo.length === 2) td.className = colinfo[1];
td.innerText = colinfo[0];
}
return td;
},
addTableRow (table, col1 = null, col2 = null, col3 = null) {
let tableRow = document.createElement("tr");
table.appendChild(tableRow);
let tablecol1 = this.maketd(tableRow, col1);
let tablecol2 = this.maketd(tableRow, col2);
let tablecol3 = this.maketd(tableRow, col3);
return tableRow;
},
getDom () {
let wrapper = document.createElement("div");
if (this.table) {
wrapper.appendChild(this.table);
}
return wrapper;
}
});

48
translations/ar.json Normal file
View File

@ -0,0 +1,48 @@
{
"LOADING": "جار التحميل …",
"YESTERDAY": "أمس",
"TODAY": "اليوم",
"TOMORROW": "غدًا",
"RUNNING": "ينتهي خلال",
"EMPTY": "لا توجد أحداث قادمة.",
"WEEK": "الأسبوع {weekNumber}",
"N": "شمال",
"NNE": "شمال شمال شرقي",
"NE": "شمال شرقي",
"ENE": "شرق شمال شرقي",
"E": "شرق",
"ESE": "شرق جنوب شرقي",
"SE": "جنوب شرقي",
"SSE": "جنوب جنوب شرقي",
"S": "جنوب",
"SSW": "جنوب جنوب غربي",
"SW": "جنوب غربي",
"WSW": "غرب جنوب غربي",
"W": "غرب",
"WNW": "غرب شمال غربي",
"NW": "شمال غربي",
"NNW": "شمال شمال غربي",
"FEELS": "كأنها {DEGREE}",
"PRECIP_POP": "احتمالية الهطول",
"PRECIP_AMOUNT": "كمية الهطول",
"MODULE_CONFIG_CHANGED": "تم تغيير خيارات التهيئة لوحدة {MODULE_NAME}.\nيرجى مراجعة الوثائق.",
"MODULE_CONFIG_ERROR": "خطأ في وحدة {MODULE_NAME}. {ERROR}",
"MODULE_ERROR_MALFORMED_URL": "رابط غير صحيح.",
"MODULE_ERROR_NO_CONNECTION": "لا يوجد اتصال بالإنترنت.",
"MODULE_ERROR_UNAUTHORIZED": "فشل التصريح.",
"MODULE_ERROR_UNSPECIFIED": "تحقق من السجلات لمزيد من التفاصيل.",
"NEWSFEED_NO_ITEMS": "لا توجد أخبار في الوقت الحالي.",
"UPDATE_NOTIFICATION": "تحديث MagicMirror² متاح.",
"UPDATE_NOTIFICATION_MODULE": "تحديث متاح لوحدة {MODULE_NAME}.",
"UPDATE_INFO_SINGLE": "التثبيت الحالي متخلف عن تحديث واحد على فرع {BRANCH_NAME}.",
"UPDATE_INFO_MULTIPLE": "التثبيت الحالي متخلف عن {COMMIT_COUNT} تحديثات على فرع {BRANCH_NAME}.",
"UPDATE_NOTIFICATION_DONE": "تم التحديث لوحدة {MODULE_NAME}",
"UPDATE_NOTIFICATION_ERROR": "حدث خطأ أثناء تحديث وحدة {MODULE_NAME}",
"UPDATE_NOTIFICATION_NEED-RESTART": "يتطلب إعادة تشغيل MagicMirror."
}

View File

@ -1,8 +1,8 @@
{ {
"LOADING": "Φόρτωση ", "LOADING": "Φόρτωση ...",
"DAYBEFOREYESTERDAY": "Προχθές", "DAYBEFOREYESTERDAY": "Προχθές",
"YESTERDAY": "Εχθές", "YESTERDAY": "Χθες",
"TODAY": "Σήμερα", "TODAY": "Σήμερα",
"TOMORROW": "Αύριο", "TOMORROW": "Αύριο",
"RUNNING": "Λήγει σε", "RUNNING": "Λήγει σε",
@ -23,5 +23,26 @@
"W": "Δ", "W": "Δ",
"WNW": "ΔΒΔ", "WNW": "ΔΒΔ",
"NW": "ΒΔ", "NW": "ΒΔ",
"NNW": "ΒΒΔ" "NNW": "ΒΒΔ",
"FEELS": "Αίσθηση {DEGREE}",
"PRECIP_POP": "Πιθ. υετού",
"PRECIP_AMOUNT": "Ποσότητα υετού",
"MODULE_CONFIG_CHANGED": "Οι επιλογές διαμόρφωσης για το module {MODULE_NAME} έχουν αλλάξει.\nΕλέγξτε την τεκμηρίωση.",
"MODULE_CONFIG_ERROR": "Σφάλμα στο module {MODULE_NAME}. {ERROR}",
"MODULE_ERROR_MALFORMED_URL": "Λανθασμένη μορφή url.",
"MODULE_ERROR_NO_CONNECTION": "Δεν υπάρχει σύνδεση στο διαδίκτυο.",
"MODULE_ERROR_UNAUTHORIZED": "Η εξουσιοδότηση απέτυχε.",
"MODULE_ERROR_UNSPECIFIED": "Ελέγξτε τα αρχεία καταγραφής για περισσότερες λεπτομέρειες.",
"NEWSFEED_NO_ITEMS": "Δεν υπάρχουν ειδήσεις αυτή τη στιγμή.",
"UPDATE_NOTIFICATION": "Διατίθεται ενημέρωση MagicMirror².",
"UPDATE_NOTIFICATION_MODULE": "Διατίθεται ενημέρωση για το module {MODULE_NAME}.",
"UPDATE_INFO_SINGLE": "Η τρέχουσα εγκατάσταση είναι {COMMIT_COUNT} commit πίσω στο branch {BRANCH_NAME}.",
"UPDATE_INFO_MULTIPLE": "Η τρέχουσα εγκατάσταση είναι {COMMIT_COUNT} commit πίσω στο branch {BRANCH_NAME}",
"UPDATE_NOTIFICATION_DONE": "Η ενημέρωση ολοκληρώθηκε για το module {MODULE_NAME}",
"UPDATE_NOTIFICATION_ERROR": "Η ενημέρωση απέτυχε για το module {MODULE_NAME}",
"UPDATE_NOTIFICATION_NEED-RESTART": "Απαιτείται επανεκκίνηση του MagicMirror."
} }

50
translations/eo.json Normal file
View File

@ -0,0 +1,50 @@
{
"LOADING": "Ŝarĝas …",
"DAYBEFOREYESTERDAY": "antaŭhieraŭ",
"YESTERDAY": "hieraŭ",
"TODAY": "hodiaŭ",
"TOMORROW": "morgaŭ",
"DAYAFTERTOMORROW": "postmorgaŭ",
"RUNNING": "ankoraŭ",
"EMPTY": "Neniu evento.",
"WEEK": "{weekNumber}a kalendara semajno",
"N": "N",
"NNE": "NNOr",
"NE": "NOr",
"ENE": "OrNOr",
"E": "Or",
"ESE": "OrSOr",
"SE": "SOr",
"SSE": "SSOr",
"S": "S",
"SSW": "SSOk",
"SW": "SOk",
"WSW": "OkSOk",
"W": "Ok",
"WNW": "OkNOk",
"NW": "NOk",
"NNW": "NNOk",
"FEELS": "Perceptite kiel {DEGREE}",
"PRECIP_POP": "Ŝanco de pluvado",
"PRECIP_AMOUNT": "Kvanto de pluvo",
"MODULE_CONFIG_CHANGED": "La agordaj opcioj por la modulo „{MODULE_NAME}“ ŝanĝiĝis. \nBonvolu kontroli la dokumentadon.",
"MODULE_CONFIG_ERROR": "Eraro en la modulo „{MODULE_NAME}“. {ERROR}",
"MODULE_ERROR_MALFORMED_URL": "Malĝusta URL.",
"MODULE_ERROR_NO_CONNECTION": "Neniu interreta konekto.",
"MODULE_ERROR_UNAUTHORIZED": "Aŭtorigo malsukcesis.",
"MODULE_ERROR_UNSPECIFIED": "Kontrolu la protokolajn dosierojn por pli da detaloj.",
"NEWSFEED_NO_ITEMS": "Momente neniu novaĵoj.",
"UPDATE_NOTIFICATION": "Ĝisdatigo por MagicMirror² disponebla.",
"UPDATE_NOTIFICATION_MODULE": "Ĝisdatigo por la modulo „{MODULE_NAME}“ disponebla.",
"UPDATE_INFO_SINGLE": "La nuna instalado estas unu komito malantaŭ la {BRANCH_NAME}-branĉo.",
"UPDATE_INFO_MULTIPLE": "La nuna instalado estas {COMMIT_COUNT} komitoj malantaŭ la {BRANCH_NAME}-branĉo.",
"UPDATE_NOTIFICATION_DONE": "Ĝisdatigo por la modulo {MODULE_NAME} finita.",
"UPDATE_NOTIFICATION_ERROR": "Eraro dum la ĝisdatigo de la modulo {MODULE_NAME}.",
"UPDATE_NOTIFICATION_NEED-RESTART": "MagicMirror devas esti restartigita."
}

View File

@ -2,6 +2,7 @@ let translations = {
en: "translations/en.json", // English en: "translations/en.json", // English
nl: "translations/nl.json", // Dutch nl: "translations/nl.json", // Dutch
de: "translations/de.json", // German de: "translations/de.json", // German
eo: "translations/eo.json", // Esperanto
fi: "translations/fi.json", // Suomi fi: "translations/fi.json", // Suomi
fr: "translations/fr.json", // French fr: "translations/fr.json", // French
fy: "translations/fy.json", // Frysk fy: "translations/fy.json", // Frysk

8
vendor/package-lock.json generated vendored
View File

@ -13,7 +13,7 @@
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"croner": "^9.0.0", "croner": "^9.0.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"moment-timezone": "^0.5.46", "moment-timezone": "^0.5.48",
"nunjucks": "^3.2.4", "nunjucks": "^3.2.4",
"suncalc": "^1.9.0", "suncalc": "^1.9.0",
"weathericons": "^2.1.0" "weathericons": "^2.1.0"
@ -74,9 +74,9 @@
} }
}, },
"node_modules/moment-timezone": { "node_modules/moment-timezone": {
"version": "0.5.46", "version": "0.5.48",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.46.tgz", "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
"integrity": "sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==", "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"moment": "^2.29.4" "moment": "^2.29.4"

2
vendor/package.json vendored
View File

@ -15,7 +15,7 @@
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"croner": "^9.0.0", "croner": "^9.0.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"moment-timezone": "^0.5.46", "moment-timezone": "^0.5.48",
"nunjucks": "^3.2.4", "nunjucks": "^3.2.4",
"suncalc": "^1.9.0", "suncalc": "^1.9.0",
"weathericons": "^2.1.0" "weathericons": "^2.1.0"