mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 11:50:00 +00:00
Merge branch 'develop' into patch-1
This commit is contained in:
commit
6e124842e8
@ -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": {
|
||||
|
4
.github/ISSUE_TEMPLATE.md
vendored
4
.github/ISSUE_TEMPLATE.md
vendored
@ -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
6
.github/codecov.yml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# advanced settings
|
||||
informational: true
|
1
.github/workflows/codecov-test-suites.yml
vendored
1
.github/workflows/codecov-test-suites.yml
vendored
@ -11,6 +11,7 @@ on:
|
||||
jobs:
|
||||
run-and-upload-coverage-report:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: |
|
||||
|
1
.github/workflows/enforce-changelog.yml
vendored
1
.github/workflows/enforce-changelog.yml
vendored
@ -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
|
||||
|
5
.github/workflows/node-ci.js.yml
vendored
5
.github/workflows/node-ci.js.yml
vendored
@ -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
1
.husky/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
_
|
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint:staged
|
@ -1,5 +1,8 @@
|
||||
package-lock.json
|
||||
/config/**/*
|
||||
/vendor/**/*
|
||||
/config
|
||||
/coverage
|
||||
/vendor
|
||||
!/vendor/vendor.js
|
||||
.github/**/*
|
||||
.github
|
||||
.nyc_output
|
||||
package-lock.json
|
||||
*.ts
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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
31
css/custom.css.sample
Normal 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;
|
||||
}
|
108
css/main.css
108
css/main.css
@ -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 {
|
||||
|
26
index.html
26
index.html
@ -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>
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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: "/",
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
69
js/loader.js
69
js/loader.js
@ -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;
|
||||
|
126
js/main.js
126
js/main.js
@ -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];
|
||||
}
|
||||
|
72
js/module.js
72
js/module.js
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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%;
|
||||
}
|
||||
|
||||
|
@ -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 `
|
||||
|
@ -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") {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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") {
|
||||
|
@ -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 }}
|
||||
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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 = " ";
|
||||
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);
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
649
modules/default/weather/providers/envcanada.js
Normal file
649
modules/default/weather/providers/envcanada.js
Normal 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;
|
||||
}
|
||||
});
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -28,6 +28,7 @@ class WeatherObject {
|
||||
this.rain = null;
|
||||
this.snow = null;
|
||||
this.precipitation = null;
|
||||
this.precipitationUnits = null;
|
||||
this.feelsLikeTemp = null;
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
3518
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
94
package.json
94
package.json
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
|
37
tests/configs/data/calendar_test_recurring.ics
Normal file
37
tests/configs/data/calendar_test_recurring.ics
Normal 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
|
@ -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>
|
||||
|
@ -3,8 +3,7 @@
|
||||
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
|
||||
* MIT Licensed.
|
||||
*/
|
||||
|
||||
var config = {
|
||||
let config = {
|
||||
port: 8080,
|
||||
ipWhitelist: [],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
34
tests/configs/modules/alert/default.js
Normal file
34
tests/configs/modules/alert/default.js
Normal 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;
|
||||
}
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
/* Magic Mirror Test config custom calendar
|
||||
*
|
||||
* By Rejas
|
||||
* MIT Licensed.
|
||||
*/
|
||||
let config = {
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
40
tests/configs/modules/calendar/recurring.js
Normal file
40
tests/configs/modules/calendar/recurring.js
Normal 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;
|
||||
}
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -27,7 +27,8 @@ let config = {
|
||||
url: "http://localhost:8080/tests/configs/data/feed_test_rodrigoramirez.xml"
|
||||
}
|
||||
],
|
||||
prohibitedWords: ["QPanel"]
|
||||
prohibitedWords: ["QPanel"],
|
||||
showDescription: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -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;
|
||||
|
@ -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"],
|
||||
|
@ -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"],
|
||||
|
@ -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"],
|
||||
|
@ -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"],
|
||||
|
@ -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"],
|
||||
|
@ -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"],
|
||||
|
39
tests/configs/modules/weather/forecastweather_units.js
Normal file
39
tests/configs/modules/weather/forecastweather_units.js
Normal 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;
|
||||
}
|
@ -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"],
|
||||
|
||||
|
@ -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"],
|
||||
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user