Merge pull request #2400 from MichMich/develop

Release 2.14.0
This commit is contained in:
Michael Teeuw 2021-01-01 19:16:03 +01:00 committed by GitHub
commit 10dc315f3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 11216 additions and 9957 deletions

15
.github/workflows/enforce-changelog.yml vendored Normal file
View 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
View 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
View File

@ -67,6 +67,10 @@ Temporary Items
# Ignore changes to the custom css files.
/css/custom.css
# Ignore users config file but keep the sample.
/config/*
!/config/config.js.sample
# Vim
## swap
[._]*.s[a-w][a-z]

View File

@ -2,3 +2,4 @@ package-lock.json
/config/**/*
/vendor/**/*
!/vendor/vendor.js
.github/**/*

View File

@ -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

View File

@ -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²
## [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
Special thanks to the following contributors: @bryanzzhu, @bugsounet, @chamakura, @cjbrunner, @easyas314, @larryare, @oemel09, @rejas, @sdetweil & @sthuber90.

View File

@ -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://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://travis-ci.com/MichMich/MagicMirror"><img src="https://travis-ci.com/MichMich/MagicMirror.svg" alt="Travis"></a>
<a href="https://snyk.io/test/github/MichMich/MagicMirror"><img src="https://snyk.io/test/github/MichMich/MagicMirror/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/MichMich/MagicMirror" style="max-width:100%;"></a>
<a href="https://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>
</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).
@ -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.
<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>
</p>

2
config/.gitignore vendored
View File

@ -1,2 +0,0 @@
*
!config.js.sample

View File

