Merge remote-tracking branch 'roramirez_github/develop' into showWeek-test-es-language

This commit is contained in:
Rodrigo Ramírez Norambuena 2017-07-25 10:52:51 -04:00
commit 00e3ef757c
57 changed files with 1092 additions and 608 deletions

View File

@ -1,72 +0,0 @@
# Various Node ignoramuses.
logs
*.log
npm-debug.log*
pids
*.pid
*.seed
lib-cov
coverage
.grunt
.lock-wscript
build/Release
node_modules
jspm_modules
.npm
.node_repl_history
# Various Windows ignoramuses.
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msm
*.msp
*.lnk
# Various OSX ignoramuses.
.DS_Store
.AppleDouble
.LSOverride
Icon
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Various Linux ignoramuses.
.fuse_hidden*
.directory
.Trash-*
# Various Magic Mirror ignoramuses and anti-ignoramuses.
# Don't ignore the node_helper core module.
!/modules/node_helper
!/modules/node_helper/**
# Ignore all modules except the default modules.
/modules/**
!/modules/default/**
# Ignore changes to the custom css files.
/css/custom.css
# Ignore unnecessary files for docker
CHANGELOG.md
LICENSE.md
README.md
Gruntfile.js
.*

13
.gitignore vendored
View File

@ -16,6 +16,9 @@ jspm_modules
.npm .npm
.node_repl_history .node_repl_history
# Visual Studio Code ignoramuses.
.vscode/
# Various Windows ignoramuses. # Various Windows ignoramuses.
Thumbs.db Thumbs.db
ehthumbs.db ehthumbs.db
@ -65,3 +68,13 @@ Temporary Items
# Ignore changes to the custom css files. # Ignore changes to the custom css files.
/css/custom.css /css/custom.css
# Vim
## swap
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
## diff patch
*.orig
*.rej
*.bak

View File

@ -10,7 +10,8 @@ before_script:
- sleep 5 - sleep 5
script: script:
- grunt - grunt
- npm test - npm run test:unit
- npm run test:e2e
cache: cache:
directories: directories:
- node_modules - node_modules

View File

@ -2,23 +2,62 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
## [2.1.2] - Unreleased ## [2.1.3] - Unreleased
### Changed ### Changed
### Added
- Add `clientonly` script to start only the electron client for a remote server.
- Add symbol and color properties of event when `CALENDAR_EVENTS` notification is broadcasted from `default/calendar` module.
- Add `.vscode/` folder to `.gitignore` to keep custom Visual Studio Code config out of git
- Add test e2e showWeek feature in spanish language.
### Updated
- Changed 'default.js' - listen on all attached interfaces by default
### Fixed
- Fixed issue with incorrect allignment of analog clock when displayed in the center column of the MM
- Fixed ipWhitelist behaviour to make empty whitelist ([]) allow any and all hosts access to the MM
## [2.1.2] - 2017-07-01
### Changed
- Revert Docker related changes in favor of [docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror). All Docker images are outsourced. ([#856](https://github.com/MichMich/MagicMirror/pull/856))
- Change Docker base image (Debian + Node) to an arm based distro (AlpineARM + Node) ([#846](https://github.com/MichMich/MagicMirror/pull/846))
- Fix the dockerfile to have it running from the first time. - Fix the dockerfile to have it running from the first time.
### Added ### Added
- Add in option to wrap long calendar events to multiple lines using `wrapEvents` configuration option. - Add in option to wrap long calendar events to multiple lines using `wrapEvents` configuration option.
- Add test e2e `show title newsfeed` for newsfeed module. - Add test e2e `show title newsfeed` for newsfeed module.
- Add task to check configuration file. - Add task to check configuration file.
- Add test e2e showWeek feature in spanish language. - Add test check URLs of vendors.
- Add test of match current week number on clock module with showWeek configuration.
- Add test default modules present modules/default/defaultmodules.js.
- Add unit test calendar_modules function capFirst.
- Add test for check if exists the directories present in defaults modules.
- Add support for showing wind direction as an arrow instead of abbreviation in currentWeather module.
- Add support for writing translation fucntions to support flexible word order
- Add test for check if exits the directories present in defaults modules.
- Add calendar option to set a separate date format for full day events.
- Add ability for `currentweather` module to display indoor temperature via INDOOR_TEMPERATURE notification
- Add ability to change the path of the `custom.css`.
- Add translation Dutch to Alert module.
- Added Romanian translation.
### Updated ### Updated
- Added missing keys to Polish translation. - Added missing keys to Polish translation.
- Added missing key to German translation. - Added missing key to German translation.
- Added better translation with flexible word order to Finnish translation
### Fixed ### Fixed
- Fix instruction in README for using automatically installer script. - Fix instruction in README for using automatically installer script.
- Bug of duplicated compliments as described in [here](https://forum.magicmirror.builders/topic/2381/compliments-module-stops-cycling-compliments).
- Fix double message about port when server is starting
- Corrected Swedish translations for TODAY/TOMORROW/DAYAFTERTOMORROW.
- Removed unused import from js/electron.js
- Made calendar.js respect config.timeFormat irrespecive of locale setting
- Fixed alignment of analog clock when a large calendar is displayed in the same side bar
## [2.1.1] - 2017-04-01 ## [2.1.1] - 2017-04-01

View File

@ -1,22 +0,0 @@
FROM node:latest
ENV NODE_ENV production
ENV MM_PORT 8080
WORKDIR /opt/magic_mirror
COPY . .
COPY /modules unmount_modules
COPY /config unmount_config
RUN apt-get update \
&& apt-get -qy install tofrodos dos2unix \
&& chmod -R 777 vendor \
&& npm install \
&& cd vendor \
&& npm install \
&& cd .. \
&& dos2unix docker-entrypoint.sh \
&& chmod +x docker-entrypoint.sh
EXPOSE $MM_PORT
ENTRYPOINT ["/opt/magic_mirror/docker-entrypoint.sh"]

View File

@ -48,6 +48,11 @@ bash -c "$(curl -sL https://raw.githubusercontent.com/MichMich/MagicMirror/maste
### Server Only ### Server Only
In some cases, you want to start the application without an actual app window. In this case, you can start MagicMirror² in server only mode by manually running `node serveronly` or using Docker. This will start the server, after which you can open the application in your browser of choice. Detailed description below. In some cases, you want to start the application without an actual app window. In this case, you can start MagicMirror² in server only mode by manually running `node serveronly` or using Docker. This will start the server, after which you can open the application in your browser of choice. Detailed description below.
### Client Only
When you have a server running remotely and want to connect a standalone client to this instance, you can manually run `node clientonly --address 192.168.1.5 --port 8080`. (Specify the ip address and port number of the server)
**Important:** Make sure that you whitelist the interface/ip in the server config where you want the client to connect to, otherwise it will not be allowed to connect to the server
#### Docker #### Docker
MagicMirror² in server only mode can be deployed using [Docker](https://docker.com). After a successful [Docker installation](https://docs.docker.com/engine/installation/) you just need to execute the following command in the shell: MagicMirror² in server only mode can be deployed using [Docker](https://docker.com). After a successful [Docker installation](https://docs.docker.com/engine/installation/) you just need to execute the following command in the shell:
@ -59,7 +64,7 @@ docker run -d \
--volume ~/magic_mirror/config:/opt/magic_mirror/config \ --volume ~/magic_mirror/config:/opt/magic_mirror/config \
--volume ~/magic_mirror/modules:/opt/magic_mirror/modules \ --volume ~/magic_mirror/modules:/opt/magic_mirror/modules \
--name magic_mirror \ --name magic_mirror \
MichMich/MagicMirror bastilimbach/docker-magicmirror
``` ```
| **Volumes** | **Description** | | **Volumes** | **Description** |
@ -75,6 +80,8 @@ var config = {
}; };
``` ```
If you want to run the server on a raspberry pi, use the `raspberry` tag. (bastilimbach/docker-magicmirror:raspberry)
#### Manual #### Manual
1. Download and install the latest Node.js version. 1. Download and install the latest Node.js version.
@ -122,7 +129,7 @@ The following properties can be configured:
| `units` | The units that will be used in the default weather modules. Possible values are `metric` or `imperial`. The default is `metric`. | | `units` | The units that will be used in the default weather modules. Possible values are `metric` or `imperial`. The default is `metric`. |
| `modules` | An array of active modules. **The array must contain objects. See the next table below for more information.** | | `modules` | An array of active modules. **The array must contain objects. See the next table below for more information.** |
| `electronOptions` | An optional array of Electron (browser) options. This allows configuration of e.g. the browser screen size and position (example: `electronOptions: { fullscreen: false, width: 800, height: 600 }`). Kiosk mode can be enabled by setting `kiosk = true`, `autoHideMenuBar = false` and `fullscreen = false`. More options can be found [here](https://github.com/electron/electron/blob/master/docs/api/browser-window.md). | | `electronOptions` | An optional array of Electron (browser) options. This allows configuration of e.g. the browser screen size and position (example: `electronOptions: { fullscreen: false, width: 800, height: 600 }`). Kiosk mode can be enabled by setting `kiosk = true`, `autoHideMenuBar = false` and `fullscreen = false`. More options can be found [here](https://github.com/electron/electron/blob/master/docs/api/browser-window.md). |
| `customCss` | The path of the `custom.css` stylesheet. The default is `css/custom.css`. |
Module configuration: Module configuration:

97
clientonly/index.js Normal file
View File

@ -0,0 +1,97 @@
/* jshint esversion: 6 */
"use strict";
// Use seperate scope to prevent global scope pollution
(function () {
var config = {};
// Helper function to get server address/hostname from either the commandline or env
function getServerAddress() {
// Helper function to get command line parameters
// Assumes that a cmdline parameter is defined with `--key [value]`
function getCommandLineParameter(key, defaultValue = undefined) {
var index = process.argv.indexOf(`--${key}`);
var value = index > -1 ? process.argv[index + 1] : undefined;
return value !== undefined ? String(value) : defaultValue;
}
// Prefer command line arguments over environment variables
["address", "port"].forEach((key) => {
config[key] = getCommandLineParameter(key, process.env[key.toUpperCase()]);
})
}
function getServerConfig(url) {
// Return new pending promise
return new Promise((resolve, reject) => {
// Select http or https module, depending on reqested url
const lib = url.startsWith("https") ? require("https") : require("http");
const request = lib.get(url, (response) => {
var configData = "";
// Gather incomming data
response.on("data", function(chunk) {
configData += chunk;
});
// Resolve promise at the end of the HTTP/HTTPS stream
response.on("end", function() {
resolve(JSON.parse(configData));
});
});
request.on("error", function(error) {
reject(new Error(`Unable to read config from server (${url} (${error.message}`));
});
})
};
function fail(message, code = 1) {
if (message !== undefined && typeof message === "string") {
console.log(message);
} else {
console.log("Usage: 'node clientonly --address 192.168.1.10 --port 8080'");
}
process.exit(code);
}
getServerAddress();
(config.address && config.port) || fail();
// Only start the client if a non-local server was provided
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) === -1) {
getServerConfig(`http://${config.address}:${config.port}/config/`)
.then(function (config) {
// Pass along the server config via an environment variable
var env = Object.create(process.env);
var options = { env: env };
config.address = config.address;
config.port = config.port;
env.config = JSON.stringify(config);
// Spawn electron application
const electron = require("electron");
const child = require("child_process").spawn(electron, ["js/electron.js"], options);
// Pipe all child process output to current stdout
child.stdout.on("data", function (buf) {
process.stdout.write(`Client: ${buf}`);
});
// Pipe all child process errors to current stderr
child.stderr.on("data", function (buf) {
process.stderr.write(`Client: ${buf}`);
});
child.on("error", function (err) {
process.stdout.write(`Client: ${err}`);
});
})
.catch(function (reason) {
fail(`Unable to connect to server: (${reason})`);
});
} else {
fail();
}
}());

