diff --git a/.eslintrc.json b/.eslintrc.json index 83309505..150a081d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,11 +4,13 @@ "quotes": ["error", "double"], "max-len": ["error", 250], "curly": "error", - "camelcase": ["error", {"properties": "never"}] + "camelcase": ["error", {"properties": "never"}], + "no-trailing-spaces": ["error"], + "no-irregular-whitespace": ["error"] }, "env": { "browser": true, "node": true, "es6": true } -} \ No newline at end of file +} diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ee3b7219..25445886 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -23,7 +23,7 @@ To run StyleLint, use `grunt stylelint`. ### 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](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) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 215950e0..acecd6f9 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,4 @@ -> Please send your pull requests the develop branch. +> Please send your pull requests the develop branch. > Don't forget to add the change to CHANGELOG.md. * Does the pull request solve a **related** issue? diff --git a/CHANGELOG.md b/CHANGELOG.md index 20f95c84..b9551b79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,65 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [2.1.0] - 2016-12-31 + +**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install` + +### Added +- Finnish translation. +- Danish translation. +- Turkish 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 minimum 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. +- Add the abilty to set timezone on the date display in the Clock Module +- Ability to set date format in calendar module +- Possibility to use currentweather for the compliments +- Added option `disabled` for modules. +- Added option `address` to set bind address. +- Added option `onlyTemp` for currentweather module to show show only current temperature and weather icon. +- Added option `remoteFile` to compliments module to load compliment array from filesystem. +- Added option `zoom` to scale the whole mirror display with a given factor. +- Added option `roundTemp` for currentweather and weatherforecast modules to display temperatures rounded to nearest integer. +- Added abilty set the classes option to compliments module for style and text size of compliments. +- Added ability to configure electronOptions +- Calendar module: option to hide private events +- Add root_path for global vars + +### Updated +- Modified translations for Frysk. +- Modified core English translations. +- 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. +- Modules are now secure, and Helmet is now used to prevent abuse of the Mirror's API. + +### 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. +- Global and Module default config values are no longer modified when setting config values. +- Hide a region if all modules in a region are hidden. Prevention unwanted margins. +- Replaced `electron-prebuilt` package with `electron` in order to fix issues that would happen after 2017. +- Documentation of alert module + ## [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 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 diff --git a/Gruntfile.js b/Gruntfile.js index e5bc690b..ec3128ef 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -6,7 +6,10 @@ module.exports = function(grunt) { options: { configFile: ".eslintrc.json" }, - target: ["js/*.js", "modules/default/*.js", "serveronly/*.js", "*.js"] + target: ["js/*.js", "modules/default/*.js", "modules/default/*/*.js", + "serveronly/*.js", "*.js", "!modules/default/alert/notificationFx.js", + "!modules/default/alert/modernizr.custom.js", "!modules/default/alert/classie.js" + ] }, stylelint: { simple: { @@ -47,7 +50,6 @@ module.exports = function(grunt) { "MD018": false, "MD012": false, "MD026": false, - "MD036": false, "MD038": false } }, @@ -64,4 +66,4 @@ module.exports = function(grunt) { grunt.loadNpmTasks("grunt-yamllint"); grunt.loadNpmTasks("grunt-markdownlint"); grunt.registerTask("default", ["eslint", "stylelint", "jsonlint", "markdownlint", "yamllint"]); -}; \ No newline at end of file +}; diff --git a/README.md b/README.md index d86cca7c..7c5286dd 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ The following wiki links are helpful in the configuration of 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: ```bash -git pull +git pull && npm install ``` If you changed nothing more than the config or the modules, this should work without any problems. @@ -73,15 +73,18 @@ Type `git status` to see your changes, if there are any, you can reset them with The following properties can be configured: - | **Option** | **Description** | | --- | --- | | `port` | The port on which the MagicMirror² server will run on. The default value is `8080`. | -| `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`| +| `address` | The ip address the accept connections. The default open bind `::` is IPv6 is available or `0.0.0.0` IPv4 run on. Example config: `192.168.10.100`. | +| `ipWhitelist` | The list of IPs from which you are allowed to access the MagicMirror². The default value is `["127.0.0.1", "::ffff:127.0.0.1", "::1"]`. It is possible to specify IPs with subnet masks (`["127.0.0.1", "127.0.0.1/24"]`) or define ip ranges (`["127.0.0.1", ["192.168.0.1", "192.168.0.100"]]`).| +| `zoom` | This allows to scale the mirror contents with a given zoom factor. The default value is `1.0`| | `language` | The language of the interface. (Note: Not all elements will be localized.) Possible values are `en`, `nl`, `ru`, `fr`, etc., but the default value is `en`. | | `timeFormat` | The form of time notation that will be used. Possible values are `12` or `24`. The default is `24`. | | `units` | The units that will be used in the default weather modules. Possible values are `metric` or `imperial`. The default is `metric`. | | `modules` | An array of active modules. **The array must contain objects. See the next table below for more information.** | +| `electronOptions` | An optional array of Electron (browser) options. This allows configuration of e.g. the browser screen size and position (defaults `.width = 800` & `.height = 600`). Kiosk mode can be enabled by setting `.kiosk = true`, `.autoHideMenuBar = false`, `.fullscreen = false`. More options can be found [here](https://github.com/electron/electron/blob/master/docs/api/browser-window.md). | + Module configuration: @@ -91,6 +94,7 @@ Module configuration: | `position` | The location of the module in which the module will be loaded. Possible values are `top_ bar`, `top_left`, `top_center`, `top_right`, `upper_third`, `middle_center`, `lower_third`, `bottom_left`, `bottom_center`, `bottom_right`, `bottom_bar`, `fullscreen_above`, and `fullscreen_below`. This field is optional but most modules require this field to set. Check the documentation of the module for more information. Multiple modules with the same position will be ordered based on the order in the configuration file. | | `classes` | Additional classes which are passed to the module. The field is optional. | | `header` | To display a header text above the module, add the header property. This field is optional. | +| `disabled` | Set disabled to `true` to skip creating the module. This field is optional. | | `config` | An object with the module configuration properties. Check the documentation of the module for more information. This field is optional, unless the module requires extra configuration. | ## Modules @@ -129,3 +133,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. Thanks for your help in making MagicMirror² better! + +

+
+ MagPi Top 50 +

