mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 11:50:00 +00:00
commit
10dc315f3b
15
.github/workflows/enforce-changelog.yml
vendored
Normal file
15
.github/workflows/enforce-changelog.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
name: "Enforce Changelog"
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Enforces the update of a changelog file on every pull request
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: dangoslen/changelog-enforcer@v1.6.1
|
||||||
|
with:
|
||||||
|
changeLogPath: 'CHANGELOG.md'
|
||||||
|
skipLabels: 'Skip Changelog'
|
35
.github/workflows/node-ci.js.yml
vendored
Normal file
35
.github/workflows/node-ci.js.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||||
|
|
||||||
|
name: Automated Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master, develop ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [10.x, 12.x, 14.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- run: |
|
||||||
|
Xvfb :99 -screen 0 1024x768x16 &
|
||||||
|
export DISPLAY=:99
|
||||||
|
npm install
|
||||||
|
npm run test:prettier
|
||||||
|
npm run test:js
|
||||||
|
npm run test:css
|
||||||
|
npm run test:e2e
|
||||||
|
npm run test:unit
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -67,6 +67,10 @@ Temporary Items
|
|||||||
# Ignore changes to the custom css files.
|
# Ignore changes to the custom css files.
|
||||||
/css/custom.css
|
/css/custom.css
|
||||||
|
|
||||||
|
# Ignore users config file but keep the sample.
|
||||||
|
/config/*
|
||||||
|
!/config/config.js.sample
|
||||||
|
|
||||||
# Vim
|
# Vim
|
||||||
## swap
|
## swap
|
||||||
[._]*.s[a-w][a-z]
|
[._]*.s[a-w][a-z]
|
||||||
|
@ -2,3 +2,4 @@ package-lock.json
|
|||||||
/config/**/*
|
/config/**/*
|
||||||
/vendor/**/*
|
/vendor/**/*
|
||||||
!/vendor/vendor.js
|
!/vendor/vendor.js
|
||||||
|
.github/**/*
|
||||||
|
25
.travis.yml
25
.travis.yml
@ -1,25 +0,0 @@
|
|||||||
dist: trusty
|
|
||||||
language: node_js
|
|
||||||
node_js:
|
|
||||||
- 10
|
|
||||||
- lts/*
|
|
||||||
- node
|
|
||||||
before_install:
|
|
||||||
- npm i -g npm
|
|
||||||
before_script:
|
|
||||||
- yarn danger ci
|
|
||||||
- "export DISPLAY=:99.0"
|
|
||||||
- "export ELECTRON_DISABLE_SANDBOX=1"
|
|
||||||
- "sh -e /etc/init.d/xvfb start"
|
|
||||||
- sleep 5
|
|
||||||
script:
|
|
||||||
- npm run test:prettier
|
|
||||||
- npm run test:js
|
|
||||||
- npm run test:css
|
|
||||||
- npm run test:e2e
|
|
||||||
- npm run test:unit
|
|
||||||
after_script:
|
|
||||||
- npm list
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- node_modules
|
|
64
CHANGELOG.md
64
CHANGELOG.md
@ -5,6 +5,70 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror²
|
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror²
|
||||||
|
|
||||||
|
## [2.14.0] - 2021-01-01
|
||||||
|
|
||||||
|
Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank, @bluemanos, @flopp999, @jakemulley, @jakobsarwary1, @marvai-vgtu, @mirontoli, @rejas, @sdetweil, @Snille & @Sub028.
|
||||||
|
|
||||||
|
ℹ️ **Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added new log level "debug" to the logger.
|
||||||
|
- Added new parameter "useKmh" to weather module for displaying wind speed as kmh.
|
||||||
|
- Chuvash translation.
|
||||||
|
- Added Weatherbit as a provider to Weather module.
|
||||||
|
- Added SMHI as a provider to Weather module.
|
||||||
|
- Added Hindi & Gujarati translation.
|
||||||
|
- Added optional support for DEGREE position in Feels like translation.
|
||||||
|
- Added support for variables in nunjucks templates for translate filter.
|
||||||
|
- Added Chuvash translation.
|
||||||
|
- Calendar: new options "limitDays" and "coloredEvents".
|
||||||
|
- Added new option "limitDays" - limit the number of discreet days displayed.
|
||||||
|
- Added new option "customEvents" - use custom symbol/color based on keyword in event title.
|
||||||
|
- Added GitHub workflows for automated testing and changelog enforcement.
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
|
||||||
|
- Merging .gitignore in the config-folder with the .gitignore in the root-folder.
|
||||||
|
- Weather module - forecast now show TODAY and TOMORROW instead of weekday, to make it easier to understand.
|
||||||
|
- Update dependencies to latest versions.
|
||||||
|
- Update dependencies eslint, feedme, simple-git and socket.io to latest versions.
|
||||||
|
- Update lithuanian translation.
|
||||||
|
- Update config sample.
|
||||||
|
- Highlight required version mismatch.
|
||||||
|
- No select Text for TouchScreen use.
|
||||||
|
- Corrected logic for timeFormat "relative" and "absolute".
|
||||||
|
- Added missing function call in module.show()
|
||||||
|
- Translator variables can have falsy values (e.g. empty string)
|
||||||
|
- Fix issue with weather module with DEGREE label in FEELS like
|
||||||
|
|
||||||
|
### Deleted
|
||||||
|
|
||||||
|
- Removed Travis CI intergration.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- JSON Parse translation files with comments crashing UI. (#2149)
|
||||||
|
- Calendar parsing where RRULE bug returns wrong date, add Windows timezone name support. (#2145, #2151)
|
||||||
|
- Wrong node-ical version installed (package.json) requested version. (#2153)
|
||||||
|
- Fix calendar fetcher subsequent timing. (#2160)
|
||||||
|
- Rename Greek translation to correct ISO 639-1 alpha-2 code (gr > el). (#2155)
|
||||||
|
- Add a space after icons of sunrise and sunset. (#2169)
|
||||||
|
- Fix calendar when no DTEND record found in event, startDate overlay when endDate set. (#2177)
|
||||||
|
- Fix windspeed convertion error in ukmetoffice weather provider. (#2189)
|
||||||
|
- Fix console.debug not having timestamps. (#2199)
|
||||||
|
- Fix calendar full day event east of UTC start time. (#2200)
|
||||||
|
- Fix non-fullday recurring rule processing. (#2216)
|
||||||
|
- Catch errors when parsing calendar data with ical. (#2022)
|
||||||
|
- Fix Default Alert Module does not hide black overlay when alert is dismissed manually. (#2228)
|
||||||
|
- Weather module - Always displays night icons when local is other then English. (#2221)
|
||||||
|
- Update Node-ical 0.12.4 , fix invalid RRULE format in cal entries
|
||||||
|
- Fix package.json for optional electron dependency (2378)
|
||||||
|
- Update node-ical version again, 0.12.5, change RRULE fix (#2371, #2379)
|
||||||
|
- Remove undefined objects from modules array (#2382)
|
||||||
|
- Update node-ical version again, 0.12.7, change RRULE fix (#2371, #2379), node-ical now throws error (which we catch)
|
||||||
|
- Update simple-git version to 2.31 unhandled promise rejection (#2383)
|
||||||
|
|
||||||
## [2.13.0] - 2020-10-01
|
## [2.13.0] - 2020-10-01
|
||||||
|
|
||||||
Special thanks to the following contributors: @bryanzzhu, @bugsounet, @chamakura, @cjbrunner, @easyas314, @larryare, @oemel09, @rejas, @sdetweil & @sthuber90.
|
Special thanks to the following contributors: @bryanzzhu, @bugsounet, @chamakura, @cjbrunner, @easyas314, @larryare, @oemel09, @rejas, @sdetweil & @sthuber90.
|
||||||
|
@ -5,8 +5,7 @@
|
|||||||
<a href="https://david-dm.org/MichMich/MagicMirror#info=devDependencies"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a>
|
<a href="https://david-dm.org/MichMich/MagicMirror#info=devDependencies"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a>
|
||||||
<a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge"></a>
|
<a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge"></a>
|
||||||
<a href="https://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
<a href="https://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
||||||
<a href="https://travis-ci.com/MichMich/MagicMirror"><img src="https://travis-ci.com/MichMich/MagicMirror.svg" alt="Travis"></a>
|
<a href="https://github.com/MichMich/MagicMirror/actions?query=workflow%3A%22Automated+Tests%22"><img src="https://github.com/MichMich/MagicMirror/workflows/Automated%20Tests/badge.svg" alt="Tests"></a>
|
||||||
<a href="https://snyk.io/test/github/MichMich/MagicMirror"><img src="https://snyk.io/test/github/MichMich/MagicMirror/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/MichMich/MagicMirror" style="max-width:100%;"></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).
|
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).
|
||||||
@ -40,6 +39,6 @@ If we receive enough donations we might even be able to free up some working hou
|
|||||||
To donate, please follow [this](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G5D8E9MR5DTD2&source=url) link.
|
To donate, please follow [this](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G5D8E9MR5DTD2&source=url) link.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<br>
|
<br>
|
||||||
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
|
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
|
||||||
</p>
|
</p>
|
||||||
|
2
config/.gitignore
vendored
2
config/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
*
|
|
||||||
!config.js.sample
|
|
@ -28,7 +28,7 @@ var config = {
|
|||||||
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
|
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
|
||||||
|
|
||||||
language: "en",
|
language: "en",
|
||||||
logLevel: ["INFO", "LOG", "WARN", "ERROR"],
|
logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging
|
||||||
timeFormat: 24,
|
timeFormat: 24,
|
||||||
units: "metric",
|
units: "metric",
|
||||||
// serverOnly: true/false/"local" ,
|
// serverOnly: true/false/"local" ,
|
||||||
@ -70,7 +70,7 @@ var config = {
|
|||||||
position: "top_right",
|
position: "top_right",
|
||||||
config: {
|
config: {
|
||||||
location: "New York",
|
location: "New York",
|
||||||
locationID: "", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
|
||||||
appid: "YOUR_OPENWEATHER_API_KEY"
|
appid: "YOUR_OPENWEATHER_API_KEY"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2,6 +2,7 @@ html {
|
|||||||
cursor: none;
|
cursor: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #000;
|
background: #000;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
@ -46,7 +46,7 @@ function checkConfigFile() {
|
|||||||
try {
|
try {
|
||||||
fs.accessSync(configFileName, fs.F_OK);
|
fs.accessSync(configFileName, fs.F_OK);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.log(Utils.colors.error(e));
|
Log.error(Utils.colors.error(e));
|
||||||
throw new Error("No permission to access config file!");
|
throw new Error("No permission to access config file!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
10
js/logger.js
10
js/logger.js
@ -10,7 +10,10 @@
|
|||||||
(function (root, factory) {
|
(function (root, factory) {
|
||||||
if (typeof exports === "object") {
|
if (typeof exports === "object") {
|
||||||
// add timestamps in front of log messages
|
// add timestamps in front of log messages
|
||||||
require("console-stamp")(console, "yyyy-mm-dd HH:MM:ss.l");
|
require("console-stamp")(console, {
|
||||||
|
pattern: "yyyy-mm-dd HH:MM:ss.l",
|
||||||
|
include: ["debug", "log", "info", "warn", "error"]
|
||||||
|
});
|
||||||
|
|
||||||
// Node, CommonJS-like
|
// Node, CommonJS-like
|
||||||
module.exports = factory(root.config);
|
module.exports = factory(root.config);
|
||||||
@ -20,10 +23,11 @@
|
|||||||
}
|
}
|
||||||
})(this, function (config) {
|
})(this, function (config) {
|
||||||
const logLevel = {
|
const logLevel = {
|
||||||
info: Function.prototype.bind.call(console.info, console),
|
debug: Function.prototype.bind.call(console.debug, console),
|
||||||
log: Function.prototype.bind.call(console.log, console),
|
log: Function.prototype.bind.call(console.log, console),
|
||||||
error: Function.prototype.bind.call(console.error, console),
|
info: Function.prototype.bind.call(console.info, console),
|
||||||
warn: Function.prototype.bind.call(console.warn, console),
|
warn: Function.prototype.bind.call(console.warn, console),
|
||||||
|
error: Function.prototype.bind.call(console.error, console),
|
||||||
group: Function.prototype.bind.call(console.group, console),
|
group: Function.prototype.bind.call(console.group, console),
|
||||||
groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console),
|
groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console),
|
||||||
groupEnd: Function.prototype.bind.call(console.groupEnd, console),
|
groupEnd: Function.prototype.bind.call(console.groupEnd, console),
|
||||||
|
@ -500,10 +500,7 @@ var MM = (function () {
|
|||||||
*/
|
*/
|
||||||
modulesStarted: function (moduleObjects) {
|
modulesStarted: function (moduleObjects) {
|
||||||
modules = [];
|
modules = [];
|
||||||
for (var m in moduleObjects) {
|
moduleObjects.forEach((module) => modules.push(module));
|
||||||
var module = moduleObjects[m];
|
|
||||||
modules[module.data.index] = module;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.info("All modules started!");
|
Log.info("All modules started!");
|
||||||
sendNotification("ALL_MODULES_STARTED");
|
sendNotification("ALL_MODULES_STARTED");
|
||||||
|
28
js/module.js
28
js/module.js
@ -175,8 +175,8 @@ var Module = Class.extend({
|
|||||||
lstripBlocks: true
|
lstripBlocks: true
|
||||||
});
|
});
|
||||||
|
|
||||||
this._nunjucksEnvironment.addFilter("translate", function (str) {
|
this._nunjucksEnvironment.addFilter("translate", function (str, variables) {
|
||||||
return self.translate(str);
|
return self.translate(str, variables);
|
||||||
});
|
});
|
||||||
|
|
||||||
return this._nunjucksEnvironment;
|
return this._nunjucksEnvironment;
|
||||||
@ -228,7 +228,7 @@ var Module = Class.extend({
|
|||||||
* Set the module config and combine it with the module defaults.
|
* Set the module config and combine it with the module defaults.
|
||||||
*
|
*
|
||||||
* @param {object} config The combined module config.
|
* @param {object} config The combined module config.
|
||||||
* @param {boolean} config Merge module config in deep.
|
* @param {boolean} deep Merge module config in deep.
|
||||||
*/
|
*/
|
||||||
setConfig: function (config, deep) {
|
setConfig: function (config, deep) {
|
||||||
this.config = deep ? configMerge({}, this.defaults, config) : Object.assign({}, this.defaults, config);
|
this.config = deep ? configMerge({}, this.defaults, config) : Object.assign({}, this.defaults, config);
|
||||||
@ -434,20 +434,22 @@ var Module = Class.extend({
|
|||||||
speed,
|
speed,
|
||||||
function () {
|
function () {
|
||||||
self.resume();
|
self.resume();
|
||||||
callback;
|
callback();
|
||||||
},
|
},
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Merging MagicMirror (or other) default/config script
|
/**
|
||||||
* merge 2 objects or/with array
|
* Merging MagicMirror (or other) default/config script by @bugsounet
|
||||||
* using:
|
* Merge 2 objects or/with array
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
* -------
|
* -------
|
||||||
* this.config = configMerge({}, this.defaults, this.config)
|
* this.config = configMerge({}, this.defaults, this.config)
|
||||||
* -------
|
* -------
|
||||||
* arg1: initial objet
|
* arg1: initial object
|
||||||
* arg2: config model
|
* arg2: config model
|
||||||
* arg3: config to merge
|
* arg3: config to merge
|
||||||
* -------
|
* -------
|
||||||
@ -456,10 +458,12 @@ var Module = Class.extend({
|
|||||||
* it don't merge all thing in deep
|
* it don't merge all thing in deep
|
||||||
* -> object in object and array is not merging
|
* -> object in object and array is not merging
|
||||||
* -------
|
* -------
|
||||||
* @bugsounet
|
*
|
||||||
* @Todo: idea of Mich determinate what do you want to merge or not
|
* Todo: idea of Mich determinate what do you want to merge or not
|
||||||
|
*
|
||||||
|
* @param {object} result the initial object
|
||||||
|
* @returns {object} the merged config
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function configMerge(result) {
|
function configMerge(result) {
|
||||||
var stack = Array.prototype.slice.call(arguments, 1);
|
var stack = Array.prototype.slice.call(arguments, 1);
|
||||||
var item;
|
var item;
|
||||||
@ -506,7 +510,7 @@ Module.register = function (name, moduleDefinition) {
|
|||||||
if (cmpVersions(window.version, moduleDefinition.requiresVersion) >= 0) {
|
if (cmpVersions(window.version, moduleDefinition.requiresVersion) >= 0) {
|
||||||
Log.log("Version is ok!");
|
Log.log("Version is ok!");
|
||||||
} else {
|
} else {
|
||||||
Log.log("Version is incorrect. Skip module: '" + name + "'");
|
Log.warn("Version is incorrect. Skip module: '" + name + "'");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ var Server = function (config, callback) {
|
|||||||
server.listen(port, config.address ? config.address : "localhost");
|
server.listen(port, config.address ? config.address : "localhost");
|
||||||
|
|
||||||
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
|
||||||
Log.info(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
|
Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(function (req, res, next) {
|
app.use(function (req, res, next) {
|
||||||
@ -49,7 +49,7 @@ var Server = function (config, callback) {
|
|||||||
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
|
res.status(403).send("This device is not allowed to access your mirror. <br> Please check your config.js or config.js.sample to change this.");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
app.use(helmet());
|
app.use(helmet({ contentSecurityPolicy: false }));
|
||||||
|
|
||||||
app.use("/js", express.static(__dirname));
|
app.use("/js", express.static(__dirname));
|
||||||
var directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
|
var directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
|
||||||
|
@ -19,7 +19,15 @@ var Translator = (function () {
|
|||||||
xhr.open("GET", file, true);
|
xhr.open("GET", file, true);
|
||||||
xhr.onreadystatechange = function () {
|
xhr.onreadystatechange = function () {
|
||||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||||
callback(JSON.parse(xhr.responseText));
|
// needs error handler try/catch at least
|
||||||
|
let fileinfo = null;
|
||||||
|
try {
|
||||||
|
fileinfo = JSON.parse(xhr.responseText);
|
||||||
|
} catch (exception) {
|
||||||
|
// nothing here, but don't die
|
||||||
|
Log.error(" loading json file =" + file + " failed");
|
||||||
|
}
|
||||||
|
callback(fileinfo);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
xhr.send(null);
|
xhr.send(null);
|
||||||
@ -60,7 +68,7 @@ var Translator = (function () {
|
|||||||
template = variables.fallback;
|
template = variables.fallback;
|
||||||
}
|
}
|
||||||
return template.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) {
|
return template.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) {
|
||||||
return variables[varName] || "{" + varName + "}";
|
return varName in variables ? variables[varName] : "{" + varName + "}";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,10 +100,13 @@ Module.register("alert", {
|
|||||||
message: image + message,
|
message: image + message,
|
||||||
effect: this.config.alert_effect,
|
effect: this.config.alert_effect,
|
||||||
ttl: params.timer,
|
ttl: params.timer,
|
||||||
|
onClose: () => this.hide_alert(sender),
|
||||||
al_no: "ns-alert"
|
al_no: "ns-alert"
|
||||||
});
|
});
|
||||||
|
|
||||||
//Show alert
|
//Show alert
|
||||||
this.alerts[sender.name].show();
|
this.alerts[sender.name].show();
|
||||||
|
|
||||||
//Add timer to dismiss alert and overlay
|
//Add timer to dismiss alert and overlay
|
||||||
if (params.timer) {
|
if (params.timer) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -11,6 +11,7 @@ Module.register("calendar", {
|
|||||||
defaults: {
|
defaults: {
|
||||||
maximumEntries: 10, // Total Maximum Entries
|
maximumEntries: 10, // Total Maximum Entries
|
||||||
maximumNumberOfDays: 365,
|
maximumNumberOfDays: 365,
|
||||||
|
limitDays: 0, // Limit the number of days shown, 0 = no limit
|
||||||
displaySymbol: true,
|
displaySymbol: true,
|
||||||
defaultSymbol: "calendar", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
|
defaultSymbol: "calendar", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
|
||||||
showLocation: false,
|
showLocation: false,
|
||||||
@ -37,6 +38,7 @@ Module.register("calendar", {
|
|||||||
hideOngoing: false,
|
hideOngoing: false,
|
||||||
colored: false,
|
colored: false,
|
||||||
coloredSymbolOnly: false,
|
coloredSymbolOnly: false,
|
||||||
|
customEvents: [], // Array of {keyword: "", symbol: "", color: ""} where Keyword is a regexp and symbol/color are to be applied for matched
|
||||||
tableClass: "small",
|
tableClass: "small",
|
||||||
calendars: [
|
calendars: [
|
||||||
{
|
{
|
||||||
@ -58,6 +60,8 @@ Module.register("calendar", {
|
|||||||
nextDaysRelative: false
|
nextDaysRelative: false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
requiresVersion: "2.1.0",
|
||||||
|
|
||||||
// Define required scripts.
|
// Define required scripts.
|
||||||
getStyles: function () {
|
getStyles: function () {
|
||||||
return ["calendar.css", "font-awesome.css"];
|
return ["calendar.css", "font-awesome.css"];
|
||||||
@ -83,6 +87,12 @@ Module.register("calendar", {
|
|||||||
// Set locale.
|
// Set locale.
|
||||||
moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
|
moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
|
||||||
|
|
||||||
|
// clear data holder before start
|
||||||
|
this.calendarData = {};
|
||||||
|
|
||||||
|
// indicate no data available yet
|
||||||
|
this.loaded = false;
|
||||||
|
|
||||||
for (var c in this.config.calendars) {
|
for (var c in this.config.calendars) {
|
||||||
var calendar = this.config.calendars[c];
|
var calendar = this.config.calendars[c];
|
||||||
calendar.url = calendar.url.replace("webcal://", "http://");
|
calendar.url = calendar.url.replace("webcal://", "http://");
|
||||||
@ -112,18 +122,10 @@ Module.register("calendar", {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tell helper to start a fetcher for this calendar
|
||||||
|
// fetcher till cycle
|
||||||
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
||||||
|
|
||||||
// Trigger ADD_CALENDAR every fetchInterval to make sure there is always a calendar
|
|
||||||
// fetcher running on the server side.
|
|
||||||
var self = this;
|
|
||||||
setInterval(function () {
|
|
||||||
self.addCalendar(calendar.url, calendar.auth, calendarConfig);
|
|
||||||
}, self.config.fetchInterval);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.calendarData = {};
|
|
||||||
this.loaded = false;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Override socket notification handler.
|
// Override socket notification handler.
|
||||||
@ -153,6 +155,12 @@ Module.register("calendar", {
|
|||||||
|
|
||||||
// Override dom generator.
|
// Override dom generator.
|
||||||
getDom: function () {
|
getDom: function () {
|
||||||
|
// Define second, minute, hour, and day constants
|
||||||
|
const oneSecond = 1000; // 1,000 milliseconds
|
||||||
|
const oneMinute = oneSecond * 60;
|
||||||
|
const oneHour = oneMinute * 60;
|
||||||
|
const oneDay = oneHour * 24;
|
||||||
|
|
||||||
var events = this.createEventList();
|
var events = this.createEventList();
|
||||||
var wrapper = document.createElement("table");
|
var wrapper = document.createElement("table");
|
||||||
wrapper.className = this.config.tableClass;
|
wrapper.className = this.config.tableClass;
|
||||||
@ -173,6 +181,8 @@ Module.register("calendar", {
|
|||||||
|
|
||||||
var currentFadeStep = 0;
|
var currentFadeStep = 0;
|
||||||
var lastSeenDate = "";
|
var lastSeenDate = "";
|
||||||
|
var ev;
|
||||||
|
var needle;
|
||||||
|
|
||||||
for (var e in events) {
|
for (var e in events) {
|
||||||
var event = events[e];
|
var event = events[e];
|
||||||
@ -218,6 +228,19 @@ Module.register("calendar", {
|
|||||||
symbolWrapper.className = "symbol align-right " + symbolClass;
|
symbolWrapper.className = "symbol align-right " + symbolClass;
|
||||||
|
|
||||||
var symbols = this.symbolsForEvent(event);
|
var symbols = this.symbolsForEvent(event);
|
||||||
|
// If symbols are displayed and custom symbol is set, replace event symbol
|
||||||
|
if (this.config.displaySymbol && this.config.customEvents.length > 0) {
|
||||||
|
for (ev in this.config.customEvents) {
|
||||||
|
if (typeof this.config.customEvents[ev].symbol !== "undefined" && this.config.customEvents[ev].symbol !== "") {
|
||||||
|
needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||||
|
if (needle.test(event.title)) {
|
||||||
|
symbols[0] = this.config.customEvents[ev].symbol;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = 0; i < symbols.length; i++) {
|
for (var i = 0; i < symbols.length; i++) {
|
||||||
var symbol = document.createElement("span");
|
var symbol = document.createElement("span");
|
||||||
symbol.className = "fa fa-fw fa-" + symbols[i];
|
symbol.className = "fa fa-fw fa-" + symbols[i];
|
||||||
@ -248,6 +271,23 @@ Module.register("calendar", {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Color events if custom color is specified
|
||||||
|
if (this.config.customEvents.length > 0) {
|
||||||
|
for (ev in this.config.customEvents) {
|
||||||
|
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
|
||||||
|
needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||||
|
if (needle.test(event.title)) {
|
||||||
|
eventWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||||
|
titleWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||||
|
if (this.config.displaySymbol) {
|
||||||
|
symbolWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle;
|
titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle;
|
||||||
|
|
||||||
var titleClass = this.titleClassForUrl(event.url);
|
var titleClass = this.titleClassForUrl(event.url);
|
||||||
@ -280,82 +320,56 @@ Module.register("calendar", {
|
|||||||
|
|
||||||
eventWrapper.appendChild(titleWrapper);
|
eventWrapper.appendChild(titleWrapper);
|
||||||
var now = new Date();
|
var now = new Date();
|
||||||
// Define second, minute, hour, and day variables
|
|
||||||
var oneSecond = 1000; // 1,000 milliseconds
|
if (this.config.timeFormat === "absolute") {
|
||||||
var oneMinute = oneSecond * 60;
|
// Use dateFormat
|
||||||
var oneHour = oneMinute * 60;
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||||
var oneDay = oneHour * 24;
|
// Add end time if showEnd
|
||||||
if (event.fullDayEvent) {
|
if (this.config.showEnd) {
|
||||||
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
timeWrapper.innerHTML += "-";
|
||||||
event.endDate -= oneSecond;
|
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
||||||
if (event.today) {
|
}
|
||||||
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
|
// For full day events we use the fullDayEventDateFormat
|
||||||
} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
|
if (event.fullDayEvent) {
|
||||||
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
|
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
||||||
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
|
event.endDate -= oneSecond;
|
||||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
||||||
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
}
|
||||||
} else {
|
if (this.config.getRelative > 0 && event.startDate < now) {
|
||||||
|
// Ongoing and getRelative is set
|
||||||
|
timeWrapper.innerHTML = this.capFirst(
|
||||||
|
this.translate("RUNNING", {
|
||||||
|
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
||||||
|
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * oneDay) {
|
||||||
|
// Within urgency days
|
||||||
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||||
|
}
|
||||||
|
if (event.fullDayEvent && this.config.nextDaysRelative) {
|
||||||
|
// Full days events within the next two days
|
||||||
|
if (event.today) {
|
||||||
|
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 {
|
||||||
|
// Show relative times
|
||||||
|
if (event.startDate >= now) {
|
||||||
|
// Use relative time
|
||||||
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
|
||||||
|
if (event.startDate - now < this.config.getRelative * oneHour) {
|
||||||
|
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/* Check to see if the user displays absolute or relative dates with their events
|
// Ongoing event
|
||||||
* Also check to see if an event is happening within an 'urgency' time frameElement
|
|
||||||
* For example, if the user set an .urgency of 7 days, those events that fall within that
|
|
||||||
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
|
|
||||||
*
|
|
||||||
* 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 * oneDay) {
|
|
||||||
// This event falls within the config.urgency period that the user has set
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").from(moment().format("YYYYMMDD")));
|
|
||||||
} else {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").from(moment().format("YYYYMMDD")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.config.showEnd) {
|
|
||||||
timeWrapper.innerHTML += "-";
|
|
||||||
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.fullDayEventDateFormat));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (event.startDate >= new Date()) {
|
|
||||||
if (event.startDate - now < 2 * oneDay) {
|
|
||||||
// This event is within the next 48 hours (2 days)
|
|
||||||
if (event.startDate - now < this.config.getRelative * oneHour) {
|
|
||||||
// If event is within 6 hour, display 'in xxx' time format or moment.fromNow()
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
|
||||||
} else {
|
|
||||||
if (this.config.timeFormat === "absolute" && !this.config.nextDaysRelative) {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
|
||||||
} else {
|
|
||||||
// Otherwise just say 'Today/Tomorrow at such-n-such time'
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
/* Check to see if the user displays absolute or relative dates with their events
|
|
||||||
* Also check to see if an event is happening within an 'urgency' time frameElement
|
|
||||||
* For example, if the user set an .urgency of 7 days, those events that fall within that
|
|
||||||
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
|
|
||||||
*
|
|
||||||
* 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 * oneDay) {
|
|
||||||
// This event falls within the config.urgency period that the user has set
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
|
||||||
} else {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(
|
timeWrapper.innerHTML = this.capFirst(
|
||||||
this.translate("RUNNING", {
|
this.translate("RUNNING", {
|
||||||
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
||||||
@ -363,12 +377,7 @@ Module.register("calendar", {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.config.showEnd) {
|
|
||||||
timeWrapper.innerHTML += "-";
|
|
||||||
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll');
|
|
||||||
timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
|
timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
|
||||||
eventWrapper.appendChild(timeWrapper);
|
eventWrapper.appendChild(timeWrapper);
|
||||||
}
|
}
|
||||||
@ -521,6 +530,35 @@ Module.register("calendar", {
|
|||||||
events.sort(function (a, b) {
|
events.sort(function (a, b) {
|
||||||
return a.startDate - b.startDate;
|
return a.startDate - b.startDate;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Limit the number of days displayed
|
||||||
|
// If limitDays is set > 0, limit display to that number of days
|
||||||
|
if (this.config.limitDays > 0) {
|
||||||
|
var newEvents = [];
|
||||||
|
var lastDate = today.clone().subtract(1, "days").format("YYYYMMDD");
|
||||||
|
var days = 0;
|
||||||
|
var eventDate;
|
||||||
|
for (var ev of events) {
|
||||||
|
eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
|
||||||
|
// if date of event is later than lastdate
|
||||||
|
// check if we already are showing max unique days
|
||||||
|
if (eventDate > lastDate) {
|
||||||
|
// if the only entry in the first day is a full day event that day is not counted as unique
|
||||||
|
if (newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
|
||||||
|
days--;
|
||||||
|
}
|
||||||
|
days++;
|
||||||
|
if (days > this.config.limitDays) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
lastDate = eventDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newEvents.push(ev);
|
||||||
|
}
|
||||||
|
events = newEvents;
|
||||||
|
}
|
||||||
|
|
||||||
return events.slice(0, this.config.maximumEntries);
|
return events.slice(0, this.config.maximumEntries);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -541,6 +579,8 @@ Module.register("calendar", {
|
|||||||
* @param {object} calendarConfig The config of the specific calendar
|
* @param {object} calendarConfig The config of the specific calendar
|
||||||
*/
|
*/
|
||||||
addCalendar: function (url, auth, calendarConfig) {
|
addCalendar: function (url, auth, calendarConfig) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
this.sendSocketNotification("ADD_CALENDAR", {
|
this.sendSocketNotification("ADD_CALENDAR", {
|
||||||
id: this.identifier,
|
id: this.identifier,
|
||||||
url: url,
|
url: url,
|
||||||
|
@ -76,7 +76,18 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = ical.parseICS(requestData);
|
let data = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = ical.parseICS(requestData);
|
||||||
|
} catch (error) {
|
||||||
|
fetchFailedCallback(self, error.message);
|
||||||
|
scheduleTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.debug(" parsed data=" + JSON.stringify(data));
|
||||||
|
|
||||||
const newEvents = [];
|
const newEvents = [];
|
||||||
|
|
||||||
// limitFunction doesn't do much limiting, see comment re: the dates array in rrule section below as to why we need to do the filtering ourselves
|
// limitFunction doesn't do much limiting, see comment re: the dates array in rrule section below as to why we need to do the filtering ourselves
|
||||||
@ -85,15 +96,15 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
};
|
};
|
||||||
|
|
||||||
const eventDate = function (event, time) {
|
const eventDate = function (event, time) {
|
||||||
return event[time].length === 8 ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
return isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||||
};
|
};
|
||||||
|
Log.debug("there are " + Object.entries(data).length + " calendar entries");
|
||||||
Object.entries(data).forEach(([key, event]) => {
|
Object.entries(data).forEach(([key, event]) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const today = moment().startOf("day").toDate();
|
const today = moment().startOf("day").toDate();
|
||||||
const future = moment().startOf("day").add(maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
const future = moment().startOf("day").add(maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||||
let past = today;
|
let past = today;
|
||||||
|
Log.debug("have entries ");
|
||||||
if (includePastEvents) {
|
if (includePastEvents) {
|
||||||
past = moment().startOf("day").subtract(maximumNumberOfDays, "days").toDate();
|
past = moment().startOf("day").subtract(maximumNumberOfDays, "days").toDate();
|
||||||
}
|
}
|
||||||
@ -111,18 +122,22 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
let startDate = eventDate(event, "start");
|
let startDate = eventDate(event, "start");
|
||||||
let endDate;
|
let endDate;
|
||||||
|
|
||||||
|
Log.debug("\nevent=" + JSON.stringify(event));
|
||||||
if (typeof event.end !== "undefined") {
|
if (typeof event.end !== "undefined") {
|
||||||
endDate = eventDate(event, "end");
|
endDate = eventDate(event, "end");
|
||||||
} else if (typeof event.duration !== "undefined") {
|
} else if (typeof event.duration !== "undefined") {
|
||||||
endDate = startDate.clone().add(moment.duration(event.duration));
|
endDate = startDate.clone().add(moment.duration(event.duration));
|
||||||
} else {
|
} else {
|
||||||
if (!isFacebookBirthday) {
|
if (!isFacebookBirthday) {
|
||||||
endDate = startDate;
|
// make copy of start date, separate storage area
|
||||||
|
endDate = moment(startDate.format("x"), "x");
|
||||||
} else {
|
} else {
|
||||||
endDate = moment(startDate).add(1, "days");
|
endDate = moment(startDate).add(1, "days");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.debug(" start=" + startDate.toDate() + " end=" + endDate.toDate());
|
||||||
|
|
||||||
// calculate the duration of the event for use with recurring events.
|
// calculate the duration of the event for use with recurring events.
|
||||||
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
|
||||||
|
|
||||||
@ -209,11 +224,19 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
pastLocal = pastMoment.toDate();
|
pastLocal = pastMoment.toDate();
|
||||||
futureLocal = futureMoment.toDate();
|
futureLocal = futureMoment.toDate();
|
||||||
} else {
|
} else {
|
||||||
pastLocal = pastMoment.subtract(past.getTimezoneOffset(), "minutes").toDate();
|
// if we want past events
|
||||||
futureLocal = futureMoment.subtract(future.getTimezoneOffset(), "minutes").toDate();
|
if (includePastEvents) {
|
||||||
|
// use the calculated past time for the between from
|
||||||
|
pastLocal = pastMoment.toDate();
|
||||||
|
} else {
|
||||||
|
// otherwise use NOW.. cause we shouldnt use any before now
|
||||||
|
pastLocal = moment().toDate(); //now
|
||||||
|
}
|
||||||
|
futureLocal = futureMoment.toDate(); // future
|
||||||
}
|
}
|
||||||
|
Log.debug(" between=" + pastLocal + " to " + futureLocal);
|
||||||
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||||
|
Log.debug("title=" + event.summary + " dates=" + JSON.stringify(dates));
|
||||||
// The "dates" array contains the set of dates within our desired date range range that are valid
|
// The "dates" array contains the set of dates within our desired date range range that are valid
|
||||||
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
|
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
|
||||||
// had its date changed from outside the range to inside the range. For the time being,
|
// had its date changed from outside the range to inside the range. For the time being,
|
||||||
@ -230,10 +253,9 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through the set of date entries to see which recurrences should be added to our event list.
|
// Loop through the set of date entries to see which recurrences should be added to our event list.
|
||||||
for (let d in dates) {
|
for (let d in dates) {
|
||||||
const date = dates[d];
|
let date = dates[d];
|
||||||
// ical.js started returning recurrences and exdates as ISOStrings without time information.
|
// ical.js started returning recurrences and exdates as ISOStrings without time information.
|
||||||
// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same
|
// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same
|
||||||
// (see https://github.com/peterbraden/ical.js/pull/84 )
|
// (see https://github.com/peterbraden/ical.js/pull/84 )
|
||||||
@ -241,8 +263,29 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
let curEvent = event;
|
let curEvent = event;
|
||||||
let showRecurrence = true;
|
let showRecurrence = true;
|
||||||
|
|
||||||
|
// for full day events, the time might be off from RRULE/Luxon problem
|
||||||
|
if (isFullDayEvent(event)) {
|
||||||
|
Log.debug("fullday");
|
||||||
|
// if the offset is negative, east of GMT where the problem is
|
||||||
|
if (date.getTimezoneOffset() < 0) {
|
||||||
|
// get the offset of today where we are processing
|
||||||
|
// this will be the correction we need to apply
|
||||||
|
let nowOffset = new Date().getTimezoneOffset();
|
||||||
|
Log.debug("now offset is " + nowOffset);
|
||||||
|
// reduce the time by the offset
|
||||||
|
Log.debug(" recurring date is " + date + " offset is " + date.getTimezoneOffset());
|
||||||
|
// apply the correction to the date/time to get it UTC relative
|
||||||
|
date = new Date(date.getTime() - Math.abs(nowOffset) * 60000);
|
||||||
|
// the duration was calculated way back at the top before we could correct the start time..
|
||||||
|
// fix it for this event entry
|
||||||
|
duration = 24 * 60 * 60 * 1000;
|
||||||
|
Log.debug("new recurring date is " + date);
|
||||||
|
}
|
||||||
|
}
|
||||||
startDate = moment(date);
|
startDate = moment(date);
|
||||||
|
|
||||||
|
let adjustDays = getCorrection(event, date);
|
||||||
|
|
||||||
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
|
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
|
||||||
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
|
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
|
||||||
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
|
||||||
@ -255,6 +298,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
// This date is an exception date, which means we should skip it in the recurrence pattern.
|
||||||
showRecurrence = false;
|
showRecurrence = false;
|
||||||
}
|
}
|
||||||
|
Log.debug("duration=" + duration);
|
||||||
|
|
||||||
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
|
||||||
if (startDate.format("x") === endDate.format("x")) {
|
if (startDate.format("x") === endDate.format("x")) {
|
||||||
@ -274,11 +318,12 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showRecurrence === true) {
|
if (showRecurrence === true) {
|
||||||
|
Log.debug("saving event =" + description);
|
||||||
addedEvents++;
|
addedEvents++;
|
||||||
newEvents.push({
|
newEvents.push({
|
||||||
title: recurrenceTitle,
|
title: recurrenceTitle,
|
||||||
startDate: startDate.format("x"),
|
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||||
endDate: endDate.format("x"),
|
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||||
fullDayEvent: isFullDayEvent(event),
|
fullDayEvent: isFullDayEvent(event),
|
||||||
recurringEvent: true,
|
recurringEvent: true,
|
||||||
class: event.class,
|
class: event.class,
|
||||||
@ -293,6 +338,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
} else {
|
} else {
|
||||||
// Single event.
|
// Single event.
|
||||||
const fullDayEvent = isFacebookBirthday ? true : isFullDayEvent(event);
|
const fullDayEvent = isFacebookBirthday ? true : isFullDayEvent(event);
|
||||||
|
// Log.debug("full day event")
|
||||||
|
|
||||||
if (includePastEvents) {
|
if (includePastEvents) {
|
||||||
// Past event is too far in the past, so skip.
|
// Past event is too far in the past, so skip.
|
||||||
@ -324,12 +370,17 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
if (fullDayEvent && startDate <= today) {
|
if (fullDayEvent && startDate <= today) {
|
||||||
startDate = moment(today);
|
startDate = moment(today);
|
||||||
}
|
}
|
||||||
|
// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
|
||||||
|
if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
|
||||||
|
endDate = endDate.endOf("day");
|
||||||
|
}
|
||||||
|
// get correction for date saving and dst change between now and then
|
||||||
|
let adjustDays = getCorrection(event, startDate.toDate());
|
||||||
// Every thing is good. Add it to the list.
|
// Every thing is good. Add it to the list.
|
||||||
newEvents.push({
|
newEvents.push({
|
||||||
title: title,
|
title: title,
|
||||||
startDate: startDate.format("x"),
|
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
|
||||||
endDate: endDate.format("x"),
|
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
|
||||||
fullDayEvent: fullDayEvent,
|
fullDayEvent: fullDayEvent,
|
||||||
class: event.class,
|
class: event.class,
|
||||||
location: location,
|
location: location,
|
||||||
@ -344,13 +395,138 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
return a.startDate - b.startDate;
|
return a.startDate - b.startDate;
|
||||||
});
|
});
|
||||||
|
|
||||||
events = newEvents.slice(0, maximumEntries);
|
// include up to maximumEntries current or upcoming events
|
||||||
|
// If past events should be included, include all past events
|
||||||
|
const now = moment();
|
||||||
|
var entries = 0;
|
||||||
|
events = [];
|
||||||
|
for (let ne of newEvents) {
|
||||||
|
if (moment(ne.endDate, "x").isBefore(now)) {
|
||||||
|
if (includePastEvents) events.push(ne);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entries++;
|
||||||
|
// If max events has been saved, skip the rest
|
||||||
|
if (entries > maximumEntries) break;
|
||||||
|
events.push(ne);
|
||||||
|
}
|
||||||
|
|
||||||
self.broadcastEvents();
|
self.broadcastEvents();
|
||||||
scheduleTimer();
|
scheduleTimer();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* get the time correction, either dst/std or full day in cases where utc time is day before plus offset
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const getCorrection = function (event, date) {
|
||||||
|
let adjustHours = 0;
|
||||||
|
// if a timezone was specified
|
||||||
|
if (!event.start.tz) {
|
||||||
|
Log.debug(" if no tz, guess based on now");
|
||||||
|
event.start.tz = moment.tz.guess();
|
||||||
|
}
|
||||||
|
Log.debug("initial tz=" + event.start.tz);
|
||||||
|
|
||||||
|
// if there is a start date specified
|
||||||
|
if (event.start.tz) {
|
||||||
|
// if this is a windows timezone
|
||||||
|
if (event.start.tz.includes(" ")) {
|
||||||
|
// use the lookup table to get theIANA name as moment and date don't know MS timezones
|
||||||
|
let tz = getIanaTZFromMS(event.start.tz);
|
||||||
|
Log.debug("corrected TZ=" + tz);
|
||||||
|
// watch out for unregistered windows timezone names
|
||||||
|
// if we had a successfule lookup
|
||||||
|
if (tz) {
|
||||||
|
// change the timezone to the IANA name
|
||||||
|
event.start.tz = tz;
|
||||||
|
// Log.debug("corrected timezone="+event.start.tz)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.debug("corrected tz=" + event.start.tz);
|
||||||
|
let current_offset = 0; // offset from TZ string or calculated
|
||||||
|
let mm = 0; // date with tz or offset
|
||||||
|
let start_offset = 0; // utc offset of created with tz
|
||||||
|
// if there is still an offset, lookup failed, use it
|
||||||
|
if (event.start.tz.startsWith("(")) {
|
||||||
|
const regex = /[+|-]\d*:\d*/;
|
||||||
|
const start_offsetString = event.start.tz.match(regex).toString().split(":");
|
||||||
|
let start_offset = parseInt(start_offsetString[0]);
|
||||||
|
start_offset *= event.start.tz[1] === "-" ? -1 : 1;
|
||||||
|
adjustHours = start_offset;
|
||||||
|
Log.debug("defined offset=" + start_offset + " hours");
|
||||||
|
current_offset = start_offset;
|
||||||
|
event.start.tz = "";
|
||||||
|
Log.debug("ical offset=" + current_offset + " date=" + date);
|
||||||
|
mm = moment(date);
|
||||||
|
let x = parseInt(moment(new Date()).utcOffset());
|
||||||
|
Log.debug("net mins=" + (current_offset * 60 - x));
|
||||||
|
|
||||||
|
mm = mm.add(x - current_offset * 60, "minutes");
|
||||||
|
adjustHours = (current_offset * 60 - x) / 60;
|
||||||
|
event.start = mm.toDate();
|
||||||
|
Log.debug("adjusted date=" + event.start);
|
||||||
|
} else {
|
||||||
|
// get the start time in that timezone
|
||||||
|
Log.debug("start date/time=" + moment(event.start).toDate());
|
||||||
|
start_offset = moment.tz(moment(event.start), event.start.tz).utcOffset();
|
||||||
|
Log.debug("start offset=" + start_offset);
|
||||||
|
|
||||||
|
Log.debug("start date/time w tz =" + moment.tz(moment(event.start), event.start.tz).toDate());
|
||||||
|
|
||||||
|
// get the specified date in that timezone
|
||||||
|
mm = moment.tz(moment(date), event.start.tz);
|
||||||
|
Log.debug("event date=" + mm.toDate());
|
||||||
|
current_offset = mm.utcOffset();
|
||||||
|
}
|
||||||
|
Log.debug("event offset=" + current_offset + " hour=" + mm.format("H") + " event date=" + mm.toDate());
|
||||||
|
|
||||||
|
// if the offset is greater than 0, east of london
|
||||||
|
if (current_offset !== start_offset) {
|
||||||
|
// big offset
|
||||||
|
Log.debug("offset");
|
||||||
|
let h = parseInt(mm.format("H"));
|
||||||
|
// check if the event time is less than the offset
|
||||||
|
if (h > 0 && h < Math.abs(current_offset) / 60) {
|
||||||
|
// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
|
||||||
|
// we need to fix that
|
||||||
|
adjustHours = 24;
|
||||||
|
// Log.debug("adjusting date")
|
||||||
|
}
|
||||||
|
//-300 > -240
|
||||||
|
//if (Math.abs(current_offset) > Math.abs(start_offset)){
|
||||||
|
if (current_offset > start_offset) {
|
||||||
|
adjustHours -= 1;
|
||||||
|
Log.debug("adjust down 1 hour dst change");
|
||||||
|
//} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
|
||||||
|
} else if (current_offset < start_offset) {
|
||||||
|
adjustHours += 1;
|
||||||
|
Log.debug("adjust up 1 hour dst change");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.debug("adjustHours=" + adjustHours);
|
||||||
|
return adjustHours;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* lookup iana tz from windows
|
||||||
|
*/
|
||||||
|
let zoneTable = null;
|
||||||
|
const getIanaTZFromMS = function (msTZName) {
|
||||||
|
if (!zoneTable) {
|
||||||
|
const p = require("path");
|
||||||
|
zoneTable = require(p.join(__dirname, "windowsZones.json"));
|
||||||
|
}
|
||||||
|
// Get hash entry
|
||||||
|
const he = zoneTable[msTZName];
|
||||||
|
// If found return iana name, else null
|
||||||
|
return he ? he.iana[0] : null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule the timer for the next update.
|
* Schedule the timer for the next update.
|
||||||
*/
|
*/
|
||||||
@ -368,7 +544,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
* @returns {boolean} True if the event is a fullday event, false otherwise
|
* @returns {boolean} True if the event is a fullday event, false otherwise
|
||||||
*/
|
*/
|
||||||
const isFullDayEvent = function (event) {
|
const isFullDayEvent = function (event) {
|
||||||
if (event.start.length === 8 || event.start.dateOnly) {
|
if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +70,6 @@ module.exports = NodeHelper.create({
|
|||||||
} else {
|
} else {
|
||||||
Log.log("Use existing calendar fetcher for url: " + url);
|
Log.log("Use existing calendar fetcher for url: " + url);
|
||||||
fetcher = self.fetchers[identifier + url];
|
fetcher = self.fetchers[identifier + url];
|
||||||
fetcher.broadcastEvents();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fetcher.startFetch();
|
fetcher.startFetch();
|
||||||
|
237
modules/default/calendar/windowsZones.json
Normal file
237
modules/default/calendar/windowsZones.json
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
{
|
||||||
|
"Dateline Standard Time": { "iana": ["Etc/GMT+12"] },
|
||||||
|
"UTC-11": { "iana": ["Etc/GMT+11"] },
|
||||||
|
"Aleutian Standard Time": { "iana": ["America/Adak"] },
|
||||||
|
"Hawaiian Standard Time": { "iana": ["Pacific/Honolulu"] },
|
||||||
|
"Marquesas Standard Time": { "iana": ["Pacific/Marquesas"] },
|
||||||
|
"Alaskan Standard Time": { "iana": ["America/Anchorage"] },
|
||||||
|
"UTC-09": { "iana": ["Etc/GMT+9"] },
|
||||||
|
"Pacific Standard Time (Mexico)": { "iana": ["America/Tijuana"] },
|
||||||
|
"UTC-08": { "iana": ["Etc/GMT+8"] },
|
||||||
|
"Pacific Standard Time": { "iana": ["America/Los_Angeles"] },
|
||||||
|
"US Mountain Standard Time": { "iana": ["America/Phoenix"] },
|
||||||
|
"Mountain Standard Time (Mexico)": { "iana": ["America/Chihuahua"] },
|
||||||
|
"Mountain Standard Time": { "iana": ["America/Denver"] },
|
||||||
|
"Central America Standard Time": { "iana": ["America/Guatemala"] },
|
||||||
|
"Central Standard Time": { "iana": ["America/Chicago"] },
|
||||||
|
"Easter Island Standard Time": { "iana": ["Pacific/Easter"] },
|
||||||
|
"Central Standard Time (Mexico)": { "iana": ["America/Mexico_City"] },
|
||||||
|
"Canada Central Standard Time": { "iana": ["America/Regina"] },
|
||||||
|
"SA Pacific Standard Time": { "iana": ["America/Bogota"] },
|
||||||
|
"Eastern Standard Time (Mexico)": { "iana": ["America/Cancun"] },
|
||||||
|
"Eastern Standard Time": { "iana": ["America/New_York"] },
|
||||||
|
"Haiti Standard Time": { "iana": ["America/Port-au-Prince"] },
|
||||||
|
"Cuba Standard Time": { "iana": ["America/Havana"] },
|
||||||
|
"US Eastern Standard Time": { "iana": ["America/Indianapolis"] },
|
||||||
|
"Turks And Caicos Standard Time": { "iana": ["America/Grand_Turk"] },
|
||||||
|
"Paraguay Standard Time": { "iana": ["America/Asuncion"] },
|
||||||
|
"Atlantic Standard Time": { "iana": ["America/Halifax"] },
|
||||||
|
"Venezuela Standard Time": { "iana": ["America/Caracas"] },
|
||||||
|
"Central Brazilian Standard Time": { "iana": ["America/Cuiaba"] },
|
||||||
|
"SA Western Standard Time": { "iana": ["America/La_Paz"] },
|
||||||
|
"Pacific SA Standard Time": { "iana": ["America/Santiago"] },
|
||||||
|
"Newfoundland Standard Time": { "iana": ["America/St_Johns"] },
|
||||||
|
"Tocantins Standard Time": { "iana": ["America/Araguaina"] },
|
||||||
|
"E. South America Standard Time": { "iana": ["America/Sao_Paulo"] },
|
||||||
|
"SA Eastern Standard Time": { "iana": ["America/Cayenne"] },
|
||||||
|
"Argentina Standard Time": { "iana": ["America/Buenos_Aires"] },
|
||||||
|
"Greenland Standard Time": { "iana": ["America/Godthab"] },
|
||||||
|
"Montevideo Standard Time": { "iana": ["America/Montevideo"] },
|
||||||
|
"Magallanes Standard Time": { "iana": ["America/Punta_Arenas"] },
|
||||||
|
"Saint Pierre Standard Time": { "iana": ["America/Miquelon"] },
|
||||||
|
"Bahia Standard Time": { "iana": ["America/Bahia"] },
|
||||||
|
"UTC-02": { "iana": ["Etc/GMT+2"] },
|
||||||
|
"Azores Standard Time": { "iana": ["Atlantic/Azores"] },
|
||||||
|
"Cape Verde Standard Time": { "iana": ["Atlantic/Cape_Verde"] },
|
||||||
|
"UTC": { "iana": ["Etc/GMT"] },
|
||||||
|
"GMT Standard Time": { "iana": ["Europe/London"] },
|
||||||
|
"Greenwich Standard Time": { "iana": ["Atlantic/Reykjavik"] },
|
||||||
|
"Sao Tome Standard Time": { "iana": ["Africa/Sao_Tome"] },
|
||||||
|
"Morocco Standard Time": { "iana": ["Africa/Casablanca"] },
|
||||||
|
"W. Europe Standard Time": { "iana": ["Europe/Berlin"] },
|
||||||
|
"Central Europe Standard Time": { "iana": ["Europe/Budapest"] },
|
||||||
|
"Romance Standard Time": { "iana": ["Europe/Paris"] },
|
||||||
|
"Central European Standard Time": { "iana": ["Europe/Warsaw"] },
|
||||||
|
"W. Central Africa Standard Time": { "iana": ["Africa/Lagos"] },
|
||||||
|
"Jordan Standard Time": { "iana": ["Asia/Amman"] },
|
||||||
|
"GTB Standard Time": { "iana": ["Europe/Bucharest"] },
|
||||||
|
"Middle East Standard Time": { "iana": ["Asia/Beirut"] },
|
||||||
|
"Egypt Standard Time": { "iana": ["Africa/Cairo"] },
|
||||||
|
"E. Europe Standard Time": { "iana": ["Europe/Chisinau"] },
|
||||||
|
"Syria Standard Time": { "iana": ["Asia/Damascus"] },
|
||||||
|
"West Bank Standard Time": { "iana": ["Asia/Hebron"] },
|
||||||
|
"South Africa Standard Time": { "iana": ["Africa/Johannesburg"] },
|
||||||
|
"FLE Standard Time": { "iana": ["Europe/Kiev"] },
|
||||||
|
"Israel Standard Time": { "iana": ["Asia/Jerusalem"] },
|
||||||
|
"Kaliningrad Standard Time": { "iana": ["Europe/Kaliningrad"] },
|
||||||
|
"Sudan Standard Time": { "iana": ["Africa/Khartoum"] },
|
||||||
|
"Libya Standard Time": { "iana": ["Africa/Tripoli"] },
|
||||||
|
"Namibia Standard Time": { "iana": ["Africa/Windhoek"] },
|
||||||
|
"Arabic Standard Time": { "iana": ["Asia/Baghdad"] },
|
||||||
|
"Turkey Standard Time": { "iana": ["Europe/Istanbul"] },
|
||||||
|
"Arab Standard Time": { "iana": ["Asia/Riyadh"] },
|
||||||
|
"Belarus Standard Time": { "iana": ["Europe/Minsk"] },
|
||||||
|
"Russian Standard Time": { "iana": ["Europe/Moscow"] },
|
||||||
|
"E. Africa Standard Time": { "iana": ["Africa/Nairobi"] },
|
||||||
|
"Iran Standard Time": { "iana": ["Asia/Tehran"] },
|
||||||
|
"Arabian Standard Time": { "iana": ["Asia/Dubai"] },
|
||||||
|
"Astrakhan Standard Time": { "iana": ["Europe/Astrakhan"] },
|
||||||
|
"Azerbaijan Standard Time": { "iana": ["Asia/Baku"] },
|
||||||
|
"Russia Time Zone 3": { "iana": ["Europe/Samara"] },
|
||||||
|
"Mauritius Standard Time": { "iana": ["Indian/Mauritius"] },
|
||||||
|
"Saratov Standard Time": { "iana": ["Europe/Saratov"] },
|
||||||
|
"Georgian Standard Time": { "iana": ["Asia/Tbilisi"] },
|
||||||
|
"Volgograd Standard Time": { "iana": ["Europe/Volgograd"] },
|
||||||
|
"Caucasus Standard Time": { "iana": ["Asia/Yerevan"] },
|
||||||
|
"Afghanistan Standard Time": { "iana": ["Asia/Kabul"] },
|
||||||
|
"West Asia Standard Time": { "iana": ["Asia/Tashkent"] },
|
||||||
|
"Ekaterinburg Standard Time": { "iana": ["Asia/Yekaterinburg"] },
|
||||||
|
"Pakistan Standard Time": { "iana": ["Asia/Karachi"] },
|
||||||
|
"Qyzylorda Standard Time": { "iana": ["Asia/Qyzylorda"] },
|
||||||
|
"India Standard Time": { "iana": ["Asia/Calcutta"] },
|
||||||
|
"Sri Lanka Standard Time": { "iana": ["Asia/Colombo"] },
|
||||||
|
"Nepal Standard Time": { "iana": ["Asia/Katmandu"] },
|
||||||
|
"Central Asia Standard Time": { "iana": ["Asia/Almaty"] },
|
||||||
|
"Bangladesh Standard Time": { "iana": ["Asia/Dhaka"] },
|
||||||
|
"Omsk Standard Time": { "iana": ["Asia/Omsk"] },
|
||||||
|
"Myanmar Standard Time": { "iana": ["Asia/Rangoon"] },
|
||||||
|
"SE Asia Standard Time": { "iana": ["Asia/Bangkok"] },
|
||||||
|
"Altai Standard Time": { "iana": ["Asia/Barnaul"] },
|
||||||
|
"W. Mongolia Standard Time": { "iana": ["Asia/Hovd"] },
|
||||||
|
"North Asia Standard Time": { "iana": ["Asia/Krasnoyarsk"] },
|
||||||
|
"N. Central Asia Standard Time": { "iana": ["Asia/Novosibirsk"] },
|
||||||
|
"Tomsk Standard Time": { "iana": ["Asia/Tomsk"] },
|
||||||
|
"China Standard Time": { "iana": ["Asia/Shanghai"] },
|
||||||
|
"North Asia East Standard Time": { "iana": ["Asia/Irkutsk"] },
|
||||||
|
"Singapore Standard Time": { "iana": ["Asia/Singapore"] },
|
||||||
|
"W. Australia Standard Time": { "iana": ["Australia/Perth"] },
|
||||||
|
"Taipei Standard Time": { "iana": ["Asia/Taipei"] },
|
||||||
|
"Ulaanbaatar Standard Time": { "iana": ["Asia/Ulaanbaatar"] },
|
||||||
|
"Aus Central W. Standard Time": { "iana": ["Australia/Eucla"] },
|
||||||
|
"Transbaikal Standard Time": { "iana": ["Asia/Chita"] },
|
||||||
|
"Tokyo Standard Time": { "iana": ["Asia/Tokyo"] },
|
||||||
|
"North Korea Standard Time": { "iana": ["Asia/Pyongyang"] },
|
||||||
|
"Korea Standard Time": { "iana": ["Asia/Seoul"] },
|
||||||
|
"Yakutsk Standard Time": { "iana": ["Asia/Yakutsk"] },
|
||||||
|
"Cen. Australia Standard Time": { "iana": ["Australia/Adelaide"] },
|
||||||
|
"AUS Central Standard Time": { "iana": ["Australia/Darwin"] },
|
||||||
|
"E. Australia Standard Time": { "iana": ["Australia/Brisbane"] },
|
||||||
|
"AUS Eastern Standard Time": { "iana": ["Australia/Sydney"] },
|
||||||
|
"West Pacific Standard Time": { "iana": ["Pacific/Port_Moresby"] },
|
||||||
|
"Tasmania Standard Time": { "iana": ["Australia/Hobart"] },
|
||||||
|
"Vladivostok Standard Time": { "iana": ["Asia/Vladivostok"] },
|
||||||
|
"Lord Howe Standard Time": { "iana": ["Australia/Lord_Howe"] },
|
||||||
|
"Bougainville Standard Time": { "iana": ["Pacific/Bougainville"] },
|
||||||
|
"Russia Time Zone 10": { "iana": ["Asia/Srednekolymsk"] },
|
||||||
|
"Magadan Standard Time": { "iana": ["Asia/Magadan"] },
|
||||||
|
"Norfolk Standard Time": { "iana": ["Pacific/Norfolk"] },
|
||||||
|
"Sakhalin Standard Time": { "iana": ["Asia/Sakhalin"] },
|
||||||
|
"Central Pacific Standard Time": { "iana": ["Pacific/Guadalcanal"] },
|
||||||
|
"Russia Time Zone 11": { "iana": ["Asia/Kamchatka"] },
|
||||||
|
"New Zealand Standard Time": { "iana": ["Pacific/Auckland"] },
|
||||||
|
"UTC+12": { "iana": ["Etc/GMT-12"] },
|
||||||
|
"Fiji Standard Time": { "iana": ["Pacific/Fiji"] },
|
||||||
|
"Chatham Islands Standard Time": { "iana": ["Pacific/Chatham"] },
|
||||||
|
"UTC+13": { "iana": ["Etc/GMT-13"] },
|
||||||
|
"Tonga Standard Time": { "iana": ["Pacific/Tongatapu"] },
|
||||||
|
"Samoa Standard Time": { "iana": ["Pacific/Apia"] },
|
||||||
|
"Line Islands Standard Time": { "iana": ["Pacific/Kiritimati"] },
|
||||||
|
"(UTC-12:00) International Date Line West": { "iana": ["Etc/GMT+12"] },
|
||||||
|
"(UTC-11:00) Midway Island, Samoa": { "iana": ["Pacific/Apia"] },
|
||||||
|
"(UTC-10:00) Hawaii": { "iana": ["Pacific/Honolulu"] },
|
||||||
|
"(UTC-09:00) Alaska": { "iana": ["America/Anchorage"] },
|
||||||
|
"(UTC-08:00) Pacific Time (US & Canada); Tijuana": { "iana": ["America/Los_Angeles"] },
|
||||||
|
"(UTC-08:00) Pacific Time (US and Canada); Tijuana": { "iana": ["America/Los_Angeles"] },
|
||||||
|
"(UTC-07:00) Mountain Time (US & Canada)": { "iana": ["America/Denver"] },
|
||||||
|
"(UTC-07:00) Mountain Time (US and Canada)": { "iana": ["America/Denver"] },
|
||||||
|
"(UTC-07:00) Chihuahua, La Paz, Mazatlan": { "iana": [null] },
|
||||||
|
"(UTC-07:00) Arizona": { "iana": ["America/Phoenix"] },
|
||||||
|
"(UTC-06:00) Central Time (US & Canada)": { "iana": ["America/Chicago"] },
|
||||||
|
"(UTC-06:00) Central Time (US and Canada)": { "iana": ["America/Chicago"] },
|
||||||
|
"(UTC-06:00) Saskatchewan": { "iana": ["America/Regina"] },
|
||||||
|
"(UTC-06:00) Guadalajara, Mexico City, Monterrey": { "iana": [null] },
|
||||||
|
"(UTC-06:00) Central America": { "iana": ["America/Guatemala"] },
|
||||||
|
"(UTC-05:00) Eastern Time (US & Canada)": { "iana": ["America/New_York"] },
|
||||||
|
"(UTC-05:00) Eastern Time (US and Canada)": { "iana": ["America/New_York"] },
|
||||||
|
"(UTC-05:00) Indiana (East)": { "iana": ["America/Indianapolis"] },
|
||||||
|
"(UTC-05:00) Bogota, Lima, Quito": { "iana": ["America/Bogota"] },
|
||||||
|
"(UTC-04:00) Atlantic Time (Canada)": { "iana": ["America/Halifax"] },
|
||||||
|
"(UTC-04:00) Georgetown, La Paz, San Juan": { "iana": ["America/La_Paz"] },
|
||||||
|
"(UTC-04:00) Santiago": { "iana": ["America/Santiago"] },
|
||||||
|
"(UTC-03:30) Newfoundland": { "iana": [null] },
|
||||||
|
"(UTC-03:00) Brasilia": { "iana": ["America/Sao_Paulo"] },
|
||||||
|
"(UTC-03:00) Georgetown": { "iana": ["America/Cayenne"] },
|
||||||
|
"(UTC-03:00) Greenland": { "iana": ["America/Godthab"] },
|
||||||
|
"(UTC-02:00) Mid-Atlantic": { "iana": [null] },
|
||||||
|
"(UTC-01:00) Azores": { "iana": ["Atlantic/Azores"] },
|
||||||
|
"(UTC-01:00) Cape Verde Islands": { "iana": ["Atlantic/Cape_Verde"] },
|
||||||
|
"(UTC) Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London": { "iana": [null] },
|
||||||
|
"(UTC) Monrovia, Reykjavik": { "iana": ["Atlantic/Reykjavik"] },
|
||||||
|
"(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague": { "iana": ["Europe/Budapest"] },
|
||||||
|
"(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb": { "iana": ["Europe/Warsaw"] },
|
||||||
|
"(UTC+01:00) Brussels, Copenhagen, Madrid, Paris": { "iana": ["Europe/Paris"] },
|
||||||
|
"(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna": { "iana": ["Europe/Berlin"] },
|
||||||
|
"(UTC+01:00) West Central Africa": { "iana": ["Africa/Lagos"] },
|
||||||
|
"(UTC+02:00) Minsk": { "iana": ["Europe/Chisinau"] },
|
||||||
|
"(UTC+02:00) Cairo": { "iana": ["Africa/Cairo"] },
|
||||||
|
"(UTC+02:00) Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius": { "iana": ["Europe/Kiev"] },
|
||||||
|
"(UTC+02:00) Athens, Bucharest, Istanbul": { "iana": ["Europe/Bucharest"] },
|
||||||
|
"(UTC+02:00) Jerusalem": { "iana": ["Asia/Jerusalem"] },
|
||||||
|
"(UTC+02:00) Harare, Pretoria": { "iana": ["Africa/Johannesburg"] },
|
||||||
|
"(UTC+03:00) Moscow, St. Petersburg, Volgograd": { "iana": ["Europe/Moscow"] },
|
||||||
|
"(UTC+03:00) Kuwait, Riyadh": { "iana": ["Asia/Riyadh"] },
|
||||||
|
"(UTC+03:00) Nairobi": { "iana": ["Africa/Nairobi"] },
|
||||||
|
"(UTC+03:00) Baghdad": { "iana": ["Asia/Baghdad"] },
|
||||||
|
"(UTC+03:30) Tehran": { "iana": ["Asia/Tehran"] },
|
||||||
|
"(UTC+04:00) Abu Dhabi, Muscat": { "iana": ["Asia/Dubai"] },
|
||||||
|
"(UTC+04:00) Baku, Tbilisi, Yerevan": { "iana": ["Asia/Yerevan"] },
|
||||||
|
"(UTC+04:30) Kabul": { "iana": [null] },
|
||||||
|
"(UTC+05:00) Ekaterinburg": { "iana": ["Asia/Yekaterinburg"] },
|
||||||
|
"(UTC+05:00) Tashkent": { "iana": ["Asia/Tashkent"] },
|
||||||
|
"(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi": { "iana": ["Asia/Calcutta"] },
|
||||||
|
"(UTC+05:45) Kathmandu": { "iana": ["Asia/Katmandu"] },
|
||||||
|
"(UTC+06:00) Astana, Dhaka": { "iana": ["Asia/Almaty"] },
|
||||||
|
"(UTC+06:00) Sri Jayawardenepura": { "iana": ["Asia/Colombo"] },
|
||||||
|
"(UTC+06:00) Almaty, Novosibirsk": { "iana": ["Asia/Novosibirsk"] },
|
||||||
|
"(UTC+06:30) Yangon (Rangoon)": { "iana": ["Asia/Rangoon"] },
|
||||||
|
"(UTC+07:00) Bangkok, Hanoi, Jakarta": { "iana": ["Asia/Bangkok"] },
|
||||||
|
"(UTC+07:00) Krasnoyarsk": { "iana": ["Asia/Krasnoyarsk"] },
|
||||||
|
"(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi": { "iana": ["Asia/Shanghai"] },
|
||||||
|
"(UTC+08:00) Kuala Lumpur, Singapore": { "iana": ["Asia/Singapore"] },
|
||||||
|
"(UTC+08:00) Taipei": { "iana": ["Asia/Taipei"] },
|
||||||
|
"(UTC+08:00) Perth": { "iana": ["Australia/Perth"] },
|
||||||
|
"(UTC+08:00) Irkutsk, Ulaanbaatar": { "iana": ["Asia/Irkutsk"] },
|
||||||
|
"(UTC+09:00) Seoul": { "iana": ["Asia/Seoul"] },
|
||||||
|
"(UTC+09:00) Osaka, Sapporo, Tokyo": { "iana": ["Asia/Tokyo"] },
|
||||||
|
"(UTC+09:00) Yakutsk": { "iana": ["Asia/Yakutsk"] },
|
||||||
|
"(UTC+09:30) Darwin": { "iana": ["Australia/Darwin"] },
|
||||||
|
"(UTC+09:30) Adelaide": { "iana": ["Australia/Adelaide"] },
|
||||||
|
"(UTC+10:00) Canberra, Melbourne, Sydney": { "iana": ["Australia/Sydney"] },
|
||||||
|
"(GMT+10:00) Canberra, Melbourne, Sydney": { "iana": ["Australia/Sydney"] },
|
||||||
|
"(UTC+10:00) Brisbane": { "iana": ["Australia/Brisbane"] },
|
||||||
|
"(UTC+10:00) Hobart": { "iana": ["Australia/Hobart"] },
|
||||||
|
"(UTC+10:00) Vladivostok": { "iana": ["Asia/Vladivostok"] },
|
||||||
|
"(UTC+10:00) Guam, Port Moresby": { "iana": ["Pacific/Port_Moresby"] },
|
||||||
|
"(UTC+11:00) Magadan, Solomon Islands, New Caledonia": { "iana": ["Pacific/Guadalcanal"] },
|
||||||
|
"(UTC+12:00) Fiji, Kamchatka, Marshall Is.": { "iana": [null] },
|
||||||
|
"(UTC+12:00) Auckland, Wellington": { "iana": ["Pacific/Auckland"] },
|
||||||
|
"(UTC+13:00) Nuku'alofa": { "iana": ["Pacific/Tongatapu"] },
|
||||||
|
"(UTC-03:00) Buenos Aires": { "iana": ["America/Buenos_Aires"] },
|
||||||
|
"(UTC+02:00) Beirut": { "iana": ["Asia/Beirut"] },
|
||||||
|
"(UTC+02:00) Amman": { "iana": ["Asia/Amman"] },
|
||||||
|
"(UTC-06:00) Guadalajara, Mexico City, Monterrey - New": { "iana": ["America/Mexico_City"] },
|
||||||
|
"(UTC-07:00) Chihuahua, La Paz, Mazatlan - New": { "iana": ["America/Chihuahua"] },
|
||||||
|
"(UTC-08:00) Tijuana, Baja California": { "iana": ["America/Tijuana"] },
|
||||||
|
"(UTC+02:00) Windhoek": { "iana": ["Africa/Windhoek"] },
|
||||||
|
"(UTC+03:00) Tbilisi": { "iana": ["Asia/Tbilisi"] },
|
||||||
|
"(UTC-04:00) Manaus": { "iana": ["America/Cuiaba"] },
|
||||||
|
"(UTC-03:00) Montevideo": { "iana": ["America/Montevideo"] },
|
||||||
|
"(UTC+04:00) Yerevan": { "iana": [null] },
|
||||||
|
"(UTC-04:30) Caracas": { "iana": ["America/Caracas"] },
|
||||||
|
"(UTC) Casablanca": { "iana": ["Africa/Casablanca"] },
|
||||||
|
"(UTC+05:00) Islamabad, Karachi": { "iana": ["Asia/Karachi"] },
|
||||||
|
"(UTC+04:00) Port Louis": { "iana": ["Indian/Mauritius"] },
|
||||||
|
"(UTC) Coordinated Universal Time": { "iana": ["Etc/GMT"] },
|
||||||
|
"(UTC-04:00) Asuncion": { "iana": ["America/Asuncion"] },
|
||||||
|
"(UTC+12:00) Petropavlovsk-Kamchatsky": { "iana": [null] }
|
||||||
|
}
|
@ -187,10 +187,10 @@ Module.register("clock", {
|
|||||||
'"><i class="fa fa-sun-o" aria-hidden="true"></i> ' +
|
'"><i class="fa fa-sun-o" aria-hidden="true"></i> ' +
|
||||||
untilNextEventString +
|
untilNextEventString +
|
||||||
"</span>" +
|
"</span>" +
|
||||||
'<span><i class="fa fa-arrow-up" aria-hidden="true"></i>' +
|
'<span><i class="fa fa-arrow-up" aria-hidden="true"></i> ' +
|
||||||
formatTime(this.config, sunTimes.sunrise) +
|
formatTime(this.config, sunTimes.sunrise) +
|
||||||
"</span>" +
|
"</span>" +
|
||||||
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i>' +
|
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i> ' +
|
||||||
formatTime(this.config, sunTimes.sunset) +
|
formatTime(this.config, sunTimes.sunset) +
|
||||||
"</span>";
|
"</span>";
|
||||||
}
|
}
|
||||||
|
@ -258,7 +258,13 @@ Module.register("currentweather", {
|
|||||||
|
|
||||||
var feelsLike = document.createElement("span");
|
var feelsLike = document.createElement("span");
|
||||||
feelsLike.className = "dimmed";
|
feelsLike.className = "dimmed";
|
||||||
feelsLike.innerHTML = this.translate("FEELS") + " " + this.feelsLike + degreeLabel;
|
var feelsLikeHtml = this.translate("FEELS");
|
||||||
|
if (feelsLikeHtml.indexOf("{DEGREE}") > -1) {
|
||||||
|
feelsLikeHtml = this.translate("FEELS", {
|
||||||
|
DEGREE: this.feelsLike + degreeLabel
|
||||||
|
});
|
||||||
|
} else feelsLikeHtml += " " + this.feelsLike + degreeLabel;
|
||||||
|
feelsLike.innerHTML = feelsLikeHtml;
|
||||||
small.appendChild(feelsLike);
|
small.appendChild(feelsLike);
|
||||||
|
|
||||||
wrapper.appendChild(small);
|
wrapper.appendChild(small);
|
||||||
|
@ -350,7 +350,7 @@ Module.register("newsfeed", {
|
|||||||
this.activeItem = 0;
|
this.activeItem = 0;
|
||||||
}
|
}
|
||||||
this.resetDescrOrFullArticleAndTimer();
|
this.resetDescrOrFullArticleAndTimer();
|
||||||
Log.info(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
||||||
this.updateDom(100);
|
this.updateDom(100);
|
||||||
} else if (notification === "ARTICLE_PREVIOUS") {
|
} else if (notification === "ARTICLE_PREVIOUS") {
|
||||||
this.activeItem--;
|
this.activeItem--;
|
||||||
@ -358,7 +358,7 @@ Module.register("newsfeed", {
|
|||||||
this.activeItem = this.newsItems.length - 1;
|
this.activeItem = this.newsItems.length - 1;
|
||||||
}
|
}
|
||||||
this.resetDescrOrFullArticleAndTimer();
|
this.resetDescrOrFullArticleAndTimer();
|
||||||
Log.info(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
Log.debug(this.name + " - going from article #" + before + " to #" + this.activeItem + " (of " + this.newsItems.length + ")");
|
||||||
this.updateDom(100);
|
this.updateDom(100);
|
||||||
}
|
}
|
||||||
// if "more details" is received the first time: show article summary, on second time show full article
|
// if "more details" is received the first time: show article summary, on second time show full article
|
||||||
@ -367,8 +367,8 @@ Module.register("newsfeed", {
|
|||||||
if (this.config.showFullArticle === true) {
|
if (this.config.showFullArticle === true) {
|
||||||
this.scrollPosition += this.config.scrollLength;
|
this.scrollPosition += this.config.scrollLength;
|
||||||
window.scrollTo(0, this.scrollPosition);
|
window.scrollTo(0, this.scrollPosition);
|
||||||
Log.info(this.name + " - scrolling down");
|
Log.debug(this.name + " - scrolling down");
|
||||||
Log.info(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength);
|
Log.debug(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength);
|
||||||
} else {
|
} else {
|
||||||
this.showFullArticle();
|
this.showFullArticle();
|
||||||
}
|
}
|
||||||
@ -376,12 +376,12 @@ Module.register("newsfeed", {
|
|||||||
if (this.config.showFullArticle === true) {
|
if (this.config.showFullArticle === true) {
|
||||||
this.scrollPosition -= this.config.scrollLength;
|
this.scrollPosition -= this.config.scrollLength;
|
||||||
window.scrollTo(0, this.scrollPosition);
|
window.scrollTo(0, this.scrollPosition);
|
||||||
Log.info(this.name + " - scrolling up");
|
Log.debug(this.name + " - scrolling up");
|
||||||
Log.info(this.name + " - ARTICLE_SCROLL_UP, scroll position: " + this.config.scrollLength);
|
Log.debug(this.name + " - ARTICLE_SCROLL_UP, scroll position: " + this.config.scrollLength);
|
||||||
}
|
}
|
||||||
} else if (notification === "ARTICLE_LESS_DETAILS") {
|
} else if (notification === "ARTICLE_LESS_DETAILS") {
|
||||||
this.resetDescrOrFullArticleAndTimer();
|
this.resetDescrOrFullArticleAndTimer();
|
||||||
Log.info(this.name + " - showing only article titles again");
|
Log.debug(this.name + " - showing only article titles again");
|
||||||
this.updateDom(100);
|
this.updateDom(100);
|
||||||
} else if (notification === "ARTICLE_TOGGLE_FULL") {
|
} else if (notification === "ARTICLE_TOGGLE_FULL") {
|
||||||
if (this.config.showFullArticle) {
|
if (this.config.showFullArticle) {
|
||||||
@ -411,7 +411,7 @@ Module.register("newsfeed", {
|
|||||||
}
|
}
|
||||||
clearInterval(this.timer);
|
clearInterval(this.timer);
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
Log.info(this.name + " - showing " + this.isShowingDescription ? "article description" : "full article");
|
Log.debug(this.name + " - showing " + this.isShowingDescription ? "article description" : "full article");
|
||||||
this.updateDom(100);
|
this.updateDom(100);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,11 @@
|
|||||||
{% if config.useBeaufort %}
|
{% if config.useBeaufort %}
|
||||||
{{ current.beaufortWindSpeed() | round }}
|
{{ current.beaufortWindSpeed() | round }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ current.windSpeed | round }}
|
{% if config.useKmh %}
|
||||||
|
{{ current.kmhWindSpeed() | round }}
|
||||||
|
{% else %}
|
||||||
|
{{ current.windSpeed | round }}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if config.showWindDirection %}
|
{% if config.showWindDirection %}
|
||||||
<sup>
|
<sup>
|
||||||
@ -65,7 +69,10 @@
|
|||||||
<div class="normal medium">
|
<div class="normal medium">
|
||||||
{% if config.showFeelsLike %}
|
{% if config.showFeelsLike %}
|
||||||
<span class="dimmed">
|
<span class="dimmed">
|
||||||
{{ "FEELS" | translate }} {{ current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }}
|
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
|
||||||
|
{% if not config.feelsLikeWithDegree %}
|
||||||
|
{{ current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }}
|
||||||
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if config.showPrecipitationAmount %}
|
{% if config.showPrecipitationAmount %}
|
||||||
|
@ -10,7 +10,13 @@
|
|||||||
{% set forecast = forecast.slice(0, numSteps) %}
|
{% set forecast = forecast.slice(0, numSteps) %}
|
||||||
{% for f in forecast %}
|
{% for f in forecast %}
|
||||||
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
|
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
|
||||||
<td class="day">{{ f.date.format('ddd') }}</td>
|
{% if (currentStep == 0) %}
|
||||||
|
<td class="day">{{ "TODAY" | translate }}</td>
|
||||||
|
{% elif (currentStep == 1) %}
|
||||||
|
<td class="day">{{ "TOMORROW" | translate }}</td>
|
||||||
|
{% else %}
|
||||||
|
<td class="day">{{ f.date.format('ddd') }}</td>
|
||||||
|
{% endif %}
|
||||||
<td class="bright weather-icon"><span class="wi weathericon wi-{{ f.weatherType }}"></span></td>
|
<td class="bright weather-icon"><span class="wi weathericon wi-{{ f.weatherType }}"></span></td>
|
||||||
<td class="align-right bright max-temp">
|
<td class="align-right bright max-temp">
|
||||||
{{ f.maxTemperature | roundValue | unit("temperature") }}
|
{{ f.maxTemperature | roundValue | unit("temperature") }}
|
||||||
|
@ -62,7 +62,7 @@ WeatherProvider.register("darksky", {
|
|||||||
|
|
||||||
// Implement WeatherDay generator.
|
// Implement WeatherDay generator.
|
||||||
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
currentWeather.date = moment();
|
currentWeather.date = moment();
|
||||||
currentWeather.humidity = parseFloat(currentWeatherData.currently.humidity);
|
currentWeather.humidity = parseFloat(currentWeatherData.currently.humidity);
|
||||||
@ -80,7 +80,7 @@ WeatherProvider.register("darksky", {
|
|||||||
const days = [];
|
const days = [];
|
||||||
|
|
||||||
for (const forecast of forecasts) {
|
for (const forecast of forecasts) {
|
||||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
weather.date = moment(forecast.time, "X");
|
weather.date = moment(forecast.time, "X");
|
||||||
weather.minTemperature = forecast.temperatureMin;
|
weather.minTemperature = forecast.temperatureMin;
|
||||||
|
@ -89,11 +89,15 @@ WeatherProvider.register("openweathermap", {
|
|||||||
* Generate a WeatherObject based on currentWeatherInformation
|
* Generate a WeatherObject based on currentWeatherInformation
|
||||||
*/
|
*/
|
||||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
currentWeather.humidity = currentWeatherData.main.humidity;
|
currentWeather.humidity = currentWeatherData.main.humidity;
|
||||||
currentWeather.temperature = currentWeatherData.main.temp;
|
currentWeather.temperature = currentWeatherData.main.temp;
|
||||||
currentWeather.windSpeed = currentWeatherData.wind.speed;
|
if (this.config.windUnits === "metric") {
|
||||||
|
currentWeather.windSpeed = this.config.useKmh ? currentWeatherData.wind.speed * 3.6 : currentWeatherData.wind.speed;
|
||||||
|
} else {
|
||||||
|
currentWeather.windSpeed = currentWeatherData.wind.speed;
|
||||||
|
}
|
||||||
currentWeather.windDirection = currentWeatherData.wind.deg;
|
currentWeather.windDirection = currentWeatherData.wind.deg;
|
||||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon);
|
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon);
|
||||||
currentWeather.sunrise = moment(currentWeatherData.sys.sunrise, "X");
|
currentWeather.sunrise = moment(currentWeatherData.sys.sunrise, "X");
|
||||||
@ -112,7 +116,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
return this.fetchForecastDaily(forecasts);
|
return this.fetchForecastDaily(forecasts);
|
||||||
}
|
}
|
||||||
// if weatherEndpoint does not match forecast or forecast/daily, what should be returned?
|
// if weatherEndpoint does not match forecast or forecast/daily, what should be returned?
|
||||||
const days = [new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits)];
|
const days = [new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh)];
|
||||||
return days;
|
return days;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -124,7 +128,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
return this.fetchOnecall(data);
|
return this.fetchOnecall(data);
|
||||||
}
|
}
|
||||||
// if weatherEndpoint does not match onecall, what should be returned?
|
// if weatherEndpoint does not match onecall, what should be returned?
|
||||||
const weatherData = { current: new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits), hours: [], days: [] };
|
const weatherData = { current: new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh), hours: [], days: [] };
|
||||||
return weatherData;
|
return weatherData;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -141,7 +145,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
let snow = 0;
|
let snow = 0;
|
||||||
// variable for date
|
// variable for date
|
||||||
let date = "";
|
let date = "";
|
||||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
for (const forecast of forecasts) {
|
for (const forecast of forecasts) {
|
||||||
if (date !== moment(forecast.dt, "X").format("YYYY-MM-DD")) {
|
if (date !== moment(forecast.dt, "X").format("YYYY-MM-DD")) {
|
||||||
@ -154,7 +158,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
// push weather information to days array
|
// push weather information to days array
|
||||||
days.push(weather);
|
days.push(weather);
|
||||||
// create new weather-object
|
// create new weather-object
|
||||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
minTemp = [];
|
minTemp = [];
|
||||||
maxTemp = [];
|
maxTemp = [];
|
||||||
@ -217,7 +221,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
const days = [];
|
const days = [];
|
||||||
|
|
||||||
for (const forecast of forecasts) {
|
for (const forecast of forecasts) {
|
||||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
weather.date = moment(forecast.dt, "X");
|
weather.date = moment(forecast.dt, "X");
|
||||||
weather.minTemperature = forecast.temp.min;
|
weather.minTemperature = forecast.temp.min;
|
||||||
@ -263,7 +267,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
let precip = false;
|
let precip = false;
|
||||||
|
|
||||||
// get current weather, if requested
|
// get current weather, if requested
|
||||||
const current = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const current = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
if (data.hasOwnProperty("current")) {
|
if (data.hasOwnProperty("current")) {
|
||||||
current.date = moment(data.current.dt, "X").utcOffset(data.timezone_offset / 60);
|
current.date = moment(data.current.dt, "X").utcOffset(data.timezone_offset / 60);
|
||||||
current.windSpeed = data.current.wind_speed;
|
current.windSpeed = data.current.wind_speed;
|
||||||
@ -295,7 +299,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
current.feelsLikeTemp = data.current.feels_like;
|
current.feelsLikeTemp = data.current.feels_like;
|
||||||
}
|
}
|
||||||
|
|
||||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
// get hourly weather, if requested
|
// get hourly weather, if requested
|
||||||
const hours = [];
|
const hours = [];
|
||||||
@ -331,7 +335,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hours.push(weather);
|
hours.push(weather);
|
||||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,7 +374,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
days.push(weather);
|
days.push(weather);
|
||||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
309
modules/default/weather/providers/smhi.js
Normal file
309
modules/default/weather/providers/smhi.js
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
/* global WeatherProvider, WeatherObject, SunCalc */
|
||||||
|
|
||||||
|
/* Magic Mirror
|
||||||
|
* Module: Weather
|
||||||
|
* Provider: SMHI
|
||||||
|
*
|
||||||
|
* By BuXXi https://github.com/buxxi
|
||||||
|
* MIT Licensed
|
||||||
|
*
|
||||||
|
* This class is a provider for SMHI (Sweden only).
|
||||||
|
* Note that SMHI doesn't provide sunrise and sundown, use SunCalc to calculate it.
|
||||||
|
* Metric system is the only supported unit.
|
||||||
|
*/
|
||||||
|
WeatherProvider.register("smhi", {
|
||||||
|
providerName: "SMHI",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements method in interface for fetching current weather
|
||||||
|
*/
|
||||||
|
fetchCurrentWeather() {
|
||||||
|
this.fetchData(this.getURL())
|
||||||
|
.then((data) => {
|
||||||
|
let closest = this.getClosestToCurrentTime(data.timeSeries);
|
||||||
|
let coordinates = this.resolveCoordinates(data);
|
||||||
|
let weatherObject = this.convertWeatherDataToObject(closest, coordinates);
|
||||||
|
this.setFetchedLocation(`(${coordinates.lat},${coordinates.lon})`);
|
||||||
|
this.setCurrentWeather(weatherObject);
|
||||||
|
})
|
||||||
|
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||||
|
.finally(() => this.updateAvailable());
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements method in interface for fetching a forecast.
|
||||||
|
* Handling hourly forecast would be easy as not grouping by day but it seems really specific for one weather provider for now.
|
||||||
|
*/
|
||||||
|
fetchWeatherForecast() {
|
||||||
|
this.fetchData(this.getURL())
|
||||||
|
.then((data) => {
|
||||||
|
let coordinates = this.resolveCoordinates(data);
|
||||||
|
let weatherObjects = this.convertWeatherDataGroupedByDay(data.timeSeries, coordinates);
|
||||||
|
this.setFetchedLocation(`(${coordinates.lat},${coordinates.lon})`);
|
||||||
|
this.setWeatherForecast(weatherObjects);
|
||||||
|
})
|
||||||
|
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||||
|
.finally(() => this.updateAvailable());
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides method for setting config with checks for the precipitationValue being unset or invalid
|
||||||
|
*
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
setConfig(config) {
|
||||||
|
this.config = config;
|
||||||
|
if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) == -1) {
|
||||||
|
console.log("invalid or not set: " + config.precipitationValue);
|
||||||
|
config.precipitationValue = "pmedian";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Of all the times returned find out which one is closest to the current time, should be the first if the data isn't old.
|
||||||
|
*
|
||||||
|
* @param times
|
||||||
|
*/
|
||||||
|
getClosestToCurrentTime(times) {
|
||||||
|
let now = moment();
|
||||||
|
let minDiff = undefined;
|
||||||
|
for (time of times) {
|
||||||
|
let diff = Math.abs(moment(time.validTime).diff(now));
|
||||||
|
if (!minDiff || diff < Math.abs(moment(minDiff.validTime).diff(now))) {
|
||||||
|
minDiff = time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return minDiff;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the forecast url for the configured coordinates
|
||||||
|
*/
|
||||||
|
getURL() {
|
||||||
|
let lon = this.config.lon;
|
||||||
|
let lat = this.config.lat;
|
||||||
|
return `https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/${lon}/lat/${lat}/data.json`;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the returned data into a WeatherObject with required properties set for both current weather and forecast.
|
||||||
|
* The returned units is always in metric system.
|
||||||
|
* Requires coordinates to determine if its daytime or nighttime to know which icon to use and also to set sunrise and sunset.
|
||||||
|
*
|
||||||
|
* @param weatherData
|
||||||
|
* @param coordinates
|
||||||
|
* @param weatherData
|
||||||
|
* @param coordinates
|
||||||
|
*/
|
||||||
|
convertWeatherDataToObject(weatherData, coordinates) {
|
||||||
|
let currentWeather = new WeatherObject("metric", "metric", "metric"); //Weather data is only for Sweden and nobody in Sweden would use imperial
|
||||||
|
|
||||||
|
currentWeather.date = moment(weatherData.validTime);
|
||||||
|
let times = SunCalc.getTimes(currentWeather.date.toDate(), coordinates.lat, coordinates.lon);
|
||||||
|
currentWeather.sunrise = moment(times.sunrise, "X");
|
||||||
|
currentWeather.sunset = moment(times.sunset, "X");
|
||||||
|
currentWeather.humidity = this.paramValue(weatherData, "r");
|
||||||
|
currentWeather.temperature = this.paramValue(weatherData, "t");
|
||||||
|
currentWeather.windSpeed = this.paramValue(weatherData, "ws");
|
||||||
|
currentWeather.windDirection = this.paramValue(weatherData, "wd");
|
||||||
|
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), this.isDayTime(currentWeather));
|
||||||
|
|
||||||
|
//Determine the precipitation amount and category and update the weatherObject with it, the valuetype to use can be configured or uses median as default.
|
||||||
|
let precipitationValue = this.paramValue(weatherData, this.config.precipitationValue);
|
||||||
|
switch (this.paramValue(weatherData, "pcat")) {
|
||||||
|
// 0 = No precipitation
|
||||||
|
case 1: // Snow
|
||||||
|
currentWeather.snow += precipitationValue;
|
||||||
|
currentWeather.precipitation += precipitationValue;
|
||||||
|
break;
|
||||||
|
case 2: // Snow and rain, treat it as 50/50 snow and rain
|
||||||
|
currentWeather.snow += precipitationValue / 2;
|
||||||
|
currentWeather.rain += precipitationValue / 2;
|
||||||
|
currentWeather.precipitation += precipitationValue;
|
||||||
|
break;
|
||||||
|
case 3: // Rain
|
||||||
|
case 4: // Drizzle
|
||||||
|
case 5: // Freezing rain
|
||||||
|
case 6: // Freezing drizzle
|
||||||
|
currentWeather.rain += precipitationValue;
|
||||||
|
currentWeather.precipitation += precipitationValue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentWeather;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes all of the data points and converts it to one WeatherObject per day.
|
||||||
|
*
|
||||||
|
* @param allWeatherData
|
||||||
|
* @param coordinates
|
||||||
|
* @param allWeatherData
|
||||||
|
* @param coordinates
|
||||||
|
*/
|
||||||
|
convertWeatherDataGroupedByDay(allWeatherData, coordinates) {
|
||||||
|
var currentWeather;
|
||||||
|
let result = [];
|
||||||
|
|
||||||
|
let allWeatherObjects = this.fillInGaps(allWeatherData).map((weatherData) => this.convertWeatherDataToObject(weatherData, coordinates));
|
||||||
|
var dayWeatherTypes = [];
|
||||||
|
|
||||||
|
for (weatherObject of allWeatherObjects) {
|
||||||
|
//If its the first object or if a day change we need to reset the summary object
|
||||||
|
if (!currentWeather || !currentWeather.date.isSame(weatherObject.date, "day")) {
|
||||||
|
currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||||
|
dayWeatherTypes = [];
|
||||||
|
currentWeather.date = weatherObject.date;
|
||||||
|
currentWeather.minTemperature = Infinity;
|
||||||
|
currentWeather.maxTemperature = -Infinity;
|
||||||
|
currentWeather.snow = 0;
|
||||||
|
currentWeather.rain = 0;
|
||||||
|
currentWeather.precipitation = 0;
|
||||||
|
result.push(currentWeather);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Keep track of what icons has been used for each hour of daytime and use the middle one for the forecast
|
||||||
|
if (this.isDayTime(weatherObject)) {
|
||||||
|
dayWeatherTypes.push(weatherObject.weatherType);
|
||||||
|
}
|
||||||
|
if (dayWeatherTypes.length > 0) {
|
||||||
|
currentWeather.weatherType = dayWeatherTypes[Math.floor(dayWeatherTypes.length / 2)];
|
||||||
|
} else {
|
||||||
|
currentWeather.weatherType = weatherObject.weatherType;
|
||||||
|
}
|
||||||
|
|
||||||
|
//All other properties is either a sum, min or max of each hour
|
||||||
|
currentWeather.minTemperature = Math.min(currentWeather.minTemperature, weatherObject.temperature);
|
||||||
|
currentWeather.maxTemperature = Math.max(currentWeather.maxTemperature, weatherObject.temperature);
|
||||||
|
currentWeather.snow += weatherObject.snow;
|
||||||
|
currentWeather.rain += weatherObject.rain;
|
||||||
|
currentWeather.precipitation += weatherObject.precipitation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve coordinates from the response data (probably preferably to use this if it's not matching the config values exactly)
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
resolveCoordinates(data) {
|
||||||
|
return { lat: data.geometry.coordinates[0][1], lon: data.geometry.coordinates[0][0] };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the weatherObject is at dayTime.
|
||||||
|
*
|
||||||
|
* @param weatherObject
|
||||||
|
*/
|
||||||
|
isDayTime(weatherObject) {
|
||||||
|
return weatherObject.date.isBetween(weatherObject.sunrise, weatherObject.sunset, undefined, "[]");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The distance between the data points is increasing in the data the more distant the prediction is.
|
||||||
|
* Find these gaps and fill them with the previous hours data to make the data returned a complete set.
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
fillInGaps(data) {
|
||||||
|
let result = [];
|
||||||
|
for (var i = 1; i < data.length; i++) {
|
||||||
|
let to = moment(data[i].validTime);
|
||||||
|
let from = moment(data[i - 1].validTime);
|
||||||
|
let hours = moment.duration(to.diff(from)).asHours();
|
||||||
|
// For each hour add a datapoint but change the validTime
|
||||||
|
for (var j = 0; j < hours; j++) {
|
||||||
|
let current = Object.assign({}, data[i]);
|
||||||
|
current.validTime = from.clone().add(j, "hours").toISOString();
|
||||||
|
result.push(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to fetch a property from the returned data set.
|
||||||
|
* The returned values is an array with always one value in it.
|
||||||
|
*
|
||||||
|
* @param currentWeatherData
|
||||||
|
* @param name
|
||||||
|
* @param currentWeatherData
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
paramValue(currentWeatherData, name) {
|
||||||
|
return currentWeatherData.parameters.filter((p) => p.name == name).flatMap((p) => p.values)[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map the icon value from SHMI to an icon that MagicMirror understands.
|
||||||
|
* Uses different icons depending if its daytime or nighttime.
|
||||||
|
* SHMI's description of what the numeric value means is the comment after the case.
|
||||||
|
*
|
||||||
|
* @param input
|
||||||
|
* @param isDayTime
|
||||||
|
* @param input
|
||||||
|
* @param isDayTime
|
||||||
|
*/
|
||||||
|
convertWeatherType(input, isDayTime) {
|
||||||
|
switch (input) {
|
||||||
|
case 1:
|
||||||
|
return isDayTime ? "day-sunny" : "night-clear"; // Clear sky
|
||||||
|
case 2:
|
||||||
|
return isDayTime ? "day-sunny-overcast" : "night-partly-cloudy"; //Nearly clear sky
|
||||||
|
case 3:
|
||||||
|
return isDayTime ? "day-cloudy" : "night-cloudy"; //Variable cloudiness
|
||||||
|
case 4:
|
||||||
|
return isDayTime ? "day-cloudy" : "night-cloudy"; //Halfclear sky
|
||||||
|
case 5:
|
||||||
|
return "cloudy"; //Cloudy sky
|
||||||
|
case 6:
|
||||||
|
return "cloudy"; //Overcast
|
||||||
|
case 7:
|
||||||
|
return "fog"; //Fog
|
||||||
|
case 8:
|
||||||
|
return "showers"; //Light rain showers
|
||||||
|
case 9:
|
||||||
|
return "showers"; //Moderate rain showers
|
||||||
|
case 10:
|
||||||
|
return "showers"; //Heavy rain showers
|
||||||
|
case 11:
|
||||||
|
return "thunderstorm"; //Thunderstorm
|
||||||
|
case 12:
|
||||||
|
return "sleet"; //Light sleet showers
|
||||||
|
case 13:
|
||||||
|
return "sleet"; //Moderate sleet showers
|
||||||
|
case 14:
|
||||||
|
return "sleet"; //Heavy sleet showers
|
||||||
|
case 15:
|
||||||
|
return "snow"; //Light snow showers
|
||||||
|
case 16:
|
||||||
|
return "snow"; //Moderate snow showers
|
||||||
|
case 17:
|
||||||
|
return "snow"; //Heavy snow showers
|
||||||
|
case 18:
|
||||||
|
return "rain"; //Light rain
|
||||||
|
case 19:
|
||||||
|
return "rain"; //Moderate rain
|
||||||
|
case 20:
|
||||||
|
return "rain"; //Heavy rain
|
||||||
|
case 21:
|
||||||
|
return "thunderstorm"; //Thunder
|
||||||
|
case 22:
|
||||||
|
return "sleet"; // Light sleet
|
||||||
|
case 23:
|
||||||
|
return "sleet"; //Moderate sleet
|
||||||
|
case 24:
|
||||||
|
return "sleet"; // Heavy sleet
|
||||||
|
case 25:
|
||||||
|
return "snow"; // Light snowfall
|
||||||
|
case 26:
|
||||||
|
return "snow"; //Moderate snowfall
|
||||||
|
case 27:
|
||||||
|
return "snow"; //Heavy snowfall
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -73,7 +73,7 @@ WeatherProvider.register("ukmetoffice", {
|
|||||||
* Generate a WeatherObject based on currentWeatherInformation
|
* Generate a WeatherObject based on currentWeatherInformation
|
||||||
*/
|
*/
|
||||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
// data times are always UTC
|
// data times are always UTC
|
||||||
let nowUtc = moment.utc();
|
let nowUtc = moment.utc();
|
||||||
@ -124,7 +124,7 @@ WeatherProvider.register("ukmetoffice", {
|
|||||||
// loop round the (5) periods getting the data
|
// loop round the (5) periods getting the data
|
||||||
// for each period array, Day is [0], Night is [1]
|
// for each period array, Day is [0], Night is [1]
|
||||||
for (var j in forecasts.SiteRep.DV.Location.Period) {
|
for (var j in forecasts.SiteRep.DV.Location.Period) {
|
||||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
// data times are always UTC
|
// data times are always UTC
|
||||||
const dateStr = forecasts.SiteRep.DV.Location.Period[j].value;
|
const dateStr = forecasts.SiteRep.DV.Location.Period[j].value;
|
||||||
@ -208,10 +208,10 @@ WeatherProvider.register("ukmetoffice", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Convert wind speed (from mph) if required
|
* Convert wind speed (from mph to m/s or km/h) if required
|
||||||
*/
|
*/
|
||||||
convertWindSpeed(windInMph) {
|
convertWindSpeed(windInMph) {
|
||||||
return this.windUnits === "metric" ? windInMph * 2.23694 : windInMph;
|
return this.windUnits === "metric" ? (this.useKmh ? windInMph * 1.60934 : windInMph / 2.23694) : windInMph;
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -87,7 +87,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
|||||||
// Did not receive usable new data.
|
// Did not receive usable new data.
|
||||||
// Maybe this needs a better check?
|
// Maybe this needs a better check?
|
||||||
Log.error("Possibly bad current/hourly data?");
|
Log.error("Possibly bad current/hourly data?");
|
||||||
Log.info(data);
|
Log.error(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
|||||||
|
|
||||||
// Create a WeatherObject using current weather data (data for the current hour)
|
// Create a WeatherObject using current weather data (data for the current hour)
|
||||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
// Extract the actual forecasts
|
// Extract the actual forecasts
|
||||||
let forecastDataHours = currentWeatherData.features[0].properties.timeSeries;
|
let forecastDataHours = currentWeatherData.features[0].properties.timeSeries;
|
||||||
@ -158,7 +158,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
|||||||
// Did not receive usable new data.
|
// Did not receive usable new data.
|
||||||
// Maybe this needs a better check?
|
// Maybe this needs a better check?
|
||||||
Log.error("Possibly bad forecast data?");
|
Log.error("Possibly bad forecast data?");
|
||||||
Log.info(data);
|
Log.error(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,7 +189,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
|||||||
|
|
||||||
// Go through each day in the forecasts
|
// Go through each day in the forecasts
|
||||||
for (day in forecastDataDays) {
|
for (day in forecastDataDays) {
|
||||||
const forecastWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const forecastWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
// Get date of forecast
|
// Get date of forecast
|
||||||
let forecastDate = moment.utc(forecastDataDays[day].time);
|
let forecastDate = moment.utc(forecastDataDays[day].time);
|
||||||
@ -254,7 +254,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
|||||||
return windInMpS;
|
return windInMpS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.config.windUnits == "kph" || this.config.windUnits == "metric") {
|
if (this.config.windUnits == "kph" || this.config.windUnits == "metric" || this.config.useKmh) {
|
||||||
return windInMpS * 3.6;
|
return windInMpS * 3.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
181
modules/default/weather/providers/weatherbit.js
Normal file
181
modules/default/weather/providers/weatherbit.js
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
/* global WeatherProvider, WeatherObject */
|
||||||
|
|
||||||
|
/* Magic Mirror
|
||||||
|
* Module: Weather
|
||||||
|
* Provider: Weatherbit
|
||||||
|
*
|
||||||
|
* By Andrew Pometti
|
||||||
|
* MIT Licensed
|
||||||
|
*
|
||||||
|
* This class is a provider for Weatherbit, based on Nicholas Hubbard's class for Dark Sky & Vince Peri's class for Weather.gov.
|
||||||
|
*/
|
||||||
|
WeatherProvider.register("weatherbit", {
|
||||||
|
// Set the name of the provider.
|
||||||
|
// Not strictly required, but helps for debugging.
|
||||||
|
providerName: "Weatherbit",
|
||||||
|
|
||||||
|
units: {
|
||||||
|
imperial: "I",
|
||||||
|
metric: "M"
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchedLocation: function () {
|
||||||
|
return this.fetchedLocationName || "";
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchCurrentWeather() {
|
||||||
|
this.fetchData(this.getUrl())
|
||||||
|
.then((data) => {
|
||||||
|
if (!data || !data.data[0] || typeof data.data[0].temp === "undefined") {
|
||||||
|
// No usable data?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentWeather = this.generateWeatherDayFromCurrentWeather(data);
|
||||||
|
this.setCurrentWeather(currentWeather);
|
||||||
|
})
|
||||||
|
.catch(function (request) {
|
||||||
|
Log.error("Could not load data ... ", request);
|
||||||
|
})
|
||||||
|
.finally(() => this.updateAvailable());
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchWeatherForecast() {
|
||||||
|
this.fetchData(this.getUrl())
|
||||||
|
.then((data) => {
|
||||||
|
if (!data || !data.data) {
|
||||||
|
// No usable data?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const forecast = this.generateWeatherObjectsFromForecast(data.data);
|
||||||
|
this.setWeatherForecast(forecast);
|
||||||
|
|
||||||
|
this.fetchedLocationName = data.city_name + ", " + data.state_code;
|
||||||
|
})
|
||||||
|
.catch(function (request) {
|
||||||
|
Log.error("Could not load data ... ", request);
|
||||||
|
})
|
||||||
|
.finally(() => this.updateAvailable());
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create a URL from the config and base URL.
|
||||||
|
getUrl() {
|
||||||
|
const units = this.units[this.config.units] || "auto";
|
||||||
|
return `${this.config.apiBase}${this.config.weatherEndpoint}?lat=${this.config.lat}&lon=${this.config.lon}&units=${units}&key=${this.config.apiKey}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Implement WeatherDay generator.
|
||||||
|
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
||||||
|
//Calculate TZ Offset and invert to convert Sunrise/Sunset times to Local
|
||||||
|
const d = new Date();
|
||||||
|
let tzOffset = d.getTimezoneOffset();
|
||||||
|
tzOffset = tzOffset * -1;
|
||||||
|
|
||||||
|
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||||
|
|
||||||
|
currentWeather.date = moment(currentWeatherData.data[0].ts, "X");
|
||||||
|
currentWeather.humidity = parseFloat(currentWeatherData.data[0].rh);
|
||||||
|
currentWeather.temperature = parseFloat(currentWeatherData.data[0].temp);
|
||||||
|
currentWeather.windSpeed = parseFloat(currentWeatherData.data[0].wind_spd);
|
||||||
|
currentWeather.windDirection = currentWeatherData.data[0].wind_dir;
|
||||||
|
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.data[0].weather.icon);
|
||||||
|
Log.log("Wx Icon: " + currentWeatherData.data[0].weather.icon);
|
||||||
|
currentWeather.sunrise = moment(currentWeatherData.data[0].sunrise, "HH:mm").add(tzOffset, "m");
|
||||||
|
currentWeather.sunset = moment(currentWeatherData.data[0].sunset, "HH:mm").add(tzOffset, "m");
|
||||||
|
|
||||||
|
this.fetchedLocationName = currentWeatherData.data[0].city_name + ", " + currentWeatherData.data[0].state_code;
|
||||||
|
|
||||||
|
return currentWeather;
|
||||||
|
},
|
||||||
|
|
||||||
|
generateWeatherObjectsFromForecast(forecasts) {
|
||||||
|
const days = [];
|
||||||
|
|
||||||
|
for (const forecast of forecasts) {
|
||||||
|
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||||
|
|
||||||
|
weather.date = moment(forecast.datetime, "YYYY-MM-DD");
|
||||||
|
weather.minTemperature = forecast.min_temp;
|
||||||
|
weather.maxTemperature = forecast.max_temp;
|
||||||
|
weather.precipitation = forecast.precip;
|
||||||
|
weather.weatherType = this.convertWeatherType(forecast.weather.icon);
|
||||||
|
|
||||||
|
days.push(weather);
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Map icons from Dark Sky to our icons.
|
||||||
|
convertWeatherType(weatherType) {
|
||||||
|
const weatherTypes = {
|
||||||
|
t01d: "day-thunderstorm",
|
||||||
|
t01n: "night-alt-thunderstorm",
|
||||||
|
t02d: "day-thunderstorm",
|
||||||
|
t02n: "night-alt-thunderstorm",
|
||||||
|
t03d: "thunderstorm",
|
||||||
|
t03n: "thunderstorm",
|
||||||
|
t04d: "day-thunderstorm",
|
||||||
|
t04n: "night-alt-thunderstorm",
|
||||||
|
t05d: "day-sleet-storm",
|
||||||
|
t05n: "night-alt-sleet-storm",
|
||||||
|
d01d: "day-sprinkle",
|
||||||
|
d01n: "night-alt-sprinkle",
|
||||||
|
d02d: "day-sprinkle",
|
||||||
|
d02n: "night-alt-sprinkle",
|
||||||
|
d03d: "day-shower",
|
||||||
|
d03n: "night-alt-shower",
|
||||||
|
r01d: "day-shower",
|
||||||
|
r01n: "night-alt-shower",
|
||||||
|
r02d: "day-rain",
|
||||||
|
r02n: "night-alt-rain",
|
||||||
|
r03d: "day-rain",
|
||||||
|
r03n: "night-alt-rain",
|
||||||
|
r04d: "day-sprinkle",
|
||||||
|
r04n: "night-alt-sprinkle",
|
||||||
|
r05d: "day-shower",
|
||||||
|
r05n: "night-alt-shower",
|
||||||
|
r06d: "day-shower",
|
||||||
|
r06n: "night-alt-shower",
|
||||||
|
f01d: "day-sleet",
|
||||||
|
f01n: "night-alt-sleet",
|
||||||
|
s01d: "day-snow",
|
||||||
|
s01n: "night-alt-snow",
|
||||||
|
s02d: "day-snow-wind",
|
||||||
|
s02n: "night-alt-snow-wind",
|
||||||
|
s03d: "snowflake-cold",
|
||||||
|
s03n: "snowflake-cold",
|
||||||
|
s04d: "day-rain-mix",
|
||||||
|
s04n: "night-alt-rain-mix",
|
||||||
|
s05d: "day-sleet",
|
||||||
|
s05n: "night-alt-sleet",
|
||||||
|
s06d: "day-snow",
|
||||||
|
s06n: "night-alt-snow",
|
||||||
|
a01d: "day-haze",
|
||||||
|
a01n: "dust",
|
||||||
|
a02d: "smoke",
|
||||||
|
a02n: "smoke",
|
||||||
|
a03d: "day-haze",
|
||||||
|
a03n: "dust",
|
||||||
|
a04d: "dust",
|
||||||
|
a04n: "dust",
|
||||||
|
a05d: "day-fog",
|
||||||
|
a05n: "night-fog",
|
||||||
|
a06d: "fog",
|
||||||
|
a06n: "fog",
|
||||||
|
c01d: "day-sunny",
|
||||||
|
c01n: "night-clear",
|
||||||
|
c02d: "day-sunny-overcast",
|
||||||
|
c02n: "night-alt-partly-cloudy",
|
||||||
|
c03d: "day-cloudy",
|
||||||
|
c03n: "night-alt-cloudy",
|
||||||
|
c04d: "cloudy",
|
||||||
|
c04n: "cloudy",
|
||||||
|
u00d: "rain-mix",
|
||||||
|
u00n: "rain-mix"
|
||||||
|
};
|
||||||
|
|
||||||
|
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||||
|
}
|
||||||
|
});
|
@ -131,11 +131,11 @@ WeatherProvider.register("weathergov", {
|
|||||||
* ... object needs data in units based on config!
|
* ... object needs data in units based on config!
|
||||||
*/
|
*/
|
||||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
currentWeather.date = moment(currentWeatherData.timestamp);
|
currentWeather.date = moment(currentWeatherData.timestamp);
|
||||||
currentWeather.temperature = this.convertTemp(currentWeatherData.temperature.value);
|
currentWeather.temperature = this.convertTemp(currentWeatherData.temperature.value);
|
||||||
currentWeather.windSpeed = this.covertSpeed(currentWeatherData.windSpeed.value);
|
currentWeather.windSpeed = this.convertSpeed(currentWeatherData.windSpeed.value);
|
||||||
currentWeather.windDirection = currentWeatherData.windDirection.value;
|
currentWeather.windDirection = currentWeatherData.windDirection.value;
|
||||||
currentWeather.minTemperature = this.convertTemp(currentWeatherData.minTemperatureLast24Hours.value);
|
currentWeather.minTemperature = this.convertTemp(currentWeatherData.minTemperatureLast24Hours.value);
|
||||||
currentWeather.maxTemperature = this.convertTemp(currentWeatherData.maxTemperatureLast24Hours.value);
|
currentWeather.maxTemperature = this.convertTemp(currentWeatherData.maxTemperatureLast24Hours.value);
|
||||||
@ -179,7 +179,7 @@ WeatherProvider.register("weathergov", {
|
|||||||
let maxTemp = [];
|
let maxTemp = [];
|
||||||
// variable for date
|
// variable for date
|
||||||
let date = "";
|
let date = "";
|
||||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
weather.precipitation = 0;
|
weather.precipitation = 0;
|
||||||
|
|
||||||
for (const forecast of forecasts) {
|
for (const forecast of forecasts) {
|
||||||
@ -191,7 +191,7 @@ WeatherProvider.register("weathergov", {
|
|||||||
// push weather information to days array
|
// push weather information to days array
|
||||||
days.push(weather);
|
days.push(weather);
|
||||||
// create new weather-object
|
// create new weather-object
|
||||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
minTemp = [];
|
minTemp = [];
|
||||||
maxTemp = [];
|
maxTemp = [];
|
||||||
@ -238,12 +238,16 @@ WeatherProvider.register("weathergov", {
|
|||||||
return temp;
|
return temp;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// conversion to mph
|
// conversion to mph or kmh
|
||||||
covertSpeed(metSec) {
|
convertSpeed(metSec) {
|
||||||
if (this.config.windUnits === "imperial") {
|
if (this.config.windUnits === "imperial") {
|
||||||
return metSec * 2.23694;
|
return metSec * 2.23694;
|
||||||
} else {
|
} else {
|
||||||
return metSec;
|
if (this.config.useKmh) {
|
||||||
|
return metSec * 3.6;
|
||||||
|
} else {
|
||||||
|
return metSec;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// conversion to inches
|
// conversion to inches
|
||||||
|
@ -12,16 +12,14 @@ Module.register("weather", {
|
|||||||
weatherProvider: "openweathermap",
|
weatherProvider: "openweathermap",
|
||||||
roundTemp: false,
|
roundTemp: false,
|
||||||
type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint)
|
type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint)
|
||||||
|
|
||||||
lat: 0,
|
lat: 0,
|
||||||
lon: 0,
|
lon: 0,
|
||||||
location: false,
|
location: false,
|
||||||
locationID: false,
|
locationID: false,
|
||||||
units: config.units,
|
units: config.units,
|
||||||
|
useKmh: false,
|
||||||
tempUnits: config.units,
|
tempUnits: config.units,
|
||||||
windUnits: config.units,
|
windUnits: config.units,
|
||||||
|
|
||||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||||
animationSpeed: 1000,
|
animationSpeed: 1000,
|
||||||
timeFormat: config.timeFormat,
|
timeFormat: config.timeFormat,
|
||||||
@ -41,24 +39,21 @@ Module.register("weather", {
|
|||||||
maxEntries: 5,
|
maxEntries: 5,
|
||||||
fade: true,
|
fade: true,
|
||||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
fadePoint: 0.25, // Start on 1/4th of the list.
|
||||||
|
|
||||||
initialLoadDelay: 0, // 0 seconds delay
|
initialLoadDelay: 0, // 0 seconds delay
|
||||||
retryDelay: 2500,
|
retryDelay: 2500,
|
||||||
|
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
apiSecret: "",
|
apiSecret: "",
|
||||||
apiVersion: "2.5",
|
apiVersion: "2.5",
|
||||||
apiBase: "https://api.openweathermap.org/data/", // TODO: this should not be part of the weather.js file, but should be contained in the openweatherprovider
|
apiBase: "https://api.openweathermap.org/data/", // TODO: this should not be part of the weather.js file, but should be contained in the openweatherprovider
|
||||||
weatherEndpoint: "/weather",
|
weatherEndpoint: "/weather",
|
||||||
|
|
||||||
appendLocationNameToHeader: true,
|
appendLocationNameToHeader: true,
|
||||||
calendarClass: "calendar",
|
calendarClass: "calendar",
|
||||||
tableClass: "small",
|
tableClass: "small",
|
||||||
|
|
||||||
onlyTemp: false,
|
onlyTemp: false,
|
||||||
showPrecipitationAmount: false,
|
showPrecipitationAmount: false,
|
||||||
colored: false,
|
colored: false,
|
||||||
showFeelsLike: true
|
showFeelsLike: true,
|
||||||
|
feelsLikeWithDegree: false
|
||||||
},
|
},
|
||||||
|
|
||||||
// Module properties.
|
// Module properties.
|
||||||
@ -94,6 +89,8 @@ Module.register("weather", {
|
|||||||
// Let the weather provider know we are starting.
|
// Let the weather provider know we are starting.
|
||||||
this.weatherProvider.start();
|
this.weatherProvider.start();
|
||||||
|
|
||||||
|
this.config.feelsLikeWithDegree = this.translate("FEELS").indexOf("{DEGREE}") > -1;
|
||||||
|
|
||||||
// Add custom filters
|
// Add custom filters
|
||||||
this.addFilters();
|
this.addFilters();
|
||||||
|
|
||||||
|
@ -10,10 +10,11 @@
|
|||||||
* As soon as we start implementing the forecast, mode properties will be added.
|
* As soon as we start implementing the forecast, mode properties will be added.
|
||||||
*/
|
*/
|
||||||
class WeatherObject {
|
class WeatherObject {
|
||||||
constructor(units, tempUnits, windUnits) {
|
constructor(units, tempUnits, windUnits, useKmh) {
|
||||||
this.units = units;
|
this.units = units;
|
||||||
this.tempUnits = tempUnits;
|
this.tempUnits = tempUnits;
|
||||||
this.windUnits = windUnits;
|
this.windUnits = windUnits;
|
||||||
|
this.useKmh = useKmh;
|
||||||
this.date = null;
|
this.date = null;
|
||||||
this.windSpeed = null;
|
this.windSpeed = null;
|
||||||
this.windDirection = null;
|
this.windDirection = null;
|
||||||
@ -67,7 +68,7 @@ class WeatherObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beaufortWindSpeed() {
|
beaufortWindSpeed() {
|
||||||
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : (this.windSpeed * 60 * 60) / 1000;
|
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : this.useKmh ? this.windSpeed : (this.windSpeed * 60 * 60) / 1000;
|
||||||
const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
|
const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
|
||||||
for (const [index, speed] of speeds.entries()) {
|
for (const [index, speed] of speeds.entries()) {
|
||||||
if (speed > windInKmh) {
|
if (speed > windInKmh) {
|
||||||
@ -77,6 +78,11 @@ class WeatherObject {
|
|||||||
return 12;
|
return 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kmhWindSpeed() {
|
||||||
|
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : (this.windSpeed * 60 * 60) / 1000;
|
||||||
|
return windInKmh;
|
||||||
|
}
|
||||||
|
|
||||||
nextSunAction() {
|
nextSunAction() {
|
||||||
return moment().isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise";
|
return moment().isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise";
|
||||||
}
|
}
|
||||||
|
@ -371,10 +371,10 @@ Module.register("weatherforecast", {
|
|||||||
var hour;
|
var hour;
|
||||||
if (forecast.dt_txt) {
|
if (forecast.dt_txt) {
|
||||||
day = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd");
|
day = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd");
|
||||||
hour = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("H");
|
hour = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").toDate().getHours();
|
||||||
} else {
|
} else {
|
||||||
day = moment(forecast.dt, "X").format("ddd");
|
day = moment(forecast.dt, "X").format("ddd");
|
||||||
hour = moment(forecast.dt, "X").format("H");
|
hour = moment(forecast.dt, "X").toDate().getHours();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (day !== lastDay) {
|
if (day !== lastDay) {
|
||||||
@ -385,7 +385,6 @@ Module.register("weatherforecast", {
|
|||||||
minTemp: this.roundValue(forecast.temp.min),
|
minTemp: this.roundValue(forecast.temp.min),
|
||||||
rain: this.processRain(forecast, forecastList)
|
rain: this.processRain(forecast, forecastList)
|
||||||
};
|
};
|
||||||
|
|
||||||
this.forecast.push(forecastData);
|
this.forecast.push(forecastData);
|
||||||
lastDay = day;
|
lastDay = day;
|
||||||
|
|
||||||
|
19342
package-lock.json
generated
19342
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "magicmirror",
|
"name": "magicmirror",
|
||||||
"version": "2.13.0",
|
"version": "2.14.0",
|
||||||
"description": "The open source modular smart mirror platform.",
|
"description": "The open source modular smart mirror platform.",
|
||||||
"main": "js/electron.js",
|
"main": "js/electron.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -44,47 +44,47 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"danger": "^3.1.3",
|
"danger": "^10.5.4",
|
||||||
"eslint-config-prettier": "^6.11.0",
|
"eslint-config-prettier": "^7.0.0",
|
||||||
"eslint-plugin-jsdoc": "^30.5.1",
|
"eslint-plugin-jsdoc": "^30.7.8",
|
||||||
"eslint-plugin-prettier": "^3.1.4",
|
"eslint-plugin-prettier": "^3.2.0",
|
||||||
"http-auth": "^3.2.3",
|
"express-basic-auth": "^1.2.0",
|
||||||
"husky": "^4.3.0",
|
"husky": "^4.3.5",
|
||||||
"jsdom": "^11.6.2",
|
"jsdom": "^16.4.0",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"mocha": "^7.1.2",
|
"mocha": "^8.2.1",
|
||||||
"mocha-each": "^2.0.1",
|
"mocha-each": "^2.0.1",
|
||||||
"mocha-logger": "^1.0.6",
|
"mocha-logger": "^1.0.7",
|
||||||
"nyc": "^15.1.0",
|
"nyc": "^15.1.0",
|
||||||
"prettier": "^2.1.2",
|
"prettier": "^2.2.1",
|
||||||
"pretty-quick": "^3.0.2",
|
"pretty-quick": "^3.1.0",
|
||||||
"spectron": "^8.0.0",
|
"spectron": "^10.0.1",
|
||||||
"stylelint": "^13.7.1",
|
"stylelint": "^13.8.0",
|
||||||
"stylelint-config-prettier": "^8.0.2",
|
"stylelint-config-prettier": "^8.0.2",
|
||||||
"stylelint-config-standard": "^20.0.0",
|
"stylelint-config-standard": "^20.0.0",
|
||||||
"stylelint-prettier": "^1.1.2"
|
"stylelint-prettier": "^1.1.2"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"electron": "^6.1.7"
|
"electron": "^8.5.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"console-stamp": "^0.2.9",
|
"console-stamp": "^3.0.0-rc4.2",
|
||||||
"eslint": "^7.9.0",
|
"eslint": "^7.15.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-ipfilter": "^1.1.2",
|
"express-ipfilter": "^1.1.2",
|
||||||
"feedme": "^1.2.0",
|
"feedme": "^2.0.2",
|
||||||
"helmet": "^3.23.3",
|
"helmet": "^4.2.0",
|
||||||
"ical": "^0.8.0",
|
"ical": "^0.8.0",
|
||||||
"iconv-lite": "^0.6.2",
|
"iconv-lite": "^0.6.2",
|
||||||
"module-alias": "^2.2.2",
|
"module-alias": "^2.2.2",
|
||||||
"moment": "^2.28.0",
|
"moment": "^2.29.1",
|
||||||
"node-ical": "^0.12.0",
|
"node-ical": "^0.12.7",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"rrule": "^2.6.6",
|
"rrule": "^2.6.6",
|
||||||
"rrule-alt": "^2.2.8",
|
"rrule-alt": "^2.2.8",
|
||||||
"simple-git": "^1.85.0",
|
"simple-git": "^2.31.0",
|
||||||
"socket.io": "^2.3.0",
|
"socket.io": "^3.0.4",
|
||||||
"valid-url": "^1.0.9"
|
"valid-url": "^1.0.9"
|
||||||
},
|
},
|
||||||
"_moduleAliases": {
|
"_moduleAliases": {
|
||||||
|
@ -22,11 +22,11 @@ let config = {
|
|||||||
config: {
|
config: {
|
||||||
calendars: [
|
calendars: [
|
||||||
{
|
{
|
||||||
|
maximumEntries: 4,
|
||||||
|
maximumNumberOfDays: 10000,
|
||||||
symbol: "birthday-cake",
|
symbol: "birthday-cake",
|
||||||
fullDaySymbol: "calendar-day",
|
fullDaySymbol: "calendar-day",
|
||||||
recurringSymbol: "undo",
|
recurringSymbol: "undo",
|
||||||
maximumEntries: 4,
|
|
||||||
maximumNumberOfDays: 10000,
|
|
||||||
url: "http://localhost:8080/tests/configs/data/calendar_test_icons.ics"
|
url: "http://localhost:8080/tests/configs/data/calendar_test_icons.ics"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -186,7 +186,7 @@ describe("Weather module", function () {
|
|||||||
const weather = generateWeatherForecast();
|
const weather = generateWeatherForecast();
|
||||||
await setup({ template, data: weather });
|
await setup({ template, data: weather });
|
||||||
|
|
||||||
const days = ["Fri", "Sat", "Sun", "Mon", "Tue"];
|
const days = ["Today", "Tomorrow", "Sun", "Mon", "Tue"];
|
||||||
|
|
||||||
for (const [index, day] of days.entries()) {
|
for (const [index, day] of days.entries()) {
|
||||||
await app.client.waitUntilTextExists(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day, 10000);
|
await app.client.waitUntilTextExists(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day, 10000);
|
||||||
|
@ -46,7 +46,7 @@ describe("Translations", function () {
|
|||||||
it(`should parse ${language}`, function (done) {
|
it(`should parse ${language}`, function (done) {
|
||||||
const dom = new JSDOM(
|
const dom = new JSDOM(
|
||||||
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
||||||
<script src="${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
|
<script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
|
||||||
{ runScripts: "dangerously", resources: "usable" }
|
{ runScripts: "dangerously", resources: "usable" }
|
||||||
);
|
);
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
@ -68,7 +68,7 @@ describe("Translations", function () {
|
|||||||
before(function (done) {
|
before(function (done) {
|
||||||
const dom = new JSDOM(
|
const dom = new JSDOM(
|
||||||
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
||||||
<script src="${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
|
<script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
|
||||||
{ runScripts: "dangerously", resources: "usable" }
|
{ runScripts: "dangerously", resources: "usable" }
|
||||||
);
|
);
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
@ -92,7 +92,7 @@ describe("Translations", function () {
|
|||||||
before(function (done) {
|
before(function (done) {
|
||||||
const dom = new JSDOM(
|
const dom = new JSDOM(
|
||||||
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
`<script>var translations = ${JSON.stringify(translations)}; var Log = {log: function(){}};</script>\
|
||||||
<script src="${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
|
<script src="file://${path.join(__dirname, "..", "..", "js", "translator.js")}">`,
|
||||||
{ runScripts: "dangerously", resources: "usable" }
|
{ runScripts: "dangerously", resources: "usable" }
|
||||||
);
|
);
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
|
@ -1,20 +1,16 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const auth = require("http-auth");
|
const auth = require("express-basic-auth");
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
var server;
|
var server;
|
||||||
|
|
||||||
var basic = auth.basic(
|
var basicAuth = auth({
|
||||||
{
|
realm: "MagicMirror Area restricted.",
|
||||||
realm: "MagicMirror Area restricted."
|
users: { MagicMirror: "CallMeADog" }
|
||||||
},
|
});
|
||||||
(username, password, callback) => {
|
|
||||||
callback(username === "MagicMirror" && password === "CallMeADog");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(auth.connect(basic));
|
app.use(basicAuth);
|
||||||
|
|
||||||
// Set available directories
|
// Set available directories
|
||||||
var directories = ["/tests/configs"];
|
var directories = ["/tests/configs"];
|
||||||
|
@ -10,7 +10,7 @@ describe("File js/class", function () {
|
|||||||
before(function (done) {
|
before(function (done) {
|
||||||
dom = new JSDOM(
|
dom = new JSDOM(
|
||||||
`<script>var Log = {log: function() {}};</script>\
|
`<script>var Log = {log: function() {}};</script>\
|
||||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "class.js")}">`,
|
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "class.js")}">`,
|
||||||
{ runScripts: "dangerously", resources: "usable" }
|
{ runScripts: "dangerously", resources: "usable" }
|
||||||
);
|
);
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
|
@ -65,7 +65,7 @@ describe("Translator", function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it("should return custom module translation", function (done) {
|
it("should return custom module translation", function (done) {
|
||||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
@ -78,7 +78,7 @@ describe("Translator", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return core translation", function (done) {
|
it("should return core translation", function (done) {
|
||||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
@ -91,7 +91,7 @@ describe("Translator", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return custom module translation fallback", function (done) {
|
it("should return custom module translation fallback", function (done) {
|
||||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
@ -102,7 +102,7 @@ describe("Translator", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return core translation fallback", function (done) {
|
it("should return core translation fallback", function (done) {
|
||||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
@ -113,7 +113,7 @@ describe("Translator", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return translation with placeholder for missing variables", function (done) {
|
it("should return translation with placeholder for missing variables", function (done) {
|
||||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
@ -124,7 +124,7 @@ describe("Translator", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return key if no translation was found", function (done) {
|
it("should return key if no translation was found", function (done) {
|
||||||
const dom = new JSDOM(`<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
setTranslations(Translator);
|
setTranslations(Translator);
|
||||||
@ -144,7 +144,7 @@ describe("Translator", function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it("should load translations", function (done) {
|
it("should load translations", function (done) {
|
||||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
const file = "TranslationTest.json";
|
const file = "TranslationTest.json";
|
||||||
@ -158,7 +158,7 @@ describe("Translator", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should load translation fallbacks", function (done) {
|
it("should load translation fallbacks", function (done) {
|
||||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
const { Translator } = dom.window;
|
const { Translator } = dom.window;
|
||||||
const file = "TranslationTest.json";
|
const file = "TranslationTest.json";
|
||||||
@ -172,7 +172,7 @@ describe("Translator", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not load translations, if module fallback exists", function (done) {
|
it("should not load translations, if module fallback exists", function (done) {
|
||||||
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
const { Translator, XMLHttpRequest } = dom.window;
|
const { Translator, XMLHttpRequest } = dom.window;
|
||||||
const file = "TranslationTest.json";
|
const file = "TranslationTest.json";
|
||||||
@ -200,7 +200,7 @@ describe("Translator", function () {
|
|||||||
it("should load core translations and fallback", function (done) {
|
it("should load core translations and fallback", function (done) {
|
||||||
const dom = new JSDOM(
|
const dom = new JSDOM(
|
||||||
`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
||||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
|
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
|
||||||
{ runScripts: "dangerously", resources: "usable" }
|
{ runScripts: "dangerously", resources: "usable" }
|
||||||
);
|
);
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
@ -219,7 +219,7 @@ describe("Translator", function () {
|
|||||||
it("should load core fallback if language cannot be found", function (done) {
|
it("should load core fallback if language cannot be found", function (done) {
|
||||||
const dom = new JSDOM(
|
const dom = new JSDOM(
|
||||||
`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
||||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
|
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
|
||||||
{ runScripts: "dangerously", resources: "usable" }
|
{ runScripts: "dangerously", resources: "usable" }
|
||||||
);
|
);
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
@ -240,7 +240,7 @@ describe("Translator", function () {
|
|||||||
it("should load core translations fallback", function (done) {
|
it("should load core translations fallback", function (done) {
|
||||||
const dom = new JSDOM(
|
const dom = new JSDOM(
|
||||||
`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
`<script>var translations = {en: "http://localhost:3000/translations/en.json"}; var Log = {log: function(){}};</script>\
|
||||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
|
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
|
||||||
{ runScripts: "dangerously", resources: "usable" }
|
{ runScripts: "dangerously", resources: "usable" }
|
||||||
);
|
);
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
@ -258,7 +258,7 @@ describe("Translator", function () {
|
|||||||
it("should load core fallback if language cannot be found", function (done) {
|
it("should load core fallback if language cannot be found", function (done) {
|
||||||
const dom = new JSDOM(
|
const dom = new JSDOM(
|
||||||
`<script>var translations = {}; var Log = {log: function(){}};</script>\
|
`<script>var translations = {}; var Log = {log: function(){}};</script>\
|
||||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
|
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`,
|
||||||
{ runScripts: "dangerously", resources: "usable" }
|
{ runScripts: "dangerously", resources: "usable" }
|
||||||
);
|
);
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
|
@ -8,7 +8,7 @@ describe("Test function cmpVersions in js/module.js", function () {
|
|||||||
before(function (done) {
|
before(function (done) {
|
||||||
const dom = new JSDOM(
|
const dom = new JSDOM(
|
||||||
`<script>var Class = {extend: function() { return {}; }};</script>\
|
`<script>var Class = {extend: function() { return {}; }};</script>\
|
||||||
<script src="${path.join(__dirname, "..", "..", "..", "js", "module.js")}">`,
|
<script src="file://${path.join(__dirname, "..", "..", "..", "js", "module.js")}">`,
|
||||||
{ runScripts: "dangerously", resources: "usable" }
|
{ runScripts: "dangerously", resources: "usable" }
|
||||||
);
|
);
|
||||||
dom.window.onload = function () {
|
dom.window.onload = function () {
|
||||||
|
34
translations/cv.json
Normal file
34
translations/cv.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"LOADING": "Тиенет …",
|
||||||
|
|
||||||
|
"TODAY": "Паян",
|
||||||
|
"TOMORROW": "Ыран",
|
||||||
|
"DAYAFTERTOMORROW": "Виҫмине",
|
||||||
|
"RUNNING": "Хальхи",
|
||||||
|
"EMPTY": "Пулас ӗҫ ҫук",
|
||||||
|
|
||||||
|
"WEEK": "{weekNumber} эрне",
|
||||||
|
|
||||||
|
"N": "Ҫ",
|
||||||
|
"NNE": "ҪҪТ",
|
||||||
|
"NE": "ҪТ",
|
||||||
|
"ENE": "ТҪТ",
|
||||||
|
"E": "Т",
|
||||||
|
"ESE": "ТКТ",
|
||||||
|
"SE": "КТ",
|
||||||
|
"SSE": "ККТ",
|
||||||
|
"S": "К",
|
||||||
|
"SSW": "ККА",
|
||||||
|
"SW": "КА",
|
||||||
|
"WSW": "АКА",
|
||||||
|
"W": "А",
|
||||||
|
"WNW": "АҪА",
|
||||||
|
"NW": "ҪА",
|
||||||
|
"NNW": "ҪҪА",
|
||||||
|
"FEELS": "Туйӑннӑ",
|
||||||
|
|
||||||
|
"UPDATE_NOTIFICATION": "MagicMirror² валли ҫӗнетӳ пур.",
|
||||||
|
"UPDATE_NOTIFICATION_MODULE": "{MODULE_NAME} модуль валли ҫӗнетӳ пур.",
|
||||||
|
"UPDATE_INFO_SINGLE": "Ҫак инсталляци {BRANCH_NAME} commit турат {COMMIT_COUNT} коммитпа кая уйрӑлса тӑрать.",
|
||||||
|
"UPDATE_INFO_MULTIPLE": "Ҫак инсталляци {BRANCH_NAME} commit турат {COMMIT_COUNT} коммитпа кая уйрӑлса тӑрать."
|
||||||
|
}
|
38
translations/gu.json
Normal file
38
translations/gu.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"LOADING": "લોડ થઈ રહ્યું છે …",
|
||||||
|
|
||||||
|
"TODAY": "આજે",
|
||||||
|
"TOMORROW": "આવતી કાલે",
|
||||||
|
"DAYAFTERTOMORROW": "પરમ દિવસે",
|
||||||
|
"RUNNING": "માં સમાપ્ત થાય છે",
|
||||||
|
"EMPTY": "કોઈ આગામી કાર્યક્રમ નથી.",
|
||||||
|
|
||||||
|
"WEEK": "સપ્તાહ {weekNumber}",
|
||||||
|
|
||||||
|
"N": "ઉ",
|
||||||
|
"NNE": "ઉઉપુ",
|
||||||
|
"NE": "ઉપુ",
|
||||||
|
"ENE": "પુઉપુ",
|
||||||
|
"E": "પુ",
|
||||||
|
"ESE": "પુદપુ",
|
||||||
|
"SE": "દપુ",
|
||||||
|
"SSE": "દદપુ",
|
||||||
|
"S": "દ",
|
||||||
|
"SSW": "દદપ",
|
||||||
|
"SW": "દપ",
|
||||||
|
"WSW": "પદપ",
|
||||||
|
"W": "પ",
|
||||||
|
"WNW": "પઉપ",
|
||||||
|
"NW": "ઉપ",
|
||||||
|
"NNW": "ઉઉપ",
|
||||||
|
|
||||||
|
"MODULE_CONFIG_CHANGED": "{MODULE_NAME} મોડ્યુલ માટે ગોઠવણી વિકલ્પો બદલાયા છે. \nકૃપા કરીને દસ્તાવેજોને તપાસો.",
|
||||||
|
|
||||||
|
"UPDATE_NOTIFICATION": "MagicMirror² અપડેટ ઉપલબ્ધ છે.",
|
||||||
|
"UPDATE_NOTIFICATION_MODULE": "{MODULE_NAME} મોડ્યુલ માટે અપડેટ ઉપલબ્ધ છે.",
|
||||||
|
"UPDATE_INFO_SINGLE": "વર્તમાન ઇન્સ્ટોલેશન એ {BRANCH_NAME} શાખા ની {COMMIT_COUNT} કમીટ પાછળ છે. ",
|
||||||
|
"UPDATE_INFO_MULTIPLE": "વર્તમાન ઇન્સ્ટોલેશન એ {BRANCH_NAME} શાખા ની {COMMIT_COUNT} કમીટ પાછળ છે. ",
|
||||||
|
|
||||||
|
"FEELS": "{DEGREE} જેવું લાગશે",
|
||||||
|
"PRECIP": "PoP"
|
||||||
|
}
|
38
translations/hi.json
Normal file
38
translations/hi.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"LOADING": "लोड हो रहा है …",
|
||||||
|
|
||||||
|
"TODAY": "आज",
|
||||||
|
"TOMORROW": "आने वाला कल",
|
||||||
|
"DAYAFTERTOMORROW": "2 दिनों में",
|
||||||
|
"RUNNING": "में समाप्त",
|
||||||
|
"EMPTY": "कोई आगामी कार्यक्रम नहीं।",
|
||||||
|
|
||||||
|
"WEEK": "सप्ताह {weekNumber}",
|
||||||
|
|
||||||
|
"N": "उ",
|
||||||
|
"NNE": "उउपु",
|
||||||
|
"NE": "उपु",
|
||||||
|
"ENE": "पुउपु",
|
||||||
|
"E": "पु",
|
||||||
|
"ESE": "पुSपु",
|
||||||
|
"SE": "दपु",
|
||||||
|
"SSE": "ददपु",
|
||||||
|
"S": "द",
|
||||||
|
"SSW": "ददW",
|
||||||
|
"SW": "दW",
|
||||||
|
"WSW": "WदW",
|
||||||
|
"W": "प",
|
||||||
|
"WNW": "पउप",
|
||||||
|
"NW": "उप",
|
||||||
|
"NNW": "उउप",
|
||||||
|
|
||||||
|
"MODULE_CONFIG_CHANGED": "{MODULE_NAME} मॉड्यूल के लिए कॉन्फ़िगरेशन विकल्प बदल गए हैं। n कृपया दस्तावेज़ देखें।",
|
||||||
|
|
||||||
|
"UPDATE_NOTIFICATION": "MagicMirror² अपडेट उपलब्ध।",
|
||||||
|
"UPDATE_NOTIFICATION_MODULE": "{MODULE_NAME} मॉड्यूल के लिए उपलब्ध अद्यतन।",
|
||||||
|
"UPDATE_INFO_SINGLE": "वर्तमान स्थापना {COMMIT_COUNT} {BRANCH_NAME} शाखा के पीछे है।",
|
||||||
|
"UPDATE_INFO_MULTIPLE": "वर्तमान स्थापना {COMMIT_COUNT} पीछे {BRANCH_NAME} शाखा पर है।",
|
||||||
|
|
||||||
|
"FEELS": "{DEGREE} की तरह लगना",
|
||||||
|
"PRECIP": "PoP"
|
||||||
|
}
|
@ -9,28 +9,28 @@
|
|||||||
|
|
||||||
"WEEK": "{weekNumber} savaitė",
|
"WEEK": "{weekNumber} savaitė",
|
||||||
|
|
||||||
"N": "N",
|
"N": "Š",
|
||||||
"NNE": "NNE",
|
"NNE": "ŠŠR",
|
||||||
"NE": "NE",
|
"NE": "ŠR",
|
||||||
"ENE": "ENE",
|
"ENE": "RŠR",
|
||||||
"E": "E",
|
"E": "R",
|
||||||
"ESE": "ESE",
|
"ESE": "RPR",
|
||||||
"SE": "SE",
|
"SE": "PR",
|
||||||
"SSE": "SSE",
|
"SSE": "PPR",
|
||||||
"S": "S",
|
"S": "P",
|
||||||
"SSW": "SSW",
|
"SSW": "PPV",
|
||||||
"SW": "SW",
|
"SW": "PV",
|
||||||
"WSW": "WSW",
|
"WSW": "VPV",
|
||||||
"W": "W",
|
"W": "V",
|
||||||
"WNW": "WNW",
|
"WNW": "VŠV",
|
||||||
"NW": "NW",
|
"NW": "ŠV",
|
||||||
"NNW": "NNW",
|
"NNW": "ŠŠV",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "Galimas MagicMirror² naujinimas.",
|
"UPDATE_NOTIFICATION": "Galimas MagicMirror² naujinimas.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Galimas {MODULE_NAME} naujinimas.",
|
"UPDATE_NOTIFICATION_MODULE": "Galimas {MODULE_NAME} naujinimas.",
|
||||||
"UPDATE_INFO_SINGLE": "Šis įdiegimas atsilieka {COMMIT_COUNT} įsipareigojimu {BRANCH_NAME} šakoje.",
|
"UPDATE_INFO_SINGLE": "Šis įdiegimas atsilieka {COMMIT_COUNT} įsipareigojimu {BRANCH_NAME} šakoje.",
|
||||||
"UPDATE_INFO_MULTIPLE": "Šis įdiegimas atsilieka {COMMIT_COUNT} įsipareigojimais {BRANCH_NAME} šakoje.",
|
"UPDATE_INFO_MULTIPLE": "Šis įdiegimas atsilieka {COMMIT_COUNT} įsipareigojimais {BRANCH_NAME} šakoje.",
|
||||||
|
|
||||||
"FEELS": "Jaučiasi kaip",
|
"FEELS": "Jutiminė temp.",
|
||||||
"PRECIP": "Krituliai"
|
"PRECIP": "Krituliai"
|
||||||
}
|
}
|
||||||
|
37
translations/ps.json
Normal file
37
translations/ps.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"LOADING": "پیلېدل",
|
||||||
|
|
||||||
|
"TODAY": "نن",
|
||||||
|
"TOMORROW": "سبا",
|
||||||
|
"DAYAFTERTOMORROW": "بل سبا",
|
||||||
|
"RUNNING": "روان",
|
||||||
|
"EMPTY": "تش",
|
||||||
|
|
||||||
|
"WEEK": "{weekNumber}. اونۍ",
|
||||||
|
|
||||||
|
"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",
|
||||||
|
|
||||||
|
"MODULE_CONFIG_CHANGED": "د {MODULE_NAME} بڼی تغیر کړی دی. \n هیله دی چی اسناد و ګوری!",
|
||||||
|
|
||||||
|
"UPDATE_NOTIFICATION": "د MagicMirror² نوې نسخه سته ",
|
||||||
|
"UPDATE_NOTIFICATION_MODULE": "د {MODULE_NAME} نوی نسخه سته",
|
||||||
|
"UPDATE_INFO_SINGLE": "اوسنی برخه {COMMIT_COUNT} د {BRANCH_NAME} څخه وروسته پاته ده",
|
||||||
|
"UPDATE_INFO_MULTIPLE": "اوسنی برخه {COMMIT_COUNT} د {BRANCH_NAME} څخه وروسته پاته ده",
|
||||||
|
|
||||||
|
"FEELS": "حس کېږی"
|
||||||
|
}
|
@ -14,6 +14,7 @@ var translations = {
|
|||||||
fy: "translations/fy.json", // Frysk
|
fy: "translations/fy.json", // Frysk
|
||||||
es: "translations/es.json", // Spanish
|
es: "translations/es.json", // Spanish
|
||||||
ca: "translations/ca.json", // Catalan
|
ca: "translations/ca.json", // Catalan
|
||||||
|
cv: "translations/cv.json", // Chuvash
|
||||||
nb: "translations/nb.json", // Norsk bokmål
|
nb: "translations/nb.json", // Norsk bokmål
|
||||||
nn: "translations/nn.json", // Norsk nynorsk
|
nn: "translations/nn.json", // Norsk nynorsk
|
||||||
pt: "translations/pt.json", // Português
|
pt: "translations/pt.json", // Português
|
||||||
@ -25,7 +26,7 @@ var translations = {
|
|||||||
"zh-tw": "translations/zh-tw.json", // Traditional Chinese
|
"zh-tw": "translations/zh-tw.json", // Traditional Chinese
|
||||||
ja: "translations/ja.json", // Japanese
|
ja: "translations/ja.json", // Japanese
|
||||||
pl: "translations/pl.json", // Polish
|
pl: "translations/pl.json", // Polish
|
||||||
gr: "translations/gr.json", // Greek
|
el: "translations/el.json", // Greek
|
||||||
da: "translations/da.json", // Danish
|
da: "translations/da.json", // Danish
|
||||||
tr: "translations/tr.json", // Turkish
|
tr: "translations/tr.json", // Turkish
|
||||||
ru: "translations/ru.json", // Russian
|
ru: "translations/ru.json", // Russian
|
||||||
@ -43,7 +44,9 @@ var translations = {
|
|||||||
tlh: "translations/tlh.json", // Klingon
|
tlh: "translations/tlh.json", // Klingon
|
||||||
"ms-my": "translations/ms-my.json", // Malay
|
"ms-my": "translations/ms-my.json", // Malay
|
||||||
he: "translations/he.json", // Hebrew
|
he: "translations/he.json", // Hebrew
|
||||||
uk: "translations/uk.json" // Ukrainian
|
uk: "translations/uk.json", // Ukrainian
|
||||||
|
hi: "translations/hi.json", // Hindi
|
||||||
|
gu: "translations/gu.json" // Gujarati
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof module !== "undefined") {
|
if (typeof module !== "undefined") {
|
||||||
|
18
vendor/package-lock.json
generated
vendored
18
vendor/package-lock.json
generated
vendored
@ -4,9 +4,9 @@
|
|||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": {
|
"@fortawesome/fontawesome-free": {
|
||||||
"version": "5.14.0",
|
"version": "5.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.1.tgz",
|
||||||
"integrity": "sha512-OfdMsF+ZQgdKHP9jUbmDcRrP0eX90XXrsXIdyjLbkmSBzmMXPABB8eobUJtivaupucYaByz6WNe1PI1JuYm3qA=="
|
"integrity": "sha512-OEdH7SyC1suTdhBGW91/zBfR6qaIhThbcN8PUXtXilY4GYnSBbVqOntdHbC1vXwsDnX0Qix2m2+DSU1J51ybOQ=="
|
||||||
},
|
},
|
||||||
"a-sync-waterfall": {
|
"a-sync-waterfall": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@ -119,14 +119,14 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"moment": {
|
"moment": {
|
||||||
"version": "2.28.0",
|
"version": "2.29.1",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||||
"integrity": "sha512-Z5KOjYmnHyd/ukynmFd/WwyXHd7L4J9vTI/nn5Ap9AVUgaAE15VvQ9MOGmJJygEUklupqIrFnor/tjTwRU+tQw=="
|
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
|
||||||
},
|
},
|
||||||
"moment-timezone": {
|
"moment-timezone": {
|
||||||
"version": "0.5.31",
|
"version": "0.5.32",
|
||||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.31.tgz",
|
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.32.tgz",
|
||||||
"integrity": "sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==",
|
"integrity": "sha512-Z8QNyuQHQAmWucp8Knmgei8YNo28aLjJq6Ma+jy1ZSpSk5nyfRT8xgUbSQvD2+2UajISfenndwvFuH3NGS+nvA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"moment": ">= 2.9.0"
|
"moment": ">= 2.9.0"
|
||||||
}
|
}
|
||||||
|
6
vendor/package.json
vendored
6
vendor/package.json
vendored
@ -10,9 +10,9 @@
|
|||||||
"url": "https://github.com/MichMich/MagicMirror/issues"
|
"url": "https://github.com/MichMich/MagicMirror/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||||
"moment": "^2.28.0",
|
"moment": "^2.29.1",
|
||||||
"moment-timezone": "^0.5.31",
|
"moment-timezone": "^0.5.32",
|
||||||
"nunjucks": "^3.2.2",
|
"nunjucks": "^3.2.2",
|
||||||
"suncalc": "^1.8.0",
|
"suncalc": "^1.8.0",
|
||||||
"weathericons": "^2.1.0"
|
"weathericons": "^2.1.0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user