Merge remote-tracking branch 'MichMich/master'

This commit is contained in:
Thomas Bachmann 2018-01-29 18:54:32 +01:00
commit b6538d5e18
264 changed files with 13789 additions and 28655 deletions

View File

@ -1,4 +1,4 @@
vendor/
vendor/*
!/vendor/vendor.js
!/modules/default/**
!/modules/node_helper

View File

@ -5,7 +5,7 @@
"max-len": ["error", 250],
"curly": "error",
"camelcase": ["error", {"properties": "never"}],
"no-trailing-spaces": ["error"],
"no-trailing-spaces": ["error", {"ignoreComments": false }],
"no-irregular-whitespace": ["error"]
},
"env": {

View File

@ -1,6 +1,13 @@
> Please send your pull requests the develop branch.
> Don't forget to add the change to CHANGELOG.md.
**Note**: Sometimes the development moves very fast. It is highly
recommended that you update your branch of `develop` before creating a
pull request to send us your changes. This makes everyone's lives
easier (including yours) and helps us out on the development team.
Thanks!
* Does the pull request solve a **related** issue?
* If so, can you reference the issue?
* What does the pull request accomplish? Use a list if needed.

15
.gitignore vendored
View File

@ -16,6 +16,9 @@ jspm_modules
.npm
.node_repl_history
# Visual Studio Code ignoramuses.
.vscode/
# Various Windows ignoramuses.
Thumbs.db
ehthumbs.db
@ -53,7 +56,7 @@ Temporary Items
# Various Magic Mirror ignoramuses and anti-ignoramuses.
# Don't ignore the node_helper nore module.
# Don't ignore the node_helper core module.
!/modules/node_helper
!/modules/node_helper/**
@ -65,3 +68,13 @@ Temporary Items
# Ignore changes to the custom css files.
/css/custom.css
# Vim
## swap
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
## diff patch
*.orig
*.rej
*.bak

View File

@ -1,7 +1,20 @@
language: node_js
node_js:
- "8"
- "7"
- "6"
- "5.1"
before_script:
- npm install grunt-cli -g
script: grunt
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
- sleep 5
script:
- grunt
- npm run test:unit
- npm run test:e2e
after_script:
- npm list
cache:
directories:
- node_modules

View File

@ -2,21 +2,195 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [2.1.1] - Unreleased
### Changed
- Installer: Use init config.js from config.js.sample.
## [2.2.2] - 2018-01-02
### Added
- Add missing `package-lock.json`.
## [2.2.1] - 2018-01-01
### Fixed
- Fixed linting errors.
## [2.2.0] - 2018-01-01
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
### Changed
- Calender week is now handled with a variable translation in order to move number language specific.
- Reverted the Electron dependency back to 1.4.15 since newer version don't seem to work on the Raspberry Pi very well.
### Added
- Add option to use [Nunjucks](https://mozilla.github.io/nunjucks/) templates in modules. (See `helloworld` module as an example.)
- Add Bulgarian translations for MagicMirror² and Alert module.
- Add graceful shutdown of modules by calling `stop` function of each `node_helper` on SIGINT before exiting.
- Link update subtext to Github diff of current version versus tracking branch.
- Add Catalan translation.
- Add ability to filter out newsfeed items based on prohibited words found in title (resolves #1071)
- Add options to truncate description support of a feed in newsfeed module
- Add reloadInterval option for particular feed in newsfeed module
- Add no-cache entries of HTTP headers in newsfeed module (fetcher)
- Add Czech translation.
- Add option for decimal symbols other than the decimal point for temperature values in both default weather modules: WeatherForecast and CurrentWeather.
### Fixed
- Fixed issue with calendar module showing more than `maximumEntries` allows
- WeatherForecast and CurrentWeather are now using HTTPS instead of HTTP
- Correcting translation for Indonesian language
- Fix issue where calendar icons wouldn't align correctly
## [2.1.3] - 2017-10-01
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
### Changed
- Remove Roboto fonts files inside `fonts` and these are installed by npm install command.
### 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 unit test the capitalizeFirstLetter function of newfeed module.
- Add new unit tests for function `shorten` in calendar module.
- Add new unit tests for function `getLocaleSpecification` in calendar module.
- Add unit test for js/class.js.
- Add unit tests for function `roundValue` in currentweather module.
- Add test e2e showWeek feature in spanish language.
- Add warning Log when is used old authentication method in the calendar module.
- Add test e2e for helloworld module with default config text.
- Add ability for `currentweather` module to display indoor humidity via INDOOR_HUMIDITY notification.
- Add Welsh (Cymraeg) translation.
- Add Slack badge to Readme.
### Updated
- Changed 'default.js' - listen on all attached interfaces by default.
- Add execution of `npm list` after the test are ran in Travis CI.
- Change hooks for the vendors e2e tests.
- Add log when clientonly failed on starting.
- Add warning color when are using full ip whitelist.
- Set version of the `express-ipfilter` on 0.3.1.
### 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.
- Fixed issue with calendar module where 'excludedEvents' count towards 'maximumEntries'.
- Fixed issue with calendar module where global configuration of maximumEntries was not overridden by calendar specific config (see module doc).
- Fixed issue where `this.file(filename)` returns a path with two hashes.
- Workaround for the WeatherForecast API limitation.
## [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.
### Added
- 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 task to check configuration file.
- 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
- Added missing keys to Polish translation.
- Added missing key to German translation.
- Added better translation with flexible word order to Finnish translation.
### Fixed
- 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
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
### Changed
- Add `anytime` group for Compliments module.
- Compliments module can use remoteFile without default daytime arrays defined.
- Installer: Use init config.js from config.js.sample.
- Switched out `rrule` package for `rrule-alt` and fixes in `ical.js` in order to fix calendar issues. ([#565](https://github.com/MichMich/MagicMirror/issues/565))
- Make mouse events pass through the region fullscreen_above to modules below.
- Scaled the splash screen down to make it a bit more subtle.
- Replace HTML tables with markdown tables in README files.
- Added `DAYAFTERTOMORROW`, `UPDATE_NOTIFICATION` and `UPDATE_NOTIFICATION_MODULE` to Finnish translations.
- Run `npm test` on Travis automatically.
- Show the splash screen image even when is reboot or halted.
- Added some missing translaton strings in the sv.json file.
- Run task jsonlint to check translation files.
- Restructured Test Suite.
### Added
- Added Docker support (Pull Request [#673](https://github.com/MichMich/MagicMirror/pull/673)).
- Calendar-specific support for `maximumEntries`, and ` maximumNumberOfDays`.
- Add loaded function to modules, providing an async callback.
- Made default newsfeed module aware of gesture events from [MMM-Gestures](https://github.com/thobach/MMM-Gestures)
- Add use pm2 for manager process into Installer RaspberryPi script
- Add use pm2 for manager process into Installer RaspberryPi script.
- Russian Translation.
- Afrikaans Translation.
- Add postinstall script to notify user that MagicMirror installed successfully despite warnings from NPM.
- Init tests using mocha.
- Option to use RegExp in Calendar's titleReplace.
- Hungarian Translation.
- Icelandic Translation.
- Add use a script to prevent when is run by SSH session set DISPLAY enviroment.
- Enable ability to set configuration file by the enviroment variable called MM_CONFIG_FILE.
- Option to give each calendar a different color.
- Option for colored min-temp and max-temp.
- Add test e2e helloworld.
- Add test e2e enviroment.
- Add `chai-as-promised` npm module to devDependencies.
- Basic set of tests for clock module.
- Run e2e test in Travis.
- Estonian Translation.
- Add test for compliments module for parts of day.
- Korean Translation.
- Added console warning on startup when deprecated config options are used.
- Add option to display temperature unit label to the current weather module.
- Added ability to disable wrapping of news items.
- Added in the ability to hide events in the calendar module based on simple string filters.
- Updated Norwegian translation.
- Added hideLoading option for News Feed module.
- Added configurable dateFormat to clock module.
- Added multiple calendar icon support.
- Added tests for Translations, dev argument, version, dev console.
- Added test anytime feature compliments module.
- Added test ipwhitelist configuration directive.
- Added test for calendar module: default, basic-auth, backward compability, fail-basic-auth.
- Added meta tags to support fullscreen mode on iOS (for server mode)
- Added `ignoreOldItems` and `ignoreOlderThan` options to the News Feed module
- Added test for MM_PORT enviroment variable.
- Added a configurable Week section to the clock module.
### Fixed
- Update .gitignore to not ignore default modules folder.
- Remove white flash on boot up.
- Added `update` in Raspberry Pi installation script.
- Fix an issue where the analog clock looked scrambled. ([#611](https://github.com/MichMich/MagicMirror/issues/611))
- If units is set to imperial, the showRainAmount option of weatherforecast will show the correct unit.
- Module currentWeather: check if temperature received from api is defined.
- Fix an issue with module hidden status changing to `true` although lock string prevented showing it.
- Fix newsfeed module bug (removeStartTags)
- Fix when is set MM_PORT enviroment variable.
- Fixed missing animation on `this.show(speed)` when module is alone in a region.
## [2.1.0] - 2016-12-31
@ -134,7 +308,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Fixed
- Added reference to Italian Translation.
- Added the missing NE translation to all languages. [#334](https://github.com/MichMich/MagicMirror/issues/344)
- Added the missing NE translation to all languages. [#344](https://github.com/MichMich/MagicMirror/issues/344)
- Added proper User-Agent string to calendar call.
### Changed

View File

@ -6,9 +6,20 @@ module.exports = function(grunt) {
options: {
configFile: ".eslintrc.json"
},
target: ["js/*.js", "modules/default/*.js", "modules/default/*/*.js",
"serveronly/*.js", "*.js", "!modules/default/alert/notificationFx.js",
"!modules/default/alert/modernizr.custom.js", "!modules/default/alert/classie.js"
target: [
"js/*.js",
"modules/default/*.js",
"modules/default/*/*.js",
"serveronly/*.js",
"*.js",
"tests/**/*.js",
"!modules/default/alert/notificationFx.js",
"!modules/default/alert/modernizr.custom.js",
"!modules/default/alert/classie.js",
"config/*",
"translations/translations.js",
"vendor/vendor.js",
"modules/node_modules/node_helper/index.js"
]
},
stylelint: {
@ -16,12 +27,26 @@ module.exports = function(grunt) {
options: {
configFile: ".stylelintrc"
},
src: ["css/main.css", "modules/default/calendar/calendar.css", "modules/default/clock/clock_styles.css", "modules/default/currentweather/currentweather.css", "modules/default/weatherforcast/weatherforcast.css"]
src: [
"css/main.css",
"modules/default/calendar/calendar.css",
"modules/default/clock/clock_styles.css",
"modules/default/currentweather/currentweather.css",
"modules/default/weatherforcast/weatherforcast.css"
]
}
},
jsonlint: {
main: {
src: ["package.json", ".eslintrc.json", ".stylelint"],
src: [
"package.json",
".eslintrc.json",
".stylelintrc",
"translations/*.json",
"modules/default/*/translations/*.json",
"installers/pm2_MagicMirror.json",
"vendor/package.js"
],
options: {
reporter: "jshint"
}
@ -53,11 +78,20 @@ module.exports = function(grunt) {
"MD038": false
}
},
src: ["README.md", "CHANGELOG.md", "LICENSE.md", "modules/README.md", "modules/default/**/*.md", "!modules/default/calendar/vendor/ical.js/readme.md"]
src: [
"README.md",
"CHANGELOG.md",
"LICENSE.md",
"modules/README.md",
"modules/default/**/*.md",
"!modules/default/calendar/vendor/ical.js/readme.md"
]
}
},
yamllint: {
all: [".travis.yml"]
all: [
".travis.yml"
]
}
});
grunt.loadNpmTasks("grunt-eslint");

