Merge remote-tracking branch 'upstream/develop' into feature/newsfeed-show-as-list

This commit is contained in:
Robert Ewald 2021-06-03 12:27:26 +02:00
commit 6014eaf8eb
118 changed files with 2495 additions and 1478 deletions

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

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

View File

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [10.x, 12.x, 14.x] node-version: [12.x, 14.x, 16.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}

1
.husky/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
_

4
.husky/pre-commit Executable file
View File

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

View File

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

View File

@ -5,6 +5,51 @@ This project adheres to [Semantic Versioning](https://semver.org/).
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror² ❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror²
## [2.16.0] - Unreleased (Develop Branch)
_This release is scheduled to be released on 2021-07-01._
Special thanks to the following contributors: @B1gG, @codac, @ezeholz, @khassel, @KristjanESPERANTO, @rejas, @earlman, Faizan Ahmed.
### Added
- Added French translations for "MODULE_CONFIG_ERROR" and "PRECIP".
- Added German translation for "PRECIP".
- Added first test for Alert module.
- Added support for `dateFormat` when not using `timeFormat: "absolute"`
- Added custom-properties for colors and fonts for improved styling experience, see `custom.css.sample` file
- Added custom-properties for gaps around body and between modules
- Added test case for recurring calendar events
- Added new Environment Canada provider for default WEATHER module (weather data for Canadian locations only)
### Updated
- Bump node-ical to v0.13.0 (now last runtime dependency using deprecated `request` package is removed).
- Use codecov in informational mode
- Refactor code into es6 where possible (e.g. var -> let/const)
- Use node v16 in github workflow (replacing node v10)
- Moved some files into better suited directories
- Update dependencies in package.json, require node >= v12, remove `rrule-alt` and `rrule`
- Update dependencies in package.json and migrate husky to v6, fix husky setup in prod environment
- Cleaned up error handling in newsfeed and calendar modules for real
### Removed
### Fixed
- Fix calendar start function logging inconsistency.
- Fix updatenotification start function logging inconsistency.
- Checks and applies the showDescription setting for the newsfeed module again
- Fix tests in weather module and add one for decimalPoint in forecast
- Fix decimalSymbol in the forecast part of the new weather module #2530
- Fix wrong treatment of `appendLocationNameToHeader` when using `ukmetofficedatahub`
- Fix alert not recognizing multiple alerts (#2522)
- Fix fetch option httpsAgent to agent in calendar module (#466)
- Fix module updatenotification which did not work for repos with many refs (#1907)
- Fix config check failing when encountering let syntax ("Parsing error: Unexpected token config")
- Fix calendar debug check
- Really run prettier over all files
## [2.15.0] - 2021-04-01 ## [2.15.0] - 2021-04-01
Special thanks to the following contributors: @EdgardosReis, @MystaraTheGreat, @TheDuffman85, @ashishtank, @buxxi, @codac, @fewieden, @khassel, @klaernie, @qu1que, @rejas, @sdetweil & @thomasrockhu. Special thanks to the following contributors: @EdgardosReis, @MystaraTheGreat, @TheDuffman85, @ashishtank, @buxxi, @codac, @fewieden, @khassel, @klaernie, @qu1que, @rejas, @sdetweil & @thomasrockhu.

View File

@ -7,7 +7,6 @@
<a href="https://codecov.io/gh/MichMich/MagicMirror"><img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg?token=LEG1KitZR6" alt="CodeCov Status"/></a> <a href="https://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://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://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> </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). **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) - Website: [https://magicmirror.builders](https://magicmirror.builders)
- Documentation: [https://docs.magicmirror.builders](https://docs.magicmirror.builders) - Documentation: [https://docs.magicmirror.builders](https://docs.magicmirror.builders)
- Forum: [https://forum.magicmirror.builders](https://forum.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) - Discord: [https://discord.gg/J5BAtvx](https://discord.gg/J5BAtvx)
- Blog: [https://michaelteeuw.nl/tagged/magicmirror](https://michaelteeuw.nl/tagged/magicmirror) - Blog: [https://michaelteeuw.nl/tagged/magicmirror](https://michaelteeuw.nl/tagged/magicmirror)
- Donations: [https://magicmirror.builders/#donate](https://magicmirror.builders/#donate) - Donations: [https://magicmirror.builders/#donate](https://magicmirror.builders/#donate)

View File

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

View File

@ -7,8 +7,7 @@
* See https://github.com/MichMich/MagicMirror#configuration * See https://github.com/MichMich/MagicMirror#configuration
* *
*/ */
let config = {
var config = {
address: "localhost", // Address to listen on, can be: address: "localhost", // Address to listen on, can be:
// - "localhost", "127.0.0.1", "::1" to listen on loopback interface // - "localhost", "127.0.0.1", "::1" to listen on loopback interface
// - another specific IPv4/6 to listen on a specific interface // - another specific IPv4/6 to listen on a specific interface

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

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

View File

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

View File

@ -1,12 +1,12 @@
{ {
"name": "magicmirror-fonts", "name": "magicmirror-fonts",
"requires": true, "requires": true,
"lockfileVersion": 1, "lockfileVersion": 1,
"dependencies": { "dependencies": {
"roboto-fontface": { "roboto-fontface": {
"version": "0.10.0", "version": "0.10.0",
"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz", "resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz",
"integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g==" "integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g=="
} }
} }
} }

View File

@ -1,55 +1,57 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>MagicMirror²</title> <title>MagicMirror²</title>
<meta name="google" content="notranslate" /> <meta name="google" content="notranslate" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" /> <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-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no" />
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes" />
<link rel="icon" href="data:;base64,iVBORw0KGgo="> <link rel="icon" href="data:;base64,iVBORw0KGgo=" />
<link rel="stylesheet" type="text/css" href="css/main.css"> <link rel="stylesheet" type="text/css" href="css/main.css" />
<link rel="stylesheet" type="text/css" href="fonts/roboto.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. --> <!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. -->
<script type="text/javascript"> <script type="text/javascript">
var version = "#VERSION#"; var version = "#VERSION#";
</script> </script>
</head> </head>
<body> <body>
<div class="region fullscreen below"><div class="container"></div></div> <div class="region fullscreen below"><div class="container"></div></div>
<div class="region top bar"> <div class="region top bar">
<div class="container"></div> <div class="container"></div>
<div class="region top left"><div class="container"></div></div> <div class="region top left"><div class="container"></div></div>
<div class="region top center"><div class="container"></div></div> <div class="region top center"><div class="container"></div></div>
<div class="region top right"><div class="container"></div></div> <div class="region top right"><div class="container"></div></div>
</div> </div>
<div class="region upper third"><div class="container"></div></div> <div class="region upper third"><div class="container"></div></div>
<div class="region middle center"><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="region bottom bar"> <div class="container"><br /></div>
<div class="container"></div> </div>
<div class="region bottom left"><div class="container"></div></div> <div class="region bottom bar">
<div class="region bottom center"><div class="container"></div></div> <div class="container"></div>
<div class="region bottom right"><div class="container"></div></div> <div class="region bottom left"><div class="container"></div></div>
</div> <div class="region bottom center"><div class="container"></div></div>
<div class="region fullscreen above"><div class="container"></div></div> <div class="region bottom right"><div class="container"></div></div>
<script type="text/javascript" src="socket.io/socket.io.js"></script> </div>
<script type="text/javascript" src="vendor/node_modules/nunjucks/browser/nunjucks.min.js"></script> <div class="region fullscreen above"><div class="container"></div></div>
<script type="text/javascript" src="js/defaults.js"></script> <script type="text/javascript" src="socket.io/socket.io.js"></script>
<script type="text/javascript" src="#CONFIG_FILE#"></script> <script type="text/javascript" src="vendor/node_modules/nunjucks/browser/nunjucks.min.js"></script>
<script type="text/javascript" src="vendor/vendor.js"></script> <script type="text/javascript" src="js/defaults.js"></script>
<script type="text/javascript" src="modules/default/defaultmodules.js"></script> <script type="text/javascript" src="#CONFIG_FILE#"></script>
<script type="text/javascript" src="js/logger.js"></script> <script type="text/javascript" src="vendor/vendor.js"></script>
<script type="text/javascript" src="translations/translations.js"></script> <script type="text/javascript" src="modules/default/defaultmodules.js"></script>
<script type="text/javascript" src="js/translator.js"></script> <script type="text/javascript" src="js/logger.js"></script>
<script type="text/javascript" src="js/class.js"></script> <script type="text/javascript" src="translations/translations.js"></script>
<script type="text/javascript" src="js/module.js"></script> <script type="text/javascript" src="js/translator.js"></script>
<script type="text/javascript" src="js/loader.js"></script> <script type="text/javascript" src="js/class.js"></script>
<script type="text/javascript" src="js/socketclient.js"></script> <script type="text/javascript" src="js/module.js"></script>
<script type="text/javascript" src="js/main.js"></script> <script type="text/javascript" src="js/loader.js"></script>
</body> <script type="text/javascript" src="js/socketclient.js"></script>
<script type="text/javascript" src="js/main.js"></script>
</body>
</html> </html>

View File

@ -52,7 +52,13 @@ function checkConfigFile() {
// I'm not sure if all ever is utf-8 // I'm not sure if all ever is utf-8
const configFile = fs.readFileSync(configFileName, "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) { if (errors.length === 0) {
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)")); Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
} else { } else {

View File

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

View File

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

View File

@ -6,25 +6,25 @@
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
var MM = (function () { const MM = (function () {
var modules = []; let modules = [];
/* Private Methods */ /* Private Methods */
/** /**
* Create dom objects for all modules that are configured for a specific position. * Create dom objects for all modules that are configured for a specific position.
*/ */
var createDomObjects = function () { const createDomObjects = function () {
var domCreationPromises = []; const domCreationPromises = [];
modules.forEach(function (module) { modules.forEach(function (module) {
if (typeof module.data.position !== "string") { if (typeof module.data.position !== "string") {
return; 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.id = module.identifier;
dom.className = module.name; dom.className = module.name;
@ -35,7 +35,7 @@ var MM = (function () {
dom.opacity = 0; dom.opacity = 0;
wrapper.appendChild(dom); wrapper.appendChild(dom);
var moduleHeader = document.createElement("header"); const moduleHeader = document.createElement("header");
moduleHeader.innerHTML = module.getHeader(); moduleHeader.innerHTML = module.getHeader();
moduleHeader.className = "module-header"; moduleHeader.className = "module-header";
dom.appendChild(moduleHeader); dom.appendChild(moduleHeader);
@ -46,11 +46,11 @@ var MM = (function () {
moduleHeader.style.display = "block;"; moduleHeader.style.display = "block;";
} }
var moduleContent = document.createElement("div"); const moduleContent = document.createElement("div");
moduleContent.className = "module-content"; moduleContent.className = "module-content";
dom.appendChild(moduleContent); dom.appendChild(moduleContent);
var domCreationPromise = updateDom(module, 0); const domCreationPromise = updateDom(module, 0);
domCreationPromises.push(domCreationPromise); domCreationPromises.push(domCreationPromise);
domCreationPromise domCreationPromise
.then(function () { .then(function () {
@ -73,11 +73,11 @@ var MM = (function () {
* *
* @returns {HTMLElement} the wrapper element * @returns {HTMLElement} the wrapper element
*/ */
var selectWrapper = function (position) { const selectWrapper = function (position) {
var classes = position.replace("_", " "); const classes = position.replace("_", " ");
var parentWrapper = document.getElementsByClassName(classes); const parentWrapper = document.getElementsByClassName(classes);
if (parentWrapper.length > 0) { if (parentWrapper.length > 0) {
var wrapper = parentWrapper[0].getElementsByClassName("container"); const wrapper = parentWrapper[0].getElementsByClassName("container");
if (wrapper.length > 0) { if (wrapper.length > 0) {
return wrapper[0]; return wrapper[0];
} }
@ -92,9 +92,9 @@ var MM = (function () {
* @param {Module} sender The module that sent the notification. * @param {Module} sender The module that sent the notification.
* @param {Module} [sendTo] The (optional) module to send the notification to. * @param {Module} [sendTo] The (optional) module to send the notification to.
*/ */
var sendNotification = function (notification, payload, sender, sendTo) { const sendNotification = function (notification, payload, sender, sendTo) {
for (var m in modules) { for (const m in modules) {
var module = modules[m]; const module = modules[m];
if (module !== sender && (!sendTo || module === sendTo)) { if (module !== sender && (!sendTo || module === sendTo)) {
module.notificationReceived(notification, payload, sender); module.notificationReceived(notification, payload, sender);
} }
@ -109,10 +109,10 @@ var MM = (function () {
* *
* @returns {Promise} Resolved when the dom is fully updated. * @returns {Promise} Resolved when the dom is fully updated.
*/ */
var updateDom = function (module, speed) { const updateDom = function (module, speed) {
return new Promise(function (resolve) { return new Promise(function (resolve) {
var newContentPromise = module.getDom(); const newHeader = module.getHeader();
var newHeader = module.getHeader(); let newContentPromise = module.getDom();
if (!(newContentPromise instanceof Promise)) { if (!(newContentPromise instanceof Promise)) {
// convert to a promise if not already one to avoid if/else's everywhere // convert to a promise if not already one to avoid if/else's everywhere
@ -121,7 +121,7 @@ var MM = (function () {
newContentPromise newContentPromise
.then(function (newContent) { .then(function (newContent) {
var updatePromise = updateDomWithContent(module, speed, newHeader, newContent); const updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
updatePromise.then(resolve).catch(Log.error); updatePromise.then(resolve).catch(Log.error);
}) })
@ -139,7 +139,7 @@ var MM = (function () {
* *
* @returns {Promise} Resolved when the module dom has been updated. * @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) { return new Promise(function (resolve) {
if (module.hidden || !speed) { if (module.hidden || !speed) {
updateModuleContent(module, newHeader, newContent); updateModuleContent(module, newHeader, newContent);
@ -177,23 +177,23 @@ var MM = (function () {
* *
* @returns {boolean} True if the module need an update, false otherwise * @returns {boolean} True if the module need an update, false otherwise
*/ */
var moduleNeedsUpdate = function (module, newHeader, newContent) { const moduleNeedsUpdate = function (module, newHeader, newContent) {
var moduleWrapper = document.getElementById(module.identifier); const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper === null) { if (moduleWrapper === null) {
return false; return false;
} }
var contentWrapper = moduleWrapper.getElementsByClassName("module-content"); const contentWrapper = moduleWrapper.getElementsByClassName("module-content");
var headerWrapper = moduleWrapper.getElementsByClassName("module-header"); const headerWrapper = moduleWrapper.getElementsByClassName("module-header");
var headerNeedsUpdate = false; let headerNeedsUpdate = false;
var contentNeedsUpdate = false; let contentNeedsUpdate;
if (headerWrapper.length > 0) { if (headerWrapper.length > 0) {
headerNeedsUpdate = newHeader !== headerWrapper[0].innerHTML; headerNeedsUpdate = newHeader !== headerWrapper[0].innerHTML;
} }
var tempContentWrapper = document.createElement("div"); const tempContentWrapper = document.createElement("div");
tempContentWrapper.appendChild(newContent); tempContentWrapper.appendChild(newContent);
contentNeedsUpdate = tempContentWrapper.innerHTML !== contentWrapper[0].innerHTML; contentNeedsUpdate = tempContentWrapper.innerHTML !== contentWrapper[0].innerHTML;
@ -207,13 +207,13 @@ var MM = (function () {
* @param {string} newHeader The new header that is generated. * @param {string} newHeader The new header that is generated.
* @param {HTMLElement} newContent The new content that is generated. * @param {HTMLElement} newContent The new content that is generated.
*/ */
var updateModuleContent = function (module, newHeader, newContent) { const updateModuleContent = function (module, newHeader, newContent) {
var moduleWrapper = document.getElementById(module.identifier); const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper === null) { if (moduleWrapper === null) {
return; return;
} }
var headerWrapper = moduleWrapper.getElementsByClassName("module-header"); const headerWrapper = moduleWrapper.getElementsByClassName("module-header");
var contentWrapper = moduleWrapper.getElementsByClassName("module-content"); const contentWrapper = moduleWrapper.getElementsByClassName("module-content");
contentWrapper[0].innerHTML = ""; contentWrapper[0].innerHTML = "";
contentWrapper[0].appendChild(newContent); contentWrapper[0].appendChild(newContent);
@ -234,7 +234,7 @@ var MM = (function () {
* @param {Function} callback Called when the animation is done. * @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the hide method. * @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 || {}; options = options || {};
// set lockString if set in options. // set lockString if set in options.
@ -245,7 +245,7 @@ var MM = (function () {
} }
} }
var moduleWrapper = document.getElementById(module.identifier); const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper !== null) { if (moduleWrapper !== null) {
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s"; moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
moduleWrapper.style.opacity = 0; moduleWrapper.style.opacity = 0;
@ -280,12 +280,12 @@ var MM = (function () {
* @param {Function} callback Called when the animation is done. * @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the show method. * @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 || {}; options = options || {};
// remove lockString if set in options. // remove lockString if set in options.
if (options.lockString) { if (options.lockString) {
var index = module.lockStrings.indexOf(options.lockString); const index = module.lockStrings.indexOf(options.lockString);
if (index !== -1) { if (index !== -1) {
module.lockStrings.splice(index, 1); module.lockStrings.splice(index, 1);
} }
@ -309,7 +309,7 @@ var MM = (function () {
module.lockStrings = []; module.lockStrings = [];
} }
var moduleWrapper = document.getElementById(module.identifier); const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper !== null) { if (moduleWrapper !== null) {
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s"; moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
// Restore the position. See hideModule() for more info. // Restore the position. See hideModule() for more info.
@ -318,7 +318,7 @@ var MM = (function () {
updateWrapperStates(); updateWrapperStates();
// Waiting for DOM-changes done in updateWrapperStates before we can start the animation. // 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; moduleWrapper.style.opacity = 1;
clearTimeout(module.showHideTimer); clearTimeout(module.showHideTimer);
@ -346,14 +346,14 @@ var MM = (function () {
* an ugly top margin. By using this function, the top bar will be hidden if the * an ugly top margin. By using this function, the top bar will be hidden if the
* update notification is not visible. * update notification is not visible.
*/ */
var updateWrapperStates = function () { const 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 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) { positions.forEach(function (position) {
var wrapper = selectWrapper(position); const wrapper = selectWrapper(position);
var moduleWrappers = wrapper.getElementsByClassName("module"); const moduleWrappers = wrapper.getElementsByClassName("module");
var showWrapper = false; let showWrapper = false;
Array.prototype.forEach.call(moduleWrappers, function (moduleWrapper) { Array.prototype.forEach.call(moduleWrappers, function (moduleWrapper) {
if (moduleWrapper.style.position === "" || moduleWrapper.style.position === "static") { if (moduleWrapper.style.position === "" || moduleWrapper.style.position === "static") {
showWrapper = true; showWrapper = true;
@ -367,7 +367,7 @@ var MM = (function () {
/** /**
* Loads the core config and combines it with the system defaults. * 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 // FIXME: Think about how to pass config around without breaking tests
/* eslint-disable */ /* eslint-disable */
if (typeof config === "undefined") { if (typeof config === "undefined") {
@ -385,7 +385,7 @@ var MM = (function () {
* *
* @param {Module[]} modules Array of modules. * @param {Module[]} modules Array of modules.
*/ */
var setSelectionMethodsForModules = function (modules) { const setSelectionMethodsForModules = function (modules) {
/** /**
* Filter modules with the specified classes. * Filter modules with the specified classes.
* *
@ -393,7 +393,7 @@ var MM = (function () {
* *
* @returns {Module[]} Filtered collection of modules. * @returns {Module[]} Filtered collection of modules.
*/ */
var withClass = function (className) { const withClass = function (className) {
return modulesByClass(className, true); return modulesByClass(className, true);
}; };
@ -404,7 +404,7 @@ var MM = (function () {
* *
* @returns {Module[]} Filtered collection of modules. * @returns {Module[]} Filtered collection of modules.
*/ */
var exceptWithClass = function (className) { const exceptWithClass = function (className) {
return modulesByClass(className, false); return modulesByClass(className, false);
}; };
@ -416,17 +416,16 @@ var MM = (function () {
* *
* @returns {Module[]} Filtered collection of modules. * @returns {Module[]} Filtered collection of modules.
*/ */
var modulesByClass = function (className, include) { const modulesByClass = function (className, include) {
var searchClasses = className; let searchClasses = className;
if (typeof className === "string") { if (typeof className === "string") {
searchClasses = className.split(" "); searchClasses = className.split(" ");
} }
var newModules = modules.filter(function (module) { const newModules = modules.filter(function (module) {
var classes = module.data.classes.toLowerCase().split(" "); const classes = module.data.classes.toLowerCase().split(" ");
for (var c in searchClasses) { for (const searchClass of searchClasses) {
var searchClass = searchClasses[c];
if (classes.indexOf(searchClass.toLowerCase()) !== -1) { if (classes.indexOf(searchClass.toLowerCase()) !== -1) {
return include; return include;
} }
@ -445,8 +444,8 @@ var MM = (function () {
* @param {object} module The module instance to remove from the collection. * @param {object} module The module instance to remove from the collection.
* @returns {Module[]} Filtered collection of modules. * @returns {Module[]} Filtered collection of modules.
*/ */
var exceptModule = function (module) { const exceptModule = function (module) {
var newModules = modules.filter(function (mod) { const newModules = modules.filter(function (mod) {
return mod.identifier !== module.identifier; return mod.identifier !== module.identifier;
}); });
@ -459,7 +458,7 @@ var MM = (function () {
* *
* @param {Function} callback The function to execute with the module as an argument. * @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) { modules.map(function (module) {
callback(module); callback(module);
}); });
@ -604,11 +603,11 @@ if (typeof Object.assign !== "function") {
if (target === undefined || target === null) { if (target === undefined || target === null) {
throw new TypeError("Cannot convert undefined or null to object"); throw new TypeError("Cannot convert undefined or null to object");
} }
var output = Object(target); const output = Object(target);
for (var index = 1; index < arguments.length; index++) { for (let index = 1; index < arguments.length; index++) {
var source = arguments[index]; const source = arguments[index];
if (source !== undefined && source !== null) { if (source !== undefined && source !== null) {
for (var nextKey in source) { for (const nextKey in source) {
if (source.hasOwnProperty(nextKey)) { if (source.hasOwnProperty(nextKey)) {
output[nextKey] = source[nextKey]; output[nextKey] = source[nextKey];
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -122,8 +122,10 @@
/** /**
* Dismiss the notification * 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; this.active = false;
clearTimeout(this.dismissttl); clearTimeout(this.dismissttl);
this.ntf.classList.remove("ns-show"); this.ntf.classList.remove("ns-show");
@ -131,7 +133,7 @@
this.ntf.classList.add("ns-hide"); this.ntf.classList.add("ns-hide");
// callback // callback
this.options.onClose(); if (close) this.options.onClose();
}, 25); }, 25);
// after animation ends remove ntf from the DOM // after animation ends remove ntf from the DOM

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,9 @@
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
// Alias modules mentioned in package.js under _moduleAliases.
require("module-alias/register");
const CalendarFetcher = require("./calendarfetcher.js"); 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 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) { fetcher.onReceive(function (fetcher) {
console.log(fetcher.events()); console.log(fetcher.events());
console.log("------------------------------------------------------------"); console.log("------------------------------------------------------------");
process.exit(0);
}); });
fetcher.onError(function (fetcher, error) { fetcher.onError(function (fetcher, error) {
console.log("Fetcher error:"); console.log("Fetcher error:");
console.log(error); console.log(error);
process.exit(1);
}); });
fetcher.startFetch(); fetcher.startFetch();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -90,8 +90,8 @@ Module.register("newsfeed", {
this.loaded = true; this.loaded = true;
this.error = null; this.error = null;
} else if (notification === "INCORRECT_URL") { } else if (notification === "NEWSFEED_ERROR") {
this.error = `Incorrect url: ${payload.url}`; this.error = this.translate(payload.error_type);
this.scheduleUpdateInterval(); this.scheduleUpdateInterval();
} }
}, },
@ -189,9 +189,9 @@ Module.register("newsfeed", {
} }
if (this.config.prohibitedWords.length > 0) { if (this.config.prohibitedWords.length > 0) {
newsItems = newsItems.filter(function (value) { newsItems = newsItems.filter(function (item) {
for (let word of this.config.prohibitedWords) { 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 false;
} }
} }

View File

@ -3,45 +3,47 @@
<ul class="newsfeed-list"> <ul class="newsfeed-list">
{% for item in items %} {% for item in items %}
<li> <li>
{% if (config.showSourceTitle and item.sourceTitle) or config.showPublishDate %} {% if (config.showSourceTitle and item.sourceTitle) or config.showPublishDate %}
<div class="newsfeed-source light small dimmed"> <div class="newsfeed-source light small dimmed">
{% if item.sourceTitle and config.showSourceTitle %} {% if item.sourceTitle and config.showSourceTitle %}
{{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}: {% endif %} {{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}: {% endif %}
{% endif %} {% endif %}
{% if config.showPublishDate %} {% if config.showPublishDate %}
{{ item.publishDate }}: {{ item.publishDate }}:
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}"> <div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
{{ item.title }} {{ item.title }}
</div> </div>
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}"> {% if config.showDescription %}
{% if config.truncDescription %} <div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
{{ item.description | truncate(config.lengthDescription) }} {% if config.truncDescription %}
{% else %} {{ item.description | truncate(config.lengthDescription) }}
{{ item.description }} {% else %}
{% endif %} {{ item.description }}
</div> {% endif %}
</div> </div>
</li> {% endif %}
{% endfor %} </li>
</ul> {% endfor %}
{% else %} </ul>
{% else %}
<div> <div>
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %} {% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
<div class="newsfeed-source light small dimmed"> <div class="newsfeed-source light small dimmed">
{% if sourceTitle and config.showSourceTitle %} {% if sourceTitle and config.showSourceTitle %}
{{ sourceTitle }}{% if config.showPublishDate %}, {% else %}: {% endif %} {{ sourceTitle }}{% if config.showPublishDate %}, {% else %}: {% endif %}
{% endif %} {% endif %}
{% if config.showPublishDate %} {% if config.showPublishDate %}
{{ publishDate }}: {{ publishDate }}:
{% endif %} {% endif %}
</div>
{% endif %}
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
{{ title }}
</div> </div>
{% endif %}
<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 }}"> <div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
{% if config.truncDescription %} {% if config.truncDescription %}
{{ description | truncate(config.lengthDescription) }} {{ description | truncate(config.lengthDescription) }}
@ -49,7 +51,8 @@
{{ description }} {{ description }}
{% endif %} {% endif %}
</div> </div>
</div> {% endif %}
</div>
{% endif %} {% endif %}
{% elseif error %} {% elseif error %}
<div class="small dimmed"> <div class="small dimmed">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,664 @@
/* 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.precipitationUnits = foreGroup[today].querySelector("precipitation accumulation amount").getAttribute("units");
weather.precipitation = foreGroup[today].querySelector("precipitation accumulation amount").textContent * 1.0;
}
// 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 cm or mm to inches
//
convertPrecipAmt(amt, units) {
if (this.config.units === "imperial") {
if (units === "cm") {
return amt * 0.394;
}
if (units === "mm") {
return amt * 0.0394;
}
} else {
return amt;
}
},
//
// Convert ensure precip units accurately reflect configured units
//
convertPrecipUnits(units) {
if (this.config.units === "imperial") {
return null;
} else {
return " " + units;
}
},
//
// Convert the icons to a more usable name.
//
convertWeatherType(weatherType) {
const weatherTypes = {
"00": "day-sunny",
"01": "day-sunny",
"02": "day-sunny-overcast",
"03": "day-cloudy",
"04": "day-cloudy",
"05": "day-cloudy",
"06": "day-sprinkle",
"07": "day-showers",
"08": "day-snow",
"09": "day-thunderstorm",
10: "cloud",
11: "showers",
12: "rain",
13: "rain",
14: "sleet",
15: "sleet",
16: "snow",
17: "snow",
18: "snow",
19: "thunderstorm",
20: "cloudy",
21: "cloudy",
22: "day-cloudy",
23: "day-haze",
24: "fog",
25: "snow-wind",
26: "sleet",
27: "sleet",
28: "rain",
29: "na",
30: "night-clear",
31: "night-clear",
32: "night-partly-cloudy",
33: "night-alt-cloudy",
34: "night-alt-cloudy",
35: "night-partly-cloudy",
36: "night-alt-showers",
37: "night-rain-mix",
38: "night-alt-snow",
39: "night-thunderstorm",
40: "snow-wind",
41: "tornado",
42: "tornado",
43: "windy",
44: "smoke",
45: "sandstorm",
46: "thunderstorm",
47: "thunderstorm",
48: "tornado"
};
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@
* *
* This class is the blueprint for a weather provider. * This class is the blueprint for a weather provider.
*/ */
var WeatherProvider = Class.extend({ const WeatherProvider = Class.extend({
// Weather Provider Properties // Weather Provider Properties
providerName: null, providerName: null,
defaults: {}, defaults: {},
@ -114,7 +114,7 @@ var WeatherProvider = Class.extend({
// A convenience function to make requests. It returns a promise. // A convenience function to make requests. It returns a promise.
fetchData: function (url, method = "GET", data = null) { fetchData: function (url, method = "GET", data = null) {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
var request = new XMLHttpRequest(); const request = new XMLHttpRequest();
request.open(method, url, true); request.open(method, url, true);
request.onreadystatechange = function () { request.onreadystatechange = function () {
if (this.readyState === 4) { if (this.readyState === 4) {

1111
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "magicmirror", "name": "magicmirror",
"version": "2.15.0", "version": "2.16.0-develop",
"description": "The open source modular smart mirror platform.", "description": "The open source modular smart mirror platform.",
"main": "js/electron.js", "main": "js/electron.js",
"scripts": { "scripts": {
@ -14,14 +14,16 @@
"test:coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text mocha tests --recursive --timeout=3000", "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:e2e": "NODE_ENV=test mocha tests/e2e --recursive",
"test:unit": "NODE_ENV=test mocha tests/unit --recursive", "test:unit": "NODE_ENV=test mocha tests/unit --recursive",
"test:prettier": "prettier --check **/*.{js,css,json,md,yml}", "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: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:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json",
"test:calendar": "node ./modules/default/calendar/debug.js", "test:calendar": "node ./modules/default/calendar/debug.js",
"config:check": "node js/check_config.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: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": { "repository": {
"type": "git", "type": "git",
@ -45,58 +47,51 @@
"devDependencies": { "devDependencies": {
"chai": "^4.3.4", "chai": "^4.3.4",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"eslint-config-prettier": "^8.1.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-jsdoc": "^32.3.0", "eslint-plugin-jsdoc": "^35.0.0",
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.4.0",
"express-basic-auth": "^1.2.0", "express-basic-auth": "^1.2.0",
"husky": "^4.3.8", "husky": "^6.0.0",
"jsdom": "^16.5.1", "jsdom": "^16.6.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mocha": "^8.3.2", "mocha": "^8.4.0",
"mocha-each": "^2.0.1", "mocha-each": "^2.0.1",
"mocha-logger": "^1.0.7", "mocha-logger": "^1.0.7",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"prettier": "^2.2.1", "prettier": "^2.3.0",
"pretty-quick": "^3.1.0", "pretty-quick": "^3.1.0",
"sinon": "^10.0.0", "sinon": "^11.1.1",
"spectron": "^13.0.0", "spectron": "^13.0.0",
"stylelint": "^13.12.0", "stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2", "stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^21.0.0", "stylelint-config-standard": "^22.0.0",
"stylelint-prettier": "^1.2.0" "stylelint-prettier": "^1.2.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"electron": "^11.3.0" "electron": "^11.4.7"
}, },
"dependencies": { "dependencies": {
"colors": "^1.4.0", "colors": "^1.4.0",
"console-stamp": "^3.0.0-rc4.2", "console-stamp": "^3.0.2",
"digest-fetch": "^1.1.6", "digest-fetch": "^1.2.0",
"eslint": "^7.23.0", "eslint": "^7.27.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-ipfilter": "^1.1.2", "express-ipfilter": "^1.2.0",
"feedme": "^2.0.2", "feedme": "^2.0.2",
"helmet": "^4.4.1", "helmet": "^4.6.0",
"iconv-lite": "^0.6.2", "iconv-lite": "^0.6.3",
"module-alias": "^2.2.2", "module-alias": "^2.2.2",
"moment": "^2.29.1", "moment": "^2.29.1",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"node-ical": "^0.12.9", "node-ical": "^0.13.0",
"rrule": "^2.6.8", "simple-git": "^2.39.0",
"rrule-alt": "^2.2.8", "socket.io": "^4.1.2"
"simple-git": "^2.37.0",
"socket.io": "^4.0.0"
}, },
"_moduleAliases": { "_moduleAliases": {
"node_helper": "js/node_helper.js", "node_helper": "js/node_helper.js",
"logger": "js/logger.js" "logger": "js/logger.js"
}, },
"engines": { "engines": {
"node": ">=10" "node": ">=12"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
} }
} }

View File

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

View File

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

View File

@ -1,44 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" <?xml version="1.0" encoding="UTF-8"?>
xmlns:content="http://purl.org/rss/1.0/modules/content/" <rss version="2.0"
xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:wfw="http://wellformedweb.org/CommentAPI/"
xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
> xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
>
<channel>
<title>Rodrigo Ramírez Norambuena</title>
<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>
<language>es-ES</language>
<sy:updatePeriod>hourly</sy:updatePeriod>
<sy:updateFrequency>1</sy:updateFrequency>
<generator>https://wordpress.org/?v=4.7.3</generator>
<item>
<title>QPanel 0.13.0</title>
<link>https://rodrigoramirez.com/qpanel-0-13-0/</link>
<comments>https://rodrigoramirez.com/qpanel-0-13-0/#comments</comments>
<pubDate>Tue, 20 Sep 2016 11:16:08 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category>
<category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category>
<category><![CDATA[spy]]></category>
<category><![CDATA[supervision]]></category>
<category><![CDATA[templates]]></category>
<category><![CDATA[whisper]]></category>
<channel> <guid isPermaLink="false">https://rodrigoramirez.com/?p=1299</guid>
<title>Rodrigo Ramírez Norambuena</title> <description><![CDATA[<p>Ya está disponible la versión 0.13.0 de QPanel Para instalar esta nueva versión, la debes descargar de https://github.com/roramirez/qpanel/tree/0.13.0 En al README.md puedes encontrar las instrucciones para hacer que funcione en tu sistema. En esta nueva versión cuenta con los siguientes cambios: Se establece un limite para el reciclado del tiempo de conexión a la base [&#8230;]</p>
<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>
<language>es-ES</language>
<sy:updatePeriod>hourly</sy:updatePeriod>
<sy:updateFrequency>1</sy:updateFrequency>
<generator>https://wordpress.org/?v=4.7.3</generator>
<item>
<title>QPanel 0.13.0</title>
<link>https://rodrigoramirez.com/qpanel-0-13-0/</link>
<comments>https://rodrigoramirez.com/qpanel-0-13-0/#comments</comments>
<pubDate>Tue, 20 Sep 2016 11:16:08 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category>
<category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category>
<category><![CDATA[spy]]></category>
<category><![CDATA[supervision]]></category>
<category><![CDATA[templates]]></category>
<category><![CDATA[whisper]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1299</guid>
<description><![CDATA[<p>Ya está disponible la versión 0.13.0 de QPanel Para instalar esta nueva versión, la debes descargar de https://github.com/roramirez/qpanel/tree/0.13.0 En al README.md puedes encontrar las instrucciones para hacer que funcione en tu sistema. En esta nueva versión cuenta con los siguientes cambios: Se establece un limite para el reciclado del tiempo de conexión a la base [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-13-0/">QPanel 0.13.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-13-0/">QPanel 0.13.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description> ]]></description>
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible la versión 0.13.0 de QPanel</p> <content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible la versión 0.13.0 de QPanel</p>
<p>Para instalar esta nueva versión, la debes descargar de</p> <p>Para instalar esta nueva versión, la debes descargar de</p>
<ul> <ul>
<li><a href="https://github.com/roramirez/qpanel/tree/0.13.0">https://github.com/roramirez/qpanel/tree/0.13.0</a></li> <li><a href="https://github.com/roramirez/qpanel/tree/0.13.0">https://github.com/roramirez/qpanel/tree/0.13.0</a></li>
@ -57,25 +57,25 @@
<p>&nbsp;</p> <p>&nbsp;</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-13-0/">QPanel 0.13.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-13-0/">QPanel 0.13.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded> ]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-13-0/feed/</wfw:commentRss> <wfw:commentRss>https://rodrigoramirez.com/qpanel-0-13-0/feed/</wfw:commentRss>
<slash:comments>3</slash:comments> <slash:comments>3</slash:comments>
</item> </item>
<item> <item>
<title>Problema VirtualBox &#8220;starting virtual machine&#8221; &#8230;</title> <title>Problema VirtualBox &#8220;starting virtual machine&#8221; &#8230;</title>
<link>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/</link> <link>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/</link>
<comments>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/#respond</comments> <comments>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/#respond</comments>
<pubDate>Sat, 10 Sep 2016 22:50:13 +0000</pubDate> <pubDate>Sat, 10 Sep 2016 22:50:13 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator> <dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category> <category><![CDATA[Linux]]></category>
<category><![CDATA[no arranca]]></category> <category><![CDATA[no arranca]]></category>
<category><![CDATA[Problema]]></category> <category><![CDATA[Problema]]></category>
<category><![CDATA[VirtualBox]]></category> <category><![CDATA[VirtualBox]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1284</guid> <guid isPermaLink="false">https://rodrigoramirez.com/?p=1284</guid>
<description><![CDATA[<p>Después de una actualización de Debian, de la rama stretch/sid, tuve un problema con VirtualBox.  La versión que se actualizó fue a la virtualbox 5.1.4-dfsg-1+b1. El gran problema era que ninguna maquina virtual quería arrancar, se quedaba en un largo limbo con el mensaje &#8220;starting virtual machine&#8221;, como el de la imagen de a continuación. [&#8230;]</p> <description><![CDATA[<p>Después de una actualización de Debian, de la rama stretch/sid, tuve un problema con VirtualBox.  La versión que se actualizó fue a la virtualbox 5.1.4-dfsg-1+b1. El gran problema era que ninguna maquina virtual quería arrancar, se quedaba en un largo limbo con el mensaje &#8220;starting virtual machine&#8221;, como el de la imagen de a continuación. [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/">Problema VirtualBox &#8220;starting virtual machine&#8221; &#8230;</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/">Problema VirtualBox &#8220;starting virtual machine&#8221; &#8230;</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description> ]]></description>
<content:encoded><![CDATA[<p>Después de una actualización de Debian, de la rama stretch/sid, tuve un problema con VirtualBox.  La versión que se actualizó fue a la virtualbox 5.1.4-dfsg-1+b1. El gran problema era que ninguna maquina virtual quería arrancar, se quedaba en un largo limbo con el mensaje &#8220;starting virtual machine&#8221;, como el de la imagen de a continuación.</p> <content:encoded><![CDATA[<p>Después de una actualización de Debian, de la rama stretch/sid, tuve un problema con VirtualBox.  La versión que se actualizó fue a la virtualbox 5.1.4-dfsg-1+b1. El gran problema era que ninguna maquina virtual quería arrancar, se quedaba en un largo limbo con el mensaje &#8220;starting virtual machine&#8221;, como el de la imagen de a continuación.</p>
<p><a href="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09.png"><img class="aligncenter wp-image-1290 size-full" src="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09.png" alt="Starting virtual machine ... VirtualBox" width="648" height="554" srcset="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09.png 648w, https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09-300x256.png 300w" sizes="(max-width: 648px) 100vw, 648px" /></a></p> <p><a href="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09.png"><img class="aligncenter wp-image-1290 size-full" src="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09.png" alt="Starting virtual machine ... VirtualBox" width="648" height="554" srcset="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09.png 648w, https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09-300x256.png 300w" sizes="(max-width: 648px) 100vw, 648px" /></a></p>
<p>Ninguna, pero ninguna maquina arrancó, se quedaban en ese mensaje. Fue de esos instantes en que sudas helado &#8230; <img src="https://s.w.org/images/core/emoji/2.2.1/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p> <p>Ninguna, pero ninguna maquina arrancó, se quedaban en ese mensaje. Fue de esos instantes en que sudas helado &#8230; <img src="https://s.w.org/images/core/emoji/2.2.1/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>Con un poco de investigación fue a parar al archivo<em> ~/.VirtualBox/VBoxSVC.log </em>que indicaba</p> <p>Con un poco de investigación fue a parar al archivo<em> ~/.VirtualBox/VBoxSVC.log </em>que indicaba</p>
@ -95,24 +95,24 @@ $ ls -lh /dev/vboxdrvu
crw-rw-rw- 1 root root 10, 56 Sep 10 12:47 /dev/vboxdrvu</pre> crw-rw-rw- 1 root root 10, 56 Sep 10 12:47 /dev/vboxdrvu</pre>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/">Problema VirtualBox &#8220;starting virtual machine&#8221; &#8230;</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/">Problema VirtualBox &#8220;starting virtual machine&#8221; &#8230;</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded> ]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/feed/</wfw:commentRss> <wfw:commentRss>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/feed/</wfw:commentRss>
<slash:comments>0</slash:comments> <slash:comments>0</slash:comments>
</item> </item>
<item> <item>
<title>Mejorando la consola interactiva de Python</title> <title>Mejorando la consola interactiva de Python</title>
<link>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/</link> <link>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/</link>
<comments>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/#comments</comments> <comments>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/#comments</comments>
<pubDate>Tue, 06 Sep 2016 04:24:43 +0000</pubDate> <pubDate>Tue, 06 Sep 2016 04:24:43 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator> <dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[desarrollo]]></category> <category><![CDATA[desarrollo]]></category>
<category><![CDATA[Desarrollo]]></category> <category><![CDATA[Desarrollo]]></category>
<category><![CDATA[Python]]></category> <category><![CDATA[Python]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1247</guid> <guid isPermaLink="false">https://rodrigoramirez.com/?p=1247</guid>
<description><![CDATA[<p>Cuando estás desarrollando en Python es muy cool estar utilizando la consola interactiva para ir probando cosas antes de ponerlas dentro del archivo de código fuente. La consola de Python funciona y cumple su cometido. Solo al tipear  python  te permite entrar en modo interactivo e ir probando cosas. El punto es que a veces [&#8230;]</p> <description><![CDATA[<p>Cuando estás desarrollando en Python es muy cool estar utilizando la consola interactiva para ir probando cosas antes de ponerlas dentro del archivo de código fuente. La consola de Python funciona y cumple su cometido. Solo al tipear  python  te permite entrar en modo interactivo e ir probando cosas. El punto es que a veces [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/">Mejorando la consola interactiva de Python</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/">Mejorando la consola interactiva de Python</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description> ]]></description>
<content:encoded><![CDATA[<p>Cuando estás desarrollando en Python es muy <em>cool</em> estar utilizando la consola interactiva para ir probando cosas antes de ponerlas dentro del archivo de código fuente.</p> <content:encoded><![CDATA[<p>Cuando estás desarrollando en Python es muy <em>cool</em> estar utilizando la consola interactiva para ir probando cosas antes de ponerlas dentro del archivo de código fuente.</p>
<p>La consola de Python funciona y cumple su cometido. Solo al tipear  <em>python  </em>te permite entrar en modo interactivo e ir probando cosas.</p> <p>La consola de Python funciona y cumple su cometido. Solo al tipear  <em>python  </em>te permite entrar en modo interactivo e ir probando cosas.</p>
<p>El punto es que a veces uno necesita ir un poco más allá. Como autocomentado de código o resaltado de sintaxis, para eso tengo dos truco que utilizo generalmente.</p> <p>El punto es que a veces uno necesita ir un poco más allá. Como autocomentado de código o resaltado de sintaxis, para eso tengo dos truco que utilizo generalmente.</p>
<h2>Truco a)</h2> <h2>Truco a)</h2>
@ -139,31 +139,31 @@ $ ls -lh /dev/vboxdrvu
<p>O lo agregas a un bashrc, zshrc o la shell que ocupes.</p> <p>O lo agregas a un bashrc, zshrc o la shell que ocupes.</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/">Mejorando la consola interactiva de Python</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/">Mejorando la consola interactiva de Python</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded> ]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/feed/</wfw:commentRss> <wfw:commentRss>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/feed/</wfw:commentRss>
<slash:comments>4</slash:comments> <slash:comments>4</slash:comments>
</item> </item>
<item> <item>
<title>QPanel 0.12.0 con estadísticas</title> <title>QPanel 0.12.0 con estadísticas</title>
<link>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/</link> <link>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/</link>
<comments>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/#respond</comments> <comments>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/#respond</comments>
<pubDate>Mon, 22 Aug 2016 04:19:03 +0000</pubDate> <pubDate>Mon, 22 Aug 2016 04:19:03 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator> <dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category> <category><![CDATA[Software]]></category>
<category><![CDATA[app_queue]]></category> <category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category> <category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category> <category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category> <category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category> <category><![CDATA[queue]]></category>
<category><![CDATA[spy]]></category> <category><![CDATA[spy]]></category>
<category><![CDATA[supervision]]></category> <category><![CDATA[supervision]]></category>
<category><![CDATA[templates]]></category> <category><![CDATA[templates]]></category>
<category><![CDATA[whisper]]></category> <category><![CDATA[whisper]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1268</guid> <guid isPermaLink="false">https://rodrigoramirez.com/?p=1268</guid>
<description><![CDATA[<p>Ya está disponible una nueva versión de QPanel, esta es la 0.12.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.12.0 En esta nueva versión las funcionalidades agregadas son: Permite remover los agentes de las cola Posibilidad de cancelar llamadas que están en espera de atención Estadísticas por rango de fecha obtenidas desde [&#8230;]</p> <description><![CDATA[<p>Ya está disponible una nueva versión de QPanel, esta es la 0.12.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.12.0 En esta nueva versión las funcionalidades agregadas son: Permite remover los agentes de las cola Posibilidad de cancelar llamadas que están en espera de atención Estadísticas por rango de fecha obtenidas desde [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/">QPanel 0.12.0 con estadísticas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/">QPanel 0.12.0 con estadísticas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description> ]]></description>
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible una nueva versión de QPanel, esta es la 0.12.0</p> <content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible una nueva versión de QPanel, esta es la 0.12.0</p>
<p>Para instalar esta nueva versión, debes visitar la siguiente URL</p> <p>Para instalar esta nueva versión, debes visitar la siguiente URL</p>
<ul> <ul>
<li><a href="https://github.com/roramirez/qpanel/tree/0.12.0">https://github.com/roramirez/qpanel/tree/0.12.0</a></li> <li><a href="https://github.com/roramirez/qpanel/tree/0.12.0">https://github.com/roramirez/qpanel/tree/0.12.0</a></li>
@ -178,31 +178,31 @@ $ ls -lh /dev/vboxdrvu
<p>Si deseas colaborar con el proyecto puedes agregar nuevas sugerencias mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a> ó colaborar mediante <a href="https://github.com/roramirez/qpanel/blob/dd42cf0f534408505f57b0d387dffee2f3688711/README.md#how-to-contribute">mediante un Pull Request</a></p> <p>Si deseas colaborar con el proyecto puedes agregar nuevas sugerencias mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a> ó colaborar mediante <a href="https://github.com/roramirez/qpanel/blob/dd42cf0f534408505f57b0d387dffee2f3688711/README.md#how-to-contribute">mediante un Pull Request</a></p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/">QPanel 0.12.0 con estadísticas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/">QPanel 0.12.0 con estadísticas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded> ]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/feed/</wfw:commentRss> <wfw:commentRss>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/feed/</wfw:commentRss>
<slash:comments>0</slash:comments> <slash:comments>0</slash:comments>
</item> </item>
<item> <item>
<title>QPanel 0.11.0 con Spy, Whisper y mas</title> <title>QPanel 0.11.0 con Spy, Whisper y mas</title>
<link>https://rodrigoramirez.com/qpanel-spy-supervisor/</link> <link>https://rodrigoramirez.com/qpanel-spy-supervisor/</link>
<comments>https://rodrigoramirez.com/qpanel-spy-supervisor/#comments</comments> <comments>https://rodrigoramirez.com/qpanel-spy-supervisor/#comments</comments>
<pubDate>Thu, 21 Jul 2016 01:53:21 +0000</pubDate> <pubDate>Thu, 21 Jul 2016 01:53:21 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator> <dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category> <category><![CDATA[Software]]></category>
<category><![CDATA[app_queue]]></category> <category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category> <category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category> <category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category> <category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category> <category><![CDATA[queue]]></category>
<category><![CDATA[spy]]></category> <category><![CDATA[spy]]></category>
<category><![CDATA[supervision]]></category> <category><![CDATA[supervision]]></category>
<category><![CDATA[templates]]></category> <category><![CDATA[templates]]></category>
<category><![CDATA[whisper]]></category> <category><![CDATA[whisper]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1245</guid> <guid isPermaLink="false">https://rodrigoramirez.com/?p=1245</guid>
<description><![CDATA[<p>Ya está disponible una nueva versión de QPanel, esta es la 0.11.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.11.0 Esta versión hemos agregado  algunas funcionalidades que los usuarios  han ido solicitando. Para esta versión es posible realizar Spy, Whisper o Barge a un canal para la supervisión de los miembros que [&#8230;]</p> <description><![CDATA[<p>Ya está disponible una nueva versión de QPanel, esta es la 0.11.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.11.0 Esta versión hemos agregado  algunas funcionalidades que los usuarios  han ido solicitando. Para esta versión es posible realizar Spy, Whisper o Barge a un canal para la supervisión de los miembros que [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-spy-supervisor/">QPanel 0.11.0 con Spy, Whisper y mas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-spy-supervisor/">QPanel 0.11.0 con Spy, Whisper y mas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description> ]]></description>
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible una nueva versión de QPanel, esta es la 0.11.0</p> <content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible una nueva versión de QPanel, esta es la 0.11.0</p>
<p>Para instalar esta nueva versión, debes visitar la siguiente URL</p> <p>Para instalar esta nueva versión, debes visitar la siguiente URL</p>
<ul> <ul>
<li><a href="https://github.com/roramirez/qpanel/tree/0.11.0">https://github.com/roramirez/qpanel/tree/0.11.0</a></li> <li><a href="https://github.com/roramirez/qpanel/tree/0.11.0">https://github.com/roramirez/qpanel/tree/0.11.0</a></li>
@ -216,22 +216,22 @@ $ ls -lh /dev/vboxdrvu
<p>El proyecto siempre está abierto a nuevas sugerencias las cuales puedes agregar mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a>.</p> <p>El proyecto siempre está abierto a nuevas sugerencias las cuales puedes agregar mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a>.</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-spy-supervisor/">QPanel 0.11.0 con Spy, Whisper y mas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-spy-supervisor/">QPanel 0.11.0 con Spy, Whisper y mas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded> ]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-spy-supervisor/feed/</wfw:commentRss> <wfw:commentRss>https://rodrigoramirez.com/qpanel-spy-supervisor/feed/</wfw:commentRss>
<slash:comments>4</slash:comments> <slash:comments>4</slash:comments>
</item> </item>
<item> <item>
<title>Añadir Swap a un sistema</title> <title>Añadir Swap a un sistema</title>
<link>https://rodrigoramirez.com/crear-swap/</link> <link>https://rodrigoramirez.com/crear-swap/</link>
<comments>https://rodrigoramirez.com/crear-swap/#respond</comments> <comments>https://rodrigoramirez.com/crear-swap/#respond</comments>
<pubDate>Fri, 15 Jul 2016 05:07:43 +0000</pubDate> <pubDate>Fri, 15 Jul 2016 05:07:43 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator> <dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category> <category><![CDATA[Linux]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1234</guid> <guid isPermaLink="false">https://rodrigoramirez.com/?p=1234</guid>
<description><![CDATA[<p>Algo que me toma generalmente hacer es cuando trabajo con maquina virtuales es asignar una cantidad determinada de Swap. La  memoria swap es un espacio de intercambio en disco para cuando el sistema ya no puede utilizar más memoria RAM. El problema para mi es que algunos sistemas de maquinas virtuales no asignan por defecto [&#8230;]</p> <description><![CDATA[<p>Algo que me toma generalmente hacer es cuando trabajo con maquina virtuales es asignar una cantidad determinada de Swap. La  memoria swap es un espacio de intercambio en disco para cuando el sistema ya no puede utilizar más memoria RAM. El problema para mi es que algunos sistemas de maquinas virtuales no asignan por defecto [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/crear-swap/">Añadir Swap a un sistema</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/crear-swap/">Añadir Swap a un sistema</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description> ]]></description>
<content:encoded><![CDATA[<p>Algo que me toma generalmente hacer es cuando trabajo con maquina virtuales es asignar una cantidad determinada de Swap.</p> <content:encoded><![CDATA[<p>Algo que me toma generalmente hacer es cuando trabajo con maquina virtuales es asignar una cantidad determinada de Swap.</p>
<p>La  memoria swap es un espacio de intercambio en disco para cuando el sistema ya no puede utilizar más memoria RAM.</p> <p>La  memoria swap es un espacio de intercambio en disco para cuando el sistema ya no puede utilizar más memoria RAM.</p>
<p>El problema para mi es que algunos sistemas de maquinas virtuales no asignan por defecto un espacio para la Swap, lo que te lleva a que el sistema pueda tener crash durante la ejecución.</p> <p>El problema para mi es que algunos sistemas de maquinas virtuales no asignan por defecto un espacio para la Swap, lo que te lleva a que el sistema pueda tener crash durante la ejecución.</p>
<p>Para comprobar la asignación de memoria, al ejecutar el comando <em>free</em> nos debería mostrar como algo similar a lo siguiente</p> <p>Para comprobar la asignación de memoria, al ejecutar el comando <em>free</em> nos debería mostrar como algo similar a lo siguiente</p>
@ -271,27 +271,27 @@ Swap:         3071          0       3071</pre>
<p>&nbsp;</p> <p>&nbsp;</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/crear-swap/">Añadir Swap a un sistema</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/crear-swap/">Añadir Swap a un sistema</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded> ]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/crear-swap/feed/</wfw:commentRss> <wfw:commentRss>https://rodrigoramirez.com/crear-swap/feed/</wfw:commentRss>
<slash:comments>0</slash:comments> <slash:comments>0</slash:comments>
</item> </item>
<item> <item>
<title>QPanel 0.10.0 con vista consolidada</title> <title>QPanel 0.10.0 con vista consolidada</title>
<link>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/</link> <link>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/</link>
<comments>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/#respond</comments> <comments>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/#respond</comments>
<pubDate>Mon, 20 Jun 2016 19:32:55 +0000</pubDate> <pubDate>Mon, 20 Jun 2016 19:32:55 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator> <dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category> <category><![CDATA[Linux]]></category>
<category><![CDATA[app_queue]]></category> <category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category> <category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category> <category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category> <category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category> <category><![CDATA[queue]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1227</guid> <guid isPermaLink="false">https://rodrigoramirez.com/?p=1227</guid>
<description><![CDATA[<p>Ya con la release numero 28 la nueva versión 0.10.0 de QPanel ya está disponible. Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.10.0 Esta versión versión nos preocupamos de realizar mejoras, refactorizaciones y agregamos una nueva funcionalidad. La nueva funcionalidad incluida es  que ahora es posible contar con una vista consolidada para [&#8230;]</p> <description><![CDATA[<p>Ya con la release numero 28 la nueva versión 0.10.0 de QPanel ya está disponible. Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.10.0 Esta versión versión nos preocupamos de realizar mejoras, refactorizaciones y agregamos una nueva funcionalidad. La nueva funcionalidad incluida es  que ahora es posible contar con una vista consolidada para [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/">QPanel 0.10.0 con vista consolidada</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/">QPanel 0.10.0 con vista consolidada</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description> ]]></description>
<content:encoded><![CDATA[<p><img class="alignleft" src="https://raw.githubusercontent.com/roramirez/qpanel/0.10.0/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="403" height="227" />Ya con la release numero 28 la nueva versión 0.10.0 de QPanel ya está disponible.</p> <content:encoded><![CDATA[<p><img class="alignleft" src="https://raw.githubusercontent.com/roramirez/qpanel/0.10.0/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="403" height="227" />Ya con la release numero 28 la nueva versión 0.10.0 de QPanel ya está disponible.</p>
<p>Para instalar esta nueva versión, debes visitar la siguiente URL</p> <p>Para instalar esta nueva versión, debes visitar la siguiente URL</p>
<ul> <ul>
<li><a href="https://github.com/roramirez/qpanel/tree/0.10.0">https://github.com/roramirez/qpanel/tree/0.10.0</a></li> <li><a href="https://github.com/roramirez/qpanel/tree/0.10.0">https://github.com/roramirez/qpanel/tree/0.10.0</a></li>
@ -301,29 +301,29 @@ Swap:         3071          0       3071</pre>
<p>El proyecto siempre está abierto a nuevas sugerencias las cuales puedes agregar mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a>.</p> <p>El proyecto siempre está abierto a nuevas sugerencias las cuales puedes agregar mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a>.</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/">QPanel 0.10.0 con vista consolidada</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/">QPanel 0.10.0 con vista consolidada</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded> ]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/feed/</wfw:commentRss> <wfw:commentRss>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/feed/</wfw:commentRss>
<slash:comments>0</slash:comments> <slash:comments>0</slash:comments>
</item> </item>
<item> <item>
<title>Nerdearla 2016, WebRTC Glue</title> <title>Nerdearla 2016, WebRTC Glue</title>
<link>https://rodrigoramirez.com/nerdearla-2016/</link> <link>https://rodrigoramirez.com/nerdearla-2016/</link>
<comments>https://rodrigoramirez.com/nerdearla-2016/#respond</comments> <comments>https://rodrigoramirez.com/nerdearla-2016/#respond</comments>
<pubDate>Wed, 15 Jun 2016 17:55:41 +0000</pubDate> <pubDate>Wed, 15 Jun 2016 17:55:41 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator> <dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category> <category><![CDATA[Linux]]></category>
<category><![CDATA[baires]]></category> <category><![CDATA[baires]]></category>
<category><![CDATA[charla]]></category> <category><![CDATA[charla]]></category>
<category><![CDATA[Computación]]></category> <category><![CDATA[Computación]]></category>
<category><![CDATA[informatica]]></category> <category><![CDATA[informatica]]></category>
<category><![CDATA[tech]]></category> <category><![CDATA[tech]]></category>
<category><![CDATA[ti]]></category> <category><![CDATA[ti]]></category>
<category><![CDATA[webrtc]]></category> <category><![CDATA[webrtc]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1218</guid> <guid isPermaLink="false">https://rodrigoramirez.com/?p=1218</guid>
<description><![CDATA[<p>Días atrás estuve participando en el evento llamado Nerdearla en Buenos Aires.  El ambiente era genial si eres de esas personas que desde niño sintio curiosidad por ver como funcionan las cosas, donde desarmabas para volver armar lo juguetes. Habían muchas cosas interesantes tanto en las presentaciones, co-working y workshop que se hubieron. Si te [&#8230;]</p> <description><![CDATA[<p>Días atrás estuve participando en el evento llamado Nerdearla en Buenos Aires.  El ambiente era genial si eres de esas personas que desde niño sintio curiosidad por ver como funcionan las cosas, donde desarmabas para volver armar lo juguetes. Habían muchas cosas interesantes tanto en las presentaciones, co-working y workshop que se hubieron. Si te [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/nerdearla-2016/">Nerdearla 2016, WebRTC Glue</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/nerdearla-2016/">Nerdearla 2016, WebRTC Glue</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description> ]]></description>
<content:encoded><![CDATA[<p>Días atrás estuve participando en el evento llamado <a href="https://nerdear.la/">Nerdearla</a> en Buenos Aires.  El ambiente era genial si eres de esas personas que desde niño sintio curiosidad por ver como funcionan las cosas, donde desarmabas para volver armar lo juguetes.</p> <content:encoded><![CDATA[<p>Días atrás estuve participando en el evento llamado <a href="https://nerdear.la/">Nerdearla</a> en Buenos Aires.  El ambiente era genial si eres de esas personas que desde niño sintio curiosidad por ver como funcionan las cosas, donde desarmabas para volver armar lo juguetes.</p>
<p>Habían muchas cosas interesantes tanto en las presentaciones, co-working y workshop que se hubieron. Si te lo perdiste te recomiendo que estés pendiente para el proximo año.</p> <p>Habían muchas cosas interesantes tanto en las presentaciones, co-working y workshop que se hubieron. Si te lo perdiste te recomiendo que estés pendiente para el proximo año.</p>
<p>&nbsp;</p> <p>&nbsp;</p>
<p>Te podias encontrar con una nuestra como esta<a href="https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS.jpg"><img class="aligncenter size-medium wp-image-1221" src="https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-300x169.jpg" alt="Kaypro II" width="300" height="169" srcset="https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-300x169.jpg 300w, https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-768x432.jpg 768w, https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-1024x576.jpg 1024w, https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS.jpg 1200w" sizes="(max-width: 300px) 100vw, 300px" /></a></p> <p>Te podias encontrar con una nuestra como esta<a href="https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS.jpg"><img class="aligncenter size-medium wp-image-1221" src="https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-300x169.jpg" alt="Kaypro II" width="300" height="169" srcset="https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-300x169.jpg 300w, https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-768x432.jpg 768w, https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-1024x576.jpg 1024w, https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS.jpg 1200w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>
@ -338,30 +338,30 @@ Swap:         3071          0       3071</pre>
&nbsp;</p> &nbsp;</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/nerdearla-2016/">Nerdearla 2016, WebRTC Glue</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/nerdearla-2016/">Nerdearla 2016, WebRTC Glue</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded> ]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/nerdearla-2016/feed/</wfw:commentRss> <wfw:commentRss>https://rodrigoramirez.com/nerdearla-2016/feed/</wfw:commentRss>
<slash:comments>0</slash:comments> <slash:comments>0</slash:comments>
</item> </item>
<item> <item>
<title>QPanel 0.9.0</title> <title>QPanel 0.9.0</title>
<link>https://rodrigoramirez.com/qpanel-0-9-0/</link> <link>https://rodrigoramirez.com/qpanel-0-9-0/</link>
<comments>https://rodrigoramirez.com/qpanel-0-9-0/#respond</comments> <comments>https://rodrigoramirez.com/qpanel-0-9-0/#respond</comments>
<pubDate>Mon, 09 May 2016 18:40:23 +0000</pubDate> <pubDate>Mon, 09 May 2016 18:40:23 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator> <dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category> <category><![CDATA[Software]]></category>
<category><![CDATA[asterisk]]></category> <category><![CDATA[asterisk]]></category>
<category><![CDATA[callcenter]]></category> <category><![CDATA[callcenter]]></category>
<category><![CDATA[colas]]></category> <category><![CDATA[colas]]></category>
<category><![CDATA[monitor]]></category> <category><![CDATA[monitor]]></category>
<category><![CDATA[monitoreo]]></category> <category><![CDATA[monitoreo]]></category>
<category><![CDATA[panel]]></category> <category><![CDATA[panel]]></category>
<category><![CDATA[qpanel]]></category> <category><![CDATA[qpanel]]></category>
<category><![CDATA[queues]]></category> <category><![CDATA[queues]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1206</guid> <guid isPermaLink="false">https://rodrigoramirez.com/?p=1206</guid>
<description><![CDATA[<p>El Panel monitor callcenter para colas de Asterisk ya cuenta con una nueva versión, la 0.9.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.9.0 Esta versión versión nos preocupamos de realizar mejoras y refactorizaciones en el codigo para dar un mejor rendimiento, como también de la compatibilidad con la versión 11 de [&#8230;]</p> <description><![CDATA[<p>El Panel monitor callcenter para colas de Asterisk ya cuenta con una nueva versión, la 0.9.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.9.0 Esta versión versión nos preocupamos de realizar mejoras y refactorizaciones en el codigo para dar un mejor rendimiento, como también de la compatibilidad con la versión 11 de [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-9-0/">QPanel 0.9.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-9-0/">QPanel 0.9.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description> ]]></description>
<content:encoded><![CDATA[<p><img class="alignleft" src="https://raw.githubusercontent.com/roramirez/qpanel/0.9.0/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="403" height="227" />El Panel monitor callcenter para colas de Asterisk ya cuenta con una nueva versión, la 0.9.0</p> <content:encoded><![CDATA[<p><img class="alignleft" src="https://raw.githubusercontent.com/roramirez/qpanel/0.9.0/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="403" height="227" />El Panel monitor callcenter para colas de Asterisk ya cuenta con una nueva versión, la 0.9.0</p>
<p>Para instalar esta nueva versión, debes visitar la siguiente URL</p> <p>Para instalar esta nueva versión, debes visitar la siguiente URL</p>
<ul> <ul>
<li><a href="https://github.com/roramirez/qpanel/tree/0.9.0">https://github.com/roramirez/qpanel/tree/0.9.0</a></li> <li><a href="https://github.com/roramirez/qpanel/tree/0.9.0">https://github.com/roramirez/qpanel/tree/0.9.0</a></li>
@ -376,35 +376,35 @@ Swap:         3071          0       3071</pre>
<p>El proyecto siempre está abierto a nuevas sugerencias las cuales puedes agregar mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a>.</p> <p>El proyecto siempre está abierto a nuevas sugerencias las cuales puedes agregar mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a>.</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-9-0/">QPanel 0.9.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-9-0/">QPanel 0.9.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded> ]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-9-0/feed/</wfw:commentRss> <wfw:commentRss>https://rodrigoramirez.com/qpanel-0-9-0/feed/</wfw:commentRss>
<slash:comments>0</slash:comments> <slash:comments>0</slash:comments>
</item> </item>
<item> <item>
<title>Mandar un email desde la shell</title> <title>Mandar un email desde la shell</title>
<link>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/</link> <link>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/</link>
<comments>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/#comments</comments> <comments>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/#comments</comments>
<pubDate>Wed, 13 Apr 2016 13:05:13 +0000</pubDate> <pubDate>Wed, 13 Apr 2016 13:05:13 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator> <dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category> <category><![CDATA[Linux]]></category>
<category><![CDATA[mini-tips]]></category> <category><![CDATA[mini-tips]]></category>
<category><![CDATA[bash]]></category> <category><![CDATA[bash]]></category>
<category><![CDATA[cli]]></category> <category><![CDATA[cli]]></category>
<category><![CDATA[Email]]></category> <category><![CDATA[Email]]></category>
<category><![CDATA[mail]]></category> <category><![CDATA[mail]]></category>
<category><![CDATA[sh]]></category> <category><![CDATA[sh]]></category>
<category><![CDATA[shell]]></category> <category><![CDATA[shell]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1172</guid> <guid isPermaLink="false">https://rodrigoramirez.com/?p=1172</guid>
<description><![CDATA[<p>Dejo esto por acá ya que es algo que siempre me olvido como es. El tema es enviar un email mediante el comando mail en un servidor con Linux. Si usas mail a secas te va pidiendo los datos para crear el correo, principalmente el body del correo. Para automatizar esto a través de un [&#8230;]</p> <description><![CDATA[<p>Dejo esto por acá ya que es algo que siempre me olvido como es. El tema es enviar un email mediante el comando mail en un servidor con Linux. Si usas mail a secas te va pidiendo los datos para crear el correo, principalmente el body del correo. Para automatizar esto a través de un [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mandar-un-email-desde-la-shell/">Mandar un email desde la shell</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mandar-un-email-desde-la-shell/">Mandar un email desde la shell</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description> ]]></description>
<content:encoded><![CDATA[<p>Dejo esto por acá ya que es algo que siempre me olvido como es. El tema es enviar un email mediante el comando <em>mail</em> en un servidor con Linux.</p> <content:encoded><![CDATA[<p>Dejo esto por acá ya que es algo que siempre me olvido como es. El tema es enviar un email mediante el comando <em>mail</em> en un servidor con Linux.</p>
<p>Si usas mail a secas te va pidiendo los datos para crear el correo, principalmente el body del correo. Para automatizar esto a través de un <em>echo</em> le pasas por pipe a <em>mail</em></p> <p>Si usas mail a secas te va pidiendo los datos para crear el correo, principalmente el body del correo. Para automatizar esto a través de un <em>echo</em> le pasas por pipe a <em>mail</em></p>
<pre>echo "Cuerpo del mensaje" | mail -s Asunto a@rodrigoramirez.com</pre> <pre>echo "Cuerpo del mensaje" | mail -s Asunto a@rodrigoramirez.com</pre>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mandar-un-email-desde-la-shell/">Mandar un email desde la shell</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p> <p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mandar-un-email-desde-la-shell/">Mandar un email desde la shell</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded> ]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/feed/</wfw:commentRss> <wfw:commentRss>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/feed/</wfw:commentRss>
<slash:comments>4</slash:comments> <slash:comments>4</slash:comments>
</item> </item>
</channel> </channel>
</rss> </rss>

View File

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

View File

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

View File

@ -0,0 +1,34 @@
/* Magic Mirror Test config sample module alert
*
* By rejas
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
module: "alert",
config: {
display_time: 1000000,
welcome_message: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,40 @@
/* Magic Mirror Test config custom calendar
*
* By Rejas
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
calendars: [
{
maximumEntries: 6,
maximumNumberOfDays: 3650,
url: "http://localhost:8080/tests/configs/data/calendar_test_recurring.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ describe("Development console tests", function () {
/* eslint-disable */ /* eslint-disable */
helpers.setupTimeout(this); helpers.setupTimeout(this);
var app = null; let app = null;
before(function () { before(function () {
// Set config sample for use in test // Set config sample for use in test

View File

@ -10,7 +10,7 @@ const afterEach = global.afterEach;
describe("Electron app environment", function () { describe("Electron app environment", function () {
helpers.setupTimeout(this); helpers.setupTimeout(this);
var app = null; let app = null;
before(function () { before(function () {
// Set config sample for use in test // Set config sample for use in test

View File

@ -8,12 +8,12 @@ const describe = global.describe;
describe("All font files from roboto.css should be downloadable", function () { describe("All font files from roboto.css should be downloadable", function () {
helpers.setupTimeout(this); helpers.setupTimeout(this);
var app; let app;
var fontFiles = []; const fontFiles = [];
// Statements below filters out all 'url' lines in the CSS file // Statements below filters out all 'url' lines in the CSS file
var fileContent = require("fs").readFileSync(__dirname + "/../../fonts/roboto.css", "utf8"); const fileContent = require("fs").readFileSync(__dirname + "/../../fonts/roboto.css", "utf8");
var regex = /\burl\(['"]([^'"]+)['"]\)/g; const regex = /\burl\(['"]([^'"]+)['"]\)/g;
var match = regex.exec(fileContent); let match = regex.exec(fileContent);
while (match !== null) { while (match !== null) {
// Push 1st match group onto fontFiles stack // Push 1st match group onto fontFiles stack
fontFiles.push(match[1]); fontFiles.push(match[1]);
@ -39,7 +39,7 @@ describe("All font files from roboto.css should be downloadable", function () {
}); });
forEach(fontFiles).it("should return 200 HTTP code for file '%s'", (fontFile, done) => { forEach(fontFiles).it("should return 200 HTTP code for file '%s'", (fontFile, done) => {
var fontUrl = "http://localhost:8080/fonts/" + fontFile; const fontUrl = "http://localhost:8080/fonts/" + fontFile;
fetch(fontUrl).then((res) => { fetch(fontUrl).then((res) => {
expect(res.status).to.equal(200); expect(res.status).to.equal(200);
done(); done();

View File

@ -1,13 +1,9 @@
/* /*
* Magic Mirror * Magic Mirror Global Setup Test Suite
*
* Global Setup Test Suite
* *
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com * By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed. * MIT Licensed.
*
*/ */
const Application = require("spectron").Application; const Application = require("spectron").Application;
const assert = require("assert"); const assert = require("assert");
const chai = require("chai"); const chai = require("chai");

View File

@ -10,7 +10,7 @@ const afterEach = global.afterEach;
describe("ipWhitelist directive configuration", function () { describe("ipWhitelist directive configuration", function () {
helpers.setupTimeout(this); helpers.setupTimeout(this);
var app = null; let app = null;
beforeEach(function () { beforeEach(function () {
return helpers return helpers
@ -31,6 +31,7 @@ describe("ipWhitelist directive configuration", function () {
// Set config sample for use in test // Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/noIpWhiteList.js"; process.env.MM_CONFIG_FILE = "tests/configs/noIpWhiteList.js";
}); });
it("should return 403", function (done) { it("should return 403", function (done) {
fetch("http://localhost:8080").then((res) => { fetch("http://localhost:8080").then((res) => {
expect(res.status).to.equal(403); expect(res.status).to.equal(403);
@ -44,6 +45,7 @@ describe("ipWhitelist directive configuration", function () {
// Set config sample for use in test // Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/empty_ipWhiteList.js"; process.env.MM_CONFIG_FILE = "tests/configs/empty_ipWhiteList.js";
}); });
it("should return 200", function (done) { it("should return 200", function (done) {
fetch("http://localhost:8080").then((res) => { fetch("http://localhost:8080").then((res) => {
expect(res.status).to.equal(200); expect(res.status).to.equal(200);

View File

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

View File

@ -76,6 +76,19 @@ describe("Calendar module", function () {
}); });
}); });
describe("Recurring event", function () {
before(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/recurring.js";
});
it("should show the recurring birthday event 6 times", async () => {
await app.client.waitUntilTextExists(".calendar", "Mar 25th", 10000);
const events = await app.client.$$(".calendar .event");
return expect(events.length).equals(6);
});
});
describe("Changed port", function () { describe("Changed port", function () {
before(function () { before(function () {
serverBasicAuth.listen(8010); serverBasicAuth.listen(8010);
@ -136,8 +149,8 @@ describe("Calendar module", function () {
serverBasicAuth.close(done()); serverBasicAuth.close(done());
}); });
it("should return No upcoming events", function () { it("should show Unauthorized error", function () {
return app.client.waitUntilTextExists(".calendar", "No upcoming events.", 10000); return app.client.waitUntilTextExists(".calendar", "Error in the calendar module. Authorization failed", 10000);
}); });
}); });
}); });

View File

@ -8,7 +8,7 @@ const afterEach = global.afterEach;
describe("Clock set to spanish language module", function () { describe("Clock set to spanish language module", function () {
helpers.setupTimeout(this); helpers.setupTimeout(this);
var app = null; let app = null;
beforeEach(function () { beforeEach(function () {
return helpers return helpers

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