Merge branch 'develop' into patch-1

This commit is contained in:
rico24 2021-06-28 22:38:10 +02:00 committed by GitHub
commit 6e124842e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
136 changed files with 4538 additions and 2679 deletions

View File

@ -1,10 +1,10 @@
{
"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"],
"plugins": ["prettier", "jsdoc"],
"plugins": ["prettier", "jsdoc", "jest"],
"env": {
"browser": true,
"es6": true,
"mocha": true,
"jest/globals": true,
"node": true
},
"globals": {

View File

@ -1,3 +1,7 @@
Hello and thank you for opening an issue.
**Please make sure that you have read the following lines before submitting your Issue:**
## I'm not sure if this is a bug
If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt)

6
.github/codecov.yml vendored Normal file
View File

@ -0,0 +1,6 @@
coverage:
status:
project:
default:
# advanced settings
informational: true

View File

@ -11,6 +11,7 @@ on:
jobs:
run-and-upload-coverage-report:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v2
- run: |

View File

@ -9,6 +9,7 @@ on:
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
- uses: dangoslen/changelog-enforcer@v1.6.1

View File

@ -12,9 +12,10 @@ on:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
node-version: [12.x, 14.x, 16.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
@ -28,5 +29,5 @@ jobs:
npm run test:prettier
npm run test:js
npm run test:css
npm run test:e2e
npm run test:unit
npm run test:e2e

1
.husky/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
_

4
.husky/pre-commit Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint:staged

View File

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

View File

@ -7,7 +7,6 @@
<a href="https://codecov.io/gh/MichMich/MagicMirror"><img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg?token=LEG1KitZR6" alt="CodeCov Status"/></a>
<a href="https://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
<a href="https://github.com/MichMich/MagicMirror/actions?query=workflow%3A%22Automated+Tests%22"><img src="https://github.com/MichMich/MagicMirror/workflows/Automated%20Tests/badge.svg" alt="Tests"></a>
<a href="https://codecov.io/gh/MichMich/MagicMirror"><img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg" /></a>
</p>
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).
@ -23,6 +22,7 @@ For the full documentation including **[installation instructions](https://docs.
- Website: [https://magicmirror.builders](https://magicmirror.builders)
- Documentation: [https://docs.magicmirror.builders](https://docs.magicmirror.builders)
- Forum: [https://forum.magicmirror.builders](https://forum.magicmirror.builders)
- Technical discussions: https://forum.magicmirror.builders/category/11/core-system
- Discord: [https://discord.gg/J5BAtvx](https://discord.gg/J5BAtvx)
- Blog: [https://michaelteeuw.nl/tagged/magicmirror](https://michaelteeuw.nl/tagged/magicmirror)
- Donations: [https://magicmirror.builders/#donate](https://magicmirror.builders/#donate)

View File

@ -2,7 +2,7 @@
// Use separate scope to prevent global scope pollution
(function () {
var config = {};
const config = {};
/**
* Helper function to get server address/hostname from either the commandline or env
@ -17,8 +17,8 @@
* @returns {string} the value of the parameter
*/
function getCommandLineParameter(key, defaultValue = undefined) {
var index = process.argv.indexOf(`--${key}`);
var value = index > -1 ? process.argv[index + 1] : undefined;
const index = process.argv.indexOf(`--${key}`);
const value = index > -1 ? process.argv[index + 1] : undefined;
return value !== undefined ? String(value) : defaultValue;
}
@ -43,7 +43,7 @@
// Select http or https module, depending on requested url
const lib = url.startsWith("https") ? require("https") : require("http");
const request = lib.get(url, (response) => {
var configData = "";
let configData = "";
// Gather incoming data
response.on("data", function (chunk) {
@ -79,15 +79,15 @@
getServerAddress();
(config.address && config.port) || fail();
var prefix = config.tls ? "https://" : "http://";
const prefix = config.tls ? "https://" : "http://";
// Only start the client if a non-local server was provided
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) === -1) {
getServerConfig(`${prefix}${config.address}:${config.port}/config/`)
.then(function (configReturn) {
// Pass along the server config via an environment variable
var env = Object.create(process.env);
var options = { env: env };
const env = Object.create(process.env);
const options = { env: env };
configReturn.address = config.address;
configReturn.port = config.port;
configReturn.tls = config.tls;

View File

@ -4,11 +4,10 @@
* MIT Licensed.
*
* For more information on how you can configure this file
* See https://github.com/MichMich/MagicMirror#configuration
*
* see https://docs.magicmirror.builders/getting-started/configuration.html#general
* and https://docs.magicmirror.builders/modules/configuration.html
*/
var config = {
let config = {
address: "localhost", // Address to listen on, can be:
// - "localhost", "127.0.0.1", "::1" to listen on loopback interface
// - another specific IPv4/6 to listen on a specific interface

31
css/custom.css.sample Normal file
View File

@ -0,0 +1,31 @@
/* Magic Mirror Custom CSS Sample
*
* Change color and fonts here.
*
* Beware that properties cannot be unitless, so for example write '--gap-body: 0px;' instead of just '--gap-body: 0;'
*
* MIT Licensed.
*/
/* Uncomment and adjust accordingly if you want to import another font from the google-fonts-api: */
/* @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;300;400;700&display=swap'); */
:root {
--color-text: #999;
--color-text-dimmed: #666;
--color-text-bright: #fff;
--color-background: black;
--font-primary: "Roboto Condensed";
--font-secondary: "Roboto";
--font-size: 20px;
--font-size-small: 0.75rem;
--gap-body-top: 60px;
--gap-body-right: 60px;
--gap-body-bottom: 60px;
--gap-body-left: 60px;
--gap-modules: 30px;
}

View File

@ -1,8 +1,29 @@
:root {
--color-text: #999;
--color-text-dimmed: #666;
--color-text-bright: #fff;
--color-background: #000;
--font-primary: "Roboto Condensed";
--font-secondary: "Roboto";
--font-size: 20px;
--font-size-small: 0.75rem;
--gap-body-top: 60px;
--gap-body-right: 60px;
--gap-body-bottom: 60px;
--gap-body-left: 60px;
--gap-modules: 30px;
}
html {
cursor: none;
overflow: hidden;
background: #000;
background: var(--color-background);
user-select: none;
font-size: var(--font-size);
}
::-webkit-scrollbar {
@ -10,16 +31,15 @@ html {
}
body {
margin: 60px;
margin: var(--gap-body-top) var(--gap-body-right) var(--gap-body-bottom) var(--gap-body-left);
position: absolute;
height: calc(100% - 120px);
width: calc(100% - 120px);
background: #000;
color: #aaa;
font-family: "Roboto Condensed", sans-serif;
height: calc(100% - var(--gap-body-top) - var(--gap-body-bottom));
width: calc(100% - var(--gap-body-right) - var(--gap-body-left));
background: var(--color-background);
color: var(--color-text);
font-family: var(--font-primary), sans-serif;
font-weight: 400;
font-size: 2em;
line-height: 1.5em;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
@ -28,60 +48,60 @@ body {
*/
.dimmed {
color: #666;
color: var(--color-text-dimmed);
}
.normal {
color: #999;
color: var(--color-text);
}
.bright {
color: #fff;
color: var(--color-text-bright);
}
.xsmall {
font-size: 15px;
line-height: 20px;
font-size: var(--font-size-small);
line-height: 1.275;
}
.small {
font-size: 20px;
line-height: 25px;
font-size: 1rem;
line-height: 1.25;
}
.medium {
font-size: 30px;
line-height: 35px;
font-size: 1.5rem;
line-height: 1.225;
}
.large {
font-size: 65px;
line-height: 65px;
font-size: 3.25rem;
line-height: 1;
}
.xlarge {
font-size: 75px;
line-height: 75px;
font-size: 3.75rem;
line-height: 1;
letter-spacing: -3px;
}
.thin {
font-family: Roboto, sans-serif;
font-family: var(--font-secondary), sans-serif;
font-weight: 100;
}
.light {
font-family: "Roboto Condensed", sans-serif;
font-family: var(--font-primary), sans-serif;
font-weight: 300;
}
.regular {
font-family: "Roboto Condensed", sans-serif;
font-family: var(--font-primary), sans-serif;
font-weight: 400;
}
.bold {
font-family: "Roboto Condensed", sans-serif;
font-family: var(--font-primary), sans-serif;
font-weight: 700;
}
@ -95,14 +115,14 @@ body {
header {
text-transform: uppercase;
font-size: 15px;
font-family: "Roboto Condensed", Arial, Helvetica, sans-serif;
font-size: var(--font-size-small);
font-family: var(--font-primary), Arial, Helvetica, sans-serif;
font-weight: 400;
border-bottom: 1px solid #666;
border-bottom: 1px solid var(--color-text-dimmed);
line-height: 15px;
padding-bottom: 5px;
margin-bottom: 10px;
color: #999;
color: var(--color-text);
}
sup {
@ -115,11 +135,11 @@ sup {
*/
.module {
margin-bottom: 30px;
margin-bottom: var(--gap-modules);
}
.region.bottom .module {
margin-top: 30px;
margin-top: var(--gap-modules);
margin-bottom: 0;
}
@ -143,10 +163,10 @@ sup {
.region.fullscreen {
position: absolute;
top: -60px;
left: -60px;
right: -60px;
bottom: -60px;
top: calc(-1 * var(--gap-body-top));
left: calc(-1 * var(--gap-body-left));
right: calc(-1 * var(--gap-body-right));
bottom: calc(-1 * var(--gap-body-bottom));
pointer-events: none;
}
@ -163,18 +183,6 @@ sup {
top: 0;
}
.region.top .container {
margin-bottom: 25px;
}
.region.bottom .container {
margin-top: 25px;
}
.region.top .container:empty {
margin-bottom: 0;
}
.region.top.center,
.region.bottom.center {
left: 50%;
@ -191,10 +199,6 @@ sup {
bottom: 0;
}
.region.bottom .container:empty {
margin-top: 0;
}
.region.bottom.right,
.region.bottom.center,
.region.bottom.left {

View File

@ -1,25 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<head>
<title>MagicMirror²</title>
<meta name="google" content="notranslate" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="format-detection" content="telephone=no">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="format-detection" content="telephone=no" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<link rel="stylesheet" type="text/css" href="css/main.css">
<link rel="stylesheet" type="text/css" href="fonts/roboto.css">
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
<link rel="stylesheet" type="text/css" href="css/main.css" />
<link rel="stylesheet" type="text/css" href="fonts/roboto.css" />
<!-- 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#";
</script>
</head>
<body>
</head>
<body>
<div class="region fullscreen below"><div class="container"></div></div>
<div class="region top bar">
<div class="container"></div>
@ -29,7 +29,9 @@
</div>
<div class="region upper third"><div class="container"></div></div>
<div class="region middle center"><div class="container"></div></div>
<div class="region lower third"><div class="container"><br/></div></div>
<div class="region lower third">
<div class="container"><br /></div>
</div>
<div class="region bottom bar">
<div class="container"></div>
<div class="region bottom left"><div class="container"></div></div>
@ -51,5 +53,5 @@
<script type="text/javascript" src="js/loader.js"></script>
<script type="text/javascript" src="js/socketclient.js"></script>
<script type="text/javascript" src="js/main.js"></script>
</body>
</body>
</html>

View File

@ -180,7 +180,6 @@ function App() {
*
* @param {string} a Version number a.
* @param {string} b Version number b.
*
* @returns {number} A positive number if a is larger than b, a negative
* number if a is smaller and 0 if they are the same
*/

View File

@ -52,7 +52,13 @@ function checkConfigFile() {
// I'm not sure if all ever is utf-8
const configFile = fs.readFileSync(configFileName, "utf-8");
const errors = linter.verify(configFile);
// Explicitly tell linter that he might encounter es6 syntax ("let config = {...}")
const errors = linter.verify(configFile, {
env: {
es6: true
}
});
if (errors.length === 0) {
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
} else {

View File

@ -84,7 +84,6 @@
* Define the clone method for later use. Helper Method.
*
* @param {object} obj Object to be cloned
*
* @returns {object} the cloned object
*/
function cloneObject(obj) {

View File

@ -6,12 +6,12 @@
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
var address = "localhost";
var port = 8080;
const address = "localhost";
let port = 8080;
if (typeof mmPort !== "undefined") {
port = mmPort;
}
var defaults = {
const defaults = {
address: address,
port: port,
basePath: "/",

View File

@ -63,6 +63,11 @@ function createWindow() {
// Open the DevTools if run with "npm start dev"
if (process.argv.includes("dev")) {
if (process.env.JEST_WORKER_ID !== undefined) {
// if we are running with jest
var devtools = new BrowserWindow(electronOptions);
mainWindow.webContents.setDevToolsWebContents(devtools.webContents);
}
mainWindow.webContents.openDevTools();
}

View File

@ -6,24 +6,24 @@
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
var Loader = (function () {
const Loader = (function () {
/* Create helper variables */
var loadedModuleFiles = [];
var loadedFiles = [];
var moduleObjects = [];
const loadedModuleFiles = [];
const loadedFiles = [];
const moduleObjects = [];
/* Private Methods */
/**
* Loops thru all modules and requests load for every module.
*/
var loadModules = function () {
var moduleData = getModuleData();
const loadModules = function () {
let moduleData = getModuleData();
var loadNextModule = function () {
const loadNextModule = function () {
if (moduleData.length > 0) {
var nextModule = moduleData[0];
const nextModule = moduleData[0];
loadModule(nextModule, function () {
moduleData = moduleData.slice(1);
loadNextModule();
@ -46,9 +46,8 @@ var Loader = (function () {
/**
* Loops thru all modules and requests start for every module.
*/
var startModules = function () {
for (var m in moduleObjects) {
var module = moduleObjects[m];
const startModules = function () {
for (const module of moduleObjects) {
module.start();
}
@ -56,7 +55,7 @@ var Loader = (function () {
MM.modulesStarted(moduleObjects);
// Starting modules also hides any modules that have requested to be initially hidden
for (let thisModule of moduleObjects) {
for (const thisModule of moduleObjects) {
if (thisModule.data.hiddenOnStartup) {
Log.info("Initially hiding " + thisModule.name);
thisModule.hide();
@ -69,7 +68,7 @@ var Loader = (function () {
*
* @returns {object[]} module data as configured in config
*/
var getAllModules = function () {
const getAllModules = function () {
return config.modules;
};
@ -78,29 +77,28 @@ var Loader = (function () {
*
* @returns {object[]} Module information.
*/
var getModuleData = function () {
var modules = getAllModules();
var moduleFiles = [];
const getModuleData = function () {
const modules = getAllModules();
const moduleFiles = [];
for (var m in modules) {
var moduleData = modules[m];
var module = moduleData.module;
modules.forEach(function (moduleData, index) {
const module = moduleData.module;
var elements = module.split("/");
var moduleName = elements[elements.length - 1];
var moduleFolder = config.paths.modules + "/" + module;
const elements = module.split("/");
const moduleName = elements[elements.length - 1];
let moduleFolder = config.paths.modules + "/" + module;
if (defaultModules.indexOf(moduleName) !== -1) {
moduleFolder = config.paths.modules + "/default/" + module;
}
if (moduleData.disabled === true) {
continue;
return;
}
moduleFiles.push({
index: m,
identifier: "module_" + m + "_" + module,
index: index,
identifier: "module_" + index + "_" + module,
name: moduleName,
path: moduleFolder + "/",
file: moduleName + ".js",
@ -111,7 +109,7 @@ var Loader = (function () {
config: moduleData.config,
classes: typeof moduleData.classes !== "undefined" ? moduleData.classes + " " + module : module
});
}
});
return moduleFiles;
};
@ -122,11 +120,11 @@ var Loader = (function () {
* @param {object} module Information about the module we want to load.
* @param {Function} callback Function called when done.
*/
var loadModule = function (module, callback) {
var url = module.path + module.file;
const loadModule = function (module, callback) {
const url = module.path + module.file;
var afterLoad = function () {
var moduleObject = Module.create(module.name);
const afterLoad = function () {
const moduleObject = Module.create(module.name);
if (moduleObject) {
bootstrapModule(module, moduleObject, function () {
callback();
@ -153,7 +151,7 @@ var Loader = (function () {
* @param {Module} mObj Modules instance.
* @param {Function} callback Function called when done.
*/
var bootstrapModule = function (module, mObj, callback) {
const bootstrapModule = function (module, mObj, callback) {
Log.info("Bootstrapping module: " + module.name);
mObj.setData(module);
@ -177,13 +175,14 @@ var Loader = (function () {
* @param {string} fileName Path of the file we want to load.
* @param {Function} callback Function called when done.
*/
var loadFile = function (fileName, callback) {
var extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1);
const loadFile = function (fileName, callback) {
const extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1);
let script, stylesheet;
switch (extension.toLowerCase()) {
case "js":
Log.log("Load script: " + fileName);
var script = document.createElement("script");
script = document.createElement("script");
script.type = "text/javascript";
script.src = fileName;
script.onload = function () {
@ -202,7 +201,7 @@ var Loader = (function () {
break;
case "css":
Log.log("Load stylesheet: " + fileName);
var stylesheet = document.createElement("link");
stylesheet = document.createElement("link");
stylesheet.rel = "stylesheet";
stylesheet.type = "text/css";
stylesheet.href = fileName;

View File

@ -6,25 +6,25 @@
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
var MM = (function () {
var modules = [];
const MM = (function () {
let modules = [];
/* Private Methods */
/**
* Create dom objects for all modules that are configured for a specific position.
*/
var createDomObjects = function () {
var domCreationPromises = [];
const createDomObjects = function () {
const domCreationPromises = [];
modules.forEach(function (module) {
if (typeof module.data.position !== "string") {
return;
}
var wrapper = selectWrapper(module.data.position);
const wrapper = selectWrapper(module.data.position);
var dom = document.createElement("div");
const dom = document.createElement("div");
dom.id = module.identifier;
dom.className = module.name;
@ -35,7 +35,7 @@ var MM = (function () {
dom.opacity = 0;
wrapper.appendChild(dom);
var moduleHeader = document.createElement("header");
const moduleHeader = document.createElement("header");
moduleHeader.innerHTML = module.getHeader();
moduleHeader.className = "module-header";
dom.appendChild(moduleHeader);
@ -46,11 +46,11 @@ var MM = (function () {
moduleHeader.style.display = "block;";
}
var moduleContent = document.createElement("div");
const moduleContent = document.createElement("div");
moduleContent.className = "module-content";
dom.appendChild(moduleContent);
var domCreationPromise = updateDom(module, 0);
const domCreationPromise = updateDom(module, 0);
domCreationPromises.push(domCreationPromise);
domCreationPromise
.then(function () {
@ -70,14 +70,13 @@ var MM = (function () {
* Select the wrapper dom object for a specific position.
*
* @param {string} position The name of the position.
*
* @returns {HTMLElement} the wrapper element
*/
var selectWrapper = function (position) {
var classes = position.replace("_", " ");
var parentWrapper = document.getElementsByClassName(classes);
const selectWrapper = function (position) {
const classes = position.replace("_", " ");
const parentWrapper = document.getElementsByClassName(classes);
if (parentWrapper.length > 0) {
var wrapper = parentWrapper[0].getElementsByClassName("container");
const wrapper = parentWrapper[0].getElementsByClassName("container");
if (wrapper.length > 0) {
return wrapper[0];
}
@ -92,9 +91,9 @@ var MM = (function () {
* @param {Module} sender The module that sent the notification.
* @param {Module} [sendTo] The (optional) module to send the notification to.
*/
var sendNotification = function (notification, payload, sender, sendTo) {
for (var m in modules) {
var module = modules[m];
const sendNotification = function (notification, payload, sender, sendTo) {
for (const m in modules) {
const module = modules[m];
if (module !== sender && (!sendTo || module === sendTo)) {
module.notificationReceived(notification, payload, sender);
}
@ -106,13 +105,12 @@ var MM = (function () {
*
* @param {Module} module The module that needs an update.
* @param {number} [speed] The (optional) number of microseconds for the animation.
*
* @returns {Promise} Resolved when the dom is fully updated.
*/
var updateDom = function (module, speed) {
const updateDom = function (module, speed) {
return new Promise(function (resolve) {
var newContentPromise = module.getDom();
var newHeader = module.getHeader();
const newHeader = module.getHeader();
let newContentPromise = module.getDom();
if (!(newContentPromise instanceof Promise)) {
// convert to a promise if not already one to avoid if/else's everywhere
@ -121,7 +119,7 @@ var MM = (function () {
newContentPromise
.then(function (newContent) {
var updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
const updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
updatePromise.then(resolve).catch(Log.error);
})
@ -136,10 +134,9 @@ var MM = (function () {
* @param {number} [speed] The (optional) number of microseconds for the animation.
* @param {string} newHeader The new header that is generated.
* @param {HTMLElement} newContent The new content that is generated.
*
* @returns {Promise} Resolved when the module dom has been updated.
*/
var updateDomWithContent = function (module, speed, newHeader, newContent) {
const updateDomWithContent = function (module, speed, newHeader, newContent) {
return new Promise(function (resolve) {
if (module.hidden || !speed) {
updateModuleContent(module, newHeader, newContent);
@ -174,26 +171,25 @@ var MM = (function () {
* @param {Module} module The module to check.
* @param {string} newHeader The new header that is generated.
* @param {HTMLElement} newContent The new content that is generated.
*
* @returns {boolean} True if the module need an update, false otherwise
*/
var moduleNeedsUpdate = function (module, newHeader, newContent) {
var moduleWrapper = document.getElementById(module.identifier);
const moduleNeedsUpdate = function (module, newHeader, newContent) {
const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper === null) {
return false;
}
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
const contentWrapper = moduleWrapper.getElementsByClassName("module-content");
const headerWrapper = moduleWrapper.getElementsByClassName("module-header");
var headerNeedsUpdate = false;
var contentNeedsUpdate = false;
let headerNeedsUpdate = false;
let contentNeedsUpdate;
if (headerWrapper.length > 0) {
headerNeedsUpdate = newHeader !== headerWrapper[0].innerHTML;
}
var tempContentWrapper = document.createElement("div");
const tempContentWrapper = document.createElement("div");
tempContentWrapper.appendChild(newContent);
contentNeedsUpdate = tempContentWrapper.innerHTML !== contentWrapper[0].innerHTML;
@ -207,13 +203,13 @@ var MM = (function () {
* @param {string} newHeader The new header that is generated.
* @param {HTMLElement} newContent The new content that is generated.
*/
var updateModuleContent = function (module, newHeader, newContent) {
var moduleWrapper = document.getElementById(module.identifier);
const updateModuleContent = function (module, newHeader, newContent) {
const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper === null) {
return;
}
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
const headerWrapper = moduleWrapper.getElementsByClassName("module-header");
const contentWrapper = moduleWrapper.getElementsByClassName("module-content");
contentWrapper[0].innerHTML = "";
contentWrapper[0].appendChild(newContent);
@ -234,7 +230,7 @@ var MM = (function () {
* @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the hide method.
*/
var hideModule = function (module, speed, callback, options) {
const hideModule = function (module, speed, callback, options) {
options = options || {};
// set lockString if set in options.
@ -245,7 +241,7 @@ var MM = (function () {
}
}
var moduleWrapper = document.getElementById(module.identifier);
const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper !== null) {
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
moduleWrapper.style.opacity = 0;
@ -280,12 +276,12 @@ var MM = (function () {
* @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the show method.
*/
var showModule = function (module, speed, callback, options) {
const showModule = function (module, speed, callback, options) {
options = options || {};
// remove lockString if set in options.
if (options.lockString) {
var index = module.lockStrings.indexOf(options.lockString);
const index = module.lockStrings.indexOf(options.lockString);
if (index !== -1) {
module.lockStrings.splice(index, 1);
}
@ -309,7 +305,7 @@ var MM = (function () {
module.lockStrings = [];
}
var moduleWrapper = document.getElementById(module.identifier);
const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper !== null) {
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
// Restore the position. See hideModule() for more info.
@ -318,7 +314,7 @@ var MM = (function () {
updateWrapperStates();
// Waiting for DOM-changes done in updateWrapperStates before we can start the animation.
var dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
const dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
moduleWrapper.style.opacity = 1;
clearTimeout(module.showHideTimer);
@ -346,14 +342,14 @@ var MM = (function () {
* an ugly top margin. By using this function, the top bar will be hidden if the
* update notification is not visible.
*/
var updateWrapperStates = function () {
var positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
const updateWrapperStates = function () {
const positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
positions.forEach(function (position) {
var wrapper = selectWrapper(position);
var moduleWrappers = wrapper.getElementsByClassName("module");
const wrapper = selectWrapper(position);
const moduleWrappers = wrapper.getElementsByClassName("module");
var showWrapper = false;
let showWrapper = false;
Array.prototype.forEach.call(moduleWrappers, function (moduleWrapper) {
if (moduleWrapper.style.position === "" || moduleWrapper.style.position === "static") {
showWrapper = true;
@ -367,7 +363,7 @@ var MM = (function () {
/**
* Loads the core config and combines it with the system defaults.
*/
var loadConfig = function () {
const loadConfig = function () {
// FIXME: Think about how to pass config around without breaking tests
/* eslint-disable */
if (typeof config === "undefined") {
@ -385,15 +381,14 @@ var MM = (function () {
*
* @param {Module[]} modules Array of modules.
*/
var setSelectionMethodsForModules = function (modules) {
const setSelectionMethodsForModules = function (modules) {
/**
* Filter modules with the specified classes.
*
* @param {string|string[]} className one or multiple classnames (array or space divided).
*
* @returns {Module[]} Filtered collection of modules.
*/
var withClass = function (className) {
const withClass = function (className) {
return modulesByClass(className, true);
};
@ -401,10 +396,9 @@ var MM = (function () {
* Filter modules without the specified classes.
*
* @param {string|string[]} className one or multiple classnames (array or space divided).
*
* @returns {Module[]} Filtered collection of modules.
*/
var exceptWithClass = function (className) {
const exceptWithClass = function (className) {
return modulesByClass(className, false);
};
@ -413,20 +407,18 @@ var MM = (function () {
*
* @param {string|string[]} className one or multiple classnames (array or space divided).
* @param {boolean} include if the filter should include or exclude the modules with the specific classes.
*
* @returns {Module[]} Filtered collection of modules.
*/
var modulesByClass = function (className, include) {
var searchClasses = className;
const modulesByClass = function (className, include) {
let searchClasses = className;
if (typeof className === "string") {
searchClasses = className.split(" ");
}
var newModules = modules.filter(function (module) {
var classes = module.data.classes.toLowerCase().split(" ");
const newModules = modules.filter(function (module) {
const classes = module.data.classes.toLowerCase().split(" ");
for (var c in searchClasses) {
var searchClass = searchClasses[c];
for (const searchClass of searchClasses) {
if (classes.indexOf(searchClass.toLowerCase()) !== -1) {
return include;
}
@ -445,8 +437,8 @@ var MM = (function () {
* @param {object} module The module instance to remove from the collection.
* @returns {Module[]} Filtered collection of modules.
*/
var exceptModule = function (module) {
var newModules = modules.filter(function (mod) {
const exceptModule = function (module) {
const newModules = modules.filter(function (mod) {
return mod.identifier !== module.identifier;
});
@ -459,7 +451,7 @@ var MM = (function () {
*
* @param {Function} callback The function to execute with the module as an argument.
*/
var enumerate = function (callback) {
const enumerate = function (callback) {
modules.map(function (module) {
callback(module);
});
@ -604,11 +596,11 @@ if (typeof Object.assign !== "function") {
if (target === undefined || target === null) {
throw new TypeError("Cannot convert undefined or null to object");
}
var output = Object(target);
for (var index = 1; index < arguments.length; index++) {
var source = arguments[index];
const output = Object(target);
for (let index = 1; index < arguments.length; index++) {
const source = arguments[index];
if (source !== undefined && source !== null) {
for (var nextKey in source) {
for (const nextKey in source) {
if (source.hasOwnProperty(nextKey)) {
output[nextKey] = source[nextKey];
}

View File

@ -6,7 +6,6 @@
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*
*/
var Module = Class.extend({
/*********************************************************
@ -82,16 +81,15 @@ var Module = Class.extend({
* @returns {HTMLElement|Promise} The dom or a promise with the dom to display.
*/
getDom: function () {
var self = this;
return new Promise(function (resolve) {
var div = document.createElement("div");
var template = self.getTemplate();
var templateData = self.getTemplateData();
return new Promise((resolve) => {
const div = document.createElement("div");
const template = this.getTemplate();
const templateData = this.getTemplateData();
// Check to see if we need to render a template string or a file.
if (/^.*((\.html)|(\.njk))$/.test(template)) {
// the template is a filename
self.nunjucksEnvironment().render(template, templateData, function (err, res) {
this.nunjucksEnvironment().render(template, templateData, function (err, res) {
if (err) {
Log.error(err);
}
@ -102,7 +100,7 @@ var Module = Class.extend({
});
} else {
// the template is a template string.
div.innerHTML = self.nunjucksEnvironment().renderString(template, templateData);
div.innerHTML = this.nunjucksEnvironment().renderString(template, templateData);
resolve(div);
}
@ -168,15 +166,13 @@ var Module = Class.extend({
return this._nunjucksEnvironment;
}
var self = this;
this._nunjucksEnvironment = new nunjucks.Environment(new nunjucks.WebLoader(this.file(""), { async: true }), {
trimBlocks: true,
lstripBlocks: true
});
this._nunjucksEnvironment.addFilter("translate", function (str, variables) {
return nunjucks.runtime.markSafe(self.translate(str, variables));
this._nunjucksEnvironment.addFilter("translate", (str, variables) => {
return nunjucks.runtime.markSafe(this.translate(str, variables));
});
return this._nunjucksEnvironment;
@ -192,14 +188,14 @@ var Module = Class.extend({
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
},
/*
/**
* Called when the module is hidden.
*/
suspend: function () {
Log.log(this.name + " is suspended.");
},
/*
/**
* Called when the module is shown.
*/
resume: function () {
@ -213,7 +209,7 @@ var Module = Class.extend({
/**
* Set the module data.
*
* @param {Module} data The module data
* @param {object} data The module data
*/
setData: function (data) {
this.data = data;
@ -245,9 +241,8 @@ var Module = Class.extend({
this._socket = new MMSocket(this.name);
}
var self = this;
this._socket.setNotificationCallback(function (notification, payload) {
self.socketNotificationReceived(notification, payload);
this._socket.setNotificationCallback((notification, payload) => {
this.socketNotificationReceived(notification, payload);
});
return this._socket;
@ -288,13 +283,12 @@ var Module = Class.extend({
* @param {Function} callback Function called when done.
*/
loadDependencies: function (funcName, callback) {
var self = this;
var dependencies = this[funcName]();
let dependencies = this[funcName]();
var loadNextDependency = function () {
const loadNextDependency = () => {
if (dependencies.length > 0) {
var nextDependency = dependencies[0];
Loader.loadFile(nextDependency, self, function () {
const nextDependency = dependencies[0];
Loader.loadFile(nextDependency, this, () => {
dependencies = dependencies.slice(1);
loadNextDependency();
});
@ -400,12 +394,11 @@ var Module = Class.extend({
callback = callback || function () {};
options = options || {};
var self = this;
MM.hideModule(
self,
this,
speed,
function () {
self.suspend();
() => {
this.suspend();
callback();
},
options
@ -464,9 +457,9 @@ var Module = Class.extend({
* @returns {object} the merged config
*/
function configMerge(result) {
var stack = Array.prototype.slice.call(arguments, 1);
var item;
var key;
const stack = Array.prototype.slice.call(arguments, 1);
let item, key;
while (stack.length) {
item = stack.shift();
for (key in item) {
@ -494,11 +487,11 @@ Module.create = function (name) {
return;
}
var moduleDefinition = Module.definitions[name];
var clonedDefinition = cloneObject(moduleDefinition);
const moduleDefinition = Module.definitions[name];
const clonedDefinition = cloneObject(moduleDefinition);
// Note that we clone the definition. Otherwise the objects are shared, which gives problems.
var ModuleClass = Module.extend(clonedDefinition);
const ModuleClass = Module.extend(clonedDefinition);
return new ModuleClass();
};
@ -526,14 +519,13 @@ Module.register = function (name, moduleDefinition) {
* number if a is smaller and 0 if they are the same
*/
function cmpVersions(a, b) {
var i, diff;
var regExStrip0 = /(\.0+)+$/;
var segmentsA = a.replace(regExStrip0, "").split(".");
var segmentsB = b.replace(regExStrip0, "").split(".");
var l = Math.min(segmentsA.length, segmentsB.length);
const regExStrip0 = /(\.0+)+$/;
const segmentsA = a.replace(regExStrip0, "").split(".");
const segmentsB = b.replace(regExStrip0, "").split(".");
const l = Math.min(segmentsA.length, segmentsB.length);
for (i = 0; i < l; i++) {
diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
for (let i = 0; i < l; i++) {
let diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
if (diff) {
return diff;
}

View File

@ -113,6 +113,32 @@ const NodeHelper = Class.extend({
}
});
NodeHelper.checkFetchStatus = function (response) {
// response.status >= 200 && response.status < 300
if (response.ok) {
return response;
} else {
throw Error(response.statusText);
}
};
/**
* Look at the specified error and return an appropriate error type, that
* can be translated to a detailed error message
*
* @param {Error} error the error from fetching something
* @returns {string} the string of the detailed error message in the translations
*/
NodeHelper.checkFetchError = function (error) {
let error_type = "MODULE_ERROR_UNSPECIFIED";
if (error.code === "EAI_AGAIN") {
error_type = "MODULE_ERROR_NO_CONNECTION";
} else if (error.message === "Unauthorized") {
error_type = "MODULE_ERROR_UNAUTHORIZED";
}
return error_type;
};
NodeHelper.create = function (moduleDefinition) {
return NodeHelper.extend(moduleDefinition);
};

View File

@ -6,49 +6,48 @@
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
var MMSocket = function (moduleName) {
var self = this;
const MMSocket = function (moduleName) {
if (typeof moduleName !== "string") {
throw new Error("Please set the module name for the MMSocket.");
}
self.moduleName = moduleName;
this.moduleName = moduleName;
// Private Methods
var base = "/";
let base = "/";
if (typeof config !== "undefined" && typeof config.basePath !== "undefined") {
base = config.basePath;
}
self.socket = io("/" + self.moduleName, {
this.socket = io("/" + this.moduleName, {
path: base + "socket.io"
});
var notificationCallback = function () {};
var onevent = self.socket.onevent;
self.socket.onevent = function (packet) {
var args = packet.data || [];
onevent.call(this, packet); // original call
let notificationCallback = function () {};
const onevent = this.socket.onevent;
this.socket.onevent = (packet) => {
const args = packet.data || [];
onevent.call(this.socket, packet); // original call
packet.data = ["*"].concat(args);
onevent.call(this, packet); // additional call to catch-all
onevent.call(this.socket, packet); // additional call to catch-all
};
// register catch all.
self.socket.on("*", function (notification, payload) {
this.socket.on("*", (notification, payload) => {
if (notification !== "*") {
notificationCallback(notification, payload);
}
});
// Public Methods
this.setNotificationCallback = function (callback) {
this.setNotificationCallback = (callback) => {
notificationCallback = callback;
};
this.sendNotification = function (notification, payload) {
this.sendNotification = (notification, payload) => {
if (typeof payload === "undefined") {
payload = {};
}
self.socket.emit(notification, payload);
this.socket.emit(notification, payload);
};
};

View File

@ -79,7 +79,7 @@ Module.register("alert", {
//If module already has an open alert close it
if (this.alerts[sender.name]) {
this.hide_alert(sender);
this.hide_alert(sender, false);
}
//Display title and message only if they are provided in notification parameters
@ -114,10 +114,10 @@ Module.register("alert", {
}, params.timer);
}
},
hide_alert: function (sender) {
hide_alert: function (sender, close = true) {
//Dismiss alert and remove from this.alerts
if (this.alerts[sender.name]) {
this.alerts[sender.name].dismiss();
this.alerts[sender.name].dismiss(close);
this.alerts[sender.name] = null;
//Remove overlay
const overlay = document.getElementById("overlay");

View File

@ -6,7 +6,6 @@
line-height: 1.4;
margin-bottom: 10px;
z-index: 1;
color: black;
font-size: 70%;
position: relative;
display: table;
@ -15,17 +14,17 @@
border-width: 1px;
border-radius: 5px;
border-style: solid;
border-color: #666;
border-color: var(--color-text-dimmed);
}
.ns-alert {
border-style: solid;
border-color: #fff;
border-color: var(--color-text-bright);
padding: 17px;
line-height: 1.4;
margin-bottom: 10px;
z-index: 3;
color: white;
color: var(--color-text-bright);
font-size: 70%;
position: fixed;
text-align: center;

View File

@ -122,8 +122,10 @@
/**
* Dismiss the notification
*
* @param {boolean} [close] call the onClose callback at the end
*/
NotificationFx.prototype.dismiss = function () {
NotificationFx.prototype.dismiss = function (close = true) {
this.active = false;
clearTimeout(this.dismissttl);
this.ntf.classList.remove("ns-show");
@ -131,7 +133,7 @@
this.ntf.classList.add("ns-hide");
// callback
this.options.onClose();
if (close) this.options.onClose();
}, 25);
// after animation ends remove ntf from the DOM

View File

@ -1,13 +1,14 @@
.calendar .symbol {
display: flex;
flex-direction: row;
justify-content: flex-end;
padding-left: 0;
padding-right: 10px;
font-size: 80%;
vertical-align: top;
font-size: var(--font-size-small);
}
.calendar .symbol span {
display: inline-block;
transform: translate(0, 2px);
padding-top: 4px;
}
.calendar .title {

View File

@ -84,7 +84,7 @@ Module.register("calendar", {
// Override start method.
start: function () {
Log.log("Starting module: " + this.name);
Log.info("Starting module: " + this.name);
// Set locale.
moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
@ -140,17 +140,17 @@ Module.register("calendar", {
if (notification === "CALENDAR_EVENTS") {
if (this.hasCalendarURL(payload.url)) {
this.calendarData[payload.url] = payload.events;
this.error = null;
this.loaded = true;
if (this.config.broadcastEvents) {
this.broadcastEvents();
}
}
} else if (notification === "FETCH_ERROR") {
Log.error("Calendar Error. Could not fetch calendar: " + payload.url);
} else if (notification === "CALENDAR_ERROR") {
let error_message = this.translate(payload.error_type);
this.error = this.translate("MODULE_CONFIG_ERROR", { MODULE_NAME: this.name, ERROR: error_message });
this.loaded = true;
} else if (notification === "INCORRECT_URL") {
Log.error("Calendar Error. Incorrect url: " + payload.url);
}
this.updateDom(this.config.animationSpeed);
@ -168,6 +168,12 @@ Module.register("calendar", {
const wrapper = document.createElement("table");
wrapper.className = this.config.tableClass;
if (this.error) {
wrapper.innerHTML = this.error;
wrapper.className = this.config.tableClass + " dimmed";
return wrapper;
}
if (events.length === 0) {
wrapper.innerHTML = this.loaded ? this.translate("EMPTY") : this.translate("LOADING");
wrapper.className = this.config.tableClass + " dimmed";
@ -305,15 +311,14 @@ Module.register("calendar", {
if (this.config.timeFormat === "dateheaders") {
if (event.fullDayEvent) {
titleWrapper.colSpan = "2";
titleWrapper.align = "left";
titleWrapper.classList.add("align-left");
} else {
const timeWrapper = document.createElement("td");
timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
timeWrapper.align = "left";
timeWrapper.className = "time light align-left " + this.timeClassForUrl(event.url);
timeWrapper.style.paddingLeft = "2px";
timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
eventWrapper.appendChild(timeWrapper);
titleWrapper.align = "right";
titleWrapper.classList.add("align-right");
}
eventWrapper.appendChild(titleWrapper);
@ -366,13 +371,14 @@ Module.register("calendar", {
if (event.startDate >= now) {
// Use relative time
if (!this.config.hideTime) {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
} else {
timeWrapper.innerHTML = this.capFirst(
moment(event.startDate, "x").calendar(null, {
sameDay: "[" + this.translate("TODAY") + "]",
nextDay: "[" + this.translate("TOMORROW") + "]",
nextWeek: "dddd"
nextWeek: "dddd",
sameElse: this.config.dateFormat
})
);
}

View File

@ -6,6 +6,7 @@
*/
const CalendarUtils = require("./calendarutils");
const Log = require("logger");
const NodeHelper = require("node_helper");
const ical = require("node-ical");
const fetch = require("node-fetch");
const digest = require("digest-fetch");
@ -52,27 +53,17 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
if (auth.method === "bearer") {
headers.Authorization = "Bearer " + auth.pass;
} else if (auth.method === "digest") {
fetcher = new digest(auth.user, auth.pass).fetch(url, { headers: headers, httpsAgent: httpsAgent });
fetcher = new digest(auth.user, auth.pass).fetch(url, { headers: headers, agent: httpsAgent });
} else {
headers.Authorization = "Basic " + Buffer.from(auth.user + ":" + auth.pass).toString("base64");
}
}
if (fetcher === null) {
fetcher = fetch(url, { headers: headers, httpsAgent: httpsAgent });
fetcher = fetch(url, { headers: headers, agent: httpsAgent });
}
fetcher
.catch((error) => {
fetchFailedCallback(this, error);
scheduleTimer();
})
.then((response) => {
if (response.status !== 200) {
fetchFailedCallback(this, response.statusText);
scheduleTimer();
}
return response;
})
.then(NodeHelper.checkFetchStatus)
.then((response) => response.text())
.then((responseData) => {
let data = [];
@ -87,12 +78,16 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
maximumNumberOfDays
});
} catch (error) {
fetchFailedCallback(this, error.message);
fetchFailedCallback(this, error);
scheduleTimer();
return;
}
this.broadcastEvents();
scheduleTimer();
})
.catch((error) => {
fetchFailedCallback(this, error);
scheduleTimer();
});
};

View File

@ -18,8 +18,8 @@ const CalendarUtils = {
* Calculate the time correction, either dst/std or full day in cases where
* utc time is day before plus offset
*
* @param {object} event
* @param {Date} date
* @param {object} event the event which needs adjustement
* @param {Date} date the date on which this event happens
* @returns {number} the necessary adjustment in hours
*/
calculateTimezoneAdjustment: function (event, date) {
@ -117,6 +117,13 @@ const CalendarUtils = {
return adjustHours;
},
/**
* Filter the events from ical according to the given config
*
* @param {object} data the calendar data from ical
* @param {object} config The configuration object
* @returns {string[]} the filtered events
*/
filterEvents: function (data, config) {
const newEvents = [];
@ -500,8 +507,8 @@ const CalendarUtils = {
/**
* Lookup iana tz from windows
*
* @param msTZName
* @returns {*|null}
* @param {string} msTZName the timezone name to lookup
* @returns {string|null} the iana name or null of none is found
*/
getIanaTZFromMS: function (msTZName) {
// Get hash entry
@ -571,12 +578,13 @@ const CalendarUtils = {
},
/**
* Determines if the user defined title filter should apply
*
* @param title
* @param filter
* @param useRegex
* @param regexFlags
* @returns {boolean|*}
* @param {string} title the title of the event
* @param {string} filter the string to look for, can be a regex also
* @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string
* @param {string} regexFlags flags that should be applied to the regex
* @returns {boolean} True if the title should be filtered out, false otherwise
*/
titleFilterApplies: function (title, filter, useRegex, regexFlags) {
if (useRegex) {

View File

@ -5,6 +5,9 @@
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
// Alias modules mentioned in package.js under _moduleAliases.
require("module-alias/register");
const CalendarFetcher = require("./calendarfetcher.js");
const url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL
@ -26,11 +29,13 @@ const fetcher = new CalendarFetcher(url, fetchInterval, [], maximumEntries, maxi
fetcher.onReceive(function (fetcher) {
console.log(fetcher.events());
console.log("------------------------------------------------------------");
process.exit(0);
});
fetcher.onError(function (fetcher, error) {
console.log("Fetcher error:");
console.log(error);
process.exit(1);
});
fetcher.startFetch();

View File

@ -40,13 +40,14 @@ module.exports = NodeHelper.create({
try {
new URL(url);
} catch (error) {
this.sendSocketNotification("INCORRECT_URL", { id: identifier, url: url });
Log.error("Calendar Error. Malformed calendar url: ", url, error);
this.sendSocketNotification("CALENDAR_ERROR", { error_type: "MODULE_ERROR_MALFORMED_URL" });
return;
}
let fetcher;
if (typeof this.fetchers[identifier + url] === "undefined") {
Log.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval);
Log.log("Create new calendarfetcher for url: " + url + " - Interval: " + fetchInterval);
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
fetcher.onReceive((fetcher) => {
@ -55,16 +56,16 @@ module.exports = NodeHelper.create({
fetcher.onError((fetcher, error) => {
Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error);
this.sendSocketNotification("FETCH_ERROR", {
let error_type = NodeHelper.checkFetchError(error);
this.sendSocketNotification("CALENDAR_ERROR", {
id: identifier,
url: fetcher.url(),
error: error
error_type
});
});
this.fetchers[identifier + url] = fetcher;
} else {
Log.log("Use existing calendar fetcher for url: " + url);
Log.log("Use existing calendarfetcher for url: " + url);
fetcher = this.fetchers[identifier + url];
fetcher.broadcastEvents();
}

View File

@ -46,62 +46,61 @@ Module.register("clock", {
Log.info("Starting module: " + this.name);
// Schedule update interval.
var self = this;
self.second = moment().second();
self.minute = moment().minute();
this.second = moment().second();
this.minute = moment().minute();
//Calculate how many ms should pass until next update depending on if seconds is displayed or not
var delayCalculator = function (reducedSeconds) {
var EXTRA_DELAY = 50; //Deliberate imperceptable delay to prevent off-by-one timekeeping errors
// Calculate how many ms should pass until next update depending on if seconds is displayed or not
const delayCalculator = (reducedSeconds) => {
const EXTRA_DELAY = 50; // Deliberate imperceptible delay to prevent off-by-one timekeeping errors
if (self.config.displaySeconds) {
if (this.config.displaySeconds) {
return 1000 - moment().milliseconds() + EXTRA_DELAY;
} else {
return (60 - reducedSeconds) * 1000 - moment().milliseconds() + EXTRA_DELAY;
}
};
//A recursive timeout function instead of interval to avoid drifting
var notificationTimer = function () {
self.updateDom();
// A recursive timeout function instead of interval to avoid drifting
const notificationTimer = () => {
this.updateDom();
//If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent)
if (self.config.displaySeconds) {
self.second = moment().second();
if (self.second !== 0) {
self.sendNotification("CLOCK_SECOND", self.second);
// If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent)
if (this.config.displaySeconds) {
this.second = moment().second();
if (this.second !== 0) {
this.sendNotification("CLOCK_SECOND", this.second);
setTimeout(notificationTimer, delayCalculator(0));
return;
}
}
//If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
self.minute = moment().minute();
self.sendNotification("CLOCK_MINUTE", self.minute);
// If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
this.minute = moment().minute();
this.sendNotification("CLOCK_MINUTE", this.minute);
setTimeout(notificationTimer, delayCalculator(0));
};
//Set the initial timeout with the amount of seconds elapsed as reducedSeconds so it will trigger when the minute changes
setTimeout(notificationTimer, delayCalculator(self.second));
// Set the initial timeout with the amount of seconds elapsed as reducedSeconds so it will trigger when the minute changes
setTimeout(notificationTimer, delayCalculator(this.second));
// Set locale.
moment.locale(config.language);
},
// Override dom generator.
getDom: function () {
var wrapper = document.createElement("div");
const wrapper = document.createElement("div");
/************************************
* Create wrappers for DIGITAL clock
*/
var dateWrapper = document.createElement("div");
var timeWrapper = document.createElement("div");
var secondsWrapper = document.createElement("sup");
var periodWrapper = document.createElement("span");
var sunWrapper = document.createElement("div");
var moonWrapper = document.createElement("div");
var weekWrapper = document.createElement("div");
const dateWrapper = document.createElement("div");
const timeWrapper = document.createElement("div");
const secondsWrapper = document.createElement("sup");
const periodWrapper = document.createElement("span");
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";
@ -114,14 +113,13 @@ Module.register("clock", {
// The moment().format("h") method has a bug on the Raspberry Pi.
// So we need to generate the timestring manually.
// See issue: https://github.com/MichMich/MagicMirror/issues/181
var timeString;
var now = moment();
this.lastDisplayedMinute = now.minute();
let timeString;
const now = moment();
if (this.config.timezone) {
now.tz(this.config.timezone);
}
var hourSymbol = "HH";
let hourSymbol = "HH";
if (this.config.timeFormat !== 24) {
hourSymbol = "h";
}
@ -160,7 +158,7 @@ Module.register("clock", {
* @returns {string} The formatted time string
*/
function formatTime(config, time) {
var formatString = hourSymbol + ":mm";
let formatString = hourSymbol + ":mm";
if (config.showPeriod && config.timeFormat !== 24) {
formatString += config.showPeriodUpper ? "A" : "a";
}
@ -170,7 +168,7 @@ Module.register("clock", {
if (this.config.showSunTimes) {
const sunTimes = SunCalc.getTimes(now, this.config.lat, this.config.lon);
const isVisible = now.isBetween(sunTimes.sunrise, sunTimes.sunset);
var nextEvent;
let nextEvent;
if (now.isBefore(sunTimes.sunrise)) {
nextEvent = sunTimes.sunrise;
} else if (now.isBefore(sunTimes.sunset)) {
@ -198,7 +196,7 @@ Module.register("clock", {
const moonIllumination = SunCalc.getMoonIllumination(now.toDate());
const moonTimes = SunCalc.getMoonTimes(now, this.config.lat, this.config.lon);
const moonRise = moonTimes.rise;
var moonSet;
let moonSet;
if (moment(moonTimes.set).isAfter(moonTimes.rise)) {
moonSet = moonTimes.set;
} else {
@ -224,6 +222,7 @@ Module.register("clock", {
/****************************************************************
* 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
@ -232,12 +231,11 @@ Module.register("clock", {
if (this.config.timezone) {
now.tz(this.config.timezone);
}
var second = now.seconds() * 6,
const second = now.seconds() * 6,
minute = now.minute() * 6 + second / 60,
hour = ((now.hours() % 12) / 12) * 360 + 90 + minute / 12;
// Create wrappers
var clockCircle = document.createElement("div");
clockCircle.className = "clockCircle";
clockCircle.style.width = this.config.analogSize;
clockCircle.style.height = this.config.analogSize;
@ -252,14 +250,14 @@ Module.register("clock", {
} else if (this.config.analogFace !== "none") {
clockCircle.style.border = "2px solid white";
}
var clockFace = document.createElement("div");
const clockFace = document.createElement("div");
clockFace.className = "clockFace";
var clockHour = document.createElement("div");
const clockHour = document.createElement("div");
clockHour.id = "clockHour";
clockHour.style.transform = "rotate(" + hour + "deg)";
clockHour.className = "clockHour";
var clockMinute = document.createElement("div");
const clockMinute = document.createElement("div");
clockMinute.id = "clockMinute";
clockMinute.style.transform = "rotate(" + minute + "deg)";
clockMinute.className = "clockMinute";
@ -269,7 +267,7 @@ Module.register("clock", {
clockFace.appendChild(clockMinute);
if (this.config.displaySeconds) {
var clockSecond = document.createElement("div");
const clockSecond = document.createElement("div");
clockSecond.id = "clockSecond";
clockSecond.style.transform = "rotate(" + second + "deg)";
clockSecond.className = "clockSecond";
@ -312,14 +310,14 @@ Module.register("clock", {
}
} else {
// Both clocks have been configured, check position
var placement = this.config.analogPlacement;
const placement = this.config.analogPlacement;
var analogWrapper = document.createElement("div");
const analogWrapper = document.createElement("div");
analogWrapper.id = "analog";
analogWrapper.style.cssFloat = "none";
analogWrapper.appendChild(clockCircle);
var digitalWrapper = document.createElement("div");
const digitalWrapper = document.createElement("div");
digitalWrapper.id = "digital";
digitalWrapper.style.cssFloat = "none";
digitalWrapper.appendChild(dateWrapper);
@ -328,8 +326,8 @@ Module.register("clock", {
digitalWrapper.appendChild(moonWrapper);
digitalWrapper.appendChild(weekWrapper);
var appendClocks = function (condition, pos1, pos2) {
var padding = [0, 0, 0, 0];
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) {

View File

@ -17,7 +17,7 @@
width: 6px;
height: 6px;
margin: -3px 0 0 -3px;
background: white;
background: var(--color-text-bright);
border-radius: 3px;
content: "";
display: block;
@ -29,9 +29,9 @@
position: absolute;
top: 50%;
left: 50%;
margin: -2px 0 -2px -25%; /* numbers much match negative length & thickness */
margin: -2px 0 -2px -25%; /* numbers must match negative length & thickness */
padding: 2px 0 2px 25%; /* indicator length & thickness */
background: white;
background: var(--color-text-bright);
transform-origin: 100% 50%;
border-radius: 3px 0 0 3px;
}
@ -44,7 +44,7 @@
left: 50%;
margin: -35% -2px 0; /* numbers must match negative length & thickness */
padding: 35% 2px 0; /* indicator length & thickness */
background: white;
background: var(--color-text-bright);
transform-origin: 50% 100%;
border-radius: 3px 0 0 3px;
}
@ -57,7 +57,7 @@
left: 50%;
margin: -38% -1px 0 0; /* numbers must match negative length & thickness */
padding: 38% 1px 0 0; /* indicator length & thickness */
background: #888;
background: var(--color-text);
transform-origin: 50% 100%;
}

View File

@ -39,37 +39,35 @@ Module.register("compliments", {
this.lastComplimentIndex = -1;
var self = this;
if (this.config.remoteFile !== null) {
this.complimentFile(function (response) {
self.config.compliments = JSON.parse(response);
self.updateDom();
this.complimentFile((response) => {
this.config.compliments = JSON.parse(response);
this.updateDom();
});
}
// Schedule update timer.
setInterval(function () {
self.updateDom(self.config.fadeSpeed);
setInterval(() => {
this.updateDom(this.config.fadeSpeed);
}, this.config.updateInterval);
},
/* randomIndex(compliments)
/**
* Generate a random index for a list of compliments.
*
* argument compliments Array<String> - Array with compliments.
*
* return Number - Random index.
* @param {string[]} compliments Array with compliments.
* @returns {number} a random index of given array
*/
randomIndex: function (compliments) {
if (compliments.length === 1) {
return 0;
}
var generate = function () {
const generate = function () {
return Math.floor(Math.random() * compliments.length);
};
var complimentIndex = generate();
let complimentIndex = generate();
while (complimentIndex === this.lastComplimentIndex) {
complimentIndex = generate();
@ -80,15 +78,15 @@ Module.register("compliments", {
return complimentIndex;
},
/* complimentArray()
/**
* Retrieve an array of compliments for the time of the day.
*
* return compliments Array<String> - Array with compliments for the time of the day.
* @returns {string[]} array with compliments for the time of the day.
*/
complimentArray: function () {
var hour = moment().hour();
var date = this.config.mockDate ? this.config.mockDate : moment().format("YYYY-MM-DD");
var compliments;
const hour = moment().hour();
const date = this.config.mockDate ? this.config.mockDate : moment().format("YYYY-MM-DD");
let compliments;
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime && this.config.compliments.hasOwnProperty("morning")) {
compliments = this.config.compliments.morning.slice(0);
@ -99,7 +97,7 @@ Module.register("compliments", {
}
if (typeof compliments === "undefined") {
compliments = new Array();
compliments = [];
}
if (this.currentWeatherType in this.config.compliments) {
@ -108,7 +106,7 @@ Module.register("compliments", {
compliments.push.apply(compliments, this.config.compliments.anytime);
for (var entry in this.config.compliments) {
for (let entry in this.config.compliments) {
if (new RegExp(entry).test(date)) {
compliments.push.apply(compliments, this.config.compliments[entry]);
}
@ -117,11 +115,13 @@ Module.register("compliments", {
return compliments;
},
/* complimentFile(callback)
/**
* Retrieve a file from the local filesystem
*
* @param {Function} callback Called when the file is retrieved.
*/
complimentFile: function (callback) {
var xobj = new XMLHttpRequest(),
const xobj = new XMLHttpRequest(),
isRemote = this.config.remoteFile.indexOf("http://") === 0 || this.config.remoteFile.indexOf("https://") === 0,
path = isRemote ? this.config.remoteFile : this.file(this.config.remoteFile);
xobj.overrideMimeType("application/json");
@ -134,16 +134,16 @@ Module.register("compliments", {
xobj.send(null);
},
/* complimentArray()
/**
* Retrieve a random compliment.
*
* return compliment string - A compliment.
* @returns {string} a compliment
*/
randomCompliment: function () {
// get the current time of day compliments list
var compliments = this.complimentArray();
const compliments = this.complimentArray();
// variable for index to next message to display
let index = 0;
let index;
// are we randomizing
if (this.config.random) {
// yes
@ -159,16 +159,16 @@ Module.register("compliments", {
// Override dom generator.
getDom: function () {
var wrapper = document.createElement("div");
const wrapper = document.createElement("div");
wrapper.className = this.config.classes ? this.config.classes : "thin xlarge bright pre-line";
// get the compliment text
var complimentText = this.randomCompliment();
const complimentText = this.randomCompliment();
// split it into parts on newline text
var parts = complimentText.split("\n");
const parts = complimentText.split("\n");
// create a span to hold it all
var compliment = document.createElement("span");
const compliment = document.createElement("span");
// process all the parts of the compliment text
for (var part of parts) {
for (const part of parts) {
// create a text element for each part
compliment.appendChild(document.createTextNode(part));
// add a break `

View File

@ -1,13 +1,10 @@
/* Magic Mirror
* Default Modules List
/* Magic Mirror Default Modules List
* Modules listed below can be loaded without the 'default/' prefix. Omitting the default folder name.
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
// Modules listed below can be loaded without the 'default/' prefix. Omitting the default folder name.
var defaultModules = ["alert", "calendar", "clock", "compliments", "currentweather", "helloworld", "newsfeed", "weatherforecast", "updatenotification", "weather"];
const defaultModules = ["alert", "calendar", "clock", "compliments", "currentweather", "helloworld", "newsfeed", "weatherforecast", "updatenotification", "weather"];
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@ -12,3 +12,12 @@ iframe.newsfeed-fullarticle {
bottom: inherit;
top: -90px;
}
.newsfeed-list {
list-style: none;
}
.newsfeed-list li {
text-align: justify;
margin-bottom: 0.5em;
}

View File

@ -14,6 +14,7 @@ Module.register("newsfeed", {
encoding: "UTF-8" //ISO-8859-1
}
],
showAsList: false,
showSourceTitle: true,
showPublishDate: true,
broadcastNewsFeeds: true,
@ -89,8 +90,8 @@ Module.register("newsfeed", {
this.loaded = true;
this.error = null;
} else if (notification === "INCORRECT_URL") {
this.error = `Incorrect url: ${payload.url}`;
} else if (notification === "NEWSFEED_ERROR") {
this.error = this.translate(payload.error_type);
this.scheduleUpdateInterval();
}
},
@ -128,6 +129,10 @@ Module.register("newsfeed", {
}
const item = this.newsItems[this.activeItem];
const items = this.newsItems.map(function (item) {
item.publishDate = moment(new Date(item.pubdate)).fromNow();
return item;
});
return {
loaded: true,
@ -135,7 +140,8 @@ Module.register("newsfeed", {
sourceTitle: item.sourceTitle,
publishDate: moment(new Date(item.pubdate)).fromNow(),
title: item.title,
description: item.description
description: item.description,
items: items
};
},
@ -183,16 +189,15 @@ Module.register("newsfeed", {
}
if (this.config.prohibitedWords.length > 0) {
newsItems = newsItems.filter(function (value) {
newsItems = newsItems.filter(function (item) {
for (let word of this.config.prohibitedWords) {
if (value["title"].toLowerCase().indexOf(word.toLowerCase()) > -1) {
if (item.title.toLowerCase().indexOf(word.toLowerCase()) > -1) {
return false;
}
}
return true;
}, this);
}
newsItems.forEach((item) => {
//Remove selected tags from the beginning of rss feed items (title or description)
if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") {

View File

@ -1,4 +1,34 @@
{% if loaded %}
{% if config.showAsList %}
<ul class="newsfeed-list">
{% for item in items %}
<li>
{% if (config.showSourceTitle and item.sourceTitle) or config.showPublishDate %}
<div class="newsfeed-source light small dimmed">
{% if item.sourceTitle and config.showSourceTitle %}
{{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}: {% endif %}
{% endif %}
{% if config.showPublishDate %}
{{ item.publishDate }}:
{% endif %}
</div>
{% endif %}
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
{{ item.title }}
</div>
{% if config.showDescription %}
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
{% if config.truncDescription %}
{{ item.description | truncate(config.lengthDescription) }}
{% else %}
{{ item.description }}
{% endif %}
</div>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<div>
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
<div class="newsfeed-source light small dimmed">
@ -13,6 +43,7 @@
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
{{ title }}
</div>
{% if config.showDescription %}
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
{% if config.truncDescription %}
{{ description | truncate(config.lengthDescription) }}
@ -20,7 +51,9 @@
{{ description }}
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
{% elseif error %}
<div class="small dimmed">
{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}

View File

@ -6,6 +6,7 @@
*/
const Log = require("logger");
const FeedMe = require("feedme");
const NodeHelper = require("node_helper");
const fetch = require("node-fetch");
const iconv = require("iconv-lite");
@ -84,12 +85,13 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
};
fetch(url, { headers: headers })
.then(NodeHelper.checkFetchStatus)
.then((response) => {
response.body.pipe(iconv.decodeStream(encoding)).pipe(parser);
})
.catch((error) => {
fetchFailedCallback(this, error);
scheduleTimer();
})
.then((res) => {
res.body.pipe(iconv.decodeStream(encoding)).pipe(parser);
});
};

View File

@ -27,8 +27,8 @@ module.exports = NodeHelper.create({
* Creates a fetcher for a new feed if it doesn't exist yet.
* Otherwise it reuses the existing one.
*
* @param {object} feed The feed object.
* @param {object} config The configuration object.
* @param {object} feed The feed object
* @param {object} config The configuration object
*/
createFetcher: function (feed, config) {
const url = feed.url || "";
@ -38,13 +38,14 @@ module.exports = NodeHelper.create({
try {
new URL(url);
} catch (error) {
this.sendSocketNotification("INCORRECT_URL", { url: url });
Log.error("Newsfeed Error. Malformed newsfeed url: ", url, error);
this.sendSocketNotification("NEWSFEED_ERROR", { error_type: "MODULE_ERROR_MALFORMED_URL" });
return;
}
let fetcher;
if (typeof this.fetchers[url] === "undefined") {
Log.log("Create new news fetcher for url: " + url + " - Interval: " + reloadInterval);
Log.log("Create new newsfetcher for url: " + url + " - Interval: " + reloadInterval);
fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings);
fetcher.onReceive(() => {
@ -52,15 +53,16 @@ module.exports = NodeHelper.create({
});
fetcher.onError((fetcher, error) => {
this.sendSocketNotification("FETCH_ERROR", {
url: fetcher.url(),
error: error
Log.error("Newsfeed Error. Could not fetch newsfeed: ", url, error);
let error_type = NodeHelper.checkFetchError(error);
this.sendSocketNotification("NEWSFEED_ERROR", {
error_type
});
});
this.fetchers[url] = fetcher;
} else {
Log.log("Use existing news fetcher for url: " + url);
Log.log("Use existing newsfetcher for url: " + url);
fetcher = this.fetchers[url];
fetcher.setReloadInterval(reloadInterval);
fetcher.broadcastItems();

View File

@ -5,33 +5,35 @@
* MIT Licensed.
*/
Module.register("updatenotification", {
// Define module defaults
defaults: {
updateInterval: 10 * 60 * 1000, // every 10 minutes
refreshInterval: 24 * 60 * 60 * 1000, // one day
ignoreModules: [],
timeout: 1000
timeout: 5000
},
suspended: false,
moduleList: {},
// Override start method.
start: function () {
var self = this;
Log.log("Start updatenotification");
Log.info("Starting module: " + this.name);
setInterval(() => {
self.moduleList = {};
self.updateDom(2);
}, self.config.refreshInterval);
this.moduleList = {};
this.updateDom(2);
}, this.config.refreshInterval);
},
notificationReceived: function (notification, payload, sender) {
if (notification === "DOM_OBJECTS_CREATED") {
this.sendSocketNotification("CONFIG", this.config);
this.sendSocketNotification("MODULES", Module.definitions);
//this.hide(0, { lockString: self.identifier });
//this.hide(0, { lockString: this.identifier });
}
},
// Override socket notification handler.
socketNotificationReceived: function (notification, payload) {
if (notification === "STATUS") {
this.updateUI(payload);
@ -39,13 +41,12 @@ Module.register("updatenotification", {
},
updateUI: function (payload) {
var self = this;
if (payload && payload.behind > 0) {
// if we haven't seen info for this module
if (this.moduleList[payload.module] === undefined) {
// save it
this.moduleList[payload.module] = payload;
self.updateDom(2);
this.updateDom(2);
}
//self.show(1000, { lockString: self.identifier });
} else if (payload && payload.behind === 0) {
@ -53,41 +54,41 @@ Module.register("updatenotification", {
if (this.moduleList[payload.module] !== undefined) {
// remove it
delete this.moduleList[payload.module];
self.updateDom(2);
this.updateDom(2);
}
}
},
diffLink: function (module, text) {
var localRef = module.hash;
var remoteRef = module.tracking.replace(/.*\//, "");
const localRef = module.hash;
const remoteRef = module.tracking.replace(/.*\//, "");
return '<a href="https://github.com/MichMich/MagicMirror/compare/' + localRef + "..." + remoteRef + '" ' + 'class="xsmall dimmed" ' + 'style="text-decoration: none;" ' + 'target="_blank" >' + text + "</a>";
},
// Override dom generator.
getDom: function () {
var wrapper = document.createElement("div");
const wrapper = document.createElement("div");
if (this.suspended === false) {
// process the hash of module info found
for (var key of Object.keys(this.moduleList)) {
for (const key of Object.keys(this.moduleList)) {
let m = this.moduleList[key];
var message = document.createElement("div");
const message = document.createElement("div");
message.className = "small bright";
var icon = document.createElement("i");
const icon = document.createElement("i");
icon.className = "fa fa-exclamation-circle";
icon.innerHTML = "&nbsp;";
message.appendChild(icon);
var updateInfoKeyName = m.behind === 1 ? "UPDATE_INFO_SINGLE" : "UPDATE_INFO_MULTIPLE";
const updateInfoKeyName = m.behind === 1 ? "UPDATE_INFO_SINGLE" : "UPDATE_INFO_MULTIPLE";
var subtextHtml = this.translate(updateInfoKeyName, {
let subtextHtml = this.translate(updateInfoKeyName, {
COMMIT_COUNT: m.behind,
BRANCH_NAME: m.current
});
var text = document.createElement("span");
const text = document.createElement("span");
if (m.module === "default") {
text.innerHTML = this.translate("UPDATE_NOTIFICATION");
subtextHtml = this.diffLink(m, subtextHtml);
@ -100,7 +101,7 @@ Module.register("updatenotification", {
wrapper.appendChild(message);
var subtext = document.createElement("div");
const subtext = document.createElement("div");
subtext.innerHTML = subtextHtml;
subtext.className = "xsmall dimmed";
wrapper.appendChild(subtext);

View File

@ -5,25 +5,31 @@
{% 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 %}>
{% if (currentStep == 0) %}
{% if (currentStep == 0) and config.ignoreToday == false %}
<td class="day">{{ "TODAY" | translate }}</td>
{% elif (currentStep == 1) %}
{% elif (currentStep == 1) and config.ignoreToday == false %}
<td class="day">{{ "TOMORROW" | translate }}</td>
{% else %}
<td class="day">{{ f.date.format('ddd') }}</td>
{% endif %}
<td class="bright weather-icon"><span class="wi weathericon wi-{{ f.weatherType }}"></span></td>
<td class="align-right bright max-temp">
{{ f.maxTemperature | roundValue | unit("temperature") }}
{{ f.maxTemperature | roundValue | unit("temperature") | decimalSymbol }}
</td>
<td class="align-right min-temp">
{{ f.minTemperature | roundValue | unit("temperature") }}
{{ f.minTemperature | roundValue | unit("temperature") | decimalSymbol }}
</td>
{% if config.showPrecipitationAmount %}
{% if f.precipitationUnits %}
<td class="align-right bright precipitation">
{{ f.precipitation }}{{ f.precipitationUnits }}
</td>
{% else %}
<td class="align-right bright precipitation">
{{ f.precipitation | unit("precip") }}
</td>
{% endif %}
{% endif %}
</tr>
{% set currentStep = currentStep + 1 %}
{% endfor %}

View File

@ -11,10 +11,16 @@
{{ hour.temperature | roundValue | unit("temperature") }}
</td>
{% if config.showPrecipitationAmount %}
{% if hour.precipitationUnits %}
<td class="align-right bright precipitation">
{{ hour.precipitation }}{{ hour.precipitationUnits }}
</td>
{% else %}
<td class="align-right bright precipitation">
{{ hour.precipitation | unit("precip") }}
</td>
{% endif %}
{% endif %}
</tr>
{% set currentStep = currentStep + 1 %}
{% endfor %}

View File

@ -0,0 +1,649 @@
/* global WeatherProvider, WeatherObject */
/* Magic Mirror
* Module: Weather
* Provider: Environment Canada (EC)
*
* This class is a provider for Environment Canada MSC Datamart
* Note that this is only for Canadian locations and does not require an API key (access is anonymous)
*
* EC Documentation at following links:
* https://dd.weather.gc.ca/citypage_weather/schema/
* https://eccc-msc.github.io/open-data/msc-datamart/readme_en/
*
* This module supports Canadian locations only and requires 2 additional config parms:
*
* siteCode - the city/town unique identifier for which weather is to be displayed. Format is 's0000000'.
*
* provCode - the 2-character province code for the selected city/town.
*
* Example: for Toronto, Ontario, the following parms would be used
*
* siteCode: 's0000458',
* provCode: 'ON'
*
* To determine the siteCode and provCode values for a Canadian city/town, look at the Environment Canada document
* at https://dd.weather.gc.ca/citypage_weather/docs/site_list_en.csv (or site_list_fr.csv). There you will find a table
* with locations you can search under column B (English Names), with the corresponding siteCode under
* column A (Codes) and provCode under column C (Province).
*
* Original by Kevin Godin
*
* License to use Environment Canada (EC) data is detailed here:
* https://eccc-msc.github.io/open-data/licence/readme_en/
*
*/
WeatherProvider.register("envcanada", {
// Set the name of the provider for debugging and alerting purposes (eg. provide eye-catcher)
providerName: "Environment Canada",
// Set the default config properties that is specific to this provider
defaults: {
siteCode: "s1234567",
provCode: "ON"
},
//
// Set config values (equates to weather module config values). Also set values pertaining to caching of
// Today's temperature forecast (for use in the Forecast functions below)
//
setConfig: function (config) {
this.config = config;
this.todayTempCacheMin = 0;
this.todayTempCacheMax = 0;
this.todayCached = false;
this.cacheCurrentTemp = 999;
},
//
// Called when the weather provider is started
//
start: function () {
Log.info(`Weather provider: ${this.providerName} started.`);
this.setFetchedLocation(this.config.location);
// Ensure kmH are ignored since these are custom-handled by this Provider
this.config.useKmh = false;
},
//
// Override the fetchCurrentWeather method to query EC and construct a Current weather object
//
fetchCurrentWeather() {
this.fetchData(this.getUrl(), "GET")
.then((data) => {
if (!data) {
// Did not receive usable new data.
return;
}
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
this.setCurrentWeather(currentWeather);
})
.catch(function (request) {
Log.error("Could not load EnvCanada site data ... ", request);
})
.finally(() => this.updateAvailable());
},
//
// Override the fetchWeatherForecast method to query EC and construct Forecast weather objects
//
fetchWeatherForecast() {
this.fetchData(this.getUrl(), "GET")
.then((data) => {
if (!data) {
// Did not receive usable new data.
return;
}
const forecastWeather = this.generateWeatherObjectsFromForecast(data);
this.setWeatherForecast(forecastWeather);
})
.catch(function (request) {
Log.error("Could not load EnvCanada forecast data ... ", request);
})
.finally(() => this.updateAvailable());
},
//
// Override the fetchWeatherHourly method to query EC and construct Forecast weather objects
//
fetchWeatherHourly() {
this.fetchData(this.getUrl(), "GET")
.then((data) => {
if (!data) {
// Did not receive usable new data.
return;
}
const hourlyWeather = this.generateWeatherObjectsFromHourly(data);
this.setWeatherHourly(hourlyWeather);
})
.catch(function (request) {
Log.error("Could not load EnvCanada hourly data ... ", request);
})
.finally(() => this.updateAvailable());
},
//
// Override fetchData function to handle XML document (base function assumes JSON)
//
fetchData: function (url, method = "GET", data = null) {
return new Promise(function (resolve, reject) {
var request = new XMLHttpRequest();
request.open(method, url, true);
request.onreadystatechange = function () {
if (this.readyState === 4) {
if (this.status === 200) {
resolve(this.responseXML);
} else {
reject(request);
}
}
};
request.send();
});
},
//////////////////////////////////////////////////////////////////////////////////
//
// Environment Canada methods - not part of the standard Provider methods
//
//////////////////////////////////////////////////////////////////////////////////
//
// Build the EC URL based on the Site Code and Province Code specified in the config parms. Note that the
// URL defaults to the Englsih version simply because there is no language dependancy in the data
// being accessed. This is only pertinent when using the EC data elements that contain a textual forecast.
//
// Also note that access is supported through a proxy service (thingproxy.freeboard.io) to mitigate
// 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;
},
//
// Generate a WeatherObject based on current EC weather conditions
//
generateWeatherObjectFromCurrentWeather(ECdoc) {
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
// There are instances where EC will update weather data and current temperature will not be
// provided. While this is a defect in the EC systems, we need to accommodate to avoid a current temp
// of NaN being displayed. Therefore... whenever we get a valid current temp from EC, we will cache
// the value. Whenever EC data is missing current temp, we will provide the cached value
// instead. This is reasonable since the cached value will typically be accurate within the previous
// hour. The only time this does not work as expected is when MM is restarted and the first query to
// EC finds no current temp. In this scenario, MM will end up displaying a current temp of null;
if (ECdoc.querySelector("siteData currentConditions temperature").textContent) {
currentWeather.temperature = this.convertTemp(ECdoc.querySelector("siteData currentConditions temperature").textContent);
this.cacheCurrentTemp = currentWeather.temperature;
} else {
currentWeather.temperature = this.cacheCurrentTemp;
}
currentWeather.windSpeed = this.convertWind(ECdoc.querySelector("siteData currentConditions wind speed").textContent);
currentWeather.windDirection = ECdoc.querySelector("siteData currentConditions wind bearing").textContent;
currentWeather.humidity = ECdoc.querySelector("siteData currentConditions relativeHumidity").textContent;
// Ensure showPrecipitationAmount is forced to false. EC does not really provide POP for current day
// and this feature for the weather module (current only) is sort of broken in that it wants
// to say POP but will display precip as an accumulated amount vs. a percentage.
this.config.showPrecipitationAmount = false;
//
// If the module config wants to showFeelsLike... default to the current temperature.
// Check for EC wind chill and humidex values and overwrite the feelsLikeTemp value.
// This assumes that the EC current conditions will never contain both a wind chill
// and humidex temperature.
//
if (this.config.showFeelsLike) {
currentWeather.feelsLikeTemp = currentWeather.temperature;
if (ECdoc.querySelector("siteData currentConditions windChill")) {
currentWeather.feelsLikeTemp = this.convertTemp(ECdoc.querySelector("siteData currentConditions windChill").textContent);
}
if (ECdoc.querySelector("siteData currentConditions humidex")) {
currentWeather.feelsLikeTemp = this.convertTemp(ECdoc.querySelector("siteData currentConditions humidex").textContent);
}
}
//
// Need to map EC weather icon to MM weatherType values
//
currentWeather.weatherType = this.convertWeatherType(ECdoc.querySelector("siteData currentConditions iconCode").textContent);
//
// Capture the sunrise and sunset values from EC data
//
var sunList = ECdoc.querySelectorAll("siteData riseSet dateTime");
currentWeather.sunrise = moment(sunList[1].querySelector("timeStamp").textContent, "YYYYMMDDhhmmss");
currentWeather.sunset = moment(sunList[3].querySelector("timeStamp").textContent, "YYYYMMDDhhmmss");
return currentWeather;
},
//
// Generate an array of WeatherObjects based on EC weather forecast
//
generateWeatherObjectsFromForecast(ECdoc) {
// Declare an array to hold each day's forecast object
const days = [];
var 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;
weather.date = moment(baseDate, "YYYYMMDDhhmmss");
var foreGroup = ECdoc.querySelectorAll("siteData forecastGroup forecast");
// For simplicity, we will only accumulate precipitation and will not try to break out
// rain vs snow accumulations
weather.rain = null;
weather.snow = null;
weather.precipitation = null;
//
// The EC forecast is held in a 12-element array - Elements 0 to 11 - with each day encompassing
// 2 elements. the first element for a day details the Today (daytime) forecast while the second
// element details the Tonight (nightime) forecast. Element 0 is always for the current day.
//
// However... the forecast is somewhat 'rolling'.
//
// If the EC forecast is queried in the morning, then Element 0 will contain Current
// Today and Element 1 will contain Current Tonight. From there, the next 5 days of forecast will be
// contained in Elements 2/3, 4/5, 6/7, 8/9, and 10/11. This module will create a 6-day forecast using
// all of these Elements.
//
// But, if the EC forecast is queried in late afternoon, the Current Today forecast will be rolled
// off and Element 0 will contain Current Tonight. From there, the next 5 days will be contained in
// Elements 1/2, 3/4, 5/6, 7/8, and 9/10. As well, Elelement 11 will contain a forecast for a 6th day,
// but only for the Today portion (not Tonight). This module will create a 6-day forecast using
// Elements 0 to 11, and will ignore the additional Todat forecast in Element 11.
//
// We need to determine if Element 0 is showing the forecast for Current Today or Current Tonight.
// This is required to understand how Min and Max temperature will be determined, and to understand
// 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;
//
// If the first Element is Current Today, look at Current Today and Current Tonight for the current day.
//
if (foreGroup[0].querySelector("period[textForecastName='Today']")) {
this.todaytempCacheMin = 0;
this.todaytempCacheMax = 0;
this.todayCached = true;
this.setMinMaxTemps(weather, foreGroup, 0, true, currentTemp);
this.setPrecipitation(weather, foreGroup, 0);
//
// Set the Element number that will reflect where the next day's forecast is located. Also set
// the Element number where the end of the forecast will be. This is important because of the
// rolling nature of the EC forecast. In the current scenario (Today and Tonight are present
// in elements 0 and 11, we know that we will have 6 full days of forecasts and we will use
// them. We will set lastDay such that we iterate through all 12 elements of the forecast.
//
nextDay = 2;
lastDay = 12;
}
//
// If the first Element is Current Tonight, look at Tonight only for the current day.
//
if (foreGroup[0].querySelector("period[textForecastName='Tonight']")) {
this.setMinMaxTemps(weather, foreGroup, 0, false, currentTemp);
this.setPrecipitation(weather, foreGroup, 0);
//
// Set the Element number that will reflect where the next day's forecast is located. Also set
// the Element number where the end of the forecast will be. This is important because of the
// rolling nature of the EC forecast. In the current scenario (only Current Tonight is present
// in Element 0, we know that we will have 6 full days of forecasts PLUS a half-day and
// forecast in the final element. Because we will only use full day forecasts, we set the
// lastDay number to ensure we ignore that final half-day (in forecast Element 11).
//
nextDay = 1;
lastDay = 11;
}
//
// Need to map EC weather icon to MM weatherType values. Always pick the first Element's icon to
// reflect either Today or Tonight depending on what the forecast is showing in Element 0.
//
weather.weatherType = this.convertWeatherType(foreGroup[0].querySelector("abbreviatedForecast iconCode").textContent);
// Push the weather object into the forecast array.
days.push(weather);
//
// Now do the the rest of the forecast starting at nextDay. We will process each day using 2 EC
// forecast Elements. This will address the fact that the EC forecast always includes Today and
// Tonight for each day. This is why we iterate through the forecast by a a count of 2, with each
// iteration looking at the current Element and the next Element.
//
var 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);
// Add 1 to the date to reflect the current forecast day we are building
lastDate = lastDate.add(1, "day");
weather.date = moment(lastDate, "X");
// Capture the temperatures for the current Element and the next Element in order to set
// the Min and Max temperatures for the forecast
this.setMinMaxTemps(weather, foreGroup, stepDay, true, currentTemp);
weather.rain = null;
weather.snow = null;
weather.precipitation = null;
this.setPrecipitation(weather, foreGroup, stepDay);
//
// Need to map EC weather icon to MM weatherType values. Always pick the first Element icon.
//
weather.weatherType = this.convertWeatherType(foreGroup[stepDay].querySelector("abbreviatedForecast iconCode").textContent);
// Push the weather object into the forecast array.
days.push(weather);
}
return days;
},
//
// Generate an array of WeatherObjects based on EC hourly weather forecast
//
generateWeatherObjectsFromHourly(ECdoc) {
// Declare an array to hold each hour's forecast object
const hours = [];
// 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");
//
// 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");
for (var stepHour = 0; stepHour < 24; stepHour += 1) {
var 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");
weather.date = moment(currTime, "X");
// Capture the temperature
weather.temperature = this.convertTemp(hourGroup[stepHour].querySelector("temperature").textContent);
// Capture Likelihood of Precipitation (LOP) and unit-of-measure values
var precipLOP = hourGroup[stepHour].querySelector("lop").textContent * 1.0;
if (precipLOP > 0) {
weather.precipitation = precipLOP;
weather.precipitationUnits = hourGroup[stepHour].querySelector("lop").getAttribute("units");
}
//
// Need to map EC weather icon to MM weatherType values. Always pick the first Element icon.
//
weather.weatherType = this.convertWeatherType(hourGroup[stepHour].querySelector("iconCode").textContent);
// Push the weather object into the forecast array.
hours.push(weather);
}
return hours;
},
//
// Determine Min and Max temp based on a supplied Forecast Element index and a boolen that denotes if
// the next Forecast element should be considered - i.e. look at Today *and* Tonight vs.Tonight-only
//
setMinMaxTemps(weather, foreGroup, today, fullDay, currentTemp) {
var todayTemp = foreGroup[today].querySelector("temperatures temperature").textContent;
var todayClass = foreGroup[today].querySelector("temperatures temperature").getAttribute("class");
//
// The following logic is largely aimed at accommodating the Current day's forecast whereby we
// can have either Current Today+Current Tonight or only Current Tonight.
//
// If fullDay is false, then we only have Tonight for the current day's forecast - meaning we have
// lost a min or max temp value for the day. Therefore, we will see if we were able to cache the the
// Today forecast for the current day. If we have, we will use them. If we do not have the cached values,
// it means that MM or the Computer has been restarted since the time EC rolled off Today from the
// forecast. In this scenario, we will simply default to the Current Conditions temperature and then
// check the Tonight temperature.
//
if (fullDay === false) {
if (this.todayCached === true) {
weather.minTemperature = this.todayTempCacheMin;
weather.maxTemperature = this.todayTempCacheMax;
} else {
weather.minTemperature = this.convertTemp(currentTemp);
weather.maxTemperature = weather.minTemperature;
}
}
//
// We will check to see if the current Element's temperature is Low or High and set weather values
// accordingly. We will also check the condition where fullDay is true *and* we are looking at forecast
// element 0. This is a special case where we will cache temperature values so that we have them later
// in the current day when the Current Today element rolls off and we have Current Tonight only.
//
if (todayClass === "low") {
weather.minTemperature = this.convertTemp(todayTemp);
if (today === 0 && fullDay === true) {
this.todayTempCacheMin = weather.minTemperature;
}
}
if (todayClass === "high") {
weather.maxTemperature = this.convertTemp(todayTemp);
if (today === 0 && fullDay === true) {
this.todayTempCacheMax = weather.maxTemperature;
}
}
var nextTemp = foreGroup[today + 1].querySelector("temperatures temperature").textContent;
var nextClass = foreGroup[today + 1].querySelector("temperatures temperature").getAttribute("class");
if (fullDay === true) {
if (nextClass === "low") {
weather.minTemperature = this.convertTemp(nextTemp);
}
if (nextClass === "high") {
weather.maxTemperature = this.convertTemp(nextTemp);
}
}
return;
},
//
// Check for a Precipitation forecast. EC can provide a forecast in 2 ways: either an accumulation figure
// or a POP percentage. If there is a POP, then that is what the module will show. If there is an accumulation,
// then it will be displayed ONLY if no POP is present.
//
// POP Logic: By default, we want to show the POP for 'daytime' since we are presuming that is what
// people are more interested in seeing. While EC provides a separate POP for daytime and nightime portions
// of each day, the weather module does not really allow for that view of a daily forecast. There we will
// ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
// the nightime forecast after a certain point in the afternoon. As such, we will be showing the nightime POP
// (if one exists) in that specific scenario.
//
// Accumulation Logic: Similar to POP, we want to show accumulation for 'daytime' since we presume that is what
// people are interested in seeing. While EC provides a separate accumulation for daytime and nightime portions
// of each day, the weather module does not really allow for that view of a daily forecast. There we will
// ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
// the nightime forecast after a certain point in that specific scenario.
//
setPrecipitation(weather, foreGroup, today) {
if (foreGroup[today].querySelector("precipitation accumulation")) {
weather.precipitation = foreGroup[today].querySelector("precipitation accumulation amount").textContent * 1.0;
weather.precipitationUnits = " " + foreGroup[today].querySelector("precipitation accumulation amount").getAttribute("units");
if (this.config.units === "imperial") {
if (weather.precipitationUnits === " cm") {
weather.precipitation = (weather.precipitation * 0.394).toFixed(2);
weather.precipitationUnits = " in";
}
if (weather.precipitationUnits === " mm") {
weather.precipitation = (weather.precipitation * 0.0394).toFixed(2);
weather.precipitationUnits = " in";
}
}
}
// Check Today element for POP
if (foreGroup[today].querySelector("abbreviatedForecast pop").textContent > 0) {
weather.precipitation = foreGroup[today].querySelector("abbreviatedForecast pop").textContent;
weather.precipitationUnits = foreGroup[today].querySelector("abbreviatedForecast pop").getAttribute("units");
}
return;
},
//
// Unit conversions
//
//
// Convert C to F temps
//
convertTemp(temp) {
if (this.config.tempUnits === "imperial") {
return 1.8 * temp + 32;
} else {
return temp;
}
},
//
// Convert km/h to mph
//
convertWind(kilo) {
if (this.config.windUnits === "imperial") {
return kilo / 1.609344;
} else {
return kilo;
}
},
//
// Convert the icons to a more usable name.
//
convertWeatherType(weatherType) {
const weatherTypes = {
"00": "day-sunny",
"01": "day-sunny",
"02": "day-sunny-overcast",
"03": "day-cloudy",
"04": "day-cloudy",
"05": "day-cloudy",
"06": "day-sprinkle",
"07": "day-showers",
"08": "day-snow",
"09": "day-thunderstorm",
10: "cloud",
11: "showers",
12: "rain",
13: "rain",
14: "sleet",
15: "sleet",
16: "snow",
17: "snow",
18: "snow",
19: "thunderstorm",
20: "cloudy",
21: "cloudy",
22: "day-cloudy",
23: "day-haze",
24: "fog",
25: "snow-wind",
26: "sleet",
27: "sleet",
28: "rain",
29: "na",
30: "night-clear",
31: "night-clear",
32: "night-partly-cloudy",
33: "night-alt-cloudy",
34: "night-alt-cloudy",
35: "night-partly-cloudy",
36: "night-alt-showers",
37: "night-rain-mix",
38: "night-alt-snow",
39: "night-thunderstorm",
40: "snow-wind",
41: "tornado",
42: "tornado",
43: "windy",
44: "smoke",
45: "sandstorm",
46: "thunderstorm",
47: "thunderstorm",
48: "tornado"
};
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
}
});

View File

@ -30,16 +30,14 @@ WeatherProvider.register("openweathermap", {
fetchCurrentWeather() {
this.fetchData(this.getUrl())
.then((data) => {
if (!data || !data.main || typeof data.main.temp === "undefined") {
// Did not receive usable new data.
// Maybe this needs a better check?
return;
}
this.setFetchedLocation(`${data.name}, ${data.sys.country}`);
if (this.config.weatherEndpoint === "/onecall") {
const weatherData = this.generateWeatherObjectsFromOnecall(data);
this.setCurrentWeather(weatherData.current);
this.setFetchedLocation(`${data.timezone}`);
} else {
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
this.setCurrentWeather(currentWeather);
}
})
.catch(function (request) {
Log.error("Could not load data ... ", request);
@ -51,16 +49,15 @@ WeatherProvider.register("openweathermap", {
fetchWeatherForecast() {
this.fetchData(this.getUrl())
.then((data) => {
if (!data || !data.list || !data.list.length) {
// Did not receive usable new data.
// Maybe this needs a better check?
return;
}
this.setFetchedLocation(`${data.city.name}, ${data.city.country}`);
if (this.config.weatherEndpoint === "/onecall") {
const weatherData = this.generateWeatherObjectsFromOnecall(data);
this.setWeatherForecast(weatherData.days);
this.setFetchedLocation(`${data.timezone}`);
} else {
const forecast = this.generateWeatherObjectsFromForecast(data.list);
this.setWeatherForecast(forecast);
this.setFetchedLocation(`${data.city.name}, ${data.city.country}`);
}
})
.catch(function (request) {
Log.error("Could not load data ... ", request);

View File

@ -74,7 +74,7 @@ WeatherProvider.register("smhi", {
getClosestToCurrentTime(times) {
let now = moment();
let minDiff = undefined;
for (time of times) {
for (const time of times) {
let diff = Math.abs(moment(time.validTime).diff(now));
if (!minDiff || diff < Math.abs(moment(minDiff.validTime).diff(now))) {
minDiff = time;
@ -149,13 +149,13 @@ WeatherProvider.register("smhi", {
* @param coordinates
*/
convertWeatherDataGroupedByDay(allWeatherData, coordinates) {
var currentWeather;
let currentWeather;
let result = [];
let allWeatherObjects = this.fillInGaps(allWeatherData).map((weatherData) => this.convertWeatherDataToObject(weatherData, coordinates));
var dayWeatherTypes = [];
let dayWeatherTypes = [];
for (weatherObject of allWeatherObjects) {
for (const weatherObject of allWeatherObjects) {
//If its the first object or if a day change we need to reset the summary object
if (!currentWeather || !currentWeather.date.isSame(weatherObject.date, "day")) {
currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
@ -216,12 +216,12 @@ WeatherProvider.register("smhi", {
*/
fillInGaps(data) {
let result = [];
for (var i = 1; i < data.length; i++) {
for (const 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 (var j = 0; j < hours; j++) {
for (const j = 0; j < hours; j++) {
let current = Object.assign({}, data[i]);
current.validTime = from.clone().add(j, "hours").toISOString();
result.push(current);

View File

@ -81,6 +81,7 @@ WeatherProvider.register("ukmetoffice", {
*/
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
const location = currentWeatherData.SiteRep.DV.Location;
// data times are always UTC
let nowUtc = moment.utc();
@ -88,8 +89,8 @@ WeatherProvider.register("ukmetoffice", {
let timeInMins = nowUtc.diff(midnightUtc, "minutes");
// loop round each of the (5) periods, look for today (the first period may be yesterday)
for (var i in currentWeatherData.SiteRep.DV.Location.Period) {
let periodDate = moment.utc(currentWeatherData.SiteRep.DV.Location.Period[i].value.substr(0, 10), "YYYY-MM-DD");
for (const period of location.Period) {
const periodDate = moment.utc(period.value.substr(0, 10), "YYYY-MM-DD");
// ignore if period is before today
if (periodDate.isSameOrAfter(moment.utc().startOf("day"))) {
@ -97,17 +98,17 @@ WeatherProvider.register("ukmetoffice", {
if (moment().diff(periodDate, "minutes") > 0) {
// loop round the reports looking for the one we are in
// $ value specifies the time in minutes-of-the-day: 0, 180, 360,...1260
for (var j in currentWeatherData.SiteRep.DV.Location.Period[i].Rep) {
let p = currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].$;
for (const rep of period.Rep) {
const p = rep.$;
if (timeInMins >= p && timeInMins - 180 < p) {
// finally got the one we want, so populate weather object
currentWeather.humidity = currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].H;
currentWeather.temperature = this.convertTemp(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].T);
currentWeather.feelsLikeTemp = this.convertTemp(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].F);
currentWeather.precipitation = parseInt(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].Pp);
currentWeather.windSpeed = this.convertWindSpeed(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].S);
currentWeather.windDirection = this.convertWindDirection(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].D);
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].W);
currentWeather.humidity = rep.H;
currentWeather.temperature = this.convertTemp(rep.T);
currentWeather.feelsLikeTemp = this.convertTemp(rep.F);
currentWeather.precipitation = parseInt(rep.Pp);
currentWeather.windSpeed = this.convertWindSpeed(rep.S);
currentWeather.windDirection = this.convertWindDirection(rep.D);
currentWeather.weatherType = this.convertWeatherType(rep.W);
}
}
}
@ -115,7 +116,7 @@ WeatherProvider.register("ukmetoffice", {
}
// determine the sunrise/sunset times - not supplied in UK Met Office data
let times = this.calcAstroData(currentWeatherData.SiteRep.DV.Location);
let times = this.calcAstroData(location);
currentWeather.sunrise = times[0];
currentWeather.sunset = times[1];
@ -130,21 +131,21 @@ WeatherProvider.register("ukmetoffice", {
// loop round the (5) periods getting the data
// for each period array, Day is [0], Night is [1]
for (var j in forecasts.SiteRep.DV.Location.Period) {
for (const period of forecasts.SiteRep.DV.Location.Period) {
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
// data times are always UTC
const dateStr = forecasts.SiteRep.DV.Location.Period[j].value;
const dateStr = period.value;
let periodDate = moment.utc(dateStr.substr(0, 10), "YYYY-MM-DD");
// ignore if period is before today
if (periodDate.isSameOrAfter(moment.utc().startOf("day"))) {
// populate the weather object
weather.date = moment.utc(dateStr.substr(0, 10), "YYYY-MM-DD");
weather.minTemperature = this.convertTemp(forecasts.SiteRep.DV.Location.Period[j].Rep[1].Nm);
weather.maxTemperature = this.convertTemp(forecasts.SiteRep.DV.Location.Period[j].Rep[0].Dm);
weather.weatherType = this.convertWeatherType(forecasts.SiteRep.DV.Location.Period[j].Rep[0].W);
weather.precipitation = parseInt(forecasts.SiteRep.DV.Location.Period[j].Rep[0].PPd);
weather.minTemperature = this.convertTemp(period.Rep[1].Nm);
weather.maxTemperature = this.convertTemp(period.Rep[0].Dm);
weather.weatherType = this.convertWeatherType(period.Rep[0].W);
weather.precipitation = parseInt(period.Rep[0].PPd);
days.push(weather);
}

View File

@ -59,9 +59,7 @@ WeatherProvider.register("ukmetofficedatahub", {
let queryStrings = "?";
queryStrings += "latitude=" + this.config.lat;
queryStrings += "&longitude=" + this.config.lon;
if (this.config.appendLocationNameToHeader) {
queryStrings += "&includeLocationName=" + true;
}
// Return URL, making sure there is a trailing "/" in the base URL.
return this.config.apiBase + (this.config.apiBase.endsWith("/") ? "" : "/") + forecastType + queryStrings;

View File

@ -33,6 +33,7 @@ Module.register("weather", {
showIndoorHumidity: false,
maxNumberOfDays: 5,
maxEntries: 5,
ignoreToday: false,
fade: true,
fadePoint: 0.25, // Start on 1/4th of the list.
initialLoadDelay: 0, // 0 seconds delay
@ -48,6 +49,9 @@ Module.register("weather", {
// Module properties.
weatherProvider: null,
// Can be used by the provider to display location of event if nothing else is specified
firstEvent: null,
// Define required scripts.
getStyles: function () {
return ["font-awesome.css", "weather-icons.css", "weather.css"];
@ -88,15 +92,13 @@ Module.register("weather", {
// Override notification handler.
notificationReceived: function (notification, payload, sender) {
if (notification === "CALENDAR_EVENTS") {
var senderClasses = sender.data.classes.toLowerCase().split(" ");
const senderClasses = sender.data.classes.toLowerCase().split(" ");
if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
this.firstEvent = false;
for (var e in payload) {
var event = payload[e];
this.firstEvent = null;
for (let event of payload) {
if (event.location || event.geo) {
this.firstEvent = event;
//Log.log("First upcoming event with location: ", event);
Log.debug("First upcoming event with location: ", event);
break;
}
}
@ -114,24 +116,30 @@ Module.register("weather", {
getTemplate: function () {
switch (this.config.type.toLowerCase()) {
case "current":
return `current.njk`;
return "current.njk";
case "hourly":
return `hourly.njk`;
return "hourly.njk";
case "daily":
case "forecast":
return `forecast.njk`;
return "forecast.njk";
//Make the invalid values use the "Loading..." from forecast
default:
return `forecast.njk`;
return "forecast.njk";
}
},
// Add all the data to the template.
getTemplateData: function () {
const forecast = this.weatherProvider.weatherForecast();
if (this.config.ignoreToday) {
forecast.splice(0, 1);
}
return {
config: this.config,
current: this.weatherProvider.currentWeather(),
forecast: this.weatherProvider.weatherForecast(),
forecast: forecast,
hourly: this.weatherProvider.weatherHourly(),
indoor: {
humidity: this.indoorHumidity,
@ -152,7 +160,7 @@ Module.register("weather", {
},
scheduleUpdate: function (delay = null) {
var nextLoad = this.config.updateInterval;
let nextLoad = this.config.updateInterval;
if (delay !== null && delay >= 0) {
nextLoad = delay;
}
@ -176,8 +184,8 @@ Module.register("weather", {
},
roundValue: function (temperature) {
var decimals = this.config.roundTemp ? 0 : 1;
var roundValue = parseFloat(temperature).toFixed(decimals);
const decimals = this.config.roundTemp ? 0 : 1;
const roundValue = parseFloat(temperature).toFixed(decimals);
return roundValue === "-0" ? 0 : roundValue;
},
@ -272,8 +280,8 @@ Module.register("weather", {
if (this.config.fadePoint < 0) {
this.config.fadePoint = 0;
}
var startingPoint = numSteps * this.config.fadePoint;
var numFadesteps = numSteps - startingPoint;
const startingPoint = numSteps * this.config.fadePoint;
const numFadesteps = numSteps - startingPoint;
if (currentStep >= startingPoint) {
return 1 - (currentStep - startingPoint) / numFadesteps;
} else {

View File

@ -28,6 +28,7 @@ class WeatherObject {
this.rain = null;
this.snow = null;
this.precipitation = null;
this.precipitationUnits = null;
this.feelsLikeTemp = null;
}

View File

@ -8,7 +8,7 @@
*
* This class is the blueprint for a weather provider.
*/
var WeatherProvider = Class.extend({
const WeatherProvider = Class.extend({
// Weather Provider Properties
providerName: null,
defaults: {},
@ -114,7 +114,7 @@ var WeatherProvider = Class.extend({
// A convenience function to make requests. It returns a promise.
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) {

View File

@ -339,7 +339,9 @@ Module.register("weatherforecast", {
*
* argument data object - Weather information received form openweather.org.
*/
processWeather: function (data) {
processWeather: function (data, momenttz) {
let mom = momenttz ? momenttz : moment; // Exception last.
// Forcast16 (paid) API endpoint provides this data. Onecall endpoint
// does not.
if (data.city) {
@ -357,8 +359,8 @@ Module.register("weatherforecast", {
var dayEnds = 17;
if (data.city && data.city.sunrise && data.city.sunset) {
dayStarts = new Date(moment.unix(data.city.sunrise).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours();
dayEnds = new Date(moment.unix(data.city.sunset).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours();
dayStarts = new Date(mom.unix(data.city.sunrise).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours();
dayEnds = new Date(mom.unix(data.city.sunset).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours();
}
// Handle different structs between forecast16 and onecall endpoints
@ -379,11 +381,11 @@ Module.register("weatherforecast", {
var day;
var hour;
if (forecast.dt_txt) {
day = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd");
hour = new Date(moment(forecast.dt_txt).locale("en").format("YYYY-MM-DD HH:mm:ss")).getHours();
day = mom(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd");
hour = new Date(mom(forecast.dt_txt).locale("en").format("YYYY-MM-DD HH:mm:ss")).getHours();
} else {
day = moment(forecast.dt, "X").format("ddd");
hour = new Date(moment(forecast.dt, "X")).getHours();
day = mom(forecast.dt, "X").format("ddd");
hour = new Date(mom(forecast.dt, "X")).getHours();
}
if (day !== lastDay) {
@ -392,7 +394,7 @@ Module.register("weatherforecast", {
icon: this.config.iconTable[forecast.weather[0].icon],
maxTemp: this.roundValue(forecast.temp.max),
minTemp: this.roundValue(forecast.temp.min),
rain: this.processRain(forecast, forecastList)
rain: this.processRain(forecast, forecastList, mom)
};
this.forecast.push(forecastData);
lastDay = day;
@ -482,16 +484,18 @@ Module.register("weatherforecast", {
* That object has a property "3h" which contains the amount of rain since the previous forecast in the list.
* This code finds all forecasts that is for the same day and sums the amount of rain and returns that.
*/
processRain: function (forecast, allForecasts) {
processRain: function (forecast, allForecasts, momenttz) {
let mom = momenttz ? momenttz : moment; // Exception last.
//If the amount of rain actually is a number, return it
if (!isNaN(forecast.rain)) {
return forecast.rain;
}
//Find all forecasts that is for the same day
var checkDateTime = forecast.dt_txt ? moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss") : moment(forecast.dt, "X");
var checkDateTime = forecast.dt_txt ? mom(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss") : mom(forecast.dt, "X");
var daysForecasts = allForecasts.filter(function (item) {
var itemDateTime = item.dt_txt ? moment(item.dt_txt, "YYYY-MM-DD hh:mm:ss") : moment(item.dt, "X");
var itemDateTime = item.dt_txt ? mom(item.dt_txt, "YYYY-MM-DD hh:mm:ss") : mom(item.dt, "X");
return itemDateTime.isSame(checkDateTime, "day") && item.rain instanceof Object;
});

3518
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "magicmirror",
"version": "2.15.0",
"version": "2.16.0-develop",
"description": "The open source modular smart mirror platform.",
"main": "js/electron.js",
"scripts": {
@ -10,18 +10,20 @@
"install": "echo \"Installing vendor files ...\n\" && cd vendor && npm install --loglevel=error",
"install-fonts": "echo \"Installing fonts ...\n\" && cd fonts && npm install --loglevel=error",
"postinstall": "npm run install-fonts && echo \"MagicMirror installation finished successfully! \n\"",
"test": "NODE_ENV=test mocha tests --recursive",
"test:coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text mocha tests --recursive --timeout=3000",
"test:e2e": "NODE_ENV=test mocha tests/e2e --recursive",
"test:unit": "NODE_ENV=test mocha tests/unit --recursive",
"test:prettier": "prettier --check **/*.{js,css,json,md,yml}",
"test": "NODE_ENV=test jest -i --forceExit",
"test:coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text jest -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:calendar": "node ./modules/default/calendar/debug.js",
"config:check": "node js/check_config.js",
"lint:prettier": "prettier --write **/*.{js,css,json,md,yml}",
"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:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json --fix",
"lint:staged": "pretty-quick --staged",
"prepare": "[ -f node_modules/.bin/husky ] && husky install || echo no husky installed."
},
"repository": {
"type": "git",
@ -43,60 +45,74 @@
},
"homepage": "https://magicmirror.builders",
"devDependencies": {
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-jsdoc": "^32.3.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-jsdoc": "^35.4.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-jest": "^24.3.6",
"express-basic-auth": "^1.2.0",
"husky": "^4.3.8",
"jsdom": "^16.5.1",
"husky": "^6.0.0",
"jest": "27.0.5",
"jsdom": "^16.6.0",
"lodash": "^4.17.21",
"mocha": "^8.3.2",
"mocha-each": "^2.0.1",
"mocha-logger": "^1.0.7",
"nyc": "^15.1.0",
"prettier": "^2.2.1",
"pretty-quick": "^3.1.0",
"sinon": "^10.0.0",
"prettier": "^2.3.1",
"pretty-quick": "^3.1.1",
"sinon": "^11.1.1",
"spectron": "^13.0.0",
"stylelint": "^13.12.0",
"stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^21.0.0",
"stylelint-config-standard": "^22.0.0",
"stylelint-prettier": "^1.2.0"
},
"optionalDependencies": {
"electron": "^11.3.0"
"electron": "^11.4.9"
},
"dependencies": {
"colors": "^1.4.0",
"console-stamp": "^3.0.0-rc4.2",
"digest-fetch": "^1.1.6",
"eslint": "^7.23.0",
"console-stamp": "^3.0.2",
"digest-fetch": "^1.2.0",
"eslint": "^7.29.0",
"express": "^4.17.1",
"express-ipfilter": "^1.1.2",
"express-ipfilter": "^1.2.0",
"feedme": "^2.0.2",
"helmet": "^4.4.1",
"iconv-lite": "^0.6.2",
"helmet": "^4.6.0",
"iconv-lite": "^0.6.3",
"module-alias": "^2.2.2",
"moment": "^2.29.1",
"node-fetch": "^2.6.1",
"node-ical": "^0.12.9",
"rrule": "^2.6.8",
"rrule-alt": "^2.2.8",
"simple-git": "^2.37.0",
"socket.io": "^4.0.0"
"node-ical": "^0.13.0",
"simple-git": "^2.40.0",
"socket.io": "^4.1.2"
},
"_moduleAliases": {
"node_helper": "js/node_helper.js",
"logger": "js/logger.js"
},
"engines": {
"node": ">=10"
"node": ">=12"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
"jest": {
"verbose": true,
"projects": [
{
"displayName": "unit",
"testMatch": [
"**/tests/unit/**/*.[jt]s?(x)"
],
"testPathIgnorePatterns": [
"<rootDir>/tests/unit/mocks"
]
},
{
"displayName": "e2e",
"testMatch": [
"**/tests/e2e/**/*.[jt]s?(x)"
],
"testPathIgnorePatterns": [
"<rootDir>/tests/e2e/modules/mocks",
"<rootDir>/tests/e2e/global-setup.js"
]
}
]
}
}

View File

@ -1,8 +1,8 @@
const app = require("../js/app.js");
const Log = require("logger");
app.start(function (config) {
var bindAddress = config.address ? config.address : "localhost";
var httpType = config.useHttps ? "https" : "http";
app.start((config) => {
const bindAddress = config.address ? config.address : "localhost";
const httpType = config.useHttps ? "https" : "http";
Log.log("\nReady to go! Please point your browser to: " + httpType + "://" + bindAddress + ":" + config.port);
});

View File

@ -0,0 +1,37 @@
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:xxx@gmail.com
X-WR-TIMEZONE:Europe/Zurich
BEGIN:VTIMEZONE
TZID:Etc/UTC
X-LIC-LOCATION:Etc/UTC
BEGIN:STANDARD
TZOFFSETFROM:+0000
TZOFFSETTO:+0000
TZNAME:GMT
DTSTART:19700101T00000--äüüßßß-0
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;VALUE=DATE:20210325
DTEND;VALUE=DATE:20210326
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1
DTSTAMP:20210421T154106Z
UID:zzz@google.com
REATED:20200831T200244Z
DESCRIPTION:
LAST-MODIFIED:20200831T200244Z
LOCATION:
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Birthday
TRANSP:OPAQUE
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:This is an event reminder
TRIGGER:-P0DT7H0M0S
END:VALARM
END:VEVENT

View File

@ -1,15 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
>
<channel>
>
<channel>
<title>Rodrigo Ramírez Norambuena</title>
<atom:link href="https://rodrigoramirez.com/feed/" rel="self" type="application/rss+xml" />
<atom:link href="https://rodrigoramirez.com/feed/" rel="self" type="application/rss+xml"/>
<link>https://rodrigoramirez.com</link>
<description>Temas sobre Linux, VoIP, Open Source, tecnología y lo relacionado.</description>
<lastBuildDate>Fri, 21 Oct 2016 21:30:22 +0000</lastBuildDate>

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: [],

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -0,0 +1,34 @@
/* Magic Mirror Test config sample module alert
*
* By 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: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
module: "alert",
config: {
display_time: 1000000,
welcome_message: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -1,5 +1,6 @@
/* Magic Mirror Test config custom calendar
*
* By Rejas
* MIT Licensed.
*/
let config = {

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -5,8 +5,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -0,0 +1,40 @@
/* Magic Mirror Test config custom calendar
*
* By Rejas
* 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: [
{
module: "calendar",
position: "bottom_bar",
config: {
calendars: [
{
maximumEntries: 6,
maximumNumberOfDays: 3650,
url: "http://localhost:8080/tests/configs/data/calendar_test_recurring.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@ -3,8 +3,7 @@
* By Sergey Morozov
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -3,8 +3,7 @@
* By Sergey Morozov
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -3,8 +3,7 @@
* By Sergey Morozov
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -3,8 +3,7 @@
* By Johan Hammar
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -1,13 +1,10 @@
/* Magic Mirror Test config for default clock module
* Language es for showWeek feature
*
* By Rodrigo Ramírez Norambuena
* https://rodrigoramirez.com
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -1,10 +1,8 @@
/* Magic Mirror Test config compliments with date type
*
* By Rejas
*
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -1,8 +1,9 @@
/* Magic Mirror Test config for display setters module using the helloworld module
*
* By Rejas
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
@ -37,6 +38,7 @@ var config = {
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -27,7 +27,8 @@ let config = {
url: "http://localhost:8080/tests/configs/data/feed_test_rodrigoramirez.xml"
}
],
prohibitedWords: ["QPanel"]
prohibitedWords: ["QPanel"],
showDescription: true
}
}
]

View File

@ -1,12 +1,9 @@
/* Magic Mirror Test config for position setters module
*
* For this case is using helloworld module
/* Magic Mirror Test config for position setters module using the helloworld module
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
@ -23,9 +20,9 @@ var config = {
modules:
// Using exotic content. This is why don't accept go to JSON configuration file
(function () {
var positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
var modules = Array();
for (var idx in positions) {
let positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
let modules = Array();
for (let idx in positions) {
modules.push({
module: "helloworld",
position: positions[idx],
@ -37,6 +34,7 @@ var config = {
return modules;
})()
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;

View File

@ -1,10 +1,8 @@
/* Magic Mirror Test config current weather compliments
*
* By rejas https://github.com/rejas
*
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -1,10 +1,8 @@
/* Magic Mirror Test config default weather
*
* By fewieden https://github.com/fewieden
*
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -1,10 +1,8 @@
/* Magic Mirror Test config default weather
*
* By fewieden https://github.com/fewieden
*
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -1,10 +1,8 @@
/* Magic Mirror Test config default weather
*
* By fewieden https://github.com/fewieden
*
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -1,10 +1,8 @@
/* Magic Mirror Test config default weather
*
* By fewieden https://github.com/fewieden
*
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -1,10 +1,8 @@
/* Magic Mirror Test config default weather
*
* By fewieden https://github.com/fewieden
*
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -0,0 +1,39 @@
/* Magic Mirror Test config default weather
*
* By rejas
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
units: "imperial",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
type: "forecast",
location: "Munich",
apiKey: "fake key",
weatherEndpoint: "/forecast/daily",
initialLoadDelay: 3000,
decimalSymbol: "_"
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["x.x.x.x"],

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8090,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],

View File

@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.10.1"],

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