View File

@ -7,6 +7,7 @@
<a href="http://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
<a href="https://travis-ci.org/MichMich/MagicMirror"><img src="https://travis-ci.org/MichMich/MagicMirror.svg" alt="Travis"></a>
<a href="https://snyk.io/test/github/MichMich/MagicMirror"><img src="https://snyk.io/test/github/MichMich/MagicMirror/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/MichMich/MagicMirror" style="max-width:100%;"></a>
<a href="http://slack.magicmirror.builders"><img src="http://slack.magicmirror.builders:3000/badge.svg" alt="Slack Status"></a>
</p>
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](http://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).
@ -19,7 +20,7 @@ MagicMirror² focuses on a modular plugin system and uses [Electron](http://elec
- [Configuration](#configuration)
- [Modules](#modules)
- [Known Issues](#known-issues)
- [community](#community)
- [Community](#community)
- [Contributing Guidelines](#contributing-guidelines)
## Usage
@ -31,13 +32,13 @@ Electron, the app wrapper around MagicMirror², only supports the Raspberry Pi 2
Execute the following command on your Raspberry Pi to install MagicMirror²:
````
curl -sL https://raw.githubusercontent.com/MichMich/MagicMirror/master/installers/raspberry.sh | bash
bash -c "$(curl -sL https://raw.githubusercontent.com/MichMich/MagicMirror/master/installers/raspberry.sh)"
````
### Manual Installation
1. Download and install the latest Node.js version.
2. Clone the repository and check out the beta branch: `git clone https://github.com/MichMich/MagicMirror`
2. Clone the repository and check out the master branch: `git clone https://github.com/MichMich/MagicMirror`
3. Enter the repository: `cd ~/MagicMirror`
4. Install and run the app: `npm install && npm start`
@ -46,8 +47,48 @@ curl -sL https://raw.githubusercontent.com/MichMich/MagicMirror/master/installer
**Note:** if you want to debug on Raspberry Pi you can use `npm start dev` which will start the MagicMirror app with Dev Tools enabled.
### 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, execute the following command from the MagicMirror folder: `node serveronly`. This will start the server, after which you can open the application in your browser of choice.
### 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
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:
```bash
docker run -d \
--publish 80:8080 \
--restart always \
--volume ~/magic_mirror/config:/opt/magic_mirror/config \
--volume ~/magic_mirror/modules:/opt/magic_mirror/modules \
--name magic_mirror \
bastilimbach/docker-magicmirror
```
| **Volumes** | **Description** |
| --- | --- |
| `/opt/magic_mirror/config` | Mount this volume to insert your own config into the docker container. |
| `/opt/magic_mirror/modules` | Mount this volume to add your own custom modules into the docker container. |
You may need to add your Docker Host IP to your `ipWhitelist` option. If you have some issues setting up this configuration, check [this forum post](https://forum.magicmirror.builders/topic/1326/ipwhitelist-howto).
```javascript
var config = {
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:172.17.0.1"]
};
```
If you want to run the server on a raspberry pi, use the `raspberry` tag. (bastilimbach/docker-magicmirror:raspberry)
#### Manual
1. Download and install the latest Node.js version.
2. Clone the repository and check out the master branch: `git clone https://github.com/MichMich/MagicMirror`
3. Enter the repository: `cd ~/MagicMirror`
4. Install and run the app: `npm install && node serveronly`
### Raspberry Configuration & Auto Start.
@ -68,23 +109,28 @@ Type `git status` to see your changes, if there are any, you can reset them with
## Configuration
1. Duplicate `config/config.js.sample` to `config/config.js`.
1. Duplicate `config/config.js.sample` to `config/config.js`. **Note:** If you used the installer script. This step is already done for you.
2. Modify your required settings.
Note: You'll can check your configuration running the follow command:
```bash
npm run config:check
```
The following properties can be configured:
| **Option** | **Description** |
| --- | --- |
| `port` | The port on which the MagicMirror² server will run on. The default value is `8080`. |
| `address` | The ip address the accept connections. The default open bind `::` is IPv6 is available or `0.0.0.0` IPv4 run on. Example config: `192.168.10.100`. |
| `ipWhitelist` | The list of IPs from which you are allowed to access the MagicMirror². The default value is `["127.0.0.1", "::ffff:127.0.0.1", "::1"]`. It is possible to specify IPs with subnet masks (`["127.0.0.1", "127.0.0.1/24"]`) or define ip ranges (`["127.0.0.1", ["192.168.0.1", "192.168.0.100"]]`).|
| `address` | The ip address the accept connections. The default open bind `localhost`. Example config: `192.168.10.100`. |
| `ipWhitelist` | The list of IPs from which you are allowed to access the MagicMirror². The default value is `["127.0.0.1", "::ffff:127.0.0.1", "::1"]`. It is possible to specify IPs with subnet masks (`["127.0.0.1", "127.0.0.1/24"]`) or define ip ranges (`["127.0.0.1", ["192.168.0.1", "192.168.0.100"]]`). Set `[]` to allow all IP addresses. For more information about how configure this directive see the [follow post ipWhitelist HowTo](https://forum.magicmirror.builders/topic/1326/ipwhitelist-howto) |
| `zoom` | This allows to scale the mirror contents with a given zoom factor. The default value is `1.0`|
| `language` | The language of the interface. (Note: Not all elements will be localized.) Possible values are `en`, `nl`, `ru`, `fr`, etc., but the default value is `en`. |
| `timeFormat` | The form of time notation that will be used. Possible values are `12` or `24`. The default is `24`. |
| `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.** |
| `electronOptions` | An optional array of Electron (browser) options. This allows configuration of e.g. the browser screen size and position (defaults `.width = 800` & `.height = 600`). Kiosk mode can be enabled by setting `.kiosk = true`, `.autoHideMenuBar = false`, `.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:

104
clientonly/index.js Normal file
View File

@ -0,0 +1,104 @@
/* 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}`);
});
child.on('close', (code) => {
if (code != 0) {
console.log(`There something wrong. The clientonly is not running code ${code}`);
}
});
})
.catch(function (reason) {
fail(`Unable to connect to server: (${reason})`);
});
} else {
fail();
}
}());

View File

@ -2,67 +2,80 @@
*
* By Michael Teeuw http://michaelteeuw.nl
* MIT Licensed.
*
* For more information how you can configurate this file
* See https://github.com/MichMich/MagicMirror#configuration
*
*/
var config = {
address: "localhost", // Address to listen on, can be:
// - "localhost", "127.0.0.1", "::1" to listen on loopback interface
// - another specific IPv4/6 to listen on a specific interface
// - "", "0.0.0.0", "::" to listen on any interface
// Default, when address config is left out, is "localhost"
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
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 :
// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.1.5"],
// or IPv4 range of 192.168.3.0 --> 192.168.3.15 use CIDR format :
// ["127.0.0.1", "::ffff:127.0.0.1", "::1", "::ffff:192.168.3.0/28"],
language: 'en',
language: "en",
timeFormat: 24,
units: 'metric',
units: "metric",
modules: [
{
module: 'alert',
module: "alert",
},
{
module: "updatenotification",
position: "top_bar"
},
{
module: 'clock',
position: 'top_left'
module: "clock",
position: "top_left"
},
{
module: 'calendar',
header: 'US Holidays',
position: 'top_left',
module: "calendar",
header: "US Holidays",
position: "top_left",
config: {
calendars: [
{
symbol: 'calendar-check-o ',
url: 'webcal://www.calendarlabs.com/templates/ical/US-Holidays.ics'
symbol: "calendar-check-o ",
url: "webcal://www.calendarlabs.com/templates/ical/US-Holidays.ics"
}
]
}
},
{
module: 'compliments',
position: 'lower_third'
module: "compliments",
position: "lower_third"
},
{
module: 'currentweather',
position: 'top_right',
module: "currentweather",
position: "top_right",
config: {
location: 'New York',
locationID: '', //ID from http://www.openweathermap.org
appid: 'YOUR_OPENWEATHER_API_KEY'
location: "New York",
locationID: "", //ID from http://www.openweathermap.org/help/city_list.txt
appid: "YOUR_OPENWEATHER_API_KEY"
}
},
{
module: 'weatherforecast',
position: 'top_right',
header: 'Weather Forecast',
module: "weatherforecast",
position: "top_right",
header: "Weather Forecast",
config: {
location: 'New York',
locationID: '5128581', //ID from http://www.openweathermap.org
appid: 'YOUR_OPENWEATHER_API_KEY'
location: "New York",
locationID: "5128581", //ID from http://www.openweathermap.org/help/city_list.txt
appid: "YOUR_OPENWEATHER_API_KEY"
}
},
{
module: 'newsfeed',
position: 'bottom_bar',
module: "newsfeed",
position: "bottom_bar",
config: {
feeds: [
{
@ -79,4 +92,4 @@ var config = {
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== 'undefined') {module.exports = config;}
if (typeof module !== "undefined") {module.exports = config;}

View File

@ -1,6 +1,7 @@
html {
cursor: none;
overflow: hidden;
background: #000;
}
::-webkit-scrollbar {
@ -94,7 +95,7 @@ body {
header {
text-transform: uppercase;
font-size: 15px;
font-family: "Roboto Condensed";
font-family: "Roboto Condensed", Arial, Helvetica, sans-serif;
font-weight: 400;
border-bottom: 1px solid #666;
line-height: 15px;
@ -121,6 +122,12 @@ sup {
margin-bottom: 0;
}
.no-wrap {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/**
* Region Definitions.
*/
@ -135,10 +142,16 @@ sup {
left: -60px;
right: -60px;
bottom: -60px;
pointer-events: none;
}
.region.fullscreen * {
pointer-events: auto;
}
.region.right {
right: 0;
text-align: right;
}
.region.top {
@ -149,6 +162,10 @@ sup {
margin-bottom: 25px;
}
.region.bottom .container {
margin-top: 25px;
}
.region.top .container:empty {
margin-bottom: 0;
}
@ -173,10 +190,6 @@ sup {
bottom: 0;
}
.region.bottom .container {
margin-top: 25px;
}
.region.bottom .container:empty {
margin-top: 0;
}
@ -219,10 +232,6 @@ sup {
text-align: left;
}
.region.right {
text-align: right;
}
.region table {
width: 100%;
border-spacing: 0;

View File

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

12
fonts/package-lock.json generated Normal file
View File

@ -0,0 +1,12 @@
{
"name": "magicmirror-fonts",
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"roboto-fontface": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.8.0.tgz",
"integrity": "sha512-ZYzRkETgBrdEGzL5JSKimvjI2CX7ioyZCkX2BpcfyjqI+079W0wHAyj5W4rIZMcDSOHgLZtgz1IdDi/vU77KEQ=="
}
}
}

15
fonts/package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "magicmirror-fonts",
"description": "Package for fonts use by MagicMirror Core.",
"repository": {
"type": "git",
"url": "git+https://github.com/MichMich/MagicMirror.git"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/MichMich/MagicMirror/issues"
},
"dependencies": {
"roboto-fontface": "^0.8.0"
}
}

View File

@ -5,9 +5,9 @@
src:
local("Roboto Thin"),
local("Roboto-Thin"),
url("Roboto-Thin/Roboto-Thin.woff2") format("woff2"),
url("Roboto-Thin/Roboto-Thin.woff") format("woff"),
url("Roboto-Thin/Roboto-Thin.ttf") format("truetype");
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff2") format("woff2"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff") format("woff"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.ttf") format("truetype");
}
@font-face {
@ -17,9 +17,9 @@
src:
local("Roboto Condensed Light"),
local("RobotoCondensed-Light"),
url("RobotoCondensed-Light/RobotoCondensed-Light.woff2") format("woff2"),
url("RobotoCondensed-Light/RobotoCondensed-Light.woff") format("woff"),
url("RobotoCondensed-Light/RobotoCondensed-Light.ttf") format("truetype");
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff2") format("woff2"),
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.woff") format("woff"),
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Light.ttf") format("truetype");
}
@font-face {
@ -29,9 +29,9 @@
src:
local("Roboto Condensed"),
local("RobotoCondensed-Regular"),
url("RobotoCondensed-Regular/RobotoCondensed-Regular.woff2") format("woff2"),
url("RobotoCondensed-Regular/RobotoCondensed-Regular.woff") format("woff"),
url("RobotoCondensed-Regular/RobotoCondensed-Regular.ttf") format("truetype");
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff2") format("woff2"),
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.woff") format("woff"),
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Regular.ttf") format("truetype");
}
@font-face {
@ -41,9 +41,9 @@
src:
local("Roboto Condensed Bold"),
local("RobotoCondensed-Bold"),
url("RobotoCondensed-Bold/RobotoCondensed-Bold.woff2") format("woff2"),
url("RobotoCondensed-Bold/RobotoCondensed-Bold.woff") format("woff"),
url("RobotoCondensed-Bold/RobotoCondensed-Bold.ttf") format("truetype");
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff2") format("woff2"),
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.woff") format("woff"),
url("node_modules/roboto-fontface/fonts/roboto-condensed/Roboto-Condensed-Bold.ttf") format("truetype");
}
@font-face {
@ -53,9 +53,9 @@
src:
local("Roboto"),
local("Roboto-Regular"),
url("Roboto-Regular/Roboto-Regular.woff2") format("woff2"),
url("Roboto-Regular/Roboto-Regular.woff") format("woff"),
url("Roboto-Regular/Roboto-Regular.ttf") format("truetype");
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff2") format("woff2"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff") format("woff"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.ttf") format("truetype");
}
@font-face {
@ -65,9 +65,9 @@
src:
local("Roboto Medium"),
local("Roboto-Medium"),
url("Roboto-Medium/Roboto-Medium.woff2") format("woff2"),
url("Roboto-Medium/Roboto-Medium.woff") format("woff"),
url("Roboto-Medium/Roboto-Medium.ttf") format("truetype");
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff2") format("woff2"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff") format("woff"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.ttf") format("truetype");
}
@font-face {
@ -77,9 +77,9 @@
src:
local("Roboto Bold"),
local("Roboto-Bold"),
url("Roboto-Bold/Roboto-Bold.woff2") format("woff2"),
url("Roboto-Bold/Roboto-Bold.woff") format("woff"),
url("Roboto-Bold/Roboto-Bold.ttf") format("truetype");
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff2") format("woff2"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff") format("woff"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.ttf") format("truetype");
}
@font-face {
@ -89,7 +89,7 @@
src:
local("Roboto Light"),
local("Roboto-Light"),
url("Roboto-Light/Roboto-Light.woff2") format("woff2"),
url("Roboto-Light/Roboto-Light.woff") format("woff"),
url("Roboto-Light/Roboto-Light.ttf") format("truetype");
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff2") format("woff2"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff") format("woff"),
url("node_modules/roboto-fontface/fonts/roboto/Roboto-Light.ttf") format("truetype");
}

View File

@ -1,9 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Magic Mirror</title>
<title>MagicMirror²</title>
<meta name="google" content="notranslate" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="format-detection" content="telephone=no">
<meta name="mobile-web-app-capable" content="yes">
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<link rel="stylesheet" type="text/css" href="css/main.css">
<link rel="stylesheet" type="text/css" href="fonts/roboto.css">
@ -32,8 +38,9 @@
</div>
<div class="region fullscreen above"><div class="container"></div></div>
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
<script type="text/javascript" src="vendor/node_modules/nunjucks/browser/nunjucks.min.js"></script>
<script type="text/javascript" src="js/defaults.js"></script>
<script type="text/javascript" src="config/config.js"></script>
<script type="text/javascript" src="#CONFIG_FILE#"></script>
<script type="text/javascript" src="vendor/vendor.js"></script>
<script type="text/javascript" src="modules/default/defaultmodules.js"></script>
<script type="text/javascript" src="js/logger.js"></script>

View File

@ -1,7 +1,7 @@
{
apps : [{
name : "MagicMirror",
script : "/home/pi/MagicMirror/installer/mm.sh",
watch : ["/home/pi/MagicMirror/config/config.js"]
"apps" : [{
"name" : "MagicMirror",
"script" : "/home/pi/MagicMirror/installers/mm.sh",
"watch" : ["/home/pi/MagicMirror/config/config.js"]
}]
}

View File

@ -0,0 +1,2 @@
echo "\033[32mMagicMirror installation successful!"
exit 0

View File

@ -32,7 +32,7 @@ if [ "$ARM" != "armv7l" ]; then
exit;
fi
#define helper methods.
# Define helper methods.
function version_gt() { test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" != "$1"; }
function command_exists () { type "$1" &> /dev/null ;}
@ -65,7 +65,7 @@ if command_exists node; then
fi
else
echo -e "\e[92mNo Node.js upgrade nessecery.\e[0m"
echo -e "\e[92mNo Node.js upgrade necessary.\e[0m"
fi
else
@ -88,7 +88,7 @@ if $NODE_INSTALL; then
echo -e "\e[92mNode.js installation Done!\e[0m"
fi
#Install magic mirror
# Install MagicMirror
cd ~
if [ -d "$HOME/MagicMirror" ] ; then
echo -e "\e[93mIt seems like MagicMirror is already installed."
@ -150,8 +150,7 @@ fi
# Use pm2 control like a service MagicMirror
read -p "Do you want use pm2 for auto starting of your MagicMirror (y/n)?" choice
if [[ $choice =~ ^[Yy]$ ]]
then
if [[ $choice =~ ^[Yy]$ ]]; then
sudo npm install -g pm2
sudo su -c "env PATH=$PATH:/usr/bin pm2 startup linux -u pi --hp /home/pi"
pm2 start ~/MagicMirror/installers/pm2_MagicMirror.json

View File

@ -7,6 +7,7 @@
var fs = require("fs");
var Server = require(__dirname + "/server.js");
var Utils = require(__dirname + "/utils.js");
var defaultModules = require(__dirname + "/../modules/default/defaultmodules.js");
var path = require("path");
@ -17,6 +18,16 @@ console.log("Starting MagicMirror: v" + global.version);
// global absolute root path
global.root_path = path.resolve(__dirname + "/../");
if (process.env.MM_CONFIG_FILE) {
global.configuration_file = process.env.MM_CONFIG_FILE;
}
// FIXME: Hotfix Pull Request
// https://github.com/MichMich/MagicMirror/pull/673
if (process.env.MM_PORT) {
global.mmPort = process.env.MM_PORT;
}
// The next part is here to prevent a major exception when there
// is no internet connection. This could probable be solved better.
process.on("uncaughtException", function (err) {
@ -41,26 +52,52 @@ var App = function() {
var loadConfig = function(callback) {
console.log("Loading config ...");
var defaults = require(__dirname + "/defaults.js");
// For this check proposed to TestSuite
// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8
var configFilename = path.resolve(global.root_path + "/config/config.js");
if (typeof(global.configuration_file) !== "undefined") {
configFilename = path.resolve(global.configuration_file);
}
try {
fs.accessSync(configFilename, fs.F_OK);
var c = require(configFilename);
checkDeprecatedOptions(c);
var config = Object.assign(defaults, c);
callback(config);
} catch (e) {
if (e.code == "ENOENT") {
console.error("WARNING! Could not find config file. Please create one. Starting with default configuration.");
callback(defaults);
console.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
} else if (e instanceof ReferenceError || e instanceof SyntaxError) {
console.error("WARNING! Could not validate config file. Please correct syntax errors. Starting with default configuration.");
callback(defaults);
console.error(Utils.colors.error("WARNING! Could not validate config file. Please correct syntax errors. Starting with default configuration."));
} else {
console.error("WARNING! Could not load config file. Starting with default configuration. Error found: " + e);
callback(defaults);
console.error(Utils.colors.error("WARNING! Could not load config file. Starting with default configuration. Error found: " + e));
}
callback(defaults);
}
};
var checkDeprecatedOptions = function(userConfig) {
var deprecated = require(global.root_path + "/js/deprecated.js");
var deprecatedOptions = deprecated.configs;
var usedDeprecated = [];
deprecatedOptions.forEach(function(option) {
if (userConfig.hasOwnProperty(option)) {
usedDeprecated.push(option);
}
});
if (usedDeprecated.length > 0) {
console.warn(Utils.colors.warn(
"WARNING! Your config is using deprecated options: " +
usedDeprecated.join(", ") +
". Check README and CHANGELOG for more up-to-date ways of getting the same functionality.")
);
}
}
/* loadModule(module)
* Loads a specific module.
*
@ -199,6 +236,33 @@ var App = function() {
});
});
};
/* stop()
* This methods stops the core app.
* This calls each node_helper's STOP() function, if it exists.
* Added to fix #1056
*/
this.stop = function() {
for (var h in nodeHelpers) {
var nodeHelper = nodeHelpers[h];
if (typeof nodeHelper.stop === "function") {
nodeHelper.stop();
}
}
};
/* Listen for SIGINT signal and call stop() function.
*
* Added to fix #1056
* Note: this is only used if running `server-only`. Otherwise
* this.stop() is called by app.on("before-quit"... in `electron.js`
*/
process.on("SIGINT", () => {
console.log("[SIGINT] Received. Shutting down server...");
setTimeout(() => { process.exit(0); }, 3000); // Force quit after 3 seconds
this.stop();
process.exit(0);
});
};
module.exports = new App();

View File

@ -90,4 +90,9 @@ function cloneObject(obj) {
}
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = Class;}
if (typeof module !== "undefined") {
module.exports = Class;
module.exports._test = {
cloneObject: cloneObject
}
}

View File

@ -7,8 +7,14 @@
* MIT Licensed.
*/
var port = 8080;
var address = "localhost";
if (typeof(mmPort) !== "undefined") {
port = mmPort;
}
var defaults = {
port: 8080,
address: address,
port: port,
kioskmode: false,
electronOptions: {},
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
@ -17,6 +23,7 @@ var defaults = {
timeFormat: 24,
units: "metric",
zoom: 1,
customCss: "css/custom.css",
modules: [
{

14
js/deprecated.js Normal file
View File

@ -0,0 +1,14 @@
/* Magic Mirror Deprecated Config Options List
*
* By Michael Teeuw http://michaelteeuw.nl
* MIT Licensed.
*
* Olex S. original idea this deprecated option
*/
var deprecated = {
configs: ["kioskmode"],
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = deprecated;}

View File

@ -2,12 +2,11 @@
"use strict";
const Server = require(__dirname + "/server.js");
const electron = require("electron");
const core = require(__dirname + "/app.js");
// Config
var config = {};
var config = process.env.config ? JSON.parse(process.env.config) : {};
// Module to control application life.
const app = electron.app;
// Module to create native browser window.
@ -18,7 +17,6 @@ const BrowserWindow = electron.BrowserWindow;
let mainWindow;
function createWindow() {
var electronOptionsDefaults = {
width: 800,
height: 600,
@ -30,7 +28,7 @@ function createWindow() {
zoomFactor: config.zoom
},
backgroundColor: "#000000"
}
};
// DEPRECATED: "kioskmode" backwards compatibility, to be removed
// settings these options directly instead provides cleaner interface
@ -47,11 +45,12 @@ function createWindow() {
mainWindow = new BrowserWindow(electronOptions);
// and load the index.html of the app.
//mainWindow.loadURL('file://' + __dirname + '../../index.html');
mainWindow.loadURL("http://localhost:" + config.port);
// If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
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"
if(process.argv[2] == "dev") {
if (process.argv.includes("dev")) {
mainWindow.webContents.openDevTools();
}
@ -97,8 +96,24 @@ app.on("activate", function() {
}
});
// Start the core application.
/* This method will be called when SIGINT is received and will call
* each node_helper's stop function if it exists. Added to fix #1056
*
* Note: this is only used if running Electron. Otherwise
* core.stop() is called by process.on("SIGINT"... in `app.js`
*/
app.on("before-quit", (event) => {
console.log("Shutting down server...");
event.preventDefault();
setTimeout(() => { process.exit(0); }, 3000); // Force-quit after 3 seconds.
core.stop();
process.exit(0);
});
// Start the core application if server is run on localhost
// 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) {
config = c;
});
}

View File

@ -32,10 +32,10 @@ var Loader = (function() {
});
} else {
// All modules loaded. Load custom.css
// This is done after all the moduels so we can
// overwrite all the defined styls.
// This is done after all the modules so we can
// overwrite all the defined styles.
loadFile("css/custom.css", function() {
loadFile(config.customCss, function() {
// custom.css loaded. Start all modules.
startModules();
});
@ -186,6 +186,11 @@ var Loader = (function() {
script.onload = function() {
if (typeof callback === "function") {callback();}
};
script.onerror = function() {
console.error("Error on loading script:", fileName);
if (typeof callback === "function") {callback();}
};
document.getElementsByTagName("body")[0].appendChild(script);
break;
case "css":
@ -197,6 +202,11 @@ var Loader = (function() {
stylesheet.onload = function() {
if (typeof callback === "function") {callback();}
};
stylesheet.onerror = function() {
console.error("Error on loading stylesheet:", fileName);
if (typeof callback === "function") {callback();}
};
document.getElementsByTagName("head")[0].appendChild(stylesheet);
break;
}

View File

@ -165,8 +165,6 @@ var MM = (function() {
if( headerWrapper.length > 0 && newHeader) {
headerWrapper[0].innerHTML = newHeader;
}
};
/* hideModule(module, speed, callback)
@ -219,7 +217,7 @@ var MM = (function() {
// remove lockString if set in options.
if (options.lockString) {
var index = module.lockStrings.indexOf(options.lockString)
var index = module.lockStrings.indexOf(options.lockString);
if ( index !== -1) {
module.lockStrings.splice(index, 1);
}
@ -232,6 +230,8 @@ var MM = (function() {
return;
}
module.hidden = false;
// If forced show, clean current lockstrings.
if (module.lockStrings.length !== 0 && options.force === true) {
Log.log("Force show of module: " + module.name);
@ -243,10 +243,13 @@ var MM = (function() {
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
// Restore the postition. See hideModule() for more info.
moduleWrapper.style.position = "static";
moduleWrapper.style.opacity = 1;
updateWrapperStates();
// Waiting for DOM-changes done in updateWrapperStates before we can start the animation.
var dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
moduleWrapper.style.opacity = 1;
clearTimeout(module.showHideTimer);
module.showHideTimer = setTimeout(function() {
if (typeof callback === "function") { callback(); }
@ -306,43 +309,36 @@ var MM = (function() {
var setSelectionMethodsForModules = function(modules) {
/* withClass(className)
* filters a collection of modules based on classname(s).
* calls modulesByClass to filter modules with the specified classes.
*
* argument className string/array - one or multiple classnames. (array or space divided)
*
* return array - Filtered collection of modules.
*/
var withClass = function(className) {
var searchClasses = className;
if (typeof className === "string") {
searchClasses = className.split(" ");
}
var newModules = modules.filter(function(module) {
var classes = module.data.classes.toLowerCase().split(" ");
for (var c in searchClasses) {
var searchClass = searchClasses[c];
if (classes.indexOf(searchClass.toLowerCase()) !== -1) {
return true;
}
}
return false;
});
setSelectionMethodsForModules(newModules);
return newModules;
return modulesByClass(className, true);
};
/* exceptWithClass(className)
* filters a collection of modules based on classname(s). (NOT)
* calls modulesByClass to filter modules without the specified classes.
*
* argument className string/array - one or multiple classnames. (array or space divided)
*
* return array - Filtered collection of modules.
*/
var exceptWithClass = function(className) {
return modulesByClass(className, false);
};
/* modulesByClass(className, include)
* filters a collection of modules based on classname(s).
*
* argument className string/array - one or multiple classnames. (array or space divided)
* argument include boolean - if the filter should include or exclude the modules with the specific classes.
*
* return array - Filtered collection of modules.
*/
var modulesByClass = function(className, include) {
var searchClasses = className;
if (typeof className === "string") {
searchClasses = className.split(" ");
@ -354,11 +350,11 @@ var MM = (function() {
for (var c in searchClasses) {
var searchClass = searchClasses[c];
if (classes.indexOf(searchClass.toLowerCase()) !== -1) {
return false;
return include;
}
}
return true;
return !include;
});
setSelectionMethodsForModules(newModules);
@ -504,7 +500,7 @@ var MM = (function() {
* argument options object - Optional settings for the hide method.
*/
showModule: function(module, speed, callback, options) {
module.hidden = false;
// do not change module.hidden yet, only if we really show it later
showModule(module, speed, callback, options);
}
};

View File

@ -27,6 +27,11 @@ var Module = Class.extend({
// visibility when hiding and showing module.
lockStrings: [],
// Storage of the nunjuck Environment,
// This should not be referenced directly.
// Use the nunjucksEnvironment() to get it.
_nunjucksEnvironment: null,
/* init()
* Is called when the module is instantiated.
*/
@ -70,23 +75,35 @@ var Module = Class.extend({
/* getDom()
* This method generates the dom which needs to be displayed. This method is called by the Magic Mirror core.
* This method needs to be subclassed if the module wants to display info on the mirror.
* This method can to be subclassed if the module wants to display info on the mirror.
* Alternatively, the getTemplete method could be subclassed.
*
* return domobject - The dom to display.
*/
getDom: function () {
var nameWrapper = document.createElement("div");
var name = document.createTextNode(this.name);
nameWrapper.appendChild(name);
var identifierWrapper = document.createElement("div");
var identifier = document.createTextNode(this.identifier);
identifierWrapper.appendChild(identifier);
identifierWrapper.className = "small dimmed";
var div = document.createElement("div");
div.appendChild(nameWrapper);
div.appendChild(identifierWrapper);
var template = this.getTemplate();
var templateData = this.getTemplateData();
// Check to see if we need to render a template string or a file.
if (/^.*((\.html)|(\.njk))$/.test(template)) {
// the template is a filename
this.nunjucksEnvironment().render(template, templateData, function (err, res) {
if (err) {
Log.error(err)
}
// The inner content of the div will be set after the template is received.
// This isn't the most optimal way, but since it's near instant
// it probably won't be an issue.
// If it gives problems, we can always add a way to pre fetch the templates.
// Let's not over optimise this ... KISS! :)
div.innerHTML = res;
});
} else {
// the template is a template string.
div.innerHTML = this.nunjucksEnvironment().renderString(template, templateData);
}
return div;
},
@ -102,6 +119,28 @@ var Module = Class.extend({
return this.data.header;
},
/* getTemplate()
* This method returns the template for the module which is used by the default getDom implementation.
* This method needs to be subclassed if the module wants to use a tempate.
* It can either return a template sting, or a template filename.
* If the string ends with '.html' it's considered a file from within the module's folder.
*
* return string - The template string of filename.
*/
getTemplate: function () {
return "<div class=\"normal\">" + this.name + "</div><div class=\"small dimmed\">" + this.identifier + "</div>";
},
/* getTemplateData()
* This method returns the data to be used in the template.
* This method needs to be subclassed if the module wants to use a custom data.
*
* return Object
*/
getTemplateData: function () {
return {}
},
/* notificationReceived(notification, payload, sender)
* This method is called when a notification arrives.
* This method is called by the Magic Mirror core.
@ -118,6 +157,30 @@ var Module = Class.extend({
}
},
/** nunjucksEnvironment()
* Returns the nunjucks environment for the current module.
* The environment is checked in the _nunjucksEnvironment instance variable.
* @returns Nunjucks Environment
*/
nunjucksEnvironment: function() {
if (this._nunjucksEnvironment != null) {
return this._nunjucksEnvironment;
}
var self = this;
this._nunjucksEnvironment = new nunjucks.Environment(new nunjucks.WebLoader(this.file(""), {async: true}), {
trimBlocks: true,
lstripBlocks: true
});
this._nunjucksEnvironment.addFilter("translate", function(str) {
return self.translate(str)
});
return this._nunjucksEnvironment;
},
/* socketNotificationReceived(notification, payload)
* This method is called when a socket notification arrives.
*
@ -194,7 +257,7 @@ var Module = Class.extend({
* return string - File path.
*/
file: function (file) {
return this.data.path + "/" + file;
return (this.data.path + "/" + file).replace("//", "/");
},
/* loadStyles()
@ -203,22 +266,7 @@ var Module = Class.extend({
* argument callback function - Function called when done.
*/
loadStyles: function (callback) {
var self = this;
var styles = this.getStyles();
var loadNextStyle = function () {
if (styles.length > 0) {
var nextStyle = styles[0];
Loader.loadFile(nextStyle, self, function () {
styles = styles.slice(1);
loadNextStyle();
});
} else {
callback();
}
};
loadNextStyle();
this.loadDependencies("getStyles", callback);
},
/* loadScripts()
@ -227,22 +275,32 @@ var Module = Class.extend({
* argument callback function - Function called when done.
*/
loadScripts: function (callback) {
var self = this;
var scripts = this.getScripts();
this.loadDependencies("getScripts", callback);
},
var loadNextScript = function () {
if (scripts.length > 0) {
var nextScript = scripts[0];
Loader.loadFile(nextScript, self, function () {
scripts = scripts.slice(1);
loadNextScript();
/* loadDependencies(funcName, callback)
* Helper method to load all dependencies.
*
* argument funcName string - Function name to call to get scripts or styles.
* argument callback function - Function called when done.
*/
loadDependencies: function (funcName, callback) {
var self = this;
var dependencies = this[funcName]();
var loadNextDependency = function () {
if (dependencies.length > 0) {
var nextDependency = dependencies[0];
Loader.loadFile(nextDependency, self, function () {
dependencies = dependencies.slice(1);
loadNextDependency();
});
} else {
callback();
}
};
loadNextScript();
loadNextDependency();
},
/* loadScripts()
@ -277,14 +335,18 @@ var Module = Class.extend({
}
},
/* translate(key, defaultValue)
* Request the translation for a given key.
/* translate(key, defaultValueOrVariables, defaultValue)
* Request the translation for a given key with optional variables and default value.
*
* argument key string - The key of the string to translage
* argument defaultValue string - The default value if no translation was found. (Optional)
* argument key string - The key of the string to translate
* 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) {
return Translator.translate(this, key) || defaultValue || "";
translate: function (key, defaultValueOrVariables, defaultValue) {
if(typeof defaultValueOrVariables === "object") {
return Translator.translate(this, key, defaultValueOrVariables) || defaultValue || "";
}
return Translator.translate(this, key) || defaultValueOrVariables || "";
},
/* updateDom(speed)
@ -415,3 +477,11 @@ Module.register = function (name, moduleDefinition) {
Log.log("Module registered: " + name);
Module.definitions[name] = moduleDefinition;
};
if (typeof exports != "undefined") { // For testing purpose only
// A good a idea move the function cmpversions a helper file.
// It's used into other side.
exports._test = {
cmpVersions: cmpVersions
}
}

View File

@ -13,14 +13,25 @@ var path = require("path");
var ipfilter = require("express-ipfilter").IpFilter;
var fs = require("fs");
var helmet = require("helmet");
var Utils = require(__dirname + "/utils.js");
var Server = function(config, callback) {
console.log("Starting server op port " + config.port + " ... ");
server.listen(config.port, config.address ? config.address : null);
var port = config.port;
if (process.env.MM_PORT) {
port = process.env.MM_PORT;
}
console.log("Starting server on port " + port + " ... ");
server.listen(port, config.address ? config.address : null);
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length == 0) {
console.info(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"))
}
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) {
return next();
}
@ -31,21 +42,31 @@ var Server = function(config, callback) {
app.use(helmet());
app.use("/js", express.static(__dirname));
app.use("/config", express.static(path.resolve(global.root_path + "/config")));
app.use("/css", express.static(path.resolve(global.root_path + "/css")));
app.use("/fonts", express.static(path.resolve(global.root_path + "/fonts")));
app.use("/modules", express.static(path.resolve(global.root_path + "/modules")));
app.use("/vendor", express.static(path.resolve(global.root_path + "/vendor")));
app.use("/translations", express.static(path.resolve(global.root_path + "/translations")));
var directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
var directory;
for (var i in directories) {
directory = directories[i];
app.use(directory, express.static(path.resolve(global.root_path + directory)));
}
app.get("/version", function(req,res) {
res.send(global.version);
});
app.get("/config", function(req,res) {
res.send(config);
});
app.get("/", function(req, res) {
var html = fs.readFileSync(path.resolve(global.root_path + "/index.html"), {encoding: "utf8"});
html = html.replace("#VERSION#", global.version);
configFile = "config/config.js";
if (typeof(global.configuration_file) !== "undefined") {
configFile = global.configuration_file;
}
html = html.replace("#CONFIG_FILE#", configFile);
res.send(html);
});

View File

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

View File

@ -111,32 +111,47 @@ var Translator = (function() {
translations: {},
translationsFallback: {},
/* translate(module, key)
/* translate(module, key, variables)
* Load a translation for a given key for a given module.
*
* argument module Module - The module to load the translation for.
* 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]) {
// 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) {
// 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]) {
// 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) {
// Log.log("Got translation for " + key + " from core translation fallback.");
return this.coreTranslationsFallback[key];
return createStringFromTemplate(this.coreTranslationsFallback[key], variables);
}
return key;

19
js/utils.js Normal file
View File

@ -0,0 +1,19 @@
/* exported Utils */
/* Magic Mirror
* Utils
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var colors = require("colors/safe");
var Utils = {
colors: {
warn: colors.yellow,
error: colors.red,
info: colors.blue
}
};
if (typeof module !== "undefined") {module.exports = Utils;}

View File

@ -78,7 +78,7 @@ The data object contains additional metadata about the module instance:
#### `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.
####'requiresVersion:'
#### `requiresVersion:`
*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.
#### `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
@ -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.
##### `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
@ -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.
####'requiresVersion:'
#### `requiresVersion:`
*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.
@ -510,6 +555,17 @@ start: function() {
}
````
#### `stop()`
This method is called when the MagicMirror server receives a `SIGINT` command and is shutting down. This method should include any commands needed to close any open connections, stop any sub-processes and gracefully exit the module.
**Example:**
````javascript
stop: function() {
console.log("Shutting down MyModule");
this.connection.close();
}
````
#### `socketNotificationReceived: function(notification, payload)`
With this method, your node helper can receive notifications from your modules. When this method is called, it has 2 arguments:

View File

@ -7,7 +7,7 @@ To use this module, add it to the modules array in the config/config.js file:
```
modules: [
{
module: 'alert',
module: "alert",
config: {
// The config property is optional.
// See 'Configuration options' for more information.
@ -21,52 +21,13 @@ modules: [
The following properties can be configured:
<table width="100%">
<!-- why, markdown... -->
<thead>
<tr>
<th>Option</th>
<th width="100%">Description</th>
</tr>
<thead>
<tbody>
<tr>
<td><code>effect</code></td>
<td>The animation effect to use for notifications.<br>
<br><b>Possible values:</b> <code>scale</code> <code>slide</code> <code>genie</code> <code>jelly</code> <code>flip</code> <code>exploader</code> <code>bouncyflip</code>
<br><b>Default value:</b> <code>slide</code>
</td>
</tr>
<td><code>alert_effect</code></td>
<td>The animation effect to use for alerts.<br>
<br><b>Possible values:</b> <code>scale</code> <code>slide</code> <code>genie</code> <code>jelly</code> <code>flip</code> <code>exploader</code> <code>bouncyflip</code>
<br><b>Default value:</b> <code>jelly</code>
</td>
</tr>
<tr>
<td><code>display_time</code></td>
<td>Time a notification is displayed in milliseconds.<br>
<br><b>Possible values:</b> <code>int</code>
<br><b>Default value:</b> <code>3500</code>
</td>
</tr>
<tr>
<tr>
<td><code>position</code></td>
<td>Position where the notifications should be displayed.<br>
<br><b>Possible values:</b> <code>left</code> <code>center</code> <code>right</code>
<br><b>Default value:</b> <code>center</code>
</td>
</tr>
<tr>
<td><code>welcome_message</code></td>
<td>Message shown at startup.<br>
<br><b>Possible values:</b> <code>string</code> <code>false</code>
<br><b>Default value:</b> <code>false</code> (no message at startup)
</td>
</tr>
</tbody>
</table>
| Option | Description
| ----------------- | -----------
| `effect` | The animation effect to use for notifications. <br><br> **Possible values:** `scale` `slide` `genie` `jelly` `flip` `exploader` `bouncyflip` <br> **Default value:** `slide`
| `alert_effect` | The animation effect to use for alerts. <br><br> **Possible values:** `scale` `slide` `genie` `jelly` `flip` `exploader` `bouncyflip` <br> **Default value:** `jelly`
| `display_time` | Time a notification is displayed in milliseconds. <br><br> **Possible values:** `int` <br> **Default value:** `3500`
| `position` | Position where the notifications should be displayed. <br><br> **Possible values:** `left` `center` `right` <br> **Default value:** `center`
| `welcome_message` | Message shown at startup. <br><br> **Possible values:** `string` `false` <br> **Default value:** `false` (no message at startup)
## Developer notes
@ -82,83 +43,21 @@ self.sendNotification("SHOW_ALERT", {});
```
### Notification params
<table width="100%">
<!-- why, markdown... -->
<thead>
<tr>
<th>Option</th>
<th width="100%">Description</th>
</tr>
<thead>
<tbody>
<tr>
<td><code>title</code></td>
<td>The title of the notification.<br>
<br><b>Possible values:</b> <code>text</code> or <code>html</code>
</td>
</tr>
<tr>
<td><code>message</code></td>
<td>The message of the notification.<br>
<br><b>Possible values:</b> <code>text</code> or <code>html</code>
</td>
</tr>
</tbody>
</table>
| Option | Description
| --------- | -----------
| `title` | The title of the notification. <br><br> **Possible values:** `text` or `html`
| `message` | The message of the notification. <br><br> **Possible values:** `text` or `html`
### Alert params
<table width="100%">
<!-- why, markdown... -->
<thead>
<tr>
<th>Option</th>
<th width="100%">Description</th>
</tr>
<thead>
<tbody>
<tr>
<td><code>title</code></td>
<td>The title of the alert.<br>
<br><b>Possible values:</b> <code>text</code> or <code>html</code>
</td>
</tr>
<tr>
<td><code>message</code></td>
<td>The message of the alert.<br>
<br><b>Possible values:</b> <code>text</code> or <code>html</code>
</td>
</tr>
<tr>
<td><code>imageUrl</code> (optional)</td>
<td>Image to show in the alert<br>
<br><b>Possible values:</b> <code>url</code> <code>path</code>
<br><b>Default value:</b> <code>none</code>
</td>
</tr>
<tr>
<td><code>imageFA</code> (optional)</td>
<td>Font Awesome icon to show in the alert<br>
<br><b>Possible values:</b> See <a href="http://fontawesome.io/icons/" target="_blank">Font Awsome</a> website.
<br><b>Default value:</b> <code>none</code>
</td>
</tr>
<tr>
<td><code>imageHeight</code> (optional even with imageUrl set)</td>
<td>Height of the image<br>
<br><b>Possible values:</b> <code>intpx</code>
<br><b>Default value:</b> <code>80px</code>
</td>
</tr>
<tr>
<td><code>timer</code> (optional)</td>
<td>How long the alert should stay visible in ms.
<br><b>Important:</b> If you do not use the <code>timer</code>, it is your duty to hide the alert by using <code>self.sendNotification("HIDE_ALERT");</code>!<br>
<br><b>Possible values:</b> <code>int</code> <code>float</code>
<br><b>Default value:</b> <code>none</code>
</td>
</tr>
</tbody>
</table>
| Option | Description
| ----------------------------------------------- | -----------
| `title` | The title of the alert. <br><br> **Possible values:** `text` or `html`
| `message` | The message of the alert. <br><br> **Possible values:** `text` or `html`
| `imageUrl` (optional) | Image to show in the alert <br><br> **Possible values:** `url` `path` <br> **Default value:** `none`
| `imageFA` (optional) | Font Awesome icon to show in the alert <br><br> **Possible values:** See [Font Awsome](http://fontawesome.io/icons/) website. <br> **Default value:** `none`
| `imageHeight` (optional even with imageUrl set) | Height of the image <br><br> **Possible values:** `intpx` <br> **Default value:** `80px`
| `timer` (optional) | How long the alert should stay visible in ms. <br> **Important:** If you do not use the `timer`, it is your duty to hide the alert by using `self.sendNotification("HIDE_ALERT");`! <br><br>**Possible values:** `int` `float` <br> **Default value:** `none`
## Open Source Licenses
###[NotificationStyles](https://github.com/codrops/NotificationStyles)

View File

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

View File

@ -0,0 +1,4 @@
{
"sysTitle": "MagicMirror нотификация",
"welcome": "Добре дошли, стартирането беше успешно"
}

View File

@ -0,0 +1,4 @@
{
"sysTitle": "MagicMirror értesítés",
"welcome": "Üdvözöljük, indulás sikeres!"
}

View File

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

View File

@ -0,0 +1,4 @@
{
"sysTitle": "MagicMirror Уведомление",
"welcome": "Добро пожаловать, старт был успешным!"
}

View File

@ -8,8 +8,8 @@ To use this module, add it to the modules array in the `config/config.js` file:
````javascript
modules: [
{
module: 'calendar',
position: 'top_left', // This can be any of the regions. Best results in left or right regions.
module: "calendar",
position: "top_left", // This can be any of the regions. Best results in left or right regions.
config: {
// The config property is optional.
// If no config is set, an example calendar is shown.
@ -24,206 +24,68 @@ modules: [
The following properties can be configured:
<table width="100%">
<!-- why, markdown... -->
<thead>
<tr>
<th>Option</th>
<th width="100%">Description</th>
</tr>
<thead>
<tbody>
<tr>
<td><code>maximumEntries</code></td>
<td>The maximum number of events shown.<br>
<br><b>Possible values:</b> <code>0</code> - <code>100</code>
<br><b>Default value:</b> <code>10</code>
</td>
</tr>
<tr>
<td><code>maximumNumberOfDays</code></td>
<td>The maximum number of days in the future.<br>
<br><b>Default value:</b> <code>365</code>
</td>
</tr>
<tr>
<td><code>displaySymbol</code></td>
<td>Display a symbol in front of an entry.<br>
<br><b>Possible values:</b> <code>true</code> or <code>false</code>
<br><b>Default value:</b> <code>true</code>
</td>
</tr>
<tr>
<td><code>defaultSymbol</code></td>
<td>The default symbol.<br>
<br><b>Possible values:</b> See <a href="http://fontawesome.io/icons/" target="_blank">Font Awsome</a> website.
<br><b>Default value:</b> <code>calendar</code>
</td>
</tr>
<tr>
<td><code>maxTitleLength</code></td>
<td>The maximum title length.<br>
<br><b>Possible values:</b> <code>10</code> - <code>50</code>
<br><b>Default value:</b> <code>25</code>
</td>
</tr>
<tr>
<td><code>fetchInterval</code></td>
<td>How often does the content needs to be fetched? (Milliseconds)<br>
<br><b>Possible values:</b> <code>1000</code> - <code>86400000</code>
<br><b>Default value:</b> <code>300000</code> (5 minutes)
</td>
</tr>
<tr>
<td><code>animationSpeed</code></td>
<td>Speed of the update animation. (Milliseconds)<br>
<br><b>Possible values:</b><code>0</code> - <code>5000</code>
<br><b>Default value:</b> <code>2000</code> (2 seconds)
</td>
</tr>
<tr>
<td><code>fade</code></td>
<td>Fade the future events to black. (Gradient)<br>
<br><b>Possible values:</b> <code>true</code> or <code>false</code>
<br><b>Default value:</b> <code>true</code>
</td>
</tr>
<tr>
<td><code>fadePoint</code></td>
<td>Where to start fade?<br>
<br><b>Possible values:</b> <code>0</code> (top of the list) - <code>1</code> (bottom of list)
<br><b>Default value:</b> <code>0.25</code>
</td>
</tr>
<tr>
<td><code>calendars</code></td>
<td>The list of calendars.<br>
<br><b>Possible values:</b> An array, see <i>calendar configuration</i> below.
<br><b>Default value:</b> <i>An example calendar.</i>
</td>
</tr>
<tr>
<td><code>titleReplace</code></td>
<td>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><b>Example:</b> <br>
<code>
titleReplace: {'Birthday of ' : '', 'foo':'bar'}
</code>
<br><b>Default value:</b>
<code>
{
"De verjaardag van ": "",
"'s birthday": ""
}
</code>
</td>
</tr>
<tr>
<td><code>displayRepeatingCountTitle</code></td>
<td>Show count title for yearly repeating events (e.g. "X. Birthday", "X. Anniversary")<br>
<br><b>Possible values:</b> <code>true</code> or <code>false</code>
<br><b>Default value:</b> <code>false</code>
</td>
</tr>
<tr>
<td><code>dateFormat</code></td>
<td>Format to use for the date of events (when using absolute dates)<br>
<br><b>Possible values:</b> See <a href="http://momentjs.com/docs/#/parsing/string-format/">Moment.js formats</a>
<br><b>Default value:</b> <code>MMM Do</code> (e.g. Jan 18th)
</td>
</tr>
<tr>
<td><code>timeFormat</code></td>
<td>Display event times as absolute dates, or relative time<br>
<br><b>Possible values:</b> <code>absolute</code> or <code>relative</code>
<br><b>Default value:</b> <code>relative</code>
</td>
</tr>
<tr>
<td><code>getRelative</code></td>
<td>How much time (in hours) should be left until calendar events start getting relative?<br>
<br><b>Possible values:</b> <code>0</code> (events stay absolute) - <code>48</code> (48 hours before the event starts)
<br><b>Default value:</b> <code>6</code>
</td>
</tr>
<tr>
<td><code>urgency</code></td>
<td>When using a timeFormat of <code>absolute</code>, the <code>urgency</code> setting allows you to display events within a specific time frame as <code>relative</code>
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><b>Possible values:</b> a positive integer representing the number of days for which you want a relative date, for example <code>7</code> (for 7 days)<br>
<br><b>Default value:</b> <code>7</code>
</td>
</tr>
<tr>
<td><code>broadcastEvents</code></td>
<td>If this property is set to true, the calendar will broadcast all the events to all other modules with the notification message: <code>CALENDAR_EVENTS</code>. The event objects are stored in an array and contain the following fields: <code>title</code>, <code>startDate</code>, <code>endDate</code>, <code>fullDayEvent</code>, <code>location</code> and <code>geo</code>.<br>
<br><b>Possible values:</b> <code>true</code>, <code>false</code> <br>
<br><b>Default value:</b> <code>true</code>
</td>
</tr>
<tr>
<td><code>hidePrivate</code></td>
<td>Hides private calendar events.<br>
<br><b>Possible values:</b> <code>true</code> or <code>false</code>
<br><b>Default value:</b> <code>false</code>
</td>
</tr>
</tbody>
</table>
| Option | Description
| ---------------------------- | -----------
| `maximumEntries` | The maximum number of events shown. / **Possible values:** `0` - `100` <br> **Default value:** `10`
| `maximumNumberOfDays` | The maximum number of days in the future. <br><br> **Default value:** `365`
| `displaySymbol` | Display a symbol in front of an entry. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
| `defaultSymbol` | The default symbol. <br><br> **Possible values:** See [Font Awsome](http://fontawesome.io/icons/) website. <br> **Default value:** `calendar`
| `maxTitleLength` | The maximum title length. <br><br> **Possible values:** `10` - `50` <br> **Default value:** `25`
| `wrapEvents` | Wrap event titles to multiple lines. Breaks lines at the length defined by `maxTitleLength`. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
| `fetchInterval` | How often does the content needs to be fetched? (Milliseconds) <br><br> **Possible values:** `1000` - `86400000` <br> **Default value:** `300000` (5 minutes)
| `animationSpeed` | Speed of the update animation. (Milliseconds) <br><br> **Possible values:**`0` - `5000` <br> **Default value:** `2000` (2 seconds)
| `fade` | Fade the future events to black. (Gradient) <br><br> **Possible values:** `true` or `false` <br> **Default value:** `true`
| `fadePoint` | Where to start fade? <br><br> **Possible values:** `0` (top of the list) - `1` (bottom of list) <br> **Default value:** `0.25`
| `calendars` | The list of calendars. <br><br> **Possible values:** An array, see _calendar configuration_ below. <br> **Default value:** _An example calendar._
| `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`
| `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`
| `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`
| `broadcastEvents` | If this property is set to true, the calendar will broadcast all the events to all other modules with the notification message: `CALENDAR_EVENTS`. The event objects are stored in an array and contain the following fields: `title`, `startDate`, `endDate`, `fullDayEvent`, `location` and `geo`. <br><br> **Possible values:** `true`, `false` <br><br> **Default value:** `true`
| `hidePrivate` | Hides private calendar events. <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
| `excludedEvents` | An array of words / phrases from event titles that will be excluded from being shown. <br><br> **Example:** `['Birthday', 'Hide This Event']` <br> **Default value:** `[]`
### Calendar configuration
The `calendars` property contains an array of the configured calendars.
The `colored` property gives the option for an individual color for each calendar.
#### Default value:
````javascript
config: {
colored: false,
calendars: [
{
url: 'http://www.calendarlabs.com/templates/ical/US-Holidays.ics',
symbol: 'calendar',
auth: {
user: 'username',
pass: 'superstrongpassword',
method: 'basic'
}
},
],
}
````
#### Calendar configuration options:
<table width="100%">
<!-- why, markdown... -->
<thead>
<tr>
<th>Option</th>
<th width="100%">Description</th>
</tr>
<thead>
<tbody>
<tr>
<td><code>url</code></td>
<td>The url of the calendar .ical. This property is required.<br>
<br><b>Possible values:</b> Any public accessble .ical calendar.
</td>
</tr>
<tr>
<td><code>symbol</code></td>
<td>The symbol to show in front of an event. This property is optional.<br>
<br><b>Possible values:</b> See <a href="http://fontawesome.io/icons/" target="_blank">Font Awesome</a> website.
</td>
</tr>
<tr>
<td><code>repeatingCountTitle</code></td>
<td>The count title for yearly repating events in this calendar. <br>
<br><b>Example:</b> <br>
<code>'Birthday'</code>
</td>
</tr>
<tr>
<td><code>user</code></td>
<td>The username for HTTP Basic authentication.</td>
</tr>
<tr>
<td><code>pass</code></td>
<td>The password for HTTP Basic authentication.</td>
</tr>
</tbody>
</table>
| Option | Description
| --------------------- | -----------
| `url` | The url of the calendar .ical. This property is required. <br><br> **Possible values:** Any public accessble .ical calendar.
| `symbol` | The symbol to show in front of an event. This property is optional. <br><br> **Possible values:** See [Font Awesome](http://fontawesome.io/icons/) website. To have multiple symbols you can define them in an array e.g. `["calendar", "plane"]`
| `color` | The font color of an event from this calendar. This property should be set if the config is set to colored: true. <br><br> **Possible values:** HEX, RGB or RGBA values (#efefef, rgb(242,242,242), rgba(242,242,242,0.5)).
| `repeatingCountTitle` | The count title for yearly repating events in this calendar. <br><br> **Example:** `'Birthday'`
| `maximumEntries` | The maximum number of events shown. Overrides global setting. **Possible values:** `0` - `100`
| `maximumNumberOfDays` | The maximum number of days in the future. Overrides global setting
| `auth` | The object containing options for authentication against the calendar.
#### Calendar authentication options:
| Option | Description
| --------------------- | -----------
| `user` | The username for HTTP authentication.
| `pass` | The password for HTTP authentication. (If you use Bearer authentication, this should be your BearerToken.)
| `method` | Which authentication method should be used. HTTP Basic, Digest and Bearer authentication methods are supported. Basic authentication is used by default if this option is omitted. **Possible values:** `digest`, `basic`, `bearer` **Default value:** `basic`

View File

@ -2,6 +2,7 @@
padding-left: 0;
padding-right: 10px;
font-size: 80%;
vertical-align: top;
}
.calendar .symbol span {
@ -19,4 +20,5 @@
.calendar .time {
padding-left: 30px;
text-align: right;
vertical-align: top;
}

View File

@ -18,15 +18,18 @@ Module.register("calendar", {
displayRepeatingCountTitle: false,
defaultRepeatingCountTitle: "",
maxTitleLength: 25,
wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength
fetchInterval: 5 * 60 * 1000, // Update every 5 minutes.
animationSpeed: 2000,
fade: true,
urgency: 7,
timeFormat: "relative",
dateFormat: "MMM Do",
fullDayEventDateFormat: "MMM Do",
getRelative: 6,
fadePoint: 0.25, // Start on 1/4th of the list.
hidePrivate: false,
colored: false,
calendars: [
{
symbol: "calendar",
@ -37,7 +40,8 @@ Module.register("calendar", {
"De verjaardag van ": "",
"'s birthday": ""
},
broadcastEvents: true
broadcastEvents: true,
excludedEvents: []
},
// Define required scripts.
@ -52,8 +56,8 @@ Module.register("calendar", {
// Define required translations.
getTranslations: function () {
// The translations for the defaut modules are defined in the core translation files.
// Therefor we can just return false. Otherwise we should have returned a dictionairy.
// The translations for the default modules are defined in the core translation files.
// Therefor we can just return false. Otherwise we should have returned a dictionary.
// If you're trying to build your own module including translations, check out the documentation.
return false;
},
@ -63,12 +67,28 @@ Module.register("calendar", {
Log.log("Starting module: " + this.name);
// Set locale.
moment.locale(config.language);
moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
for (var c in this.config.calendars) {
var calendar = this.config.calendars[c];
calendar.url = calendar.url.replace("webcal://", "http://");
this.addCalendar(calendar.url, calendar.user, calendar.pass);
var calendarConfig = {
maximumEntries: calendar.maximumEntries,
maximumNumberOfDays: calendar.maximumNumberOfDays
};
// we check user and password here for backwards compatibility with old configs
if(calendar.user && calendar.pass) {
Log.warn("Deprecation warning: Please update your calendar authentication configuration.");
Log.warn("https://github.com/MichMich/MagicMirror/tree/v2.1.2/modules/default/calendar#calendar-authentication-options");
calendar.auth = {
user: calendar.user,
pass: calendar.pass
}
}
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
}
this.calendarData = {};
@ -112,23 +132,36 @@ Module.register("calendar", {
for (var e in events) {
var event = events[e];
var eventWrapper = document.createElement("tr");
if (this.config.colored) {
eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
}
eventWrapper.className = "normal";
if (this.config.displaySymbol) {
var symbolWrapper = document.createElement("td");
symbolWrapper.className = "symbol";
symbolWrapper.className = "symbol align-right";
var symbols = this.symbolsForUrl(event.url);
if(typeof symbols === "string") {
symbols = [symbols];
}
for(var i = 0; i < symbols.length; i++) {
var symbol = document.createElement("span");
symbol.className = "fa fa-" + this.symbolForUrl(event.url);
symbol.className = "fa fa-fw fa-" + symbols[i];
if(i > 0){
symbol.style.paddingLeft = "5px";
}
symbolWrapper.appendChild(symbol);
}
eventWrapper.appendChild(symbolWrapper);
}
var titleWrapper = document.createElement("td"),
repeatingCountTitle = "";
if (this.config.displayRepeatingCountTitle) {
repeatingCountTitle = this.countTitleForUrl(event.url);
@ -142,7 +175,13 @@ Module.register("calendar", {
}
titleWrapper.innerHTML = this.titleTransform(event.title) + repeatingCountTitle;
if (!this.config.colored) {
titleWrapper.className = "title bright";
} else {
titleWrapper.className = "title";
}
eventWrapper.appendChild(titleWrapper);
var timeWrapper = document.createElement("td");
@ -177,7 +216,7 @@ Module.register("calendar", {
// This event falls within the config.urgency period that the user has set
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
} 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 {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
@ -214,7 +253,12 @@ Module.register("calendar", {
}
}
} 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');
@ -241,10 +285,35 @@ Module.register("calendar", {
return wrapper;
},
/**
* This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
* corresponding timeformat to be used in the calendar display. If no number is given (or otherwise invalid input)
* it will a localeSpecification object with the system locale time format.
*
* @param {number} timeFormat Specifies either 12 or 24 hour time format
* @returns {moment.LocaleSpecification}
*/
getLocaleSpecification: function(timeFormat) {
switch (timeFormat) {
case 12: {
return { longDateFormat: {LT: "h:mm A"} };
break;
}
case 24: {
return { longDateFormat: {LT: "HH:mm"} };
break;
}
default: {
return { longDateFormat: {LT: moment.localeData().longDateFormat("LT")} };
break;
}
}
},
/* hasCalendarURL(url)
* Check if this config contains the calendar url.
*
* argument url sting - Url to look for.
* argument url string - Url to look for.
*
* return bool - Has calendar url
*/
@ -293,74 +362,117 @@ Module.register("calendar", {
/* createEventList(url)
* Requests node helper to add calendar url.
*
* argument url sting - Url to add.
* argument url string - Url to add.
*/
addCalendar: function (url, user, pass) {
addCalendar: function (url, auth, calendarConfig) {
this.sendSocketNotification("ADD_CALENDAR", {
url: url,
maximumEntries: this.config.maximumEntries,
maximumNumberOfDays: this.config.maximumNumberOfDays,
excludedEvents: calendarConfig.excludedEvents || this.config.excludedEvents,
maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries,
maximumNumberOfDays: calendarConfig.maximumNumberOfDays || this.config.maximumNumberOfDays,
fetchInterval: this.config.fetchInterval,
user: user,
pass: pass
auth: auth
});
},
/* symbolForUrl(url)
* Retrieves the symbol for a specific url.
/* symbolsForUrl(url)
* Retrieves the symbols for a specific url.
*
* argument url sting - Url to look for.
* argument url string - Url to look for.
*
* return string - The Symbol
* return string/array - The Symbols
*/
symbolForUrl: function (url) {
for (var c in this.config.calendars) {
var calendar = this.config.calendars[c];
if (calendar.url === url && typeof calendar.symbol === "string") {
return calendar.symbol;
}
}
return this.config.defaultSymbol;
symbolsForUrl: function (url) {
return this.getCalendarProperty(url, "symbol", this.config.defaultSymbol);
},
/* colorForUrl(url)
* Retrieves the color for a specific url.
*
* argument url string - Url to look for.
*
* return string - The Color
*/
colorForUrl: function (url) {
return this.getCalendarProperty(url, "color", "#fff");
},
/* countTitleForUrl(url)
* Retrieves the name for a specific url.
*
* argument url sting - Url to look for.
* argument url string - Url to look for.
*
* return string - The Symbol
*/
countTitleForUrl: function (url) {
for (var c in this.config.calendars) {
var calendar = this.config.calendars[c];
if (calendar.url === url && typeof calendar.repeatingCountTitle === "string") {
return calendar.repeatingCountTitle;
}
}
return this.config.defaultRepeatingCountTitle;
return this.getCalendarProperty(url, "repeatingCountTitle", this.config.defaultRepeatingCountTitle);
},
/* shorten(string, maxLength)
* Shortens a sting if it's longer than maxLenthg.
* Adds an ellipsis to the end.
/* getCalendarProperty(url, property, defaultValue)
* Helper method to retrieve the property for a specific url.
*
* argument string string - The string to shorten.
* argument maxLength number - The max lenth of the string.
* argument url string - Url to look for.
* argument property string - Property to look for.
* argument defaultValue string - Value if property is not found.
*
* return string - The shortened string.
* return string - The Property
*/
shorten: function (string, maxLength) {
if (string.length > maxLength) {
return string.slice(0, maxLength) + "&hellip;";
getCalendarProperty: function (url, property, defaultValue) {
for (var c in this.config.calendars) {
var calendar = this.config.calendars[c];
if (calendar.url === url && calendar.hasOwnProperty(property)) {
return calendar[property];
}
}
return string;
return defaultValue;
},
/**
* Shortens a string if it's longer than maxLength and add a ellipsis to the end
*
* @param {string} string Text string to shorten
* @param {number} maxLength The max length of the string
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
* @returns {string} The shortened string
*/
shorten: function (string, maxLength, wrapEvents) {
if (typeof string !== "string") {
return "";
}
if (wrapEvents === true) {
var temp = "";
var currentLine = "";
var words = string.split(" ");
for (var i = 0; i < words.length; i++) {
var word = words[i];
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) { // max - 1 to account for a space
currentLine += (word + " ");
} else {
if (currentLine.length > 0) {
temp += (currentLine + "<br>" + word + " ");
} else {
temp += (word + "<br>");
}
currentLine = "";
}
}
return (temp + currentLine).trim();
} else {
if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
return string.trim().slice(0, maxLength) + "&hellip;";
} else {
return string.trim();
}
}
},
/* capFirst(string)
* Capitalize the first letter of a string
* Eeturn capitalized string
* Return capitalized string
*/
capFirst: function (string) {
@ -370,7 +482,7 @@ Module.register("calendar", {
/* titleTransform(title)
* Transforms the title of an event for usage.
* Replaces parts of the text as defined in config.titleReplace.
* Shortens title based on config.maxTitleLength
* Shortens title based on config.maxTitleLength and config.wrapEvents
*
* argument title string - The title to transform.
*
@ -379,10 +491,17 @@ Module.register("calendar", {
titleTransform: function (title) {
for (var needle in this.config.titleReplace) {
var replacement = this.config.titleReplace[needle];
var regParts = needle.match(/^\/(.+)\/([gim]*)$/);
if (regParts) {
// the parsed pattern is a regexp.
needle = new RegExp(regParts[1], regParts[2]);
}
title = title.replace(needle, replacement);
}
title = this.shorten(title, this.config.maxTitleLength);
title = this.shorten(title, this.config.maxTitleLength, this.config.wrapEvents);
return title;
},
@ -392,10 +511,12 @@ Module.register("calendar", {
*/
broadcastEvents: function () {
var eventList = [];
for (url in this.calendarData) {
for (var url in this.calendarData) {
var calendar = this.calendarData[url];
for (e in calendar) {
for (var e in calendar) {
var event = cloneObject(calendar[e]);
event.symbol = this.symbolsForUrl(url);
event.color = this.colorForUrl(url);
delete event.url;
eventList.push(event);
}

View File

@ -8,7 +8,7 @@
var ical = require("./vendor/ical.js");
var moment = require("moment");
var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumberOfDays, user, pass) {
var CalendarFetcher = function(url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth) {
var self = this;
var reloadTimer = null;
@ -32,11 +32,23 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
}
};
if (user && pass) {
if (auth) {
if(auth.method === "bearer"){
opts.auth = {
user: user,
pass: pass,
sendImmediately: true
bearer: auth.pass
}
}else{
opts.auth = {
user: auth.user,
pass: auth.pass
};
if(auth.method === "digest"){
opts.auth.sendImmediately = false;
}else{
opts.auth.sendImmediately = true;
}
}
}
@ -52,6 +64,10 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
var limitFunction = function(date, i) {return i < maximumEntries;};
var eventDate = function(event, time) {
return (event[time].length === 8) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
};
for (var e in data) {
var event = data[e];
var now = new Date();
@ -70,10 +86,10 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
if (event.type === "VEVENT") {
var startDate = (event.start.length === 8) ? moment(event.start, "YYYYMMDD") : moment(new Date(event.start));
var startDate = eventDate(event, "start");
var endDate;
if (typeof event.end !== "undefined") {
endDate = (event.end.length === 8) ? moment(event.end, "YYYYMMDD") : moment(new Date(event.end));
endDate = eventDate(event, "end");
} else {
if (!isFacebookBirthday) {
endDate = startDate;
@ -97,6 +113,19 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
title = event.description;
}
var excluded = false;
for (var f in excludedEvents) {
var filter = excludedEvents[f];
if (title.toLowerCase().includes(filter.toLowerCase())) {
excluded = true;
break;
}
}
if (excluded) {
continue;
}
var location = event.location || false;
var geo = event.geo || false;
var description = event.description || false;

View File

@ -8,14 +8,22 @@
var CalendarFetcher = require("./calendarfetcher.js");
var url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics";
var url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL
// var url = "https://www.googleapis.com/calendar/v3/calendars/primary/events/"; // URL for Bearer auth (must be configured in Google OAuth2 first)
var fetchInterval = 60 * 60 * 1000;
var maximumEntries = 10;
var maximumNumberOfDays = 365;
var user = "magicmirror";
var pass = "MyStrongPass";
var auth = {
user: user,
pass: pass
};
console.log("Create fetcher ...");
fetcher = new CalendarFetcher(url, fetchInterval, maximumEntries, maximumNumberOfDays);
fetcher = new CalendarFetcher(url, fetchInterval, maximumEntries, maximumNumberOfDays, auth);
fetcher.onReceive(function(fetcher) {
console.log(fetcher.events());

View File

@ -12,7 +12,6 @@ var CalendarFetcher = require("./calendarfetcher.js");
module.exports = NodeHelper.create({
// Override start method.
start: function() {
var self = this;
var events = [];
this.fetchers = [];
@ -25,7 +24,7 @@ module.exports = NodeHelper.create({
socketNotificationReceived: function(notification, payload) {
if (notification === "ADD_CALENDAR") {
//console.log('ADD_CALENDAR: ');
this.createFetcher(payload.url, payload.fetchInterval, payload.maximumEntries, payload.maximumNumberOfDays, payload.user, payload.pass);
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth);
}
},
@ -37,7 +36,7 @@ module.exports = NodeHelper.create({
* attribute reloadInterval number - Reload interval in milliseconds.
*/
createFetcher: function(url, fetchInterval, maximumEntries, maximumNumberOfDays, user, pass) {
createFetcher: function(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth) {
var self = this;
if (!validUrl.isUri(url)) {
@ -48,7 +47,7 @@ module.exports = NodeHelper.create({
var fetcher;
if (typeof self.fetchers[url] === "undefined") {
console.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval);
fetcher = new CalendarFetcher(url, fetchInterval, maximumEntries, maximumNumberOfDays, user, pass);
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth);
fetcher.onReceive(function(fetcher) {
//console.log('Broadcast events.');

View File

@ -90,7 +90,6 @@
return dt
}
var dateParam = function(name){
return function(val, params, curr){
@ -143,6 +142,16 @@
}
}
var exdateParam = function(name){
return function(val, params, curr){
var date = dateParam(name)(val, params, curr);
if (date.exdates === undefined) {
date.exdates = [];
}
date.exdates.push(date.exdate);
return date;
}
}
var geoParam = function(name){
return function(val, params, curr){
@ -240,6 +249,7 @@
, 'LOCATION' : storeParam('location')
, 'DTSTART' : dateParam('start')
, 'DTEND' : dateParam('end')
, 'EXDATE' : exdateParam('exdate')
,' CLASS' : storeParam('class')
, 'TRANSP' : storeParam('transparency')
, 'GEO' : geoParam('geo')

View File

@ -17,7 +17,8 @@ exports.parseFile = function(filename){
}
var rrule = require('rrule').RRule
var rrule = require('rrule-alt').RRule
var rrulestr = rrule.rrulestr
ical.objectHandlers['RRULE'] = function(val, params, curr, stack, line){
curr.rrule = line;
@ -26,7 +27,7 @@ ical.objectHandlers['RRULE'] = function(val, params, curr, stack, line){
var originalEnd = ical.objectHandlers['END'];
ical.objectHandlers['END'] = function(val, params, curr, stack){
if (curr.rrule) {
var rule = curr.rrule.replace('RRULE:', '');
var rule = curr.rrule;
if (rule.indexOf('DTSTART') === -1) {
if (curr.start.length === 8) {
@ -36,10 +37,14 @@ ical.objectHandlers['END'] = function(val, params, curr, stack){
}
}
rule += ';DTSTART=' + curr.start.toISOString().replace(/[-:]/g, '');
rule += ' DTSTART:' + curr.start.toISOString().replace(/[-:]/g, '');
rule = rule.replace(/\.[0-9]{3}/, '');
}
curr.rrule = rrule.fromString(rule);
for (var i in curr.exdates) {
rule += ' EXDATE:' + curr.exdates[i].toISOString().replace(/[-:]/g, '');
rule = rule.replace(/\.[0-9]{3}/, '');
}
curr.rrule = rrulestr(rule);
}
return originalEnd.call(this, val, params, curr, stack);
}

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