Merge pull request #1 from MichMich/develop

Develop
This commit is contained in:
RedNax67 2016-10-20 08:41:40 +02:00 committed by GitHub
commit 89234c0163
53 changed files with 1485 additions and 405 deletions

View File

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

View File

@ -3,57 +3,47 @@ Contribution Policy for MagicMirror²
Thanks for contributing to MagicMirror²! Thanks for contributing to MagicMirror²!
We hold our code to standard, and these standards are documented below. We hold our code to standard, and these standards are documented below.
First, before you run the linters, you will need to install them all **and** install the development dependencies: If you wish to run both linters, use `grunt` without any arguments.
```bash ### JavaScript: Run ESLint
(sudo) npm install -g jscs stylelint html-validator-cli
npm install
```
### JavaScript: Run JSCS We use [ESLint](http://eslint.org) on our JavaScript files.
We use [JSCS](http://jscs.info) on our JavaScript files. Our ESLint configuration is in our .eslintrc.json and .eslintignore files.
Our JSCS configuration is in our .jscsrc file. To run ESLint, use `grunt eslint`.
To run JSCS, use `npm run jscs`.
### CSS: Run StyleLint ### CSS: Run StyleLint
We use [StyleLint](http://stylelint.io) to lint our CSS. Our configuration is in our .stylelintrc file. We use [StyleLint](http://stylelint.io) to lint our CSS. Our configuration is in our .stylelintrc file.
To run StyleLint, use `npm run stylelint`. To run StyleLint, use `grunt stylelint`.
### HTML: Run HTML Validator ### Submitting Issues
We use [NU Validator](https://validator.w3.org/nu) to validate our HTML. The configuration is in the command in the package.json file.
To run HTML Validator, use `npm run htmlvalidator`.
## Submitting Issues
Please only submit reproducible issues. Please only submit reproducible issues.
If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: https://forum.magicmirror.builders/category/15/bug-hunt - Problems installing or configuring your MagicMirror? Check out: https://forum.magicmirror.builders/category/10/troubleshooting If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt)
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
When submitting a new issue, please supply the following information: When submitting a new issue, please supply the following information:
**Platform** [ Raspberry Pi 2/3, Windows, Mac OS X, Linux, Etc ... ]: **Platform**: Place your platform here... give us your web browser/Electron version *and* your hardware (Raspberry Pi 2/3, Windows, Mac, Linux, System V UNIX).
**Node Version** [ 0.12.13 or later ]: **Node Version**: Make sure it's version 0.12.13 or later.
**MagicMirror Version** [ V1 / V2-Beta ]: **MagicMirror Version**: Now that the versions have split, tell us if you are using the PHP version (v1) or the newer JavaScript version (v2).
**Description:** Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem. **Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem.
**Steps to Reproduce:** List the step by step process to reproduce the issue. **Steps to Reproduce**: List the step by step process to reproduce the issue.
**Expected Results:** Describe what you expected to see. **Expected Results**: Describe what you expected to see.
**Actual Results:** Describe what you actually saw. **Actual Results**: Describe what you actually saw.
**Configuration:** What does the used config.js file look like? (Don't forget to remove any sensitive information.) **Configuration**: What does the used config.js file look like? Don't forget to remove any sensitive information!
**Additional Notes:** Provide any other relevant notes not previously mentioned (optional) **Additional Notes**: Provide any other relevant notes not previously mentioned. This is optional.

View File

@ -1,21 +1,24 @@
Please only submit reproducible issues. Please only submit reproducible issues.
If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: https://forum.magicmirror.builders/category/15/bug-hunt - Problems installing or configuring your MagicMirror? Check out: https://forum.magicmirror.builders/category/10/troubleshooting If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt)
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
**Platform** [ Raspberry Pi 2/3, Windows, Mac OS X, Linux, Etc ... ]: When submitting a new issue, please supply the following information:
**Node Version** [ 0.12.13 or later ]: **Platform**: Place your platform here... give us your web browser/Electron version *and* your hardware (Raspberry Pi 2/3, Windows, Mac, Linux, System V UNIX).
**MagicMirror Version** [ V1 / V2-Beta ]: **Node Version**: Make sure it's version 0.12.13 or later.
**Description:** Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem. **MagicMirror Version**: Now that the versions have split, tell us if you are using the PHP version (v1) or the newer JavaScript version (v2).
**Steps to Reproduce:** List the step by step process to reproduce the issue. **Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem.
**Expected Results:** Describe what you expected to see. **Steps to Reproduce**: List the step by step process to reproduce the issue.
**Actual Results:** Describe what you actually saw. **Expected Results**: Describe what you expected to see.
**Configuration:** What does the used config.js file look like? (Don't forget to remove any sensitive information.) **Actual Results**: Describe what you actually saw.
**Additional Notes:** Provide any other relevant notes not previously mentioned (optional) **Configuration**: What does the used config.js file look like? Don't forget to remove any sensitive information!
**Additional Notes**: Provide any other relevant notes not previously mentioned. This is optional.

View File

@ -1,9 +1,7 @@
> Please send your PR's the develop branch. > Please send your pull requests the develop branch.
> Don't forget to add the change to changelog.md. > Don't forget to add the change to CHANGELOG.md.
* Does the pull request solve a **related** issue? [yes | no] * Does the pull request solve a **related** issue?
* If so, can you reference the issue? * If so, can you reference the issue?
* What does the pull request accomplish? (please list) * What does the pull request accomplish? Use a list if needed.
* If it includes major visual changes please add screenshots. * If it includes major visual changes please add screenshots.

View File

@ -1,4 +1,5 @@
{ {
"extends": "stylelint-config-standard", "extends": "stylelint-config-standard",
"font-family-name-quotes": "double-where-recommended" "font-family-name-quotes": "double-where-recommended",
} "block-no-empty": false
}

View File

@ -2,16 +2,62 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
## Unpublished ## [2.1.0] - Unreleased
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`
### Added
- Finnish translation.
- Danish translation.
- Option to limit access to certain IP addresses based on the value of `ipWhitelist` in the `config.js`, default is access from localhost only (Issue [#456](https://github.com/MichMich/MagicMirror/issues/456)).
- Added ability to change the point of time when calendar events get relative.
- Add Splash screen on boot.
- Add option to show humidity in currentWeather module.
- Add VSCode IntelliSense support.
- Module API: Add Visibility locking to module system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#visibility-locking) for more information.
- Module API: Method to overwrite the module's header. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#getheader) for more information.
- Module API: Option to define the minimumn MagicMirror version to run a module. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules#requiresversion) for more information.
- Calendar module now broadcasts the event list to all other modules using the notification system. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/calendar) for more information.
- Possibility to use the the calendar feed as the source for the weather (currentweather & weatherforecast) location data. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/weatherforecast) for more information.
- Added option to show rain amount in the weatherforecast default module
- Add module `updatenotification` to get an update whenever a new version is availabe. [See documentation](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/updatenotification) for more information.
### Updated ### Updated
- Force fullscreen when kioskmode is active. - Modified translations for Frysk.
- Updated package.json as a result of Snyk security update.
- Improve object instantiation to prevent reference errors.
- Improve logger. `Log.log()` now accepts multiple arguments.
- Remove extensive logging in newsfeed node helper.
- Calendar times are now uniformly capitalized.
### Fixed
- Solve an issue where module margins would appear when the first module of a section was hidden.
- Solved visual display errors on chrome, if all modules in one of the right sections are hidden
## [2.0.5] - 2016-09-20
### Added
- Added ability to remove tags from the beginning or end of newsfeed items in 'newsfeed.js'.
- Added ability to define "the day after tomorrow" for calendar events (Definition for German and Dutch already included).
- Added CII Badge (we are compliant with the CII Best Practices)
- Add support for doing http basic auth when loading calendars
- Add the abilty to turn off and on the date display in the Clock Module
### Fixed
- Fix typo in installer.
- Add message to unsupported Pi error to mention that Pi Zeros must use server only mode, as ARMv6 is unsupported. Closes #374.
- Fix API url for weather API.
### Updated
- Force fullscreen when kioskmode is active.
- Update the .github templates and information with more modern information.
- Update the Gruntfile with a more functional StyleLint implementation.
## [2.0.4] - 2016-08-07 ## [2.0.4] - 2016-08-07
### Added ### Added
- Brazilian Portuguese Translation. - Brazilian Portuguese Translation.
- Option to enable Kios mode. - Option to enable Kiosk mode.
- Added ability to start the app with Dev Tools. - Added ability to start the app with Dev Tools.
- Added ability to turn off the date display in `clock.js` when in analog mode. - Added ability to turn off the date display in `clock.js` when in analog mode.
- Greek Translation - Greek Translation
@ -76,4 +122,4 @@ It includes (but is not limited to) the following features:
## [1.0.0] - 2014-02-16 ## [1.0.0] - 2014-02-16
### Initial release of MagicMirror. ### Initial release of MagicMirror.
This was part of the blogpost: http://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the This was part of the blogpost: [http://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the](http://michaelteeuw.nl/post/83916869600/magic-mirror-part-vi-production-of-the)

View File

@ -8,21 +8,59 @@ module.exports = function(grunt) {
}, },
target: ["js/*.js", "modules/default/*.js", "serveronly/*.js", "*.js"] target: ["js/*.js", "modules/default/*.js", "serveronly/*.js", "*.js"]
}, },
postcss: { stylelint: {
lint: { simple: {
options: { options: {
processors: [ configFile: ".stylelintrc"
require("stylelint")({"extends": "stylelint-config-standard", "font-family-name-quotes": "double-where-recommended"}),
require("postcss-reporter")({ clearMessages: true })
]
}, },
dist: { 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" }
},
jsonlint: {
main: {
src: ["package.json", ".eslintrc.json", ".stylelint"],
options: {
reporter: "jshint"
} }
} }
},
markdownlint: {
all: {
options: {
config: {
"default": true,
"line-length": false,
"blanks-around-headers": false,
"no-duplicate-header": false,
"no-inline-html": false,
"MD010": false,
"MD001": false,
"MD031": false,
"MD040": false,
"MD002": false,
"MD029": false,
"MD041": false,
"MD032": false,
"MD036": false,
"MD037": false,
"MD009": false,
"MD018": false,
"MD012": false,
"MD026": false,
"MD038": false
}
},
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"]
} }
}); });
grunt.loadNpmTasks("grunt-eslint"); grunt.loadNpmTasks("grunt-eslint");
grunt.loadNpmTasks("grunt-postcss"); grunt.loadNpmTasks("grunt-stylelint");
grunt.registerTask("default", ["eslint", "postcss:lint"]); grunt.loadNpmTasks("grunt-jsonlint");
grunt.loadNpmTasks("grunt-yamllint");
grunt.loadNpmTasks("grunt-markdownlint");
grunt.registerTask("default", ["eslint", "stylelint", "jsonlint", "markdownlint", "yamllint"]);
}; };

View File

@ -3,7 +3,7 @@
<p align="center"> <p align="center">
<a href="https://david-dm.org/MichMich/MagicMirror"><img src="https://david-dm.org/MichMich/MagicMirror.svg" alt="Dependency Status"></a> <a href="https://david-dm.org/MichMich/MagicMirror"><img src="https://david-dm.org/MichMich/MagicMirror.svg" alt="Dependency Status"></a>
<a href="https://david-dm.org/MichMich/MagicMirror#info=devDependencies"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a> <a href="https://david-dm.org/MichMich/MagicMirror#info=devDependencies"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a>
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-v5.10.1-brightgreen.svg" alt="Node Version"></a> <a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge"></a>
<a href="http://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a> <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://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="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>
@ -24,17 +24,17 @@ MagicMirror² focuses on a modular plugin system and uses [Electron](http://elec
## Usage ## Usage
#### Raspberry Pi Support ### Raspberry Pi Support
Electron, the app wrapper around MagicMirror², only supports the Raspberry Pi 2 & 3. The Raspberry Pi 1 is currently **not** supported. If you want to run this on a Raspberry Pi 1, use the [server only](#server-only) feature and setup a fullscreen browser yourself. Electron, the app wrapper around MagicMirror², only supports the Raspberry Pi 2 & 3. The Raspberry Pi 1 is currently **not** supported. If you want to run this on a Raspberry Pi 1, use the [server only](#server-only) feature and setup a fullscreen browser yourself.
#### Automatic Installer (Raspberry Pi Only!) ### Automatic Installer (Raspberry Pi Only!)
Execute the following command on your Raspberry Pi to install MagicMirror²: Execute the following command on your Raspberry Pi to install MagicMirror²:
```` ````
curl -sL https://raw.githubusercontent.com/MichMich/MagicMirror/master/installers/raspberry.sh | bash curl -sL https://raw.githubusercontent.com/MichMich/MagicMirror/master/installers/raspberry.sh | bash
```` ````
#### Manual Installation ### Manual Installation
1. Download and install the latest Node.js version. 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 beta branch: `git clone https://github.com/MichMich/MagicMirror`
@ -45,23 +45,23 @@ 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. **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 ### Server Only
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. 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.
#### Raspberry Configuration & Auto Start. ### Raspberry Configuration & Auto Start.
The following wiki links are helpful in the configuration of your MagicMirror² operating system: The following wiki links are helpful in the configuration of your MagicMirror² operating system:
- [Configuring the Raspberry Pi](https://github.com/MichMich/MagicMirror/wiki/Configuring-the-Raspberry-Pi) - [Configuring the Raspberry Pi](https://github.com/MichMich/MagicMirror/wiki/Configuring-the-Raspberry-Pi)
- [Auto Starting MagicMirror](https://github.com/MichMich/MagicMirror/wiki/Auto-Starting-MagicMirror) - [Auto Starting MagicMirror](https://github.com/MichMich/MagicMirror/wiki/Auto-Starting-MagicMirror)
#### Updating you MagicMirror² ### Updating your MagicMirror²
If you want to update your MagicMirror² to the latest version, use your terminal to go to your Magic Mirror folder and type the following command: If you want to update your MagicMirror² to the latest version, use your terminal to go to your Magic Mirror folder and type the following command:
```` ```bash
git pull git pull && npm install
```` ```
If you changed nothing more than the config or the modules, this should work without any problems. If you changed nothing more than the config or the modules, this should work without any problems.
Type `git status` to see your changes, if there are any, you can reset them with `git reset --hard`. After that, git pull should be possible. Type `git status` to see your changes, if there are any, you can reset them with `git reset --hard`. After that, git pull should be possible.
@ -73,16 +73,17 @@ Type `git status` to see your changes, if there are any, you can reset them with
The following properties can be configured: The following properties can be configured:
| **Option** | **Description** | | **Option** | **Description** |
| --- | --- | | --- | --- |
| `port` | The port on which the MagicMirror² server will run on. The default value is `8080`. | | `port` | The port on which the MagicMirror² server will run on. The default value is `8080`. |
| `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"]]`).|
| `kioskmode` | This allows MagicMirror² to run in Kiosk Mode. It protects from other programs popping on top of your screen. The default value is `false`| | `kioskmode` | This allows MagicMirror² to run in Kiosk Mode. It protects from other programs popping on top of your screen. The default value is `false`|
| `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`. | | `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`. | | `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`. | | `units` | The units that will be used in the default weather modules. Possible values are `metric` or `imperial`. The default is `metric`. |
| `modules` | An array of active modules. **The array must contain objects. See the next table below for more information.** | | `modules` | An array of active modules. **The array must contain objects. See the next table below for more information.** |
Module configuration: Module configuration:
| **Option** | **Description** | | **Option** | **Description** |
@ -129,3 +130,8 @@ Please keep the following in mind:
- **New Features**: please please discuss in a GitHub issue before you start to alter a big part of the code. Without discussion upfront, the pull request will not be accepted / merged. - **New Features**: please please discuss in a GitHub issue before you start to alter a big part of the code. Without discussion upfront, the pull request will not be accepted / merged.
Thanks for your help in making MagicMirror² better! Thanks for your help in making MagicMirror² better!
<p align="center">
<br>
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
</p>

View File

@ -6,6 +6,7 @@
var config = { var config = {
port: 8080, port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: 'en', language: 'en',
timeFormat: 24, timeFormat: 24,
@ -15,6 +16,10 @@ var config = {
{ {
module: 'alert', module: 'alert',
}, },
{
module: "updatenotification",
position: "top_center"
},
{ {
module: 'clock', module: 'clock',
position: 'top_left' position: 'top_left'

View File

@ -1,10 +1,10 @@
html { html {
cursor: none; cursor: none;
overflow:hidden; overflow: hidden;
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
display: none; display: none;
} }
body { body {
@ -113,11 +113,12 @@ sup {
*/ */
.module { .module {
margin-top: 30px; margin-bottom: 30px;
} }
.module:first-child { .region.bottom .module {
margin-top: 0; margin-top: 30px;
margin-bottom: 0;
} }
/** /**

View File

@ -8,6 +8,10 @@
<link rel="stylesheet" type="text/css" href="css/main.css"> <link rel="stylesheet" type="text/css" href="css/main.css">
<link rel="stylesheet" type="text/css" href="fonts/roboto.css"> <link rel="stylesheet" type="text/css" href="fonts/roboto.css">
<!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. --> <!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. -->
<script type="text/javascript">
var version = "#VERSION#";
</script>
</head> </head>
<body> <body>
<div class="region fullscreen below"><div class="container"></div></div> <div class="region fullscreen below"><div class="container"></div></div>

View File

@ -28,6 +28,7 @@ ARM=$(uname -m)
if [ "$ARM" != "armv7l" ]; then if [ "$ARM" != "armv7l" ]; then
echo -e "\e[91mSorry, your Raspberry Pi is not supported." echo -e "\e[91mSorry, your Raspberry Pi is not supported."
echo -e "\e[91mPlease run MagicMirror on a Raspberry Pi 2 or 3." echo -e "\e[91mPlease run MagicMirror on a Raspberry Pi 2 or 3."
echo -e "\e[91mIf this is a Pi Zero, you are in the same boat as the original Raspberry Pi. You must run in server only mode."
exit; exit;
fi fi
@ -68,7 +69,7 @@ else
NODE_INSTALL=true NODE_INSTALL=true
fi fi
# Install or upgare node if nessecery. # Install or upgrade node if necessary.
if $NODE_INSTALL; then if $NODE_INSTALL; then
echo -e "\e[96mInstalling Node.js ...\e[90m" echo -e "\e[96mInstalling Node.js ...\e[90m"
@ -112,6 +113,34 @@ else
exit; exit;
fi fi
# Check if plymouth is installed (default with PIXEL desktop environment), then install custom splashscreen.
echo -e "\e[96mCheck plymouth installation ...\e[0m"
if command_exists plymouth; then
THEME_DIR="/usr/share/plymouth/themes"
echo -e "\e[90mSplashscreen: Checking themes directory.\e[0m"
if [ -d $THEME_DIR ]; then
echo -e "\e[90mSplashscreen: Create theme directory if not exists.\e[0m"
if [ ! -d $THEME_DIR/MagicMirror ]; then
sudo mkdir $THEME_DIR/MagicMirror
fi
if sudo cp ~/MagicMirror/splashscreen/splash.png $THEME_DIR/MagicMirror/splash.png && sudo cp ~/MagicMirror/splashscreen/MagicMirror.plymouth $THEME_DIR/MagicMirror/MagicMirror.plymouth && sudo cp ~/MagicMirror/splashscreen/MagicMirror.script $THEME_DIR/MagicMirror/MagicMirror.script; then
echo -e "\e[90mSplashscreen: Theme copied successfully.\e[0m"
if sudo plymouth-set-default-theme -R MagicMirror; then
echo -e "\e[92mSplashscreen: Changed theme to MagicMirror successfully.\e[0m"
else
echo -e "\e[91mSplashscreen: Couldn't change theme to MagicMirror!\e[0m"
fi
else
echo -e "\e[91mSplashscreen: Copying theme failed!\e[0m"
fi
else
echo -e "\e[91mSplashscreen: Themes folder doesn't exist!\e[0m"
fi
else
echo -e "\e[93mplymouth is not installed.\e[0m";
fi
echo " " echo " "
echo -e "\e[92mWe're ready! Run \e[1m\e[97mDISPLAY=:0 npm start\e[0m\e[92m from the ~/MagicMirror directory to start your MagicMirror.\e[0m" echo -e "\e[92mWe're ready! Run \e[1m\e[97mDISPLAY=:0 npm start\e[0m\e[92m from the ~/MagicMirror directory to start your MagicMirror.\e[0m"
echo " " echo " "

View File

@ -10,6 +10,10 @@ var Server = require(__dirname + "/server.js");
var defaultModules = require(__dirname + "/../modules/default/defaultmodules.js"); var defaultModules = require(__dirname + "/../modules/default/defaultmodules.js");
var path = require("path"); var path = require("path");
// Get version number.
global.version = JSON.parse(fs.readFileSync("package.json", "utf8")).version;
console.log("Starting MagicMirror: v" + global.version);
// The next part is here to prevent a major exception when there // The next part is here to prevent a major exception when there
// is no internet connection. This could probable be solved better. // is no internet connection. This could probable be solved better.
process.on("uncaughtException", function (err) { process.on("uncaughtException", function (err) {
@ -82,6 +86,17 @@ var App = function() {
if (loadModule) { if (loadModule) {
var Module = require(helperPath); var Module = require(helperPath);
var m = new Module(); var m = new Module();
if (m.requiresVersion) {
console.log("Check MagicMirror version for node helper '" + moduleName + "' - Minimum version: " + m.requiresVersion + " - Current version: " + global.version);
if (cmpVersions(global.version, m.requiresVersion) >= 0) {
console.log("Version is ok!");
} else {
console.log("Version is incorrect. Skip module: '" + moduleName + "'");
return;
}
}
m.setName(moduleName); m.setName(moduleName);
m.setPath(path.resolve(moduleFolder)); m.setPath(path.resolve(moduleFolder));
nodeHelpers.push(m); nodeHelpers.push(m);
@ -103,6 +118,28 @@ var App = function() {
console.log("All module helpers loaded."); console.log("All module helpers loaded.");
}; };
/* cmpVersions(a,b)
* Compare two symantic version numbers and return the difference.
*
* argument a string - Version number a.
* argument a string - Version number b.
*/
function cmpVersions(a, b) {
var i, diff;
var regExStrip0 = /(\.0+)+$/;
var segmentsA = a.replace(regExStrip0, "").split(".");
var segmentsB = b.replace(regExStrip0, "").split(".");
var l = Math.min(segmentsA.length, segmentsB.length);
for (i = 0; i < l; i++) {
diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
if (diff) {
return diff;
}
}
return segmentsA.length - segmentsB.length;
}
/* start(callback) /* start(callback)
* This methods starts the core app. * This methods starts the core app.
* It loads the config, then it loads all modules. * It loads the config, then it loads all modules.

View File

@ -21,28 +21,31 @@
var prototype = new this(); var prototype = new this();
initializing = false; initializing = false;
// Make a copy of all prototype properies, to prevent reference issues.
for (var name in prototype) {
prototype[name] = cloneObject(prototype[name]);
}
// Copy the properties over onto the new prototype // Copy the properties over onto the new prototype
for (var name in prop) { for (var name in prop) {
// Check if we're overwriting an existing function // Check if we're overwriting an existing function
prototype[name] = typeof prop[name] == "function" && prototype[name] = typeof prop[name] == "function" &&
typeof _super[name] == "function" && fnTest.test(prop[name]) ? typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn) {
(function(name, fn) { return function() {
return function() { var tmp = this._super;
var tmp = this._super;
// Add a new ._super() method that is the same method // Add a new ._super() method that is the same method
// but on the super-class // but on the super-class
this._super = _super[name]; this._super = _super[name];
// The method only need to be bound temporarily, so we // The method only need to be bound temporarily, so we
// remove it when we're done executing // remove it when we're done executing
var ret = fn.apply(this, arguments); var ret = fn.apply(this, arguments);
this._super = tmp; this._super = tmp;
return ret; return ret;
}; };
})(name, prop[name]) : })(name, prop[name]) : prop[name];
prop[name];
} }
// The dummy class constructor // The dummy class constructor
@ -66,5 +69,24 @@
}; };
})(); })();
//Define the clone method for later use.
//Helper Method
function cloneObject(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
var temp = obj.constructor(); // give temp the original obj's constructor
for (var key in obj) {
temp[key] = cloneObject(obj[key]);
if (key === "lockStrings") {
Log.log(key);
}
}
return temp;
}
/*************** DO NOT EDIT THE LINE BELOW ***************/ /*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = Class;} if (typeof module !== "undefined") {module.exports = Class;}

View File

@ -10,12 +10,17 @@
var defaults = { var defaults = {
port: 8080, port: 8080,
kioskmode: false, kioskmode: false,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en", language: "en",
timeFormat: 24, timeFormat: 24,
units: "metric", units: "metric",
modules: [ modules: [
{
module: "updatenotification",
position: "top_center"
},
{ {
module: "helloworld", module: "helloworld",
position: "upper_third", position: "upper_third",

View File

@ -117,9 +117,13 @@ var Loader = (function() {
var afterLoad = function() { var afterLoad = function() {
var moduleObject = Module.create(module.name); var moduleObject = Module.create(module.name);
bootstrapModule(module, moduleObject, function() { if (moduleObject) {
bootstrapModule(module, moduleObject, function() {
callback();
});
} else {
callback(); callback();
}); }
}; };
if (loadedModuleFiles.indexOf(url) !== -1) { if (loadedModuleFiles.indexOf(url) !== -1) {

View File

@ -13,35 +13,35 @@
var Log = (function() { var Log = (function() {
return { return {
info: function(message) { info: function() {
console.info(message); console.info.apply(console, arguments);
}, },
log: function(message) { log: function() {
console.log(message); console.log.apply(console, arguments);
}, },
error: function(message) { error: function() {
console.error(message); console.error.apply(console, arguments);
}, },
warn: function(message) { warn: function() {
console.warn(message); console.warn.apply(console, arguments);
}, },
group: function(message) { group: function() {
console.group(message); console.group.apply(console, arguments);
}, },
groupCollapsed: function(message) { groupCollapsed: function() {
console.groupCollapsed(message); console.groupCollapsed.apply(console, arguments);
}, },
groupEnd: function() { groupEnd: function() {
console.groupEnd(); console.groupEnd();
}, },
time: function(message) { time: function() {
console.time(message); console.time.apply(console, arguments);
}, },
timeEnd: function(message) { timeEnd: function() {
console.timeEnd(message); console.timeEnd.apply(console, arguments);
}, },
timeStamp: function(message) { timeStamp: function() {
console.timeStamp(message); console.timeStamp.apply(console, arguments);
} }
}; };
})(); })();

View File

@ -40,6 +40,7 @@ var MM = (function() {
if (typeof module.data.header !== "undefined" && module.data.header !== "") { if (typeof module.data.header !== "undefined" && module.data.header !== "") {
var moduleHeader = document.createElement("header"); var moduleHeader = document.createElement("header");
moduleHeader.innerHTML = module.data.header; moduleHeader.innerHTML = module.data.header;
moduleHeader.className = "module-header";
dom.appendChild(moduleHeader); dom.appendChild(moduleHeader);
} }
@ -94,26 +95,27 @@ var MM = (function() {
*/ */
var updateDom = function(module, speed) { var updateDom = function(module, speed) {
var newContent = module.getDom(); var newContent = module.getDom();
var newHeader = module.getHeader();
if (!module.hidden) { if (!module.hidden) {
if (!moduleNeedsUpdate(module, newContent)) { if (!moduleNeedsUpdate(module, newHeader, newContent)) {
return; return;
} }
if (!speed) { if (!speed) {
updateModuleContent(module, newContent); updateModuleContent(module, newHeader, newContent);
return; return;
} }
hideModule(module, speed / 2, function() { hideModule(module, speed / 2, function() {
updateModuleContent(module, newContent); updateModuleContent(module, newHeader, newContent);
if (!module.hidden) { if (!module.hidden) {
showModule(module, speed / 2); showModule(module, speed / 2);
} }
}); });
} else { } else {
updateModuleContent(module, newContent); updateModuleContent(module, newHeader, newContent);
} }
}; };
@ -125,14 +127,23 @@ var MM = (function() {
* *
* return bool - Does the module need an update? * return bool - Does the module need an update?
*/ */
var moduleNeedsUpdate = function(module, newContent) { var moduleNeedsUpdate = function(module, newHeader, newContent) {
var moduleWrapper = document.getElementById(module.identifier); var moduleWrapper = document.getElementById(module.identifier);
var contentWrapper = moduleWrapper.getElementsByClassName("module-content")[0]; var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
var tempWrapper = document.createElement("div"); var headerNeedsUpdate = false;
tempWrapper.appendChild(newContent); var contentNeedsUpdate = false;
return tempWrapper.innerHTML !== contentWrapper.innerHTML; if (headerWrapper.length > 0) {
headerNeedsUpdate = newHeader !== headerWrapper.innerHTML;
}
var tempContentWrapper = document.createElement("div");
tempContentWrapper.appendChild(newContent);
contentNeedsUpdate = tempContentWrapper.innerHTML !== contentWrapper[0].innerHTML;
return headerNeedsUpdate || contentNeedsUpdate;
}; };
/* moduleNeedsUpdate(module, newContent) /* moduleNeedsUpdate(module, newContent)
@ -141,12 +152,19 @@ var MM = (function() {
* argument module Module - The module to check. * argument module Module - The module to check.
* argument newContent Domobject - The new content that is generated. * argument newContent Domobject - The new content that is generated.
*/ */
var updateModuleContent = function(module, content) { var updateModuleContent = function(module, newHeader, newContent) {
var moduleWrapper = document.getElementById(module.identifier); var moduleWrapper = document.getElementById(module.identifier);
var contentWrapper = moduleWrapper.getElementsByClassName("module-content")[0]; var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
contentWrapper[0].innerHTML = "";
contentWrapper[0].appendChild(newContent);
if( headerWrapper.length > 0 && newHeader) {
headerWrapper[0].innerHTML = newHeader;
}
contentWrapper.innerHTML = "";
contentWrapper.appendChild(content);
}; };
/* hideModule(module, speed, callback) /* hideModule(module, speed, callback)
@ -156,7 +174,17 @@ var MM = (function() {
* argument speed Number - The speed of the hide animation. * argument speed Number - The speed of the hide animation.
* argument callback function - Called when the animation is done. * argument callback function - Called when the animation is done.
*/ */
var hideModule = function(module, speed, callback) { var hideModule = function(module, speed, callback, options) {
options = options || {};
// set lockString if set in options.
if (options.lockString) {
// Log.log("Has lockstring: " + options.lockString);
if (module.lockStrings.indexOf(options.lockString) === -1) {
module.lockStrings.push(options.lockString);
}
}
var moduleWrapper = document.getElementById(module.identifier); var moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper !== null) { if (moduleWrapper !== null) {
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s"; moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
@ -165,10 +193,10 @@ var MM = (function() {
clearTimeout(module.showHideTimer); clearTimeout(module.showHideTimer);
module.showHideTimer = setTimeout(function() { module.showHideTimer = setTimeout(function() {
// To not take up any space, we just make the position absolute. // To not take up any space, we just make the position absolute.
// since it"s fade out anyway, we can see it lay above or // since it's fade out anyway, we can see it lay above or
// below other modules. This works way better than adjusting // below other modules. This works way better than adjusting
// the .display property. // the .display property.
moduleWrapper.style.position = "absolute"; moduleWrapper.style.position = "fixed";
if (typeof callback === "function") { callback(); } if (typeof callback === "function") { callback(); }
}, speed); }, speed);
@ -182,7 +210,30 @@ var MM = (function() {
* argument speed Number - The speed of the show animation. * argument speed Number - The speed of the show animation.
* argument callback function - Called when the animation is done. * argument callback function - Called when the animation is done.
*/ */
var showModule = function(module, speed, callback) { var showModule = function(module, speed, callback, options) {
options = options || {};
// remove lockString if set in options.
if (options.lockString) {
var index = module.lockStrings.indexOf(options.lockString)
if ( index !== -1) {
module.lockStrings.splice(index, 1);
}
}
// Check if there are no more lockstrings set, or the force option is set.
// Otherwise cancel show action.
if (module.lockStrings.length !== 0 && options.force !== true) {
Log.log("Will not show " + module.name + ". LockStrings active: " + module.lockStrings.join(","));
return;
}
// If forced show, clean current lockstrings.
if (module.lockStrings.length !== 0 && options.force === true) {
Log.log("Force show of module: " + module.name);
module.lockStrings = [];
}
var moduleWrapper = document.getElementById(module.identifier); var moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper !== null) { if (moduleWrapper !== null) {
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s"; moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
@ -401,10 +452,11 @@ var MM = (function() {
* argument module Module - The module hide. * argument module Module - The module hide.
* argument speed Number - The speed of the hide animation. * argument speed Number - The speed of the hide animation.
* argument callback function - Called when the animation is done. * argument callback function - Called when the animation is done.
* argument options object - Optional settings for the hide method.
*/ */
hideModule: function(module, speed, callback) { hideModule: function(module, speed, callback, options) {
module.hidden = true; module.hidden = true;
hideModule(module, speed, callback); hideModule(module, speed, callback, options);
}, },
/* showModule(module, speed, callback) /* showModule(module, speed, callback)
@ -413,10 +465,11 @@ var MM = (function() {
* argument module Module - The module show. * argument module Module - The module show.
* argument speed Number - The speed of the show animation. * argument speed Number - The speed of the show animation.
* argument callback function - Called when the animation is done. * argument callback function - Called when the animation is done.
* argument options object - Optional settings for the hide method.
*/ */
showModule: function(module, speed, callback) { showModule: function(module, speed, callback, options) {
module.hidden = false; module.hidden = false;
showModule(module, speed, callback); showModule(module, speed, callback, options);
} }
}; };

View File

@ -14,23 +14,30 @@ var Module = Class.extend({
* All methods (and properties) below can be subclassed. * * All methods (and properties) below can be subclassed. *
*********************************************************/ *********************************************************/
// Set the minimum MagicMirror module version for this module.
requiresVersion: "2.0.0",
// Module config defaults. // Module config defaults.
defaults: {}, defaults: {},
// Timer reference used for showHide animation callbacks. // Timer reference used for showHide animation callbacks.
showHideTimer: null, showHideTimer: null,
// Array to store lockStrings. These stings are used to lock
// visibility when hiding and showing module.
lockStrings: [],
/* init() /* init()
* Is called when the module is instantiated. * Is called when the module is instantiated.
*/ */
init: function() { init: function () {
//Log.log(this.defaults); //Log.log(this.defaults);
}, },
/* start() /* start()
* Is called when the module is started. * Is called when the module is started.
*/ */
start: function() { start: function () {
Log.info("Starting module: " + this.name); Log.info("Starting module: " + this.name);
}, },
@ -39,7 +46,7 @@ var Module = Class.extend({
* *
* return Array<String> - An array with filenames. * return Array<String> - An array with filenames.
*/ */
getScripts: function() { getScripts: function () {
return []; return [];
}, },
@ -48,7 +55,7 @@ var Module = Class.extend({
* *
* return Array<String> - An array with filenames. * return Array<String> - An array with filenames.
*/ */
getStyles: function() { getStyles: function () {
return []; return [];
}, },
@ -57,7 +64,7 @@ var Module = Class.extend({
* *
* return Map<String, String> - A map with langKeys and filenames. * return Map<String, String> - A map with langKeys and filenames.
*/ */
getTranslations: function() { getTranslations: function () {
return false; return false;
}, },
@ -67,7 +74,7 @@ var Module = Class.extend({
* *
* return domobject - The dom to display. * return domobject - The dom to display.
*/ */
getDom: function() { getDom: function () {
var nameWrapper = document.createElement("div"); var nameWrapper = document.createElement("div");
var name = document.createTextNode(this.name); var name = document.createTextNode(this.name);
nameWrapper.appendChild(name); nameWrapper.appendChild(name);
@ -84,6 +91,17 @@ var Module = Class.extend({
return div; return div;
}, },
/* getHeader()
* This method generates the header string which needs to be displayed if a user has a header configured for this module.
* This method is called by the Magic Mirror core, but only if the user has configured a default header for the module.
* This method needs to be subclassed if the module wants to display modified headers on the mirror.
*
* return string - The header to display above the header.
*/
getHeader: function () {
return this.data.header;
},
/* notificationReceived(notification, payload, sender) /* notificationReceived(notification, payload, sender)
* This method is called when a notification arrives. * This method is called when a notification arrives.
* This method is called by the Magic Mirror core. * This method is called by the Magic Mirror core.
@ -92,7 +110,7 @@ var Module = Class.extend({
* argument payload mixed - The payload of the notification. * argument payload mixed - The payload of the notification.
* argument sender Module - The module that sent the notification. * argument sender Module - The module that sent the notification.
*/ */
notificationReceived: function(notification, payload, sender) { notificationReceived: function (notification, payload, sender) {
if (sender) { if (sender) {
Log.log(this.name + " received a module notification: " + notification + " from sender: " + sender.name); Log.log(this.name + " received a module notification: " + notification + " from sender: " + sender.name);
} else { } else {
@ -106,21 +124,21 @@ var Module = Class.extend({
* argument notification string - The identifier of the noitication. * argument notification string - The identifier of the noitication.
* argument payload mixed - The payload of the notification. * argument payload mixed - The payload of the notification.
*/ */
socketNotificationReceived: function(notification, payload) { socketNotificationReceived: function (notification, payload) {
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload); Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
}, },
/* suspend() /* suspend()
* This method is called when a module is hidden. * This method is called when a module is hidden.
*/ */
suspend: function() { suspend: function () {
Log.log(this.name + " is suspended."); Log.log(this.name + " is suspended.");
}, },
/* resume() /* resume()
* This method is called when a module is shown. * This method is called when a module is shown.
*/ */
resume: function() { resume: function () {
Log.log(this.name + " is resumed."); Log.log(this.name + " is resumed.");
}, },
@ -133,7 +151,7 @@ var Module = Class.extend({
* *
* argument data obejct - Module data. * argument data obejct - Module data.
*/ */
setData: function(data) { setData: function (data) {
this.data = data; this.data = data;
this.name = data.name; this.name = data.name;
this.identifier = data.identifier; this.identifier = data.identifier;
@ -147,7 +165,7 @@ var Module = Class.extend({
* *
* argument config obejct - Module config. * argument config obejct - Module config.
*/ */
setConfig: function(config) { setConfig: function (config) {
this.config = Object.assign(this.defaults, config); this.config = Object.assign(this.defaults, config);
}, },
@ -155,13 +173,13 @@ var Module = Class.extend({
* Returns a socket object. If it doesn"t exist, it"s created. * Returns a socket object. If it doesn"t exist, it"s created.
* It also registers the notification callback. * It also registers the notification callback.
*/ */
socket: function() { socket: function () {
if (typeof this._socket === "undefined") { if (typeof this._socket === "undefined") {
this._socket = this._socket = new MMSocket(this.name); this._socket = this._socket = new MMSocket(this.name);
} }
var self = this; var self = this;
this._socket.setNotificationCallback(function(notification, payload) { this._socket.setNotificationCallback(function (notification, payload) {
self.socketNotificationReceived(notification, payload); self.socketNotificationReceived(notification, payload);
}); });
@ -175,7 +193,7 @@ var Module = Class.extend({
* *
* return string - File path. * return string - File path.
*/ */
file: function(file) { file: function (file) {
return this.data.path + "/" + file; return this.data.path + "/" + file;
}, },
@ -184,14 +202,14 @@ var Module = Class.extend({
* *
* argument callback function - Function called when done. * argument callback function - Function called when done.
*/ */
loadStyles: function(callback) { loadStyles: function (callback) {
var self = this; var self = this;
var styles = this.getStyles(); var styles = this.getStyles();
var loadNextStyle = function() { var loadNextStyle = function () {
if (styles.length > 0) { if (styles.length > 0) {
var nextStyle = styles[0]; var nextStyle = styles[0];
Loader.loadFile(nextStyle, self, function() { Loader.loadFile(nextStyle, self, function () {
styles = styles.slice(1); styles = styles.slice(1);
loadNextStyle(); loadNextStyle();
}); });
@ -208,14 +226,14 @@ var Module = Class.extend({
* *
* argument callback function - Function called when done. * argument callback function - Function called when done.
*/ */
loadScripts: function(callback) { loadScripts: function (callback) {
var self = this; var self = this;
var scripts = this.getScripts(); var scripts = this.getScripts();
var loadNextScript = function() { var loadNextScript = function () {
if (scripts.length > 0) { if (scripts.length > 0) {
var nextScript = scripts[0]; var nextScript = scripts[0];
Loader.loadFile(nextScript, self, function() { Loader.loadFile(nextScript, self, function () {
scripts = scripts.slice(1); scripts = scripts.slice(1);
loadNextScript(); loadNextScript();
}); });
@ -232,14 +250,14 @@ var Module = Class.extend({
* *
* argument callback function - Function called when done. * argument callback function - Function called when done.
*/ */
loadTranslations: function(callback) { loadTranslations: function (callback) {
var self = this; var self = this;
var translations = this.getTranslations(); var translations = this.getTranslations();
var lang = config.language.toLowerCase(); var lang = config.language.toLowerCase();
// The variable `first` will contain the first // The variable `first` will contain the first
// defined translation after the following line. // defined translation after the following line.
for (var first in translations) {break;} for (var first in translations) { break; }
if (translations) { if (translations) {
var translationFile = translations[lang] || undefined; var translationFile = translations[lang] || undefined;
@ -248,7 +266,7 @@ var Module = Class.extend({
// If a translation file is set, load it and then also load the fallback translation file. // If a translation file is set, load it and then also load the fallback translation file.
// Otherwise only load the fallback translation file. // Otherwise only load the fallback translation file.
if (translationFile !== undefined && translationFile !== translationsFallbackFile) { if (translationFile !== undefined && translationFile !== translationsFallbackFile) {
Translator.load(self, translationFile, false, function() { Translator.load(self, translationFile, false, function () {
Translator.load(self, translationsFallbackFile, true, callback); Translator.load(self, translationsFallbackFile, true, callback);
}); });
} else { } else {
@ -259,13 +277,13 @@ var Module = Class.extend({
} }
}, },
/* translate(key, defaultValue) /* translate(key, defaultValue)
* Request the translation for a given key. * Request the translation for a given key.
* *
* argument key string - The key of the string to translage * argument key string - The key of the string to translage
* argument defaultValue string - The default value if no translation was found. (Optional) * argument defaultValue string - The default value if no translation was found. (Optional)
*/ */
translate: function(key, defaultValue) { translate: function (key, defaultValue) {
return Translator.translate(this, key) || defaultValue || ""; return Translator.translate(this, key) || defaultValue || "";
}, },
@ -274,7 +292,7 @@ var Module = Class.extend({
* *
* argument speed Number - The speed of the animation. (Optional) * argument speed Number - The speed of the animation. (Optional)
*/ */
updateDom: function(speed) { updateDom: function (speed) {
MM.updateDom(this, speed); MM.updateDom(this, speed);
}, },
@ -284,7 +302,7 @@ var Module = Class.extend({
* argument notification string - The identifier of the noitication. * argument notification string - The identifier of the noitication.
* argument payload mixed - The payload of the notification. * argument payload mixed - The payload of the notification.
*/ */
sendNotification: function(notification, payload) { sendNotification: function (notification, payload) {
MM.sendNotification(notification, payload, this); MM.sendNotification(notification, payload, this);
}, },
@ -294,7 +312,7 @@ var Module = Class.extend({
* argument notification string - The identifier of the noitication. * argument notification string - The identifier of the noitication.
* argument payload mixed - The payload of the notification. * argument payload mixed - The payload of the notification.
*/ */
sendSocketNotification: function(notification, payload) { sendSocketNotification: function (notification, payload) {
this.socket().sendNotification(notification, payload); this.socket().sendNotification(notification, payload);
}, },
@ -303,15 +321,22 @@ var Module = Class.extend({
* *
* argument speed Number - The speed of the hide animation. * argument speed Number - The speed of the hide animation.
* argument callback function - Called when the animation is done. * argument callback function - Called when the animation is done.
* argument options object - Optional settings for the hide method.
*/ */
hide: function(speed, callback) { hide: function (speed, callback, options) {
callback = callback || function() {}; if (typeof callback === "object") {
options = callback;
callback = function () { };
}
callback = callback || function () { };
options = options || {};
var self = this; var self = this;
MM.hideModule(self, speed, function() { MM.hideModule(self, speed, function () {
self.suspend(); self.suspend();
callback(); callback();
}); }, options);
}, },
/* showModule(module, speed, callback) /* showModule(module, speed, callback)
@ -319,29 +344,29 @@ var Module = Class.extend({
* *
* argument speed Number - The speed of the show animation. * argument speed Number - The speed of the show animation.
* argument callback function - Called when the animation is done. * argument callback function - Called when the animation is done.
* argument options object - Optional settings for the hide method.
*/ */
show: function(speed, callback) { show: function (speed, callback, options) {
if (typeof callback === "object") {
options = callback;
callback = function () { };
}
callback = callback || function () { };
options = options || {};
this.resume(); this.resume();
MM.showModule(this, speed, callback); MM.showModule(this, speed, callback, options);
} }
}); });
Module.definitions = {}; Module.definitions = {};
Module.create = function(name) { Module.create = function (name) {
//Define the clone method for later use. // Make sure module definition is available.
function cloneObject(obj) { if (!Module.definitions[name]) {
if (obj === null || typeof obj !== "object") { return;
return obj;
}
var temp = obj.constructor(); // give temp the original obj"s constructor
for (var key in obj) {
temp[key] = cloneObject(obj[key]);
}
return temp;
} }
var moduleDefinition = Module.definitions[name]; var moduleDefinition = Module.definitions[name];
@ -354,7 +379,39 @@ Module.create = function(name) {
}; };
Module.register = function(name, moduleDefinition) { /* cmpVersions(a,b)
* Compare two symantic version numbers and return the difference.
*
* argument a string - Version number a.
* argument a string - Version number b.
*/
function cmpVersions(a, b) {
var i, diff;
var regExStrip0 = /(\.0+)+$/;
var segmentsA = a.replace(regExStrip0, "").split(".");
var segmentsB = b.replace(regExStrip0, "").split(".");
var l = Math.min(segmentsA.length, segmentsB.length);
for (i = 0; i < l; i++) {
diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
if (diff) {
return diff;
}
}
return segmentsA.length - segmentsB.length;
}
Module.register = function (name, moduleDefinition) {
if (moduleDefinition.requiresVersion) {
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + version);
if (cmpVersions(version, moduleDefinition.requiresVersion) >= 0) {
Log.log("Version is ok!");
} else {
Log.log("Version is incorrect. Skip module: '" + name + "'");
return;
}
}
Log.log("Module registered: " + name); Log.log("Module registered: " + name);
Module.definitions[name] = moduleDefinition; Module.definitions[name] = moduleDefinition;
}; };

View File

@ -10,11 +10,24 @@ var app = require("express")();
var server = require("http").Server(app); var server = require("http").Server(app);
var io = require("socket.io")(server); var io = require("socket.io")(server);
var path = require("path"); var path = require("path");
var ipfilter = require("express-ipfilter").IpFilter;
var fs = require("fs");
var Server = function(config, callback) { var Server = function(config, callback) {
console.log("Starting server op port " + config.port + " ... "); console.log("Starting server op port " + config.port + " ... ");
server.listen(config.port); server.listen(config.port);
app.use(function(req, res, next) {
var result = ipfilter(config.ipWhitelist, {mode: "allow", log: false})(req, res, function(err) {
if (err === undefined) {
return next();
}
console.log(err.message);
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
});
});
app.use("/js", express.static(__dirname)); app.use("/js", express.static(__dirname));
app.use("/config", express.static(path.resolve(__dirname + "/../config"))); app.use("/config", express.static(path.resolve(__dirname + "/../config")));
app.use("/css", express.static(path.resolve(__dirname + "/../css"))); app.use("/css", express.static(path.resolve(__dirname + "/../css")));
@ -23,8 +36,15 @@ var Server = function(config, callback) {
app.use("/vendor", express.static(path.resolve(__dirname + "/../vendor"))); app.use("/vendor", express.static(path.resolve(__dirname + "/../vendor")));
app.use("/translations", express.static(path.resolve(__dirname + "/../translations"))); app.use("/translations", express.static(path.resolve(__dirname + "/../translations")));
app.get("/version", function(req,res) {
res.send(global.version);
});
app.get("/", function(req, res) { app.get("/", function(req, res) {
res.sendFile(path.resolve(__dirname + "/../index.html")); var html = fs.readFileSync(path.resolve(__dirname + "/../index.html"), {encoding: "utf8"});
html = html.replace("#VERSION#", global.version);
res.send(html);
}); });
if (typeof callback === "function") { if (typeof callback === "function") {

13
jsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=759670
// for the documentation about the jsconfig.json format
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"allowSyntheticDefaultImports": true
},
"exclude": [
"modules",
"node_modules"
]
}

View File

@ -4,13 +4,13 @@ This document describes the way to develop your own MagicMirror² modules.
## Module structure ## Module structure
All modules are loaded in de `modules` folder. The default modules are grouped together in the `modules/default` folder. Your module should be placed in a subfolder of `modules`. Note that any file or folder your create in the `modules` folder will be ignored by git, allowing you to upgrade the MagicMirror² without the loss of your files. All modules are loaded in the `modules` folder. The default modules are grouped together in the `modules/default` folder. Your module should be placed in a subfolder of `modules`. Note that any file or folder your create in the `modules` folder will be ignored by git, allowing you to upgrade the MagicMirror² without the loss of your files.
A module can be placed in one single folder. Or multiple modules can be grouped in a subfoler. Note that name of the module must be unique. Even when a module with a similar name is placed in a different folder, they can't be loaded at the same time. A module can be placed in one single folder. Or multiple modules can be grouped in a subfolder. Note that name of the module must be unique. Even when a module with a similar name is placed in a different folder, they can't be loaded at the same time.
### Files ### Files
- **modulename/modulename.js** - This is your core module script. - **modulename/modulename.js** - This is your core module script.
- **modulename/node_helper.js** - This is an optional helper that whill be loaded by the node script. The node helper and module script can communicate with each other using an intergrated socket system. - **modulename/node_helper.js** - This is an optional helper that will be loaded by the node script. The node helper and module script can communicate with each other using an intergrated socket system.
- **modulename/public** - Any files in this folder can be accesed via the browser on `/modulename/filename.ext`. - **modulename/public** - Any files in this folder can be accesed via the browser on `/modulename/filename.ext`.
- **modulename/anyfileorfolder** Any other file or folder in the module folder can be used by the core module script. For example: *modulename/css/modulename.css* would be a good path for your additional module styles. - **modulename/anyfileorfolder** Any other file or folder in the module folder can be used by the core module script. For example: *modulename/css/modulename.css* would be a good path for your additional module styles.
@ -78,6 +78,19 @@ The data object contains additional metadata about the module instance:
####`defaults: {}` ####`defaults: {}`
Any properties defined in the defaults object, will be merged with the module config as defined in the user's config.js file. This is the best place to set your modules's configuration defaults. Any of the module configuration properties can be accessed using `this.config.propertyName`, but more about that later. Any properties defined in the defaults object, will be merged with the module config as defined in the user's config.js file. This is the best place to set your modules's configuration defaults. Any of the module configuration properties can be accessed using `this.config.propertyName`, but more about that later.
####'requiresVersion:'
*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. Make sure to also set this value in the Node helper.
**Note:** Since this check is introduced in version 2.1.0, this check will not be run in older versions. Keep this in mind if you get issue reports on your module.
Example:
````javascript
requiresVersion: "2.1.0",
````
### Subclassable module methods ### Subclassable module methods
####`init()` ####`init()`
@ -104,7 +117,7 @@ The getScripts method is called to request any additional scripts that need to b
getScripts: function() { getScripts: function() {
return [ return [
'script.js', // will try to load it from the vendor folder, otherwise it will load is from the module folder. 'script.js', // will try to load it from the vendor folder, otherwise it will load is from the module folder.
'moment.js', // this file is available in the vendor folder, so it doesn't need to be avialable in the module folder. 'moment.js', // this file is available in the vendor folder, so it doesn't need to be available in the module folder.
this.file('anotherfile.js'), // this file will be loaded straight from the module folder. this.file('anotherfile.js'), // this file will be loaded straight from the module folder.
'https://code.jquery.com/jquery-2.2.3.min.js', // this file will be loaded from the jquery servers. 'https://code.jquery.com/jquery-2.2.3.min.js', // this file will be loaded from the jquery servers.
] ]
@ -154,7 +167,7 @@ getTranslations: function() {
####`getDom()` ####`getDom()`
**Should return:** Dom Object **Should return:** Dom Object
Whenever the MagicMirror needs to update the information on screen (because it starts, or because your module asked a refresh using `this.updateDom()`), the system calls the getDom method. This method should therefor return a dom object. Whenever the MagicMirror needs to update the information on screen (because it starts, or because your module asked a refresh using `this.updateDom()`), the system calls the getDom method. This method should therefore return a dom object.
**Example:** **Example:**
````javascript ````javascript
@ -166,6 +179,23 @@ getDom: function() {
```` ````
####`getHeader()`
**Should return:** String
Whenever the MagicMirror needs to update the information on screen (because it starts, or because your module asked a refresh using `this.updateDom()`), the system calls the getHeader method to retrieve the module's header. This method should therefor return a string. If this method is not subclassed, this function will return the user's configured header.
If you want to use the original user's configured header, reference `this.data.header`.
**NOTE:** If the user did not confiugure a default header, no header will be displayed and thus this method will not be called.
**Example:**
````javascript
getHeader: function() {
return this.data.header + ' Foo Bar';
}
````
####`notificationReceived(notification, payload, sender)` ####`notificationReceived(notification, payload, sender)`
That MagicMirror core has the ability to send notifications to modules. Or even better: the modules have the possibility to send notifications to other modules. When this module is called, it has 3 arguments: That MagicMirror core has the ability to send notifications to modules. Or even better: the modules have the possibility to send notifications to other modules. When this module is called, it has 3 arguments:
@ -185,7 +215,7 @@ notificationReceived: function(notification, payload, sender) {
} }
```` ````
**Note:** the system sends two notifiations when starting up. These notifications could come in handy! **Note:** the system sends two notifications when starting up. These notifications could come in handy!
- `ALL_MODULES_STARTED` - All modules are started. You can now send notifications to other modules. - `ALL_MODULES_STARTED` - All modules are started. You can now send notifications to other modules.
@ -198,8 +228,8 @@ When using a node_helper, the node helper can send your module notifications. Wh
- `notification` - String - The notification identifier. - `notification` - String - The notification identifier.
- `payload` - AnyType - The payload of a notification. - `payload` - AnyType - The payload of a notification.
**Note 1:** When a node helper send a notification, all modules of that module type receive the same notifications. <br> **Note 1:** When a node helper sends a notification, all modules of that module type receive the same notifications. <br>
**Note 2:** The socket connection is established as soon as the module sends it's first message using [sendSocketNotification](thissendsocketnotificationnotification-payload). **Note 2:** The socket connection is established as soon as the module sends its first message using [sendSocketNotification](thissendsocketnotificationnotification-payload).
**Example:** **Example:**
````javascript ````javascript
@ -217,7 +247,7 @@ When a module will be shown after it was previously hidden (using the `module.sh
### Module instance methods ### Module instance methods
Each module instance has some handy methods which can be helpfull building your module. Each module instance has some handy methods which can be helpful building your module.
####`this.file(filename)` ####`this.file(filename)`
@ -229,7 +259,7 @@ If you want to create a path to a file in your module folder, use the `file()` m
####`this.updateDom(speed)` ####`this.updateDom(speed)`
***speed* Number** - Optional. Animation speed in milliseconds.<br> ***speed* Number** - Optional. Animation speed in milliseconds.<br>
Whenever your module need to be updated, call the `updateDom(speed)` method. It requests the MagicMirror core to update it's dom object. If you define the speed, the content update will be animated, but only if the content will realy change. Whenever your module need to be updated, call the `updateDom(speed)` method. It requests the MagicMirror core to update its dom object. If you define the speed, the content update will be animated, but only if the content will really change.
As an example: the clock modules calls this method every second: As an example: the clock modules calls this method every second:
@ -248,7 +278,7 @@ start: function() {
***notification* String** - The notification identifier.<br> ***notification* String** - The notification identifier.<br>
***payload* AnyType** - Optional. A notification payload.<br> ***payload* AnyType** - Optional. A notification payload.<br>
If you want to send a notification to all other modules, use the `sendNotification(notification, payload)`. All other modules will receive the message via the [notificationReceived](#notificationreceivednotification-payload-sender) method. In that case, the sender is automaticly set to the instance calling the sendNotification method. If you want to send a notification to all other modules, use the `sendNotification(notification, payload)`. All other modules will receive the message via the [notificationReceived](#notificationreceivednotification-payload-sender) method. In that case, the sender is automatically set to the instance calling the sendNotification method.
**Example:** **Example:**
````javascript ````javascript
@ -259,33 +289,103 @@ this.sendNotification('MYMODULE_READY_FOR_ACTION', {foo:bar});
***notification* String** - The notification identifier.<br> ***notification* String** - The notification identifier.<br>
***payload* AnyType** - Optional. A notification payload.<br> ***payload* AnyType** - Optional. A notification payload.<br>
If you want to send a notification to the node_helper, use the `sendSocketNotification(notification, payload)`. Only the node_helper of this module will recieve the socket notification. If you want to send a notification to the node_helper, use the `sendSocketNotification(notification, payload)`. Only the node_helper of this module will receive the socket notification.
**Example:** **Example:**
````javascript ````javascript
this.sendSocketNotification('SET_CONFIG', this.config); this.sendSocketNotification('SET_CONFIG', this.config);
```` ````
####`this.hide(speed, callback)` ####`this.hide(speed, callback, options)`
***speed* Number** - Optional, The speed of the hide animation in milliseconds. ***speed* Number** - Optional (Required when setting callback or options), The speed of the hide animation in milliseconds.
***callback* Function** - Optional, The callback after the hide animation is finished. ***callback* Function** - Optional, The callback after the hide animation is finished.
***options* Function** - Optional, Object with additional options for the hide action (see below). (*Introduced in version: 2.1.0.*)
To hide a module, you can call the `hide(speed, callback)` method. You can call the hide method on the module instance itself using `this.hide()`, but of course you can also hide another module using `anOtherModule.hide()`.
Possible configurable options:
- `lockString` - String - When setting lock string, the module can not be shown without passing the correct lockstring. This way (multiple) modules can prevent a module from showing. It's considered best practice to use your modules identifier as the locksString: `this.identifier`. See *visibility locking* below.
To hide a module, you can call the `hide(speed, callback)` method. You can call the hide method on the module instance itselve using `this.hide()`, but of course you can also hide an other module using `anOtherModule.hide()`.
**Note 1:** If the hide animation is canceled, for instance because the show method is called before the hide animation was finished, the callback will not be called.<br> **Note 1:** If the hide animation is canceled, for instance because the show method is called before the hide animation was finished, the callback will not be called.<br>
**Note 2:** If the hide animation is hijacked (an other method calls hide on the same module), the callback will not be called.<br> **Note 2:** If the hide animation is hijacked (an other method calls hide on the same module), the callback will not be called.<br>
**Note 3:** If the dom is not yet created, the hide method won't work. Wait for the `DOM_OBJECTS_CREATED` [notification](#notificationreceivednotification-payload-sender). **Note 3:** If the dom is not yet created, the hide method won't work. Wait for the `DOM_OBJECTS_CREATED` [notification](#notificationreceivednotification-payload-sender).
####`this.show(speed, callback)`
***speed* Number** - Optional, The speed of the show animation in milliseconds.
***callback* Function** - Optional, The callback after the show animation is finished.
To show a module, you can call the `show(speed, callback)` method. You can call the show method on the module instance itselve using `this.show()`, but of course you can also show an other module using `anOtherModule.show()`. ####`this.show(speed, callback, options)`
***speed* Number** - Optional (Required when setting callback or options), The speed of the show animation in milliseconds.
***callback* Function** - Optional, The callback after the show animation is finished.
***options* Function** - Optional, Object with additional options for the show action (see below). (*Introduced in version: 2.1.0.*)
To show a module, you can call the `show(speed, callback)` method. You can call the show method on the module instance itself using `this.show()`, but of course you can also show another module using `anOtherModule.show()`.
Possible configurable options:
- `lockString` - String - When setting lock string, the module can not be shown without passing the correct lockstring. This way (multiple) modules can prevent a module from showing. See *visibility locking* below.
- `force` - Boolean - When setting the force tag to `true`, the locking mechanism will be overwritten. Use this option with caution. It's considered best practice to let the usage of the force option be use- configurable. See *visibility locking* below.
**Note 1:** If the show animation is canceled, for instance because the hide method is called before the show animation was finished, the callback will not be called.<br> **Note 1:** If the show animation is canceled, for instance because the hide method is called before the show animation was finished, the callback will not be called.<br>
**Note 2:** If the show animation is hijacked (an other method calls show on the same module), the callback will not be called.<br> **Note 2:** If the show animation is hijacked (an other method calls show on the same module), the callback will not be called.<br>
**Note 3:** If the dom is not yet created, the show method won't work. Wait for the `DOM_OBJECTS_CREATED` [notification](#notificationreceivednotification-payload-sender). **Note 3:** If the dom is not yet created, the show method won't work. Wait for the `DOM_OBJECTS_CREATED` [notification](#notificationreceivednotification-payload-sender).
####Visibility locking
(*Introduced in version: 2.1.0.*)
Visiblity locking helps the module system to prevent unwanted hide/show actions. The following scenario explains the concept:
**Module B asks module A to hide:**
````javascript
moduleA.hide(0, {lockString: "module_b_identifier"});
````
Module A is now hidden, and has an lock array with the following strings:
````javascript
moduleA.lockStrings == ["module_b_identifier"]
moduleA.hidden == true
````
**Module C asks module A to hide:**
````javascript
moduleA.hide(0, {lockString: "module_c_identifier"});
````
Module A is now hidden, and has an lock array with the following strings:
````javascript
moduleA.lockStrings == ["module_b_identifier", "module_c_identifier"]
moduleA.hidden == true
````
**Module B asks module A to show:**
````javascript
moduleA.show(0, {lockString: "module_b_identifier"});
````
The lockString will be removed from moduleAs locks array, but since there still is an other lock string available, the module remains hidden:
````javascript
moduleA.lockStrings == ["module_c_identifier"]
moduleA.hidden == true
````
**Module C asks module A to show:**
````javascript
moduleA.show(0, {lockString: "module_c_identifier"});
````
The lockString will be removed from moduleAs locks array, and since this will result in an empty lock array, the module will be visible:
````javascript
moduleA.lockStrings == []
moduleA.hidden == false
````
**Note:** The locking mechanism can be overwritten by using the force tag:
````javascript
moduleA.show(0, {force: true});
````
This will reset the lockstring array, and will show the module.
````javascript
moduleA.lockStrings == []
moduleA.hidden == false
````
Use this `force` method with caution. See `show()` method for more information.
####`this.translate(identifier)` ####`this.translate(identifier)`
***identifier* String** - Identifier of the string that should be translated. ***identifier* String** - Identifier of the string that should be translated.
@ -328,7 +428,7 @@ var NodeHelper = require("node_helper");
module.exports = NodeHelper.create({}); module.exports = NodeHelper.create({});
```` ````
Of course, the above helper would not do anything usefull. So with the information above, you should be able to make it a bit more sophisticated. Of course, the above helper would not do anything useful. So with the information above, you should be able to make it a bit more sophisticated.
### Available module instance properties ### Available module instance properties
@ -356,7 +456,7 @@ start: function() {
} }
```` ````
**Note: ** By default, a public path to your module's public folder will be created: **Note:** By default, a public path to your module's public folder will be created:
````javascript ````javascript
this.expressApp.use("/" + this.name, express.static(this.path + "/public")); this.expressApp.use("/" + this.name, express.static(this.path + "/public"));
```` ````
@ -366,13 +466,26 @@ this.expressApp.use("/" + this.name, express.static(this.path + "/public"));
This is a link to the IO instance. It will allow you to do some Socket.IO magic. In most cases you won't need this, since the Node Helper has a few convenience methods to make this simple. This is a link to the IO instance. It will allow you to do some Socket.IO magic. In most cases you won't need this, since the Node Helper has a few convenience methods to make this simple.
####'requiresVersion:'
*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.
**Note:** Since this check is introduced in version 2.1.0, this check will not be run in older versions. Keep this in mind if you get issue reports on your module.
Example:
````javascript
requiresVersion: "2.1.0",
````
### Subclassable module methods ### Subclassable module methods
####`init()` ####`init()`
This method is called when a node helper gets instantiated. In most cases you do not need to subclass this method. This method is called when a node helper gets instantiated. In most cases you do not need to subclass this method.
####`start()` ####`start()`
This method is called when all node helper are loaded an the system is ready to boot up. The start method is a perfect place to define any additional module properties: This method is called when all node helpers are loaded and the system is ready to boot up. The start method is a perfect place to define any additional module properties:
**Example:** **Example:**
````javascript ````javascript
@ -383,12 +496,12 @@ start: function() {
```` ````
####`socketNotificationReceived: function(notification, payload)` ####`socketNotificationReceived: function(notification, payload)`
With this method, your node helper can receive notifications form your modules. When this method is called, it has 2 arguments: With this method, your node helper can receive notifications from your modules. When this method is called, it has 2 arguments:
- `notification` - String - The notification identifier. - `notification` - String - The notification identifier.
- `payload` - AnyType - The payload of a notification. - `payload` - AnyType - The payload of a notification.
**Note:** The socket connection is established as soon as the module sends it's first message using [sendSocketNotification](thissendsocketnotificationnotification-payload). **Note:** The socket connection is established as soon as the module sends its first message using [sendSocketNotification](thissendsocketnotificationnotification-payload).
**Example:** **Example:**
````javascript ````javascript
@ -399,15 +512,15 @@ socketNotificationReceived: function(notification, payload) {
### Module instance methods ### Module instance methods
Each node helper has some handy methods which can be helpfull building your module. Each node helper has some handy methods which can be helpful building your module.
####`this.sendSocketNotification(notification, payload)` ####`this.sendSocketNotification(notification, payload)`
***notification* String** - The notification identifier.<br> ***notification* String** - The notification identifier.<br>
***payload* AnyType** - Optional. A notification payload.<br> ***payload* AnyType** - Optional. A notification payload.<br>
If you want to send a notification to all your modules, use the `sendSocketNotification(notification, payload)`. Only the module of your module type will recieve the socket notification. If you want to send a notification to all your modules, use the `sendSocketNotification(notification, payload)`. Only the module of your module type will receive the socket notification.
**Note:** Since all instances of you module will receive the notifications, it's your task to make sure the right module responds to your messages. **Note:** Since all instances of your module will receive the notifications, it's your task to make sure the right module responds to your messages.
**Example:** **Example:**
````javascript ````javascript
@ -430,10 +543,10 @@ To make a selection of all currently loaded module instances, run the `MM.getMod
#####`.withClass(classnames)` #####`.withClass(classnames)`
***classnames* String or Array** - The class names on which you want to filer. ***classnames* String or Array** - The class names on which you want to filter.
**Returns Array** - An array with module instances.<br> **Returns Array** - An array with module instances.<br>
If you want to make a selection based on one ore more class names, use the withClass method on a result of the `MM.getModules()` method. The argument of the `withClass(classname)` method can be an array, or space separated string. If you want to make a selection based on one or more class names, use the withClass method on a result of the `MM.getModules()` method. The argument of the `withClass(classname)` method can be an array, or space separated string.
**Examples:** **Examples:**
````javascript ````javascript
@ -459,7 +572,7 @@ var modules = MM.getModules().exceptWithClass(['classname1','classname2']);
***module* Module Object** - The reference to a module you want to remove from the results. ***module* Module Object** - The reference to a module you want to remove from the results.
**Returns Array** - An array with module instances.<br> **Returns Array** - An array with module instances.<br>
If you to remove a specific module instance from a selection based on a classname, use the exceptWithClass method on a result of the `MM.getModules()` method. This can be helpfull if you want to select all module instances except the instance of your module. If you to remove a specific module instance from a selection based on a classname, use the exceptWithClass method on a result of the `MM.getModules()` method. This can be helpful if you want to select all module instances except the instance of your module.
**Examples:** **Examples:**
````javascript ````javascript

View File

@ -106,7 +106,6 @@ The following properties can be configured:
<td><code>titleReplace</code></td> <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> <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> <br><b>Example:</b> <br>
<code> <code>
titleReplace: {'Birthday of ' : '', 'foo':'bar'} titleReplace: {'Birthday of ' : '', 'foo':'bar'}
</code> </code>
@ -126,6 +125,13 @@ The following properties can be configured:
<br><b>Default value:</b> <code>relative</code> <br><b>Default value:</b> <code>relative</code>
</td> </td>
</tr> </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> <tr>
<td><code>urgency</code></td> <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> <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>
@ -134,6 +140,13 @@ The following properties can be configured:
<br><b>Default value:</b> <code>0</code> (disabled) <br><b>Default value:</b> <code>0</code> (disabled)
</td> </td>
</tr> </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>
</tbody> </tbody>
</table> </table>
@ -173,7 +186,7 @@ config: {
<tr> <tr>
<td><code> symbol </code></td> <td><code> symbol </code></td>
<td>The symbol to show in front of an event. This property is optional.<br> <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 Awsome</a> website. <br><b>Possible values:</b> See <a href="http://fontawesome.io/icons/" target="_blank">Font Awesome</a> website.
</td> </td>
</tr> </tr>
<tr> <tr>
@ -183,5 +196,13 @@ config: {
<code>'Birthday'</code> <code>'Birthday'</code>
</td> </td>
</tr> </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> </tbody>
</table> </table>

View File

@ -7,7 +7,7 @@
* MIT Licensed. * MIT Licensed.
*/ */
Module.register("calendar",{ Module.register("calendar", {
// Define module defaults // Define module defaults
defaults: { defaults: {
@ -16,13 +16,14 @@ Module.register("calendar",{
displaySymbol: true, displaySymbol: true,
defaultSymbol: "calendar", // Fontawesome Symbol see http://fontawesome.io/cheatsheet/ defaultSymbol: "calendar", // Fontawesome Symbol see http://fontawesome.io/cheatsheet/
displayRepeatingCountTitle: false, displayRepeatingCountTitle: false,
defaultRepeatingCountTitle: '', defaultRepeatingCountTitle: "",
maxTitleLength: 25, maxTitleLength: 25,
fetchInterval: 5 * 60 * 1000, // Update every 5 minutes. fetchInterval: 5 * 60 * 1000, // Update every 5 minutes.
animationSpeed: 2000, animationSpeed: 2000,
fade: true, fade: true,
urgency: 7, urgency: 7,
timeFormat: "relative", timeFormat: "relative",
getRelative: 6,
fadePoint: 0.25, // Start on 1/4th of the list. fadePoint: 0.25, // Start on 1/4th of the list.
calendars: [ calendars: [
{ {
@ -34,20 +35,21 @@ Module.register("calendar",{
"De verjaardag van ": "", "De verjaardag van ": "",
"'s birthday": "" "'s birthday": ""
}, },
broadcastEvents: true
}, },
// Define required scripts. // Define required scripts.
getStyles: function() { getStyles: function () {
return ["calendar.css", "font-awesome.css"]; return ["calendar.css", "font-awesome.css"];
}, },
// Define required scripts. // Define required scripts.
getScripts: function() { getScripts: function () {
return ["moment.js"]; return ["moment.js"];
}, },
// Define required translations. // Define required translations.
getTranslations: function() { getTranslations: function () {
// The translations for the defaut modules are defined in the core translation files. // 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. // Therefor we can just return false. Otherwise we should have returned a dictionairy.
// If you're trying to build your own module including translations, check out the documentation. // If you're trying to build your own module including translations, check out the documentation.
@ -55,7 +57,7 @@ Module.register("calendar",{
}, },
// Override start method. // Override start method.
start: function() { start: function () {
Log.log("Starting module: " + this.name); Log.log("Starting module: " + this.name);
// Set locale. // Set locale.
@ -64,7 +66,7 @@ Module.register("calendar",{
for (var c in this.config.calendars) { for (var c in this.config.calendars) {
var calendar = this.config.calendars[c]; var calendar = this.config.calendars[c];
calendar.url = calendar.url.replace("webcal://", "http://"); calendar.url = calendar.url.replace("webcal://", "http://");
this.addCalendar(calendar.url); this.addCalendar(calendar.url, calendar.user, calendar.pass);
} }
this.calendarData = {}; this.calendarData = {};
@ -72,11 +74,15 @@ Module.register("calendar",{
}, },
// Override socket notification handler. // Override socket notification handler.
socketNotificationReceived: function(notification, payload) { socketNotificationReceived: function (notification, payload) {
if (notification === "CALENDAR_EVENTS") { if (notification === "CALENDAR_EVENTS") {
if (this.hasCalendarURL(payload.url)) { if (this.hasCalendarURL(payload.url)) {
this.calendarData[payload.url] = payload.events; this.calendarData[payload.url] = payload.events;
this.loaded = true; this.loaded = true;
if (this.config.broadcastEvents) {
this.broadcastEvents();
}
} }
} else if (notification === "FETCH_ERROR") { } else if (notification === "FETCH_ERROR") {
Log.error("Calendar Error. Could not fetch calendar: " + payload.url); Log.error("Calendar Error. Could not fetch calendar: " + payload.url);
@ -90,7 +96,7 @@ Module.register("calendar",{
}, },
// Override dom generator. // Override dom generator.
getDom: function() { getDom: function () {
var events = this.createEventList(); var events = this.createEventList();
var wrapper = document.createElement("table"); var wrapper = document.createElement("table");
@ -109,27 +115,27 @@ Module.register("calendar",{
eventWrapper.className = "normal"; eventWrapper.className = "normal";
if (this.config.displaySymbol) { if (this.config.displaySymbol) {
var symbolWrapper = document.createElement("td"); var symbolWrapper = document.createElement("td");
symbolWrapper.className = "symbol"; symbolWrapper.className = "symbol";
var symbol = document.createElement("span"); var symbol = document.createElement("span");
symbol.className = "fa fa-" + this.symbolForUrl(event.url); symbol.className = "fa fa-" + this.symbolForUrl(event.url);
symbolWrapper.appendChild(symbol); symbolWrapper.appendChild(symbol);
eventWrapper.appendChild(symbolWrapper); eventWrapper.appendChild(symbolWrapper);
} }
var titleWrapper = document.createElement("td"), var titleWrapper = document.createElement("td"),
repeatingCountTitle = ''; repeatingCountTitle = "";
if (this.config.displayRepeatingCountTitle) { if (this.config.displayRepeatingCountTitle) {
repeatingCountTitle = this.countTitleForUrl(event.url); repeatingCountTitle = this.countTitleForUrl(event.url);
if(repeatingCountTitle !== '') { if (repeatingCountTitle !== "") {
var thisYear = new Date().getFullYear(), var thisYear = new Date().getFullYear(),
yearDiff = thisYear - event.firstYear; yearDiff = thisYear - event.firstYear;
repeatingCountTitle = ', '+ yearDiff + '. ' + repeatingCountTitle; repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle;
} }
} }
@ -137,19 +143,25 @@ Module.register("calendar",{
titleWrapper.className = "title bright"; titleWrapper.className = "title bright";
eventWrapper.appendChild(titleWrapper); eventWrapper.appendChild(titleWrapper);
var timeWrapper = document.createElement("td"); var timeWrapper = document.createElement("td");
//console.log(event.today); //console.log(event.today);
var now = new Date(); var now = new Date();
// Define second, minute, hour, and day variables // Define second, minute, hour, and day variables
var one_second = 1000; // 1,000 milliseconds var oneSecond = 1000; // 1,000 milliseconds
var one_minute = one_second * 60; var oneMinute = oneSecond * 60;
var one_hour = one_minute * 60; var oneHour = oneMinute * 60;
var one_day = one_hour * 24; var oneDay = oneHour * 24;
if (event.fullDayEvent) { if (event.fullDayEvent) {
if (event.today) { if (event.today) {
timeWrapper.innerHTML = this.translate("TODAY"); timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
} else if (event.startDate - now < one_day && event.startDate - now > 0) { } else if (event.startDate - now < oneDay && event.startDate - now > 0) {
timeWrapper.innerHTML = this.translate("TOMORROW"); timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
} else {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
}
} else { } else {
/* Check to see if the user displays absolute or relative dates with their events /* Check to see if the user displays absolute or relative dates with their events
* Also check to see if an event is happening within an 'urgency' time frameElement * Also check to see if an event is happening within an 'urgency' time frameElement
@ -159,26 +171,26 @@ Module.register("calendar",{
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim * Note: this needs to be put in its own function, as the whole thing repeats again verbatim
*/ */
if (this.config.timeFormat === "absolute") { if (this.config.timeFormat === "absolute") {
if ((this.config.urgency > 1) && (event.startDate - now < (this.config.urgency * one_day))) { if ((this.config.urgency > 1) && (event.startDate - now < (this.config.urgency * oneDay))) {
// This event falls within the config.urgency period that the user has set // This event falls within the config.urgency period that the user has set
timeWrapper.innerHTML = moment(event.startDate, "x").fromNow(); timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
} else { } else {
timeWrapper.innerHTML = moment(event.startDate, "x").format("MMM Do"); timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format("MMM Do"));
} }
} else { } else {
timeWrapper.innerHTML = moment(event.startDate, "x").fromNow(); timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
} }
} }
} else { } else {
if (event.startDate >= new Date()) { if (event.startDate >= new Date()) {
if (event.startDate - now < 2 * one_day) { if (event.startDate - now < 2 * oneDay) {
// This event is within the next 48 hours (2 days) // This event is within the next 48 hours (2 days)
if (event.startDate - now < 6 * one_hour) { if (event.startDate - now < this.config.getRelative * oneHour) {
// If event is within 6 hour, display 'in xxx' time format or moment.fromNow() // If event is within 6 hour, display 'in xxx' time format or moment.fromNow()
timeWrapper.innerHTML = moment(event.startDate, "x").fromNow(); timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
} else { } else {
// Otherwise just say 'Today/Tomorrow at such-n-such time' // Otherwise just say 'Today/Tomorrow at such-n-such time'
timeWrapper.innerHTML = moment(event.startDate, "x").calendar(); timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
} }
} else { } else {
/* Check to see if the user displays absolute or relative dates with their events /* Check to see if the user displays absolute or relative dates with their events
@ -189,18 +201,18 @@ Module.register("calendar",{
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim * Note: this needs to be put in its own function, as the whole thing repeats again verbatim
*/ */
if (this.config.timeFormat === "absolute") { if (this.config.timeFormat === "absolute") {
if ((this.config.urgency > 1) && (event.startDate - now < (this.config.urgency * one_day))) { if ((this.config.urgency > 1) && (event.startDate - now < (this.config.urgency * oneDay))) {
// This event falls within the config.urgency period that the user has set // This event falls within the config.urgency period that the user has set
timeWrapper.innerHTML = moment(event.startDate, "x").fromNow(); timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
} else { } else {
timeWrapper.innerHTML = moment(event.startDate, "x").format("MMM Do"); timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format("MMM Do"));
} }
} else { } else {
timeWrapper.innerHTML = moment(event.startDate, "x").fromNow(); timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
} }
} }
} else { } else {
timeWrapper.innerHTML = this.translate("RUNNING") + ' ' + moment(event.endDate,"x").fromNow(true); timeWrapper.innerHTML = this.capFirst(this.translate("RUNNING")) + " " + moment(event.endDate, "x").fromNow(true);
} }
} }
//timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll'); //timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll');
@ -234,7 +246,7 @@ Module.register("calendar",{
* *
* return bool - Has calendar url * return bool - Has calendar url
*/ */
hasCalendarURL: function(url) { hasCalendarURL: function (url) {
for (var c in this.config.calendars) { for (var c in this.config.calendars) {
var calendar = this.config.calendars[c]; var calendar = this.config.calendars[c];
if (calendar.url === url) { if (calendar.url === url) {
@ -250,7 +262,7 @@ Module.register("calendar",{
* *
* return array - Array with events. * return array - Array with events.
*/ */
createEventList: function() { createEventList: function () {
var events = []; var events = [];
var today = moment().startOf("day"); var today = moment().startOf("day");
for (var c in this.calendarData) { for (var c in this.calendarData) {
@ -263,7 +275,7 @@ Module.register("calendar",{
} }
} }
events.sort(function(a, b) { events.sort(function (a, b) {
return a.startDate - b.startDate; return a.startDate - b.startDate;
}); });
@ -275,12 +287,14 @@ Module.register("calendar",{
* *
* argument url sting - Url to add. * argument url sting - Url to add.
*/ */
addCalendar: function(url) { addCalendar: function (url, user, pass) {
this.sendSocketNotification("ADD_CALENDAR", { this.sendSocketNotification("ADD_CALENDAR", {
url: url, url: url,
maximumEntries: this.config.maximumEntries, maximumEntries: this.config.maximumEntries,
maximumNumberOfDays: this.config.maximumNumberOfDays, maximumNumberOfDays: this.config.maximumNumberOfDays,
fetchInterval: this.config.fetchInterval fetchInterval: this.config.fetchInterval,
user: user,
pass: pass
}); });
}, },
@ -291,10 +305,10 @@ Module.register("calendar",{
* *
* return string - The Symbol * return string - The Symbol
*/ */
symbolForUrl: function(url) { symbolForUrl: function (url) {
for (var c in this.config.calendars) { for (var c in this.config.calendars) {
var calendar = this.config.calendars[c]; var calendar = this.config.calendars[c];
if (calendar.url === url && typeof calendar.symbol === "string") { if (calendar.url === url && typeof calendar.symbol === "string") {
return calendar.symbol; return calendar.symbol;
} }
} }
@ -308,10 +322,10 @@ Module.register("calendar",{
* *
* return string - The Symbol * return string - The Symbol
*/ */
countTitleForUrl: function(url) { countTitleForUrl: function (url) {
for (var c in this.config.calendars) { for (var c in this.config.calendars) {
var calendar = this.config.calendars[c]; var calendar = this.config.calendars[c];
if (calendar.url === url && typeof calendar.repeatingCountTitle === "string") { if (calendar.url === url && typeof calendar.repeatingCountTitle === "string") {
return calendar.repeatingCountTitle; return calendar.repeatingCountTitle;
} }
} }
@ -328,14 +342,23 @@ Module.register("calendar",{
* *
* return string - The shortened string. * return string - The shortened string.
*/ */
shorten: function(string, maxLength) { shorten: function (string, maxLength) {
if (string.length > maxLength) { if (string.length > maxLength) {
return string.slice(0,maxLength) + "&hellip;"; return string.slice(0, maxLength) + "&hellip;";
} }
return string; return string;
}, },
/* capFirst(string)
* Capitalize the first letter of a string
* Eeturn capitalized string
*/
capFirst: function (string) {
return string.charAt(0).toUpperCase() + string.slice(1);
},
/* titleTransform(title) /* titleTransform(title)
* Transforms the title of an event for usage. * Transforms the title of an event for usage.
* Replaces parts of the text as defined in config.titleReplace. * Replaces parts of the text as defined in config.titleReplace.
@ -345,7 +368,7 @@ Module.register("calendar",{
* *
* return string - The transformed title. * return string - The transformed title.
*/ */
titleTransform: function(title) { titleTransform: function (title) {
for (var needle in this.config.titleReplace) { for (var needle in this.config.titleReplace) {
var replacement = this.config.titleReplace[needle]; var replacement = this.config.titleReplace[needle];
title = title.replace(needle, replacement); title = title.replace(needle, replacement);
@ -353,5 +376,28 @@ Module.register("calendar",{
title = this.shorten(title, this.config.maxTitleLength); title = this.shorten(title, this.config.maxTitleLength);
return title; return title;
},
/* broadcastEvents()
* Broadcasts the events to all other modules for reuse.
* The all events available in one array, sorted on startdate.
*/
broadcastEvents: function () {
var eventList = [];
for (url in this.calendarData) {
var calendar = this.calendarData[url];
for (e in calendar) {
var event = cloneObject(calendar[e]);
delete event.url;
eventList.push(event);
}
}
eventList.sort(function(a,b) {
return a.startDate - b.startDate;
});
this.sendNotification("CALENDAR_EVENTS", eventList);
} }
}); });

View File

@ -8,7 +8,7 @@
var ical = require("./vendor/ical.js"); var ical = require("./vendor/ical.js");
var moment = require("moment"); var moment = require("moment");
var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumberOfDays) { var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumberOfDays, user, pass) {
var self = this; var self = this;
var reloadTimer = null; var reloadTimer = null;
@ -27,9 +27,18 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
var opts = { var opts = {
headers: { headers: {
'User-Agent': 'Mozilla/5.0 (Node.js 6.0.0) MagicMirror/v2 (https://github.com/MichMich/MagicMirror/)' "User-Agent": "Mozilla/5.0 (Node.js 6.0.0) MagicMirror/v2 (https://github.com/MichMich/MagicMirror/)"
}
};
if (user && pass) {
opts.auth = {
user: user,
pass: pass,
sendImmediately: true
} }
} }
ical.fromURL(url, opts, function(err, data) { ical.fromURL(url, opts, function(err, data) {
if (err) { if (err) {
fetchFailedCallback(self, err); fetchFailedCallback(self, err);
@ -68,7 +77,7 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
if (!isFacebookBirthday) { if (!isFacebookBirthday) {
endDate = startDate; endDate = startDate;
} else { } else {
endDate = moment(startDate).add(1, 'days'); endDate = moment(startDate).add(1, "days");
} }
} }
@ -92,7 +101,7 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
for (var d in dates) { for (var d in dates) {
startDate = moment(new Date(dates[d])); startDate = moment(new Date(dates[d]));
endDate = moment(parseInt(startDate.format("x")) + duration, 'x'); endDate = moment(parseInt(startDate.format("x")) + duration, "x");
if (endDate.format("x") > now) { if (endDate.format("x") > now) {
newEvents.push({ newEvents.push({
title: title, title: title,
@ -123,12 +132,19 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
continue; continue;
} }
// Every thing is good. Add it to the list.
var location = event.location || false;
var geo = event.geo || false;
// Every thing is good. Add it to the list.
newEvents.push({ newEvents.push({
title: title, title: title,
startDate: startDate.format("x"), startDate: startDate.format("x"),
endDate: endDate.format("x"), endDate: endDate.format("x"),
fullDayEvent: fullDayEvent fullDayEvent: fullDayEvent,
location: location,
geo: geo
}); });
} }
@ -239,4 +255,4 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe
}; };
module.exports = CalendarFetcher; module.exports = CalendarFetcher;

View File

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

View File

@ -66,6 +66,13 @@ The following properties can be configured:
<br><b>Default value:</b> <code>false</code> <br><b>Default value:</b> <code>false</code>
</td> </td>
</tr> </tr>
<tr>
<td><code>showDate</code></td>
<td>Turn off or on the Date section.<br>
<br><b>Possible values:</b> <code>true</code> or <code>false</code>
<br><b>Default value:</b> <code>true</code>
</td>
</tr>
<tr> <tr>
<td><code>displayType</code></td> <td><code>displayType</code></td>
<td>Display a digital clock, analog clock, or both together.<br> <td>Display a digital clock, analog clock, or both together.<br>
@ -109,4 +116,4 @@ The following properties can be configured:
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -15,6 +15,7 @@ Module.register("clock",{
showPeriod: true, showPeriod: true,
showPeriodUpper: false, showPeriodUpper: false,
clockBold: false, clockBold: false,
showDate: true,
/* specific to the analog clock */ /* specific to the analog clock */
analogSize: '200px', analogSize: '200px',
@ -85,7 +86,9 @@ Module.register("clock",{
timeString = moment().format("h:mm"); timeString = moment().format("h:mm");
} }
} }
if(this.config.showDate){
dateWrapper.innerHTML = moment().format("dddd, LL"); dateWrapper.innerHTML = moment().format("dddd, LL");
}
timeWrapper.innerHTML = timeString; timeWrapper.innerHTML = timeString;
secondsWrapper.innerHTML = moment().format("ss"); secondsWrapper.innerHTML = moment().format("ss");
if (this.config.showPeriodUpper) { if (this.config.showPeriodUpper) {
@ -224,4 +227,4 @@ Module.register("clock",{
// Return the wrapper to the dom. // Return the wrapper to the dom.
return wrapper; return wrapper;
} }
}); });

View File

@ -1,74 +1,68 @@
#analog {
}
#digital {
}
.clockCircle { .clockCircle {
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
border-radius: 50%; border-radius: 50%;
background-size: 100%; background-size: 100%;
} }
.clockFace { .clockFace {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.clockFace:after { .clockFace::after {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
width: 6px; width: 6px;
height: 6px; height: 6px;
margin: -3px 0 0 -3px; margin: -3px 0 0 -3px;
background: white; background: white;
border-radius: 3px; border-radius: 3px;
content: ""; content: "";
display: block; display: block;
} }
.clockHour { .clockHour {
width: 0; width: 0;
height: 0; height: 0;
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
margin: -2px 0 -2px -25%; /* numbers much match negative length & thickness */ margin: -2px 0 -2px -25%; /* numbers much match negative length & thickness */
padding: 2px 0 2px 25%; /* indicator length & thickness */ padding: 2px 0 2px 25%; /* indicator length & thickness */
background: white; background: white;
-webkit-transform-origin: 100% 50%; -webkit-transform-origin: 100% 50%;
-ms-transform-origin: 100% 50%; -ms-transform-origin: 100% 50%;
transform-origin: 100% 50%; transform-origin: 100% 50%;
border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px;
} }
.clockMinute { .clockMinute {
width: 0; width: 0;
height: 0; height: 0;
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
margin: -35% -2px 0; /* numbers must match negative length & thickness */ margin: -35% -2px 0; /* numbers must match negative length & thickness */
padding: 35% 2px 0; /* indicator length & thickness */ padding: 35% 2px 0; /* indicator length & thickness */
background: white; background: white;
-webkit-transform-origin: 50% 100%; -webkit-transform-origin: 50% 100%;
-ms-transform-origin: 50% 100%; -ms-transform-origin: 50% 100%;
transform-origin: 50% 100%; transform-origin: 50% 100%;
border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px;
} }
.clockSecond { .clockSecond {
width: 0; width: 0;
height: 0; height: 0;
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
margin: -38% -1px 0 0; /* numbers must match negative length & thickness */ margin: -38% -1px 0 0; /* numbers must match negative length & thickness */
padding: 38% 1px 0 0; /* indicator length & thickness */ padding: 38% 1px 0 0; /* indicator length & thickness */
background: #888888; background: #888;
-webkit-transform-origin: 50% 100%; -webkit-transform-origin: 50% 100%;
-ms-transform-origin: 50% 100%; -ms-transform-origin: 50% 100%;
transform-origin: 50% 100%; transform-origin: 50% 100%;
} }

View File

@ -35,19 +35,20 @@ The following properties can be configured:
</tr> </tr>
<thead> <thead>
<tbody> <tbody>
<tr> <tr>
<td><code>location</code></td> <td><code>location</code></td>
<td>The location used for weather information.<br> <td>The location used for weather information.<br>
<br><b>Example:</b> <code>Amsterdam,Netherlands</code> <br><b>Example:</b> <code>'Amsterdam,Netherlands'</code>
<br><b>Default value:</b> <code>New York</code> <br><b>Default value:</b> <code>New York</code><br><br>
<strong>Note:</strong> When the <code>location</code> and <code>locationID</code> are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
</td> </td>
</tr> </tr>
<tr> <tr>
<td><code>locationID</code></td> <td><code>locationID</code></td>
<td>Location ID from <a href="http://bulk.openweather.org/sample/">OpenWeather</a> <b>This will override anything you put in location.</b><br>Leave blank if you want to use location. <td>Location ID from <a href="http://bulk.openweather.org/sample/">OpenWeather</a> <b>This will override anything you put in location.</b><br>Leave blank if you want to use location.
<br><b>Example:</b> <code>1234567</code> <br><b>Example:</b> <code>1234567</code>
<br><b>Default value:</b> <code></code> <br><b>Default value:</b> <code></code><br><br>
<strong>Note:</strong> When the <code>location</code> and <code>locationID</code> are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
</td> </td>
</tr> </tr>
<tr> <tr>
@ -106,6 +107,13 @@ The following properties can be configured:
</td> </td>
</tr> </tr>
<tr> <tr>
<td><code>showHumidity</code></td>
<td>Show the current humidity<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>useBeaufort</code></td> <td><code>useBeaufort</code></td>
<td>Pick between using the Beaufort scale for wind speed or using the default units.<br> <td>Pick between using the Beaufort scale for wind speed or using the default units.<br>
<br><b>Possible values:</b> <code>true</code> or <code>false</code> <br><b>Possible values:</b> <code>true</code> or <code>false</code>
@ -151,6 +159,18 @@ The following properties can be configured:
<br><b>Default value:</b> <code>'weather'</code> <br><b>Default value:</b> <code>'weather'</code>
</td> </td>
</tr> </tr>
<tr>
<td><code>appendLocationNameToHeader</code></td>
<td>If set to <code>true</code>, the returned location name will be appended to the header of the module, if the header is enabled. This is mainly intresting when using calender based weather.<br>
<br><b>Default value:</b> <code>true</code>
</td>
</tr>
<tr>
<td><code>calendarClass</code></td>
<td>The class for the calender module to base the event based weather information on.<br>
<br><b>Default value:</b> <code>'calendar'</code>
</td>
</tr>
<tr> <tr>
<td><code>iconTable</code></td> <td><code>iconTable</code></td>
<td>The conversion table to convert the weather conditions to weather-icons.<br> <td>The conversion table to convert the weather conditions to weather-icons.<br>

View File

@ -6,3 +6,11 @@
-webkit-transform: translate(0, -3px); /* Safari */ -webkit-transform: translate(0, -3px); /* Safari */
transform: translate(0, -3px); transform: translate(0, -3px);
} }
.currentweather .humidityIcon {
padding-right: 4px;
}
.currentweather .humidity-padding {
padding-bottom: 6px;
}

View File

@ -11,8 +11,8 @@ Module.register("currentweather",{
// Default module config. // Default module config.
defaults: { defaults: {
location: "", location: false,
locationID: "", locationID: false,
appid: "", appid: "",
units: config.units, units: config.units,
updateInterval: 10 * 60 * 1000, // every 10 minutes updateInterval: 10 * 60 * 1000, // every 10 minutes
@ -23,6 +23,7 @@ Module.register("currentweather",{
showWindDirection: true, showWindDirection: true,
useBeaufort: true, useBeaufort: true,
lang: config.language, lang: config.language,
showHumidity: false,
initialLoadDelay: 0, // 0 seconds delay initialLoadDelay: 0, // 0 seconds delay
retryDelay: 2500, retryDelay: 2500,
@ -31,6 +32,9 @@ Module.register("currentweather",{
apiBase: "http://api.openweathermap.org/data/", apiBase: "http://api.openweathermap.org/data/",
weatherEndpoint: "weather", weatherEndpoint: "weather",
appendLocationNameToHeader: true,
calendarClass: "calendar",
iconTable: { iconTable: {
"01d": "wi-day-sunny", "01d": "wi-day-sunny",
"02d": "wi-day-cloudy", "02d": "wi-day-cloudy",
@ -53,6 +57,12 @@ Module.register("currentweather",{
}, },
}, },
// create a variable for the first upcoming calendaar event. Used if no location is specified.
firstEvent: false,
// create a variable to hold the location name based on the API result.
fetchedLocatioName: "",
// Define required scripts. // Define required scripts.
getScripts: function() { getScripts: function() {
return ["moment.js"]; return ["moment.js"];
@ -102,12 +112,6 @@ Module.register("currentweather",{
return wrapper; return wrapper;
} }
if (this.config.location === "") {
wrapper.innerHTML = "Please set the openweather <i>location</i> in the config for module: " + this.name + ".";
wrapper.className = "dimmed light small";
return wrapper;
}
if (!this.loaded) { if (!this.loaded) {
wrapper.innerHTML = this.translate('LOADING'); wrapper.innerHTML = this.translate('LOADING');
wrapper.className = "dimmed light small"; wrapper.className = "dimmed light small";
@ -117,6 +121,7 @@ Module.register("currentweather",{
var small = document.createElement("div"); var small = document.createElement("div");
small.className = "normal medium"; small.className = "normal medium";
var windIcon = document.createElement("span"); var windIcon = document.createElement("span");
windIcon.className = "wi wi-strong-wind dimmed"; windIcon.className = "wi wi-strong-wind dimmed";
small.appendChild(windIcon); small.appendChild(windIcon);
@ -134,6 +139,22 @@ Module.register("currentweather",{
spacer.innerHTML = "&nbsp;"; spacer.innerHTML = "&nbsp;";
small.appendChild(spacer); small.appendChild(spacer);
if (this.config.showHumidity) {
var humidity = document.createElement("span");
humidity.innerHTML = this.humidity;
var spacer = document.createElement("sup");
spacer.innerHTML = "&nbsp;";
var humidityIcon = document.createElement("sup");
humidityIcon.className = "wi wi-humidity humidityIcon";
humidityIcon.innerHTML = "&nbsp;";
small.appendChild(humidity);
small.appendChild(spacer);
small.appendChild(humidityIcon);
}
var sunriseSunsetIcon = document.createElement("span"); var sunriseSunsetIcon = document.createElement("span");
sunriseSunsetIcon.className = "wi dimmed " + this.sunriseSunsetIcon; sunriseSunsetIcon.className = "wi dimmed " + this.sunriseSunsetIcon;
small.appendChild(sunriseSunsetIcon); small.appendChild(sunriseSunsetIcon);
@ -142,6 +163,8 @@ Module.register("currentweather",{
sunriseSunsetTime.innerHTML = " " + this.sunriseSunsetTime; sunriseSunsetTime.innerHTML = " " + this.sunriseSunsetTime;
small.appendChild(sunriseSunsetTime); small.appendChild(sunriseSunsetTime);
wrapper.appendChild(small);
var large = document.createElement("div"); var large = document.createElement("div");
large.className = "large light"; large.className = "large light";
@ -154,17 +177,55 @@ Module.register("currentweather",{
temperature.innerHTML = " " + this.temperature + "&deg;"; temperature.innerHTML = " " + this.temperature + "&deg;";
large.appendChild(temperature); large.appendChild(temperature);
wrapper.appendChild(small);
wrapper.appendChild(large); wrapper.appendChild(large);
return wrapper; return wrapper;
}, },
// Override getHeader method.
getHeader: function() {
if (this.config.appendLocationNameToHeader) {
return this.data.header + " " + this.fetchedLocatioName;
}
return this.data.header;
},
// Override notification handler.
notificationReceived: function(notification, payload, sender) {
if (notification === "DOM_OBJECTS_CREATED") {
if (this.config.appendLocationNameToHeader) {
this.hide(0, {lockString: this.identifier});
}
}
if (notification === "CALENDAR_EVENTS") {
var senderClasses = sender.data.classes.toLowerCase().split(" ");
if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
var lastEvent = this.firstEvent;
this.firstEvent = false;
for (e in payload) {
var event = payload[e];
if (event.location || event.geo) {
this.firstEvent = event;
//Log.log("First upcoming event with location: ", event);
break;
}
}
}
}
},
/* updateWeather(compliments) /* updateWeather(compliments)
* Requests new data from openweather.org. * Requests new data from openweather.org.
* Calls processWeather on succesfull response. * Calls processWeather on succesfull response.
*/ */
updateWeather: function() { updateWeather: function() {
var url = this.config.apiBase + this.config.apiVersion + "/" + this.config.weatherEndpoint + '/' + this.getParams(); if (this.config.appid === "") {
Log.error("CurrentWeather: APPID not set!");
return;
}
var url = this.config.apiBase + this.config.apiVersion + "/" + this.config.weatherEndpoint + this.getParams();
var self = this; var self = this;
var retry = true; var retry = true;
@ -175,11 +236,10 @@ Module.register("currentweather",{
if (this.status === 200) { if (this.status === 200) {
self.processWeather(JSON.parse(this.response)); self.processWeather(JSON.parse(this.response));
} else if (this.status === 401) { } else if (this.status === 401) {
self.config.appid = "";
self.updateDom(self.config.animationSpeed); self.updateDom(self.config.animationSpeed);
Log.error(self.name + ": Incorrect APPID."); Log.error(self.name + ": Incorrect APPID.");
retry = false; retry = true;
} else { } else {
Log.error(self.name + ": Could not load weather."); Log.error(self.name + ": Could not load weather.");
} }
@ -199,11 +259,19 @@ Module.register("currentweather",{
*/ */
getParams: function() { getParams: function() {
var params = "?"; var params = "?";
if(this.config.locationID !== "") { if(this.config.locationID !== false) {
params += "id=" + this.config.locationID; params += "id=" + this.config.locationID;
} else { } else if(this.config.location !== false) {
params += "q=" + this.config.location; params += "q=" + this.config.location;
} else if (this.firstEvent && this.firstEvent.geo) {
params += "lat=" + this.firstEvent.geo.lat + "&lon=" + this.firstEvent.geo.lon
} else if (this.firstEvent && this.firstEvent.location) {
params += "q=" + this.firstEvent.location;
} else {
this.hide(this.config.animationSpeed, {lockString:this.identifier});
return;
} }
params += "&units=" + this.config.units; params += "&units=" + this.config.units;
params += "&lang=" + this.config.lang; params += "&lang=" + this.config.lang;
params += "&APPID=" + this.config.appid; params += "&APPID=" + this.config.appid;
@ -224,6 +292,7 @@ Module.register("currentweather",{
return; return;
} }
this.humidity = parseFloat(data.main.humidity);
this.temperature = this.roundValue(data.main.temp); this.temperature = this.roundValue(data.main.temp);
if (this.config.useBeaufort){ if (this.config.useBeaufort){
@ -265,7 +334,7 @@ Module.register("currentweather",{
this.sunriseSunsetIcon = (sunrise < now && sunset > now) ? "wi-sunset" : "wi-sunrise"; this.sunriseSunsetIcon = (sunrise < now && sunset > now) ? "wi-sunset" : "wi-sunrise";
this.show(this.config.animationSpeed, {lockString:this.identifier});
this.loaded = true; this.loaded = true;
this.updateDom(this.config.animationSpeed); this.updateDom(this.config.animationSpeed);
}, },

View File

@ -15,7 +15,8 @@ var defaultModules = [
"currentweather", "currentweather",
"helloworld", "helloworld",
"newsfeed", "newsfeed",
"weatherforecast" "weatherforecast",
"updatenotification"
]; ];
/*************** DO NOT EDIT THE LINE BELOW ***************/ /*************** DO NOT EDIT THE LINE BELOW ***************/

View File

@ -98,7 +98,7 @@ The following properties can be configured:
<td><code>animationSpeed</code></td> <td><code>animationSpeed</code></td>
<td>Speed of the update animation. (Milliseconds)<br> <td>Speed of the update animation. (Milliseconds)<br>
<br><b>Possible values:</b><code>0</code> - <code>5000</code> <br><b>Possible values:</b><code>0</code> - <code>5000</code>
<br><b>Default value:</b> <code>2000</code> (2.5 seconds) <br><b>Default value:</b> <code>2000</code> (2 seconds)
</td> </td>
</tr> </tr>
<tr> <tr>
@ -108,7 +108,37 @@ The following properties can be configured:
<br><b>Default value:</b> <code>0</code> <br><b>Default value:</b> <code>0</code>
</td> </td>
</tr> </tr>
removeStartTags: false,
removeEndTags: false,
startTags: [],
endTags: []
<tr>
<td><code>removeStartTags</code></td>
<td>Some newsfeeds feature tags at the <B>beginning</B> of their titles or descriptions, such as <em>[VIDEO]</em>.
This setting allows for the removal of specified tags from the beginning of an item's description and/or title.<br>
<br><b>Possible values:</b><code>'title'</code>, <code>'description'</code>, <code>'both'</code>
</td>
</tr>
<tr>
<td><code>startTags</code></td>
<td>List the tags you would like to have removed at the beginning of the feed item<br>
<br><b>Possible values:</b> <code>['TAG']</code> or <code>['TAG1','TAG2',...]</code>
</td>
</tr>
<tr>
<td><code>removeEndTags</code></td>
<td>Remove specified tags from the <B>end</B> of an item's description and/or title.<br>
<br><b>Possible values:</b><code>'title'</code>, <code>'description'</code>, <code>'both'</code>
</td>
</tr>
<tr>
<td><code>endTags</code></td>
<td>List the tags you would like to have removed at the end of the feed item<br>
<br><b>Possible values:</b> <code>['TAG']</code> or <code>['TAG1','TAG2',...]</code>
</td>
</tr>
</tbody> </tbody>
</table> </table>

View File

@ -60,11 +60,11 @@ var Fetcher = function(url, reloadInterval, encoding) {
} else { } else {
console.log("Can't parse feed item:"); // console.log("Can't parse feed item:");
console.log(item); // console.log(item);
console.log('Title: ' + title); // console.log('Title: ' + title);
console.log('Description: ' + description); // console.log('Description: ' + description);
console.log('Pubdate: ' + pubdate); // console.log('Pubdate: ' + pubdate);
} }
}); });

View File

@ -24,7 +24,12 @@ Module.register("newsfeed",{
reloadInterval: 5 * 60 * 1000, // every 5 minutes reloadInterval: 5 * 60 * 1000, // every 5 minutes
updateInterval: 10 * 1000, updateInterval: 10 * 1000,
animationSpeed: 2.5 * 1000, animationSpeed: 2.5 * 1000,
maxNewsItems: 0 // 0 for unlimited maxNewsItems: 0, // 0 for unlimited
removeStartTags: '',
removeEndTags: '',
startTags: [],
endTags: []
}, },
// Define required scripts. // Define required scripts.
@ -96,11 +101,54 @@ Module.register("newsfeed",{
wrapper.appendChild(sourceAndTimestamp); wrapper.appendChild(sourceAndTimestamp);
} }
//Remove selected tags from the beginning of rss feed items (title or description)
if (this.config.removeStartTags == 'title' || 'both') {
for (f=0; f<this.config.startTags.length;f++) {
if (this.newsItems[this.activeItem].title.slice(0,this.config.startTags[f].length) == this.config.startTags[f]) {
this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].title.slice(this.config.startTags[f].length,this.newsItems[this.activeItem].title.length);
}
}
}
if (this.config.removeStartTags == 'description' || 'both') {
if (this.config.showDescription) {
for (f=0; f<this.config.startTags.length;f++) {
if (this.newsItems[this.activeItem].description.slice(0,this.config.startTags[f].length) == this.config.startTags[f]) {
this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].description.slice(this.config.startTags[f].length,this.newsItems[this.activeItem].description.length);
}
}
}
}
//Remove selected tags from the end of rss feed items (title or description)
if (this.config.removeEndTags) {
for (f=0; f<this.config.endTags.length;f++) {
if (this.newsItems[this.activeItem].title.slice(-this.config.endTags[f].length)==this.config.endTags[f]) {
this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].title.slice(0,-this.config.endTags[f].length);
}
}
if (this.config.showDescription) {
for (f=0; f<this.config.endTags.length;f++) {
if (this.newsItems[this.activeItem].description.slice(-this.config.endTags[f].length)==this.config.endTags[f]) {
this.newsItems[this.activeItem].description = this.newsItems[this.activeItem].description.slice(0,-this.config.endTags[f].length);
}
}
}
}
var title = document.createElement("div"); var title = document.createElement("div");
title.className = "bright medium light"; title.className = "bright medium light";
title.innerHTML = this.newsItems[this.activeItem].title; title.innerHTML = this.newsItems[this.activeItem].title;
wrapper.appendChild(title); wrapper.appendChild(title);
if (this.config.showDescription) { if (this.config.showDescription) {
var description = document.createElement("div"); var description = document.createElement("div");
description.className = "small light"; description.className = "small light";
@ -215,5 +263,7 @@ Module.register("newsfeed",{
*/ */
capitalizeFirstLetter: function(string) { capitalizeFirstLetter: function(string) {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
} },
}); });

View File

@ -0,0 +1,42 @@
# Module: Update Notification
The `updatenotification` module is one of the default modules of the MagicMirror.
This will display a message whenever a new version of the MagicMirror application is available.
## Using the module
To use this module, add it to the modules array in the `config/config.js` file:
````javascript
modules: [
{
module: 'updatenotification',
position: 'top_center', // This can be any of the regions.
config: {
// The config property is optional.
// See 'Configuration options' for more information.
}
}
]
````
## Configuration options
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>updateInterval</code></td>
<td>How often do you want to check for a new version? This value represents the interval in milliseconds.<br>
<br><b>Possible values:</b> Any value above <code>60000</code> (1 minute);
<br><b>Default value:</b> <code>600000</code> (10 minutes);
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,44 @@
var simpleGit = require("simple-git")(__dirname + "/../..");
var NodeHelper = require("node_helper");
module.exports = NodeHelper.create({
config: {},
updateTimer: null,
start: function () {
},
socketNotificationReceived: function (notification, payload) {
if (notification === "CONFIG") {
this.config = payload;
this.preformFetch();
}
},
preformFetch() {
var self = this;
simpleGit.fetch().status(function(err, data) {
if (!err) {
self.sendSocketNotification("STATUS", data);
}
});
this.scheduleNextFetch(this.config.updateInterval);
},
scheduleNextFetch: function(delay) {
if (delay < 60 * 1000) {
delay = 60 * 1000
}
var self = this;
clearTimeout(this.updateTimer);
this.updateTimer = setTimeout(function() {
self.preformFetch();
}, delay);
}
});

View File

@ -0,0 +1,67 @@
Module.register("updatenotification", {
defaults: {
updateInterval: 10 * 60 * 1000, // every 10 minutes
},
status: false,
start: function () {
Log.log("Start updatenotification");
},
notificationReceived: function(notification, payload, sender) {
if (notification === "DOM_OBJECTS_CREATED") {
this.sendSocketNotification("CONFIG", this.config);
this.hide(0,{lockString: self.identifier});
}
},
socketNotificationReceived: function (notification, payload) {
if (notification === "STATUS") {
this.status = payload;
this.updateUI();
}
},
updateUI: function() {
var self = this;
if (this.status && this.status.behind > 0) {
self.updateDom(0);
self.show(1000, {lockString: self.identifier});
}
},
// Override dom generator.
getDom: function () {
var wrapper = document.createElement("div");
if (this.status && this.status.behind > 0) {
var message = document.createElement("div");
message.className = "small bright";
var icon = document.createElement("i");
icon.className = "fa fa-exclamation-circle";
icon.innerHTML = "&nbsp;";
message.appendChild(icon);
var text = document.createElement("span");
text.innerHTML = this.translate("UPDATE_NOTIFICATION");
message.appendChild(text);
wrapper.appendChild(message);
var subtext = document.createElement("div");
subtext.innerHTML = this.translate("UPDATE_INFO")
.replace("COMMIT_COUNT", this.status.behind + " " + ((this.status.behind == 1)? 'commit' : 'commits'))
.replace("BRANCH_NAME", this.status.current);
subtext.className = "xsmall dimmed";
wrapper.appendChild(subtext);
}
return wrapper;
}
});

View File

@ -35,19 +35,20 @@ The following properties can be configured:
</tr> </tr>
<thead> <thead>
<tbody> <tbody>
<tr> <tr>
<td><code>location</code></td> <td><code>location</code></td>
<td>The location used for weather information.<br> <td>The location used for weather information.<br>
<br><b>Example:</b> <code>Amsterdam,Netherlands</code> <br><b>Example:</b> <code>'Amsterdam,Netherlands'</code>
<br><b>Default value:</b> <code>New York</code> <br><b>Default value:</b> <code>New York</code><br><br>
<strong>Note:</strong> When the <code>location</code> and <code>locationID</code> are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
</td> </td>
</tr> </tr>
<tr> <tr>
<td><code>locationID</code></td> <td><code>locationID</code></td>
<td>Location ID from <a href="http://bulk.openweather.org/sample/">OpenWeather</a> <b>This will override anything you put in location.</b><br>Leave blank if you want to use location. <td>Location ID from <a href="http://bulk.openweather.org/sample/">OpenWeather</a> <b>This will override anything you put in location.</b><br>Leave blank if you want to use location.
<br><b>Example:</b> <code>1234567</code> <br><b>Example:</b> <code>1234567</code>
<br><b>Default value:</b> <code></code> <br><b>Default value:</b> <code></code><br><br>
<strong>Note:</strong> When the <code>location</code> and <code>locationID</code> are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used.
</td> </td>
</tr> </tr>
<tr> <tr>
@ -71,6 +72,14 @@ The following properties can be configured:
<br>This value is optional. By default the weatherforecast module will return 7 days. <br>This value is optional. By default the weatherforecast module will return 7 days.
</td> </td>
</tr> </tr>
<tr>
<td><code>showRainAmount</code></td>
<td>Should the predicted rain amount be displayed?<br>
<br><b>Possible values:</b> <code>true</code> or <code>false</code>
<br><b>Default value:</b> <code>false</code>
<br>This value is optional. By default the weatherforecast module will not display the predicted amount of rain.
</td>
</tr>
<tr> <tr>
<td><code>updateInterval</code></td> <td><code>updateInterval</code></td>
<td>How often does the content needs to be fetched? (Milliseconds)<br> <td>How often does the content needs to be fetched? (Milliseconds)<br>
@ -85,7 +94,6 @@ The following properties can be configured:
<br><b>Default value:</b> <code>2000</code> (2 seconds) <br><b>Default value:</b> <code>2000</code> (2 seconds)
</td> </td>
</tr> </tr>
<tr> <tr>
<td><code>lang</code></td> <td><code>lang</code></td>
<td>The language of the days.<br> <td>The language of the days.<br>
@ -139,6 +147,18 @@ The following properties can be configured:
<br><b>Default value:</b> <code>'forecast/daily'</code> <br><b>Default value:</b> <code>'forecast/daily'</code>
</td> </td>
</tr> </tr>
<tr>
<td><code>appendLocationNameToHeader</code></td>
<td>If set to <code>true</code>, the returned location name will be appended to the header of the module, if the header is enabled. This is mainly intresting when using calender based weather.<br>
<br><b>Default value:</b> <code>true</code>
</td>
</tr>
<tr>
<td><code>calendarClass</code></td>
<td>The class for the calender module to base the event based weather information on.<br>
<br><b>Default value:</b> <code>'calendar'</code>
</td>
</tr>
<tr> <tr>
<td><code>iconTable</code></td> <td><code>iconTable</code></td>
<td>The conversion table to convert the weather conditions to weather-icons.<br> <td>The conversion table to convert the weather conditions to weather-icons.<br>
@ -164,6 +184,5 @@ The following properties can be configured:
}</code> }</code>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -12,3 +12,8 @@
padding-left: 20px; padding-left: 20px;
padding-right: 0; padding-right: 0;
} }
.weatherforecast .rain {
padding-left: 20px;
padding-right: 0;
}

View File

@ -11,11 +11,12 @@ Module.register("weatherforecast",{
// Default module config. // Default module config.
defaults: { defaults: {
location: "", location: false,
locationID: "", locationID: false,
appid: "", appid: "",
units: config.units, units: config.units,
maxNumberOfDays: 7, maxNumberOfDays: 7,
showRainAmount: false,
updateInterval: 10 * 60 * 1000, // every 10 minutes updateInterval: 10 * 60 * 1000, // every 10 minutes
animationSpeed: 1000, animationSpeed: 1000,
timeFormat: config.timeFormat, timeFormat: config.timeFormat,
@ -30,6 +31,9 @@ Module.register("weatherforecast",{
apiBase: "http://api.openweathermap.org/data/", apiBase: "http://api.openweathermap.org/data/",
forecastEndpoint: "forecast/daily", forecastEndpoint: "forecast/daily",
appendLocationNameToHeader: true,
calendarClass: "calendar",
iconTable: { iconTable: {
"01d": "wi-day-sunny", "01d": "wi-day-sunny",
"02d": "wi-day-cloudy", "02d": "wi-day-cloudy",
@ -52,6 +56,12 @@ Module.register("weatherforecast",{
}, },
}, },
// create a variable for the first upcoming calendaar event. Used if no location is specified.
firstEvent: false,
// create a variable to hold the location name based on the API result.
fetchedLocatioName: "",
// Define required scripts. // Define required scripts.
getScripts: function() { getScripts: function() {
return ["moment.js"]; return ["moment.js"];
@ -95,14 +105,8 @@ Module.register("weatherforecast",{
return wrapper; return wrapper;
} }
if (this.config.location === "") {
wrapper.innerHTML = "Please set the openweather <i>location</i> in the config for module: " + this.name + ".";
wrapper.className = "dimmed light small";
return wrapper;
}
if (!this.loaded) { if (!this.loaded) {
wrapper.innerHTML = this.translate('LOADING'); wrapper.innerHTML = this.translate("LOADING");
wrapper.className = "dimmed light small"; wrapper.className = "dimmed light small";
return wrapper; return wrapper;
} }
@ -139,6 +143,17 @@ Module.register("weatherforecast",{
minTempCell.className = "align-right min-temp"; minTempCell.className = "align-right min-temp";
row.appendChild(minTempCell); row.appendChild(minTempCell);
if (this.config.showRainAmount) {
var rainCell = document.createElement("td");
if (isNaN(forecast.rain)) {
rainCell.innerHTML = "";
} else {
rainCell.innerHTML = forecast.rain + " mm";
}
rainCell.className = "align-right bright rain";
row.appendChild(rainCell);
}
if (this.config.fade && this.config.fadePoint < 1) { if (this.config.fade && this.config.fadePoint < 1) {
if (this.config.fadePoint < 0) { if (this.config.fadePoint < 0) {
this.config.fadePoint = 0; this.config.fadePoint = 0;
@ -156,12 +171,51 @@ Module.register("weatherforecast",{
return table; return table;
}, },
// Override getHeader method.
getHeader: function() {
if (this.config.appendLocationNameToHeader) {
return this.data.header + " " + this.fetchedLocatioName;
}
return this.data.header;
},
// Override notification handler.
notificationReceived: function(notification, payload, sender) {
if (notification === "DOM_OBJECTS_CREATED") {
if (this.config.appendLocationNameToHeader) {
this.hide(0, {lockString: this.identifier});
}
}
if (notification === "CALENDAR_EVENTS") {
var senderClasses = sender.data.classes.toLowerCase().split(" ");
if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
var lastEvent = this.firstEvent;
this.firstEvent = false;
for (e in payload) {
var event = payload[e];
if (event.location || event.geo) {
this.firstEvent = event;
//Log.log("First upcoming event with location: ", event);
break;
}
}
}
}
},
/* updateWeather(compliments) /* updateWeather(compliments)
* Requests new data from openweather.org. * Requests new data from openweather.org.
* Calls processWeather on succesfull response. * Calls processWeather on succesfull response.
*/ */
updateWeather: function() { updateWeather: function() {
var url = this.config.apiBase + this.config.apiVersion + "/" + this.config.forecastEndpoint + '/' + this.getParams(); if (this.config.appid === "") {
Log.error("WeatherForecast: APPID not set!");
return;
}
var url = this.config.apiBase + this.config.apiVersion + "/" + this.config.forecastEndpoint + this.getParams();
var self = this; var self = this;
var retry = true; var retry = true;
@ -172,11 +226,10 @@ Module.register("weatherforecast",{
if (this.status === 200) { if (this.status === 200) {
self.processWeather(JSON.parse(this.response)); self.processWeather(JSON.parse(this.response));
} else if (this.status === 401) { } else if (this.status === 401) {
self.config.appid = "";
self.updateDom(self.config.animationSpeed); self.updateDom(self.config.animationSpeed);
Log.error(self.name + ": Incorrect APPID."); Log.error(self.name + ": Incorrect APPID.");
retry = false; retry = true;
} else { } else {
Log.error(self.name + ": Could not load weather."); Log.error(self.name + ": Could not load weather.");
} }
@ -196,11 +249,19 @@ Module.register("weatherforecast",{
*/ */
getParams: function() { getParams: function() {
var params = "?"; var params = "?";
if(this.config.locationID !== "") { if(this.config.locationID !== false) {
params += "id=" + this.config.locationID; params += "id=" + this.config.locationID;
} else { } else if(this.config.location !== false) {
params += "q=" + this.config.location; params += "q=" + this.config.location;
} else if (this.firstEvent && this.firstEvent.geo) {
params += "lat=" + this.firstEvent.geo.lat + "&lon=" + this.firstEvent.geo.lon
} else if (this.firstEvent && this.firstEvent.location) {
params += "q=" + this.firstEvent.location;
} else {
this.hide(this.config.animationSpeed, {lockString:this.identifier});
return;
} }
params += "&units=" + this.config.units; params += "&units=" + this.config.units;
params += "&lang=" + this.config.lang; params += "&lang=" + this.config.lang;
/* /*
@ -220,6 +281,7 @@ Module.register("weatherforecast",{
* argument data object - Weather information received form openweather.org. * argument data object - Weather information received form openweather.org.
*/ */
processWeather: function(data) { processWeather: function(data) {
this.fetchedLocatioName = data.city.name + ", " + data.city.country;
this.forecast = []; this.forecast = [];
for (var i = 0, count = data.list.length; i < count; i++) { for (var i = 0, count = data.list.length; i < count; i++) {
@ -230,13 +292,14 @@ Module.register("weatherforecast",{
day: moment(forecast.dt, "X").format("ddd"), day: moment(forecast.dt, "X").format("ddd"),
icon: this.config.iconTable[forecast.weather[0].icon], icon: this.config.iconTable[forecast.weather[0].icon],
maxTemp: this.roundValue(forecast.temp.max), maxTemp: this.roundValue(forecast.temp.max),
minTemp: this.roundValue(forecast.temp.min) minTemp: this.roundValue(forecast.temp.min),
rain: this.roundValue(forecast.rain)
}); });
} }
//Log.log(this.forecast); //Log.log(this.forecast);
this.show(this.config.animationSpeed, {lockString:this.identifier});
this.loaded = true; this.loaded = true;
this.updateDom(this.config.animationSpeed); this.updateDom(this.config.animationSpeed);
}, },

View File

@ -1,13 +1,10 @@
{ {
"name": "magicmirror", "name": "magicmirror",
"version": "2.0.0", "version": "2.1.0",
"description": "A modular interface for smart mirrors.", "description": "A modular interface for smart mirrors.",
"main": "js/electron.js", "main": "js/electron.js",
"scripts": { "scripts": {
"start": "electron js/electron.js", "start": "electron js/electron.js",
"jscs": "jscs **/**/**/**/*.js",
"stylelint": "stylelint css/main.css fonts/roboto.css",
"htmlvalidator": "html-validator --file=index.html",
"test": "snyk test", "test": "snyk test",
"snyk-protect": "snyk protect", "snyk-protect": "snyk protect",
"prepublish": "npm run snyk-protect" "prepublish": "npm run snyk-protect"
@ -30,26 +27,29 @@
}, },
"homepage": "https://github.com/MichMich/MagicMirror#readme", "homepage": "https://github.com/MichMich/MagicMirror#readme",
"devDependencies": { "devDependencies": {
"electron-prebuilt": "latest",
"grunt": "latest", "grunt": "latest",
"grunt-eslint": "latest", "grunt-eslint": "latest",
"grunt-postcss": "latest", "grunt-jsonlint": "latest",
"postcss-reporter": "latest", "grunt-markdownlint": "^1.0.4",
"stylelint": "latest", "grunt-stylelint": "latest",
"grunt-yamllint": "latest",
"stylelint-config-standard": "latest", "stylelint-config-standard": "latest",
"time-grunt": "latest" "time-grunt": "latest"
}, },
"dependencies": { "dependencies": {
"electron-prebuilt": "^0.37.2",
"express": "^4.14.0", "express": "^4.14.0",
"express-ipfilter": "latest",
"feedme": "latest", "feedme": "latest",
"iconv-lite": "latest", "iconv-lite": "latest",
"moment": "latest", "moment": "latest",
"request": "^2.74.0", "request": "^2.74.0",
"rrule": "latest", "rrule": "latest",
"simple-git": "^1.54.0",
"snyk": "^1.14.1", "snyk": "^1.14.1",
"socket.io": "^1.4.6", "socket.io": "^1.4.6",
"valid-url": "latest", "valid-url": "latest",
"walk": "latest" "walk": "latest"
}, },
"snyk": true "snyk": true
} }

View File

@ -0,0 +1,8 @@
[Plymouth Theme]
Name=MagicMirror
Description=Mirror Splash
ModuleName=script
[script]
ImageDir=/usr/share/plymouth/themes/MagicMirror
ScriptFile=/usr/share/plymouth/themes/MagicMirror/MagicMirror.script

View File

@ -0,0 +1,50 @@
screen_width = Window.GetWidth();
screen_height = Window.GetHeight();
theme_image = Image("splash.png");
image_width = theme_image.GetWidth();
image_height = theme_image.GetHeight();
scale_x = image_width / screen_width;
scale_y = image_height / screen_height;
flag = 1;
if (scale_x > 1 || scale_y > 1)
{
if (scale_x > scale_y)
{
resized_image = theme_image.Scale (screen_width, image_height / scale_x);
image_x = 0;
image_y = (screen_height - ((image_height * screen_width) / image_width)) / 2;
}
else
{
resized_image = theme_image.Scale (image_width / scale_y, screen_height);
image_x = (screen_width - ((image_width * screen_height) / image_height)) / 2;
image_y = 0;
}
}
else
{
resized_image = theme_image.Scale (image_width, image_height);
image_x = (screen_width - image_width) / 2;
image_y = (screen_height - image_height) / 2;
}
if (Plymouth.GetMode() != "shutdown")
{
sprite = Sprite (resized_image);
sprite.SetPosition (image_x, image_y, -100);
}
message_sprite = Sprite();
message_sprite.SetPosition(screen_width * 0.1, screen_height * 0.9, 10000);
fun message_callback (text) {
my_image = Image.Text(text, 1, 1, 1);
message_sprite.SetImage(my_image);
sprite.SetImage (resized_image);
}
Plymouth.SetUpdateStatusFunction(message_callback);

BIN
splashscreen/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

28
translations/da.json Normal file
View File

@ -0,0 +1,28 @@
{
/* GENERAL */
"LOADING": "Indlæser &hellip;",
/* CALENDAR */
"TODAY": "I dag",
"TOMORROW": "I morgen",
"RUNNING": "Slutter om",
"EMPTY": "Ingen kommende begivenheder.",
/* WEATHER */
"N": "N",
"NNE": "NNØ",
"NE": "NØ",
"ENE": "ØNØ",
"E": "Ø",
"ESE": "ØSØ",
"SE": "SØ",
"SSE": "SSØ",
"S": "S",
"SSW": "SSV",
"SW": "SV",
"WSW": "VSV",
"W": "V",
"WNW": "VNV",
"NW": "NV",
"NNW": "NNV"
}

View File

@ -5,6 +5,7 @@
/* CALENDAR */ /* CALENDAR */
"TODAY": "Heute", "TODAY": "Heute",
"TOMORROW": "Morgen", "TOMORROW": "Morgen",
"DAYAFTERTOMORROW": "&Uuml;bermorgen",
"RUNNING": "noch", "RUNNING": "noch",
"EMPTY": "Keine Termine.", "EMPTY": "Keine Termine.",
@ -24,5 +25,9 @@
"W": "W", "W": "W",
"WNW": "WNW", "WNW": "WNW",
"NW": "NW", "NW": "NW",
"NNW": "NNW" "NNW": "NNW",
/* UPDATE INFO */
"UPDATE_NOTIFICATION": "Aktualisierung für MagicMirror² verfügbar.",
"UPDATE_INFO": "Die aktuelle Installation ist COMMIT_COUNT hinter dem BRANCH_NAME branch."
} }

View File

@ -24,5 +24,9 @@
"W": "W", "W": "W",
"WNW": "WNW", "WNW": "WNW",
"NW": "NW", "NW": "NW",
"NNW": "NNW" "NNW": "NNW",
/* UPDATE INFO */
"UPDATE_NOTIFICATION": "MagicMirror² update available.",
"UPDATE_INFO": "The current installation is COMMIT_COUNT behind on the BRANCH_NAME branch."
} }

28
translations/fi.json Normal file
View File

@ -0,0 +1,28 @@
{
/* GENERAL */
"LOADING": "Lataa &hellip;",
/* CALENDAR */
"TODAY": "Tänään",
"TOMORROW": "Huomenna",
"RUNNING": "Meneillään",
"EMPTY": "Ei tulevia tapahtumia.",
/* WEATHER */
"N": "P",
"NNE": "PPI",
"NE": "PI",
"ENE": "IPI",
"E": "I",
"ESE": "IEI",
"SE": "EI",
"SSE": "EEI",
"S": "E",
"SSW": "EEL",
"SW": "EL",
"WSW": "LEL",
"W": "L",
"WNW": "LPL",
"NW": "PL",
"NNW": "PPL"
}

View File

@ -5,6 +5,7 @@
/* CALENDAR */ /* CALENDAR */
"TODAY": "Hjoed", "TODAY": "Hjoed",
"TOMORROW": "Moarn", "TOMORROW": "Moarn",
"DAYAFTERTOMORROW": "Oaremoarn",
"RUNNING": "Einigest oer", "RUNNING": "Einigest oer",
"EMPTY": "Gjin plande &ocirc;fspraken.", "EMPTY": "Gjin plande &ocirc;fspraken.",

View File

@ -5,7 +5,8 @@
/* CALENDAR */ /* CALENDAR */
"TODAY": "Vandaag", "TODAY": "Vandaag",
"TOMORROW": "Morgen", "TOMORROW": "Morgen",
"RUNNING": "Eindigd over", "DAYAFTERTOMORROW": "Overmorgen",
"RUNNING": "Eindigt over",
"EMPTY": "Geen geplande afspraken.", "EMPTY": "Geen geplande afspraken.",
/* WEATHER */ /* WEATHER */
@ -24,5 +25,9 @@
"W": "W", "W": "W",
"WNW": "WNW", "WNW": "WNW",
"NW": "NW", "NW": "NW",
"NNW": "NNW" "NNW": "NNW",
/* UPDATE INFO */
"UPDATE_NOTIFICATION": "MagicMirror² update beschikbaar.",
"UPDATE_INFO": "De huidige installatie loopt COMMIT_COUNT achter op de BRANCH_NAME branch."
} }

View File

@ -9,6 +9,7 @@ var translations = {
"en" : "translations/en.json", // English "en" : "translations/en.json", // English
"nl" : "translations/nl.json", // Dutch "nl" : "translations/nl.json", // Dutch
"de" : "translations/de.json", // German "de" : "translations/de.json", // German
"fi" : "translations/fi.json", // Suomi
"fr" : "translations/fr.json", // French "fr" : "translations/fr.json", // French
"fy" : "translations/fy.json", // Frysk "fy" : "translations/fy.json", // Frysk
"es" : "translations/es.json", // Spanish "es" : "translations/es.json", // Spanish
@ -23,4 +24,5 @@ var translations = {
"ja" : "translations/ja.json", // Japanese "ja" : "translations/ja.json", // Japanese
"pl" : "translations/pl.json", // Polish "pl" : "translations/pl.json", // Polish
"gr" : "translations/gr.json", // Greek "gr" : "translations/gr.json", // Greek
"da" : "translations/da.json", // Danish
}; };