View File

@ -9,6 +9,7 @@
*/ */
var config = { var config = {
address: "localhost",
port: 8080, port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], // Set [] to allow all IP addresses ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], // Set [] to allow all IP addresses
// or add a specific IPv4 of 192.168.1.5 : // or add a specific IPv4 of 192.168.1.5 :

View File

@ -1,11 +0,0 @@
#!/bin/bash
if [ ! -f /opt/magic_mirror/modules ]; then
cp -R /opt/magic_mirror/unmount_modules/. /opt/magic_mirror/modules
fi
if [ ! -f /opt/magic_mirror/config ]; then
cp -Rn /opt/magic_mirror/unmount_config/. /opt/magic_mirror/config
fi
node serveronly

View File

@ -150,8 +150,7 @@ fi
# Use pm2 control like a service MagicMirror # Use pm2 control like a service MagicMirror
read -p "Do you want use pm2 for auto starting of your MagicMirror (y/n)?" choice read -p "Do you want use pm2 for auto starting of your MagicMirror (y/n)?" choice
if [[ $choice =~ ^[Yy]$ ]] if [[ $choice =~ ^[Yy]$ ]]; then
then
sudo npm install -g pm2 sudo npm install -g pm2
sudo su -c "env PATH=$PATH:/usr/bin pm2 startup linux -u pi --hp /home/pi" sudo su -c "env PATH=$PATH:/usr/bin pm2 startup linux -u pi --hp /home/pi"
pm2 start ~/MagicMirror/installers/pm2_MagicMirror.json pm2 start ~/MagicMirror/installers/pm2_MagicMirror.json

View File

@ -8,10 +8,12 @@
*/ */
var port = 8080; var port = 8080;
var address = ""; // Default to listening on all interfaces
if (typeof(mmPort) !== "undefined") { if (typeof(mmPort) !== "undefined") {
port = mmPort; port = mmPort;
} }
var defaults = { var defaults = {
address: address,
port: port, port: port,
kioskmode: false, kioskmode: false,
electronOptions: {}, electronOptions: {},
@ -21,6 +23,7 @@ var defaults = {
timeFormat: 24, timeFormat: 24,
units: "metric", units: "metric",
zoom: 1, zoom: 1,
customCss: "css/custom.css",
modules: [ modules: [
{ {

View File

@ -2,12 +2,11 @@
"use strict"; "use strict";
const Server = require(__dirname + "/server.js");
const electron = require("electron"); const electron = require("electron");
const core = require(__dirname + "/app.js"); const core = require(__dirname + "/app.js");
// Config // Config
var config = {}; var config = process.env.config ? JSON.parse(process.env.config) : {};
// Module to control application life. // Module to control application life.
const app = electron.app; const app = electron.app;
// Module to create native browser window. // Module to create native browser window.
@ -18,7 +17,6 @@ const BrowserWindow = electron.BrowserWindow;
let mainWindow; let mainWindow;
function createWindow() { function createWindow() {
var electronOptionsDefaults = { var electronOptionsDefaults = {
width: 800, width: 800,
height: 600, height: 600,
@ -30,7 +28,7 @@ function createWindow() {
zoomFactor: config.zoom zoomFactor: config.zoom
}, },
backgroundColor: "#000000" backgroundColor: "#000000"
} };
// DEPRECATED: "kioskmode" backwards compatibility, to be removed // DEPRECATED: "kioskmode" backwards compatibility, to be removed
// settings these options directly instead provides cleaner interface // settings these options directly instead provides cleaner interface
@ -47,11 +45,12 @@ function createWindow() {
mainWindow = new BrowserWindow(electronOptions); mainWindow = new BrowserWindow(electronOptions);
// and load the index.html of the app. // and load the index.html of the app.
//mainWindow.loadURL('file://' + __dirname + '../../index.html'); // If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
mainWindow.loadURL("http://localhost:" + config.port); var address = (config.address === void 0) | (config.address === "") ? (config.address = "localhost") : config.address;
mainWindow.loadURL(`http://${address}:${config.port}`);
// Open the DevTools if run with "npm start dev" // Open the DevTools if run with "npm start dev"
if(process.argv[2] == "dev") { if (process.argv.includes("dev")) {
mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools();
} }
@ -97,8 +96,10 @@ app.on("activate", function() {
} }
}); });
// Start the core application. // Start the core application if server is run on localhost
// This starts all node helpers and starts the webserver. // This starts all node helpers and starts the webserver.
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) > -1) {
core.start(function(c) { core.start(function(c) {
config = c; config = c;
}); });
}

View File

@ -32,10 +32,10 @@ var Loader = (function() {
}); });
} else { } else {
// All modules loaded. Load custom.css // All modules loaded. Load custom.css
// This is done after all the moduels so we can // This is done after all the modules so we can
// overwrite all the defined styls. // overwrite all the defined styles.
loadFile("css/custom.css", function() { loadFile(config.customCss, function() {
// custom.css loaded. Start all modules. // custom.css loaded. Start all modules.
startModules(); startModules();
}); });

View File

@ -165,8 +165,6 @@ var MM = (function() {
if( headerWrapper.length > 0 && newHeader) { if( headerWrapper.length > 0 && newHeader) {
headerWrapper[0].innerHTML = newHeader; headerWrapper[0].innerHTML = newHeader;
} }
}; };
/* hideModule(module, speed, callback) /* hideModule(module, speed, callback)
@ -219,7 +217,7 @@ var MM = (function() {
// 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) var index = module.lockStrings.indexOf(options.lockString);
if ( index !== -1) { if ( index !== -1) {
module.lockStrings.splice(index, 1); module.lockStrings.splice(index, 1);
} }

View File

@ -272,14 +272,18 @@ var Module = Class.extend({
} }
}, },
/* translate(key, defaultValue) /* translate(key, defaultValueOrVariables, defaultValue)
* Request the translation for a given key. * Request the translation for a given key with optional variables and default value.
* *
* argument key string - The key of the string to translage * argument key string - The key of the string to translate
* argument defaultValue string - The default value if no translation was found. (Optional) * argument defaultValueOrVariables string/object - The default value or variables for translating. (Optional)
* argument defaultValue string - The default value with variables. (Optional)
*/ */
translate: function (key, defaultValue) { translate: function (key, defaultValueOrVariables, defaultValue) {
return Translator.translate(this, key) || defaultValue || ""; if(typeof defaultValueOrVariables === "object") {
return Translator.translate(this, key, defaultValueOrVariables) || defaultValue || "";
}
return Translator.translate(this, key) || defaultValueOrVariables || "";
}, },
/* updateDom(speed) /* updateDom(speed)

View File

@ -15,14 +15,13 @@ var fs = require("fs");
var helmet = require("helmet"); var helmet = require("helmet");
var Server = function(config, callback) { var Server = function(config, callback) {
console.log("Starting server on port " + config.port + " ... ");
var port = config.port; var port = config.port;
if (process.env.MM_PORT) { if (process.env.MM_PORT) {
port = process.env.MM_PORT; port = process.env.MM_PORT;
} }
console.log("Starting server op port " + port + " ... "); console.log("Starting server on port " + port + " ... ");
server.listen(port, config.address ? config.address : null); server.listen(port, config.address ? config.address : null);
@ -31,7 +30,7 @@ var Server = function(config, callback) {
} }
app.use(function(req, res, next) { app.use(function(req, res, next) {
var result = ipfilter(config.ipWhitelist, {mode: "allow", log: false})(req, res, function(err) { var result = ipfilter(config.ipWhitelist, {mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false})(req, res, function(err) {
if (err === undefined) { if (err === undefined) {
return next(); return next();
} }
@ -44,7 +43,7 @@ var Server = function(config, callback) {
app.use("/js", express.static(__dirname)); app.use("/js", express.static(__dirname));
var directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"]; var directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
var directory; var directory;
for (i in directories) { for (var i in directories) {
directory = directories[i]; directory = directories[i];
app.use(directory, express.static(path.resolve(global.root_path + directory))); app.use(directory, express.static(path.resolve(global.root_path + directory)));
} }
@ -53,6 +52,10 @@ var Server = function(config, callback) {
res.send(global.version); res.send(global.version);
}); });
app.get("/config", function(req,res) {
res.send(config);
});
app.get("/", function(req, res) { app.get("/", function(req, res) {
var html = fs.readFileSync(path.resolve(global.root_path + "/index.html"), {encoding: "utf8"}); var html = fs.readFileSync(path.resolve(global.root_path + "/index.html"), {encoding: "utf8"});
html = html.replace("#VERSION#", global.version); html = html.replace("#VERSION#", global.version);

View File

@ -22,7 +22,6 @@ var MMSocket = function(moduleName) {
// register catch all. // register catch all.
self.socket.on("*", function(notification, payload) { self.socket.on("*", function(notification, payload) {
if (notification !== "*") { if (notification !== "*") {
//console.log('Received notification: ' + notification +', payload: ' + payload);
notificationCallback(notification, payload); notificationCallback(notification, payload);
} }
}); });

View File

@ -111,32 +111,47 @@ var Translator = (function() {
translations: {}, translations: {},
translationsFallback: {}, translationsFallback: {},
/* translate(module, key) /* translate(module, key, variables)
* Load a translation for a given key for a given module. * Load a translation for a given key for a given module.
* *
* argument module Module - The module to load the translation for. * argument module Module - The module to load the translation for.
* argument key string - The key of the text to translate. * argument key string - The key of the text to translate.
* argument variables - The variables to use within the translation template (optional)
*/ */
translate: function(module, key) { translate: function(module, key, variables) {
variables = variables || {}; //Empty object by default
// Combines template and variables like:
// template: "Please wait for {timeToWait} before continuing with {work}."
// variables: {timeToWait: "2 hours", work: "painting"}
// to: "Please wait for 2 hours before continuing with painting."
function createStringFromTemplate(template, variables) {
if(variables.fallback && !template.match(new RegExp("\{.+\}"))) {
template = variables.fallback;
}
return template.replace(new RegExp("\{([^\}]+)\}", "g"), function(_unused, varName){
return variables[varName] || "{"+varName+"}";
});
}
if(this.translations[module.name] && key in this.translations[module.name]) { if(this.translations[module.name] && key in this.translations[module.name]) {
// Log.log("Got translation for " + key + " from module translation: "); // Log.log("Got translation for " + key + " from module translation: ");
return this.translations[module.name][key]; return createStringFromTemplate(this.translations[module.name][key], variables);
} }
if (key in this.coreTranslations) { if (key in this.coreTranslations) {
// Log.log("Got translation for " + key + " from core translation."); // Log.log("Got translation for " + key + " from core translation.");
return this.coreTranslations[key]; return createStringFromTemplate(this.coreTranslations[key], variables);
} }
if (this.translationsFallback[module.name] && key in this.translationsFallback[module.name]) { if (this.translationsFallback[module.name] && key in this.translationsFallback[module.name]) {
// Log.log("Got translation for " + key + " from module translation fallback."); // Log.log("Got translation for " + key + " from module translation fallback.");
return this.translationsFallback[module.name][key]; return createStringFromTemplate(this.translationsFallback[module.name][key], variables);
} }
if (key in this.coreTranslationsFallback) { if (key in this.coreTranslationsFallback) {
// Log.log("Got translation for " + key + " from core translation fallback."); // Log.log("Got translation for " + key + " from core translation fallback.");
return this.coreTranslationsFallback[key]; return createStringFromTemplate(this.coreTranslationsFallback[key], variables);
} }
return key; return key;

View File

@ -78,7 +78,7 @@ The data object contains additional metadata about the module instance:
#### `defaults: {}` #### `defaults: {}`
Any properties defined in the defaults object, will be merged with the module config as defined in the user's config.js file. This is the best place to set your modules's configuration defaults. Any of the module configuration properties can be accessed using `this.config.propertyName`, but more about that later. Any properties defined in the defaults object, will be merged with the module config as defined in the user's config.js file. This is the best place to set your modules's configuration defaults. Any of the module configuration properties can be accessed using `this.config.propertyName`, but more about that later.
####'requiresVersion:' #### `requiresVersion:`
*Introduced in version: 2.1.0.* *Introduced in version: 2.1.0.*
@ -257,7 +257,7 @@ socketNotificationReceived: function(notification, payload) {
When a module is hidden (using the `module.hide()` method), the `suspend()` method will be called. By subclassing this method you can perform tasks like halting the update timers. When a module is hidden (using the `module.hide()` method), the `suspend()` method will be called. By subclassing this method you can perform tasks like halting the update timers.
#### `resume()` #### `resume()`
When a module will be shown after it was previously hidden (using the `module.show()` method), the `resume()` method will be called. By subclassing this method you can perform tasks restarting the update timers. When a module is requested to be shown (using the `module.show()` method), the `resume()` method will be called. By subclassing this method you can perform tasks restarting the update timers.
### Module instance methods ### Module instance methods
@ -429,6 +429,51 @@ this.translate("INFO") //Will return a translated string for the identifier INFO
**Note:** although comments are officially not supported in JSON files, MagicMirror allows it by stripping the comments before parsing the JSON file. Comments in translation files could help other translators. **Note:** although comments are officially not supported in JSON files, MagicMirror allows it by stripping the comments before parsing the JSON file. Comments in translation files could help other translators.
##### `this.translate(identifier, variables)`
***identifier* String** - Identifier of the string that should be translated.
***variables* Object** - Object of variables to be used in translation.
This improved and backwards compatible way to handle translations behaves like the normal translation function and follows the rules described above. It's recommended to use this new format for translating everywhere. It allows translator to change the word order in the sentence to be translated.
**Example:**
````javascript
var timeUntilEnd = moment(event.endDate, "x").fromNow(true);
this.translate("RUNNING", { "timeUntilEnd": timeUntilEnd) }); // Will return a translated string for the identifier RUNNING, replacing `{timeUntilEnd}` with the contents of the variable `timeUntilEnd` in the order that translator intended.
````
**Example English .json file:**
````javascript
{
"RUNNING": "Ends in {timeUntilEnd}",
}
````
**Example Finnish .json file:**
````javascript
{
"RUNNING": "Päättyy {timeUntilEnd} päästä",
}
````
**Note:** The *variables* Object has an special case called `fallback`. It's used to support old translations in translation files that do not have the variables in them. If you are upgrading an old module that had translations that did not support the word order, it is recommended to have the fallback layout.
**Example:**
````javascript
var timeUntilEnd = moment(event.endDate, "x").fromNow(true);
this.translate("RUNNING", {
"fallback": this.translate("RUNNING") + " {timeUntilEnd}"
"timeUntilEnd": timeUntilEnd
)}); // Will return a translated string for the identifier RUNNING, replacing `{timeUntilEnd}` with the contents of the variable `timeUntilEnd` in the order that translator intended. (has a fallback)
````
**Example swedish .json file that does not have the variable in it:**
````javascript
{
"RUNNING": "Slutar",
}
````
In this case the `translate`-function will not find any variables in the translation, will look for `fallback` variable and use that if possible to create the translation.
## The Node Helper: node_helper.js ## The Node Helper: node_helper.js
@ -482,7 +527,7 @@ this.expressApp.use("/" + this.name, express.static(this.path + "/public"));
This is a link to the IO instance. It will allow you to do some Socket.IO magic. In most cases you won't need this, since the Node Helper has a few convenience methods to make this simple. This is a link to the IO instance. It will allow you to do some Socket.IO magic. In most cases you won't need this, since the Node Helper has a few convenience methods to make this simple.
####'requiresVersion:' #### `requiresVersion:`
*Introduced in version: 2.1.0.* *Introduced in version: 2.1.0.*
A string that defines the minimum version of the MagicMirror framework. If it is set, the system compares the required version with the users version. If the version of the user is out of date, it won't run the module. A string that defines the minimum version of the MagicMirror framework. If it is set, the system compares the required version with the users version. If the version of the user is out of date, it won't run the module.

View File

@ -30,7 +30,8 @@ Module.register("alert",{
getTranslations: function() { getTranslations: function() {
return { return {
en: "translations/en.json", en: "translations/en.json",
de: "translations/de.json" de: "translations/de.json",
nl: "translations/nl.json",
}; };
}, },
show_notification: function(message) { show_notification: function(message) {

View File

@ -0,0 +1,4 @@
{
"sysTitle": "MagicMirror Notificatie",
"welcome": "Welkom, Succesvol gestart!"
}

View File

@ -40,6 +40,7 @@ The following properties can be configured:
| `titleReplace` | An object of textual replacements applied to the tile of the event. This allow to remove or replace certains words in the title. <br><br> **Example:** `{'Birthday of ' : '', 'foo':'bar'}` <br> **Default value:** `{ "De verjaardag van ": "", "'s birthday": "" }` | `titleReplace` | An object of textual replacements applied to the tile of the event. This allow to remove or replace certains words in the title. <br><br> **Example:** `{'Birthday of ' : '', 'foo':'bar'}` <br> **Default value:** `{ "De verjaardag van ": "", "'s birthday": "" }`
| `displayRepeatingCountTitle` | Show count title for yearly repeating events (e.g. "X. Birthday", "X. Anniversary") <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false` | `displayRepeatingCountTitle` | Show count title for yearly repeating events (e.g. "X. Birthday", "X. Anniversary") <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
| `dateFormat` | Format to use for the date of events (when using absolute dates) <br><br> **Possible values:** See [Moment.js formats](http://momentjs.com/docs/#/parsing/string-format/) <br> **Default value:** `MMM Do` (e.g. Jan 18th) | `dateFormat` | Format to use for the date of events (when using absolute dates) <br><br> **Possible values:** See [Moment.js formats](http://momentjs.com/docs/#/parsing/string-format/) <br> **Default value:** `MMM Do` (e.g. Jan 18th)
| `fullDayEventDateFormat` | Format to use for the date of full day events (when using absolute dates) <br><br> **Possible values:** See [Moment.js formats](http://momentjs.com/docs/#/parsing/string-format/) <br> **Default value:** `MMM Do` (e.g. Jan 18th)
| `timeFormat` | Display event times as absolute dates, or relative time <br><br> **Possible values:** `absolute` or `relative` <br> **Default value:** `relative` | `timeFormat` | Display event times as absolute dates, or relative time <br><br> **Possible values:** `absolute` or `relative` <br> **Default value:** `relative`
| `getRelative` | How much time (in hours) should be left until calendar events start getting relative? <br><br> **Possible values:** `0` (events stay absolute) - `48` (48 hours before the event starts) <br> **Default value:** `6` | `getRelative` | How much time (in hours) should be left until calendar events start getting relative? <br><br> **Possible values:** `0` (events stay absolute) - `48` (48 hours before the event starts) <br> **Default value:** `6`
| `urgency` | When using a timeFormat of `absolute`, the `urgency` setting allows you to display events within a specific time frame as `relative`. This allows events within a certain time frame to be displayed as relative (in xx days) while others are displayed as absolute dates <br><br> **Possible values:** a positive integer representing the number of days for which you want a relative date, for example `7` (for 7 days) <br><br> **Default value:** `7` | `urgency` | When using a timeFormat of `absolute`, the `urgency` setting allows you to display events within a specific time frame as `relative`. This allows events within a certain time frame to be displayed as relative (in xx days) while others are displayed as absolute dates <br><br> **Possible values:** a positive integer representing the number of days for which you want a relative date, for example `7` (for 7 days) <br><br> **Default value:** `7`

View File

@ -25,6 +25,7 @@ Module.register("calendar", {
urgency: 7, urgency: 7,
timeFormat: "relative", timeFormat: "relative",
dateFormat: "MMM Do", dateFormat: "MMM Do",
fullDayEventDateFormat: "MMM Do",
getRelative: 6, getRelative: 6,
fadePoint: 0.25, // Start on 1/4th of the list. fadePoint: 0.25, // Start on 1/4th of the list.
hidePrivate: false, hidePrivate: false,
@ -68,6 +69,29 @@ Module.register("calendar", {
// Set locale. // Set locale.
moment.locale(config.language); moment.locale(config.language);
switch (config.timeFormat) {
case 12: {
moment.updateLocale(config.language, {
longDateFormat: {
LT: "h:mm A"
}
});
break;
}
case 24: {
moment.updateLocale(config.language, {
longDateFormat: {
LT: "HH:mm"
}
});
break;
}
// If config.timeFormat was not given (or has invalid format) default to locale default
default: {
break;
}
}
for (var c in this.config.calendars) { for (var c in this.config.calendars) {
var calendar = this.config.calendars[c]; var calendar = this.config.calendars[c];
calendar.url = calendar.url.replace("webcal://", "http://"); calendar.url = calendar.url.replace("webcal://", "http://");
@ -173,7 +197,6 @@ Module.register("calendar", {
var titleWrapper = document.createElement("td"), var titleWrapper = document.createElement("td"),
repeatingCountTitle = ""; repeatingCountTitle = "";
if (this.config.displayRepeatingCountTitle) { if (this.config.displayRepeatingCountTitle) {
repeatingCountTitle = this.countTitleForUrl(event.url); repeatingCountTitle = this.countTitleForUrl(event.url);
@ -228,7 +251,7 @@ Module.register("calendar", {
// This event falls within the config.urgency period that the user has set // This event falls within the config.urgency period that the user has set
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow()); timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
} else { } else {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat)); timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
} }
} else { } else {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow()); timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
@ -265,7 +288,12 @@ Module.register("calendar", {
} }
} }
} else { } else {
timeWrapper.innerHTML = this.capFirst(this.translate("RUNNING")) + " " + moment(event.endDate, "x").fromNow(true); timeWrapper.innerHTML = this.capFirst(
this.translate("RUNNING", {
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
})
);
} }
} }
//timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll'); //timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll');
@ -490,10 +518,12 @@ Module.register("calendar", {
*/ */
broadcastEvents: function () { broadcastEvents: function () {
var eventList = []; var eventList = [];
for (url in this.calendarData) { for (var url in this.calendarData) {
var calendar = this.calendarData[url]; var calendar = this.calendarData[url];
for (e in calendar) { for (var e in calendar) {
var event = cloneObject(calendar[e]); var event = cloneObject(calendar[e]);
event.symbol = this.symbolsForUrl(url);
event.color = this.colorForUrl(url);
delete event.url; delete event.url;
eventList.push(event); eventList.push(event);
} }

View File

@ -127,7 +127,6 @@ Module.register("clock",{
hour = ((now.hours() % 12) / 12) * 360 + 90 + minute / 12; hour = ((now.hours() % 12) / 12) * 360 + 90 + minute / 12;
// Create wrappers // Create wrappers
var wrapper = document.createElement("div");
var clockCircle = document.createElement("div"); var clockCircle = document.createElement("div");
clockCircle.className = "clockCircle"; clockCircle.className = "clockCircle";
clockCircle.style.width = this.config.analogSize; clockCircle.style.width = this.config.analogSize;
@ -181,7 +180,6 @@ Module.register("clock",{
wrapper.appendChild(weekWrapper); wrapper.appendChild(weekWrapper);
} else if (this.config.displayType === "analog") { } else if (this.config.displayType === "analog") {
// Display only an analog clock // Display only an analog clock
dateWrapper.style.textAlign = "center";
if (this.config.showWeek) { if (this.config.showWeek) {
weekWrapper.style.paddingBottom = "15px"; weekWrapper.style.paddingBottom = "15px";

View File

@ -13,7 +13,7 @@ modules: [
// Best results in one of the middle regions like: lower_third // Best results in one of the middle regions like: lower_third
config: { config: {
// The config property is optional. // The config property is optional.
// If no config is set, an example calendar is shown. // If no config is set, the default compliments are shown.
// See 'Configuration options' for more information. // See 'Configuration options' for more information.
} }
} }
@ -31,6 +31,7 @@ The following properties can be configured:
| `fadeSpeed` | Speed of the update animation. (Milliseconds) <br><br> **Possible values:**`0` - `5000` <br> **Default value:** `4000` (4 seconds) | `fadeSpeed` | Speed of the update animation. (Milliseconds) <br><br> **Possible values:**`0` - `5000` <br> **Default value:** `4000` (4 seconds)
| `compliments` | The list of compliments. <br><br> **Possible values:** An object with four arrays: `morning`, `afternoon`, `evening` and `anytime`. See _compliment configuration_ below. <br> **Default value:** See _compliment configuration_ below. | `compliments` | The list of compliments. <br><br> **Possible values:** An object with four arrays: `morning`, `afternoon`, `evening` and `anytime`. See _compliment configuration_ below. <br> **Default value:** See _compliment configuration_ below.
| `remoteFile` | External file from which to load the compliments <br><br> **Possible values:** Path to a JSON file containing compliments, configured as per the value of the _compliments configuration_ (see below). An object with four arrays: `morning`, `afternoon`, `evening` and `anytime`. - `compliments.json` <br> **Default value:** `null` (Do not load from file) | `remoteFile` | External file from which to load the compliments <br><br> **Possible values:** Path to a JSON file containing compliments, configured as per the value of the _compliments configuration_ (see below). An object with four arrays: `morning`, `afternoon`, `evening` and `anytime`. - `compliments.json` <br> **Default value:** `null` (Do not load from file)
| `classes` | Override the CSS classes of the div showing the compliments <br><br> **Default value:** `thin xlarge bright`
### Compliment configuration ### Compliment configuration
@ -88,7 +89,7 @@ config: {
], ],
afternoon: [ afternoon: [
"Hello, beauty!", "Hello, beauty!",
'You look sexy!', "You look sexy!",
"Looking good today!" "Looking good today!"
], ],
evening: [ evening: [

View File

@ -96,14 +96,14 @@ Module.register("compliments", {
*/ */
complimentArray: function() { complimentArray: function() {
var hour = moment().hour(); var hour = moment().hour();
var compliments = null; var compliments;
if (hour >= 3 && hour < 12) { if (hour >= 3 && hour < 12 && this.config.compliments.hasOwnProperty("morning")) {
compliments = this.config.compliments.morning; compliments = this.config.compliments.morning.slice(0);
} else if (hour >= 12 && hour < 17) { } else if (hour >= 12 && hour < 17 && this.config.compliments.hasOwnProperty("afternoon")) {
compliments = this.config.compliments.afternoon; compliments = this.config.compliments.afternoon.slice(0);
} else { } else if(this.config.compliments.hasOwnProperty("evening")) {
compliments = this.config.compliments.evening; compliments = this.config.compliments.evening.slice(0);
} }
if (typeof compliments === "undefined") { if (typeof compliments === "undefined") {
@ -117,7 +117,6 @@ Module.register("compliments", {
compliments.push.apply(compliments, this.config.compliments.anytime); compliments.push.apply(compliments, this.config.compliments.anytime);
return compliments; return compliments;
}, },
/* complimentFile(callback) /* complimentFile(callback)

View File

@ -40,7 +40,9 @@ The following properties can be configured:
| `showPeriod` | Show the period (am/pm) with 12 hour format <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true` | `showPeriod` | Show the period (am/pm) with 12 hour format <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
| `showPeriodUpper` | Show the period (AM/PM) with 12 hour format as uppercase <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false` | `showPeriodUpper` | Show the period (AM/PM) with 12 hour format as uppercase <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
| `showWindDirection` | Show the wind direction next to the wind speed. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true` | `showWindDirection` | Show the wind direction next to the wind speed. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
| `showWindDirectionAsArrow` | Show the wind direction as an arrow instead of abbreviation <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
| `showHumidity` | Show the current humidity <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false` | `showHumidity` | Show the current humidity <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
| `showIndoorTemperature` | If you have another module that emits the INDOOR_TEMPERATURE notification, the indoor temperature will be displayed <br> **Default value:** `false`
| `onlyTemp` | Show only current Temperature and weather icon. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false` | `onlyTemp` | Show only current Temperature and weather icon. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
| `useBeaufort` | Pick between using the Beaufort scale for wind speed or using the default units. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true` | `useBeaufort` | Pick between using the Beaufort scale for wind speed or using the default units. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
| `lang` | The language of the days. <br><br> **Possible values:** `en`, `nl`, `ru`, etc ... <br> **Default value:** uses value of _config.language_ | `lang` | The language of the days. <br><br> **Possible values:** `en`, `nl`, `ru`, etc ... <br> **Default value:** uses value of _config.language_

View File

@ -1,4 +1,5 @@
.currentweather .weathericon { .currentweather .weathericon,
.currentweather .fa-home {
font-size: 75%; font-size: 75%;
line-height: 65px; line-height: 65px;
display: inline-block; display: inline-block;

View File

@ -21,10 +21,12 @@ Module.register("currentweather",{
showPeriod: true, showPeriod: true,
showPeriodUpper: false, showPeriodUpper: false,
showWindDirection: true, showWindDirection: true,
showWindDirectionAsArrow: false,
useBeaufort: true, useBeaufort: true,
lang: config.language, lang: config.language,
showHumidity: false, showHumidity: false,
degreeLabel: false, degreeLabel: false,
showIndoorTemperature: false,
initialLoadDelay: 0, // 0 seconds delay initialLoadDelay: 0, // 0 seconds delay
retryDelay: 2500, retryDelay: 2500,
@ -94,9 +96,11 @@ Module.register("currentweather",{
this.windSpeed = null; this.windSpeed = null;
this.windDirection = null; this.windDirection = null;
this.windDeg = null;
this.sunriseSunsetTime = null; this.sunriseSunsetTime = null;
this.sunriseSunsetIcon = null; this.sunriseSunsetIcon = null;
this.temperature = null; this.temperature = null;
this.indoorTemperature = null;
this.weatherType = null; this.weatherType = null;
this.loaded = false; this.loaded = false;
@ -111,7 +115,6 @@ Module.register("currentweather",{
var small = document.createElement("div"); var small = document.createElement("div");
small.className = "normal medium"; small.className = "normal medium";
var windIcon = document.createElement("span"); var windIcon = document.createElement("span");
windIcon.className = "wi wi-strong-wind dimmed"; windIcon.className = "wi wi-strong-wind dimmed";
small.appendChild(windIcon); small.appendChild(windIcon);
@ -122,7 +125,13 @@ Module.register("currentweather",{
if (this.config.showWindDirection) { if (this.config.showWindDirection) {
var windDirection = document.createElement("sup"); var windDirection = document.createElement("sup");
if (this.config.showWindDirectionAsArrow) {
if(this.windDeg !== null) {
windDirection.innerHTML = " &nbsp;<i class=\"fa fa-long-arrow-down\" style=\"transform:rotate("+this.windDeg+"deg);\"></i>&nbsp;";
}
} else {
windDirection.innerHTML = " " + this.translate(this.windDirection); windDirection.innerHTML = " " + this.translate(this.windDirection);
}
small.appendChild(windDirection); small.appendChild(windDirection);
} }
var spacer = document.createElement("span"); var spacer = document.createElement("span");
@ -203,6 +212,17 @@ Module.register("currentweather",{
temperature.innerHTML = " " + this.temperature + "&deg;" + degreeLabel; temperature.innerHTML = " " + this.temperature + "&deg;" + degreeLabel;
large.appendChild(temperature); large.appendChild(temperature);
if (this.config.showIndoorTemperature && this.indoorTemperature) {
var indoorIcon = document.createElement("span");
indoorIcon.className = "fa fa-home";
large.appendChild(indoorIcon);
var indoorTemperatureElem = document.createElement("span");
indoorTemperatureElem.className = "bright";
indoorTemperatureElem.innerHTML = " " + this.indoorTemperature + "&deg;" + degreeLabel;
large.appendChild(indoorTemperatureElem);
}
wrapper.appendChild(large); wrapper.appendChild(large);
return wrapper; return wrapper;
}, },
@ -226,10 +246,9 @@ Module.register("currentweather",{
if (notification === "CALENDAR_EVENTS") { if (notification === "CALENDAR_EVENTS") {
var senderClasses = sender.data.classes.toLowerCase().split(" "); var senderClasses = sender.data.classes.toLowerCase().split(" ");
if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) { if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
var lastEvent = this.firstEvent;
this.firstEvent = false; this.firstEvent = false;
for (e in payload) { for (var e in payload) {
var event = payload[e]; var event = payload[e];
if (event.location || event.geo) { if (event.location || event.geo) {
this.firstEvent = event; this.firstEvent = event;
@ -239,6 +258,10 @@ Module.register("currentweather",{
} }
} }
} }
if (notification === "INDOOR_TEMPERATURE") {
this.indoorTemperature = this.roundValue(payload);
this.updateDom(self.config.animationSpeed);
}
}, },
/* updateWeather(compliments) /* updateWeather(compliments)
@ -327,8 +350,8 @@ Module.register("currentweather",{
this.windSpeed = parseFloat(data.wind.speed).toFixed(0); this.windSpeed = parseFloat(data.wind.speed).toFixed(0);
} }
this.windDirection = this.deg2Cardinal(data.wind.deg); this.windDirection = this.deg2Cardinal(data.wind.deg);
this.windDeg = data.wind.deg;
this.weatherType = this.config.iconTable[data.weather[0].icon]; this.weatherType = this.config.iconTable[data.weather[0].icon];
var now = new Date(); var now = new Date();
@ -359,7 +382,6 @@ Module.register("currentweather",{
this.sunriseSunsetTime = timeString; this.sunriseSunsetTime = timeString;
this.sunriseSunsetIcon = (sunrise < now && sunset > now) ? "wi-sunset" : "wi-sunrise"; this.sunriseSunsetIcon = (sunrise < now && sunset > now) ? "wi-sunset" : "wi-sunrise";
this.show(this.config.animationSpeed, {lockString:this.identifier}); this.show(this.config.animationSpeed, {lockString:this.identifier});
this.loaded = true; this.loaded = true;
this.updateDom(this.config.animationSpeed); this.updateDom(this.config.animationSpeed);
@ -386,6 +408,10 @@ Module.register("currentweather",{
/* ms2Beaufort(ms) /* ms2Beaufort(ms)
* Converts m2 to beaufort (windspeed). * Converts m2 to beaufort (windspeed).
* *
* see:
* http://www.spc.noaa.gov/faq/tornado/beaufort.html
* https://en.wikipedia.org/wiki/Beaufort_scale#Modern_scale
*
* argument ms number - Windspeed in m/s. * argument ms number - Windspeed in m/s.
* *
* return number - Windspeed in beaufort. * return number - Windspeed in beaufort.

View File

@ -63,7 +63,7 @@ Module.register("weatherforecast",{
firstEvent: false, firstEvent: false,
// create a variable to hold the location name based on the API result. // create a variable to hold the location name based on the API result.
fetchedLocatioName: "", fetchedLocationName: "",
// Define required scripts. // Define required scripts.
getScripts: function() { getScripts: function() {
@ -175,7 +175,6 @@ Module.register("weatherforecast",{
row.style.opacity = 1 - (1 / steps * currentStep); row.style.opacity = 1 - (1 / steps * currentStep);
} }
} }
} }
return table; return table;
@ -184,7 +183,7 @@ Module.register("weatherforecast",{
// Override getHeader method. // Override getHeader method.
getHeader: function() { getHeader: function() {
if (this.config.appendLocationNameToHeader) { if (this.config.appendLocationNameToHeader) {
return this.data.header + " " + this.fetchedLocatioName; return this.data.header + " " + this.fetchedLocationName;
} }
return this.data.header; return this.data.header;
@ -200,10 +199,9 @@ Module.register("weatherforecast",{
if (notification === "CALENDAR_EVENTS") { if (notification === "CALENDAR_EVENTS") {
var senderClasses = sender.data.classes.toLowerCase().split(" "); var senderClasses = sender.data.classes.toLowerCase().split(" ");
if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) { if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
var lastEvent = this.firstEvent;
this.firstEvent = false; this.firstEvent = false;
for (e in payload) { for (var e in payload) {
var event = payload[e]; var event = payload[e];
if (event.location || event.geo) { if (event.location || event.geo) {
this.firstEvent = event; this.firstEvent = event;
@ -291,7 +289,7 @@ Module.register("weatherforecast",{
* argument data object - Weather information received form openweather.org. * argument data object - Weather information received form openweather.org.
*/ */
processWeather: function(data) { processWeather: function(data) {
this.fetchedLocatioName = data.city.name + ", " + data.city.country; this.fetchedLocationName = data.city.name + ", " + data.city.country;
this.forecast = []; this.forecast = [];
for (var i = 0, count = data.list.length; i < count; i++) { for (var i = 0, count = data.list.length; i < count; i++) {
@ -335,6 +333,10 @@ Module.register("weatherforecast",{
/* ms2Beaufort(ms) /* ms2Beaufort(ms)
* Converts m2 to beaufort (windspeed). * Converts m2 to beaufort (windspeed).
* *
* see:
* http://www.spc.noaa.gov/faq/tornado/beaufort.html
* https://en.wikipedia.org/wiki/Beaufort_scale#Modern_scale
*
* argument ms number - Windspeed in m/s. * argument ms number - Windspeed in m/s.
* *
* return number - Windspeed in beaufort. * return number - Windspeed in beaufort.

View File

@ -1,6 +1,6 @@
{ {
"name": "magicmirror", "name": "magicmirror",
"version": "2.1.2-dev", "version": "2.1.3-dev",
"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": {
@ -34,33 +34,35 @@
"devDependencies": { "devDependencies": {
"chai": "^3.5.0", "chai": "^3.5.0",
"chai-as-promised": "^6.0.0", "chai-as-promised": "^6.0.0",
"current-week-number": "^1.0.7",
"grunt": "latest", "grunt": "latest",
"grunt-eslint": "latest", "grunt-eslint": "latest",
"grunt-jsonlint": "latest", "grunt-jsonlint": "latest",
"grunt-markdownlint": "^1.0.13", "grunt-markdownlint": "^1.0.39",
"grunt-stylelint": "latest", "grunt-stylelint": "latest",
"grunt-yamllint": "latest", "grunt-yamllint": "latest",
"jshint": "^2.9.4",
"http-auth": "^3.1.3", "http-auth": "^3.1.3",
"mocha": "^3.2.0", "jshint": "^2.9.4",
"spectron": "^3.4.1", "mocha": "^3.4.2",
"spectron": "^3.6.4",
"stylelint": "^7.11.0",
"stylelint-config-standard": "latest", "stylelint-config-standard": "latest",
"time-grunt": "latest" "time-grunt": "latest"
}, },
"dependencies": { "dependencies": {
"body-parser": "^1.17.1", "body-parser": "^1.17.2",
"colors": "^1.1.2", "colors": "^1.1.2",
"electron": "^1.4.7", "electron": "^1.6.10",
"express": "^4.14.0", "express": "^4.15.3",
"express-ipfilter": "latest", "express-ipfilter": "latest",
"feedme": "latest", "feedme": "latest",
"helmet": "^3.1.0", "helmet": "^3.6.1",
"iconv-lite": "latest", "iconv-lite": "latest",
"moment": "latest", "moment": "latest",
"request": "^2.78.0", "request": "^2.81.0",
"rrule-alt": "^2.2.3", "rrule-alt": "^2.2.5",
"simple-git": "^1.62.0", "simple-git": "^1.73.0",
"socket.io": "^1.7.3", "socket.io": "^2.0.2",
"valid-url": "latest", "valid-url": "latest",
"walk": "latest" "walk": "latest"
} }

View File

@ -1,50 +1,65 @@
const Application = require("spectron").Application; const helpers = require("./global-setup");
const path = require("path"); const path = require("path");
const chai = require("chai"); const request = require("request");
const expect = chai.expect;
const chaiAsPromised = require("chai-as-promised");
var electronPath = path.join(__dirname, "../../", "node_modules", ".bin", "electron"); const expect = require("chai").expect;
if (process.platform === "win32") { const describe = global.describe;
electronPath += ".cmd"; const it = global.it;
} const beforeEach = global.beforeEach;
const afterEach = global.afterEach;
var appPath = path.join(__dirname, "../../js/electron.js"); describe("Development console tests", function() {
// This tests fail and crash another tests
// Suspect problem with window focus
// FIXME
return false;
var app = new Application({ helpers.setupTimeout(this);
path: electronPath
});
global.before(function () { var app = null;
chai.should();
chai.use(chaiAsPromised);
});
describe("Argument 'dev'", function () {
this.timeout(20000);
before(function() { before(function() {
// Set config sample for use in test // Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/env.js"; process.env.MM_CONFIG_FILE = "tests/configs/env.js";
}); });
afterEach(function (done) { describe("Without 'dev' commandline argument", function() {
app.stop().then(function() { done(); }); before(function() {
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function(startedApp) {
app = startedApp;
});
});
after(function() {
return helpers.stopApplication(app);
}); });
it("should not open dev console when absent", function() { it("should not open dev console when absent", function() {
app.args = [appPath];
return app.start().then(function() {
return expect(app.browserWindow.isDevToolsOpened()).to.eventually.equal(false); return expect(app.browserWindow.isDevToolsOpened()).to.eventually.equal(false);
}); });
}); });
it("should open dev console when provided", function () { describe("With 'dev' commandline argument", function() {
app.args = [appPath, "dev"]; before(function() {
return helpers
.startApplication({
args: ["js/electron.js", "dev"]
})
.then(function(startedApp) {
app = startedApp;
});
});
return app.start().then(function() { after(function() {
return helpers.stopApplication(app);
});
it("should open dev console when provided", function() {
return expect(app.browserWindow.isDevToolsOpened()).to.eventually.equal(true); return expect(app.browserWindow.isDevToolsOpened()).to.eventually.equal(true);
}); });
}); });

View File

@ -1,33 +1,56 @@
const globalSetup = require("./global-setup"); const helpers = require("./global-setup");
const app = globalSetup.app; const path = require("path");
const request = require("request"); const request = require("request");
const chai = require("chai");
const expect = chai.expect; const expect = require("chai").expect;
const describe = global.describe;
const it = global.it;
const beforeEach = global.beforeEach;
const afterEach = global.afterEach;
describe("Electron app environment", function() { describe("Electron app environment", function() {
this.timeout(20000); helpers.setupTimeout(this);
var app = null;
before(function() { before(function() {
// Set config sample for use in test // Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/env.js"; process.env.MM_CONFIG_FILE = "tests/configs/env.js";
}); });
beforeEach(function (done) { beforeEach(function() {
app.start().then(function() { done(); } ); return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function(startedApp) {
app = startedApp;
});
}); });
afterEach(function (done) { afterEach(function() {
app.stop().then(function() { done(); }); return helpers.stopApplication(app);
}); });
it("is set to open new app window", function () { it("should open a browserwindow", function() {
return app.client.waitUntilWindowLoaded() return app.client
.getWindowCount().should.eventually.equal(1); .waitUntilWindowLoaded()
}); .browserWindow.focus()
.getWindowCount()
it("sets correct window title", function () { .should.eventually.equal(1)
return app.client.waitUntilWindowLoaded() .browserWindow.isMinimized()
.getTitle().should.eventually.equal("Magic Mirror"); .should.eventually.be.false.browserWindow.isDevToolsOpened()
.should.eventually.be.false.browserWindow.isVisible()
.should.eventually.be.true.browserWindow.isFocused()
.should.eventually.be.true.browserWindow.getBounds()
.should.eventually.have.property("width")
.and.be.above(0)
.browserWindow.getBounds()
.should.eventually.have.property("height")
.and.be.above(0)
.browserWindow.getTitle()
.should.eventually.equal("Magic Mirror");
}); });
it("get request from http://localhost:8080 should return 200", function(done) { it("get request from http://localhost:8080 should return 200", function(done) {
@ -43,5 +66,4 @@ describe("Electron app environment", function () {
done(); done();
}); });
}); });
}); });

View File

@ -9,26 +9,54 @@
*/ */
const Application = require("spectron").Application; const Application = require("spectron").Application;
const path = require("path"); const assert = require("assert");
const chai = require("chai"); const chai = require("chai");
const chaiAsPromised = require("chai-as-promised"); const chaiAsPromised = require("chai-as-promised");
var electronPath = path.join(__dirname, "../../", "node_modules", ".bin", "electron"); const path = require("path");
if (process.platform === "win32") {
electronPath += ".cmd";
}
var appPath = path.join(__dirname, "../../js/electron.js");
var app = new Application({
path: electronPath,
args: [appPath]
});
global.before(function() { global.before(function() {
chai.should(); chai.should();
chai.use(chaiAsPromised); chai.use(chaiAsPromised);
}); });
exports.app = app; exports.getElectronPath = function() {
var electronPath = path.join(__dirname, "..", "..", "node_modules", ".bin", "electron");
if (process.platform === "win32") {
electronPath += ".cmd";
}
return electronPath;
};
// Set timeout - if this is run within Travis, increase timeout
exports.setupTimeout = function(test) {
if (process.env.CI) {
test.timeout(30000);
} else {
test.timeout(10000);
}
};
exports.startApplication = function(options) {
options.path = exports.getElectronPath();
if (process.env.CI) {
options.startTimeout = 30000;
}
var app = new Application(options);
return app.start().then(function() {
assert.equal(app.isRunning(), true);
chaiAsPromised.transferPromiseness = app.transferPromiseness;
return app;
});
};
exports.stopApplication = function(app) {
if (!app || !app.isRunning()) {
return;
}
return app.stop().then(function() {
assert.equal(app.isRunning(), false);
});
};

View File

@ -1,20 +1,27 @@
const globalSetup = require("./global-setup"); const helpers = require("./global-setup");
const app = globalSetup.app; const path = require("path");
const request = require("request"); const request = require("request");
const chai = require("chai");
const expect = chai.expect;
const expect = require("chai").expect;
const describe = global.describe;
const it = global.it;
const beforeEach = global.beforeEach;
const afterEach = global.afterEach;
describe("ipWhitelist directive configuration", function () { describe("ipWhitelist directive configuration", function () {
helpers.setupTimeout(this);
this.timeout(20000); var app = null;
beforeEach(function (done) { beforeEach(function () {
app.start().then(function() { done(); } ); return helpers.startApplication({
args: ["js/electron.js"]
}).then(function (startedApp) { app = startedApp; })
}); });
afterEach(function (done) { afterEach(function () {
app.stop().then(function() { done(); }); return helpers.stopApplication(app);
}); });
describe("Set ipWhitelist without access", function () { describe("Set ipWhitelist without access", function () {

View File

@ -1,19 +1,32 @@
const globalSetup = require("../global-setup"); const helpers = require("../global-setup");
const path = require("path");
const request = require("request");
const serverBasicAuth = require("../../servers/basic-auth.js"); const serverBasicAuth = require("../../servers/basic-auth.js");
const app = globalSetup.app;
const chai = require("chai"); const expect = require("chai").expect;
const expect = chai.expect;
const describe = global.describe;
const it = global.it;
const beforeEach = global.beforeEach;
const afterEach = global.afterEach;
describe("Calendar module", function() { describe("Calendar module", function() {
helpers.setupTimeout(this);
this.timeout(20000); var app = null;
beforeEach(function (done) { beforeEach(function() {
app.start().then(function() { done(); } ); return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function(startedApp) {
app = startedApp;
});
}); });
afterEach(function (done) { afterEach(function() {
app.stop().then(function() { done(); }); return helpers.stopApplication(app);
}); });
describe("Default configuration", function() { describe("Default configuration", function() {
@ -27,7 +40,6 @@ describe("Calendar module", function () {
}); });
}); });
describe("Basic auth", function() { describe("Basic auth", function() {
before(function() { before(function() {
serverBasicAuth.listen(8010); serverBasicAuth.listen(8010);
@ -35,12 +47,15 @@ describe("Calendar module", function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/basic-auth.js"; process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/basic-auth.js";
}); });
after(function(done) {
serverBasicAuth.close(done());
});
it("Should return TestEvents", function() { it("Should return TestEvents", function() {
return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000); return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
}); });
}); });
describe("Basic auth by default", function() { describe("Basic auth by default", function() {
before(function() { before(function() {
serverBasicAuth.listen(8011); serverBasicAuth.listen(8011);
@ -48,6 +63,10 @@ describe("Calendar module", function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/auth-default.js"; process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/auth-default.js";
}); });
after(function(done) {
serverBasicAuth.close(done());
});
it("Should return TestEvents", function() { it("Should return TestEvents", function() {
return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000); return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
}); });
@ -60,6 +79,10 @@ describe("Calendar module", function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/old-basic-auth.js"; process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/old-basic-auth.js";
}); });
after(function(done) {
serverBasicAuth.close(done());
});
it("Should return TestEvents", function() { it("Should return TestEvents", function() {
return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000); return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
}); });
@ -72,10 +95,12 @@ describe("Calendar module", function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/fail-basic-auth.js"; process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/fail-basic-auth.js";
}); });
after(function(done) {
serverBasicAuth.close(done());
});
it("Should return No upcoming events", function() { it("Should return No upcoming events", function() {
return app.client.waitUntilTextExists(".calendar", "No upcoming events.", 10000); return app.client.waitUntilTextExists(".calendar", "No upcoming events.", 10000);
}); });
}); });
}); });

View File

@ -1,8 +1,32 @@
const globalSetup = require("../global-setup"); const helpers = require("../global-setup");
const app = globalSetup.app; const path = require("path");
const request = require("request");
const expect = require("chai").expect;
const describe = global.describe;
const it = global.it;
const beforeEach = global.beforeEach;
const afterEach = global.afterEach;
describe("Clock set to spanish language module", function() { describe("Clock set to spanish language module", function() {
this.timeout(20000); helpers.setupTimeout(this);
var app = null;
beforeEach(function() {
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function(startedApp) {
app = startedApp;
});
});
afterEach(function() {
return helpers.stopApplication(app);
});
describe("with default 24hr clock config", function() { describe("with default 24hr clock config", function() {
before(function() { before(function() {
@ -10,24 +34,14 @@ describe("Clock set to spanish language module", function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/es/clock_24hr.js"; process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/es/clock_24hr.js";
}); });
beforeEach(function (done) {
app.start().then(function() { done(); } );
});
afterEach(function (done) {
app.stop().then(function() { done(); });
});
it("shows date with correct format", function() { it("shows date with correct format", function() {
const dateRegex = /^(?:lunes|martes|miércoles|jueves|viernes|sábado|domingo), \d{1,2} de (?:enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre) de \d{4}$/; const dateRegex = /^(?:lunes|martes|miércoles|jueves|viernes|sábado|domingo), \d{1,2} de (?:enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre) de \d{4}$/;
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".clock .date").should.eventually.match(dateRegex);
.getText(".clock .date").should.eventually.match(dateRegex);
}); });
it("shows time in 24hr format", function() { it("shows time in 24hr format", function() {
const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/ const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/;
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".clock .time").should.eventually.match(timeRegex);
.getText(".clock .time").should.eventually.match(timeRegex);
}); });
}); });
@ -37,24 +51,14 @@ describe("Clock set to spanish language module", function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/es/clock_12hr.js"; process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/es/clock_12hr.js";
}); });
beforeEach(function (done) {
app.start().then(function() { done(); } );
});
afterEach(function (done) {
app.stop().then(function() { done(); });
});
it("shows date with correct format", function() { it("shows date with correct format", function() {
const dateRegex = /^(?:lunes|martes|miércoles|jueves|viernes|sábado|domingo), \d{1,2} de (?:enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre) de \d{4}$/; const dateRegex = /^(?:lunes|martes|miércoles|jueves|viernes|sábado|domingo), \d{1,2} de (?:enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre) de \d{4}$/;
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".clock .date").should.eventually.match(dateRegex);
.getText(".clock .date").should.eventually.match(dateRegex);
}); });
it("shows time in 12hr format", function() { it("shows time in 12hr format", function() {
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/; const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/;
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".clock .time").should.eventually.match(timeRegex);
.getText(".clock .time").should.eventually.match(timeRegex);
}); });
}); });
@ -64,18 +68,9 @@ describe("Clock set to spanish language module", function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/es/clock_showPeriodUpper.js"; process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/es/clock_showPeriodUpper.js";
}); });
beforeEach(function (done) {
app.start().then(function() { done(); } );
});
afterEach(function (done) {
app.stop().then(function() { done(); });
});
it("shows 12hr time with upper case AM/PM", function() { it("shows 12hr time with upper case AM/PM", function() {
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/; const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/;
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".clock .time").should.eventually.match(timeRegex);
.getText(".clock .time").should.eventually.match(timeRegex);
}); });
}); });

View File

@ -1,8 +1,32 @@
const globalSetup = require("../global-setup"); const helpers = require("../global-setup");
const app = globalSetup.app; const path = require("path");
const request = require("request");
const expect = require("chai").expect;
const describe = global.describe;
const it = global.it;
const beforeEach = global.beforeEach;
const afterEach = global.afterEach;
describe("Clock module", function() { describe("Clock module", function() {
this.timeout(20000); helpers.setupTimeout(this);
var app = null;
beforeEach(function() {
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function(startedApp) {
app = startedApp;
});
});
afterEach(function() {
return helpers.stopApplication(app);
});
describe("with default 24hr clock config", function() { describe("with default 24hr clock config", function() {
before(function() { before(function() {
@ -10,24 +34,14 @@ describe("Clock module", function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_24hr.js"; process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_24hr.js";
}); });
beforeEach(function (done) {
app.start().then(function() { done(); } );
});
afterEach(function (done) {
app.stop().then(function() { done(); });
});
it("shows date with correct format", function() { it("shows date with correct format", function() {
const dateRegex = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$/; const dateRegex = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$/;
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".clock .date").should.eventually.match(dateRegex);
.getText(".clock .date").should.eventually.match(dateRegex);
}); });
it("shows time in 24hr format", function() { it("shows time in 24hr format", function() {
const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/ const timeRegex = /^(?:2[0-3]|[01]\d):[0-5]\d[0-5]\d$/;
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".clock .time").should.eventually.match(timeRegex);
.getText(".clock .time").should.eventually.match(timeRegex);
}); });
}); });
@ -37,24 +51,14 @@ describe("Clock module", function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_12hr.js"; process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_12hr.js";
}); });
beforeEach(function (done) {
app.start().then(function() { done(); } );
});
afterEach(function (done) {
app.stop().then(function() { done(); });
});
it("shows date with correct format", function() { it("shows date with correct format", function() {
const dateRegex = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$/; const dateRegex = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{1,2}, \d{4}$/;
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".clock .date").should.eventually.match(dateRegex);
.getText(".clock .date").should.eventually.match(dateRegex);
}); });
it("shows time in 12hr format", function() { it("shows time in 12hr format", function() {
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/; const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[ap]m$/;
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".clock .time").should.eventually.match(timeRegex);
.getText(".clock .time").should.eventually.match(timeRegex);
}); });
}); });
@ -64,18 +68,9 @@ describe("Clock module", function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_showPeriodUpper.js"; process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_showPeriodUpper.js";
}); });
beforeEach(function (done) {
app.start().then(function() { done(); } );
});
afterEach(function (done) {
app.stop().then(function() { done(); });
});
it("shows 12hr time with upper case AM/PM", function() { it("shows 12hr time with upper case AM/PM", function() {
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/; const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[0-5]\d[AP]M$/;
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".clock .time").should.eventually.match(timeRegex);
.getText(".clock .time").should.eventually.match(timeRegex);
}); });
}); });
@ -85,18 +80,9 @@ describe("Clock module", function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_displaySeconds_false.js"; process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_displaySeconds_false.js";
}); });
beforeEach(function (done) {
app.start().then(function() { done(); } );
});
afterEach(function (done) {
app.stop().then(function() { done(); });
});
it("shows 12hr time without seconds am/pm", function() { it("shows 12hr time without seconds am/pm", function() {
const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[ap]m$/; const timeRegex = /^(?:1[0-2]|[1-9]):[0-5]\d[ap]m$/;
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".clock .time").should.eventually.match(timeRegex);
.getText(".clock .time").should.eventually.match(timeRegex);
}); });
}); });
@ -106,19 +92,17 @@ describe("Clock module", function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_showWeek.js"; process.env.MM_CONFIG_FILE = "tests/configs/modules/clock/clock_showWeek.js";
}); });
beforeEach(function (done) {
app.start().then(function() { done(); } );
});
afterEach(function (done) {
app.stop().then(function() { done(); });
});
it("shows week with correct format", function() { it("shows week with correct format", function() {
const weekRegex = /^Week [0-9]{1,2}$/; const weekRegex = /^Week [0-9]{1,2}$/;
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".clock .week").should.eventually.match(weekRegex);
.getText(".clock .week").should.eventually.match(weekRegex);
});
}); });
it("shows week with correct number of week of year", function() {
it("FIXME: if the day is a sunday this not match");
// const currentWeekNumber = require("current-week-number")();
// const weekToShow = "Week " + currentWeekNumber;
// return app.client.waitUntilWindowLoaded()
// .getText(".clock .week").should.eventually.equal(weekToShow);
});
});
}); });

View File

@ -1,23 +1,34 @@
const globalSetup = require("../global-setup"); const helpers = require("../global-setup");
const app = globalSetup.app; const path = require("path");
const chai = require("chai"); const request = require("request");
const expect = chai.expect;
const expect = require("chai").expect;
const describe = global.describe;
const it = global.it;
const beforeEach = global.beforeEach;
const afterEach = global.afterEach;
describe("Compliments module", function() { describe("Compliments module", function() {
this.timeout(20000); helpers.setupTimeout(this);
var app = null;
beforeEach(function (done) { beforeEach(function() {
app.start().then(function() { done(); } ); return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function(startedApp) {
app = startedApp;
});
}); });
afterEach(function (done) { afterEach(function() {
app.stop().then(function() { done(); }); return helpers.stopApplication(app);
}); });
describe("parts of days", function() { describe("parts of days", function() {
before(function() { before(function() {
// Set config sample for use in test // Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/compliments/compliments_parts_day.js"; process.env.MM_CONFIG_FILE = "tests/configs/modules/compliments/compliments_parts_day.js";
@ -27,10 +38,9 @@ describe("Compliments module", function () {
var hour = new Date().getHours(); var hour = new Date().getHours();
if (hour >= 3 && hour < 12) { if (hour >= 3 && hour < 12) {
// if morning check // if morning check
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".compliments").then(function(text) {
.getText(".compliments").then(function (text) {
expect(text).to.be.oneOf(["Hi", "Good Morning", "Morning test"]); expect(text).to.be.oneOf(["Hi", "Good Morning", "Morning test"]);
}) });
} }
}); });
@ -38,10 +48,9 @@ describe("Compliments module", function () {
var hour = new Date().getHours(); var hour = new Date().getHours();
if (hour >= 12 && hour < 17) { if (hour >= 12 && hour < 17) {
// if morning check // if morning check
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".compliments").then(function(text) {
.getText(".compliments").then(function (text) {
expect(text).to.be.oneOf(["Hello", "Good Afternoon", "Afternoon test"]); expect(text).to.be.oneOf(["Hello", "Good Afternoon", "Afternoon test"]);
}) });
} }
}); });
@ -49,18 +58,14 @@ describe("Compliments module", function () {
var hour = new Date().getHours(); var hour = new Date().getHours();
if (!(hour >= 3 && hour < 12) && !(hour >= 12 && hour < 17)) { if (!(hour >= 3 && hour < 12) && !(hour >= 12 && hour < 17)) {
// if evening check // if evening check
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".compliments").then(function(text) {
.getText(".compliments").then(function (text) {
expect(text).to.be.oneOf(["Hello There", "Good Evening", "Evening test"]); expect(text).to.be.oneOf(["Hello There", "Good Evening", "Evening test"]);
}) });
} }
}); });
}); });
describe("Feature anytime in compliments module", function() { describe("Feature anytime in compliments module", function() {
describe("Set anytime and empty compliments for morning, evening and afternoon ", function() { describe("Set anytime and empty compliments for morning, evening and afternoon ", function() {
before(function() { before(function() {
// Set config sample for use in test // Set config sample for use in test
@ -68,10 +73,9 @@ describe("Compliments module", function () {
}); });
it("Show anytime because if configure empty parts of day compliments and set anytime compliments", function() { it("Show anytime because if configure empty parts of day compliments and set anytime compliments", function() {
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".compliments").then(function(text) {
.getText(".compliments").then(function (text) {
expect(text).to.be.oneOf(["Anytime here"]); expect(text).to.be.oneOf(["Anytime here"]);
}) });
}); });
}); });
@ -82,14 +86,10 @@ describe("Compliments module", function () {
}); });
it("Show anytime compliments", function() { it("Show anytime compliments", function() {
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".compliments").then(function(text) {
.getText(".compliments").then(function (text) {
expect(text).to.be.oneOf(["Anytime here"]); expect(text).to.be.oneOf(["Anytime here"]);
})
}); });
}); });
}); });
});
}); });

View File

@ -1,24 +1,39 @@
const globalSetup = require("../global-setup"); const helpers = require("../global-setup");
const app = globalSetup.app; const path = require("path");
const request = require("request");
const expect = require("chai").expect;
const describe = global.describe;
const it = global.it;
const beforeEach = global.beforeEach;
const afterEach = global.afterEach;
describe("Test helloworld module", function() { describe("Test helloworld module", function() {
this.timeout(20000); helpers.setupTimeout(this);
var app = null;
beforeEach(function() {
return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function(startedApp) {
app = startedApp;
});
});
before(function() { before(function() {
// Set config sample for use in test // Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/helloworld/helloworld.js"; process.env.MM_CONFIG_FILE = "tests/configs/modules/helloworld/helloworld.js";
}); });
beforeEach(function (done) { afterEach(function() {
app.start().then(function() { done(); } ); return helpers.stopApplication(app);
});
afterEach(function (done) {
app.stop().then(function() { done(); });
}); });
it("Test message helloworld module", function() { it("Test message helloworld module", function() {
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded().getText(".helloworld").should.eventually.equal("Test HelloWorld Module");
.getText(".helloworld").should.eventually.equal("Test HelloWorld Module");
}); });
}); });

View File

@ -1,22 +1,34 @@
const globalSetup = require("../global-setup"); const helpers = require("../global-setup");
const app = globalSetup.app; const path = require("path");
const chai = require("chai"); const request = require("request");
const expect = chai.expect;
const expect = require("chai").expect;
const describe = global.describe;
const it = global.it;
const beforeEach = global.beforeEach;
const afterEach = global.afterEach;
describe("Newsfeed module", function() { describe("Newsfeed module", function() {
helpers.setupTimeout(this);
this.timeout(20000); var app = null;
beforeEach(function (done) { beforeEach(function() {
app.start().then(function() { done(); } ); return helpers
.startApplication({
args: ["js/electron.js"]
})
.then(function(startedApp) {
app = startedApp;
});
}); });
afterEach(function (done) { afterEach(function() {
app.stop().then(function() { done(); }); return helpers.stopApplication(app);
}); });
describe("Default configuration", function() { describe("Default configuration", function() {
before(function() { before(function() {
process.env.MM_CONFIG_FILE = "tests/configs/modules/newsfeed/default.js"; process.env.MM_CONFIG_FILE = "tests/configs/modules/newsfeed/default.js";
}); });

View File

@ -1,21 +1,29 @@
const globalSetup = require("./global-setup"); const helpers = require("./global-setup");
const app = globalSetup.app; const path = require("path");
const chai = require("chai"); const request = require("request");
const expect = chai.expect;
const expect = require("chai").expect;
const describe = global.describe;
const it = global.it;
const beforeEach = global.beforeEach;
const afterEach = global.afterEach;
describe("Position of modules", function () { describe("Position of modules", function () {
this.timeout(20000); helpers.setupTimeout(this);
var app = null;
beforeEach(function (done) { beforeEach(function () {
app.start().then(function() { done(); } ); return helpers.startApplication({
args: ["js/electron.js"]
}).then(function (startedApp) { app = startedApp; })
}); });
afterEach(function (done) { afterEach(function () {
app.stop().then(function() { done(); }); return helpers.stopApplication(app);
}); });
describe("Using helloworld", function () { describe("Using helloworld", function () {
before(function () { before(function () {

View File

@ -1,20 +1,27 @@
const globalSetup = require("./global-setup"); const helpers = require("./global-setup");
const app = globalSetup.app; const path = require("path");
const request = require("request"); const request = require("request");
const chai = require("chai");
const expect = chai.expect;
const expect = require("chai").expect;
const describe = global.describe;
const it = global.it;
const beforeEach = global.beforeEach;
const afterEach = global.afterEach;
describe("port directive configuration", function () { describe("port directive configuration", function () {
helpers.setupTimeout(this);
this.timeout(20000); var app = null;
beforeEach(function (done) { beforeEach(function () {
app.start().then(function() { done(); } ); return helpers.startApplication({
args: ["js/electron.js"]
}).then(function (startedApp) { app = startedApp; })
}); });
afterEach(function (done) { afterEach(function () {
app.stop().then(function() { done(); }); return helpers.stopApplication(app);
}); });
describe("Set port 8090", function () { describe("Set port 8090", function () {
@ -22,6 +29,7 @@ describe("port directive configuration", function () {
// Set config sample for use in this test // Set config sample for use in this test
process.env.MM_CONFIG_FILE = "tests/configs/port_8090.js"; process.env.MM_CONFIG_FILE = "tests/configs/port_8090.js";
}); });
it("should return 200", function (done) { it("should return 200", function (done) {
request.get("http://localhost:8090", function (err, res, body) { request.get("http://localhost:8090", function (err, res, body) {
expect(res.statusCode).to.equal(200); expect(res.statusCode).to.equal(200);
@ -40,6 +48,7 @@ describe("port directive configuration", function () {
after(function () { after(function () {
delete process.env.MM_PORT; delete process.env.MM_PORT;
}); });
it("should return 200", function (done) { it("should return 200", function (done) {
request.get("http://localhost:8100", function (err, res, body) { request.get("http://localhost:8100", function (err, res, body) {
expect(res.statusCode).to.equal(200); expect(res.statusCode).to.equal(200);

43
tests/e2e/vendor_spec.js Normal file
View File

@ -0,0 +1,43 @@
const helpers = require("./global-setup");
const path = require("path");
const request = require("request");
const expect = require("chai").expect;
const describe = global.describe;
const it = global.it;
const beforeEach = global.beforeEach;
const afterEach = global.afterEach;
describe("Vendors", function () {
helpers.setupTimeout(this);
var app = null;
beforeEach(function () {
return helpers.startApplication({
args: ["js/electron.js"]
}).then(function (startedApp) { app = startedApp; })
});
afterEach(function () {
return helpers.stopApplication(app);
});
describe("Get list vendors", function () {
before(function () {
process.env.MM_CONFIG_FILE = "tests/configs/env.js";
});
var vendors = require(__dirname + "/../../vendor/vendor.js");
Object.keys(vendors).forEach(vendor => {
it(`should return 200 HTTP code for vendor "${vendor}"`, function () {
urlVendor = "http://localhost:8080/vendor/" + vendors[vendor];
request.get(urlVendor, function (err, res, body) {
expect(res.statusCode).to.equal(200);
});
});
});
});
});

View File

@ -1,44 +1,34 @@
const Application = require("spectron").Application; const helpers = require("./global-setup");
const path = require("path"); const path = require("path");
const chai = require("chai"); const request = require("request");
const chaiAsPromised = require("chai-as-promised");
var electronPath = path.join(__dirname, "../../", "node_modules", ".bin", "electron");
if (process.platform === "win32") {
electronPath += ".cmd";
}
var appPath = path.join(__dirname, "../../js/electron.js");
var app = new Application({
path: electronPath,
args: [appPath]
});
global.before(function () {
chai.should();
chai.use(chaiAsPromised);
});
const expect = require("chai").expect;
const describe = global.describe;
const it = global.it;
const beforeEach = global.beforeEach;
const afterEach = global.afterEach;
describe("Check configuration without modules", function () { describe("Check configuration without modules", function () {
this.timeout(20000); helpers.setupTimeout(this);
var app = null;
beforeEach(function () {
return helpers.startApplication({
args: ["js/electron.js"]
}).then(function (startedApp) { app = startedApp; })
});
afterEach(function () {
return helpers.stopApplication(app);
});
before(function () { before(function () {
// Set config sample for use in test // Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/without_modules.js"; process.env.MM_CONFIG_FILE = "tests/configs/without_modules.js";
}); });
beforeEach(function (done) {
app.start().then(function() { done(); } );
});
afterEach(function (done) {
app.stop().then(function() { done(); });
});
it("Show the message MagicMirror title", function () { it("Show the message MagicMirror title", function () {
return app.client.waitUntilWindowLoaded() return app.client.waitUntilWindowLoaded()
.getText("#module_1_helloworld .module-content").should.eventually.equal("Magic Mirror2") .getText("#module_1_helloworld .module-content").should.eventually.equal("Magic Mirror2")

View File

@ -1,16 +1,21 @@
var http = require("http"); var http = require("http");
var path = require("path"); var path = require("path");
var auth = require("http-auth"); var auth = require("http-auth");
var express = require("express") var express = require("express");
var app = express();
var basic = auth.basic({ var server;
var basic = auth.basic(
{
realm: "MagicMirror Area restricted." realm: "MagicMirror Area restricted."
}, (username, password, callback) => { },
(username, password, callback) => {
callback(username === "MagicMirror" && password === "CallMeADog"); callback(username === "MagicMirror" && password === "CallMeADog");
}); }
);
this.server = express(); app.use(auth.connect(basic));
this.server.use(auth.connect(basic));
// Set directories availables // Set directories availables
var directories = ["/tests/configs"]; var directories = ["/tests/configs"];
@ -18,13 +23,13 @@ var directory;
rootPath = path.resolve(__dirname + "/../../"); rootPath = path.resolve(__dirname + "/../../");
for (i in directories) { for (i in directories) {
directory = directories[i]; directory = directories[i];
this.server.use(directory, express.static(path.resolve(rootPath + directory))); app.use(directory, express.static(path.resolve(rootPath + directory)));
} }
exports.listen = function() { exports.listen = function() {
this.server.listen.apply(this.server, arguments); server = app.listen.apply(app, arguments);
}; };
exports.close = function(callback) { exports.close = function(callback) {
this.server.close(callback); server.close(callback);
}; };

View File

@ -0,0 +1,36 @@
var fs = require("fs");
var path = require("path");
var chai = require("chai");
var expect = chai.expect;
var vm = require("vm");
describe("Functions into modules/default/calendar/calendar.js", function() {
// Fake for use by calendar.js
Module = {}
Module.definitions = {};
Module.register = function (name, moduleDefinition) {
Module.definitions[name] = moduleDefinition;
};
// load calendar.js
require("../../../modules/default/calendar/calendar.js");
describe("capFirst", function() {
words = {
"rodrigo": "Rodrigo",
"123m": "123m",
"magic mirror": "Magic mirror",
",a": ",a",
"ñandú": "Ñandú"
};
Object.keys(words).forEach(word => {
it(`for ${word} should return ${words[word]}`, function() {
expect(Module.definitions.calendar.capFirst(word)).to.equal(words[word]);
});
});
});
});

View File

@ -0,0 +1,66 @@
var fs = require("fs");
var path = require("path");
var chai = require("chai");
var expect = chai.expect;
var vm = require("vm");
before(function() {
var basedir = path.join(__dirname, "../../..");
var fileName = "js/app.js";
var filePath = path.join(basedir, fileName);
var code = fs.readFileSync(filePath);
this.sandbox = {
module: {},
__dirname: path.dirname(filePath),
global: {},
console: {
log: function() { /*console.log("console.log(", arguments, ")");*/ }
},
process: {
on: function() { /*console.log("process.on called with: ", arguments);*/ },
env: {}
}
};
this.sandbox.require = function(filename) {
// This modifies the global slightly,
// but supplies vm with essential code
return require(filename);
};
vm.runInNewContext(code, this.sandbox, fileName);
});
after(function() {
//console.log(global);
});
describe("Default modules set in modules/default/defaultmodules.js", function() {
var expectedDefaultModules = [
"alert",
"calendar",
"clock",
"compliments",
"currentweather",
"helloworld",
"newsfeed",
"weatherforecast",
"updatenotification"
];
expectedDefaultModules.forEach(defaultModule => {
it(`contains default module "${defaultModule}"`, function() {
expect(this.sandbox.defaultModules).to.include(defaultModule);
});
});
expectedDefaultModules.forEach(defaultModule => {
it(`contains a folder for modules/default/${defaultModule}"`, function() {
expect(fs.existsSync(path.join(this.sandbox.global.root_path, "modules/default", defaultModule))).to.equal(true);
});
});
});

View File

@ -4,7 +4,7 @@
"TODAY": "Tänään", "TODAY": "Tänään",
"TOMORROW": "Huomenna", "TOMORROW": "Huomenna",
"DAYAFTERTOMORROW": "Ylihuomenna", "DAYAFTERTOMORROW": "Ylihuomenna",
"RUNNING": "Meneillään", "RUNNING": "Päättyy {timeUntilEnd} päästä",
"EMPTY": "Ei tulevia tapahtumia.", "EMPTY": "Ei tulevia tapahtumia.",
"N": "P", "N": "P",

View File

@ -7,6 +7,8 @@
"RUNNING": "Slutter om", "RUNNING": "Slutter om",
"EMPTY": "Ingen kommende arrangementer.", "EMPTY": "Ingen kommende arrangementer.",
"WEEK": "Uke",
"N": "N", "N": "N",
"NNE": "NNØ", "NNE": "NNØ",
"NE": "NØ", "NE": "NØ",
@ -24,7 +26,7 @@
"NW": "NV", "NW": "NV",
"NNW": "NNV", "NNW": "NNV",
"UPDATE_NOTIFICATION": "MagicMirror² oppdatering er tilgjengelig.", "UPDATE_NOTIFICATION": "MagicMirror²-oppdatering er tilgjengelig.",
"UPDATE_NOTIFICATION_MODULE": "Oppdatering tilgjengelig for modulen MODULE_NAME.", "UPDATE_NOTIFICATION_MODULE": "Oppdatering tilgjengelig for modulen MODULE_NAME.",
"UPDATE_INFO": "Nåværende installasjon er COMMIT_COUNT bak BRANCH_NAME grenen." "UPDATE_INFO": "Nåværende installasjon er COMMIT_COUNT bak BRANCH_NAME grenen."
} }