diff --git a/config/config.js.sample b/config/config.js.sample index 04c7fba9..913dbc43 100644 --- a/config/config.js.sample +++ b/config/config.js.sample @@ -6,6 +6,7 @@ var config = { port: 8080, + ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], language: 'en', timeFormat: 24, @@ -15,6 +16,10 @@ var config = { { module: 'alert', }, + { + module: "updatenotification", + position: "top_bar" + }, { module: 'clock', position: 'top_left' @@ -50,9 +55,9 @@ var config = { position: 'top_right', header: 'Weather Forecast', config: { - location: 'New York', + location: 'New York', locationID: '5128581', //ID from http://www.openweathermap.org - appid: 'YOUR_OPENWEATHER_API_KEY' + appid: 'YOUR_OPENWEATHER_API_KEY' } }, { diff --git a/css/main.css b/css/main.css index 7ae902d6..5e63e596 100644 --- a/css/main.css +++ b/css/main.css @@ -113,11 +113,12 @@ sup { */ .module { - margin-top: 30px; + margin-bottom: 30px; } -.module:first-child { - margin-top: 0; +.region.bottom .module { + margin-top: 30px; + margin-bottom: 0; } /** diff --git a/index.html b/index.html index 4288eba1..86a69a4a 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,10 @@ + +
diff --git a/installers/raspberry.sh b/installers/raspberry.sh index ce37e86f..cd995a1e 100644 --- a/installers/raspberry.sh +++ b/installers/raspberry.sh @@ -18,7 +18,7 @@ echo ' \$$$$$$ |' echo ' \______/' echo -e "\e[0m" -# Define the tested version of Node.js. +# Define the tested version of Node.js. NODE_TESTED="v5.1.0" #Determine which Pi is running. @@ -49,19 +49,19 @@ if command_exists node; then echo -e "\e[0mMinimum Node version: \e[1m$NODE_TESTED\e[0m" echo -e "\e[0mInstalled Node version: \e[1m$NODE_CURRENT\e[0m" if version_gt $NODE_TESTED $NODE_CURRENT; then - echo -e "\e[96mNode should be upgraded.\e[0m" - NODE_INSTALL=true + echo -e "\e[96mNode should be upgraded.\e[0m" + NODE_INSTALL=true - #Check if a node process is currenlty running. - #If so abort installation. - if pgrep "node" > /dev/null; then - echo -e "\e[91mA Node process is currently running. Can't upgrade." - echo "Please quit all Node processes and restart the installer." - exit; + #Check if a node process is currenlty running. + #If so abort installation. + if pgrep "node" > /dev/null; then + echo -e "\e[91mA Node process is currently running. Can't upgrade." + echo "Please quit all Node processes and restart the installer." + exit; fi - else - echo -e "\e[92mNo Node.js upgrade nessecery.\e[0m" + else + echo -e "\e[92mNo Node.js upgrade nessecery.\e[0m" fi else @@ -113,6 +113,34 @@ else exit; 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 -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 " " diff --git a/js/app.js b/js/app.js index 14f10e8b..ba6cd1a9 100644 --- a/js/app.js +++ b/js/app.js @@ -10,6 +10,13 @@ var Server = require(__dirname + "/server.js"); var defaultModules = require(__dirname + "/../modules/default/defaultmodules.js"); var path = require("path"); +// Get version number. +global.version = JSON.parse(fs.readFileSync("package.json", "utf8")).version; +console.log("Starting MagicMirror: v" + global.version); + +// global absolute root path +global.root_path = path.resolve(__dirname + "/../"); + // The next part is here to prevent a major exception when there // is no internet connection. This could probable be solved better. process.on("uncaughtException", function (err) { @@ -34,7 +41,7 @@ var App = function() { var loadConfig = function(callback) { console.log("Loading config ..."); var defaults = require(__dirname + "/defaults.js"); - var configFilename = path.resolve(__dirname + "/../config/config.js"); + var configFilename = path.resolve(global.root_path + "/config/config.js"); try { fs.accessSync(configFilename, fs.F_OK); var c = require(configFilename); @@ -82,6 +89,17 @@ var App = function() { if (loadModule) { var Module = require(helperPath); 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.setPath(path.resolve(moduleFolder)); nodeHelpers.push(m); @@ -103,6 +121,28 @@ var App = function() { 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) * This methods starts the core app. * It loads the config, then it loads all modules. @@ -119,7 +159,7 @@ var App = function() { for (var m in config.modules) { var module = config.modules[m]; - if (modules.indexOf(module.module) === -1) { + if (modules.indexOf(module.module) === -1 && !module.disabled) { modules.push(module.module); } } @@ -147,4 +187,4 @@ var App = function() { }; }; -module.exports = new App(); \ No newline at end of file +module.exports = new App(); diff --git a/js/class.js b/js/class.js index 87e847fd..3c44250e 100644 --- a/js/class.js +++ b/js/class.js @@ -21,28 +21,32 @@ var prototype = new this(); 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 for (var name in prop) { // Check if we're overwriting an existing function prototype[name] = typeof prop[name] == "function" && - typeof _super[name] == "function" && fnTest.test(prop[name]) ? - (function(name, fn) { - return function() { - var tmp = this._super; + typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn) { + return function() { + var tmp = this._super; - // Add a new ._super() method that is the same method - // but on the super-class - this._super = _super[name]; + // Add a new ._super() method that is the same method + // but on the super-class + this._super = _super[name]; - // The method only need to be bound temporarily, so we - // remove it when we're done executing - var ret = fn.apply(this, arguments); - this._super = tmp; + // The method only need to be bound temporarily, so we + // remove it when we're done executing + var ret = fn.apply(this, arguments); + this._super = tmp; - return ret; - }; -})(name, prop[name]) : -prop[name]; + + return ret; + }; + })(name, prop[name]) : prop[name]; } // The dummy class constructor @@ -66,5 +70,24 @@ prop[name]; }; })(); +//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 ***************/ if (typeof module !== "undefined") {module.exports = Class;} diff --git a/js/defaults.js b/js/defaults.js index 0688595c..4639e2b3 100644 --- a/js/defaults.js +++ b/js/defaults.js @@ -10,12 +10,19 @@ var defaults = { port: 8080, kioskmode: false, + electronOptions: {}, + ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], language: "en", timeFormat: 24, units: "metric", + zoom: 1, modules: [ + { + module: "updatenotification", + position: "top_center" + }, { module: "helloworld", position: "upper_third", diff --git a/js/electron.js b/js/electron.js index 75dc47fd..173abe68 100644 --- a/js/electron.js +++ b/js/electron.js @@ -18,13 +18,33 @@ const BrowserWindow = electron.BrowserWindow; let mainWindow; function createWindow() { - // Create the browser window. - if (config.kioskmode) { - mainWindow = new BrowserWindow({width: 800, height: 600, x: 0, y: 0, kiosk:true, darkTheme: true, webPreferences: {nodeIntegration: false}}); - } else { - mainWindow = new BrowserWindow({width: 800, height: 600, x: 0, y: 0, fullscreen: true, autoHideMenuBar: true, darkTheme: true, webPreferences: {nodeIntegration: false}}); + + var electronOptionsDefaults = { + width: 800, + height: 600, + x: 0, + y: 0, + darkTheme: true, + webPreferences: { + nodeIntegration: false, + zoomFactor: config.zoom + } } + // DEPRECATED: "kioskmode" backwards compatibility, to be removed + // settings these options directly instead provides cleaner interface + if (config.kioskmode) { + electronOptionsDefaults.kiosk = true; + } else { + electronOptionsDefaults.fullscreen = true; + electronOptionsDefaults.autoHideMenuBar = true; + } + + var electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions); + + // Create the browser window. + mainWindow = new BrowserWindow(electronOptions); + // and load the index.html of the app. //mainWindow.loadURL('file://' + __dirname + '../../index.html'); mainWindow.loadURL("http://localhost:" + config.port); diff --git a/js/loader.js b/js/loader.js index 3656c7cd..0c7c643a 100644 --- a/js/loader.js +++ b/js/loader.js @@ -89,6 +89,10 @@ var Loader = (function() { moduleFolder = config.paths.modules + "/default/" + module; } + if (moduleData.disabled === true) { + continue; + } + moduleFiles.push({ index: m, identifier: "module_" + m + "_" + module, @@ -117,9 +121,13 @@ var Loader = (function() { var afterLoad = function() { var moduleObject = Module.create(module.name); - bootstrapModule(module, moduleObject, function() { + if (moduleObject) { + bootstrapModule(module, moduleObject, function() { + callback(); + }); + } else { callback(); - }); + } }; if (loadedModuleFiles.indexOf(url) !== -1) { diff --git a/js/logger.js b/js/logger.js index 5b2df990..1392a72b 100644 --- a/js/logger.js +++ b/js/logger.js @@ -13,35 +13,15 @@ var Log = (function() { return { - info: function(message) { - console.info(message); - }, - log: function(message) { - console.log(message); - }, - error: function(message) { - console.error(message); - }, - warn: function(message) { - console.warn(message); - }, - group: function(message) { - console.group(message); - }, - groupCollapsed: function(message) { - console.groupCollapsed(message); - }, - groupEnd: function() { - console.groupEnd(); - }, - time: function(message) { - console.time(message); - }, - timeEnd: function(message) { - console.timeEnd(message); - }, - timeStamp: function(message) { - console.timeStamp(message); - } + info: Function.prototype.bind.call(console.info, console), + log: Function.prototype.bind.call(console.log, console), + error: Function.prototype.bind.call(console.error, console), + warn: Function.prototype.bind.call(console.warn, console), + group: Function.prototype.bind.call(console.group, console), + groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console), + groupEnd: Function.prototype.bind.call(console.groupEnd, console), + time: Function.prototype.bind.call(console.time, console), + timeEnd: Function.prototype.bind.call(console.timeEnd, console), + timeStamp: Function.prototype.bind.call(console.timeStamp, console) }; })(); diff --git a/js/main.js b/js/main.js index abd93e40..325a7efe 100644 --- a/js/main.js +++ b/js/main.js @@ -40,6 +40,7 @@ var MM = (function() { if (typeof module.data.header !== "undefined" && module.data.header !== "") { var moduleHeader = document.createElement("header"); moduleHeader.innerHTML = module.data.header; + moduleHeader.className = "module-header"; dom.appendChild(moduleHeader); } @@ -51,6 +52,8 @@ var MM = (function() { } } + updateWrapperStates(); + sendNotification("DOM_OBJECTS_CREATED"); }; @@ -73,7 +76,7 @@ var MM = (function() { /* sendNotification(notification, payload, sender) * Send a notification to all modules. * - * argument notification string - The identifier of the noitication. + * argument notification string - The identifier of the notification. * argument payload mixed - The payload of the notification. * argument sender Module - The module that sent the notification. */ @@ -94,26 +97,27 @@ var MM = (function() { */ var updateDom = function(module, speed) { var newContent = module.getDom(); + var newHeader = module.getHeader(); if (!module.hidden) { - if (!moduleNeedsUpdate(module, newContent)) { + if (!moduleNeedsUpdate(module, newHeader, newContent)) { return; } if (!speed) { - updateModuleContent(module, newContent); + updateModuleContent(module, newHeader, newContent); return; } hideModule(module, speed / 2, function() { - updateModuleContent(module, newContent); + updateModuleContent(module, newHeader, newContent); if (!module.hidden) { showModule(module, speed / 2); } }); } else { - updateModuleContent(module, newContent); + updateModuleContent(module, newHeader, newContent); } }; @@ -125,14 +129,23 @@ var MM = (function() { * * 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 contentWrapper = moduleWrapper.getElementsByClassName("module-content")[0]; + var contentWrapper = moduleWrapper.getElementsByClassName("module-content"); + var headerWrapper = moduleWrapper.getElementsByClassName("module-header"); - var tempWrapper = document.createElement("div"); - tempWrapper.appendChild(newContent); + var headerNeedsUpdate = false; + var contentNeedsUpdate = false; - return tempWrapper.innerHTML !== contentWrapper.innerHTML; + if (headerWrapper.length > 0) { + headerNeedsUpdate = newHeader !== headerWrapper[0].innerHTML; + } + + var tempContentWrapper = document.createElement("div"); + tempContentWrapper.appendChild(newContent); + contentNeedsUpdate = tempContentWrapper.innerHTML !== contentWrapper[0].innerHTML; + + return headerNeedsUpdate || contentNeedsUpdate; }; /* moduleNeedsUpdate(module, newContent) @@ -141,12 +154,19 @@ var MM = (function() { * argument module Module - The module to check. * 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 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) @@ -156,7 +176,17 @@ var MM = (function() { * argument speed Number - The speed of the hide animation. * 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); if (moduleWrapper !== null) { moduleWrapper.style.transition = "opacity " + speed / 1000 + "s"; @@ -165,10 +195,12 @@ var MM = (function() { clearTimeout(module.showHideTimer); module.showHideTimer = setTimeout(function() { // 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 // the .display property. - moduleWrapper.style.position = "absolute"; + moduleWrapper.style.position = "fixed"; + + updateWrapperStates(); if (typeof callback === "function") { callback(); } }, speed); @@ -182,7 +214,30 @@ var MM = (function() { * argument speed Number - The speed of the show animation. * 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); if (moduleWrapper !== null) { moduleWrapper.style.transition = "opacity " + speed / 1000 + "s"; @@ -190,6 +245,8 @@ var MM = (function() { moduleWrapper.style.position = "static"; moduleWrapper.style.opacity = 1; + updateWrapperStates(); + clearTimeout(module.showHideTimer); module.showHideTimer = setTimeout(function() { if (typeof callback === "function") { callback(); } @@ -198,6 +255,36 @@ var MM = (function() { } }; + /* updateWrapperStates() + * Checks for all positions if it has visible content. + * If not, if will hide the position to prevent unwanted margins. + * This method schould be called by the show and hide methods. + * + * Example: + * If the top_bar only contains the update notification. And no update is available, + * the update notification is hidden. The top bar still occupies space making for + * an ugly top margin. By using this function, the top bar will be hidden if the + * update notification is not visible. + */ + + var updateWrapperStates = function() { + var positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"]; + + positions.forEach(function(position) { + var wrapper = selectWrapper(position); + var moduleWrappers = wrapper.getElementsByClassName("module"); + + var showWrapper = false; + Array.prototype.forEach.call(moduleWrappers, function(moduleWrapper) { + if (moduleWrapper.style.position == "" || moduleWrapper.style.position == "static") { + showWrapper = true; + } + }); + + wrapper.style.display = showWrapper ? "block" : "none"; + }); + }; + /* loadConfig() * Loads the core config and combines it with de system defaults. */ @@ -208,7 +295,7 @@ var MM = (function() { return; } - config = Object.assign(defaults, config); + config = Object.assign({}, defaults, config); }; /* setSelectionMethodsForModules() @@ -221,7 +308,7 @@ var MM = (function() { /* withClass(className) * filters a collection of modules based on classname(s). * - * argument className string/array - one or multiple classnames. (array or space devided) + * argument className string/array - one or multiple classnames. (array or space divided) * * return array - Filtered collection of modules. */ @@ -251,7 +338,7 @@ var MM = (function() { /* exceptWithClass(className) * filters a collection of modules based on classname(s). (NOT) * - * argument className string/array - one or multiple classnames. (array or space devided) + * argument className string/array - one or multiple classnames. (array or space divided) * * return array - Filtered collection of modules. */ @@ -401,10 +488,11 @@ var MM = (function() { * argument module Module - The module hide. * argument speed Number - The speed of the hide animation. * 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; - hideModule(module, speed, callback); + hideModule(module, speed, callback, options); }, /* showModule(module, speed, callback) @@ -413,10 +501,11 @@ var MM = (function() { * argument module Module - The module show. * argument speed Number - The speed of the show animation. * 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; - showModule(module, speed, callback); + showModule(module, speed, callback, options); } }; diff --git a/js/module.js b/js/module.js index 293d00b5..67305a9c 100644 --- a/js/module.js +++ b/js/module.js @@ -14,23 +14,30 @@ var Module = Class.extend({ * All methods (and properties) below can be subclassed. * *********************************************************/ + // Set the minimum MagicMirror module version for this module. + requiresVersion: "2.0.0", + // Module config defaults. defaults: {}, // Timer reference used for showHide animation callbacks. showHideTimer: null, + // Array to store lockStrings. These strings are used to lock + // visibility when hiding and showing module. + lockStrings: [], + /* init() * Is called when the module is instantiated. */ - init: function() { + init: function () { //Log.log(this.defaults); }, /* start() * Is called when the module is started. */ - start: function() { + start: function () { Log.info("Starting module: " + this.name); }, @@ -39,7 +46,7 @@ var Module = Class.extend({ * * return Array - An array with filenames. */ - getScripts: function() { + getScripts: function () { return []; }, @@ -48,7 +55,7 @@ var Module = Class.extend({ * * return Array - An array with filenames. */ - getStyles: function() { + getStyles: function () { return []; }, @@ -57,7 +64,7 @@ var Module = Class.extend({ * * return Map - A map with langKeys and filenames. */ - getTranslations: function() { + getTranslations: function () { return false; }, @@ -67,7 +74,7 @@ var Module = Class.extend({ * * return domobject - The dom to display. */ - getDom: function() { + getDom: function () { var nameWrapper = document.createElement("div"); var name = document.createTextNode(this.name); nameWrapper.appendChild(name); @@ -84,15 +91,26 @@ var Module = Class.extend({ 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) * This method is called when a notification arrives. * This method is called by the Magic Mirror core. * - * argument notification string - The identifier of the noitication. + * argument notification string - The identifier of the notification. * argument payload mixed - The payload of the notification. * argument sender Module - The module that sent the notification. */ - notificationReceived: function(notification, payload, sender) { + notificationReceived: function (notification, payload, sender) { if (sender) { Log.log(this.name + " received a module notification: " + notification + " from sender: " + sender.name); } else { @@ -103,24 +121,24 @@ var Module = Class.extend({ /* socketNotificationReceived(notification, payload) * This method is called when a socket notification arrives. * - * argument notification string - The identifier of the noitication. + * argument notification string - The identifier 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); }, /* suspend() * This method is called when a module is hidden. */ - suspend: function() { + suspend: function () { Log.log(this.name + " is suspended."); }, /* resume() * This method is called when a module is shown. */ - resume: function() { + resume: function () { Log.log(this.name + " is resumed."); }, @@ -133,7 +151,7 @@ var Module = Class.extend({ * * argument data obejct - Module data. */ - setData: function(data) { + setData: function (data) { this.data = data; this.name = data.name; this.identifier = data.identifier; @@ -147,21 +165,21 @@ var Module = Class.extend({ * * argument config obejct - Module config. */ - setConfig: function(config) { - this.config = Object.assign(this.defaults, config); + setConfig: function (config) { + this.config = Object.assign({}, this.defaults, config); }, /* socket() * Returns a socket object. If it doesn"t exist, it"s created. * It also registers the notification callback. */ - socket: function() { + socket: function () { if (typeof this._socket === "undefined") { this._socket = this._socket = new MMSocket(this.name); } var self = this; - this._socket.setNotificationCallback(function(notification, payload) { + this._socket.setNotificationCallback(function (notification, payload) { self.socketNotificationReceived(notification, payload); }); @@ -175,7 +193,7 @@ var Module = Class.extend({ * * return string - File path. */ - file: function(file) { + file: function (file) { return this.data.path + "/" + file; }, @@ -184,14 +202,14 @@ var Module = Class.extend({ * * argument callback function - Function called when done. */ - loadStyles: function(callback) { + loadStyles: function (callback) { var self = this; var styles = this.getStyles(); - var loadNextStyle = function() { + var loadNextStyle = function () { if (styles.length > 0) { var nextStyle = styles[0]; - Loader.loadFile(nextStyle, self, function() { + Loader.loadFile(nextStyle, self, function () { styles = styles.slice(1); loadNextStyle(); }); @@ -208,14 +226,14 @@ var Module = Class.extend({ * * argument callback function - Function called when done. */ - loadScripts: function(callback) { + loadScripts: function (callback) { var self = this; var scripts = this.getScripts(); - var loadNextScript = function() { + var loadNextScript = function () { if (scripts.length > 0) { var nextScript = scripts[0]; - Loader.loadFile(nextScript, self, function() { + Loader.loadFile(nextScript, self, function () { scripts = scripts.slice(1); loadNextScript(); }); @@ -232,14 +250,14 @@ var Module = Class.extend({ * * argument callback function - Function called when done. */ - loadTranslations: function(callback) { + loadTranslations: function (callback) { var self = this; var translations = this.getTranslations(); var lang = config.language.toLowerCase(); // The variable `first` will contain the first // defined translation after the following line. - for (var first in translations) {break;} + for (var first in translations) { break; } if (translations) { 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. // Otherwise only load the fallback translation file. if (translationFile !== undefined && translationFile !== translationsFallbackFile) { - Translator.load(self, translationFile, false, function() { + Translator.load(self, translationFile, false, function () { Translator.load(self, translationsFallbackFile, true, callback); }); } else { @@ -259,13 +277,13 @@ var Module = Class.extend({ } }, - /* translate(key, defaultValue) - * Request the translation for a given key. - * - * argument key string - The key of the string to translage - * argument defaultValue string - The default value if no translation was found. (Optional) - */ - translate: function(key, defaultValue) { + /* translate(key, defaultValue) + * Request the translation for a given key. + * + * argument key string - The key of the string to translage + * argument defaultValue string - The default value if no translation was found. (Optional) + */ + translate: function (key, defaultValue) { return Translator.translate(this, key) || defaultValue || ""; }, @@ -274,27 +292,27 @@ var Module = Class.extend({ * * argument speed Number - The speed of the animation. (Optional) */ - updateDom: function(speed) { + updateDom: function (speed) { MM.updateDom(this, speed); }, /* sendNotification(notification, payload) * Send a notification to all modules. * - * argument notification string - The identifier of the noitication. + * argument notification string - The identifier of the notification. * argument payload mixed - The payload of the notification. */ - sendNotification: function(notification, payload) { + sendNotification: function (notification, payload) { MM.sendNotification(notification, payload, this); }, /* sendSocketNotification(notification, payload) * Send a socket notification to the node helper. * - * argument notification string - The identifier of the noitication. + * argument notification string - The identifier of the notification. * argument payload mixed - The payload of the notification. */ - sendSocketNotification: function(notification, payload) { + sendSocketNotification: function (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 callback function - Called when the animation is done. + * argument options object - Optional settings for the hide method. */ - hide: function(speed, callback) { - callback = callback || function() {}; + hide: function (speed, callback, options) { + if (typeof callback === "object") { + options = callback; + callback = function () { }; + } + + callback = callback || function () { }; + options = options || {}; var self = this; - MM.hideModule(self, speed, function() { + MM.hideModule(self, speed, function () { self.suspend(); callback(); - }); + }, options); }, /* showModule(module, speed, callback) @@ -319,29 +344,29 @@ var Module = Class.extend({ * * argument speed Number - The speed of the show animation. * 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(); - MM.showModule(this, speed, callback); + MM.showModule(this, speed, callback, options); } }); Module.definitions = {}; -Module.create = function(name) { +Module.create = function (name) { - //Define the clone method for later use. - 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]); - } - - return temp; + // Make sure module definition is available. + if (!Module.definitions[name]) { + return; } 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); Module.definitions[name] = moduleDefinition; }; diff --git a/js/server.js b/js/server.js index 2ab0b1b9..f1de2420 100644 --- a/js/server.js +++ b/js/server.js @@ -10,21 +10,43 @@ var app = require("express")(); var server = require("http").Server(app); var io = require("socket.io")(server); var path = require("path"); +var ipfilter = require("express-ipfilter").IpFilter; +var fs = require("fs"); +var helmet = require("helmet"); var Server = function(config, callback) { console.log("Starting server op port " + config.port + " ... "); - server.listen(config.port); + server.listen(config.port, config.address ? config.address : null); + + 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.
Please check your config.js or config.js.sample to change this."); + }); + }); + app.use(helmet()); + app.use("/js", express.static(__dirname)); - app.use("/config", express.static(path.resolve(__dirname + "/../config"))); - app.use("/css", express.static(path.resolve(__dirname + "/../css"))); - app.use("/fonts", express.static(path.resolve(__dirname + "/../fonts"))); - app.use("/modules", express.static(path.resolve(__dirname + "/../modules"))); - app.use("/vendor", express.static(path.resolve(__dirname + "/../vendor"))); - app.use("/translations", express.static(path.resolve(__dirname + "/../translations"))); + app.use("/config", express.static(path.resolve(global.root_path + "/config"))); + app.use("/css", express.static(path.resolve(global.root_path + "/css"))); + app.use("/fonts", express.static(path.resolve(global.root_path + "/fonts"))); + app.use("/modules", express.static(path.resolve(global.root_path + "/modules"))); + app.use("/vendor", express.static(path.resolve(global.root_path + "/vendor"))); + app.use("/translations", express.static(path.resolve(global.root_path + "/translations"))); + + app.get("/version", function(req,res) { + res.send(global.version); + }); app.get("/", function(req, res) { - res.sendFile(path.resolve(__dirname + "/../index.html")); + var html = fs.readFileSync(path.resolve(global.root_path + "/index.html"), {encoding: "utf8"}); + html = html.replace("#VERSION#", global.version); + + res.send(html); }); if (typeof callback === "function") { diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 00000000..2b354edc --- /dev/null +++ b/jsconfig.json @@ -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" + ] +} diff --git a/modules/README.md b/modules/README.md index f4a8debd..42a31dfa 100644 --- a/modules/README.md +++ b/modules/README.md @@ -6,11 +6,11 @@ This document describes the way to develop your own MagicMirror² modules. 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 - **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/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: {}` 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 ####`init()` @@ -104,7 +117,7 @@ The getScripts method is called to request any additional scripts that need to b getScripts: function() { return [ '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. '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()` **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:** ````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 configure 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)` 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. @@ -198,8 +228,8 @@ When using a node_helper, the node helper can send your module notifications. Wh - `notification` - String - The notification identifier. - `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.
-**Note 2:** The socket connection is established as soon as the module sends it's first message using [sendSocketNotification](thissendsocketnotificationnotification-payload). +**Note 1:** When a node helper sends a notification, all modules of that module type receive the same notifications.
+**Note 2:** The socket connection is established as soon as the module sends its first message using [sendSocketNotification](thissendsocketnotificationnotification-payload). **Example:** ````javascript @@ -217,7 +247,7 @@ When a module will be shown after it was previously hidden (using the `module.sh ### 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)` @@ -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)` ***speed* Number** - Optional. Animation speed in milliseconds.
-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: @@ -248,7 +278,7 @@ start: function() { ***notification* String** - The notification identifier.
***payload* AnyType** - Optional. A notification payload.
-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:** ````javascript @@ -259,33 +289,103 @@ this.sendNotification('MYMODULE_READY_FOR_ACTION', {foo:bar}); ***notification* String** - The notification identifier.
***payload* AnyType** - Optional. A notification payload.
-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:** ````javascript this.sendSocketNotification('SET_CONFIG', this.config); ```` -####`this.hide(speed, callback)` -***speed* Number** - Optional, The speed of the hide animation in milliseconds. +####`this.hide(speed, callback, options)` +***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. +***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.
**Note 2:** If the hide animation is hijacked (an other method calls hide on the same module), the callback will not be called.
**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.
**Note 2:** If the show animation is hijacked (an other method calls show on the same module), the callback will not be called.
**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 moduleA’s 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 moduleA’s 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)` ***identifier* String** - Identifier of the string that should be translated. @@ -328,7 +428,7 @@ var NodeHelper = require("node_helper"); 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 @@ -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 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. + +####'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 ####`init()` This method is called when a node helper gets instantiated. In most cases you do not need to subclass this method. ####`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:** ````javascript @@ -383,12 +496,12 @@ start: function() { ```` ####`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. - `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:** ````javascript @@ -399,15 +512,15 @@ socketNotificationReceived: function(notification, payload) { ### 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)` ***notification* String** - The notification identifier.
***payload* AnyType** - Optional. A notification payload.
-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:** ````javascript @@ -430,10 +543,10 @@ To make a selection of all currently loaded module instances, run the `MM.getMod #####`.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.
-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:** ````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. **Returns Array** - An array with module instances.
-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:** ````javascript diff --git a/modules/default/alert/README.md b/modules/default/alert/README.md index 5c04848c..d566313d 100644 --- a/modules/default/alert/README.md +++ b/modules/default/alert/README.md @@ -45,9 +45,9 @@ The following properties can be configured: display_time - Time a notification is displayed in seconds.
-
Possible values: float int -
Default value: 3.5 + Time a notification is displayed in milliseconds.
+
Possible values: int +
Default value: 3500 @@ -62,7 +62,7 @@ The following properties can be configured: welcome_message Message shown at startup.

Possible values: string false -
Default value: Welcome, start was successfull! +
Default value: false (no message at startup) @@ -162,4 +162,4 @@ self.sendNotification("SHOW_ALERT", {}); ## Open Source Licenses ###[NotificationStyles](https://github.com/codrops/NotificationStyles) -See [ympanus.net](http://tympanus.net/codrops/licensing/) for license. \ No newline at end of file +See [ympanus.net](http://tympanus.net/codrops/licensing/) for license. diff --git a/modules/default/alert/alert.js b/modules/default/alert/alert.js index cc25a31c..787a0b4a 100644 --- a/modules/default/alert/alert.js +++ b/modules/default/alert/alert.js @@ -35,7 +35,6 @@ Module.register("alert",{ }, show_notification: function(message) { if (this.config.effect == "slide") {this.config.effect = this.config.effect + "-" + this.config.position;} - msg = ""; if (message.title) { msg += "" + message.title + ""; @@ -46,7 +45,7 @@ Module.register("alert",{ } msg += "" + message.message + ""; } - + new NotificationFx({ message: msg, layout: "growl", diff --git a/modules/default/alert/translations/da.json b/modules/default/alert/translations/da.json new file mode 100644 index 00000000..234f22b1 --- /dev/null +++ b/modules/default/alert/translations/da.json @@ -0,0 +1,4 @@ +{ + "sysTitle": "MagicMirror Notifikation", + "welcome": "Velkommen, modulet er succesfuldt startet!" +} diff --git a/modules/default/alert/translations/es.json b/modules/default/alert/translations/es.json new file mode 100644 index 00000000..39478251 --- /dev/null +++ b/modules/default/alert/translations/es.json @@ -0,0 +1,4 @@ +{ + "sysTitle": "MagicMirror Notificaciones", + "welcome": "Bienvenido, ¡se iniciado correctamente!" +} diff --git a/modules/default/calendar/README.md b/modules/default/calendar/README.md index 936abdde..c5745152 100644 --- a/modules/default/calendar/README.md +++ b/modules/default/calendar/README.md @@ -106,10 +106,16 @@ The following properties can be configured: titleReplace An object of textual replacements applied to the tile of the event. This allow to remove or replace certains words in the title.

Example:
- titleReplace: {'Birthday of ' : '', 'foo':'bar'} +
Default value: + + { + "De verjaardag van ": "", + "'s birthday": "" + } + @@ -119,6 +125,13 @@ The following properties can be configured:
Default value: false + + dateFormat + Format to use for the date of events (when using absolute dates)
+
Possible values: See Moment.js formats +
Default value: MMM Do (e.g. Jan 18th) + + timeFormat Display event times as absolute dates, or relative time
@@ -126,12 +139,33 @@ The following properties can be configured:
Default value: relative + + getRelative + How much time (in hours) should be left until calendar events start getting relative?
+
Possible values: 0 (events stay absolute) - 48 (48 hours before the event starts) +
Default value: 6 + + urgency When using a timeFormat of absolute, the urgency setting allows you to display events within a specific time frame as relative This allows events within a certain time frame to be displayed as relative (in xx days) while others are displayed as absolute dates

Possible values: a positive integer representing the number of days for which you want a relative date, for example 7 (for 7 days)
-
Default value: 0 (disabled) +
Default value: 7 + + + + broadcastEvents + If this property is set to true, the calendar will broadcast all the events to all other modules with the notification message: CALENDAR_EVENTS. The event objects are stored in an array and contain the following fields: title, startDate, endDate, fullDayEvent, location and geo.
+
Possible values: true, false
+
Default value: true + + + + hidePrivate + Hides private calendar events.
+
Possible values: true or false +
Default value: false @@ -171,24 +205,24 @@ config: { - symbol + symbol The symbol to show in front of an event. This property is optional.

Possible values: See Font Awesome website. - repeatingCountTitle + repeatingCountTitle The count title for yearly repating events in this calendar.

Example:
'Birthday' - user + user The username for HTTP Basic authentication. - pass + pass The password for HTTP Basic authentication. diff --git a/modules/default/calendar/calendar.js b/modules/default/calendar/calendar.js index 3151467a..b636b30b 100644 --- a/modules/default/calendar/calendar.js +++ b/modules/default/calendar/calendar.js @@ -7,7 +7,7 @@ * MIT Licensed. */ -Module.register("calendar",{ +Module.register("calendar", { // Define module defaults defaults: { @@ -16,14 +16,17 @@ Module.register("calendar",{ displaySymbol: true, defaultSymbol: "calendar", // Fontawesome Symbol see http://fontawesome.io/cheatsheet/ displayRepeatingCountTitle: false, - defaultRepeatingCountTitle: '', + defaultRepeatingCountTitle: "", maxTitleLength: 25, fetchInterval: 5 * 60 * 1000, // Update every 5 minutes. animationSpeed: 2000, fade: true, urgency: 7, timeFormat: "relative", + dateFormat: "MMM Do", + getRelative: 6, fadePoint: 0.25, // Start on 1/4th of the list. + hidePrivate: false, calendars: [ { symbol: "calendar", @@ -34,20 +37,21 @@ Module.register("calendar",{ "De verjaardag van ": "", "'s birthday": "" }, + broadcastEvents: true }, // Define required scripts. - getStyles: function() { + getStyles: function () { return ["calendar.css", "font-awesome.css"]; }, // Define required scripts. - getScripts: function() { + getScripts: function () { return ["moment.js"]; }, // Define required translations. - getTranslations: function() { + getTranslations: function () { // The translations for the defaut modules are defined in the core translation files. // Therefor we can just return false. Otherwise we should have returned a dictionairy. // If you're trying to build your own module including translations, check out the documentation. @@ -55,7 +59,7 @@ Module.register("calendar",{ }, // Override start method. - start: function() { + start: function () { Log.log("Starting module: " + this.name); // Set locale. @@ -72,11 +76,15 @@ Module.register("calendar",{ }, // Override socket notification handler. - socketNotificationReceived: function(notification, payload) { + socketNotificationReceived: function (notification, payload) { if (notification === "CALENDAR_EVENTS") { if (this.hasCalendarURL(payload.url)) { this.calendarData[payload.url] = payload.events; this.loaded = true; + + if (this.config.broadcastEvents) { + this.broadcastEvents(); + } } } else if (notification === "FETCH_ERROR") { Log.error("Calendar Error. Could not fetch calendar: " + payload.url); @@ -90,7 +98,7 @@ Module.register("calendar",{ }, // Override dom generator. - getDom: function() { + getDom: function () { var events = this.createEventList(); var wrapper = document.createElement("table"); @@ -109,27 +117,27 @@ Module.register("calendar",{ eventWrapper.className = "normal"; if (this.config.displaySymbol) { - var symbolWrapper = document.createElement("td"); + var symbolWrapper = document.createElement("td"); symbolWrapper.className = "symbol"; - var symbol = document.createElement("span"); + var symbol = document.createElement("span"); symbol.className = "fa fa-" + this.symbolForUrl(event.url); symbolWrapper.appendChild(symbol); eventWrapper.appendChild(symbolWrapper); } var titleWrapper = document.createElement("td"), - repeatingCountTitle = ''; + repeatingCountTitle = ""; if (this.config.displayRepeatingCountTitle) { repeatingCountTitle = this.countTitleForUrl(event.url); - if(repeatingCountTitle !== '') { - var thisYear = new Date().getFullYear(), + if (repeatingCountTitle !== "") { + var thisYear = new Date(parseInt(event.startDate)).getFullYear(), yearDiff = thisYear - event.firstYear; - repeatingCountTitle = ', '+ yearDiff + '. ' + repeatingCountTitle; + repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle; } } @@ -137,28 +145,24 @@ Module.register("calendar",{ titleWrapper.className = "title bright"; eventWrapper.appendChild(titleWrapper); - var timeWrapper = document.createElement("td"); + var timeWrapper = document.createElement("td"); //console.log(event.today); var now = new Date(); // Define second, minute, hour, and day variables - var one_second = 1000; // 1,000 milliseconds - var one_minute = one_second * 60; - var one_hour = one_minute * 60; - var one_day = one_hour * 24; + var oneSecond = 1000; // 1,000 milliseconds + var oneMinute = oneSecond * 60; + var oneHour = oneMinute * 60; + var oneDay = oneHour * 24; if (event.fullDayEvent) { if (event.today) { - timeWrapper.innerHTML = this.translate("TODAY"); - } else if (event.startDate - now < one_day && event.startDate - now > 0) { - timeWrapper.innerHTML = this.translate("TOMORROW"); - } else if (event.startDate - now < 2*one_day && event.startDate - now > 0) { - /*Provide ability to show "the day after tomorrow" instead of "in a day" - *if "DAYAFTERTOMORROW" is configured in a language's translation .json file, - *,which can be found in MagicMirror/translations/ - */ - if (this.translate('DAYAFTERTOMORROW') !== 'DAYAFTERTOMORROW') { - timeWrapper.innerHTML = this.translate("DAYAFTERTOMORROW"); + timeWrapper.innerHTML = this.capFirst(this.translate("TODAY")); + } else if (event.startDate - now < oneDay && event.startDate - now > 0) { + 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 = moment(event.startDate, "x").fromNow(); + timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow()); } } else { /* Check to see if the user displays absolute or relative dates with their events @@ -169,26 +173,26 @@ Module.register("calendar",{ * 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.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 - timeWrapper.innerHTML = moment(event.startDate, "x").fromNow(); + timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow()); } else { - timeWrapper.innerHTML = moment(event.startDate, "x").format("MMM Do"); + timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat)); } } else { - timeWrapper.innerHTML = moment(event.startDate, "x").fromNow(); + timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow()); } } } else { 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) - 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() - timeWrapper.innerHTML = moment(event.startDate, "x").fromNow(); + timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow()); } else { // 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 { /* Check to see if the user displays absolute or relative dates with their events @@ -199,18 +203,18 @@ Module.register("calendar",{ * 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.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 - timeWrapper.innerHTML = moment(event.startDate, "x").fromNow(); + timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow()); } else { - timeWrapper.innerHTML = moment(event.startDate, "x").format("MMM Do"); + timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat)); } } else { - timeWrapper.innerHTML = moment(event.startDate, "x").fromNow(); + timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow()); } } } 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'); @@ -244,7 +248,7 @@ Module.register("calendar",{ * * return bool - Has calendar url */ - hasCalendarURL: function(url) { + hasCalendarURL: function (url) { for (var c in this.config.calendars) { var calendar = this.config.calendars[c]; if (calendar.url === url) { @@ -260,20 +264,26 @@ Module.register("calendar",{ * * return array - Array with events. */ - createEventList: function() { + createEventList: function () { var events = []; var today = moment().startOf("day"); for (var c in this.calendarData) { var calendar = this.calendarData[c]; for (var e in calendar) { var event = calendar[e]; + if(this.config.hidePrivate) { + if(event.class === "PRIVATE") { + // do not add the current event, skip it + continue; + } + } event.url = c; event.today = event.startDate >= today && event.startDate < (today + 24 * 60 * 60 * 1000); events.push(event); } } - events.sort(function(a, b) { + events.sort(function (a, b) { return a.startDate - b.startDate; }); @@ -285,7 +295,7 @@ Module.register("calendar",{ * * argument url sting - Url to add. */ - addCalendar: function(url, user, pass) { + addCalendar: function (url, user, pass) { this.sendSocketNotification("ADD_CALENDAR", { url: url, maximumEntries: this.config.maximumEntries, @@ -303,10 +313,10 @@ Module.register("calendar",{ * * return string - The Symbol */ - symbolForUrl: function(url) { + symbolForUrl: function (url) { for (var c in this.config.calendars) { 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; } } @@ -320,10 +330,10 @@ Module.register("calendar",{ * * return string - The Symbol */ - countTitleForUrl: function(url) { + countTitleForUrl: function (url) { for (var c in this.config.calendars) { 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; } } @@ -340,14 +350,23 @@ Module.register("calendar",{ * * return string - The shortened string. */ - shorten: function(string, maxLength) { + shorten: function (string, maxLength) { if (string.length > maxLength) { - return string.slice(0,maxLength) + "…"; + return string.slice(0, maxLength) + "…"; } 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) * Transforms the title of an event for usage. * Replaces parts of the text as defined in config.titleReplace. @@ -357,7 +376,7 @@ Module.register("calendar",{ * * return string - The transformed title. */ - titleTransform: function(title) { + titleTransform: function (title) { for (var needle in this.config.titleReplace) { var replacement = this.config.titleReplace[needle]; title = title.replace(needle, replacement); @@ -365,5 +384,28 @@ Module.register("calendar",{ title = this.shorten(title, this.config.maxTitleLength); 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); + } }); diff --git a/modules/default/calendar/calendarfetcher.js b/modules/default/calendar/calendarfetcher.js index 214e2e5a..e24ee004 100644 --- a/modules/default/calendar/calendarfetcher.js +++ b/modules/default/calendar/calendarfetcher.js @@ -25,9 +25,10 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe clearTimeout(reloadTimer); reloadTimer = null; + nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); var opts = { 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 "+ nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)" } }; @@ -77,10 +78,11 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe if (!isFacebookBirthday) { endDate = startDate; } else { - endDate = moment(startDate).add(1, 'days'); + endDate = moment(startDate).add(1, "days"); } } + // calculate the duration f the event for use with recurring events. var duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x")); @@ -95,20 +97,28 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe title = event.description; } + var location = event.location || false; + var geo = event.geo || false; + var description = event.description || false; + if (typeof event.rrule != "undefined" && !isFacebookBirthday) { var rule = event.rrule; var dates = rule.between(today, future, true, limitFunction); for (var d in dates) { 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) { newEvents.push({ title: title, startDate: startDate.format("x"), endDate: endDate.format("x"), fullDayEvent: isFullDayEvent(event), - firstYear: event.start.getFullYear() + class: event.class, + firstYear: event.start.getFullYear(), + location: location, + geo: geo, + description: description }); } } @@ -132,14 +142,19 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe continue; } - // Every thing is good. Add it to the list. + // Every thing is good. Add it to the list. + newEvents.push({ title: title, startDate: startDate.format("x"), endDate: endDate.format("x"), - fullDayEvent: fullDayEvent + fullDayEvent: fullDayEvent, + class: event.class, + location: location, + geo: geo, + description: description }); - + } } } @@ -186,7 +201,7 @@ var CalendarFetcher = function(url, reloadInterval, maximumEntries, maximumNumbe if (end - start === 24 * 60 * 60 * 1000 && startDate.getHours() === 0 && startDate.getMinutes() === 0) { // Is 24 hours, and starts on the middle of the night. - return true; + return true; } return false; diff --git a/modules/default/calendar/debug.js b/modules/default/calendar/debug.js index bc0adae6..9b72d51d 100644 --- a/modules/default/calendar/debug.js +++ b/modules/default/calendar/debug.js @@ -1,5 +1,5 @@ /* CalendarFetcher Tester - * use this script with `node debug.js` to test the fetcher without the need + * use this script with `node debug.js` to test the fetcher without the need * of starting the MagicMirror core. Adjust the values below to your desire. * * By Michael Teeuw http://michaelteeuw.nl @@ -8,18 +8,18 @@ var CalendarFetcher = require("./calendarfetcher.js"); -var url = 'https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics'; +var url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; var fetchInterval = 60 * 60 * 1000; var maximumEntries = 10; var maximumNumberOfDays = 365; -console.log('Create fetcher ...'); +console.log("Create fetcher ..."); fetcher = new CalendarFetcher(url, fetchInterval, maximumEntries, maximumNumberOfDays); fetcher.onReceive(function(fetcher) { - console.log(fetcher.events()); - console.log('------------------------------------------------------------'); + console.log(fetcher.events()); + console.log("------------------------------------------------------------"); }); fetcher.onError(function(fetcher, error) { @@ -29,5 +29,4 @@ fetcher.onError(function(fetcher, error) { fetcher.startFetch(); -console.log('Create fetcher done! '); - +console.log("Create fetcher done! "); diff --git a/modules/default/clock/README.md b/modules/default/clock/README.md index 6eca1efb..76a979d4 100644 --- a/modules/default/clock/README.md +++ b/modules/default/clock/README.md @@ -115,5 +115,13 @@ The following properties can be configured:
Default value: top + + timezone + Specific a timezone to show clock.
+
Possible examples values: America/New_York, America/Santiago, Etc/GMT+10 +
Default value: none + + + - \ No newline at end of file + diff --git a/modules/default/clock/clock.js b/modules/default/clock/clock.js index db41d7e0..c84c133e 100644 --- a/modules/default/clock/clock.js +++ b/modules/default/clock/clock.js @@ -8,7 +8,7 @@ Module.register("clock",{ // Module config defaults. defaults: { - displayType: 'digital', // options: digital, analog, both + displayType: "digital", // options: digital, analog, both timeFormat: config.timeFormat, displaySeconds: true, @@ -18,15 +18,16 @@ Module.register("clock",{ showDate: true, /* specific to the analog clock */ - analogSize: '200px', - analogFace: 'simple', // options: 'none', 'simple', 'face-###' (where ### is 001 to 012 inclusive) - analogPlacement: 'bottom', // options: 'top', 'bottom', 'left', 'right' - analogShowDate: 'top', // options: false, 'top', or 'bottom' - secondsColor: '#888888', + analogSize: "200px", + analogFace: "simple", // options: 'none', 'simple', 'face-###' (where ### is 001 to 012 inclusive) + analogPlacement: "bottom", // options: 'top', 'bottom', 'left', 'right' + analogShowDate: "top", // options: false, 'top', or 'bottom' + secondsColor: "#888888", + timezone: null, }, // Define required scripts. getScripts: function() { - return ["moment.js"]; + return ["moment.js", "moment-timezone.js"]; }, // Define styles. getStyles: function() { @@ -69,10 +70,14 @@ Module.register("clock",{ // So we need to generate the timestring manually. // See issue: https://github.com/MichMich/MagicMirror/issues/181 var timeString; + var now = moment(); + if (this.config.timezone) { + now.tz(this.config.timezone); + } if (this.config.clockBold === true) { - timeString = moment().format("HH[]mm[]"); + timeString = now.format("HH[]mm[]"); } else { - timeString = moment().format("HH:mm"); + timeString = now.format("HH:mm"); } if (this.config.timeFormat !== 24) { @@ -80,27 +85,21 @@ Module.register("clock",{ // var hours = now.getHours() % 12 || 12; if (this.config.clockBold === true) { //timeString = hours + moment().format("[]mm[]"); - timeString = moment().format("h[]mm[]"); + timeString = now.format("h[]mm[]"); } else { //timeString = hours + moment().format(":mm"); - timeString = moment().format("h:mm"); + timeString = now.format("h:mm"); } } if(this.config.showDate){ - dateWrapper.innerHTML = moment().format("dddd, LL"); + dateWrapper.innerHTML = now.format("dddd, LL"); } timeWrapper.innerHTML = timeString; - secondsWrapper.innerHTML = moment().format("ss"); + secondsWrapper.innerHTML = now.format("ss"); if (this.config.showPeriodUpper) { - periodWrapper.innerHTML = moment().format("A"); + periodWrapper.innerHTML = now.format("A"); } else { - periodWrapper.innerHTML = moment().format("a"); - } - if (this.config.displaySeconds) { - timeWrapper.appendChild(secondsWrapper); - } - if (this.config.showPeriod && this.config.timeFormat !== 24) { - timeWrapper.appendChild(periodWrapper); + periodWrapper.innerHTML = now.format("a"); } if (this.config.displaySeconds) { timeWrapper.appendChild(secondsWrapper); @@ -113,12 +112,15 @@ Module.register("clock",{ * Create wrappers for ANALOG clock, only if specified in config */ - if (this.config.displayType !== 'digital') { + if (this.config.displayType !== "digital") { // If it isn't 'digital', then an 'analog' clock was also requested // Calculate the degree offset for each hand of the clock - var now = moment(), - second = now.seconds() * 6, + var now = moment(); + if (this.config.timezone) { + now.tz(this.config.timezone); + } + var second = now.seconds() * 6, minute = now.minute() * 6 + second / 60, hour = ((now.hours() % 12) / 12) * 360 + 90 + minute / 12; @@ -129,10 +131,10 @@ Module.register("clock",{ clockCircle.style.width = this.config.analogSize; clockCircle.style.height = this.config.analogSize; - if (this.config.analogFace != '' && this.config.analogFace != 'simple' && this.config.analogFace != 'none') { + if (this.config.analogFace != "" && this.config.analogFace != "simple" && this.config.analogFace != "none") { clockCircle.style.background = "url("+ this.data.path + "faces/" + this.config.analogFace + ".svg)"; clockCircle.style.backgroundSize = "100%"; - } else if (this.config.analogFace != 'none') { + } else if (this.config.analogFace != "none") { clockCircle.style.border = "2px solid white"; } var clockFace = document.createElement("div"); @@ -166,18 +168,18 @@ Module.register("clock",{ * Combine wrappers, check for .displayType */ - if (this.config.displayType === 'digital') { + if (this.config.displayType === "digital") { // Display only a digital clock wrapper.appendChild(dateWrapper); wrapper.appendChild(timeWrapper); - } else if (this.config.displayType === 'analog') { + } else if (this.config.displayType === "analog") { // Display only an analog clock dateWrapper.style.textAlign = "center"; dateWrapper.style.paddingBottom = "15px"; - if (this.config.analogShowDate === 'top') { + if (this.config.analogShowDate === "top") { wrapper.appendChild(dateWrapper); wrapper.appendChild(clockCircle); - } else if (this.config.analogShowDate === 'bottom') { + } else if (this.config.analogShowDate === "bottom") { wrapper.appendChild(clockCircle); wrapper.appendChild(dateWrapper); } else { @@ -197,11 +199,11 @@ Module.register("clock",{ digitalWrapper.appendChild(dateWrapper); digitalWrapper.appendChild(timeWrapper); - if (placement === 'left' || placement === 'right') { + if (placement === "left" || placement === "right") { digitalWrapper.style.display = "inline-block"; digitalWrapper.style.verticalAlign = "top"; analogWrapper.style.display = "inline-block"; - if (placement === 'left') { + if (placement === "left") { analogWrapper.style.padding = "0 20px 0 0"; wrapper.appendChild(analogWrapper); wrapper.appendChild(digitalWrapper); @@ -212,13 +214,13 @@ Module.register("clock",{ } } else { digitalWrapper.style.textAlign = "center"; - if (placement === 'top') { + if (placement === "top") { analogWrapper.style.padding = "0 0 20px 0"; wrapper.appendChild(analogWrapper); wrapper.appendChild(digitalWrapper); } else { analogWrapper.style.padding = "20px 0 0 0"; - wrapper.appendChild(digitalWrapper); + wrapper.appendChild(digitalWrapper); wrapper.appendChild(analogWrapper); } } @@ -227,4 +229,4 @@ Module.register("clock",{ // Return the wrapper to the dom. return wrapper; } -}); \ No newline at end of file +}); diff --git a/modules/default/compliments/README.md b/modules/default/compliments/README.md index bf5b9538..430c4fbf 100644 --- a/modules/default/compliments/README.md +++ b/modules/default/compliments/README.md @@ -56,6 +56,15 @@ The following properties can be configured:
Default value: See compliment configuration below. + + remoteFile + External file from which to load the compliments
+
Possible values:Path to a JSON file containing compliments, configured + as per the value of the compliments configuration (see below). An object with three arrays: + morning, afternoon and evening. - compliments.json +
Default value: null (Do not load from file) + + @@ -63,6 +72,44 @@ The following properties can be configured: The `compliments` property contains an object with three arrays: morning, afternoon andevening. Based on the time of the day, the compliments will be picked out of one of these arrays. The arrays contain one or multiple compliments. + +If use the currentweather is possible use a actual weather for set compliments. The availables properties are: +* day_sunny +* day_cloudy +* cloudy +* cloudy_windy +* showers +* rain +* thunderstorm +* snow +* fog +* night_clear +* night_cloudy +* night_showers +* night_rain +* night_thunderstorm +* night_snow +* night_alt_cloudy_windy + +#### Example use with currentweather module +````javascript +config: { + compliments: { + day_sunny: [ + 'Today is a sunny day', + 'It\'s a beautiful day' + ], + snow: [ + 'Snowball battle!' + ], + rain: [ + 'Don\'t forget your umbrella' + ] + } +} +```` + + #### Default value: ````javascript config: { @@ -85,3 +132,32 @@ config: { } } ```` + +### External Compliment File +You may specify an external file that contains the three compliment arrays. This is particularly useful if you have a +large number of compliments and do not wish to crowd your `config.js` file with a large array of compliments. +Adding the `remoteFile` variable will override an array you specify in the configuration file. + +This file must be straight JSON. Note that the array names need quotes +around them ("morning", "afternoon", "evening", "snow", "rain", etc.). +#### Example compliments.json file: +````json +{ + "morning" : [ + "Good morning, sunshine!", + "Who needs coffee when you have your smile?", + "Go get 'em, Tiger!" + ], + "afternoon" : [ + "Hitting your stride!", + "You are making a difference!", + "You're more fun than bubble wrap!" + ], + "evening" : [ + "You made someone smile today, I know it.", + "You are making a difference.", + "The day was better for your efforts." + ] +} +```` + diff --git a/modules/default/compliments/compliments.js b/modules/default/compliments/compliments.js index 61f9ec5f..9f514ff1 100644 --- a/modules/default/compliments/compliments.js +++ b/modules/default/compliments/compliments.js @@ -29,9 +29,13 @@ Module.register("compliments",{ ] }, updateInterval: 30000, + remoteFile: null, fadeSpeed: 4000 }, + // Set currentweather from module + currentWeatherType: "", + // Define required scripts. getScripts: function() { return ["moment.js"]; @@ -43,6 +47,12 @@ Module.register("compliments",{ this.lastComplimentIndex = -1; + if (this.config.remoteFile != null) { + this.complimentFile((response) => { + this.config.compliments = JSON.parse(response); + }); + } + // Schedule update timer. var self = this; setInterval(function() { @@ -84,14 +94,36 @@ Module.register("compliments",{ */ complimentArray: function() { var hour = moment().hour(); + var compliments = null; if (hour >= 3 && hour < 12) { - return this.config.compliments.morning; + compliments = this.config.compliments.morning; } else if (hour >= 12 && hour < 17) { - return this.config.compliments.afternoon; + compliments = this.config.compliments.afternoon; } else { - return this.config.compliments.evening; + compliments = this.config.compliments.evening; } + + if ( this.currentWeatherType in this.config.compliments) { + compliments.push.apply(compliments, this.config.compliments[this.currentWeatherType]); + } + return compliments; + + }, + + /* complimentFile(callback) + * Retrieve a file from the local filesystem + */ + complimentFile: function(callback) { + var xobj = new XMLHttpRequest(); + xobj.overrideMimeType("application/json"); + xobj.open("GET", this.file(this.config.remoteFile), true); + xobj.onreadystatechange = function () { + if (xobj.readyState == 4 && xobj.status == "200") { + callback(xobj.responseText); + } + }; + xobj.send(null); }, /* complimentArray() @@ -112,10 +144,44 @@ Module.register("compliments",{ var compliment = document.createTextNode(complimentText); var wrapper = document.createElement("div"); - wrapper.className = "thin xlarge bright"; + wrapper.className = this.config.classes ? this.config.classes : "thin xlarge bright"; wrapper.appendChild(compliment); return wrapper; - } + }, + + + // From data currentweather set weather type + setCurrentWeatherType: function(data) { + var weatherIconTable = { + "01d": "day_sunny", + "02d": "day_cloudy", + "03d": "cloudy", + "04d": "cloudy_windy", + "09d": "showers", + "10d": "rain", + "11d": "thunderstorm", + "13d": "snow", + "50d": "fog", + "01n": "night_clear", + "02n": "night_cloudy", + "03n": "night_cloudy", + "04n": "night_cloudy", + "09n": "night_showers", + "10n": "night_rain", + "11n": "night_thunderstorm", + "13n": "night_snow", + "50n": "night_alt_cloudy_windy" + }; + this.currentWeatherType = weatherIconTable[data.weather[0].icon]; + }, + + + // Override notification handler. + notificationReceived: function(notification, payload, sender) { + if (notification == "CURRENTWEATHER_DATA") { + this.setCurrentWeatherType(payload.data); + } + }, }); diff --git a/modules/default/currentweather/README.md b/modules/default/currentweather/README.md index 2a6f7844..ccef543c 100644 --- a/modules/default/currentweather/README.md +++ b/modules/default/currentweather/README.md @@ -14,7 +14,7 @@ modules: [ config: { // See 'Configuration options' for more information. location: 'Amsterdam,Netherlands', - locationID: '', //Location ID from http://bulk.openweather.org/sample/ + locationID: '', //Location ID from http://openweathermap.org/help/city_list.txt appid: 'abcde12345abcde12345abcde12345ab' //openweathermap.org API key. } } @@ -35,19 +35,20 @@ The following properties can be configured: - location The location used for weather information.
-
Example: Amsterdam,Netherlands -
Default value: New York +
Example: 'Amsterdam,Netherlands' +
Default value: false

+ Note: When the location and locationID 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. locationID - Location ID from OpenWeather This will override anything you put in location.
Leave blank if you want to use location. + Location ID from OpenWeatherMap This will override anything you put in location.
Leave blank if you want to use location.
Example: 1234567 -
Default value: +
Default value: false

+ Note: When the location and locationID 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. @@ -63,18 +64,25 @@ The following properties can be configured:
Default value: config.units + + roundTemp + Round temperature value to nearest integer.
+
Possible values: true (round to integer) or false (display exact value with decimal point) +
Default value: false + + updateInterval How often does the content needs to be fetched? (Milliseconds)

Possible values: 1000 - 86400000 -
Default value: 300000 (10 minutes) +
Default value: 600000 (10 minutes) animationSpeed Speed of the update animation. (Milliseconds)

Possible values:0 - 5000 -
Default value: 2000 (2 seconds) +
Default value: 1000 (1 second) @@ -105,6 +113,20 @@ The following properties can be configured:
Default value: true + + showHumidity + Show the current humidity
+
Possible values: true or false +
Default value: false + + + + onlyTemp + Show only current Temperature and weather icon.
+
Possible values: true or false +
Default value: false + + useBeaufort Pick between using the Beaufort scale for wind speed or using the default units.
@@ -151,6 +173,18 @@ The following properties can be configured:
Default value: 'weather' + + appendLocationNameToHeader + If set to true, 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.
+
Default value: true + + + + calendarClass + The class for the calender module to base the event based weather information on.
+
Default value: 'calendar' + + iconTable The conversion table to convert the weather conditions to weather-icons.
diff --git a/modules/default/currentweather/currentweather.css b/modules/default/currentweather/currentweather.css index 9e9d9ed3..a40be878 100644 --- a/modules/default/currentweather/currentweather.css +++ b/modules/default/currentweather/currentweather.css @@ -6,3 +6,11 @@ -webkit-transform: translate(0, -3px); /* Safari */ transform: translate(0, -3px); } + +.currentweather .humidityIcon { + padding-right: 4px; +} + +.currentweather .humidity-padding { + padding-bottom: 6px; +} diff --git a/modules/default/currentweather/currentweather.js b/modules/default/currentweather/currentweather.js index 5265b099..e92550ea 100644 --- a/modules/default/currentweather/currentweather.js +++ b/modules/default/currentweather/currentweather.js @@ -11,8 +11,8 @@ Module.register("currentweather",{ // Default module config. defaults: { - location: "", - locationID: "", + location: false, + locationID: false, appid: "", units: config.units, updateInterval: 10 * 60 * 1000, // every 10 minutes @@ -23,6 +23,7 @@ Module.register("currentweather",{ showWindDirection: true, useBeaufort: true, lang: config.language, + showHumidity: false, initialLoadDelay: 0, // 0 seconds delay retryDelay: 2500, @@ -31,6 +32,12 @@ Module.register("currentweather",{ apiBase: "http://api.openweathermap.org/data/", weatherEndpoint: "weather", + appendLocationNameToHeader: true, + calendarClass: "calendar", + + onlyTemp: false, + roundTemp: false, + iconTable: { "01d": "wi-day-sunny", "02d": "wi-day-cloudy", @@ -53,6 +60,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. getScripts: function() { return ["moment.js"]; @@ -88,35 +101,16 @@ Module.register("currentweather",{ this.loaded = false; this.scheduleUpdate(this.config.initialLoadDelay); - this.updateTimer = null; - }, - // Override dom generator. - getDom: function() { - var wrapper = document.createElement("div"); - - if (this.config.appid === "") { - wrapper.innerHTML = "Please set the correct openweather appid in the config for module: " + this.name + "."; - wrapper.className = "dimmed light small"; - return wrapper; - } - - if (this.config.location === "") { - wrapper.innerHTML = "Please set the openweather location in the config for module: " + this.name + "."; - wrapper.className = "dimmed light small"; - return wrapper; - } - - if (!this.loaded) { - wrapper.innerHTML = this.translate('LOADING'); - wrapper.className = "dimmed light small"; - return wrapper; - } + // add extra information of current weather + // windDirection, humidity, sunrise and sunset + addExtraInfoWeather: function(wrapper) { var small = document.createElement("div"); small.className = "normal medium"; + var windIcon = document.createElement("span"); windIcon.className = "wi wi-strong-wind dimmed"; small.appendChild(windIcon); @@ -134,6 +128,22 @@ Module.register("currentweather",{ spacer.innerHTML = " "; small.appendChild(spacer); + if (this.config.showHumidity) { + var humidity = document.createElement("span"); + humidity.innerHTML = this.humidity; + + var spacer = document.createElement("sup"); + spacer.innerHTML = " "; + + var humidityIcon = document.createElement("sup"); + humidityIcon.className = "wi wi-humidity humidityIcon"; + humidityIcon.innerHTML = " "; + + small.appendChild(humidity); + small.appendChild(spacer); + small.appendChild(humidityIcon); + } + var sunriseSunsetIcon = document.createElement("span"); sunriseSunsetIcon.className = "wi dimmed " + this.sunriseSunsetIcon; small.appendChild(sunriseSunsetIcon); @@ -142,6 +152,29 @@ Module.register("currentweather",{ sunriseSunsetTime.innerHTML = " " + this.sunriseSunsetTime; small.appendChild(sunriseSunsetTime); + wrapper.appendChild(small); + }, + + // Override dom generator. + getDom: function() { + var wrapper = document.createElement("div"); + + if (this.config.appid === "") { + wrapper.innerHTML = "Please set the correct openweather appid in the config for module: " + this.name + "."; + wrapper.className = "dimmed light small"; + return wrapper; + } + + if (!this.loaded) { + wrapper.innerHTML = this.translate("LOADING"); + wrapper.className = "dimmed light small"; + return wrapper; + } + + if (this.config.onlyTemp === false) { + this.addExtraInfoWeather(wrapper); + } + var large = document.createElement("div"); large.className = "large light"; @@ -154,16 +187,54 @@ Module.register("currentweather",{ temperature.innerHTML = " " + this.temperature + "°"; large.appendChild(temperature); - wrapper.appendChild(small); wrapper.appendChild(large); 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) * Requests new data from openweather.org. * Calls processWeather on succesfull response. */ updateWeather: function() { + 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 retry = true; @@ -175,11 +246,10 @@ Module.register("currentweather",{ if (this.status === 200) { self.processWeather(JSON.parse(this.response)); } else if (this.status === 401) { - self.config.appid = ""; self.updateDom(self.config.animationSpeed); Log.error(self.name + ": Incorrect APPID."); - retry = false; + retry = true; } else { Log.error(self.name + ": Could not load weather."); } @@ -199,11 +269,19 @@ Module.register("currentweather",{ */ getParams: function() { var params = "?"; - if(this.config.locationID !== "") { + if(this.config.locationID) { params += "id=" + this.config.locationID; - } else { + } else if(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 += "&lang=" + this.config.lang; params += "&APPID=" + this.config.appid; @@ -224,6 +302,7 @@ Module.register("currentweather",{ return; } + this.humidity = parseFloat(data.main.humidity); this.temperature = this.roundValue(data.main.temp); if (this.config.useBeaufort){ @@ -244,20 +323,20 @@ Module.register("currentweather",{ // So we need to generate the timestring manually. // See issue: https://github.com/MichMich/MagicMirror/issues/181 var sunriseSunsetDateObject = (sunrise < now && sunset > now) ? sunset : sunrise; - var timeString = moment(sunriseSunsetDateObject).format('HH:mm'); + var timeString = moment(sunriseSunsetDateObject).format("HH:mm"); if (this.config.timeFormat !== 24) { //var hours = sunriseSunsetDateObject.getHours() % 12 || 12; if (this.config.showPeriod) { if (this.config.showPeriodUpper) { //timeString = hours + moment(sunriseSunsetDateObject).format(':mm A'); - timeString = moment(sunriseSunsetDateObject).format('h:mm A'); + timeString = moment(sunriseSunsetDateObject).format("h:mm A"); } else { //timeString = hours + moment(sunriseSunsetDateObject).format(':mm a'); - timeString = moment(sunriseSunsetDateObject).format('h:mm a'); + timeString = moment(sunriseSunsetDateObject).format("h:mm a"); } } else { //timeString = hours + moment(sunriseSunsetDateObject).format(':mm'); - timeString = moment(sunriseSunsetDateObject).format('h:mm'); + timeString = moment(sunriseSunsetDateObject).format("h:mm"); } } @@ -265,9 +344,10 @@ Module.register("currentweather",{ this.sunriseSunsetIcon = (sunrise < now && sunset > now) ? "wi-sunset" : "wi-sunrise"; - + this.show(this.config.animationSpeed, {lockString:this.identifier}); this.loaded = true; this.updateDom(this.config.animationSpeed); + this.sendNotification("CURRENTWEATHER_DATA", {data: data}); }, /* scheduleUpdate() @@ -306,52 +386,51 @@ Module.register("currentweather",{ return 12; }, + deg2Cardinal: function(deg) { + if (deg>11.25 && deg<=33.75){ + return "NNE"; + } else if (deg > 33.75 && deg <= 56.25) { + return "NE"; + } else if (deg > 56.25 && deg <= 78.75) { + return "ENE"; + } else if (deg > 78.75 && deg <= 101.25) { + return "E"; + } else if (deg > 101.25 && deg <= 123.75) { + return "ESE"; + } else if (deg > 123.75 && deg <= 146.25) { + return "SE"; + } else if (deg > 146.25 && deg <= 168.75) { + return "SSE"; + } else if (deg > 168.75 && deg <= 191.25) { + return "S"; + } else if (deg > 191.25 && deg <= 213.75) { + return "SSW"; + } else if (deg > 213.75 && deg <= 236.25) { + return "SW"; + } else if (deg > 236.25 && deg <= 258.75) { + return "WSW"; + } else if (deg > 258.75 && deg <= 281.25) { + return "W"; + } else if (deg > 281.25 && deg <= 303.75) { + return "WNW"; + } else if (deg > 303.75 && deg <= 326.25) { + return "NW"; + } else if (deg > 326.25 && deg <= 348.75) { + return "NNW"; + } else { + return "N"; + } + }, + /* function(temperature) - * Rounds a temperature to 1 decimal. + * Rounds a temperature to 1 decimal or integer (depending on config.roundTemp). * * argument temperature number - Temperature. * * return number - Rounded Temperature. */ - - deg2Cardinal: function(deg) { - if (deg>11.25 && deg<=33.75){ - return "NNE"; - } else if (deg > 33.75 && deg <= 56.25) { - return "NE"; - } else if (deg > 56.25 && deg <= 78.75) { - return "ENE"; - } else if (deg > 78.75 && deg <= 101.25) { - return "E"; - } else if (deg > 101.25 && deg <= 123.75) { - return "ESE"; - } else if (deg > 123.75 && deg <= 146.25) { - return "SE"; - } else if (deg > 146.25 && deg <= 168.75) { - return "SSE"; - } else if (deg > 168.75 && deg <= 191.25) { - return "S"; - } else if (deg > 191.25 && deg <= 213.75) { - return "SSW"; - } else if (deg > 213.75 && deg <= 236.25) { - return "SW"; - } else if (deg > 236.25 && deg <= 258.75) { - return "WSW"; - } else if (deg > 258.75 && deg <= 281.25) { - return "W"; - } else if (deg > 281.25 && deg <= 303.75) { - return "WNW"; - } else if (deg > 303.75 && deg <= 326.25) { - return "NW"; - } else if (deg > 326.25 && deg <= 348.75) { - return "NNW"; - } else { - return "N"; - } - }, - - roundValue: function(temperature) { - return parseFloat(temperature).toFixed(1); + var decimals = this.config.roundTemp ? 0 : 1; + return parseFloat(temperature).toFixed(decimals); } }); diff --git a/modules/default/defaultmodules.js b/modules/default/defaultmodules.js index 42e89ee0..fccf3c52 100644 --- a/modules/default/defaultmodules.js +++ b/modules/default/defaultmodules.js @@ -15,7 +15,8 @@ var defaultModules = [ "currentweather", "helloworld", "newsfeed", - "weatherforecast" + "weatherforecast", + "updatenotification" ]; /*************** DO NOT EDIT THE LINE BELOW ***************/ diff --git a/modules/default/newsfeed/README.md b/modules/default/newsfeed/README.md index 951fa51c..58c7b9ae 100644 --- a/modules/default/newsfeed/README.md +++ b/modules/default/newsfeed/README.md @@ -53,6 +53,7 @@ The following properties can be configured: { title: "New York Times", url: "http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml", + encoding: "UTF-8" } ] @@ -98,7 +99,7 @@ The following properties can be configured: animationSpeed Speed of the update animation. (Milliseconds)

Possible values:0 - 5000 -
Default value: 2000 (2 seconds) +
Default value: 2500 (2.5 seconds) @@ -112,13 +113,13 @@ The following properties can be configured: removeEndTags: false, startTags: [], endTags: [] - - + + removeStartTags Some newsfeeds feature tags at the beginning of their titles or descriptions, such as [VIDEO]. This setting allows for the removal of specified tags from the beginning of an item's description and/or title.
-
Possible values:'title', 'description', 'both' +
Possible values:'title', 'description', 'both' @@ -130,7 +131,7 @@ The following properties can be configured: removeEndTags Remove specified tags from the end of an item's description and/or title.
-
Possible values:'title', 'description', 'both' +
Possible values:'title', 'description', 'both' @@ -172,7 +173,7 @@ The `feeds` property contains an array with multiple objects. These objects have The encoding of the news feed.

This property is optional.
Possible values:'UTF-8', 'ISO-8859-1', etc ... -
Default value: 'UTF-8' +
Default value: 'UTF-8' diff --git a/modules/default/newsfeed/fetcher.js b/modules/default/newsfeed/fetcher.js index e7c3c388..b7511de9 100644 --- a/modules/default/newsfeed/fetcher.js +++ b/modules/default/newsfeed/fetcher.js @@ -44,8 +44,9 @@ var Fetcher = function(url, reloadInterval, encoding) { parser.on("item", function(item) { var title = item.title; - var description = item.description || item.summary || item.content || ''; + var description = item.description || item.summary || item.content || ""; var pubdate = item.pubdate || item.published || item.updated; + var url = item.url || item.link || ""; if (title && pubdate) { @@ -56,15 +57,16 @@ var Fetcher = function(url, reloadInterval, encoding) { title: title, description: description, pubdate: pubdate, + url: url, }); } else { - console.log("Can't parse feed item:"); - console.log(item); - console.log('Title: ' + title); - console.log('Description: ' + description); - console.log('Pubdate: ' + pubdate); + // console.log("Can't parse feed item:"); + // console.log(item); + // console.log('Title: ' + title); + // console.log('Description: ' + description); + // console.log('Pubdate: ' + pubdate); } }); @@ -79,13 +81,11 @@ var Fetcher = function(url, reloadInterval, encoding) { scheduleTimer(); }); - var headers = {'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A'}; - request({uri: url, encoding: null, headers: headers}) - .on('error', function(error) { - fetchFailedCallback(self, error); - scheduleTimer(); - }) - .pipe(iconv.decodeStream(encoding)).pipe(parser); + + nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); + headers = {"User-Agent": "Mozilla/5.0 (Node.js "+ nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"} + + request({uri: url, encoding: null, headers: headers}).pipe(iconv.decodeStream(encoding)).pipe(parser); }; diff --git a/modules/default/newsfeed/newsfeed.js b/modules/default/newsfeed/newsfeed.js index 7d8d08c7..74014208 100644 --- a/modules/default/newsfeed/newsfeed.js +++ b/modules/default/newsfeed/newsfeed.js @@ -25,11 +25,11 @@ Module.register("newsfeed",{ updateInterval: 10 * 1000, animationSpeed: 2.5 * 1000, maxNewsItems: 0, // 0 for unlimited - removeStartTags: '', - removeEndTags: '', + removeStartTags: "", + removeEndTags: "", startTags: [], endTags: [] - + }, // Define required scripts. @@ -93,62 +93,70 @@ Module.register("newsfeed",{ var sourceAndTimestamp = document.createElement("div"); sourceAndTimestamp.className = "light small dimmed"; - if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== '') sourceAndTimestamp.innerHTML = this.newsItems[this.activeItem].sourceTitle; - if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== '' && this.config.showPublishDate) sourceAndTimestamp.innerHTML += ', '; - if (this.config.showPublishDate) sourceAndTimestamp.innerHTML += moment(new Date(this.newsItems[this.activeItem].pubdate)).fromNow(); - if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== '' || this.config.showPublishDate) sourceAndTimestamp.innerHTML += ':'; + if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") { + sourceAndTimestamp.innerHTML = this.newsItems[this.activeItem].sourceTitle; + } + if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "" && this.config.showPublishDate) { + sourceAndTimestamp.innerHTML += ", "; + } + if (this.config.showPublishDate) { + sourceAndTimestamp.innerHTML += moment(new Date(this.newsItems[this.activeItem].pubdate)).fromNow(); + } + if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "" || this.config.showPublishDate) { + sourceAndTimestamp.innerHTML += ":"; + } wrapper.appendChild(sourceAndTimestamp); } //Remove selected tags from the beginning of rss feed items (title or description) - if (this.config.removeStartTags == 'title' || 'both') { - + if (this.config.removeStartTags == "title" || "both") { + for (f=0; f + + + + Option + Description + + + + + updateInterval + How often do you want to check for a new version? This value represents the interval in milliseconds.
+
Possible values: Any value above 60000 (1 minute); +
Default value: 600000 (10 minutes); + + + + \ No newline at end of file diff --git a/modules/default/updatenotification/node_helper.js b/modules/default/updatenotification/node_helper.js new file mode 100644 index 00000000..6196552f --- /dev/null +++ b/modules/default/updatenotification/node_helper.js @@ -0,0 +1,87 @@ +var SimpleGit = require("simple-git"); +var simpleGits = []; +var fs = require("fs"); +var path = require("path"); +var defaultModules = require(__dirname + "/../defaultmodules.js"); +var NodeHelper = require("node_helper"); + +module.exports = NodeHelper.create({ + + config: {}, + + updateTimer: null, + + start: function () { + }, + + configureModules: function(modules) { + for (moduleName in modules) { + if (defaultModules.indexOf(moduleName) < 0) { + // Default modules are included in the main MagicMirror repo + var moduleFolder = path.normalize(__dirname + "/../../" + moduleName); + + var stat; + try { + stat = fs.statSync(path.join(moduleFolder, ".git")); + } catch(err) { + // Error when directory .git doesn't exist + // This module is not managed with git, skip + continue; + } + + var res = function(mn, mf) { + var git = SimpleGit(mf); + git.getRemotes(true, function(err, remotes) { + if (remotes.length < 1 || remotes[0].name.length < 1) { + // No valid remote for folder, skip + return; + } + + // Folder has .git and has at least one git remote, watch this folder + simpleGits.push({"module": mn, "git": git}); + }); + }(moduleName, moduleFolder); + } + } + + // Push MagicMirror itself last, biggest chance it'll show up last in UI and isn't overwritten + simpleGits.push({"module": "default", "git": SimpleGit(path.normalize(__dirname + "/../../../"))}); + }, + + socketNotificationReceived: function (notification, payload) { + if (notification === "CONFIG") { + this.config = payload; + } else if(notification === "MODULES") { + this.configureModules(payload); + this.preformFetch(); + } + }, + + preformFetch() { + var self = this; + + simpleGits.forEach(function(sg) { + sg.git.fetch().status(function(err, data) { + data.module = sg.module; + 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); + } + +}); diff --git a/modules/default/updatenotification/updatenotification.js b/modules/default/updatenotification/updatenotification.js new file mode 100644 index 00000000..f663f593 --- /dev/null +++ b/modules/default/updatenotification/updatenotification.js @@ -0,0 +1,70 @@ +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.sendSocketNotification("MODULES", Module.definitions); + 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 = " "; + message.appendChild(icon); + + var text = document.createElement("span"); + if (this.status.module == "default") { + text.innerHTML = this.translate("UPDATE_NOTIFICATION"); + } else { + text.innerHTML = this.translate("UPDATE_NOTIFICATION_MODULE").replace("MODULE_NAME", this.status.module); + } + 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; + } +}); diff --git a/modules/default/weatherforecast/README.md b/modules/default/weatherforecast/README.md index aba03a41..2842af54 100644 --- a/modules/default/weatherforecast/README.md +++ b/modules/default/weatherforecast/README.md @@ -14,7 +14,7 @@ modules: [ config: { // See 'Configuration options' for more information. location: 'Amsterdam,Netherlands', - locationID: '', //Location ID from http://bulk.openweather.org/sample/ + locationID: '', //Location ID from http://openweathermap.org/help/city_list.txt appid: 'abcde12345abcde12345abcde12345ab' //openweathermap.org API key. } } @@ -35,19 +35,20 @@ The following properties can be configured: - location The location used for weather information.
-
Example: Amsterdam,Netherlands -
Default value: New York +
Example: 'Amsterdam,Netherlands' +
Default value: false

+ Note: When the location and locationID 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. locationID - Location ID from OpenWeather This will override anything you put in location.
Leave blank if you want to use location. + Location ID from OpenWeatherMap This will override anything you put in location.
Leave blank if you want to use location.
Example: 1234567 -
Default value: +
Default value: false

+ Note: When the location and locationID 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. @@ -63,6 +64,13 @@ The following properties can be configured:
Default value: config.units + + roundTemp + Round temperature values to nearest integer.
+
Possible values: true (round to integer) or false (display exact value with decimal point) +
Default value: false + + maxNumberOfDays How many days of forecast to return. Specified by config.js
@@ -71,21 +79,28 @@ The following properties can be configured:
This value is optional. By default the weatherforecast module will return 7 days. + + showRainAmount + Should the predicted rain amount be displayed?
+
Possible values: true or false +
Default value: false +
This value is optional. By default the weatherforecast module will not display the predicted amount of rain. + + updateInterval How often does the content needs to be fetched? (Milliseconds)

Possible values: 1000 - 86400000 -
Default value: 300000 (10 minutes) +
Default value: 600000 (10 minutes) animationSpeed Speed of the update animation. (Milliseconds)

Possible values:0 - 5000 -
Default value: 2000 (2 seconds) +
Default value: 1000 (1 second) - lang The language of the days.
@@ -111,7 +126,7 @@ The following properties can be configured: initialLoadDelay The initial delay before loading. If you have multiple modules that use the same API key, you might want to delay one of the requests. (Milliseconds)

Possible values: 1000 - 5000 -
Default value: 0 +
Default value: 2500 (2.5 seconds delay. This delay is used to keep the OpenWeather API happy.) @@ -134,11 +149,23 @@ The following properties can be configured: - weatherEndpoint + forecastEndpoint The OpenWeatherMap API endPoint.

Default value: 'forecast/daily' + + appendLocationNameToHeader + If set to true, 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.
+
Default value: true + + + + calendarClass + The class for the calender module to base the event based weather information on.
+
Default value: 'calendar' + + iconTable The conversion table to convert the weather conditions to weather-icons.
@@ -164,6 +191,5 @@ The following properties can be configured: } - diff --git a/modules/default/weatherforecast/weatherforecast.css b/modules/default/weatherforecast/weatherforecast.css index 7b12c42d..62c9767f 100644 --- a/modules/default/weatherforecast/weatherforecast.css +++ b/modules/default/weatherforecast/weatherforecast.css @@ -12,3 +12,8 @@ padding-left: 20px; padding-right: 0; } + +.weatherforecast .rain { + padding-left: 20px; + padding-right: 0; +} diff --git a/modules/default/weatherforecast/weatherforecast.js b/modules/default/weatherforecast/weatherforecast.js index a8a8ca6c..9bd15ba8 100644 --- a/modules/default/weatherforecast/weatherforecast.js +++ b/modules/default/weatherforecast/weatherforecast.js @@ -11,11 +11,12 @@ Module.register("weatherforecast",{ // Default module config. defaults: { - location: "", - locationID: "", + location: false, + locationID: false, appid: "", units: config.units, maxNumberOfDays: 7, + showRainAmount: false, updateInterval: 10 * 60 * 1000, // every 10 minutes animationSpeed: 1000, timeFormat: config.timeFormat, @@ -30,6 +31,11 @@ Module.register("weatherforecast",{ apiBase: "http://api.openweathermap.org/data/", forecastEndpoint: "forecast/daily", + appendLocationNameToHeader: true, + calendarClass: "calendar", + + roundTemp: false, + iconTable: { "01d": "wi-day-sunny", "02d": "wi-day-cloudy", @@ -52,6 +58,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. getScripts: function() { return ["moment.js"]; @@ -95,12 +107,6 @@ Module.register("weatherforecast",{ return wrapper; } - if (this.config.location === "") { - wrapper.innerHTML = "Please set the openweather location in the config for module: " + this.name + "."; - wrapper.className = "dimmed light small"; - return wrapper; - } - if (!this.loaded) { wrapper.innerHTML = this.translate("LOADING"); wrapper.className = "dimmed light small"; @@ -139,6 +145,17 @@ Module.register("weatherforecast",{ minTempCell.className = "align-right min-temp"; 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.fadePoint < 0) { this.config.fadePoint = 0; @@ -156,11 +173,50 @@ Module.register("weatherforecast",{ 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) * Requests new data from openweather.org. * Calls processWeather on succesfull response. */ updateWeather: function() { + 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 retry = true; @@ -172,11 +228,10 @@ Module.register("weatherforecast",{ if (this.status === 200) { self.processWeather(JSON.parse(this.response)); } else if (this.status === 401) { - self.config.appid = ""; self.updateDom(self.config.animationSpeed); Log.error(self.name + ": Incorrect APPID."); - retry = false; + retry = true; } else { Log.error(self.name + ": Could not load weather."); } @@ -196,11 +251,19 @@ Module.register("weatherforecast",{ */ getParams: function() { var params = "?"; - if(this.config.locationID !== "") { + if(this.config.locationID) { params += "id=" + this.config.locationID; - } else { + } else if(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 += "&lang=" + this.config.lang; /* @@ -220,6 +283,7 @@ Module.register("weatherforecast",{ * argument data object - Weather information received form openweather.org. */ processWeather: function(data) { + this.fetchedLocatioName = data.city.name + ", " + data.city.country; this.forecast = []; for (var i = 0, count = data.list.length; i < count; i++) { @@ -230,13 +294,14 @@ Module.register("weatherforecast",{ day: moment(forecast.dt, "X").format("ddd"), icon: this.config.iconTable[forecast.weather[0].icon], 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); - + this.show(this.config.animationSpeed, {lockString:this.identifier}); this.loaded = true; this.updateDom(this.config.animationSpeed); }, @@ -279,13 +344,14 @@ Module.register("weatherforecast",{ }, /* function(temperature) - * Rounds a temperature to 1 decimal. + * Rounds a temperature to 1 decimal or integer (depending on config.roundTemp). * * argument temperature number - Temperature. * * return number - Rounded Temperature. */ roundValue: function(temperature) { - return parseFloat(temperature).toFixed(1); + var decimals = this.config.roundTemp ? 0 : 1; + return parseFloat(temperature).toFixed(decimals); } }); diff --git a/package.json b/package.json index ad2c0109..118c9837 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,10 @@ { "name": "magicmirror", - "version": "2.0.0", + "version": "2.1.0", "description": "A modular interface for smart mirrors.", "main": "js/electron.js", "scripts": { - "start": "electron js/electron.js", - "test": "snyk test", - "snyk-protect": "snyk protect", - "prepublish": "npm run snyk-protect" + "start": "electron js/electron.js" }, "repository": { "type": "git", @@ -20,7 +17,9 @@ "modular" ], "author": "Michael Teeuw", - "contributors": "https://github.com/MichMich/MagicMirror/graphs/contributors", + "contributors": [ + "https://github.com/MichMich/MagicMirror/graphs/contributors" + ], "license": "MIT", "bugs": { "url": "https://github.com/MichMich/MagicMirror/issues" @@ -30,24 +29,25 @@ "grunt": "latest", "grunt-eslint": "latest", "grunt-jsonlint": "latest", - "grunt-markdownlint": "^1.0.4", + "grunt-markdownlint": "^1.0.13", "grunt-stylelint": "latest", "grunt-yamllint": "latest", "stylelint-config-standard": "latest", "time-grunt": "latest" }, "dependencies": { - "electron-prebuilt": "latest", + "electron": "^1.4.7", "express": "^4.14.0", + "express-ipfilter": "latest", "feedme": "latest", + "helmet": "^3.1.0", "iconv-lite": "latest", "moment": "latest", - "request": "^2.74.0", + "request": "^2.78.0", "rrule": "latest", - "socket.io": "^1.4.6", + "simple-git": "^1.62.0", + "socket.io": "^1.5.1", "valid-url": "latest", - "walk": "latest", - "snyk": "^1.14.1" - }, - "snyk": true + "walk": "latest" + } } diff --git a/serveronly/index.js b/serveronly/index.js index ea435a8e..ccd4c294 100644 --- a/serveronly/index.js +++ b/serveronly/index.js @@ -1,5 +1,6 @@ var app = require("../js/app.js"); app.start(function(config) { console.log(""); - console.log("Ready to go! Please point your browser to: http://localhost:" + config.port); + var bindAddress = config.address ? config.address : "localhost"; + console.log("Ready to go! Please point your browser to: http://" + bindAddress + ":" + config.port); }); diff --git a/splashscreen/MagicMirror.plymouth b/splashscreen/MagicMirror.plymouth new file mode 100644 index 00000000..b6887bf5 --- /dev/null +++ b/splashscreen/MagicMirror.plymouth @@ -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 diff --git a/splashscreen/MagicMirror.script b/splashscreen/MagicMirror.script new file mode 100644 index 00000000..9b614389 --- /dev/null +++ b/splashscreen/MagicMirror.script @@ -0,0 +1,48 @@ +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; + +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); diff --git a/splashscreen/splash.png b/splashscreen/splash.png new file mode 100644 index 00000000..f3da394b Binary files /dev/null and b/splashscreen/splash.png differ diff --git a/translations/da.json b/translations/da.json new file mode 100644 index 00000000..4b346437 --- /dev/null +++ b/translations/da.json @@ -0,0 +1,35 @@ +{ + /* GENERAL */ + "LOADING": "Indlæser …", + + /* CALENDAR */ + "TODAY": "I dag", + "TOMORROW": "I morgen", + "DAYAFTERTOMORROW": "I overmorgen", + "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" + + + /* UPDATE INFO */ + "UPDATE_NOTIFICATION": "MagicMirror² opdatering tilgængelig.", + "UPDATE_NOTIFICATION_MODULE": "Opdatering tilgængelig for MODULE_NAME modulet.", + "UPDATE_INFO": "Den nuværende installation er COMMIT_COUNT bagud på BRANCH_NAME branch'en." +} diff --git a/translations/de.json b/translations/de.json index d8a254a9..54ac5015 100644 --- a/translations/de.json +++ b/translations/de.json @@ -10,20 +10,25 @@ "EMPTY": "Keine Termine.", /* WEATHER */ - "N": "N", - "NNE": "NNO", - "NE": "NO", - "ENE": "ONO", - "E": "O", - "ESE": "OSO", - "SE": "SO", - "SSE": "SSO", - "S": "S", - "SSW": "SSW", - "SW": "SW", - "WSW": "WSW", - "W": "W", - "WNW": "WNW", - "NW": "NW", - "NNW": "NNW" + "N": "N", + "NNE": "NNO", + "NE": "NO", + "ENE": "ONO", + "E": "O", + "ESE": "OSO", + "SE": "SO", + "SSE": "SSO", + "S": "S", + "SSW": "SSW", + "SW": "SW", + "WSW": "WSW", + "W": "W", + "WNW": "WNW", + "NW": "NW", + "NNW": "NNW", + + /* UPDATE INFO */ + "UPDATE_NOTIFICATION": "Aktualisierung für MagicMirror² verfügbar.", + "UPDATE_NOTIFICATION_MODULE": "Aktualisierung für das MODULE_NAME Modul verfügbar.", + "UPDATE_INFO": "Die aktuelle Installation ist COMMIT_COUNT hinter dem BRANCH_NAME branch." } diff --git a/translations/en.json b/translations/en.json index a2d5933a..45609f61 100644 --- a/translations/en.json +++ b/translations/en.json @@ -5,24 +5,30 @@ /* CALENDAR */ "TODAY": "Today", "TOMORROW": "Tomorrow", + "DAYAFTERTOMORROW": "The day after tomorrow", "RUNNING": "Ends in", "EMPTY": "No upcoming events.", /* WEATHER */ - "N": "N", - "NNE": "NNE", - "NE": "NE", - "ENE": "ENE", - "E": "E", - "ESE": "ESE", - "SE": "SE", - "SSE": "SSE", - "S": "S", - "SSW": "SSW", - "SW": "SW", - "WSW": "WSW", - "W": "W", - "WNW": "WNW", - "NW": "NW", - "NNW": "NNW" + "N": "N", + "NNE": "NNE", + "NE": "NE", + "ENE": "ENE", + "E": "E", + "ESE": "ESE", + "SE": "SE", + "SSE": "SSE", + "S": "S", + "SSW": "SSW", + "SW": "SW", + "WSW": "WSW", + "W": "W", + "WNW": "WNW", + "NW": "NW", + "NNW": "NNW", + + /* UPDATE INFO */ + "UPDATE_NOTIFICATION": "MagicMirror² update available.", + "UPDATE_NOTIFICATION_MODULE": "Update available for MODULE_NAME module.", + "UPDATE_INFO": "The current installation is COMMIT_COUNT behind on the BRANCH_NAME branch." } diff --git a/translations/es.json b/translations/es.json index 6de069c7..9b80b45a 100644 --- a/translations/es.json +++ b/translations/es.json @@ -5,24 +5,30 @@ /* CALENDAR */ "TODAY": "Hoy", "TOMORROW": "Mañana", + "DAYAFTERTOMORROW": "Pasado mañana", "RUNNING": "Termina en", "EMPTY": "No hay eventos programados.", /* WEATHER */ - "N": "N", - "NNE": "NNE", - "NE": "NE", - "ENE": "ENE", - "E": "E", - "ESE": "ESE", - "SE": "SE", - "SSE": "SSE", - "S": "S", - "SSW": "SSO", - "SW": "SO", - "WSW": "OSO", - "W": "O", - "WNW": "ONO", - "NW": "NO", - "NNW": "NNO" + "N": "N", + "NNE": "NNE", + "NE": "NE", + "ENE": "ENE", + "E": "E", + "ESE": "ESE", + "SE": "SE", + "SSE": "SSE", + "S": "S", + "SSW": "SSO", + "SW": "SO", + "WSW": "OSO", + "W": "O", + "WNW": "ONO", + "NW": "NO", + "NNW": "NNO", + + /* UPDATE INFO */ + "UPDATE_NOTIFICATION": "MagicMirror² actualización disponible.", + "UPDATE_NOTIFICATION_MODULE": "Disponible una actualización para el módulo MODULE_NAME.", + "UPDATE_INFO": "Tu actual instalación está COMMIT_COUNT cambios detrás de la rama BRANCH_NAME." } diff --git a/translations/fi.json b/translations/fi.json new file mode 100644 index 00000000..b753039e --- /dev/null +++ b/translations/fi.json @@ -0,0 +1,28 @@ +{ + /* GENERAL */ + "LOADING": "Lataa …", + + /* 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" +} diff --git a/translations/fr.json b/translations/fr.json index 7cea0700..2701eb08 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -9,20 +9,20 @@ "EMPTY": "Aucun RDV.", /* WEATHER */ - "N": "N", - "NNE": "NNE", - "NE": "NE", - "ENE": "ENE", - "E": "E", - "ESE": "ESE", - "SE": "SE", - "SSE": "SSE", - "S": "S", - "SSW": "SSO", - "SW": "SO", - "WSW": "OSO", - "W": "O", - "WNW": "ONO", - "NW": "NO", - "NNW": "NNO" + "N": "N", + "NNE": "NNE", + "NE": "NE", + "ENE": "ENE", + "E": "E", + "ESE": "ESE", + "SE": "SE", + "SSE": "SSE", + "S": "S", + "SSW": "SSO", + "SW": "SO", + "WSW": "OSO", + "W": "O", + "WNW": "ONO", + "NW": "NO", + "NNW": "NNO" } diff --git a/translations/fy.json b/translations/fy.json index af6bcde8..44f1b476 100644 --- a/translations/fy.json +++ b/translations/fy.json @@ -5,24 +5,25 @@ /* CALENDAR */ "TODAY": "Hjoed", "TOMORROW": "Moarn", + "DAYAFTERTOMORROW": "Oaremoarn", "RUNNING": "Einigest oer", "EMPTY": "Gjin plande ôfspraken.", /* WEATHER */ - "N": "N", - "NNE": "NNE", - "NE": "NE", - "ENE": "ENE", - "E": "E", - "ESE": "ESE", - "SE": "SE", - "SSE": "SSE", - "S": "S", - "SSW": "SSW", - "SW": "SW", - "WSW": "WSW", - "W": "W", - "WNW": "WNW", - "NW": "NW", - "NNW": "NNW" + "N": "N", + "NNE": "NNE", + "NE": "NE", + "ENE": "ENE", + "E": "E", + "ESE": "ESE", + "SE": "SE", + "SSE": "SSE", + "S": "S", + "SSW": "SSW", + "SW": "SW", + "WSW": "WSW", + "W": "W", + "WNW": "WNW", + "NW": "NW", + "NNW": "NNW" } diff --git a/translations/it.json b/translations/it.json index 74048a8b..bde608b4 100644 --- a/translations/it.json +++ b/translations/it.json @@ -9,20 +9,20 @@ "EMPTY": "Nessun evento in arrivo.", /* WEATHER */ - "N": "N", - "NNE": "NNE", - "NE": "NE", - "ENE": "ENE", - "E": "E", - "ESE": "ESE", - "SE": "SE", - "SSE": "SSE", - "S": "S", - "SSW": "SSW", - "SW": "SW", - "WSW": "WSW", - "W": "W", - "WNW": "WNW", - "NW": "NW", - "NNW": "NNW" + "N": "N", + "NNE": "NNE", + "NE": "NE", + "ENE": "ENE", + "E": "E", + "ESE": "ESE", + "SE": "SE", + "SSE": "SSE", + "S": "S", + "SSW": "SSW", + "SW": "SW", + "WSW": "WSW", + "W": "W", + "WNW": "WNW", + "NW": "NW", + "NNW": "NNW" } diff --git a/translations/ja.json b/translations/ja.json index 40d5996a..72849559 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -1,28 +1,28 @@ { /* GENERAL */ - "LOADING": "ローディング …", + "LOADING": "ローディング …", - /* CALENDAR */ - "TODAY":"今日", - "TOMORROW":"明日", - "RUNNING":"で終わります", - "EMPTY":"直近のイベントはありません", + /* CALENDAR */ + "TODAY": "今日", + "TOMORROW": "明日", + "RUNNING": "で終わります", + "EMPTY": "直近のイベントはありません", - /* WEATHER */ - "N":"北", - "NNE":"北北東", - "NE":"北東", - "ENE":"東北東", - "E":"東", - "ESE":"東南東", - "SE":"南東", - "SSE":"南南東", - "S":"南", - "SSW":"南南西", - "SW":"南西", - "WSW":"西南西", - "W":"西", - "WNW":"西北西", - "NW":"北西", - "NNW":"北北西" + /* WEATHER */ + "N": "北", + "NNE": "北北東", + "NE": "北東", + "ENE": "東北東", + "E": "東", + "ESE": "東南東", + "SE": "南東", + "SSE": "南南東", + "S": "南", + "SSW": "南南西", + "SW": "南西", + "WSW": "西南西", + "W": "西", + "WNW": "西北西", + "NW": "北西", + "NNW": "北北西" } diff --git a/translations/nb.json b/translations/nb.json index 737d0e26..48f0ac6f 100644 --- a/translations/nb.json +++ b/translations/nb.json @@ -9,20 +9,20 @@ "EMPTY": "Ingen kommende arrangementer.", /* 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" + "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" } diff --git a/translations/nl.json b/translations/nl.json index e4e882ea..803db06c 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -10,20 +10,25 @@ "EMPTY": "Geen geplande afspraken.", /* WEATHER */ - "N": "N", - "NNE": "NNO", - "NE": "NO", - "ENE": "ONO", - "E": "O", - "ESE": "OZO", - "SE": "ZO", - "SSE": "ZZO", - "S": "Z", - "SSW": "ZZW", - "SW": "ZW", - "WSW": "WZW", - "W": "W", - "WNW": "WNW", - "NW": "NW", - "NNW": "NNW" + "N": "N", + "NNE": "NNO", + "NE": "NO", + "ENE": "ONO", + "E": "O", + "ESE": "OZO", + "SE": "ZO", + "SSE": "ZZO", + "S": "Z", + "SSW": "ZZW", + "SW": "ZW", + "WSW": "WZW", + "W": "W", + "WNW": "WNW", + "NW": "NW", + "NNW": "NNW", + + /* UPDATE INFO */ + "UPDATE_NOTIFICATION": "MagicMirror² update beschikbaar.", + "UPDATE_NOTIFICATION_MODULE": "Update beschikbaar voor MODULE_NAME module.", + "UPDATE_INFO": "De huidige installatie loopt COMMIT_COUNT achter op de BRANCH_NAME branch." } diff --git a/translations/nn.json b/translations/nn.json index 2eb072ea..b7dbe3ea 100644 --- a/translations/nn.json +++ b/translations/nn.json @@ -9,20 +9,20 @@ "EMPTY": "Ingen komande hendingar.", /* WEATHER */ - "N": "N", - "NNE": "NNA", - "NE": "NA", - "ENE": "ANA", - "E": "A", - "ESE": "ASA", - "SE": "SA", - "SSE": "SSA", - "S": "S", - "SSW": "SSV", - "SW": "SV", - "WSW": "VSV", - "W": "V", - "WNW": "VNV", - "NW": "NV", - "NNW": "NNV" + "N": "N", + "NNE": "NNA", + "NE": "NA", + "ENE": "ANA", + "E": "A", + "ESE": "ASA", + "SE": "SA", + "SSE": "SSA", + "S": "S", + "SSW": "SSV", + "SW": "SV", + "WSW": "VSV", + "W": "V", + "WNW": "VNV", + "NW": "NV", + "NNW": "NNV" } diff --git a/translations/pl.json b/translations/pl.json index bc6753ac..7a47745b 100644 --- a/translations/pl.json +++ b/translations/pl.json @@ -9,20 +9,25 @@ "EMPTY": "Brak wydarzeń.", /* WEATHER */ - "N": "N", - "NNE": "NNE", - "NE": "NE", - "ENE": "ENE", - "E": "E", - "ESE": "ESE", - "SE": "SE", - "SSE": "SSE", - "S": "S", - "SSW": "SSW", - "SW": "SW", - "WSW": "WSW", - "W": "W", - "WNW": "WNW", - "NW": "NW", - "NNW": "NNW" + "N": "N", + "NNE": "NNE", + "NE": "NE", + "ENE": "ENE", + "E": "E", + "ESE": "ESE", + "SE": "SE", + "SSE": "SSE", + "S": "S", + "SSW": "SSW", + "SW": "SW", + "WSW": "WSW", + "W": "W", + "WNW": "WNW", + "NW": "NW", + "NNW": "NNW" + + /* UPDATE INFO */ + "UPDATE_NOTIFICATION": "Dostępna jest aktualizacja MagicMirror².", + "UPDATE_NOTIFICATION_MODULE": "Dostępna jest aktualizacja modułu MODULE_NAME.", + "UPDATE_INFO": "The current installation is COMMIT_COUNT behind on the BRANCH_NAME branch." } diff --git a/translations/pt.json b/translations/pt.json index e7269ac1..f8797f0f 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -9,20 +9,20 @@ "EMPTY": "Sem eventos a chegar.", /* WEATHER */ - "N": "N", - "NNE": "NNE", - "NE": "NE", - "ENE": "ENE", - "E": "E", - "ESE": "ESE", - "SE": "SE", - "SSE": "SSE", - "S": "S", - "SSW": "SSO", - "SW": "SO", - "WSW": "OSO", - "W": "O", - "WNW": "ONO", - "NW": "NO", - "NNW": "NNO" + "N": "N", + "NNE": "NNE", + "NE": "NE", + "ENE": "ENE", + "E": "E", + "ESE": "ESE", + "SE": "SE", + "SSE": "SSE", + "S": "S", + "SSW": "SSO", + "SW": "SO", + "WSW": "OSO", + "W": "O", + "WNW": "ONO", + "NW": "NO", + "NNW": "NNO" } diff --git a/translations/pt_br.json b/translations/pt_br.json index 57bb4314..17d3e636 100644 --- a/translations/pt_br.json +++ b/translations/pt_br.json @@ -9,20 +9,20 @@ "EMPTY": "Nenhum evento novo.", /* WEATHER */ - "N": "N", - "NNE": "NNE", - "NE": "NE", - "ENE": "ENE", - "E": "E", - "ESE": "ESE", - "SE": "SE", - "SSE": "SSE", - "S": "S", - "SSW": "SSO", - "SW": "SO", - "WSW": "OSO", - "W": "O", - "WNW": "ONO", - "NW": "NO", - "NNW": "NNO" + "N": "N", + "NNE": "NNE", + "NE": "NE", + "ENE": "ENE", + "E": "E", + "ESE": "ESE", + "SE": "SE", + "SSE": "SSE", + "S": "S", + "SSW": "SSO", + "SW": "SO", + "WSW": "OSO", + "W": "O", + "WNW": "ONO", + "NW": "NO", + "NNW": "NNO" } diff --git a/translations/sv.json b/translations/sv.json index 6badd3bc..9643a07b 100644 --- a/translations/sv.json +++ b/translations/sv.json @@ -9,20 +9,20 @@ "EMPTY": "Inga kommande händelser.", /* WEATHER */ - "N": "N", - "NNE": "NNO", - "NE": "NO", - "ENE": "ONO", - "E": "Ö", - "ESE": "OSO", - "SE": "SO", - "SSE": "SSO", - "S": "S", - "SSW": "SSV", - "SW": "SV", - "WSW": "VSV", - "W": "V", - "WNW": "VNV", - "NW": "NV", - "NNW": "NNV" + "N": "N", + "NNE": "NNO", + "NE": "NO", + "ENE": "ONO", + "E": "Ö", + "ESE": "OSO", + "SE": "SO", + "SSE": "SSO", + "S": "S", + "SSW": "SSV", + "SW": "SV", + "WSW": "VSV", + "W": "V", + "WNW": "VNV", + "NW": "NV", + "NNW": "NNV" } diff --git a/translations/tr.json b/translations/tr.json new file mode 100644 index 00000000..a0d61ac7 --- /dev/null +++ b/translations/tr.json @@ -0,0 +1,28 @@ +{ + /* GENERAL */ + "LOADING": "Yükleniyor …", + + /* CALENDAR */ + "TODAY": "Bugün", + "TOMORROW": "Yarın", + "RUNNING": "Biten", + "EMPTY": "Yakında etkinlik yok.", + + /* WEATHER */ + "N": "K", + "NNE": "KKD", + "NE": "KD", + "ENE": "DKD", + "E": "D", + "ESE": "DGD", + "SE": "GD", + "SSE": "GGD", + "S": "G", + "SSW": "GGB", + "SW": "GB", + "WSW": "BGB", + "W": "B", + "WNW": "BKB", + "NW": "KB", + "NNW": "KKB" +} diff --git a/translations/translations.js b/translations/translations.js index 2a458ef2..d572c803 100644 --- a/translations/translations.js +++ b/translations/translations.js @@ -9,6 +9,7 @@ var translations = { "en" : "translations/en.json", // English "nl" : "translations/nl.json", // Dutch "de" : "translations/de.json", // German + "fi" : "translations/fi.json", // Suomi "fr" : "translations/fr.json", // French "fy" : "translations/fy.json", // Frysk "es" : "translations/es.json", // Spanish @@ -23,4 +24,6 @@ var translations = { "ja" : "translations/ja.json", // Japanese "pl" : "translations/pl.json", // Polish "gr" : "translations/gr.json", // Greek + "da" : "translations/da.json", // Danish + "tr" : "translations/tr.json", // Turkish }; diff --git a/translations/zh_cn.json b/translations/zh_cn.json index b6d21d37..a8c32d70 100644 --- a/translations/zh_cn.json +++ b/translations/zh_cn.json @@ -9,20 +9,20 @@ "EMPTY": "没有更多的活动。", /* WEATHER */ - "N": "北风", - "NNE": "北偏东风", - "NE": "东北风", - "ENE": "东偏北风", - "E": "东风", - "ESE": "东偏南风", - "SE": "东南风", - "SSE": "南偏东风", - "S": "南风", - "SSW": "南偏西风", - "SW": "西南风", - "WSW": "西偏南风", - "W": "西风", - "WNW": "西偏北风", - "NW": "西北风", - "NNW": "北偏西风" + "N": "北风", + "NNE": "北偏东风", + "NE": "东北风", + "ENE": "东偏北风", + "E": "东风", + "ESE": "东偏南风", + "SE": "东南风", + "SSE": "南偏东风", + "S": "南风", + "SSW": "南偏西风", + "SW": "西南风", + "WSW": "西偏南风", + "W": "西风", + "WNW": "西偏北风", + "NW": "西北风", + "NNW": "北偏西风" } diff --git a/translations/zh_tw.json b/translations/zh_tw.json index b6f78f12..1a5827be 100644 --- a/translations/zh_tw.json +++ b/translations/zh_tw.json @@ -9,20 +9,20 @@ "EMPTY": "沒有更多的活動。", /* WEATHER */ - "N": "北風", - "NNE": "北偏東風", - "NE": "東北風", - "ENE": "東偏北風", - "E": "東風", - "ESE": "東偏南風", - "SE": "東南風", - "SSE": "南偏東風", - "S": "南風", - "SSW": "南偏西風", - "SW": "西南風", - "WSW": "西偏南風", - "W": "西風", - "WNW": "西偏北風", - "NW": "西北風", - "NNW": "北偏西風" + "N": "北風", + "NNE": "北偏東風", + "NE": "東北風", + "ENE": "東偏北風", + "E": "東風", + "ESE": "東偏南風", + "SE": "東南風", + "SSE": "南偏東風", + "S": "南風", + "SSW": "南偏西風", + "SW": "西南風", + "WSW": "西偏南風", + "W": "西風", + "WNW": "西偏北風", + "NW": "西北風", + "NNW": "北偏西風" } diff --git a/vendor/moment/moment-timezone.js b/vendor/moment/moment-timezone.js new file mode 100644 index 00000000..e98675cc --- /dev/null +++ b/vendor/moment/moment-timezone.js @@ -0,0 +1,1197 @@ +//! moment-timezone.js +//! version : 0.5.7 +//! author : Tim Wood +//! license : MIT +//! github.com/moment/moment-timezone + +(function (root, factory) { + "use strict"; + + /*global define*/ + if (typeof define === 'function' && define.amd) { + define(['moment'], factory); // AMD + } else if (typeof module === 'object' && module.exports) { + module.exports = factory(require('moment')); // Node + } else { + factory(root.moment); // Browser + } +}(this, function (moment) { + "use strict"; + + // Do not load moment-timezone a second time. + if (moment.tz !== undefined) { + logError('Moment Timezone ' + moment.tz.version + ' was already loaded ' + (moment.tz.dataVersion ? 'with data from ' : 'without any data') + moment.tz.dataVersion); + return moment; + } + + var VERSION = "0.5.7", + zones = {}, + links = {}, + names = {}, + guesses = {}, + cachedGuess, + + momentVersion = moment.version.split('.'), + major = +momentVersion[0], + minor = +momentVersion[1]; + + // Moment.js version check + if (major < 2 || (major === 2 && minor < 6)) { + logError('Moment Timezone requires Moment.js >= 2.6.0. You are using Moment.js ' + moment.version + '. See momentjs.com'); + } + + /************************************ + Unpacking + ************************************/ + + function charCodeToInt(charCode) { + if (charCode > 96) { + return charCode - 87; + } else if (charCode > 64) { + return charCode - 29; + } + return charCode - 48; + } + + function unpackBase60(string) { + var i = 0, + parts = string.split('.'), + whole = parts[0], + fractional = parts[1] || '', + multiplier = 1, + num, + out = 0, + sign = 1; + + // handle negative numbers + if (string.charCodeAt(0) === 45) { + i = 1; + sign = -1; + } + + // handle digits before the decimal + for (i; i < whole.length; i++) { + num = charCodeToInt(whole.charCodeAt(i)); + out = 60 * out + num; + } + + // handle digits after the decimal + for (i = 0; i < fractional.length; i++) { + multiplier = multiplier / 60; + num = charCodeToInt(fractional.charCodeAt(i)); + out += num * multiplier; + } + + return out * sign; + } + + function arrayToInt (array) { + for (var i = 0; i < array.length; i++) { + array[i] = unpackBase60(array[i]); + } + } + + function intToUntil (array, length) { + for (var i = 0; i < length; i++) { + array[i] = Math.round((array[i - 1] || 0) + (array[i] * 60000)); // minutes to milliseconds + } + + array[length - 1] = Infinity; + } + + function mapIndices (source, indices) { + var out = [], i; + + for (i = 0; i < indices.length; i++) { + out[i] = source[indices[i]]; + } + + return out; + } + + function unpack (string) { + var data = string.split('|'), + offsets = data[2].split(' '), + indices = data[3].split(''), + untils = data[4].split(' '); + + arrayToInt(offsets); + arrayToInt(indices); + arrayToInt(untils); + + intToUntil(untils, indices.length); + + return { + name : data[0], + abbrs : mapIndices(data[1].split(' '), indices), + offsets : mapIndices(offsets, indices), + untils : untils, + population : data[5] | 0 + }; + } + + /************************************ + Zone object + ************************************/ + + function Zone (packedString) { + if (packedString) { + this._set(unpack(packedString)); + } + } + + Zone.prototype = { + _set : function (unpacked) { + this.name = unpacked.name; + this.abbrs = unpacked.abbrs; + this.untils = unpacked.untils; + this.offsets = unpacked.offsets; + this.population = unpacked.population; + }, + + _index : function (timestamp) { + var target = +timestamp, + untils = this.untils, + i; + + for (i = 0; i < untils.length; i++) { + if (target < untils[i]) { + return i; + } + } + }, + + parse : function (timestamp) { + var target = +timestamp, + offsets = this.offsets, + untils = this.untils, + max = untils.length - 1, + offset, offsetNext, offsetPrev, i; + + for (i = 0; i < max; i++) { + offset = offsets[i]; + offsetNext = offsets[i + 1]; + offsetPrev = offsets[i ? i - 1 : i]; + + if (offset < offsetNext && tz.moveAmbiguousForward) { + offset = offsetNext; + } else if (offset > offsetPrev && tz.moveInvalidForward) { + offset = offsetPrev; + } + + if (target < untils[i] - (offset * 60000)) { + return offsets[i]; + } + } + + return offsets[max]; + }, + + abbr : function (mom) { + return this.abbrs[this._index(mom)]; + }, + + offset : function (mom) { + return this.offsets[this._index(mom)]; + } + }; + + /************************************ + Current Timezone + ************************************/ + + function OffsetAt(at) { + var timeString = at.toTimeString(); + var abbr = timeString.match(/\([a-z ]+\)/i); + if (abbr && abbr[0]) { + // 17:56:31 GMT-0600 (CST) + // 17:56:31 GMT-0600 (Central Standard Time) + abbr = abbr[0].match(/[A-Z]/g); + abbr = abbr ? abbr.join('') : undefined; + } else { + // 17:56:31 CST + // 17:56:31 GMT+0800 (台北標準時間) + abbr = timeString.match(/[A-Z]{3,5}/g); + abbr = abbr ? abbr[0] : undefined; + } + + if (abbr === 'GMT') { + abbr = undefined; + } + + this.at = +at; + this.abbr = abbr; + this.offset = at.getTimezoneOffset(); + } + + function ZoneScore(zone) { + this.zone = zone; + this.offsetScore = 0; + this.abbrScore = 0; + } + + ZoneScore.prototype.scoreOffsetAt = function (offsetAt) { + this.offsetScore += Math.abs(this.zone.offset(offsetAt.at) - offsetAt.offset); + if (this.zone.abbr(offsetAt.at).replace(/[^A-Z]/g, '') !== offsetAt.abbr) { + this.abbrScore++; + } + }; + + function findChange(low, high) { + var mid, diff; + + while ((diff = ((high.at - low.at) / 12e4 | 0) * 6e4)) { + mid = new OffsetAt(new Date(low.at + diff)); + if (mid.offset === low.offset) { + low = mid; + } else { + high = mid; + } + } + + return low; + } + + function userOffsets() { + var startYear = new Date().getFullYear() - 2, + last = new OffsetAt(new Date(startYear, 0, 1)), + offsets = [last], + change, next, i; + + for (i = 1; i < 48; i++) { + next = new OffsetAt(new Date(startYear, i, 1)); + if (next.offset !== last.offset) { + change = findChange(last, next); + offsets.push(change); + offsets.push(new OffsetAt(new Date(change.at + 6e4))); + } + last = next; + } + + for (i = 0; i < 4; i++) { + offsets.push(new OffsetAt(new Date(startYear + i, 0, 1))); + offsets.push(new OffsetAt(new Date(startYear + i, 6, 1))); + } + + return offsets; + } + + function sortZoneScores (a, b) { + if (a.offsetScore !== b.offsetScore) { + return a.offsetScore - b.offsetScore; + } + if (a.abbrScore !== b.abbrScore) { + return a.abbrScore - b.abbrScore; + } + return b.zone.population - a.zone.population; + } + + function addToGuesses (name, offsets) { + var i, offset; + arrayToInt(offsets); + for (i = 0; i < offsets.length; i++) { + offset = offsets[i]; + guesses[offset] = guesses[offset] || {}; + guesses[offset][name] = true; + } + } + + function guessesForUserOffsets (offsets) { + var offsetsLength = offsets.length, + filteredGuesses = {}, + out = [], + i, j, guessesOffset; + + for (i = 0; i < offsetsLength; i++) { + guessesOffset = guesses[offsets[i].offset] || {}; + for (j in guessesOffset) { + if (guessesOffset.hasOwnProperty(j)) { + filteredGuesses[j] = true; + } + } + } + + for (i in filteredGuesses) { + if (filteredGuesses.hasOwnProperty(i)) { + out.push(names[i]); + } + } + + return out; + } + + function rebuildGuess () { + + // use Intl API when available and returning valid time zone + try { + var intlName = Intl.DateTimeFormat().resolvedOptions().timeZone; + if (intlName){ + var name = names[normalizeName(intlName)]; + if (name) { + return name; + } + logError("Moment Timezone found " + intlName + " from the Intl api, but did not have that data loaded."); + } + } catch (e) { + // Intl unavailable, fall back to manual guessing. + } + + var offsets = userOffsets(), + offsetsLength = offsets.length, + guesses = guessesForUserOffsets(offsets), + zoneScores = [], + zoneScore, i, j; + + for (i = 0; i < guesses.length; i++) { + zoneScore = new ZoneScore(getZone(guesses[i]), offsetsLength); + for (j = 0; j < offsetsLength; j++) { + zoneScore.scoreOffsetAt(offsets[j]); + } + zoneScores.push(zoneScore); + } + + zoneScores.sort(sortZoneScores); + + return zoneScores.length > 0 ? zoneScores[0].zone.name : undefined; + } + + function guess (ignoreCache) { + if (!cachedGuess || ignoreCache) { + cachedGuess = rebuildGuess(); + } + return cachedGuess; + } + + /************************************ + Global Methods + ************************************/ + + function normalizeName (name) { + return (name || '').toLowerCase().replace(/\//g, '_'); + } + + function addZone (packed) { + var i, name, split, normalized; + + if (typeof packed === "string") { + packed = [packed]; + } + + for (i = 0; i < packed.length; i++) { + split = packed[i].split('|'); + name = split[0]; + normalized = normalizeName(name); + zones[normalized] = packed[i]; + names[normalized] = name; + if (split[5]) { + addToGuesses(normalized, split[2].split(' ')); + } + } + } + + function getZone (name, caller) { + name = normalizeName(name); + + var zone = zones[name]; + var link; + + if (zone instanceof Zone) { + return zone; + } + + if (typeof zone === 'string') { + zone = new Zone(zone); + zones[name] = zone; + return zone; + } + + // Pass getZone to prevent recursion more than 1 level deep + if (links[name] && caller !== getZone && (link = getZone(links[name], getZone))) { + zone = zones[name] = new Zone(); + zone._set(link); + zone.name = names[name]; + return zone; + } + + return null; + } + + function getNames () { + var i, out = []; + + for (i in names) { + if (names.hasOwnProperty(i) && (zones[i] || zones[links[i]]) && names[i]) { + out.push(names[i]); + } + } + + return out.sort(); + } + + function addLink (aliases) { + var i, alias, normal0, normal1; + + if (typeof aliases === "string") { + aliases = [aliases]; + } + + for (i = 0; i < aliases.length; i++) { + alias = aliases[i].split('|'); + + normal0 = normalizeName(alias[0]); + normal1 = normalizeName(alias[1]); + + links[normal0] = normal1; + names[normal0] = alias[0]; + + links[normal1] = normal0; + names[normal1] = alias[1]; + } + } + + function loadData (data) { + addZone(data.zones); + addLink(data.links); + tz.dataVersion = data.version; + } + + function zoneExists (name) { + if (!zoneExists.didShowError) { + zoneExists.didShowError = true; + logError("moment.tz.zoneExists('" + name + "') has been deprecated in favor of !moment.tz.zone('" + name + "')"); + } + return !!getZone(name); + } + + function needsOffset (m) { + return !!(m._a && (m._tzm === undefined)); + } + + function logError (message) { + if (typeof console !== 'undefined' && typeof console.error === 'function') { + console.error(message); + } + } + + /************************************ + moment.tz namespace + ************************************/ + + function tz (input) { + var args = Array.prototype.slice.call(arguments, 0, -1), + name = arguments[arguments.length - 1], + zone = getZone(name), + out = moment.utc.apply(null, args); + + if (zone && !moment.isMoment(input) && needsOffset(out)) { + out.add(zone.parse(out), 'minutes'); + } + + out.tz(name); + + return out; + } + + tz.version = VERSION; + tz.dataVersion = ''; + tz._zones = zones; + tz._links = links; + tz._names = names; + tz.add = addZone; + tz.link = addLink; + tz.load = loadData; + tz.zone = getZone; + tz.zoneExists = zoneExists; // deprecated in 0.1.0 + tz.guess = guess; + tz.names = getNames; + tz.Zone = Zone; + tz.unpack = unpack; + tz.unpackBase60 = unpackBase60; + tz.needsOffset = needsOffset; + tz.moveInvalidForward = true; + tz.moveAmbiguousForward = false; + + /************************************ + Interface with Moment.js + ************************************/ + + var fn = moment.fn; + + moment.tz = tz; + + moment.defaultZone = null; + + moment.updateOffset = function (mom, keepTime) { + var zone = moment.defaultZone, + offset; + + if (mom._z === undefined) { + if (zone && needsOffset(mom) && !mom._isUTC) { + mom._d = moment.utc(mom._a)._d; + mom.utc().add(zone.parse(mom), 'minutes'); + } + mom._z = zone; + } + if (mom._z) { + offset = mom._z.offset(mom); + if (Math.abs(offset) < 16) { + offset = offset / 60; + } + if (mom.utcOffset !== undefined) { + mom.utcOffset(-offset, keepTime); + } else { + mom.zone(offset, keepTime); + } + } + }; + + fn.tz = function (name) { + if (name) { + this._z = getZone(name); + if (this._z) { + moment.updateOffset(this); + } else { + logError("Moment Timezone has no data for " + name + ". See http://momentjs.com/timezone/docs/#/data-loading/."); + } + return this; + } + if (this._z) { return this._z.name; } + }; + + function abbrWrap (old) { + return function () { + if (this._z) { return this._z.abbr(this); } + return old.call(this); + }; + } + + function resetZoneWrap (old) { + return function () { + this._z = null; + return old.apply(this, arguments); + }; + } + + fn.zoneName = abbrWrap(fn.zoneName); + fn.zoneAbbr = abbrWrap(fn.zoneAbbr); + fn.utc = resetZoneWrap(fn.utc); + + moment.tz.setDefault = function(name) { + if (major < 2 || (major === 2 && minor < 9)) { + logError('Moment Timezone setDefault() requires Moment.js >= 2.9.0. You are using Moment.js ' + moment.version + '.'); + } + moment.defaultZone = name ? getZone(name) : null; + return moment; + }; + + // Cloning a moment should include the _z property. + var momentProperties = moment.momentProperties; + if (Object.prototype.toString.call(momentProperties) === '[object Array]') { + // moment 2.8.1+ + momentProperties.push('_z'); + momentProperties.push('_a'); + } else if (momentProperties) { + // moment 2.7.0 + momentProperties._z = null; + } + + loadData({ + "version": "2016h", + "zones": [ + "Africa/Abidjan|GMT|0|0||48e5", + "Africa/Khartoum|EAT|-30|0||51e5", + "Africa/Algiers|CET|-10|0||26e5", + "Africa/Lagos|WAT|-10|0||17e6", + "Africa/Maputo|CAT|-20|0||26e5", + "Africa/Cairo|EET EEST|-20 -30|010101010|1Cby0 Fb0 c10 8n0 8Nd0 gL0 e10 mn0|15e6", + "Africa/Casablanca|WET WEST|0 -10|01010101010101010101010101010101010101010|1Cco0 Db0 1zd0 Lz0 1Nf0 wM0 co0 go0 1o00 s00 dA0 vc0 11A0 A00 e00 y00 11A0 uM0 e00 Dc0 11A0 s00 e00 IM0 WM0 mo0 gM0 LA0 WM0 jA0 e00 Rc0 11A0 e00 e00 U00 11A0 8o0 e00 11A0|32e5", + "Europe/Paris|CET CEST|-10 -20|01010101010101010101010|1BWp0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|11e6", + "Africa/Johannesburg|SAST|-20|0||84e5", + "Africa/Tripoli|EET CET CEST|-20 -10 -20|0120|1IlA0 TA0 1o00|11e5", + "Africa/Windhoek|WAST WAT|-20 -10|01010101010101010101010|1C1c0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 11B0|32e4", + "America/Adak|HST HDT|a0 90|01010101010101010101010|1BR00 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|326", + "America/Anchorage|AKST AKDT|90 80|01010101010101010101010|1BQX0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|30e4", + "America/Santo_Domingo|AST|40|0||29e5", + "America/Araguaina|BRT BRST|30 20|010|1IdD0 Lz0|14e4", + "America/Argentina/Buenos_Aires|ART|30|0|", + "America/Asuncion|PYST PYT|30 40|01010101010101010101010|1C430 1a10 1fz0 1a10 1fz0 1cN0 17b0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1fB0 19X0 1fB0 19X0 1ip0 17b0 1ip0 17b0 1ip0|28e5", + "America/Panama|EST|50|0||15e5", + "America/Bahia|BRT BRST|30 20|010|1FJf0 Rb0|27e5", + "America/Bahia_Banderas|MST CDT CST|70 50 60|01212121212121212121212|1C1l0 1nW0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0|84e3", + "America/Fortaleza|BRT|30|0||34e5", + "America/Managua|CST|60|0||22e5", + "America/Manaus|AMT|40|0||19e5", + "America/Bogota|COT|50|0||90e5", + "America/Denver|MST MDT|70 60|01010101010101010101010|1BQV0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|26e5", + "America/Campo_Grande|AMST AMT|30 40|01010101010101010101010|1BIr0 1zd0 On0 1zd0 Rb0 1zd0 Lz0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 On0 1zd0 On0 1C10 Lz0 1C10 Lz0 1C10|77e4", + "America/Cancun|CST CDT EST|60 50 50|010101010102|1C1k0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 Dd0|63e4", + "America/Caracas|VET VET|4u 40|01|1QMT0|29e5", + "America/Cayenne|GFT|30|0||58e3", + "America/Chicago|CST CDT|60 50|01010101010101010101010|1BQU0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|92e5", + "America/Chihuahua|MST MDT|70 60|01010101010101010101010|1C1l0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0|81e4", + "America/Phoenix|MST|70|0||42e5", + "America/Los_Angeles|PST PDT|80 70|01010101010101010101010|1BQW0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|15e6", + "America/New_York|EST EDT|50 40|01010101010101010101010|1BQT0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|21e6", + "America/Rio_Branco|AMT ACT|40 50|01|1KLE0|31e4", + "America/Fort_Nelson|PST PDT MST|80 70 70|010101010102|1BQW0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0|39e2", + "America/Halifax|AST ADT|40 30|01010101010101010101010|1BQS0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|39e4", + "America/Godthab|WGT WGST|30 20|01010101010101010101010|1BWp0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|17e3", + "America/Goose_Bay|AST ADT|40 30|01010101010101010101010|1BQQ1 1zb0 Op0 1zcX Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|76e2", + "America/Grand_Turk|EST EDT AST|50 40 40|0101010101012|1BQT0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|37e2", + "America/Guayaquil|ECT|50|0||27e5", + "America/Guyana|GYT|40|0||80e4", + "America/Havana|CST CDT|50 40|01010101010101010101010|1BQR0 1wo0 U00 1zc0 U00 1qM0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0|21e5", + "America/La_Paz|BOT|40|0||19e5", + "America/Lima|PET|50|0||11e6", + "America/Mexico_City|CST CDT|60 50|01010101010101010101010|1C1k0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 14p0 1lb0|20e6", + "America/Metlakatla|PST AKST AKDT|80 90 80|012121212121|1PAa0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|14e2", + "America/Miquelon|PMST PMDT|30 20|01010101010101010101010|1BQR0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|61e2", + "America/Montevideo|UYST UYT|20 30|010101010101|1BQQ0 1ld0 14n0 1ld0 14n0 1o10 11z0 1o10 11z0 1o10 11z0|17e5", + "America/Noronha|FNT|20|0||30e2", + "America/North_Dakota/Beulah|MST MDT CST CDT|70 60 60 50|01232323232323232323232|1BQV0 1zb0 Oo0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0", + "America/Paramaribo|SRT|30|0||24e4", + "America/Port-au-Prince|EST EDT|50 40|010101010|1GI70 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|23e5", + "America/Santiago|CLST CLT|30 40|010101010101010101010|1C1f0 1fB0 1nX0 G10 1EL0 Op0 1zb0 Rd0 1wn0 Rd0 46n0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0|62e5", + "America/Sao_Paulo|BRST BRT|20 30|01010101010101010101010|1BIq0 1zd0 On0 1zd0 Rb0 1zd0 Lz0 1C10 Lz0 1C10 On0 1zd0 On0 1zd0 On0 1zd0 On0 1C10 Lz0 1C10 Lz0 1C10|20e6", + "America/Scoresbysund|EGT EGST|10 0|01010101010101010101010|1BWp0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|452", + "America/St_Johns|NST NDT|3u 2u|01010101010101010101010|1BQPv 1zb0 Op0 1zcX Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0|11e4", + "Antarctica/Casey|+11 +08|-b0 -80|0101|1BN30 40P0 KL0|10", + "Antarctica/Davis|+05 +07|-50 -70|0101|1BPw0 3Wn0 KN0|70", + "Antarctica/DumontDUrville|+10|-a0|0||80", + "Antarctica/Macquarie|AEDT MIST|-b0 -b0|01|1C140|1", + "Asia/Tashkent|+05|-50|0||23e5", + "Pacific/Auckland|NZDT NZST|-d0 -c0|01010101010101010101010|1C120 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00|14e5", + "Antarctica/Rothera|-03|30|0||130", + "Antarctica/Syowa|+03|-30|0||20", + "Antarctica/Troll|+00 +02|0 -20|01010101010101010101010|1BWp0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|40", + "Asia/Almaty|+06|-60|0||15e5", + "Asia/Baghdad|AST|-30|0||66e5", + "Asia/Amman|EET EEST|-20 -30|010101010101010101010|1BVy0 1qM0 11A0 1o00 11A0 4bX0 Dd0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0|25e5", + "Asia/Kamchatka|+12 +11|-c0 -b0|010|1Dp30 WM0|18e4", + "Asia/Baku|+04 +05|-40 -50|0101010101010|1BWo0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00|27e5", + "Asia/Bangkok|ICT|-70|0||15e6", + "Asia/Barnaul|+06 +07|-60 -70|010101|1BWk0 1qM0 WM0 8Hz0 3rd0", + "Asia/Beirut|EET EEST|-20 -30|01010101010101010101010|1BWm0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0|22e5", + "Asia/Brunei|BNT|-80|0||42e4", + "Asia/Kolkata|IST|-5u|0||15e6", + "Asia/Chita|+09 +10 +08|-90 -a0 -80|010120|1BWh0 1qM0 WM0 8Hz0 3re0|33e4", + "Asia/Choibalsan|CHOT CHOST|-80 -90|0101010101010|1O8G0 1cJ0 1cP0 1cJ0 1cP0 1fx0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0|38e3", + "Asia/Shanghai|CST|-80|0||23e6", + "Asia/Colombo|+0530|-5u|0||22e5", + "Asia/Dhaka|BDT|-60|0||16e6", + "Asia/Damascus|EET EEST|-20 -30|01010101010101010101010|1C0m0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0|26e5", + "Asia/Dili|TLT|-90|0||19e4", + "Asia/Dubai|GST|-40|0||39e5", + "Asia/Gaza|EET EEST|-20 -30|01010101010101010101010|1BVW1 SKX 1xd1 MKX 1AN0 1a00 1fA0 1cL0 1cN0 1nX0 1210 1nz0 1220 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0|18e5", + "Asia/Hebron|EET EEST|-20 -30|0101010101010101010101010|1BVy0 Tb0 1xd1 MKX bB0 cn0 1cN0 1a00 1fA0 1cL0 1cN0 1nX0 1210 1nz0 1220 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0|25e4", + "Asia/Hong_Kong|HKT|-80|0||73e5", + "Asia/Hovd|HOVT HOVST|-70 -80|0101010101010|1O8H0 1cJ0 1cP0 1cJ0 1cP0 1fx0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0|81e3", + "Asia/Irkutsk|+08 +09|-80 -90|01010|1BWi0 1qM0 WM0 8Hz0|60e4", + "Europe/Istanbul|EET EEST +03|-20 -30 -30|010101010101012|1BWp0 1qM0 Xc0 1qo0 WM0 1qM0 11A0 1o00 1200 1nA0 11A0 1tA0 U00 15w0|13e6", + "Asia/Jakarta|WIB|-70|0||31e6", + "Asia/Jayapura|WIT|-90|0||26e4", + "Asia/Jerusalem|IST IDT|-20 -30|01010101010101010101010|1BVA0 17X0 1kp0 1dz0 1c10 1aL0 1eN0 1oL0 10N0 1oL0 10N0 1oL0 10N0 1rz0 W10 1rz0 W10 1rz0 10N0 1oL0 10N0 1oL0|81e4", + "Asia/Kabul|AFT|-4u|0||46e5", + "Asia/Karachi|PKT|-50|0||24e6", + "Asia/Urumqi|XJT|-60|0||32e5", + "Asia/Kathmandu|NPT|-5J|0||12e5", + "Asia/Khandyga|+10 +11 +09|-a0 -b0 -90|010102|1BWg0 1qM0 WM0 17V0 7zD0|66e2", + "Asia/Krasnoyarsk|+07 +08|-70 -80|01010|1BWj0 1qM0 WM0 8Hz0|10e5", + "Asia/Kuala_Lumpur|MYT|-80|0||71e5", + "Asia/Magadan|+11 +12 +10|-b0 -c0 -a0|010120|1BWf0 1qM0 WM0 8Hz0 3Cq0|95e3", + "Asia/Makassar|WITA|-80|0||15e5", + "Asia/Manila|PHT|-80|0||24e6", + "Europe/Athens|EET EEST|-20 -30|01010101010101010101010|1BWp0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|35e5", + "Asia/Novokuznetsk|+07 +06|-70 -60|010|1Dp80 WM0|55e4", + "Asia/Novosibirsk|+06 +07|-60 -70|010101|1BWk0 1qM0 WM0 8Hz0 4eN0|15e5", + "Asia/Omsk|+06 +07|-60 -70|01010|1BWk0 1qM0 WM0 8Hz0|12e5", + "Asia/Pyongyang|KST KST|-90 -8u|01|1P4D0|29e5", + "Asia/Rangoon|MMT|-6u|0||48e5", + "Asia/Sakhalin|+10 +11|-a0 -b0|010101|1BWg0 1qM0 WM0 8Hz0 3rd0|58e4", + "Asia/Seoul|KST|-90|0||23e6", + "Asia/Singapore|SGT|-80|0||56e5", + "Asia/Srednekolymsk|+11 +12|-b0 -c0|01010|1BWf0 1qM0 WM0 8Hz0|35e2", + "Asia/Tbilisi|+04|-40|0||11e5", + "Asia/Tehran|IRST IRDT|-3u -4u|01010101010101010101010|1BTUu 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0 1cp0 1dz0 1cp0 1dz0|14e6", + "Asia/Thimphu|BTT|-60|0||79e3", + "Asia/Tokyo|JST|-90|0||38e6", + "Asia/Tomsk|+06 +07|-60 -70|010101|1BWk0 1qM0 WM0 8Hz0 3Qp0|10e5", + "Asia/Ulaanbaatar|ULAT ULAST|-80 -90|0101010101010|1O8G0 1cJ0 1cP0 1cJ0 1cP0 1fx0 1cP0 1cJ0 1cP0 1cJ0 1cP0 1cJ0|12e5", + "Asia/Ust-Nera|+11 +12 +10|-b0 -c0 -a0|010102|1BWf0 1qM0 WM0 17V0 7zD0|65e2", + "Asia/Vladivostok|+10 +11|-a0 -b0|01010|1BWg0 1qM0 WM0 8Hz0|60e4", + "Asia/Yakutsk|+09 +10|-90 -a0|01010|1BWh0 1qM0 WM0 8Hz0|28e4", + "Asia/Yekaterinburg|+05 +06|-50 -60|01010|1BWl0 1qM0 WM0 8Hz0|14e5", + "Asia/Yerevan|+04 +05|-40 -50|01010|1BWm0 1qM0 WM0 1qM0|13e5", + "Atlantic/Azores|AZOT AZOST|10 0|01010101010101010101010|1BWp0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|25e4", + "Europe/Lisbon|WET WEST|0 -10|01010101010101010101010|1BWp0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|27e5", + "Atlantic/Cape_Verde|CVT|10|0||50e4", + "Atlantic/South_Georgia|GST|20|0||30", + "Atlantic/Stanley|FKST FKT|30 40|010|1C6R0 U10|21e2", + "Australia/Sydney|AEDT AEST|-b0 -a0|01010101010101010101010|1C140 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0|40e5", + "Australia/Adelaide|ACDT ACST|-au -9u|01010101010101010101010|1C14u 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0|11e5", + "Australia/Brisbane|AEST|-a0|0||20e5", + "Australia/Darwin|ACST|-9u|0||12e4", + "Australia/Eucla|ACWST|-8J|0||368", + "Australia/Lord_Howe|LHDT LHST|-b0 -au|01010101010101010101010|1C130 1cMu 1cLu 1cMu 1cLu 1fAu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1fAu 1cLu 1cMu 1cLu 1cMu|347", + "Australia/Perth|AWST|-80|0||18e5", + "Pacific/Easter|EASST EAST|50 60|010101010101010101010|1C1f0 1fB0 1nX0 G10 1EL0 Op0 1zb0 Rd0 1wn0 Rd0 46n0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0 1Nb0 Ap0|30e2", + "Europe/Dublin|GMT IST|0 -10|01010101010101010101010|1BWp0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|12e5", + "Etc/GMT+1|-01|10|0|", + "Etc/GMT+10|-10|a0|0|", + "Etc/GMT+11|-11|b0|0|", + "Etc/GMT+12|-12|c0|0|", + "Etc/GMT+2|-02|20|0|", + "Etc/GMT+4|-04|40|0|", + "Etc/GMT+5|-05|50|0|", + "Etc/GMT+6|-06|60|0|", + "Etc/GMT+7|-07|70|0|", + "Etc/GMT+8|-08|80|0|", + "Etc/GMT+9|-09|90|0|", + "Etc/GMT-1|+01|-10|0|", + "Etc/GMT-11|+11|-b0|0|", + "Etc/GMT-12|+12|-c0|0|", + "Etc/GMT-13|+13|-d0|0|", + "Etc/GMT-14|+14|-e0|0|", + "Etc/GMT-2|+02|-20|0|", + "Etc/GMT-7|+07|-70|0|", + "Etc/GMT-8|+08|-80|0|", + "Etc/GMT-9|+09|-90|0|", + "Etc/UCT|UCT|0|0|", + "Etc/UTC|UTC|0|0|", + "Europe/Astrakhan|+03 +04|-30 -40|010101|1BWn0 1qM0 WM0 8Hz0 3rd0", + "Europe/London|GMT BST|0 -10|01010101010101010101010|1BWp0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|10e6", + "Europe/Chisinau|EET EEST|-20 -30|01010101010101010101010|1BWo0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00|67e4", + "Europe/Kaliningrad|EET EEST +03|-20 -30 -30|01020|1BWo0 1qM0 WM0 8Hz0|44e4", + "Europe/Volgograd|+03 +04|-30 -40|01010|1BWn0 1qM0 WM0 8Hz0|10e5", + "Europe/Minsk|EET EEST +03|-20 -30 -30|0102|1BWo0 1qM0 WM0|19e5", + "Europe/Moscow|MSK MSD MSK|-30 -40 -40|01020|1BWn0 1qM0 WM0 8Hz0|16e6", + "Europe/Samara|+04 +03|-40 -30|010|1Dpb0 WM0|12e5", + "Europe/Simferopol|EET EEST MSK MSK|-20 -30 -40 -30|01010101023|1BWp0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11z0 1nW0|33e4", + "Pacific/Honolulu|HST|a0|0||37e4", + "Indian/Chagos|IOT|-60|0||30e2", + "Indian/Christmas|CXT|-70|0||21e2", + "Indian/Cocos|CCT|-6u|0||596", + "Indian/Mahe|SCT|-40|0||79e3", + "Indian/Maldives|MVT|-50|0||35e4", + "Indian/Mauritius|MUT|-40|0||15e4", + "Indian/Reunion|RET|-40|0||84e4", + "Pacific/Majuro|MHT|-c0|0||28e3", + "MET|MET MEST|-10 -20|01010101010101010101010|1BWp0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00", + "Pacific/Chatham|CHADT CHAST|-dJ -cJ|01010101010101010101010|1C120 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00|600", + "Pacific/Apia|SST SDT WSDT WSST|b0 a0 -e0 -d0|01012323232323232323232|1Dbn0 1ff0 1a00 CI0 AQ0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1cM0 1fA0 1a00 1fA0 1a00|37e3", + "Pacific/Bougainville|PGT BST|-a0 -b0|01|1NwE0|18e4", + "Pacific/Chuuk|CHUT|-a0|0||49e3", + "Pacific/Efate|VUT|-b0|0||66e3", + "Pacific/Enderbury|PHOT|-d0|0||1", + "Pacific/Fakaofo|TKT TKT|b0 -d0|01|1Gfn0|483", + "Pacific/Fiji|FJST FJT|-d0 -c0|01010101010101010101010|1BWe0 1o00 Rc0 1wo0 Ao0 1Nc0 Ao0 1Q00 xz0 1SN0 uM0 1SM0 uM0 1VA0 s00 1VA0 uM0 1SM0 uM0 1SM0 uM0 1SM0|88e4", + "Pacific/Funafuti|TVT|-c0|0||45e2", + "Pacific/Galapagos|GALT|60|0||25e3", + "Pacific/Gambier|GAMT|90|0||125", + "Pacific/Guadalcanal|SBT|-b0|0||11e4", + "Pacific/Guam|ChST|-a0|0||17e4", + "Pacific/Kiritimati|LINT|-e0|0||51e2", + "Pacific/Kosrae|KOST|-b0|0||66e2", + "Pacific/Marquesas|MART|9u|0||86e2", + "Pacific/Pago_Pago|SST|b0|0||37e2", + "Pacific/Nauru|NRT|-c0|0||10e3", + "Pacific/Niue|NUT|b0|0||12e2", + "Pacific/Norfolk|NFT NFT|-bu -b0|01|1PoCu|25e4", + "Pacific/Noumea|NCT|-b0|0||98e3", + "Pacific/Palau|PWT|-90|0||21e3", + "Pacific/Pitcairn|PST|80|0||56", + "Pacific/Pohnpei|PONT|-b0|0||34e3", + "Pacific/Port_Moresby|PGT|-a0|0||25e4", + "Pacific/Rarotonga|CKT|a0|0||13e3", + "Pacific/Tahiti|TAHT|a0|0||18e4", + "Pacific/Tarawa|GILT|-c0|0||29e3", + "Pacific/Tongatapu|TOT|-d0|0||75e3", + "Pacific/Wake|WAKT|-c0|0||16e3", + "Pacific/Wallis|WFT|-c0|0||94" + ], + "links": [ + "Africa/Abidjan|Africa/Accra", + "Africa/Abidjan|Africa/Bamako", + "Africa/Abidjan|Africa/Banjul", + "Africa/Abidjan|Africa/Bissau", + "Africa/Abidjan|Africa/Conakry", + "Africa/Abidjan|Africa/Dakar", + "Africa/Abidjan|Africa/Freetown", + "Africa/Abidjan|Africa/Lome", + "Africa/Abidjan|Africa/Monrovia", + "Africa/Abidjan|Africa/Nouakchott", + "Africa/Abidjan|Africa/Ouagadougou", + "Africa/Abidjan|Africa/Sao_Tome", + "Africa/Abidjan|Africa/Timbuktu", + "Africa/Abidjan|America/Danmarkshavn", + "Africa/Abidjan|Atlantic/Reykjavik", + "Africa/Abidjan|Atlantic/St_Helena", + "Africa/Abidjan|Etc/GMT", + "Africa/Abidjan|Etc/GMT+0", + "Africa/Abidjan|Etc/GMT-0", + "Africa/Abidjan|Etc/GMT0", + "Africa/Abidjan|Etc/Greenwich", + "Africa/Abidjan|GMT", + "Africa/Abidjan|GMT+0", + "Africa/Abidjan|GMT-0", + "Africa/Abidjan|GMT0", + "Africa/Abidjan|Greenwich", + "Africa/Abidjan|Iceland", + "Africa/Algiers|Africa/Tunis", + "Africa/Cairo|Egypt", + "Africa/Casablanca|Africa/El_Aaiun", + "Africa/Johannesburg|Africa/Maseru", + "Africa/Johannesburg|Africa/Mbabane", + "Africa/Khartoum|Africa/Addis_Ababa", + "Africa/Khartoum|Africa/Asmara", + "Africa/Khartoum|Africa/Asmera", + "Africa/Khartoum|Africa/Dar_es_Salaam", + "Africa/Khartoum|Africa/Djibouti", + "Africa/Khartoum|Africa/Juba", + "Africa/Khartoum|Africa/Kampala", + "Africa/Khartoum|Africa/Mogadishu", + "Africa/Khartoum|Africa/Nairobi", + "Africa/Khartoum|Indian/Antananarivo", + "Africa/Khartoum|Indian/Comoro", + "Africa/Khartoum|Indian/Mayotte", + "Africa/Lagos|Africa/Bangui", + "Africa/Lagos|Africa/Brazzaville", + "Africa/Lagos|Africa/Douala", + "Africa/Lagos|Africa/Kinshasa", + "Africa/Lagos|Africa/Libreville", + "Africa/Lagos|Africa/Luanda", + "Africa/Lagos|Africa/Malabo", + "Africa/Lagos|Africa/Ndjamena", + "Africa/Lagos|Africa/Niamey", + "Africa/Lagos|Africa/Porto-Novo", + "Africa/Maputo|Africa/Blantyre", + "Africa/Maputo|Africa/Bujumbura", + "Africa/Maputo|Africa/Gaborone", + "Africa/Maputo|Africa/Harare", + "Africa/Maputo|Africa/Kigali", + "Africa/Maputo|Africa/Lubumbashi", + "Africa/Maputo|Africa/Lusaka", + "Africa/Tripoli|Libya", + "America/Adak|America/Atka", + "America/Adak|US/Aleutian", + "America/Anchorage|America/Juneau", + "America/Anchorage|America/Nome", + "America/Anchorage|America/Sitka", + "America/Anchorage|America/Yakutat", + "America/Anchorage|US/Alaska", + "America/Argentina/Buenos_Aires|America/Argentina/Catamarca", + "America/Argentina/Buenos_Aires|America/Argentina/ComodRivadavia", + "America/Argentina/Buenos_Aires|America/Argentina/Cordoba", + "America/Argentina/Buenos_Aires|America/Argentina/Jujuy", + "America/Argentina/Buenos_Aires|America/Argentina/La_Rioja", + "America/Argentina/Buenos_Aires|America/Argentina/Mendoza", + "America/Argentina/Buenos_Aires|America/Argentina/Rio_Gallegos", + "America/Argentina/Buenos_Aires|America/Argentina/Salta", + "America/Argentina/Buenos_Aires|America/Argentina/San_Juan", + "America/Argentina/Buenos_Aires|America/Argentina/San_Luis", + "America/Argentina/Buenos_Aires|America/Argentina/Tucuman", + "America/Argentina/Buenos_Aires|America/Argentina/Ushuaia", + "America/Argentina/Buenos_Aires|America/Buenos_Aires", + "America/Argentina/Buenos_Aires|America/Catamarca", + "America/Argentina/Buenos_Aires|America/Cordoba", + "America/Argentina/Buenos_Aires|America/Jujuy", + "America/Argentina/Buenos_Aires|America/Mendoza", + "America/Argentina/Buenos_Aires|America/Rosario", + "America/Campo_Grande|America/Cuiaba", + "America/Chicago|America/Indiana/Knox", + "America/Chicago|America/Indiana/Tell_City", + "America/Chicago|America/Knox_IN", + "America/Chicago|America/Matamoros", + "America/Chicago|America/Menominee", + "America/Chicago|America/North_Dakota/Center", + "America/Chicago|America/North_Dakota/New_Salem", + "America/Chicago|America/Rainy_River", + "America/Chicago|America/Rankin_Inlet", + "America/Chicago|America/Resolute", + "America/Chicago|America/Winnipeg", + "America/Chicago|CST6CDT", + "America/Chicago|Canada/Central", + "America/Chicago|US/Central", + "America/Chicago|US/Indiana-Starke", + "America/Chihuahua|America/Mazatlan", + "America/Chihuahua|Mexico/BajaSur", + "America/Denver|America/Boise", + "America/Denver|America/Cambridge_Bay", + "America/Denver|America/Edmonton", + "America/Denver|America/Inuvik", + "America/Denver|America/Ojinaga", + "America/Denver|America/Shiprock", + "America/Denver|America/Yellowknife", + "America/Denver|Canada/Mountain", + "America/Denver|MST7MDT", + "America/Denver|Navajo", + "America/Denver|US/Mountain", + "America/Fortaleza|America/Belem", + "America/Fortaleza|America/Maceio", + "America/Fortaleza|America/Recife", + "America/Fortaleza|America/Santarem", + "America/Halifax|America/Glace_Bay", + "America/Halifax|America/Moncton", + "America/Halifax|America/Thule", + "America/Halifax|Atlantic/Bermuda", + "America/Halifax|Canada/Atlantic", + "America/Havana|Cuba", + "America/Los_Angeles|America/Dawson", + "America/Los_Angeles|America/Ensenada", + "America/Los_Angeles|America/Santa_Isabel", + "America/Los_Angeles|America/Tijuana", + "America/Los_Angeles|America/Vancouver", + "America/Los_Angeles|America/Whitehorse", + "America/Los_Angeles|Canada/Pacific", + "America/Los_Angeles|Canada/Yukon", + "America/Los_Angeles|Mexico/BajaNorte", + "America/Los_Angeles|PST8PDT", + "America/Los_Angeles|US/Pacific", + "America/Los_Angeles|US/Pacific-New", + "America/Managua|America/Belize", + "America/Managua|America/Costa_Rica", + "America/Managua|America/El_Salvador", + "America/Managua|America/Guatemala", + "America/Managua|America/Regina", + "America/Managua|America/Swift_Current", + "America/Managua|America/Tegucigalpa", + "America/Managua|Canada/East-Saskatchewan", + "America/Managua|Canada/Saskatchewan", + "America/Manaus|America/Boa_Vista", + "America/Manaus|America/Porto_Velho", + "America/Manaus|Brazil/West", + "America/Mexico_City|America/Merida", + "America/Mexico_City|America/Monterrey", + "America/Mexico_City|Mexico/General", + "America/New_York|America/Detroit", + "America/New_York|America/Fort_Wayne", + "America/New_York|America/Indiana/Indianapolis", + "America/New_York|America/Indiana/Marengo", + "America/New_York|America/Indiana/Petersburg", + "America/New_York|America/Indiana/Vevay", + "America/New_York|America/Indiana/Vincennes", + "America/New_York|America/Indiana/Winamac", + "America/New_York|America/Indianapolis", + "America/New_York|America/Iqaluit", + "America/New_York|America/Kentucky/Louisville", + "America/New_York|America/Kentucky/Monticello", + "America/New_York|America/Louisville", + "America/New_York|America/Montreal", + "America/New_York|America/Nassau", + "America/New_York|America/Nipigon", + "America/New_York|America/Pangnirtung", + "America/New_York|America/Thunder_Bay", + "America/New_York|America/Toronto", + "America/New_York|Canada/Eastern", + "America/New_York|EST5EDT", + "America/New_York|US/East-Indiana", + "America/New_York|US/Eastern", + "America/New_York|US/Michigan", + "America/Noronha|Brazil/DeNoronha", + "America/Panama|America/Atikokan", + "America/Panama|America/Cayman", + "America/Panama|America/Coral_Harbour", + "America/Panama|America/Jamaica", + "America/Panama|EST", + "America/Panama|Jamaica", + "America/Phoenix|America/Creston", + "America/Phoenix|America/Dawson_Creek", + "America/Phoenix|America/Hermosillo", + "America/Phoenix|MST", + "America/Phoenix|US/Arizona", + "America/Rio_Branco|America/Eirunepe", + "America/Rio_Branco|America/Porto_Acre", + "America/Rio_Branco|Brazil/Acre", + "America/Santiago|Antarctica/Palmer", + "America/Santiago|Chile/Continental", + "America/Santo_Domingo|America/Anguilla", + "America/Santo_Domingo|America/Antigua", + "America/Santo_Domingo|America/Aruba", + "America/Santo_Domingo|America/Barbados", + "America/Santo_Domingo|America/Blanc-Sablon", + "America/Santo_Domingo|America/Curacao", + "America/Santo_Domingo|America/Dominica", + "America/Santo_Domingo|America/Grenada", + "America/Santo_Domingo|America/Guadeloupe", + "America/Santo_Domingo|America/Kralendijk", + "America/Santo_Domingo|America/Lower_Princes", + "America/Santo_Domingo|America/Marigot", + "America/Santo_Domingo|America/Martinique", + "America/Santo_Domingo|America/Montserrat", + "America/Santo_Domingo|America/Port_of_Spain", + "America/Santo_Domingo|America/Puerto_Rico", + "America/Santo_Domingo|America/St_Barthelemy", + "America/Santo_Domingo|America/St_Kitts", + "America/Santo_Domingo|America/St_Lucia", + "America/Santo_Domingo|America/St_Thomas", + "America/Santo_Domingo|America/St_Vincent", + "America/Santo_Domingo|America/Tortola", + "America/Santo_Domingo|America/Virgin", + "America/Sao_Paulo|Brazil/East", + "America/St_Johns|Canada/Newfoundland", + "Antarctica/DumontDUrville|Etc/GMT-10", + "Antarctica/Rothera|Etc/GMT+3", + "Antarctica/Syowa|Etc/GMT-3", + "Asia/Almaty|Antarctica/Vostok", + "Asia/Almaty|Asia/Bishkek", + "Asia/Almaty|Asia/Qyzylorda", + "Asia/Almaty|Etc/GMT-6", + "Asia/Baghdad|Asia/Aden", + "Asia/Baghdad|Asia/Bahrain", + "Asia/Baghdad|Asia/Kuwait", + "Asia/Baghdad|Asia/Qatar", + "Asia/Baghdad|Asia/Riyadh", + "Asia/Bangkok|Asia/Ho_Chi_Minh", + "Asia/Bangkok|Asia/Phnom_Penh", + "Asia/Bangkok|Asia/Saigon", + "Asia/Bangkok|Asia/Vientiane", + "Asia/Dhaka|Asia/Dacca", + "Asia/Dubai|Asia/Muscat", + "Asia/Hong_Kong|Hongkong", + "Asia/Jakarta|Asia/Pontianak", + "Asia/Jerusalem|Asia/Tel_Aviv", + "Asia/Jerusalem|Israel", + "Asia/Kamchatka|Asia/Anadyr", + "Asia/Kathmandu|Asia/Katmandu", + "Asia/Kolkata|Asia/Calcutta", + "Asia/Kuala_Lumpur|Asia/Kuching", + "Asia/Makassar|Asia/Ujung_Pandang", + "Asia/Rangoon|Asia/Yangon", + "Asia/Seoul|ROK", + "Asia/Shanghai|Asia/Chongqing", + "Asia/Shanghai|Asia/Chungking", + "Asia/Shanghai|Asia/Harbin", + "Asia/Shanghai|Asia/Macao", + "Asia/Shanghai|Asia/Macau", + "Asia/Shanghai|Asia/Taipei", + "Asia/Shanghai|PRC", + "Asia/Shanghai|ROC", + "Asia/Singapore|Singapore", + "Asia/Tashkent|Antarctica/Mawson", + "Asia/Tashkent|Asia/Aqtau", + "Asia/Tashkent|Asia/Aqtobe", + "Asia/Tashkent|Asia/Ashgabat", + "Asia/Tashkent|Asia/Ashkhabad", + "Asia/Tashkent|Asia/Dushanbe", + "Asia/Tashkent|Asia/Oral", + "Asia/Tashkent|Asia/Samarkand", + "Asia/Tashkent|Etc/GMT-5", + "Asia/Tashkent|Indian/Kerguelen", + "Asia/Tbilisi|Etc/GMT-4", + "Asia/Tehran|Iran", + "Asia/Thimphu|Asia/Thimbu", + "Asia/Tokyo|Japan", + "Asia/Ulaanbaatar|Asia/Ulan_Bator", + "Asia/Urumqi|Asia/Kashgar", + "Australia/Adelaide|Australia/Broken_Hill", + "Australia/Adelaide|Australia/South", + "Australia/Adelaide|Australia/Yancowinna", + "Australia/Brisbane|Australia/Lindeman", + "Australia/Brisbane|Australia/Queensland", + "Australia/Darwin|Australia/North", + "Australia/Lord_Howe|Australia/LHI", + "Australia/Perth|Australia/West", + "Australia/Sydney|Australia/ACT", + "Australia/Sydney|Australia/Canberra", + "Australia/Sydney|Australia/Currie", + "Australia/Sydney|Australia/Hobart", + "Australia/Sydney|Australia/Melbourne", + "Australia/Sydney|Australia/NSW", + "Australia/Sydney|Australia/Tasmania", + "Australia/Sydney|Australia/Victoria", + "Etc/UCT|UCT", + "Etc/UTC|Etc/Universal", + "Etc/UTC|Etc/Zulu", + "Etc/UTC|UTC", + "Etc/UTC|Universal", + "Etc/UTC|Zulu", + "Europe/Astrakhan|Europe/Ulyanovsk", + "Europe/Athens|Asia/Nicosia", + "Europe/Athens|EET", + "Europe/Athens|Europe/Bucharest", + "Europe/Athens|Europe/Helsinki", + "Europe/Athens|Europe/Kiev", + "Europe/Athens|Europe/Mariehamn", + "Europe/Athens|Europe/Nicosia", + "Europe/Athens|Europe/Riga", + "Europe/Athens|Europe/Sofia", + "Europe/Athens|Europe/Tallinn", + "Europe/Athens|Europe/Uzhgorod", + "Europe/Athens|Europe/Vilnius", + "Europe/Athens|Europe/Zaporozhye", + "Europe/Chisinau|Europe/Tiraspol", + "Europe/Dublin|Eire", + "Europe/Istanbul|Asia/Istanbul", + "Europe/Istanbul|Turkey", + "Europe/Lisbon|Atlantic/Canary", + "Europe/Lisbon|Atlantic/Faeroe", + "Europe/Lisbon|Atlantic/Faroe", + "Europe/Lisbon|Atlantic/Madeira", + "Europe/Lisbon|Portugal", + "Europe/Lisbon|WET", + "Europe/London|Europe/Belfast", + "Europe/London|Europe/Guernsey", + "Europe/London|Europe/Isle_of_Man", + "Europe/London|Europe/Jersey", + "Europe/London|GB", + "Europe/London|GB-Eire", + "Europe/Moscow|W-SU", + "Europe/Paris|Africa/Ceuta", + "Europe/Paris|Arctic/Longyearbyen", + "Europe/Paris|Atlantic/Jan_Mayen", + "Europe/Paris|CET", + "Europe/Paris|Europe/Amsterdam", + "Europe/Paris|Europe/Andorra", + "Europe/Paris|Europe/Belgrade", + "Europe/Paris|Europe/Berlin", + "Europe/Paris|Europe/Bratislava", + "Europe/Paris|Europe/Brussels", + "Europe/Paris|Europe/Budapest", + "Europe/Paris|Europe/Busingen", + "Europe/Paris|Europe/Copenhagen", + "Europe/Paris|Europe/Gibraltar", + "Europe/Paris|Europe/Ljubljana", + "Europe/Paris|Europe/Luxembourg", + "Europe/Paris|Europe/Madrid", + "Europe/Paris|Europe/Malta", + "Europe/Paris|Europe/Monaco", + "Europe/Paris|Europe/Oslo", + "Europe/Paris|Europe/Podgorica", + "Europe/Paris|Europe/Prague", + "Europe/Paris|Europe/Rome", + "Europe/Paris|Europe/San_Marino", + "Europe/Paris|Europe/Sarajevo", + "Europe/Paris|Europe/Skopje", + "Europe/Paris|Europe/Stockholm", + "Europe/Paris|Europe/Tirane", + "Europe/Paris|Europe/Vaduz", + "Europe/Paris|Europe/Vatican", + "Europe/Paris|Europe/Vienna", + "Europe/Paris|Europe/Warsaw", + "Europe/Paris|Europe/Zagreb", + "Europe/Paris|Europe/Zurich", + "Europe/Paris|Poland", + "Europe/Volgograd|Europe/Kirov", + "Pacific/Auckland|Antarctica/McMurdo", + "Pacific/Auckland|Antarctica/South_Pole", + "Pacific/Auckland|NZ", + "Pacific/Chatham|NZ-CHAT", + "Pacific/Chuuk|Pacific/Truk", + "Pacific/Chuuk|Pacific/Yap", + "Pacific/Easter|Chile/EasterIsland", + "Pacific/Guam|Pacific/Saipan", + "Pacific/Honolulu|HST", + "Pacific/Honolulu|Pacific/Johnston", + "Pacific/Honolulu|US/Hawaii", + "Pacific/Majuro|Kwajalein", + "Pacific/Majuro|Pacific/Kwajalein", + "Pacific/Pago_Pago|Pacific/Midway", + "Pacific/Pago_Pago|Pacific/Samoa", + "Pacific/Pago_Pago|US/Samoa", + "Pacific/Pohnpei|Pacific/Ponape" + ] + }); + + + return moment; +})); diff --git a/vendor/vendor.js b/vendor/vendor.js index e21ae502..bc437311 100644 --- a/vendor/vendor.js +++ b/vendor/vendor.js @@ -9,6 +9,7 @@ var vendor = { 'moment.js' : 'moment/moment-with-locales.js', + 'moment-timezone.js' : 'moment/moment-timezone.js', 'weather-icons.css': 'weather-icons/css/weather-icons.css', 'weather-icons-wind.css': 'weather-icons/css/weather-icons-wind.css', 'font-awesome.css': 'font-awesome-4.5.0/css/font-awesome.min.css'