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

View File

@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.18.1, 20.x, 22.x, 23.x]
node-version: [22.14.0, 22.x, 23.x]
steps:
- name: Checkout code
uses: actions/checkout@v4
@ -22,7 +22,9 @@ jobs:
- name: Install @electron/rebuild
run: npm install @electron/rebuild
- 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
run: npm install node-libgpiod
- name: Run electron-rebuild

View File

@ -28,4 +28,4 @@ jobs:
run: |
npm run install-mm:dev
- 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
operations-per-run: 100
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².
## [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
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-activate `eslint-plugin-package-json` to lint `package.json` (#3643)
- [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)
- [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)
@ -103,7 +152,7 @@ Thanks to: @bugsounet, @dkallen78, @jargordon, @khassel, @KristjanESPERANTO, @Ma
- [core] Detail optimizations in `config_check.js`
- [core] Updated minimal needed node version in `package.json` (currently v20.9.0) (#3559) and except for v21 (no security updates) (#3561)
- [linter] Switch to ESLint v9 and flat config and replace `eslint-plugin-unicorn` by `@eslint/js`
- [core] fix discovering module positions twice after #3450
- [core] Fix discovering module positions twice after #3450
### Fixed
@ -167,7 +216,7 @@ For more info, please read the following post: [A New Chapter for MagicMirror: T
### 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)
- Use node prefix for build-in modules (#3340)
- 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)
[2.31.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.30.0...v2.31.0
[2.30.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.29.0...v2.30.0
[2.29.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.28.0...v2.29.0
[2.28.0]: https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.27.0...v2.28.0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -153,11 +153,23 @@ function App () {
*/
function checkDeprecatedOptions (userConfig) {
const deprecated = require(`${global.root_path}/js/deprecated`);
const deprecatedOptions = deprecated.configs;
// check for deprecated core options
const deprecatedOptions = deprecated.configs;
const usedDeprecated = deprecatedOptions.filter((option) => userConfig.hasOwnProperty(option));
if (usedDeprecated.length > 0) {
Log.warn(`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;
} else {
// running in Jest, allow defaultModules placed under moduleDir for testing
if (env.modulesDir === "modules") {
if (env.modulesDir === "modules" || env.modulesDir === "tests/mocks") {
moduleFolder = defaultModuleFolder;
}
}

View File

@ -1,3 +1,4 @@
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, "");
const staticData = await si.get({
system: "manufacturer, model, raspberry, virtual",
system: "manufacturer, model, virtual",
osInfo: "platform, distro, release, arch",
versions: "kernel, node, npm, pm2"
});
let systemDataString = "System information:";
systemDataString += `\n### SYSTEM: manufacturer: ${staticData.system.manufacturer}; model: ${staticData.system.model}; virtual: ${staticData.system.virtual}`;
systemDataString += `\n### OS: platform: ${staticData.osInfo.platform}; distro: ${staticData.osInfo.distro}; release: ${staticData.osInfo.release}; arch: ${staticData.osInfo.arch}; kernel: ${staticData.versions.kernel}`;
systemDataString += `\n### VERSIONS: electron: ${process.versions.electron}; used node: ${staticData.versions.node}; installed node: ${installedNodeVersion}; npm: ${staticData.versions.npm}; pm2: ${staticData.versions.pm2}`;
systemDataString += `\n### OTHER: timeZone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}; ELECTRON_ENABLE_GPU: ${process.env.ELECTRON_ENABLE_GPU}`;
let systemDataString = `System information:
### SYSTEM: manufacturer: ${staticData.system.manufacturer}; model: ${staticData.system.model}; virtual: ${staticData.system.virtual}
### OS: platform: ${staticData.osInfo.platform}; distro: ${staticData.osInfo.distro}; release: ${staticData.osInfo.release}; arch: ${staticData.osInfo.arch}; kernel: ${staticData.versions.kernel}
### VERSIONS: electron: ${process.versions.electron}; used node: ${staticData.versions.node}; installed node: ${installedNodeVersion}; npm: ${staticData.versions.npm}; pm2: ${staticData.versions.pm2}
### OTHER: timeZone: ${Intl.DateTimeFormat().resolvedOptions().timeZone}; ELECTRON_ENABLE_GPU: ${process.env.ELECTRON_ENABLE_GPU}`
.replace(/\t/g, "");
Log.info(systemDataString);
// Return is currently only for jest

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -57,7 +57,7 @@ Module.register("newsfeed", {
// Define required translations.
getTranslations () {
// The translations for the default modules are defined in the core translation files.
// Therefor we can just return false. Otherwise we should have returned a dictionary.
// Therefore we can just return false. Otherwise we should have returned a dictionary.
// If you're trying to build your own module including translations, check out the documentation.
return false;
},
@ -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.
* @param {object} feeds An object with feeds returned by the node helper.
@ -188,7 +200,7 @@ Module.register("newsfeed", {
if (this.subscribedToFeed(feed)) {
for (let item of feedItems) {
item.sourceTitle = this.titleForFeed(feed);
if (!(this.config.ignoreOldItems && Date.now() - new Date(item.pubdate) > this.config.ignoreOlderThan)) {
if (!(this.getFeedProperty(feed, "ignoreOldItems") && Date.now() - new Date(item.pubdate) > this.getFeedProperty(feed, "ignoreOlderThan"))) {
newsItems.push(item);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,8 @@ Module.register("weather", {
updateInterval: 10 * 60 * 1000, // every 10 minutes
animationSpeed: 1000,
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,
showIndoorTemperature: false,
allowOverrideNotification: false,

View File

@ -128,14 +128,14 @@ const WeatherUtils = {
} else if (tempInF > 80 && humidity > 40) {
feelsLike
= -42.379
+ 2.04901523 * tempInF
+ 10.14333127 * humidity
- 0.22475541 * tempInF * humidity
- 6.83783 * Math.pow(10, -3) * tempInF * tempInF
- 5.481717 * Math.pow(10, -2) * humidity * humidity
+ 1.22874 * Math.pow(10, -3) * tempInF * tempInF * humidity
+ 8.5282 * Math.pow(10, -4) * tempInF * humidity * humidity
- 1.99 * Math.pow(10, -6) * tempInF * tempInF * humidity * humidity;
+ 2.04901523 * tempInF
+ 10.14333127 * humidity
- 0.22475541 * tempInF * humidity
- 6.83783 * Math.pow(10, -3) * tempInF * tempInF
- 5.481717 * Math.pow(10, -2) * humidity * humidity
+ 1.22874 * Math.pow(10, -3) * tempInF * tempInF * humidity
+ 8.5282 * Math.pow(10, -4) * tempInF * humidity * humidity
- 1.99 * Math.pow(10, -6) * tempInF * tempInF * humidity * humidity;
}
return ((feelsLike - 32) * 5) / 9;

2173
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "magicmirror",
"version": "2.30.0",
"version": "2.31.0",
"description": "The open source modular smart mirror platform.",
"keywords": [
"magic mirror",
@ -30,7 +30,7 @@
"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",
"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:prettier": "prettier . --write",
"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:dev": "npm run start:windows -- dev",
"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:calendar": "node ./modules/default/calendar/debug.js",
"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:e2e": "NODE_ENV=test jest --selectProjects e2e -i --forceExit",
"test:electron": "NODE_ENV=test jest --selectProjects electron -i --forceExit",
"test:js": "eslint .",
"test:js": "eslint",
"test:markdown": "markdownlint-cli2 .",
"test:prettier": "prettier . --check",
"test:spelling": "cspell . --gitignore",
@ -63,14 +63,14 @@
},
"dependencies": {
"ajv": "^8.17.1",
"ansis": "^3.5.2",
"ansis": "^3.17.0",
"console-stamp": "^3.1.2",
"envsub": "^4.1.0",
"eslint": "^9.17.0",
"eslint": "^9.23.0",
"express": "^4.21.2",
"express-ipfilter": "^1.3.2",
"feedme": "^2.0.2",
"helmet": "^8.0.0",
"helmet": "^8.1.0",
"html-to-text": "^9.0.5",
"iconv-lite": "^0.6.3",
"module-alias": "^2.2.3",
@ -79,34 +79,34 @@
"pm2": "^5.4.3",
"socket.io": "^4.8.1",
"suncalc": "^1.9.0",
"systeminformation": "^5.24.3",
"undici": "^7.2.0"
"systeminformation": "^5.25.11",
"undici": "^7.6.0"
},
"devDependencies": {
"@stylistic/eslint-plugin": "^2.12.1",
"cspell": "^8.17.1",
"@stylistic/eslint-plugin": "^4.2.0",
"cspell": "^8.18.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.10.0",
"eslint-plugin-jsdoc": "^50.6.1",
"eslint-plugin-package-json": "^0.19.0",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-jsdoc": "^50.6.9",
"eslint-plugin-package-json": "^0.29.0",
"express-basic-auth": "^1.2.1",
"husky": "^9.1.7",
"jest": "^29.7.0",
"jsdom": "^25.0.1",
"lint-staged": "^15.3.0",
"markdownlint-cli2": "^0.17.1",
"playwright": "^1.49.1",
"prettier": "^3.4.2",
"sinon": "^19.0.2",
"stylelint": "^16.12.0",
"stylelint-config-standard": "^36.0.1",
"stylelint-prettier": "^5.0.2"
"jsdom": "^26.0.0",
"lint-staged": "^15.5.0",
"markdownlint-cli2": "^0.17.2",
"playwright": "^1.51.1",
"prettier": "^3.5.3",
"sinon": "^20.0.0",
"stylelint": "^16.17.0",
"stylelint-config-standard": "^37.0.0",
"stylelint-prettier": "^5.0.3"
},
"optionalDependencies": {
"electron": "^32.2.7"
"electron": "^35.1.2"
},
"engines": {
"node": ">=20.18.1 <21 || >=22"
"node": ">=22.14.0"
},
"_moduleAliases": {
"node_helper": "js/node_helper.js",

View File

@ -4,5 +4,5 @@ const Log = require("../js/logger");
app.start().then((config) => {
const bindAddress = config.address ? config.address : "localhost";
const httpType = config.useHttps ? "https" : "http";
Log.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$/;
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", () => {

View File

@ -19,7 +19,7 @@ describe("Electron app environment", () => {
describe("Development console tests", () => {
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 () => {

View File

@ -3,7 +3,7 @@
// https://www.anycodings.com/1questions/958135/can-i-set-the-date-for-playwright-browser
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.page = null;
process.env.MM_CONFIG_FILE = configFilename;
@ -13,6 +13,13 @@ exports.startApplication = async (configFilename, systemDate = null, electronPar
}
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 });
await global.electronApp.firstWindow();
@ -42,10 +49,10 @@ exports.stopApplication = async () => {
process.env.MOCK_DATE = undefined;
};
exports.getElement = async (selector) => {
exports.getElement = async (selector, state = "visible") => {
expect(global.page).not.toBeNull();
let elem = global.page.locator(selector);
await elem.waitFor();
const elem = global.page.locator(selector);
await elem.waitFor({ state: state });
expect(elem).not.toBeNull();
return elem;
};

View File

@ -13,9 +13,9 @@ describe("Calendar module", () => {
return true;
};
const doTestCount = async () => {
const doTestCount = async (locator = ".calendar .event") => {
expect(global.page).not.toBeNull();
const loc = await global.page.locator(".calendar .event");
const loc = await global.page.locator(locator);
const elem = loc.first();
await elem.waitFor();
expect(elem).not.toBeNull();
@ -32,8 +32,8 @@ describe("Calendar module", () => {
// uses playwright nth locator syntax
const doTestTableContent = async (table_row, table_column, content, row = first) => {
const elem = await global.page.locator(table_row);
const date = await global.page.locator(table_column).locator(`nth=${row}`);
await expect(date.textContent()).resolves.toContain(content);
const column = await elem.locator(table_column).locator(`nth=${row}`);
await expect(column.textContent()).resolves.toContain(content);
return true;
};
@ -81,7 +81,7 @@ describe("Calendar module", () => {
*/
describe("rrule", () => {
it("Issue #3393 recurrence dates past rrule until date", async () => {
await helpers.startApplication("tests/configs/modules/calendar/rrule_until.js", "07 Mar 2024 10:38:00 GMT-07:00", ["js/electron.js"], "America/Los_Angeles");
await 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);
});
});
@ -98,20 +98,20 @@ describe("Calendar module", () => {
*/
describe("Exdate: LA crossover DST before midnight GMT", () => {
it("LA crossover DST before midnight GMT should have 2 events", async () => {
await helpers.startApplication("tests/configs/modules/calendar/exdate_la_before_midnight.js", "19 Oct 2023 12:30:00 GMT-07:00", ["js/electron.js"], "America/Los_Angeles");
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);
});
});
describe("Exdate: LA crossover DST at midnight GMT local STD", () => {
it("LA crossover DST before midnight GMT should have 2 events", async () => {
await helpers.startApplication("tests/configs/modules/calendar/exdate_la_at_midnight_std.js", "19 Oct 2023 12:30:00 GMT-07:00", ["js/electron.js"], "America/Los_Angeles");
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);
});
});
describe("Exdate: LA crossover DST at midnight GMT local DST", () => {
it("LA crossover DST before midnight GMT should have 2 events", async () => {
await helpers.startApplication("tests/configs/modules/calendar/exdate_la_at_midnight_dst.js", "19 Oct 2023 12:30:00 GMT-07:00", ["js/electron.js"], "America/Los_Angeles");
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);
});
});
@ -128,19 +128,19 @@ describe("Calendar module", () => {
*/
describe("Exdate: SYD crossover DST before midnight GMT", () => {
it("LA crossover DST before midnight GMT should have 2 events", async () => {
await helpers.startApplication("tests/configs/modules/calendar/exdate_syd_before_midnight.js", "14 Sep 2023 12:30:00 GMT+10:00", ["js/electron.js"], "Australia/Sydney");
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);
});
});
describe("Exdate: SYD crossover DST at midnight GMT local STD", () => {
it("LA crossover DST before midnight GMT should have 2 events", async () => {
await helpers.startApplication("tests/configs/modules/calendar/exdate_syd_at_midnight_std.js", "14 Sep 2023 12:30:00 GMT+10:00", ["js/electron.js"], "Australia/Sydney");
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);
});
});
describe("Exdate: SYD crossover DST at midnight GMT local DST", () => {
it("SYD crossover DST at midnight GMT local DST should have 2 events", async () => {
await helpers.startApplication("tests/configs/modules/calendar/exdate_syd_at_midnight_dst.js", "14 Sep 2023 12:30:00 GMT+10:00", ["js/electron.js"], "Australia/Sydney");
await 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);
});
});
@ -151,7 +151,7 @@ describe("Calendar module", () => {
*/
describe("sliceMultiDayEvents", () => {
it("Issue #3452 split multiday in Europe", async () => {
await helpers.startApplication("tests/configs/modules/calendar/sliceMultiDayEvents.js", "01 Sept 2024 10:38:00 GMT+02:00", ["js/electron.js"], "Europe/Berlin");
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();
const loc = await global.page.locator(".calendar .event");
const elem = loc.first();
@ -164,56 +164,56 @@ describe("Calendar module", () => {
describe("sliceMultiDayEvents direct count", () => {
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);
});
});
describe("germany timezone", () => {
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);
});
});
describe("germany all day repeating moved (recurrence and exdate)", () => {
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);
});
});
describe("chicago late in timezone", () => {
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);
});
});
describe("berlin late in day event moved, viewed from berlin", () => {
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);
});
});
describe("berlin late in day event moved, viewed from sydney", () => {
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);
});
});
describe("berlin late in day event moved, viewed from chicago", () => {
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);
});
});
describe("berlin multi-events inside offset", () => {
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", "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", () => {
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", "27th.Oct", first)).resolves.toBe(true);
});
@ -229,7 +229,7 @@ describe("Calendar module", () => {
describe("berlin 11pm-midnight", () => {
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", "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", () => {
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
await expect(doTestTableContent(".calendar .event", ".time", "27th.Oct, 14:30-15:30", last)).resolves.toBe(true);
// moved before start at oct 23
@ -249,15 +249,20 @@ describe("Calendar module", () => {
describe("one event diff tz", () => {
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
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", () => {
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
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", () => {
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
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", () => {
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
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.
* @returns {boolean} result
*/
const doTest = async (complimentsArray) => {
await helpers.getElement(".compliments");
const elem = await helpers.getElement(".module-content");
const doTest = async (complimentsArray, state = "visible") => {
await helpers.getElement(".compliments", state);
const elem = await helpers.getElement(".module-content", state);
expect(elem).not.toBeNull();
expect(complimentsArray).toContain(await elem.textContent());
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 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", () => {

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": "Προχθές",
"YESTERDAY": "Εχθές",
"YESTERDAY": "Χθες",
"TODAY": "Σήμερα",
"TOMORROW": "Αύριο",
"RUNNING": "Λήγει σε",
@ -23,5 +23,26 @@
"W": "Δ",
"WNW": "ΔΒΔ",
"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
nl: "translations/nl.json", // Dutch
de: "translations/de.json", // German
eo: "translations/eo.json", // Esperanto
fi: "translations/fi.json", // Suomi
fr: "translations/fr.json", // French
fy: "translations/fy.json", // Frysk

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

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

2
vendor/package.json vendored
View File

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