@ -28,7 +28,7 @@ var config = {
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
language: "en",
logLevel: ["INFO", "LOG", "WARN", "ERROR"],
logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging
timeFormat: 24,
units: "metric",
// serverOnly: true/false/"local" ,
@ -70,7 +70,7 @@ var config = {
position: "top_right",
config: {
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"
}
},

View File

@ -2,6 +2,7 @@ html {
cursor: none;
overflow: hidden;
background: #000;
user-select: none;
}
::-webkit-scrollbar {

View File

@ -46,7 +46,7 @@ function checkConfigFile() {
try {
fs.accessSync(configFileName, fs.F_OK);
} catch (e) {
Log.log(Utils.colors.error(e));
Log.error(Utils.colors.error(e));
throw new Error("No permission to access config file!");
}

View File

@ -10,7 +10,10 @@
(function (root, factory) {
if (typeof exports === "object") {
// 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
module.exports = factory(root.config);
@ -20,10 +23,11 @@
}
})(this, function (config) {
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),
error: Function.prototype.bind.call(console.error, console),
info: Function.prototype.bind.call(console.info, 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),
groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console),
groupEnd: Function.prototype.bind.call(console.groupEnd, console),

View File

@ -500,10 +500,7 @@ var MM = (function () {
*/
modulesStarted: function (moduleObjects) {
modules = [];
for (var m in moduleObjects) {
var module = moduleObjects[m];
modules[module.data.index] = module;
}
moduleObjects.forEach((module) => modules.push(module));
Log.info("All modules started!");
sendNotification("ALL_MODULES_STARTED");

View File

@ -175,8 +175,8 @@ var Module = Class.extend({
lstripBlocks: true
});
this._nunjucksEnvironment.addFilter("translate", function (str) {
return self.translate(str);
this._nunjucksEnvironment.addFilter("translate", function (str, variables) {
return self.translate(str, variables);
});
return this._nunjucksEnvironment;
@ -228,7 +228,7 @@ var Module = Class.extend({
* Set the module config and combine it with the module defaults.
*
* @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) {
this.config = deep ? configMerge({}, this.defaults, config) : Object.assign({}, this.defaults, config);
@ -434,20 +434,22 @@ var Module = Class.extend({
speed,
function () {
self.resume();
callback;
callback();
},
options
);
}
});
/** Merging MagicMirror (or other) default/config script
* merge 2 objects or/with array
* using:
/**
* Merging MagicMirror (or other) default/config script by @bugsounet
* Merge 2 objects or/with array
*
* Usage:
* -------
* this.config = configMerge({}, this.defaults, this.config)
* -------
* arg1: initial objet
* arg1: initial object
* arg2: config model
* arg3: config to merge
* -------
@ -456,10 +458,12 @@ var Module = Class.extend({
* it don't merge all thing in deep
* -> 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) {
var stack = Array.prototype.slice.call(arguments, 1);
var item;
@ -506,7 +510,7 @@ Module.register = function (name, moduleDefinition) {
if (cmpVersions(window.version, moduleDefinition.requiresVersion) >= 0) {
Log.log("Version is ok!");
} else {
Log.log("Version is incorrect. Skip module: '" + name + "'");
Log.warn("Version is incorrect. Skip module: '" + name + "'");
return;
}
}

View File

@ -37,7 +37,7 @@ var Server = function (config, callback) {
server.listen(port, config.address ? config.address : "localhost");
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) {
@ -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.");
});
});
app.use(helmet());
app.use(helmet({ contentSecurityPolicy: false }));
app.use("/js", express.static(__dirname));
var directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];

View File

@ -19,7 +19,15 @@ var Translator = (function () {
xhr.open("GET", file, true);
xhr.onreadystatechange = function () {
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);
@ -60,7 +68,7 @@ var Translator = (function () {
template = variables.fallback;
}
return template.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) {
return variables[varName] || "{" + varName + "}";
return varName in variables ? variables[varName] : "{" + varName + "}";
});
}

View File

@ -100,10 +100,13 @@ Module.register("alert", {
message: image + message,
effect: this.config.alert_effect,
ttl: params.timer,
onClose: () => this.hide_alert(sender),
al_no: "ns-alert"
});
//Show alert
this.alerts[sender.name].show();
//Add timer to dismiss alert and overlay
if (params.timer) {
setTimeout(() => {

View File

@ -11,6 +11,7 @@ Module.register("calendar", {
defaults: {
maximumEntries: 10, // Total Maximum Entries
maximumNumberOfDays: 365,
limitDays: 0, // Limit the number of days shown, 0 = no limit
displaySymbol: true,
defaultSymbol: "calendar", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
showLocation: false,
@ -37,6 +38,7 @@ Module.register("calendar", {
hideOngoing: false,
colored: 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",
calendars: [
{
@ -58,6 +60,8 @@ Module.register("calendar", {
nextDaysRelative: false
},
requiresVersion: "2.1.0",
// Define required scripts.
getStyles: function () {
return ["calendar.css", "font-awesome.css"];
@ -83,6 +87,12 @@ Module.register("calendar", {
// Set locale.
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) {
var calendar = this.config.calendars[c];
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);
// 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.
@ -153,6 +155,12 @@ Module.register("calendar", {
// Override dom generator.
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 wrapper = document.createElement("table");
wrapper.className = this.config.tableClass;
@ -173,6 +181,8 @@ Module.register("calendar", {
var currentFadeStep = 0;
var lastSeenDate = "";
var ev;
var needle;
for (var e in events) {
var event = events[e];
@ -218,6 +228,19 @@ Module.register("calendar", {
symbolWrapper.className = "symbol align-right " + symbolClass;
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++) {
var symbol = document.createElement("span");
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;
var titleClass = this.titleClassForUrl(event.url);
@ -280,14 +320,35 @@ Module.register("calendar", {
eventWrapper.appendChild(titleWrapper);
var now = new Date();
// Define second, minute, hour, and day variables
var oneSecond = 1000; // 1,000 milliseconds
var oneMinute = oneSecond * 60;
var oneHour = oneMinute * 60;
var oneDay = oneHour * 24;
if (this.config.timeFormat === "absolute") {
// Use dateFormat
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
// Add end time if showEnd
if (this.config.showEnd) {
timeWrapper.innerHTML += "-";
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
}
// For full day events we use the fullDayEventDateFormat
if (event.fullDayEvent) {
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
event.endDate -= oneSecond;
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
}
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) {
@ -295,67 +356,20 @@ Module.register("calendar", {
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
} else {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
}
} else {
/* 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").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'
// Show relative times
if (event.startDate >= now) {
// Use relative 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 {
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());
}
}
} else {
// Ongoing event
timeWrapper.innerHTML = this.capFirst(
this.translate("RUNNING", {
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);
eventWrapper.appendChild(timeWrapper);
}
@ -521,6 +530,35 @@ Module.register("calendar", {
events.sort(function (a, b) {
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);
},
@ -541,6 +579,8 @@ Module.register("calendar", {
* @param {object} calendarConfig The config of the specific calendar
*/
addCalendar: function (url, auth, calendarConfig) {
var self = this;
this.sendSocketNotification("ADD_CALENDAR", {
id: this.identifier,
url: url,

View File

@ -76,7 +76,18 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
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 = [];
// 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) {
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]) => {
const now = new Date();
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.
let past = today;
Log.debug("have entries ");
if (includePastEvents) {
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 endDate;
Log.debug("\nevent=" + JSON.stringify(event));
if (typeof event.end !== "undefined") {
endDate = eventDate(event, "end");
} else if (typeof event.duration !== "undefined") {
endDate = startDate.clone().add(moment.duration(event.duration));
} else {
if (!isFacebookBirthday) {
endDate = startDate;
// make copy of start date, separate storage area
endDate = moment(startDate.format("x"), "x");
} else {
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.
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
@ -209,11 +224,19 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
pastLocal = pastMoment.toDate();
futureLocal = futureMoment.toDate();
} else {
pastLocal = pastMoment.subtract(past.getTimezoneOffset(), "minutes").toDate();
futureLocal = futureMoment.subtract(future.getTimezoneOffset(), "minutes").toDate();
// if we want past events
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);
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
// 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,
@ -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.
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.
// .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 )
@ -241,8 +263,29 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
let curEvent = event;
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);
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.
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.
@ -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.
showRecurrence = false;
}
Log.debug("duration=" + duration);
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
if (startDate.format("x") === endDate.format("x")) {
@ -274,11 +318,12 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
}
if (showRecurrence === true) {
Log.debug("saving event =" + description);
addedEvents++;
newEvents.push({
title: recurrenceTitle,
startDate: startDate.format("x"),
endDate: endDate.format("x"),
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
fullDayEvent: isFullDayEvent(event),
recurringEvent: true,
class: event.class,
@ -293,6 +338,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
} else {
// Single event.
const fullDayEvent = isFacebookBirthday ? true : isFullDayEvent(event);
// Log.debug("full day event")
if (includePastEvents) {
// 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) {
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.
newEvents.push({
title: title,
startDate: startDate.format("x"),
endDate: endDate.format("x"),
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
fullDayEvent: fullDayEvent,
class: event.class,
location: location,
@ -344,13 +395,138 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
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();
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.
*/
@ -368,7 +544,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
* @returns {boolean} True if the event is a fullday event, false otherwise
*/
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;
}

View File

@ -70,7 +70,6 @@ module.exports = NodeHelper.create({
} else {
Log.log("Use existing calendar fetcher for url: " + url);
fetcher = self.fetchers[identifier + url];
fetcher.broadcastEvents();
}
fetcher.startFetch();

View 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] }
}

View File

@ -187,10 +187,10 @@ Module.register("clock", {
'"><i class="fa fa-sun-o" aria-hidden="true"></i> ' +
untilNextEventString +
"</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) +
"</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) +
"</span>";
}

View File

@ -258,7 +258,13 @@ Module.register("currentweather", {
var feelsLike = document.createElement("span");
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);
wrapper.appendChild(small);

View File

@ -350,7 +350,7 @@ Module.register("newsfeed", {
this.activeItem = 0;
}
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);
} else if (notification === "ARTICLE_PREVIOUS") {
this.activeItem--;
@ -358,7 +358,7 @@ Module.register("newsfeed", {
this.activeItem = this.newsItems.length - 1;
}
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);
}
// 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) {
this.scrollPosition += this.config.scrollLength;
window.scrollTo(0, this.scrollPosition);
Log.info(this.name + " - scrolling down");
Log.info(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength);
Log.debug(this.name + " - scrolling down");
Log.debug(this.name + " - ARTICLE_MORE_DETAILS, scroll position: " + this.config.scrollLength);
} else {
this.showFullArticle();
}
@ -376,12 +376,12 @@ Module.register("newsfeed", {
if (this.config.showFullArticle === true) {
this.scrollPosition -= this.config.scrollLength;
window.scrollTo(0, this.scrollPosition);
Log.info(this.name + " - scrolling up");
Log.info(this.name + " - ARTICLE_SCROLL_UP, scroll position: " + this.config.scrollLength);
Log.debug(this.name + " - scrolling up");
Log.debug(this.name + " - ARTICLE_SCROLL_UP, scroll position: " + this.config.scrollLength);
}
} else if (notification === "ARTICLE_LESS_DETAILS") {
this.resetDescrOrFullArticleAndTimer();
Log.info(this.name + " - showing only article titles again");
Log.debug(this.name + " - showing only article titles again");
this.updateDom(100);
} else if (notification === "ARTICLE_TOGGLE_FULL") {
if (this.config.showFullArticle) {
@ -411,7 +411,7 @@ Module.register("newsfeed", {
}
clearInterval(this.timer);
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);
}
});

View File

@ -8,9 +8,13 @@
<span>
{% if config.useBeaufort %}
{{ current.beaufortWindSpeed() | round }}
{% else %}
{% if config.useKmh %}
{{ current.kmhWindSpeed() | round }}
{% else %}
{{ current.windSpeed | round }}
{% endif %}
{% endif %}
{% if config.showWindDirection %}
<sup>
{% if config.showWindDirectionAsArrow %}
@ -65,7 +69,10 @@
<div class="normal medium">
{% if config.showFeelsLike %}
<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>
{% endif %}
{% if config.showPrecipitationAmount %}

View File

@ -10,7 +10,13 @@
{% set forecast = forecast.slice(0, numSteps) %}
{% for f in forecast %}
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
{% 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="align-right bright max-temp">
{{ f.maxTemperature | roundValue | unit("temperature") }}

View File

@ -62,7 +62,7 @@ WeatherProvider.register("darksky", {
// Implement WeatherDay generator.
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.humidity = parseFloat(currentWeatherData.currently.humidity);
@ -80,7 +80,7 @@ WeatherProvider.register("darksky", {
const days = [];
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.minTemperature = forecast.temperatureMin;

View File

@ -89,11 +89,15 @@ WeatherProvider.register("openweathermap", {
* Generate a WeatherObject based on currentWeatherInformation
*/
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.temperature = currentWeatherData.main.temp;
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.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon);
currentWeather.sunrise = moment(currentWeatherData.sys.sunrise, "X");
@ -112,7 +116,7 @@ WeatherProvider.register("openweathermap", {
return this.fetchForecastDaily(forecasts);
}
// 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;
},
@ -124,7 +128,7 @@ WeatherProvider.register("openweathermap", {
return this.fetchOnecall(data);
}
// 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;
},
@ -141,7 +145,7 @@ WeatherProvider.register("openweathermap", {
let snow = 0;
// variable for 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) {
if (date !== moment(forecast.dt, "X").format("YYYY-MM-DD")) {
@ -154,7 +158,7 @@ WeatherProvider.register("openweathermap", {
// push weather information to days array
days.push(weather);
// 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 = [];
maxTemp = [];
@ -217,7 +221,7 @@ WeatherProvider.register("openweathermap", {
const days = [];
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.minTemperature = forecast.temp.min;
@ -263,7 +267,7 @@ WeatherProvider.register("openweathermap", {
let precip = false;
// 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")) {
current.date = moment(data.current.dt, "X").utcOffset(data.timezone_offset / 60);
current.windSpeed = data.current.wind_speed;
@ -295,7 +299,7 @@ WeatherProvider.register("openweathermap", {
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
const hours = [];
@ -331,7 +335,7 @@ WeatherProvider.register("openweathermap", {
}
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);
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);
}
}

View 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 "";
}
}
});

View File

@ -73,7 +73,7 @@ WeatherProvider.register("ukmetoffice", {
* Generate a WeatherObject based on currentWeatherInformation
*/
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
let nowUtc = moment.utc();
@ -124,7 +124,7 @@ WeatherProvider.register("ukmetoffice", {
// loop round the (5) periods getting the data
// for each period array, Day is [0], Night is [1]
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
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) {
return this.windUnits === "metric" ? windInMph * 2.23694 : windInMph;
return this.windUnits === "metric" ? (this.useKmh ? windInMph * 1.60934 : windInMph / 2.23694) : windInMph;
},
/*

View File

@ -87,7 +87,7 @@ WeatherProvider.register("ukmetofficedatahub", {
// Did not receive usable new data.
// Maybe this needs a better check?
Log.error("Possibly bad current/hourly data?");
Log.info(data);
Log.error(data);
return;
}
@ -108,7 +108,7 @@ WeatherProvider.register("ukmetofficedatahub", {
// Create a WeatherObject using current weather data (data for the current hour)
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
let forecastDataHours = currentWeatherData.features[0].properties.timeSeries;
@ -158,7 +158,7 @@ WeatherProvider.register("ukmetofficedatahub", {
// Did not receive usable new data.
// Maybe this needs a better check?
Log.error("Possibly bad forecast data?");
Log.info(data);
Log.error(data);
return;
}
@ -189,7 +189,7 @@ WeatherProvider.register("ukmetofficedatahub", {
// Go through each day in the forecasts
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
let forecastDate = moment.utc(forecastDataDays[day].time);
@ -254,7 +254,7 @@ WeatherProvider.register("ukmetofficedatahub", {
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;
}

View 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;
}
});

View File

@ -131,11 +131,11 @@ WeatherProvider.register("weathergov", {
* ... object needs data in units based on config!
*/
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.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.minTemperature = this.convertTemp(currentWeatherData.minTemperatureLast24Hours.value);
currentWeather.maxTemperature = this.convertTemp(currentWeatherData.maxTemperatureLast24Hours.value);
@ -179,7 +179,7 @@ WeatherProvider.register("weathergov", {
let maxTemp = [];
// variable for 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;
for (const forecast of forecasts) {
@ -191,7 +191,7 @@ WeatherProvider.register("weathergov", {
// push weather information to days array
days.push(weather);
// 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 = [];
maxTemp = [];
@ -238,13 +238,17 @@ WeatherProvider.register("weathergov", {
return temp;
}
},
// conversion to mph
covertSpeed(metSec) {
// conversion to mph or kmh
convertSpeed(metSec) {
if (this.config.windUnits === "imperial") {
return metSec * 2.23694;
} else {
if (this.config.useKmh) {
return metSec * 3.6;
} else {
return metSec;
}
}
},
// conversion to inches
convertLength(meters) {

View File

@ -12,16 +12,14 @@ Module.register("weather", {
weatherProvider: "openweathermap",
roundTemp: false,
type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint)
lat: 0,
lon: 0,
location: false,
locationID: false,
units: config.units,
useKmh: false,
tempUnits: config.units,
windUnits: config.units,
updateInterval: 10 * 60 * 1000, // every 10 minutes
animationSpeed: 1000,
timeFormat: config.timeFormat,
@ -41,24 +39,21 @@ Module.register("weather", {
maxEntries: 5,
fade: true,
fadePoint: 0.25, // Start on 1/4th of the list.
initialLoadDelay: 0, // 0 seconds delay
retryDelay: 2500,
apiKey: "",
apiSecret: "",
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
weatherEndpoint: "/weather",
appendLocationNameToHeader: true,
calendarClass: "calendar",
tableClass: "small",
onlyTemp: false,
showPrecipitationAmount: false,
colored: false,
showFeelsLike: true
showFeelsLike: true,
feelsLikeWithDegree: false
},
// Module properties.
@ -94,6 +89,8 @@ Module.register("weather", {
// Let the weather provider know we are starting.
this.weatherProvider.start();
this.config.feelsLikeWithDegree = this.translate("FEELS").indexOf("{DEGREE}") > -1;
// Add custom filters
this.addFilters();

View File

@ -10,10 +10,11 @@
* As soon as we start implementing the forecast, mode properties will be added.
*/
class WeatherObject {
constructor(units, tempUnits, windUnits) {
constructor(units, tempUnits, windUnits, useKmh) {
this.units = units;
this.tempUnits = tempUnits;
this.windUnits = windUnits;
this.useKmh = useKmh;
this.date = null;
this.windSpeed = null;
this.windDirection = null;
@ -67,7 +68,7 @@ class WeatherObject {
}
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];
for (const [index, speed] of speeds.entries()) {
if (speed > windInKmh) {
@ -77,6 +78,11 @@ class WeatherObject {
return 12;
}
kmhWindSpeed() {
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : (this.windSpeed * 60 * 60) / 1000;
return windInKmh;
}
nextSunAction() {
return moment().isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise";
}

View File

@ -371,10 +371,10 @@ Module.register("weatherforecast", {
var hour;
if (forecast.dt_txt) {
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 {
day = moment(forecast.dt, "X").format("ddd");
hour = moment(forecast.dt, "X").format("H");
hour = moment(forecast.dt, "X").toDate().getHours();
}
if (day !== lastDay) {
@ -385,7 +385,6 @@ Module.register("weatherforecast", {
minTemp: this.roundValue(forecast.temp.min),
rain: this.processRain(forecast, forecastList)
};
this.forecast.push(forecastData);
lastDay = day;

6626
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "magicmirror",
"version": "2.13.0",
"version": "2.14.0",
"description": "The open source modular smart mirror platform.",
"main": "js/electron.js",
"scripts": {
@ -44,47 +44,47 @@
"devDependencies": {
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"danger": "^3.1.3",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-jsdoc": "^30.5.1",
"eslint-plugin-prettier": "^3.1.4",
"http-auth": "^3.2.3",
"husky": "^4.3.0",
"jsdom": "^11.6.2",
"danger": "^10.5.4",
"eslint-config-prettier": "^7.0.0",
"eslint-plugin-jsdoc": "^30.7.8",
"eslint-plugin-prettier": "^3.2.0",
"express-basic-auth": "^1.2.0",
"husky": "^4.3.5",
"jsdom": "^16.4.0",
"lodash": "^4.17.20",
"mocha": "^7.1.2",
"mocha": "^8.2.1",
"mocha-each": "^2.0.1",
"mocha-logger": "^1.0.6",
"mocha-logger": "^1.0.7",
"nyc": "^15.1.0",
"prettier": "^2.1.2",
"pretty-quick": "^3.0.2",
"spectron": "^8.0.0",
"stylelint": "^13.7.1",
"prettier": "^2.2.1",
"pretty-quick": "^3.1.0",
"spectron": "^10.0.1",
"stylelint": "^13.8.0",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^20.0.0",
"stylelint-prettier": "^1.1.2"
},
"optionalDependencies": {
"electron": "^6.1.7"
"electron": "^8.5.3"
},
"dependencies": {
"colors": "^1.4.0",
"console-stamp": "^0.2.9",
"eslint": "^7.9.0",
"console-stamp": "^3.0.0-rc4.2",
"eslint": "^7.15.0",
"express": "^4.17.1",
"express-ipfilter": "^1.1.2",
"feedme": "^1.2.0",
"helmet": "^3.23.3",
"feedme": "^2.0.2",
"helmet": "^4.2.0",
"ical": "^0.8.0",
"iconv-lite": "^0.6.2",
"module-alias": "^2.2.2",
"moment": "^2.28.0",
"node-ical": "^0.12.0",
"moment": "^2.29.1",
"node-ical": "^0.12.7",
"request": "^2.88.2",
"rrule": "^2.6.6",
"rrule-alt": "^2.2.8",
"simple-git": "^1.85.0",
"socket.io": "^2.3.0",
"simple-git": "^2.31.0",
"socket.io": "^3.0.4",
"valid-url": "^1.0.9"
},
"_moduleAliases": {

View File

@ -22,11 +22,11 @@ let config = {
config: {
calendars: [
{
maximumEntries: 4,
maximumNumberOfDays: 10000,
symbol: "birthday-cake",
fullDaySymbol: "calendar-day",
recurringSymbol: "undo",
maximumEntries: 4,
maximumNumberOfDays: 10000,
url: "http://localhost:8080/tests/configs/data/calendar_test_icons.ics"
}
]

View File

@ -186,7 +186,7 @@ describe("Weather module", function () {
const weather = generateWeatherForecast();
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()) {
await app.client.waitUntilTextExists(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day, 10000);

View File

@ -46,7 +46,7 @@ describe("Translations", function () {
it(`should parse ${language}`, function (done) {
const dom = new JSDOM(
`<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" }
);
dom.window.onload = function () {
@ -68,7 +68,7 @@ describe("Translations", function () {
before(function (done) {
const dom = new JSDOM(
`<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" }
);
dom.window.onload = function () {
@ -92,7 +92,7 @@ describe("Translations", function () {
before(function (done) {
const dom = new JSDOM(
`<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" }
);
dom.window.onload = function () {

View File

@ -1,20 +1,16 @@
const path = require("path");
const auth = require("http-auth");
const auth = require("express-basic-auth");
const express = require("express");
const app = express();
var server;
var basic = auth.basic(
{
realm: "MagicMirror Area restricted."
},
(username, password, callback) => {
callback(username === "MagicMirror" && password === "CallMeADog");
}
);
var basicAuth = auth({
realm: "MagicMirror Area restricted.",
users: { MagicMirror: "CallMeADog" }
});
app.use(auth.connect(basic));
app.use(basicAuth);
// Set available directories
var directories = ["/tests/configs"];

View File

@ -10,7 +10,7 @@ describe("File js/class", function () {
before(function (done) {
dom = new JSDOM(
`<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" }
);
dom.window.onload = function () {

View File

@ -65,7 +65,7 @@ describe("Translator", function () {
}
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 () {
const { Translator } = dom.window;
setTranslations(Translator);
@ -78,7 +78,7 @@ describe("Translator", function () {
});
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 () {
const { Translator } = dom.window;
setTranslations(Translator);
@ -91,7 +91,7 @@ describe("Translator", function () {
});
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 () {
const { Translator } = dom.window;
setTranslations(Translator);
@ -102,7 +102,7 @@ describe("Translator", function () {
});
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 () {
const { Translator } = dom.window;
setTranslations(Translator);
@ -113,7 +113,7 @@ describe("Translator", function () {
});
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 () {
const { Translator } = dom.window;
setTranslations(Translator);
@ -124,7 +124,7 @@ describe("Translator", function () {
});
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 () {
const { Translator } = dom.window;
setTranslations(Translator);
@ -144,7 +144,7 @@ describe("Translator", function () {
};
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 () {
const { Translator } = dom.window;
const file = "TranslationTest.json";
@ -158,7 +158,7 @@ describe("Translator", function () {
});
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 () {
const { Translator } = dom.window;
const file = "TranslationTest.json";
@ -172,7 +172,7 @@ describe("Translator", function () {
});
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 () {
const { Translator, XMLHttpRequest } = dom.window;
const file = "TranslationTest.json";
@ -200,7 +200,7 @@ describe("Translator", function () {
it("should load core translations and fallback", function (done) {
const dom = new JSDOM(
`<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" }
);
dom.window.onload = function () {
@ -219,7 +219,7 @@ describe("Translator", function () {
it("should load core fallback if language cannot be found", function (done) {
const dom = new JSDOM(
`<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" }
);
dom.window.onload = function () {
@ -240,7 +240,7 @@ describe("Translator", function () {
it("should load core translations fallback", function (done) {
const dom = new JSDOM(
`<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" }
);
dom.window.onload = function () {
@ -258,7 +258,7 @@ describe("Translator", function () {
it("should load core fallback if language cannot be found", function (done) {
const dom = new JSDOM(
`<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" }
);
dom.window.onload = function () {

View File

@ -8,7 +8,7 @@ describe("Test function cmpVersions in js/module.js", function () {
before(function (done) {
const dom = new JSDOM(
`<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" }
);
dom.window.onload = function () {

34
translations/cv.json Normal file
View File

@ -0,0 +1,34 @@
{
"LOADING": "Тиенет &hellip;",
"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
View File

@ -0,0 +1,38 @@
{
"LOADING": "લોડ થઈ રહ્યું છે &hellip;",
"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
View File

@ -0,0 +1,38 @@
{
"LOADING": "लोड हो रहा है &hellip;",
"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"
}

View File

@ -9,28 +9,28 @@
"WEEK": "{weekNumber} savaitė",
"N": "N",
"NNE": "NNE",
"NE": "NE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSW",
"SW": "SW",
"WSW": "WSW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW",
"N": "Š",
"NNE": "ŠŠR",
"NE": "ŠR",
"ENE": "RŠR",
"E": "R",
"ESE": "RPR",
"SE": "PR",
"SSE": "PPR",
"S": "P",
"SSW": "PPV",
"SW": "PV",
"WSW": "VPV",
"W": "V",
"WNW": "VŠV",
"NW": "ŠV",
"NNW": "ŠŠV",
"UPDATE_NOTIFICATION": "Galimas MagicMirror² naujinimas.",
"UPDATE_NOTIFICATION_MODULE": "Galimas {MODULE_NAME} naujinimas.",
"UPDATE_INFO_SINGLE": "Šis įdiegimas atsilieka {COMMIT_COUNT} įsipareigojimu {BRANCH_NAME} šakoje.",
"UPDATE_INFO_MULTIPLE": "Šis įdiegimas atsilieka {COMMIT_COUNT} įsipareigojimais {BRANCH_NAME} šakoje.",
"FEELS": "Jaučiasi kaip",
"FEELS": "Jutiminė temp.",
"PRECIP": "Krituliai"
}

37
translations/ps.json Normal file
View 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": "حس کېږی"
}

View File

@ -14,6 +14,7 @@ var translations = {
fy: "translations/fy.json", // Frysk
es: "translations/es.json", // Spanish
ca: "translations/ca.json", // Catalan
cv: "translations/cv.json", // Chuvash
nb: "translations/nb.json", // Norsk bokmål
nn: "translations/nn.json", // Norsk nynorsk
pt: "translations/pt.json", // Português
@ -25,7 +26,7 @@ var translations = {
"zh-tw": "translations/zh-tw.json", // Traditional Chinese
ja: "translations/ja.json", // Japanese
pl: "translations/pl.json", // Polish
gr: "translations/gr.json", // Greek
el: "translations/el.json", // Greek
da: "translations/da.json", // Danish
tr: "translations/tr.json", // Turkish
ru: "translations/ru.json", // Russian
@ -43,7 +44,9 @@ var translations = {
tlh: "translations/tlh.json", // Klingon
"ms-my": "translations/ms-my.json", // Malay
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") {

18
vendor/package-lock.json generated vendored
View File

@ -4,9 +4,9 @@
"lockfileVersion": 1,
"dependencies": {
"@fortawesome/fontawesome-free": {
"version": "5.14.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.14.0.tgz",
"integrity": "sha512-OfdMsF+ZQgdKHP9jUbmDcRrP0eX90XXrsXIdyjLbkmSBzmMXPABB8eobUJtivaupucYaByz6WNe1PI1JuYm3qA=="
"version": "5.15.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.1.tgz",
"integrity": "sha512-OEdH7SyC1suTdhBGW91/zBfR6qaIhThbcN8PUXtXilY4GYnSBbVqOntdHbC1vXwsDnX0Qix2m2+DSU1J51ybOQ=="
},
"a-sync-waterfall": {
"version": "1.0.1",
@ -119,14 +119,14 @@
"optional": true
},
"moment": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.28.0.tgz",
"integrity": "sha512-Z5KOjYmnHyd/ukynmFd/WwyXHd7L4J9vTI/nn5Ap9AVUgaAE15VvQ9MOGmJJygEUklupqIrFnor/tjTwRU+tQw=="
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"moment-timezone": {
"version": "0.5.31",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.31.tgz",
"integrity": "sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==",
"version": "0.5.32",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.32.tgz",
"integrity": "sha512-Z8QNyuQHQAmWucp8Knmgei8YNo28aLjJq6Ma+jy1ZSpSk5nyfRT8xgUbSQvD2+2UajISfenndwvFuH3NGS+nvA==",
"requires": {
"moment": ">= 2.9.0"
}

6
vendor/package.json vendored
View File

@ -10,9 +10,9 @@
"url": "https://github.com/MichMich/MagicMirror/issues"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.14.0",
"moment": "^2.28.0",
"moment-timezone": "^0.5.31",
"@fortawesome/fontawesome-free": "^5.15.1",
"moment": "^2.29.1",
"moment-timezone": "^0.5.32",
"nunjucks": "^3.2.2",
"suncalc": "^1.8.0",
"weathericons": "^2.1.0"