Release 2.23.0 (#3078)

## [2.23.0] - 2023-04-04

Thanks to: @angeldeejay, @buxxi, @CarJem, @dariom, @DaveChild, @dWoolridge, @grenagit, @Hirschberger, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @nfogal, @psieg, @rajniszp, @retroflex, @SkySails and @tomzt.

Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome!

### Added

- Added increments for hourly forecasts in weather module (#2996)
- Added tests for hourly weather forecast
- Added possibility to ignore MagicMirror repo in updatenotification module
- Added Pirate Weather as new weather provider (#3005)
- Added possibility to use your own templates in Alert module
- Added error message if `<modulename>.js` file is missing in module folder to get a hint in the logs (#2403)
- Added possibility to use environment variables in `config.js` (#1756)
- Added option `pastDaysCount` to default calendar module to control of how many days past events should be displayed
- Added thai language to alert module
- Added option `sendNotifications` in clock module (#3056)

### Removed

- Removed darksky weather provider
- Removed unneeded (and unwanted) '.' after the year in calendar repeatingCountTitle (#2896)

### Updated

- Use develop as target branch for dependabot
- Update issue template, contributing doc and sample config
- The weather modules clearly separates precipitation amount and probability (risk of rain/snow)
  - This requires all providers that only supports probability to change the config from `showPrecipitationAmount` to `showPrecipitationProbability`.
- Update tests for weather and calendar module
- Changed updatenotification module for MagicMirror repo only: Send only notifications for `master` if there is a tag on a newer commit
- Update dates in Calendar widgets every minute
- Cleanup jest coverage for patches
- Update `stylelint` dependencies, switch to `stylelint-config-standard` and handle `stylelint` issues, update `main.css` matching new rules
- Update Eslint config, add new rule and handle issue
- Convert lots of callbacks to async/await
- Revise require imports (#3071 and #3072)

### Fixed

- Fix wrong day labels in envcanada forecast (#2987)
- Fix for missing default class name prefix for customEvents in calendar
- Fix electron flashing white screen on startup (#1919)
- Fix weathergov provider hourly forecast (#3008)
- Fix message display with HTML code into alert module (#2828)
- Fix typo in french translation
- Yr wind direction is no longer inverted
- Fix async node_helper stopping electron start (#2487)
- The wind direction arrow now points in the direction the wind is flowing, not into the wind (#3019)
- Fix precipitation css styles and rounding value
- Fix wrong vertical alignment of calendar title column when wrapEvents is true (#3053)
- Fix empty news feed stopping the reload forever
- Fix e2e tests (failed after async changes) by running calendar and newsfeed tests last
- Lint: Use template literals instead of string concatenation
- Fix default alert module to render HTML for title and message
- Fix Open-Meteo wind speed units
This commit is contained in:
Michael Teeuw
2023-04-04 20:44:32 +02:00
committed by GitHub
parent f14e956166
commit abe5c08a52
162 changed files with 6619 additions and 3019 deletions

View File

@@ -1,9 +1,9 @@
{ {
"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"], "extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"],
"plugins": ["prettier", "jsdoc", "jest"], "plugins": ["prettier", "import", "jsdoc", "jest"],
"env": { "env": {
"browser": true, "browser": true,
"es6": true, "es2022": true,
"jest/globals": true, "jest/globals": true,
"node": true "node": true
}, },
@@ -16,16 +16,18 @@
}, },
"parserOptions": { "parserOptions": {
"sourceType": "module", "sourceType": "module",
"ecmaVersion": 2020, "ecmaVersion": 2022,
"ecmaFeatures": { "ecmaFeatures": {
"globalReturn": true "globalReturn": true
} }
}, },
"rules": { "rules": {
"prettier/prettier": "error",
"eqeqeq": "error", "eqeqeq": "error",
"import/order": "error",
"no-prototype-builtins": "off", "no-prototype-builtins": "off",
"no-throw-literal": "error",
"no-unused-vars": "off", "no-unused-vars": "off",
"no-useless-return": "error" "no-useless-return": "error",
"prefer-template": "error"
} }
} }

View File

@@ -6,7 +6,7 @@ We hold our code to standard, and these standards are documented below.
## Linters ## Linters
If you wish to run our linters, use `npm run lint` without any arguments. We use prettier for automatic linting of all our files: `npm run lint:prettier`.
### JavaScript: Run ESLint ### JavaScript: Run ESLint
@@ -18,7 +18,7 @@ To run ESLint, use `npm run lint:js`.
### CSS: Run StyleLint ### CSS: Run StyleLint
We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is in our .stylelintrc file. We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is in our `.stylelintrc` file.
To run StyleLint, use `npm run lint:css`. To run StyleLint, use `npm run lint:css`.
@@ -28,7 +28,8 @@ We use [Jest](https://jestjs.io) for JavaScript testing.
To run all tests, use `npm run test`. To run all tests, use `npm run test`.
The specific test commands are defined in `package.json`. So you can also run the specific tests with other commands, e.g. `npm run test:unit` or `npx jest tests/e2e/env_spec.js`. The specific test commands are defined in `package.json`.
So you can also run the specific tests with other commands, e.g. `npm run test:unit` or `npx jest tests/e2e/env_spec.js`.
## Submitting Issues ## Submitting Issues

View File

@@ -4,3 +4,7 @@ coverage:
default: default:
# advanced settings # advanced settings
informational: true informational: true
patch:
default:
threshold: 0%
target: 0

View File

@@ -4,3 +4,4 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
target-branch: "develop"

View File

@@ -20,14 +20,14 @@ jobs:
matrix: matrix:
node-version: [14.x, 16.x, 18.x] node-version: [14.x, 16.x, 18.x]
steps: steps:
- name: Checkout code - name: "Checkout code"
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }} - name: "Use Node.js ${{ matrix.node-version }}"
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: "npm" cache: "npm"
- name: Install dependencies and run tests - name: "Install dependencies and run tests"
run: | run: |
Xvfb :99 -screen 0 1024x768x16 & Xvfb :99 -screen 0 1024x768x16 &
export DISPLAY=:99 export DISPLAY=:99

View File

@@ -17,16 +17,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- name: Checkout code - name: "Checkout code"
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install dependencies and run coverage - name: "Install dependencies and run coverage"
run: | run: |
Xvfb :99 -screen 0 1024x768x16 & Xvfb :99 -screen 0 1024x768x16 &
export DISPLAY=:99 export DISPLAY=:99
npm ci npm ci
touch css/custom.css touch css/custom.css
npm run test:coverage npm run test:coverage
- name: Upload coverage results to codecov - name: "Upload coverage results to codecov"
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3
with: with:
files: ./coverage/lcov.info files: ./coverage/lcov.info

View File

@@ -1,4 +1,8 @@
name: "Dependency Review" # This workflow scans your pull requests for dependency changes, and will raise an error if any vulnerabilities or invalid licenses are being introduced.
# For more information see: https://github.com/actions/dependency-review-action
name: "Review Dependencies"
on: [pull_request] on: [pull_request]
permissions: permissions:
@@ -8,7 +12,7 @@ jobs:
dependency-review: dependency-review:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: "Checkout Repository" - name: "Checkout code"
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: "Dependency Review" - name: "Dependency Review"
uses: actions/dependency-review-action@v2 uses: actions/dependency-review-action@v3

View File

@@ -1,19 +0,0 @@
# This workflow enforces the update of a changelog file on every pull request
# For more information see: https://github.com/dangoslen/changelog-enforcer
name: "Enforce Changelog"
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Enforce changelog
uses: dangoslen/changelog-enforcer@v3
with:
changeLogPath: "CHANGELOG.md"
skipLabels: "Skip Changelog"

View File

@@ -0,0 +1,28 @@
# This workflow enforces on every pull request:
# - the update of our CHANGELOG.md file, see: https://github.com/dangoslen/changelog-enforcer
# - that the PR is not based against master, taken from https://github.com/oppia/oppia-android/pull/2832/files
name: "Enforce Pull-Request Rules"
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: "Enforce changelog"
uses: dangoslen/changelog-enforcer@v3
with:
changeLogPath: "CHANGELOG.md"
skipLabels: "Skip Changelog"
- name: "Enforce develop branch"
if: ${{ github.base_ref == 'master' && !contains(github.event.pull_request.labels.*.name, 'mastermerge') }}
run: |
echo "This PR is based against the master branch and not a release or hotfix."
echo "Please don't do this. Switch the branch to 'develop'."
exit 1
env:
BASE_BRANCH: ${{ github.base_ref }}

View File

@@ -1,5 +1,5 @@
{ {
"extends": ["stylelint-prettier/recommended"], "extends": ["stylelint-config-standard"],
"plugins": ["stylelint-prettier"], "plugins": ["stylelint-prettier"],
"rules": { "rules": {
"prettier/prettier": true "prettier/prettier": true

View File

@@ -5,6 +5,64 @@ This project adheres to [Semantic Versioning](https://semver.org/).
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror². ❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror².
## [2.23.0] - 2023-04-04
Thanks to: @angeldeejay, @buxxi, @CarJem, @dariom, @DaveChild, @dWoolridge, @grenagit, @Hirschberger, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @nfogal, @psieg, @rajniszp, @retroflex, @SkySails and @tomzt.
Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome!
### Added
- Added increments for hourly forecasts in weather module (#2996)
- Added tests for hourly weather forecast
- Added possibility to ignore MagicMirror repo in updatenotification module
- Added Pirate Weather as new weather provider (#3005)
- Added possibility to use your own templates in Alert module
- Added error message if `<modulename>.js` file is missing in module folder to get a hint in the logs (#2403)
- Added possibility to use environment variables in `config.js` (#1756)
- Added option `pastDaysCount` to default calendar module to control of how many days past events should be displayed
- Added thai language to alert module
- Added option `sendNotifications` in clock module (#3056)
### Removed
- Removed darksky weather provider
- Removed unneeded (and unwanted) '.' after the year in calendar repeatingCountTitle (#2896)
### Updated
- Use develop as target branch for dependabot
- Update issue template, contributing doc and sample config
- The weather modules clearly separates precipitation amount and probability (risk of rain/snow)
- This requires all providers that only supports probability to change the config from `showPrecipitationAmount` to `showPrecipitationProbability`.
- Update tests for weather and calendar module
- Changed updatenotification module for MagicMirror repo only: Send only notifications for `master` if there is a tag on a newer commit
- Update dates in Calendar widgets every minute
- Cleanup jest coverage for patches
- Update `stylelint` dependencies, switch to `stylelint-config-standard` and handle `stylelint` issues, update `main.css` matching new rules
- Update Eslint config, add new rule and handle issue
- Convert lots of callbacks to async/await
- Revise require imports (#3071 and #3072)
### Fixed
- Fix wrong day labels in envcanada forecast (#2987)
- Fix for missing default class name prefix for customEvents in calendar
- Fix electron flashing white screen on startup (#1919)
- Fix weathergov provider hourly forecast (#3008)
- Fix message display with HTML code into alert module (#2828)
- Fix typo in french translation
- Yr wind direction is no longer inverted
- Fix async node_helper stopping electron start (#2487)
- The wind direction arrow now points in the direction the wind is flowing, not into the wind (#3019)
- Fix precipitation css styles and rounding value
- Fix wrong vertical alignment of calendar title column when wrapEvents is true (#3053)
- Fix empty news feed stopping the reload forever
- Fix e2e tests (failed after async changes) by running calendar and newsfeed tests last
- Lint: Use template literals instead of string concatenation
- Fix default alert module to render HTML for title and message
- Fix Open-Meteo wind speed units
## [2.22.0] - 2023-01-01 ## [2.22.0] - 2023-01-01
Thanks to: @angeldeejay, @buxxi, @dariom, @dWoolridge, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @retroflex, @SkySails and @Tom. Thanks to: @angeldeejay, @buxxi, @dariom, @dWoolridge, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @retroflex, @SkySails and @Tom.
@@ -13,9 +71,9 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al
### Added ### Added
- Added new calendar options for colored entries and improved styling (#3033)
- Added test for remoteFile option in compliments module - Added test for remoteFile option in compliments module
- Added hourlyWeather functionality to Weather.gov weather provider - Added hourlyWeather functionality to Weather.gov weather provider
- Removed weatherEndpoint definition from weathergov.js (not used)
- Added css class names "today" and "tomorrow" for default calendar - Added css class names "today" and "tomorrow" for default calendar
- Added Collaboration.md - Added Collaboration.md
- Added new github action for dependency review (#2862) - Added new github action for dependency review (#2862)
@@ -23,10 +81,12 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al
- Added Yr as a weather provider - Added Yr as a weather provider
- Added config options "ignoreXOriginHeader" and "ignoreContentSecurityPolicy" - Added config options "ignoreXOriginHeader" and "ignoreContentSecurityPolicy"
- Added thai language - Added thai language
- Added workflow rule to make sure PRs are based against develop
### Removed ### Removed
- Removed usage of internal fetch function of node until it is more stable - Removed usage of internal fetch function of node until it is more stable
- Removed weatherEndpoint definition from weathergov.js (not used)
### Updated ### Updated
@@ -40,7 +100,7 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al
- Reworked how weatherproviders handle units (#2849) - Reworked how weatherproviders handle units (#2849)
- Use unix() method for parsing times, fix suntimes on the way (#2950) - Use unix() method for parsing times, fix suntimes on the way (#2950)
- Refactor conversion functions into utils class (#2958) - Refactor conversion functions into utils class (#2958)
- The `cors`-method in `server.js` now supports sending and recieving HTTP headers - The `cors`-method in `server.js` now supports sending and receiving HTTP headers
- Replace `&hellip;` by `…` - Replace `&hellip;` by `…`
- Cleanup compliments module - Cleanup compliments module
- Updated dependencies including electron to v22 (#2903) - Updated dependencies including electron to v22 (#2903)

View File

@@ -5,8 +5,14 @@ This document describes how collaborators of this repository should work togethe
- never merge your own PR's - never merge your own PR's
- never merge without someone having approved (approving and merging from same person is allowed) - never merge without someone having approved (approving and merging from same person is allowed)
- wait for all approvals requested (or the author decides something different in the comments) - wait for all approvals requested (or the author decides something different in the comments)
- never merge to `master`, except for releases (because of update notification)
- merges to master should be tagged with the "mastermerge" label so that the test runs through
## Issues ## Issues
- "real" Issues are closed if the problem is solved and the fix is released - "real" Issues are closed if the problem is solved and the fix is released
- unrelated Issues (e.g. related to a foreign module) are closed immediately with a comment to open an issue in the module repository or to discuss this further in the forum or discord - unrelated Issues (e.g. related to a foreign module) are closed immediately with a comment to open an issue in the module repository or to discuss this further in the forum or discord
## Releases
- are done by @MichMich only

View File

@@ -6,17 +6,21 @@
* For more information on how you can configure this file * For more information on how you can configure this file
* see https://docs.magicmirror.builders/configuration/introduction.html * see https://docs.magicmirror.builders/configuration/introduction.html
* and https://docs.magicmirror.builders/modules/configuration.html * and https://docs.magicmirror.builders/modules/configuration.html
*
* You can use environment variables using a `config.js.template` file instead of `config.js`
* which will be converted to `config.js` while starting. For more information
* see https://docs.magicmirror.builders/configuration/introduction.html#enviromnent-variables
*/ */
let config = { let config = {
address: "localhost", // Address to listen on, can be: address: "localhost", // Address to listen on, can be:
// - "localhost", "127.0.0.1", "::1" to listen on loopback interface // - "localhost", "127.0.0.1", "::1" to listen on loopback interface
// - another specific IPv4/6 to listen on a specific interface // - another specific IPv4/6 to listen on a specific interface
// - "0.0.0.0", "::" to listen on any interface // - "0.0.0.0", "::" to listen on any interface
// Default, when address config is left out or empty, is "localhost" // Default, when address config is left out or empty, is "localhost"
port: 8080, port: 8080,
basePath: "/", // The URL path where MagicMirror² is hosted. If you are using a Reverse proxy basePath: "/", // The URL path where MagicMirror² is hosted. If you are using a Reverse proxy
// you must set the sub path here. basePath must end with a / // you must set the sub path here. basePath must end with a /
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], // Set [] to allow all IP addresses ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], // Set [] to allow all IP addresses
// or add a specific IPv4 of 192.168.1.5 : // or add a specific IPv4 of 192.168.1.5 :
// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.1.5"], // ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.1.5"],
// or IPv4 range of 192.168.3.0 --> 192.168.3.15 use CIDR format : // or IPv4 range of 192.168.3.0 --> 192.168.3.15 use CIDR format :
@@ -31,11 +35,6 @@ let config = {
logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging
timeFormat: 24, timeFormat: 24,
units: "metric", units: "metric",
// serverOnly: true/false/"local" ,
// local for armv6l processors, default
// starts serveronly and then starts chrome browser
// false, default for all NON-armv6l devices
// true, force serveronly mode, because you want to.. no UI on this device
modules: [ modules: [
{ {

View File

@@ -3,22 +3,18 @@
--color-text-dimmed: #666; --color-text-dimmed: #666;
--color-text-bright: #fff; --color-text-bright: #fff;
--color-background: #000; --color-background: #000;
--font-primary: "Roboto Condensed"; --font-primary: "Roboto Condensed";
--font-secondary: "Roboto"; --font-secondary: "Roboto";
--font-size: 20px; --font-size: 20px;
--font-size-xsmall: 0.75rem; --font-size-xsmall: 0.75rem;
--font-size-small: 1rem; --font-size-small: 1rem;
--font-size-medium: 1.5rem; --font-size-medium: 1.5rem;
--font-size-large: 3.25rem; --font-size-large: 3.25rem;
--font-size-xlarge: 3.75rem; --font-size-xlarge: 3.75rem;
--gap-body-top: 60px; --gap-body-top: 60px;
--gap-body-right: 60px; --gap-body-right: 60px;
--gap-body-bottom: 60px; --gap-body-bottom: 60px;
--gap-body-left: 60px; --gap-body-left: 60px;
--gap-modules: 30px; --gap-modules: 30px;
} }
@@ -175,10 +171,7 @@ sup {
.region.fullscreen { .region.fullscreen {
position: absolute; position: absolute;
top: calc(-1 * var(--gap-body-top)); inset: calc(-1 * var(--gap-body-top)) calc(-1 * var(--gap-body-right)) calc(-1 * var(--gap-body-bottom)) calc(-1 * var(--gap-body-left));
left: calc(-1 * var(--gap-body-left));
right: calc(-1 * var(--gap-body-right));
bottom: calc(-1 * var(--gap-body-bottom));
pointer-events: none; pointer-events: none;
} }

View File

@@ -2,7 +2,7 @@ module.exports = async () => {
return { return {
verbose: true, verbose: true,
testTimeout: 20000, testTimeout: 20000,
testSequencer: "<rootDir>/tests/configs/test_sequencer.js", testSequencer: "<rootDir>/tests/utils/test_sequencer.js",
projects: [ projects: [
{ {
displayName: "unit", displayName: "unit",
@@ -25,7 +25,7 @@ module.exports = async () => {
testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers/", "<rootDir>/tests/e2e/mocks"] testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers/", "<rootDir>/tests/e2e/mocks"]
} }
], ],
collectCoverageFrom: ["./clientonly/**/*.js", "./js/**/*.js", "./modules/**/*.js", "./serveronly/**/*.js"], collectCoverageFrom: ["./clientonly/**/*.js", "./js/**/*.js", "./modules/default/**/*.js", "./serveronly/**/*.js"],
coverageReporters: ["lcov", "text"], coverageReporters: ["lcov", "text"],
coverageProvider: "v8" coverageProvider: "v8"
}; };

239
js/app.js
View File

@@ -10,6 +10,7 @@ require("module-alias/register");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const envsub = require("envsub");
const Log = require("logger"); const Log = require("logger");
const Server = require(`${__dirname}/server`); const Server = require(`${__dirname}/server`);
const Utils = require(`${__dirname}/utils`); const Utils = require(`${__dirname}/utils`);
@@ -17,7 +18,7 @@ const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`)
// Get version number. // Get version number.
global.version = require(`${__dirname}/../package.json`).version; global.version = require(`${__dirname}/../package.json`).version;
Log.log("Starting MagicMirror: v" + global.version); Log.log(`Starting MagicMirror: v${global.version}`);
// global absolute root path // global absolute root path
global.root_path = path.resolve(`${__dirname}/../`); global.root_path = path.resolve(`${__dirname}/../`);
@@ -51,25 +52,73 @@ function App() {
let httpServer; let httpServer;
/** /**
* Loads the config file. Combines it with the defaults, and runs the * Loads the config file. Combines it with the defaults and returns the config
* callback with the found config as argument.
* *
* @param {Function} callback Function to be called after loading the config * @async
* @returns {Promise<object>} the loaded config or the defaults if something goes wrong
*/ */
function loadConfig(callback) { async function loadConfig() {
Log.log("Loading config ..."); Log.log("Loading config ...");
const defaults = require(`${__dirname}/defaults`); const defaults = require(`${__dirname}/defaults`);
// For this check proposed to TestSuite // For this check proposed to TestSuite
// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8 // https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8
const configFilename = path.resolve(global.configuration_file || `${global.root_path}/config/config.js`); const configFilename = path.resolve(global.configuration_file || `${global.root_path}/config/config.js`);
let templateFile = `${configFilename}.template`;
// check if templateFile exists
try {
fs.accessSync(templateFile, fs.F_OK);
} catch (err) {
templateFile = null;
Log.debug("config template file not exists, no envsubst");
}
if (templateFile) {
// save current config.js
try {
if (fs.existsSync(configFilename)) {
fs.copyFileSync(configFilename, `${configFilename}_${Date.now()}`);
}
} catch (err) {
Log.warn(`Could not copy ${configFilename}: ${err.message}`);
}
// check if config.env exists
const envFiles = [];
const configEnvFile = `${configFilename.substr(0, configFilename.lastIndexOf("."))}.env`;
try {
if (fs.existsSync(configEnvFile)) {
envFiles.push(configEnvFile);
}
} catch (err) {
Log.debug(`${configEnvFile} does not exist. ${err.message}`);
}
let options = {
all: true,
diff: false,
envFiles: envFiles,
protect: false,
syntax: "default",
system: true
};
// envsubst variables in templateFile and create new config.js
// naming for envsub must be templateFile and outputFile
const outputFile = configFilename;
try {
await envsub({ templateFile, outputFile, options });
} catch (err) {
Log.error(`Could not envsubst variables: ${err.message}`);
}
}
try { try {
fs.accessSync(configFilename, fs.F_OK); fs.accessSync(configFilename, fs.F_OK);
const c = require(configFilename); const c = require(configFilename);
checkDeprecatedOptions(c); checkDeprecatedOptions(c);
const config = Object.assign(defaults, c); return Object.assign(defaults, c);
callback(config);
} catch (e) { } catch (e) {
if (e.code === "ENOENT") { if (e.code === "ENOENT") {
Log.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration.")); Log.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
@@ -78,8 +127,9 @@ function App() {
} else { } else {
Log.error(Utils.colors.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`)); Log.error(Utils.colors.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`));
} }
callback(defaults);
} }
return defaults;
} }
/** /**
@@ -102,9 +152,8 @@ function App() {
* Loads a specific module. * Loads a specific module.
* *
* @param {string} module The name of the module (including subpath). * @param {string} module The name of the module (including subpath).
* @param {Function} callback Function to be called after loading
*/ */
function loadModule(module, callback) { function loadModule(module) {
const elements = module.split("/"); const elements = module.split("/");
const moduleName = elements[elements.length - 1]; const moduleName = elements[elements.length - 1];
let moduleFolder = `${__dirname}/../modules/${module}`; let moduleFolder = `${__dirname}/../modules/${module}`;
@@ -113,6 +162,14 @@ function App() {
moduleFolder = `${__dirname}/../modules/default/${module}`; moduleFolder = `${__dirname}/../modules/default/${module}`;
} }
const moduleFile = `${moduleFolder}/${module}.js`;
try {
fs.accessSync(moduleFile, fs.R_OK);
} catch (e) {
Log.warn(`No ${moduleFile} found for module: ${moduleName}.`);
}
const helperPath = `${moduleFolder}/node_helper.js`; const helperPath = `${moduleFolder}/node_helper.js`;
let loadHelper = true; let loadHelper = true;
@@ -141,39 +198,37 @@ function App() {
m.setPath(path.resolve(moduleFolder)); m.setPath(path.resolve(moduleFolder));
nodeHelpers.push(m); nodeHelpers.push(m);
m.loaded(callback); m.loaded();
} else {
callback();
} }
} }
/** /**
* Loads all modules. * Loads all modules.
* *
* @param {Module[]} modules All modules to be loaded * @param {string[]} modules All modules to be loaded
* @param {Function} callback Function to be called after loading
*/ */
function loadModules(modules, callback) { async function loadModules(modules) {
Log.log("Loading module helpers ..."); return new Promise((resolve) => {
Log.log("Loading module helpers ...");
/** /**
* *
*/ */
function loadNextModule() { function loadNextModule() {
if (modules.length > 0) { if (modules.length > 0) {
const nextModule = modules[0]; const nextModule = modules[0];
loadModule(nextModule, function () { loadModule(nextModule);
modules = modules.slice(1); modules = modules.slice(1);
loadNextModule(); loadNextModule();
}); } else {
} else { // All modules are loaded
// All modules are loaded Log.log("All module helpers loaded.");
Log.log("All module helpers loaded."); resolve();
callback(); }
} }
}
loadNextModule(); loadNextModule();
});
} }
/** /**
@@ -203,58 +258,53 @@ function App() {
/** /**
* Start the core app. * Start the core app.
* *
* It loads the config, then it loads all modules. When it's done it * It loads the config, then it loads all modules.
* executes the callback with the config as argument.
* *
* @param {Function} callback Function to be called after start * @async
* @returns {Promise<object>} the config used
*/ */
this.start = function (callback) { this.start = async function () {
loadConfig(function (c) { config = await loadConfig();
config = c;
Log.setLogLevel(config.logLevel); Log.setLogLevel(config.logLevel);
let modules = []; let modules = [];
for (const module of config.modules) {
for (const module of config.modules) { if (!modules.includes(module.module) && !module.disabled) {
if (!modules.includes(module.module) && !module.disabled) { modules.push(module.module);
modules.push(module.module);
}
} }
}
await loadModules(modules);
loadModules(modules, async function () { httpServer = new Server(config);
httpServer = new Server(config); const { app, io } = await httpServer.open();
const { app, io } = await httpServer.open(); Log.log("Server started ...");
Log.log("Server started ...");
const nodePromises = []; const nodePromises = [];
for (let nodeHelper of nodeHelpers) { for (let nodeHelper of nodeHelpers) {
nodeHelper.setExpressApp(app); nodeHelper.setExpressApp(app);
nodeHelper.setSocketIO(io); nodeHelper.setSocketIO(io);
try { try {
nodePromises.push(nodeHelper.start()); nodePromises.push(nodeHelper.start());
} catch (error) { } catch (error) {
Log.error(`Error when starting node_helper for module ${nodeHelper.name}:`); Log.error(`Error when starting node_helper for module ${nodeHelper.name}:`);
Log.error(error); Log.error(error);
} }
} }
Promise.allSettled(nodePromises).then((results) => { const results = await Promise.allSettled(nodePromises);
// Log errors that happened during async node_helper startup
results.forEach((result) => {
if (result.status === "rejected") {
Log.error(result.reason);
}
});
Log.log("Sockets connected & modules started ..."); // Log errors that happened during async node_helper startup
if (typeof callback === "function") { results.forEach((result) => {
callback(config); if (result.status === "rejected") {
} Log.error(result.reason);
}); }
});
}); });
Log.log("Sockets connected & modules started ...");
return config;
}; };
/** /**
@@ -263,15 +313,40 @@ function App() {
* *
* Added to fix #1056 * Added to fix #1056
* *
* @param {Function} callback Function to be called after the app has stopped * @returns {Promise} A promise that is resolved when all node_helpers and
* the http server has been closed
*/ */
this.stop = function (callback) { this.stop = async function () {
for (const nodeHelper of nodeHelpers) { const nodePromises = [];
if (typeof nodeHelper.stop === "function") { for (let nodeHelper of nodeHelpers) {
nodeHelper.stop(); try {
if (typeof nodeHelper.stop === "function") {
nodePromises.push(nodeHelper.stop());
}
} catch (error) {
Log.error(`Error when stopping node_helper for module ${nodeHelper.name}:`);
console.error(error);
} }
} }
httpServer.close().then(callback);
const results = await Promise.allSettled(nodePromises);
// Log errors that happened during async node_helper stopping
results.forEach((result) => {
if (result.status === "rejected") {
Log.error(result.reason);
}
});
Log.log("Node_helpers stopped ...");
// To be able to stop the app even if it hasn't been started (when
// running with Electron against another server)
if (!httpServer) {
return Promise.resolve();
}
return httpServer.close();
}; };
/** /**
@@ -281,12 +356,12 @@ function App() {
* Note: this is only used if running `server-only`. Otherwise * Note: this is only used if running `server-only`. Otherwise
* this.stop() is called by app.on("before-quit"... in `electron.js` * this.stop() is called by app.on("before-quit"... in `electron.js`
*/ */
process.on("SIGINT", () => { process.on("SIGINT", async () => {
Log.log("[SIGINT] Received. Shutting down server..."); Log.log("[SIGINT] Received. Shutting down server...");
setTimeout(() => { setTimeout(() => {
process.exit(0); process.exit(0);
}, 3000); // Force quit after 3 seconds }, 3000); // Force quit after 3 seconds
this.stop(); await this.stop();
process.exit(0); process.exit(0);
}); });
@@ -294,12 +369,12 @@ function App() {
* Listen to SIGTERM signals so we can stop everything when we * Listen to SIGTERM signals so we can stop everything when we
* are asked to stop by the OS. * are asked to stop by the OS.
*/ */
process.on("SIGTERM", () => { process.on("SIGTERM", async () => {
Log.log("[SIGTERM] Received. Shutting down server..."); Log.log("[SIGTERM] Received. Shutting down server...");
setTimeout(() => { setTimeout(() => {
process.exit(0); process.exit(0);
}, 3000); // Force quit after 3 seconds }, 3000); // Force quit after 3 seconds
this.stop(); await this.stop();
process.exit(0); process.exit(0);
}); });
} }

View File

@@ -5,11 +5,11 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com * By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed. * MIT Licensed.
*/ */
const Linter = require("eslint").Linter;
const linter = new Linter();
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const { Linter } = require("eslint");
const linter = new Linter();
const rootPath = path.resolve(`${__dirname}/../`); const rootPath = path.resolve(`${__dirname}/../`);
const Log = require(`${rootPath}/js/logger.js`); const Log = require(`${rootPath}/js/logger.js`);

View File

@@ -1,8 +1,8 @@
"use strict"; "use strict";
const electron = require("electron"); const electron = require("electron");
const core = require("./app.js"); const core = require("./app");
const Log = require("logger"); const Log = require("./logger");
// Config // Config
let config = process.env.config ? JSON.parse(process.env.config) : {}; let config = process.env.config ? JSON.parse(process.env.config) : {};
@@ -46,8 +46,10 @@ function createWindow() {
if (config.kioskmode) { if (config.kioskmode) {
electronOptionsDefaults.kiosk = true; electronOptionsDefaults.kiosk = true;
} else { } else {
electronOptionsDefaults.fullscreen = true; electronOptionsDefaults.show = false;
electronOptionsDefaults.autoHideMenuBar = true; electronOptionsDefaults.frame = false;
electronOptionsDefaults.transparent = true;
electronOptionsDefaults.hasShadow = false;
} }
const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions); const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
@@ -117,6 +119,11 @@ function createWindow() {
callback({ responseHeaders: curHeaders }); callback({ responseHeaders: curHeaders });
}); });
mainWindow.once("ready-to-show", () => {
mainWindow.setFullScreen(true);
mainWindow.show();
});
} }
// This method will be called when Electron has finished // This method will be called when Electron has finished
@@ -150,18 +157,19 @@ app.on("activate", function () {
* Note: this is only used if running Electron. Otherwise * Note: this is only used if running Electron. Otherwise
* core.stop() is called by process.on("SIGINT"... in `app.js` * core.stop() is called by process.on("SIGINT"... in `app.js`
*/ */
app.on("before-quit", (event) => { app.on("before-quit", async (event) => {
Log.log("Shutting down server..."); Log.log("Shutting down server...");
event.preventDefault(); event.preventDefault();
setTimeout(() => { setTimeout(() => {
process.exit(0); process.exit(0);
}, 3000); // Force-quit after 3 seconds. }, 3000); // Force-quit after 3 seconds.
core.stop(); await core.stop();
process.exit(0); process.exit(0);
}); });
/* handle errors from self signed certificates */ /**
* Handle errors from self-signed certificates
*/
app.on("certificate-error", (event, webContents, url, error, certificate, callback) => { app.on("certificate-error", (event, webContents, url, error, certificate, callback) => {
event.preventDefault(); event.preventDefault();
callback(true); callback(true);
@@ -170,7 +178,5 @@ app.on("certificate-error", (event, webContents, url, error, certificate, callba
// Start the core application if server is run on localhost // Start the core application if server is run on localhost
// This starts all node helpers and starts the webserver. // This starts all node helpers and starts the webserver.
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) { if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) {
core.start(function (c) { core.start().then((c) => (config = c));
config = c;
});
} }

View File

@@ -16,48 +16,35 @@ const Loader = (function () {
/* Private Methods */ /* Private Methods */
/** /**
* Loops thru all modules and requests load for every module. * Loops through all modules and requests start for every module.
*/ */
const loadModules = function () { const startModules = async function () {
let moduleData = getModuleData(); const modulePromises = [];
const loadNextModule = function () {
if (moduleData.length > 0) {
const nextModule = moduleData[0];
loadModule(nextModule, function () {
moduleData = moduleData.slice(1);
loadNextModule();
});
} else {
// All modules loaded. Load custom.css
// This is done after all the modules so we can
// overwrite all the defined styles.
loadFile(config.customCss, function () {
// custom.css loaded. Start all modules.
startModules();
});
}
};
loadNextModule();
};
/**
* Loops thru all modules and requests start for every module.
*/
const startModules = function () {
for (const module of moduleObjects) { for (const module of moduleObjects) {
module.start(); try {
modulePromises.push(module.start());
} catch (error) {
Log.error(`Error when starting node_helper for module ${module.name}:`);
Log.error(error);
}
} }
const results = await Promise.allSettled(modulePromises);
// Log errors that happened during async node_helper startup
results.forEach((result) => {
if (result.status === "rejected") {
Log.error(result.reason);
}
});
// Notify core of loaded modules. // Notify core of loaded modules.
MM.modulesStarted(moduleObjects); MM.modulesStarted(moduleObjects);
// Starting modules also hides any modules that have requested to be initially hidden // Starting modules also hides any modules that have requested to be initially hidden
for (const thisModule of moduleObjects) { for (const thisModule of moduleObjects) {
if (thisModule.data.hiddenOnStartup) { if (thisModule.data.hiddenOnStartup) {
Log.info("Initially hiding " + thisModule.name); Log.info(`Initially hiding ${thisModule.name}`);
thisModule.hide(); thisModule.hide();
} }
} }
@@ -86,10 +73,10 @@ const Loader = (function () {
const elements = module.split("/"); const elements = module.split("/");
const moduleName = elements[elements.length - 1]; const moduleName = elements[elements.length - 1];
let moduleFolder = config.paths.modules + "/" + module; let moduleFolder = `${config.paths.modules}/${module}`;
if (defaultModules.indexOf(moduleName) !== -1) { if (defaultModules.indexOf(moduleName) !== -1) {
moduleFolder = config.paths.modules + "/default/" + module; moduleFolder = `${config.paths.modules}/default/${module}`;
} }
if (moduleData.disabled === true) { if (moduleData.disabled === true) {
@@ -98,16 +85,16 @@ const Loader = (function () {
moduleFiles.push({ moduleFiles.push({
index: index, index: index,
identifier: "module_" + index + "_" + module, identifier: `module_${index}_${module}`,
name: moduleName, name: moduleName,
path: moduleFolder + "/", path: `${moduleFolder}/`,
file: moduleName + ".js", file: `${moduleName}.js`,
position: moduleData.position, position: moduleData.position,
hiddenOnStartup: moduleData.hiddenOnStartup, hiddenOnStartup: moduleData.hiddenOnStartup,
header: moduleData.header, header: moduleData.header,
configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false, configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false,
config: moduleData.config, config: moduleData.config,
classes: typeof moduleData.classes !== "undefined" ? moduleData.classes + " " + module : module classes: typeof moduleData.classes !== "undefined" ? `${moduleData.classes} ${module}` : module
}); });
}); });
@@ -115,32 +102,30 @@ const Loader = (function () {
}; };
/** /**
* Load modules via ajax request and create module objects.s * Load modules via ajax request and create module objects.
* *
* @param {object} module Information about the module we want to load. * @param {object} module Information about the module we want to load.
* @param {Function} callback Function called when done. * @returns {Promise<void>} resolved when module is loaded
*/ */
const loadModule = function (module, callback) { const loadModule = async function (module) {
const url = module.path + module.file; const url = module.path + module.file;
const afterLoad = function () { /**
* @returns {Promise<void>}
*/
const afterLoad = async function () {
const moduleObject = Module.create(module.name); const moduleObject = Module.create(module.name);
if (moduleObject) { if (moduleObject) {
bootstrapModule(module, moduleObject, function () { await bootstrapModule(module, moduleObject);
callback();
});
} else {
callback();
} }
}; };
if (loadedModuleFiles.indexOf(url) !== -1) { if (loadedModuleFiles.indexOf(url) !== -1) {
afterLoad(); await afterLoad();
} else { } else {
loadFile(url, function () { await loadFile(url);
loadedModuleFiles.push(url); loadedModuleFiles.push(url);
afterLoad(); await afterLoad();
});
} }
}; };
@@ -149,76 +134,66 @@ const Loader = (function () {
* *
* @param {object} module Information about the module we want to load. * @param {object} module Information about the module we want to load.
* @param {Module} mObj Modules instance. * @param {Module} mObj Modules instance.
* @param {Function} callback Function called when done.
*/ */
const bootstrapModule = function (module, mObj, callback) { const bootstrapModule = async function (module, mObj) {
Log.info("Bootstrapping module: " + module.name); Log.info(`Bootstrapping module: ${module.name}`);
mObj.setData(module); mObj.setData(module);
mObj.loadScripts(function () { await mObj.loadScripts();
Log.log("Scripts loaded for: " + module.name); Log.log(`Scripts loaded for: ${module.name}`);
mObj.loadStyles(function () {
Log.log("Styles loaded for: " + module.name); await mObj.loadStyles();
mObj.loadTranslations(function () { Log.log(`Styles loaded for: ${module.name}`);
Log.log("Translations loaded for: " + module.name);
moduleObjects.push(mObj); await mObj.loadTranslations();
callback(); Log.log(`Translations loaded for: ${module.name}`);
});
}); moduleObjects.push(mObj);
});
}; };
/** /**
* Load a script or stylesheet by adding it to the dom. * Load a script or stylesheet by adding it to the dom.
* *
* @param {string} fileName Path of the file we want to load. * @param {string} fileName Path of the file we want to load.
* @param {Function} callback Function called when done. * @returns {Promise} resolved when the file is loaded
*/ */
const loadFile = function (fileName, callback) { const loadFile = async function (fileName) {
const extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1); const extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1);
let script, stylesheet; let script, stylesheet;
switch (extension.toLowerCase()) { switch (extension.toLowerCase()) {
case "js": case "js":
Log.log("Load script: " + fileName); return new Promise((resolve) => {
script = document.createElement("script"); Log.log(`Load script: ${fileName}`);
script.type = "text/javascript"; script = document.createElement("script");
script.src = fileName; script.type = "text/javascript";
script.onload = function () { script.src = fileName;
if (typeof callback === "function") { script.onload = function () {
callback(); resolve();
} };
}; script.onerror = function () {
script.onerror = function () { Log.error("Error on loading script:", fileName);
Log.error("Error on loading script:", fileName); resolve();
if (typeof callback === "function") { };
callback(); document.getElementsByTagName("body")[0].appendChild(script);
} });
};
document.getElementsByTagName("body")[0].appendChild(script);
break;
case "css": case "css":
Log.log("Load stylesheet: " + fileName); return new Promise((resolve) => {
stylesheet = document.createElement("link"); Log.log(`Load stylesheet: ${fileName}`);
stylesheet.rel = "stylesheet";
stylesheet.type = "text/css";
stylesheet.href = fileName;
stylesheet.onload = function () {
if (typeof callback === "function") {
callback();
}
};
stylesheet.onerror = function () {
Log.error("Error on loading stylesheet:", fileName);
if (typeof callback === "function") {
callback();
}
};
document.getElementsByTagName("head")[0].appendChild(stylesheet); stylesheet = document.createElement("link");
break; stylesheet.rel = "stylesheet";
stylesheet.type = "text/css";
stylesheet.href = fileName;
stylesheet.onload = function () {
resolve();
};
stylesheet.onerror = function () {
Log.error("Error on loading stylesheet:", fileName);
resolve();
};
document.getElementsByTagName("head")[0].appendChild(stylesheet);
});
} }
}; };
@@ -227,8 +202,28 @@ const Loader = (function () {
/** /**
* Load all modules as defined in the config. * Load all modules as defined in the config.
*/ */
loadModules: function () { loadModules: async function () {
loadModules(); let moduleData = getModuleData();
/**
* @returns {Promise<void>} when all modules are loaded
*/
const loadNextModule = async function () {
if (moduleData.length > 0) {
const nextModule = moduleData[0];
await loadModule(nextModule);
moduleData = moduleData.slice(1);
await loadNextModule();
} else {
// All modules loaded. Load custom.css
// This is done after all the modules so we can
// overwrite all the defined styles.
await loadFile(config.customCss);
// custom.css loaded. Start all modules.
await startModules();
}
};
await loadNextModule();
}, },
/** /**
@@ -237,12 +232,11 @@ const Loader = (function () {
* *
* @param {string} fileName Path of the file we want to load. * @param {string} fileName Path of the file we want to load.
* @param {Module} module The module that calls the loadFile function. * @param {Module} module The module that calls the loadFile function.
* @param {Function} callback Function called when done. * @returns {Promise} resolved when the file is loaded
*/ */
loadFile: function (fileName, module, callback) { loadFileForModule: async function (fileName, module) {
if (loadedFiles.indexOf(fileName.toLowerCase()) !== -1) { if (loadedFiles.indexOf(fileName.toLowerCase()) !== -1) {
Log.log("File already loaded: " + fileName); Log.log(`File already loaded: ${fileName}`);
callback();
return; return;
} }
@@ -250,22 +244,20 @@ const Loader = (function () {
// This is an absolute or relative path. // This is an absolute or relative path.
// Load it and then return. // Load it and then return.
loadedFiles.push(fileName.toLowerCase()); loadedFiles.push(fileName.toLowerCase());
loadFile(fileName, callback); return loadFile(fileName);
return;
} }
if (vendor[fileName] !== undefined) { if (vendor[fileName] !== undefined) {
// This file is available in the vendor folder. // This file is available in the vendor folder.
// Load it from this vendor folder. // Load it from this vendor folder.
loadedFiles.push(fileName.toLowerCase()); loadedFiles.push(fileName.toLowerCase());
loadFile(config.paths.vendor + "/" + vendor[fileName], callback); return loadFile(`${config.paths.vendor}/${vendor[fileName]}`);
return;
} }
// File not loaded yet. // File not loaded yet.
// Load it based on the module path. // Load it based on the module path.
loadedFiles.push(fileName.toLowerCase()); loadedFiles.push(fileName.toLowerCase());
loadFile(module.file(fileName), callback); return loadFile(module.file(fileName));
} }
}; };
})(); })();

View File

@@ -29,7 +29,7 @@ const MM = (function () {
dom.className = module.name; dom.className = module.name;
if (typeof module.data.classes === "string") { if (typeof module.data.classes === "string") {
dom.className = "module " + dom.className + " " + module.data.classes; dom.className = `module ${dom.className} ${module.data.classes}`;
} }
dom.opacity = 0; dom.opacity = 0;
@@ -243,7 +243,7 @@ const MM = (function () {
const moduleWrapper = document.getElementById(module.identifier); const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper !== null) { if (moduleWrapper !== null) {
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s"; moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
moduleWrapper.style.opacity = 0; moduleWrapper.style.opacity = 0;
moduleWrapper.classList.add("hidden"); moduleWrapper.classList.add("hidden");
@@ -291,7 +291,7 @@ const MM = (function () {
// Check if there are no more lockstrings set, or the force option is set. // Check if there are no more lockstrings set, or the force option is set.
// Otherwise cancel show action. // Otherwise cancel show action.
if (module.lockStrings.length !== 0 && options.force !== true) { if (module.lockStrings.length !== 0 && options.force !== true) {
Log.log("Will not show " + module.name + ". LockStrings active: " + module.lockStrings.join(",")); Log.log(`Will not show ${module.name}. LockStrings active: ${module.lockStrings.join(",")}`);
if (typeof options.onError === "function") { if (typeof options.onError === "function") {
options.onError(new Error("LOCK_STRING_ACTIVE")); options.onError(new Error("LOCK_STRING_ACTIVE"));
} }
@@ -302,13 +302,13 @@ const MM = (function () {
// If forced show, clean current lockstrings. // If forced show, clean current lockstrings.
if (module.lockStrings.length !== 0 && options.force === true) { if (module.lockStrings.length !== 0 && options.force === true) {
Log.log("Force show of module: " + module.name); Log.log(`Force show of module: ${module.name}`);
module.lockStrings = []; module.lockStrings = [];
} }
const moduleWrapper = document.getElementById(module.identifier); const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper !== null) { if (moduleWrapper !== null) {
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s"; moduleWrapper.style.transition = `opacity ${speed / 1000}s`;
// Restore the position. See hideModule() for more info. // Restore the position. See hideModule() for more info.
moduleWrapper.style.position = "static"; moduleWrapper.style.position = "static";
moduleWrapper.classList.remove("hidden"); moduleWrapper.classList.remove("hidden");
@@ -479,14 +479,14 @@ const MM = (function () {
/** /**
* Main init method. * Main init method.
*/ */
init: function () { init: async function () {
Log.info("Initializing MagicMirror²."); Log.info("Initializing MagicMirror².");
loadConfig(); loadConfig();
Log.setLogLevel(config.logLevel); Log.setLogLevel(config.logLevel);
Translator.loadCoreTranslations(config.language); await Translator.loadCoreTranslations(config.language);
Loader.loadModules(); await Loader.loadModules();
}, },
/** /**

View File

@@ -25,7 +25,7 @@ const Module = Class.extend({
// visibility when hiding and showing module. // visibility when hiding and showing module.
lockStrings: [], lockStrings: [],
// Storage of the nunjuck Environment, // Storage of the nunjucks Environment,
// This should not be referenced directly. // This should not be referenced directly.
// Use the nunjucksEnvironment() to get it. // Use the nunjucksEnvironment() to get it.
_nunjucksEnvironment: null, _nunjucksEnvironment: null,
@@ -40,8 +40,8 @@ const Module = Class.extend({
/** /**
* Called when the module is started. * Called when the module is started.
*/ */
start: function () { start: async function () {
Log.info("Starting module: " + this.name); Log.info(`Starting module: ${this.name}`);
}, },
/** /**
@@ -127,7 +127,7 @@ const Module = Class.extend({
* @returns {string} The template string of filename. * @returns {string} The template string of filename.
*/ */
getTemplate: function () { getTemplate: function () {
return '<div class="normal">' + this.name + '</div><div class="small dimmed">' + this.identifier + "</div>"; return `<div class="normal">${this.name}</div><div class="small dimmed">${this.identifier}</div>`;
}, },
/** /**
@@ -185,21 +185,21 @@ const Module = Class.extend({
* @param {*} payload The payload of the notification. * @param {*} payload The payload of the notification.
*/ */
socketNotificationReceived: function (notification, payload) { socketNotificationReceived: function (notification, payload) {
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload); Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
}, },
/** /**
* Called when the module is hidden. * Called when the module is hidden.
*/ */
suspend: function () { suspend: function () {
Log.log(this.name + " is suspended."); Log.log(`${this.name} is suspended.`);
}, },
/** /**
* Called when the module is shown. * Called when the module is shown.
*/ */
resume: function () { resume: function () {
Log.log(this.name + " is resumed."); Log.log(`${this.name} is resumed.`);
}, },
/********************************************* /*********************************************
@@ -255,57 +255,54 @@ const Module = Class.extend({
* @returns {string} the file path * @returns {string} the file path
*/ */
file: function (file) { file: function (file) {
return (this.data.path + "/" + file).replace("//", "/"); return `${this.data.path}/${file}`.replace("//", "/");
}, },
/** /**
* Load all required stylesheets by requesting the MM object to load the files. * Load all required stylesheets by requesting the MM object to load the files.
* *
* @param {Function} callback Function called when done. * @returns {Promise<void>}
*/ */
loadStyles: function (callback) { loadStyles: function () {
this.loadDependencies("getStyles", callback); return this.loadDependencies("getStyles");
}, },
/** /**
* Load all required scripts by requesting the MM object to load the files. * Load all required scripts by requesting the MM object to load the files.
* *
* @param {Function} callback Function called when done. * @returns {Promise<void>}
*/ */
loadScripts: function (callback) { loadScripts: function () {
this.loadDependencies("getScripts", callback); return this.loadDependencies("getScripts");
}, },
/** /**
* Helper method to load all dependencies. * Helper method to load all dependencies.
* *
* @param {string} funcName Function name to call to get scripts or styles. * @param {string} funcName Function name to call to get scripts or styles.
* @param {Function} callback Function called when done. * @returns {Promise<void>}
*/ */
loadDependencies: function (funcName, callback) { loadDependencies: async function (funcName) {
let dependencies = this[funcName](); let dependencies = this[funcName]();
const loadNextDependency = () => { const loadNextDependency = async () => {
if (dependencies.length > 0) { if (dependencies.length > 0) {
const nextDependency = dependencies[0]; const nextDependency = dependencies[0];
Loader.loadFile(nextDependency, this, () => { await Loader.loadFileForModule(nextDependency, this);
dependencies = dependencies.slice(1); dependencies = dependencies.slice(1);
loadNextDependency(); await loadNextDependency();
});
} else { } else {
callback(); return Promise.resolve();
} }
}; };
loadNextDependency(); await loadNextDependency();
}, },
/** /**
* Load all translations. * Load all translations.
*
* @param {Function} callback Function called when done.
*/ */
loadTranslations(callback) { loadTranslations: async function () {
const translations = this.getTranslations() || {}; const translations = this.getTranslations() || {};
const language = config.language.toLowerCase(); const language = config.language.toLowerCase();
@@ -313,7 +310,6 @@ const Module = Class.extend({
const fallbackLanguage = languages[0]; const fallbackLanguage = languages[0];
if (languages.length === 0) { if (languages.length === 0) {
callback();
return; return;
} }
@@ -321,17 +317,14 @@ const Module = Class.extend({
const translationsFallbackFile = translations[fallbackLanguage]; const translationsFallbackFile = translations[fallbackLanguage];
if (!translationFile) { if (!translationFile) {
Translator.load(this, translationsFallbackFile, true, callback); return Translator.load(this, translationsFallbackFile, true);
return;
} }
Translator.load(this, translationFile, false, () => { await Translator.load(this, translationFile, false);
if (translationFile !== translationsFallbackFile) {
Translator.load(this, translationsFallbackFile, true, callback); if (translationFile !== translationsFallbackFile) {
} else { return Translator.load(this, translationsFallbackFile, true);
callback(); }
}
});
}, },
/** /**
@@ -498,15 +491,15 @@ Module.create = function (name) {
Module.register = function (name, moduleDefinition) { Module.register = function (name, moduleDefinition) {
if (moduleDefinition.requiresVersion) { if (moduleDefinition.requiresVersion) {
Log.log("Check MagicMirror² version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.mmVersion); Log.log(`Check MagicMirror² version for module '${name}' - Minimum version: ${moduleDefinition.requiresVersion} - Current version: ${window.mmVersion}`);
if (cmpVersions(window.mmVersion, moduleDefinition.requiresVersion) >= 0) { if (cmpVersions(window.mmVersion, moduleDefinition.requiresVersion) >= 0) {
Log.log("Version is ok!"); Log.log("Version is ok!");
} else { } else {
Log.warn("Version is incorrect. Skip module: '" + name + "'"); Log.warn(`Version is incorrect. Skip module: '${name}'`);
return; return;
} }
} }
Log.log("Module registered: " + name); Log.log(`Module registered: ${name}`);
Module.definitions[name] = moduleDefinition; Module.definitions[name] = moduleDefinition;
}; };

View File

@@ -4,18 +4,17 @@
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
const Class = require("./class.js");
const Log = require("logger");
const express = require("express"); const express = require("express");
const Log = require("logger");
const Class = require("./class");
const NodeHelper = Class.extend({ const NodeHelper = Class.extend({
init() { init() {
Log.log("Initializing new module helper ..."); Log.log("Initializing new module helper ...");
}, },
loaded(callback) { loaded() {
Log.log(`Module helper loaded: ${this.name}`); Log.log(`Module helper loaded: ${this.name}`);
callback();
}, },
start() { start() {

View File

@@ -4,15 +4,18 @@
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
const express = require("express");
const path = require("path");
const ipfilter = require("express-ipfilter").IpFilter;
const fs = require("fs"); const fs = require("fs");
const http = require("http");
const https = require("https");
const path = require("path");
const express = require("express");
const ipfilter = require("express-ipfilter").IpFilter;
const helmet = require("helmet"); const helmet = require("helmet");
const socketio = require("socket.io");
const Log = require("logger"); const Log = require("logger");
const Utils = require("./utils.js"); const Utils = require("./utils");
const { cors, getConfig, getHtml, getVersion } = require("./server_functions.js"); const { cors, getConfig, getHtml, getVersion } = require("./server_functions");
/** /**
* Server * Server
@@ -38,11 +41,11 @@ function Server(config) {
key: fs.readFileSync(config.httpsPrivateKey), key: fs.readFileSync(config.httpsPrivateKey),
cert: fs.readFileSync(config.httpsCertificate) cert: fs.readFileSync(config.httpsCertificate)
}; };
server = require("https").Server(options, app); server = https.Server(options, app);
} else { } else {
server = require("http").Server(app); server = http.Server(app);
} }
const io = require("socket.io")(server, { const io = socketio(server, {
cors: { cors: {
origin: /.*$/, origin: /.*$/,
credentials: true credentials: true

View File

@@ -1,7 +1,7 @@
const fetch = require("./fetch");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const Log = require("logger"); const Log = require("logger");
const fetch = require("./fetch");
/** /**
* Gets the config. * Gets the config.
@@ -14,7 +14,7 @@ function getConfig(req, res) {
} }
/** /**
* A method that forewards HTTP Get-methods to the internet to avoid CORS-errors. * A method that forwards HTTP Get-methods to the internet to avoid CORS-errors.
* *
* Example input request url: /cors?sendheaders=header1:value1,header2:value2&expectedheaders=header1,header2&url=http://www.test.com/path?param1=value1 * Example input request url: /cors?sendheaders=header1:value1,header2:value2&expectedheaders=header1,header2&url=http://www.test.com/path?param1=value1
* *
@@ -26,11 +26,11 @@ function getConfig(req, res) {
async function cors(req, res) { async function cors(req, res) {
try { try {
const urlRegEx = "url=(.+?)$"; const urlRegEx = "url=(.+?)$";
let url = ""; let url;
const match = new RegExp(urlRegEx, "g").exec(req.url); const match = new RegExp(urlRegEx, "g").exec(req.url);
if (!match) { if (!match) {
url = "invalid url: " + req.url; url = `invalid url: ${req.url}`;
Log.error(url); Log.error(url);
res.send(url); res.send(url);
} else { } else {
@@ -39,7 +39,7 @@ async function cors(req, res) {
const headersToSend = getHeadersToSend(req.url); const headersToSend = getHeadersToSend(req.url);
const expectedRecievedHeaders = geExpectedRecievedHeaders(req.url); const expectedRecievedHeaders = geExpectedRecievedHeaders(req.url);
Log.log("cors url: " + url); Log.log(`cors url: ${url}`);
const response = await fetch(url, { headers: headersToSend }); const response = await fetch(url, { headers: headersToSend });
for (const header of expectedRecievedHeaders) { for (const header of expectedRecievedHeaders) {
@@ -56,13 +56,13 @@ async function cors(req, res) {
} }
/** /**
* Gets headers and values to attatch to the web request. * Gets headers and values to attach to the web request.
* *
* @param {string} url - The url containing the headers and values to send. * @param {string} url - The url containing the headers and values to send.
* @returns {object} An object specifying name and value of the headers. * @returns {object} An object specifying name and value of the headers.
*/ */
function getHeadersToSend(url) { function getHeadersToSend(url) {
const headersToSend = { "User-Agent": "Mozilla/5.0 MagicMirror/" + global.version }; const headersToSend = { "User-Agent": `Mozilla/5.0 MagicMirror/${global.version}` };
const headersToSendMatch = new RegExp("sendheaders=(.+?)(&|$)", "g").exec(url); const headersToSendMatch = new RegExp("sendheaders=(.+?)(&|$)", "g").exec(url);
if (headersToSendMatch) { if (headersToSendMatch) {
const headers = headersToSendMatch[1].split(","); const headers = headersToSendMatch[1].split(",");

View File

@@ -18,8 +18,8 @@ const MMSocket = function (moduleName) {
if (typeof config !== "undefined" && typeof config.basePath !== "undefined") { if (typeof config !== "undefined" && typeof config.basePath !== "undefined") {
base = config.basePath; base = config.basePath;
} }
this.socket = io("/" + this.moduleName, { this.socket = io(`/${this.moduleName}`, {
path: base + "socket.io" path: `${base}socket.io`
}); });
let notificationCallback = function () {}; let notificationCallback = function () {};

View File

@@ -11,26 +11,28 @@ const Translator = (function () {
* Load a JSON file via XHR. * Load a JSON file via XHR.
* *
* @param {string} file Path of the file we want to load. * @param {string} file Path of the file we want to load.
* @param {Function} callback Function called when done. * @returns {Promise<object>} the translations in the specified file
*/ */
function loadJSON(file, callback) { async function loadJSON(file) {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.overrideMimeType("application/json"); return new Promise(function (resolve, reject) {
xhr.open("GET", file, true); xhr.overrideMimeType("application/json");
xhr.onreadystatechange = function () { xhr.open("GET", file, true);
if (xhr.readyState === 4 && xhr.status === 200) { xhr.onreadystatechange = function () {
// needs error handler try/catch at least if (xhr.readyState === 4 && xhr.status === 200) {
let fileinfo = null; // needs error handler try/catch at least
try { let fileinfo = null;
fileinfo = JSON.parse(xhr.responseText); try {
} catch (exception) { fileinfo = JSON.parse(xhr.responseText);
// nothing here, but don't die } catch (exception) {
Log.error(" loading json file =" + file + " failed"); // nothing here, but don't die
Log.error(` loading json file =${file} failed`);
}
resolve(fileinfo);
} }
callback(fileinfo); };
} xhr.send(null);
}; });
xhr.send(null);
} }
return { return {
@@ -48,7 +50,7 @@ const Translator = (function () {
* @returns {string} the translated key * @returns {string} the translated key
*/ */
translate: function (module, key, variables) { translate: function (module, key, variables) {
variables = variables || {}; //Empty object by default variables = variables || {}; // Empty object by default
/** /**
* Combines template and variables like: * Combines template and variables like:
@@ -68,7 +70,7 @@ const Translator = (function () {
template = variables.fallback; template = variables.fallback;
} }
return template.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) { return template.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) {
return varName in variables ? variables[varName] : "{" + varName + "}"; return varName in variables ? variables[varName] : `{${varName}}`;
}); });
} }
@@ -101,21 +103,17 @@ const Translator = (function () {
* @param {Module} module The module to load the translation file for. * @param {Module} module The module to load the translation file for.
* @param {string} file Path of the file we want to load. * @param {string} file Path of the file we want to load.
* @param {boolean} isFallback Flag to indicate fallback translations. * @param {boolean} isFallback Flag to indicate fallback translations.
* @param {Function} callback Function called when done.
*/ */
load(module, file, isFallback, callback) { async load(module, file, isFallback) {
Log.log(`${module.name} - Load translation${isFallback ? " fallback" : ""}: ${file}`); Log.log(`${module.name} - Load translation${isFallback ? " fallback" : ""}: ${file}`);
if (this.translationsFallback[module.name]) { if (this.translationsFallback[module.name]) {
callback();
return; return;
} }
loadJSON(module.file(file), (json) => { const json = await loadJSON(module.file(file));
const property = isFallback ? "translationsFallback" : "translations"; const property = isFallback ? "translationsFallback" : "translations";
this[property][module.name] = json; this[property][module.name] = json;
callback();
});
}, },
/** /**
@@ -123,30 +121,26 @@ const Translator = (function () {
* *
* @param {string} lang The language identifier of the core language. * @param {string} lang The language identifier of the core language.
*/ */
loadCoreTranslations: function (lang) { loadCoreTranslations: async function (lang) {
if (lang in translations) { if (lang in translations) {
Log.log("Loading core translation file: " + translations[lang]); Log.log(`Loading core translation file: ${translations[lang]}`);
loadJSON(translations[lang], (translations) => { this.coreTranslations = await loadJSON(translations[lang]);
this.coreTranslations = translations;
});
} else { } else {
Log.log("Configured language not found in core translations."); Log.log("Configured language not found in core translations.");
} }
this.loadCoreTranslationsFallback(); await this.loadCoreTranslationsFallback();
}, },
/** /**
* Load the core translations fallback. * Load the core translations' fallback.
* The first language defined in translations.js will be used. * The first language defined in translations.js will be used.
*/ */
loadCoreTranslationsFallback: function () { loadCoreTranslationsFallback: async function () {
let first = Object.keys(translations)[0]; let first = Object.keys(translations)[0];
if (first) { if (first) {
Log.log("Loading core translation fallback file: " + translations[first]); Log.log(`Loading core translation fallback file: ${translations[first]}`);
loadJSON(translations[first], (translations) => { this.coreTranslationsFallback = await loadJSON(translations[first]);
this.coreTranslationsFallback = translations;
});
} }
} }
}; };

View File

@@ -35,7 +35,8 @@ Module.register("alert", {
fr: "translations/fr.json", fr: "translations/fr.json",
hu: "translations/hu.json", hu: "translations/hu.json",
nl: "translations/nl.json", nl: "translations/nl.json",
ru: "translations/ru.json" ru: "translations/ru.json",
th: "translations/th.json"
}; };
}, },
@@ -43,7 +44,7 @@ Module.register("alert", {
return `templates/${type}.njk`; return `templates/${type}.njk`;
}, },
start() { async start() {
Log.info(`Starting module: ${this.name}`); Log.info(`Starting module: ${this.name}`);
if (this.config.effect === "slide") { if (this.config.effect === "slide") {
@@ -52,7 +53,7 @@ Module.register("alert", {
if (this.config.welcome_message) { if (this.config.welcome_message) {
const message = this.config.welcome_message === true ? this.translate("welcome") : this.config.welcome_message; const message = this.config.welcome_message === true ? this.translate("welcome") : this.config.welcome_message;
this.showNotification({ title: this.translate("sysTitle"), message }); await this.showNotification({ title: this.translate("sysTitle"), message });
} }
}, },
@@ -69,7 +70,7 @@ Module.register("alert", {
}, },
async showNotification(notification) { async showNotification(notification) {
const message = await this.renderMessage("notification", notification); const message = await this.renderMessage(notification.templateName || "notification", notification);
new NotificationFx({ new NotificationFx({
message, message,
@@ -90,7 +91,7 @@ Module.register("alert", {
this.toggleBlur(true); this.toggleBlur(true);
} }
const message = await this.renderMessage("alert", alert); const message = await this.renderMessage(alert.templateName || "alert", alert);
// Store alert in this.alerts // Store alert in this.alerts
this.alerts[sender.name] = new NotificationFx({ this.alerts[sender.name] = new NotificationFx({

View File

@@ -80,7 +80,7 @@
NotificationFx.prototype._init = function () { NotificationFx.prototype._init = function () {
// create HTML structure // create HTML structure
this.ntf = document.createElement("div"); this.ntf = document.createElement("div");
this.ntf.className = this.options.al_no + " ns-" + this.options.layout + " ns-effect-" + this.options.effect + " ns-type-" + this.options.type; this.ntf.className = `${this.options.al_no} ns-${this.options.layout} ns-effect-${this.options.effect} ns-type-${this.options.type}`;
let strinner = '<div class="ns-box-inner">'; let strinner = '<div class="ns-box-inner">';
strinner += this.options.message; strinner += this.options.message;
strinner += "</div>"; strinner += "</div>";

View File

@@ -1,7 +1,7 @@
/* Based on work by https://tympanus.net/codrops/licensing/ */ /* Based on work by https://tympanus.net/codrops/licensing/ */
.ns-box { .ns-box {
background-color: rgba(0, 0, 0, 0.93); background-color: rgb(0 0 0 / 93%);
padding: 17px; padding: 17px;
line-height: 1.4; line-height: 1.4;
margin-bottom: 10px; margin-bottom: 10px;
@@ -55,15 +55,15 @@
.ns-effect-flip.ns-show, .ns-effect-flip.ns-show,
.ns-effect-flip.ns-hide { .ns-effect-flip.ns-hide {
animation-name: animFlipFront; animation-name: anim-flip-front;
animation-duration: 0.3s; animation-duration: 0.3s;
} }
.ns-effect-flip.ns-hide { .ns-effect-flip.ns-hide {
animation-name: animFlipBack; animation-name: anim-flip-back;
} }
@keyframes animFlipFront { @keyframes anim-flip-front {
0% { 0% {
transform: perspective(1000px) rotate3d(1, 0, 0, -90deg); transform: perspective(1000px) rotate3d(1, 0, 0, -90deg);
} }
@@ -73,7 +73,7 @@
} }
} }
@keyframes animFlipBack { @keyframes anim-flip-back {
0% { 0% {
transform: perspective(1000px) rotate3d(1, 0, 0, 90deg); transform: perspective(1000px) rotate3d(1, 0, 0, 90deg);
} }
@@ -85,11 +85,11 @@
.ns-effect-bouncyflip.ns-show, .ns-effect-bouncyflip.ns-show,
.ns-effect-bouncyflip.ns-hide { .ns-effect-bouncyflip.ns-hide {
animation-name: flipInX; animation-name: flip-in-x;
animation-duration: 0.8s; animation-duration: 0.8s;
} }
@keyframes flipInX { @keyframes flip-in-x {
0% { 0% {
transform: perspective(400px) rotate3d(1, 0, 0, -90deg); transform: perspective(400px) rotate3d(1, 0, 0, -90deg);
transition-timing-function: ease-in; transition-timing-function: ease-in;
@@ -117,11 +117,11 @@
} }
.ns-effect-bouncyflip.ns-hide { .ns-effect-bouncyflip.ns-hide {
animation-name: flipInXSimple; animation-name: flip-in-x-simple;
animation-duration: 0.3s; animation-duration: 0.3s;
} }
@keyframes flipInXSimple { @keyframes flip-in-x-simple {
0% { 0% {
transform: perspective(400px) rotate3d(1, 0, 0, -90deg); transform: perspective(400px) rotate3d(1, 0, 0, -90deg);
transition-timing-function: ease-in; transition-timing-function: ease-in;
@@ -141,11 +141,11 @@
} }
.ns-effect-exploader.ns-show { .ns-effect-exploader.ns-show {
animation-name: animLoad; animation-name: anim-load;
animation-duration: 1s; animation-duration: 1s;
} }
@keyframes animLoad { @keyframes anim-load {
0% { 0% {
opacity: 1; opacity: 1;
transform: scale3d(0, 0.3, 1); transform: scale3d(0, 0.3, 1);
@@ -158,7 +158,7 @@
} }
.ns-effect-exploader.ns-hide { .ns-effect-exploader.ns-hide {
animation-name: animFade; animation-name: anim-fade;
animation-duration: 0.3s; animation-duration: 0.3s;
} }
@@ -170,15 +170,15 @@
} }
.ns-effect-exploader.ns-show .ns-close { .ns-effect-exploader.ns-show .ns-close {
animation-name: animFade; animation-name: anim-fade;
} }
.ns-effect-exploader.ns-show .ns-box-inner { .ns-effect-exploader.ns-show .ns-box-inner {
animation-name: animFadeMove; animation-name: anim-fade-move;
animation-timing-function: ease-out; animation-timing-function: ease-out;
} }
@keyframes animFadeMove { @keyframes anim-fade-move {
0% { 0% {
opacity: 0; opacity: 0;
transform: translate3d(0, 10px, 0); transform: translate3d(0, 10px, 0);
@@ -190,7 +190,7 @@
} }
} }
@keyframes animFade { @keyframes anim-fade {
0% { 0% {
opacity: 0; opacity: 0;
} }
@@ -202,11 +202,11 @@
.ns-effect-scale.ns-show, .ns-effect-scale.ns-show,
.ns-effect-scale.ns-hide { .ns-effect-scale.ns-hide {
animation-name: animScale; animation-name: anim-scale;
animation-duration: 0.25s; animation-duration: 0.25s;
} }
@keyframes animScale { @keyframes anim-scale {
0% { 0% {
opacity: 0; opacity: 0;
transform: translate3d(0, 40px, 0) scale3d(0.1, 0.6, 1); transform: translate3d(0, 40px, 0) scale3d(0.1, 0.6, 1);
@@ -219,168 +219,169 @@
} }
.ns-effect-jelly.ns-show { .ns-effect-jelly.ns-show {
animation-name: animJelly; animation-name: anim-jelly;
animation-duration: 1s; animation-duration: 1s;
animation-timing-function: linear; animation-timing-function: linear;
} }
.ns-effect-jelly.ns-hide { .ns-effect-jelly.ns-hide {
animation-name: animFade; animation-name: anim-fade;
animation-duration: 0.3s; animation-duration: 0.3s;
} }
@keyframes animFade { @keyframes anim-fade {
0% { 0% {
opacity: 0; opacity: 0;
} }
100% { 100% {
opacity: 1; opacity: 1;
} }
} }
@keyframes animJelly { @keyframes anim-jelly {
0% { 0% {
transform: matrix3d(0.7, 0, 0, 0, 0, 0.7, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.7, 0, 0, 0, 0, 0.7, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
2.083333% { 2.083333% {
transform: matrix3d(0.75266, 0, 0, 0, 0, 0.76342, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.7527, 0, 0, 0, 0, 0.7634, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
4.166667% { 4.166667% {
transform: matrix3d(0.81071, 0, 0, 0, 0, 0.84545, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.8107, 0, 0, 0, 0, 0.8454, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
6.25% { 6.25% {
transform: matrix3d(0.86808, 0, 0, 0, 0, 0.9286, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.8681, 0, 0, 0, 0, 0.929, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
8.333333% { 8.333333% {
transform: matrix3d(0.92038, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.9204, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
10.416667% { 10.416667% {
transform: matrix3d(0.96482, 0, 0, 0, 0, 1.05202, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.9648, 0, 0, 0, 0, 1.052, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
12.5% { 12.5% {
transform: matrix3d(1, 0, 0, 0, 0, 1.08204, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1.082, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
14.583333% { 14.583333% {
transform: matrix3d(1.02563, 0, 0, 0, 0, 1.09149, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0256, 0, 0, 0, 0, 1.0915, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
16.666667% { 16.666667% {
transform: matrix3d(1.04227, 0, 0, 0, 0, 1.08453, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0423, 0, 0, 0, 0, 1.0845, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
18.75% { 18.75% {
transform: matrix3d(1.05102, 0, 0, 0, 0, 1.06666, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.051, 0, 0, 0, 0, 1.0667, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
20.833333% { 20.833333% {
transform: matrix3d(1.05334, 0, 0, 0, 0, 1.04355, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0533, 0, 0, 0, 0, 1.0436, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
22.916667% { 22.916667% {
transform: matrix3d(1.05078, 0, 0, 0, 0, 1.02012, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0508, 0, 0, 0, 0, 1.0201, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
25% { 25% {
transform: matrix3d(1.04487, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0449, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
27.083333% { 27.083333% {
transform: matrix3d(1.03699, 0, 0, 0, 0, 0.98534, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.037, 0, 0, 0, 0, 0.9853, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
29.166667% { 29.166667% {
transform: matrix3d(1.02831, 0, 0, 0, 0, 0.97688, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0283, 0, 0, 0, 0, 0.9769, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
31.25% { 31.25% {
transform: matrix3d(1.01973, 0, 0, 0, 0, 0.97422, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0197, 0, 0, 0, 0, 0.9742, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
33.333333% { 33.333333% {
transform: matrix3d(1.01191, 0, 0, 0, 0, 0.97618, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0119, 0, 0, 0, 0, 0.9762, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
35.416667% { 35.416667% {
transform: matrix3d(1.00526, 0, 0, 0, 0, 0.98122, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0053, 0, 0, 0, 0, 0.9812, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
37.5% { 37.5% {
transform: matrix3d(1, 0, 0, 0, 0, 0.98773, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 0.9877, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
39.583333% { 39.583333% {
transform: matrix3d(0.99617, 0, 0, 0, 0, 0.99433, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.9962, 0, 0, 0, 0, 0.9943, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
41.666667% { 41.666667% {
transform: matrix3d(0.99368, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.9937, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
43.75% { 43.75% {
transform: matrix3d(0.99237, 0, 0, 0, 0, 1.00413, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.9924, 0, 0, 0, 0, 1.0041, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
45.833333% { 45.833333% {
transform: matrix3d(0.99202, 0, 0, 0, 0, 1.00651, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.992, 0, 0, 0, 0, 1.0065, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
47.916667% { 47.916667% {
transform: matrix3d(0.99241, 0, 0, 0, 0, 1.00726, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.9924, 0, 0, 0, 0, 1.0073, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
50% { 50% {
transform: matrix3d(0.99329, 0, 0, 0, 0, 1.00671, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.9933, 0, 0, 0, 0, 1.0067, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
52.083333% { 52.083333% {
transform: matrix3d(0.99447, 0, 0, 0, 0, 1.00529, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.9945, 0, 0, 0, 0, 1.0053, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
54.166667% { 54.166667% {
transform: matrix3d(0.99577, 0, 0, 0, 0, 1.00346, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.9958, 0, 0, 0, 0, 1.0035, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
56.25% { 56.25% {
transform: matrix3d(0.99705, 0, 0, 0, 0, 1.0016, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.997, 0, 0, 0, 0, 1.002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
58.333333% { 58.333333% {
transform: matrix3d(0.99822, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.9982, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
60.416667% { 60.416667% {
transform: matrix3d(0.99921, 0, 0, 0, 0, 0.99884, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.9992, 0, 0, 0, 0, 0.9989, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
62.5% { 62.5% {
transform: matrix3d(1, 0, 0, 0, 0, 0.99816, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 0.9982, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
64.583333% { 64.583333% {
transform: matrix3d(1.00057, 0, 0, 0, 0, 0.99795, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0006, 0, 0, 0, 0, 0.998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
66.666667% { 66.666667% {
transform: matrix3d(1.00095, 0, 0, 0, 0, 0.99811, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.001, 0, 0, 0, 0, 0.9981, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
68.75% { 68.75% {
transform: matrix3d(1.00114, 0, 0, 0, 0, 0.99851, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0011, 0, 0, 0, 0, 0.9985, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
70.833333% { 70.833333% {
transform: matrix3d(1.00119, 0, 0, 0, 0, 0.99903, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0012, 0, 0, 0, 0, 0.999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
72.916667% { 72.916667% {
transform: matrix3d(1.00114, 0, 0, 0, 0, 0.99955, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0011, 0, 0, 0, 0, 0.9996, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
75% { 75% {
@@ -388,47 +389,47 @@
} }
77.083333% { 77.083333% {
transform: matrix3d(1.00083, 0, 0, 0, 0, 1.00033, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0008, 0, 0, 0, 0, 1.0003, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
79.166667% { 79.166667% {
transform: matrix3d(1.00063, 0, 0, 0, 0, 1.00052, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0006, 0, 0, 0, 0, 1.0005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
81.25% { 81.25% {
transform: matrix3d(1.00044, 0, 0, 0, 0, 1.00058, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0004, 0, 0, 0, 0, 1.0006, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
83.333333% { 83.333333% {
transform: matrix3d(1.00027, 0, 0, 0, 0, 1.00053, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0003, 0, 0, 0, 0, 1.0005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
85.416667% { 85.416667% {
transform: matrix3d(1.00012, 0, 0, 0, 0, 1.00042, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1.0001, 0, 0, 0, 0, 1.0004, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
87.5% { 87.5% {
transform: matrix3d(1, 0, 0, 0, 0, 1.00027, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1.0003, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
89.583333% { 89.583333% {
transform: matrix3d(0.99991, 0, 0, 0, 0, 1.00013, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.9999, 0, 0, 0, 0, 1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
91.666667% { 91.666667% {
transform: matrix3d(0.99986, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
93.75% { 93.75% {
transform: matrix3d(0.99983, 0, 0, 0, 0, 0.99991, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
95.833333% { 95.833333% {
transform: matrix3d(0.99982, 0, 0, 0, 0, 0.99985, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
97.916667% { 97.916667% {
transform: matrix3d(0.99983, 0, 0, 0, 0, 0.99984, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); transform: matrix3d(0.9998, 0, 0, 0, 0, 0.9998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
} }
100% { 100% {
@@ -437,162 +438,162 @@
} }
.ns-effect-slide-left.ns-show { .ns-effect-slide-left.ns-show {
animation-name: animSlideElasticLeft; animation-name: anim-slide-elastic-left;
animation-duration: 1s; animation-duration: 1s;
animation-timing-function: linear; animation-timing-function: linear;
} }
@keyframes animSlideElasticLeft { @keyframes anim-slide-elastic-left {
0% { 0% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -1000, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -1000, 0, 0, 1);
} }
1.666667% { 1.666667% {
transform: matrix3d(1.92933, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -739.26805, 0, 0, 1); transform: matrix3d(1.9293, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -739.2681, 0, 0, 1);
} }
3.333333% { 3.333333% {
transform: matrix3d(1.96989, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -521.82545, 0, 0, 1); transform: matrix3d(1.9699, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -521.8255, 0, 0, 1);
} }
5% { 5% {
transform: matrix3d(1.70901, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -349.26115, 0, 0, 1); transform: matrix3d(1.709, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -349.2612, 0, 0, 1);
} }
6.666667% { 6.666667% {
transform: matrix3d(1.4235, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -218.3238, 0, 0, 1); transform: matrix3d(1.424, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -218.324, 0, 0, 1);
} }
8.333333% { 8.333333% {
transform: matrix3d(1.21065, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -123.29848, 0, 0, 1); transform: matrix3d(1.2107, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -123.2985, 0, 0, 1);
} }
10% { 10% {
transform: matrix3d(1.08167, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -57.59273, 0, 0, 1); transform: matrix3d(1.0817, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -57.5927, 0, 0, 1);
} }
11.666667% { 11.666667% {
transform: matrix3d(1.0165, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -14.72371, 0, 0, 1); transform: matrix3d(1.017, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -14.7237, 0, 0, 1);
} }
13.333333% { 13.333333% {
transform: matrix3d(0.99057, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.12794, 0, 0, 1); transform: matrix3d(0.9906, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.1279, 0, 0, 1);
} }
15% { 15% {
transform: matrix3d(0.98478, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 24.86339, 0, 0, 1); transform: matrix3d(0.9848, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 24.8634, 0, 0, 1);
} }
16.666667% { 16.666667% {
transform: matrix3d(0.98719, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.40503, 0, 0, 1); transform: matrix3d(0.9872, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.405, 0, 0, 1);
} }
18.333333% { 18.333333% {
transform: matrix3d(0.9916, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.75275, 0, 0, 1); transform: matrix3d(0.992, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 30.7528, 0, 0, 1);
} }
20% { 20% {
transform: matrix3d(0.99541, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 28.10141, 0, 0, 1); transform: matrix3d(0.9954, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 28.1014, 0, 0, 1);
} }
21.666667% { 21.666667% {
transform: matrix3d(0.99795, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 23.98271, 0, 0, 1); transform: matrix3d(0.998, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 23.9827, 0, 0, 1);
} }
23.333333% { 23.333333% {
transform: matrix3d(0.99936, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 19.40752, 0, 0, 1); transform: matrix3d(0.9994, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 19.4075, 0, 0, 1);
} }
25% { 25% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 14.99558, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 14.9956, 0, 0, 1);
} }
26.666667% { 26.666667% {
transform: matrix3d(1.00021, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.08575, 0, 0, 1); transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 11.0858, 0, 0, 1);
} }
28.333333% { 28.333333% {
transform: matrix3d(1.00022, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 7.82507, 0, 0, 1); transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 7.8251, 0, 0, 1);
} }
30% { 30% {
transform: matrix3d(1.00016, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5.23737, 0, 0, 1); transform: matrix3d(1.0002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5.2374, 0, 0, 1);
} }
31.666667% { 31.666667% {
transform: matrix3d(1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 3.27389, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 3.2739, 0, 0, 1);
} }
33.333333% { 33.333333% {
transform: matrix3d(1.00005, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1.84893, 0, 0, 1); transform: matrix3d(1.0001, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1.8489, 0, 0, 1);
} }
35% { 35% {
transform: matrix3d(1.00002, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.86364, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.8636, 0, 0, 1);
} }
36.666667% { 36.666667% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.22079, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.2208, 0, 0, 1);
} }
38.333333% { 38.333333% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.16687, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1669, 0, 0, 1);
} }
40% { 40% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.37284, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.3728, 0, 0, 1);
} }
41.666667% { 41.666667% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.45594, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4559, 0, 0, 1);
} }
43.333333% { 43.333333% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.46116, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4612, 0, 0, 1);
} }
45% { 45% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.4214, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.421, 0, 0, 1);
} }
46.666667% { 46.666667% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.35963, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.3596, 0, 0, 1);
} }
48.333333% { 48.333333% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.29103, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.291, 0, 0, 1);
} }
50% { 50% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.22487, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.2249, 0, 0, 1);
} }
51.666667% { 51.666667% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.16624, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1662, 0, 0, 1);
} }
53.333333% { 53.333333% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.11734, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.1173, 0, 0, 1);
} }
55% { 55% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.07854, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0785, 0, 0, 1);
} }
56.666667% { 56.666667% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.04909, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0491, 0, 0, 1);
} }
58.333333% { 58.333333% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.02773, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0277, 0, 0, 1);
} }
60% { 60% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.01295, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.013, 0, 0, 1);
} }
61.666667% { 61.666667% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00331, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0033, 0, 0, 1);
} }
63.333333% { 63.333333% {
@@ -600,67 +601,67 @@
} }
65% { 65% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00559, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0056, 0, 0, 1);
} }
66.666667% { 66.666667% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00684, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0068, 0, 0, 1);
} }
68.333333% { 68.333333% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00692, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0069, 0, 0, 1);
} }
70% { 70% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00632, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0063, 0, 0, 1);
} }
71.666667% { 71.666667% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00539, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0054, 0, 0, 1);
} }
73.333333% { 73.333333% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00436, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0044, 0, 0, 1);
} }
75% { 75% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00337, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0034, 0, 0, 1);
} }
76.666667% { 76.666667% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00249, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0025, 0, 0, 1);
} }
78.333333% { 78.333333% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00176, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0018, 0, 0, 1);
} }
80% { 80% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00118, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0012, 0, 0, 1);
} }
81.666667% { 81.666667% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00074, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0007, 0, 0, 1);
} }
83.333333% { 83.333333% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00042, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0004, 0, 0, 1);
} }
85% { 85% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00019, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0002, 0, 0, 1);
} }
86.666667% { 86.666667% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.00005, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.0001, 0, 0, 1);
} }
88.333333% { 88.333333% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00004, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
} }
90% { 90% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00008, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
} }
91.666667% { 91.666667% {
@@ -672,15 +673,15 @@
} }
95% { 95% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00009, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
} }
96.666667% { 96.666667% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00008, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
} }
98.333333% { 98.333333% {
transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.00007, 0, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.0001, 0, 0, 1);
} }
100% { 100% {
@@ -689,11 +690,11 @@
} }
.ns-effect-slide-left.ns-hide { .ns-effect-slide-left.ns-hide {
animation-name: animSlideLeft; animation-name: anim-slide-left;
animation-duration: 0.25s; animation-duration: 0.25s;
} }
@keyframes animSlideLeft { @keyframes anim-slide-left {
0% { 0% {
transform: translate3d(-30px, 0, 0) translate3d(-100%, 0, 0); transform: translate3d(-30px, 0, 0) translate3d(-100%, 0, 0);
} }
@@ -704,10 +705,10 @@
} }
.ns-effect-slide-right.ns-show { .ns-effect-slide-right.ns-show {
animation: animSlideElasticRight 2000ms linear both; animation: anim-slide-elastic-right 2000ms linear both;
} }
@keyframes animSlideElasticRight { @keyframes anim-slide-elastic-right {
0% { 0% {
transform: matrix3d(2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1000, 0, 0, 1); transform: matrix3d(2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1000, 0, 0, 1);
} }
@@ -786,11 +787,11 @@
} }
.ns-effect-slide-right.ns-hide { .ns-effect-slide-right.ns-hide {
animation-name: animSlideRight; animation-name: anim-slide-right;
animation-duration: 0.25s; animation-duration: 0.25s;
} }
@keyframes animSlideRight { @keyframes anim-slide-right {
0% { 0% {
transform: translate3d(30px, 0, 0) translate3d(100%, 0, 0); transform: translate3d(30px, 0, 0) translate3d(100%, 0, 0);
} }
@@ -801,10 +802,10 @@
} }
.ns-effect-slide-center.ns-show { .ns-effect-slide-center.ns-show {
animation: animSlideElasticCenter 2000ms linear both; animation: anim-slide-elastic-center 2000ms linear both;
} }
@keyframes animSlideElasticCenter { @keyframes anim-slide-elastic-center {
0% { 0% {
transform: matrix3d(1, 0, 0, 0, 0, 3, 0, 0, 0, 0, 1, 0, 0, -300, 0, 1); transform: matrix3d(1, 0, 0, 0, 0, 3, 0, 0, 0, 0, 1, 0, 0, -300, 0, 1);
} }
@@ -883,11 +884,11 @@
} }
.ns-effect-slide-center.ns-hide { .ns-effect-slide-center.ns-hide {
animation-name: animSlideCenter; animation-name: anim-slide-center;
animation-duration: 0.25s; animation-duration: 0.25s;
} }
@keyframes animSlideCenter { @keyframes anim-slide-center {
0% { 0% {
transform: translate3d(0, -30px, 0) translate3d(0, -100%, 0); transform: translate3d(0, -30px, 0) translate3d(0, -100%, 0);
} }
@@ -899,11 +900,11 @@
.ns-effect-genie.ns-show, .ns-effect-genie.ns-show,
.ns-effect-genie.ns-hide { .ns-effect-genie.ns-hide {
animation-name: animGenie; animation-name: anim-genie;
animation-duration: 0.4s; animation-duration: 0.4s;
} }
@keyframes animGenie { @keyframes anim-genie {
0% { 0% {
opacity: 0; opacity: 0;
transform: translate3d(0, calc(200% + 30px), 0) scale3d(0, 1, 1); transform: translate3d(0, calc(200% + 30px), 0) scale3d(0, 1, 1);

View File

@@ -8,11 +8,11 @@
<br/> <br/>
{% endif %} {% endif %}
{% if title %} {% if title %}
<span class="thin dimmed medium">{{ title }}</span> <span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
{% endif %} {% endif %}
{% if message %} {% if message %}
{% if title %} {% if title %}
<br/> <br/>
{% endif %} {% endif %}
<span class="light bright small">{{ message }}</span> <span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
{% endif %} {% endif %}

View File

@@ -1,9 +1,9 @@
{% if title %} {% if title %}
<span class="thin dimmed medium">{{ title }}</span> <span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
{% endif %} {% endif %}
{% if message %} {% if message %}
{% if title %} {% if title %}
<br/> <br/>
{% endif %} {% endif %}
<span class="light bright small">{{ message }}</span> <span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
{% endif %} {% endif %}

View File

@@ -0,0 +1,4 @@
{
"sysTitle": "การแจ้งเตือน MagicMirror²",
"welcome": "ยินดีต้อนรับ การเริ่มต้นสำเร็จแล้ว!"
}

View File

@@ -14,6 +14,7 @@
.calendar .title { .calendar .title {
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
vertical-align: top;
} }
.calendar .time { .calendar .time {

View File

@@ -12,6 +12,7 @@ Module.register("calendar", {
maximumEntries: 10, // Total Maximum Entries maximumEntries: 10, // Total Maximum Entries
maximumNumberOfDays: 365, maximumNumberOfDays: 365,
limitDays: 0, // Limit the number of days shown, 0 = no limit limitDays: 0, // Limit the number of days shown, 0 = no limit
pastDaysCount: 0,
displaySymbol: true, displaySymbol: true,
defaultSymbol: "calendar-alt", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io defaultSymbol: "calendar-alt", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
defaultSymbolClassName: "fas fa-fw fa-", defaultSymbolClassName: "fas fa-fw fa-",
@@ -40,7 +41,6 @@ Module.register("calendar", {
hideTime: false, hideTime: false,
showTimeToday: false, showTimeToday: false,
colored: false, colored: false,
coloredSymbolOnly: false,
customEvents: [], // Array of {keyword: "", symbol: "", color: ""} where Keyword is a regexp and symbol/color are to be applied for matched customEvents: [], // Array of {keyword: "", symbol: "", color: ""} where Keyword is a regexp and symbol/color are to be applied for matched
tableClass: "small", tableClass: "small",
calendars: [ calendars: [
@@ -61,7 +61,13 @@ Module.register("calendar", {
sliceMultiDayEvents: false, sliceMultiDayEvents: false,
broadcastPastEvents: false, broadcastPastEvents: false,
nextDaysRelative: false, nextDaysRelative: false,
selfSignedCert: false selfSignedCert: false,
coloredText: false,
coloredBorder: false,
coloredSymbol: false,
coloredBackground: false,
limitDaysNeverSkip: false,
flipDateHeaderTitle: false
}, },
requiresVersion: "2.1.0", requiresVersion: "2.1.0",
@@ -86,7 +92,20 @@ Module.register("calendar", {
// Override start method. // Override start method.
start: function () { start: function () {
Log.info("Starting module: " + this.name); const ONE_MINUTE = 60 * 1000;
Log.info(`Starting module: ${this.name}`);
if (this.config.colored) {
Log.warn("Your are using the deprecated config values 'colored'. Please switch to 'coloredSymbol' & 'coloredText'!");
this.config.coloredText = true;
this.config.coloredSymbol = true;
}
if (this.config.coloredSymbolOnly) {
Log.warn("Your are using the deprecated config values 'coloredSymbolOnly'. Please switch to 'coloredSymbol' & 'coloredText'!");
this.config.coloredText = false;
this.config.coloredSymbol = true;
}
// Set locale. // Set locale.
moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat)); moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
@@ -103,6 +122,7 @@ Module.register("calendar", {
const calendarConfig = { const calendarConfig = {
maximumEntries: calendar.maximumEntries, maximumEntries: calendar.maximumEntries,
maximumNumberOfDays: calendar.maximumNumberOfDays, maximumNumberOfDays: calendar.maximumNumberOfDays,
pastDaysCount: calendar.pastDaysCount,
broadcastPastEvents: calendar.broadcastPastEvents, broadcastPastEvents: calendar.broadcastPastEvents,
selfSignedCert: calendar.selfSignedCert selfSignedCert: calendar.selfSignedCert
}; };
@@ -131,6 +151,14 @@ Module.register("calendar", {
// fetcher till cycle // fetcher till cycle
this.addCalendar(calendar.url, calendar.auth, calendarConfig); this.addCalendar(calendar.url, calendar.auth, calendarConfig);
}); });
// Refresh the DOM every minute if needed: When using relative date format for events that start
// or end in less than an hour, the date shows minute granularity and we want to keep that accurate.
setTimeout(() => {
setInterval(() => {
this.updateDom(1);
}, ONE_MINUTE);
}, ONE_MINUTE - (new Date() % ONE_MINUTE));
}, },
// Override socket notification handler. // Override socket notification handler.
@@ -175,13 +203,13 @@ Module.register("calendar", {
if (this.error) { if (this.error) {
wrapper.innerHTML = this.error; wrapper.innerHTML = this.error;
wrapper.className = this.config.tableClass + " dimmed"; wrapper.className = `${this.config.tableClass} dimmed`;
return wrapper; return wrapper;
} }
if (events.length === 0) { if (events.length === 0) {
wrapper.innerHTML = this.loaded ? this.translate("EMPTY") : this.translate("LOADING"); wrapper.innerHTML = this.loaded ? this.translate("EMPTY") : this.translate("LOADING");
wrapper.className = this.config.tableClass + " dimmed"; wrapper.className = `${this.config.tableClass} dimmed`;
return wrapper; return wrapper;
} }
@@ -204,9 +232,12 @@ Module.register("calendar", {
if (this.config.timeFormat === "dateheaders") { if (this.config.timeFormat === "dateheaders") {
if (lastSeenDate !== dateAsString) { if (lastSeenDate !== dateAsString) {
const dateRow = document.createElement("tr"); const dateRow = document.createElement("tr");
dateRow.className = "normal"; dateRow.className = "dateheader normal";
if (event.today) dateRow.className += " today"; if (event.today) dateRow.className += " today";
else if (event.dayBeforeYesterday) dateRow.className += " dayBeforeYesterday";
else if (event.yesterday) dateRow.className += " yesterday";
else if (event.tomorrow) dateRow.className += " tomorrow"; else if (event.tomorrow) dateRow.className += " tomorrow";
else if (event.dayAfterTomorrow) dateRow.className += " dayAfterTomorrow";
const dateCell = document.createElement("td"); const dateCell = document.createElement("td");
dateCell.colSpan = "3"; dateCell.colSpan = "3";
@@ -227,23 +258,34 @@ Module.register("calendar", {
const eventWrapper = document.createElement("tr"); const eventWrapper = document.createElement("tr");
if (this.config.colored && !this.config.coloredSymbolOnly) { if (this.config.coloredText) {
eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url); eventWrapper.style.cssText = `color:${this.colorForUrl(event.url, false)}`;
} }
eventWrapper.className = "normal event"; if (this.config.coloredBackground) {
eventWrapper.style.backgroundColor = this.colorForUrl(event.url, true);
}
if (this.config.coloredBorder) {
eventWrapper.style.borderColor = this.colorForUrl(event.url, false);
}
eventWrapper.className = "event-wrapper normal event";
if (event.today) eventWrapper.className += " today"; if (event.today) eventWrapper.className += " today";
else if (event.dayBeforeYesterday) eventWrapper.className += " dayBeforeYesterday";
else if (event.yesterday) eventWrapper.className += " yesterday";
else if (event.tomorrow) eventWrapper.className += " tomorrow"; else if (event.tomorrow) eventWrapper.className += " tomorrow";
else if (event.dayAfterTomorrow) eventWrapper.className += " dayAfterTomorrow";
const symbolWrapper = document.createElement("td"); const symbolWrapper = document.createElement("td");
if (this.config.displaySymbol) { if (this.config.displaySymbol) {
if (this.config.colored && this.config.coloredSymbolOnly) { if (this.config.coloredSymbol) {
symbolWrapper.style.cssText = "color:" + this.colorForUrl(event.url); symbolWrapper.style.cssText = `color:${this.colorForUrl(event.url, false)}`;
} }
const symbolClass = this.symbolClassForUrl(event.url); const symbolClass = this.symbolClassForUrl(event.url);
symbolWrapper.className = "symbol align-right " + symbolClass; symbolWrapper.className = `symbol align-right ${symbolClass}`;
const symbols = this.symbolsForEvent(event); const symbols = this.symbolsForEvent(event);
symbols.forEach((s, index) => { symbols.forEach((s, index) => {
@@ -271,7 +313,7 @@ Module.register("calendar", {
const thisYear = new Date(parseInt(event.startDate)).getFullYear(), const thisYear = new Date(parseInt(event.startDate)).getFullYear(),
yearDiff = thisYear - event.firstYear; yearDiff = thisYear - event.firstYear;
repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle; repeatingCountTitle = `, ${yearDiff}. ${repeatingCountTitle}`;
} }
} }
@@ -282,12 +324,12 @@ Module.register("calendar", {
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi"); let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
if (needle.test(event.title)) { if (needle.test(event.title)) {
// Respect parameter ColoredSymbolOnly also for custom events // Respect parameter ColoredSymbolOnly also for custom events
if (!this.config.coloredSymbolOnly) { if (this.config.coloredText) {
eventWrapper.style.cssText = "color:" + this.config.customEvents[ev].color; eventWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
titleWrapper.style.cssText = "color:" + this.config.customEvents[ev].color; titleWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
} }
if (this.config.displaySymbol) { if (this.config.displaySymbol && this.config.coloredSymbol) {
symbolWrapper.style.cssText = "color:" + this.config.customEvents[ev].color; symbolWrapper.style.cssText = `color:${this.config.customEvents[ev].color}`;
} }
break; break;
} }
@@ -299,32 +341,35 @@ Module.register("calendar", {
const titleClass = this.titleClassForUrl(event.url); const titleClass = this.titleClassForUrl(event.url);
if (!this.config.colored) { if (!this.config.coloredText) {
titleWrapper.className = "title bright " + titleClass; titleWrapper.className = `title bright ${titleClass}`;
} else { } else {
titleWrapper.className = "title " + titleClass; titleWrapper.className = `title ${titleClass}`;
} }
if (this.config.timeFormat === "dateheaders") { if (this.config.timeFormat === "dateheaders") {
if (this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper);
if (event.fullDayEvent) { if (event.fullDayEvent) {
titleWrapper.colSpan = "2"; titleWrapper.colSpan = "2";
titleWrapper.classList.add("align-left"); titleWrapper.classList.add("align-left");
} else { } else {
const timeWrapper = document.createElement("td"); const timeWrapper = document.createElement("td");
timeWrapper.className = "time light align-left " + this.timeClassForUrl(event.url); timeWrapper.className = `time light ${this.config.flipDateHeaderTitle ? "align-right " : "align-left "}${this.timeClassForUrl(event.url)}`;
timeWrapper.style.paddingLeft = "2px"; timeWrapper.style.paddingLeft = "2px";
timeWrapper.style.textAlign = this.config.flipDateHeaderTitle ? "right" : "left";
timeWrapper.innerHTML = moment(event.startDate, "x").format("LT"); timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
// Add endDate to dataheaders if showEnd is enabled // Add endDate to dataheaders if showEnd is enabled
if (this.config.showEnd) { if (this.config.showEnd) {
timeWrapper.innerHTML += " - " + moment(event.endDate, "x").format("LT"); timeWrapper.innerHTML += ` - ${this.capFirst(moment(event.endDate, "x").format("LT"))}`;
} }
eventWrapper.appendChild(timeWrapper); eventWrapper.appendChild(timeWrapper);
titleWrapper.classList.add("align-right");
}
eventWrapper.appendChild(titleWrapper); if (!this.config.flipDateHeaderTitle) titleWrapper.classList.add("align-right");
}
if (!this.config.flipDateHeaderTitle) eventWrapper.appendChild(titleWrapper);
} else { } else {
const timeWrapper = document.createElement("td"); const timeWrapper = document.createElement("td");
@@ -348,7 +393,7 @@ Module.register("calendar", {
// Ongoing and getRelative is set // Ongoing and getRelative is set
timeWrapper.innerHTML = this.capFirst( timeWrapper.innerHTML = this.capFirst(
this.translate("RUNNING", { this.translate("RUNNING", {
fallback: this.translate("RUNNING") + " {timeUntilEnd}", fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
timeUntilEnd: moment(event.endDate, "x").fromNow(true) timeUntilEnd: moment(event.endDate, "x").fromNow(true)
}) })
); );
@@ -360,6 +405,8 @@ Module.register("calendar", {
// Full days events within the next two days // Full days events within the next two days
if (event.today) { if (event.today) {
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY")); timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
} else if (event.yesterday) {
timeWrapper.innerHTML = this.capFirst(this.translate("YESTERDAY"));
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) { } else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW")); timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) { } else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
@@ -377,8 +424,8 @@ Module.register("calendar", {
} else { } else {
timeWrapper.innerHTML = this.capFirst( timeWrapper.innerHTML = this.capFirst(
moment(event.startDate, "x").calendar(null, { moment(event.startDate, "x").calendar(null, {
sameDay: this.config.showTimeToday ? "LT" : "[" + this.translate("TODAY") + "]", sameDay: this.config.showTimeToday ? "LT" : `[${this.translate("TODAY")}]`,
nextDay: "[" + this.translate("TOMORROW") + "]", nextDay: `[${this.translate("TOMORROW")}]`,
nextWeek: "dddd", nextWeek: "dddd",
sameElse: event.fullDayEvent ? this.config.fullDayEventDateFormat : this.config.dateFormat sameElse: event.fullDayEvent ? this.config.fullDayEventDateFormat : this.config.dateFormat
}) })
@@ -388,6 +435,12 @@ Module.register("calendar", {
// Full days events within the next two days // Full days events within the next two days
if (event.today) { if (event.today) {
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY")); timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
} else if (event.dayBeforeYesterday) {
if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") {
timeWrapper.innerHTML = this.capFirst(this.translate("DAYBEFOREYESTERDAY"));
}
} else if (event.yesterday) {
timeWrapper.innerHTML = this.capFirst(this.translate("YESTERDAY"));
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) { } else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW")); timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) { } else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
@@ -403,36 +456,50 @@ Module.register("calendar", {
// Ongoing event // Ongoing event
timeWrapper.innerHTML = this.capFirst( timeWrapper.innerHTML = this.capFirst(
this.translate("RUNNING", { this.translate("RUNNING", {
fallback: this.translate("RUNNING") + " {timeUntilEnd}", fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
timeUntilEnd: moment(event.endDate, "x").fromNow(true) timeUntilEnd: moment(event.endDate, "x").fromNow(true)
}) })
); );
} }
} }
timeWrapper.className = "time light " + this.timeClassForUrl(event.url); timeWrapper.className = `time light ${this.timeClassForUrl(event.url)}`;
eventWrapper.appendChild(timeWrapper); eventWrapper.appendChild(timeWrapper);
} }
wrapper.appendChild(eventWrapper);
// Create fade effect. // Create fade effect.
if (index >= startFade) { if (index >= startFade) {
currentFadeStep = index - startFade; currentFadeStep = index - startFade;
eventWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep; eventWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
} }
wrapper.appendChild(eventWrapper);
if (this.config.showLocation) { if (this.config.showLocation) {
if (event.location !== false) { if (event.location !== false) {
const locationRow = document.createElement("tr"); const locationRow = document.createElement("tr");
locationRow.className = "normal xsmall light"; locationRow.className = "event-wrapper-location normal xsmall light";
if (event.today) locationRow.className += " today"; if (event.today) locationRow.className += " today";
else if (event.dayBeforeYesterday) locationRow.className += " dayBeforeYesterday";
else if (event.yesterday) locationRow.className += " yesterday";
else if (event.tomorrow) locationRow.className += " tomorrow"; else if (event.tomorrow) locationRow.className += " tomorrow";
else if (event.dayAfterTomorrow) locationRow.className += " dayAfterTomorrow";
if (this.config.displaySymbol) { if (this.config.displaySymbol) {
const symbolCell = document.createElement("td"); const symbolCell = document.createElement("td");
locationRow.appendChild(symbolCell); locationRow.appendChild(symbolCell);
} }
if (this.config.coloredText) {
locationRow.style.cssText = `color:${this.colorForUrl(event.url, false)}`;
}
if (this.config.coloredBackground) {
locationRow.style.backgroundColor = this.colorForUrl(event.url, true);
}
if (this.config.coloredBorder) {
locationRow.style.borderColor = this.colorForUrl(event.url, false);
}
const descCell = document.createElement("td"); const descCell = document.createElement("td");
descCell.className = "location"; descCell.className = "location";
descCell.colSpan = "2"; descCell.colSpan = "2";
@@ -510,6 +577,7 @@ Module.register("calendar", {
for (const calendarUrl in this.calendarData) { for (const calendarUrl in this.calendarData) {
const calendar = this.calendarData[calendarUrl]; const calendar = this.calendarData[calendarUrl];
let remainingEntries = this.maximumEntriesForUrl(calendarUrl); let remainingEntries = this.maximumEntriesForUrl(calendarUrl);
let maxPastDaysCompare = now - this.maximumPastDaysForUrl(calendarUrl) * ONE_DAY;
for (const e in calendar) { for (const e in calendar) {
const event = JSON.parse(JSON.stringify(calendar[e])); // clone object const event = JSON.parse(JSON.stringify(calendar[e])); // clone object
@@ -518,7 +586,7 @@ Module.register("calendar", {
continue; continue;
} }
if (limitNumberOfEntries) { if (limitNumberOfEntries) {
if (event.endDate < now) { if (event.endDate < maxPastDaysCompare) {
continue; continue;
} }
if (this.config.hideOngoing && event.startDate < now) { if (this.config.hideOngoing && event.startDate < now) {
@@ -533,7 +601,10 @@ Module.register("calendar", {
} }
event.url = calendarUrl; event.url = calendarUrl;
event.today = event.startDate >= today && event.startDate < today + ONE_DAY; event.today = event.startDate >= today && event.startDate < today + ONE_DAY;
event.dayBeforeYesterday = event.startDate >= today - ONE_DAY * 2 && event.startDate < today - ONE_DAY;
event.yesterday = event.startDate >= today - ONE_DAY && event.startDate < today;
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY; event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
event.dayAfterTomorrow = !event.tomorrow && event.startDate >= today + ONE_DAY * 2 && event.startDate < today + 3 * ONE_DAY;
/* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days, /* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
* otherwise, esp. in dateheaders mode it is not clear how long these events are. * otherwise, esp. in dateheaders mode it is not clear how long these events are.
@@ -548,7 +619,7 @@ Module.register("calendar", {
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + ONE_DAY; thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + ONE_DAY;
thisEvent.tomorrow = !thisEvent.today && thisEvent.startDate >= today + ONE_DAY && thisEvent.startDate < today + 2 * ONE_DAY; thisEvent.tomorrow = !thisEvent.today && thisEvent.startDate >= today + ONE_DAY && thisEvent.startDate < today + 2 * ONE_DAY;
thisEvent.endDate = midnight; thisEvent.endDate = midnight;
thisEvent.title += " (" + count + "/" + maxCount + ")"; thisEvent.title += ` (${count}/${maxCount})`;
splitEvents.push(thisEvent); splitEvents.push(thisEvent);
event.startDate = midnight; event.startDate = midnight;
@@ -556,7 +627,7 @@ Module.register("calendar", {
midnight = moment(midnight, "x").add(1, "day").format("x"); // next day midnight = moment(midnight, "x").add(1, "day").format("x"); // next day
} }
// Last day // Last day
event.title += " (" + count + "/" + maxCount + ")"; event.title += ` (${count}/${maxCount})`;
event.today += event.startDate >= today && event.startDate < today + ONE_DAY; event.today += event.startDate >= today && event.startDate < today + ONE_DAY;
event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY; event.tomorrow = !event.today && event.startDate >= today + ONE_DAY && event.startDate < today + 2 * ONE_DAY;
splitEvents.push(event); splitEvents.push(event);
@@ -592,7 +663,7 @@ Module.register("calendar", {
// check if we already are showing max unique days // check if we already are showing max unique days
if (eventDate > lastDate) { if (eventDate > lastDate) {
// if the only entry in the first day is a full day event that day is not counted as unique // if the only entry in the first day is a full day event that day is not counted as unique
if (newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) { if (!this.config.limitDaysNeverSkip && newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
days--; days--;
} }
days++; days++;
@@ -633,6 +704,7 @@ Module.register("calendar", {
excludedEvents: calendarConfig.excludedEvents || this.config.excludedEvents, excludedEvents: calendarConfig.excludedEvents || this.config.excludedEvents,
maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries, maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries,
maximumNumberOfDays: calendarConfig.maximumNumberOfDays || this.config.maximumNumberOfDays, maximumNumberOfDays: calendarConfig.maximumNumberOfDays || this.config.maximumNumberOfDays,
pastDaysCount: calendarConfig.pastDaysCount || this.config.pastDaysCount,
fetchInterval: this.config.fetchInterval, fetchInterval: this.config.fetchInterval,
symbolClass: calendarConfig.symbolClass, symbolClass: calendarConfig.symbolClass,
titleClass: calendarConfig.titleClass, titleClass: calendarConfig.titleClass,
@@ -665,7 +737,9 @@ Module.register("calendar", {
if (typeof ev.symbol !== "undefined" && ev.symbol !== "") { if (typeof ev.symbol !== "undefined" && ev.symbol !== "") {
let needle = new RegExp(ev.keyword, "gi"); let needle = new RegExp(ev.keyword, "gi");
if (needle.test(event.title)) { if (needle.test(event.title)) {
symbols[0] = ev.symbol; // Get the default prefix for this class name and add to the custom symbol provided
const className = this.getCalendarProperty(event.url, "symbolClassName", this.config.defaultSymbolClassName);
symbols[0] = className + ev.symbol;
break; break;
} }
} }
@@ -726,10 +800,11 @@ Module.register("calendar", {
* Retrieves the color for a specific calendar url. * Retrieves the color for a specific calendar url.
* *
* @param {string} url The calendar url * @param {string} url The calendar url
* @param {boolean} isBg Determines if we fetch the bgColor or not
* @returns {string} The color * @returns {string} The color
*/ */
colorForUrl: function (url) { colorForUrl: function (url, isBg) {
return this.getCalendarProperty(url, "color", "#fff"); return this.getCalendarProperty(url, isBg ? "bgColor" : "color", "#fff");
}, },
/** /**
@@ -752,6 +827,16 @@ Module.register("calendar", {
return this.getCalendarProperty(url, "maximumEntries", this.config.maximumEntries); return this.getCalendarProperty(url, "maximumEntries", this.config.maximumEntries);
}, },
/**
* Retrieves the maximum count of past days which events of should be displayed for a specific calendar url.
*
* @param {string} url The calendar url
* @returns {number} The maximum past days count
*/
maximumPastDaysForUrl: function (url) {
return this.getCalendarProperty(url, "pastDaysCount", this.config.pastDaysCount);
},
/** /**
* Helper method to retrieve the property for a specific calendar url. * Helper method to retrieve the property for a specific calendar url.
* *
@@ -809,7 +894,7 @@ Module.register("calendar", {
const word = words[i]; const word = words[i];
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) { if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
// max - 1 to account for a space // max - 1 to account for a space
currentLine += word + " "; currentLine += `${word} `;
} else { } else {
line++; line++;
if (line > maxTitleLines - 1) { if (line > maxTitleLines - 1) {
@@ -820,9 +905,9 @@ Module.register("calendar", {
} }
if (currentLine.length > 0) { if (currentLine.length > 0) {
temp += currentLine + "<br>" + word + " "; temp += `${currentLine}<br>${word} `;
} else { } else {
temp += word + "<br>"; temp += `${word}<br>`;
} }
currentLine = ""; currentLine = "";
} }
@@ -831,7 +916,7 @@ Module.register("calendar", {
return (temp + currentLine).trim(); return (temp + currentLine).trim();
} else { } else {
if (maxLength && typeof maxLength === "number" && string.length > maxLength) { if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
return string.trim().slice(0, maxLength) + "…"; return `${string.trim().slice(0, maxLength)}`;
} else { } else {
return string.trim(); return string.trim();
} }
@@ -886,7 +971,7 @@ Module.register("calendar", {
for (const event of eventList) { for (const event of eventList) {
event.symbol = this.symbolsForEvent(event); event.symbol = this.symbolsForEvent(event);
event.calendarName = this.calendarNameForUrl(event.url); event.calendarName = this.calendarNameForUrl(event.url);
event.color = this.colorForUrl(event.url); event.color = this.colorForUrl(event.url, false);
delete event.url; delete event.url;
} }

View File

@@ -4,13 +4,14 @@
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
const CalendarUtils = require("./calendarutils");
const Log = require("logger"); const https = require("https");
const NodeHelper = require("node_helper"); const digest = require("digest-fetch");
const ical = require("node-ical"); const ical = require("node-ical");
const fetch = require("fetch"); const fetch = require("fetch");
const digest = require("digest-fetch"); const Log = require("logger");
const https = require("https"); const NodeHelper = require("node_helper");
const CalendarUtils = require("./calendarutils");
/** /**
* *
@@ -41,7 +42,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
let fetcher = null; let fetcher = null;
let httpsAgent = null; let httpsAgent = null;
let headers = { let headers = {
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version "User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`
}; };
if (selfSignedCert) { if (selfSignedCert) {
@@ -51,11 +52,11 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
} }
if (auth) { if (auth) {
if (auth.method === "bearer") { if (auth.method === "bearer") {
headers.Authorization = "Bearer " + auth.pass; headers.Authorization = `Bearer ${auth.pass}`;
} else if (auth.method === "digest") { } else if (auth.method === "digest") {
fetcher = new digest(auth.user, auth.pass).fetch(url, { headers: headers, agent: httpsAgent }); fetcher = new digest(auth.user, auth.pass).fetch(url, { headers: headers, agent: httpsAgent });
} else { } else {
headers.Authorization = "Basic " + Buffer.from(auth.user + ":" + auth.pass).toString("base64"); headers.Authorization = `Basic ${Buffer.from(`${auth.user}:${auth.pass}`).toString("base64")}`;
} }
} }
if (fetcher === null) { if (fetcher === null) {
@@ -70,7 +71,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
try { try {
data = ical.parseICS(responseData); data = ical.parseICS(responseData);
Log.debug("parsed data=" + JSON.stringify(data)); Log.debug(`parsed data=${JSON.stringify(data)}`);
events = CalendarUtils.filterEvents(data, { events = CalendarUtils.filterEvents(data, {
excludedEvents, excludedEvents,
includePastEvents, includePastEvents,
@@ -114,7 +115,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
* Broadcast the existing events. * Broadcast the existing events.
*/ */
this.broadcastEvents = function () { this.broadcastEvents = function () {
Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events."); Log.info(`Calendar-Fetcher: Broadcasting ${events.length} events.`);
eventsReceivedCallback(this); eventsReceivedCallback(this);
}; };

View File

@@ -8,10 +8,10 @@
/** /**
* @external Moment * @external Moment
*/ */
const moment = require("moment");
const path = require("path"); const path = require("path");
const moment = require("moment");
const zoneTable = require(path.join(__dirname, "windowsZones.json")); const zoneTable = require(path.join(__dirname, "windowsZones.json"));
const Log = require("../../../js/logger.js"); const Log = require("../../../js/logger");
const CalendarUtils = { const CalendarUtils = {
/** /**
@@ -29,7 +29,7 @@ const CalendarUtils = {
Log.debug(" if no tz, guess based on now"); Log.debug(" if no tz, guess based on now");
event.start.tz = moment.tz.guess(); event.start.tz = moment.tz.guess();
} }
Log.debug("initial tz=" + event.start.tz); Log.debug(`initial tz=${event.start.tz}`);
// if there is a start date specified // if there is a start date specified
if (event.start.tz) { if (event.start.tz) {
@@ -37,7 +37,7 @@ const CalendarUtils = {
if (event.start.tz.includes(" ")) { if (event.start.tz.includes(" ")) {
// use the lookup table to get theIANA name as moment and date don't know MS timezones // use the lookup table to get theIANA name as moment and date don't know MS timezones
let tz = CalendarUtils.getIanaTZFromMS(event.start.tz); let tz = CalendarUtils.getIanaTZFromMS(event.start.tz);
Log.debug("corrected TZ=" + tz); Log.debug(`corrected TZ=${tz}`);
// watch out for unregistered windows timezone names // watch out for unregistered windows timezone names
// if we had a successful lookup // if we had a successful lookup
if (tz) { if (tz) {
@@ -46,7 +46,7 @@ const CalendarUtils = {
// Log.debug("corrected timezone="+event.start.tz) // Log.debug("corrected timezone="+event.start.tz)
} }
} }
Log.debug("corrected tz=" + event.start.tz); Log.debug(`corrected tz=${event.start.tz}`);
let current_offset = 0; // offset from TZ string or calculated let current_offset = 0; // offset from TZ string or calculated
let mm = 0; // date with tz or offset let mm = 0; // date with tz or offset
let start_offset = 0; // utc offset of created with tz let start_offset = 0; // utc offset of created with tz
@@ -57,18 +57,18 @@ const CalendarUtils = {
let start_offset = parseInt(start_offsetString[0]); let start_offset = parseInt(start_offsetString[0]);
start_offset *= event.start.tz[1] === "-" ? -1 : 1; start_offset *= event.start.tz[1] === "-" ? -1 : 1;
adjustHours = start_offset; adjustHours = start_offset;
Log.debug("defined offset=" + start_offset + " hours"); Log.debug(`defined offset=${start_offset} hours`);
current_offset = start_offset; current_offset = start_offset;
event.start.tz = ""; event.start.tz = "";
Log.debug("ical offset=" + current_offset + " date=" + date); Log.debug(`ical offset=${current_offset} date=${date}`);
mm = moment(date); mm = moment(date);
let x = parseInt(moment(new Date()).utcOffset()); let x = parseInt(moment(new Date()).utcOffset());
Log.debug("net mins=" + (current_offset * 60 - x)); Log.debug(`net mins=${current_offset * 60 - x}`);
mm = mm.add(x - current_offset * 60, "minutes"); mm = mm.add(x - current_offset * 60, "minutes");
adjustHours = (current_offset * 60 - x) / 60; adjustHours = (current_offset * 60 - x) / 60;
event.start = mm.toDate(); event.start = mm.toDate();
Log.debug("adjusted date=" + event.start); Log.debug(`adjusted date=${event.start}`);
} else { } else {
// get the start time in that timezone // get the start time in that timezone
let es = moment(event.start); let es = moment(event.start);
@@ -76,18 +76,18 @@ const CalendarUtils = {
if (es.format("YYYY") < 2007) { if (es.format("YYYY") < 2007) {
es.set("year", 2013); // if so, use a closer date es.set("year", 2013); // if so, use a closer date
} }
Log.debug("start date/time=" + es.toDate()); Log.debug(`start date/time=${es.toDate()}`);
start_offset = moment.tz(es, event.start.tz).utcOffset(); start_offset = moment.tz(es, event.start.tz).utcOffset();
Log.debug("start offset=" + start_offset); Log.debug(`start offset=${start_offset}`);
Log.debug("start date/time w tz =" + moment.tz(moment(event.start), event.start.tz).toDate()); Log.debug(`start date/time w tz =${moment.tz(moment(event.start), event.start.tz).toDate()}`);
// get the specified date in that timezone // get the specified date in that timezone
mm = moment.tz(moment(date), event.start.tz); mm = moment.tz(moment(date), event.start.tz);
Log.debug("event date=" + mm.toDate()); Log.debug(`event date=${mm.toDate()}`);
current_offset = mm.utcOffset(); current_offset = mm.utcOffset();
} }
Log.debug("event offset=" + current_offset + " hour=" + mm.format("H") + " event date=" + mm.toDate()); Log.debug(`event offset=${current_offset} hour=${mm.format("H")} event date=${mm.toDate()}`);
// if the offset is greater than 0, east of london // if the offset is greater than 0, east of london
if (current_offset !== start_offset) { if (current_offset !== start_offset) {
@@ -113,7 +113,7 @@ const CalendarUtils = {
} }
} }
} }
Log.debug("adjustHours=" + adjustHours); Log.debug(`adjustHours=${adjustHours}`);
return adjustHours; return adjustHours;
}, },
@@ -138,7 +138,7 @@ const CalendarUtils = {
return CalendarUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time])); return CalendarUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
}; };
Log.debug("There are " + Object.entries(data).length + " calendar entries."); Log.debug(`There are ${Object.entries(data).length} calendar entries.`);
Object.entries(data).forEach(([key, event]) => { Object.entries(data).forEach(([key, event]) => {
Log.debug("Processing entry..."); Log.debug("Processing entry...");
const now = new Date(); const now = new Date();
@@ -160,7 +160,7 @@ const CalendarUtils = {
} }
if (event.type === "VEVENT") { if (event.type === "VEVENT") {
Log.debug("Event:\n" + JSON.stringify(event)); Log.debug(`Event:\n${JSON.stringify(event)}`);
let startDate = eventDate(event, "start"); let startDate = eventDate(event, "start");
let endDate; let endDate;
@@ -177,12 +177,12 @@ const CalendarUtils = {
} }
} }
Log.debug("start: " + startDate.toDate()); Log.debug(`start: ${startDate.toDate()}`);
Log.debug("end:: " + endDate.toDate()); Log.debug(`end:: ${endDate.toDate()}`);
// Calculate the duration of the event for use with recurring events. // Calculate the duration of the event for use with recurring events.
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x")); let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
Log.debug("duration: " + duration); Log.debug(`duration: ${duration}`);
// FIXME: Since the parsed json object from node-ical comes with time information // FIXME: Since the parsed json object from node-ical comes with time information
// this check could be removed (?) // this check could be removed (?)
@@ -191,7 +191,7 @@ const CalendarUtils = {
} }
const title = CalendarUtils.getTitleFromEvent(event); const title = CalendarUtils.getTitleFromEvent(event);
Log.debug("title: " + title); Log.debug(`title: ${title}`);
let excluded = false, let excluded = false,
dateFilter = null; dateFilter = null;
@@ -271,8 +271,8 @@ const CalendarUtils = {
pastLocal = pastMoment.toDate(); pastLocal = pastMoment.toDate();
futureLocal = futureMoment.toDate(); futureLocal = futureMoment.toDate();
Log.debug("pastLocal: " + pastLocal); Log.debug(`pastLocal: ${pastLocal}`);
Log.debug("futureLocal: " + futureLocal); Log.debug(`futureLocal: ${futureLocal}`);
} else { } else {
// if we want past events // if we want past events
if (config.includePastEvents) { if (config.includePastEvents) {
@@ -284,9 +284,9 @@ const CalendarUtils = {
} }
futureLocal = futureMoment.toDate(); // future futureLocal = futureMoment.toDate(); // future
} }
Log.debug("Search for recurring events between: " + pastLocal + " and " + futureLocal); Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`);
const dates = rule.between(pastLocal, futureLocal, true, limitFunction); const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
Log.debug("Title: " + event.summary + ", with dates: " + JSON.stringify(dates)); Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`);
// The "dates" array contains the set of dates within our desired date range range that are valid // The "dates" array contains the set of dates within our desired date range range that are valid
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that // for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
// had its date changed from outside the range to inside the range. For the time being, // had its date changed from outside the range to inside the range. For the time being,
@@ -294,7 +294,7 @@ const CalendarUtils = {
// because the logic below will filter out any recurrences that don't actually belong within // because the logic below will filter out any recurrences that don't actually belong within
// our display range. // our display range.
// Would be great if there was a better way to handle this. // Would be great if there was a better way to handle this.
Log.debug("event.recurrences: " + event.recurrences); Log.debug(`event.recurrences: ${event.recurrences}`);
if (event.recurrences !== undefined) { if (event.recurrences !== undefined) {
for (let r in event.recurrences) { for (let r in event.recurrences) {
// Only add dates that weren't already in the range we added from the rrule so that // Only add dates that weren't already in the range we added from the rrule so that
@@ -323,10 +323,10 @@ const CalendarUtils = {
let dateoffset = date.getTimezoneOffset(); let dateoffset = date.getTimezoneOffset();
// Reduce the time by the following offset. // Reduce the time by the following offset.
Log.debug(" recurring date is " + date + " offset is " + dateoffset); Log.debug(` recurring date is ${date} offset is ${dateoffset}`);
let dh = moment(date).format("HH"); let dh = moment(date).format("HH");
Log.debug(" recurring date is " + date + " offset is " + dateoffset / 60 + " Hour is " + dh); Log.debug(` recurring date is ${date} offset is ${dateoffset / 60} Hour is ${dh}`);
if (CalendarUtils.isFullDayEvent(event)) { if (CalendarUtils.isFullDayEvent(event)) {
Log.debug("Fullday"); Log.debug("Fullday");
@@ -342,7 +342,7 @@ const CalendarUtils = {
// the duration was calculated way back at the top before we could correct the start time.. // the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry // fix it for this event entry
//duration = 24 * 60 * 60 * 1000; //duration = 24 * 60 * 60 * 1000;
Log.debug("new recurring date1 fulldate is " + date); Log.debug(`new recurring date1 fulldate is ${date}`);
} }
} else { } else {
// if the timezones are the same, correct date if needed // if the timezones are the same, correct date if needed
@@ -357,7 +357,7 @@ const CalendarUtils = {
// the duration was calculated way back at the top before we could correct the start time.. // the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry // fix it for this event entry
//duration = 24 * 60 * 60 * 1000; //duration = 24 * 60 * 60 * 1000;
Log.debug("new recurring date2 fulldate is " + date); Log.debug(`new recurring date2 fulldate is ${date}`);
} }
//} //}
} }
@@ -376,7 +376,7 @@ const CalendarUtils = {
// the duration was calculated way back at the top before we could correct the start time.. // the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry // fix it for this event entry
//duration = 24 * 60 * 60 * 1000; //duration = 24 * 60 * 60 * 1000;
Log.debug("new recurring date1 is " + date); Log.debug(`new recurring date1 is ${date}`);
} }
} else { } else {
// if the timezones are the same, correct date if needed // if the timezones are the same, correct date if needed
@@ -391,13 +391,13 @@ const CalendarUtils = {
// the duration was calculated way back at the top before we could correct the start time.. // the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry // fix it for this event entry
//duration = 24 * 60 * 60 * 1000; //duration = 24 * 60 * 60 * 1000;
Log.debug("new recurring date2 is " + date); Log.debug(`new recurring date2 is ${date}`);
} }
//} //}
} }
} }
startDate = moment(date); startDate = moment(date);
Log.debug("Corrected startDate: " + startDate.toDate()); Log.debug(`Corrected startDate: ${startDate.toDate()}`);
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date); let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date);
@@ -413,7 +413,7 @@ const CalendarUtils = {
// This date is an exception date, which means we should skip it in the recurrence pattern. // This date is an exception date, which means we should skip it in the recurrence pattern.
showRecurrence = false; showRecurrence = false;
} }
Log.debug("duration: " + duration); Log.debug(`duration: ${duration}`);
endDate = moment(parseInt(startDate.format("x")) + duration, "x"); endDate = moment(parseInt(startDate.format("x")) + duration, "x");
if (startDate.format("x") === endDate.format("x")) { if (startDate.format("x") === endDate.format("x")) {
@@ -433,7 +433,7 @@ const CalendarUtils = {
} }
if (showRecurrence === true) { if (showRecurrence === true) {
Log.debug("saving event: " + description); Log.debug(`saving event: ${description}`);
addedEvents++; addedEvents++;
newEvents.push({ newEvents.push({
title: recurrenceTitle, title: recurrenceTitle,
@@ -573,7 +573,7 @@ const CalendarUtils = {
if (filter) { if (filter) {
const until = filter.split(" "), const until = filter.split(" "),
value = parseInt(until[0]), value = parseInt(until[0]),
increment = until[1].slice(-1) === "s" ? until[1] : until[1] + "s", // Massage the data for moment js increment = until[1].slice(-1) === "s" ? until[1] : `${until[1]}s`, // Massage the data for moment js
filterUntil = moment(endDate.format()).subtract(value, increment); filterUntil = moment(endDate.format()).subtract(value, increment);
return now < filterUntil.format("x"); return now < filterUntil.format("x");

View File

@@ -8,7 +8,7 @@
// Alias modules mentioned in package.js under _moduleAliases. // Alias modules mentioned in package.js under _moduleAliases.
require("module-alias/register"); require("module-alias/register");
const CalendarFetcher = require("./calendarfetcher.js"); const CalendarFetcher = require("./calendarfetcher");
const url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL const url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL
//const url = "https://www.googleapis.com/calendar/v3/calendars/primary/events/"; // URL for Bearer auth (must be configured in Google OAuth2 first) //const url = "https://www.googleapis.com/calendar/v3/calendars/primary/events/"; // URL for Bearer auth (must be configured in Google OAuth2 first)

View File

@@ -5,13 +5,13 @@
* MIT Licensed. * MIT Licensed.
*/ */
const NodeHelper = require("node_helper"); const NodeHelper = require("node_helper");
const CalendarFetcher = require("./calendarfetcher.js");
const Log = require("logger"); const Log = require("logger");
const CalendarFetcher = require("./calendarfetcher");
module.exports = NodeHelper.create({ module.exports = NodeHelper.create({
// Override start method. // Override start method.
start: function () { start: function () {
Log.log("Starting node helper for: " + this.name); Log.log(`Starting node helper for: ${this.name}`);
this.fetchers = []; this.fetchers = [];
}, },
@@ -55,7 +55,7 @@ module.exports = NodeHelper.create({
let fetcher; let fetcher;
if (typeof this.fetchers[identifier + url] === "undefined") { if (typeof this.fetchers[identifier + url] === "undefined") {
Log.log("Create new calendarfetcher for url: " + url + " - Interval: " + fetchInterval); Log.log(`Create new calendarfetcher for url: ${url} - Interval: ${fetchInterval}`);
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert); fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
fetcher.onReceive((fetcher) => { fetcher.onReceive((fetcher) => {
@@ -73,7 +73,7 @@ module.exports = NodeHelper.create({
this.fetchers[identifier + url] = fetcher; this.fetchers[identifier + url] = fetcher;
} else { } else {
Log.log("Use existing calendarfetcher for url: " + url); Log.log(`Use existing calendarfetcher for url: ${url}`);
fetcher = this.fetchers[identifier + url]; fetcher = this.fetchers[identifier + url];
fetcher.broadcastEvents(); fetcher.broadcastEvents();
} }

View File

@@ -22,6 +22,7 @@ Module.register("clock", {
showTime: true, showTime: true,
showWeek: false, showWeek: false,
dateFormat: "dddd, LL", dateFormat: "dddd, LL",
sendNotifications: false,
/* specific to the analog clock */ /* specific to the analog clock */
analogSize: "200px", analogSize: "200px",
@@ -45,7 +46,7 @@ Module.register("clock", {
}, },
// Define start sequence. // Define start sequence.
start: function () { start: function () {
Log.info("Starting module: " + this.name); Log.info(`Starting module: ${this.name}`);
// Schedule update interval. // Schedule update interval.
this.second = moment().second(); this.second = moment().second();
@@ -66,23 +67,27 @@ Module.register("clock", {
const notificationTimer = () => { const notificationTimer = () => {
this.updateDom(); this.updateDom();
// If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent) if (this.config.sendNotifications) {
if (this.config.displaySeconds) { // If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent)
this.second = moment().second(); if (this.config.displaySeconds) {
if (this.second !== 0) { this.second = moment().second();
this.sendNotification("CLOCK_SECOND", this.second); if (this.second !== 0) {
setTimeout(notificationTimer, delayCalculator(0)); this.sendNotification("CLOCK_SECOND", this.second);
return; setTimeout(notificationTimer, delayCalculator(0));
return;
}
} }
// If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
this.minute = moment().minute();
this.sendNotification("CLOCK_MINUTE", this.minute);
} }
// If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
this.minute = moment().minute();
this.sendNotification("CLOCK_MINUTE", this.minute);
setTimeout(notificationTimer, delayCalculator(0)); setTimeout(notificationTimer, delayCalculator(0));
}; };
// Set the initial timeout with the amount of seconds elapsed as reducedSeconds so it will trigger when the minute changes // Set the initial timeout with the amount of seconds elapsed as
// reducedSeconds, so it will trigger when the minute changes
setTimeout(notificationTimer, delayCalculator(this.second)); setTimeout(notificationTimer, delayCalculator(this.second));
// Set locale. // Set locale.
@@ -91,13 +96,13 @@ Module.register("clock", {
// Override dom generator. // Override dom generator.
getDom: function () { getDom: function () {
const wrapper = document.createElement("div"); const wrapper = document.createElement("div");
wrapper.classList.add("clockGrid"); wrapper.classList.add("clock-grid");
/************************************ /************************************
* Create wrappers for analog and digital clock * Create wrappers for analog and digital clock
*/ */
const analogWrapper = document.createElement("div"); const analogWrapper = document.createElement("div");
analogWrapper.className = "clockCircle"; analogWrapper.className = "clock-circle";
const digitalWrapper = document.createElement("div"); const digitalWrapper = document.createElement("div");
digitalWrapper.className = "digital"; digitalWrapper.className = "digital";
digitalWrapper.style.gridArea = "center"; digitalWrapper.style.gridArea = "center";
@@ -137,9 +142,9 @@ Module.register("clock", {
} }
if (this.config.clockBold) { if (this.config.clockBold) {
timeString = now.format(hourSymbol + '[<span class="bold">]mm[</span>]'); timeString = now.format(`${hourSymbol}[<span class="bold">]mm[</span>]`);
} else { } else {
timeString = now.format(hourSymbol + ":mm"); timeString = now.format(`${hourSymbol}:mm`);
} }
if (this.config.showDate) { if (this.config.showDate) {
@@ -172,7 +177,7 @@ Module.register("clock", {
* @returns {string} The formatted time string * @returns {string} The formatted time string
*/ */
function formatTime(config, time) { function formatTime(config, time) {
let formatString = hourSymbol + ":mm"; let formatString = `${hourSymbol}:mm`;
if (config.showPeriod && config.timeFormat !== 24) { if (config.showPeriod && config.timeFormat !== 24) {
formatString += config.showPeriodUpper ? "A" : "a"; formatString += config.showPeriodUpper ? "A" : "a";
} }
@@ -195,19 +200,11 @@ Module.register("clock", {
nextEvent = tomorrowSunTimes.sunrise; nextEvent = tomorrowSunTimes.sunrise;
} }
const untilNextEvent = moment.duration(moment(nextEvent).diff(now)); const untilNextEvent = moment.duration(moment(nextEvent).diff(now));
const untilNextEventString = untilNextEvent.hours() + "h " + untilNextEvent.minutes() + "m"; const untilNextEventString = `${untilNextEvent.hours()}h ${untilNextEvent.minutes()}m`;
sunWrapper.innerHTML = sunWrapper.innerHTML =
'<span class="' + `<span class="${isVisible ? "bright" : ""}"><i class="fas fa-sun" aria-hidden="true"></i> ${untilNextEventString}</span>` +
(isVisible ? "bright" : "") + `<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ${formatTime(this.config, sunTimes.sunrise)}</span>` +
'"><i class="fas fa-sun" aria-hidden="true"></i> ' + `<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ${formatTime(this.config, sunTimes.sunset)}</span>`;
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>";
digitalWrapper.appendChild(sunWrapper); digitalWrapper.appendChild(sunWrapper);
} }
@@ -226,19 +223,11 @@ Module.register("clock", {
moonSet = nextMoonTimes.set; moonSet = nextMoonTimes.set;
} }
const isVisible = now.isBetween(moonRise, moonSet) || moonTimes.alwaysUp === true; const isVisible = now.isBetween(moonRise, moonSet) || moonTimes.alwaysUp === true;
const illuminatedFractionString = Math.round(moonIllumination.fraction * 100) + "%"; const illuminatedFractionString = `${Math.round(moonIllumination.fraction * 100)}%`;
moonWrapper.innerHTML = moonWrapper.innerHTML =
'<span class="' + `<span class="${isVisible ? "bright" : ""}"><i class="fas fa-moon" aria-hidden="true"></i> ${illuminatedFractionString}</span>` +
(isVisible ? "bright" : "") + `<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ${moonRise ? formatTime(this.config, moonRise) : "..."}</span>` +
'"><i class="fas fa-moon" aria-hidden="true"></i> ' + `<span><i class="fas fa-arrow-down" aria-hidden="true"></i> ${moonSet ? formatTime(this.config, moonSet) : "..."}</span>`;
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>";
digitalWrapper.appendChild(moonWrapper); digitalWrapper.appendChild(moonWrapper);
} }
@@ -266,7 +255,7 @@ Module.register("clock", {
analogWrapper.style.height = this.config.analogSize; analogWrapper.style.height = this.config.analogSize;
if (this.config.analogFace !== "" && this.config.analogFace !== "simple" && this.config.analogFace !== "none") { if (this.config.analogFace !== "" && this.config.analogFace !== "simple" && this.config.analogFace !== "none") {
analogWrapper.style.background = "url(" + this.data.path + "faces/" + this.config.analogFace + ".svg)"; analogWrapper.style.background = `url(${this.data.path}faces/${this.config.analogFace}.svg)`;
analogWrapper.style.backgroundSize = "100%"; analogWrapper.style.backgroundSize = "100%";
// The following line solves issue: https://github.com/MichMich/MagicMirror/issues/611 // The following line solves issue: https://github.com/MichMich/MagicMirror/issues/611
@@ -276,16 +265,16 @@ Module.register("clock", {
analogWrapper.style.border = "2px solid white"; analogWrapper.style.border = "2px solid white";
} }
const clockFace = document.createElement("div"); const clockFace = document.createElement("div");
clockFace.className = "clockFace"; clockFace.className = "clock-face";
const clockHour = document.createElement("div"); const clockHour = document.createElement("div");
clockHour.id = "clockHour"; clockHour.id = "clock-hour";
clockHour.style.transform = "rotate(" + hour + "deg)"; clockHour.style.transform = `rotate(${hour}deg)`;
clockHour.className = "clockHour"; clockHour.className = "clock-hour";
const clockMinute = document.createElement("div"); const clockMinute = document.createElement("div");
clockMinute.id = "clockMinute"; clockMinute.id = "clock-minute";
clockMinute.style.transform = "rotate(" + minute + "deg)"; clockMinute.style.transform = `rotate(${minute}deg)`;
clockMinute.className = "clockMinute"; clockMinute.className = "clock-minute";
// Combine analog wrappers // Combine analog wrappers
clockFace.appendChild(clockHour); clockFace.appendChild(clockHour);
@@ -293,9 +282,9 @@ Module.register("clock", {
if (this.config.displaySeconds) { if (this.config.displaySeconds) {
const clockSecond = document.createElement("div"); const clockSecond = document.createElement("div");
clockSecond.id = "clockSecond"; clockSecond.id = "clock-second";
clockSecond.style.transform = "rotate(" + second + "deg)"; clockSecond.style.transform = `rotate(${second}deg)`;
clockSecond.className = "clockSecond"; clockSecond.className = "clock-second";
clockSecond.style.backgroundColor = this.config.secondsColor; clockSecond.style.backgroundColor = this.config.secondsColor;
clockFace.appendChild(clockSecond); clockFace.appendChild(clockSecond);
} }
@@ -308,15 +297,15 @@ Module.register("clock", {
if (this.config.displayType === "analog") { if (this.config.displayType === "analog") {
// Display only an analog clock // Display only an analog clock
if (this.config.analogShowDate === "top") { if (this.config.analogShowDate === "top") {
wrapper.classList.add("clockGrid--bottom"); wrapper.classList.add("clock-grid-bottom");
} else if (this.config.analogShowDate === "bottom") { } else if (this.config.analogShowDate === "bottom") {
wrapper.classList.add("clockGrid--top"); wrapper.classList.add("clock-grid-top");
} }
wrapper.appendChild(analogWrapper); wrapper.appendChild(analogWrapper);
} else if (this.config.displayType === "digital") { } else if (this.config.displayType === "digital") {
wrapper.appendChild(digitalWrapper); wrapper.appendChild(digitalWrapper);
} else if (this.config.displayType === "both") { } else if (this.config.displayType === "both") {
wrapper.classList.add("clockGrid--" + this.config.analogPlacement); wrapper.classList.add(`clock-grid-${this.config.analogPlacement}`);
wrapper.appendChild(analogWrapper); wrapper.appendChild(analogWrapper);
wrapper.appendChild(digitalWrapper); wrapper.appendChild(digitalWrapper);
} }

View File

@@ -1,37 +1,37 @@
.clockGrid { .clock-grid {
display: inline-flex; display: inline-flex;
gap: 15px; gap: 15px;
} }
.clockGrid--left { .clock-grid-left {
flex-direction: row; flex-direction: row;
} }
.clockGrid--right { .clock-grid-right {
flex-direction: row-reverse; flex-direction: row-reverse;
} }
.clockGrid--top { .clock-grid-top {
flex-direction: column; flex-direction: column;
} }
.clockGrid--bottom { .clock-grid-bottom {
flex-direction: column-reverse; flex-direction: column-reverse;
} }
.clockCircle { .clock-circle {
place-self: center; place-self: center;
position: relative; position: relative;
border-radius: 50%; border-radius: 50%;
background-size: 100%; background-size: 100%;
} }
.clockFace { .clock-face {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.clockFace::after { .clock-face::after {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
@@ -44,7 +44,7 @@
display: block; display: block;
} }
.clockHour { .clock-hour {
width: 0; width: 0;
height: 0; height: 0;
position: absolute; position: absolute;
@@ -57,7 +57,7 @@
border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px;
} }
.clockMinute { .clock-minute {
width: 0; width: 0;
height: 0; height: 0;
position: absolute; position: absolute;
@@ -70,7 +70,7 @@
border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px;
} }
.clockSecond { .clock-second {
width: 0; width: 0;
height: 0; height: 0;
position: absolute; position: absolute;

View File

@@ -33,16 +33,15 @@ Module.register("compliments", {
}, },
// Define start sequence. // Define start sequence.
start: function () { start: async function () {
Log.info("Starting module: " + this.name); Log.info(`Starting module: ${this.name}`);
this.lastComplimentIndex = -1; this.lastComplimentIndex = -1;
if (this.config.remoteFile !== null) { if (this.config.remoteFile !== null) {
this.loadComplimentFile().then((response) => { const response = await this.loadComplimentFile();
this.config.compliments = JSON.parse(response); this.config.compliments = JSON.parse(response);
this.updateDom(); this.updateDom();
});
} }
// Schedule update timer. // Schedule update timer.

View File

@@ -1,5 +1,6 @@
iframe.newsfeed-fullarticle { iframe.newsfeed-fullarticle {
width: 100vw; width: 100vw;
/* very large height value to allow scrolling */ /* very large height value to allow scrolling */
height: 3000px; height: 3000px;
top: 0; top: 0;

View File

@@ -44,7 +44,7 @@ Module.register("newsfeed", {
getUrlPrefix: function (item) { getUrlPrefix: function (item) {
if (item.useCorsProxy) { if (item.useCorsProxy) {
return location.protocol + "//" + location.host + "/cors?url="; return `${location.protocol}//${location.host}/cors?url=`;
} else { } else {
return ""; return "";
} }
@@ -70,7 +70,7 @@ Module.register("newsfeed", {
// Define start sequence. // Define start sequence.
start: function () { start: function () {
Log.info("Starting module: " + this.name); Log.info(`Starting module: ${this.name}`);
// Set locale. // Set locale.
moment.locale(config.language); moment.locale(config.language);
@@ -346,7 +346,7 @@ Module.register("newsfeed", {
this.activeItem = 0; this.activeItem = 0;
} }
this.resetDescrOrFullArticleAndTimer(); this.resetDescrOrFullArticleAndTimer();
Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")"); Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
this.updateDom(100); this.updateDom(100);
} else if (notification === "ARTICLE_PREVIOUS") { } else if (notification === "ARTICLE_PREVIOUS") {
this.activeItem--; this.activeItem--;
@@ -354,7 +354,7 @@ Module.register("newsfeed", {
this.activeItem = this.newsItems.length - 1; this.activeItem = this.newsItems.length - 1;
} }
this.resetDescrOrFullArticleAndTimer(); this.resetDescrOrFullArticleAndTimer();
Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")"); Log.debug(`${this.name} - going from article #${before} to #${this.activeItem} (of ${this.newsItems.length})`);
this.updateDom(100); this.updateDom(100);
} }
// if "more details" is received the first time: show article summary, on second time show full article // if "more details" is received the first time: show article summary, on second time show full article
@@ -363,8 +363,8 @@ Module.register("newsfeed", {
if (this.config.showFullArticle === true) { if (this.config.showFullArticle === true) {
this.scrollPosition += this.config.scrollLength; this.scrollPosition += this.config.scrollLength;
window.scrollTo(0, this.scrollPosition); window.scrollTo(0, this.scrollPosition);
Log.debug(this.name + " - scrolling down"); Log.debug(`${this.name} - scrolling down`);
Log.debug(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength); Log.debug(`${this.name} - ARTICLE_MORE_DETAILS, scroll position: ${this.config.scrollLength}`);
} else { } else {
this.showFullArticle(); this.showFullArticle();
} }
@@ -372,12 +372,12 @@ Module.register("newsfeed", {
if (this.config.showFullArticle === true) { if (this.config.showFullArticle === true) {
this.scrollPosition -= this.config.scrollLength; this.scrollPosition -= this.config.scrollLength;
window.scrollTo(0, this.scrollPosition); window.scrollTo(0, this.scrollPosition);
Log.debug(this.name + " - scrolling up"); Log.debug(`${this.name} - scrolling up`);
Log.debug(this.name + " - ARTICLE_SCROLL_UP, scroll position: " + this.config.scrollLength); Log.debug(`${this.name} - ARTICLE_SCROLL_UP, scroll position: ${this.config.scrollLength}`);
} }
} else if (notification === "ARTICLE_LESS_DETAILS") { } else if (notification === "ARTICLE_LESS_DETAILS") {
this.resetDescrOrFullArticleAndTimer(); this.resetDescrOrFullArticleAndTimer();
Log.debug(this.name + " - showing only article titles again"); Log.debug(`${this.name} - showing only article titles again`);
this.updateDom(100); this.updateDom(100);
} else if (notification === "ARTICLE_TOGGLE_FULL") { } else if (notification === "ARTICLE_TOGGLE_FULL") {
if (this.config.showFullArticle) { if (this.config.showFullArticle) {
@@ -406,7 +406,7 @@ Module.register("newsfeed", {
} }
clearInterval(this.timer); clearInterval(this.timer);
this.timer = null; this.timer = null;
Log.debug(this.name + " - showing " + this.isShowingDescription ? "article description" : "full article"); Log.debug(`${this.name} - showing ${this.isShowingDescription ? "article description" : "full article"}`);
this.updateDom(100); this.updateDom(100);
} }
}); });

View File

@@ -4,12 +4,13 @@
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
const Log = require("logger");
const FeedMe = require("feedme");
const NodeHelper = require("node_helper");
const fetch = require("fetch");
const iconv = require("iconv-lite");
const stream = require("stream"); const stream = require("stream");
const FeedMe = require("feedme");
const iconv = require("iconv-lite");
const fetch = require("fetch");
const Log = require("logger");
const NodeHelper = require("node_helper");
/** /**
* Responsible for requesting an update on the set interval and broadcasting the data. * Responsible for requesting an update on the set interval and broadcasting the data.
@@ -64,15 +65,14 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
} else if (logFeedWarnings) { } else if (logFeedWarnings) {
Log.warn("Can't parse feed item:"); Log.warn("Can't parse feed item:");
Log.warn(item); Log.warn(item);
Log.warn("Title: " + title); Log.warn(`Title: ${title}`);
Log.warn("Description: " + description); Log.warn(`Description: ${description}`);
Log.warn("Pubdate: " + pubdate); Log.warn(`Pubdate: ${pubdate}`);
} }
}); });
parser.on("end", () => { parser.on("end", () => {
this.broadcastItems(); this.broadcastItems();
scheduleTimer();
}); });
parser.on("error", (error) => { parser.on("error", (error) => {
@@ -80,22 +80,27 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
scheduleTimer(); scheduleTimer();
}); });
//"end" event is not broadcast if the feed is empty but "finish" is used for both
parser.on("finish", () => {
scheduleTimer();
});
parser.on("ttl", (minutes) => { parser.on("ttl", (minutes) => {
try { try {
// 86400000 = 24 hours is mentioned in the docs as maximum value: // 86400000 = 24 hours is mentioned in the docs as maximum value:
const ttlms = Math.min(minutes * 60 * 1000, 86400000); const ttlms = Math.min(minutes * 60 * 1000, 86400000);
if (ttlms > reloadInterval) { if (ttlms > reloadInterval) {
reloadInterval = ttlms; reloadInterval = ttlms;
Log.info("Newsfeed-Fetcher: reloadInterval set to ttl=" + reloadInterval + " for url " + url); Log.info(`Newsfeed-Fetcher: reloadInterval set to ttl=${reloadInterval} for url ${url}`);
} }
} catch (error) { } catch (error) {
Log.warn("Newsfeed-Fetcher: feed ttl is no valid integer=" + minutes + " for url " + url); Log.warn(`Newsfeed-Fetcher: feed ttl is no valid integer=${minutes} for url ${url}`);
} }
}); });
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
const headers = { const headers = {
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version, "User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`,
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate", "Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
Pragma: "no-cache" Pragma: "no-cache"
}; };
@@ -155,7 +160,7 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
Log.info("Newsfeed-Fetcher: No items to broadcast yet."); Log.info("Newsfeed-Fetcher: No items to broadcast yet.");
return; return;
} }
Log.info("Newsfeed-Fetcher: Broadcasting " + items.length + " items."); Log.info(`Newsfeed-Fetcher: Broadcasting ${items.length} items.`);
itemsReceivedCallback(this); itemsReceivedCallback(this);
}; };

View File

@@ -6,13 +6,13 @@
*/ */
const NodeHelper = require("node_helper"); const NodeHelper = require("node_helper");
const NewsfeedFetcher = require("./newsfeedfetcher.js");
const Log = require("logger"); const Log = require("logger");
const NewsfeedFetcher = require("./newsfeedfetcher");
module.exports = NodeHelper.create({ module.exports = NodeHelper.create({
// Override start method. // Override start method.
start: function () { start: function () {
Log.log("Starting node helper for: " + this.name); Log.log(`Starting node helper for: ${this.name}`);
this.fetchers = []; this.fetchers = [];
}, },
@@ -47,7 +47,7 @@ module.exports = NodeHelper.create({
let fetcher; let fetcher;
if (typeof this.fetchers[url] === "undefined") { if (typeof this.fetchers[url] === "undefined") {
Log.log("Create new newsfetcher for url: " + url + " - Interval: " + reloadInterval); Log.log(`Create new newsfetcher for url: ${url} - Interval: ${reloadInterval}`);
fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings, useCorsProxy); fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings, useCorsProxy);
fetcher.onReceive(() => { fetcher.onReceive(() => {
@@ -64,7 +64,7 @@ module.exports = NodeHelper.create({
this.fetchers[url] = fetcher; this.fetchers[url] = fetcher;
} else { } else {
Log.log("Use existing newsfetcher for url: " + url); Log.log(`Use existing newsfetcher for url: ${url}`);
fetcher = this.fetchers[url]; fetcher = this.fetchers[url];
fetcher.setReloadInterval(reloadInterval); fetcher.setReloadInterval(reloadInterval);
fetcher.broadcastItems(); fetcher.broadcastItems();

View File

@@ -36,7 +36,7 @@ class GitHelper {
async add(moduleName) { async add(moduleName) {
let moduleFolder = BASE_DIR; let moduleFolder = BASE_DIR;
if (moduleName !== "default") { if (moduleName !== "MagicMirror") {
moduleFolder = `${moduleFolder}modules/${moduleName}`; moduleFolder = `${moduleFolder}modules/${moduleName}`;
} }
@@ -68,7 +68,7 @@ class GitHelper {
isBehindInStatus: false isBehindInStatus: false
}; };
if (repo.module === "default") { if (repo.module === "MagicMirror") {
// the hash is only needed for the mm repo // the hash is only needed for the mm repo
const { stderr, stdout } = await this.execShell(`cd ${repo.folder} && git rev-parse HEAD`); const { stderr, stdout } = await this.execShell(`cd ${repo.folder} && git rev-parse HEAD`);
@@ -117,11 +117,11 @@ class GitHelper {
return; return;
} }
if (gitInfo.isBehindInStatus) { if (gitInfo.isBehindInStatus && (gitInfo.module !== "MagicMirror" || gitInfo.current !== "master")) {
return gitInfo; return gitInfo;
} }
const { stderr } = await this.execShell(`cd ${repo.folder} && git fetch --dry-run`); const { stderr } = await this.execShell(`cd ${repo.folder} && git fetch -n --dry-run`);
// example output: // example output:
// From https://github.com/MichMich/MagicMirror // From https://github.com/MichMich/MagicMirror
@@ -129,16 +129,41 @@ class GitHelper {
// here the result is in stderr (this is a git default, don't ask why ...) // here the result is in stderr (this is a git default, don't ask why ...)
const matches = stderr.match(this.getRefRegex(gitInfo.current)); const matches = stderr.match(this.getRefRegex(gitInfo.current));
if (!matches || !matches[0]) { // this is the default if there was no match from "git fetch -n --dry-run".
// no refs found, nothing to do // Its a fallback because if there was a real "git fetch", the above "git fetch -n --dry-run" would deliver nothing.
return; let refDiff = `${gitInfo.current}..origin/${gitInfo.current}`;
if (matches && matches[0]) {
refDiff = matches[0];
} }
// get behind with refs // get behind with refs
try { try {
const { stdout } = await this.execShell(`cd ${repo.folder} && git rev-list --ancestry-path --count ${matches[0]}`); const { stdout } = await this.execShell(`cd ${repo.folder} && git rev-list --ancestry-path --count ${refDiff}`);
gitInfo.behind = parseInt(stdout); gitInfo.behind = parseInt(stdout);
// for MagicMirror-Repo and "master" branch avoid getting notified when no tag is in refDiff
// so only releases are reported and we can change e.g. the README.md without sending notifications
if (gitInfo.behind > 0 && gitInfo.module === "MagicMirror" && gitInfo.current === "master") {
let tagList = "";
try {
const { stdout } = await this.execShell(`cd ${repo.folder} && git ls-remote -q --tags --refs`);
tagList = stdout.trim();
} catch (err) {
Log.error(`Failed to get tag list for ${repo.module}: ${err}`);
}
// check if tag is between commits and only report behind > 0 if so
try {
const { stdout } = await this.execShell(`cd ${repo.folder} && git rev-list --ancestry-path ${refDiff}`);
let cnt = 0;
for (const ref of stdout.trim().split("\n")) {
if (tagList.includes(ref)) cnt++; // tag found
}
if (cnt === 0) gitInfo.behind = 0;
} catch (err) {
Log.error(`Failed to get git revisions for ${repo.module}: ${err}`);
}
}
return gitInfo; return gitInfo;
} catch (err) { } catch (err) {
Log.error(`Failed to get git revisions for ${repo.module}: ${err}`); Log.error(`Failed to get git revisions for ${repo.module}: ${err}`);

View File

@@ -1,6 +1,6 @@
const GitHelper = require("./git_helper");
const defaultModules = require("../defaultmodules");
const NodeHelper = require("node_helper"); const NodeHelper = require("node_helper");
const defaultModules = require("../defaultmodules");
const GitHelper = require("./git_helper");
const ONE_MINUTE = 60 * 1000; const ONE_MINUTE = 60 * 1000;
@@ -19,7 +19,9 @@ module.exports = NodeHelper.create({
} }
} }
await this.gitHelper.add("default"); if (!this.ignoreUpdateChecking("MagicMirror")) {
await this.gitHelper.add("MagicMirror");
}
}, },
async socketNotificationReceived(notification, payload) { async socketNotificationReceived(notification, payload) {

View File

@@ -77,7 +77,7 @@ Module.register("updatenotification", {
addFilters() { addFilters() {
this.nunjucksEnvironment().addFilter("diffLink", (text, status) => { this.nunjucksEnvironment().addFilter("diffLink", (text, status) => {
if (status.module !== "default") { if (status.module !== "MagicMirror") {
return text; return text;
} }

View File

@@ -3,7 +3,7 @@
<div class="small bright"> <div class="small bright">
<i class="fas fa-exclamation-circle"></i> <i class="fas fa-exclamation-circle"></i>
<span> <span>
{% set mainTextLabel = "UPDATE_NOTIFICATION" if name === "default" else "UPDATE_NOTIFICATION_MODULE" %} {% set mainTextLabel = "UPDATE_NOTIFICATION" if name === "MagicMirror" else "UPDATE_NOTIFICATION_MODULE" %}
{{ mainTextLabel | translate({MODULE_NAME: name}) }} {{ mainTextLabel | translate({MODULE_NAME: name}) }}
</span> </span>
</div> </div>

View File

@@ -5,8 +5,8 @@
* @param {string} type what contenttype to expect in the response, can be "json" or "xml" * @param {string} type what contenttype to expect in the response, can be "json" or "xml"
* @param {boolean} useCorsProxy A flag to indicate * @param {boolean} useCorsProxy A flag to indicate
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send * @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve * @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
* @returns {Promise} resolved when the fetch is done. The response headers is placed in a headers-property (provided the response does not allready contain a headers-property). * @returns {Promise} resolved when the fetch is done. The response headers is placed in a headers-property (provided the response does not already contain a headers-property).
*/ */
async function performWebRequest(url, type = "json", useCorsProxy = false, requestHeaders = undefined, expectedResponseHeaders = undefined) { async function performWebRequest(url, type = "json", useCorsProxy = false, requestHeaders = undefined, expectedResponseHeaders = undefined) {
const request = {}; const request = {};
@@ -36,7 +36,7 @@ async function performWebRequest(url, type = "json", useCorsProxy = false, reque
* *
* @param {string} url the url to fetch from * @param {string} url the url to fetch from
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send * @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve * @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
* @returns {string} to be used as URL when calling CORS-method on server. * @returns {string} to be used as URL when calling CORS-method on server.
*/ */
const getCorsUrl = function (url, requestHeaders, expectedResponseHeaders) { const getCorsUrl = function (url, requestHeaders, expectedResponseHeaders) {
@@ -84,7 +84,7 @@ const getRequestHeaderString = function (requestHeaders) {
}; };
/** /**
* Gets headers and values to attatch to the web request. * Gets headers and values to attach to the web request.
* *
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send * @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
* @returns {object} An object specifying name and value of the headers. * @returns {object} An object specifying name and value of the headers.
@@ -101,9 +101,9 @@ const getHeadersToSend = (requestHeaders) => {
}; };
/** /**
* Gets the part of the CORS URL that represents the expected HTTP headers to recieve. * Gets the part of the CORS URL that represents the expected HTTP headers to receive.
* *
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve * @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
* @returns {string} to be used as the expected HTTP-headers component in CORS URL. * @returns {string} to be used as the expected HTTP-headers component in CORS URL.
*/ */
const getExpectedResponseHeadersString = function (expectedResponseHeaders) { const getExpectedResponseHeadersString = function (expectedResponseHeaders) {
@@ -124,7 +124,7 @@ const getExpectedResponseHeadersString = function (expectedResponseHeaders) {
/** /**
* Gets the values for the expected headers from the response. * Gets the values for the expected headers from the response.
* *
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve * @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
* @param {Response} response the HTTP response * @param {Response} response the HTTP response
* @returns {string} to be used as the expected HTTP-headers component in CORS URL. * @returns {string} to be used as the expected HTTP-headers component in CORS URL.
*/ */

View File

@@ -7,7 +7,7 @@
{% if config.showWindDirection %} {% if config.showWindDirection %}
<sup> <sup>
{% if config.showWindDirectionAsArrow %} {% if config.showWindDirectionAsArrow %}
<i class="fas fa-long-arrow-alt-up" style="transform:rotate({{ current.windDirection }}deg);"></i> <i class="fas fa-long-arrow-alt-down" style="transform:rotate({{ current.windFromDirection }}deg);"></i>
{% else %} {% else %}
{{ current.cardinalWindDirection() | translate }} {{ current.cardinalWindDirection() | translate }}
{% endif %} {% endif %}
@@ -16,7 +16,7 @@
{% endif %} {% endif %}
</span> </span>
{% if config.showHumidity and current.humidity %} {% if config.showHumidity and current.humidity %}
<span>{{ current.humidity | decimalSymbol }}</span><sup>&nbsp;<i class="wi wi-humidity humidityIcon"></i></sup> <span>{{ current.humidity | decimalSymbol }}</span><sup>&nbsp;<i class="wi wi-humidity humidity-icon"></i></sup>
{% endif %} {% endif %}
{% if config.showSun %} {% if config.showSun %}
<span class="wi dimmed wi-{{ current.nextSunAction() }}"></span> <span class="wi dimmed wi-{{ current.nextSunAction() }}"></span>
@@ -54,16 +54,21 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% if (config.showFeelsLike or config.showPrecipitationAmount) and not config.onlyTemp %} {% if (config.showFeelsLike or config.showPrecipitationAmount or config.showPrecipitationProbability) and not config.onlyTemp %}
<div class="normal medium feelslike"> <div class="normal medium feelslike">
{% if config.showFeelsLike %} {% if config.showFeelsLike %}
<span class="dimmed"> <span class="dimmed">
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }} {{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
</span> </span><br/>
{% endif %} {% endif %}
{% if config.showPrecipitationAmount %} {% if config.showPrecipitationAmount and current.precipitationAmount %}
<span class="dimmed"> <span class="dimmed">
{{ "PRECIP" | translate }} {{ current.precipitation | unit("precip") }} <span class="precipitationLeadText">{{ "PRECIP_AMOUNT" | translate }}</span> {{ current.precipitationAmount | unit("precip", current.precipitationUnits) }}
</span><br/>
{% endif %}
{% if config.showPrecipitationProbability and current.precipitationProbability %}
<span class="dimmed">
<span class="precipitationLeadText">{{ "PRECIP_POP" | translate }}</span> {{ current.precipitationProbability }}%
</span> </span>
{% endif %} {% endif %}
</div> </div>

View File

@@ -23,15 +23,14 @@
{{ f.minTemperature | roundValue | unit("temperature") | decimalSymbol }} {{ f.minTemperature | roundValue | unit("temperature") | decimalSymbol }}
</td> </td>
{% if config.showPrecipitationAmount %} {% if config.showPrecipitationAmount %}
{% if f.precipitationUnits %} <td class="align-right bright precipitation-amount">
<td class="align-right bright precipitation"> {{ f.precipitationAmount | unit("precip", f.precipitationUnits) }}
{{ f.precipitation }}{{ f.precipitationUnits }} </td>
</td> {% endif %}
{% else %} {% if config.showPrecipitationProbability %}
<td class="align-right bright precipitation"> <td class="align-right bright precipitation-prob">
{{ f.precipitation | unit("precip") }} {{ f.precipitationProbability | unit("precip", "%") }}
</td> </td>
{% endif %}
{% endif %} {% endif %}
</tr> </tr>
{% set currentStep = currentStep + 1 %} {% set currentStep = currentStep + 1 %}

View File

@@ -11,15 +11,14 @@
{{ hour.temperature | roundValue | unit("temperature") }} {{ hour.temperature | roundValue | unit("temperature") }}
</td> </td>
{% if config.showPrecipitationAmount %} {% if config.showPrecipitationAmount %}
{% if hour.precipitationUnits %} <td class="align-right bright precipitation-amount">
<td class="align-right bright precipitation"> {{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }}
{{ hour.precipitation }}{{ hour.precipitationUnits }} </td>
</td> {% endif %}
{% else %} {% if config.showPrecipitationProbability %}
<td class="align-right bright precipitation"> <td class="align-right bright precipitation-prob">
{{ hour.precipitation | unit("precip") }} {{ hour.precipitationProbability | unit("precip", "%") }}
</td> </td>
{% endif %}
{% endif %} {% endif %}
</tr> </tr>
{% set currentStep = currentStep + 1 %} {% set currentStep = currentStep + 1 %}

View File

@@ -138,7 +138,7 @@ WeatherProvider.register("envcanada", {
// being accessed. This is only pertinent when using the EC data elements that contain a textual forecast. // being accessed. This is only pertinent when using the EC data elements that contain a textual forecast.
// //
getUrl() { getUrl() {
return "https://dd.weather.gc.ca/citypage_weather/xml/" + this.config.provCode + "/" + this.config.siteCode + "_e.xml"; return `https://dd.weather.gc.ca/citypage_weather/xml/${this.config.provCode}/${this.config.siteCode}_e.xml`;
}, },
// //
@@ -165,7 +165,7 @@ WeatherProvider.register("envcanada", {
currentWeather.windSpeed = WeatherUtils.convertWindToMs(ECdoc.querySelector("siteData currentConditions wind speed").textContent); currentWeather.windSpeed = WeatherUtils.convertWindToMs(ECdoc.querySelector("siteData currentConditions wind speed").textContent);
currentWeather.windDirection = ECdoc.querySelector("siteData currentConditions wind bearing").textContent; currentWeather.windFromDirection = ECdoc.querySelector("siteData currentConditions wind bearing").textContent;
currentWeather.humidity = ECdoc.querySelector("siteData currentConditions relativeHumidity").textContent; currentWeather.humidity = ECdoc.querySelector("siteData currentConditions relativeHumidity").textContent;
@@ -230,12 +230,7 @@ WeatherProvider.register("envcanada", {
const foreGroup = ECdoc.querySelectorAll("siteData forecastGroup forecast"); const foreGroup = ECdoc.querySelectorAll("siteData forecastGroup forecast");
// For simplicity, we will only accumulate precipitation and will not try to break out weather.precipitationAmount = null;
// rain vs snow accumulations
weather.rain = null;
weather.snow = null;
weather.precipitation = null;
// //
// The EC forecast is held in a 12-element array - Elements 0 to 11 - with each day encompassing // The EC forecast is held in a 12-element array - Elements 0 to 11 - with each day encompassing
@@ -336,16 +331,14 @@ WeatherProvider.register("envcanada", {
// Add 1 to the date to reflect the current forecast day we are building // Add 1 to the date to reflect the current forecast day we are building
lastDate = lastDate.add(1, "day"); lastDate = lastDate.add(1, "day");
weather.date = moment.unix(lastDate); weather.date = moment(lastDate);
// Capture the temperatures for the current Element and the next Element in order to set // Capture the temperatures for the current Element and the next Element in order to set
// the Min and Max temperatures for the forecast // the Min and Max temperatures for the forecast
this.setMinMaxTemps(weather, foreGroup, stepDay, true, currentTemp); this.setMinMaxTemps(weather, foreGroup, stepDay, true, currentTemp);
weather.rain = null; weather.precipitationAmount = null;
weather.snow = null;
weather.precipitation = null;
this.setPrecipitation(weather, foreGroup, stepDay); this.setPrecipitation(weather, foreGroup, stepDay);
@@ -402,8 +395,7 @@ WeatherProvider.register("envcanada", {
const precipLOP = hourGroup[stepHour].querySelector("lop").textContent * 1.0; const precipLOP = hourGroup[stepHour].querySelector("lop").textContent * 1.0;
if (precipLOP > 0) { if (precipLOP > 0) {
weather.precipitation = precipLOP; weather.precipitationProbability = precipLOP;
weather.precipitationUnits = hourGroup[stepHour].querySelector("lop").getAttribute("units");
} }
// //
@@ -508,27 +500,14 @@ WeatherProvider.register("envcanada", {
setPrecipitation(weather, foreGroup, today) { setPrecipitation(weather, foreGroup, today) {
if (foreGroup[today].querySelector("precipitation accumulation")) { if (foreGroup[today].querySelector("precipitation accumulation")) {
weather.precipitation = foreGroup[today].querySelector("precipitation accumulation amount").textContent * 1.0; weather.precipitationAmount = foreGroup[today].querySelector("precipitation accumulation amount").textContent * 1.0;
weather.precipitationUnits = foreGroup[today].querySelector("precipitation accumulation amount").getAttribute("units");
weather.precipitationUnits = " " + foreGroup[today].querySelector("precipitation accumulation amount").getAttribute("units");
if (this.config.units === "imperial") {
if (weather.precipitationUnits === " cm") {
weather.precipitation = (weather.precipitation * 0.394).toFixed(2);
weather.precipitationUnits = " in";
}
if (weather.precipitationUnits === " mm") {
weather.precipitation = (weather.precipitation * 0.0394).toFixed(2);
weather.precipitationUnits = " in";
}
}
} }
// Check Today element for POP // Check Today element for POP
if (foreGroup[today].querySelector("abbreviatedForecast pop").textContent > 0) { if (foreGroup[today].querySelector("abbreviatedForecast pop").textContent > 0) {
weather.precipitation = foreGroup[today].querySelector("abbreviatedForecast pop").textContent; weather.precipitationProbability = foreGroup[today].querySelector("abbreviatedForecast pop").textContent;
weather.precipitationUnits = foreGroup[today].querySelector("abbreviatedForecast pop").getAttribute("units");
} }
}, },

View File

@@ -24,7 +24,7 @@ WeatherProvider.register("openmeteo", {
apiBase: OPEN_METEO_BASE, apiBase: OPEN_METEO_BASE,
lat: 0, lat: 0,
lon: 0, lon: 0,
past_days: 0, pastDays: 0,
type: "current" type: "current"
}, },
@@ -227,12 +227,12 @@ WeatherProvider.register("openmeteo", {
longitude: this.config.lon, longitude: this.config.lon,
timeformat: "unixtime", timeformat: "unixtime",
timezone: "auto", timezone: "auto",
past_days: this.config.past_days ?? 0, past_days: this.config.pastDays ?? 0,
daily: this.dailyParams, daily: this.dailyParams,
hourly: this.hourlyParams, hourly: this.hourlyParams,
// Fixed units as metric // Fixed units as metric
temperature_unit: "celsius", temperature_unit: "celsius",
windspeed_unit: "kmh", windspeed_unit: "ms",
precipitation_unit: "mm" precipitation_unit: "mm"
}; };
@@ -264,9 +264,9 @@ WeatherProvider.register("openmeteo", {
switch (key) { switch (key) {
case "hourly": case "hourly":
case "daily": case "daily":
return encodeURIComponent(key) + "=" + params[key].join(","); return `${encodeURIComponent(key)}=${params[key].join(",")}`;
default: default:
return encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); return `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`;
} }
}) })
.join("&"); .join("&");
@@ -367,11 +367,11 @@ WeatherProvider.register("openmeteo", {
* `current_weather` object. * `current_weather` object.
*/ */
const h = moment().hour(); const h = moment().hour();
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits); const currentWeather = new WeatherObject();
currentWeather.date = weather.current_weather.time; currentWeather.date = weather.current_weather.time;
currentWeather.windSpeed = weather.current_weather.windspeed; currentWeather.windSpeed = weather.current_weather.windspeed;
currentWeather.windDirection = weather.current_weather.winddirection; currentWeather.windFromDirection = weather.current_weather.winddirection;
currentWeather.sunrise = weather.daily[0].sunrise; currentWeather.sunrise = weather.daily[0].sunrise;
currentWeather.sunset = weather.daily[0].sunset; currentWeather.sunset = weather.daily[0].sunset;
currentWeather.temperature = parseFloat(weather.current_weather.temperature); currentWeather.temperature = parseFloat(weather.current_weather.temperature);
@@ -381,7 +381,7 @@ WeatherProvider.register("openmeteo", {
currentWeather.humidity = parseFloat(weather.hourly[h].relativehumidity_2m); currentWeather.humidity = parseFloat(weather.hourly[h].relativehumidity_2m);
currentWeather.rain = parseFloat(weather.hourly[h].rain); currentWeather.rain = parseFloat(weather.hourly[h].rain);
currentWeather.snow = parseFloat(weather.hourly[h].snowfall * 10); currentWeather.snow = parseFloat(weather.hourly[h].snowfall * 10);
currentWeather.precipitation = parseFloat(weather.hourly[h].precipitation); currentWeather.precipitationAmount = parseFloat(weather.hourly[h].precipitation);
return currentWeather; return currentWeather;
}, },
@@ -391,11 +391,11 @@ WeatherProvider.register("openmeteo", {
const days = []; const days = [];
weathers.daily.forEach((weather, i) => { weathers.daily.forEach((weather, i) => {
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits); const currentWeather = new WeatherObject();
currentWeather.date = weather.time; currentWeather.date = weather.time;
currentWeather.windSpeed = weather.windspeed_10m_max; currentWeather.windSpeed = weather.windspeed_10m_max;
currentWeather.windDirection = weather.winddirection_10m_dominant; currentWeather.windFromDirection = weather.winddirection_10m_dominant;
currentWeather.sunrise = weather.sunrise; currentWeather.sunrise = weather.sunrise;
currentWeather.sunset = weather.sunset; currentWeather.sunset = weather.sunset;
currentWeather.temperature = parseFloat((weather.apparent_temperature_max + weather.apparent_temperature_min) / 2); currentWeather.temperature = parseFloat((weather.apparent_temperature_max + weather.apparent_temperature_min) / 2);
@@ -404,7 +404,7 @@ WeatherProvider.register("openmeteo", {
currentWeather.weatherType = this.convertWeatherType(weather.weathercode, currentWeather.isDayTime()); currentWeather.weatherType = this.convertWeatherType(weather.weathercode, currentWeather.isDayTime());
currentWeather.rain = parseFloat(weather.rain_sum); currentWeather.rain = parseFloat(weather.rain_sum);
currentWeather.snow = parseFloat(weather.snowfall_sum * 10); currentWeather.snow = parseFloat(weather.snowfall_sum * 10);
currentWeather.precipitation = parseFloat(weather.precipitation_sum); currentWeather.precipitationAmount = parseFloat(weather.precipitation_sum);
days.push(currentWeather); days.push(currentWeather);
}); });
@@ -422,12 +422,12 @@ WeatherProvider.register("openmeteo", {
return; return;
} }
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits); const currentWeather = new WeatherObject();
const h = Math.ceil((i + 1) / 24) - 1; const h = Math.ceil((i + 1) / 24) - 1;
currentWeather.date = weather.time; currentWeather.date = weather.time;
currentWeather.windSpeed = weather.windspeed_10m; currentWeather.windSpeed = weather.windspeed_10m;
currentWeather.windDirection = weather.winddirection_10m; currentWeather.windFromDirection = weather.winddirection_10m;
currentWeather.sunrise = weathers.daily[h].sunrise; currentWeather.sunrise = weathers.daily[h].sunrise;
currentWeather.sunset = weathers.daily[h].sunset; currentWeather.sunset = weathers.daily[h].sunset;
currentWeather.temperature = parseFloat(weather.apparent_temperature); currentWeather.temperature = parseFloat(weather.apparent_temperature);
@@ -437,7 +437,7 @@ WeatherProvider.register("openmeteo", {
currentWeather.humidity = parseFloat(weather.relativehumidity_2m); currentWeather.humidity = parseFloat(weather.relativehumidity_2m);
currentWeather.rain = parseFloat(weather.rain); currentWeather.rain = parseFloat(weather.rain);
currentWeather.snow = parseFloat(weather.snowfall * 10); currentWeather.snow = parseFloat(weather.snowfall * 10);
currentWeather.precipitation = parseFloat(weather.precipitation); currentWeather.precipitationAmount = parseFloat(weather.precipitation);
hours.push(currentWeather); hours.push(currentWeather);
}); });

View File

@@ -132,7 +132,7 @@ WeatherProvider.register("openweathermap", {
currentWeather.temperature = currentWeatherData.main.temp; currentWeather.temperature = currentWeatherData.main.temp;
currentWeather.feelsLikeTemp = currentWeatherData.main.feels_like; currentWeather.feelsLikeTemp = currentWeatherData.main.feels_like;
currentWeather.windSpeed = currentWeatherData.wind.speed; currentWeather.windSpeed = currentWeatherData.wind.speed;
currentWeather.windDirection = currentWeatherData.wind.deg; currentWeather.windFromDirection = currentWeatherData.wind.deg;
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon); currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon);
currentWeather.sunrise = moment.unix(currentWeatherData.sys.sunrise); currentWeather.sunrise = moment.unix(currentWeatherData.sys.sunrise);
currentWeather.sunset = moment.unix(currentWeatherData.sys.sunset); currentWeather.sunset = moment.unix(currentWeatherData.sys.sunset);
@@ -145,9 +145,9 @@ WeatherProvider.register("openweathermap", {
*/ */
generateWeatherObjectsFromForecast(forecasts) { generateWeatherObjectsFromForecast(forecasts) {
if (this.config.weatherEndpoint === "/forecast") { if (this.config.weatherEndpoint === "/forecast") {
return this.fetchForecastHourly(forecasts); return this.generateForecastHourly(forecasts);
} else if (this.config.weatherEndpoint === "/forecast/daily") { } else if (this.config.weatherEndpoint === "/forecast/daily") {
return this.fetchForecastDaily(forecasts); return this.generateForecastDaily(forecasts);
} }
// if weatherEndpoint does not match forecast or forecast/daily, what should be returned? // if weatherEndpoint does not match forecast or forecast/daily, what should be returned?
return [new WeatherObject()]; return [new WeatherObject()];
@@ -165,9 +165,10 @@ WeatherProvider.register("openweathermap", {
}, },
/* /*
* fetch forecast information for 3-hourly forecast (available for free subscription). * Generate forecast information for 3-hourly forecast (available for free
* subscription).
*/ */
fetchForecastHourly(forecasts) { generateForecastHourly(forecasts) {
// initial variable declaration // initial variable declaration
const days = []; const days = [];
// variables for temperature range and rain // variables for temperature range and rain
@@ -186,7 +187,7 @@ WeatherProvider.register("openweathermap", {
weather.maxTemperature = Math.max.apply(null, maxTemp); weather.maxTemperature = Math.max.apply(null, maxTemp);
weather.rain = rain; weather.rain = rain;
weather.snow = snow; weather.snow = snow;
weather.precipitation = weather.rain + weather.snow; weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
// push weather information to days array // push weather information to days array
days.push(weather); days.push(weather);
// create new weather-object // create new weather-object
@@ -216,20 +217,12 @@ WeatherProvider.register("openweathermap", {
minTemp.push(forecast.main.temp_min); minTemp.push(forecast.main.temp_min);
maxTemp.push(forecast.main.temp_max); maxTemp.push(forecast.main.temp_max);
if (forecast.hasOwnProperty("rain")) { if (forecast.hasOwnProperty("rain") && !isNaN(forecast.rain["3h"])) {
if (this.config.units === "imperial" && !isNaN(forecast.rain["3h"])) { rain += forecast.rain["3h"];
rain += forecast.rain["3h"] / 25.4;
} else if (!isNaN(forecast.rain["3h"])) {
rain += forecast.rain["3h"];
}
} }
if (forecast.hasOwnProperty("snow")) { if (forecast.hasOwnProperty("snow") && !isNaN(forecast.snow["3h"])) {
if (this.config.units === "imperial" && !isNaN(forecast.snow["3h"])) { snow += forecast.snow["3h"];
snow += forecast.snow["3h"] / 25.4;
} else if (!isNaN(forecast.snow["3h"])) {
snow += forecast.snow["3h"];
}
} }
} }
@@ -239,16 +232,17 @@ WeatherProvider.register("openweathermap", {
weather.maxTemperature = Math.max.apply(null, maxTemp); weather.maxTemperature = Math.max.apply(null, maxTemp);
weather.rain = rain; weather.rain = rain;
weather.snow = snow; weather.snow = snow;
weather.precipitation = weather.rain + weather.snow; weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
// push weather information to days array // push weather information to days array
days.push(weather); days.push(weather);
return days.slice(1); return days.slice(1);
}, },
/* /*
* fetch forecast information for daily forecast (available for paid subscription or old apiKey). * Generate forecast information for daily forecast (available for paid
* subscription or old apiKey).
*/ */
fetchForecastDaily(forecasts) { generateForecastDaily(forecasts) {
// initial variable declaration // initial variable declaration
const days = []; const days = [];
@@ -264,25 +258,18 @@ WeatherProvider.register("openweathermap", {
// forecast.rain not available if amount is zero // forecast.rain not available if amount is zero
// The API always returns in millimeters // The API always returns in millimeters
if (forecast.hasOwnProperty("rain")) { if (forecast.hasOwnProperty("rain") && !isNaN(forecast.rain)) {
if (this.config.units === "imperial" && !isNaN(forecast.rain)) { weather.rain = forecast.rain;
weather.rain = forecast.rain / 25.4;
} else if (!isNaN(forecast.rain)) {
weather.rain = forecast.rain;
}
} }
// forecast.snow not available if amount is zero // forecast.snow not available if amount is zero
// The API always returns in millimeters // The API always returns in millimeters
if (forecast.hasOwnProperty("snow")) { if (forecast.hasOwnProperty("snow") && !isNaN(forecast.snow)) {
if (this.config.units === "imperial" && !isNaN(forecast.snow)) { weather.snow = forecast.snow;
weather.snow = forecast.snow / 25.4;
} else if (!isNaN(forecast.snow)) {
weather.snow = forecast.snow;
}
} }
weather.precipitation = weather.rain + weather.snow; weather.precipitationAmount = weather.rain + weather.snow;
weather.precipitationProbability = forecast.pop ? forecast.pop * 100 : undefined;
days.push(weather); days.push(weather);
} }
@@ -303,30 +290,22 @@ WeatherProvider.register("openweathermap", {
if (data.hasOwnProperty("current")) { if (data.hasOwnProperty("current")) {
current.date = moment.unix(data.current.dt).utcOffset(data.timezone_offset / 60); current.date = moment.unix(data.current.dt).utcOffset(data.timezone_offset / 60);
current.windSpeed = data.current.wind_speed; current.windSpeed = data.current.wind_speed;
current.windDirection = data.current.wind_deg; current.windFromDirection = data.current.wind_deg;
current.sunrise = moment.unix(data.current.sunrise).utcOffset(data.timezone_offset / 60); current.sunrise = moment.unix(data.current.sunrise).utcOffset(data.timezone_offset / 60);
current.sunset = moment.unix(data.current.sunset).utcOffset(data.timezone_offset / 60); current.sunset = moment.unix(data.current.sunset).utcOffset(data.timezone_offset / 60);
current.temperature = data.current.temp; current.temperature = data.current.temp;
current.weatherType = this.convertWeatherType(data.current.weather[0].icon); current.weatherType = this.convertWeatherType(data.current.weather[0].icon);
current.humidity = data.current.humidity; current.humidity = data.current.humidity;
if (data.current.hasOwnProperty("rain") && !isNaN(data.current["rain"]["1h"])) { if (data.current.hasOwnProperty("rain") && !isNaN(data.current["rain"]["1h"])) {
if (this.config.units === "imperial") { current.rain = data.current["rain"]["1h"];
current.rain = data.current["rain"]["1h"] / 25.4;
} else {
current.rain = data.current["rain"]["1h"];
}
precip = true; precip = true;
} }
if (data.current.hasOwnProperty("snow") && !isNaN(data.current["snow"]["1h"])) { if (data.current.hasOwnProperty("snow") && !isNaN(data.current["snow"]["1h"])) {
if (this.config.units === "imperial") { current.snow = data.current["snow"]["1h"];
current.snow = data.current["snow"]["1h"] / 25.4;
} else {
current.snow = data.current["snow"]["1h"];
}
precip = true; precip = true;
} }
if (precip) { if (precip) {
current.precipitation = current.rain + current.snow; current.precipitationAmount = (current.rain ?? 0) + (current.snow ?? 0);
} }
current.feelsLikeTemp = data.current.feels_like; current.feelsLikeTemp = data.current.feels_like;
} }
@@ -342,27 +321,20 @@ WeatherProvider.register("openweathermap", {
weather.feelsLikeTemp = hour.feels_like; weather.feelsLikeTemp = hour.feels_like;
weather.humidity = hour.humidity; weather.humidity = hour.humidity;
weather.windSpeed = hour.wind_speed; weather.windSpeed = hour.wind_speed;
weather.windDirection = hour.wind_deg; weather.windFromDirection = hour.wind_deg;
weather.weatherType = this.convertWeatherType(hour.weather[0].icon); weather.weatherType = this.convertWeatherType(hour.weather[0].icon);
weather.precipitationProbability = hour.pop ? hour.pop * 100 : undefined;
precip = false; precip = false;
if (hour.hasOwnProperty("rain") && !isNaN(hour.rain["1h"])) { if (hour.hasOwnProperty("rain") && !isNaN(hour.rain["1h"])) {
if (this.config.units === "imperial") { weather.rain = hour.rain["1h"];
weather.rain = hour.rain["1h"] / 25.4;
} else {
weather.rain = hour.rain["1h"];
}
precip = true; precip = true;
} }
if (hour.hasOwnProperty("snow") && !isNaN(hour.snow["1h"])) { if (hour.hasOwnProperty("snow") && !isNaN(hour.snow["1h"])) {
if (this.config.units === "imperial") { weather.snow = hour.snow["1h"];
weather.snow = hour.snow["1h"] / 25.4;
} else {
weather.snow = hour.snow["1h"];
}
precip = true; precip = true;
} }
if (precip) { if (precip) {
weather.precipitation = weather.rain + weather.snow; weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
} }
hours.push(weather); hours.push(weather);
@@ -381,27 +353,20 @@ WeatherProvider.register("openweathermap", {
weather.maxTemperature = day.temp.max; weather.maxTemperature = day.temp.max;
weather.humidity = day.humidity; weather.humidity = day.humidity;
weather.windSpeed = day.wind_speed; weather.windSpeed = day.wind_speed;
weather.windDirection = day.wind_deg; weather.windFromDirection = day.wind_deg;
weather.weatherType = this.convertWeatherType(day.weather[0].icon); weather.weatherType = this.convertWeatherType(day.weather[0].icon);
weather.precipitationProbability = day.pop ? day.pop * 100 : undefined;
precip = false; precip = false;
if (!isNaN(day.rain)) { if (!isNaN(day.rain)) {
if (this.config.units === "imperial") { weather.rain = day.rain;
weather.rain = day.rain / 25.4;
} else {
weather.rain = day.rain;
}
precip = true; precip = true;
} }
if (!isNaN(day.snow)) { if (!isNaN(day.snow)) {
if (this.config.units === "imperial") { weather.snow = day.snow;
weather.snow = day.snow / 25.4;
} else {
weather.snow = day.snow;
}
precip = true; precip = true;
} }
if (precip) { if (precip) {
weather.precipitation = weather.rain + weather.snow; weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
} }
days.push(weather); days.push(weather);
@@ -448,8 +413,8 @@ WeatherProvider.register("openweathermap", {
getParams() { getParams() {
let params = "?"; let params = "?";
if (this.config.weatherEndpoint === "/onecall") { if (this.config.weatherEndpoint === "/onecall") {
params += "lat=" + this.config.lat; params += `lat=${this.config.lat}`;
params += "&lon=" + this.config.lon; params += `&lon=${this.config.lon}`;
if (this.config.type === "current") { if (this.config.type === "current") {
params += "&exclude=minutely,hourly,daily"; params += "&exclude=minutely,hourly,daily";
} else if (this.config.type === "hourly") { } else if (this.config.type === "hourly") {
@@ -460,23 +425,23 @@ WeatherProvider.register("openweathermap", {
params += "&exclude=minutely"; params += "&exclude=minutely";
} }
} else if (this.config.lat && this.config.lon) { } else if (this.config.lat && this.config.lon) {
params += "lat=" + this.config.lat + "&lon=" + this.config.lon; params += `lat=${this.config.lat}&lon=${this.config.lon}`;
} else if (this.config.locationID) { } else if (this.config.locationID) {
params += "id=" + this.config.locationID; params += `id=${this.config.locationID}`;
} else if (this.config.location) { } else if (this.config.location) {
params += "q=" + this.config.location; params += `q=${this.config.location}`;
} else if (this.firstEvent && this.firstEvent.geo) { } else if (this.firstEvent && this.firstEvent.geo) {
params += "lat=" + this.firstEvent.geo.lat + "&lon=" + this.firstEvent.geo.lon; params += `lat=${this.firstEvent.geo.lat}&lon=${this.firstEvent.geo.lon}`;
} else if (this.firstEvent && this.firstEvent.location) { } else if (this.firstEvent && this.firstEvent.location) {
params += "q=" + this.firstEvent.location; params += `q=${this.firstEvent.location}`;
} else { } else {
this.hide(this.config.animationSpeed, { lockString: this.identifier }); this.hide(this.config.animationSpeed, { lockString: this.identifier });
return; return;
} }
params += "&units=metric"; // WeatherProviders should use metric internally and use the units only for when displaying data params += "&units=metric"; // WeatherProviders should use metric internally and use the units only for when displaying data
params += "&lang=" + this.config.lang; params += `&lang=${this.config.lang}`;
params += "&APPID=" + this.config.apiKey; params += `&APPID=${this.config.apiKey}`;
return params; return params;
} }

View File

@@ -2,24 +2,23 @@
/* MagicMirror² /* MagicMirror²
* Module: Weather * Module: Weather
* Provider: Dark Sky * Provider: Pirate Weather
* *
* By Nicholas Hubbard https://github.com/nhubbard * Written by Nicholas Hubbard https://github.com/nhubbard for formerly Dark Sky Provider
* Modified by Karsten Hassel for Pirate Weather
* MIT Licensed * MIT Licensed
* *
* This class is a provider for Dark Sky. * This class is a provider for Pirate Weather, it is a replacement for Dark Sky (same api).
* Note that the Dark Sky API does not provide rainfall. Instead it provides
* snowfall and precipitation probability
*/ */
WeatherProvider.register("darksky", { WeatherProvider.register("pirateweather", {
// Set the name of the provider. // Set the name of the provider.
// Not strictly required, but helps for debugging. // Not strictly required, but helps for debugging.
providerName: "Dark Sky", providerName: "pirateweather",
// Set the default config properties that is specific to this provider // Set the default config properties that is specific to this provider
defaults: { defaults: {
useCorsProxy: true, useCorsProxy: true,
apiBase: "https://api.darksky.net", apiBase: "https://api.pirateweather.net",
weatherEndpoint: "/forecast", weatherEndpoint: "/forecast",
apiKey: "", apiKey: "",
lat: 0, lat: 0,
@@ -73,7 +72,7 @@ WeatherProvider.register("darksky", {
currentWeather.humidity = parseFloat(currentWeatherData.currently.humidity); currentWeather.humidity = parseFloat(currentWeatherData.currently.humidity);
currentWeather.temperature = parseFloat(currentWeatherData.currently.temperature); currentWeather.temperature = parseFloat(currentWeatherData.currently.temperature);
currentWeather.windSpeed = parseFloat(currentWeatherData.currently.windSpeed); currentWeather.windSpeed = parseFloat(currentWeatherData.currently.windSpeed);
currentWeather.windDirection = currentWeatherData.currently.windBearing; currentWeather.windFromDirection = currentWeatherData.currently.windBearing;
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.currently.icon); currentWeather.weatherType = this.convertWeatherType(currentWeatherData.currently.icon);
currentWeather.sunrise = moment.unix(currentWeatherData.daily.data[0].sunriseTime); currentWeather.sunrise = moment.unix(currentWeatherData.daily.data[0].sunriseTime);
currentWeather.sunset = moment.unix(currentWeatherData.daily.data[0].sunsetTime); currentWeather.sunset = moment.unix(currentWeatherData.daily.data[0].sunsetTime);
@@ -92,19 +91,21 @@ WeatherProvider.register("darksky", {
weather.maxTemperature = forecast.temperatureMax; weather.maxTemperature = forecast.temperatureMax;
weather.weatherType = this.convertWeatherType(forecast.icon); weather.weatherType = this.convertWeatherType(forecast.icon);
weather.snow = 0; weather.snow = 0;
weather.rain = 0;
// The API will return centimeters if units is 'si' and will return inches for 'us' let precip = 0;
// Note that the Dark Sky API does not provide rainfall.
// Instead it provides snowfall and precipitation probability
if (forecast.hasOwnProperty("precipAccumulation")) { if (forecast.hasOwnProperty("precipAccumulation")) {
if (this.config.units === "imperial" && !isNaN(forecast.precipAccumulation)) { precip = forecast.precipAccumulation * 10;
weather.snow = forecast.precipAccumulation;
} else if (!isNaN(forecast.precipAccumulation)) {
weather.snow = forecast.precipAccumulation * 10;
}
} }
weather.precipitation = weather.snow; weather.precipitationAmount = precip;
if (forecast.hasOwnProperty("precipType")) {
if (forecast.precipType === "snow") {
weather.snow = precip;
} else {
weather.rain = precip;
}
}
days.push(weather); days.push(weather);
} }
@@ -112,7 +113,7 @@ WeatherProvider.register("darksky", {
return days; return days;
}, },
// Map icons from Dark Sky to our icons. // Map icons from Pirate Weather to our icons.
convertWeatherType(weatherType) { convertWeatherType(weatherType) {
const weatherTypes = { const weatherTypes = {
"clear-day": "day-sunny", "clear-day": "day-sunny",

View File

@@ -33,7 +33,7 @@ WeatherProvider.register("smhi", {
this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`); this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`);
this.setCurrentWeather(weatherObject); this.setCurrentWeather(weatherObject);
}) })
.catch((error) => Log.error("Could not load data: " + error.message)) .catch((error) => Log.error(`Could not load data: ${error.message}`))
.finally(() => this.updateAvailable()); .finally(() => this.updateAvailable());
}, },
@@ -48,7 +48,7 @@ WeatherProvider.register("smhi", {
this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`); this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`);
this.setWeatherForecast(weatherObjects); this.setWeatherForecast(weatherObjects);
}) })
.catch((error) => Log.error("Could not load data: " + error.message)) .catch((error) => Log.error(`Could not load data: ${error.message}`))
.finally(() => this.updateAvailable()); .finally(() => this.updateAvailable());
}, },
@@ -63,7 +63,7 @@ WeatherProvider.register("smhi", {
this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`); this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`);
this.setWeatherHourly(weatherObjects); this.setWeatherHourly(weatherObjects);
}) })
.catch((error) => Log.error("Could not load data: " + error.message)) .catch((error) => Log.error(`Could not load data: ${error.message}`))
.finally(() => this.updateAvailable()); .finally(() => this.updateAvailable());
}, },
@@ -75,7 +75,7 @@ WeatherProvider.register("smhi", {
setConfig(config) { setConfig(config) {
this.config = config; this.config = config;
if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) === -1) { if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) === -1) {
Log.log("invalid or not set: " + config.precipitationValue); Log.log(`invalid or not set: ${config.precipitationValue}`);
config.precipitationValue = this.defaults.precipitationValue; config.precipitationValue = this.defaults.precipitationValue;
} }
}, },
@@ -145,7 +145,7 @@ WeatherProvider.register("smhi", {
currentWeather.humidity = this.paramValue(weatherData, "r"); currentWeather.humidity = this.paramValue(weatherData, "r");
currentWeather.temperature = this.paramValue(weatherData, "t"); currentWeather.temperature = this.paramValue(weatherData, "t");
currentWeather.windSpeed = this.paramValue(weatherData, "ws"); currentWeather.windSpeed = this.paramValue(weatherData, "ws");
currentWeather.windDirection = this.paramValue(weatherData, "wd"); currentWeather.windFromDirection = this.paramValue(weatherData, "wd");
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), currentWeather.isDayTime()); currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), currentWeather.isDayTime());
currentWeather.feelsLikeTemp = this.calculateApparentTemperature(weatherData); currentWeather.feelsLikeTemp = this.calculateApparentTemperature(weatherData);
@@ -157,19 +157,19 @@ WeatherProvider.register("smhi", {
// 0 = No precipitation // 0 = No precipitation
case 1: // Snow case 1: // Snow
currentWeather.snow += precipitationValue; currentWeather.snow += precipitationValue;
currentWeather.precipitation += precipitationValue; currentWeather.precipitationAmount += precipitationValue;
break; break;
case 2: // Snow and rain, treat it as 50/50 snow and rain case 2: // Snow and rain, treat it as 50/50 snow and rain
currentWeather.snow += precipitationValue / 2; currentWeather.snow += precipitationValue / 2;
currentWeather.rain += precipitationValue / 2; currentWeather.rain += precipitationValue / 2;
currentWeather.precipitation += precipitationValue; currentWeather.precipitationAmount += precipitationValue;
break; break;
case 3: // Rain case 3: // Rain
case 4: // Drizzle case 4: // Drizzle
case 5: // Freezing rain case 5: // Freezing rain
case 6: // Freezing drizzle case 6: // Freezing drizzle
currentWeather.rain += precipitationValue; currentWeather.rain += precipitationValue;
currentWeather.precipitation += precipitationValue; currentWeather.precipitationAmount += precipitationValue;
break; break;
} }
@@ -202,7 +202,7 @@ WeatherProvider.register("smhi", {
currentWeather.maxTemperature = -Infinity; currentWeather.maxTemperature = -Infinity;
currentWeather.snow = 0; currentWeather.snow = 0;
currentWeather.rain = 0; currentWeather.rain = 0;
currentWeather.precipitation = 0; currentWeather.precipitationAmount = 0;
result.push(currentWeather); result.push(currentWeather);
} }
@@ -221,7 +221,7 @@ WeatherProvider.register("smhi", {
currentWeather.maxTemperature = Math.max(currentWeather.maxTemperature, weatherObject.temperature); currentWeather.maxTemperature = Math.max(currentWeather.maxTemperature, weatherObject.temperature);
currentWeather.snow += weatherObject.snow; currentWeather.snow += weatherObject.snow;
currentWeather.rain += weatherObject.rain; currentWeather.rain += weatherObject.rain;
currentWeather.precipitation += weatherObject.precipitation; currentWeather.precipitationAmount += weatherObject.precipitationAmount;
} }
return result; return result;

View File

@@ -100,9 +100,9 @@ WeatherProvider.register("ukmetoffice", {
currentWeather.humidity = rep.H; currentWeather.humidity = rep.H;
currentWeather.temperature = rep.T; currentWeather.temperature = rep.T;
currentWeather.feelsLikeTemp = rep.F; currentWeather.feelsLikeTemp = rep.F;
currentWeather.precipitation = parseInt(rep.Pp); currentWeather.precipitationProbability = parseInt(rep.Pp);
currentWeather.windSpeed = WeatherUtils.convertWindToMetric(rep.S); currentWeather.windSpeed = WeatherUtils.convertWindToMetric(rep.S);
currentWeather.windDirection = WeatherUtils.convertWindDirection(rep.D); currentWeather.windFromDirection = WeatherUtils.convertWindDirection(rep.D);
currentWeather.weatherType = this.convertWeatherType(rep.W); currentWeather.weatherType = this.convertWeatherType(rep.W);
} }
} }
@@ -138,7 +138,7 @@ WeatherProvider.register("ukmetoffice", {
weather.minTemperature = period.Rep[1].Nm; weather.minTemperature = period.Rep[1].Nm;
weather.maxTemperature = period.Rep[0].Dm; weather.maxTemperature = period.Rep[0].Dm;
weather.weatherType = this.convertWeatherType(period.Rep[0].W); weather.weatherType = this.convertWeatherType(period.Rep[0].W);
weather.precipitation = parseInt(period.Rep[0].PPd); weather.precipitationProbability = parseInt(period.Rep[0].PPd);
days.push(weather); days.push(weather);
} }
@@ -195,8 +195,8 @@ WeatherProvider.register("ukmetoffice", {
*/ */
getParams(forecastType) { getParams(forecastType) {
let params = "?"; let params = "?";
params += "res=" + forecastType; params += `res=${forecastType}`;
params += "&key=" + this.config.apiKey; params += `&key=${this.config.apiKey}`;
return params; return params;
} }
}); });

View File

@@ -55,9 +55,9 @@ WeatherProvider.register("ukmetofficedatahub", {
// Build URL with query strings according to DataHub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api) // Build URL with query strings according to DataHub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
getUrl(forecastType) { getUrl(forecastType) {
let queryStrings = "?"; let queryStrings = "?";
queryStrings += "latitude=" + this.config.lat; queryStrings += `latitude=${this.config.lat}`;
queryStrings += "&longitude=" + this.config.lon; queryStrings += `&longitude=${this.config.lon}`;
queryStrings += "&includeLocationName=" + true; queryStrings += `&includeLocationName=${true}`;
// Return URL, making sure there is a trailing "/" in the base URL. // Return URL, making sure there is a trailing "/" in the base URL.
return this.config.apiBase + (this.config.apiBase.endsWith("/") ? "" : "/") + forecastType + queryStrings; return this.config.apiBase + (this.config.apiBase.endsWith("/") ? "" : "/") + forecastType + queryStrings;
@@ -104,7 +104,7 @@ WeatherProvider.register("ukmetofficedatahub", {
}) })
// Catch any error(s) // Catch any error(s)
.catch((error) => Log.error("Could not load data: " + error.message)) .catch((error) => Log.error(`Could not load data: ${error.message}`))
// Let the module know there is data available // Let the module know there is data available
.finally(() => this.updateAvailable()); .finally(() => this.updateAvailable());
@@ -126,7 +126,7 @@ WeatherProvider.register("ukmetofficedatahub", {
if (nowUtc.isSameOrAfter(forecastTime) && nowUtc.isBefore(moment(forecastTime.add(1, "h")))) { if (nowUtc.isSameOrAfter(forecastTime) && nowUtc.isBefore(moment(forecastTime.add(1, "h")))) {
currentWeather.date = forecastTime; currentWeather.date = forecastTime;
currentWeather.windSpeed = forecastDataHours[hour].windSpeed10m; currentWeather.windSpeed = forecastDataHours[hour].windSpeed10m;
currentWeather.windDirection = forecastDataHours[hour].windDirectionFrom10m; currentWeather.windFromDirection = forecastDataHours[hour].windDirectionFrom10m;
currentWeather.temperature = forecastDataHours[hour].screenTemperature; currentWeather.temperature = forecastDataHours[hour].screenTemperature;
currentWeather.minTemperature = forecastDataHours[hour].minScreenAirTemp; currentWeather.minTemperature = forecastDataHours[hour].minScreenAirTemp;
currentWeather.maxTemperature = forecastDataHours[hour].maxScreenAirTemp; currentWeather.maxTemperature = forecastDataHours[hour].maxScreenAirTemp;
@@ -134,7 +134,7 @@ WeatherProvider.register("ukmetofficedatahub", {
currentWeather.humidity = forecastDataHours[hour].screenRelativeHumidity; currentWeather.humidity = forecastDataHours[hour].screenRelativeHumidity;
currentWeather.rain = forecastDataHours[hour].totalPrecipAmount; currentWeather.rain = forecastDataHours[hour].totalPrecipAmount;
currentWeather.snow = forecastDataHours[hour].totalSnowAmount; currentWeather.snow = forecastDataHours[hour].totalSnowAmount;
currentWeather.precipitation = forecastDataHours[hour].probOfPrecipitation; currentWeather.precipitationProbability = forecastDataHours[hour].probOfPrecipitation;
currentWeather.feelsLikeTemp = forecastDataHours[hour].feelsLikeTemperature; currentWeather.feelsLikeTemp = forecastDataHours[hour].feelsLikeTemperature;
// Pass on full details, so they can be used in custom templates // Pass on full details, so they can be used in custom templates
@@ -173,7 +173,7 @@ WeatherProvider.register("ukmetofficedatahub", {
}) })
// Catch any error(s) // Catch any error(s)
.catch((error) => Log.error("Could not load data: " + error.message)) .catch((error) => Log.error(`Could not load data: ${error.message}`))
// Let the module know there is new data available // Let the module know there is new data available
.finally(() => this.updateAvailable()); .finally(() => this.updateAvailable());
@@ -204,9 +204,9 @@ WeatherProvider.register("ukmetofficedatahub", {
// Using daytime forecast values // Using daytime forecast values
forecastWeather.windSpeed = forecastDataDays[day].midday10MWindSpeed; forecastWeather.windSpeed = forecastDataDays[day].midday10MWindSpeed;
forecastWeather.windDirection = forecastDataDays[day].midday10MWindDirection; forecastWeather.windFromDirection = forecastDataDays[day].midday10MWindDirection;
forecastWeather.weatherType = this.convertWeatherType(forecastDataDays[day].daySignificantWeatherCode); forecastWeather.weatherType = this.convertWeatherType(forecastDataDays[day].daySignificantWeatherCode);
forecastWeather.precipitation = forecastDataDays[day].dayProbabilityOfPrecipitation; forecastWeather.precipitationProbability = forecastDataDays[day].dayProbabilityOfPrecipitation;
forecastWeather.temperature = forecastDataDays[day].dayMaxScreenTemperature; forecastWeather.temperature = forecastDataDays[day].dayMaxScreenTemperature;
forecastWeather.humidity = forecastDataDays[day].middayRelativeHumidity; forecastWeather.humidity = forecastDataDays[day].middayRelativeHumidity;
forecastWeather.rain = forecastDataDays[day].dayProbabilityOfRain; forecastWeather.rain = forecastDataDays[day].dayProbabilityOfRain;

View File

@@ -55,7 +55,7 @@ WeatherProvider.register("weatherbit", {
const forecast = this.generateWeatherObjectsFromForecast(data.data); const forecast = this.generateWeatherObjectsFromForecast(data.data);
this.setWeatherForecast(forecast); this.setWeatherForecast(forecast);
this.fetchedLocationName = data.city_name + ", " + data.state_code; this.fetchedLocationName = `${data.city_name}, ${data.state_code}`;
}) })
.catch(function (request) { .catch(function (request) {
Log.error("Could not load data ... ", request); Log.error("Could not load data ... ", request);
@@ -106,12 +106,12 @@ WeatherProvider.register("weatherbit", {
currentWeather.humidity = parseFloat(currentWeatherData.data[0].rh); currentWeather.humidity = parseFloat(currentWeatherData.data[0].rh);
currentWeather.temperature = parseFloat(currentWeatherData.data[0].temp); currentWeather.temperature = parseFloat(currentWeatherData.data[0].temp);
currentWeather.windSpeed = parseFloat(currentWeatherData.data[0].wind_spd); currentWeather.windSpeed = parseFloat(currentWeatherData.data[0].wind_spd);
currentWeather.windDirection = currentWeatherData.data[0].wind_dir; currentWeather.windFromDirection = currentWeatherData.data[0].wind_dir;
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.data[0].weather.icon); currentWeather.weatherType = this.convertWeatherType(currentWeatherData.data[0].weather.icon);
currentWeather.sunrise = moment(currentWeatherData.data[0].sunrise, "HH:mm").add(tzOffset, "m"); currentWeather.sunrise = moment(currentWeatherData.data[0].sunrise, "HH:mm").add(tzOffset, "m");
currentWeather.sunset = moment(currentWeatherData.data[0].sunset, "HH:mm").add(tzOffset, "m"); currentWeather.sunset = moment(currentWeatherData.data[0].sunset, "HH:mm").add(tzOffset, "m");
this.fetchedLocationName = currentWeatherData.data[0].city_name + ", " + currentWeatherData.data[0].state_code; this.fetchedLocationName = `${currentWeatherData.data[0].city_name}, ${currentWeatherData.data[0].state_code}`;
return currentWeather; return currentWeather;
}, },
@@ -125,7 +125,8 @@ WeatherProvider.register("weatherbit", {
weather.date = moment(forecast.datetime, "YYYY-MM-DD"); weather.date = moment(forecast.datetime, "YYYY-MM-DD");
weather.minTemperature = forecast.min_temp; weather.minTemperature = forecast.min_temp;
weather.maxTemperature = forecast.max_temp; weather.maxTemperature = forecast.max_temp;
weather.precipitation = forecast.precip; weather.precipitationAmount = forecast.precip;
weather.precipitationProbability = forecast.pop;
weather.weatherType = this.convertWeatherType(forecast.weather.icon); weather.weatherType = this.convertWeatherType(forecast.weather.icon);
days.push(weather); days.push(weather);

View File

@@ -32,7 +32,7 @@ WeatherProvider.register("weatherflow", {
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.windSpeed = WeatherUtils.convertWindToMs(data.current_conditions.wind_avg); currentWeather.windSpeed = WeatherUtils.convertWindToMs(data.current_conditions.wind_avg);
currentWeather.windDirection = data.current_conditions.wind_direction; currentWeather.windFromDirection = data.current_conditions.wind_direction;
currentWeather.weatherType = data.forecast.daily[0].icon; currentWeather.weatherType = data.forecast.daily[0].icon;
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);
@@ -55,6 +55,7 @@ WeatherProvider.register("weatherflow", {
weather.date = moment.unix(forecast.day_start_local); weather.date = moment.unix(forecast.day_start_local);
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.weatherType = forecast.icon; weather.weatherType = forecast.icon;
weather.snow = 0; weather.snow = 0;

View File

@@ -129,10 +129,10 @@ WeatherProvider.register("weathergov", {
// points URL did not respond with usable data. // points URL did not respond with usable data.
return; return;
} }
this.fetchedLocationName = data.properties.relativeLocation.properties.city + ", " + data.properties.relativeLocation.properties.state; this.fetchedLocationName = `${data.properties.relativeLocation.properties.city}, ${data.properties.relativeLocation.properties.state}`;
Log.log("Forecast location is " + this.fetchedLocationName); Log.log(`Forecast location is ${this.fetchedLocationName}`);
this.forecastURL = data.properties.forecast + "?units=si"; this.forecastURL = `${data.properties.forecast}?units=si`;
this.forecastHourlyURL = data.properties.forecastHourly + "?units=si"; this.forecastHourlyURL = `${data.properties.forecastHourly}?units=si`;
this.forecastGridDataURL = data.properties.forecastGridData; this.forecastGridDataURL = data.properties.forecastGridData;
this.observationStationsURL = data.properties.observationStations; this.observationStationsURL = data.properties.observationStations;
// with this URL, we chain another promise for the station obs URL // with this URL, we chain another promise for the station obs URL
@@ -143,7 +143,7 @@ WeatherProvider.register("weathergov", {
// obs station URL did not respond with usable data. // obs station URL did not respond with usable data.
return; return;
} }
this.stationObsURL = obsData.features[0].id + "/observations/latest"; this.stationObsURL = `${obsData.features[0].id}/observations/latest`;
}) })
.catch((err) => { .catch((err) => {
Log.error(err); Log.error(err);
@@ -179,9 +179,9 @@ WeatherProvider.register("weathergov", {
} else { } else {
weather.windSpeed = forecast.windSpeed.slice(0, forecast.windSpeed.search(" ")); weather.windSpeed = forecast.windSpeed.slice(0, forecast.windSpeed.search(" "));
} }
weather.windDirection = this.convertWindDirection(forecast.windDirection); weather.windSpeed = WeatherUtils.convertWindToMs(weather.windSpeed);
weather.windFromDirection = forecast.windDirection;
weather.temperature = forecast.temperature; weather.temperature = forecast.temperature;
weather.tempUnits = forecast.temperatureUnit;
// use the forecast isDayTime attribute to help build the weatherType label // use the forecast isDayTime attribute to help build the weatherType label
weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime); weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime);
@@ -206,13 +206,11 @@ WeatherProvider.register("weathergov", {
currentWeather.date = moment(currentWeatherData.timestamp); currentWeather.date = moment(currentWeatherData.timestamp);
currentWeather.temperature = currentWeatherData.temperature.value; currentWeather.temperature = currentWeatherData.temperature.value;
currentWeather.windSpeed = WeatherUtils.convertWindToMs(currentWeatherData.windSpeed.value); currentWeather.windSpeed = WeatherUtils.convertWindToMs(currentWeatherData.windSpeed.value);
currentWeather.windDirection = currentWeatherData.windDirection.value; currentWeather.windFromDirection = currentWeatherData.windDirection.value;
currentWeather.minTemperature = currentWeatherData.minTemperatureLast24Hours.value; currentWeather.minTemperature = currentWeatherData.minTemperatureLast24Hours.value;
currentWeather.maxTemperature = currentWeatherData.maxTemperatureLast24Hours.value; currentWeather.maxTemperature = currentWeatherData.maxTemperatureLast24Hours.value;
currentWeather.humidity = Math.round(currentWeatherData.relativeHumidity.value); currentWeather.humidity = Math.round(currentWeatherData.relativeHumidity.value);
currentWeather.rain = null; currentWeather.precipitationAmount = currentWeatherData.precipitationLastHour.value;
currentWeather.snow = null;
currentWeather.precipitation = this.convertLength(currentWeatherData.precipitationLastHour.value);
if (currentWeatherData.heatIndex.value !== null) { if (currentWeatherData.heatIndex.value !== null) {
currentWeather.feelsLikeTemp = currentWeatherData.heatIndex.value; currentWeather.feelsLikeTemp = currentWeatherData.heatIndex.value;
} else if (currentWeatherData.windChill.value !== null) { } else if (currentWeatherData.windChill.value !== null) {
@@ -240,6 +238,8 @@ WeatherProvider.register("weathergov", {
* fetch forecast information for daily forecast. * fetch forecast information for daily forecast.
*/ */
fetchForecastDaily(forecasts) { fetchForecastDaily(forecasts) {
const precipitationProbabilityRegEx = "Chance of precipitation is ([0-9]+?)%";
// initial variable declaration // initial variable declaration
const days = []; const days = [];
// variables for temperature range and rain // variables for temperature range and rain
@@ -248,7 +248,6 @@ WeatherProvider.register("weathergov", {
// variable for date // variable for date
let date = ""; let date = "";
let weather = new WeatherObject(); let weather = new WeatherObject();
weather.precipitation = 0;
for (const forecast of forecasts) { for (const forecast of forecasts) {
if (date !== moment(forecast.startTime).format("YYYY-MM-DD")) { if (date !== moment(forecast.startTime).format("YYYY-MM-DD")) {
@@ -263,7 +262,8 @@ WeatherProvider.register("weathergov", {
minTemp = []; minTemp = [];
maxTemp = []; maxTemp = [];
weather.precipitation = 0; const precipitation = new RegExp(precipitationProbabilityRegEx, "g").exec(forecast.detailedForecast);
if (precipitation) weather.precipitationProbability = precipitation[1];
// set new date // set new date
date = moment(forecast.startTime).format("YYYY-MM-DD"); date = moment(forecast.startTime).format("YYYY-MM-DD");
@@ -295,18 +295,6 @@ WeatherProvider.register("weathergov", {
return days.slice(1); return days.slice(1);
}, },
/*
* Unit conversions
*/
// conversion to inches
convertLength(meters) {
if (this.config.units === "imperial") {
return meters * 39.3701;
} else {
return meters;
}
},
/* /*
* Convert the icons to a more usable name. * Convert the icons to a more usable name.
*/ */

View File

@@ -7,7 +7,7 @@
* By Magnus Marthinsen * By Magnus Marthinsen
* MIT Licensed * MIT Licensed
* *
* This class is a provider for Yr.no, a norwegian sweather service. * This class is a provider for Yr.no, a norwegian weather service.
* *
* Terms of service: https://developer.yr.no/doc/TermsOfService/ * Terms of service: https://developer.yr.no/doc/TermsOfService/
*/ */
@@ -47,7 +47,7 @@ WeatherProvider.register("yr", {
const getRequests = [this.getWeatherData(), this.getStellarData()]; const getRequests = [this.getWeatherData(), this.getStellarData()];
const [weatherData, stellarData] = await Promise.all(getRequests); const [weatherData, stellarData] = await Promise.all(getRequests);
if (!stellarData) { if (!stellarData) {
Log.warn("No stelar data available."); Log.warn("No stellar data available.");
} }
if (!weatherData.properties.timeseries || !weatherData.properties.timeseries[0]) { if (!weatherData.properties.timeseries || !weatherData.properties.timeseries[0]) {
Log.error("No weather data available."); Log.error("No weather data available.");
@@ -65,7 +65,8 @@ WeatherProvider.register("yr", {
} }
const forecastXHours = this.getForecastForXHoursFrom(forecast.data); const forecastXHours = this.getForecastForXHoursFrom(forecast.data);
forecast.weatherType = this.convertWeatherType(forecastXHours.summary.symbol_code, forecast.time); forecast.weatherType = this.convertWeatherType(forecastXHours.summary.symbol_code, forecast.time);
forecast.precipitation = forecastXHours.details?.precipitation_amount; forecast.precipitationAmount = forecastXHours.details?.precipitation_amount;
forecast.precipitationProbability = forecastXHours.details?.probability_of_precipitation;
forecast.minTemperature = forecastXHours.details?.air_temperature_min; forecast.minTemperature = forecastXHours.details?.air_temperature_min;
forecast.maxTemperature = forecastXHours.details?.air_temperature_max; forecast.maxTemperature = forecastXHours.details?.air_temperature_max;
return this.getWeatherDataFrom(forecast, stellarData, weatherData.properties.meta.units); return this.getWeatherDataFrom(forecast, stellarData, weatherData.properties.meta.units);
@@ -251,12 +252,12 @@ WeatherProvider.register("yr", {
this.cacheStellarData(stellarData); this.cacheStellarData(stellarData);
resolve(stellarData); resolve(stellarData);
} else { } else {
reject("No stellar data returned from Yr for " + tomorrow); reject(`No stellar data returned from Yr for ${tomorrow}`);
} }
}) })
.catch((err) => { .catch((err) => {
Log.error(err); Log.error(err);
reject("Unable to get stellar data from Yr for " + tomorrow); reject(`Unable to get stellar data from Yr for ${tomorrow}`);
}) })
.finally(() => { .finally(() => {
localStorage.removeItem("yrIsFetchingStellarData"); localStorage.removeItem("yrIsFetchingStellarData");
@@ -274,7 +275,7 @@ WeatherProvider.register("yr", {
this.cacheStellarData(stellarData); this.cacheStellarData(stellarData);
resolve(stellarData); resolve(stellarData);
} else { } else {
Log.error("Something went wrong when fetching stellar data. Responses: " + stellarData); Log.error(`Something went wrong when fetching stellar data. Responses: ${stellarData}`);
reject(stellarData); reject(stellarData);
} }
}) })
@@ -358,19 +359,20 @@ WeatherProvider.register("yr", {
}, },
getWeatherDataFrom(forecast, stellarData, units) { getWeatherDataFrom(forecast, stellarData, units) {
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh); const weather = new WeatherObject();
const stellarTimesToday = stellarData?.today ? this.getStellarTimesFrom(stellarData.today, moment().format("YYYY-MM-DD")) : undefined; const stellarTimesToday = stellarData?.today ? this.getStellarTimesFrom(stellarData.today, moment().format("YYYY-MM-DD")) : undefined;
const stellarTimesTomorrow = stellarData?.tomorrow ? this.getStellarTimesFrom(stellarData.tomorrow, moment().add(1, "days").format("YYYY-MM-DD")) : undefined; const stellarTimesTomorrow = stellarData?.tomorrow ? this.getStellarTimesFrom(stellarData.tomorrow, moment().add(1, "days").format("YYYY-MM-DD")) : undefined;
weather.date = moment(forecast.time); weather.date = moment(forecast.time);
weather.windSpeed = forecast.data.instant.details.wind_speed; weather.windSpeed = forecast.data.instant.details.wind_speed;
weather.windDirection = (forecast.data.instant.details.wind_from_direction + 180) % 360; weather.windFromDirection = forecast.data.instant.details.wind_from_direction;
weather.temperature = forecast.data.instant.details.air_temperature; weather.temperature = forecast.data.instant.details.air_temperature;
weather.minTemperature = forecast.minTemperature; weather.minTemperature = forecast.minTemperature;
weather.maxTemperature = forecast.maxTemperature; weather.maxTemperature = forecast.maxTemperature;
weather.weatherType = forecast.weatherType; weather.weatherType = forecast.weatherType;
weather.humidity = forecast.data.instant.details.relative_humidity; weather.humidity = forecast.data.instant.details.relative_humidity;
weather.precipitation = forecast.precipitation; weather.precipitationAmount = forecast.precipitationAmount;
weather.precipitationProbability = forecast.precipitationProbability;
weather.precipitationUnits = units.precipitation_amount; weather.precipitationUnits = units.precipitation_amount;
if (stellarTimesToday) { if (stellarTimesToday) {
@@ -530,7 +532,7 @@ WeatherProvider.register("yr", {
return; return;
} }
if (!stellarData) { if (!stellarData) {
Log.warn("No stelar data available."); Log.warn("No stellar data available.");
} }
let forecasts; let forecasts;
switch (type) { switch (type) {
@@ -554,7 +556,8 @@ WeatherProvider.register("yr", {
for (const forecast of weatherData.properties.timeseries) { for (const forecast of weatherData.properties.timeseries) {
forecast.symbol = forecast.data.next_1_hours?.summary?.symbol_code; forecast.symbol = forecast.data.next_1_hours?.summary?.symbol_code;
forecast.precipitation = 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.minTemperature = forecast.data.next_1_hours?.details?.air_temperature_min; forecast.minTemperature = forecast.data.next_1_hours?.details?.air_temperature_min;
forecast.maxTemperature = forecast.data.next_1_hours?.details?.air_temperature_max; forecast.maxTemperature = forecast.data.next_1_hours?.details?.air_temperature_max;
forecast.weatherType = this.convertWeatherType(forecast.symbol, forecast.time); forecast.weatherType = this.convertWeatherType(forecast.symbol, forecast.time);
@@ -599,7 +602,8 @@ WeatherProvider.register("yr", {
const forecastXHours = forecast.data.next_12_hours ?? forecast.data.next_6_hours ?? forecast.data.next_1_hours; const forecastXHours = forecast.data.next_12_hours ?? forecast.data.next_6_hours ?? forecast.data.next_1_hours;
if (forecastXHours) { if (forecastXHours) {
forecast.symbol = forecastXHours.summary?.symbol_code; forecast.symbol = forecastXHours.summary?.symbol_code;
forecast.precipitation = forecastXHours.details?.precipitation_amount; forecast.precipitationAmount = forecastXHours.details?.precipitation_amount ?? forecast.data.next_6_hours?.details?.precipitation_amount; // 6 hours is likely to have precipitation amount even if 12 hours does not
forecast.precipitationProbability = forecastXHours.details?.probability_of_precipitation;
forecast.minTemperature = minTemperature; forecast.minTemperature = minTemperature;
forecast.maxTemperature = maxTemperature; forecast.maxTemperature = maxTemperature;

View File

@@ -6,7 +6,7 @@
transform: translate(0, -3px); transform: translate(0, -3px);
} }
.weather .humidityIcon { .weather .humidity-icon {
padding-right: 4px; padding-right: 4px;
} }
@@ -29,7 +29,8 @@
padding-right: 0; padding-right: 0;
} }
.weather .precipitation { .weather .precipitation-amount,
.weather .precipitation-prob {
padding-left: 20px; padding-left: 20px;
padding-right: 0; padding-right: 0;
} }

View File

@@ -12,23 +12,26 @@ Module.register("weather", {
weatherProvider: "openweathermap", weatherProvider: "openweathermap",
roundTemp: false, roundTemp: false,
type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint) type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint)
lang: config.language,
units: config.units, units: config.units,
tempUnits: config.units, tempUnits: config.units,
windUnits: config.units, windUnits: config.units,
timeFormat: config.timeFormat,
updateInterval: 10 * 60 * 1000, // every 10 minutes updateInterval: 10 * 60 * 1000, // every 10 minutes
animationSpeed: 1000, animationSpeed: 1000,
timeFormat: config.timeFormat, showFeelsLike: true,
showHumidity: false,
showIndoorHumidity: false,
showIndoorTemperature: false,
showPeriod: true, showPeriod: true,
showPeriodUpper: false, showPeriodUpper: false,
showPrecipitationAmount: false,
showPrecipitationProbability: false,
showSun: true,
showWindDirection: true, showWindDirection: true,
showWindDirectionAsArrow: false, showWindDirectionAsArrow: false,
lang: config.language,
showHumidity: false,
showSun: true,
degreeLabel: false, degreeLabel: false,
decimalSymbol: ".", decimalSymbol: ".",
showIndoorTemperature: false,
showIndoorHumidity: false,
maxNumberOfDays: 5, maxNumberOfDays: 5,
maxEntries: 5, maxEntries: 5,
ignoreToday: false, ignoreToday: false,
@@ -39,10 +42,9 @@ Module.register("weather", {
calendarClass: "calendar", calendarClass: "calendar",
tableClass: "small", tableClass: "small",
onlyTemp: false, onlyTemp: false,
showPrecipitationAmount: false,
colored: false, colored: false,
showFeelsLike: true, absoluteDates: false,
absoluteDates: false hourlyForecastIncrements: 1
}, },
// Module properties. // Module properties.
@@ -58,13 +60,13 @@ Module.register("weather", {
// Return the scripts that are necessary for the weather module. // Return the scripts that are necessary for the weather module.
getScripts: function () { getScripts: function () {
return ["moment.js", this.file("../utils.js"), "weatherutils.js", "weatherprovider.js", "weatherobject.js", "suncalc.js", this.file("providers/" + this.config.weatherProvider.toLowerCase() + ".js")]; return ["moment.js", this.file("../utils.js"), "weatherutils.js", "weatherprovider.js", "weatherobject.js", "suncalc.js", this.file(`providers/${this.config.weatherProvider.toLowerCase()}.js`)];
}, },
// Override getHeader method. // Override getHeader method.
getHeader: function () { getHeader: function () {
if (this.config.appendLocationNameToHeader && this.weatherProvider) { if (this.config.appendLocationNameToHeader && this.weatherProvider) {
if (this.data.header) return this.data.header + " " + this.weatherProvider.fetchedLocation(); if (this.data.header) return `${this.data.header} ${this.weatherProvider.fetchedLocation()}`;
else return this.weatherProvider.fetchedLocation(); else return this.weatherProvider.fetchedLocation();
} }
@@ -137,13 +139,17 @@ Module.register("weather", {
// Add all the data to the template. // Add all the data to the template.
getTemplateData: function () { getTemplateData: function () {
const forecast = this.weatherProvider.weatherForecast(); const currentData = this.weatherProvider.currentWeather();
const forecastData = this.weatherProvider.weatherForecast();
// Skip some hourly forecast entries if configured
const hourlyData = this.weatherProvider.weatherHourly()?.filter((e, i) => (i + 1) % this.config.hourlyForecastIncrements === this.config.hourlyForecastIncrements - 1);
return { return {
config: this.config, config: this.config,
current: this.weatherProvider.currentWeather(), current: currentData,
forecast: forecast, forecast: forecastData,
hourly: this.weatherProvider.weatherHourly(), hourly: hourlyData,
indoor: { indoor: {
humidity: this.indoorHumidity, humidity: this.indoorHumidity,
temperature: this.indoorTemperature temperature: this.indoorTemperature
@@ -225,9 +231,9 @@ Module.register("weather", {
this.nunjucksEnvironment().addFilter( this.nunjucksEnvironment().addFilter(
"unit", "unit",
function (value, type) { function (value, type, valueUnit) {
if (type === "temperature") { if (type === "temperature") {
value = this.roundValue(WeatherUtils.convertTemp(value, this.config.tempUnits)) + "°"; value = `${this.roundValue(WeatherUtils.convertTemp(value, this.config.tempUnits))}°`;
if (this.config.degreeLabel) { if (this.config.degreeLabel) {
if (this.config.tempUnits === "metric") { if (this.config.tempUnits === "metric") {
value += "C"; value += "C";
@@ -241,11 +247,7 @@ Module.register("weather", {
if (value === null || isNaN(value) || value === 0 || value.toFixed(2) === "0.00") { if (value === null || isNaN(value) || value === 0 || value.toFixed(2) === "0.00") {
value = ""; value = "";
} else { } else {
if (this.config.weatherProvider === "ukmetoffice" || this.config.weatherProvider === "ukmetofficedatahub") { value = WeatherUtils.convertPrecipitationUnit(value, valueUnit, this.config.units);
value += "%";
} else {
value = `${value.toFixed(2)} ${this.config.units === "imperial" ? "in" : "mm"}`;
}
} }
} else if (type === "humidity") { } else if (type === "humidity") {
value += "%"; value += "%";

View File

@@ -11,6 +11,10 @@
* Currently this is focused on the information which is necessary for the current weather. * Currently this is focused on the information which is necessary for the current weather.
* As soon as we start implementing the forecast, mode properties will be added. * As soon as we start implementing the forecast, mode properties will be added.
*/ */
/**
* @external Moment
*/
class WeatherObject { class WeatherObject {
/** /**
* Constructor for a WeatherObject * Constructor for a WeatherObject
@@ -18,7 +22,7 @@ class WeatherObject {
constructor() { constructor() {
this.date = null; this.date = null;
this.windSpeed = null; this.windSpeed = null;
this.windDirection = null; this.windFromDirection = null;
this.sunrise = null; this.sunrise = null;
this.sunset = null; this.sunset = null;
this.temperature = null; this.temperature = null;
@@ -26,77 +30,65 @@ class WeatherObject {
this.maxTemperature = null; this.maxTemperature = null;
this.weatherType = null; this.weatherType = null;
this.humidity = null; this.humidity = null;
this.rain = null; this.precipitationAmount = null;
this.snow = null;
this.precipitation = null;
this.precipitationUnits = null; this.precipitationUnits = null;
this.precipitationProbability = null;
this.feelsLikeTemp = null; this.feelsLikeTemp = null;
} }
cardinalWindDirection() { cardinalWindDirection() {
if (this.windDirection > 11.25 && this.windDirection <= 33.75) { if (this.windFromDirection > 11.25 && this.windFromDirection <= 33.75) {
return "NNE"; return "NNE";
} else if (this.windDirection > 33.75 && this.windDirection <= 56.25) { } else if (this.windFromDirection > 33.75 && this.windFromDirection <= 56.25) {
return "NE"; return "NE";
} else if (this.windDirection > 56.25 && this.windDirection <= 78.75) { } else if (this.windFromDirection > 56.25 && this.windFromDirection <= 78.75) {
return "ENE"; return "ENE";
} else if (this.windDirection > 78.75 && this.windDirection <= 101.25) { } else if (this.windFromDirection > 78.75 && this.windFromDirection <= 101.25) {
return "E"; return "E";
} else if (this.windDirection > 101.25 && this.windDirection <= 123.75) { } else if (this.windFromDirection > 101.25 && this.windFromDirection <= 123.75) {
return "ESE"; return "ESE";
} else if (this.windDirection > 123.75 && this.windDirection <= 146.25) { } else if (this.windFromDirection > 123.75 && this.windFromDirection <= 146.25) {
return "SE"; return "SE";
} else if (this.windDirection > 146.25 && this.windDirection <= 168.75) { } else if (this.windFromDirection > 146.25 && this.windFromDirection <= 168.75) {
return "SSE"; return "SSE";
} else if (this.windDirection > 168.75 && this.windDirection <= 191.25) { } else if (this.windFromDirection > 168.75 && this.windFromDirection <= 191.25) {
return "S"; return "S";
} else if (this.windDirection > 191.25 && this.windDirection <= 213.75) { } else if (this.windFromDirection > 191.25 && this.windFromDirection <= 213.75) {
return "SSW"; return "SSW";
} else if (this.windDirection > 213.75 && this.windDirection <= 236.25) { } else if (this.windFromDirection > 213.75 && this.windFromDirection <= 236.25) {
return "SW"; return "SW";
} else if (this.windDirection > 236.25 && this.windDirection <= 258.75) { } else if (this.windFromDirection > 236.25 && this.windFromDirection <= 258.75) {
return "WSW"; return "WSW";
} else if (this.windDirection > 258.75 && this.windDirection <= 281.25) { } else if (this.windFromDirection > 258.75 && this.windFromDirection <= 281.25) {
return "W"; return "W";
} else if (this.windDirection > 281.25 && this.windDirection <= 303.75) { } else if (this.windFromDirection > 281.25 && this.windFromDirection <= 303.75) {
return "WNW"; return "WNW";
} else if (this.windDirection > 303.75 && this.windDirection <= 326.25) { } else if (this.windFromDirection > 303.75 && this.windFromDirection <= 326.25) {
return "NW"; return "NW";
} else if (this.windDirection > 326.25 && this.windDirection <= 348.75) { } else if (this.windFromDirection > 326.25 && this.windFromDirection <= 348.75) {
return "NNW"; return "NNW";
} else { } else {
return "N"; return "N";
} }
} }
nextSunAction() { /**
return moment().isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise"; * Determines if the sun sets or rises next. Uses the current time and not
* the date from the weather-forecast.
*
* @param {Moment} date an optional date where you want to get the next
* action for. Useful only in tests, defaults to the current time.
* @returns {string} "sunset" or "sunrise"
*/
nextSunAction(date = moment()) {
return date.isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise";
} }
feelsLike() { feelsLike() {
if (this.feelsLikeTemp) { if (this.feelsLikeTemp) {
return this.feelsLikeTemp; return this.feelsLikeTemp;
} }
const windInMph = WeatherUtils.convertWind(this.windSpeed, "imperial"); return WeatherUtils.calculateFeelsLike(this.temperature, this.windSpeed, this.humidity);
const tempInF = WeatherUtils.convertTemp(this.temperature, "imperial");
let feelsLike = tempInF;
if (windInMph > 3 && tempInF < 50) {
feelsLike = Math.round(35.74 + 0.6215 * tempInF - 35.75 * Math.pow(windInMph, 0.16) + 0.4275 * tempInF * Math.pow(windInMph, 0.16));
} else if (tempInF > 80 && this.humidity > 40) {
feelsLike =
-42.379 +
2.04901523 * tempInF +
10.14333127 * this.humidity -
0.22475541 * tempInF * this.humidity -
6.83783 * Math.pow(10, -3) * tempInF * tempInF -
5.481717 * Math.pow(10, -2) * this.humidity * this.humidity +
1.22874 * Math.pow(10, -3) * tempInF * tempInF * this.humidity +
8.5282 * Math.pow(10, -4) * tempInF * this.humidity * this.humidity -
1.99 * Math.pow(10, -6) * tempInF * tempInF * this.humidity * this.humidity;
}
return ((feelsLike - 32) * 5) / 9;
} }
/** /**
@@ -105,7 +97,8 @@ class WeatherObject {
* @returns {boolean} true if it is at dayTime * @returns {boolean} true if it is at dayTime
*/ */
isDayTime() { isDayTime() {
return this.date.isBetween(this.sunrise, this.sunset, undefined, "[]"); const now = !this.date ? moment() : this.date;
return now.isBetween(this.sunrise, this.sunset, undefined, "[]");
} }
/** /**

View File

@@ -12,7 +12,7 @@ const WeatherUtils = {
* @returns {number} the speed in beaufort * @returns {number} the speed in beaufort
*/ */
beaufortWindSpeed(speedInMS) { beaufortWindSpeed(speedInMS) {
const windInKmh = (speedInMS * 3600) / 1000; const windInKmh = this.convertWind(speedInMS, "kmh");
const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000]; const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
for (const [index, speed] of speeds.entries()) { for (const [index, speed] of speeds.entries()) {
if (speed > windInKmh) { if (speed > windInKmh) {
@@ -22,6 +22,29 @@ const WeatherUtils = {
return 12; return 12;
}, },
/**
* Convert a value in a given unit to a string with a converted
* value and a postfix matching the output unit system.
*
* @param {number} value - The value to convert.
* @param {string} valueUnit - The unit the values has. Default is mm.
* @param {string} outputUnit - The unit system (imperial/metric) the return value should have.
* @returns {string} - A string with tha value and a unit postfix.
*/
convertPrecipitationUnit(value, valueUnit, outputUnit) {
if (outputUnit === "imperial") {
if (valueUnit && valueUnit.toLowerCase() === "cm") value = value * 0.3937007874;
else value = value * 0.03937007874;
valueUnit = "in";
} else {
valueUnit = valueUnit ? valueUnit : "mm";
}
if (valueUnit === "%") return `${value.toFixed(0)} ${valueUnit}`;
return `${value.toFixed(2)} ${valueUnit}`;
},
/** /**
* Convert temp (from degrees C) into imperial or metric unit depending on * Convert temp (from degrees C) into imperial or metric unit depending on
* your config * your config
@@ -90,6 +113,29 @@ const WeatherUtils = {
convertWindToMs(kmh) { convertWindToMs(kmh) {
return kmh * 0.27777777777778; return kmh * 0.27777777777778;
},
calculateFeelsLike(temperature, windSpeed, humidity) {
const windInMph = this.convertWind(windSpeed, "imperial");
const tempInF = this.convertTemp(temperature, "imperial");
let feelsLike = tempInF;
if (windInMph > 3 && tempInF < 50) {
feelsLike = Math.round(35.74 + 0.6215 * tempInF - 35.75 * Math.pow(windInMph, 0.16) + 0.4275 * tempInF * Math.pow(windInMph, 0.16));
} 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;
}
return ((feelsLike - 32) * 5) / 9;
} }
}; };

4655
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.22.0", "version": "2.23.0",
"description": "The open source modular smart mirror platform.", "description": "The open source modular smart mirror platform.",
"main": "js/electron.js", "main": "js/electron.js",
"scripts": { "scripts": {
@@ -13,10 +13,10 @@
"install-fonts": "echo \"Installing fonts ...\n\" && cd fonts && npm install --loglevel=error --no-audit --no-fund --no-update-notifier", "install-fonts": "echo \"Installing fonts ...\n\" && cd fonts && npm install --loglevel=error --no-audit --no-fund --no-update-notifier",
"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\"",
"test": "NODE_ENV=test jest -i --forceExit", "test": "NODE_ENV=test jest -i --forceExit",
"test:coverage": "NODE_ENV=test jest --coverage -i --forceExit", "test:coverage": "NODE_ENV=test jest --coverage -i --verbose false --forceExit",
"test:electron": "NODE_ENV=test jest --selectProjects electron -i --forceExit", "test:electron": "NODE_ENV=test jest --selectProjects electron -i --forceExit",
"test:e2e": "NODE_ENV=test jest --selectProjects e2e -i --forceExit", "test:e2e": "NODE_ENV=test jest --selectProjects e2e -i --forceExit",
"test:unit": "NODE_ENV=test jest --selectProjects unit -i --forceExit", "test:unit": "NODE_ENV=test jest --selectProjects unit",
"test:prettier": "prettier . --check", "test:prettier": "prettier . --check",
"test:js": "eslint 'js/**/*.js' 'modules/default/**/*.js' 'clientonly/*.js' 'serveronly/*.js' 'translations/*.js' 'vendor/*.js' 'tests/**/*.js' 'config/*' --config .eslintrc.json", "test:js": "eslint 'js/**/*.js' 'modules/default/**/*.js' 'clientonly/*.js' 'serveronly/*.js' 'translations/*.js' 'vendor/*.js' 'tests/**/*.js' 'config/*' --config .eslintrc.json",
"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",
@@ -49,44 +49,45 @@
}, },
"homepage": "https://magicmirror.builders", "homepage": "https://magicmirror.builders",
"devDependencies": { "devDependencies": {
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-jest": "^27.1.7", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsdoc": "^39.6.4", "eslint-plugin-jest": "^27.2.1",
"eslint-plugin-jsdoc": "^40.1.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"express-basic-auth": "^1.2.1", "express-basic-auth": "^1.2.1",
"husky": "^8.0.2", "husky": "^8.0.3",
"jest": "^29.3.1", "jest": "^29.5.0",
"jsdom": "^20.0.3", "jsdom": "^21.1.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"playwright": "^1.29.1", "playwright": "^1.32.1",
"prettier": "^2.8.1", "prettier": "^2.8.7",
"pretty-quick": "^3.1.3", "pretty-quick": "^3.1.3",
"sinon": "^15.0.1", "sinon": "^15.0.2",
"stylelint": "^14.16.0", "stylelint": "^15.3.0",
"stylelint-config-prettier": "^9.0.4", "stylelint-config-standard": "^31.0.0",
"stylelint-config-standard": "^29.0.0", "stylelint-prettier": "^3.0.0",
"stylelint-prettier": "^2.0.0",
"suncalc": "^1.9.0" "suncalc": "^1.9.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"electron": "^22.0.0" "electron": "^22.3.4"
}, },
"dependencies": { "dependencies": {
"colors": "^1.4.0", "colors": "^1.4.0",
"console-stamp": "^3.1.0", "console-stamp": "^3.1.1",
"digest-fetch": "^2.0.1", "digest-fetch": "^2.0.1",
"eslint": "^8.30.0", "envsub": "^4.1.0",
"eslint": "^8.36.0",
"express": "^4.18.2", "express": "^4.18.2",
"express-ipfilter": "^1.3.1", "express-ipfilter": "^1.3.1",
"feedme": "^2.0.2", "feedme": "^2.0.2",
"helmet": "^6.0.1", "helmet": "^6.0.1",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"luxon": "^1.28.0", "luxon": "^1.28.1",
"module-alias": "^2.2.2", "module-alias": "^2.2.2",
"moment": "^2.29.4", "moment": "^2.29.4",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.9",
"node-ical": "^0.15.3", "node-ical": "^0.16.0",
"socket.io": "^4.5.4" "socket.io": "^4.6.1"
}, },
"_moduleAliases": { "_moduleAliases": {
"node_helper": "js/node_helper.js", "node_helper": "js/node_helper.js",

View File

@@ -1,8 +1,8 @@
const app = require("../js/app.js"); const app = require("../js/app");
const Log = require("logger"); const Log = require("../js/logger");
app.start((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.log("\nReady to go! Please point your browser to: " + httpType + "://" + bindAddress + ":" + config.port); Log.log(`\nReady to go! Please point your browser to: ${httpType}://${bindAddress}:${config.port}`);
}); });

View File

@@ -3,7 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com * By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed. * MIT Licensed.
*/ */
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({ let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
ipWhitelist: [] ipWhitelist: []
}); });

View File

@@ -11,9 +11,10 @@ let config = {
module: "calendar", module: "calendar",
position: "bottom_bar", position: "bottom_bar",
config: { config: {
customEvents: [{ keyword: "CustomEvent", symbol: "dice" }],
calendars: [ calendars: [
{ {
maximumEntries: 4, maximumEntries: 5,
maximumNumberOfDays: 10000, maximumNumberOfDays: 10000,
symbol: "birthday-cake", symbol: "birthday-cake",
fullDaySymbol: "calendar-day", fullDaySymbol: "calendar-day",

View File

@@ -14,7 +14,7 @@ let config = {
module: "helloworld", module: "helloworld",
position: positions[idx], position: positions[idx],
config: { config: {
text: "Text in " + positions[idx] text: `Text in ${positions[idx]}`
} }
}); });
} }

View File

@@ -15,7 +15,8 @@ let config = {
location: "Munich", location: "Munich",
mockData: '"#####WEATHERDATA#####"', mockData: '"#####WEATHERDATA#####"',
weatherEndpoint: "/forecast/daily", weatherEndpoint: "/forecast/daily",
decimalSymbol: "_" decimalSymbol: "_",
showPrecipitationAmount: true
} }
} }
] ]

View File

@@ -0,0 +1,25 @@
/* MagicMirror² Test config hourly weather
*
* By rejas https://github.com/rejas
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
type: "hourly",
location: "Berlin",
mockData: '"#####WEATHERDATA#####"'
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -0,0 +1,26 @@
/* MagicMirror² Test config hourly weather options
*
* By rejas https://github.com/rejas
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
type: "hourly",
location: "Berlin",
mockData: '"#####WEATHERDATA#####"',
hourlyForecastIncrements: 2
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -0,0 +1,27 @@
/* MagicMirror² Test config hourly weather
*
* By rejas https://github.com/rejas
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
type: "hourly",
location: "Berlin",
mockData: '"#####WEATHERDATA#####"',
showPrecipitationAmount: true,
showPrecipitationProbability: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -3,7 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com * By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed. * MIT Licensed.
*/ */
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({ let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
ipWhitelist: ["x.x.x.x"] ipWhitelist: ["x.x.x.x"]
}); });

View File

@@ -3,7 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com * By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed. * MIT Licensed.
*/ */
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({ let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
port: 8090 port: 8090
}); });

View File

@@ -0,0 +1 @@
MM_PORT=8090

View File

@@ -0,0 +1,13 @@
/* MagicMirror² Test config sample environment set port 8090
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
port: ${MM_PORT}
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -3,7 +3,7 @@ const helpers = require("./helpers/global-setup");
describe("All font files from roboto.css should be downloadable", () => { describe("All font files from roboto.css should be downloadable", () => {
const fontFiles = []; const fontFiles = [];
// Statements below filters out all 'url' lines in the CSS file // Statements below filters out all 'url' lines in the CSS file
const fileContent = require("fs").readFileSync(__dirname + "/../../fonts/roboto.css", "utf8"); const fileContent = require("fs").readFileSync(`${__dirname}/../../fonts/roboto.css`, "utf8");
const regex = /\burl\(['"]([^'"]+)['"]\)/g; const regex = /\burl\(['"]([^'"]+)['"]\)/g;
let match = regex.exec(fileContent); let match = regex.exec(fileContent);
while (match !== null) { while (match !== null) {
@@ -21,7 +21,7 @@ describe("All font files from roboto.css should be downloadable", () => {
}); });
test.each(fontFiles)("should return 200 HTTP code for file '%s'", async (fontFile) => { test.each(fontFiles)("should return 200 HTTP code for file '%s'", async (fontFile) => {
const fontUrl = "http://localhost:8080/fonts/" + fontFile; const fontUrl = `http://localhost:8080/fonts/${fontFile}`;
const res = await helpers.fetch(fontUrl); const res = await helpers.fetch(fontUrl);
expect(res.status).toBe(200); expect(res.status).toBe(200);
}); });

View File

@@ -12,7 +12,7 @@ app.use(basicAuth);
// Set available directories // Set available directories
const directories = ["/tests/configs", "/tests/mocks"]; const directories = ["/tests/configs", "/tests/mocks"];
const rootPath = path.resolve(__dirname + "/../../../"); const rootPath = path.resolve(`${__dirname}/../../../`);
for (let directory of directories) { for (let directory of directories) {
app.use(directory, express.static(path.resolve(rootPath + directory))); app.use(directory, express.static(path.resolve(rootPath + directory)));

View File

@@ -13,26 +13,22 @@ exports.startApplication = async (configFilename, exec) => {
process.env.MM_CONFIG_FILE = configFilename; process.env.MM_CONFIG_FILE = configFilename;
} }
if (exec) exec; if (exec) exec;
global.app = require("app.js"); global.app = require("../../../js/app");
return new Promise((resolve) => { return global.app.start();
global.app.start(resolve);
});
}; };
exports.stopApplication = async () => { exports.stopApplication = async () => {
if (global.app) { if (!global.app) {
return new Promise((resolve) => { return Promise.resolve();
global.app.stop(resolve);
delete global.app;
});
} }
return Promise.resolve(); await global.app.stop();
delete global.app;
}; };
exports.getDocument = () => { exports.getDocument = () => {
return new Promise((resolve) => { return new Promise((resolve) => {
const url = "http://" + (config.address || "localhost") + ":" + (config.port || "8080"); const url = `http://${config.address || "localhost"}:${config.port || "8080"}`;
jsdom.JSDOM.fromURL(url, { resources: "usable", runScripts: "dangerously" }).then((dom) => { jsdom.JSDOM.fromURL(url, { resources: "usable", runScripts: "dangerously" }).then((dom) => {
dom.window.name = "jsdom"; dom.window.name = "jsdom";
dom.window.fetch = corefetch; dom.window.fetch = corefetch;

View File

@@ -1,7 +1,5 @@
const { injectMockData } = require("../../utils/weather_mocker");
const helpers = require("./global-setup"); const helpers = require("./global-setup");
const path = require("path");
const fs = require("fs");
const { generateWeather, generateWeatherForecast } = require("../../mocks/weather_test");
exports.getText = async (element, result) => { exports.getText = async (element, result) => {
const elem = await helpers.waitForElement(element); const elem = await helpers.waitForElement(element);
@@ -14,16 +12,8 @@ exports.getText = async (element, result) => {
).toBe(result); ).toBe(result);
}; };
exports.startApp = async (configFile, additionalMockData) => { exports.startApp = async (configFileName, additionalMockData) => {
let mockWeather; injectMockData(configFileName, additionalMockData);
if (configFile.includes("forecast")) {
mockWeather = generateWeatherForecast(additionalMockData);
} else {
mockWeather = generateWeather(additionalMockData);
}
let content = fs.readFileSync(path.resolve(__dirname + "../../../../" + configFile)).toString();
content = content.replace("#####WEATHERDATA#####", mockWeather);
fs.writeFileSync(path.resolve(__dirname + "../../../../config/config.js"), content);
await helpers.startApplication(""); await helpers.startApplication("");
await helpers.getDocument(); await helpers.getDocument();
}; };

View File

@@ -1,5 +1,5 @@
const helpers = require("../helpers/global-setup"); const helpers = require("../helpers/global-setup");
const serverBasicAuth = require("../helpers/basic-auth.js"); const serverBasicAuth = require("../helpers/basic-auth");
describe("Calendar module", () => { describe("Calendar module", () => {
/** /**
@@ -48,14 +48,18 @@ describe("Calendar module", () => {
await helpers.getDocument(); await helpers.getDocument();
}); });
it("should show the custom maximumEntries of 4", async () => { it("should show the custom maximumEntries of 5", async () => {
await testElementLength(".calendar .event", 4); await testElementLength(".calendar .event", 5);
}); });
it("should show the custom calendar symbol in each event", async () => { it("should show the custom calendar symbol in four events", async () => {
await testElementLength(".calendar .event .fa-birthday-cake", 4); await testElementLength(".calendar .event .fa-birthday-cake", 4);
}); });
it("should show a customEvent calendar symbol in one event", async () => {
await testElementLength(".calendar .event .fa-dice", 1);
});
it("should show two custom icons for repeating events", async () => { it("should show two custom icons for repeating events", async () => {
await testElementLength(".calendar .event .fa-undo", 2); await testElementLength(".calendar .event .fa-undo", 2);
}); });
@@ -87,7 +91,7 @@ describe("Calendar module", () => {
await helpers.getDocument(); await helpers.getDocument();
}); });
it('should contain text "Mar 25th" in timezone UTC ' + -i, async () => { it(`should contain text "Mar 25th" in timezone UTC ${-i}`, async () => {
await testTextContain(".calendar", "Mar 25th"); await testTextContain(".calendar", "Mar 25th");
}); });
}); });

View File

@@ -1,5 +1,5 @@
const helpers = require("../helpers/global-setup");
const moment = require("moment"); const moment = require("moment");
const helpers = require("../helpers/global-setup");
describe("Clock module", () => { describe("Clock module", () => {
afterAll(async () => { afterAll(async () => {
@@ -89,7 +89,7 @@ describe("Clock module", () => {
it("should show the week with the correct number of week of year", async () => { it("should show the week with the correct number of week of year", async () => {
const currentWeekNumber = moment().week(); const currentWeekNumber = moment().week();
const weekToShow = "Week " + currentWeekNumber; const weekToShow = `Week ${currentWeekNumber}`;
const elem = await helpers.waitForElement(".clock .week"); const elem = await helpers.waitForElement(".clock .week");
expect(elem).not.toBe(null); expect(elem).not.toBe(null);
expect(elem.textContent).toBe(weekToShow); expect(elem.textContent).toBe(weekToShow);
@@ -103,7 +103,7 @@ describe("Clock module", () => {
}); });
it("should show the analog clock face", async () => { it("should show the analog clock face", async () => {
const elem = helpers.waitForElement(".clockCircle"); const elem = helpers.waitForElement(".clock-circle");
expect(elem).not.toBe(null); expect(elem).not.toBe(null);
}); });
}); });

View File

@@ -46,7 +46,7 @@ describe("Weather module", () => {
}); });
it("should render windDirection with an arrow", async () => { it("should render windDirection with an arrow", async () => {
const elem = await helpers.waitForElement(".weather .normal.medium sup i.fa-long-arrow-alt-up"); const elem = await helpers.waitForElement(".weather .normal.medium sup i.fa-long-arrow-alt-down");
expect(elem).not.toBe(null); expect(elem).not.toBe(null);
expect(elem.outerHTML).toContain("transform:rotate(250deg);"); expect(elem.outerHTML).toContain("transform:rotate(250deg);");
}); });

View File

@@ -13,14 +13,14 @@ describe("Weather module: Weather Forecast", () => {
const days = ["Today", "Tomorrow", "Sun", "Mon", "Tue"]; const days = ["Today", "Tomorrow", "Sun", "Mon", "Tue"];
for (const [index, day] of days.entries()) { for (const [index, day] of days.entries()) {
it("should render day " + day, async () => { it(`should render day ${day}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day); await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day);
}); });
} }
const icons = ["day-cloudy", "rain", "day-sunny", "day-sunny", "day-sunny"]; const icons = ["day-cloudy", "rain", "day-sunny", "day-sunny", "day-sunny"];
for (const [index, icon] of icons.entries()) { for (const [index, icon] of icons.entries()) {
it("should render icon " + icon, async () => { it(`should render icon ${icon}`, async () => {
const elem = await helpers.waitForElement(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(2) span.wi-${icon}`); const elem = await helpers.waitForElement(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(2) span.wi-${icon}`);
expect(elem).not.toBe(null); expect(elem).not.toBe(null);
}); });
@@ -28,21 +28,21 @@ describe("Weather module: Weather Forecast", () => {
const maxTemps = ["24.4°", "21.0°", "22.9°", "23.4°", "20.6°"]; const maxTemps = ["24.4°", "21.0°", "22.9°", "23.4°", "20.6°"];
for (const [index, temp] of maxTemps.entries()) { for (const [index, temp] of maxTemps.entries()) {
it("should render max temperature " + temp, async () => { it(`should render max temperature ${temp}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp); await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp);
}); });
} }
const minTemps = ["15.3°", "13.6°", "13.8°", "13.9°", "10.9°"]; const minTemps = ["15.3°", "13.6°", "13.8°", "13.9°", "10.9°"];
for (const [index, temp] of minTemps.entries()) { for (const [index, temp] of minTemps.entries()) {
it("should render min temperature " + temp, async () => { it(`should render min temperature ${temp}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(4)`, temp); await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(4)`, temp);
}); });
} }
const opacities = [1, 1, 0.8, 0.5333333333333333, 0.2666666666666667]; const opacities = [1, 1, 0.8, 0.5333333333333333, 0.2666666666666667];
for (const [index, opacity] of opacities.entries()) { for (const [index, opacity] of opacities.entries()) {
it("should render fading of rows with opacity=" + opacity, async () => { it(`should render fading of rows with opacity=${opacity}`, async () => {
const elem = await helpers.waitForElement(`.weather table.small tr:nth-child(${index + 1})`); const elem = await helpers.waitForElement(`.weather table.small tr:nth-child(${index + 1})`);
expect(elem).not.toBe(null); expect(elem).not.toBe(null);
expect(elem.outerHTML).toContain(`<tr style="opacity: ${opacity};">`); expect(elem.outerHTML).toContain(`<tr style="opacity: ${opacity};">`);
@@ -57,7 +57,7 @@ describe("Weather module: Weather Forecast", () => {
const days = ["Fri", "Sat", "Sun", "Mon", "Tue"]; const days = ["Fri", "Sat", "Sun", "Mon", "Tue"];
for (const [index, day] of days.entries()) { for (const [index, day] of days.entries()) {
it("should render day " + day, async () => { it(`should render day ${day}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day); await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day);
}); });
} }
@@ -79,18 +79,40 @@ describe("Weather module: Weather Forecast", () => {
expect(table.rows).not.toBe(null); expect(table.rows).not.toBe(null);
expect(table.rows.length).toBe(5); expect(table.rows.length).toBe(5);
}); });
const precipitations = [undefined, "2.51 mm"];
for (const [index, precipitation] of precipitations.entries()) {
if (precipitation) {
it(`should render precipitation amount ${precipitation}`, async () => {
await weatherFunc.getText(`.weather table tr:nth-child(${index + 1}) td.precipitation-amount`, precipitation);
});
}
}
}); });
describe("Forecast weather units", () => { describe("Forecast weather with imperial units", () => {
beforeAll(async () => { beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/forecastweather_units.js", {}); await weatherFunc.startApp("tests/configs/modules/weather/forecastweather_units.js", {});
}); });
const temperatures = ["75_9°", "69_8°", "73_2°", "74_1°", "69_1°"]; describe("Temperature units", () => {
for (const [index, temp] of temperatures.entries()) { const temperatures = ["75_9°", "69_8°", "73_2°", "74_1°", "69_1°"];
it("should render custom decimalSymbol = '_' for temp " + temp, async () => { for (const [index, temp] of temperatures.entries()) {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp); it(`should render custom decimalSymbol = '_' for temp ${temp}`, async () => {
}); await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp);
} });
}
});
describe("Precipitation units", () => {
const precipitations = [undefined, "0.10 in"];
for (const [index, precipitation] of precipitations.entries()) {
if (precipitation) {
it(`should render precipitation amount ${precipitation}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`, precipitation);
});
}
}
});
}); });
}); });

View File

@@ -0,0 +1,64 @@
const helpers = require("../helpers/global-setup");
const weatherFunc = require("../helpers/weather-functions");
describe("Weather module: Weather Hourly Forecast", () => {
afterAll(async () => {
await helpers.stopApplication();
});
describe("Default configuration", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/hourlyweather_default.js", {});
});
const minTemps = ["7:00 pm", "8:00 pm", "9:00 pm", "10:00 pm", "11:00 pm"];
for (const [index, hour] of minTemps.entries()) {
it(`should render forecast for hour ${hour}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.day`, hour);
});
}
});
describe("Hourly weather options", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/hourlyweather_options.js", {});
});
describe("Hourly increments of 2", () => {
const minTemps = ["7:00 pm", "9:00 pm", "11:00 pm", "1:00 am", "3:00 am"];
for (const [index, hour] of minTemps.entries()) {
it(`should render forecast for hour ${hour}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.day`, hour);
});
}
});
});
describe("Show precipitations", () => {
beforeAll(async () => {
await weatherFunc.startApp("tests/configs/modules/weather/hourlyweather_showPrecipitation.js", {});
});
describe("Shows precipitation amount", () => {
const amounts = [undefined, undefined, undefined, "0.13 mm", "0.13 mm"];
for (const [index, amount] of amounts.entries()) {
if (amount) {
it(`should render precipitation amount ${amount}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-amount`, amount);
});
}
}
});
describe("Shows precipitation probability", () => {
const propabilities = [undefined, undefined, "12 %", "36 %", "44 %"];
for (const [index, pop] of propabilities.entries()) {
if (pop) {
it(`should render probability ${pop}`, async () => {
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td.precipitation-prob`, pop);
});
}
}
});
});
});

View File

@@ -13,10 +13,10 @@ describe("Position of modules", () => {
for (const position of positions) { for (const position of positions) {
const className = position.replace("_", "."); const className = position.replace("_", ".");
it("should show text in " + position, async () => { it(`should show text in ${position}`, async () => {
const elem = await helpers.waitForElement("." + className); const elem = await helpers.waitForElement(`.${className}`);
expect(elem).not.toBe(null); expect(elem).not.toBe(null);
expect(elem.textContent).toContain("Text in " + position); expect(elem.textContent).toContain(`Text in ${position}`);
}); });
} }
}); });

View File

@@ -0,0 +1,15 @@
const helpers = require("./helpers/global-setup");
describe("templated config with port variable", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/port_variable.js");
});
afterAll(async () => {
await helpers.stopApplication();
});
it("should return 200", async () => {
const res = await helpers.fetch("http://localhost:8090");
expect(res.status).toBe(200);
});
});

View File

@@ -1,10 +1,10 @@
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const translations = require("../../translations/translations.js");
const helmet = require("helmet"); const helmet = require("helmet");
const { JSDOM } = require("jsdom"); const { JSDOM } = require("jsdom");
const express = require("express"); const express = require("express");
const sinon = require("sinon"); const sinon = require("sinon");
const translations = require("../../translations/translations");
describe("Translations", () => { describe("Translations", () => {
let server; let server;
@@ -21,8 +21,8 @@ describe("Translations", () => {
server = app.listen(3000); server = app.listen(3000);
}); });
afterAll(() => { afterAll(async () => {
server.close(); await server.close();
}); });
it("should have a translation file in the specified path", () => { it("should have a translation file in the specified path", () => {
@@ -48,17 +48,15 @@ describe("Translations", () => {
dom.window.onload = async () => { dom.window.onload = async () => {
const { Translator, Module, config } = dom.window; const { Translator, Module, config } = dom.window;
config.language = "en"; config.language = "en";
Translator.load = sinon.stub().callsFake((_m, _f, _fb, callback) => callback()); Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
Module.register("name", { getTranslations: () => translations }); Module.register("name", { getTranslations: () => translations });
const MMM = Module.create("name"); const MMM = Module.create("name");
const loaded = sinon.stub(); await MMM.loadTranslations();
MMM.loadTranslations(loaded);
expect(loaded.callCount).toBe(1);
expect(Translator.load.args.length).toBe(1); expect(Translator.load.args.length).toBe(1);
expect(Translator.load.calledWith(MMM, "translations/en.json", false, sinon.match.func)).toBe(true); expect(Translator.load.calledWith(MMM, "translations/en.json", false)).toBe(true);
done(); done();
}; };
@@ -67,18 +65,16 @@ describe("Translations", () => {
it("should load translation + fallback file", (done) => { it("should load translation + fallback file", (done) => {
dom.window.onload = async () => { dom.window.onload = async () => {
const { Translator, Module } = dom.window; const { Translator, Module } = dom.window;
Translator.load = sinon.stub().callsFake((_m, _f, _fb, callback) => callback()); Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
Module.register("name", { getTranslations: () => translations }); Module.register("name", { getTranslations: () => translations });
const MMM = Module.create("name"); const MMM = Module.create("name");
const loaded = sinon.stub(); await MMM.loadTranslations();
MMM.loadTranslations(loaded);
expect(loaded.callCount).toBe(1);
expect(Translator.load.args.length).toBe(2); expect(Translator.load.args.length).toBe(2);
expect(Translator.load.calledWith(MMM, "translations/de.json", false, sinon.match.func)).toBe(true); expect(Translator.load.calledWith(MMM, "translations/de.json", false)).toBe(true);
expect(Translator.load.calledWith(MMM, "translations/en.json", true, sinon.match.func)).toBe(true); expect(Translator.load.calledWith(MMM, "translations/en.json", true)).toBe(true);
done(); done();
}; };
@@ -88,17 +84,15 @@ describe("Translations", () => {
dom.window.onload = async () => { dom.window.onload = async () => {
const { Translator, Module, config } = dom.window; const { Translator, Module, config } = dom.window;
config.language = "--"; config.language = "--";
Translator.load = sinon.stub().callsFake((_m, _f, _fb, callback) => callback()); Translator.load = sinon.stub().callsFake((_m, _f, _fb) => null);
Module.register("name", { getTranslations: () => translations }); Module.register("name", { getTranslations: () => translations });
const MMM = Module.create("name"); const MMM = Module.create("name");
const loaded = sinon.stub(); await MMM.loadTranslations();
MMM.loadTranslations(loaded);
expect(loaded.callCount).toBe(1);
expect(Translator.load.args.length).toBe(1); expect(Translator.load.args.length).toBe(1);
expect(Translator.load.calledWith(MMM, "translations/en.json", true, sinon.match.func)).toBe(true); expect(Translator.load.calledWith(MMM, "translations/en.json", true)).toBe(true);
done(); done();
}; };
@@ -112,10 +106,8 @@ describe("Translations", () => {
Module.register("name", {}); Module.register("name", {});
const MMM = Module.create("name"); const MMM = Module.create("name");
const loaded = sinon.stub(); await MMM.loadTranslations();
MMM.loadTranslations(loaded);
expect(loaded.callCount).toBe(1);
expect(Translator.load.callCount).toBe(0); expect(Translator.load.callCount).toBe(0);
done(); done();
@@ -138,14 +130,13 @@ describe("Translations", () => {
<script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`, <script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" } { runScripts: "dangerously", resources: "usable" }
); );
dom.window.onload = () => { dom.window.onload = async () => {
const { Translator } = dom.window; const { Translator } = dom.window;
Translator.load(mmm, translations[language], false, () => { await Translator.load(mmm, translations[language], false);
expect(typeof Translator.translations[mmm.name]).toBe("object"); expect(typeof Translator.translations[mmm.name]).toBe("object");
expect(Object.keys(Translator.translations[mmm.name]).length).toBeGreaterThanOrEqual(1); expect(Object.keys(Translator.translations[mmm.name]).length).toBeGreaterThanOrEqual(1);
done(); done();
});
}; };
}); });
} }
@@ -161,13 +152,12 @@ describe("Translations", () => {
<script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`, <script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" } { runScripts: "dangerously", resources: "usable" }
); );
dom.window.onload = () => { dom.window.onload = async () => {
const { Translator } = dom.window; const { Translator } = dom.window;
Translator.load(mmm, translations.de, false, () => { await Translator.load(mmm, translations.de, false);
base = Object.keys(Translator.translations[mmm.name]).sort(); base = Object.keys(Translator.translations[mmm.name]).sort();
done(); done();
});
}; };
}); });
@@ -191,13 +181,12 @@ describe("Translations", () => {
<script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`, <script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
{ runScripts: "dangerously", resources: "usable" } { runScripts: "dangerously", resources: "usable" }
); );
dom.window.onload = () => { dom.window.onload = async () => {
const { Translator } = dom.window; const { Translator } = dom.window;
Translator.load(mmm, translations[language], false, () => { await Translator.load(mmm, translations[language], false);
keys = Object.keys(Translator.translations[mmm.name]).sort(); keys = Object.keys(Translator.translations[mmm.name]).sort();
done(); done();
});
}; };
}); });

View File

@@ -9,11 +9,11 @@ describe("Vendors", () => {
}); });
describe("Get list vendors", () => { describe("Get list vendors", () => {
const vendors = require(__dirname + "/../../vendor/vendor.js"); const vendors = require(`${__dirname}/../../vendor/vendor.js`);
Object.keys(vendors).forEach((vendor) => { Object.keys(vendors).forEach((vendor) => {
it(`should return 200 HTTP code for vendor "${vendor}"`, async () => { it(`should return 200 HTTP code for vendor "${vendor}"`, async () => {
const urlVendor = "http://localhost:8080/vendor/" + vendors[vendor]; const urlVendor = `http://localhost:8080/vendor/${vendors[vendor]}`;
const res = await helpers.fetch(urlVendor); const res = await helpers.fetch(urlVendor);
expect(res.status).toBe(200); expect(res.status).toBe(200);
}); });
@@ -21,7 +21,7 @@ describe("Vendors", () => {
Object.keys(vendors).forEach((vendor) => { Object.keys(vendors).forEach((vendor) => {
it(`should return 404 HTTP code for vendor https://localhost/"${vendor}"`, async () => { it(`should return 404 HTTP code for vendor https://localhost/"${vendor}"`, async () => {
const urlVendor = "http://localhost:8080/" + vendors[vendor]; const urlVendor = `http://localhost:8080/${vendors[vendor]}`;
const res = await helpers.fetch(urlVendor); const res = await helpers.fetch(urlVendor);
expect(res.status).toBe(404); expect(res.status).toBe(404);
}); });

View File

@@ -1,5 +1,5 @@
const helpers = require("./helpers/global-setup");
const events = require("events"); const events = require("events");
const helpers = require("./helpers/global-setup");
describe("Electron app environment", () => { describe("Electron app environment", () => {
beforeEach(async () => { beforeEach(async () => {
@@ -13,7 +13,7 @@ describe("Electron app environment", () => {
it("should open browserwindow", async () => { it("should open browserwindow", async () => {
const module = await helpers.getElement("#module_0_helloworld"); const module = await helpers.getElement("#module_0_helloworld");
expect(await module.textContent()).toContain("Test Display Header"); expect(await module.textContent()).toContain("Test Display Header");
expect(await global.electronApp.windows().length).toBe(1); expect(global.electronApp.windows().length).toBe(1);
}); });
}); });

View File

@@ -8,7 +8,6 @@ exports.startApplication = async (configFilename, systemDate = null, electronPar
global.page = null; global.page = null;
process.env.MM_CONFIG_FILE = configFilename; process.env.MM_CONFIG_FILE = configFilename;
process.env.TZ = "GMT"; process.env.TZ = "GMT";
jest.retryTimes(3);
global.electronApp = await electron.launch({ args: electronParams }); global.electronApp = await electron.launch({ args: electronParams });
await global.electronApp.firstWindow(); await global.electronApp.firstWindow();

View File

@@ -1,7 +1,5 @@
const { injectMockData } = require("../../utils/weather_mocker");
const helpers = require("./global-setup"); const helpers = require("./global-setup");
const path = require("path");
const fs = require("fs");
const { generateWeather, generateWeatherForecast } = require("../../mocks/weather_test");
exports.getText = async (element, result) => { exports.getText = async (element, result) => {
const elem = await helpers.getElement(element); const elem = await helpers.getElement(element);
@@ -15,15 +13,7 @@ exports.getText = async (element, result) => {
).toBe(result); ).toBe(result);
}; };
exports.startApp = async (configFile, systemDate) => { exports.startApp = async (configFileNameName, systemDate) => {
let mockWeather; injectMockData(configFileNameName);
if (configFile.includes("forecast")) {
mockWeather = generateWeatherForecast();
} else {
mockWeather = generateWeather();
}
let content = fs.readFileSync(path.resolve(__dirname + "../../../../" + configFile)).toString();
content = content.replace("#####WEATHERDATA#####", mockWeather);
fs.writeFileSync(path.resolve(__dirname + "../../../../config/config.js"), content);
await helpers.startApplication("", systemDate); await helpers.startApplication("", systemDate);
}; };

View File

@@ -7,11 +7,8 @@ describe("Calendar module", () => {
* @param {string} cssClass css selector * @param {string} cssClass css selector
*/ */
const doTest = async (cssClass) => { const doTest = async (cssClass) => {
await helpers.getElement(".calendar"); let elem = await helpers.getElement(`.calendar .module-content .event${cssClass}`);
await helpers.getElement(".module-content"); expect(await elem.isVisible()).toBe(true);
const events = await global.page.locator(".event");
const elem = await events.locator(cssClass);
expect(elem).not.toBe(null);
}; };
afterEach(async () => { afterEach(async () => {
@@ -19,14 +16,29 @@ describe("Calendar module", () => {
}); });
describe("Test css classes", () => { describe("Test css classes", () => {
it("has css class dayBeforeYesterday", async () => {
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "03 Jan 2030 12:30:00 GMT");
await doTest(".dayBeforeYesterday");
});
it("has css class yesterday", async () => {
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "02 Jan 2030 12:30:00 GMT");
await doTest(".yesterday");
});
it("has css class today", async () => { it("has css class today", async () => {
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "01 Jan 2030 12:30:00 GMT"); await helpers.startApplication("tests/configs/modules/calendar/custom.js", "01 Jan 2030 12:30:00 GMT");
await doTest(".today"); await doTest(".today");
}); });
it("has css class tomorrow", async () => { it("has css class tomorrow", async () => {
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "31 Dez 2029 12:30:00 GMT"); await helpers.startApplication("tests/configs/modules/calendar/custom.js", "31 Dec 2029 12:30:00 GMT");
await doTest(".tomorrow"); await doTest(".tomorrow");
}); });
it("has css class dayAfterTomorrow", async () => {
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "30 Dec 2029 12:30:00 GMT");
await doTest(".dayAfterTomorrow");
});
}); });
}); });

Some files were not shown because too many files have changed in this diff Show More