mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-28 09:42:21 -04:00
Release 2.22.0 (#2983)
## [2.22.0] - 2023-01-01 Thanks to: @angeldeejay, @buxxi, @dariom, @dWoolridge, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @retroflex, @SkySails and @Tom. 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! ### Added - Added test for remoteFile option in compliments module - 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 Collaboration.md - Added new github action for dependency review (#2862) - Added a WeatherProvider for Open-Meteo - Added Yr as a weather provider - Added config options "ignoreXOriginHeader" and "ignoreContentSecurityPolicy" ### Removed - Removed usage of internal fetch function of node until it is more stable ### Updated - Cleaned up test directory (#2937) and jest config (#2959) - Wait for all modules to start before declaring the system ready (#2487) - Updated e2e tests (moved `done()` in helper functions) and use es6 syntax in all tests - Updated da translation - Rework weather module - Make sure smhi provider api only gets a maximum of 6 digits coordinates (#2955) - Use fetch instead of XMLHttpRequest in weatherprovider (#2935) - Reworked how weatherproviders handle units (#2849) - Use unix() method for parsing times, fix suntimes on the way (#2950) - Refactor conversion functions into utils class (#2958) - The `cors`-method in `server.js` now supports sending and recieving HTTP headers - Replace `…` by `…` - Cleanup compliments module - Updated dependencies including electron to v22 (#2903) ### Fixed - Correctly show apparent temperature in SMHI weather provider - Ensure updatenotification module isn't shown when local is _ahead_ of remote - Handle node_helper errors during startup (#2944) - Possibility to change FontAwesome class in calendar, so icons like `fab fa-facebook-square` works. - Fix cors problems with newsfeed articles (as far as possible), allow disabling cors per feed with option `useCorsProxy: false` (#2840) - Tests not waiting for the application to start and stop before starting the next test - Fix electron tests failing sometimes in github workflow - Fixed gap in clock module when displayed on the left side with displayType=digital - Fixed playwright issue by upgrading to v1.29.1 (#2969) Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com> Co-authored-by: Karsten Hassel <hassel@gmx.de> Co-authored-by: Malte Hallström <46646495+SkySails@users.noreply.github.com> Co-authored-by: Veeck <github@veeck.de> Co-authored-by: veeck <michael@veeck.de> Co-authored-by: dWoolridge <dwoolridge@charter.net> Co-authored-by: Johan <jojjepersson@yahoo.se> Co-authored-by: Dario Mratovich <dario_mratovich@hotmail.com> Co-authored-by: Dario Mratovich <dario.mratovich@outlook.com> Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com> Co-authored-by: Naveen <172697+naveensrinivasan@users.noreply.github.com> Co-authored-by: buxxi <buxxi@omfilm.net> Co-authored-by: Thomas Hirschberger <47733292+Tom-Hirschberger@users.noreply.github.com> Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com>
This commit is contained in:
parent
9e0293047f
commit
0300ce05d5
151 changed files with 5890 additions and 4949 deletions
14
.github/workflows/depsreview.yaml
vendored
Normal file
14
.github/workflows/depsreview.yaml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
name: "Dependency Review"
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-review:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: "Checkout Repository"
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: "Dependency Review"
|
||||||
|
uses: actions/dependency-review-action@v2
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -16,7 +16,6 @@ vendor/node_modules/**/*
|
||||||
jspm_modules
|
jspm_modules
|
||||||
.npm
|
.npm
|
||||||
.node_repl_history
|
.node_repl_history
|
||||||
.nyc_output/
|
|
||||||
|
|
||||||
# Visual Studio Code ignoramuses.
|
# Visual Studio Code ignoramuses.
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
/config
|
/config
|
||||||
/coverage
|
/coverage
|
||||||
.nyc_output
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
74
CHANGELOG.md
74
CHANGELOG.md
|
@ -5,30 +5,82 @@ 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.22.0] - 2023-01-01
|
||||||
|
|
||||||
|
Thanks to: @angeldeejay, @buxxi, @dariom, @dWoolridge, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @retroflex, @SkySails and @Tom.
|
||||||
|
|
||||||
|
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!
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added test for remoteFile option in compliments module
|
||||||
|
- 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 Collaboration.md
|
||||||
|
- Added new github action for dependency review (#2862)
|
||||||
|
- Added a WeatherProvider for Open-Meteo
|
||||||
|
- Added Yr as a weather provider
|
||||||
|
- Added config options "ignoreXOriginHeader" and "ignoreContentSecurityPolicy"
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Removed usage of internal fetch function of node until it is more stable
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
|
||||||
|
- Cleaned up test directory (#2937) and jest config (#2959)
|
||||||
|
- Wait for all modules to start before declaring the system ready (#2487)
|
||||||
|
- Updated e2e tests (moved `done()` in helper functions) and use es6 syntax in all tests
|
||||||
|
- Updated da translation
|
||||||
|
- Rework weather module
|
||||||
|
- Make sure smhi provider api only gets a maximum of 6 digits coordinates (#2955)
|
||||||
|
- Use fetch instead of XMLHttpRequest in weatherprovider (#2935)
|
||||||
|
- Reworked how weatherproviders handle units (#2849)
|
||||||
|
- Use unix() method for parsing times, fix suntimes on the way (#2950)
|
||||||
|
- Refactor conversion functions into utils class (#2958)
|
||||||
|
- The `cors`-method in `server.js` now supports sending and recieving HTTP headers
|
||||||
|
- Replace `…` by `…`
|
||||||
|
- Cleanup compliments module
|
||||||
|
- Updated dependencies including electron to v22 (#2903)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Correctly show apparent temperature in SMHI weather provider
|
||||||
|
- Ensure updatenotification module isn't shown when local is _ahead_ of remote
|
||||||
|
- Handle node_helper errors during startup (#2944)
|
||||||
|
- Possibility to change FontAwesome class in calendar, so icons like `fab fa-facebook-square` works.
|
||||||
|
- Fix cors problems with newsfeed articles (as far as possible), allow disabling cors per feed with option `useCorsProxy: false` (#2840)
|
||||||
|
- Tests not waiting for the application to start and stop before starting the next test
|
||||||
|
- Fix electron tests failing sometimes in github workflow
|
||||||
|
- Fixed gap in clock module when displayed on the left side with displayType=digital
|
||||||
|
- Fixed playwright issue by upgrading to v1.29.1 (#2969)
|
||||||
|
|
||||||
## [2.21.0] - 2022-10-01
|
## [2.21.0] - 2022-10-01
|
||||||
|
|
||||||
Special thanks to: @BKeyport, @buxxi, @davide125, @khassel, @kolbyjack, @krukle, @MikeBishop, @rejas, @sdetweil, @SkySails and @veeck
|
Special thanks to: @BKeyport, @buxxi, @davide125, @khassel, @kolbyjack, @krukle, @MikeBishop, @rejas, @sdetweil, @SkySails and @veeck
|
||||||
|
|
||||||
## Added
|
### Added
|
||||||
|
|
||||||
- Possibility to fetch calendars through socket notifications.
|
- Added possibility to fetch calendars through socket notifications.
|
||||||
- New scripts `install-mm` (and `install-mm:dev`) for simplifying mm installation (now: `npm run install-mm`) and adding params `--no-audit --no-fund --no-update-notifier` for less noise.
|
- New scripts `install-mm` (and `install-mm:dev`) for simplifying mm installation (now: `npm run install-mm`) and adding params `--no-audit --no-fund --no-update-notifier` for less noise.
|
||||||
- New `showTimeToday` option in calendar module shows time for current-day events even if `timeFormat` is `"relative"`.
|
- New `showTimeToday` option in calendar module shows time for current-day events even if `timeFormat` is `"relative"`.
|
||||||
- Add hourly forecasts, apparent temperature & custom location name to SMHI weather provider.
|
- Added hourly forecasts, apparent temperature & custom location name to SMHI weather provider.
|
||||||
|
- Added new electron tests for calendar and moved some compliments tests from `e2e` to `electron` because of date mocking, removed mock stuff from compliments module.
|
||||||
|
|
||||||
## Removed
|
### Removed
|
||||||
|
|
||||||
- Old weather deprecated modules `currentweather` and `weatherforecast`.
|
|
||||||
|
|
||||||
## Updated
|
|
||||||
|
|
||||||
|
- Removed old and deprecated weather modules `currentweather` and `weatherforecast`.
|
||||||
- Removed `DAYAFTERTOMORROW` from English.
|
- Removed `DAYAFTERTOMORROW` from English.
|
||||||
- Update dependencies.
|
|
||||||
|
### Updated
|
||||||
|
|
||||||
|
- Updated dependencies.
|
||||||
- Updated jsdoc.
|
- Updated jsdoc.
|
||||||
- Updated font tree to use variables consistantly.
|
- Updated font tree to use variables consistently.
|
||||||
- Removed deprecated Docker Repository from issue template.
|
- Removed deprecated Docker Repository from issue template.
|
||||||
|
|
||||||
## Fixed
|
### Fixed
|
||||||
|
|
||||||
- Broadcast all calendar events while still honoring global and per-calendar maximumEntries.
|
- Broadcast all calendar events while still honoring global and per-calendar maximumEntries.
|
||||||
- Respect rss ttl provided by newsfeed (#2883).
|
- Respect rss ttl provided by newsfeed (#2883).
|
||||||
|
|
12
Collaboration.md
Normal file
12
Collaboration.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
This document describes how collaborators of this repository should work together.
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
- never merge your own PR's
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
|
||||||
|
- "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
|
32
jest.config.js
Normal file
32
jest.config.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
module.exports = async () => {
|
||||||
|
return {
|
||||||
|
verbose: true,
|
||||||
|
testTimeout: 20000,
|
||||||
|
testSequencer: "<rootDir>/tests/configs/test_sequencer.js",
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
displayName: "unit",
|
||||||
|
moduleNameMapper: {
|
||||||
|
logger: "<rootDir>/js/logger.js"
|
||||||
|
},
|
||||||
|
testMatch: ["**/tests/unit/**/*.[jt]s?(x)"],
|
||||||
|
testPathIgnorePatterns: ["<rootDir>/tests/unit/mocks"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: "electron",
|
||||||
|
testMatch: ["**/tests/electron/**/*.[jt]s?(x)"],
|
||||||
|
testPathIgnorePatterns: ["<rootDir>/tests/electron/helpers/"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: "e2e",
|
||||||
|
setupFilesAfterEnv: ["<rootDir>/tests/e2e/helpers/mock-console.js"],
|
||||||
|
testMatch: ["**/tests/e2e/**/*.[jt]s?(x)"],
|
||||||
|
modulePaths: ["<rootDir>/js/"],
|
||||||
|
testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers/", "<rootDir>/tests/e2e/mocks"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
collectCoverageFrom: ["./clientonly/**/*.js", "./js/**/*.js", "./modules/**/*.js", "./serveronly/**/*.js"],
|
||||||
|
coverageReporters: ["lcov", "text"],
|
||||||
|
coverageProvider: "v8"
|
||||||
|
};
|
||||||
|
};
|
29
js/app.js
29
js/app.js
|
@ -222,18 +222,33 @@ function App() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadModules(modules, function () {
|
loadModules(modules, async function () {
|
||||||
httpServer = new Server(config, function (app, io) {
|
httpServer = new Server(config);
|
||||||
|
const { app, io } = await httpServer.open();
|
||||||
Log.log("Server started ...");
|
Log.log("Server started ...");
|
||||||
|
|
||||||
|
const nodePromises = [];
|
||||||
for (let nodeHelper of nodeHelpers) {
|
for (let nodeHelper of nodeHelpers) {
|
||||||
nodeHelper.setExpressApp(app);
|
nodeHelper.setExpressApp(app);
|
||||||
nodeHelper.setSocketIO(io);
|
nodeHelper.setSocketIO(io);
|
||||||
nodeHelper.start();
|
|
||||||
|
try {
|
||||||
|
nodePromises.push(nodeHelper.start());
|
||||||
|
} catch (error) {
|
||||||
|
Log.error(`Error when starting node_helper for module ${nodeHelper.name}:`);
|
||||||
|
Log.error(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.log("Sockets connected & modules started ...");
|
Promise.allSettled(nodePromises).then((results) => {
|
||||||
|
// 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 ...");
|
||||||
if (typeof callback === "function") {
|
if (typeof callback === "function") {
|
||||||
callback(config);
|
callback(config);
|
||||||
}
|
}
|
||||||
|
@ -247,14 +262,16 @@ function App() {
|
||||||
* exists.
|
* exists.
|
||||||
*
|
*
|
||||||
* Added to fix #1056
|
* Added to fix #1056
|
||||||
|
*
|
||||||
|
* @param {Function} callback Function to be called after the app has stopped
|
||||||
*/
|
*/
|
||||||
this.stop = function () {
|
this.stop = function (callback) {
|
||||||
for (const nodeHelper of nodeHelpers) {
|
for (const nodeHelper of nodeHelpers) {
|
||||||
if (typeof nodeHelper.stop === "function") {
|
if (typeof nodeHelper.stop === "function") {
|
||||||
nodeHelper.stop();
|
nodeHelper.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
httpServer.close();
|
httpServer.close().then(callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -103,6 +103,20 @@ function createWindow() {
|
||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//remove response headers that prevent sites of being embedded into iframes if configured
|
||||||
|
mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||||
|
let curHeaders = details.responseHeaders;
|
||||||
|
if (config["ignoreXOriginHeader"] || false) {
|
||||||
|
curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !/x-frame-options/i.test(header[0])));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config["ignoreContentSecurityPolicy"] || false) {
|
||||||
|
curHeaders = Object.fromEntries(Object.entries(curHeaders).filter((header) => !/content-security-policy/i.test(header[0])));
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({ responseHeaders: curHeaders });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
|
|
26
js/fetch.js
26
js/fetch.js
|
@ -1,20 +1,28 @@
|
||||||
/**
|
/**
|
||||||
* fetch
|
* Helper class to provide either third party fetch library or (if node >= 18)
|
||||||
|
* return internal node fetch implementation.
|
||||||
*
|
*
|
||||||
|
* Attention: After some discussion we always return the third party
|
||||||
|
* implementation until the node implementation is stable and more tested
|
||||||
|
*
|
||||||
|
* @see https://github.com/MichMich/MagicMirror/pull/2952
|
||||||
|
* @see https://github.com/MichMich/MagicMirror/issues/2649
|
||||||
* @param {string} url to be fetched
|
* @param {string} url to be fetched
|
||||||
* @param {object} options object e.g. for headers
|
* @param {object} options object e.g. for headers
|
||||||
* @class
|
* @class
|
||||||
*/
|
*/
|
||||||
async function fetch(url, options) {
|
async function fetch(url, options = {}) {
|
||||||
const nodeVersion = process.version.match(/^v(\d+)\.*/)[1];
|
// const nodeVersion = process.version.match(/^v(\d+)\.*/)[1];
|
||||||
if (nodeVersion >= 18) {
|
// if (nodeVersion >= 18) {
|
||||||
// node version >= 18
|
// // node version >= 18
|
||||||
return global.fetch(url, options);
|
// return global.fetch(url, options);
|
||||||
} else {
|
// } else {
|
||||||
// node version < 18
|
// // node version < 18
|
||||||
|
// const nodefetch = require("node-fetch");
|
||||||
|
// return nodefetch(url, options);
|
||||||
|
// }
|
||||||
const nodefetch = require("node-fetch");
|
const nodefetch = require("node-fetch");
|
||||||
return nodefetch(url, options);
|
return nodefetch(url, options);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = fetch;
|
module.exports = fetch;
|
||||||
|
|
|
@ -70,7 +70,7 @@ const MM = (function () {
|
||||||
* Select the wrapper dom object for a specific position.
|
* Select the wrapper dom object for a specific position.
|
||||||
*
|
*
|
||||||
* @param {string} position The name of the position.
|
* @param {string} position The name of the position.
|
||||||
* @returns {HTMLElement} the wrapper element
|
* @returns {HTMLElement | void} the wrapper element
|
||||||
*/
|
*/
|
||||||
const selectWrapper = function (position) {
|
const selectWrapper = function (position) {
|
||||||
const classes = position.replace("_", " ");
|
const classes = position.replace("_", " ");
|
||||||
|
|
88
js/server.js
88
js/server.js
|
@ -5,28 +5,34 @@
|
||||||
* MIT Licensed.
|
* MIT Licensed.
|
||||||
*/
|
*/
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const app = require("express")();
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const ipfilter = require("express-ipfilter").IpFilter;
|
const ipfilter = require("express-ipfilter").IpFilter;
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const helmet = require("helmet");
|
const helmet = require("helmet");
|
||||||
const fetch = require("fetch");
|
|
||||||
|
|
||||||
const Log = require("logger");
|
const Log = require("logger");
|
||||||
const Utils = require("./utils.js");
|
const Utils = require("./utils.js");
|
||||||
|
const { cors, getConfig, getHtml, getVersion } = require("./server_functions.js");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Server
|
* Server
|
||||||
*
|
*
|
||||||
* @param {object} config The MM config
|
* @param {object} config The MM config
|
||||||
* @param {Function} callback Function called when done.
|
|
||||||
* @class
|
* @class
|
||||||
*/
|
*/
|
||||||
function Server(config, callback) {
|
function Server(config) {
|
||||||
|
const app = express();
|
||||||
const port = process.env.MM_PORT || config.port;
|
const port = process.env.MM_PORT || config.port;
|
||||||
const serverSockets = new Set();
|
const serverSockets = new Set();
|
||||||
|
|
||||||
let server = null;
|
let server = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the server for incoming connections
|
||||||
|
*
|
||||||
|
* @returns {Promise} A promise that is resolved when the server listens to connections
|
||||||
|
*/
|
||||||
|
this.open = function () {
|
||||||
|
return new Promise((resolve) => {
|
||||||
if (config.useHttps) {
|
if (config.useHttps) {
|
||||||
const options = {
|
const options = {
|
||||||
key: fs.readFileSync(config.httpsPrivateKey),
|
key: fs.readFileSync(config.httpsPrivateKey),
|
||||||
|
@ -52,7 +58,6 @@ function Server(config, callback) {
|
||||||
});
|
});
|
||||||
|
|
||||||
Log.log(`Starting server on port ${port} ... `);
|
Log.log(`Starting server on port ${port} ... `);
|
||||||
|
|
||||||
server.listen(port, config.address || "localhost");
|
server.listen(port, config.address || "localhost");
|
||||||
|
|
||||||
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
||||||
|
@ -69,72 +74,45 @@ function Server(config, callback) {
|
||||||
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
|
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.use(helmet(config.httpHeaders));
|
|
||||||
|
|
||||||
|
app.use(helmet(config.httpHeaders));
|
||||||
app.use("/js", express.static(__dirname));
|
app.use("/js", express.static(__dirname));
|
||||||
|
|
||||||
const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
|
// TODO add tests directory only when running tests?
|
||||||
|
const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs", "/tests/mocks"];
|
||||||
for (const directory of directories) {
|
for (const directory of directories) {
|
||||||
app.use(directory, express.static(path.resolve(global.root_path + directory)));
|
app.use(directory, express.static(path.resolve(global.root_path + directory)));
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("/cors", async function (req, res) {
|
app.get("/cors", async (req, res) => await cors(req, res));
|
||||||
// example: http://localhost:8080/cors?url=https://google.de
|
|
||||||
|
|
||||||
try {
|
app.get("/version", (req, res) => getVersion(req, res));
|
||||||
const reg = "^/cors.+url=(.*)";
|
|
||||||
let url = "";
|
|
||||||
|
|
||||||
let match = new RegExp(reg, "g").exec(req.url);
|
app.get("/config", (req, res) => getConfig(req, res));
|
||||||
if (!match) {
|
|
||||||
url = "invalid url: " + req.url;
|
app.get("/", (req, res) => getHtml(req, res));
|
||||||
Log.error(url);
|
|
||||||
res.send(url);
|
server.on("listening", () => {
|
||||||
} else {
|
resolve({
|
||||||
url = match[1];
|
app,
|
||||||
Log.log("cors url: " + url);
|
io
|
||||||
const response = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0 MagicMirror/" + global.version } });
|
|
||||||
const header = response.headers.get("Content-Type");
|
|
||||||
const data = await response.text();
|
|
||||||
if (header) res.set("Content-Type", header);
|
|
||||||
res.send(data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
Log.error(error);
|
|
||||||
res.send(error);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/version", function (req, res) {
|
|
||||||
res.send(global.version);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/config", function (req, res) {
|
|
||||||
res.send(config);
|
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
app.get("/", function (req, res) {
|
/**
|
||||||
let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" });
|
* Closes the server and destroys all lingering connections to it.
|
||||||
html = html.replace("#VERSION#", global.version);
|
*
|
||||||
|
* @returns {Promise} A promise that resolves when server has successfully shut down
|
||||||
let configFile = "config/config.js";
|
*/
|
||||||
if (typeof global.configuration_file !== "undefined") {
|
|
||||||
configFile = global.configuration_file;
|
|
||||||
}
|
|
||||||
html = html.replace("#CONFIG_FILE#", configFile);
|
|
||||||
|
|
||||||
res.send(html);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (typeof callback === "function") {
|
|
||||||
callback(app, io);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.close = function () {
|
this.close = function () {
|
||||||
|
return new Promise((resolve) => {
|
||||||
for (const socket of serverSockets.values()) {
|
for (const socket of serverSockets.values()) {
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
}
|
}
|
||||||
server.close();
|
server.close(resolve);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
127
js/server_functions.js
Normal file
127
js/server_functions.js
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
const fetch = require("./fetch");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const Log = require("logger");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the config.
|
||||||
|
*
|
||||||
|
* @param {Request} req - the request
|
||||||
|
* @param {Response} res - the result
|
||||||
|
*/
|
||||||
|
function getConfig(req, res) {
|
||||||
|
res.send(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A method that forewards 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
|
||||||
|
*
|
||||||
|
* Only the url-param of the input request url is required. It must be the last parameter.
|
||||||
|
*
|
||||||
|
* @param {Request} req - the request
|
||||||
|
* @param {Response} res - the result
|
||||||
|
*/
|
||||||
|
async function cors(req, res) {
|
||||||
|
try {
|
||||||
|
const urlRegEx = "url=(.+?)$";
|
||||||
|
let url = "";
|
||||||
|
|
||||||
|
const match = new RegExp(urlRegEx, "g").exec(req.url);
|
||||||
|
if (!match) {
|
||||||
|
url = "invalid url: " + req.url;
|
||||||
|
Log.error(url);
|
||||||
|
res.send(url);
|
||||||
|
} else {
|
||||||
|
url = match[1];
|
||||||
|
|
||||||
|
const headersToSend = getHeadersToSend(req.url);
|
||||||
|
const expectedRecievedHeaders = geExpectedRecievedHeaders(req.url);
|
||||||
|
|
||||||
|
Log.log("cors url: " + url);
|
||||||
|
const response = await fetch(url, { headers: headersToSend });
|
||||||
|
|
||||||
|
for (const header of expectedRecievedHeaders) {
|
||||||
|
const headerValue = response.headers.get(header);
|
||||||
|
if (header) res.set(header, headerValue);
|
||||||
|
}
|
||||||
|
const data = await response.text();
|
||||||
|
res.send(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Log.error(error);
|
||||||
|
res.send(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets headers and values to attatch to the web request.
|
||||||
|
*
|
||||||
|
* @param {string} url - The url containing the headers and values to send.
|
||||||
|
* @returns {object} An object specifying name and value of the headers.
|
||||||
|
*/
|
||||||
|
function getHeadersToSend(url) {
|
||||||
|
const headersToSend = { "User-Agent": "Mozilla/5.0 MagicMirror/" + global.version };
|
||||||
|
const headersToSendMatch = new RegExp("sendheaders=(.+?)(&|$)", "g").exec(url);
|
||||||
|
if (headersToSendMatch) {
|
||||||
|
const headers = headersToSendMatch[1].split(",");
|
||||||
|
for (const header of headers) {
|
||||||
|
const keyValue = header.split(":");
|
||||||
|
if (keyValue.length !== 2) {
|
||||||
|
throw new Error(`Invalid format for header ${header}`);
|
||||||
|
}
|
||||||
|
headersToSend[keyValue[0]] = decodeURIComponent(keyValue[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return headersToSend;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the headers expected from the response.
|
||||||
|
*
|
||||||
|
* @param {string} url - The url containing the expected headers from the response.
|
||||||
|
* @returns {string[]} headers - The name of the expected headers.
|
||||||
|
*/
|
||||||
|
function geExpectedRecievedHeaders(url) {
|
||||||
|
const expectedRecievedHeaders = ["Content-Type"];
|
||||||
|
const expectedRecievedHeadersMatch = new RegExp("expectedheaders=(.+?)(&|$)", "g").exec(url);
|
||||||
|
if (expectedRecievedHeadersMatch) {
|
||||||
|
const headers = expectedRecievedHeadersMatch[1].split(",");
|
||||||
|
for (const header of headers) {
|
||||||
|
expectedRecievedHeaders.push(header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expectedRecievedHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the HTML to display the magic mirror.
|
||||||
|
*
|
||||||
|
* @param {Request} req - the request
|
||||||
|
* @param {Response} res - the result
|
||||||
|
*/
|
||||||
|
function getHtml(req, res) {
|
||||||
|
let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" });
|
||||||
|
html = html.replace("#VERSION#", global.version);
|
||||||
|
|
||||||
|
let configFile = "config/config.js";
|
||||||
|
if (typeof global.configuration_file !== "undefined") {
|
||||||
|
configFile = global.configuration_file;
|
||||||
|
}
|
||||||
|
html = html.replace("#CONFIG_FILE#", configFile);
|
||||||
|
|
||||||
|
res.send(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the MagicMirror version.
|
||||||
|
*
|
||||||
|
* @param {Request} req - the request
|
||||||
|
* @param {Response} res - the result
|
||||||
|
*/
|
||||||
|
function getVersion(req, res) {
|
||||||
|
res.send(global.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { cors, getConfig, getHtml, getVersion };
|
|
@ -14,6 +14,7 @@ Module.register("calendar", {
|
||||||
limitDays: 0, // Limit the number of days shown, 0 = no limit
|
limitDays: 0, // Limit the number of days shown, 0 = no limit
|
||||||
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-",
|
||||||
showLocation: false,
|
showLocation: false,
|
||||||
displayRepeatingCountTitle: false,
|
displayRepeatingCountTitle: false,
|
||||||
defaultRepeatingCountTitle: "",
|
defaultRepeatingCountTitle: "",
|
||||||
|
@ -163,11 +164,10 @@ Module.register("calendar", {
|
||||||
|
|
||||||
// Override dom generator.
|
// Override dom generator.
|
||||||
getDom: function () {
|
getDom: function () {
|
||||||
// Define second, minute, hour, and day constants
|
const ONE_SECOND = 1000; // 1,000 milliseconds
|
||||||
const oneSecond = 1000; // 1,000 milliseconds
|
const ONE_MINUTE = ONE_SECOND * 60;
|
||||||
const oneMinute = oneSecond * 60;
|
const ONE_HOUR = ONE_MINUTE * 60;
|
||||||
const oneHour = oneMinute * 60;
|
const ONE_DAY = ONE_HOUR * 24;
|
||||||
const oneDay = oneHour * 24;
|
|
||||||
|
|
||||||
const events = this.createEventList(true);
|
const events = this.createEventList(true);
|
||||||
const wrapper = document.createElement("table");
|
const wrapper = document.createElement("table");
|
||||||
|
@ -205,6 +205,8 @@ Module.register("calendar", {
|
||||||
if (lastSeenDate !== dateAsString) {
|
if (lastSeenDate !== dateAsString) {
|
||||||
const dateRow = document.createElement("tr");
|
const dateRow = document.createElement("tr");
|
||||||
dateRow.className = "normal";
|
dateRow.className = "normal";
|
||||||
|
if (event.today) dateRow.className += " today";
|
||||||
|
else if (event.tomorrow) dateRow.className += " tomorrow";
|
||||||
|
|
||||||
const dateCell = document.createElement("td");
|
const dateCell = document.createElement("td");
|
||||||
dateCell.colSpan = "3";
|
dateCell.colSpan = "3";
|
||||||
|
@ -230,6 +232,8 @@ Module.register("calendar", {
|
||||||
}
|
}
|
||||||
|
|
||||||
eventWrapper.className = "normal event";
|
eventWrapper.className = "normal event";
|
||||||
|
if (event.today) eventWrapper.className += " today";
|
||||||
|
else if (event.tomorrow) eventWrapper.className += " tomorrow";
|
||||||
|
|
||||||
const symbolWrapper = document.createElement("td");
|
const symbolWrapper = document.createElement("td");
|
||||||
|
|
||||||
|
@ -244,7 +248,7 @@ Module.register("calendar", {
|
||||||
const symbols = this.symbolsForEvent(event);
|
const symbols = this.symbolsForEvent(event);
|
||||||
symbols.forEach((s, index) => {
|
symbols.forEach((s, index) => {
|
||||||
const symbol = document.createElement("span");
|
const symbol = document.createElement("span");
|
||||||
symbol.className = "fas fa-fw fa-" + s;
|
symbol.className = s;
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
symbol.style.paddingLeft = "5px";
|
symbol.style.paddingLeft = "5px";
|
||||||
}
|
}
|
||||||
|
@ -338,7 +342,7 @@ Module.register("calendar", {
|
||||||
// For full day events we use the fullDayEventDateFormat
|
// For full day events we use the fullDayEventDateFormat
|
||||||
if (event.fullDayEvent) {
|
if (event.fullDayEvent) {
|
||||||
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
||||||
event.endDate -= oneSecond;
|
event.endDate -= ONE_SECOND;
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
||||||
} else if (this.config.getRelative > 0 && event.startDate < now) {
|
} else if (this.config.getRelative > 0 && event.startDate < now) {
|
||||||
// Ongoing and getRelative is set
|
// Ongoing and getRelative is set
|
||||||
|
@ -348,7 +352,7 @@ Module.register("calendar", {
|
||||||
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * oneDay) {
|
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * ONE_DAY) {
|
||||||
// Within urgency days
|
// Within urgency days
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||||
}
|
}
|
||||||
|
@ -356,9 +360,9 @@ 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.startDate - now < oneDay && 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 * oneDay && event.startDate - now > 0) {
|
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
|
||||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||||
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||||
}
|
}
|
||||||
|
@ -384,14 +388,14 @@ 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.startDate - now < oneDay && 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 * oneDay && event.startDate - now > 0) {
|
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
|
||||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||||
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (event.startDate - now < this.config.getRelative * oneHour) {
|
} else if (event.startDate - now < this.config.getRelative * ONE_HOUR) {
|
||||||
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
|
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||||
}
|
}
|
||||||
|
@ -421,6 +425,8 @@ Module.register("calendar", {
|
||||||
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 = "normal xsmall light";
|
||||||
|
if (event.today) locationRow.className += " today";
|
||||||
|
else if (event.tomorrow) locationRow.className += " tomorrow";
|
||||||
|
|
||||||
if (this.config.displaySymbol) {
|
if (this.config.displaySymbol) {
|
||||||
const symbolCell = document.createElement("td");
|
const symbolCell = document.createElement("td");
|
||||||
|
@ -491,6 +497,11 @@ Module.register("calendar", {
|
||||||
* @returns {object[]} Array with events.
|
* @returns {object[]} Array with events.
|
||||||
*/
|
*/
|
||||||
createEventList: function (limitNumberOfEntries) {
|
createEventList: function (limitNumberOfEntries) {
|
||||||
|
const ONE_SECOND = 1000; // 1,000 milliseconds
|
||||||
|
const ONE_MINUTE = ONE_SECOND * 60;
|
||||||
|
const ONE_HOUR = ONE_MINUTE * 60;
|
||||||
|
const ONE_DAY = ONE_HOUR * 24;
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const today = moment().startOf("day");
|
const today = moment().startOf("day");
|
||||||
const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
|
const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
|
||||||
|
@ -521,19 +532,21 @@ Module.register("calendar", {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
event.url = calendarUrl;
|
event.url = calendarUrl;
|
||||||
event.today = event.startDate >= today && event.startDate < today + 24 * 60 * 60 * 1000;
|
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;
|
||||||
|
|
||||||
/* 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.
|
||||||
*/
|
*/
|
||||||
const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / (1000 * 60 * 60 * 24)) + 1;
|
const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / ONE_DAY) + 1;
|
||||||
if (this.config.sliceMultiDayEvents && maxCount > 1) {
|
if (this.config.sliceMultiDayEvents && maxCount > 1) {
|
||||||
const splitEvents = [];
|
const splitEvents = [];
|
||||||
let midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
|
let midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
|
||||||
let count = 1;
|
let count = 1;
|
||||||
while (event.endDate > midnight) {
|
while (event.endDate > midnight) {
|
||||||
const thisEvent = JSON.parse(JSON.stringify(event)); // clone object
|
const thisEvent = JSON.parse(JSON.stringify(event)); // clone object
|
||||||
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + 24 * 60 * 60 * 1000;
|
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + ONE_DAY;
|
||||||
|
thisEvent.tomorrow = !thisEvent.today && thisEvent.startDate >= today + ONE_DAY && thisEvent.startDate < today + 2 * ONE_DAY;
|
||||||
thisEvent.endDate = midnight;
|
thisEvent.endDate = midnight;
|
||||||
thisEvent.title += " (" + count + "/" + maxCount + ")";
|
thisEvent.title += " (" + count + "/" + maxCount + ")";
|
||||||
splitEvents.push(thisEvent);
|
splitEvents.push(thisEvent);
|
||||||
|
@ -544,6 +557,8 @@ Module.register("calendar", {
|
||||||
}
|
}
|
||||||
// Last day
|
// Last day
|
||||||
event.title += " (" + count + "/" + maxCount + ")";
|
event.title += " (" + count + "/" + maxCount + ")";
|
||||||
|
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;
|
||||||
splitEvents.push(event);
|
splitEvents.push(event);
|
||||||
|
|
||||||
for (let splitEvent of splitEvents) {
|
for (let splitEvent of splitEvents) {
|
||||||
|
@ -757,6 +772,11 @@ Module.register("calendar", {
|
||||||
|
|
||||||
getCalendarPropertyAsArray: function (url, property, defaultValue) {
|
getCalendarPropertyAsArray: function (url, property, defaultValue) {
|
||||||
let p = this.getCalendarProperty(url, property, defaultValue);
|
let p = this.getCalendarProperty(url, property, defaultValue);
|
||||||
|
if (property === "symbol" || property === "recurringSymbol" || property === "fullDaySymbol") {
|
||||||
|
const className = this.getCalendarProperty(url, "symbolClassName", this.config.defaultSymbolClassName);
|
||||||
|
p = className + p;
|
||||||
|
}
|
||||||
|
|
||||||
if (!(p instanceof Array)) p = [p];
|
if (!(p instanceof Array)) p = [p];
|
||||||
return p;
|
return p;
|
||||||
},
|
},
|
||||||
|
@ -794,7 +814,7 @@ Module.register("calendar", {
|
||||||
line++;
|
line++;
|
||||||
if (line > maxTitleLines - 1) {
|
if (line > maxTitleLines - 1) {
|
||||||
if (i < words.length) {
|
if (i < words.length) {
|
||||||
currentLine += "…";
|
currentLine += "…";
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -811,7 +831,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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -303,7 +303,7 @@ Module.register("clock", {
|
||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************
|
/*******************************************
|
||||||
* Update placement, respect old analogShowDate even if its not needed anymore
|
* Update placement, respect old analogShowDate even if it's not needed anymore
|
||||||
*/
|
*/
|
||||||
if (this.config.displayType === "analog") {
|
if (this.config.displayType === "analog") {
|
||||||
// Display only an analog clock
|
// Display only an analog clock
|
||||||
|
@ -311,15 +311,15 @@ Module.register("clock", {
|
||||||
wrapper.classList.add("clockGrid--bottom");
|
wrapper.classList.add("clockGrid--bottom");
|
||||||
} else if (this.config.analogShowDate === "bottom") {
|
} else if (this.config.analogShowDate === "bottom") {
|
||||||
wrapper.classList.add("clockGrid--top");
|
wrapper.classList.add("clockGrid--top");
|
||||||
} else {
|
|
||||||
//analogWrapper.style.gridArea = "center";
|
|
||||||
}
|
}
|
||||||
|
wrapper.appendChild(analogWrapper);
|
||||||
|
} else if (this.config.displayType === "digital") {
|
||||||
|
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("clockGrid--" + this.config.analogPlacement);
|
||||||
}
|
|
||||||
|
|
||||||
wrapper.appendChild(analogWrapper);
|
wrapper.appendChild(analogWrapper);
|
||||||
wrapper.appendChild(digitalWrapper);
|
wrapper.appendChild(digitalWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
// Return the wrapper to the dom.
|
// Return the wrapper to the dom.
|
||||||
return wrapper;
|
return wrapper;
|
||||||
|
|
|
@ -21,8 +21,7 @@ Module.register("compliments", {
|
||||||
morningEndTime: 12,
|
morningEndTime: 12,
|
||||||
afternoonStartTime: 12,
|
afternoonStartTime: 12,
|
||||||
afternoonEndTime: 17,
|
afternoonEndTime: 17,
|
||||||
random: true,
|
random: true
|
||||||
mockDate: null
|
|
||||||
},
|
},
|
||||||
lastIndexUsed: -1,
|
lastIndexUsed: -1,
|
||||||
// Set currentweather from module
|
// Set currentweather from module
|
||||||
|
@ -40,7 +39,7 @@ Module.register("compliments", {
|
||||||
this.lastComplimentIndex = -1;
|
this.lastComplimentIndex = -1;
|
||||||
|
|
||||||
if (this.config.remoteFile !== null) {
|
if (this.config.remoteFile !== null) {
|
||||||
this.complimentFile((response) => {
|
this.loadComplimentFile().then((response) => {
|
||||||
this.config.compliments = JSON.parse(response);
|
this.config.compliments = JSON.parse(response);
|
||||||
this.updateDom();
|
this.updateDom();
|
||||||
});
|
});
|
||||||
|
@ -85,30 +84,30 @@ Module.register("compliments", {
|
||||||
*/
|
*/
|
||||||
complimentArray: function () {
|
complimentArray: function () {
|
||||||
const hour = moment().hour();
|
const hour = moment().hour();
|
||||||
const date = this.config.mockDate ? this.config.mockDate : moment().format("YYYY-MM-DD");
|
const date = moment().format("YYYY-MM-DD");
|
||||||
let compliments;
|
let compliments = [];
|
||||||
|
|
||||||
|
// Add time of day compliments
|
||||||
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime && this.config.compliments.hasOwnProperty("morning")) {
|
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime && this.config.compliments.hasOwnProperty("morning")) {
|
||||||
compliments = this.config.compliments.morning.slice(0);
|
compliments = [...this.config.compliments.morning];
|
||||||
} else if (hour >= this.config.afternoonStartTime && hour < this.config.afternoonEndTime && this.config.compliments.hasOwnProperty("afternoon")) {
|
} else if (hour >= this.config.afternoonStartTime && hour < this.config.afternoonEndTime && this.config.compliments.hasOwnProperty("afternoon")) {
|
||||||
compliments = this.config.compliments.afternoon.slice(0);
|
compliments = [...this.config.compliments.afternoon];
|
||||||
} else if (this.config.compliments.hasOwnProperty("evening")) {
|
} else if (this.config.compliments.hasOwnProperty("evening")) {
|
||||||
compliments = this.config.compliments.evening.slice(0);
|
compliments = [...this.config.compliments.evening];
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof compliments === "undefined") {
|
|
||||||
compliments = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add compliments based on weather
|
||||||
if (this.currentWeatherType in this.config.compliments) {
|
if (this.currentWeatherType in this.config.compliments) {
|
||||||
compliments.push.apply(compliments, this.config.compliments[this.currentWeatherType]);
|
Array.prototype.push.apply(compliments, this.config.compliments[this.currentWeatherType]);
|
||||||
}
|
}
|
||||||
|
|
||||||
compliments.push.apply(compliments, this.config.compliments.anytime);
|
// Add compliments for anytime
|
||||||
|
Array.prototype.push.apply(compliments, this.config.compliments.anytime);
|
||||||
|
|
||||||
|
// Add compliments for special days
|
||||||
for (let entry in this.config.compliments) {
|
for (let entry in this.config.compliments) {
|
||||||
if (new RegExp(entry).test(date)) {
|
if (new RegExp(entry).test(date)) {
|
||||||
compliments.push.apply(compliments, this.config.compliments[entry]);
|
Array.prototype.push.apply(compliments, this.config.compliments[entry]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,20 +117,13 @@ Module.register("compliments", {
|
||||||
/**
|
/**
|
||||||
* Retrieve a file from the local filesystem
|
* Retrieve a file from the local filesystem
|
||||||
*
|
*
|
||||||
* @param {Function} callback Called when the file is retrieved.
|
* @returns {Promise} Resolved when the file is loaded
|
||||||
*/
|
*/
|
||||||
complimentFile: function (callback) {
|
loadComplimentFile: async function () {
|
||||||
const xobj = new XMLHttpRequest(),
|
const isRemote = this.config.remoteFile.indexOf("http://") === 0 || this.config.remoteFile.indexOf("https://") === 0,
|
||||||
isRemote = this.config.remoteFile.indexOf("http://") === 0 || this.config.remoteFile.indexOf("https://") === 0,
|
url = isRemote ? this.config.remoteFile : this.file(this.config.remoteFile);
|
||||||
path = isRemote ? this.config.remoteFile : this.file(this.config.remoteFile);
|
const response = await fetch(url);
|
||||||
xobj.overrideMimeType("application/json");
|
return await response.text();
|
||||||
xobj.open("GET", path, true);
|
|
||||||
xobj.onreadystatechange = function () {
|
|
||||||
if (xobj.readyState === 4 && xobj.status === 200) {
|
|
||||||
callback(xobj.responseText);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xobj.send(null);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,7 +131,7 @@ Module.register("compliments", {
|
||||||
*
|
*
|
||||||
* @returns {string} a compliment
|
* @returns {string} a compliment
|
||||||
*/
|
*/
|
||||||
randomCompliment: function () {
|
getRandomCompliment: function () {
|
||||||
// get the current time of day compliments list
|
// get the current time of day compliments list
|
||||||
const compliments = this.complimentArray();
|
const compliments = this.complimentArray();
|
||||||
// variable for index to next message to display
|
// variable for index to next message to display
|
||||||
|
@ -162,34 +154,33 @@ Module.register("compliments", {
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
wrapper.className = this.config.classes ? this.config.classes : "thin xlarge bright pre-line";
|
wrapper.className = this.config.classes ? this.config.classes : "thin xlarge bright pre-line";
|
||||||
// get the compliment text
|
// get the compliment text
|
||||||
const complimentText = this.randomCompliment();
|
const complimentText = this.getRandomCompliment();
|
||||||
// split it into parts on newline text
|
// split it into parts on newline text
|
||||||
const parts = complimentText.split("\n");
|
const parts = complimentText.split("\n");
|
||||||
// create a span to hold it all
|
// create a span to hold the compliment
|
||||||
const compliment = document.createElement("span");
|
const compliment = document.createElement("span");
|
||||||
// process all the parts of the compliment text
|
// process all the parts of the compliment text
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
|
if (part !== "") {
|
||||||
// create a text element for each part
|
// create a text element for each part
|
||||||
compliment.appendChild(document.createTextNode(part));
|
compliment.appendChild(document.createTextNode(part));
|
||||||
// add a break `
|
// add a break
|
||||||
compliment.appendChild(document.createElement("BR"));
|
compliment.appendChild(document.createElement("BR"));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// only add compliment to wrapper if there is actual text in there
|
||||||
|
if (compliment.children.length > 0) {
|
||||||
// remove the last break
|
// remove the last break
|
||||||
compliment.lastElementChild.remove();
|
compliment.lastElementChild.remove();
|
||||||
wrapper.appendChild(compliment);
|
wrapper.appendChild(compliment);
|
||||||
|
}
|
||||||
return wrapper;
|
return wrapper;
|
||||||
},
|
},
|
||||||
|
|
||||||
// From data currentweather set weather type
|
|
||||||
setCurrentWeatherType: function (type) {
|
|
||||||
this.currentWeatherType = type;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Override notification handler.
|
// Override notification handler.
|
||||||
notificationReceived: function (notification, payload, sender) {
|
notificationReceived: function (notification, payload, sender) {
|
||||||
if (notification === "CURRENTWEATHER_TYPE") {
|
if (notification === "CURRENTWEATHER_TYPE") {
|
||||||
this.setCurrentWeatherType(payload.type);
|
this.currentWeatherType = payload.type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -42,6 +42,14 @@ Module.register("newsfeed", {
|
||||||
dangerouslyDisableAutoEscaping: false
|
dangerouslyDisableAutoEscaping: false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getUrlPrefix: function (item) {
|
||||||
|
if (item.useCorsProxy) {
|
||||||
|
return location.protocol + "//" + location.host + "/cors?url=";
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Define required scripts.
|
// Define required scripts.
|
||||||
getScripts: function () {
|
getScripts: function () {
|
||||||
return ["moment.js"];
|
return ["moment.js"];
|
||||||
|
@ -142,14 +150,19 @@ Module.register("newsfeed", {
|
||||||
sourceTitle: item.sourceTitle,
|
sourceTitle: item.sourceTitle,
|
||||||
publishDate: moment(new Date(item.pubdate)).fromNow(),
|
publishDate: moment(new Date(item.pubdate)).fromNow(),
|
||||||
title: item.title,
|
title: item.title,
|
||||||
url: item.url,
|
url: this.getUrlPrefix(item) + item.url,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
items: items
|
items: items
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getActiveItemURL: function () {
|
getActiveItemURL: function () {
|
||||||
return typeof this.newsItems[this.activeItem].url === "string" ? this.newsItems[this.activeItem].url : this.newsItems[this.activeItem].url.href;
|
const item = this.newsItems[this.activeItem];
|
||||||
|
if (item) {
|
||||||
|
return typeof item.url === "string" ? this.getUrlPrefix(item) + item.url : this.getUrlPrefix(item) + item.url.href;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -18,9 +18,10 @@ const stream = require("stream");
|
||||||
* @param {number} reloadInterval Reload interval in milliseconds.
|
* @param {number} reloadInterval Reload interval in milliseconds.
|
||||||
* @param {string} encoding Encoding of the feed.
|
* @param {string} encoding Encoding of the feed.
|
||||||
* @param {boolean} logFeedWarnings If true log warnings when there is an error parsing a news article.
|
* @param {boolean} logFeedWarnings If true log warnings when there is an error parsing a news article.
|
||||||
|
* @param {boolean} useCorsProxy If true cors proxy is used for article url's.
|
||||||
* @class
|
* @class
|
||||||
*/
|
*/
|
||||||
const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
|
const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings, useCorsProxy) {
|
||||||
let reloadTimer = null;
|
let reloadTimer = null;
|
||||||
let items = [];
|
let items = [];
|
||||||
|
|
||||||
|
@ -57,7 +58,8 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||||
title: title,
|
title: title,
|
||||||
description: description,
|
description: description,
|
||||||
pubdate: pubdate,
|
pubdate: pubdate,
|
||||||
url: url
|
url: url,
|
||||||
|
useCorsProxy: useCorsProxy
|
||||||
});
|
});
|
||||||
} else if (logFeedWarnings) {
|
} else if (logFeedWarnings) {
|
||||||
Log.warn("Can't parse feed item:");
|
Log.warn("Can't parse feed item:");
|
||||||
|
|
|
@ -34,6 +34,8 @@ module.exports = NodeHelper.create({
|
||||||
const url = feed.url || "";
|
const url = feed.url || "";
|
||||||
const encoding = feed.encoding || "UTF-8";
|
const encoding = feed.encoding || "UTF-8";
|
||||||
const reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
|
const reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
|
||||||
|
let useCorsProxy = feed.useCorsProxy;
|
||||||
|
if (useCorsProxy === undefined) useCorsProxy = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
new URL(url);
|
new URL(url);
|
||||||
|
@ -46,7 +48,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);
|
fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings, useCorsProxy);
|
||||||
|
|
||||||
fetcher.onReceive(() => {
|
fetcher.onReceive(() => {
|
||||||
this.broadcastFeeds();
|
this.broadcastFeeds();
|
||||||
|
|
|
@ -92,20 +92,18 @@ class GitHelper {
|
||||||
// examples for status:
|
// examples for status:
|
||||||
// ## develop...origin/develop
|
// ## develop...origin/develop
|
||||||
// ## master...origin/master [behind 8]
|
// ## master...origin/master [behind 8]
|
||||||
status = status.match(/(?![.#])([^.]*)/g);
|
// ## master...origin/master [ahead 8, behind 1]
|
||||||
|
status = status.match(/## (.*)\.\.\.([^ ]*)(?: .*behind (\d+))?/);
|
||||||
// examples for status:
|
// examples for status:
|
||||||
// [ ' develop', 'origin/develop', '' ]
|
// [ '## develop...origin/develop', 'develop', 'origin/develop' ]
|
||||||
// [ ' master', 'origin/master [behind 8]', '' ]
|
// [ '## master...origin/master [behind 8]', 'master', 'origin/master', '8' ]
|
||||||
gitInfo.current = status[0].trim();
|
// [ '## master...origin/master [ahead 8, behind 1]', 'master', 'origin/master', '1' ]
|
||||||
status = status[1].split(" ");
|
gitInfo.current = status[1];
|
||||||
// examples for status:
|
gitInfo.tracking = status[2];
|
||||||
// [ 'origin/develop' ]
|
|
||||||
// [ 'origin/master', '[behind', '8]' ]
|
|
||||||
gitInfo.tracking = status[0].trim();
|
|
||||||
|
|
||||||
if (status[2]) {
|
if (status[3]) {
|
||||||
// git fetch was already called before so `git status -sb` delivers already the behind number
|
// git fetch was already called before so `git status -sb` delivers already the behind number
|
||||||
gitInfo.behind = parseInt(status[2].substring(0, status[2].length - 1));
|
gitInfo.behind = parseInt(status[3]);
|
||||||
gitInfo.isBehindInStatus = true;
|
gitInfo.isBehindInStatus = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
147
modules/default/utils.js
Normal file
147
modules/default/utils.js
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
/**
|
||||||
|
* A function to make HTTP requests via the server to avoid CORS-errors.
|
||||||
|
*
|
||||||
|
* @param {string} url the url to fetch from
|
||||||
|
* @param {string} type what contenttype to expect in the response, can be "json" or "xml"
|
||||||
|
* @param {boolean} useCorsProxy A flag to indicate
|
||||||
|
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||||
|
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||||
|
* @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).
|
||||||
|
*/
|
||||||
|
async function performWebRequest(url, type = "json", useCorsProxy = false, requestHeaders = undefined, expectedResponseHeaders = undefined) {
|
||||||
|
const request = {};
|
||||||
|
if (useCorsProxy) {
|
||||||
|
url = getCorsUrl(url, requestHeaders, expectedResponseHeaders);
|
||||||
|
} else {
|
||||||
|
request.headers = getHeadersToSend(requestHeaders);
|
||||||
|
}
|
||||||
|
const response = await fetch(url, request);
|
||||||
|
const data = await response.text();
|
||||||
|
|
||||||
|
if (type === "xml") {
|
||||||
|
return new DOMParser().parseFromString(data, "text/html");
|
||||||
|
} else {
|
||||||
|
if (!data || !data.length > 0) return undefined;
|
||||||
|
|
||||||
|
const dataResponse = JSON.parse(data);
|
||||||
|
if (!dataResponse.headers) {
|
||||||
|
dataResponse.headers = getHeadersFromResponse(expectedResponseHeaders, response);
|
||||||
|
}
|
||||||
|
return dataResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a URL that will be used when calling the CORS-method on the server.
|
||||||
|
*
|
||||||
|
* @param {string} url the url to fetch from
|
||||||
|
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||||
|
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||||
|
* @returns {string} to be used as URL when calling CORS-method on server.
|
||||||
|
*/
|
||||||
|
const getCorsUrl = function (url, requestHeaders, expectedResponseHeaders) {
|
||||||
|
if (!url || url.length < 1) {
|
||||||
|
throw new Error(`Invalid URL: ${url}`);
|
||||||
|
} else {
|
||||||
|
let corsUrl = `${location.protocol}//${location.host}/cors?`;
|
||||||
|
|
||||||
|
const requestHeaderString = getRequestHeaderString(requestHeaders);
|
||||||
|
if (requestHeaderString) corsUrl = `${corsUrl}sendheaders=${requestHeaderString}`;
|
||||||
|
|
||||||
|
const expectedResponseHeadersString = getExpectedResponseHeadersString(expectedResponseHeaders);
|
||||||
|
if (requestHeaderString && expectedResponseHeadersString) {
|
||||||
|
corsUrl = `${corsUrl}&expectedheaders=${expectedResponseHeadersString}`;
|
||||||
|
} else if (expectedResponseHeadersString) {
|
||||||
|
corsUrl = `${corsUrl}expectedheaders=${expectedResponseHeadersString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestHeaderString || expectedResponseHeadersString) {
|
||||||
|
return `${corsUrl}&url=${url}`;
|
||||||
|
}
|
||||||
|
return `${corsUrl}url=${url}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the part of the CORS URL that represents the HTTP headers to send.
|
||||||
|
*
|
||||||
|
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||||
|
* @returns {string} to be used as request-headers component in CORS URL.
|
||||||
|
*/
|
||||||
|
const getRequestHeaderString = function (requestHeaders) {
|
||||||
|
let requestHeaderString = "";
|
||||||
|
if (requestHeaders) {
|
||||||
|
for (const header of requestHeaders) {
|
||||||
|
if (requestHeaderString.length === 0) {
|
||||||
|
requestHeaderString = `${header.name}:${encodeURIComponent(header.value)}`;
|
||||||
|
} else {
|
||||||
|
requestHeaderString = `${requestHeaderString},${header.name}:${encodeURIComponent(header.value)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requestHeaderString;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets headers and values to attatch to the web request.
|
||||||
|
*
|
||||||
|
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||||
|
* @returns {object} An object specifying name and value of the headers.
|
||||||
|
*/
|
||||||
|
const getHeadersToSend = (requestHeaders) => {
|
||||||
|
const headersToSend = {};
|
||||||
|
if (requestHeaders) {
|
||||||
|
for (const header of requestHeaders) {
|
||||||
|
headersToSend[header.name] = header.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return headersToSend;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the part of the CORS URL that represents the expected HTTP headers to recieve.
|
||||||
|
*
|
||||||
|
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||||
|
* @returns {string} to be used as the expected HTTP-headers component in CORS URL.
|
||||||
|
*/
|
||||||
|
const getExpectedResponseHeadersString = function (expectedResponseHeaders) {
|
||||||
|
let expectedResponseHeadersString = "";
|
||||||
|
if (expectedResponseHeaders) {
|
||||||
|
for (const header of expectedResponseHeaders) {
|
||||||
|
if (expectedResponseHeadersString.length === 0) {
|
||||||
|
expectedResponseHeadersString = `${header}`;
|
||||||
|
} else {
|
||||||
|
expectedResponseHeadersString = `${expectedResponseHeadersString},${header}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expectedResponseHeaders;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the values for the expected headers from the response.
|
||||||
|
*
|
||||||
|
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||||
|
* @param {Response} response the HTTP response
|
||||||
|
* @returns {string} to be used as the expected HTTP-headers component in CORS URL.
|
||||||
|
*/
|
||||||
|
const getHeadersFromResponse = (expectedResponseHeaders, response) => {
|
||||||
|
const responseHeaders = [];
|
||||||
|
|
||||||
|
if (expectedResponseHeaders) {
|
||||||
|
for (const header of expectedResponseHeaders) {
|
||||||
|
const headerValue = response.headers.get(header);
|
||||||
|
responseHeaders.push({ name: header, value: headerValue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseHeaders;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof module !== "undefined")
|
||||||
|
module.exports = {
|
||||||
|
performWebRequest
|
||||||
|
};
|
|
@ -3,15 +3,7 @@
|
||||||
<div class="normal medium">
|
<div class="normal medium">
|
||||||
<span class="wi wi-strong-wind dimmed"></span>
|
<span class="wi wi-strong-wind dimmed"></span>
|
||||||
<span>
|
<span>
|
||||||
{% if config.useBeaufort %}
|
{{ current.windSpeed | unit("wind") | round }}
|
||||||
{{ current.beaufortWindSpeed() | round }}
|
|
||||||
{% else %}
|
|
||||||
{% if config.useKmh %}
|
|
||||||
{{ current.kmhWindSpeed() | round }}
|
|
||||||
{% else %}
|
|
||||||
{{ current.windSpeed | round }}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% if config.showWindDirection %}
|
{% if config.showWindDirection %}
|
||||||
<sup>
|
<sup>
|
||||||
{% if config.showWindDirectionAsArrow %}
|
{% if config.showWindDirectionAsArrow %}
|
||||||
|
|
|
@ -26,11 +26,6 @@ WeatherProvider.register("darksky", {
|
||||||
lon: 0
|
lon: 0
|
||||||
},
|
},
|
||||||
|
|
||||||
units: {
|
|
||||||
imperial: "us",
|
|
||||||
metric: "si"
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchCurrentWeather() {
|
fetchCurrentWeather() {
|
||||||
this.fetchData(this.getUrl())
|
this.fetchData(this.getUrl())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
@ -67,13 +62,12 @@ WeatherProvider.register("darksky", {
|
||||||
|
|
||||||
// Create a URL from the config and base URL.
|
// Create a URL from the config and base URL.
|
||||||
getUrl() {
|
getUrl() {
|
||||||
const units = this.units[this.config.units] || "auto";
|
return `${this.config.apiBase}${this.config.weatherEndpoint}/${this.config.apiKey}/${this.config.lat},${this.config.lon}?units=si&lang=${this.config.lang}`;
|
||||||
return `${this.config.apiBase}${this.config.weatherEndpoint}/${this.config.apiKey}/${this.config.lat},${this.config.lon}?units=${units}&lang=${this.config.lang}`;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Implement WeatherDay generator.
|
// Implement WeatherDay generator.
|
||||||
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
const currentWeather = new WeatherObject();
|
||||||
|
|
||||||
currentWeather.date = moment();
|
currentWeather.date = moment();
|
||||||
currentWeather.humidity = parseFloat(currentWeatherData.currently.humidity);
|
currentWeather.humidity = parseFloat(currentWeatherData.currently.humidity);
|
||||||
|
@ -81,8 +75,8 @@ WeatherProvider.register("darksky", {
|
||||||
currentWeather.windSpeed = parseFloat(currentWeatherData.currently.windSpeed);
|
currentWeather.windSpeed = parseFloat(currentWeatherData.currently.windSpeed);
|
||||||
currentWeather.windDirection = currentWeatherData.currently.windBearing;
|
currentWeather.windDirection = currentWeatherData.currently.windBearing;
|
||||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.currently.icon);
|
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.currently.icon);
|
||||||
currentWeather.sunrise = moment(currentWeatherData.daily.data[0].sunriseTime, "X");
|
currentWeather.sunrise = moment.unix(currentWeatherData.daily.data[0].sunriseTime);
|
||||||
currentWeather.sunset = moment(currentWeatherData.daily.data[0].sunsetTime, "X");
|
currentWeather.sunset = moment.unix(currentWeatherData.daily.data[0].sunsetTime);
|
||||||
|
|
||||||
return currentWeather;
|
return currentWeather;
|
||||||
},
|
},
|
||||||
|
@ -91,9 +85,9 @@ WeatherProvider.register("darksky", {
|
||||||
const days = [];
|
const days = [];
|
||||||
|
|
||||||
for (const forecast of forecasts) {
|
for (const forecast of forecasts) {
|
||||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
const weather = new WeatherObject();
|
||||||
|
|
||||||
weather.date = moment(forecast.time, "X");
|
weather.date = moment.unix(forecast.time);
|
||||||
weather.minTemperature = forecast.temperatureMin;
|
weather.minTemperature = forecast.temperatureMin;
|
||||||
weather.maxTemperature = forecast.temperatureMax;
|
weather.maxTemperature = forecast.temperatureMax;
|
||||||
weather.weatherType = this.convertWeatherType(forecast.icon);
|
weather.weatherType = this.convertWeatherType(forecast.icon);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global WeatherProvider, WeatherObject */
|
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||||
|
|
||||||
/* MagicMirror²
|
/* MagicMirror²
|
||||||
* Module: Weather
|
* Module: Weather
|
||||||
|
@ -11,13 +11,13 @@
|
||||||
* https://dd.weather.gc.ca/citypage_weather/schema/
|
* https://dd.weather.gc.ca/citypage_weather/schema/
|
||||||
* https://eccc-msc.github.io/open-data/msc-datamart/readme_en/
|
* https://eccc-msc.github.io/open-data/msc-datamart/readme_en/
|
||||||
*
|
*
|
||||||
* This module supports Canadian locations only and requires 2 additional config parms:
|
* This module supports Canadian locations only and requires 2 additional config parameters:
|
||||||
*
|
*
|
||||||
* siteCode - the city/town unique identifier for which weather is to be displayed. Format is 's0000000'.
|
* siteCode - the city/town unique identifier for which weather is to be displayed. Format is 's0000000'.
|
||||||
*
|
*
|
||||||
* provCode - the 2-character province code for the selected city/town.
|
* provCode - the 2-character province code for the selected city/town.
|
||||||
*
|
*
|
||||||
* Example: for Toronto, Ontario, the following parms would be used
|
* Example: for Toronto, Ontario, the following parameters would be used
|
||||||
*
|
*
|
||||||
* siteCode: 's0000458',
|
* siteCode: 's0000458',
|
||||||
* provCode: 'ON'
|
* provCode: 'ON'
|
||||||
|
@ -64,17 +64,13 @@ WeatherProvider.register("envcanada", {
|
||||||
start: function () {
|
start: function () {
|
||||||
Log.info(`Weather provider: ${this.providerName} started.`);
|
Log.info(`Weather provider: ${this.providerName} started.`);
|
||||||
this.setFetchedLocation(this.config.location);
|
this.setFetchedLocation(this.config.location);
|
||||||
|
|
||||||
// Ensure kmH are ignored since these are custom-handled by this Provider
|
|
||||||
|
|
||||||
this.config.useKmh = false;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
// Override the fetchCurrentWeather method to query EC and construct a Current weather object
|
// Override the fetchCurrentWeather method to query EC and construct a Current weather object
|
||||||
//
|
//
|
||||||
fetchCurrentWeather() {
|
fetchCurrentWeather() {
|
||||||
this.fetchData(this.getUrl(), "GET", "xml")
|
this.fetchData(this.getUrl(), "xml")
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
// Did not receive usable new data.
|
// Did not receive usable new data.
|
||||||
|
@ -94,7 +90,7 @@ WeatherProvider.register("envcanada", {
|
||||||
// Override the fetchWeatherForecast method to query EC and construct Forecast weather objects
|
// Override the fetchWeatherForecast method to query EC and construct Forecast weather objects
|
||||||
//
|
//
|
||||||
fetchWeatherForecast() {
|
fetchWeatherForecast() {
|
||||||
this.fetchData(this.getUrl(), "GET", "xml")
|
this.fetchData(this.getUrl(), "xml")
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
// Did not receive usable new data.
|
// Did not receive usable new data.
|
||||||
|
@ -114,7 +110,7 @@ WeatherProvider.register("envcanada", {
|
||||||
// Override the fetchWeatherHourly method to query EC and construct Forecast weather objects
|
// Override the fetchWeatherHourly method to query EC and construct Forecast weather objects
|
||||||
//
|
//
|
||||||
fetchWeatherHourly() {
|
fetchWeatherHourly() {
|
||||||
this.fetchData(this.getUrl(), "GET", "xml")
|
this.fetchData(this.getUrl(), "xml")
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
// Did not receive usable new data.
|
// Did not receive usable new data.
|
||||||
|
@ -137,8 +133,8 @@ WeatherProvider.register("envcanada", {
|
||||||
//////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
//
|
//
|
||||||
// Build the EC URL based on the Site Code and Province Code specified in the config parms. Note that the
|
// Build the EC URL based on the Site Code and Province Code specified in the config params. Note that the
|
||||||
// URL defaults to the Englsih version simply because there is no language dependancy in the data
|
// URL defaults to the English version simply because there is no language dependency in the data
|
||||||
// being accessed. This is only pertinent when using the EC data elements that contain a textual forecast.
|
// being accessed. This is only pertinent when using the EC data elements that contain a textual forecast.
|
||||||
//
|
//
|
||||||
getUrl() {
|
getUrl() {
|
||||||
|
@ -150,7 +146,7 @@ WeatherProvider.register("envcanada", {
|
||||||
//
|
//
|
||||||
|
|
||||||
generateWeatherObjectFromCurrentWeather(ECdoc) {
|
generateWeatherObjectFromCurrentWeather(ECdoc) {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const currentWeather = new WeatherObject();
|
||||||
|
|
||||||
// There are instances where EC will update weather data and current temperature will not be
|
// There are instances where EC will update weather data and current temperature will not be
|
||||||
// provided. While this is a defect in the EC systems, we need to accommodate to avoid a current temp
|
// provided. While this is a defect in the EC systems, we need to accommodate to avoid a current temp
|
||||||
|
@ -161,13 +157,13 @@ WeatherProvider.register("envcanada", {
|
||||||
// EC finds no current temp. In this scenario, MM will end up displaying a current temp of null;
|
// EC finds no current temp. In this scenario, MM will end up displaying a current temp of null;
|
||||||
|
|
||||||
if (ECdoc.querySelector("siteData currentConditions temperature").textContent) {
|
if (ECdoc.querySelector("siteData currentConditions temperature").textContent) {
|
||||||
currentWeather.temperature = this.convertTemp(ECdoc.querySelector("siteData currentConditions temperature").textContent);
|
currentWeather.temperature = ECdoc.querySelector("siteData currentConditions temperature").textContent;
|
||||||
this.cacheCurrentTemp = currentWeather.temperature;
|
this.cacheCurrentTemp = currentWeather.temperature;
|
||||||
} else {
|
} else {
|
||||||
currentWeather.temperature = this.cacheCurrentTemp;
|
currentWeather.temperature = this.cacheCurrentTemp;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentWeather.windSpeed = this.convertWind(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.windDirection = ECdoc.querySelector("siteData currentConditions wind bearing").textContent;
|
||||||
|
|
||||||
|
@ -190,11 +186,11 @@ WeatherProvider.register("envcanada", {
|
||||||
currentWeather.feelsLikeTemp = currentWeather.temperature;
|
currentWeather.feelsLikeTemp = currentWeather.temperature;
|
||||||
|
|
||||||
if (ECdoc.querySelector("siteData currentConditions windChill")) {
|
if (ECdoc.querySelector("siteData currentConditions windChill")) {
|
||||||
currentWeather.feelsLikeTemp = this.convertTemp(ECdoc.querySelector("siteData currentConditions windChill").textContent);
|
currentWeather.feelsLikeTemp = ECdoc.querySelector("siteData currentConditions windChill").textContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ECdoc.querySelector("siteData currentConditions humidex")) {
|
if (ECdoc.querySelector("siteData currentConditions humidex")) {
|
||||||
currentWeather.feelsLikeTemp = this.convertTemp(ECdoc.querySelector("siteData currentConditions humidex").textContent);
|
currentWeather.feelsLikeTemp = ECdoc.querySelector("siteData currentConditions humidex").textContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,7 +221,7 @@ WeatherProvider.register("envcanada", {
|
||||||
|
|
||||||
const days = [];
|
const days = [];
|
||||||
|
|
||||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const weather = new WeatherObject();
|
||||||
|
|
||||||
const foreBaseDates = ECdoc.querySelectorAll("siteData forecastGroup dateTime");
|
const foreBaseDates = ECdoc.querySelectorAll("siteData forecastGroup dateTime");
|
||||||
const baseDate = foreBaseDates[1].querySelector("timeStamp").textContent;
|
const baseDate = foreBaseDates[1].querySelector("timeStamp").textContent;
|
||||||
|
@ -326,7 +322,7 @@ WeatherProvider.register("envcanada", {
|
||||||
days.push(weather);
|
days.push(weather);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Now do the the rest of the forecast starting at nextDay. We will process each day using 2 EC
|
// Now do the rest of the forecast starting at nextDay. We will process each day using 2 EC
|
||||||
// forecast Elements. This will address the fact that the EC forecast always includes Today and
|
// forecast Elements. This will address the fact that the EC forecast always includes Today and
|
||||||
// Tonight for each day. This is why we iterate through the forecast by a a count of 2, with each
|
// Tonight for each day. This is why we iterate through the forecast by a a count of 2, with each
|
||||||
// iteration looking at the current Element and the next Element.
|
// iteration looking at the current Element and the next Element.
|
||||||
|
@ -335,12 +331,12 @@ WeatherProvider.register("envcanada", {
|
||||||
let lastDate = moment(baseDate, "YYYYMMDDhhmmss");
|
let lastDate = moment(baseDate, "YYYYMMDDhhmmss");
|
||||||
|
|
||||||
for (let stepDay = nextDay; stepDay < lastDay; stepDay += 2) {
|
for (let stepDay = nextDay; stepDay < lastDay; stepDay += 2) {
|
||||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
let weather = new WeatherObject();
|
||||||
|
|
||||||
// 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(lastDate, "X");
|
weather.date = moment.unix(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
|
||||||
|
@ -389,17 +385,17 @@ WeatherProvider.register("envcanada", {
|
||||||
const hourGroup = ECdoc.querySelectorAll("siteData hourlyForecastGroup hourlyForecast");
|
const hourGroup = ECdoc.querySelectorAll("siteData hourlyForecastGroup hourlyForecast");
|
||||||
|
|
||||||
for (let stepHour = 0; stepHour < 24; stepHour += 1) {
|
for (let stepHour = 0; stepHour < 24; stepHour += 1) {
|
||||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const weather = new WeatherObject();
|
||||||
|
|
||||||
// Determine local time by applying UTC offset to the forecast timestamp
|
// Determine local time by applying UTC offset to the forecast timestamp
|
||||||
|
|
||||||
const foreTime = moment(hourGroup[stepHour].getAttribute("dateTimeUTC"), "YYYYMMDDhhmmss");
|
const foreTime = moment(hourGroup[stepHour].getAttribute("dateTimeUTC"), "YYYYMMDDhhmmss");
|
||||||
const currTime = foreTime.add(hourOffset, "hours");
|
const currTime = foreTime.add(hourOffset, "hours");
|
||||||
weather.date = moment(currTime, "X");
|
weather.date = moment.unix(currTime);
|
||||||
|
|
||||||
// Capture the temperature
|
// Capture the temperature
|
||||||
|
|
||||||
weather.temperature = this.convertTemp(hourGroup[stepHour].querySelector("temperature").textContent);
|
weather.temperature = hourGroup[stepHour].querySelector("temperature").textContent;
|
||||||
|
|
||||||
// Capture Likelihood of Precipitation (LOP) and unit-of-measure values
|
// Capture Likelihood of Precipitation (LOP) and unit-of-measure values
|
||||||
|
|
||||||
|
@ -450,7 +446,7 @@ WeatherProvider.register("envcanada", {
|
||||||
weather.minTemperature = this.todayTempCacheMin;
|
weather.minTemperature = this.todayTempCacheMin;
|
||||||
weather.maxTemperature = this.todayTempCacheMax;
|
weather.maxTemperature = this.todayTempCacheMax;
|
||||||
} else {
|
} else {
|
||||||
weather.minTemperature = this.convertTemp(currentTemp);
|
weather.minTemperature = currentTemp;
|
||||||
weather.maxTemperature = weather.minTemperature;
|
weather.maxTemperature = weather.minTemperature;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -463,14 +459,14 @@ WeatherProvider.register("envcanada", {
|
||||||
//
|
//
|
||||||
|
|
||||||
if (todayClass === "low") {
|
if (todayClass === "low") {
|
||||||
weather.minTemperature = this.convertTemp(todayTemp);
|
weather.minTemperature = todayTemp;
|
||||||
if (today === 0 && fullDay === true) {
|
if (today === 0 && fullDay === true) {
|
||||||
this.todayTempCacheMin = weather.minTemperature;
|
this.todayTempCacheMin = weather.minTemperature;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (todayClass === "high") {
|
if (todayClass === "high") {
|
||||||
weather.maxTemperature = this.convertTemp(todayTemp);
|
weather.maxTemperature = todayTemp;
|
||||||
if (today === 0 && fullDay === true) {
|
if (today === 0 && fullDay === true) {
|
||||||
this.todayTempCacheMax = weather.maxTemperature;
|
this.todayTempCacheMax = weather.maxTemperature;
|
||||||
}
|
}
|
||||||
|
@ -482,11 +478,11 @@ WeatherProvider.register("envcanada", {
|
||||||
|
|
||||||
if (fullDay === true) {
|
if (fullDay === true) {
|
||||||
if (nextClass === "low") {
|
if (nextClass === "low") {
|
||||||
weather.minTemperature = this.convertTemp(nextTemp);
|
weather.minTemperature = nextTemp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextClass === "high") {
|
if (nextClass === "high") {
|
||||||
weather.maxTemperature = this.convertTemp(nextTemp);
|
weather.maxTemperature = nextTemp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -536,31 +532,6 @@ WeatherProvider.register("envcanada", {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
|
||||||
// Unit conversions
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// Convert C to F temps
|
|
||||||
//
|
|
||||||
convertTemp(temp) {
|
|
||||||
if (this.config.tempUnits === "imperial") {
|
|
||||||
return 1.8 * temp + 32;
|
|
||||||
} else {
|
|
||||||
return temp;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
|
||||||
// Convert km/h to mph
|
|
||||||
//
|
|
||||||
convertWind(kilo) {
|
|
||||||
if (this.config.windUnits === "imperial") {
|
|
||||||
return kilo / 1.609344;
|
|
||||||
} else {
|
|
||||||
return kilo;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Convert the icons to a more usable name.
|
// Convert the icons to a more usable name.
|
||||||
//
|
//
|
||||||
|
|
537
modules/default/weather/providers/openmeteo.js
Normal file
537
modules/default/weather/providers/openmeteo.js
Normal file
|
@ -0,0 +1,537 @@
|
||||||
|
/* global WeatherProvider, WeatherObject */
|
||||||
|
|
||||||
|
/* MagicMirror²
|
||||||
|
* Module: Weather
|
||||||
|
* Provider: Open-Meteo
|
||||||
|
*
|
||||||
|
* By Andrés Vanegas
|
||||||
|
* MIT Licensed
|
||||||
|
*
|
||||||
|
* This class is a provider for Open-Meteo, based on Andrew Pometti's class
|
||||||
|
* for Weatherbit.
|
||||||
|
*/
|
||||||
|
// https://www.bigdatacloud.com/docs/api/free-reverse-geocode-to-city-api
|
||||||
|
const GEOCODE_BASE = "https://api.bigdatacloud.net/data/reverse-geocode-client";
|
||||||
|
const OPEN_METEO_BASE = "https://api.open-meteo.com/v1";
|
||||||
|
|
||||||
|
WeatherProvider.register("openmeteo", {
|
||||||
|
// Set the name of the provider.
|
||||||
|
// Not strictly required, but helps for debugging.
|
||||||
|
providerName: "Open-Meteo",
|
||||||
|
|
||||||
|
// Set the default config properties that is specific to this provider
|
||||||
|
defaults: {
|
||||||
|
apiBase: OPEN_METEO_BASE,
|
||||||
|
lat: 0,
|
||||||
|
lon: 0,
|
||||||
|
past_days: 0,
|
||||||
|
type: "current"
|
||||||
|
},
|
||||||
|
|
||||||
|
// https://open-meteo.com/en/docs
|
||||||
|
hourlyParams: [
|
||||||
|
// Air temperature at 2 meters above ground
|
||||||
|
"temperature_2m",
|
||||||
|
// Relative humidity at 2 meters above ground
|
||||||
|
"relativehumidity_2m",
|
||||||
|
// Dew point temperature at 2 meters above ground
|
||||||
|
"dewpoint_2m",
|
||||||
|
// Apparent temperature is the perceived feels-like temperature combining wind chill factor, relative humidity and solar radiation
|
||||||
|
"apparent_temperature",
|
||||||
|
// Atmospheric air pressure reduced to mean sea level (msl) or pressure at surface. Typically pressure on mean sea level is used in meteorology. Surface pressure gets lower with increasing elevation.
|
||||||
|
"pressure_msl",
|
||||||
|
"surface_pressure",
|
||||||
|
// Total cloud cover as an area fraction
|
||||||
|
"cloudcover",
|
||||||
|
// Low level clouds and fog up to 3 km altitude
|
||||||
|
"cloudcover_low",
|
||||||
|
// Mid level clouds from 3 to 8 km altitude
|
||||||
|
"cloudcover_mid",
|
||||||
|
// High level clouds from 8 km altitude
|
||||||
|
"cloudcover_high",
|
||||||
|
// Wind speed at 10, 80, 120 or 180 meters above ground. Wind speed on 10 meters is the standard level.
|
||||||
|
"windspeed_10m",
|
||||||
|
"windspeed_80m",
|
||||||
|
"windspeed_120m",
|
||||||
|
"windspeed_180m",
|
||||||
|
// Wind direction at 10, 80, 120 or 180 meters above ground
|
||||||
|
"winddirection_10m",
|
||||||
|
"winddirection_80m",
|
||||||
|
"winddirection_120m",
|
||||||
|
"winddirection_180m",
|
||||||
|
// Gusts at 10 meters above ground as a maximum of the preceding hour
|
||||||
|
"windgusts_10m",
|
||||||
|
// Shortwave solar radiation as average of the preceding hour. This is equal to the total global horizontal irradiation
|
||||||
|
"shortwave_radiation",
|
||||||
|
// Direct solar radiation as average of the preceding hour on the horizontal plane and the normal plane (perpendicular to the sun)
|
||||||
|
"direct_radiation",
|
||||||
|
"direct_normal_irradiance",
|
||||||
|
// Diffuse solar radiation as average of the preceding hour
|
||||||
|
"diffuse_radiation",
|
||||||
|
// Vapor Pressure Deificit (VPD) in kilopascal (kPa). For high VPD (>1.6), water transpiration of plants increases. For low VPD (<0.4), transpiration decreases
|
||||||
|
"vapor_pressure_deficit",
|
||||||
|
// Evapotranspration from land surface and plants that weather models assumes for this location. Available soil water is considered. 1 mm evapotranspiration per hour equals 1 liter of water per spare meter.
|
||||||
|
"evapotranspiration",
|
||||||
|
// ET₀ Reference Evapotranspiration of a well watered grass field. Based on FAO-56 Penman-Monteith equations ET₀ is calculated from temperature, wind speed, humidity and solar radiation. Unlimited soil water is assumed. ET₀ is commonly used to estimate the required irrigation for plants.
|
||||||
|
"et0_fao_evapotranspiration",
|
||||||
|
// Total precipitation (rain, showers, snow) sum of the preceding hour
|
||||||
|
"precipitation",
|
||||||
|
// Snowfall amount of the preceding hour in centimeters. For the water equivalent in millimeter, divide by 7. E.g. 7 cm snow = 10 mm precipitation water equivalent
|
||||||
|
"snowfall",
|
||||||
|
// Rain from large scale weather systems of the preceding hour in millimeter
|
||||||
|
"rain",
|
||||||
|
// Showers from convective precipitation in millimeters from the preceding hour
|
||||||
|
"showers",
|
||||||
|
// Weather condition as a numeric code. Follow WMO weather interpretation codes.
|
||||||
|
"weathercode",
|
||||||
|
// Snow depth on the ground
|
||||||
|
"snow_depth",
|
||||||
|
// Altitude above sea level of the 0°C level
|
||||||
|
"freezinglevel_height",
|
||||||
|
// Temperature in the soil at 0, 6, 18 and 54 cm depths. 0 cm is the surface temperature on land or water surface temperature on water.
|
||||||
|
"soil_temperature_0cm",
|
||||||
|
"soil_temperature_6cm",
|
||||||
|
"soil_temperature_18cm",
|
||||||
|
"soil_temperature_54cm",
|
||||||
|
// Average soil water content as volumetric mixing ratio at 0-1, 1-3, 3-9, 9-27 and 27-81 cm depths.
|
||||||
|
"soil_moisture_0_1cm",
|
||||||
|
"soil_moisture_1_3cm",
|
||||||
|
"soil_moisture_3_9cm",
|
||||||
|
"soil_moisture_9_27cm",
|
||||||
|
"soil_moisture_27_81cm"
|
||||||
|
],
|
||||||
|
|
||||||
|
dailyParams: [
|
||||||
|
// Maximum and minimum daily air temperature at 2 meters above ground
|
||||||
|
"temperature_2m_max",
|
||||||
|
"temperature_2m_min",
|
||||||
|
// Maximum and minimum daily apparent temperature
|
||||||
|
"apparent_temperature_min",
|
||||||
|
"apparent_temperature_max",
|
||||||
|
// Sum of daily precipitation (including rain, showers and snowfall)
|
||||||
|
"precipitation_sum",
|
||||||
|
// Sum of daily rain
|
||||||
|
"rain_sum",
|
||||||
|
// Sum of daily showers
|
||||||
|
"showers_sum",
|
||||||
|
// Sum of daily snowfall
|
||||||
|
"snowfall_sum",
|
||||||
|
// The number of hours with rain
|
||||||
|
"precipitation_hours",
|
||||||
|
// The most severe weather condition on a given day
|
||||||
|
"weathercode",
|
||||||
|
// Sun rise and set times
|
||||||
|
"sunrise",
|
||||||
|
"sunset",
|
||||||
|
// Maximum wind speed and gusts on a day
|
||||||
|
"windspeed_10m_max",
|
||||||
|
"windgusts_10m_max",
|
||||||
|
// Dominant wind direction
|
||||||
|
"winddirection_10m_dominant",
|
||||||
|
// The sum of solar radiation on a given day in Megajoules
|
||||||
|
"shortwave_radiation_sum",
|
||||||
|
// Daily sum of ET₀ Reference Evapotranspiration of a well watered grass field
|
||||||
|
"et0_fao_evapotranspiration"
|
||||||
|
],
|
||||||
|
|
||||||
|
fetchedLocation: function () {
|
||||||
|
return this.fetchedLocationName || "";
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchCurrentWeather() {
|
||||||
|
this.fetchData(this.getUrl())
|
||||||
|
.then((data) => this.parseWeatherApiResponse(data))
|
||||||
|
.then((parsedData) => {
|
||||||
|
if (!parsedData) {
|
||||||
|
// No usable data?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentWeather = this.generateWeatherDayFromCurrentWeather(parsedData);
|
||||||
|
this.setCurrentWeather(currentWeather);
|
||||||
|
})
|
||||||
|
.catch(function (request) {
|
||||||
|
Log.error("Could not load data ... ", request);
|
||||||
|
})
|
||||||
|
.finally(() => this.updateAvailable());
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchWeatherForecast() {
|
||||||
|
this.fetchData(this.getUrl())
|
||||||
|
.then((data) => this.parseWeatherApiResponse(data))
|
||||||
|
.then((parsedData) => {
|
||||||
|
if (!parsedData) {
|
||||||
|
// No usable data?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dailyForecast = this.generateWeatherObjectsFromForecast(parsedData);
|
||||||
|
this.setWeatherForecast(dailyForecast);
|
||||||
|
})
|
||||||
|
.catch(function (request) {
|
||||||
|
Log.error("Could not load data ... ", request);
|
||||||
|
})
|
||||||
|
.finally(() => this.updateAvailable());
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchWeatherHourly() {
|
||||||
|
this.fetchData(this.getUrl())
|
||||||
|
.then((data) => this.parseWeatherApiResponse(data))
|
||||||
|
.then((parsedData) => {
|
||||||
|
if (!parsedData) {
|
||||||
|
// No usable data?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hourlyForecast = this.generateWeatherObjectsFromHourly(parsedData);
|
||||||
|
this.setWeatherHourly(hourlyForecast);
|
||||||
|
})
|
||||||
|
.catch(function (request) {
|
||||||
|
Log.error("Could not load data ... ", request);
|
||||||
|
})
|
||||||
|
.finally(() => this.updateAvailable());
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides method for setting config to check if endpoint is correct for hourly
|
||||||
|
*
|
||||||
|
* @param {object} config The configuration object
|
||||||
|
*/
|
||||||
|
setConfig(config) {
|
||||||
|
this.config = {
|
||||||
|
lang: config.lang ?? "en",
|
||||||
|
...this.defaults,
|
||||||
|
...config
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set properly maxNumberOfDays and max Entries properties according to config and value ranges allowed in the documentation
|
||||||
|
const maxEntriesLimit = ["daily", "forecast"].includes(this.config.type) ? 7 : this.config.type === "hourly" ? 48 : 0;
|
||||||
|
if (this.config.hasOwnProperty("maxNumberOfDays") && !isNaN(parseFloat(this.config.maxNumberOfDays))) {
|
||||||
|
const daysFactor = ["daily", "forecast"].includes(this.config.type) ? 1 : this.config.type === "hourly" ? 24 : 0;
|
||||||
|
this.config.maxEntries = Math.max(1, Math.min(Math.round(parseFloat(this.config.maxNumberOfDays)) * daysFactor, maxEntriesLimit));
|
||||||
|
this.config.maxNumberOfDays = Math.ceil(this.config.maxEntries / Math.max(1, daysFactor));
|
||||||
|
}
|
||||||
|
this.config.maxEntries = Math.max(1, Math.min(this.config.maxEntries, maxEntriesLimit));
|
||||||
|
|
||||||
|
if (!this.config.type) {
|
||||||
|
Log.error("type not configured and could not resolve it");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchLocation();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Generate valid query params to perform the request
|
||||||
|
getQueryParameters() {
|
||||||
|
let params = {
|
||||||
|
latitude: this.config.lat,
|
||||||
|
longitude: this.config.lon,
|
||||||
|
timeformat: "unixtime",
|
||||||
|
timezone: "auto",
|
||||||
|
past_days: this.config.past_days ?? 0,
|
||||||
|
daily: this.dailyParams,
|
||||||
|
hourly: this.hourlyParams,
|
||||||
|
// Fixed units as metric
|
||||||
|
temperature_unit: "celsius",
|
||||||
|
windspeed_unit: "kmh",
|
||||||
|
precipitation_unit: "mm"
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDate = moment().startOf("day");
|
||||||
|
const endDate = moment(startDate)
|
||||||
|
.add(Math.max(0, Math.min(7, this.config.maxNumberOfDays)), "days")
|
||||||
|
.endOf("day");
|
||||||
|
|
||||||
|
params["start_date"] = startDate.format("YYYY-MM-DD");
|
||||||
|
|
||||||
|
switch (this.config.type) {
|
||||||
|
case "hourly":
|
||||||
|
case "daily":
|
||||||
|
case "forecast":
|
||||||
|
params["end_date"] = endDate.format("YYYY-MM-DD");
|
||||||
|
break;
|
||||||
|
case "current":
|
||||||
|
params["current_weather"] = true;
|
||||||
|
params["end_date"] = params["start_date"];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Failsafe
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(params)
|
||||||
|
.filter((key) => (params[key] ? true : false))
|
||||||
|
.map((key) => {
|
||||||
|
switch (key) {
|
||||||
|
case "hourly":
|
||||||
|
case "daily":
|
||||||
|
return encodeURIComponent(key) + "=" + params[key].join(",");
|
||||||
|
default:
|
||||||
|
return encodeURIComponent(key) + "=" + encodeURIComponent(params[key]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join("&");
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create a URL from the config and base URL.
|
||||||
|
getUrl() {
|
||||||
|
return `${this.config.apiBase}/forecast?${this.getQueryParameters()}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Transpose hourly and daily data matrices
|
||||||
|
transposeDataMatrix(data) {
|
||||||
|
return data.time.map((_, index) =>
|
||||||
|
Object.keys(data).reduce((row, key) => {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
// Parse time values as momentjs instances
|
||||||
|
[key]: ["time", "sunrise", "sunset"].includes(key) ? moment.unix(data[key][index]) : data[key][index]
|
||||||
|
};
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Sanitize and validate API response
|
||||||
|
parseWeatherApiResponse(data) {
|
||||||
|
const validByType = {
|
||||||
|
current: data.current_weather && data.current_weather.time,
|
||||||
|
hourly: data.hourly && data.hourly.time && Array.isArray(data.hourly.time) && data.hourly.time.length > 0,
|
||||||
|
daily: data.daily && data.daily.time && Array.isArray(data.daily.time) && data.daily.time.length > 0
|
||||||
|
};
|
||||||
|
// backwards compatibility
|
||||||
|
const type = ["daily", "forecast"].includes(this.config.type) ? "daily" : this.config.type;
|
||||||
|
|
||||||
|
if (!validByType[type]) return;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "current":
|
||||||
|
if (!validByType.daily && !validByType.hourly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "hourly":
|
||||||
|
case "daily":
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of ["hourly", "daily"]) {
|
||||||
|
if (typeof data[key] === "object") {
|
||||||
|
data[key] = this.transposeDataMatrix(data[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.current_weather) {
|
||||||
|
data.current_weather.time = moment.unix(data.current_weather.time);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Reverse geocoding from latitude and longitude provided
|
||||||
|
fetchLocation() {
|
||||||
|
this.fetchData(`${GEOCODE_BASE}?latitude=${this.config.lat}&longitude=${this.config.lon}&localityLanguage=${this.config.lang}`)
|
||||||
|
.then((data) => {
|
||||||
|
if (!data || !data.city) {
|
||||||
|
// No usable data?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.fetchedLocationName = `${data.city}, ${data.principalSubdivisionCode}`;
|
||||||
|
})
|
||||||
|
.catch((request) => {
|
||||||
|
Log.error("Could not load data ... ", request);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Implement WeatherDay generator.
|
||||||
|
generateWeatherDayFromCurrentWeather(weather) {
|
||||||
|
/**
|
||||||
|
* Since some units comes from API response "splitted" into daily, hourly and current_weather
|
||||||
|
* every time you request it, you have to ensure to get the data from the right place every time.
|
||||||
|
* For the current weather case, the response have the following structure (after transposing):
|
||||||
|
* ```
|
||||||
|
* {
|
||||||
|
* current_weather: { ...<some current weather here> },
|
||||||
|
* hourly: [
|
||||||
|
* 0: {...<data for hour zero here> },
|
||||||
|
* 1: {...<data for hour one here> },
|
||||||
|
* ...
|
||||||
|
* ],
|
||||||
|
* daily: [
|
||||||
|
* {...<summary data for current day here> },
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* Some data should be returned from `hourly` array data when the index matches the current hour,
|
||||||
|
* some data from the first and only one object received in `daily` array and some from the
|
||||||
|
* `current_weather` object.
|
||||||
|
*/
|
||||||
|
const h = moment().hour();
|
||||||
|
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||||
|
|
||||||
|
currentWeather.date = weather.current_weather.time;
|
||||||
|
currentWeather.windSpeed = weather.current_weather.windspeed;
|
||||||
|
currentWeather.windDirection = weather.current_weather.winddirection;
|
||||||
|
currentWeather.sunrise = weather.daily[0].sunrise;
|
||||||
|
currentWeather.sunset = weather.daily[0].sunset;
|
||||||
|
currentWeather.temperature = parseFloat(weather.current_weather.temperature);
|
||||||
|
currentWeather.minTemperature = parseFloat(weather.daily[0].temperature_2m_min);
|
||||||
|
currentWeather.maxTemperature = parseFloat(weather.daily[0].temperature_2m_max);
|
||||||
|
currentWeather.weatherType = this.convertWeatherType(weather.current_weather.weathercode, currentWeather.isDayTime());
|
||||||
|
currentWeather.humidity = parseFloat(weather.hourly[h].relativehumidity_2m);
|
||||||
|
currentWeather.rain = parseFloat(weather.hourly[h].rain);
|
||||||
|
currentWeather.snow = parseFloat(weather.hourly[h].snowfall * 10);
|
||||||
|
currentWeather.precipitation = parseFloat(weather.hourly[h].precipitation);
|
||||||
|
|
||||||
|
return currentWeather;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Implement WeatherForecast generator.
|
||||||
|
generateWeatherObjectsFromForecast(weathers) {
|
||||||
|
const days = [];
|
||||||
|
|
||||||
|
weathers.daily.forEach((weather, i) => {
|
||||||
|
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||||
|
|
||||||
|
currentWeather.date = weather.time;
|
||||||
|
currentWeather.windSpeed = weather.windspeed_10m_max;
|
||||||
|
currentWeather.windDirection = weather.winddirection_10m_dominant;
|
||||||
|
currentWeather.sunrise = weather.sunrise;
|
||||||
|
currentWeather.sunset = weather.sunset;
|
||||||
|
currentWeather.temperature = parseFloat((weather.apparent_temperature_max + weather.apparent_temperature_min) / 2);
|
||||||
|
currentWeather.minTemperature = parseFloat(weather.apparent_temperature_min);
|
||||||
|
currentWeather.maxTemperature = parseFloat(weather.apparent_temperature_max);
|
||||||
|
currentWeather.weatherType = this.convertWeatherType(weather.weathercode, currentWeather.isDayTime());
|
||||||
|
currentWeather.rain = parseFloat(weather.rain_sum);
|
||||||
|
currentWeather.snow = parseFloat(weather.snowfall_sum * 10);
|
||||||
|
currentWeather.precipitation = parseFloat(weather.precipitation_sum);
|
||||||
|
|
||||||
|
days.push(currentWeather);
|
||||||
|
});
|
||||||
|
|
||||||
|
return days;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Implement WeatherHourly generator.
|
||||||
|
generateWeatherObjectsFromHourly(weathers) {
|
||||||
|
const hours = [];
|
||||||
|
const now = moment();
|
||||||
|
|
||||||
|
weathers.hourly.forEach((weather, i) => {
|
||||||
|
if ((hours.length === 0 && weather.time.hour() <= now.hour()) || hours.length >= this.config.maxEntries) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||||
|
const h = Math.ceil((i + 1) / 24) - 1;
|
||||||
|
|
||||||
|
currentWeather.date = weather.time;
|
||||||
|
currentWeather.windSpeed = weather.windspeed_10m;
|
||||||
|
currentWeather.windDirection = weather.winddirection_10m;
|
||||||
|
currentWeather.sunrise = weathers.daily[h].sunrise;
|
||||||
|
currentWeather.sunset = weathers.daily[h].sunset;
|
||||||
|
currentWeather.temperature = parseFloat(weather.apparent_temperature);
|
||||||
|
currentWeather.minTemperature = parseFloat(weathers.daily[h].apparent_temperature_min);
|
||||||
|
currentWeather.maxTemperature = parseFloat(weathers.daily[h].apparent_temperature_max);
|
||||||
|
currentWeather.weatherType = this.convertWeatherType(weather.weathercode, currentWeather.isDayTime());
|
||||||
|
currentWeather.humidity = parseFloat(weather.relativehumidity_2m);
|
||||||
|
currentWeather.rain = parseFloat(weather.rain);
|
||||||
|
currentWeather.snow = parseFloat(weather.snowfall * 10);
|
||||||
|
currentWeather.precipitation = parseFloat(weather.precipitation);
|
||||||
|
|
||||||
|
hours.push(currentWeather);
|
||||||
|
});
|
||||||
|
|
||||||
|
return hours;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Map icons from Dark Sky to our icons.
|
||||||
|
convertWeatherType(weathercode, isDayTime) {
|
||||||
|
const weatherConditions = {
|
||||||
|
0: "clear",
|
||||||
|
1: "mainly-clear",
|
||||||
|
2: "partly-cloudy",
|
||||||
|
3: "overcast",
|
||||||
|
45: "fog",
|
||||||
|
48: "depositing-rime-fog",
|
||||||
|
51: "drizzle-light-intensity",
|
||||||
|
53: "drizzle-moderate-intensity",
|
||||||
|
55: "drizzle-dense-intensity",
|
||||||
|
56: "freezing-drizzle-light-intensity",
|
||||||
|
57: "freezing-drizzle-dense-intensity",
|
||||||
|
61: "rain-slight-intensity",
|
||||||
|
63: "rain-moderate-intensity",
|
||||||
|
65: "rain-heavy-intensity",
|
||||||
|
66: "freezing-rain-light-heavy-intensity",
|
||||||
|
67: "freezing-rain-heavy-intensity",
|
||||||
|
71: "snow-fall-slight-intensity",
|
||||||
|
73: "snow-fall-moderate-intensity",
|
||||||
|
75: "snow-fall-heavy-intensity",
|
||||||
|
77: "snow-grains",
|
||||||
|
80: "rain-showers-slight",
|
||||||
|
81: "rain-showers-moderate",
|
||||||
|
82: "rain-showers-violent",
|
||||||
|
85: "snow-showers-slight",
|
||||||
|
86: "snow-showers-heavy",
|
||||||
|
95: "thunderstorm",
|
||||||
|
96: "thunderstorm-slight-hail",
|
||||||
|
99: "thunderstorm-heavy-hail"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!Object.keys(weatherConditions).includes(`${weathercode}`)) return null;
|
||||||
|
|
||||||
|
switch (weatherConditions[`${weathercode}`]) {
|
||||||
|
case "clear":
|
||||||
|
return isDayTime ? "day-sunny" : "night-clear";
|
||||||
|
case "mainly-clear":
|
||||||
|
case "partly-cloudy":
|
||||||
|
return isDayTime ? "day-cloudy" : "night-alt-cloudy";
|
||||||
|
case "overcast":
|
||||||
|
return isDayTime ? "day-sunny-overcast" : "night-alt-partly-cloudy";
|
||||||
|
case "fog":
|
||||||
|
case "depositing-rime-fog":
|
||||||
|
return isDayTime ? "day-fog" : "night-fog";
|
||||||
|
case "drizzle-light-intensity":
|
||||||
|
case "rain-slight-intensity":
|
||||||
|
case "rain-showers-slight":
|
||||||
|
return isDayTime ? "day-sprinkle" : "night-sprinkle";
|
||||||
|
case "drizzle-moderate-intensity":
|
||||||
|
case "rain-moderate-intensity":
|
||||||
|
case "rain-showers-moderate":
|
||||||
|
return isDayTime ? "day-showers" : "night-showers";
|
||||||
|
case "drizzle-dense-intensity":
|
||||||
|
case "rain-heavy-intensity":
|
||||||
|
case "rain-showers-violent":
|
||||||
|
return isDayTime ? "day-thunderstorm" : "night-thunderstorm";
|
||||||
|
case "freezing-rain-light-intensity":
|
||||||
|
return isDayTime ? "day-rain-mix" : "night-rain-mix";
|
||||||
|
case "freezing-drizzle-light-intensity":
|
||||||
|
case "freezing-drizzle-dense-intensity":
|
||||||
|
return "snowflake-cold";
|
||||||
|
case "snow-grains":
|
||||||
|
return isDayTime ? "day-sleet" : "night-sleet";
|
||||||
|
case "snow-fall-slight-intensity":
|
||||||
|
case "snow-fall-moderate-intensity":
|
||||||
|
return isDayTime ? "day-snow-wind" : "night-snow-wind";
|
||||||
|
case "snow-fall-heavy-intensity":
|
||||||
|
case "freezing-rain-heavy-intensity":
|
||||||
|
return isDayTime ? "day-snow-thunderstorm" : "night-snow-thunderstorm";
|
||||||
|
case "snow-showers-slight":
|
||||||
|
case "snow-showers-heavy":
|
||||||
|
return isDayTime ? "day-rain-mix" : "night-rain-mix";
|
||||||
|
case "thunderstorm":
|
||||||
|
return isDayTime ? "day-thunderstorm" : "night-thunderstorm";
|
||||||
|
case "thunderstorm-slight-hail":
|
||||||
|
return isDayTime ? "day-sleet" : "night-sleet";
|
||||||
|
case "thunderstorm-heavy-hail":
|
||||||
|
return isDayTime ? "day-sleet-storm" : "night-sleet-storm";
|
||||||
|
default:
|
||||||
|
return "na";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Define required scripts.
|
||||||
|
getScripts: function () {
|
||||||
|
return ["moment.js"];
|
||||||
|
}
|
||||||
|
});
|
|
@ -21,7 +21,7 @@ WeatherProvider.register("openweathermap", {
|
||||||
weatherEndpoint: "", // can be "onecall", "forecast" or "weather" (for current)
|
weatherEndpoint: "", // can be "onecall", "forecast" or "weather" (for current)
|
||||||
locationID: false,
|
locationID: false,
|
||||||
location: false,
|
location: false,
|
||||||
lat: 0, // the onecall endpoint needs lat / lon values, it doesn'T support the locationId
|
lat: 0, // the onecall endpoint needs lat / lon values, it doesn't support the locationId
|
||||||
lon: 0,
|
lon: 0,
|
||||||
apiKey: ""
|
apiKey: ""
|
||||||
},
|
},
|
||||||
|
@ -30,14 +30,14 @@ WeatherProvider.register("openweathermap", {
|
||||||
fetchCurrentWeather() {
|
fetchCurrentWeather() {
|
||||||
this.fetchData(this.getUrl())
|
this.fetchData(this.getUrl())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
let currentWeather;
|
||||||
if (this.config.weatherEndpoint === "/onecall") {
|
if (this.config.weatherEndpoint === "/onecall") {
|
||||||
const weatherData = this.generateWeatherObjectsFromOnecall(data);
|
currentWeather = this.generateWeatherObjectsFromOnecall(data).current;
|
||||||
this.setCurrentWeather(weatherData.current);
|
|
||||||
this.setFetchedLocation(`${data.timezone}`);
|
this.setFetchedLocation(`${data.timezone}`);
|
||||||
} else {
|
} else {
|
||||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
||||||
this.setCurrentWeather(currentWeather);
|
|
||||||
}
|
}
|
||||||
|
this.setCurrentWeather(currentWeather);
|
||||||
})
|
})
|
||||||
.catch(function (request) {
|
.catch(function (request) {
|
||||||
Log.error("Could not load data ... ", request);
|
Log.error("Could not load data ... ", request);
|
||||||
|
@ -49,15 +49,17 @@ WeatherProvider.register("openweathermap", {
|
||||||
fetchWeatherForecast() {
|
fetchWeatherForecast() {
|
||||||
this.fetchData(this.getUrl())
|
this.fetchData(this.getUrl())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
let forecast;
|
||||||
|
let location;
|
||||||
if (this.config.weatherEndpoint === "/onecall") {
|
if (this.config.weatherEndpoint === "/onecall") {
|
||||||
const weatherData = this.generateWeatherObjectsFromOnecall(data);
|
forecast = this.generateWeatherObjectsFromOnecall(data).days;
|
||||||
this.setWeatherForecast(weatherData.days);
|
location = `${data.timezone}`;
|
||||||
this.setFetchedLocation(`${data.timezone}`);
|
|
||||||
} else {
|
} else {
|
||||||
const forecast = this.generateWeatherObjectsFromForecast(data.list);
|
forecast = this.generateWeatherObjectsFromForecast(data.list);
|
||||||
this.setWeatherForecast(forecast);
|
location = `${data.city.name}, ${data.city.country}`;
|
||||||
this.setFetchedLocation(`${data.city.name}, ${data.city.country}`);
|
|
||||||
}
|
}
|
||||||
|
this.setWeatherForecast(forecast);
|
||||||
|
this.setFetchedLocation(location);
|
||||||
})
|
})
|
||||||
.catch(function (request) {
|
.catch(function (request) {
|
||||||
Log.error("Could not load data ... ", request);
|
Log.error("Could not load data ... ", request);
|
||||||
|
@ -123,16 +125,17 @@ WeatherProvider.register("openweathermap", {
|
||||||
* Generate a WeatherObject based on currentWeatherInformation
|
* Generate a WeatherObject based on currentWeatherInformation
|
||||||
*/
|
*/
|
||||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
const currentWeather = new WeatherObject();
|
||||||
|
|
||||||
|
currentWeather.date = moment.unix(currentWeatherData.dt);
|
||||||
currentWeather.humidity = currentWeatherData.main.humidity;
|
currentWeather.humidity = currentWeatherData.main.humidity;
|
||||||
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.windDirection = currentWeatherData.wind.deg;
|
||||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon);
|
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon);
|
||||||
currentWeather.sunrise = moment(currentWeatherData.sys.sunrise, "X");
|
currentWeather.sunrise = moment.unix(currentWeatherData.sys.sunrise);
|
||||||
currentWeather.sunset = moment(currentWeatherData.sys.sunset, "X");
|
currentWeather.sunset = moment.unix(currentWeatherData.sys.sunset);
|
||||||
|
|
||||||
return currentWeather;
|
return currentWeather;
|
||||||
},
|
},
|
||||||
|
@ -147,8 +150,7 @@ WeatherProvider.register("openweathermap", {
|
||||||
return this.fetchForecastDaily(forecasts);
|
return this.fetchForecastDaily(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?
|
||||||
const days = [new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh)];
|
return [new WeatherObject()];
|
||||||
return days;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -159,8 +161,7 @@ WeatherProvider.register("openweathermap", {
|
||||||
return this.fetchOnecall(data);
|
return this.fetchOnecall(data);
|
||||||
}
|
}
|
||||||
// if weatherEndpoint does not match onecall, what should be returned?
|
// if weatherEndpoint does not match onecall, what should be returned?
|
||||||
const weatherData = { current: new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh), hours: [], days: [] };
|
return { current: new WeatherObject(), hours: [], days: [] };
|
||||||
return weatherData;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -176,10 +177,10 @@ WeatherProvider.register("openweathermap", {
|
||||||
let snow = 0;
|
let snow = 0;
|
||||||
// variable for date
|
// variable for date
|
||||||
let date = "";
|
let date = "";
|
||||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
let weather = new WeatherObject();
|
||||||
|
|
||||||
for (const forecast of forecasts) {
|
for (const forecast of forecasts) {
|
||||||
if (date !== moment(forecast.dt, "X").format("YYYY-MM-DD")) {
|
if (date !== moment.unix(forecast.dt).format("YYYY-MM-DD")) {
|
||||||
// calculate minimum/maximum temperature, specify rain amount
|
// calculate minimum/maximum temperature, specify rain amount
|
||||||
weather.minTemperature = Math.min.apply(null, minTemp);
|
weather.minTemperature = Math.min.apply(null, minTemp);
|
||||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||||
|
@ -189,7 +190,7 @@ WeatherProvider.register("openweathermap", {
|
||||||
// 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
|
||||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
weather = new WeatherObject();
|
||||||
|
|
||||||
minTemp = [];
|
minTemp = [];
|
||||||
maxTemp = [];
|
maxTemp = [];
|
||||||
|
@ -197,16 +198,16 @@ WeatherProvider.register("openweathermap", {
|
||||||
snow = 0;
|
snow = 0;
|
||||||
|
|
||||||
// set new date
|
// set new date
|
||||||
date = moment(forecast.dt, "X").format("YYYY-MM-DD");
|
date = moment.unix(forecast.dt).format("YYYY-MM-DD");
|
||||||
|
|
||||||
// specify date
|
// specify date
|
||||||
weather.date = moment(forecast.dt, "X");
|
weather.date = moment.unix(forecast.dt);
|
||||||
|
|
||||||
// If the first value of today is later than 17:00, we have an icon at least!
|
// If the first value of today is later than 17:00, we have an icon at least!
|
||||||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (moment(forecast.dt, "X").format("H") >= 8 && moment(forecast.dt, "X").format("H") <= 17) {
|
if (moment.unix(forecast.dt).format("H") >= 8 && moment.unix(forecast.dt).format("H") <= 17) {
|
||||||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,9 +253,9 @@ WeatherProvider.register("openweathermap", {
|
||||||
const days = [];
|
const days = [];
|
||||||
|
|
||||||
for (const forecast of forecasts) {
|
for (const forecast of forecasts) {
|
||||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
const weather = new WeatherObject();
|
||||||
|
|
||||||
weather.date = moment(forecast.dt, "X");
|
weather.date = moment.unix(forecast.dt);
|
||||||
weather.minTemperature = forecast.temp.min;
|
weather.minTemperature = forecast.temp.min;
|
||||||
weather.maxTemperature = forecast.temp.max;
|
weather.maxTemperature = forecast.temp.max;
|
||||||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||||
|
@ -298,13 +299,13 @@ WeatherProvider.register("openweathermap", {
|
||||||
let precip = false;
|
let precip = false;
|
||||||
|
|
||||||
// get current weather, if requested
|
// get current weather, if requested
|
||||||
const current = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
const current = new WeatherObject();
|
||||||
if (data.hasOwnProperty("current")) {
|
if (data.hasOwnProperty("current")) {
|
||||||
current.date = moment(data.current.dt, "X").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.windDirection = data.current.wind_deg;
|
||||||
current.sunrise = moment(data.current.sunrise, "X").utcOffset(data.timezone_offset / 60);
|
current.sunrise = moment.unix(data.current.sunrise).utcOffset(data.timezone_offset / 60);
|
||||||
current.sunset = moment(data.current.sunset, "X").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;
|
||||||
|
@ -330,14 +331,13 @@ WeatherProvider.register("openweathermap", {
|
||||||
current.feelsLikeTemp = data.current.feels_like;
|
current.feelsLikeTemp = data.current.feels_like;
|
||||||
}
|
}
|
||||||
|
|
||||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
let weather = new WeatherObject();
|
||||||
|
|
||||||
// get hourly weather, if requested
|
// get hourly weather, if requested
|
||||||
const hours = [];
|
const hours = [];
|
||||||
if (data.hasOwnProperty("hourly")) {
|
if (data.hasOwnProperty("hourly")) {
|
||||||
for (const hour of data.hourly) {
|
for (const hour of data.hourly) {
|
||||||
weather.date = moment(hour.dt, "X").utcOffset(data.timezone_offset / 60);
|
weather.date = moment.unix(hour.dt).utcOffset(data.timezone_offset / 60);
|
||||||
// weather.date = moment(hour.dt, "X").utcOffset(data.timezone_offset/60).format(onecallDailyFormat+","+onecallHourlyFormat);
|
|
||||||
weather.temperature = hour.temp;
|
weather.temperature = hour.temp;
|
||||||
weather.feelsLikeTemp = hour.feels_like;
|
weather.feelsLikeTemp = hour.feels_like;
|
||||||
weather.humidity = hour.humidity;
|
weather.humidity = hour.humidity;
|
||||||
|
@ -366,7 +366,7 @@ WeatherProvider.register("openweathermap", {
|
||||||
}
|
}
|
||||||
|
|
||||||
hours.push(weather);
|
hours.push(weather);
|
||||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
weather = new WeatherObject();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -374,9 +374,9 @@ WeatherProvider.register("openweathermap", {
|
||||||
const days = [];
|
const days = [];
|
||||||
if (data.hasOwnProperty("daily")) {
|
if (data.hasOwnProperty("daily")) {
|
||||||
for (const day of data.daily) {
|
for (const day of data.daily) {
|
||||||
weather.date = moment(day.dt, "X").utcOffset(data.timezone_offset / 60);
|
weather.date = moment.unix(day.dt).utcOffset(data.timezone_offset / 60);
|
||||||
weather.sunrise = moment(day.sunrise, "X").utcOffset(data.timezone_offset / 60);
|
weather.sunrise = moment.unix(day.sunrise).utcOffset(data.timezone_offset / 60);
|
||||||
weather.sunset = moment(day.sunset, "X").utcOffset(data.timezone_offset / 60);
|
weather.sunset = moment.unix(day.sunset).utcOffset(data.timezone_offset / 60);
|
||||||
weather.minTemperature = day.temp.min;
|
weather.minTemperature = day.temp.min;
|
||||||
weather.maxTemperature = day.temp.max;
|
weather.maxTemperature = day.temp.max;
|
||||||
weather.humidity = day.humidity;
|
weather.humidity = day.humidity;
|
||||||
|
@ -405,7 +405,7 @@ WeatherProvider.register("openweathermap", {
|
||||||
}
|
}
|
||||||
|
|
||||||
days.push(weather);
|
days.push(weather);
|
||||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
weather = new WeatherObject();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -474,7 +474,7 @@ WeatherProvider.register("openweathermap", {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
params += "&units=" + this.config.units;
|
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;
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,8 @@ WeatherProvider.register("smhi", {
|
||||||
|
|
||||||
// Set the default config properties that is specific to this provider
|
// Set the default config properties that is specific to this provider
|
||||||
defaults: {
|
defaults: {
|
||||||
lat: 0,
|
lat: 0, // Cant have more than 6 digits
|
||||||
lon: 0,
|
lon: 0, // Cant have more than 6 digits
|
||||||
precipitationValue: "pmedian",
|
precipitationValue: "pmedian",
|
||||||
location: false
|
location: false
|
||||||
},
|
},
|
||||||
|
@ -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) {
|
||||||
console.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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -104,8 +104,12 @@ WeatherProvider.register("smhi", {
|
||||||
* @returns {string} the url for the specified coordinates
|
* @returns {string} the url for the specified coordinates
|
||||||
*/
|
*/
|
||||||
getURL() {
|
getURL() {
|
||||||
let lon = this.config.lon;
|
const formatter = new Intl.NumberFormat("en-US", {
|
||||||
let lat = this.config.lat;
|
minimumFractionDigits: 6,
|
||||||
|
maximumFractionDigits: 6
|
||||||
|
});
|
||||||
|
const lon = formatter.format(this.config.lon);
|
||||||
|
const lat = formatter.format(this.config.lat);
|
||||||
return `https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/${lon}/lat/${lat}/data.json`;
|
return `https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/${lon}/lat/${lat}/data.json`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -134,8 +138,7 @@ WeatherProvider.register("smhi", {
|
||||||
* @returns {WeatherObject} The converted weatherdata at the specified location
|
* @returns {WeatherObject} The converted weatherdata at the specified location
|
||||||
*/
|
*/
|
||||||
convertWeatherDataToObject(weatherData, coordinates) {
|
convertWeatherDataToObject(weatherData, coordinates) {
|
||||||
// Weather data is only for Sweden and nobody in Sweden would use imperial
|
let currentWeather = new WeatherObject();
|
||||||
let currentWeather = new WeatherObject("metric", "metric", "metric");
|
|
||||||
|
|
||||||
currentWeather.date = moment(weatherData.validTime);
|
currentWeather.date = moment(weatherData.validTime);
|
||||||
currentWeather.updateSunTime(coordinates.lat, coordinates.lon);
|
currentWeather.updateSunTime(coordinates.lat, coordinates.lon);
|
||||||
|
@ -144,7 +147,7 @@ WeatherProvider.register("smhi", {
|
||||||
currentWeather.windSpeed = this.paramValue(weatherData, "ws");
|
currentWeather.windSpeed = this.paramValue(weatherData, "ws");
|
||||||
currentWeather.windDirection = this.paramValue(weatherData, "wd");
|
currentWeather.windDirection = 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.calculateAT(weatherData);
|
currentWeather.feelsLikeTemp = this.calculateApparentTemperature(weatherData);
|
||||||
|
|
||||||
// Determine the precipitation amount and category and update the
|
// Determine the precipitation amount and category and update the
|
||||||
// weatherObject with it, the valuetype to use can be configured or uses
|
// weatherObject with it, the valuetype to use can be configured or uses
|
||||||
|
@ -174,7 +177,7 @@ WeatherProvider.register("smhi", {
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes all of the data points and converts it to one WeatherObject per day.
|
* Takes all the data points and converts it to one WeatherObject per day.
|
||||||
*
|
*
|
||||||
* @param {object[]} allWeatherData Array of weatherdata
|
* @param {object[]} allWeatherData Array of weatherdata
|
||||||
* @param {object} coordinates Coordinates of the locations of the weather
|
* @param {object} coordinates Coordinates of the locations of the weather
|
||||||
|
@ -191,7 +194,7 @@ WeatherProvider.register("smhi", {
|
||||||
for (const weatherObject of allWeatherObjects) {
|
for (const weatherObject of allWeatherObjects) {
|
||||||
//If its the first object or if a day/hour change we need to reset the summary object
|
//If its the first object or if a day/hour change we need to reset the summary object
|
||||||
if (!currentWeather || !currentWeather.date.isSame(weatherObject.date, groupBy)) {
|
if (!currentWeather || !currentWeather.date.isSame(weatherObject.date, groupBy)) {
|
||||||
currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
currentWeather = new WeatherObject();
|
||||||
dayWeatherTypes = [];
|
dayWeatherTypes = [];
|
||||||
currentWeather.temperature = weatherObject.temperature;
|
currentWeather.temperature = weatherObject.temperature;
|
||||||
currentWeather.date = weatherObject.date;
|
currentWeather.date = weatherObject.date;
|
||||||
|
@ -203,7 +206,7 @@ WeatherProvider.register("smhi", {
|
||||||
result.push(currentWeather);
|
result.push(currentWeather);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Keep track of what icons has been used for each hour of daytime and use the middle one for the forecast
|
//Keep track of what icons have been used for each hour of daytime and use the middle one for the forecast
|
||||||
if (weatherObject.isDayTime()) {
|
if (weatherObject.isDayTime()) {
|
||||||
dayWeatherTypes.push(weatherObject.weatherType);
|
dayWeatherTypes.push(weatherObject.weatherType);
|
||||||
}
|
}
|
||||||
|
@ -271,7 +274,7 @@ WeatherProvider.register("smhi", {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map the icon value from SMHI to an icon that MagicMirror² understands.
|
* Map the icon value from SMHI to an icon that MagicMirror² understands.
|
||||||
* Uses different icons depending if its daytime or nighttime.
|
* Uses different icons depending on if its daytime or nighttime.
|
||||||
* SMHI's description of what the numeric value means is the comment after the case.
|
* SMHI's description of what the numeric value means is the comment after the case.
|
||||||
*
|
*
|
||||||
* @param {number} input The SMHI icon value
|
* @param {number} input The SMHI icon value
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global WeatherProvider, WeatherObject */
|
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||||
|
|
||||||
/* MagicMirror²
|
/* MagicMirror²
|
||||||
* Module: Weather
|
* Module: Weather
|
||||||
|
@ -21,11 +21,6 @@ WeatherProvider.register("ukmetoffice", {
|
||||||
apiKey: ""
|
apiKey: ""
|
||||||
},
|
},
|
||||||
|
|
||||||
units: {
|
|
||||||
imperial: "us",
|
|
||||||
metric: "si"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Overwrite the fetchCurrentWeather method.
|
// Overwrite the fetchCurrentWeather method.
|
||||||
fetchCurrentWeather() {
|
fetchCurrentWeather() {
|
||||||
this.fetchData(this.getUrl("3hourly"))
|
this.fetchData(this.getUrl("3hourly"))
|
||||||
|
@ -80,7 +75,7 @@ WeatherProvider.register("ukmetoffice", {
|
||||||
* Generate a WeatherObject based on currentWeatherInformation
|
* Generate a WeatherObject based on currentWeatherInformation
|
||||||
*/
|
*/
|
||||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
const currentWeather = new WeatherObject();
|
||||||
const location = currentWeatherData.SiteRep.DV.Location;
|
const location = currentWeatherData.SiteRep.DV.Location;
|
||||||
|
|
||||||
// data times are always UTC
|
// data times are always UTC
|
||||||
|
@ -103,11 +98,11 @@ WeatherProvider.register("ukmetoffice", {
|
||||||
if (timeInMins >= p && timeInMins - 180 < p) {
|
if (timeInMins >= p && timeInMins - 180 < p) {
|
||||||
// finally got the one we want, so populate weather object
|
// finally got the one we want, so populate weather object
|
||||||
currentWeather.humidity = rep.H;
|
currentWeather.humidity = rep.H;
|
||||||
currentWeather.temperature = this.convertTemp(rep.T);
|
currentWeather.temperature = rep.T;
|
||||||
currentWeather.feelsLikeTemp = this.convertTemp(rep.F);
|
currentWeather.feelsLikeTemp = rep.F;
|
||||||
currentWeather.precipitation = parseInt(rep.Pp);
|
currentWeather.precipitation = parseInt(rep.Pp);
|
||||||
currentWeather.windSpeed = this.convertWindSpeed(rep.S);
|
currentWeather.windSpeed = WeatherUtils.convertWindToMetric(rep.S);
|
||||||
currentWeather.windDirection = this.convertWindDirection(rep.D);
|
currentWeather.windDirection = WeatherUtils.convertWindDirection(rep.D);
|
||||||
currentWeather.weatherType = this.convertWeatherType(rep.W);
|
currentWeather.weatherType = this.convertWeatherType(rep.W);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,7 +125,7 @@ WeatherProvider.register("ukmetoffice", {
|
||||||
// loop round the (5) periods getting the data
|
// loop round the (5) periods getting the data
|
||||||
// for each period array, Day is [0], Night is [1]
|
// for each period array, Day is [0], Night is [1]
|
||||||
for (const period of forecasts.SiteRep.DV.Location.Period) {
|
for (const period of forecasts.SiteRep.DV.Location.Period) {
|
||||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
const weather = new WeatherObject();
|
||||||
|
|
||||||
// data times are always UTC
|
// data times are always UTC
|
||||||
const dateStr = period.value;
|
const dateStr = period.value;
|
||||||
|
@ -140,8 +135,8 @@ WeatherProvider.register("ukmetoffice", {
|
||||||
if (periodDate.isSameOrAfter(moment.utc().startOf("day"))) {
|
if (periodDate.isSameOrAfter(moment.utc().startOf("day"))) {
|
||||||
// populate the weather object
|
// populate the weather object
|
||||||
weather.date = moment.utc(dateStr.substr(0, 10), "YYYY-MM-DD");
|
weather.date = moment.utc(dateStr.substr(0, 10), "YYYY-MM-DD");
|
||||||
weather.minTemperature = this.convertTemp(period.Rep[1].Nm);
|
weather.minTemperature = period.Rep[1].Nm;
|
||||||
weather.maxTemperature = this.convertTemp(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.precipitation = parseInt(period.Rep[0].PPd);
|
||||||
|
|
||||||
|
@ -192,46 +187,6 @@ WeatherProvider.register("ukmetoffice", {
|
||||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
|
||||||
* Convert temp (from degrees C) if required
|
|
||||||
*/
|
|
||||||
convertTemp(tempInC) {
|
|
||||||
return this.tempUnits === "imperial" ? (tempInC * 9) / 5 + 32 : tempInC;
|
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Convert wind speed (from mph to m/s or km/h) if required
|
|
||||||
*/
|
|
||||||
convertWindSpeed(windInMph) {
|
|
||||||
return this.windUnits === "metric" ? (this.useKmh ? windInMph * 1.60934 : windInMph / 2.23694) : windInMph;
|
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Convert the wind direction cardinal to value
|
|
||||||
*/
|
|
||||||
convertWindDirection(windDirection) {
|
|
||||||
const windCardinals = {
|
|
||||||
N: 0,
|
|
||||||
NNE: 22,
|
|
||||||
NE: 45,
|
|
||||||
ENE: 67,
|
|
||||||
E: 90,
|
|
||||||
ESE: 112,
|
|
||||||
SE: 135,
|
|
||||||
SSE: 157,
|
|
||||||
S: 180,
|
|
||||||
SSW: 202,
|
|
||||||
SW: 225,
|
|
||||||
WSW: 247,
|
|
||||||
W: 270,
|
|
||||||
WNW: 292,
|
|
||||||
NW: 315,
|
|
||||||
NNW: 337
|
|
||||||
};
|
|
||||||
|
|
||||||
return windCardinals.hasOwnProperty(windDirection) ? windCardinals[windDirection] : null;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates an url with api parameters based on the config.
|
* Generates an url with api parameters based on the config.
|
||||||
*
|
*
|
||||||
|
|
|
@ -20,11 +20,9 @@
|
||||||
* weatherProvider: "ukmetofficedatahub",
|
* weatherProvider: "ukmetofficedatahub",
|
||||||
* apiBase: "https://api-metoffice.apiconnect.ibmcloud.com/metoffice/production/v0/forecasts/point/",
|
* apiBase: "https://api-metoffice.apiconnect.ibmcloud.com/metoffice/production/v0/forecasts/point/",
|
||||||
* apiKey: "[YOUR API KEY]",
|
* apiKey: "[YOUR API KEY]",
|
||||||
* apiSecret: "[YOUR API SECRET]]",
|
* apiSecret: "[YOUR API SECRET]",
|
||||||
* lat: [LATITUDE (DECIMAL)],
|
* lat: [LATITUDE (DECIMAL)],
|
||||||
* lon: [LONGITUDE (DECIMAL)],
|
* lon: [LONGITUDE (DECIMAL)]
|
||||||
* windUnits: "mps" | "kph" | "mph" (default)
|
|
||||||
* tempUnits: "imperial" | "metric" (default)
|
|
||||||
*
|
*
|
||||||
* At time of writing, free accounts are limited to 360 requests a day per service (hourly, 3hourly, daily); take this in mind when
|
* At time of writing, free accounts are limited to 360 requests a day per service (hourly, 3hourly, daily); take this in mind when
|
||||||
* setting your update intervals. For reference, 360 requests per day is once every 4 minutes.
|
* setting your update intervals. For reference, 360 requests per day is once every 4 minutes.
|
||||||
|
@ -51,8 +49,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
apiSecret: "",
|
apiSecret: "",
|
||||||
lat: 0,
|
lat: 0,
|
||||||
lon: 0,
|
lon: 0
|
||||||
windUnits: "mph"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 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)
|
||||||
|
@ -89,7 +86,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||||
fetchCurrentWeather() {
|
fetchCurrentWeather() {
|
||||||
this.fetchWeather(this.getUrl("hourly"), this.getHeaders())
|
this.fetchWeather(this.getUrl("hourly"), this.getHeaders())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
// Check data is useable
|
// Check data is usable
|
||||||
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length === 0) {
|
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length === 0) {
|
||||||
// Did not receive usable new data.
|
// Did not receive usable new data.
|
||||||
// Maybe this needs a better check?
|
// Maybe this needs a better check?
|
||||||
|
@ -109,13 +106,13 @@ 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're new data available
|
// Let the module know there is data available
|
||||||
.finally(() => this.updateAvailable());
|
.finally(() => this.updateAvailable());
|
||||||
},
|
},
|
||||||
|
|
||||||
// Create a WeatherObject using current weather data (data for the current hour)
|
// Create a WeatherObject using current weather data (data for the current hour)
|
||||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
const currentWeather = new WeatherObject();
|
||||||
|
|
||||||
// Extract the actual forecasts
|
// Extract the actual forecasts
|
||||||
let forecastDataHours = currentWeatherData.features[0].properties.timeSeries;
|
let forecastDataHours = currentWeatherData.features[0].properties.timeSeries;
|
||||||
|
@ -128,19 +125,19 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||||
let forecastTime = moment.utc(forecastDataHours[hour].time);
|
let forecastTime = moment.utc(forecastDataHours[hour].time);
|
||||||
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 = this.convertWindSpeed(forecastDataHours[hour].windSpeed10m);
|
currentWeather.windSpeed = forecastDataHours[hour].windSpeed10m;
|
||||||
currentWeather.windDirection = forecastDataHours[hour].windDirectionFrom10m;
|
currentWeather.windDirection = forecastDataHours[hour].windDirectionFrom10m;
|
||||||
currentWeather.temperature = this.convertTemp(forecastDataHours[hour].screenTemperature);
|
currentWeather.temperature = forecastDataHours[hour].screenTemperature;
|
||||||
currentWeather.minTemperature = this.convertTemp(forecastDataHours[hour].minScreenAirTemp);
|
currentWeather.minTemperature = forecastDataHours[hour].minScreenAirTemp;
|
||||||
currentWeather.maxTemperature = this.convertTemp(forecastDataHours[hour].maxScreenAirTemp);
|
currentWeather.maxTemperature = forecastDataHours[hour].maxScreenAirTemp;
|
||||||
currentWeather.weatherType = this.convertWeatherType(forecastDataHours[hour].significantWeatherCode);
|
currentWeather.weatherType = this.convertWeatherType(forecastDataHours[hour].significantWeatherCode);
|
||||||
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.precipitation = forecastDataHours[hour].probOfPrecipitation;
|
||||||
currentWeather.feelsLikeTemp = this.convertTemp(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
|
||||||
// Note the units of the supplied data when using this (see top of file)
|
// Note the units of the supplied data when using this (see top of file)
|
||||||
currentWeather.rawData = forecastDataHours[hour];
|
currentWeather.rawData = forecastDataHours[hour];
|
||||||
}
|
}
|
||||||
|
@ -148,7 +145,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||||
|
|
||||||
// Determine the sunrise/sunset times - (still) not supplied in UK Met Office data
|
// Determine the sunrise/sunset times - (still) not supplied in UK Met Office data
|
||||||
// Passes {longitude, latitude} to SunCalc, could pass height to, but
|
// Passes {longitude, latitude} to SunCalc, could pass height to, but
|
||||||
// SunCalc.getTimes doesnt take that into account
|
// SunCalc.getTimes doesn't take that into account
|
||||||
currentWeather.updateSunTime(this.config.lat, this.config.lon);
|
currentWeather.updateSunTime(this.config.lat, this.config.lon);
|
||||||
|
|
||||||
return currentWeather;
|
return currentWeather;
|
||||||
|
@ -158,7 +155,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||||
fetchWeatherForecast() {
|
fetchWeatherForecast() {
|
||||||
this.fetchWeather(this.getUrl("daily"), this.getHeaders())
|
this.fetchWeather(this.getUrl("daily"), this.getHeaders())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
// Check data is useable
|
// Check data is usable
|
||||||
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length === 0) {
|
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length === 0) {
|
||||||
// Did not receive usable new data.
|
// Did not receive usable new data.
|
||||||
// Maybe this needs a better check?
|
// Maybe this needs a better check?
|
||||||
|
@ -178,7 +175,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're new data available
|
// Let the module know there is new data available
|
||||||
.finally(() => this.updateAvailable());
|
.finally(() => this.updateAvailable());
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -194,7 +191,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||||
|
|
||||||
// Go through each day in the forecasts
|
// Go through each day in the forecasts
|
||||||
for (let day in forecastDataDays) {
|
for (let day in forecastDataDays) {
|
||||||
const forecastWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
const forecastWeather = new WeatherObject();
|
||||||
|
|
||||||
// Get date of forecast
|
// Get date of forecast
|
||||||
let forecastDate = moment.utc(forecastDataDays[day].time);
|
let forecastDate = moment.utc(forecastDataDays[day].time);
|
||||||
|
@ -202,11 +199,11 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||||
// Check if forecast is for today or in the future (i.e., ignore yesterday's forecast)
|
// Check if forecast is for today or in the future (i.e., ignore yesterday's forecast)
|
||||||
if (forecastDate.isSameOrAfter(today)) {
|
if (forecastDate.isSameOrAfter(today)) {
|
||||||
forecastWeather.date = forecastDate;
|
forecastWeather.date = forecastDate;
|
||||||
forecastWeather.minTemperature = this.convertTemp(forecastDataDays[day].nightMinScreenTemperature);
|
forecastWeather.minTemperature = forecastDataDays[day].nightMinScreenTemperature;
|
||||||
forecastWeather.maxTemperature = this.convertTemp(forecastDataDays[day].dayMaxScreenTemperature);
|
forecastWeather.maxTemperature = forecastDataDays[day].dayMaxScreenTemperature;
|
||||||
|
|
||||||
// Using daytime forecast values
|
// Using daytime forecast values
|
||||||
forecastWeather.windSpeed = this.convertWindSpeed(forecastDataDays[day].midday10MWindSpeed);
|
forecastWeather.windSpeed = forecastDataDays[day].midday10MWindSpeed;
|
||||||
forecastWeather.windDirection = forecastDataDays[day].midday10MWindDirection;
|
forecastWeather.windDirection = forecastDataDays[day].midday10MWindDirection;
|
||||||
forecastWeather.weatherType = this.convertWeatherType(forecastDataDays[day].daySignificantWeatherCode);
|
forecastWeather.weatherType = this.convertWeatherType(forecastDataDays[day].daySignificantWeatherCode);
|
||||||
forecastWeather.precipitation = forecastDataDays[day].dayProbabilityOfPrecipitation;
|
forecastWeather.precipitation = forecastDataDays[day].dayProbabilityOfPrecipitation;
|
||||||
|
@ -214,9 +211,9 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||||
forecastWeather.humidity = forecastDataDays[day].middayRelativeHumidity;
|
forecastWeather.humidity = forecastDataDays[day].middayRelativeHumidity;
|
||||||
forecastWeather.rain = forecastDataDays[day].dayProbabilityOfRain;
|
forecastWeather.rain = forecastDataDays[day].dayProbabilityOfRain;
|
||||||
forecastWeather.snow = forecastDataDays[day].dayProbabilityOfSnow;
|
forecastWeather.snow = forecastDataDays[day].dayProbabilityOfSnow;
|
||||||
forecastWeather.feelsLikeTemp = this.convertTemp(forecastDataDays[day].dayMaxFeelsLikeTemp);
|
forecastWeather.feelsLikeTemp = forecastDataDays[day].dayMaxFeelsLikeTemp;
|
||||||
|
|
||||||
// Pass on full details so they can be used in custom templates
|
// Pass on full details, so they can be used in custom templates
|
||||||
// Note the units of the supplied data when using this (see top of file)
|
// Note the units of the supplied data when using this (see top of file)
|
||||||
forecastWeather.rawData = forecastDataDays[day];
|
forecastWeather.rawData = forecastDataDays[day];
|
||||||
|
|
||||||
|
@ -232,27 +229,6 @@ WeatherProvider.register("ukmetofficedatahub", {
|
||||||
this.fetchedLocationName = name;
|
this.fetchedLocationName = name;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Convert temperatures to Fahrenheit (from degrees C), if required
|
|
||||||
convertTemp(tempInC) {
|
|
||||||
return this.config.tempUnits === "imperial" ? (tempInC * 9) / 5 + 32 : tempInC;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Convert wind speed from metres per second
|
|
||||||
// To keep the supplied metres per second units, use "mps"
|
|
||||||
// To use kilometres per hour, use "kph"
|
|
||||||
// Else assumed imperial and the value is returned in miles per hour (a Met Office user is likely to be UK-based)
|
|
||||||
convertWindSpeed(windInMpS) {
|
|
||||||
if (this.config.windUnits === "mps") {
|
|
||||||
return windInMpS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.config.windUnits === "kph" || this.config.windUnits === "metric" || this.config.useKmh) {
|
|
||||||
return windInMpS * 3.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
return windInMpS * 2.23694;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Match the Met Office "significant weather code" to a weathericons.css icon
|
// Match the Met Office "significant weather code" to a weathericons.css icon
|
||||||
// Use: https://metoffice.apiconnect.ibmcloud.com/metoffice/production/node/264
|
// Use: https://metoffice.apiconnect.ibmcloud.com/metoffice/production/node/264
|
||||||
// and: https://erikflowers.github.io/weather-icons/
|
// and: https://erikflowers.github.io/weather-icons/
|
||||||
|
|
|
@ -23,11 +23,6 @@ WeatherProvider.register("weatherbit", {
|
||||||
lon: 0
|
lon: 0
|
||||||
},
|
},
|
||||||
|
|
||||||
units: {
|
|
||||||
imperial: "I",
|
|
||||||
metric: "M"
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchedLocation: function () {
|
fetchedLocation: function () {
|
||||||
return this.fetchedLocationName || "";
|
return this.fetchedLocationName || "";
|
||||||
},
|
},
|
||||||
|
@ -95,8 +90,7 @@ WeatherProvider.register("weatherbit", {
|
||||||
|
|
||||||
// Create a URL from the config and base URL.
|
// Create a URL from the config and base URL.
|
||||||
getUrl() {
|
getUrl() {
|
||||||
const units = this.units[this.config.units] || "auto";
|
return `${this.config.apiBase}${this.config.weatherEndpoint}?lat=${this.config.lat}&lon=${this.config.lon}&units=M&key=${this.config.apiKey}`;
|
||||||
return `${this.config.apiBase}${this.config.weatherEndpoint}?lat=${this.config.lat}&lon=${this.config.lon}&units=${units}&key=${this.config.apiKey}`;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Implement WeatherDay generator.
|
// Implement WeatherDay generator.
|
||||||
|
@ -106,9 +100,9 @@ WeatherProvider.register("weatherbit", {
|
||||||
let tzOffset = d.getTimezoneOffset();
|
let tzOffset = d.getTimezoneOffset();
|
||||||
tzOffset = tzOffset * -1;
|
tzOffset = tzOffset * -1;
|
||||||
|
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const currentWeather = new WeatherObject();
|
||||||
|
|
||||||
currentWeather.date = moment(currentWeatherData.data[0].ts, "X");
|
currentWeather.date = moment.unix(currentWeatherData.data[0].ts);
|
||||||
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);
|
||||||
|
@ -126,7 +120,7 @@ WeatherProvider.register("weatherbit", {
|
||||||
const days = [];
|
const days = [];
|
||||||
|
|
||||||
for (const forecast of forecasts) {
|
for (const forecast of forecasts) {
|
||||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const weather = new WeatherObject();
|
||||||
|
|
||||||
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;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global WeatherProvider, WeatherObject */
|
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||||
|
|
||||||
/* MagicMirror²
|
/* MagicMirror²
|
||||||
* Module: Weather
|
* Module: Weather
|
||||||
|
@ -23,36 +23,19 @@ WeatherProvider.register("weatherflow", {
|
||||||
stationid: ""
|
stationid: ""
|
||||||
},
|
},
|
||||||
|
|
||||||
units: {
|
|
||||||
imperial: {
|
|
||||||
temp: "f",
|
|
||||||
wind: "mph",
|
|
||||||
pressure: "hpa",
|
|
||||||
precip: "in",
|
|
||||||
distance: "mi"
|
|
||||||
},
|
|
||||||
metric: {
|
|
||||||
temp: "c",
|
|
||||||
wind: "kph",
|
|
||||||
pressure: "mb",
|
|
||||||
precip: "mm",
|
|
||||||
distance: "km"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchCurrentWeather() {
|
fetchCurrentWeather() {
|
||||||
this.fetchData(this.getUrl())
|
this.fetchData(this.getUrl())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
const currentWeather = new WeatherObject();
|
||||||
currentWeather.date = moment();
|
currentWeather.date = moment();
|
||||||
|
|
||||||
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 = data.current_conditions.wind_avg;
|
currentWeather.windSpeed = WeatherUtils.convertWindToMs(data.current_conditions.wind_avg);
|
||||||
currentWeather.windDirection = data.current_conditions.wind_direction;
|
currentWeather.windDirection = data.current_conditions.wind_direction;
|
||||||
currentWeather.weatherType = data.forecast.daily[0].icon;
|
currentWeather.weatherType = data.forecast.daily[0].icon;
|
||||||
currentWeather.sunrise = moment(data.forecast.daily[0].sunrise, "X");
|
currentWeather.sunrise = moment.unix(data.forecast.daily[0].sunrise);
|
||||||
currentWeather.sunset = moment(data.forecast.daily[0].sunset, "X");
|
currentWeather.sunset = moment.unix(data.forecast.daily[0].sunset);
|
||||||
this.setCurrentWeather(currentWeather);
|
this.setCurrentWeather(currentWeather);
|
||||||
})
|
})
|
||||||
.catch(function (request) {
|
.catch(function (request) {
|
||||||
|
@ -67,9 +50,9 @@ WeatherProvider.register("weatherflow", {
|
||||||
const days = [];
|
const days = [];
|
||||||
|
|
||||||
for (const forecast of data.forecast.daily) {
|
for (const forecast of data.forecast.daily) {
|
||||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
const weather = new WeatherObject();
|
||||||
|
|
||||||
weather.date = moment(forecast.day_start_local, "X");
|
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.weatherType = forecast.icon;
|
weather.weatherType = forecast.icon;
|
||||||
|
@ -88,22 +71,6 @@ WeatherProvider.register("weatherflow", {
|
||||||
|
|
||||||
// Create a URL from the config and base URL.
|
// Create a URL from the config and base URL.
|
||||||
getUrl() {
|
getUrl() {
|
||||||
return (
|
return `${this.config.apiBase}better_forecast?station_id=${this.config.stationid}&units_temp=c&units_wind=kph&units_pressure=mb&units_precip=mm&units_distance=km&token=${this.config.token}`;
|
||||||
this.config.apiBase +
|
|
||||||
"better_forecast?station_id=" +
|
|
||||||
this.config.stationid +
|
|
||||||
"&units_temp=" +
|
|
||||||
this.units[this.config.units].temp +
|
|
||||||
"&units_wind=" +
|
|
||||||
this.units[this.config.units].wind +
|
|
||||||
"&units_pressure=" +
|
|
||||||
this.units[this.config.units].pressure +
|
|
||||||
"&units_precip=" +
|
|
||||||
this.units[this.config.units].precip +
|
|
||||||
"&units_distance=" +
|
|
||||||
this.units[this.config.units].distance +
|
|
||||||
"&token=" +
|
|
||||||
this.config.token
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global WeatherProvider, WeatherObject */
|
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||||
|
|
||||||
/* MagicMirror²
|
/* MagicMirror²
|
||||||
* Module: Weather
|
* Module: Weather
|
||||||
|
@ -22,7 +22,6 @@ WeatherProvider.register("weathergov", {
|
||||||
// Set the default config properties that is specific to this provider
|
// Set the default config properties that is specific to this provider
|
||||||
defaults: {
|
defaults: {
|
||||||
apiBase: "https://api.weather.gov/points/",
|
apiBase: "https://api.weather.gov/points/",
|
||||||
weatherEndpoint: "/forecast",
|
|
||||||
lat: 0,
|
lat: 0,
|
||||||
lon: 0
|
lon: 0
|
||||||
},
|
},
|
||||||
|
@ -57,7 +56,7 @@ WeatherProvider.register("weathergov", {
|
||||||
// Overwrite the fetchCurrentWeather method.
|
// Overwrite the fetchCurrentWeather method.
|
||||||
fetchCurrentWeather() {
|
fetchCurrentWeather() {
|
||||||
if (!this.configURLs) {
|
if (!this.configURLs) {
|
||||||
Log.info("fetch wx waiting on config URLs");
|
Log.info("fetchCurrentWeather: fetch wx waiting on config URLs");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.fetchData(this.stationObsURL)
|
this.fetchData(this.stationObsURL)
|
||||||
|
@ -78,7 +77,7 @@ WeatherProvider.register("weathergov", {
|
||||||
// Overwrite the fetchWeatherForecast method.
|
// Overwrite the fetchWeatherForecast method.
|
||||||
fetchWeatherForecast() {
|
fetchWeatherForecast() {
|
||||||
if (!this.configURLs) {
|
if (!this.configURLs) {
|
||||||
Log.info("fetch wx waiting on config URLs");
|
Log.info("fetchWeatherForecast: fetch wx waiting on config URLs");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.fetchData(this.forecastURL)
|
this.fetchData(this.forecastURL)
|
||||||
|
@ -96,6 +95,28 @@ WeatherProvider.register("weathergov", {
|
||||||
.finally(() => this.updateAvailable());
|
.finally(() => this.updateAvailable());
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Overwrite the fetchWeatherHourly method.
|
||||||
|
fetchWeatherHourly() {
|
||||||
|
if (!this.configURLs) {
|
||||||
|
Log.info("fetchWeatherHourly: fetch wx waiting on config URLs");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.fetchData(this.forecastHourlyURL)
|
||||||
|
.then((data) => {
|
||||||
|
if (!data) {
|
||||||
|
// Did not receive usable new data.
|
||||||
|
// Maybe this needs a better check?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hourly = this.generateWeatherObjectsFromHourly(data.properties.periods);
|
||||||
|
this.setWeatherHourly(hourly);
|
||||||
|
})
|
||||||
|
.catch(function (request) {
|
||||||
|
Log.error("Could not load data ... ", request);
|
||||||
|
})
|
||||||
|
.finally(() => this.updateAvailable());
|
||||||
|
},
|
||||||
|
|
||||||
/** Weather.gov Specific Methods - These are not part of the default provider methods */
|
/** Weather.gov Specific Methods - These are not part of the default provider methods */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -110,8 +131,8 @@ WeatherProvider.register("weathergov", {
|
||||||
}
|
}
|
||||||
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;
|
this.forecastURL = data.properties.forecast + "?units=si";
|
||||||
this.forecastHourlyURL = data.properties.forecastHourly;
|
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
|
||||||
|
@ -130,14 +151,49 @@ WeatherProvider.register("weathergov", {
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
// excellent, let's fetch some actual wx data
|
// excellent, let's fetch some actual wx data
|
||||||
this.configURLs = true;
|
this.configURLs = true;
|
||||||
|
|
||||||
// handle 'forecast' config, fall back to 'current'
|
// handle 'forecast' config, fall back to 'current'
|
||||||
if (config.type === "forecast") {
|
if (config.type === "forecast") {
|
||||||
this.fetchWeatherForecast();
|
this.fetchWeatherForecast();
|
||||||
|
} else if (config.type === "hourly") {
|
||||||
|
this.fetchWeatherHourly();
|
||||||
} else {
|
} else {
|
||||||
this.fetchCurrentWeather();
|
this.fetchCurrentWeather();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/*
|
||||||
|
* Generate a WeatherObject based on hourlyWeatherInformation
|
||||||
|
* Weather.gov API uses specific units; API does not include choice of units
|
||||||
|
* ... object needs data in units based on config!
|
||||||
|
*/
|
||||||
|
generateWeatherObjectsFromHourly(forecasts) {
|
||||||
|
const days = [];
|
||||||
|
|
||||||
|
// variable for date
|
||||||
|
let weather = new WeatherObject();
|
||||||
|
for (const forecast of forecasts) {
|
||||||
|
weather.date = moment(forecast.startTime.slice(0, 19));
|
||||||
|
if (forecast.windSpeed.search(" ") < 0) {
|
||||||
|
weather.windSpeed = forecast.windSpeed;
|
||||||
|
} else {
|
||||||
|
weather.windSpeed = forecast.windSpeed.slice(0, forecast.windSpeed.search(" "));
|
||||||
|
}
|
||||||
|
weather.windDirection = this.convertWindDirection(forecast.windDirection);
|
||||||
|
weather.temperature = forecast.temperature;
|
||||||
|
weather.tempUnits = forecast.temperatureUnit;
|
||||||
|
// use the forecast isDayTime attribute to help build the weatherType label
|
||||||
|
weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime);
|
||||||
|
|
||||||
|
days.push(weather);
|
||||||
|
|
||||||
|
weather = new WeatherObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
// push weather information to days array
|
||||||
|
days.push(weather);
|
||||||
|
return days;
|
||||||
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Generate a WeatherObject based on currentWeatherInformation
|
* Generate a WeatherObject based on currentWeatherInformation
|
||||||
|
@ -145,24 +201,24 @@ WeatherProvider.register("weathergov", {
|
||||||
* ... object needs data in units based on config!
|
* ... object needs data in units based on config!
|
||||||
*/
|
*/
|
||||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
const currentWeather = new WeatherObject();
|
||||||
|
|
||||||
currentWeather.date = moment(currentWeatherData.timestamp);
|
currentWeather.date = moment(currentWeatherData.timestamp);
|
||||||
currentWeather.temperature = this.convertTemp(currentWeatherData.temperature.value);
|
currentWeather.temperature = currentWeatherData.temperature.value;
|
||||||
currentWeather.windSpeed = this.convertSpeed(currentWeatherData.windSpeed.value);
|
currentWeather.windSpeed = WeatherUtils.convertWindToMs(currentWeatherData.windSpeed.value);
|
||||||
currentWeather.windDirection = currentWeatherData.windDirection.value;
|
currentWeather.windDirection = currentWeatherData.windDirection.value;
|
||||||
currentWeather.minTemperature = this.convertTemp(currentWeatherData.minTemperatureLast24Hours.value);
|
currentWeather.minTemperature = currentWeatherData.minTemperatureLast24Hours.value;
|
||||||
currentWeather.maxTemperature = this.convertTemp(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.rain = null;
|
||||||
currentWeather.snow = null;
|
currentWeather.snow = null;
|
||||||
currentWeather.precipitation = this.convertLength(currentWeatherData.precipitationLastHour.value);
|
currentWeather.precipitation = this.convertLength(currentWeatherData.precipitationLastHour.value);
|
||||||
if (currentWeatherData.heatIndex.value !== null) {
|
if (currentWeatherData.heatIndex.value !== null) {
|
||||||
currentWeather.feelsLikeTemp = this.convertTemp(currentWeatherData.heatIndex.value);
|
currentWeather.feelsLikeTemp = currentWeatherData.heatIndex.value;
|
||||||
} else if (currentWeatherData.windChill.value !== null) {
|
} else if (currentWeatherData.windChill.value !== null) {
|
||||||
currentWeather.feelsLikeTemp = this.convertTemp(currentWeatherData.windChill.value);
|
currentWeather.feelsLikeTemp = currentWeatherData.windChill.value;
|
||||||
} else {
|
} else {
|
||||||
currentWeather.feelsLikeTemp = this.convertTemp(currentWeatherData.temperature.value);
|
currentWeather.feelsLikeTemp = currentWeatherData.temperature.value;
|
||||||
}
|
}
|
||||||
// determine the sunrise/sunset times - not supplied in weather.gov data
|
// determine the sunrise/sunset times - not supplied in weather.gov data
|
||||||
currentWeather.updateSunTime(this.config.lat, this.config.lon);
|
currentWeather.updateSunTime(this.config.lat, this.config.lon);
|
||||||
|
@ -191,7 +247,7 @@ WeatherProvider.register("weathergov", {
|
||||||
let maxTemp = [];
|
let maxTemp = [];
|
||||||
// variable for date
|
// variable for date
|
||||||
let date = "";
|
let date = "";
|
||||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
let weather = new WeatherObject();
|
||||||
weather.precipitation = 0;
|
weather.precipitation = 0;
|
||||||
|
|
||||||
for (const forecast of forecasts) {
|
for (const forecast of forecasts) {
|
||||||
|
@ -203,7 +259,7 @@ WeatherProvider.register("weathergov", {
|
||||||
// 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
|
||||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
weather = new WeatherObject();
|
||||||
|
|
||||||
minTemp = [];
|
minTemp = [];
|
||||||
maxTemp = [];
|
maxTemp = [];
|
||||||
|
@ -242,26 +298,6 @@ WeatherProvider.register("weathergov", {
|
||||||
/*
|
/*
|
||||||
* Unit conversions
|
* Unit conversions
|
||||||
*/
|
*/
|
||||||
// conversion to fahrenheit
|
|
||||||
convertTemp(temp) {
|
|
||||||
if (this.config.tempUnits === "imperial") {
|
|
||||||
return (9 / 5) * temp + 32;
|
|
||||||
} else {
|
|
||||||
return temp;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// conversion to mph or kmh
|
|
||||||
convertSpeed(metSec) {
|
|
||||||
if (this.config.windUnits === "imperial") {
|
|
||||||
return metSec * 2.23694;
|
|
||||||
} else {
|
|
||||||
if (this.config.useKmh) {
|
|
||||||
return metSec * 3.6;
|
|
||||||
} else {
|
|
||||||
return metSec;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// conversion to inches
|
// conversion to inches
|
||||||
convertLength(meters) {
|
convertLength(meters) {
|
||||||
if (this.config.units === "imperial") {
|
if (this.config.units === "imperial") {
|
||||||
|
@ -339,31 +375,5 @@ WeatherProvider.register("weathergov", {
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
|
||||||
|
|
||||||
/*
|
|
||||||
Convert the direction into Degrees
|
|
||||||
*/
|
|
||||||
convertWindDirection(windDirection) {
|
|
||||||
const windCardinals = {
|
|
||||||
N: 0,
|
|
||||||
NNE: 22,
|
|
||||||
NE: 45,
|
|
||||||
ENE: 67,
|
|
||||||
E: 90,
|
|
||||||
ESE: 112,
|
|
||||||
SE: 135,
|
|
||||||
SSE: 157,
|
|
||||||
S: 180,
|
|
||||||
SSW: 202,
|
|
||||||
SW: 225,
|
|
||||||
WSW: 247,
|
|
||||||
W: 270,
|
|
||||||
WNW: 292,
|
|
||||||
NW: 315,
|
|
||||||
NNW: 337
|
|
||||||
};
|
|
||||||
|
|
||||||
return windCardinals.hasOwnProperty(windDirection) ? windCardinals[windDirection] : null;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
626
modules/default/weather/providers/yr.js
Normal file
626
modules/default/weather/providers/yr.js
Normal file
|
@ -0,0 +1,626 @@
|
||||||
|
/* global WeatherProvider, WeatherObject */
|
||||||
|
|
||||||
|
/* MagicMirror²
|
||||||
|
* Module: Weather
|
||||||
|
* Provider: Yr.no
|
||||||
|
*
|
||||||
|
* By Magnus Marthinsen
|
||||||
|
* MIT Licensed
|
||||||
|
*
|
||||||
|
* This class is a provider for Yr.no, a norwegian sweather service.
|
||||||
|
*
|
||||||
|
* Terms of service: https://developer.yr.no/doc/TermsOfService/
|
||||||
|
*/
|
||||||
|
WeatherProvider.register("yr", {
|
||||||
|
providerName: "Yr",
|
||||||
|
|
||||||
|
// Set the default config properties that is specific to this provider
|
||||||
|
defaults: {
|
||||||
|
useCorsProxy: true,
|
||||||
|
apiBase: "https://api.met.no/weatherapi",
|
||||||
|
altitude: 0,
|
||||||
|
currentForecastHours: 1 //1, 6 or 12
|
||||||
|
},
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (typeof Storage === "undefined") {
|
||||||
|
//local storage unavailable
|
||||||
|
Log.error("The Yr weather provider requires local storage.");
|
||||||
|
throw new Error("Local storage not available");
|
||||||
|
}
|
||||||
|
Log.info(`Weather provider: ${this.providerName} started.`);
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchCurrentWeather() {
|
||||||
|
this.getCurrentWeather()
|
||||||
|
.then((currentWeather) => {
|
||||||
|
this.setCurrentWeather(currentWeather);
|
||||||
|
this.updateAvailable();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
Log.error(error);
|
||||||
|
throw new Error(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async getCurrentWeather() {
|
||||||
|
const getRequests = [this.getWeatherData(), this.getStellarData()];
|
||||||
|
const [weatherData, stellarData] = await Promise.all(getRequests);
|
||||||
|
if (!stellarData) {
|
||||||
|
Log.warn("No stelar data available.");
|
||||||
|
}
|
||||||
|
if (!weatherData.properties.timeseries || !weatherData.properties.timeseries[0]) {
|
||||||
|
Log.error("No weather data available.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentTime = moment();
|
||||||
|
let forecast = weatherData.properties.timeseries[0];
|
||||||
|
let closestTimeInPast = currentTime.diff(moment(forecast.time));
|
||||||
|
for (const forecastTime of weatherData.properties.timeseries) {
|
||||||
|
const comparison = currentTime.diff(moment(forecastTime.time));
|
||||||
|
if (0 < comparison && comparison < closestTimeInPast) {
|
||||||
|
closestTimeInPast = comparison;
|
||||||
|
forecast = forecastTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const forecastXHours = this.getForecastForXHoursFrom(forecast.data);
|
||||||
|
forecast.weatherType = this.convertWeatherType(forecastXHours.summary.symbol_code, forecast.time);
|
||||||
|
forecast.precipitation = forecastXHours.details?.precipitation_amount;
|
||||||
|
forecast.minTemperature = forecastXHours.details?.air_temperature_min;
|
||||||
|
forecast.maxTemperature = forecastXHours.details?.air_temperature_max;
|
||||||
|
return this.getWeatherDataFrom(forecast, stellarData, weatherData.properties.meta.units);
|
||||||
|
},
|
||||||
|
|
||||||
|
getWeatherData() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// If a user has several Yr-modules, for instance one current and one forecast, the API calls must be synchronized across classes.
|
||||||
|
// This is to avoid multiple similar calls to the API.
|
||||||
|
let shouldWait = localStorage.getItem("yrIsFetchingWeatherData");
|
||||||
|
if (shouldWait) {
|
||||||
|
const checkForGo = setInterval(function () {
|
||||||
|
shouldWait = localStorage.getItem("yrIsFetchingWeatherData");
|
||||||
|
}, 100);
|
||||||
|
setTimeout(function () {
|
||||||
|
clearInterval(checkForGo);
|
||||||
|
shouldWait = false;
|
||||||
|
}, 5000); //Assume other fetch finished but failed to remove lock
|
||||||
|
const attemptFetchWeather = setInterval(() => {
|
||||||
|
if (!shouldWait) {
|
||||||
|
clearInterval(checkForGo);
|
||||||
|
clearInterval(attemptFetchWeather);
|
||||||
|
this.getWeatherDataFromYrOrCache(resolve, reject);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
this.getWeatherDataFromYrOrCache(resolve, reject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getWeatherDataFromYrOrCache(resolve, reject) {
|
||||||
|
localStorage.setItem("yrIsFetchingWeatherData", "true");
|
||||||
|
|
||||||
|
let weatherData = this.getWeatherDataFromCache();
|
||||||
|
if (this.weatherDataIsValid(weatherData)) {
|
||||||
|
localStorage.removeItem("yrIsFetchingWeatherData");
|
||||||
|
Log.debug("Weather data found in cache.");
|
||||||
|
resolve(weatherData);
|
||||||
|
} else {
|
||||||
|
this.getWeatherDataFromYr(weatherData?.downloadedAt)
|
||||||
|
.then((weatherData) => {
|
||||||
|
Log.debug("Got weather data from yr.");
|
||||||
|
if (weatherData) {
|
||||||
|
this.cacheWeatherData(weatherData);
|
||||||
|
} else {
|
||||||
|
//Undefined if unchanged
|
||||||
|
weatherData = this.getWeatherDataFromCache();
|
||||||
|
}
|
||||||
|
resolve(weatherData);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Log.error(err);
|
||||||
|
reject("Unable to get weather data from Yr.");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
localStorage.removeItem("yrIsFetchingWeatherData");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
weatherDataIsValid(weatherData) {
|
||||||
|
return (
|
||||||
|
weatherData &&
|
||||||
|
weatherData.timeout &&
|
||||||
|
0 < moment(weatherData.timeout).diff(moment()) &&
|
||||||
|
(!weatherData.geometry || !weatherData.geometry.coordinates || !weatherData.geometry.coordinates.length < 2 || (weatherData.geometry.coordinates[0] === this.config.lat && weatherData.geometry.coordinates[1] === this.config.lon))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
getWeatherDataFromCache() {
|
||||||
|
const weatherData = localStorage.getItem("weatherData");
|
||||||
|
if (weatherData) {
|
||||||
|
return JSON.parse(weatherData);
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getWeatherDataFromYr(currentDataFetchedAt) {
|
||||||
|
const requestHeaders = [{ name: "Accept", value: "application/json" }];
|
||||||
|
if (currentDataFetchedAt) {
|
||||||
|
requestHeaders.push({ name: "If-Modified-Since", value: currentDataFetchedAt });
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedResponseHeaders = ["expires", "date"];
|
||||||
|
|
||||||
|
return this.fetchData(this.getForecastUrl(), "json", requestHeaders, expectedResponseHeaders)
|
||||||
|
.then((data) => {
|
||||||
|
if (!data || !data.headers) return data;
|
||||||
|
data.timeout = data.headers.find((header) => header.name === "expires").value;
|
||||||
|
data.downloadedAt = data.headers.find((header) => header.name === "date").value;
|
||||||
|
data.headers = undefined;
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Log.error("Could not load weather data.", err);
|
||||||
|
throw new Error(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getForecastUrl() {
|
||||||
|
if (!this.config.lat) {
|
||||||
|
Log.error("Latitude not provided.");
|
||||||
|
throw new Error("Latitude not provided.");
|
||||||
|
}
|
||||||
|
if (!this.config.lon) {
|
||||||
|
Log.error("Longitude not provided.");
|
||||||
|
throw new Error("Longitude not provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let lat = this.config.lat.toString();
|
||||||
|
let lon = this.config.lon.toString();
|
||||||
|
const altitude = this.config.altitude ?? 0;
|
||||||
|
|
||||||
|
if (lat.includes(".") && lat.split(".")[1].length > 4) {
|
||||||
|
Log.warn("Latitude is too specific for weather data. Do not use more than four decimals. Trimming to maximum length.");
|
||||||
|
const latParts = lat.split(".");
|
||||||
|
lat = `${latParts[0]}.${latParts[1].substring(0, 4)}`;
|
||||||
|
}
|
||||||
|
if (lon.includes(".") && lon.split(".")[1].length > 4) {
|
||||||
|
Log.warn("Longitude is too specific for weather data. Do not use more than four decimals. Trimming to maximum length.");
|
||||||
|
const lonParts = lon.split(".");
|
||||||
|
lon = `${lonParts[0]}.${lonParts[1].substring(0, 4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${this.config.apiBase}/locationforecast/2.0/complete?&altitude=${altitude}&lat=${lat}&lon=${lon}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
cacheWeatherData(weatherData) {
|
||||||
|
localStorage.setItem("weatherData", JSON.stringify(weatherData));
|
||||||
|
},
|
||||||
|
|
||||||
|
getAuthenticationString() {
|
||||||
|
if (!this.config.authenticationEmail) throw new Error("Authentication email not provided.");
|
||||||
|
return `${this.config.applicaitionName} ${this.config.authenticationEmail}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
getStellarData() {
|
||||||
|
// If a user has several Yr-modules, for instance one current and one forecast, the API calls must be synchronized across classes.
|
||||||
|
// This is to avoid multiple similar calls to the API.
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let shouldWait = localStorage.getItem("yrIsFetchingStellarData");
|
||||||
|
if (shouldWait) {
|
||||||
|
const checkForGo = setInterval(function () {
|
||||||
|
shouldWait = localStorage.getItem("yrIsFetchingStellarData");
|
||||||
|
}, 100);
|
||||||
|
setTimeout(function () {
|
||||||
|
clearInterval(checkForGo);
|
||||||
|
shouldWait = false;
|
||||||
|
}, 5000); //Assume other fetch finished but failed to remove lock
|
||||||
|
const attemptFetchWeather = setInterval(() => {
|
||||||
|
if (!shouldWait) {
|
||||||
|
clearInterval(checkForGo);
|
||||||
|
clearInterval(attemptFetchWeather);
|
||||||
|
this.getStellarDataFromYrOrCache(resolve, reject);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
this.getStellarDataFromYrOrCache(resolve, reject);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getStellarDataFromYrOrCache(resolve, reject) {
|
||||||
|
localStorage.setItem("yrIsFetchingStellarData", "true");
|
||||||
|
|
||||||
|
let stellarData = this.getStellarDataFromCache();
|
||||||
|
const today = moment().format("YYYY-MM-DD");
|
||||||
|
const tomorrow = moment().add(1, "days").format("YYYY-MM-DD");
|
||||||
|
if (stellarData && stellarData.today && stellarData.today.date === today && stellarData.tomorrow && stellarData.tomorrow.date === tomorrow) {
|
||||||
|
Log.debug("Stellar data found in cache.");
|
||||||
|
localStorage.removeItem("yrIsFetchingStellarData");
|
||||||
|
resolve(stellarData);
|
||||||
|
} else if (stellarData && stellarData.tomorrow && stellarData.tomorrow.date === today) {
|
||||||
|
Log.debug("stellar data for today found in cache, but not for tomorrow.");
|
||||||
|
stellarData.today = stellarData.tomorrow;
|
||||||
|
this.getStellarDataFromYr(tomorrow)
|
||||||
|
.then((data) => {
|
||||||
|
if (data) {
|
||||||
|
data.date = tomorrow;
|
||||||
|
stellarData.tomorrow = data;
|
||||||
|
this.cacheStellarData(stellarData);
|
||||||
|
resolve(stellarData);
|
||||||
|
} else {
|
||||||
|
reject("No stellar data returned from Yr for " + tomorrow);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Log.error(err);
|
||||||
|
reject("Unable to get stellar data from Yr for " + tomorrow);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
localStorage.removeItem("yrIsFetchingStellarData");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.getStellarDataFromYr(today, 2)
|
||||||
|
.then((stellarData) => {
|
||||||
|
if (stellarData) {
|
||||||
|
stellarData = {
|
||||||
|
today: stellarData
|
||||||
|
};
|
||||||
|
stellarData.tomorrow = Object.assign({}, stellarData.today);
|
||||||
|
stellarData.today.date = today;
|
||||||
|
stellarData.tomorrow.date = tomorrow;
|
||||||
|
this.cacheStellarData(stellarData);
|
||||||
|
resolve(stellarData);
|
||||||
|
} else {
|
||||||
|
Log.error("Something went wrong when fetching stellar data. Responses: " + stellarData);
|
||||||
|
reject(stellarData);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Log.error(err);
|
||||||
|
reject("Unable to get stellar data from Yr.");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
localStorage.removeItem("yrIsFetchingStellarData");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getStellarDataFromCache() {
|
||||||
|
const stellarData = localStorage.getItem("stellarData");
|
||||||
|
if (stellarData) {
|
||||||
|
return JSON.parse(stellarData);
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getStellarDataFromYr(date, days = 1) {
|
||||||
|
const requestHeaders = [{ name: "Accept", value: "application/json" }];
|
||||||
|
return this.fetchData(this.getStellarDatatUrl(date, days), "json", requestHeaders)
|
||||||
|
.then((data) => {
|
||||||
|
Log.debug("Got stellar data from yr.");
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Log.error("Could not load weather data.", err);
|
||||||
|
throw new Error(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getStellarDatatUrl(date, days) {
|
||||||
|
if (!this.config.lat) {
|
||||||
|
Log.error("Latitude not provided.");
|
||||||
|
throw new Error("Latitude not provided.");
|
||||||
|
}
|
||||||
|
if (!this.config.lon) {
|
||||||
|
Log.error("Longitude not provided.");
|
||||||
|
throw new Error("Longitude not provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let lat = this.config.lat.toString();
|
||||||
|
let lon = this.config.lon.toString();
|
||||||
|
const altitude = this.config.altitude ?? 0;
|
||||||
|
|
||||||
|
if (lat.includes(".") && lat.split(".")[1].length > 4) {
|
||||||
|
Log.warn("Latitude is too specific for stellar data. Do not use more than four decimals. Trimming to maximum length.");
|
||||||
|
const latParts = lat.split(".");
|
||||||
|
lat = `${latParts[0]}.${latParts[1].substring(0, 4)}`;
|
||||||
|
}
|
||||||
|
if (lon.includes(".") && lon.split(".")[1].length > 4) {
|
||||||
|
Log.warn("Longitude is too specific for stellar data. Do not use more than four decimals. Trimming to maximum length.");
|
||||||
|
const lonParts = lon.split(".");
|
||||||
|
lon = `${lonParts[0]}.${lonParts[1].substring(0, 4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let utcOffset = moment().utcOffset() / 60;
|
||||||
|
let utcOffsetPrefix = "%2B";
|
||||||
|
if (utcOffset < 0) {
|
||||||
|
utcOffsetPrefix = "-";
|
||||||
|
}
|
||||||
|
utcOffset = Math.abs(utcOffset);
|
||||||
|
let minutes = "00";
|
||||||
|
if (utcOffset % 1 !== 0) {
|
||||||
|
minutes = "30";
|
||||||
|
}
|
||||||
|
let hours = Math.floor(utcOffset).toString();
|
||||||
|
if (hours.length < 2) {
|
||||||
|
hours = `0${hours}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${this.config.apiBase}/sunrise/2.0/.json?date=${date}&days=${days}&height=${altitude}&lat=${lat}&lon=${lon}&offset=${utcOffsetPrefix}${hours}%3A${minutes}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
cacheStellarData(data) {
|
||||||
|
localStorage.setItem("stellarData", JSON.stringify(data));
|
||||||
|
},
|
||||||
|
|
||||||
|
getWeatherDataFrom(forecast, stellarData, units) {
|
||||||
|
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
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;
|
||||||
|
|
||||||
|
weather.date = moment(forecast.time);
|
||||||
|
weather.windSpeed = forecast.data.instant.details.wind_speed;
|
||||||
|
weather.windDirection = (forecast.data.instant.details.wind_from_direction + 180) % 360;
|
||||||
|
weather.temperature = forecast.data.instant.details.air_temperature;
|
||||||
|
weather.minTemperature = forecast.minTemperature;
|
||||||
|
weather.maxTemperature = forecast.maxTemperature;
|
||||||
|
weather.weatherType = forecast.weatherType;
|
||||||
|
weather.humidity = forecast.data.instant.details.relative_humidity;
|
||||||
|
weather.precipitation = forecast.precipitation;
|
||||||
|
weather.precipitationUnits = units.precipitation_amount;
|
||||||
|
|
||||||
|
if (stellarTimesToday) {
|
||||||
|
weather.sunset = moment(stellarTimesToday.sunset.time);
|
||||||
|
weather.sunrise = weather.sunset < moment() && stellarTimesTomorrow ? moment(stellarTimesTomorrow.sunrise.time) : moment(stellarTimesToday.sunrise.time);
|
||||||
|
}
|
||||||
|
|
||||||
|
return weather;
|
||||||
|
},
|
||||||
|
|
||||||
|
convertWeatherType(weatherType, weatherTime) {
|
||||||
|
const weatherHour = moment(weatherTime).format("HH");
|
||||||
|
|
||||||
|
const weatherTypes = {
|
||||||
|
clearsky_day: "day-sunny",
|
||||||
|
clearsky_night: "night-clear",
|
||||||
|
clearsky_polartwilight: weatherHour < 14 ? "sunrise" : "sunset",
|
||||||
|
cloudy: "cloudy",
|
||||||
|
fair_day: "day-sunny-overcast",
|
||||||
|
fair_night: "night-alt-partly-cloudy",
|
||||||
|
fair_polartwilight: "day-sunny-overcast",
|
||||||
|
fog: "fog",
|
||||||
|
heavyrain: "rain", // Possibly raindrops or raindrop
|
||||||
|
heavyrainandthunder: "thunderstorm",
|
||||||
|
heavyrainshowers_day: "day-rain",
|
||||||
|
heavyrainshowers_night: "night-alt-rain",
|
||||||
|
heavyrainshowers_polartwilight: "day-rain",
|
||||||
|
heavyrainshowersandthunder_day: "day-thunderstorm",
|
||||||
|
heavyrainshowersandthunder_night: "night-alt-thunderstorm",
|
||||||
|
heavyrainshowersandthunder_polartwilight: "day-thunderstorm",
|
||||||
|
heavysleet: "sleet",
|
||||||
|
heavysleetandthunder: "day-sleet-storm",
|
||||||
|
heavysleetshowers_day: "day-sleet",
|
||||||
|
heavysleetshowers_night: "night-alt-sleet",
|
||||||
|
heavysleetshowers_polartwilight: "day-sleet",
|
||||||
|
heavysleetshowersandthunder_day: "day-sleet-storm",
|
||||||
|
heavysleetshowersandthunder_night: "night-alt-sleet-storm",
|
||||||
|
heavysleetshowersandthunder_polartwilight: "day-sleet-storm",
|
||||||
|
heavysnow: "snow-wind",
|
||||||
|
heavysnowandthunder: "day-snow-thunderstorm",
|
||||||
|
heavysnowshowers_day: "day-snow-wind",
|
||||||
|
heavysnowshowers_night: "night-alt-snow-wind",
|
||||||
|
heavysnowshowers_polartwilight: "day-snow-wind",
|
||||||
|
heavysnowshowersandthunder_day: "day-snow-thunderstorm",
|
||||||
|
heavysnowshowersandthunder_night: "night-alt-snow-thunderstorm",
|
||||||
|
heavysnowshowersandthunder_polartwilight: "day-snow-thunderstorm",
|
||||||
|
lightrain: "rain-mix",
|
||||||
|
lightrainandthunder: "thunderstorm",
|
||||||
|
lightrainshowers_day: "day-rain-mix",
|
||||||
|
lightrainshowers_night: "night-alt-rain-mix",
|
||||||
|
lightrainshowers_polartwilight: "day-rain-mix",
|
||||||
|
lightrainshowersandthunder_day: "thunderstorm",
|
||||||
|
lightrainshowersandthunder_night: "thunderstorm",
|
||||||
|
lightrainshowersandthunder_polartwilight: "thunderstorm",
|
||||||
|
lightsleet: "day-sleet",
|
||||||
|
lightsleetandthunder: "day-sleet-storm",
|
||||||
|
lightsleetshowers_day: "day-sleet",
|
||||||
|
lightsleetshowers_night: "night-alt-sleet",
|
||||||
|
lightsleetshowers_polartwilight: "day-sleet",
|
||||||
|
lightsnow: "snowflake-cold",
|
||||||
|
lightsnowandthunder: "day-snow-thunderstorm",
|
||||||
|
lightsnowshowers_day: "day-snow-wind",
|
||||||
|
lightsnowshowers_night: "night-alt-snow-wind",
|
||||||
|
lightsnowshowers_polartwilight: "day-snow-wind",
|
||||||
|
lightssleetshowersandthunder_day: "day-sleet-storm",
|
||||||
|
lightssleetshowersandthunder_night: "night-alt-sleet-storm",
|
||||||
|
lightssleetshowersandthunder_polartwilight: "day-sleet-storm",
|
||||||
|
lightssnowshowersandthunder_day: "day-snow-thunderstorm",
|
||||||
|
lightssnowshowersandthunder_night: "night-alt-snow-thunderstorm",
|
||||||
|
lightssnowshowersandthunder_polartwilight: "day-snow-thunderstorm",
|
||||||
|
partlycloudy_day: "day-cloudy",
|
||||||
|
partlycloudy_night: "night-alt-cloudy",
|
||||||
|
partlycloudy_polartwilight: "day-cloudy",
|
||||||
|
rain: "rain",
|
||||||
|
rainandthunder: "thunderstorm",
|
||||||
|
rainshowers_day: "day-rain",
|
||||||
|
rainshowers_night: "night-alt-rain",
|
||||||
|
rainshowers_polartwilight: "day-rain",
|
||||||
|
rainshowersandthunder_day: "thunderstorm",
|
||||||
|
rainshowersandthunder_night: "lightning",
|
||||||
|
rainshowersandthunder_polartwilight: "thunderstorm",
|
||||||
|
sleet: "sleet",
|
||||||
|
sleetandthunder: "day-sleet-storm",
|
||||||
|
sleetshowers_day: "day-sleet",
|
||||||
|
sleetshowers_night: "night-alt-sleet",
|
||||||
|
sleetshowers_polartwilight: "day-sleet",
|
||||||
|
sleetshowersandthunder_day: "day-sleet-storm",
|
||||||
|
sleetshowersandthunder_night: "night-alt-sleet-storm",
|
||||||
|
sleetshowersandthunder_polartwilight: "day-sleet-storm",
|
||||||
|
snow: "snowflake-cold",
|
||||||
|
snowandthunder: "lightning",
|
||||||
|
snowshowers_day: "day-snow-wind",
|
||||||
|
snowshowers_night: "night-alt-snow-wind",
|
||||||
|
snowshowers_polartwilight: "day-snow-wind",
|
||||||
|
snowshowersandthunder_day: "day-snow-thunderstorm",
|
||||||
|
snowshowersandthunder_night: "night-alt-snow-thunderstorm",
|
||||||
|
snowshowersandthunder_polartwilight: "day-snow-thunderstorm"
|
||||||
|
};
|
||||||
|
|
||||||
|
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
getStellarTimesFrom(stellarData, date) {
|
||||||
|
for (const time of stellarData.location.time) {
|
||||||
|
if (time.date === date) {
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
getForecastForXHoursFrom(weather) {
|
||||||
|
if (this.config.currentForecastHours === 1) {
|
||||||
|
if (weather.next_1_hours) {
|
||||||
|
return weather.next_1_hours;
|
||||||
|
} else if (weather.next_6_hours) {
|
||||||
|
return weather.next_6_hours;
|
||||||
|
} else {
|
||||||
|
return weather.next_12_hours;
|
||||||
|
}
|
||||||
|
} else if (this.config.currentForecastHours === 6) {
|
||||||
|
if (weather.next_6_hours) {
|
||||||
|
return weather.next_6_hours;
|
||||||
|
} else if (weather.next_12_hours) {
|
||||||
|
return weather.next_12_hours;
|
||||||
|
} else {
|
||||||
|
return weather.next_1_hours;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (weather.next_12_hours) {
|
||||||
|
return weather.next_12_hours;
|
||||||
|
} else if (weather.next_6_hours) {
|
||||||
|
return weather.next_6_hours;
|
||||||
|
} else {
|
||||||
|
return weather.next_1_hours;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchWeatherHourly() {
|
||||||
|
this.getWeatherForecast("hourly")
|
||||||
|
.then((forecast) => {
|
||||||
|
this.setWeatherHourly(forecast);
|
||||||
|
this.updateAvailable();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
Log.error(error);
|
||||||
|
throw new Error(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async getWeatherForecast(type) {
|
||||||
|
const getRequests = [this.getWeatherData(), this.getStellarData()];
|
||||||
|
const [weatherData, stellarData] = await Promise.all(getRequests);
|
||||||
|
if (!weatherData.properties.timeseries || !weatherData.properties.timeseries[0]) {
|
||||||
|
Log.error("No weather data available.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!stellarData) {
|
||||||
|
Log.warn("No stelar data available.");
|
||||||
|
}
|
||||||
|
let forecasts;
|
||||||
|
switch (type) {
|
||||||
|
case "hourly":
|
||||||
|
forecasts = this.getHourlyForecastFrom(weatherData);
|
||||||
|
break;
|
||||||
|
case "daily":
|
||||||
|
default:
|
||||||
|
forecasts = this.getDailyForecastFrom(weatherData);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const series = [];
|
||||||
|
for (const forecast of forecasts) {
|
||||||
|
series.push(this.getWeatherDataFrom(forecast, stellarData, weatherData.properties.meta.units));
|
||||||
|
}
|
||||||
|
return series;
|
||||||
|
},
|
||||||
|
|
||||||
|
getHourlyForecastFrom(weatherData) {
|
||||||
|
const series = [];
|
||||||
|
|
||||||
|
for (const forecast of weatherData.properties.timeseries) {
|
||||||
|
forecast.symbol = forecast.data.next_1_hours?.summary?.symbol_code;
|
||||||
|
forecast.precipitation = forecast.data.next_1_hours?.details?.precipitation_amount;
|
||||||
|
forecast.minTemperature = forecast.data.next_1_hours?.details?.air_temperature_min;
|
||||||
|
forecast.maxTemperature = forecast.data.next_1_hours?.details?.air_temperature_max;
|
||||||
|
forecast.weatherType = this.convertWeatherType(forecast.symbol, forecast.time);
|
||||||
|
series.push(forecast);
|
||||||
|
}
|
||||||
|
return series;
|
||||||
|
},
|
||||||
|
|
||||||
|
getDailyForecastFrom(weatherData) {
|
||||||
|
const series = [];
|
||||||
|
|
||||||
|
const days = weatherData.properties.timeseries.reduce(function (days, forecast) {
|
||||||
|
const date = moment(forecast.time).format("YYYY-MM-DD");
|
||||||
|
days[date] = days[date] || [];
|
||||||
|
days[date].push(forecast);
|
||||||
|
return days;
|
||||||
|
}, Object.create(null));
|
||||||
|
|
||||||
|
Object.keys(days).forEach(function (time, index) {
|
||||||
|
let minTemperature = undefined;
|
||||||
|
let maxTemperature = undefined;
|
||||||
|
|
||||||
|
//Default to first entry
|
||||||
|
let forecast = days[time][0];
|
||||||
|
forecast.symbol = forecast.data.next_12_hours?.summary?.symbol_code;
|
||||||
|
forecast.precipitation = forecast.data.next_12_hours?.details?.precipitation_amount;
|
||||||
|
|
||||||
|
//Coming days
|
||||||
|
let forecastDiffToEight = undefined;
|
||||||
|
for (const timeseries of days[time]) {
|
||||||
|
if (!timeseries.data.next_6_hours) continue; //next_6_hours has the most data
|
||||||
|
|
||||||
|
if (!minTemperature || timeseries.data.next_6_hours.details.air_temperature_min < minTemperature) minTemperature = timeseries.data.next_6_hours.details.air_temperature_min;
|
||||||
|
if (!maxTemperature || maxTemperature < timeseries.data.next_6_hours.details.air_temperature_max) maxTemperature = timeseries.data.next_6_hours.details.air_temperature_max;
|
||||||
|
|
||||||
|
let closestTime = Math.abs(moment(timeseries.time).local().set({ hour: 8, minute: 0, second: 0, millisecond: 0 }).diff(moment(timeseries.time).local()));
|
||||||
|
if ((forecastDiffToEight === undefined || closestTime < forecastDiffToEight) && timeseries.data.next_12_hours) {
|
||||||
|
forecastDiffToEight = closestTime;
|
||||||
|
forecast = timeseries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const forecastXHours = forecast.data.next_12_hours ?? forecast.data.next_6_hours ?? forecast.data.next_1_hours;
|
||||||
|
if (forecastXHours) {
|
||||||
|
forecast.symbol = forecastXHours.summary?.symbol_code;
|
||||||
|
forecast.precipitation = forecastXHours.details?.precipitation_amount;
|
||||||
|
forecast.minTemperature = minTemperature;
|
||||||
|
forecast.maxTemperature = maxTemperature;
|
||||||
|
|
||||||
|
series.push(forecast);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (const forecast of series) {
|
||||||
|
forecast.weatherType = this.convertWeatherType(forecast.symbol, forecast.time);
|
||||||
|
}
|
||||||
|
return series;
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchWeatherForecast() {
|
||||||
|
this.getWeatherForecast("daily")
|
||||||
|
.then((forecast) => {
|
||||||
|
this.setWeatherForecast(forecast);
|
||||||
|
this.updateAvailable();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
Log.error(error);
|
||||||
|
throw new Error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
/* global WeatherProvider */
|
/* global WeatherProvider, WeatherUtils */
|
||||||
|
|
||||||
/* MagicMirror²
|
/* MagicMirror²
|
||||||
* Module: Weather
|
* Module: Weather
|
||||||
|
@ -13,7 +13,6 @@ Module.register("weather", {
|
||||||
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)
|
||||||
units: config.units,
|
units: config.units,
|
||||||
useKmh: false,
|
|
||||||
tempUnits: config.units,
|
tempUnits: config.units,
|
||||||
windUnits: config.units,
|
windUnits: config.units,
|
||||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||||
|
@ -23,7 +22,6 @@ Module.register("weather", {
|
||||||
showPeriodUpper: false,
|
showPeriodUpper: false,
|
||||||
showWindDirection: true,
|
showWindDirection: true,
|
||||||
showWindDirectionAsArrow: false,
|
showWindDirectionAsArrow: false,
|
||||||
useBeaufort: true,
|
|
||||||
lang: config.language,
|
lang: config.language,
|
||||||
showHumidity: false,
|
showHumidity: false,
|
||||||
showSun: true,
|
showSun: true,
|
||||||
|
@ -60,7 +58,7 @@ 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", "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.
|
||||||
|
@ -77,6 +75,14 @@ Module.register("weather", {
|
||||||
start: function () {
|
start: function () {
|
||||||
moment.locale(this.config.lang);
|
moment.locale(this.config.lang);
|
||||||
|
|
||||||
|
if (this.config.useKmh) {
|
||||||
|
Log.warn("Your are using the deprecated config values 'useKmh'. Please switch to windUnits!");
|
||||||
|
this.windUnits = "kmh";
|
||||||
|
} else if (this.config.useBeaufort) {
|
||||||
|
Log.warn("Your are using the deprecated config values 'useBeaufort'. Please switch to windUnits!");
|
||||||
|
this.windUnits = "beaufort";
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the weather provider.
|
// Initialize the weather provider.
|
||||||
this.weatherProvider = WeatherProvider.initialize(this.config.weatherProvider, this);
|
this.weatherProvider = WeatherProvider.initialize(this.config.weatherProvider, this);
|
||||||
|
|
||||||
|
@ -221,9 +227,7 @@ Module.register("weather", {
|
||||||
"unit",
|
"unit",
|
||||||
function (value, type) {
|
function (value, type) {
|
||||||
if (type === "temperature") {
|
if (type === "temperature") {
|
||||||
if (this.config.tempUnits === "metric" || this.config.tempUnits === "imperial") {
|
value = this.roundValue(WeatherUtils.convertTemp(value, this.config.tempUnits)) + "°";
|
||||||
value += "°";
|
|
||||||
}
|
|
||||||
if (this.config.degreeLabel) {
|
if (this.config.degreeLabel) {
|
||||||
if (this.config.tempUnits === "metric") {
|
if (this.config.tempUnits === "metric") {
|
||||||
value += "C";
|
value += "C";
|
||||||
|
@ -245,8 +249,9 @@ Module.register("weather", {
|
||||||
}
|
}
|
||||||
} else if (type === "humidity") {
|
} else if (type === "humidity") {
|
||||||
value += "%";
|
value += "%";
|
||||||
|
} else if (type === "wind") {
|
||||||
|
value = WeatherUtils.convertWind(value, this.config.windUnits);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global SunCalc */
|
/* global SunCalc, WeatherUtils */
|
||||||
|
|
||||||
/* MagicMirror²
|
/* MagicMirror²
|
||||||
* Module: Weather
|
* Module: Weather
|
||||||
|
@ -14,17 +14,8 @@
|
||||||
class WeatherObject {
|
class WeatherObject {
|
||||||
/**
|
/**
|
||||||
* Constructor for a WeatherObject
|
* Constructor for a WeatherObject
|
||||||
*
|
|
||||||
* @param {string} units what units to use, "imperial" or "metric"
|
|
||||||
* @param {string} tempUnits what tempunits to use
|
|
||||||
* @param {string} windUnits what windunits to use
|
|
||||||
* @param {boolean} useKmh use kmh if true, mps if false
|
|
||||||
*/
|
*/
|
||||||
constructor(units, tempUnits, windUnits, useKmh) {
|
constructor() {
|
||||||
this.units = units;
|
|
||||||
this.tempUnits = tempUnits;
|
|
||||||
this.windUnits = windUnits;
|
|
||||||
this.useKmh = useKmh;
|
|
||||||
this.date = null;
|
this.date = null;
|
||||||
this.windSpeed = null;
|
this.windSpeed = null;
|
||||||
this.windDirection = null;
|
this.windDirection = null;
|
||||||
|
@ -78,21 +69,6 @@ class WeatherObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
beaufortWindSpeed() {
|
|
||||||
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : this.useKmh ? this.windSpeed : (this.windSpeed * 60 * 60) / 1000;
|
|
||||||
const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
|
|
||||||
for (const [index, speed] of speeds.entries()) {
|
|
||||||
if (speed > windInKmh) {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
kmhWindSpeed() {
|
|
||||||
return this.windUnits === "imperial" ? this.windSpeed * 1.609344 : (this.windSpeed * 60 * 60) / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextSunAction() {
|
nextSunAction() {
|
||||||
return moment().isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise";
|
return moment().isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise";
|
||||||
}
|
}
|
||||||
|
@ -101,8 +77,8 @@ class WeatherObject {
|
||||||
if (this.feelsLikeTemp) {
|
if (this.feelsLikeTemp) {
|
||||||
return this.feelsLikeTemp;
|
return this.feelsLikeTemp;
|
||||||
}
|
}
|
||||||
const windInMph = this.windUnits === "imperial" ? this.windSpeed : this.windSpeed * 2.23694;
|
const windInMph = WeatherUtils.convertWind(this.windSpeed, "imperial");
|
||||||
const tempInF = this.tempUnits === "imperial" ? this.temperature : (this.temperature * 9) / 5 + 32;
|
const tempInF = WeatherUtils.convertTemp(this.temperature, "imperial");
|
||||||
let feelsLike = tempInF;
|
let feelsLike = tempInF;
|
||||||
|
|
||||||
if (windInMph > 3 && tempInF < 50) {
|
if (windInMph > 3 && tempInF < 50) {
|
||||||
|
@ -120,7 +96,7 @@ class WeatherObject {
|
||||||
1.99 * Math.pow(10, -6) * tempInF * tempInF * this.humidity * this.humidity;
|
1.99 * Math.pow(10, -6) * tempInF * tempInF * this.humidity * this.humidity;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.tempUnits === "imperial" ? feelsLike : ((feelsLike - 32) * 5) / 9;
|
return ((feelsLike - 32) * 5) / 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -134,17 +110,17 @@ class WeatherObject {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the sunrise / sunset time depending on the location. This can be
|
* Update the sunrise / sunset time depending on the location. This can be
|
||||||
* used if your provider doesnt provide that data by itself. Then SunCalc
|
* used if your provider doesn't provide that data by itself. Then SunCalc
|
||||||
* is used here to calculate them according to the location.
|
* is used here to calculate them according to the location.
|
||||||
*
|
*
|
||||||
* @param {number} lat latitude
|
* @param {number} lat latitude
|
||||||
* @param {number} lon longitude
|
* @param {number} lon longitude
|
||||||
*/
|
*/
|
||||||
updateSunTime(lat, lon) {
|
updateSunTime(lat, lon) {
|
||||||
let now = !this.date ? new Date() : this.date.toDate();
|
const now = !this.date ? new Date() : this.date.toDate();
|
||||||
let times = SunCalc.getTimes(now, lat, lon);
|
const times = SunCalc.getTimes(now, lat, lon);
|
||||||
this.sunrise = moment(times.sunrise, "X");
|
this.sunrise = moment(times.sunrise);
|
||||||
this.sunset = moment(times.sunset, "X");
|
this.sunset = moment(times.sunset);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global Class */
|
/* global Class, performWebRequest */
|
||||||
|
|
||||||
/* MagicMirror²
|
/* MagicMirror²
|
||||||
* Module: Weather
|
* Module: Weather
|
||||||
|
@ -111,45 +111,23 @@ const WeatherProvider = Class.extend({
|
||||||
this.delegate.updateAvailable(this);
|
this.delegate.updateAvailable(this);
|
||||||
},
|
},
|
||||||
|
|
||||||
getCorsUrl: function () {
|
/**
|
||||||
if (this.config.mockData || typeof this.config.useCorsProxy === "undefined" || !this.config.useCorsProxy) {
|
* A convenience function to make requests.
|
||||||
return "";
|
*
|
||||||
} else {
|
* @param {string} url the url to fetch from
|
||||||
return location.protocol + "//" + location.host + "/cors?url=";
|
* @param {string} type what contenttype to expect in the response, can be "json" or "xml"
|
||||||
}
|
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
|
||||||
},
|
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
|
||||||
|
* @returns {Promise} resolved when the fetch is done
|
||||||
// A convenience function to make requests. It returns a promise.
|
*/
|
||||||
fetchData: function (url, method = "GET", type = "json") {
|
fetchData: async function (url, type = "json", requestHeaders = undefined, expectedResponseHeaders = undefined) {
|
||||||
url = this.getCorsUrl() + url;
|
const mockData = this.config.mockData;
|
||||||
const getData = function (mockData) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
if (mockData) {
|
if (mockData) {
|
||||||
let data = mockData;
|
const data = mockData.substring(1, mockData.length - 1);
|
||||||
data = data.substring(1, data.length - 1);
|
return JSON.parse(data);
|
||||||
resolve(JSON.parse(data));
|
|
||||||
} else {
|
|
||||||
const request = new XMLHttpRequest();
|
|
||||||
request.open(method, url, true);
|
|
||||||
request.onreadystatechange = function () {
|
|
||||||
if (this.readyState === 4) {
|
|
||||||
if (this.status === 200) {
|
|
||||||
if (type === "xml") {
|
|
||||||
resolve(this.responseXML);
|
|
||||||
} else {
|
|
||||||
resolve(JSON.parse(this.response));
|
|
||||||
}
|
}
|
||||||
} else {
|
const useCorsProxy = typeof this.config.useCorsProxy !== "undefined" && this.config.useCorsProxy;
|
||||||
reject(request);
|
return performWebRequest(url, type, useCorsProxy, requestHeaders, expectedResponseHeaders);
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
request.send();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return getData(this.config.mockData);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
98
modules/default/weather/weatherutils.js
Normal file
98
modules/default/weather/weatherutils.js
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
/* MagicMirror²
|
||||||
|
* Weather Util Methods
|
||||||
|
*
|
||||||
|
* By Rejas
|
||||||
|
* MIT Licensed.
|
||||||
|
*/
|
||||||
|
const WeatherUtils = {
|
||||||
|
/**
|
||||||
|
* Convert wind (from m/s) to beaufort scale
|
||||||
|
*
|
||||||
|
* @param {number} speedInMS the windspeed you want to convert
|
||||||
|
* @returns {number} the speed in beaufort
|
||||||
|
*/
|
||||||
|
beaufortWindSpeed(speedInMS) {
|
||||||
|
const windInKmh = (speedInMS * 3600) / 1000;
|
||||||
|
const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
|
||||||
|
for (const [index, speed] of speeds.entries()) {
|
||||||
|
if (speed > windInKmh) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 12;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert temp (from degrees C) into imperial or metric unit depending on
|
||||||
|
* your config
|
||||||
|
*
|
||||||
|
* @param {number} tempInC the temperature in celsius you want to convert
|
||||||
|
* @param {string} unit can be 'imperial' or 'metric'
|
||||||
|
* @returns {number} the converted temperature
|
||||||
|
*/
|
||||||
|
convertTemp(tempInC, unit) {
|
||||||
|
return unit === "imperial" ? tempInC * 1.8 + 32 : tempInC;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert wind speed into another unit.
|
||||||
|
*
|
||||||
|
* @param {number} windInMS the windspeed in meter/sec you want to convert
|
||||||
|
* @param {string} unit can be 'beaufort', 'kmh', 'knots, 'imperial' (mph)
|
||||||
|
* or 'metric' (mps)
|
||||||
|
* @returns {number} the converted windspeed
|
||||||
|
*/
|
||||||
|
convertWind(windInMS, unit) {
|
||||||
|
switch (unit) {
|
||||||
|
case "beaufort":
|
||||||
|
return this.beaufortWindSpeed(windInMS);
|
||||||
|
case "kmh":
|
||||||
|
return (windInMS * 3600) / 1000;
|
||||||
|
case "knots":
|
||||||
|
return windInMS * 1.943844;
|
||||||
|
case "imperial":
|
||||||
|
return windInMS * 2.2369362920544;
|
||||||
|
case "metric":
|
||||||
|
default:
|
||||||
|
return windInMS;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Convert the wind direction cardinal to value
|
||||||
|
*/
|
||||||
|
convertWindDirection(windDirection) {
|
||||||
|
const windCardinals = {
|
||||||
|
N: 0,
|
||||||
|
NNE: 22,
|
||||||
|
NE: 45,
|
||||||
|
ENE: 67,
|
||||||
|
E: 90,
|
||||||
|
ESE: 112,
|
||||||
|
SE: 135,
|
||||||
|
SSE: 157,
|
||||||
|
S: 180,
|
||||||
|
SSW: 202,
|
||||||
|
SW: 225,
|
||||||
|
WSW: 247,
|
||||||
|
W: 270,
|
||||||
|
WNW: 292,
|
||||||
|
NW: 315,
|
||||||
|
NNW: 337
|
||||||
|
};
|
||||||
|
|
||||||
|
return windCardinals.hasOwnProperty(windDirection) ? windCardinals[windDirection] : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
convertWindToMetric(mph) {
|
||||||
|
return mph / 2.2369362920544;
|
||||||
|
},
|
||||||
|
|
||||||
|
convertWindToMs(kmh) {
|
||||||
|
return kmh * 0.27777777777778;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof module !== "undefined") {
|
||||||
|
module.exports = WeatherUtils;
|
||||||
|
}
|
5357
package-lock.json
generated
5357
package-lock.json
generated
File diff suppressed because it is too large
Load diff
86
package.json
86
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "magicmirror",
|
"name": "magicmirror",
|
||||||
"version": "2.21.0",
|
"version": "2.22.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,7 +13,7 @@
|
||||||
"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 nyc --reporter=lcov --reporter=text jest -i --forceExit",
|
"test:coverage": "NODE_ENV=test jest --coverage -i --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 -i --forceExit",
|
||||||
|
@ -50,44 +50,43 @@
|
||||||
"homepage": "https://magicmirror.builders",
|
"homepage": "https://magicmirror.builders",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-jest": "^27.0.4",
|
"eslint-plugin-jest": "^27.1.7",
|
||||||
"eslint-plugin-jsdoc": "^39.3.6",
|
"eslint-plugin-jsdoc": "^39.6.4",
|
||||||
"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.1",
|
"husky": "^8.0.2",
|
||||||
"jest": "^29.0.3",
|
"jest": "^29.3.1",
|
||||||
"jsdom": "^20.0.0",
|
"jsdom": "^20.0.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"nyc": "^15.1.0",
|
"playwright": "^1.29.1",
|
||||||
"playwright": "^1.26.1",
|
"prettier": "^2.8.1",
|
||||||
"prettier": "^2.7.1",
|
|
||||||
"pretty-quick": "^3.1.3",
|
"pretty-quick": "^3.1.3",
|
||||||
"sinon": "^14.0.0",
|
"sinon": "^15.0.1",
|
||||||
"stylelint": "^14.12.1",
|
"stylelint": "^14.16.0",
|
||||||
"stylelint-config-prettier": "^9.0.3",
|
"stylelint-config-prettier": "^9.0.4",
|
||||||
"stylelint-config-standard": "^28.0.0",
|
"stylelint-config-standard": "^29.0.0",
|
||||||
"stylelint-prettier": "^2.0.0",
|
"stylelint-prettier": "^2.0.0",
|
||||||
"suncalc": "^1.9.0"
|
"suncalc": "^1.9.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"electron": "^19.1.0"
|
"electron": "^22.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"console-stamp": "^3.0.6",
|
"console-stamp": "^3.1.0",
|
||||||
"digest-fetch": "^1.3.0",
|
"digest-fetch": "^2.0.1",
|
||||||
"eslint": "^8.24.0",
|
"eslint": "^8.30.0",
|
||||||
"express": "^4.18.1",
|
"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.0",
|
"helmet": "^6.0.1",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"luxon": "^1.28.0",
|
"luxon": "^1.28.0",
|
||||||
"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.7",
|
||||||
"node-ical": "^0.15.1",
|
"node-ical": "^0.15.3",
|
||||||
"socket.io": "^4.5.2"
|
"socket.io": "^4.5.4"
|
||||||
},
|
},
|
||||||
"_moduleAliases": {
|
"_moduleAliases": {
|
||||||
"node_helper": "js/node_helper.js",
|
"node_helper": "js/node_helper.js",
|
||||||
|
@ -96,48 +95,5 @@
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"verbose": true,
|
|
||||||
"testTimeout": 20000,
|
|
||||||
"testSequencer": "<rootDir>/tests/configs/test_sequencer.js",
|
|
||||||
"projects": [
|
|
||||||
{
|
|
||||||
"displayName": "unit",
|
|
||||||
"moduleNameMapper": {
|
|
||||||
"logger": "<rootDir>/js/logger.js"
|
|
||||||
},
|
|
||||||
"testMatch": [
|
|
||||||
"**/tests/unit/**/*.[jt]s?(x)"
|
|
||||||
],
|
|
||||||
"testPathIgnorePatterns": [
|
|
||||||
"<rootDir>/tests/unit/mocks"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"displayName": "electron",
|
|
||||||
"testMatch": [
|
|
||||||
"**/tests/electron/**/*.[jt]s?(x)"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"displayName": "e2e",
|
|
||||||
"setupFilesAfterEnv": [
|
|
||||||
"<rootDir>/tests/e2e/mock-console.js"
|
|
||||||
],
|
|
||||||
"testMatch": [
|
|
||||||
"**/tests/e2e/**/*.[jt]s?(x)"
|
|
||||||
],
|
|
||||||
"modulePaths": [
|
|
||||||
"<rootDir>/js/"
|
|
||||||
],
|
|
||||||
"testPathIgnorePatterns": [
|
|
||||||
"<rootDir>/tests/e2e/modules/mocks",
|
|
||||||
"<rootDir>/tests/e2e/modules/basic-auth.js",
|
|
||||||
"<rootDir>/tests/e2e/global-setup.js",
|
|
||||||
"<rootDir>/tests/e2e/mock-console.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
{
|
|
||||||
"LOADING": "Loading …",
|
|
||||||
|
|
||||||
"TODAY": "Today",
|
|
||||||
"TOMORROW": "Tomorrow",
|
|
||||||
"DAYAFTERTOMORROW": "In 2 days",
|
|
||||||
"RUNNING": "Ends in",
|
|
||||||
"EMPTY": "No upcoming events.",
|
|
||||||
|
|
||||||
"WEEK": "Week {weekNumber}",
|
|
||||||
|
|
||||||
"N": "N",
|
|
||||||
"NNE": "NNE",
|
|
||||||
"NE": "NE",
|
|
||||||
"ENE": "ENE",
|
|
||||||
"E": "E",
|
|
||||||
"ESE": "ESE",
|
|
||||||
"SE": "SE",
|
|
||||||
"SSE": "SSE",
|
|
||||||
"S": "S",
|
|
||||||
"SSW": "SSW",
|
|
||||||
"SW": "SW",
|
|
||||||
"WSW": "WSW",
|
|
||||||
"W": "W",
|
|
||||||
"WNW": "WNW",
|
|
||||||
"NW": "NW",
|
|
||||||
"NNW": "NNW",
|
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "MagicMirror² update available.",
|
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Update available for MODULE_NAME module.",
|
|
||||||
"UPDATE_INFO_SINGLE": "The current installation is COMMIT_COUNT commit behind on the BRANCH_NAME branch.",
|
|
||||||
"UPDATE_INFO_MULTIPLE": "The current installation is COMMIT_COUNT commits behind on the BRANCH_NAME branch."
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@
|
||||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||||
* MIT Licensed.
|
* MIT Licensed.
|
||||||
*/
|
*/
|
||||||
exports.configFactory = function (options) {
|
exports.configFactory = (options) => {
|
||||||
return Object.assign(
|
return Object.assign(
|
||||||
{
|
{
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
|
|
|
@ -14,7 +14,7 @@ let config = {
|
||||||
calendars: [
|
calendars: [
|
||||||
{
|
{
|
||||||
maximumNumberOfDays: 10000,
|
maximumNumberOfDays: 10000,
|
||||||
url: "http://localhost:8080/tests/configs/data/calendar_test.ics",
|
url: "http://localhost:8080/tests/mocks/calendar_test.ics",
|
||||||
auth: {
|
auth: {
|
||||||
user: "MagicMirror",
|
user: "MagicMirror",
|
||||||
pass: "CallMeADog"
|
pass: "CallMeADog"
|
||||||
|
|
|
@ -14,7 +14,7 @@ let config = {
|
||||||
calendars: [
|
calendars: [
|
||||||
{
|
{
|
||||||
maximumNumberOfDays: 10000,
|
maximumNumberOfDays: 10000,
|
||||||
url: "http://localhost:8080/tests/configs/data/calendar_test.ics",
|
url: "http://localhost:8080/tests/mocks/calendar_test.ics",
|
||||||
auth: {
|
auth: {
|
||||||
user: "MagicMirror",
|
user: "MagicMirror",
|
||||||
pass: "CallMeADog",
|
pass: "CallMeADog",
|
||||||
|
|
|
@ -14,7 +14,7 @@ let config = {
|
||||||
calendars: [
|
calendars: [
|
||||||
{
|
{
|
||||||
maximumNumberOfDays: 10000,
|
maximumNumberOfDays: 10000,
|
||||||
url: "http://localhost:8010/tests/configs/data/calendar_test.ics",
|
url: "http://localhost:8010/tests/mocks/calendar_test.ics",
|
||||||
auth: {
|
auth: {
|
||||||
user: "MagicMirror",
|
user: "MagicMirror",
|
||||||
pass: "CallMeADog"
|
pass: "CallMeADog"
|
||||||
|
|
|
@ -18,7 +18,7 @@ let config = {
|
||||||
symbol: "birthday-cake",
|
symbol: "birthday-cake",
|
||||||
fullDaySymbol: "calendar-day",
|
fullDaySymbol: "calendar-day",
|
||||||
recurringSymbol: "undo",
|
recurringSymbol: "undo",
|
||||||
url: "http://localhost:8080/tests/configs/data/calendar_test_icons.ics"
|
url: "http://localhost:8080/tests/mocks/calendar_test_icons.ics"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ let config = {
|
||||||
calendars: [
|
calendars: [
|
||||||
{
|
{
|
||||||
maximumNumberOfDays: 10000,
|
maximumNumberOfDays: 10000,
|
||||||
url: "http://localhost:8080/tests/configs/data/calendar_test.ics"
|
url: "http://localhost:8080/tests/mocks/calendar_test.ics"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ let config = {
|
||||||
calendars: [
|
calendars: [
|
||||||
{
|
{
|
||||||
maximumNumberOfDays: 10000,
|
maximumNumberOfDays: 10000,
|
||||||
url: "http://localhost:8020/tests/configs/data/calendar_test.ics",
|
url: "http://localhost:8020/tests/mocks/calendar_test.ics",
|
||||||
auth: {
|
auth: {
|
||||||
user: "MagicMirror",
|
user: "MagicMirror",
|
||||||
pass: "StairwayToHeaven",
|
pass: "StairwayToHeaven",
|
||||||
|
|
|
@ -14,7 +14,7 @@ let config = {
|
||||||
calendars: [
|
calendars: [
|
||||||
{
|
{
|
||||||
maximumNumberOfDays: 10000,
|
maximumNumberOfDays: 10000,
|
||||||
url: "http://localhost:8080/tests/configs/data/calendar_test.ics",
|
url: "http://localhost:8080/tests/mocks/calendar_test.ics",
|
||||||
user: "MagicMirror",
|
user: "MagicMirror",
|
||||||
pass: "CallMeADog"
|
pass: "CallMeADog"
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ let config = {
|
||||||
{
|
{
|
||||||
maximumEntries: 6,
|
maximumEntries: 6,
|
||||||
maximumNumberOfDays: 3650,
|
maximumNumberOfDays: 3650,
|
||||||
url: "http://localhost:8080/tests/configs/data/calendar_test_recurring.ics"
|
url: "http://localhost:8080/tests/mocks/calendar_test_recurring.ics"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ let config = {
|
||||||
module: "compliments",
|
module: "compliments",
|
||||||
position: "middle_center",
|
position: "middle_center",
|
||||||
config: {
|
config: {
|
||||||
mockDate: "2020-01-01",
|
|
||||||
compliments: {
|
compliments: {
|
||||||
morning: [],
|
morning: [],
|
||||||
afternoon: [],
|
afternoon: [],
|
||||||
|
|
21
tests/configs/modules/compliments/compliments_remote.js
Normal file
21
tests/configs/modules/compliments/compliments_remote.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/* MagicMirror² Test config compliments with remote file
|
||||||
|
*
|
||||||
|
* By Rejas
|
||||||
|
* MIT Licensed.
|
||||||
|
*/
|
||||||
|
let config = {
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
module: "compliments",
|
||||||
|
position: "middle_center",
|
||||||
|
config: {
|
||||||
|
remoteFile: "http://localhost:8080/tests/mocks/compliments_test.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||||
|
if (typeof module !== "undefined") {
|
||||||
|
module.exports = config;
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ let config = {
|
||||||
feeds: [
|
feeds: [
|
||||||
{
|
{
|
||||||
title: "Rodrigo Ramirez Blog",
|
title: "Rodrigo Ramirez Blog",
|
||||||
url: "http://localhost:8080/tests/configs/data/feed_test_rodrigoramirez.xml"
|
url: "http://localhost:8080/tests/mocks/newsfeed_test.xml"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ let config = {
|
||||||
feeds: [
|
feeds: [
|
||||||
{
|
{
|
||||||
title: "Rodrigo Ramirez Blog",
|
title: "Rodrigo Ramirez Blog",
|
||||||
url: "http://localhost:8080/tests/configs/data/feed_test_rodrigoramirez.xml"
|
url: "http://localhost:8080/tests/mocks/newsfeed_test.xml"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
ignoreOldItems: true
|
ignoreOldItems: true
|
||||||
|
|
|
@ -13,7 +13,7 @@ let config = {
|
||||||
feeds: [
|
feeds: [
|
||||||
{
|
{
|
||||||
title: "Rodrigo Ramirez Blog",
|
title: "Rodrigo Ramirez Blog",
|
||||||
url: "http://localhost:8080/tests/configs/data/feed_test_rodrigoramirez.xml"
|
url: "http://localhost:8080/tests/mocks/newsfeed_test.xml"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
prohibitedWords: ["QPanel"],
|
prohibitedWords: ["QPanel"],
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
let config = {
|
let config = {
|
||||||
modules:
|
modules:
|
||||||
// Using exotic content. This is why don't accept go to JSON configuration file
|
// Using exotic content. This is why don't accept go to JSON configuration file
|
||||||
(function () {
|
(() => {
|
||||||
let positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
let positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||||
let modules = Array();
|
let modules = Array();
|
||||||
for (let idx in positions) {
|
for (let idx in positions) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ let config = {
|
||||||
config: {
|
config: {
|
||||||
location: "Munich",
|
location: "Munich",
|
||||||
mockData: '"#####WEATHERDATA#####"',
|
mockData: '"#####WEATHERDATA#####"',
|
||||||
useBeaufort: false,
|
windUnits: "beaufort",
|
||||||
showWindDirectionAsArrow: true,
|
showWindDirectionAsArrow: true,
|
||||||
showSun: false,
|
showSun: false,
|
||||||
showHumidity: true,
|
showHumidity: true,
|
||||||
|
|
|
@ -1,34 +1,27 @@
|
||||||
const fetch = require("fetch");
|
const helpers = require("./helpers/global-setup");
|
||||||
const helpers = require("./global-setup");
|
|
||||||
|
|
||||||
describe("App environment", () => {
|
describe("App environment", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/default.js");
|
await helpers.startApplication("tests/configs/default.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await helpers.stopApplication();
|
await helpers.stopApplication();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("get request from http://localhost:8080 should return 200", (done) => {
|
it("get request from http://localhost:8080 should return 200", async () => {
|
||||||
fetch("http://localhost:8080").then((res) => {
|
const res = await helpers.fetch("http://localhost:8080");
|
||||||
done();
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it("get request from http://localhost:8080/nothing should return 404", (done) => {
|
it("get request from http://localhost:8080/nothing should return 404", async () => {
|
||||||
fetch("http://localhost:8080/nothing").then((res) => {
|
const res = await helpers.fetch("http://localhost:8080/nothing");
|
||||||
done();
|
|
||||||
expect(res.status).toBe(404);
|
expect(res.status).toBe(404);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it("should show the title MagicMirror²", (done) => {
|
it("should show the title MagicMirror²", async () => {
|
||||||
helpers.waitForElement("title").then((elem) => {
|
const elem = await helpers.waitForElement("title");
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
expect(elem.textContent).toBe("MagicMirror²");
|
expect(elem.textContent).toBe("MagicMirror²");
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
const fetch = require("fetch");
|
const helpers = require("./helpers/global-setup");
|
||||||
const helpers = require("./global-setup");
|
|
||||||
|
|
||||||
describe("All font files from roboto.css should be downloadable", function () {
|
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");
|
||||||
|
@ -14,18 +13,16 @@ describe("All font files from roboto.css should be downloadable", function () {
|
||||||
match = regex.exec(fileContent);
|
match = regex.exec(fileContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeAll(function () {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/without_modules.js");
|
await helpers.startApplication("tests/configs/without_modules.js");
|
||||||
});
|
});
|
||||||
afterAll(async function () {
|
afterAll(async () => {
|
||||||
await helpers.stopApplication();
|
await helpers.stopApplication();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each(fontFiles)("should return 200 HTTP code for file '%s'", (fontFile, done) => {
|
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;
|
||||||
fetch(fontUrl).then((res) => {
|
const res = await helpers.fetch(fontUrl);
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -11,7 +11,7 @@ const basicAuth = auth({
|
||||||
app.use(basicAuth);
|
app.use(basicAuth);
|
||||||
|
|
||||||
// Set available directories
|
// Set available directories
|
||||||
const directories = ["/tests/configs"];
|
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) {
|
||||||
|
@ -20,10 +20,10 @@ for (let directory of directories) {
|
||||||
|
|
||||||
let server;
|
let server;
|
||||||
|
|
||||||
exports.listen = function () {
|
exports.listen = (...args) => {
|
||||||
server = app.listen.apply(app, arguments);
|
server = app.listen.apply(app, args);
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.close = function (callback) {
|
exports.close = async () => {
|
||||||
server.close(callback);
|
await server.close();
|
||||||
};
|
};
|
|
@ -1,8 +1,11 @@
|
||||||
const jsdom = require("jsdom");
|
const jsdom = require("jsdom");
|
||||||
|
const corefetch = require("fetch");
|
||||||
|
|
||||||
exports.startApplication = (configFilename, exec) => {
|
exports.startApplication = async (configFilename, exec) => {
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
this.stopApplication();
|
if (global.app) {
|
||||||
|
await this.stopApplication();
|
||||||
|
}
|
||||||
// Set config sample for use in test
|
// Set config sample for use in test
|
||||||
if (configFilename === "") {
|
if (configFilename === "") {
|
||||||
process.env.MM_CONFIG_FILE = "config/config.js";
|
process.env.MM_CONFIG_FILE = "config/config.js";
|
||||||
|
@ -11,25 +14,34 @@ exports.startApplication = (configFilename, exec) => {
|
||||||
}
|
}
|
||||||
if (exec) exec;
|
if (exec) exec;
|
||||||
global.app = require("app.js");
|
global.app = require("app.js");
|
||||||
global.app.start();
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
global.app.start(resolve);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.stopApplication = async () => {
|
exports.stopApplication = async () => {
|
||||||
if (global.app) {
|
if (global.app) {
|
||||||
global.app.stop();
|
return new Promise((resolve) => {
|
||||||
|
global.app.stop(resolve);
|
||||||
|
delete global.app;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
return Promise.resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getDocument = (callback) => {
|
exports.getDocument = () => {
|
||||||
|
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.onload = () => {
|
dom.window.onload = () => {
|
||||||
global.document = dom.window.document;
|
global.document = dom.window.document;
|
||||||
callback();
|
resolve();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.waitForElement = (selector, ignoreValue = "") => {
|
exports.waitForElement = (selector, ignoreValue = "") => {
|
||||||
|
@ -71,3 +83,17 @@ exports.waitForAllElements = (selector) => {
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.fetch = (url) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
corefetch(url).then((res) => {
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.testMatch = async (element, regex) => {
|
||||||
|
const elem = await this.waitForElement(element);
|
||||||
|
expect(elem).not.toBe(null);
|
||||||
|
expect(elem.textContent).toMatch(regex);
|
||||||
|
};
|
|
@ -3,13 +3,13 @@
|
||||||
*
|
*
|
||||||
* @param {string} err The error message.
|
* @param {string} err The error message.
|
||||||
*/
|
*/
|
||||||
function mockError(err) {
|
const mockError = (err) => {
|
||||||
if (err.includes("ECONNREFUSED") || err.includes("ECONNRESET") || err.includes("socket hang up") || err.includes("exports is not defined") || err.includes("write EPIPE")) {
|
if (err.includes("ECONNREFUSED") || err.includes("ECONNRESET") || err.includes("socket hang up") || err.includes("exports is not defined") || err.includes("write EPIPE")) {
|
||||||
jest.fn();
|
jest.fn();
|
||||||
} else {
|
} else {
|
||||||
console.dir(err);
|
console.dir(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
global.console = {
|
global.console = {
|
||||||
log: jest.fn(),
|
log: jest.fn(),
|
29
tests/e2e/helpers/weather-functions.js
Normal file
29
tests/e2e/helpers/weather-functions.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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) => {
|
||||||
|
const elem = await helpers.waitForElement(element);
|
||||||
|
expect(elem).not.toBe(null);
|
||||||
|
expect(
|
||||||
|
elem.textContent
|
||||||
|
.trim()
|
||||||
|
.replace(/(\r\n|\n|\r)/gm, "")
|
||||||
|
.replace(/[ ]+/g, " ")
|
||||||
|
).toBe(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.startApp = async (configFile, additionalMockData) => {
|
||||||
|
let mockWeather;
|
||||||
|
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.getDocument();
|
||||||
|
};
|
|
@ -1,36 +1,31 @@
|
||||||
const fetch = require("fetch");
|
const helpers = require("./helpers/global-setup");
|
||||||
const helpers = require("./global-setup");
|
|
||||||
|
|
||||||
describe("ipWhitelist directive configuration", function () {
|
describe("ipWhitelist directive configuration", () => {
|
||||||
describe("Set ipWhitelist without access", function () {
|
describe("Set ipWhitelist without access", () => {
|
||||||
beforeAll(function () {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/noIpWhiteList.js");
|
await helpers.startApplication("tests/configs/noIpWhiteList.js");
|
||||||
});
|
});
|
||||||
afterAll(async function () {
|
afterAll(async () => {
|
||||||
await helpers.stopApplication();
|
await helpers.stopApplication();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 403", function (done) {
|
it("should return 403", async () => {
|
||||||
fetch("http://localhost:8080").then((res) => {
|
const res = await helpers.fetch("http://localhost:8080");
|
||||||
expect(res.status).toBe(403);
|
expect(res.status).toBe(403);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Set ipWhitelist []", function () {
|
describe("Set ipWhitelist []", () => {
|
||||||
beforeAll(function () {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/empty_ipWhiteList.js");
|
await helpers.startApplication("tests/configs/empty_ipWhiteList.js");
|
||||||
});
|
});
|
||||||
afterAll(async function () {
|
afterAll(async () => {
|
||||||
await helpers.stopApplication();
|
await helpers.stopApplication();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 200", function (done) {
|
it("should return 200", async () => {
|
||||||
fetch("http://localhost:8080").then((res) => {
|
const res = await helpers.fetch("http://localhost:8080");
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
const helpers = require("../global-setup");
|
const helpers = require("../helpers/global-setup");
|
||||||
|
|
||||||
describe("Alert module", () => {
|
describe("Alert module", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/alert/default.js");
|
await helpers.startApplication("tests/configs/modules/alert/default.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await helpers.stopApplication();
|
await helpers.stopApplication();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the welcome message", (done) => {
|
it("should show the welcome message", async () => {
|
||||||
helpers.waitForElement(".ns-box .ns-box-inner .light.bright.small").then((elem) => {
|
const elem = await helpers.waitForElement(".ns-box .ns-box-inner .light.bright.small");
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
expect(elem.textContent).toContain("Welcome, start was successful!");
|
expect(elem.textContent).toContain("Welcome, start was successful!");
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,31 +1,26 @@
|
||||||
const helpers = require("../global-setup");
|
const helpers = require("../helpers/global-setup");
|
||||||
const serverBasicAuth = require("./basic-auth.js");
|
const serverBasicAuth = require("../helpers/basic-auth.js");
|
||||||
|
|
||||||
describe("Calendar module", () => {
|
describe("Calendar module", () => {
|
||||||
/**
|
/**
|
||||||
* @param {string} done test done
|
|
||||||
* @param {string} element css selector
|
* @param {string} element css selector
|
||||||
* @param {string} result expected number
|
* @param {string} result expected number
|
||||||
* @param {string} not reverse result
|
* @param {string} not reverse result
|
||||||
*/
|
*/
|
||||||
const testElementLength = (done, element, result, not) => {
|
const testElementLength = async (element, result, not) => {
|
||||||
helpers.waitForAllElements(element).then((elem) => {
|
const elem = await helpers.waitForAllElements(element);
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
if (not === "not") {
|
if (not === "not") {
|
||||||
expect(elem.length).not.toBe(result);
|
expect(elem.length).not.toBe(result);
|
||||||
} else {
|
} else {
|
||||||
expect(elem.length).toBe(result);
|
expect(elem.length).toBe(result);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const testTextContain = (done, element, text) => {
|
const testTextContain = async (element, text) => {
|
||||||
helpers.waitForElement(element, "undefinedLoading").then((elem) => {
|
const elem = await helpers.waitForElement(element, "undefinedLoading");
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
expect(elem.textContent).toContain(text);
|
expect(elem.textContent).toContain(text);
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -33,133 +28,133 @@ describe("Calendar module", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Default configuration", () => {
|
describe("Default configuration", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/calendar/default.js");
|
await helpers.startApplication("tests/configs/modules/calendar/default.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the default maximumEntries of 10", (done) => {
|
it("should show the default maximumEntries of 10", async () => {
|
||||||
testElementLength(done, ".calendar .event", 10);
|
await testElementLength(".calendar .event", 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the default calendar symbol in each event", (done) => {
|
it("should show the default calendar symbol in each event", async () => {
|
||||||
testElementLength(done, ".calendar .event .fa-calendar-alt", 0, "not");
|
await testElementLength(".calendar .event .fa-calendar-alt", 0, "not");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Custom configuration", () => {
|
describe("Custom configuration", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/calendar/custom.js");
|
await helpers.startApplication("tests/configs/modules/calendar/custom.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the custom maximumEntries of 4", (done) => {
|
it("should show the custom maximumEntries of 4", async () => {
|
||||||
testElementLength(done, ".calendar .event", 4);
|
await testElementLength(".calendar .event", 4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the custom calendar symbol in each event", (done) => {
|
it("should show the custom calendar symbol in each event", async () => {
|
||||||
testElementLength(done, ".calendar .event .fa-birthday-cake", 4);
|
await testElementLength(".calendar .event .fa-birthday-cake", 4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show two custom icons for repeating events", (done) => {
|
it("should show two custom icons for repeating events", async () => {
|
||||||
testElementLength(done, ".calendar .event .fa-undo", 2);
|
await testElementLength(".calendar .event .fa-undo", 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show two custom icons for day events", (done) => {
|
it("should show two custom icons for day events", async () => {
|
||||||
testElementLength(done, ".calendar .event .fa-calendar-day", 2);
|
await testElementLength(".calendar .event .fa-calendar-day", 2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Recurring event", () => {
|
describe("Recurring event", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/calendar/recurring.js");
|
await helpers.startApplication("tests/configs/modules/calendar/recurring.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the recurring birthday event 6 times", (done) => {
|
it("should show the recurring birthday event 6 times", async () => {
|
||||||
testElementLength(done, ".calendar .event", 6);
|
await testElementLength(".calendar .event", 6);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.setMaxListeners(0);
|
process.setMaxListeners(0);
|
||||||
for (let i = -12; i < 12; i++) {
|
for (let i = -12; i < 12; i++) {
|
||||||
describe("Recurring event per timezone", () => {
|
describe("Recurring event per timezone", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
Date.prototype.getTimezoneOffset = () => {
|
Date.prototype.getTimezoneOffset = () => {
|
||||||
return i * 60;
|
return i * 60;
|
||||||
};
|
};
|
||||||
helpers.startApplication("tests/configs/modules/calendar/recurring.js");
|
await helpers.startApplication("tests/configs/modules/calendar/recurring.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain text "Mar 25th" in timezone UTC ' + -i, (done) => {
|
it('should contain text "Mar 25th" in timezone UTC ' + -i, async () => {
|
||||||
testTextContain(done, ".calendar", "Mar 25th");
|
await testTextContain(".calendar", "Mar 25th");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Changed port", () => {
|
describe("Changed port", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/calendar/changed-port.js");
|
await helpers.startApplication("tests/configs/modules/calendar/changed-port.js");
|
||||||
serverBasicAuth.listen(8010);
|
serverBasicAuth.listen(8010);
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll((done) => {
|
afterAll(async () => {
|
||||||
serverBasicAuth.close(done());
|
await serverBasicAuth.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return TestEvents", (done) => {
|
it("should return TestEvents", async () => {
|
||||||
testElementLength(done, ".calendar .event", 0, "not");
|
await testElementLength(".calendar .event", 0, "not");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Basic auth", () => {
|
describe("Basic auth", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/calendar/basic-auth.js");
|
await helpers.startApplication("tests/configs/modules/calendar/basic-auth.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return TestEvents", (done) => {
|
it("should return TestEvents", async () => {
|
||||||
testElementLength(done, ".calendar .event", 0, "not");
|
await testElementLength(".calendar .event", 0, "not");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Basic auth by default", () => {
|
describe("Basic auth by default", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/calendar/auth-default.js");
|
await helpers.startApplication("tests/configs/modules/calendar/auth-default.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return TestEvents", (done) => {
|
it("should return TestEvents", async () => {
|
||||||
testElementLength(done, ".calendar .event", 0, "not");
|
await testElementLength(".calendar .event", 0, "not");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Basic auth backward compatibility configuration: DEPRECATED", () => {
|
describe("Basic auth backward compatibility configuration: DEPRECATED", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/calendar/old-basic-auth.js");
|
await helpers.startApplication("tests/configs/modules/calendar/old-basic-auth.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return TestEvents", (done) => {
|
it("should return TestEvents", async () => {
|
||||||
testElementLength(done, ".calendar .event", 0, "not");
|
await testElementLength(".calendar .event", 0, "not");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Fail Basic auth", () => {
|
describe("Fail Basic auth", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/calendar/fail-basic-auth.js");
|
await helpers.startApplication("tests/configs/modules/calendar/fail-basic-auth.js");
|
||||||
serverBasicAuth.listen(8020);
|
serverBasicAuth.listen(8020);
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll((done) => {
|
afterAll(async () => {
|
||||||
serverBasicAuth.close(done());
|
await serverBasicAuth.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show Unauthorized error", (done) => {
|
it("should show Unauthorized error", async () => {
|
||||||
testTextContain(done, ".calendar", "Error in the calendar module. Authorization failed");
|
await testTextContain(".calendar", "Error in the calendar module. Authorization failed");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,73 +1,65 @@
|
||||||
const helpers = require("../global-setup");
|
const helpers = require("../helpers/global-setup");
|
||||||
|
|
||||||
describe("Clock set to spanish language module", () => {
|
describe("Clock set to spanish language module", () => {
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await helpers.stopApplication();
|
await helpers.stopApplication();
|
||||||
});
|
});
|
||||||
|
|
||||||
const testMatch = (done, element, regex) => {
|
|
||||||
helpers.waitForElement(element).then((elem) => {
|
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
|
||||||
expect(elem.textContent).toMatch(regex);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("with default 24hr clock config", () => {
|
describe("with default 24hr clock config", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/clock/es/clock_24hr.js");
|
await helpers.startApplication("tests/configs/modules/clock/es/clock_24hr.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows date with correct format", (done) => {
|
it("shows date with correct format", async () => {
|
||||||
const dateRegex = /^(?:lunes|martes|miércoles|jueves|viernes|sábado|domingo), \d{1,2} de (?:enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre) de \d{4}$/;
|
const dateRegex = /^(?:lunes|martes|miércoles|jueves|viernes|sábado|domingo), \d{1,2} de (?:enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre) de \d{4}$/;
|
||||||
testMatch(done, ".clock .date", dateRegex);
|
await helpers.testMatch(".clock .date", dateRegex);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows time in 24hr format", (done) => {
|
it("shows time in 24hr format", async () => {
|
||||||
const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/;
|
const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/;
|
||||||
testMatch(done, ".clock .time", timeRegex);
|
await helpers.testMatch(".clock .time", timeRegex);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with default 12hr clock config", () => {
|
describe("with default 12hr clock config", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/clock/es/clock_12hr.js");
|
await helpers.startApplication("tests/configs/modules/clock/es/clock_12hr.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows date with correct format", (done) => {
|
it("shows date with correct format", async () => {
|
||||||
const dateRegex = /^(?:lunes|martes|miércoles|jueves|viernes|sábado|domingo), \d{1,2} de (?:enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre) de \d{4}$/;
|
const dateRegex = /^(?:lunes|martes|miércoles|jueves|viernes|sábado|domingo), \d{1,2} de (?:enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre) de \d{4}$/;
|
||||||
testMatch(done, ".clock .date", dateRegex);
|
await helpers.testMatch(".clock .date", dateRegex);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows time in 12hr format", (done) => {
|
it("shows time in 12hr format", async () => {
|
||||||
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/;
|
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/;
|
||||||
testMatch(done, ".clock .time", timeRegex);
|
await helpers.testMatch(".clock .time", timeRegex);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with showPeriodUpper config enabled", () => {
|
describe("with showPeriodUpper config enabled", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/clock/es/clock_showPeriodUpper.js");
|
await helpers.startApplication("tests/configs/modules/clock/es/clock_showPeriodUpper.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows 12hr time with upper case AM/PM", (done) => {
|
it("shows 12hr time with upper case AM/PM", async () => {
|
||||||
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/;
|
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/;
|
||||||
testMatch(done, ".clock .time", timeRegex);
|
await helpers.testMatch(".clock .time", timeRegex);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with showWeek config enabled", () => {
|
describe("with showWeek config enabled", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/clock/es/clock_showWeek.js");
|
await helpers.startApplication("tests/configs/modules/clock/es/clock_showWeek.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows week with correct format", (done) => {
|
it("shows week with correct format", async () => {
|
||||||
const weekRegex = /^Semana [0-9]{1,2}$/;
|
const weekRegex = /^Semana [0-9]{1,2}$/;
|
||||||
testMatch(done, ".clock .week", weekRegex);
|
await helpers.testMatch(".clock .week", weekRegex);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const helpers = require("../global-setup");
|
const helpers = require("../helpers/global-setup");
|
||||||
const moment = require("moment");
|
const moment = require("moment");
|
||||||
|
|
||||||
describe("Clock module", () => {
|
describe("Clock module", () => {
|
||||||
|
@ -6,118 +6,105 @@ describe("Clock module", () => {
|
||||||
await helpers.stopApplication();
|
await helpers.stopApplication();
|
||||||
});
|
});
|
||||||
|
|
||||||
const testMatch = (done, element, regex) => {
|
|
||||||
helpers.waitForElement(element).then((elem) => {
|
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
|
||||||
expect(elem.textContent).toMatch(regex);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("with default 24hr clock config", () => {
|
describe("with default 24hr clock config", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/clock/clock_24hr.js");
|
await helpers.startApplication("tests/configs/modules/clock/clock_24hr.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the date in the correct format", (done) => {
|
it("should show the date in the correct format", async () => {
|
||||||
const dateRegex = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$/;
|
const dateRegex = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$/;
|
||||||
testMatch(done, ".clock .date", dateRegex);
|
await helpers.testMatch(".clock .date", dateRegex);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the time in 24hr format", (done) => {
|
it("should show the time in 24hr format", async () => {
|
||||||
const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/;
|
const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/;
|
||||||
testMatch(done, ".clock .time", timeRegex);
|
await helpers.testMatch(".clock .time", timeRegex);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with default 12hr clock config", () => {
|
describe("with default 12hr clock config", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/clock/clock_12hr.js");
|
await helpers.startApplication("tests/configs/modules/clock/clock_12hr.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the date in the correct format", (done) => {
|
it("should show the date in the correct format", async () => {
|
||||||
const dateRegex = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$/;
|
const dateRegex = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$/;
|
||||||
testMatch(done, ".clock .date", dateRegex);
|
await helpers.testMatch(".clock .date", dateRegex);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the time in 12hr format", (done) => {
|
it("should show the time in 12hr format", async () => {
|
||||||
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/;
|
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/;
|
||||||
testMatch(done, ".clock .time", timeRegex);
|
await helpers.testMatch(".clock .time", timeRegex);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with showPeriodUpper config enabled", () => {
|
describe("with showPeriodUpper config enabled", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/clock/clock_showPeriodUpper.js");
|
await helpers.startApplication("tests/configs/modules/clock/clock_showPeriodUpper.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show 12hr time with upper case AM/PM", (done) => {
|
it("should show 12hr time with upper case AM/PM", async () => {
|
||||||
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/;
|
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/;
|
||||||
testMatch(done, ".clock .time", timeRegex);
|
await helpers.testMatch(".clock .time", timeRegex);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with displaySeconds config disabled", () => {
|
describe("with displaySeconds config disabled", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/clock/clock_displaySeconds_false.js");
|
await helpers.startApplication("tests/configs/modules/clock/clock_displaySeconds_false.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show 12hr time without seconds am/pm", (done) => {
|
it("should show 12hr time without seconds am/pm", async () => {
|
||||||
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[ap]m$/;
|
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[ap]m$/;
|
||||||
testMatch(done, ".clock .time", timeRegex);
|
await helpers.testMatch(".clock .time", timeRegex);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with showTime config disabled", () => {
|
describe("with showTime config disabled", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/clock/clock_showTime.js");
|
await helpers.startApplication("tests/configs/modules/clock/clock_showTime.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not show the time when digital clock is shown", (done) => {
|
it("should not show the time when digital clock is shown", async () => {
|
||||||
const elem = document.querySelector(".clock .digital .time");
|
const elem = await document.querySelector(".clock .digital .time");
|
||||||
done();
|
|
||||||
expect(elem).toBe(null);
|
expect(elem).toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with showWeek config enabled", () => {
|
describe("with showWeek config enabled", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/clock/clock_showWeek.js");
|
await helpers.startApplication("tests/configs/modules/clock/clock_showWeek.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the week in the correct format", (done) => {
|
it("should show the week in the correct format", async () => {
|
||||||
const weekRegex = /^Week [0-9]{1,2}$/;
|
const weekRegex = /^Week [0-9]{1,2}$/;
|
||||||
testMatch(done, ".clock .week", weekRegex);
|
await helpers.testMatch(".clock .week", weekRegex);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the week with the correct number of week of year", (done) => {
|
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;
|
||||||
helpers.waitForElement(".clock .week").then((elem) => {
|
const elem = await helpers.waitForElement(".clock .week");
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
expect(elem.textContent).toBe(weekToShow);
|
expect(elem.textContent).toBe(weekToShow);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe("with analog clock face enabled", () => {
|
describe("with analog clock face enabled", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/clock/clock_analog.js");
|
await helpers.startApplication("tests/configs/modules/clock/clock_analog.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the analog clock face", (done) => {
|
it("should show the analog clock face", async () => {
|
||||||
helpers.waitForElement(".clockCircle").then((elem) => {
|
const elem = helpers.waitForElement(".clockCircle");
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,98 +1,55 @@
|
||||||
const helpers = require("../global-setup");
|
const helpers = require("../helpers/global-setup");
|
||||||
|
|
||||||
/**
|
|
||||||
* move similar tests in function doTest
|
|
||||||
*
|
|
||||||
* @param {string} done test done
|
|
||||||
* @param {Array} complimentsArray The array of compliments.
|
|
||||||
*/
|
|
||||||
const doTest = (done, complimentsArray) => {
|
|
||||||
helpers.waitForElement(".compliments").then((elem) => {
|
|
||||||
expect(elem).not.toBe(null);
|
|
||||||
helpers.waitForElement(".module-content").then((elem) => {
|
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
|
||||||
expect(complimentsArray).toContain(elem.textContent);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("Compliments module", () => {
|
describe("Compliments module", () => {
|
||||||
|
/**
|
||||||
|
* move similar tests in function doTest
|
||||||
|
*
|
||||||
|
* @param {Array} complimentsArray The array of compliments.
|
||||||
|
*/
|
||||||
|
const doTest = async (complimentsArray) => {
|
||||||
|
let elem = await helpers.waitForElement(".compliments");
|
||||||
|
expect(elem).not.toBe(null);
|
||||||
|
elem = await helpers.waitForElement(".module-content");
|
||||||
|
expect(elem).not.toBe(null);
|
||||||
|
expect(complimentsArray).toContain(elem.textContent);
|
||||||
|
};
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await helpers.stopApplication();
|
await helpers.stopApplication();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("parts of days", () => {
|
|
||||||
beforeAll((done) => {
|
|
||||||
helpers.startApplication("tests/configs/modules/compliments/compliments_parts_day.js");
|
|
||||||
helpers.getDocument(done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("if Morning compliments for that part of day", (done) => {
|
|
||||||
const hour = new Date().getHours();
|
|
||||||
if (hour >= 3 && hour < 12) {
|
|
||||||
// if morning check
|
|
||||||
doTest(done, ["Hi", "Good Morning", "Morning test"]);
|
|
||||||
} else {
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("if Afternoon show Compliments for that part of day", (done) => {
|
|
||||||
const hour = new Date().getHours();
|
|
||||||
if (hour >= 12 && hour < 17) {
|
|
||||||
// if afternoon check
|
|
||||||
doTest(done, ["Hello", "Good Afternoon", "Afternoon test"]);
|
|
||||||
} else {
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("if Evening show Compliments for that part of day", (done) => {
|
|
||||||
const hour = new Date().getHours();
|
|
||||||
if (!(hour >= 3 && hour < 12) && !(hour >= 12 && hour < 17)) {
|
|
||||||
// if evening check
|
|
||||||
doTest(done, ["Hello There", "Good Evening", "Evening test"]);
|
|
||||||
} else {
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Feature anytime in compliments module", () => {
|
describe("Feature anytime in compliments module", () => {
|
||||||
describe("Set anytime and empty compliments for morning, evening and afternoon ", () => {
|
describe("Set anytime and empty compliments for morning, evening and afternoon ", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/compliments/compliments_anytime.js");
|
await helpers.startApplication("tests/configs/modules/compliments/compliments_anytime.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Show anytime because if configure empty parts of day compliments and set anytime compliments", (done) => {
|
it("Show anytime because if configure empty parts of day compliments and set anytime compliments", async () => {
|
||||||
doTest(done, ["Anytime here"]);
|
await doTest(["Anytime here"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Only anytime present in configuration compliments", () => {
|
describe("Only anytime present in configuration compliments", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/compliments/compliments_only_anytime.js");
|
await helpers.startApplication("tests/configs/modules/compliments/compliments_only_anytime.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Show anytime compliments", (done) => {
|
it("Show anytime compliments", async () => {
|
||||||
doTest(done, ["Anytime here"]);
|
await doTest(["Anytime here"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Feature date in compliments module", () => {
|
describe("remoteFile option", () => {
|
||||||
describe("Set date and empty compliments for anytime, morning, evening and afternoon", () => {
|
beforeAll(async () => {
|
||||||
beforeAll((done) => {
|
await helpers.startApplication("tests/configs/modules/compliments/compliments_remote.js");
|
||||||
helpers.startApplication("tests/configs/modules/compliments/compliments_date.js");
|
await helpers.getDocument();
|
||||||
helpers.getDocument(done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Show happy new year compliment on new years day", (done) => {
|
it("should show compliments from a remote file", async () => {
|
||||||
doTest(done, ["Happy new year!"]);
|
await doTest(["Remote compliment file works!"]);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const helpers = require("../global-setup");
|
const helpers = require("../helpers/global-setup");
|
||||||
|
|
||||||
describe("Test helloworld module", () => {
|
describe("Test helloworld module", () => {
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -6,32 +6,28 @@ describe("Test helloworld module", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("helloworld set config text", () => {
|
describe("helloworld set config text", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/helloworld/helloworld.js");
|
await helpers.startApplication("tests/configs/modules/helloworld/helloworld.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Test message helloworld module", (done) => {
|
it("Test message helloworld module", async () => {
|
||||||
helpers.waitForElement(".helloworld").then((elem) => {
|
const elem = await helpers.waitForElement(".helloworld");
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
expect(elem.textContent).toContain("Test HelloWorld Module");
|
expect(elem.textContent).toContain("Test HelloWorld Module");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe("helloworld default config text", () => {
|
describe("helloworld default config text", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/helloworld/helloworld_default.js");
|
await helpers.startApplication("tests/configs/modules/helloworld/helloworld_default.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Test message helloworld module", (done) => {
|
it("Test message helloworld module", async () => {
|
||||||
helpers.waitForElement(".helloworld").then((elem) => {
|
const elem = await helpers.waitForElement(".helloworld");
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
expect(elem.textContent).toContain("Hello World!");
|
expect(elem.textContent).toContain("Hello World!");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
const generateWeather = require("./weather_current");
|
|
||||||
const generateWeatherForecast = require("./weather_forecast");
|
|
||||||
|
|
||||||
module.exports = { generateWeather, generateWeatherForecast };
|
|
|
@ -1,64 +0,0 @@
|
||||||
const _ = require("lodash");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {object} extendedData extra data to add to the default mock data
|
|
||||||
* @returns {string} mocked current weather data
|
|
||||||
*/
|
|
||||||
function generateWeather(extendedData = {}) {
|
|
||||||
return JSON.stringify(
|
|
||||||
_.merge(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
coord: {
|
|
||||||
lon: 11.58,
|
|
||||||
lat: 48.14
|
|
||||||
},
|
|
||||||
weather: [
|
|
||||||
{
|
|
||||||
id: 615,
|
|
||||||
main: "Snow",
|
|
||||||
description: "light rain and snow",
|
|
||||||
icon: "13d"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 500,
|
|
||||||
main: "Rain",
|
|
||||||
description: "light rain",
|
|
||||||
icon: "10d"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
base: "stations",
|
|
||||||
main: {
|
|
||||||
temp: 1.49,
|
|
||||||
pressure: 1005,
|
|
||||||
humidity: 93.7,
|
|
||||||
temp_min: 1,
|
|
||||||
temp_max: 2
|
|
||||||
},
|
|
||||||
visibility: 7000,
|
|
||||||
wind: {
|
|
||||||
speed: 11.8,
|
|
||||||
deg: 250
|
|
||||||
},
|
|
||||||
clouds: {
|
|
||||||
all: 75
|
|
||||||
},
|
|
||||||
dt: 1547387400,
|
|
||||||
sys: {
|
|
||||||
type: 1,
|
|
||||||
id: 1267,
|
|
||||||
message: 0.0031,
|
|
||||||
country: "DE",
|
|
||||||
sunrise: 1547362817,
|
|
||||||
sunset: 1547394301
|
|
||||||
},
|
|
||||||
id: 2867714,
|
|
||||||
name: "Munich",
|
|
||||||
cod: 200
|
|
||||||
},
|
|
||||||
extendedData
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = generateWeather;
|
|
|
@ -1,4 +1,4 @@
|
||||||
const helpers = require("../global-setup");
|
const helpers = require("../helpers/global-setup");
|
||||||
|
|
||||||
describe("Newsfeed module", () => {
|
describe("Newsfeed module", () => {
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -6,86 +6,72 @@ describe("Newsfeed module", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Default configuration", () => {
|
describe("Default configuration", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/newsfeed/default.js");
|
await helpers.startApplication("tests/configs/modules/newsfeed/default.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the newsfeed title", (done) => {
|
it("should show the newsfeed title", async () => {
|
||||||
helpers.waitForElement(".newsfeed .newsfeed-source").then((elem) => {
|
const elem = await helpers.waitForElement(".newsfeed .newsfeed-source");
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
expect(elem.textContent).toContain("Rodrigo Ramirez Blog");
|
expect(elem.textContent).toContain("Rodrigo Ramirez Blog");
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it("should show the newsfeed article", (done) => {
|
it("should show the newsfeed article", async () => {
|
||||||
helpers.waitForElement(".newsfeed .newsfeed-title").then((elem) => {
|
const elem = await helpers.waitForElement(".newsfeed .newsfeed-title");
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
expect(elem.textContent).toContain("QPanel");
|
expect(elem.textContent).toContain("QPanel");
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it("should NOT show the newsfeed description", (done) => {
|
it("should NOT show the newsfeed description", async () => {
|
||||||
helpers.waitForElement(".newsfeed").then((elem) => {
|
await helpers.waitForElement(".newsfeed");
|
||||||
const element = document.querySelector(".newsfeed .newsfeed-desc");
|
const element = document.querySelector(".newsfeed .newsfeed-desc");
|
||||||
done();
|
|
||||||
expect(element).toBe(null);
|
expect(element).toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe("Custom configuration", () => {
|
describe("Custom configuration", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/newsfeed/prohibited_words.js");
|
await helpers.startApplication("tests/configs/modules/newsfeed/prohibited_words.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not show articles with prohibited words", (done) => {
|
it("should not show articles with prohibited words", async () => {
|
||||||
helpers.waitForElement(".newsfeed .newsfeed-title").then((elem) => {
|
const elem = await helpers.waitForElement(".newsfeed .newsfeed-title");
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
expect(elem.textContent).toContain("Problema VirtualBox");
|
expect(elem.textContent).toContain("Problema VirtualBox");
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it("should show the newsfeed description", (done) => {
|
it("should show the newsfeed description", async () => {
|
||||||
helpers.waitForElement(".newsfeed .newsfeed-desc").then((elem) => {
|
const elem = await helpers.waitForElement(".newsfeed .newsfeed-desc");
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
expect(elem.textContent.length).not.toBe(0);
|
expect(elem.textContent.length).not.toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe("Invalid configuration", () => {
|
describe("Invalid configuration", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/newsfeed/incorrect_url.js");
|
await helpers.startApplication("tests/configs/modules/newsfeed/incorrect_url.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show malformed url warning", (done) => {
|
it("should show malformed url warning", async () => {
|
||||||
helpers.waitForElement(".newsfeed .small", "No news at the moment.").then((elem) => {
|
const elem = await helpers.waitForElement(".newsfeed .small", "No news at the moment.");
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
expect(elem.textContent).toContain("Error in the Newsfeed module. Malformed url.");
|
expect(elem.textContent).toContain("Error in the Newsfeed module. Malformed url.");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe("Ignore items", () => {
|
describe("Ignore items", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/newsfeed/ignore_items.js");
|
await helpers.startApplication("tests/configs/modules/newsfeed/ignore_items.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show empty items info message", (done) => {
|
it("should show empty items info message", async () => {
|
||||||
helpers.waitForElement(".newsfeed .small").then((elem) => {
|
const elem = await helpers.waitForElement(".newsfeed .small");
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
expect(elem.textContent).toContain("No news at the moment.");
|
expect(elem.textContent).toContain("No news at the moment.");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
84
tests/e2e/modules/weather_current_spec.js
Normal file
84
tests/e2e/modules/weather_current_spec.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
const helpers = require("../helpers/global-setup");
|
||||||
|
const weatherFunc = require("../helpers/weather-functions");
|
||||||
|
|
||||||
|
describe("Weather module", () => {
|
||||||
|
afterAll(async () => {
|
||||||
|
await helpers.stopApplication();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Current weather", () => {
|
||||||
|
describe("Default configuration", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await weatherFunc.startApp("tests/configs/modules/weather/currentweather_default.js", {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render wind speed and wind direction", async () => {
|
||||||
|
await weatherFunc.getText(".weather .normal.medium span:nth-child(2)", "12 WSW");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render temperature with icon", async () => {
|
||||||
|
await weatherFunc.getText(".weather .large.light span.bright", "1.5°");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render feels like temperature", async () => {
|
||||||
|
await weatherFunc.getText(".weather .normal.medium.feelslike span.dimmed", "Feels like -5.6°");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Compliments Integration", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await weatherFunc.startApp("tests/configs/modules/weather/currentweather_compliments.js", {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render a compliment based on the current weather", async () => {
|
||||||
|
await weatherFunc.getText(".compliments .module-content span", "snow");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Configuration Options", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await weatherFunc.startApp("tests/configs/modules/weather/currentweather_options.js", {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render windUnits in beaufort", async () => {
|
||||||
|
await weatherFunc.getText(".weather .normal.medium span:nth-child(2)", "6");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render windDirection with an arrow", async () => {
|
||||||
|
const elem = await helpers.waitForElement(".weather .normal.medium sup i.fa-long-arrow-alt-up");
|
||||||
|
expect(elem).not.toBe(null);
|
||||||
|
expect(elem.outerHTML).toContain("transform:rotate(250deg);");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render humidity", async () => {
|
||||||
|
await weatherFunc.getText(".weather .normal.medium span:nth-child(3)", "93.7");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render degreeLabel for temp", async () => {
|
||||||
|
await weatherFunc.getText(".weather .large.light span.bright", "1°C");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render degreeLabel for feels like", async () => {
|
||||||
|
await weatherFunc.getText(".weather .normal.medium.feelslike span.dimmed", "Feels like -6°C");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Current weather with imperial units", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await weatherFunc.startApp("tests/configs/modules/weather/currentweather_units.js", {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render wind in imperial units", async () => {
|
||||||
|
await weatherFunc.getText(".weather .normal.medium span:nth-child(2)", "26 WSW");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render temperatures in fahrenheit", async () => {
|
||||||
|
await weatherFunc.getText(".weather .large.light span.bright", "34,7°");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render 'feels like' in fahrenheit", async () => {
|
||||||
|
await weatherFunc.getText(".weather .normal.medium.feelslike span.dimmed", "Feels like 21,9°");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
96
tests/e2e/modules/weather_forecast_spec.js
Normal file
96
tests/e2e/modules/weather_forecast_spec.js
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
const helpers = require("../helpers/global-setup");
|
||||||
|
const weatherFunc = require("../helpers/weather-functions");
|
||||||
|
|
||||||
|
describe("Weather module: Weather Forecast", () => {
|
||||||
|
afterAll(async () => {
|
||||||
|
await helpers.stopApplication();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Default configuration", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await weatherFunc.startApp("tests/configs/modules/weather/forecastweather_default.js", {});
|
||||||
|
});
|
||||||
|
|
||||||
|
const days = ["Today", "Tomorrow", "Sun", "Mon", "Tue"];
|
||||||
|
for (const [index, day] of days.entries()) {
|
||||||
|
it("should render day " + day, async () => {
|
||||||
|
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"];
|
||||||
|
for (const [index, icon] of icons.entries()) {
|
||||||
|
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}`);
|
||||||
|
expect(elem).not.toBe(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxTemps = ["24.4°", "21.0°", "22.9°", "23.4°", "20.6°"];
|
||||||
|
for (const [index, temp] of maxTemps.entries()) {
|
||||||
|
it("should render max temperature " + temp, async () => {
|
||||||
|
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°"];
|
||||||
|
for (const [index, temp] of minTemps.entries()) {
|
||||||
|
it("should render min temperature " + temp, async () => {
|
||||||
|
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];
|
||||||
|
for (const [index, opacity] of opacities.entries()) {
|
||||||
|
it("should render fading of rows with opacity=" + opacity, async () => {
|
||||||
|
const elem = await helpers.waitForElement(`.weather table.small tr:nth-child(${index + 1})`);
|
||||||
|
expect(elem).not.toBe(null);
|
||||||
|
expect(elem.outerHTML).toContain(`<tr style="opacity: ${opacity};">`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Absolute configuration", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await weatherFunc.startApp("tests/configs/modules/weather/forecastweather_absolute.js", {});
|
||||||
|
});
|
||||||
|
|
||||||
|
const days = ["Fri", "Sat", "Sun", "Mon", "Tue"];
|
||||||
|
for (const [index, day] of days.entries()) {
|
||||||
|
it("should render day " + day, async () => {
|
||||||
|
await weatherFunc.getText(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Configuration Options", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await weatherFunc.startApp("tests/configs/modules/weather/forecastweather_options.js", {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render custom table class", async () => {
|
||||||
|
const elem = await helpers.waitForElement(".weather table.myTableClass");
|
||||||
|
expect(elem).not.toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render colored rows", async () => {
|
||||||
|
const table = await helpers.waitForElement(".weather table.myTableClass");
|
||||||
|
expect(table).not.toBe(null);
|
||||||
|
expect(table.rows).not.toBe(null);
|
||||||
|
expect(table.rows.length).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Forecast weather units", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await weatherFunc.startApp("tests/configs/modules/weather/forecastweather_units.js", {});
|
||||||
|
});
|
||||||
|
|
||||||
|
const temperatures = ["75_9°", "69_8°", "73_2°", "74_1°", "69_1°"];
|
||||||
|
for (const [index, temp] of temperatures.entries()) {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,273 +0,0 @@
|
||||||
const moment = require("moment");
|
|
||||||
const helpers = require("../global-setup");
|
|
||||||
const path = require("path");
|
|
||||||
const fs = require("fs");
|
|
||||||
const { generateWeather, generateWeatherForecast } = require("./mocks");
|
|
||||||
|
|
||||||
describe("Weather module", () => {
|
|
||||||
/**
|
|
||||||
* @param {string} done test done
|
|
||||||
* @param {string} element css selector
|
|
||||||
* @param {string} result Expected text in given selector
|
|
||||||
*/
|
|
||||||
const getText = (done, element, result) => {
|
|
||||||
helpers.waitForElement(element).then((elem) => {
|
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
|
||||||
expect(
|
|
||||||
elem.textContent
|
|
||||||
.trim()
|
|
||||||
.replace(/(\r\n|\n|\r)/gm, "")
|
|
||||||
.replace(/[ ]+/g, " ")
|
|
||||||
).toBe(result);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} configFile path to configuration file
|
|
||||||
* @param {string} additionalMockData special data for mocking
|
|
||||||
* @param {string} callback callback
|
|
||||||
*/
|
|
||||||
const startApp = (configFile, additionalMockData, callback) => {
|
|
||||||
let mockWeather;
|
|
||||||
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);
|
|
||||||
helpers.startApplication("");
|
|
||||||
helpers.getDocument(callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await helpers.stopApplication();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Current weather", () => {
|
|
||||||
describe("Default configuration", () => {
|
|
||||||
beforeAll((done) => {
|
|
||||||
startApp("tests/configs/modules/weather/currentweather_default.js", {}, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render wind speed and wind direction", (done) => {
|
|
||||||
getText(done, ".weather .normal.medium span:nth-child(2)", "6 WSW"); // now "12"
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render temperature with icon", (done) => {
|
|
||||||
getText(done, ".weather .large.light span.bright", "1.5°"); // now "1°C"
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render feels like temperature", (done) => {
|
|
||||||
getText(done, ".weather .normal.medium.feelslike span.dimmed", "Feels like -5.6°"); // now "Feels like -6°C"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Default configuration with sunrise", () => {
|
|
||||||
beforeAll((done) => {
|
|
||||||
const sunrise = moment().startOf("day").unix();
|
|
||||||
const sunset = moment().startOf("day").unix();
|
|
||||||
startApp("tests/configs/modules/weather/currentweather_default.js", { sys: { sunrise, sunset } }, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render sunrise", (done) => {
|
|
||||||
getText(done, ".weather .normal.medium span:nth-child(4)", "12:00 am");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Default configuration with sunset", () => {
|
|
||||||
beforeAll((done) => {
|
|
||||||
const sunrise = moment().startOf("day").unix();
|
|
||||||
const sunset = moment().endOf("day").unix();
|
|
||||||
startApp("tests/configs/modules/weather/currentweather_default.js", { sys: { sunrise, sunset } }, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render sunset", (done) => {
|
|
||||||
getText(done, ".weather .normal.medium span:nth-child(4)", "11:59 pm");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Compliments Integration", () => {
|
|
||||||
beforeAll((done) => {
|
|
||||||
startApp("tests/configs/modules/weather/currentweather_compliments.js", {}, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render a compliment based on the current weather", (done) => {
|
|
||||||
getText(done, ".compliments .module-content span", "snow");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Configuration Options", () => {
|
|
||||||
beforeAll((done) => {
|
|
||||||
startApp("tests/configs/modules/weather/currentweather_options.js", {}, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render useBeaufort = false", (done) => {
|
|
||||||
getText(done, ".weather .normal.medium span:nth-child(2)", "12");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render showWindDirectionAsArrow = true", (done) => {
|
|
||||||
helpers.waitForElement(".weather .normal.medium sup i.fa-long-arrow-alt-up").then((elem) => {
|
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
|
||||||
expect(elem.outerHTML).toContain("transform:rotate(250deg);");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render showHumidity = true", (done) => {
|
|
||||||
getText(done, ".weather .normal.medium span:nth-child(3)", "93.7");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render degreeLabel = true for temp", (done) => {
|
|
||||||
getText(done, ".weather .large.light span.bright", "1°C");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render degreeLabel = true for feels like", (done) => {
|
|
||||||
getText(done, ".weather .normal.medium.feelslike span.dimmed", "Feels like -6°C");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Current weather units", () => {
|
|
||||||
beforeAll((done) => {
|
|
||||||
startApp(
|
|
||||||
"tests/configs/modules/weather/currentweather_units.js",
|
|
||||||
{
|
|
||||||
main: {
|
|
||||||
temp: (1.49 * 9) / 5 + 32,
|
|
||||||
temp_min: (1 * 9) / 5 + 32,
|
|
||||||
temp_max: (2 * 9) / 5 + 32
|
|
||||||
},
|
|
||||||
wind: {
|
|
||||||
speed: 11.8 * 2.23694
|
|
||||||
}
|
|
||||||
},
|
|
||||||
done
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render imperial units for wind", (done) => {
|
|
||||||
getText(done, ".weather .normal.medium span:nth-child(2)", "6 WSW");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render imperial units for temp", (done) => {
|
|
||||||
getText(done, ".weather .large.light span.bright", "34,7°");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render imperial units for feels like", (done) => {
|
|
||||||
getText(done, ".weather .normal.medium.feelslike span.dimmed", "Feels like 22,0°");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render custom decimalSymbol = ',' for humidity", (done) => {
|
|
||||||
getText(done, ".weather .normal.medium span:nth-child(3)", "93,7");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render custom decimalSymbol = ',' for temp", (done) => {
|
|
||||||
getText(done, ".weather .large.light span.bright", "34,7°");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render custom decimalSymbol = ',' for feels like", (done) => {
|
|
||||||
getText(done, ".weather .normal.medium.feelslike span.dimmed", "Feels like 22,0°");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Weather Forecast", () => {
|
|
||||||
describe("Default configuration", () => {
|
|
||||||
beforeAll((done) => {
|
|
||||||
startApp("tests/configs/modules/weather/forecastweather_default.js", {}, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
const days = ["Today", "Tomorrow", "Sun", "Mon", "Tue"];
|
|
||||||
for (const [index, day] of days.entries()) {
|
|
||||||
it("should render day " + day, (done) => {
|
|
||||||
getText(done, `.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const icons = ["day-cloudy", "rain", "day-sunny", "day-sunny", "day-sunny"];
|
|
||||||
for (const [index, icon] of icons.entries()) {
|
|
||||||
it("should render icon " + icon, (done) => {
|
|
||||||
helpers.waitForElement(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(2) span.wi-${icon}`).then((elem) => {
|
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxTemps = ["24.4°", "21.0°", "22.9°", "23.4°", "20.6°"];
|
|
||||||
for (const [index, temp] of maxTemps.entries()) {
|
|
||||||
it("should render max temperature " + temp, (done) => {
|
|
||||||
getText(done, `.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°"];
|
|
||||||
for (const [index, temp] of minTemps.entries()) {
|
|
||||||
it("should render min temperature " + temp, (done) => {
|
|
||||||
getText(done, `.weather table.small tr:nth-child(${index + 1}) td:nth-child(4)`, temp);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const opacities = [1, 1, 0.8, 0.5333333333333333, 0.2666666666666667];
|
|
||||||
for (const [index, opacity] of opacities.entries()) {
|
|
||||||
it("should render fading of rows with opacity=" + opacity, (done) => {
|
|
||||||
helpers.waitForElement(`.weather table.small tr:nth-child(${index + 1})`).then((elem) => {
|
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
|
||||||
expect(elem.outerHTML).toContain(`<tr style="opacity: ${opacity};">`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Absolute configuration", () => {
|
|
||||||
beforeAll((done) => {
|
|
||||||
startApp("tests/configs/modules/weather/forecastweather_absolute.js", {}, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
const days = ["Fri", "Sat", "Sun", "Mon", "Tue"];
|
|
||||||
for (const [index, day] of days.entries()) {
|
|
||||||
it("should render day " + day, (done) => {
|
|
||||||
getText(done, `.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Configuration Options", () => {
|
|
||||||
beforeAll((done) => {
|
|
||||||
startApp("tests/configs/modules/weather/forecastweather_options.js", {}, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render custom table class", (done) => {
|
|
||||||
helpers.waitForElement(".weather table.myTableClass").then((elem) => {
|
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render colored rows", (done) => {
|
|
||||||
helpers.waitForElement(".weather table.myTableClass").then((table) => {
|
|
||||||
done();
|
|
||||||
expect(table).not.toBe(null);
|
|
||||||
expect(table.rows).not.toBe(null);
|
|
||||||
expect(table.rows.length).toBe(5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Forecast weather units", () => {
|
|
||||||
beforeAll((done) => {
|
|
||||||
startApp("tests/configs/modules/weather/forecastweather_units.js", {}, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
const temperatures = ["24_4°", "21_0°", "22_9°", "23_4°", "20_6°"];
|
|
||||||
for (const [index, temp] of temperatures.entries()) {
|
|
||||||
it("should render custom decimalSymbol = '_' for temp " + temp, (done) => {
|
|
||||||
getText(done, `.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,28 +1,24 @@
|
||||||
const helpers = require("./global-setup");
|
const helpers = require("./helpers/global-setup");
|
||||||
|
|
||||||
describe("Display of modules", () => {
|
describe("Display of modules", () => {
|
||||||
beforeAll(function (done) {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/display.js");
|
await helpers.startApplication("tests/configs/modules/display.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await helpers.stopApplication();
|
await helpers.stopApplication();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show the test header", (done) => {
|
it("should show the test header", async () => {
|
||||||
helpers.waitForElement("#module_0_helloworld .module-header").then((elem) => {
|
const elem = await helpers.waitForElement("#module_0_helloworld .module-header");
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
// textContent gibt hier lowercase zurück, das uppercase wird durch css realisiert, was daher nicht in textContent landet
|
// textContent gibt hier lowercase zurück, das uppercase wird durch css realisiert, was daher nicht in textContent landet
|
||||||
expect(elem.textContent).toBe("test_header");
|
expect(elem.textContent).toBe("test_header");
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it("should show no header if no header text is specified", (done) => {
|
it("should show no header if no header text is specified", async () => {
|
||||||
helpers.waitForElement("#module_1_helloworld .module-header").then((elem) => {
|
const elem = await helpers.waitForElement("#module_1_helloworld .module-header");
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
expect(elem.textContent).toBe("undefined");
|
expect(elem.textContent).toBe("undefined");
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
23
tests/e2e/modules_empty_spec.js
Normal file
23
tests/e2e/modules_empty_spec.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
const helpers = require("./helpers/global-setup");
|
||||||
|
|
||||||
|
describe("Check configuration without modules", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await helpers.startApplication("tests/configs/without_modules.js");
|
||||||
|
await helpers.getDocument();
|
||||||
|
});
|
||||||
|
afterAll(async () => {
|
||||||
|
await helpers.stopApplication();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Show the message MagicMirror² title", async () => {
|
||||||
|
const elem = await helpers.waitForElement("#module_1_helloworld .module-content");
|
||||||
|
expect(elem).not.toBe(null);
|
||||||
|
expect(elem.textContent).toContain("MagicMirror²");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Show the url of michael's website", async () => {
|
||||||
|
const elem = await helpers.waitForElement("#module_5_helloworld .module-content");
|
||||||
|
expect(elem).not.toBe(null);
|
||||||
|
expect(elem.textContent).toContain("www.michaelteeuw.nl");
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,9 +1,9 @@
|
||||||
const helpers = require("./global-setup");
|
const helpers = require("./helpers/global-setup");
|
||||||
|
|
||||||
describe("Position of modules", () => {
|
describe("Position of modules", () => {
|
||||||
beforeAll((done) => {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/modules/positions.js");
|
await helpers.startApplication("tests/configs/modules/positions.js");
|
||||||
helpers.getDocument(done);
|
await helpers.getDocument();
|
||||||
});
|
});
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await helpers.stopApplication();
|
await helpers.stopApplication();
|
||||||
|
@ -13,12 +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, (done) => {
|
it("should show text in " + position, async () => {
|
||||||
helpers.waitForElement("." + className).then((elem) => {
|
const elem = await helpers.waitForElement("." + className);
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
expect(elem).not.toBe(null);
|
||||||
expect(elem.textContent).toContain("Text in " + position);
|
expect(elem.textContent).toContain("Text in " + position);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
const fetch = require("fetch");
|
|
||||||
const helpers = require("./global-setup");
|
|
||||||
|
|
||||||
describe("port directive configuration", function () {
|
|
||||||
describe("Set port 8090", function () {
|
|
||||||
beforeAll(function () {
|
|
||||||
helpers.startApplication("tests/configs/port_8090.js");
|
|
||||||
});
|
|
||||||
afterAll(async function () {
|
|
||||||
await helpers.stopApplication();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return 200", function (done) {
|
|
||||||
fetch("http://localhost:8090").then((res) => {
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Set port 8100 on environment variable MM_PORT", function () {
|
|
||||||
beforeAll(function () {
|
|
||||||
helpers.startApplication("tests/configs/port_8090.js", (process.env.MM_PORT = 8100));
|
|
||||||
});
|
|
||||||
afterAll(async function () {
|
|
||||||
await helpers.stopApplication();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return 200", function (done) {
|
|
||||||
fetch("http://localhost:8100").then((res) => {
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
31
tests/e2e/port_spec.js
Normal file
31
tests/e2e/port_spec.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
const helpers = require("./helpers/global-setup");
|
||||||
|
|
||||||
|
describe("port directive configuration", () => {
|
||||||
|
describe("Set port 8090", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await helpers.startApplication("tests/configs/port_8090.js");
|
||||||
|
});
|
||||||
|
afterAll(async () => {
|
||||||
|
await helpers.stopApplication();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 200", async () => {
|
||||||
|
const res = await helpers.fetch("http://localhost:8090");
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Set port 8100 on environment variable MM_PORT", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await helpers.startApplication("tests/configs/port_8090.js", (process.env.MM_PORT = 8100));
|
||||||
|
});
|
||||||
|
afterAll(async () => {
|
||||||
|
await helpers.stopApplication();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 200", async () => {
|
||||||
|
const res = await helpers.fetch("http://localhost:8100");
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,13 +6,13 @@ const { JSDOM } = require("jsdom");
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const sinon = require("sinon");
|
const sinon = require("sinon");
|
||||||
|
|
||||||
describe("Translations", function () {
|
describe("Translations", () => {
|
||||||
let server;
|
let server;
|
||||||
|
|
||||||
beforeAll(function () {
|
beforeAll(() => {
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(helmet());
|
app.use(helmet());
|
||||||
app.use(function (req, res, next) {
|
app.use((req, res, next) => {
|
||||||
res.header("Access-Control-Allow-Origin", "*");
|
res.header("Access-Control-Allow-Origin", "*");
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
@ -21,11 +21,11 @@ describe("Translations", function () {
|
||||||
server = app.listen(3000);
|
server = app.listen(3000);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(function () {
|
afterAll(() => {
|
||||||
server.close();
|
server.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have a translation file in the specified path", function () {
|
it("should have a translation file in the specified path", () => {
|
||||||
for (let language in translations) {
|
for (let language in translations) {
|
||||||
const file = fs.statSync(translations[language]);
|
const file = fs.statSync(translations[language]);
|
||||||
expect(file.isFile()).toBe(true);
|
expect(file.isFile()).toBe(true);
|
||||||
|
@ -37,7 +37,7 @@ describe("Translations", function () {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dom = new JSDOM(
|
dom = new JSDOM(
|
||||||
`<script>var Translator = {}; var Log = {log: function(){}}; var config = {language: 'de'};</script>\
|
`<script>var Translator = {}; var Log = {log: () => {}}; var config = {language: 'de'};</script>\
|
||||||
<script src="file://${path.join(__dirname, "..", "..", "js", "class.js")}"></script>\
|
<script src="file://${path.join(__dirname, "..", "..", "js", "class.js")}"></script>\
|
||||||
<script src="file://${path.join(__dirname, "..", "..", "js", "module.js")}"></script>`,
|
<script src="file://${path.join(__dirname, "..", "..", "js", "module.js")}"></script>`,
|
||||||
{ runScripts: "dangerously", resources: "usable" }
|
{ runScripts: "dangerously", resources: "usable" }
|
||||||
|
@ -45,7 +45,7 @@ describe("Translations", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should load translation file", (done) => {
|
it("should load translation file", (done) => {
|
||||||
dom.window.onload = async function () {
|
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, callback) => callback());
|
||||||
|
@ -65,7 +65,7 @@ describe("Translations", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should load translation + fallback file", (done) => {
|
it("should load translation + fallback file", (done) => {
|
||||||
dom.window.onload = async function () {
|
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, callback) => callback());
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ describe("Translations", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should load translation fallback file", (done) => {
|
it("should load translation fallback file", (done) => {
|
||||||
dom.window.onload = async function () {
|
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, callback) => callback());
|
||||||
|
@ -105,7 +105,7 @@ describe("Translations", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should load no file", (done) => {
|
it("should load no file", (done) => {
|
||||||
dom.window.onload = async function () {
|
dom.window.onload = async () => {
|
||||||
const { Translator, Module } = dom.window;
|
const { Translator, Module } = dom.window;
|
||||||
Translator.load = sinon.stub();
|
Translator.load = sinon.stub();
|
||||||
|
|
||||||
|
@ -130,18 +130,18 @@ describe("Translations", function () {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("Parsing language files through the Translator class", function () {
|
describe("Parsing language files through the Translator class", () => {
|
||||||
for (let language in translations) {
|
for (let language in translations) {
|
||||||
it(`should parse ${language}`, function (done) {
|
it(`should parse ${language}`, (done) => {
|
||||||
const dom = new JSDOM(
|
const dom = new JSDOM(
|
||||||
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: () => {}};</script>\
|
||||||
<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 = function () {
|
dom.window.onload = () => {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
|
|
||||||
Translator.load(mmm, translations[language], false, function () {
|
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();
|
||||||
|
@ -151,27 +151,27 @@ describe("Translations", function () {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Same keys", function () {
|
describe("Same keys", () => {
|
||||||
let base;
|
let base;
|
||||||
let missing = [];
|
let missing = [];
|
||||||
|
|
||||||
beforeAll(function (done) {
|
beforeAll((done) => {
|
||||||
const dom = new JSDOM(
|
const dom = new JSDOM(
|
||||||
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: () => {}};</script>\
|
||||||
<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 = function () {
|
dom.window.onload = () => {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
|
|
||||||
Translator.load(mmm, translations.de, false, function () {
|
Translator.load(mmm, translations.de, false, () => {
|
||||||
base = Object.keys(Translator.translations[mmm.name]).sort();
|
base = Object.keys(Translator.translations[mmm.name]).sort();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(function () {
|
afterAll(() => {
|
||||||
console.log(missing);
|
console.log(missing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -182,32 +182,32 @@ describe("Translations", function () {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe(`Translation keys of ${language}`, function () {
|
describe(`Translation keys of ${language}`, () => {
|
||||||
let keys;
|
let keys;
|
||||||
|
|
||||||
beforeAll(function (done) {
|
beforeAll((done) => {
|
||||||
const dom = new JSDOM(
|
const dom = new JSDOM(
|
||||||
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: () => {}};</script>\
|
||||||
<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 = function () {
|
dom.window.onload = () => {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
|
|
||||||
Translator.load(mmm, translations[language], false, function () {
|
Translator.load(mmm, translations[language], false, () => {
|
||||||
keys = Object.keys(Translator.translations[mmm.name]).sort();
|
keys = Object.keys(Translator.translations[mmm.name]).sort();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`${language} keys should be in base`, function () {
|
it(`${language} keys should be in base`, () => {
|
||||||
keys.forEach(function (key) {
|
keys.forEach((key) => {
|
||||||
expect(base.indexOf(key)).toBeGreaterThanOrEqual(0);
|
expect(base.indexOf(key)).toBeGreaterThanOrEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`${language} should contain all base keys`, function () {
|
it(`${language} should contain all base keys`, () => {
|
||||||
// TODO: when all translations are fixed, use
|
// TODO: when all translations are fixed, use
|
||||||
// expect(keys).toEqual(base);
|
// expect(keys).toEqual(base);
|
||||||
// instead of the try-catch-block
|
// instead of the try-catch-block
|
||||||
|
|
|
@ -1,34 +1,29 @@
|
||||||
const fetch = require("fetch");
|
const helpers = require("./helpers/global-setup");
|
||||||
const helpers = require("./global-setup");
|
|
||||||
|
|
||||||
describe("Vendors", function () {
|
describe("Vendors", () => {
|
||||||
beforeAll(function () {
|
beforeAll(async () => {
|
||||||
helpers.startApplication("tests/configs/default.js");
|
await helpers.startApplication("tests/configs/default.js");
|
||||||
});
|
});
|
||||||
afterAll(async function () {
|
afterAll(async () => {
|
||||||
await helpers.stopApplication();
|
await helpers.stopApplication();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Get list vendors", function () {
|
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}"`, function (done) {
|
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];
|
||||||
fetch(urlVendor).then((res) => {
|
const res = await helpers.fetch(urlVendor);
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(vendors).forEach((vendor) => {
|
Object.keys(vendors).forEach((vendor) => {
|
||||||
it(`should return 404 HTTP code for vendor https://localhost/"${vendor}"`, function (done) {
|
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];
|
||||||
fetch(urlVendor).then((res) => {
|
const res = await helpers.fetch(urlVendor);
|
||||||
expect(res.status).toBe(404);
|
expect(res.status).toBe(404);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
const helpers = require("./global-setup");
|
|
||||||
|
|
||||||
describe("Check configuration without modules", () => {
|
|
||||||
beforeAll((done) => {
|
|
||||||
helpers.startApplication("tests/configs/without_modules.js");
|
|
||||||
helpers.getDocument(done);
|
|
||||||
});
|
|
||||||
afterAll(async () => {
|
|
||||||
await helpers.stopApplication();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Show the message MagicMirror² title", (done) => {
|
|
||||||
helpers.waitForElement("#module_1_helloworld .module-content").then((elem) => {
|
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
|
||||||
expect(elem.textContent).toContain("MagicMirror²");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Show the text Michael's website", (done) => {
|
|
||||||
helpers.waitForElement("#module_5_helloworld .module-content").then((elem) => {
|
|
||||||
done();
|
|
||||||
expect(elem).not.toBe(null);
|
|
||||||
expect(elem.textContent).toContain("www.michaelteeuw.nl");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,42 +1,34 @@
|
||||||
// see https://playwright.dev/docs/api/class-electronapplication
|
const helpers = require("./helpers/global-setup");
|
||||||
|
const events = require("events");
|
||||||
|
|
||||||
const { _electron: electron } = require("playwright");
|
describe("Electron app environment", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
let electronApp = null;
|
await helpers.startApplication("tests/configs/modules/display.js");
|
||||||
process.env.MM_CONFIG_FILE = "tests/configs/modules/display.js";
|
|
||||||
jest.retryTimes(3);
|
|
||||||
|
|
||||||
describe("Electron app environment", function () {
|
|
||||||
beforeEach(async function () {
|
|
||||||
electronApp = await electron.launch({ args: ["js/electron.js"] });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function () {
|
afterEach(async () => {
|
||||||
await electronApp.close();
|
await helpers.stopApplication();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should open browserwindow", async function () {
|
it("should open browserwindow", async () => {
|
||||||
expect(await electronApp.windows().length).toBe(1);
|
const module = await helpers.getElement("#module_0_helloworld");
|
||||||
const page = await electronApp.firstWindow();
|
|
||||||
expect(await page.title()).toBe("MagicMirror²");
|
|
||||||
expect(await page.isVisible("body")).toBe(true);
|
|
||||||
const module = page.locator("#module_0_helloworld");
|
|
||||||
await module.waitFor();
|
|
||||||
expect(await module.textContent()).toContain("Test Display Header");
|
expect(await module.textContent()).toContain("Test Display Header");
|
||||||
|
expect(await global.electronApp.windows().length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Development console tests", function () {
|
describe("Development console tests", () => {
|
||||||
beforeEach(async function () {
|
beforeEach(async () => {
|
||||||
electronApp = await electron.launch({ args: ["js/electron.js", "dev"] });
|
await helpers.startApplication("tests/configs/modules/display.js", null, ["js/electron.js", "dev"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function () {
|
afterEach(async () => {
|
||||||
await electronApp.close();
|
await helpers.stopApplication();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should open browserwindow and dev console", async function () {
|
it("should open browserwindow and dev console", async () => {
|
||||||
const pageArray = await electronApp.windows();
|
while (global.electronApp.windows().length < 2) await events.once(global.electronApp, "window");
|
||||||
|
const pageArray = await global.electronApp.windows();
|
||||||
expect(pageArray.length).toBe(2);
|
expect(pageArray.length).toBe(2);
|
||||||
for (const page of pageArray) {
|
for (const page of pageArray) {
|
||||||
expect(["MagicMirror²", "DevTools"]).toContain(await page.title());
|
expect(["MagicMirror²", "DevTools"]).toContain(await page.title());
|
||||||
|
|
46
tests/electron/helpers/global-setup.js
Normal file
46
tests/electron/helpers/global-setup.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// see https://playwright.dev/docs/api/class-electronapplication
|
||||||
|
// https://github.com/microsoft/playwright/issues/6347#issuecomment-1085850728
|
||||||
|
// https://www.anycodings.com/1questions/958135/can-i-set-the-date-for-playwright-browser
|
||||||
|
const { _electron: electron } = require("playwright");
|
||||||
|
|
||||||
|
exports.startApplication = async (configFilename, systemDate = null, electronParams = ["js/electron.js"]) => {
|
||||||
|
global.electronApp = null;
|
||||||
|
global.page = null;
|
||||||
|
process.env.MM_CONFIG_FILE = configFilename;
|
||||||
|
process.env.TZ = "GMT";
|
||||||
|
jest.retryTimes(3);
|
||||||
|
global.electronApp = await electron.launch({ args: electronParams });
|
||||||
|
|
||||||
|
await global.electronApp.firstWindow();
|
||||||
|
|
||||||
|
for (const win of global.electronApp.windows()) {
|
||||||
|
const title = await win.title();
|
||||||
|
expect(["MagicMirror²", "DevTools"]).toContain(title);
|
||||||
|
if (title === "MagicMirror²") {
|
||||||
|
global.page = win;
|
||||||
|
if (systemDate) {
|
||||||
|
await global.page.evaluate((systemDate) => {
|
||||||
|
Date.now = () => {
|
||||||
|
return new Date(systemDate);
|
||||||
|
};
|
||||||
|
}, systemDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.stopApplication = async () => {
|
||||||
|
if (global.electronApp) {
|
||||||
|
await global.electronApp.close();
|
||||||
|
}
|
||||||
|
global.electronApp = null;
|
||||||
|
global.page = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getElement = async (selector) => {
|
||||||
|
expect(global.page);
|
||||||
|
let elem = global.page.locator(selector);
|
||||||
|
await elem.waitFor();
|
||||||
|
expect(elem).not.toBe(null);
|
||||||
|
return elem;
|
||||||
|
};
|
29
tests/electron/helpers/weather-setup.js
Normal file
29
tests/electron/helpers/weather-setup.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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) => {
|
||||||
|
const elem = await helpers.getElement(element);
|
||||||
|
await expect(elem).not.toBe(null);
|
||||||
|
const text = await elem.textContent();
|
||||||
|
await expect(
|
||||||
|
text
|
||||||
|
.trim()
|
||||||
|
.replace(/(\r\n|\n|\r)/gm, "")
|
||||||
|
.replace(/[ ]+/g, " ")
|
||||||
|
).toBe(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.startApp = async (configFile, systemDate) => {
|
||||||
|
let mockWeather;
|
||||||
|
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);
|
||||||
|
};
|
32
tests/electron/modules/calendar_spec.js
Normal file
32
tests/electron/modules/calendar_spec.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
const helpers = require("../helpers/global-setup");
|
||||||
|
|
||||||
|
describe("Calendar module", () => {
|
||||||
|
/**
|
||||||
|
* move similar tests in function doTest
|
||||||
|
*
|
||||||
|
* @param {string} cssClass css selector
|
||||||
|
*/
|
||||||
|
const doTest = async (cssClass) => {
|
||||||
|
await helpers.getElement(".calendar");
|
||||||
|
await helpers.getElement(".module-content");
|
||||||
|
const events = await global.page.locator(".event");
|
||||||
|
const elem = await events.locator(cssClass);
|
||||||
|
expect(elem).not.toBe(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await helpers.stopApplication();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test css classes", () => {
|
||||||
|
it("has css class today", async () => {
|
||||||
|
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "01 Jan 2030 12:30:00 GMT");
|
||||||
|
await doTest(".today");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has css class tomorrow", async () => {
|
||||||
|
await helpers.startApplication("tests/configs/modules/calendar/custom.js", "31 Dez 2029 12:30:00 GMT");
|
||||||
|
await doTest(".tomorrow");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
45
tests/electron/modules/compliments_spec.js
Normal file
45
tests/electron/modules/compliments_spec.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
const helpers = require("../helpers/global-setup");
|
||||||
|
|
||||||
|
describe("Compliments module", () => {
|
||||||
|
/**
|
||||||
|
* move similar tests in function doTest
|
||||||
|
*
|
||||||
|
* @param {Array} complimentsArray The array of compliments.
|
||||||
|
*/
|
||||||
|
const doTest = async (complimentsArray) => {
|
||||||
|
await helpers.getElement(".compliments");
|
||||||
|
const elem = await helpers.getElement(".module-content");
|
||||||
|
expect(elem).not.toBe(null);
|
||||||
|
expect(complimentsArray).toContain(await elem.textContent());
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await helpers.stopApplication();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parts of days", () => {
|
||||||
|
it("Morning compliments for that part of day", async () => {
|
||||||
|
await helpers.startApplication("tests/configs/modules/compliments/compliments_parts_day.js", "01 Oct 2022 10:00:00 GMT");
|
||||||
|
await doTest(["Hi", "Good Morning", "Morning test"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Afternoon show Compliments for that part of day", async () => {
|
||||||
|
await helpers.startApplication("tests/configs/modules/compliments/compliments_parts_day.js", "01 Oct 2022 15:00:00 GMT");
|
||||||
|
await doTest(["Hello", "Good Afternoon", "Afternoon test"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Evening show Compliments for that part of day", async () => {
|
||||||
|
await helpers.startApplication("tests/configs/modules/compliments/compliments_parts_day.js", "01 Oct 2022 20:00:00 GMT");
|
||||||
|
await doTest(["Hello There", "Good Evening", "Evening test"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Feature date in compliments module", () => {
|
||||||
|
describe("Set date and empty compliments for anytime, morning, evening and afternoon", () => {
|
||||||
|
it("Show happy new year compliment on new years day", async () => {
|
||||||
|
await helpers.startApplication("tests/configs/modules/compliments/compliments_date.js", "01 Jan 2022 10:00:00 GMT");
|
||||||
|
await doTest(["Happy new year!"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
28
tests/electron/modules/weather_spec.js
Normal file
28
tests/electron/modules/weather_spec.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
const helpers = require("../helpers/global-setup");
|
||||||
|
const weatherHelper = require("../helpers/weather-setup");
|
||||||
|
|
||||||
|
describe("Weather module", () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
await helpers.stopApplication();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Current weather with sunrise", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await weatherHelper.startApp("tests/configs/modules/weather/currentweather_default.js", "13 Jan 2019 00:30:00 GMT");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render sunrise", async () => {
|
||||||
|
await weatherHelper.getText(".weather .normal.medium span:nth-child(4)", "7:00 am");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Current weather with sunset", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await weatherHelper.startApp("tests/configs/modules/weather/currentweather_default.js", "13 Jan 2019 12:30:00 GMT");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render sunset", async () => {
|
||||||
|
await weatherHelper.getText(".weather .normal.medium span:nth-child(4)", "3:45 pm");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
3
tests/mocks/compliments_test.json
Normal file
3
tests/mocks/compliments_test.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"anytime": ["Remote compliment file works!"]
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"LOADING": "Loading …",
|
"LOADING": "Loading …",
|
||||||
|
|
||||||
"TODAY": "Today",
|
"TODAY": "Today",
|
||||||
"TOMORROW": "Tomorrow",
|
"TOMORROW": "Tomorrow",
|
|
@ -1,10 +1,71 @@
|
||||||
const _ = require("lodash");
|
const _ = require("lodash");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} extendedData extra data to add to the default mock data
|
||||||
|
* @returns {string} mocked current weather data
|
||||||
|
*/
|
||||||
|
const generateWeather = (extendedData = {}) => {
|
||||||
|
return JSON.stringify(
|
||||||
|
_.merge(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
coord: {
|
||||||
|
lon: 11.58,
|
||||||
|
lat: 48.14
|
||||||
|
},
|
||||||
|
weather: [
|
||||||
|
{
|
||||||
|
id: 615,
|
||||||
|
main: "Snow",
|
||||||
|
description: "light rain and snow",
|
||||||
|
icon: "13d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 500,
|
||||||
|
main: "Rain",
|
||||||
|
description: "light rain",
|
||||||
|
icon: "10d"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
base: "stations",
|
||||||
|
main: {
|
||||||
|
temp: 1.49,
|
||||||
|
pressure: 1005,
|
||||||
|
humidity: 93.7,
|
||||||
|
temp_min: 1,
|
||||||
|
temp_max: 2
|
||||||
|
},
|
||||||
|
visibility: 7000,
|
||||||
|
wind: {
|
||||||
|
speed: 11.8,
|
||||||
|
deg: 250
|
||||||
|
},
|
||||||
|
clouds: {
|
||||||
|
all: 75
|
||||||
|
},
|
||||||
|
dt: 1547387400,
|
||||||
|
sys: {
|
||||||
|
type: 1,
|
||||||
|
id: 1267,
|
||||||
|
message: 0.0031,
|
||||||
|
country: "DE",
|
||||||
|
sunrise: 1547362817,
|
||||||
|
sunset: 1547394301
|
||||||
|
},
|
||||||
|
id: 2867714,
|
||||||
|
name: "Munich",
|
||||||
|
cod: 200
|
||||||
|
},
|
||||||
|
extendedData
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} extendedData extra data to add to the default mock data
|
* @param {object} extendedData extra data to add to the default mock data
|
||||||
* @returns {string} mocked forecast weather data
|
* @returns {string} mocked forecast weather data
|
||||||
*/
|
*/
|
||||||
function generateWeatherForecast(extendedData = {}) {
|
const generateWeatherForecast = (extendedData = {}) => {
|
||||||
return JSON.stringify(
|
return JSON.stringify(
|
||||||
_.merge(
|
_.merge(
|
||||||
{},
|
{},
|
||||||
|
@ -110,6 +171,6 @@ function generateWeatherForecast(extendedData = {}) {
|
||||||
extendedData
|
extendedData
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = generateWeatherForecast;
|
module.exports = { generateWeather, generateWeatherForecast };
|
|
@ -1,39 +1,39 @@
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { JSDOM } = require("jsdom");
|
const { JSDOM } = require("jsdom");
|
||||||
|
|
||||||
describe("File js/class", function () {
|
describe("File js/class", () => {
|
||||||
describe("Test function cloneObject", function () {
|
describe("Test function cloneObject", () => {
|
||||||
let clone;
|
let clone;
|
||||||
let dom;
|
let dom;
|
||||||
|
|
||||||
beforeAll(function (done) {
|
beforeAll((done) => {
|
||||||
dom = new JSDOM(
|
dom = new JSDOM(
|
||||||
`<script>var Log = {log: function() {}};</script>\
|
`<script>var Log = {log: () => {}};</script>\
|
||||||
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "class.js")}">`,
|
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "class.js")}">`,
|
||||||
{ runScripts: "dangerously", resources: "usable" }
|
{ runScripts: "dangerously", resources: "usable" }
|
||||||
);
|
);
|
||||||
dom.window.onload = function () {
|
dom.window.onload = () => {
|
||||||
const { cloneObject } = dom.window;
|
const { cloneObject } = dom.window;
|
||||||
clone = cloneObject;
|
clone = cloneObject;
|
||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should clone object", function () {
|
it("should clone object", () => {
|
||||||
const expected = { name: "Rodrigo", web: "https://rodrigoramirez.com", project: "MagicMirror" };
|
const expected = { name: "Rodrigo", web: "https://rodrigoramirez.com", project: "MagicMirror" };
|
||||||
const obj = clone(expected);
|
const obj = clone(expected);
|
||||||
expect(obj).toEqual(expected);
|
expect(obj).toEqual(expected);
|
||||||
expect(expected === obj).toBe(false);
|
expect(expected === obj).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should clone array", function () {
|
it("should clone array", () => {
|
||||||
const expected = [1, null, undefined, "TEST"];
|
const expected = [1, null, undefined, "TEST"];
|
||||||
const obj = clone(expected);
|
const obj = clone(expected);
|
||||||
expect(obj).toEqual(expected);
|
expect(obj).toEqual(expected);
|
||||||
expect(expected === obj).toBe(false);
|
expect(expected === obj).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should clone number", function () {
|
it("should clone number", () => {
|
||||||
let expected = 1;
|
let expected = 1;
|
||||||
let obj = clone(expected);
|
let obj = clone(expected);
|
||||||
expect(obj).toBe(expected);
|
expect(obj).toBe(expected);
|
||||||
|
@ -43,25 +43,25 @@ describe("File js/class", function () {
|
||||||
expect(obj).toBe(expected);
|
expect(obj).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should clone string", function () {
|
it("should clone string", () => {
|
||||||
const expected = "Perfect stranger";
|
const expected = "Perfect stranger";
|
||||||
const obj = clone(expected);
|
const obj = clone(expected);
|
||||||
expect(obj).toBe(expected);
|
expect(obj).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should clone undefined", function () {
|
it("should clone undefined", () => {
|
||||||
const expected = undefined;
|
const expected = undefined;
|
||||||
const obj = clone(expected);
|
const obj = clone(expected);
|
||||||
expect(obj).toBe(expected);
|
expect(obj).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should clone null", function () {
|
it("should clone null", () => {
|
||||||
const expected = null;
|
const expected = null;
|
||||||
const obj = clone(expected);
|
const obj = clone(expected);
|
||||||
expect(obj).toBe(expected);
|
expect(obj).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should clone nested object", function () {
|
it("should clone nested object", () => {
|
||||||
const expected = {
|
const expected = {
|
||||||
name: "fewieden",
|
name: "fewieden",
|
||||||
link: "https://github.com/fewieden",
|
link: "https://github.com/fewieden",
|
||||||
|
@ -83,21 +83,21 @@ describe("File js/class", function () {
|
||||||
expect(expected.properties.items[1] === obj.properties.items[1]).toBe(false);
|
expect(expected.properties.items[1] === obj.properties.items[1]).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test lockstring code", function () {
|
describe("Test lockstring code", () => {
|
||||||
let log;
|
let log;
|
||||||
|
|
||||||
beforeAll(function () {
|
beforeAll(() => {
|
||||||
log = dom.window.Log.log;
|
log = dom.window.Log.log;
|
||||||
dom.window.Log.log = function cmp(str) {
|
dom.window.Log.log = (str) => {
|
||||||
expect(str).toBe("lockStrings");
|
expect(str).toBe("lockStrings");
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(function () {
|
afterAll(() => {
|
||||||
dom.window.Log.log = log;
|
dom.window.Log.log = log;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should clone object and log lockStrings", function () {
|
it("should clone object and log lockStrings", () => {
|
||||||
const expected = { name: "Module", lockStrings: "stringLock" };
|
const expected = { name: "Module", lockStrings: "stringLock" };
|
||||||
const obj = clone(expected);
|
const obj = clone(expected);
|
||||||
expect(obj).toEqual(expected);
|
expect(obj).toEqual(expected);
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
const deprecated = require("../../../js/deprecated");
|
const deprecated = require("../../../js/deprecated");
|
||||||
|
|
||||||
describe("Deprecated", function () {
|
describe("Deprecated", () => {
|
||||||
it("should be an object", function () {
|
it("should be an object", () => {
|
||||||
expect(typeof deprecated).toBe("object");
|
expect(typeof deprecated).toBe("object");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should contain configs array with deprecated options as strings", function () {
|
it("should contain configs array with deprecated options as strings", () => {
|
||||||
expect(Array.isArray(["deprecated.configs"])).toBe(true);
|
expect(Array.isArray(["deprecated.configs"])).toBe(true);
|
||||||
for (let option of deprecated.configs) {
|
for (let option of deprecated.configs) {
|
||||||
expect(typeof option).toBe("string");
|
expect(typeof option).toBe("string");
|
||||||
|
|
|
@ -4,17 +4,17 @@ const { JSDOM } = require("jsdom");
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const sockets = new Set();
|
const sockets = new Set();
|
||||||
|
|
||||||
describe("Translator", function () {
|
describe("Translator", () => {
|
||||||
let server;
|
let server;
|
||||||
|
|
||||||
beforeAll(function () {
|
beforeAll(() => {
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(helmet());
|
app.use(helmet());
|
||||||
app.use(function (req, res, next) {
|
app.use((req, res, next) => {
|
||||||
res.header("Access-Control-Allow-Origin", "*");
|
res.header("Access-Control-Allow-Origin", "*");
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
app.use("/translations", express.static(path.join(__dirname, "..", "..", "..", "tests", "configs", "data")));
|
app.use("/translations", express.static(path.join(__dirname, "..", "..", "..", "tests", "mocks")));
|
||||||
|
|
||||||
server = app.listen(3000);
|
server = app.listen(3000);
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ describe("Translator", function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(function () {
|
afterAll(() => {
|
||||||
for (const socket of sockets) {
|
for (const socket of sockets) {
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ describe("Translator", function () {
|
||||||
server.close();
|
server.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("translate", function () {
|
describe("translate", () => {
|
||||||
const translations = {
|
const translations = {
|
||||||
"MMM-Module": {
|
"MMM-Module": {
|
||||||
Hello: "Hallo",
|
Hello: "Hallo",
|
||||||
|
@ -70,16 +70,16 @@ describe("Translator", function () {
|
||||||
/**
|
/**
|
||||||
* @param {object} Translator the global Translator object
|
* @param {object} Translator the global Translator object
|
||||||
*/
|
*/
|
||||||
function setTranslations(Translator) {
|
const setTranslations = (Translator) => {
|
||||||
Translator.translations = translations;
|
Translator.translations = translations;
|
||||||
Translator.coreTranslations = coreTranslations;
|
Translator.coreTranslations = coreTranslations;
|
||||||
Translator.translationsFallback = translationsFallback;
|
Translator.translationsFallback = translationsFallback;
|
||||||
Translator.coreTranslationsFallback = coreTranslationsFallback;
|
Translator.coreTranslationsFallback = coreTranslationsFallback;
|
||||||
}
|
};
|
||||||
|
|
||||||
it("should return custom module translation", function (done) {
|
it("should return custom module translation", (done) => {
|
||||||
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = () => {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
let translation = Translator.translate({ name: "MMM-Module" }, "Hello");
|
let translation = Translator.translate({ name: "MMM-Module" }, "Hello");
|
||||||
|
@ -90,9 +90,9 @@ describe("Translator", function () {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return core translation", function (done) {
|
it("should return core translation", (done) => {
|
||||||
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = () => {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
let translation = Translator.translate({ name: "MMM-Module" }, "FOO");
|
let translation = Translator.translate({ name: "MMM-Module" }, "FOO");
|
||||||
|
@ -103,9 +103,9 @@ describe("Translator", function () {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return custom module translation fallback", function (done) {
|
it("should return custom module translation fallback", (done) => {
|
||||||
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = () => {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
const translation = Translator.translate({ name: "MMM-Module" }, "A key");
|
const translation = Translator.translate({ name: "MMM-Module" }, "A key");
|
||||||
|
@ -114,9 +114,9 @@ describe("Translator", function () {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return core translation fallback", function (done) {
|
it("should return core translation fallback", (done) => {
|
||||||
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = () => {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
const translation = Translator.translate({ name: "MMM-Module" }, "Fallback");
|
const translation = Translator.translate({ name: "MMM-Module" }, "Fallback");
|
||||||
|
@ -125,9 +125,9 @@ describe("Translator", function () {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return translation with placeholder for missing variables", function (done) {
|
it("should return translation with placeholder for missing variables", (done) => {
|
||||||
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = () => {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
const translation = Translator.translate({ name: "MMM-Module" }, "Hello {username}");
|
const translation = Translator.translate({ name: "MMM-Module" }, "Hello {username}");
|
||||||
|
@ -136,9 +136,9 @@ describe("Translator", function () {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return key if no translation was found", function (done) {
|
it("should return key if no translation was found", (done) => {
|
||||||
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = () => {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
const translation = Translator.translate({ name: "MMM-Module" }, "MISSING");
|
const translation = Translator.translate({ name: "MMM-Module" }, "MISSING");
|
||||||
|
@ -148,7 +148,7 @@ describe("Translator", function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("load", function () {
|
describe("load", () => {
|
||||||
const mmm = {
|
const mmm = {
|
||||||
name: "TranslationTest",
|
name: "TranslationTest",
|
||||||
file(file) {
|
file(file) {
|
||||||
|
@ -156,41 +156,41 @@ describe("Translator", function () {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should load translations", function (done) {
|
it("should load translations", (done) => {
|
||||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script>var Log = {log: () => {}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = () => {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
const file = "TranslationTest.json";
|
const file = "translation_test.json";
|
||||||
|
|
||||||
Translator.load(mmm, file, false, function () {
|
Translator.load(mmm, file, false, () => {
|
||||||
const json = require(path.join(__dirname, "..", "..", "..", "tests", "configs", "data", file));
|
const json = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", file));
|
||||||
expect(Translator.translations[mmm.name]).toEqual(json);
|
expect(Translator.translations[mmm.name]).toEqual(json);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should load translation fallbacks", function (done) {
|
it("should load translation fallbacks", (done) => {
|
||||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script>var Log = {log: () => {}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = () => {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
const file = "TranslationTest.json";
|
const file = "translation_test.json";
|
||||||
|
|
||||||
Translator.load(mmm, file, true, function () {
|
Translator.load(mmm, file, true, () => {
|
||||||
const json = require(path.join(__dirname, "..", "..", "..", "tests", "configs", "data", file));
|
const json = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", file));
|
||||||
expect(Translator.translationsFallback[mmm.name]).toEqual(json);
|
expect(Translator.translationsFallback[mmm.name]).toEqual(json);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not load translations, if module fallback exists", function (done) {
|
it("should not load translations, if module fallback exists", (done) => {
|
||||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script>var Log = {log: () => {}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = () => {
|
||||||
const { Translator, XMLHttpRequest } = dom.window;
|
const { Translator, XMLHttpRequest } = dom.window;
|
||||||
const file = "TranslationTest.json";
|
const file = "translation_test.json";
|
||||||
|
|
||||||
XMLHttpRequest.prototype.send = function () {
|
XMLHttpRequest.prototype.send = () => {
|
||||||
throw "Shouldn't load files";
|
throw "Shouldn't load files";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -198,7 +198,7 @@ describe("Translator", function () {
|
||||||
Hello: "Hallo"
|
Hello: "Hallo"
|
||||||
};
|
};
|
||||||
|
|
||||||
Translator.load(mmm, file, false, function () {
|
Translator.load(mmm, file, false, () => {
|
||||||
expect(Translator.translations[mmm.name]).toBe(undefined);
|
expect(Translator.translations[mmm.name]).toBe(undefined);
|
||||||
expect(Translator.translationsFallback[mmm.name]).toEqual({
|
expect(Translator.translationsFallback[mmm.name]).toEqual({
|
||||||
Hello: "Hallo"
|
Hello: "Hallo"
|
||||||
|
@ -209,19 +209,19 @@ describe("Translator", function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("loadCoreTranslations", function () {
|
describe("loadCoreTranslations", () => {
|
||||||
it("should load core translations and fallback", function (done) {
|
it("should load core translations and fallback", (done) => {
|
||||||
const dom = new JSDOM(
|
const dom = new JSDOM(
|
||||||
`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
`<script>var translations = {en: "http://localhost:3000/translations/translation_test.json"}; var Log = {log: () => {}};</script>\
|
||||||
<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 = function () {
|
dom.window.onload = () => {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
Translator.loadCoreTranslations("en");
|
Translator.loadCoreTranslations("en");
|
||||||
|
|
||||||
const en = require(path.join(__dirname, "..", "..", "..", "tests", "configs", "data", "en.json"));
|
const en = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", "translation_test.json"));
|
||||||
setTimeout(function () {
|
setTimeout(() => {
|
||||||
expect(Translator.coreTranslations).toEqual(en);
|
expect(Translator.coreTranslations).toEqual(en);
|
||||||
expect(Translator.coreTranslationsFallback).toEqual(en);
|
expect(Translator.coreTranslationsFallback).toEqual(en);
|
||||||
done();
|
done();
|
||||||
|
@ -229,18 +229,18 @@ describe("Translator", function () {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should load core fallback if language cannot be found", function (done) {
|
it("should load core fallback if language cannot be found", (done) => {
|
||||||
const dom = new JSDOM(
|
const dom = new JSDOM(
|
||||||
`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
`<script>var translations = {en: "http://localhost:3000/translations/translation_test.json"}; var Log = {log: () => {}};</script>\
|
||||||
<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 = function () {
|
dom.window.onload = () => {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
Translator.loadCoreTranslations("MISSINGLANG");
|
Translator.loadCoreTranslations("MISSINGLANG");
|
||||||
|
|
||||||
const en = require(path.join(__dirname, "..", "..", "..", "tests", "configs", "data", "en.json"));
|
const en = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", "translation_test.json"));
|
||||||
setTimeout(function () {
|
setTimeout(() => {
|
||||||
expect(Translator.coreTranslations).toEqual({});
|
expect(Translator.coreTranslations).toEqual({});
|
||||||
expect(Translator.coreTranslationsFallback).toEqual(en);
|
expect(Translator.coreTranslationsFallback).toEqual(en);
|
||||||
done();
|
done();
|
||||||
|
@ -249,36 +249,36 @@ describe("Translator", function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("loadCoreTranslationsFallback", function () {
|
describe("loadCoreTranslationsFallback", () => {
|
||||||
it("should load core translations fallback", function (done) {
|
it("should load core translations fallback", (done) => {
|
||||||
const dom = new JSDOM(
|
const dom = new JSDOM(
|
||||||
`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
`<script>var translations = {en: "http://localhost:3000/translations/translation_test.json"}; var Log = {log: () => {}};</script>\
|
||||||
<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 = function () {
|
dom.window.onload = () => {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
Translator.loadCoreTranslationsFallback();
|
Translator.loadCoreTranslationsFallback();
|
||||||
|
|
||||||
const en = require(path.join(__dirname, "..", "..", "..", "tests", "configs", "data", "en.json"));
|
const en = require(path.join(__dirname, "..", "..", "..", "tests", "mocks", "translation_test.json"));
|
||||||
setTimeout(function () {
|
setTimeout(() => {
|
||||||
expect(Translator.coreTranslationsFallback).toEqual(en);
|
expect(Translator.coreTranslationsFallback).toEqual(en);
|
||||||
done();
|
done();
|
||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should load core fallback if language cannot be found", function (done) {
|
it("should load core fallback if language cannot be found", (done) => {
|
||||||
const dom = new JSDOM(
|
const dom = new JSDOM(
|
||||||
`<script>var translations = {}; var Log = {log: function(){}};</script>\
|
`<script>var translations = {}; var Log = {log: () => {}};</script>\
|
||||||
<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 = function () {
|
dom.window.onload = () => {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
Translator.loadCoreTranslations();
|
Translator.loadCoreTranslations();
|
||||||
|
|
||||||
setTimeout(function () {
|
setTimeout(() => {
|
||||||
expect(Translator.coreTranslationsFallback).toEqual({});
|
expect(Translator.coreTranslationsFallback).toEqual({});
|
||||||
done();
|
done();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
const Utils = require("../../../js/utils.js");
|
const Utils = require("../../../js/utils.js");
|
||||||
const colors = require("colors/safe");
|
const colors = require("colors/safe");
|
||||||
|
|
||||||
describe("Utils", function () {
|
describe("Utils", () => {
|
||||||
describe("colors", function () {
|
describe("colors", () => {
|
||||||
const colorsEnabled = colors.enabled;
|
const colorsEnabled = colors.enabled;
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(() => {
|
||||||
colors.enabled = colorsEnabled;
|
colors.enabled = colorsEnabled;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have info, warn and error properties", function () {
|
it("should have info, warn and error properties", () => {
|
||||||
expect(Utils.colors).toHaveProperty("info");
|
expect(Utils.colors).toHaveProperty("info");
|
||||||
expect(Utils.colors).toHaveProperty("warn");
|
expect(Utils.colors).toHaveProperty("warn");
|
||||||
expect(Utils.colors).toHaveProperty("error");
|
expect(Utils.colors).toHaveProperty("error");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("properties should be functions", function () {
|
it("properties should be functions", () => {
|
||||||
expect(typeof Utils.colors.info).toBe("function");
|
expect(typeof Utils.colors.info).toBe("function");
|
||||||
expect(typeof Utils.colors.warn).toBe("function");
|
expect(typeof Utils.colors.warn).toBe("function");
|
||||||
expect(typeof Utils.colors.error).toBe("function");
|
expect(typeof Utils.colors.error).toBe("function");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should print colored message in supported consoles", function () {
|
it("should print colored message in supported consoles", () => {
|
||||||
colors.enabled = true;
|
colors.enabled = true;
|
||||||
expect(Utils.colors.info("some informations")).toBe("\u001b[34msome informations\u001b[39m");
|
expect(Utils.colors.info("some informations")).toBe("\u001b[34msome informations\u001b[39m");
|
||||||
expect(Utils.colors.warn("a warning")).toBe("\u001b[33ma warning\u001b[39m");
|
expect(Utils.colors.warn("a warning")).toBe("\u001b[33ma warning\u001b[39m");
|
||||||
expect(Utils.colors.error("ERROR!")).toBe("\u001b[31mERROR!\u001b[39m");
|
expect(Utils.colors.error("ERROR!")).toBe("\u001b[31mERROR!\u001b[39m");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should print message in unsupported consoles", function () {
|
it("should print message in unsupported consoles", () => {
|
||||||
colors.enabled = false;
|
colors.enabled = false;
|
||||||
expect(Utils.colors.info("some informations")).toBe("some informations");
|
expect(Utils.colors.info("some informations")).toBe("some informations");
|
||||||
expect(Utils.colors.warn("a warning")).toBe("a warning");
|
expect(Utils.colors.warn("a warning")).toBe("a warning");
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
global.moment = require("moment");
|
global.moment = require("moment");
|
||||||
|
|
||||||
describe("Functions into modules/default/calendar/calendar.js", function () {
|
describe("Functions into modules/default/calendar/calendar.js", () => {
|
||||||
// Fake for use by calendar.js
|
// Fake for use by calendar.js
|
||||||
Module = {};
|
Module = {};
|
||||||
Module.definitions = {};
|
Module.definitions = {};
|
||||||
Module.register = function (name, moduleDefinition) {
|
Module.register = (name, moduleDefinition) => {
|
||||||
Module.definitions[name] = moduleDefinition;
|
Module.definitions[name] = moduleDefinition;
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(function () {
|
beforeAll(() => {
|
||||||
// load calendar.js
|
// load calendar.js
|
||||||
require("../../../modules/default/calendar/calendar.js");
|
require("../../../modules/default/calendar/calendar.js");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("capFirst", function () {
|
describe("capFirst", () => {
|
||||||
const words = {
|
const words = {
|
||||||
rodrigo: "Rodrigo",
|
rodrigo: "Rodrigo",
|
||||||
"123m": "123m",
|
"123m": "123m",
|
||||||
|
@ -23,61 +23,61 @@ describe("Functions into modules/default/calendar/calendar.js", function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.keys(words).forEach((word) => {
|
Object.keys(words).forEach((word) => {
|
||||||
it(`for '${word}' should return '${words[word]}'`, function () {
|
it(`for '${word}' should return '${words[word]}'`, () => {
|
||||||
expect(Module.definitions.calendar.capFirst(word)).toBe(words[word]);
|
expect(Module.definitions.calendar.capFirst(word)).toBe(words[word]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getLocaleSpecification", function () {
|
describe("getLocaleSpecification", () => {
|
||||||
it("should return a valid moment.LocaleSpecification for a 12-hour format", function () {
|
it("should return a valid moment.LocaleSpecification for a 12-hour format", () => {
|
||||||
expect(Module.definitions.calendar.getLocaleSpecification(12)).toEqual({ longDateFormat: { LT: "h:mm A" } });
|
expect(Module.definitions.calendar.getLocaleSpecification(12)).toEqual({ longDateFormat: { LT: "h:mm A" } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return a valid moment.LocaleSpecification for a 24-hour format", function () {
|
it("should return a valid moment.LocaleSpecification for a 24-hour format", () => {
|
||||||
expect(Module.definitions.calendar.getLocaleSpecification(24)).toEqual({ longDateFormat: { LT: "HH:mm" } });
|
expect(Module.definitions.calendar.getLocaleSpecification(24)).toEqual({ longDateFormat: { LT: "HH:mm" } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return the current system locale when called without timeFormat number", function () {
|
it("should return the current system locale when called without timeFormat number", () => {
|
||||||
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: moment.localeData().longDateFormat("LT") } });
|
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: moment.localeData().longDateFormat("LT") } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return a 12-hour longDateFormat when using the 'en' locale", function () {
|
it("should return a 12-hour longDateFormat when using the 'en' locale", () => {
|
||||||
const localeBackup = moment.locale();
|
const localeBackup = moment.locale();
|
||||||
moment.locale("en");
|
moment.locale("en");
|
||||||
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
|
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
|
||||||
moment.locale(localeBackup);
|
moment.locale(localeBackup);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return a 12-hour longDateFormat when using the 'au' locale", function () {
|
it("should return a 12-hour longDateFormat when using the 'au' locale", () => {
|
||||||
const localeBackup = moment.locale();
|
const localeBackup = moment.locale();
|
||||||
moment.locale("au");
|
moment.locale("au");
|
||||||
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
|
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
|
||||||
moment.locale(localeBackup);
|
moment.locale(localeBackup);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return a 12-hour longDateFormat when using the 'eg' locale", function () {
|
it("should return a 12-hour longDateFormat when using the 'eg' locale", () => {
|
||||||
const localeBackup = moment.locale();
|
const localeBackup = moment.locale();
|
||||||
moment.locale("eg");
|
moment.locale("eg");
|
||||||
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
|
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
|
||||||
moment.locale(localeBackup);
|
moment.locale(localeBackup);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return a 24-hour longDateFormat when using the 'nl' locale", function () {
|
it("should return a 24-hour longDateFormat when using the 'nl' locale", () => {
|
||||||
const localeBackup = moment.locale();
|
const localeBackup = moment.locale();
|
||||||
moment.locale("nl");
|
moment.locale("nl");
|
||||||
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
|
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
|
||||||
moment.locale(localeBackup);
|
moment.locale(localeBackup);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return a 24-hour longDateFormat when using the 'fr' locale", function () {
|
it("should return a 24-hour longDateFormat when using the 'fr' locale", () => {
|
||||||
const localeBackup = moment.locale();
|
const localeBackup = moment.locale();
|
||||||
moment.locale("fr");
|
moment.locale("fr");
|
||||||
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
|
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
|
||||||
moment.locale(localeBackup);
|
moment.locale(localeBackup);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return a 24-hour longDateFormat when using the 'uk' locale", function () {
|
it("should return a 24-hour longDateFormat when using the 'uk' locale", () => {
|
||||||
const localeBackup = moment.locale();
|
const localeBackup = moment.locale();
|
||||||
moment.locale("uk");
|
moment.locale("uk");
|
||||||
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
|
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
|
||||||
|
@ -85,43 +85,43 @@ describe("Functions into modules/default/calendar/calendar.js", function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("shorten", function () {
|
describe("shorten", () => {
|
||||||
const strings = {
|
const strings = {
|
||||||
" String with whitespace at the beginning that needs trimming": { length: 16, return: "String with whit…" },
|
" String with whitespace at the beginning that needs trimming": { length: 16, return: "String with whit…" },
|
||||||
"long string that needs shortening": { length: 16, return: "long string that…" },
|
"long string that needs shortening": { length: 16, return: "long string that…" },
|
||||||
"short string": { length: 16, return: "short string" },
|
"short string": { length: 16, return: "short string" },
|
||||||
"long string with no maxLength defined": { return: "long string with no maxLength defined" }
|
"long string with no maxLength defined": { return: "long string with no maxLength defined" }
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.keys(strings).forEach((string) => {
|
Object.keys(strings).forEach((string) => {
|
||||||
it(`for '${string}' should return '${strings[string].return}'`, function () {
|
it(`for '${string}' should return '${strings[string].return}'`, () => {
|
||||||
expect(Module.definitions.calendar.shorten(string, strings[string].length)).toBe(strings[string].return);
|
expect(Module.definitions.calendar.shorten(string, strings[string].length)).toBe(strings[string].return);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return an empty string if shorten is called with a non-string", function () {
|
it("should return an empty string if shorten is called with a non-string", () => {
|
||||||
expect(Module.definitions.calendar.shorten(100)).toBe("");
|
expect(Module.definitions.calendar.shorten(100)).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not shorten the string if shorten is called with a non-number maxLength", function () {
|
it("should not shorten the string if shorten is called with a non-number maxLength", () => {
|
||||||
expect(Module.definitions.calendar.shorten("This is a test string", "This is not a number")).toBe("This is a test string");
|
expect(Module.definitions.calendar.shorten("This is a test string", "This is not a number")).toBe("This is a test string");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should wrap the string instead of shorten it if shorten is called with wrapEvents = true (with maxLength defined as 20)", function () {
|
it("should wrap the string instead of shorten it if shorten is called with wrapEvents = true (with maxLength defined as 20)", () => {
|
||||||
expect(Module.definitions.calendar.shorten("This is a wrapEvent test. Should wrap the string instead of shorten it if called with wrapEvent = true", 20, true)).toBe(
|
expect(Module.definitions.calendar.shorten("This is a wrapEvent test. Should wrap the string instead of shorten it if called with wrapEvent = true", 20, true)).toBe(
|
||||||
"This is a <br>wrapEvent test. Should wrap <br>the string instead of <br>shorten it if called with <br>wrapEvent = true"
|
"This is a <br>wrapEvent test. Should wrap <br>the string instead of <br>shorten it if called with <br>wrapEvent = true"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should wrap the string instead of shorten it if shorten is called with wrapEvents = true (without maxLength defined, default 25)", function () {
|
it("should wrap the string instead of shorten it if shorten is called with wrapEvents = true (without maxLength defined, default 25)", () => {
|
||||||
expect(Module.definitions.calendar.shorten("This is a wrapEvent test. Should wrap the string instead of shorten it if called with wrapEvent = true", undefined, true)).toBe(
|
expect(Module.definitions.calendar.shorten("This is a wrapEvent test. Should wrap the string instead of shorten it if called with wrapEvent = true", undefined, true)).toBe(
|
||||||
"This is a wrapEvent <br>test. Should wrap the string <br>instead of shorten it if called <br>with wrapEvent = true"
|
"This is a wrapEvent <br>test. Should wrap the string <br>instead of shorten it if called <br>with wrapEvent = true"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should wrap and shorten the string in the second line if called with wrapEvents = true and maxTitleLines = 2", function () {
|
it("should wrap and shorten the string in the second line if called with wrapEvents = true and maxTitleLines = 2", () => {
|
||||||
expect(Module.definitions.calendar.shorten("This is a wrapEvent and maxTitleLines test. Should wrap and shorten the string in the second line if called with wrapEvents = true and maxTitleLines = 2", undefined, true, 2)).toBe(
|
expect(Module.definitions.calendar.shorten("This is a wrapEvent and maxTitleLines test. Should wrap and shorten the string in the second line if called with wrapEvents = true and maxTitleLines = 2", undefined, true, 2)).toBe(
|
||||||
"This is a wrapEvent and <br>maxTitleLines test. Should wrap and …"
|
"This is a wrapEvent and <br>maxTitleLines test. Should wrap and …"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue