Merge pull request #2672 from MichMich/develop

Release 2.17.0
This commit is contained in:
Michael Teeuw
2021-10-01 15:17:49 +02:00
committed by GitHub
123 changed files with 3934 additions and 4261 deletions

View File

@@ -25,6 +25,7 @@
"prettier/prettier": "error",
"eqeqeq": "error",
"no-prototype-builtins": "off",
"no-unused-vars": "off"
"no-unused-vars": "off",
"no-useless-return": "error"
}
}

View File

@@ -18,7 +18,7 @@ To run ESLint, use `npm run lint:js`.
We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is in our .stylelintrc file.
To run StyleLint, use `npm run lint:style`.
To run StyleLint, use `npm run lint:css`.
### Submitting Issues
@@ -32,9 +32,9 @@ When submitting a new issue, please supply the following information:
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX).
**Node Version**: Make sure it's version 10 or later.
**Node Version**: Make sure it's version 12 or later (recommended is 14).
**MagicMirror Version**: Now that the versions have split, tell us if you are using the PHP version (v1) or the newer JavaScript version (v2).
**MagicMirror Version**: Please let us know which version of MagicMirror you are running. It can be found in the `package.json` file.
**Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem.

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +1,2 @@
github: MichMich
custom: ['https://magicmirror.builders/#donate']
custom: ["https://magicmirror.builders/#donate"]

View File

@@ -19,8 +19,10 @@ If you are facing an issue or found a bug while trying to install MagicMirror vi
## I found a bug in the MagicMirror Docker image
If you are facing an issue or found a bug while running MagicMirror inside a Docker container please create an issue in the GitHub repository of the MagicMirror Docker image:
[https://github.com/bastilimbach/docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror)
If you are facing an issue or found a bug while running MagicMirror inside a Docker container please create an issue in the corresponding repository:
- karsten13/magicmirror: [https://gitlab.com/khassel/magicmirror](https://gitlab.com/khassel/magicmirror)
- (deprecated) bastilimbach/docker-magicmirror: [https://github.com/bastilimbach/docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror)
---
@@ -31,9 +33,9 @@ When submitting a new issue, please supply the following information:
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX).
**Node Version**: Make sure it's version 10 or later.
**Node Version**: Make sure it's version 12 or later (recommended is 14).
**MagicMirror Version**: Please let us now which version of MagicMirror you are running. It can be found in the `package.log` file.
**MagicMirror Version**: Please let us know which version of MagicMirror you are running. It can be found in the `package.json` file.
**Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem.

View File

@@ -2,23 +2,20 @@ Hello and thank you for wanting to contribute to the MagicMirror project
**Please make sure that you have followed these 4 rules before submitting your Pull Request:**
> 1) Base your pull requests against the `develop` branch.
> 1. Base your pull requests against the `develop` branch.
>
> 2. Include these infos in the description:
>
> 2) Include these infos in the description:
> * Does the pull request solve a **related** issue?
> * If so, can you reference the issue like this `Fixes #<issue_number>`?
> * What does the pull request accomplish? Use a list if needed.
> * If it includes major visual changes please add screenshots.
> - Does the pull request solve a **related** issue?
> - If so, can you reference the issue like this `Fixes #<issue_number>`?
> - What does the pull request accomplish? Use a list if needed.
> - If it includes major visual changes please add screenshots.
>
> 3. Please run `npm run lint:prettier` before submitting so that
> style issues are fixed.
>
> 3) Please run `npm run lint:prettier` before submitting so that
> style issues are fixed.
>
>
> 4) Don't forget to add an entry about your changes to
> the CHANGELOG.md file.
> 4. Don't forget to add an entry about your changes to
> the CHANGELOG.md file.
**Note**: Sometimes the development moves very fast. It is highly
recommended that you update your branch of `develop` before creating a

BIN
.github/header.psd vendored

Binary file not shown.

37
.github/workflows/automated-tests.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: "Run Automated Tests"
on:
push:
branches: [master, develop]
pull_request:
branches: [master, develop]
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies and run tests
run: |
Xvfb :99 -screen 0 1024x768x16 &
export DISPLAY=:99
npm install
touch css/custom.css
npm run test:prettier
npm run test:js
npm run test:css
npm run test:unit
npm run test:e2e
npm run test:electron

View File

@@ -4,22 +4,26 @@ name: "Run Codecov Tests"
on:
push:
branches: [ master, develop ]
branches: [master, develop]
pull_request:
branches: [ master, develop ]
branches: [master, develop]
jobs:
run-and-upload-coverage-report:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v2
- run: |
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies and run coverage
run: |
Xvfb :99 -screen 0 1024x768x16 &
export DISPLAY=:99
npm ci
touch css/custom.css
npm run test:coverage
- uses: codecov/codecov-action@v1
- name: Upload coverage results to codecov
uses: codecov/codecov-action@v1
with:
file: ./coverage/lcov.info
fail_ci_if_error: true

View File

@@ -4,15 +4,17 @@ name: "Enforce Changelog"
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
- uses: dangoslen/changelog-enforcer@v1.6.1
with:
changeLogPath: 'CHANGELOG.md'
skipLabels: 'Skip Changelog'
- name: Checkout code
uses: actions/checkout@v2
- name: Enforce changelog
uses: dangoslen/changelog-enforcer@v1.6.1
with:
changeLogPath: "CHANGELOG.md"
skipLabels: "Skip Changelog"

View File

@@ -1,33 +0,0 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: "Run Automated Tests"
on:
push:
branches: [ master, develop ]
pull_request:
branches: [ master, develop ]
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: |
Xvfb :99 -screen 0 1024x768x16 &
export DISPLAY=:99
npm install
npm run test:prettier
npm run test:js
npm run test:css
npm run test:unit
npm run test:e2e

1
.husky/.gitignore vendored
View File

@@ -1 +0,0 @@
_

View File

@@ -1,8 +1,4 @@
/config
/coverage
/vendor
!/vendor/vendor.js
.github
.nyc_output
package-lock.json
*.ts

View File

@@ -5,6 +5,38 @@ This project adheres to [Semantic Versioning](https://semver.org/).
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror²
## [2.17.0] - 2021-10-01
Special thanks to the following contributors: @apiontek, @eouia, @jupadin, @khassel and @rejas.
### Added
- Added showTime parameter to clock module for enabling/disabling time display in analog clock.
- Added custom electron switches from user config (`config.electronSwitches`).
- Added unit tests for updatenotification module.
### Updated
- Bump electron to v13 (and spectron to v15) and update other dependencies in package.json.
- Refactor test configs, use default test config for all tests.
- Updated github templates.
- Actually test all js and css files when lint script is run.
- Update jsdocs and print warnings during testing too.
- Update weathergov provider to try fetching not just current, but also foreacst, when API URLs available.
- Refactored clock layout.
- Refactored methods from weatherproviders into weatherobject (isDaytime, updateSunTime).
- Use of `logger.js` in jest tests.
- Run prettier over all relevant files.
- Move tests needing electron in new category `electron`, use `server only` mode in `e2e` tests.
- Update dependencies in package.json.
### Fixed
- Fix undefined error with ignoreToday option in weather module (#2620).
- Fix time zone correction in calendar module when the date hour is equal to the time zone correction value (#2632).
- Fix black cursor on startup when using electron.
- Fix update notification not working for own repository (#2644).
## [2.16.0] - 2021-07-01
Special thanks to the following contributors: @210954, @B1gG, @codac, @Crazylegstoo, @daniel, @earlman, @ezeholz, @FrancoisRmn, @jupadin, @khassel, @KristjanESPERANTO, @njwilliams, @oemel09, @r3wald, @rejas, @rico24, Faizan Ahmed.
@@ -412,6 +444,7 @@ Special thanks to @sdetweil for all his great contributions!
- Update `ical.js` to solve various calendar issues.
- Update weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676)
- Only update clock once per minute when seconds aren't shown
- Update weatherprovider documentation.
### Fixed

View File

@@ -57,7 +57,8 @@ let config = {
calendars: [
{
symbol: "calendar-check",
url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics" }
url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics"
}
]
}
},

View File

@@ -16,7 +16,7 @@
<!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. -->
<script type="text/javascript">
var version = "#VERSION#";
window.mmVersion = "#VERSION#";
</script>
</head>
<body>

View File

@@ -48,6 +48,7 @@ process.on("uncaughtException", function (err) {
*/
function App() {
let nodeHelpers = [];
let httpServer;
/**
* Loads the config file. Combines it with the defaults, and runs the
@@ -222,7 +223,7 @@ function App() {
}
loadModules(modules, function () {
const server = new Server(config, function (app, io) {
httpServer = new Server(config, function (app, io) {
Log.log("Server started ...");
for (let nodeHelper of nodeHelpers) {
@@ -253,6 +254,7 @@ function App() {
nodeHelper.stop();
}
}
httpServer.close();
};
/**

View File

@@ -8,8 +8,8 @@
* MIT Licensed.
*/
(function () {
var initializing = false;
var fnTest = /xyz/.test(function () {
let initializing = false;
const fnTest = /xyz/.test(function () {
xyz;
})
? /\b_super\b/
@@ -20,27 +20,27 @@
// Create a new Class that inherits from this class
Class.extend = function (prop) {
var _super = this.prototype;
let _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
initializing = true;
var prototype = new this();
const prototype = new this();
initializing = false;
// Make a copy of all prototype properties, to prevent reference issues.
for (var p in prototype) {
for (const p in prototype) {
prototype[p] = cloneObject(prototype[p]);
}
// Copy the properties over onto the new prototype
for (var name in prop) {
for (const name in prop) {
// Check if we're overwriting an existing function
prototype[name] =
typeof prop[name] === "function" && typeof _super[name] === "function" && fnTest.test(prop[name])
? (function (name, fn) {
return function () {
var tmp = this._super;
const tmp = this._super;
// Add a new ._super() method that is the same method
// but on the super-class
@@ -48,7 +48,7 @@
// The method only need to be bound temporarily, so we
// remove it when we're done executing
var ret = fn.apply(this, arguments);
const ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
@@ -91,8 +91,8 @@ function cloneObject(obj) {
return obj;
}
var temp = obj.constructor(); // give temp the original obj's constructor
for (var key in obj) {
const temp = obj.constructor(); // give temp the original obj's constructor
for (const key in obj) {
temp[key] = cloneObject(obj[key]);
if (key === "lockStrings") {

View File

@@ -19,7 +19,8 @@ let mainWindow;
*
*/
function createWindow() {
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
let electronSwitchesDefaults = ["autoplay-policy", "no-user-gesture-required"];
app.commandLine.appendSwitch(...new Set(electronSwitchesDefaults, config.electronSwitches));
let electronOptionsDefaults = {
width: 800,
height: 600,
@@ -65,12 +66,17 @@ function createWindow() {
if (process.argv.includes("dev")) {
if (process.env.JEST_WORKER_ID !== undefined) {
// if we are running with jest
var devtools = new BrowserWindow(electronOptions);
const devtools = new BrowserWindow(electronOptions);
mainWindow.webContents.setDevToolsWebContents(devtools.webContents);
}
mainWindow.webContents.openDevTools();
}
// simulate mouse move to hide black cursor on start
mainWindow.webContents.on("dom-ready", (event) => {
mainWindow.webContents.sendInputEvent({ type: "mouseMove", x: 0, y: 0 });
});
// Set responders for window events.
mainWindow.on("closed", function () {
mainWindow = null;
@@ -102,7 +108,12 @@ app.on("ready", function () {
// Quit when all windows are closed.
app.on("window-all-closed", function () {
createWindow();
if (process.env.JEST_WORKER_ID !== undefined) {
// if we are running with jest
app.quit();
} else {
createWindow();
}
});
app.on("activate", function () {

View File

@@ -9,12 +9,13 @@
*/
(function (root, factory) {
if (typeof exports === "object") {
// add timestamps in front of log messages
require("console-stamp")(console, {
pattern: "yyyy-mm-dd HH:MM:ss.l",
include: ["debug", "log", "info", "warn", "error"]
});
if (process.env.JEST_WORKER_ID === undefined) {
// add timestamps in front of log messages
require("console-stamp")(console, {
pattern: "yyyy-mm-dd HH:MM:ss.l",
include: ["debug", "log", "info", "warn", "error"]
});
}
// Node, CommonJS-like
module.exports = factory(root.config);
} else {
@@ -22,29 +23,57 @@
root.Log = factory(root.config);
}
})(this, function (config) {
const logLevel = {
debug: Function.prototype.bind.call(console.debug, console),
log: Function.prototype.bind.call(console.log, console),
info: Function.prototype.bind.call(console.info, console),
warn: Function.prototype.bind.call(console.warn, console),
error: Function.prototype.bind.call(console.error, console),
group: Function.prototype.bind.call(console.group, console),
groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console),
groupEnd: Function.prototype.bind.call(console.groupEnd, console),
time: Function.prototype.bind.call(console.time, console),
timeEnd: Function.prototype.bind.call(console.timeEnd, console),
timeStamp: Function.prototype.bind.call(console.timeStamp, console)
};
let logLevel;
let enableLog;
if (typeof exports === "object") {
// in nodejs and not running with jest
enableLog = process.env.JEST_WORKER_ID === undefined;
} else {
// in browser and not running with jsdom
enableLog = typeof window === "object" && window.name !== "jsdom";
}
logLevel.setLogLevel = function (newLevel) {
if (newLevel) {
Object.keys(logLevel).forEach(function (key, index) {
if (!newLevel.includes(key.toLocaleUpperCase())) {
logLevel[key] = function () {};
}
});
}
};
if (enableLog) {
logLevel = {
debug: Function.prototype.bind.call(console.debug, console),
log: Function.prototype.bind.call(console.log, console),
info: Function.prototype.bind.call(console.info, console),
warn: Function.prototype.bind.call(console.warn, console),
error: Function.prototype.bind.call(console.error, console),
group: Function.prototype.bind.call(console.group, console),
groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console),
groupEnd: Function.prototype.bind.call(console.groupEnd, console),
time: Function.prototype.bind.call(console.time, console),
timeEnd: Function.prototype.bind.call(console.timeEnd, console),
timeStamp: Function.prototype.bind.call(console.timeStamp, console)
};
logLevel.setLogLevel = function (newLevel) {
if (newLevel) {
Object.keys(logLevel).forEach(function (key, index) {
if (!newLevel.includes(key.toLocaleUpperCase())) {
logLevel[key] = function () {};
}
});
}
};
} else {
logLevel = {
debug: function () {},
log: function () {},
info: function () {},
warn: function () {},
error: function () {},
group: function () {},
groupCollapsed: function () {},
groupEnd: function () {},
time: function () {},
timeEnd: function () {},
timeStamp: function () {}
};
logLevel.setLogLevel = function () {};
}
return logLevel;
});

View File

@@ -7,7 +7,7 @@
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
var Module = Class.extend({
const Module = Class.extend({
/*********************************************************
* All methods (and properties) below can be subclassed. *
*********************************************************/
@@ -498,8 +498,8 @@ Module.create = function (name) {
Module.register = function (name, moduleDefinition) {
if (moduleDefinition.requiresVersion) {
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.version);
if (cmpVersions(window.version, moduleDefinition.requiresVersion) >= 0) {
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.mmVersion);
if (cmpVersions(window.mmVersion, moduleDefinition.requiresVersion) >= 0) {
Log.log("Version is ok!");
} else {
Log.warn("Version is incorrect. Skip module: '" + name + "'");
@@ -510,6 +510,8 @@ Module.register = function (name, moduleDefinition) {
Module.definitions[name] = moduleDefinition;
};
window.Module = Module;
/**
* Compare two semantic version numbers and return the difference.
*

View File

@@ -23,6 +23,7 @@ const Utils = require("./utils.js");
*/
function Server(config, callback) {
const port = process.env.MM_PORT || config.port;
const serverSockets = new Set();
let server = null;
if (config.useHttps) {
@@ -42,6 +43,13 @@ function Server(config, callback) {
allowEIO3: true
});
server.on("connection", (socket) => {
serverSockets.add(socket);
socket.on("close", () => {
serverSockets.delete(socket);
});
});
Log.log(`Starting server on port ${port} ... `);
server.listen(port, config.address || "localhost");
@@ -92,6 +100,13 @@ function Server(config, callback) {
if (typeof callback === "function") {
callback(app, io);
}
this.close = function () {
for (const socket of serverSockets.values()) {
socket.destroy();
}
server.close();
};
}
module.exports = Server;

View File

@@ -6,7 +6,7 @@
* By Christopher Fenner https://github.com/CFenner
* MIT Licensed.
*/
var Translator = (function () {
const Translator = (function () {
/**
* Load a JSON file via XHR.
*
@@ -141,12 +141,7 @@ var Translator = (function () {
* The first language defined in translations.js will be used.
*/
loadCoreTranslationsFallback: function () {
// The variable `first` will contain the first
// defined translation after the following line.
for (var first in translations) {
break;
}
let first = Object.keys(translations)[0];
if (first) {
Log.log("Loading core translation fallback file: " + translations[first]);
loadJSON(translations[first], (translations) => {
@@ -156,3 +151,5 @@ var Translator = (function () {
}
};
})();
window.Translator = Translator;

View File

@@ -1,16 +1,16 @@
type ModuleProperties = {
defaults?: object,
start?(): void,
getHeader?(): string,
getTemplate?(): string,
getTemplateData?(): object,
notificationReceived?(notification: string, payload: any, sender: object): void,
socketNotificationReceived?(notification: string, payload: any): void,
suspend?(): void,
resume?(): void,
getDom?(): HTMLElement,
getStyles?(): string[],
[key: string]: any,
defaults?: object;
start?(): void;
getHeader?(): string;
getTemplate?(): string;
getTemplateData?(): object;
notificationReceived?(notification: string, payload: any, sender: object): void;
socketNotificationReceived?(notification: string, payload: any): void;
suspend?(): void;
resume?(): void;
getDom?(): HTMLElement;
getStyles?(): string[];
[key: string]: any;
};
export declare const Module: {
@@ -18,14 +18,14 @@ export declare const Module: {
};
export declare const Log: {
info(message?: any, ...optionalParams: any[]): void,
log(message?: any, ...optionalParams: any[]): void,
error(message?: any, ...optionalParams: any[]): void,
warn(message?: any, ...optionalParams: any[]): void,
group(groupTitle?: string, ...optionalParams: any[]): void,
groupCollapsed(groupTitle?: string, ...optionalParams: any[]): void,
groupEnd(): void,
time(timerName?: string): void,
timeEnd(timerName?: string): void,
timeStamp(timerName?: string): void,
};
info(message?: any, ...optionalParams: any[]): void;
log(message?: any, ...optionalParams: any[]): void;
error(message?: any, ...optionalParams: any[]): void;
warn(message?: any, ...optionalParams: any[]): void;
group(groupTitle?: string, ...optionalParams: any[]): void;
groupCollapsed(groupTitle?: string, ...optionalParams: any[]): void;
groupEnd(): void;
time(timerName?: string): void;
timeEnd(timerName?: string): void;
timeStamp(timerName?: string): void;
};

View File

@@ -138,13 +138,14 @@ const CalendarUtils = {
return CalendarUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
};
Log.debug("there are " + Object.entries(data).length + " calendar entries");
Log.debug("There are " + Object.entries(data).length + " calendar entries.");
Object.entries(data).forEach(([key, event]) => {
Log.debug("Processing entry...");
const now = new Date();
const today = moment().startOf("day").toDate();
const future = moment().startOf("day").add(config.maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
let past = today;
Log.debug("have entries ");
if (config.includePastEvents) {
past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
}
@@ -159,10 +160,10 @@ const CalendarUtils = {
}
if (event.type === "VEVENT") {
Log.debug("\nEvent: " + JSON.stringify(event));
let startDate = eventDate(event, "start");
let endDate;
Log.debug("\nevent=" + JSON.stringify(event));
if (typeof event.end !== "undefined") {
endDate = eventDate(event, "end");
} else if (typeof event.duration !== "undefined") {
@@ -176,16 +177,21 @@ const CalendarUtils = {
}
}
Log.debug(" start=" + startDate.toDate() + " end=" + endDate.toDate());
Log.debug("startDate (local): " + startDate.toDate());
Log.debug("endDate (local): " + endDate.toDate());
// calculate the duration of the event for use with recurring events.
// Calculate the duration of the event for use with recurring events.
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
Log.debug("duration: " + duration);
// FIXME: Since the parsed json object from node-ical comes with time information
// this check could be removed (?)
if (event.start.length === 8) {
startDate = startDate.startOf("day");
}
const title = CalendarUtils.getTitleFromEvent(event);
Log.debug("title: " + title);
let excluded = false,
dateFilter = null;
@@ -260,9 +266,13 @@ const CalendarUtils = {
let pastLocal = 0;
let futureLocal = 0;
if (CalendarUtils.isFullDayEvent(event)) {
Log.debug("fullday");
// if full day event, only use the date part of the ranges
pastLocal = pastMoment.toDate();
futureLocal = futureMoment.toDate();
Log.debug("pastLocal: " + pastLocal);
Log.debug("futureLocal: " + futureLocal);
} else {
// if we want past events
if (config.includePastEvents) {
@@ -274,9 +284,9 @@ const CalendarUtils = {
}
futureLocal = futureMoment.toDate(); // future
}
Log.debug(" between=" + pastLocal + " to " + futureLocal);
Log.debug("Search for recurring events between: " + pastLocal + " and " + futureLocal);
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
Log.debug("title=" + event.summary + " dates=" + JSON.stringify(dates));
Log.debug("Title: " + event.summary + ", with dates: " + JSON.stringify(dates));
// The "dates" array contains the set of dates within our desired date range range that are valid
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
// had its date changed from outside the range to inside the range. For the time being,
@@ -284,6 +294,7 @@ const CalendarUtils = {
// because the logic below will filter out any recurrences that don't actually belong within
// our display range.
// Would be great if there was a better way to handle this.
Log.debug("event.recurrences: " + event.recurrences);
if (event.recurrences !== undefined) {
for (let r in event.recurrences) {
// Only add dates that weren't already in the range we added from the rrule so that
@@ -296,38 +307,42 @@ const CalendarUtils = {
// Loop through the set of date entries to see which recurrences should be added to our event list.
for (let d in dates) {
let date = dates[d];
// ical.js started returning recurrences and exdates as ISOStrings without time information.
// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same
// (see https://github.com/peterbraden/ical.js/pull/84 )
// Remove the time information of each date by using its substring, using the following method:
// .toISOString().substring(0,10).
// since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
// (see https://momentjs.com/docs/#/displaying/as-iso-string/).
const dateKey = date.toISOString().substring(0, 10);
let curEvent = event;
let showRecurrence = true;
// get the offset of today where we are processing
// this will be the correction we need to apply
// Get the offset of today where we are processing
// This will be the correction, we need to apply.
let nowOffset = new Date().getTimezoneOffset();
// for full day events, the time might be off from RRULE/Luxon problem
// get time zone offset of the rule calculated event
// For full day events, the time might be off from RRULE/Luxon problem
// Get time zone offset of the rule calculated event
let dateoffset = date.getTimezoneOffset();
// reduce the time by the offset
// Reduce the time by the following offset.
Log.debug(" recurring date is " + date + " offset is " + dateoffset);
let dh = moment(date).format("HH");
Log.debug(" recurring date is " + date + " offset is " + dateoffset / 60 + " Hour is " + dh);
if (CalendarUtils.isFullDayEvent(event)) {
Log.debug("fullday");
// if the offset is negative, east of GMT where the problem is
Log.debug("Fullday");
// If the offset is negative (east of GMT), where the problem is
if (dateoffset < 0) {
// if the date hour is less than the offset
if (dh < Math.abs(dateoffset / 60)) {
// reduce the time by the offset
Log.debug(" recurring date is " + date + " offset is " + dateoffset);
// apply the correction to the date/time to get it UTC relative
date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
Log.debug("new recurring date1 is " + date);
}
// Remove the offset, independently of the comparison between the date hour and the offset,
// since in the case that *date houre < offset*, the *new Date* command will handle this by
// representing the day before.
// Reduce the time by the offset:
// Apply the correction to the date/time to get it UTC relative
date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
Log.debug("new recurring date1 is " + date);
} else {
// if the timezones are the same, correct date if needed
if (event.start.tz === moment.tz.guess()) {
@@ -348,9 +363,8 @@ const CalendarUtils = {
if (dateoffset < 0) {
// if the date hour is less than the offset
if (dh < Math.abs(dateoffset / 60)) {
// reduce the time by the offset
Log.debug(" recurring date is " + date + " offset is " + dateoffset);
// apply the correction to the date/time to get it UTC relative
// Reduce the time by the offset:
// Apply the correction to the date/time to get it UTC relative
date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
@@ -373,6 +387,7 @@ const CalendarUtils = {
}
}
startDate = moment(date);
Log.debug("Corrected startDate (local): " + startDate.toDate());
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date);
@@ -388,7 +403,7 @@ const CalendarUtils = {
// This date is an exception date, which means we should skip it in the recurrence pattern.
showRecurrence = false;
}
Log.debug("duration=" + duration);
Log.debug("duration: " + duration);
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
if (startDate.format("x") === endDate.format("x")) {
@@ -408,7 +423,7 @@ const CalendarUtils = {
}
if (showRecurrence === true) {
Log.debug("saving event =" + description);
Log.debug("saving event: " + description);
addedEvents++;
newEvents.push({
title: recurrenceTitle,
@@ -424,7 +439,7 @@ const CalendarUtils = {
});
}
}
// end recurring event parsing
// End recurring event parsing.
} else {
// Single event.
const fullDayEvent = isFacebookBirthday ? true : CalendarUtils.isFullDayEvent(event);

View File

@@ -12,11 +12,14 @@ Module.register("clock", {
displayType: "digital", // options: digital, analog, both
timeFormat: config.timeFormat,
timezone: null,
displaySeconds: true,
showPeriod: true,
showPeriodUpper: false,
clockBold: false,
showDate: true,
showTime: true,
showWeek: false,
dateFormat: "dddd, LL",
@@ -24,9 +27,8 @@ Module.register("clock", {
analogSize: "200px",
analogFace: "simple", // options: 'none', 'simple', 'face-###' (where ### is 001 to 012 inclusive)
analogPlacement: "bottom", // options: 'top', 'bottom', 'left', 'right'
analogShowDate: "top", // options: false, 'top', or 'bottom'
analogShowDate: "top", // OBSOLETE, can be replaced with analogPlacement and showTime, options: false, 'top', or 'bottom'
secondsColor: "#888888",
timezone: null,
showSunTimes: false,
showMoonTimes: false,
@@ -89,11 +91,20 @@ Module.register("clock", {
// Override dom generator.
getDom: function () {
const wrapper = document.createElement("div");
wrapper.classList.add("clockGrid");
/************************************
* Create wrappers for analog and digital clock
*/
const analogWrapper = document.createElement("div");
analogWrapper.className = "clockCircle";
const digitalWrapper = document.createElement("div");
digitalWrapper.className = "digital";
digitalWrapper.style.gridArea = "center";
/************************************
* Create wrappers for DIGITAL clock
*/
const dateWrapper = document.createElement("div");
const timeWrapper = document.createElement("div");
const secondsWrapper = document.createElement("sup");
@@ -101,10 +112,11 @@ Module.register("clock", {
const sunWrapper = document.createElement("div");
const moonWrapper = document.createElement("div");
const weekWrapper = document.createElement("div");
// Style Wrappers
dateWrapper.className = "date normal medium";
timeWrapper.className = "time bright large light";
secondsWrapper.className = "dimmed";
secondsWrapper.className = "seconds dimmed";
sunWrapper.className = "sun dimmed small";
moonWrapper.className = "moon dimmed small";
weekWrapper.className = "week dimmed medium";
@@ -124,7 +136,7 @@ Module.register("clock", {
hourSymbol = "h";
}
if (this.config.clockBold === true) {
if (this.config.clockBold) {
timeString = now.format(hourSymbol + '[<span class="bold">]mm[</span>]');
} else {
timeString = now.format(hourSymbol + ":mm");
@@ -132,22 +144,24 @@ Module.register("clock", {
if (this.config.showDate) {
dateWrapper.innerHTML = now.format(this.config.dateFormat);
digitalWrapper.appendChild(dateWrapper);
}
if (this.config.showWeek) {
weekWrapper.innerHTML = this.translate("WEEK", { weekNumber: now.week() });
}
timeWrapper.innerHTML = timeString;
secondsWrapper.innerHTML = now.format("ss");
if (this.config.showPeriodUpper) {
periodWrapper.innerHTML = now.format("A");
} else {
periodWrapper.innerHTML = now.format("a");
}
if (this.config.displaySeconds) {
timeWrapper.appendChild(secondsWrapper);
}
if (this.config.showPeriod && this.config.timeFormat !== 24) {
timeWrapper.appendChild(periodWrapper);
if (this.config.displayType !== "analog" && this.config.showTime) {
timeWrapper.innerHTML = timeString;
secondsWrapper.innerHTML = now.format("ss");
if (this.config.showPeriodUpper) {
periodWrapper.innerHTML = now.format("A");
} else {
periodWrapper.innerHTML = now.format("a");
}
if (this.config.displaySeconds) {
timeWrapper.appendChild(secondsWrapper);
}
if (this.config.showPeriod && this.config.timeFormat !== 24) {
timeWrapper.appendChild(periodWrapper);
}
digitalWrapper.appendChild(timeWrapper);
}
/**
@@ -165,6 +179,9 @@ Module.register("clock", {
return moment(time).format(formatString);
}
/****************************************************************
* Create wrappers for Sun Times, only if specified in config
*/
if (this.config.showSunTimes) {
const sunTimes = SunCalc.getTimes(now, this.config.lat, this.config.lon);
const isVisible = now.isBetween(sunTimes.sunrise, sunTimes.sunset);
@@ -191,7 +208,12 @@ Module.register("clock", {
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i> ' +
formatTime(this.config, sunTimes.sunset) +
"</span>";
digitalWrapper.appendChild(sunWrapper);
}
/****************************************************************
* Create wrappers for Moon Times, only if specified in config
*/
if (this.config.showMoonTimes) {
const moonIllumination = SunCalc.getMoonIllumination(now.toDate());
const moonTimes = SunCalc.getMoonTimes(now, this.config.lat, this.config.lon);
@@ -217,13 +239,17 @@ Module.register("clock", {
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i> ' +
(moonSet ? formatTime(this.config, moonSet) : "...") +
"</span>";
digitalWrapper.appendChild(moonWrapper);
}
if (this.config.showWeek) {
weekWrapper.innerHTML = this.translate("WEEK", { weekNumber: now.week() });
digitalWrapper.appendChild(weekWrapper);
}
/****************************************************************
* Create wrappers for ANALOG clock, only if specified in config
*/
const clockCircle = document.createElement("div");
if (this.config.displayType !== "digital") {
// If it isn't 'digital', then an 'analog' clock was also requested
@@ -236,19 +262,18 @@ Module.register("clock", {
hour = ((now.hours() % 12) / 12) * 360 + 90 + minute / 12;
// Create wrappers
clockCircle.className = "clockCircle";
clockCircle.style.width = this.config.analogSize;
clockCircle.style.height = this.config.analogSize;
analogWrapper.style.width = this.config.analogSize;
analogWrapper.style.height = this.config.analogSize;
if (this.config.analogFace !== "" && this.config.analogFace !== "simple" && this.config.analogFace !== "none") {
clockCircle.style.background = "url(" + this.data.path + "faces/" + this.config.analogFace + ".svg)";
clockCircle.style.backgroundSize = "100%";
analogWrapper.style.background = "url(" + this.data.path + "faces/" + this.config.analogFace + ".svg)";
analogWrapper.style.backgroundSize = "100%";
// The following line solves issue: https://github.com/MichMich/MagicMirror/issues/611
// clockCircle.style.border = "1px solid black";
clockCircle.style.border = "rgba(0, 0, 0, 0.1)"; //Updated fix for Issue 611 where non-black backgrounds are used
// analogWrapper.style.border = "1px solid black";
analogWrapper.style.border = "rgba(0, 0, 0, 0.1)"; //Updated fix for Issue 611 where non-black backgrounds are used
} else if (this.config.analogFace !== "none") {
clockCircle.style.border = "2px solid white";
analogWrapper.style.border = "2px solid white";
}
const clockFace = document.createElement("div");
clockFace.className = "clockFace";
@@ -274,84 +299,28 @@ Module.register("clock", {
clockSecond.style.backgroundColor = this.config.secondsColor;
clockFace.appendChild(clockSecond);
}
clockCircle.appendChild(clockFace);
analogWrapper.appendChild(clockFace);
}
/*******************************************
* Combine wrappers, check for .displayType
* Update placement, respect old analogShowDate even if its not needed anymore
*/
if (this.config.displayType === "digital") {
// Display only a digital clock
wrapper.appendChild(dateWrapper);
wrapper.appendChild(timeWrapper);
wrapper.appendChild(sunWrapper);
wrapper.appendChild(moonWrapper);
wrapper.appendChild(weekWrapper);
} else if (this.config.displayType === "analog") {
if (this.config.displayType === "analog") {
// Display only an analog clock
if (this.config.showWeek) {
weekWrapper.style.paddingBottom = "15px";
} else {
dateWrapper.style.paddingBottom = "15px";
}
if (this.config.analogShowDate === "top") {
wrapper.appendChild(dateWrapper);
wrapper.appendChild(weekWrapper);
wrapper.appendChild(clockCircle);
wrapper.classList.add("clockGrid--bottom");
} else if (this.config.analogShowDate === "bottom") {
wrapper.appendChild(clockCircle);
wrapper.appendChild(dateWrapper);
wrapper.appendChild(weekWrapper);
wrapper.classList.add("clockGrid--top");
} else {
wrapper.appendChild(clockCircle);
}
} else {
// Both clocks have been configured, check position
const placement = this.config.analogPlacement;
const analogWrapper = document.createElement("div");
analogWrapper.id = "analog";
analogWrapper.style.cssFloat = "none";
analogWrapper.appendChild(clockCircle);
const digitalWrapper = document.createElement("div");
digitalWrapper.id = "digital";
digitalWrapper.style.cssFloat = "none";
digitalWrapper.appendChild(dateWrapper);
digitalWrapper.appendChild(timeWrapper);
digitalWrapper.appendChild(sunWrapper);
digitalWrapper.appendChild(moonWrapper);
digitalWrapper.appendChild(weekWrapper);
const appendClocks = (condition, pos1, pos2) => {
const padding = [0, 0, 0, 0];
padding[placement === condition ? pos1 : pos2] = "20px";
analogWrapper.style.padding = padding.join(" ");
if (placement === condition) {
wrapper.appendChild(analogWrapper);
wrapper.appendChild(digitalWrapper);
} else {
wrapper.appendChild(digitalWrapper);
wrapper.appendChild(analogWrapper);
}
};
if (placement === "left" || placement === "right") {
digitalWrapper.style.display = "inline-block";
digitalWrapper.style.verticalAlign = "top";
analogWrapper.style.display = "inline-block";
appendClocks("left", 1, 3);
} else {
digitalWrapper.style.textAlign = "center";
appendClocks("top", 2, 0);
//analogWrapper.style.gridArea = "center";
}
} else if (this.config.displayType === "both") {
wrapper.classList.add("clockGrid--" + this.config.analogPlacement);
}
wrapper.appendChild(analogWrapper);
wrapper.appendChild(digitalWrapper);
// Return the wrapper to the dom.
return wrapper;
}

View File

@@ -1,5 +1,26 @@
.clockGrid {
display: inline-flex;
gap: 15px;
}
.clockGrid--left {
flex-direction: row;
}
.clockGrid--right {
flex-direction: row-reverse;
}
.clockGrid--top {
flex-direction: column;
}
.clockGrid--bottom {
flex-direction: column-reverse;
}
.clockCircle {
margin: 0 auto;
place-self: center;
position: relative;
border-radius: 50%;
background-size: 100%;

View File

@@ -1,3 +1,5 @@
/* eslint-disable */
/* Magic Mirror
* Module: CurrentWeather
*

View File

@@ -0,0 +1,177 @@
const util = require("util");
const exec = util.promisify(require("child_process").exec);
const fs = require("fs");
const path = require("path");
const Log = require("logger");
class gitHelper {
constructor() {
this.gitRepos = [];
this.baseDir = path.normalize(__dirname + "/../../../");
}
getRefRegex(branch) {
return new RegExp("s*([a-z,0-9]+[.][.][a-z,0-9]+) " + branch, "g");
}
async execShell(command) {
let res = { stdout: "", stderr: "" };
const { stdout, stderr } = await exec(command);
res.stdout = stdout;
res.stderr = stderr;
return res;
}
async isGitRepo(moduleFolder) {
let res = await this.execShell("cd " + moduleFolder + " && git remote -v");
if (res.stderr) {
Log.error("Failed to fetch git data for " + moduleFolder + ": " + res.stderr);
return false;
} else {
return true;
}
}
add(moduleName) {
let moduleFolder = this.baseDir;
if (moduleName !== "default") {
moduleFolder = moduleFolder + "modules/" + moduleName;
}
try {
Log.info("Checking git for module: " + moduleName);
// Throws error if file doesn't exist
fs.statSync(path.join(moduleFolder, ".git"));
// Fetch the git or throw error if no remotes
if (this.isGitRepo(moduleFolder)) {
// Folder has .git and has at least one git remote, watch this folder
this.gitRepos.unshift({ module: moduleName, folder: moduleFolder });
}
} catch (err) {
// Error when directory .git doesn't exist or doesn't have any remotes
// This module is not managed with git, skip
}
}
async getStatusInfo(repo) {
let gitInfo = {
module: repo.module,
// commits behind:
behind: 0,
// branch name:
current: "",
// current hash:
hash: "",
// remote branch:
tracking: "",
isBehindInStatus: false
};
let res;
if (repo.module === "default") {
// the hash is only needed for the mm repo
res = await this.execShell("cd " + repo.folder + " && git rev-parse HEAD");
if (res.stderr) {
Log.error("Failed to get current commit hash for " + repo.module + ": " + res.stderr);
}
gitInfo.hash = res.stdout;
}
if (repo.res) {
// mocking
res = repo.res;
} else {
res = await this.execShell("cd " + repo.folder + " && git status -sb");
}
if (res.stderr) {
Log.error("Failed to get git status for " + repo.module + ": " + res.stderr);
// exit without git status info
return;
}
// only the first line of stdout is evaluated
let status = res.stdout.split("\n")[0];
// examples for status:
// ## develop...origin/develop
// ## master...origin/master [behind 8]
status = status.match(/(?![.#])([^.]*)/g);
// examples for status:
// [ ' develop', 'origin/develop', '' ]
// [ ' master', 'origin/master [behind 8]', '' ]
gitInfo.current = status[0].trim();
status = status[1].split(" ");
// examples for status:
// [ 'origin/develop' ]
// [ 'origin/master', '[behind', '8]' ]
gitInfo.tracking = status[0].trim();
if (status[2]) {
// 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.isBehindInStatus = true;
}
return gitInfo;
}
async getRepoInfo(repo) {
let gitInfo;
if (repo.gitInfo) {
// mocking
gitInfo = repo.gitInfo;
} else {
gitInfo = await this.getStatusInfo(repo);
}
if (!gitInfo) {
return;
}
if (gitInfo.isBehindInStatus) {
return gitInfo;
}
let res;
if (repo.res) {
// mocking
res = repo.res;
} else {
res = await this.execShell("cd " + repo.folder + " && git fetch --dry-run");
}
// example output:
// From https://github.com/MichMich/MagicMirror
// e40ddd4..06389e3 develop -> origin/develop
// here the result is in stderr (this is a git default, don't ask why ...)
const matches = res.stderr.match(this.getRefRegex(gitInfo.current));
if (!matches || !matches[0]) {
// no refs found, nothing to do
return;
}
// get behind with refs
try {
res = await this.execShell("cd " + repo.folder + " && git rev-list --ancestry-path --count " + matches[0]);
gitInfo.behind = parseInt(res.stdout);
return gitInfo;
} catch (err) {
Log.error("Failed to get git revisions for " + repo.module + ": " + err);
}
}
async getStatus() {
const gitResultList = [];
for (let repo of this.gitRepos) {
const gitInfo = await this.getStatusInfo(repo);
if (gitInfo) {
gitResultList.unshift(gitInfo);
}
}
return gitResultList;
}
async getRepos() {
const gitResultList = [];
for (let repo of this.gitRepos) {
const gitInfo = await this.getRepoInfo(repo);
if (gitInfo) {
gitResultList.unshift(gitInfo);
}
}
return gitResultList;
}
}
module.exports.gitHelper = gitHelper;

View File

@@ -1,9 +1,5 @@
const SimpleGit = require("simple-git");
const simpleGits = [];
const fs = require("fs");
const path = require("path");
const GitHelper = require(__dirname + "/git_helper.js");
const defaultModules = require(__dirname + "/../defaultmodules.js");
const Log = require("logger");
const NodeHelper = require("node_helper");
module.exports = NodeHelper.create({
@@ -12,32 +8,19 @@ module.exports = NodeHelper.create({
updateTimer: null,
updateProcessStarted: false,
gitHelper: new GitHelper.gitHelper(),
start: function () {},
configureModules: async function (modules) {
// Push MagicMirror itself , biggest chance it'll show up last in UI and isn't overwritten
// others will be added in front
// this method returns promises so we can't wait for every one to resolve before continuing
simpleGits.push({ module: "default", git: this.createGit(path.normalize(__dirname + "/../../../")) });
this.gitHelper.add("default");
for (let moduleName in modules) {
if (!this.ignoreUpdateChecking(moduleName)) {
// Default modules are included in the main MagicMirror repo
let moduleFolder = path.normalize(__dirname + "/../../" + moduleName);
try {
Log.info("Checking git for module: " + moduleName);
// Throws error if file doesn't exist
fs.statSync(path.join(moduleFolder, ".git"));
// Fetch the git or throw error if no remotes
let git = await this.resolveRemote(moduleFolder);
// Folder has .git and has at least one git remote, watch this folder
simpleGits.unshift({ module: moduleName, git: git });
} catch (err) {
// Error when directory .git doesn't exist or doesn't have any remotes
// This module is not managed with git, skip
continue;
}
this.gitHelper.add(moduleName);
}
}
},
@@ -54,35 +37,9 @@ module.exports = NodeHelper.create({
}
},
resolveRemote: async function (moduleFolder) {
let git = this.createGit(moduleFolder);
let remotes = await git.getRemotes(true);
if (remotes.length < 1 || remotes[0].name.length < 1) {
throw new Error("No valid remote for folder " + moduleFolder);
}
return git;
},
performFetch: async function () {
for (let sg of simpleGits) {
try {
let fetchData = await sg.git.fetch(["--dry-run"]).status();
let logData = await sg.git.log({ "-1": null });
if (logData.latest && "hash" in logData.latest) {
this.sendSocketNotification("STATUS", {
module: sg.module,
behind: fetchData.behind,
current: fetchData.current,
hash: logData.latest.hash,
tracking: fetchData.tracking
});
}
} catch (err) {
Log.error("Failed to fetch git data for " + sg.module + ": " + err);
}
for (let gitInfo of await this.gitHelper.getRepos()) {
this.sendSocketNotification("STATUS", gitInfo);
}
this.scheduleNextFetch(this.config.updateInterval);
@@ -100,10 +57,6 @@ module.exports = NodeHelper.create({
}, delay);
},
createGit: function (folder) {
return SimpleGit({ baseDir: folder, timeout: { block: this.config.timeout } });
},
ignoreUpdateChecking: function (moduleName) {
// Should not check for updates for default modules
if (defaultModules.indexOf(moduleName) >= 0) {

View File

@@ -2,6 +2,9 @@
{% set numSteps = forecast | calcNumSteps %}
{% set currentStep = 0 %}
<table class="{{ config.tableClass }}">
{% if config.ignoreToday %}
{% set forecast = forecast.splice(1) %}
{% endif %}
{% set forecast = forecast.slice(0, numSteps) %}
{% for f in forecast %}
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
@@ -20,10 +23,10 @@
{{ f.minTemperature | roundValue | unit("temperature") | decimalSymbol }}
</td>
{% if config.showPrecipitationAmount %}
{% if f.precipitationUnits %}
{% if f.precipitationUnits %}
<td class="align-right bright precipitation">
{{ f.precipitation }}{{ f.precipitationUnits }}
</td>
</td>
{% else %}
<td class="align-right bright precipitation">
{{ f.precipitation | unit("precip") }}

View File

@@ -1,148 +1,3 @@
# MagicMirror² Weather Module Weather Provider Development Documentation
# Weather Module Weather Provider Development Documentation
This document describes the way to develop your own MagicMirror² weather module weather provider.
Table of Contents:
- The weather provider file: yourprovider.js
- [Weather provider methods to implement](#weather-provider-methods-to-implement)
- [Weather Provider instance methods](#weather-provider-instance-methods)
- [WeatherObject](#weatherobject)
---
## The weather provider file: yourprovider.js
This is the script in which the weather provider will be defined. In its most simple form, the weather provider must implement the following:
```javascript
WeatherProvider.register("yourprovider", {
providerName: "YourProvider",
fetchCurrentWeather() {},
fetchWeatherForecast() {}
});
```
### Weather provider methods to implement
#### `fetchCurrentWeather()`
This method is called when the weather module tries to fetch the current weather of your provider. The implementation of this method is required for current weather support.
The implementation can make use of the already implemented function `this.fetchData(url, method, data);`, which is returning a promise.
After the response is processed, the current weather information (as a [WeatherObject](#weatherobject)) needs to be set with `this.setCurrentWeather(currentWeather);`.
It will then automatically refresh the module DOM with the new data.
#### `fetchWeatherForecast()`
This method is called when the weather module tries to fetch the weather of your provider. The implementation of this method is required for forecast support.
The implementation can make use of the already implemented function `this.fetchData(url, method, data);`, which is returning a promise.
After the response is processed, the weather forecast information (as an array of [WeatherObject](#weatherobject)s) needs to be set with `this.setWeatherForecast(forecast);`.
It will then automatically refresh the module DOM with the new data.
#### `fetchWeatherHourly()`
This method is called when the weather module tries to fetch the weather of your provider. The implementation of this method is required for hourly support.
The implementation can make use of the already implemented function `this.fetchData(url, method, data);`, which is returning a promise.
After the response is processed, the hourly weather forecast information (as an array of [WeatherObject](#weatherobject)s) needs to be set with `this.setWeatherHourly(forecast);`.
It will then automatically refresh the module DOM with the new data.
### Weather Provider instance methods
#### `init()`
Called when a weather provider is initialized.
#### `setConfig(config)`
Called to set the config, this config is the same as the weather module's config.
#### `start()`
Called when the weather provider is about to start.
#### `currentWeather()`
This returns a WeatherDay object for the current weather.
#### `weatherForecast()`
This returns an array of WeatherDay objects for the weather forecast.
#### `weatherHourly()`
This returns an array of WeatherDay objects for the hourly weather forecast.
#### `fetchedLocation()`
This returns the name of the fetched location or an empty string.
#### `setCurrentWeather(currentWeatherObject)`
Set the currentWeather and notify the delegate that new information is available.
#### `setWeatherForecast(weatherForecastArray)`
Set the weatherForecastArray and notify the delegate that new information is available.
#### `setWeatherHourly(weatherHourlyArray)`
Set the weatherHourlyArray and notify the delegate that new information is available.
#### `setFetchedLocation(name)`
Set the fetched location name.
#### `updateAvailable()`
Notify the delegate that new weather is available.
#### `fetchData(url, method, data)`
A convenience function to make requests. It returns a promise.
### WeatherObject
| Property | Type | Value/Unit |
| -------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
| units | `string` | Gets initialized with the constructor. <br> Possible values: `metric`, `imperial` |
| tempUnits | `string` | Gets initialized with the constructor. <br> Possible values: `metric`, `imperial` |
| windUnits | `string` | Gets initialized with the constructor. <br> Possible values: `metric`, `imperial` |
| date | `object` | [Moment.js](https://momentjs.com/) object of the time/date. |
| windSpeed | `number` | Metric: `meter/second` <br> Imperial: `miles/hour` |
| windDirection | `number` | Direction of the wind in degrees. |
| sunrise | `object` | [Moment.js](https://momentjs.com/) object of sunrise. |
| sunset | `object` | [Moment.js](https://momentjs.com/) object of sunset. |
| temperature | `number` | Current temperature |
| minTemperature | `number` | Lowest temperature of the day. |
| maxTemperature | `number` | Highest temperature of the day. |
| weatherType | `string` | Icon name of the weather type. <br> Possible values: [WeatherIcons](https://www.npmjs.com/package/weathericons) |
| humidity | `number` | Percentage of humidity |
| rain | `number` | Metric: `millimeters` <br> Imperial: `inches` |
| snow | `number` | Metric: `millimeters` <br> Imperial: `inches` |
| precipitation | `number` | Metric: `millimeters` <br> Imperial: `inches` <br> UK Met Office provider: `percent` |
#### Current weather
For the current weather object the following properties are required:
- humidity
- sunrise
- sunset
- temperature
- units
- weatherType
- windDirection
- windSpeed
#### Weather forecast
For the forecast weather object the following properties are required:
- date
- maxTemperature
- minTemperature
- rain
- units
- weatherType
For how to develop your own weather provider, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/development/weather-provider.html).

View File

@@ -8,7 +8,8 @@
* MIT Licensed
*
* This class is a provider for Dark Sky.
* Note that the Dark Sky API does not provide rainfall. Instead it provides snowfall and precipitation probability
* Note that the Dark Sky API does not provide rainfall. Instead it provides
* snowfall and precipitation probability
*/
WeatherProvider.register("darksky", {
// Set the name of the provider.
@@ -98,7 +99,8 @@ WeatherProvider.register("darksky", {
weather.snow = 0;
// The API will return centimeters if units is 'si' and will return inches for 'us'
// Note that the Dark Sky API does not provide rainfall. Instead it provides snowfall and precipitation probability
// Note that the Dark Sky API does not provide rainfall.
// Instead it provides snowfall and precipitation probability
if (forecast.hasOwnProperty("precipAccumulation")) {
if (this.config.units === "imperial" && !isNaN(forecast.precipAccumulation)) {
weather.snow = forecast.precipAccumulation;

View File

@@ -134,7 +134,7 @@ WeatherProvider.register("envcanada", {
//
fetchData: function (url, method = "GET", data = null) {
return new Promise(function (resolve, reject) {
var request = new XMLHttpRequest();
const request = new XMLHttpRequest();
request.open(method, url, true);
request.onreadystatechange = function () {
if (this.readyState === 4) {
@@ -164,9 +164,7 @@ WeatherProvider.register("envcanada", {
// CORS errors when accessing EC
//
getUrl() {
var path = "https://thingproxy.freeboard.io/fetch/https://dd.weather.gc.ca/citypage_weather/xml/" + this.config.provCode + "/" + this.config.siteCode + "_e.xml";
return path;
return "https://thingproxy.freeboard.io/fetch/https://dd.weather.gc.ca/citypage_weather/xml/" + this.config.provCode + "/" + this.config.siteCode + "_e.xml";
},
//
@@ -232,7 +230,7 @@ WeatherProvider.register("envcanada", {
// Capture the sunrise and sunset values from EC data
//
var sunList = ECdoc.querySelectorAll("siteData riseSet dateTime");
const sunList = ECdoc.querySelectorAll("siteData riseSet dateTime");
currentWeather.sunrise = moment(sunList[1].querySelector("timeStamp").textContent, "YYYYMMDDhhmmss");
currentWeather.sunset = moment(sunList[3].querySelector("timeStamp").textContent, "YYYYMMDDhhmmss");
@@ -249,14 +247,14 @@ WeatherProvider.register("envcanada", {
const days = [];
var weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
var foreBaseDates = ECdoc.querySelectorAll("siteData forecastGroup dateTime");
var baseDate = foreBaseDates[1].querySelector("timeStamp").textContent;
const foreBaseDates = ECdoc.querySelectorAll("siteData forecastGroup dateTime");
const baseDate = foreBaseDates[1].querySelector("timeStamp").textContent;
weather.date = moment(baseDate, "YYYYMMDDhhmmss");
var foreGroup = ECdoc.querySelectorAll("siteData forecastGroup forecast");
const foreGroup = ECdoc.querySelectorAll("siteData forecastGroup forecast");
// For simplicity, we will only accumulate precipitation and will not try to break out
// rain vs snow accumulations
@@ -288,9 +286,9 @@ WeatherProvider.register("envcanada", {
// where the next day's (aka Tomorrow's) forecast is located in the forecast array.
//
var nextDay = 0;
var lastDay = 0;
var currentTemp = ECdoc.querySelector("siteData currentConditions temperature").textContent;
let nextDay = 0;
let lastDay = 0;
const currentTemp = ECdoc.querySelector("siteData currentConditions temperature").textContent;
//
// If the first Element is Current Today, look at Current Today and Current Tonight for the current day.
@@ -356,10 +354,10 @@ WeatherProvider.register("envcanada", {
// iteration looking at the current Element and the next Element.
//
var lastDate = moment(baseDate, "YYYYMMDDhhmmss");
let lastDate = moment(baseDate, "YYYYMMDDhhmmss");
for (var stepDay = nextDay; stepDay < lastDay; stepDay += 2) {
var weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
for (let stepDay = nextDay; stepDay < lastDay; stepDay += 2) {
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
// Add 1 to the date to reflect the current forecast day we are building
@@ -402,23 +400,23 @@ WeatherProvider.register("envcanada", {
// Get local timezone UTC offset so that each hourly time can be calculated properly
var baseHours = ECdoc.querySelectorAll("siteData hourlyForecastGroup dateTime");
var hourOffset = baseHours[1].getAttribute("UTCOffset");
const baseHours = ECdoc.querySelectorAll("siteData hourlyForecastGroup dateTime");
const hourOffset = baseHours[1].getAttribute("UTCOffset");
//
// The EC hourly forecast is held in a 24-element array - Elements 0 to 23 - with Element 0 holding
// the forecast for the next 'on the hour' timeslot. This means the array is a rolling 24 hours.
//
var hourGroup = ECdoc.querySelectorAll("siteData hourlyForecastGroup hourlyForecast");
const hourGroup = ECdoc.querySelectorAll("siteData hourlyForecastGroup hourlyForecast");
for (var stepHour = 0; stepHour < 24; stepHour += 1) {
var weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
for (let stepHour = 0; stepHour < 24; stepHour += 1) {
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
// Determine local time by applying UTC offset to the forecast timestamp
var foreTime = moment(hourGroup[stepHour].getAttribute("dateTimeUTC"), "YYYYMMDDhhmmss");
var currTime = foreTime.add(hourOffset, "hours");
const foreTime = moment(hourGroup[stepHour].getAttribute("dateTimeUTC"), "YYYYMMDDhhmmss");
const currTime = foreTime.add(hourOffset, "hours");
weather.date = moment(currTime, "X");
// Capture the temperature
@@ -427,7 +425,7 @@ WeatherProvider.register("envcanada", {
// Capture Likelihood of Precipitation (LOP) and unit-of-measure values
var precipLOP = hourGroup[stepHour].querySelector("lop").textContent * 1.0;
const precipLOP = hourGroup[stepHour].querySelector("lop").textContent * 1.0;
if (precipLOP > 0) {
weather.precipitation = precipLOP;
@@ -453,9 +451,9 @@ WeatherProvider.register("envcanada", {
//
setMinMaxTemps(weather, foreGroup, today, fullDay, currentTemp) {
var todayTemp = foreGroup[today].querySelector("temperatures temperature").textContent;
const todayTemp = foreGroup[today].querySelector("temperatures temperature").textContent;
var todayClass = foreGroup[today].querySelector("temperatures temperature").getAttribute("class");
const todayClass = foreGroup[today].querySelector("temperatures temperature").getAttribute("class");
//
// The following logic is largely aimed at accommodating the Current day's forecast whereby we
@@ -500,9 +498,9 @@ WeatherProvider.register("envcanada", {
}
}
var nextTemp = foreGroup[today + 1].querySelector("temperatures temperature").textContent;
const nextTemp = foreGroup[today + 1].querySelector("temperatures temperature").textContent;
var nextClass = foreGroup[today + 1].querySelector("temperatures temperature").getAttribute("class");
const nextClass = foreGroup[today + 1].querySelector("temperatures temperature").getAttribute("class");
if (fullDay === true) {
if (nextClass === "low") {
@@ -513,8 +511,6 @@ WeatherProvider.register("envcanada", {
weather.maxTemperature = this.convertTemp(nextTemp);
}
}
return;
},
//
@@ -560,8 +556,6 @@ WeatherProvider.register("envcanada", {
weather.precipitation = foreGroup[today].querySelector("abbreviatedForecast pop").textContent;
weather.precipitationUnits = foreGroup[today].querySelector("abbreviatedForecast pop").getAttribute("units");
}
return;
},
//
@@ -577,6 +571,7 @@ WeatherProvider.register("envcanada", {
return temp;
}
},
//
// Convert km/h to mph
//

View File

@@ -18,10 +18,10 @@ WeatherProvider.register("openweathermap", {
defaults: {
apiVersion: "2.5",
apiBase: "https://api.openweathermap.org/data/",
weatherEndpoint: "",
weatherEndpoint: "", // can be "onecall", "forecast" or "weather" (for current)
locationID: false,
location: false,
lat: 0,
lat: 0, // the onecall endpoint needs lat / lon values, it doesn'T support the locationId
lon: 0,
apiKey: ""
},
@@ -89,7 +89,7 @@ WeatherProvider.register("openweathermap", {
/**
* Overrides method for setting config to check if endpoint is correct for hourly
*
* @param config
* @param {object} config The configuration object
*/
setConfig(config) {
this.config = config;

View File

@@ -1,4 +1,4 @@
/* global WeatherProvider, WeatherObject, SunCalc */
/* global WeatherProvider, WeatherObject */
/* Magic Mirror
* Module: Weather
@@ -7,9 +7,8 @@
* By BuXXi https://github.com/buxxi
* MIT Licensed
*
* This class is a provider for SMHI (Sweden only).
* Note that SMHI doesn't provide sunrise and sundown, use SunCalc to calculate it.
* Metric system is the only supported unit.
* This class is a provider for SMHI (Sweden only). Metric system is the only
* supported unit.
*/
WeatherProvider.register("smhi", {
providerName: "SMHI",
@@ -56,11 +55,11 @@ WeatherProvider.register("smhi", {
/**
* Overrides method for setting config with checks for the precipitationValue being unset or invalid
*
* @param config
* @param {object} config The configuration object
*/
setConfig(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);
config.precipitationValue = this.defaults.precipitationValue;
}
@@ -69,7 +68,8 @@ WeatherProvider.register("smhi", {
/**
* Of all the times returned find out which one is closest to the current time, should be the first if the data isn't old.
*
* @param times
* @param {object[]} times Array of time objects
* @returns {object} The weatherdata closest to the current time
*/
getClosestToCurrentTime(times) {
let now = moment();
@@ -85,6 +85,8 @@ WeatherProvider.register("smhi", {
/**
* Get the forecast url for the configured coordinates
*
* @returns {string} the url for the specified coordinates
*/
getURL() {
let lon = this.config.lon;
@@ -97,25 +99,25 @@ WeatherProvider.register("smhi", {
* The returned units is always in metric system.
* Requires coordinates to determine if its daytime or nighttime to know which icon to use and also to set sunrise and sunset.
*
* @param weatherData
* @param coordinates
* @param weatherData
* @param coordinates
* @param {object} weatherData Weatherdata to convert
* @param {object} coordinates Coordinates of the locations of the weather
* @returns {WeatherObject} The converted weatherdata at the specified location
*/
convertWeatherDataToObject(weatherData, coordinates) {
let currentWeather = new WeatherObject("metric", "metric", "metric"); //Weather data is only for Sweden and nobody in Sweden would use imperial
// Weather data is only for Sweden and nobody in Sweden would use imperial
let currentWeather = new WeatherObject("metric", "metric", "metric");
currentWeather.date = moment(weatherData.validTime);
let times = SunCalc.getTimes(currentWeather.date.toDate(), coordinates.lat, coordinates.lon);
currentWeather.sunrise = moment(times.sunrise, "X");
currentWeather.sunset = moment(times.sunset, "X");
currentWeather.updateSunTime(coordinates.lat, coordinates.lon);
currentWeather.humidity = this.paramValue(weatherData, "r");
currentWeather.temperature = this.paramValue(weatherData, "t");
currentWeather.windSpeed = this.paramValue(weatherData, "ws");
currentWeather.windDirection = this.paramValue(weatherData, "wd");
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), this.isDayTime(currentWeather));
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), currentWeather.isDayTime());
//Determine the precipitation amount and category and update the weatherObject with it, the valuetype to use can be configured or uses median as default.
// Determine the precipitation amount and category and update the
// weatherObject with it, the valuetype to use can be configured or uses
// median as default.
let precipitationValue = this.paramValue(weatherData, this.config.precipitationValue);
switch (this.paramValue(weatherData, "pcat")) {
// 0 = No precipitation
@@ -143,10 +145,9 @@ WeatherProvider.register("smhi", {
/**
* Takes all of the data points and converts it to one WeatherObject per day.
*
* @param allWeatherData
* @param coordinates
* @param allWeatherData
* @param coordinates
* @param {object[]} allWeatherData Array of weatherdata
* @param {object} coordinates Coordinates of the locations of the weather
* @returns {WeatherObject[]} Array of weatherobjects
*/
convertWeatherDataGroupedByDay(allWeatherData, coordinates) {
let currentWeather;
@@ -170,7 +171,7 @@ WeatherProvider.register("smhi", {
}
//Keep track of what icons has been used for each hour of daytime and use the middle one for the forecast
if (this.isDayTime(weatherObject)) {
if (weatherObject.isDayTime()) {
dayWeatherTypes.push(weatherObject.weatherType);
}
if (dayWeatherTypes.length > 0) {
@@ -191,37 +192,31 @@ WeatherProvider.register("smhi", {
},
/**
* Resolve coordinates from the response data (probably preferably to use this if it's not matching the config values exactly)
* Resolve coordinates from the response data (probably preferably to use
* this if it's not matching the config values exactly)
*
* @param data
* @param {object} data Response data from the weather service
* @returns {{lon, lat}} the lat/long coordinates of the data
*/
resolveCoordinates(data) {
return { lat: data.geometry.coordinates[0][1], lon: data.geometry.coordinates[0][0] };
},
/**
* Checks if the weatherObject is at dayTime.
*
* @param weatherObject
*/
isDayTime(weatherObject) {
return weatherObject.date.isBetween(weatherObject.sunrise, weatherObject.sunset, undefined, "[]");
},
/**
* The distance between the data points is increasing in the data the more distant the prediction is.
* Find these gaps and fill them with the previous hours data to make the data returned a complete set.
*
* @param data
* @param {object[]} data Response data from the weather service
* @returns {object[]} Given data with filled gaps
*/
fillInGaps(data) {
let result = [];
for (const i = 1; i < data.length; i++) {
for (let i = 1; i < data.length; i++) {
let to = moment(data[i].validTime);
let from = moment(data[i - 1].validTime);
let hours = moment.duration(to.diff(from)).asHours();
// For each hour add a datapoint but change the validTime
for (const j = 0; j < hours; j++) {
for (let j = 0; j < hours; j++) {
let current = Object.assign({}, data[i]);
current.validTime = from.clone().add(j, "hours").toISOString();
result.push(current);
@@ -231,84 +226,81 @@ WeatherProvider.register("smhi", {
},
/**
* Helper method to fetch a property from the returned data set.
* The returned values is an array with always one value in it.
* Helper method to get a property from the returned data set.
*
* @param currentWeatherData
* @param name
* @param currentWeatherData
* @param name
* @param {object} currentWeatherData Weatherdata to get from
* @param {string} name The name of the property
* @returns {*} The value of the property in the weatherdata
*/
paramValue(currentWeatherData, name) {
return currentWeatherData.parameters.filter((p) => p.name == name).flatMap((p) => p.values)[0];
return currentWeatherData.parameters.filter((p) => p.name === name).flatMap((p) => p.values)[0];
},
/**
* Map the icon value from SHMI 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.
* SHMI'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 input
* @param isDayTime
* @param input
* @param isDayTime
* @param {number} input The SMHI icon value
* @param {boolean} isDayTime True if the icon should be for daytime, false for nighttime
* @returns {string} The icon name for the MagicMirror
*/
convertWeatherType(input, isDayTime) {
switch (input) {
case 1:
return isDayTime ? "day-sunny" : "night-clear"; // Clear sky
case 2:
return isDayTime ? "day-sunny-overcast" : "night-partly-cloudy"; //Nearly clear sky
return isDayTime ? "day-sunny-overcast" : "night-partly-cloudy"; // Nearly clear sky
case 3:
return isDayTime ? "day-cloudy" : "night-cloudy"; //Variable cloudiness
return isDayTime ? "day-cloudy" : "night-cloudy"; // Variable cloudiness
case 4:
return isDayTime ? "day-cloudy" : "night-cloudy"; //Halfclear sky
return isDayTime ? "day-cloudy" : "night-cloudy"; // Halfclear sky
case 5:
return "cloudy"; //Cloudy sky
return "cloudy"; // Cloudy sky
case 6:
return "cloudy"; //Overcast
return "cloudy"; // Overcast
case 7:
return "fog"; //Fog
return "fog"; // Fog
case 8:
return "showers"; //Light rain showers
return "showers"; // Light rain showers
case 9:
return "showers"; //Moderate rain showers
return "showers"; // Moderate rain showers
case 10:
return "showers"; //Heavy rain showers
return "showers"; // Heavy rain showers
case 11:
return "thunderstorm"; //Thunderstorm
return "thunderstorm"; // Thunderstorm
case 12:
return "sleet"; //Light sleet showers
return "sleet"; // Light sleet showers
case 13:
return "sleet"; //Moderate sleet showers
return "sleet"; // Moderate sleet showers
case 14:
return "sleet"; //Heavy sleet showers
return "sleet"; // Heavy sleet showers
case 15:
return "snow"; //Light snow showers
return "snow"; // Light snow showers
case 16:
return "snow"; //Moderate snow showers
return "snow"; // Moderate snow showers
case 17:
return "snow"; //Heavy snow showers
return "snow"; // Heavy snow showers
case 18:
return "rain"; //Light rain
return "rain"; // Light rain
case 19:
return "rain"; //Moderate rain
return "rain"; // Moderate rain
case 20:
return "rain"; //Heavy rain
return "rain"; // Heavy rain
case 21:
return "thunderstorm"; //Thunder
return "thunderstorm"; // Thunder
case 22:
return "sleet"; // Light sleet
case 23:
return "sleet"; //Moderate sleet
return "sleet"; // Moderate sleet
case 24:
return "sleet"; // Heavy sleet
case 25:
return "snow"; // Light snowfall
case 26:
return "snow"; //Moderate snowfall
return "snow"; // Moderate snowfall
case 27:
return "snow"; //Heavy snowfall
return "snow"; // Heavy snowfall
default:
return "";
}

View File

@@ -1,4 +1,4 @@
/* global WeatherProvider, WeatherObject, SunCalc */
/* global WeatherProvider, WeatherObject */
/* Magic Mirror
* Module: Weather
@@ -116,9 +116,7 @@ WeatherProvider.register("ukmetoffice", {
}
// determine the sunrise/sunset times - not supplied in UK Met Office data
let times = this.calcAstroData(location);
currentWeather.sunrise = times[0];
currentWeather.sunset = times[1];
currentWeather.updateSunTime(location.lat, location.lon);
return currentWeather;
},
@@ -154,20 +152,6 @@ WeatherProvider.register("ukmetoffice", {
return days;
},
/*
* calculate the astronomical data
*/
calcAstroData(location) {
const sunTimes = [];
// determine the sunrise/sunset times
let times = SunCalc.getTimes(new Date(), location.lat, location.lon);
sunTimes.push(moment(times.sunrise, "X"));
sunTimes.push(moment(times.sunset, "X"));
return sunTimes;
},
/*
* Convert the Met Office icons to a more usable name.
*/
@@ -248,16 +232,16 @@ WeatherProvider.register("ukmetoffice", {
return windCardinals.hasOwnProperty(windDirection) ? windCardinals[windDirection] : null;
},
/*
/**
* Generates an url with api parameters based on the config.
*
* return String - URL params.
* @param {string} forecastType daily or 3hourly forecast
* @returns {string} url
*/
getParams(forecastType) {
let params = "?";
params += "res=" + forecastType;
params += "&key=" + this.config.apiKey;
return params;
}
});

View File

@@ -1,3 +1,5 @@
/* global WeatherProvider, WeatherObject */
/* Magic Mirror
* Module: Weather
*
@@ -11,9 +13,8 @@
* Hourly data for next 2 days ("hourly") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-hourly.pdf
* 3-hourly data for the next 7 days ("3hourly") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-3-hourly.pdf
* Daily data for the next 7 days ("daily") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-daily.pdf
*/
/* NOTES
*
* NOTES
* This provider requires longitude/latitude coordinates, rather than a location ID (as with the previous Met Office provider)
* Provide the following in your config.js file:
* weatherProvider: "ukmetofficedatahub",
@@ -69,13 +70,11 @@ WeatherProvider.register("ukmetofficedatahub", {
// For DataHub requests, the API key/secret are sent in the headers rather than as query strings.
// Headers defined according to Data Hub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
getHeaders() {
let headers = {
return {
accept: "application/json",
"x-ibm-client-id": this.config.apiKey,
"x-ibm-client-secret": this.config.apiSecret
};
return headers;
},
// Fetch data using supplied URL and request headers
@@ -91,7 +90,7 @@ WeatherProvider.register("ukmetofficedatahub", {
this.fetchWeather(this.getUrl("hourly"), this.getHeaders())
.then((data) => {
// Check data is useable
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.
// Maybe this needs a better check?
Log.error("Possibly bad current/hourly data?");
@@ -125,7 +124,7 @@ WeatherProvider.register("ukmetofficedatahub", {
let nowUtc = moment.utc();
// Find hour that contains the current time
for (hour in forecastDataHours) {
for (let hour in forecastDataHours) {
let forecastTime = moment.utc(forecastDataHours[hour].time);
if (nowUtc.isSameOrAfter(forecastTime) && nowUtc.isBefore(moment(forecastTime.add(1, "h")))) {
currentWeather.date = forecastTime;
@@ -148,11 +147,9 @@ WeatherProvider.register("ukmetofficedatahub", {
}
// Determine the sunrise/sunset times - (still) not supplied in UK Met Office data
// Passes {longitude, latitude, height} to calcAstroData
// Could just pass lat/long from this.config, but returned data from MO also contains elevation
let times = this.calcAstroData(currentWeatherData.features[0].geometry.coordinates);
currentWeather.sunrise = times[0];
currentWeather.sunset = times[1];
// Passes {longitude, latitude} to SunCalc, could pass height to, but
// SunCalc.getTimes doesnt take that into account
currentWeather.updateSunTime(this.config.lat, this.config.lon);
return currentWeather;
},
@@ -162,7 +159,7 @@ WeatherProvider.register("ukmetofficedatahub", {
this.fetchWeather(this.getUrl("daily"), this.getHeaders())
.then((data) => {
// Check data is useable
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.
// Maybe this needs a better check?
Log.error("Possibly bad forecast data?");
@@ -196,7 +193,7 @@ WeatherProvider.register("ukmetofficedatahub", {
let today = moment.utc().startOf("date");
// Go through each day in the forecasts
for (day in forecastDataDays) {
for (let day in forecastDataDays) {
const forecastWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
// Get date of forecast
@@ -221,7 +218,6 @@ WeatherProvider.register("ukmetofficedatahub", {
// Pass on full details so they can be used in custom templates
// Note the units of the supplied data when using this (see top of file)
forecastWeather.rawData = forecastDataDays[day];
dailyForecasts.push(forecastWeather);
@@ -236,18 +232,6 @@ WeatherProvider.register("ukmetofficedatahub", {
this.fetchedLocationName = name;
},
// Calculate sunrise/sunset times
calcAstroData(location) {
const sunTimes = [];
// Careful to pass values to SunCalc in correct order (latitude, longitude, elevation)
let times = SunCalc.getTimes(new Date(), location[1], location[0], location[2]);
sunTimes.push(moment(times.sunrise, "X"));
sunTimes.push(moment(times.sunset, "X"));
return sunTimes;
},
// Convert temperatures to Fahrenheit (from degrees C), if required
convertTemp(tempInC) {
return this.config.tempUnits === "imperial" ? (tempInC * 9) / 5 + 32 : tempInC;
@@ -258,11 +242,11 @@ WeatherProvider.register("ukmetofficedatahub", {
// 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") {
if (this.config.windUnits === "mps") {
return windInMpS;
}
if (this.config.windUnits == "kph" || this.config.windUnits == "metric" || this.config.useKmh) {
if (this.config.windUnits === "kph" || this.config.windUnits === "metric" || this.config.useKmh) {
return windInMpS * 3.6;
}

View File

@@ -7,7 +7,8 @@
* By Andrew Pometti
* MIT Licensed
*
* This class is a provider for Weatherbit, based on Nicholas Hubbard's class for Dark Sky & Vince Peri's class for Weather.gov.
* This class is a provider for Weatherbit, based on Nicholas Hubbard's class
* for Dark Sky & Vince Peri's class for Weather.gov.
*/
WeatherProvider.register("weatherbit", {
// Set the name of the provider.
@@ -89,7 +90,6 @@ WeatherProvider.register("weatherbit", {
currentWeather.windSpeed = parseFloat(currentWeatherData.data[0].wind_spd);
currentWeather.windDirection = currentWeatherData.data[0].wind_dir;
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.data[0].weather.icon);
Log.log("Wx Icon: " + currentWeatherData.data[0].weather.icon);
currentWeather.sunrise = moment(currentWeatherData.data[0].sunrise, "HH:mm").add(tzOffset, "m");
currentWeather.sunset = moment(currentWeatherData.data[0].sunset, "HH:mm").add(tzOffset, "m");

View File

@@ -1,4 +1,4 @@
/* global WeatherProvider, WeatherObject, SunCalc */
/* global WeatherProvider, WeatherObject */
/* Magic Mirror
* Module: Weather
@@ -21,7 +21,7 @@ WeatherProvider.register("weathergov", {
// Set the default config properties that is specific to this provider
defaults: {
apiBase: "https://api.weatherbit.io/v2.0",
apiBase: "https://api.weather.gov/points/",
weatherEndpoint: "/forecast",
lat: 0,
lon: 0
@@ -129,7 +129,12 @@ WeatherProvider.register("weathergov", {
.finally(() => {
// excellent, let's fetch some actual wx data
this.configURLs = true;
this.fetchCurrentWeather();
// handle 'forecast' config, fall back to 'current'
if (config.type === "forecast") {
this.fetchWeatherForecast();
} else {
this.fetchCurrentWeather();
}
});
},
@@ -153,18 +158,11 @@ WeatherProvider.register("weathergov", {
currentWeather.precipitation = this.convertLength(currentWeatherData.precipitationLastHour.value);
currentWeather.feelsLikeTemp = this.convertTemp(currentWeatherData.heatIndex.value);
let isDaytime = true;
if (currentWeatherData.icon.includes("day")) {
isDaytime = true;
} else {
isDaytime = false;
}
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.textDescription, isDaytime);
// determine the sunrise/sunset times - not supplied in weather.gov data
let times = this.calcAstroData(this.config.lat, this.config.lon);
currentWeather.sunrise = times[0];
currentWeather.sunset = times[1];
currentWeather.updateSunTime(this.config.lat, this.config.lon);
// update weatherType
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.textDescription, currentWeather.isDayTime());
return currentWeather;
},
@@ -267,20 +265,6 @@ WeatherProvider.register("weathergov", {
}
},
/*
* Calculate the astronomical data
*/
calcAstroData(lat, lon) {
const sunTimes = [];
// determine the sunrise/sunset times
let times = SunCalc.getTimes(new Date(), lat, lon);
sunTimes.push(moment(times.sunrise, "X"));
sunTimes.push(moment(times.sunset, "X"));
return sunTimes;
},
/*
* Convert the icons to a more usable name.
*/

View File

@@ -132,10 +132,6 @@ Module.register("weather", {
getTemplateData: function () {
const forecast = this.weatherProvider.weatherForecast();
if (this.config.ignoreToday) {
forecast.splice(0, 1);
}
return {
config: this.config,
current: this.weatherProvider.currentWeather(),

View File

@@ -1,3 +1,5 @@
/* global SunCalc */
/* Magic Mirror
* Module: Weather
*
@@ -10,6 +12,14 @@
* As soon as we start implementing the forecast, mode properties will be added.
*/
class 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) {
this.units = units;
this.tempUnits = tempUnits;
@@ -80,8 +90,7 @@ class WeatherObject {
}
kmhWindSpeed() {
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : (this.windSpeed * 60 * 60) / 1000;
return windInKmh;
return this.windUnits === "imperial" ? this.windSpeed * 1.609344 : (this.windSpeed * 60 * 60) / 1000;
}
nextSunAction() {
@@ -113,4 +122,33 @@ class WeatherObject {
return this.tempUnits === "imperial" ? feelsLike : ((feelsLike - 32) * 5) / 9;
}
/**
* Checks if the weatherObject is at dayTime.
*
* @returns {boolean} true if it is at dayTime
*/
isDayTime() {
return this.date.isBetween(this.sunrise, this.sunset, undefined, "[]");
}
/**
* Update the sunrise / sunset time depending on the location. This can be
* used if your provider doesnt provide that data by itself. Then SunCalc
* is used here to calculate them according to the location.
*
* @param {number} lat latitude
* @param {number} lon longitude
*/
updateSunTime(lat, lon) {
let now = !this.date ? new Date() : this.date.toDate();
let times = SunCalc.getTimes(now, lat, lon);
this.sunrise = moment(times.sunrise, "X");
this.sunset = moment(times.sunset, "X");
}
}
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = WeatherObject;
}

View File

@@ -1,3 +1,5 @@
/* eslint-disable */
/* Magic Mirror
* Module: WeatherForecast
*

4555
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "magicmirror",
"version": "2.16.0",
"version": "2.17.0",
"description": "The open source modular smart mirror platform.",
"main": "js/electron.js",
"scripts": {
@@ -12,16 +12,17 @@
"postinstall": "npm run install-fonts && echo \"MagicMirror installation finished successfully! \n\"",
"test": "NODE_ENV=test jest -i --forceExit",
"test:coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text jest -i --forceExit",
"test:electron": "NODE_ENV=test jest --selectProjects electron -i --forceExit",
"test:e2e": "NODE_ENV=test jest --selectProjects e2e -i --forceExit",
"test:unit": "NODE_ENV=test jest --selectProjects unit -i --forceExit",
"test:prettier": "prettier . --check",
"test:js": "eslint js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --quiet",
"test:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json",
"test:js": "eslint 'js/**/*.js' 'modules/default/**/*.js' 'clientonly/*.js' 'serveronly/*.js' 'translations/*.js' 'vendor/*.js' 'tests/**/*.js' 'config/*' --config .eslintrc.json",
"test:css": "stylelint 'css/main.css' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.css' --config .stylelintrc.json",
"test:calendar": "node ./modules/default/calendar/debug.js",
"config:check": "node js/check_config.js",
"lint:prettier": "prettier . --write",
"lint:js": "eslint js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --fix",
"lint:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json --fix",
"lint:js": "eslint 'js/**/*.js' 'modules/default/**/*.js' 'clientonly/*.js' 'serveronly/*.js' 'translations/*.js' 'vendor/*.js' 'tests/**/*.js' 'config/*' --config .eslintrc.json --fix",
"lint:css": "stylelint 'css/main.css' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.css' --config .stylelintrc.json --fix",
"lint:staged": "pretty-quick --staged",
"prepare": "[ -f node_modules/.bin/husky ] && husky install || echo no husky installed."
},
@@ -46,32 +47,33 @@
"homepage": "https://magicmirror.builders",
"devDependencies": {
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-jsdoc": "^35.4.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-jest": "^24.4.2",
"eslint-plugin-jsdoc": "^36.1.0",
"eslint-plugin-prettier": "^4.0.0",
"express-basic-auth": "^1.2.0",
"husky": "^6.0.0",
"jest": "27.0.5",
"jsdom": "^16.6.0",
"husky": "^7.0.2",
"jest": "^27.2.2",
"jsdom": "^17.0.0",
"lodash": "^4.17.21",
"nyc": "^15.1.0",
"prettier": "^2.3.1",
"prettier": "^2.4.1",
"pretty-quick": "^3.1.1",
"sinon": "^11.1.1",
"spectron": "^13.0.0",
"sinon": "^11.1.2",
"spectron": "^15.0.0",
"stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0",
"stylelint-prettier": "^1.2.0"
"stylelint-prettier": "^1.2.0",
"suncalc": "^1.8.0"
},
"optionalDependencies": {
"electron": "^11.4.9"
"electron": "^13.5.0"
},
"dependencies": {
"colors": "^1.4.0",
"console-stamp": "^3.0.2",
"digest-fetch": "^1.2.0",
"eslint": "^7.29.0",
"console-stamp": "^3.0.3",
"digest-fetch": "^1.2.1",
"eslint": "^7.32.0",
"express": "^4.17.1",
"express-ipfilter": "^1.2.0",
"feedme": "^2.0.2",
@@ -79,10 +81,9 @@
"iconv-lite": "^0.6.3",
"module-alias": "^2.2.2",
"moment": "^2.29.1",
"node-fetch": "^2.6.1",
"node-fetch": "^2.6.5",
"node-ical": "^0.13.0",
"simple-git": "^2.40.0",
"socket.io": "^4.1.2"
"socket.io": "^4.2.0"
},
"_moduleAliases": {
"node_helper": "js/node_helper.js",
@@ -96,6 +97,9 @@
"projects": [
{
"displayName": "unit",
"moduleNameMapper": {
"logger": "<rootDir>/js/logger.js"
},
"testMatch": [
"**/tests/unit/**/*.[jt]s?(x)"
],
@@ -103,14 +107,31 @@
"<rootDir>/tests/unit/mocks"
]
},
{
"displayName": "electron",
"testMatch": [
"**/tests/electron/**/*.[jt]s?(x)"
],
"testPathIgnorePatterns": [
"<rootDir>/tests/electron/modules/mocks",
"<rootDir>/tests/electron/global-setup.js",
"<rootDir>/tests/electron/modules/basic-auth.js"
]
},
{
"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/global-setup.js"
"<rootDir>/tests/e2e/global-setup.js",
"<rootDir>/tests/e2e/mock-console.js"
]
}
]

21
tests/configs/default.js Normal file
View File

@@ -0,0 +1,21 @@
/* Magic Mirror Test default config for modules
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
exports.configFactory = function (options) {
return Object.assign(
{
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: false
}
},
modules: []
},
options
);
};

View File

@@ -3,22 +3,9 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: [],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: []
};
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
ipWhitelist: []
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,22 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: []
};
let config = require(process.cwd() + "/tests/configs/default.js").configFactory();
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -4,19 +4,6 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
module: "alert",

View File

@@ -3,19 +3,8 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
@@ -35,7 +24,7 @@ let config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,19 +3,8 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
@@ -36,7 +25,7 @@ let config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,19 +3,8 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
@@ -35,7 +24,7 @@ let config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,19 +3,8 @@
* By Rejas
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
@@ -35,7 +24,7 @@ let config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,19 +3,8 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
@@ -31,7 +20,7 @@ let config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -5,19 +5,8 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
@@ -38,7 +27,7 @@ let config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,19 +3,8 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
@@ -33,7 +22,7 @@ let config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,19 +3,8 @@
* By Rejas
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
@@ -32,7 +21,7 @@ let config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -4,18 +4,7 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{

View File

@@ -4,19 +4,6 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
module: "clock",

View File

@@ -3,19 +3,6 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
module: "clock",

View File

@@ -4,18 +4,7 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{

View File

@@ -4,18 +4,7 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{

View File

@@ -0,0 +1,23 @@
/* Magic Mirror Test config for default clock module
*
* By Johan Hammar
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "clock",
position: "middle_center",
config: {
showTime: false
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -4,18 +4,7 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{

View File

@@ -4,18 +4,8 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "es",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{

View File

@@ -4,18 +4,7 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "es",
timeFormat: 24,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{

View File

@@ -4,18 +4,8 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "es",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{

View File

@@ -5,18 +5,8 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "es",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{

View File

@@ -4,18 +4,7 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{

View File

@@ -4,18 +4,7 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{

View File

@@ -4,18 +4,7 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{

View File

@@ -4,18 +4,7 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{

View File

@@ -4,22 +4,6 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
fullscreen: false,
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
module: "helloworld",

View File

@@ -4,19 +4,6 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
module: "helloworld",

View File

@@ -4,19 +4,6 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
module: "helloworld",

View File

@@ -4,18 +4,7 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{

View File

@@ -3,18 +3,7 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{

View File

@@ -3,18 +3,7 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{

View File

@@ -4,19 +4,6 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules:
// Using exotic content. This is why don't accept go to JSON configuration file
(function () {

View File

@@ -3,21 +3,7 @@
* By rejas https://github.com/rejas
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
fullscreen: false,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
modules: [
{
module: "compliments",
@@ -39,7 +25,7 @@ let config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,19 +3,8 @@
* By fewieden https://github.com/fewieden
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
@@ -28,7 +17,7 @@ let config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,20 +3,7 @@
* By fewieden https://github.com/fewieden
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
modules: [
{
module: "weather",
@@ -34,7 +21,7 @@ let config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,19 +3,8 @@
* By fewieden https://github.com/fewieden
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
units: "imperial",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
@@ -30,7 +19,7 @@ let config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,19 +3,8 @@
* By fewieden https://github.com/fewieden
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
@@ -30,7 +19,7 @@ let config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,19 +3,8 @@
* By fewieden https://github.com/fewieden
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
@@ -33,7 +22,7 @@ let config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,19 +3,8 @@
* By rejas
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
units: "imperial",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
@@ -31,7 +20,7 @@ let config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,22 +3,9 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["x.x.x.x"],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: []
};
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
ipWhitelist: ["x.x.x.x"]
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,22 +3,9 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = {
port: 8090,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: []
};
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
port: 8090
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -4,18 +4,7 @@
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.10.1"],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
}
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.10.1"]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/

View File

@@ -1,42 +1,13 @@
const helpers = require("./global-setup");
const fetch = require("node-fetch");
const helpers = require("./global-setup");
describe("Electron app environment", function () {
helpers.setupTimeout(this);
let app = null;
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/env.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/default.js");
helpers.getDocument(done);
});
beforeEach(function () {
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function (startedApp) {
app = startedApp;
});
});
afterEach(function () {
return helpers.stopApplication(app);
});
it("should open a browserwindow", async function () {
await app.client.waitUntilWindowLoaded();
app.browserWindow.focus();
expect(await app.client.getWindowCount()).toBe(1);
expect(await app.browserWindow.isMinimized()).toBe(false);
expect(await app.browserWindow.isDevToolsOpened()).toBe(false);
expect(await app.browserWindow.isVisible()).toBe(true);
expect(await app.browserWindow.isFocused()).toBe(true);
const bounds = await app.browserWindow.getBounds();
expect(bounds.width).toBeGreaterThan(0);
expect(bounds.height).toBeGreaterThan(0);
expect(await app.browserWindow.getTitle()).toBe("MagicMirror²");
afterAll(function () {
helpers.stopApplication();
});
it("get request from http://localhost:8080 should return 200", function (done) {
@@ -52,4 +23,10 @@ describe("Electron app environment", function () {
done();
});
});
it("should show the title MagicMirror²", function () {
const elem = document.querySelector("title");
expect(elem).not.toBe(null);
expect(elem.textContent).toBe("MagicMirror²");
});
});

View File

@@ -1,10 +1,7 @@
const helpers = require("./global-setup");
const fetch = require("node-fetch");
const helpers = require("./global-setup");
describe("All font files from roboto.css should be downloadable", function () {
helpers.setupTimeout(this);
let app;
const fontFiles = [];
// Statements below filters out all 'url' lines in the CSS file
const fileContent = require("fs").readFileSync(__dirname + "/../../fonts/roboto.css", "utf8");
@@ -18,20 +15,10 @@ describe("All font files from roboto.css should be downloadable", function () {
}
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/without_modules.js";
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function (startedApp) {
app = startedApp;
});
helpers.startApplication("tests/configs/without_modules.js");
});
afterAll(function () {
return helpers.stopApplication(app);
helpers.stopApplication();
});
test.each(fontFiles)("should return 200 HTTP code for file '%s'", (fontFile, done) => {

View File

@@ -1,53 +1,32 @@
/*
* Magic Mirror Global Setup Test Suite
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
const Application = require("spectron").Application;
const assert = require("assert");
const path = require("path");
const EventEmitter = require("events");
const jsdom = require("jsdom");
exports.getElectronPath = function () {
let electronPath = path.join(__dirname, "..", "..", "node_modules", ".bin", "electron");
if (process.platform === "win32") {
electronPath += ".cmd";
exports.startApplication = function (configFilename, exec) {
jest.resetModules();
if (global.app) {
global.app.stop();
}
return electronPath;
// Set config sample for use in test
process.env.MM_CONFIG_FILE = configFilename;
if (exec) exec;
global.app = require("app.js");
global.app.start();
};
// Set timeout - if this is run as CI Job, increase timeout
exports.setupTimeout = function (test) {
if (process.env.CI) {
jest.setTimeout(30000);
} else {
jest.setTimeout(10000);
exports.stopApplication = function () {
if (global.app) {
global.app.stop();
}
};
exports.startApplication = function (options) {
const emitter = new EventEmitter();
emitter.setMaxListeners(100);
options.path = exports.getElectronPath();
if (process.env.CI) {
options.startTimeout = 30000;
}
const app = new Application(options);
return app.start().then(function () {
assert.strictEqual(app.isRunning(), true);
return app;
});
};
exports.stopApplication = function (app) {
if (!app || !app.isRunning()) {
return;
}
return app.stop().then(function () {
assert.strictEqual(app.isRunning(), false);
exports.getDocument = function (callback, ms) {
const url = "http://" + (config.address || "localhost") + ":" + (config.port || "8080");
jsdom.JSDOM.fromURL(url, { resources: "usable", runScripts: "dangerously" }).then((dom) => {
dom.window.name = "jsdom";
dom.window.onload = function () {
global.document = dom.window.document;
setTimeout(() => {
callback();
}, ms);
};
});
};

View File

@@ -1,29 +1,13 @@
const helpers = require("./global-setup");
const fetch = require("node-fetch");
const helpers = require("./global-setup");
describe("ipWhitelist directive configuration", function () {
helpers.setupTimeout(this);
let app = null;
beforeEach(function () {
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function (startedApp) {
app = startedApp;
});
});
afterEach(function () {
return helpers.stopApplication(app);
});
describe("Set ipWhitelist without access", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/noIpWhiteList.js";
helpers.startApplication("tests/configs/noIpWhiteList.js");
});
afterAll(function () {
helpers.stopApplication();
});
it("should return 403", function (done) {
@@ -36,8 +20,10 @@ describe("ipWhitelist directive configuration", function () {
describe("Set ipWhitelist []", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/empty_ipWhiteList.js";
helpers.startApplication("tests/configs/empty_ipWhiteList.js");
});
afterAll(function () {
helpers.stopApplication();
});
it("should return 200", function (done) {

21
tests/e2e/mock-console.js Normal file
View File

@@ -0,0 +1,21 @@
/**
* Suppresses errors concerning web server already shut down.
*
* @param {string} err The error message.
*/
function mockError(err) {
if (err.includes("ECONNREFUSED") || err.includes("ECONNRESET") || err.includes("socket hang up") || err.includes("exports is not defined")) {
jest.fn();
} else {
console.dir(err);
}
}
global.console = {
log: jest.fn(),
dir: console.dir,
error: mockError,
warn: console.warn,
info: jest.fn(),
debug: console.debug
};

View File

@@ -1,32 +1,17 @@
const helpers = require("../global-setup");
describe("Alert module", function () {
helpers.setupTimeout(this);
let app = null;
beforeEach(function () {
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function (startedApp) {
app = startedApp;
});
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/alert/default.js");
helpers.getDocument(done, 1000);
});
afterAll(function () {
helpers.stopApplication();
});
afterEach(function () {
return helpers.stopApplication(app);
});
describe("Default configuration", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/alert/default.js";
});
it("should show the welcome message", function () {
return app.client.waitUntilTextExists(".ns-box .ns-box-inner .light.bright.small", "Welcome, start was successful!", 10000);
});
it("should show the welcome message", function () {
const elem = document.querySelector(".ns-box .ns-box-inner .light.bright.small");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("Welcome, start was successful!");
});
});

View File

@@ -1,86 +1,71 @@
const helpers = require("../global-setup");
describe("Clock set to spanish language module", function () {
helpers.setupTimeout(this);
afterAll(function () {
helpers.stopApplication();
});
let app = null;
testMatch = async function (element, regex) {
await app.client.waitUntilWindowLoaded();
const elem = await app.client.$(element);
const txt = await elem.getText(element);
return expect(txt).toMatch(regex);
const testMatch = function (element, regex) {
const elem = document.querySelector(element);
expect(elem).not.toBe(null);
expect(elem.textContent).toMatch(regex);
};
beforeEach(function () {
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function (startedApp) {
app = startedApp;
});
});
afterEach(function () {
return helpers.stopApplication(app);
});
describe("with default 24hr clock config", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/es/clock_24hr.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/clock/es/clock_24hr.js");
helpers.getDocument(done, 1000);
});
it("shows date with correct format", async function () {
it("shows date with correct format", function () {
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}$/;
return testMatch(".clock .date", dateRegex);
testMatch(".clock .date", dateRegex);
});
it("shows time in 24hr format", async function () {
it("shows time in 24hr format", function () {
const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/;
return testMatch(".clock .time", timeRegex);
testMatch(".clock .time", timeRegex);
});
});
describe("with default 12hr clock config", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/es/clock_12hr.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/clock/es/clock_12hr.js");
helpers.getDocument(done, 1000);
});
it("shows date with correct format", async function () {
it("shows date with correct format", function () {
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}$/;
return testMatch(".clock .date", dateRegex);
testMatch(".clock .date", dateRegex);
});
it("shows time in 12hr format", async function () {
it("shows time in 12hr format", function () {
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/;
return testMatch(".clock .time", timeRegex);
testMatch(".clock .time", timeRegex);
});
});
describe("with showPeriodUpper config enabled", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/es/clock_showPeriodUpper.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/clock/es/clock_showPeriodUpper.js");
helpers.getDocument(done, 1000);
});
it("shows 12hr time with upper case AM/PM", async function () {
it("shows 12hr time with upper case AM/PM", function () {
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/;
return testMatch(".clock .time", timeRegex);
testMatch(".clock .time", timeRegex);
});
});
describe("with showWeek config enabled", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/es/clock_showWeek.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/clock/es/clock_showWeek.js");
helpers.getDocument(done, 1000);
});
it("shows week with correct format", async function () {
it("shows week with correct format", function () {
const weekRegex = /^Semana [0-9]{1,2}$/;
return testMatch(".clock .week", weekRegex);
testMatch(".clock .week", weekRegex);
});
});
});

View File

@@ -2,120 +2,115 @@ const helpers = require("../global-setup");
const moment = require("moment");
describe("Clock module", function () {
helpers.setupTimeout(this);
afterAll(function () {
helpers.stopApplication();
});
let app = null;
testMatch = async function (element, regex) {
await app.client.waitUntilWindowLoaded();
const elem = await app.client.$(element);
const txt = await elem.getText(element);
return expect(txt).toMatch(regex);
const testMatch = function (element, regex) {
const elem = document.querySelector(element);
expect(elem).not.toBe(null);
expect(elem.textContent).toMatch(regex);
};
beforeEach(function () {
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function (startedApp) {
app = startedApp;
});
});
afterEach(function () {
return helpers.stopApplication(app);
});
describe("with default 24hr clock config", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_24hr.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/clock/clock_24hr.js");
helpers.getDocument(done, 1000);
});
it("should show the date in the correct format", async function () {
it("should show the date in the correct format", function () {
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}$/;
return testMatch(".clock .date", dateRegex);
testMatch(".clock .date", dateRegex);
});
it("should show the time in 24hr format", async function () {
it("should show the time in 24hr format", function () {
const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/;
return testMatch(".clock .time", timeRegex);
testMatch(".clock .time", timeRegex);
});
});
describe("with default 12hr clock config", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_12hr.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/clock/clock_12hr.js");
helpers.getDocument(done, 1000);
});
it("should show the date in the correct format", async function () {
it("should show the date in the correct format", function () {
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}$/;
return testMatch(".clock .date", dateRegex);
testMatch(".clock .date", dateRegex);
});
it("should show the time in 12hr format", async function () {
it("should show the time in 12hr format", function () {
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/;
return testMatch(".clock .time", timeRegex);
testMatch(".clock .time", timeRegex);
});
});
describe("with showPeriodUpper config enabled", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_showPeriodUpper.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/clock/clock_showPeriodUpper.js");
helpers.getDocument(done, 1000);
});
it("should show 12hr time with upper case AM/PM", async function () {
it("should show 12hr time with upper case AM/PM", function () {
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/;
return testMatch(".clock .time", timeRegex);
testMatch(".clock .time", timeRegex);
});
});
describe("with displaySeconds config disabled", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_displaySeconds_false.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/clock/clock_displaySeconds_false.js");
helpers.getDocument(done, 1000);
});
it("should show 12hr time without seconds am/pm", async function () {
it("should show 12hr time without seconds am/pm", function () {
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[ap]m$/;
return testMatch(".clock .time", timeRegex);
testMatch(".clock .time", timeRegex);
});
});
describe("with showTime config disabled", function () {
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/clock/clock_showTime.js");
helpers.getDocument(done, 1000);
});
it("should show not show the time when digital clock is shown", function () {
const elem = document.querySelector(".clock .digital .time");
expect(elem).toBe(null);
});
});
describe("with showWeek config enabled", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_showWeek.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/clock/clock_showWeek.js");
helpers.getDocument(done, 1000);
});
it("should show the week in the correct format", async function () {
it("should show the week in the correct format", function () {
const weekRegex = /^Week [0-9]{1,2}$/;
return testMatch(".clock .week", weekRegex);
testMatch(".clock .week", weekRegex);
});
it("should show the week with the correct number of week of year", async function () {
it("should show the week with the correct number of week of year", function () {
const currentWeekNumber = moment().week();
const weekToShow = "Week " + currentWeekNumber;
await app.client.waitUntilWindowLoaded();
const elem = await app.client.$(".clock .week");
const txt = await elem.getText(".clock .week");
return expect(txt).toBe(weekToShow);
const elem = document.querySelector(".clock .week");
expect(elem).not.toBe(null);
expect(elem.textContent).toBe(weekToShow);
});
});
describe("with analog clock face enabled", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_analog.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/clock/clock_analog.js");
helpers.getDocument(done, 1000);
});
it("should show the analog clock face", async () => {
await app.client.waitUntilWindowLoaded();
const clock = await app.client.$$(".clockCircle");
return expect(clock.length).toBe(1);
it("should show the analog clock face", () => {
const elem = document.querySelector(".clockCircle");
expect(elem).not.toBe(null);
});
});
});

View File

@@ -1,106 +1,87 @@
const helpers = require("../global-setup");
/**
* move similar tests in function doTest
*
* @param {Array} complimentsArray The array of compliments.
*/
function doTest(complimentsArray) {
let elem = document.querySelector(".compliments");
expect(elem).not.toBe(null);
elem = document.querySelector(".module-content");
expect(elem).not.toBe(null);
expect(complimentsArray).toContain(elem.textContent);
}
describe("Compliments module", function () {
helpers.setupTimeout(this);
let app = null;
beforeEach(function () {
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function (startedApp) {
app = startedApp;
});
});
afterEach(function () {
return helpers.stopApplication(app);
afterAll(function () {
helpers.stopApplication();
});
describe("parts of days", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/compliments/compliments_parts_day.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/compliments/compliments_parts_day.js");
helpers.getDocument(done, 1000);
});
it("if Morning compliments for that part of day", async function () {
it("if Morning compliments for that part of day", function () {
const hour = new Date().getHours();
if (hour >= 3 && hour < 12) {
// if morning check
const elem = await app.client.$(".compliments");
return elem.getText(".compliments").then(function (text) {
expect(["Hi", "Good Morning", "Morning test"]).toContain(text);
});
doTest(["Hi", "Good Morning", "Morning test"]);
}
});
it("if Afternoon show Compliments for that part of day", async function () {
it("if Afternoon show Compliments for that part of day", function () {
const hour = new Date().getHours();
if (hour >= 12 && hour < 17) {
// if afternoon check
const elem = await app.client.$(".compliments");
return elem.getText(".compliments").then(function (text) {
expect(["Hello", "Good Afternoon", "Afternoon test"]).toContain(text);
});
doTest(["Hello", "Good Afternoon", "Afternoon test"]);
}
});
it("if Evening show Compliments for that part of day", async function () {
it("if Evening show Compliments for that part of day", function () {
const hour = new Date().getHours();
if (!(hour >= 3 && hour < 12) && !(hour >= 12 && hour < 17)) {
// if evening check
const elem = await app.client.$(".compliments");
return elem.getText(".compliments").then(function (text) {
expect(["Hello There", "Good Evening", "Evening test"]).toContain(text);
});
doTest(["Hello There", "Good Evening", "Evening test"]);
}
});
});
describe("Feature anytime in compliments module", function () {
describe("Set anytime and empty compliments for morning, evening and afternoon ", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/compliments/compliments_anytime.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/compliments/compliments_anytime.js");
helpers.getDocument(done, 1000);
});
it("Show anytime because if configure empty parts of day compliments and set anytime compliments", async function () {
const elem = await app.client.$(".compliments");
return elem.getText(".compliments").then(function (text) {
expect(["Anytime here"]).toContain(text);
});
it("Show anytime because if configure empty parts of day compliments and set anytime compliments", function () {
doTest(["Anytime here"]);
});
});
describe("Only anytime present in configuration compliments", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/compliments/compliments_only_anytime.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/compliments/compliments_only_anytime.js");
helpers.getDocument(done, 1000);
});
it("Show anytime compliments", async function () {
const elem = await app.client.$(".compliments");
return elem.getText(".compliments").then(function (text) {
expect(["Anytime here"]).toContain(text);
});
it("Show anytime compliments", function () {
doTest(["Anytime here"]);
});
});
});
describe("Feature date in compliments module", function () {
describe("Set date and empty compliments for anytime, morning, evening and afternoon", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/compliments/compliments_date.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/compliments/compliments_date.js");
helpers.getDocument(done, 1000);
});
it("Show happy new year compliment on new years day", async function () {
const elem = await app.client.$(".compliments");
return elem.getText(".compliments").then(function (text) {
expect(["Happy new year!"]).toContain(text);
});
it("Show happy new year compliment on new years day", function () {
doTest(["Happy new year!"]);
});
});
});

View File

@@ -1,45 +1,33 @@
const helpers = require("../global-setup");
describe("Test helloworld module", function () {
helpers.setupTimeout(this);
let app = null;
beforeEach(function () {
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function (startedApp) {
app = startedApp;
});
});
afterEach(function () {
return helpers.stopApplication(app);
afterAll(function () {
helpers.stopApplication();
});
describe("helloworld set config text", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/helloworld/helloworld.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/helloworld/helloworld.js");
helpers.getDocument(done, 1000);
});
it("Test message helloworld module", async function () {
const elem = await app.client.$(".helloworld");
return expect(await elem.getText(".helloworld")).toBe("Test HelloWorld Module");
it("Test message helloworld module", function () {
const elem = document.querySelector(".helloworld");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("Test HelloWorld Module");
});
});
describe("helloworld default config text", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/helloworld/helloworld_default.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/helloworld/helloworld_default.js");
helpers.getDocument(done, 1000);
});
it("Test message helloworld module", async function () {
const elem = await app.client.$(".helloworld");
return expect(await elem.getText(".helloworld")).toBe("Hello World!");
it("Test message helloworld module", function () {
const elem = document.querySelector(".helloworld");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("Hello World!");
});
});
});

View File

@@ -1,67 +1,63 @@
const helpers = require("../global-setup");
describe("Newsfeed module", function () {
helpers.setupTimeout(this);
let app = null;
beforeEach(function () {
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function (startedApp) {
app = startedApp;
});
});
afterEach(function () {
return helpers.stopApplication(app);
afterAll(function () {
helpers.stopApplication();
});
describe("Default configuration", function () {
beforeAll(function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/newsfeed/default.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/newsfeed/default.js");
helpers.getDocument(done, 3000);
});
it("should show the newsfeed title", function () {
return app.client.waitUntilTextExists(".newsfeed .newsfeed-source", "Rodrigo Ramirez Blog", 10000);
const elem = document.querySelector(".newsfeed .newsfeed-source");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("Rodrigo Ramirez Blog");
});
it("should show the newsfeed article", function () {
return app.client.waitUntilTextExists(".newsfeed .newsfeed-title", "QPanel", 10000);
const elem = document.querySelector(".newsfeed .newsfeed-title");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("QPanel");
});
it("should NOT show the newsfeed description", async () => {
await app.client.waitUntilTextExists(".newsfeed .newsfeed-title", "QPanel", 10000);
const events = await app.client.$$(".newsfeed .newsfeed-desc");
return expect(events.length).toBe(0);
it("should NOT show the newsfeed description", () => {
const elem = document.querySelector(".newsfeed .newsfeed-desc");
expect(elem).toBe(null);
});
});
describe("Custom configuration", function () {
beforeAll(function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/newsfeed/prohibited_words.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/newsfeed/prohibited_words.js");
helpers.getDocument(done, 3000);
});
it("should not show articles with prohibited words", function () {
return app.client.waitUntilTextExists(".newsfeed .newsfeed-title", "Problema VirtualBox", 10000);
const elem = document.querySelector(".newsfeed .newsfeed-title");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("Problema VirtualBox");
});
it("should show the newsfeed description", async () => {
await app.client.waitUntilTextExists(".newsfeed .newsfeed-title", "Problema VirtualBox", 10000);
const events = await app.client.$$(".newsfeed .newsfeed-desc");
return expect(events.length).toBe(1);
it("should show the newsfeed description", () => {
const elem = document.querySelector(".newsfeed .newsfeed-desc");
expect(elem).not.toBe(null);
expect(elem.textContent.length).not.toBe(0);
});
});
describe("Invalid configuration", function () {
beforeAll(function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/newsfeed/incorrect_url.js";
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/newsfeed/incorrect_url.js");
helpers.getDocument(done, 3000);
});
it("should show malformed url warning", function () {
return app.client.waitUntilTextExists(".newsfeed .small", "Error in the Newsfeed module. Malformed url.", 10000);
const elem = document.querySelector(".newsfeed .small");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("Error in the Newsfeed module. Malformed url.");
});
});
});

View File

@@ -1,38 +1,24 @@
const helpers = require("./global-setup");
describe("Display of modules", function () {
helpers.setupTimeout(this);
let app = null;
beforeEach(function () {
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function (startedApp) {
app = startedApp;
});
beforeAll(function (done) {
helpers.startApplication("tests/configs/modules/display.js");
helpers.getDocument(done);
});
afterAll(function () {
helpers.stopApplication();
});
afterEach(function () {
return helpers.stopApplication(app);
it("should show the test header", function () {
const elem = document.querySelector("#module_0_helloworld .module-header");
expect(elem).not.toBe(null);
// textContent gibt hier lowercase zurück, das uppercase wird durch css realisiert, was daher nicht in textContent landet
expect(elem.textContent).toBe("test_header");
});
describe("Using helloworld", function () {
beforeAll(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/display.js";
});
it("should show the test header", async () => {
const elem = await app.client.$("#module_0_helloworld .module-header", 10000);
return expect(await elem.getText("#module_0_helloworld .module-header")).toBe("TEST_HEADER");
});
it("should show no header if no header text is specified", async () => {
const elem = await app.client.$("#module_1_helloworld .module-header", 10000);
return expect(await elem.getText("#module_1_helloworld .module-header")).toBe("");
});
it("should show no header if no header text is specified", function () {
const elem = document.querySelector("#module_1_helloworld .module-header");
expect(elem).not.toBe(null);
expect(elem.textContent).toBe("undefined");
});
});

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