32
translations/ro.json Normal file
View File

@ -0,0 +1,32 @@
{
"LOADING": "Se încarcă &hellip;",
"TODAY": "Astăzi",
"TOMORROW": "Mâine",
"DAYAFTERTOMORROW": "Poimâine",
"RUNNING": "Se termină în",
"EMPTY": "Nici un eveniment.",
"WEEK": "Săptămâna",
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSW",
"SW": "SW",
"WSW": "WSW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW",
"UPDATE_NOTIFICATION": "Un update este disponibil pentru MagicMirror².",
"UPDATE_NOTIFICATION_MODULE": "Un update este disponibil pentru modulul MODULE_NAME.",
"UPDATE_INFO": "Există COMMIT_COUNT commit-uri noi pe branch-ul BRANCH_NAME."
}

View File

@ -33,6 +33,7 @@ var translations = {
"is" : "translations/is.json", // Icelandic "is" : "translations/is.json", // Icelandic
"et" : "translations/et.json", // Estonian "et" : "translations/et.json", // Estonian
"kr" : "translations/kr.json", // Korean "kr" : "translations/kr.json", // Korean
"ro" : "translations/ro.json", // Romanian
}; };
if (typeof module !== "undefined") {module.exports = translations;} if (typeof module !== "undefined") {module.exports = translations;}

2
vendor/vendor.js vendored
View File

@ -14,3 +14,5 @@ var vendor = {
"weather-icons-wind.css": "node_modules/weathericons/css/weather-icons-wind.css", "weather-icons-wind.css": "node_modules/weathericons/css/weather-icons-wind.css",
"font-awesome.css": "node_modules/font-awesome/css/font-awesome.min.css" "font-awesome.css": "node_modules/font-awesome/css/font-awesome.min.css"
}; };
if (typeof module !== "undefined"){module.exports = vendor;}