integrated onecall usage into existing types

This commit is contained in:
Bryan Zhu 2020-08-01 02:59:08 -04:00
commit b1a67d1fc5
48 changed files with 1526 additions and 1536 deletions

6
.gitignore vendored
View File

@ -1,5 +1,4 @@
# Various Node ignoramuses. # Various Node ignoramuses.
logs logs
*.log *.log
npm-debug.log* npm-debug.log*
@ -13,9 +12,11 @@ build/Release
/node_modules/**/* /node_modules/**/*
fonts/node_modules/**/* fonts/node_modules/**/*
vendor/node_modules/**/* vendor/node_modules/**/*
!/tests/node_modules/**/*
jspm_modules jspm_modules
.npm .npm
.node_repl_history .node_repl_history
.nyc_output/
# Visual Studio Code ignoramuses. # Visual Studio Code ignoramuses.
.vscode/ .vscode/
@ -53,7 +54,6 @@ Temporary Items
.apdisk .apdisk
# Various Linux ignoramuses. # Various Linux ignoramuses.
.fuse_hidden* .fuse_hidden*
.directory .directory
.Trash-* .Trash-*
@ -76,5 +76,3 @@ Temporary Items
*.orig *.orig
*.rej *.rej
*.bak *.bak
!/tests/node_modules/**/*

View File

@ -1,5 +1,4 @@
package-lock.json package-lock.json
/config/**/* /config/**/*
/modules/default/calendar/vendor/ical.js/**/*
/vendor/**/* /vendor/**/*
!/vendor/vendor.js !/vendor/vendor.js

View File

@ -11,20 +11,27 @@ _This release is scheduled to be released on 2020-10-01._
### Added ### Added
- Added current, hourly (max 48), and daily (max 7) weather forecasts to the default Weather module via the OpenWeatherMap One Call API - Test coverage with Istanbul, run it with `npm run test:coverage`.
- Add lithuanian language.
- Added support in weatherforecast for OpenWeather onecall API.
- Added config option to calendar-icons for recurring- and fullday-events
- Added current, hourly (max 48), and daily (max 7) weather forecasts to weather module via OpenWeatherMap One Call API
### Updated ### Updated
- Change incorrect weather.js default properties. - Change incorrect weather.js default properties.
- Cleaned up newsfeed module.
### Deleted ### Deleted
### Fixed ### Fixed
- Fix backward compatibility issues for Safari < 11. [#1985](https://github.com/MichMich/MagicMirror/issues/1985)
- Fix the use of "maxNumberOfDays" in the module "weatherforecast depending on the endpoint (forecast/daily or forecast)". [#2018](https://github.com/MichMich/MagicMirror/issues/2018) - Fix the use of "maxNumberOfDays" in the module "weatherforecast depending on the endpoint (forecast/daily or forecast)". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
- Fix calendar display. Account for current timezone. [#2068](https://github.com/MichMich/MagicMirror/issues/2068) - Fix calendar display. Account for current timezone. [#2068](https://github.com/MichMich/MagicMirror/issues/2068)
- Fix logLevel being set before loading config. - Fix logLevel being set before loading config.
- Fix incorrect namespace links in svg clockfaces. [#2072](https://github.com/MichMich/MagicMirror/issues/2072) - Fix incorrect namespace links in svg clockfaces. [#2072](https://github.com/MichMich/MagicMirror/issues/2072)
- Fix weather/providers/weathergov for API guidelines [#2045]
## [2.12.0] - 2020-07-01 ## [2.12.0] - 2020-07-01

View File

@ -19,7 +19,7 @@
root.Log = factory(root.config); root.Log = factory(root.config);
} }
})(this, function (config) { })(this, function (config) {
let logLevel = { const logLevel = {
info: Function.prototype.bind.call(console.info, console), info: Function.prototype.bind.call(console.info, console),
log: Function.prototype.bind.call(console.log, console), log: Function.prototype.bind.call(console.log, console),
error: Function.prototype.bind.call(console.error, console), error: Function.prototype.bind.call(console.error, console),

View File

@ -42,7 +42,7 @@ var MM = (function () {
dom.appendChild(moduleHeader); dom.appendChild(moduleHeader);
if (typeof module.getHeader() === "undefined" || module.getHeader() !== "") { if (typeof module.getHeader() === "undefined" || module.getHeader() !== "") {
moduleHeader.style = "display: none;"; moduleHeader.style.display = "none;";
} }
var moduleContent = document.createElement("div"); var moduleContent = document.createElement("div");
@ -216,7 +216,11 @@ var MM = (function () {
contentWrapper[0].appendChild(newContent); contentWrapper[0].appendChild(newContent);
headerWrapper[0].innerHTML = newHeader; headerWrapper[0].innerHTML = newHeader;
headerWrapper[0].style = headerWrapper.length > 0 && newHeader ? undefined : "display: none;"; if (headerWrapper.length > 0 && newHeader) {
delete headerWrapper[0].style;
} else {
headerWrapper[0].style.display = "none";
}
}; };
/* hideModule(module, speed, callback) /* hideModule(module, speed, callback)

View File

@ -19,92 +19,12 @@ var Translator = (function () {
xhr.open("GET", file, true); xhr.open("GET", file, true);
xhr.onreadystatechange = function () { xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) { if (xhr.readyState === 4 && xhr.status === 200) {
callback(JSON.parse(stripComments(xhr.responseText))); callback(JSON.parse(xhr.responseText));
} }
}; };
xhr.send(null); xhr.send(null);
} }
/* loadJSON(str, options)
* Remove any commenting from a json file so it can be parsed.
*
* argument str string - The string that contains json with comments.
* argument opts function - Strip options.
*
* return the stripped string.
*/
function stripComments(str, opts) {
// strip comments copied from: https://github.com/sindresorhus/strip-json-comments
var singleComment = 1;
var multiComment = 2;
function stripWithoutWhitespace() {
return "";
}
function stripWithWhitespace(str, start, end) {
return str.slice(start, end).replace(/\S/g, " ");
}
opts = opts || {};
var currentChar;
var nextChar;
var insideString = false;
var insideComment = false;
var offset = 0;
var ret = "";
var strip = opts.whitespace === false ? stripWithoutWhitespace : stripWithWhitespace;
for (var i = 0; i < str.length; i++) {
currentChar = str[i];
nextChar = str[i + 1];
if (!insideComment && currentChar === '"') {
var escaped = str[i - 1] === "\\" && str[i - 2] !== "\\";
if (!escaped) {
insideString = !insideString;
}
}
if (insideString) {
continue;
}
if (!insideComment && currentChar + nextChar === "//") {
ret += str.slice(offset, i);
offset = i;
insideComment = singleComment;
i++;
} else if (insideComment === singleComment && currentChar + nextChar === "\r\n") {
i++;
insideComment = false;
ret += strip(str, offset, i);
offset = i;
continue;
} else if (insideComment === singleComment && currentChar === "\n") {
insideComment = false;
ret += strip(str, offset, i);
offset = i;
} else if (!insideComment && currentChar + nextChar === "/*") {
ret += str.slice(offset, i);
offset = i;
insideComment = multiComment;
i++;
continue;
} else if (insideComment === multiComment && currentChar + nextChar === "*/") {
i++;
insideComment = false;
ret += strip(str, offset, i + 1);
offset = i + 1;
continue;
}
}
return ret + (insideComment ? strip(str.substr(offset)) : str.substr(offset));
}
return { return {
coreTranslations: {}, coreTranslations: {},
coreTranslationsFallback: {}, coreTranslationsFallback: {},

View File

@ -205,7 +205,7 @@ Module.register("calendar", {
eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url); eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
} }
eventWrapper.className = "normal"; eventWrapper.className = "normal event";
if (this.config.displaySymbol) { if (this.config.displaySymbol) {
var symbolWrapper = document.createElement("td"); var symbolWrapper = document.createElement("td");
@ -217,11 +217,7 @@ Module.register("calendar", {
var symbolClass = this.symbolClassForUrl(event.url); var symbolClass = this.symbolClassForUrl(event.url);
symbolWrapper.className = "symbol align-right " + symbolClass; symbolWrapper.className = "symbol align-right " + symbolClass;
var symbols = this.symbolsForUrl(event.url); var symbols = this.symbolsForEvent(event);
if (typeof symbols === "string") {
symbols = [symbols];
}
for (var i = 0; i < symbols.length; i++) { for (var i = 0; i < symbols.length; i++) {
var symbol = document.createElement("span"); var symbol = document.createElement("span");
symbol.className = "fa fa-fw fa-" + symbols[i]; symbol.className = "fa fa-fw fa-" + symbols[i];
@ -230,6 +226,7 @@ Module.register("calendar", {
} }
symbolWrapper.appendChild(symbol); symbolWrapper.appendChild(symbol);
} }
eventWrapper.appendChild(symbolWrapper); eventWrapper.appendChild(symbolWrapper);
} else if (this.config.timeFormat === "dateheaders") { } else if (this.config.timeFormat === "dateheaders") {
var blankCell = document.createElement("td"); var blankCell = document.createElement("td");
@ -559,15 +556,33 @@ Module.register("calendar", {
}, },
/** /**
* symbolsForUrl(url) * symbolsForEvent(event)
* Retrieves the symbols for a specific url. * Retrieves the symbols for a specific event.
* *
* argument url string - Url to look for. * argument event object - Event to look for.
* *
* return string/array - The Symbols * return array - The Symbols
*/ */
symbolsForUrl: function (url) { symbolsForEvent: function (event) {
return this.getCalendarProperty(url, "symbol", this.config.defaultSymbol); let symbols = this.getCalendarPropertyAsArray(event.url, "symbol", this.config.defaultSymbol);
if (event.recurringEvent === true && this.hasCalendarProperty(event.url, "recurringSymbol")) {
symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "recurringSymbol", this.config.defaultSymbol), symbols);
}
if (event.fullDayEvent === true && this.hasCalendarProperty(event.url, "fullDaySymbol")) {
symbols = this.mergeUnique(this.getCalendarPropertyAsArray(event.url, "fullDaySymbol", this.config.defaultSymbol), symbols);
}
return symbols;
},
mergeUnique: function (arr1, arr2) {
return arr1.concat(
arr2.filter(function (item) {
return arr1.indexOf(item) === -1;
})
);
}, },
/** /**
@ -659,6 +674,16 @@ Module.register("calendar", {
return defaultValue; return defaultValue;
}, },
getCalendarPropertyAsArray: function (url, property, defaultValue) {
let p = this.getCalendarProperty(url, property, defaultValue);
if (!(p instanceof Array)) p = [p];
return p;
},
hasCalendarProperty: function (url, property) {
return !!this.getCalendarProperty(url, property, undefined);
},
/** /**
* Shortens a string if it's longer than maxLength and add a ellipsis to the end * Shortens a string if it's longer than maxLength and add a ellipsis to the end
* *
@ -756,7 +781,7 @@ Module.register("calendar", {
var calendar = this.calendarData[url]; var calendar = this.calendarData[url];
for (var e in calendar) { for (var e in calendar) {
var event = cloneObject(calendar[e]); var event = cloneObject(calendar[e]);
event.symbol = this.symbolsForUrl(url); event.symbol = this.symbolsForEvent(event);
event.calendarName = this.calendarNameForUrl(url); event.calendarName = this.calendarNameForUrl(url);
event.color = this.colorForUrl(url); event.color = this.colorForUrl(url);
delete event.url; delete event.url;

View File

@ -9,7 +9,7 @@ const ical = require("ical");
const moment = require("moment"); const moment = require("moment");
const request = require("request"); const request = require("request");
const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNumberOfDays, auth, includePastEvents) { const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents) {
const self = this; const self = this;
let reloadTimer = null; let reloadTimer = null;
@ -254,6 +254,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
startDate: startDate.format("x"), startDate: startDate.format("x"),
endDate: endDate.format("x"), endDate: endDate.format("x"),
fullDayEvent: isFullDayEvent(event), fullDayEvent: isFullDayEvent(event),
recurringEvent: true,
class: event.class, class: event.class,
firstYear: event.start.getFullYear(), firstYear: event.start.getFullYear(),
location: location, location: location,
@ -317,7 +318,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumNu
return a.startDate - b.startDate; return a.startDate - b.startDate;
}); });
events = newEvents; events = newEvents.slice(0, maximumEntries);
self.broadcastEvents(); self.broadcastEvents();
scheduleTimer(); scheduleTimer();

View File

@ -20,7 +20,7 @@ module.exports = NodeHelper.create({
// Override socketNotificationReceived method. // Override socketNotificationReceived method.
socketNotificationReceived: function (notification, payload) { socketNotificationReceived: function (notification, payload) {
if (notification === "ADD_CALENDAR") { if (notification === "ADD_CALENDAR") {
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.id); this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.id);
} }
}, },
@ -31,7 +31,7 @@ module.exports = NodeHelper.create({
* attribute url string - URL of the news feed. * attribute url string - URL of the news feed.
* attribute reloadInterval number - Reload interval in milliseconds. * attribute reloadInterval number - Reload interval in milliseconds.
*/ */
createFetcher: function (url, fetchInterval, excludedEvents, maximumNumberOfDays, auth, broadcastPastEvents, identifier) { createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, identifier) {
var self = this; var self = this;
if (!validUrl.isUri(url)) { if (!validUrl.isUri(url)) {
@ -42,7 +42,7 @@ module.exports = NodeHelper.create({
var fetcher; var fetcher;
if (typeof self.fetchers[identifier + url] === "undefined") { if (typeof self.fetchers[identifier + url] === "undefined") {
Log.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval); Log.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval);
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumNumberOfDays, auth, broadcastPastEvents); fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents);
fetcher.onReceive(function (fetcher) { fetcher.onReceive(function (fetcher) {
self.sendSocketNotification("CALENDAR_EVENTS", { self.sendSocketNotification("CALENDAR_EVENTS", {

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -84,11 +84,11 @@ Module.register("newsfeed", {
// Override dom generator. // Override dom generator.
getDom: function () { getDom: function () {
var wrapper = document.createElement("div"); const wrapper = document.createElement("div");
if (this.config.feedUrl) { if (this.config.feedUrl) {
wrapper.className = "small bright"; wrapper.className = "small bright";
wrapper.innerHTML = this.translate("configuration_changed"); wrapper.innerHTML = this.translate("MODULE_CONFIG_CHANGED", { MODULE_NAME: "Newsfeed" });
return wrapper; return wrapper;
} }
@ -99,7 +99,7 @@ Module.register("newsfeed", {
if (this.newsItems.length > 0) { if (this.newsItems.length > 0) {
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications // this.config.showFullArticle is a run-time configuration, triggered by optional notifications
if (!this.config.showFullArticle && (this.config.showSourceTitle || this.config.showPublishDate)) { if (!this.config.showFullArticle && (this.config.showSourceTitle || this.config.showPublishDate)) {
var sourceAndTimestamp = document.createElement("div"); const sourceAndTimestamp = document.createElement("div");
sourceAndTimestamp.className = "newsfeed-source light small dimmed"; sourceAndTimestamp.className = "newsfeed-source light small dimmed";
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") { if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") {
@ -157,22 +157,22 @@ Module.register("newsfeed", {
} }
if (!this.config.showFullArticle) { if (!this.config.showFullArticle) {
var title = document.createElement("div"); const title = document.createElement("div");
title.className = "newsfeed-title bright medium light" + (!this.config.wrapTitle ? " no-wrap" : ""); title.className = "newsfeed-title bright medium light" + (!this.config.wrapTitle ? " no-wrap" : "");
title.innerHTML = this.newsItems[this.activeItem].title; title.innerHTML = this.newsItems[this.activeItem].title;
wrapper.appendChild(title); wrapper.appendChild(title);
} }
if (this.isShowingDescription) { if (this.isShowingDescription) {
var description = document.createElement("div"); const description = document.createElement("div");
description.className = "newsfeed-desc small light" + (!this.config.wrapDescription ? " no-wrap" : ""); description.className = "newsfeed-desc small light" + (!this.config.wrapDescription ? " no-wrap" : "");
var txtDesc = this.newsItems[this.activeItem].description; const txtDesc = this.newsItems[this.activeItem].description;
description.innerHTML = this.config.truncDescription ? (txtDesc.length > this.config.lengthDescription ? txtDesc.substring(0, this.config.lengthDescription) + "..." : txtDesc) : txtDesc; description.innerHTML = this.config.truncDescription ? (txtDesc.length > this.config.lengthDescription ? txtDesc.substring(0, this.config.lengthDescription) + "..." : txtDesc) : txtDesc;
wrapper.appendChild(description); wrapper.appendChild(description);
} }
if (this.config.showFullArticle) { if (this.config.showFullArticle) {
var fullArticle = document.createElement("iframe"); const fullArticle = document.createElement("iframe");
fullArticle.className = ""; fullArticle.className = "";
fullArticle.style.width = "100vw"; fullArticle.style.width = "100vw";
// very large height value to allow scrolling // very large height value to allow scrolling
@ -332,17 +332,6 @@ Module.register("newsfeed", {
}, this.config.updateInterval); }, this.config.updateInterval);
}, },
/* capitalizeFirstLetter(string)
* Capitalizes the first character of a string.
*
* argument string string - Input string.
*
* return string - Capitalized output string.
*/
capitalizeFirstLetter: function (string) {
return string.charAt(0).toUpperCase() + string.slice(1);
},
resetDescrOrFullArticleAndTimer: function () { resetDescrOrFullArticleAndTimer: function () {
this.isShowingDescription = this.config.showDescription; this.isShowingDescription = this.config.showDescription;
this.config.showFullArticle = false; this.config.showFullArticle = false;
@ -356,7 +345,7 @@ Module.register("newsfeed", {
}, },
notificationReceived: function (notification, payload, sender) { notificationReceived: function (notification, payload, sender) {
var before = this.activeItem; const before = this.activeItem;
if (notification === "ARTICLE_NEXT") { if (notification === "ARTICLE_NEXT") {
this.activeItem++; this.activeItem++;
if (this.activeItem >= this.newsItems.length) { if (this.activeItem >= this.newsItems.length) {

View File

@ -1,10 +1,9 @@
/* Magic Mirror /* Magic Mirror
* Fetcher * Node Helper: Newsfeed - NewsfeedFetcher
* *
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
const Log = require("../../../js/logger.js"); const Log = require("../../../js/logger.js");
const FeedMe = require("feedme"); const FeedMe = require("feedme");
const request = require("request"); const request = require("request");
@ -17,39 +16,39 @@ const iconv = require("iconv-lite");
* attribute reloadInterval number - Reload interval in milliseconds. * attribute reloadInterval number - Reload interval in milliseconds.
* attribute logFeedWarnings boolean - Log warnings when there is an error parsing a news article. * attribute logFeedWarnings boolean - Log warnings when there is an error parsing a news article.
*/ */
const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
const self = this;
let reloadTimer = null;
let items = [];
let fetchFailedCallback = function () {};
let itemsReceivedCallback = function () {};
var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
var self = this;
if (reloadInterval < 1000) { if (reloadInterval < 1000) {
reloadInterval = 1000; reloadInterval = 1000;
} }
var reloadTimer = null;
var items = [];
var fetchFailedCallback = function () {};
var itemsReceivedCallback = function () {};
/* private methods */ /* private methods */
/* fetchNews() /* fetchNews()
* Request the new items. * Request the new items.
*/ */
var fetchNews = function () { const fetchNews = function () {
clearTimeout(reloadTimer); clearTimeout(reloadTimer);
reloadTimer = null; reloadTimer = null;
items = []; items = [];
var parser = new FeedMe(); const parser = new FeedMe();
parser.on("item", function (item) { parser.on("item", function (item) {
var title = item.title; const title = item.title;
var description = item.description || item.summary || item.content || ""; let description = item.description || item.summary || item.content || "";
var pubdate = item.pubdate || item.published || item.updated || item["dc:date"]; const pubdate = item.pubdate || item.published || item.updated || item["dc:date"];
var url = item.url || item.link || ""; const url = item.url || item.link || "";
if (title && pubdate) { if (title && pubdate) {
var regex = /(<([^>]+)>)/gi; const regex = /(<([^>]+)>)/gi;
description = description.toString().replace(regex, ""); description = description.toString().replace(regex, "");
items.push({ items.push({
@ -77,10 +76,17 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
scheduleTimer(); scheduleTimer();
}); });
var nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
var headers = { "User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)", "Cache-Control": "max-age=0, no-cache, no-store, must-revalidate", Pragma: "no-cache" }; const opts = {
headers: {
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)",
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
Pragma: "no-cache"
},
encoding: null
};
request({ uri: url, encoding: null, headers: headers }) request(url, opts)
.on("error", function (error) { .on("error", function (error) {
fetchFailedCallback(self, error); fetchFailedCallback(self, error);
scheduleTimer(); scheduleTimer();
@ -92,7 +98,7 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
/* scheduleTimer() /* scheduleTimer()
* Schedule the timer for the next update. * Schedule the timer for the next update.
*/ */
var scheduleTimer = function () { const scheduleTimer = function () {
clearTimeout(reloadTimer); clearTimeout(reloadTimer);
reloadTimer = setTimeout(function () { reloadTimer = setTimeout(function () {
fetchNews(); fetchNews();
@ -148,4 +154,4 @@ var Fetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
}; };
}; };
module.exports = Fetcher; module.exports = NewsfeedFetcher;

View File

@ -7,7 +7,7 @@
const NodeHelper = require("node_helper"); const NodeHelper = require("node_helper");
const validUrl = require("valid-url"); const validUrl = require("valid-url");
const Fetcher = require("./fetcher.js"); const NewsfeedFetcher = require("./newsfeedfetcher.js");
const Log = require("../../../js/logger"); const Log = require("../../../js/logger");
module.exports = NodeHelper.create({ module.exports = NodeHelper.create({
@ -32,37 +32,35 @@ module.exports = NodeHelper.create({
* attribute config object - A configuration object containing reload interval in milliseconds. * attribute config object - A configuration object containing reload interval in milliseconds.
*/ */
createFetcher: function (feed, config) { createFetcher: function (feed, config) {
var self = this; const url = feed.url || "";
const encoding = feed.encoding || "UTF-8";
var url = feed.url || ""; const reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
var encoding = feed.encoding || "UTF-8";
var reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
if (!validUrl.isUri(url)) { if (!validUrl.isUri(url)) {
self.sendSocketNotification("INCORRECT_URL", url); this.sendSocketNotification("INCORRECT_URL", url);
return; return;
} }
var fetcher; let fetcher;
if (typeof self.fetchers[url] === "undefined") { if (typeof this.fetchers[url] === "undefined") {
Log.log("Create new news fetcher for url: " + url + " - Interval: " + reloadInterval); Log.log("Create new news fetcher for url: " + url + " - Interval: " + reloadInterval);
fetcher = new Fetcher(url, reloadInterval, encoding, config.logFeedWarnings); fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings);
fetcher.onReceive(function (fetcher) { fetcher.onReceive(() => {
self.broadcastFeeds(); this.broadcastFeeds();
}); });
fetcher.onError(function (fetcher, error) { fetcher.onError((fetcher, error) => {
self.sendSocketNotification("FETCH_ERROR", { this.sendSocketNotification("FETCH_ERROR", {
url: fetcher.url(), url: fetcher.url(),
error: error error: error
}); });
}); });
self.fetchers[url] = fetcher; this.fetchers[url] = fetcher;
} else { } else {
Log.log("Use existing news fetcher for url: " + url); Log.log("Use existing news fetcher for url: " + url);
fetcher = self.fetchers[url]; fetcher = this.fetchers[url];
fetcher.setReloadInterval(reloadInterval); fetcher.setReloadInterval(reloadInterval);
fetcher.broadcastItems(); fetcher.broadcastItems();
} }

View File

@ -1,3 +0,0 @@
{
"configuration_changed": "Die Konfigurationsoptionen für das Newsfeed-Modul haben sich geändert. \nBitte überprüfen Sie die Dokumentation."
}

View File

@ -1,3 +0,0 @@
{
"configuration_changed": "The configuration options for the newsfeed module have changed.\nPlease check the documentation."
}

View File

@ -1,3 +0,0 @@
{
"configuration_changed": "Las opciones de configuración para el módulo de suministro de noticias han cambiado. \nVerifique la documentación."
}

View File

@ -1,3 +0,0 @@
{
"configuration_changed": "Les options de configuration du module newsfeed ont changé. \nVeuillez consulter la documentation."
}

View File

@ -1,4 +1,7 @@
{% if current %} {% if current or weatherData %}
{% if weatherData %}
{% set current = weatherData.current %}
{% endif %}
{% if not config.onlyTemp %} {% if not config.onlyTemp %}
<div class="normal medium"> <div class="normal medium">
<span class="wi wi-strong-wind dimmed"></span> <span class="wi wi-strong-wind dimmed"></span>

View File

@ -1,4 +1,7 @@
{% if forecast %} {% if forecast or weatherData %}
{% if weatherData %}
{% set forecast = weatherData.days %}
{% endif %}
{% set numSteps = forecast | calcNumSteps %} {% set numSteps = forecast | calcNumSteps %}
{% set currentStep = 0 %} {% set currentStep = 0 %}
<table class="{{ config.tableClass }}"> <table class="{{ config.tableClass }}">

View File

@ -1,8 +1,11 @@
{% if wData %} {% if hourly or weatherData %}
{% set numSteps = wData.hours | calcNumEntries %} {% if weatherData %}
{% set hourly = weatherData.hours %}
{% endif %}
{% set numSteps = hourly | calcNumEntries %}
{% set currentStep = 0 %} {% set currentStep = 0 %}
<table class="{{ config.tableClass }}"> <table class="{{ config.tableClass }}">
{% set hours = wData.hours.slice(0, numSteps) %} {% set hours = hourly.slice(0, numSteps) %}
{% for hour in hours %} {% for hour in hours %}
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}> <tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
<td class="day">{{ hour.date | formatTime }}</td> <td class="day">{{ hour.date | formatTime }}</td>
@ -25,5 +28,5 @@
</div> </div>
{% endif %} {% endif %}
<!-- Uncomment the line below to see the contents of the `wData` object. --> <!-- Uncomment the line below to see the contents of the `hourly` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{wData | dump}}</div> --> <!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{weatherData | dump}}</div> -->

View File

@ -68,8 +68,8 @@ WeatherProvider.register("openweathermap", {
this.setFetchedLocation(`(${data.lat},${data.lon})`); this.setFetchedLocation(`(${data.lat},${data.lon})`);
const wData = this.generateWeatherObjectsFromOnecall(data); const weatherData = this.generateWeatherObjectsFromOnecall(data);
this.setWeatherData(wData); this.setWeatherData(weatherData);
}) })
.catch(function (request) { .catch(function (request) {
Log.error("Could not load data ... ", request); Log.error("Could not load data ... ", request);
@ -124,8 +124,8 @@ WeatherProvider.register("openweathermap", {
return this.fetchOnecall(data); return this.fetchOnecall(data);
} }
// if weatherEndpoint does not match onecall, what should be returned? // if weatherEndpoint does not match onecall, what should be returned?
const wData = {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), hours: [], days: []};
return wData; return weatherData;
}, },
/* /*
@ -415,11 +415,11 @@ WeatherProvider.register("openweathermap", {
if (this.config.weatherEndpoint === "/onecall") { if (this.config.weatherEndpoint === "/onecall") {
params += "lat=" + this.config.lat; params += "lat=" + this.config.lat;
params += "&lon=" + this.config.lon; params += "&lon=" + this.config.lon;
if (this.config.type === "wDataCurrent") { if (this.config.type === "current") {
params += "&exclude=minutely,hourly,daily"; params += "&exclude=minutely,hourly,daily";
} else if (this.config.type === "wDataHourly") { } else if (this.config.type === "hourly") {
params += "&exclude=current,minutely,daily"; params += "&exclude=current,minutely,daily";
} else if (this.config.type === "wDataDaily") { } else if (this.config.type === "daily" || this.config.type === "forecast") {
params += "&exclude=current,minutely,hourly"; params += "&exclude=current,minutely,hourly";
} else { } else {
params += "&exclude=minutely"; params += "&exclude=minutely";

View File

@ -3,76 +3,155 @@
/* Magic Mirror /* Magic Mirror
* Module: Weather * Module: Weather
* Provider: weather.gov * Provider: weather.gov
* https://weather-gov.github.io/api/general-faqs
* *
* By Vince Peri * Original by Vince Peri
* MIT Licensed. * MIT Licensed.
* *
* This class is a provider for weather.gov. * This class is a provider for weather.gov.
* Note that this is only for US locations (lat and lon) and does not require an API key * Note that this is only for US locations (lat and lon) and does not require an API key
* Since it is free, there are some items missing - like sunrise, sunset, humidity, etc. * Since it is free, there are some items missing - like sunrise, sunset
*/ */
WeatherProvider.register("weathergov", { WeatherProvider.register("weathergov", {
// Set the name of the provider. // Set the name of the provider.
// This isn't strictly necessary, since it will fallback to the provider identifier // This isn't strictly necessary, since it will fallback to the provider identifier
// But for debugging (and future alerts) it would be nice to have the real name. // But for debugging (and future alerts) it would be nice to have the real name.
providerName: "Weather.gov", providerName: "Weather.gov",
// Flag all needed URLs availability
configURLs: false,
//This API has multiple urls involved
forecastURL: "tbd",
forecastHourlyURL: "tbd",
forecastGridDataURL: "tbd",
observationStationsURL: "tbd",
stationObsURL: "tbd",
// Called to set the config, this config is the same as the weather module's config.
setConfig: function (config) {
this.config = config;
(this.config.apiBase = "https://api.weather.gov"), this.fetchWxGovURLs(this.config);
},
// Called when the weather provider is about to start.
start: function () {
Log.info(`Weather provider: ${this.providerName} started.`);
},
// This returns the name of the fetched location or an empty string.
fetchedLocation: function () {
return this.fetchedLocationName || "";
},
// Overwrite the fetchCurrentWeather method. // Overwrite the fetchCurrentWeather method.
fetchCurrentWeather() { fetchCurrentWeather() {
this.fetchData(this.getUrl()) if (!this.configURLs) {
.then((data) => { Log.info("fetch wx waiting on config URLs");
if (!data || !data.properties || !data.properties.periods || !data.properties.periods.length) {
// Did not receive usable new data.
// Maybe this needs a better check?
return; return;
} }
this.fetchData(this.stationObsURL)
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data.properties.periods[0]); .then((data) => {
if (!data || !data.properties) {
// Did not receive usable new data.
return;
}
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data.properties);
this.setCurrentWeather(currentWeather); this.setCurrentWeather(currentWeather);
}) })
.catch(function (request) { .catch(function (request) {
Log.error("Could not load data ... ", request); Log.error("Could not load station obs data ... ", request);
}) })
.finally(() => this.updateAvailable()); .finally(() => this.updateAvailable());
}, },
// Overwrite the fetchCurrentWeather method. // Overwrite the fetchWeatherForecast method.
fetchWeatherForecast() { fetchWeatherForecast() {
this.fetchData(this.getUrl()) if (!this.configURLs) {
Log.info("fetch wx waiting on config URLs");
return;
}
this.fetchData(this.forecastURL)
.then((data) => { .then((data) => {
if (!data || !data.properties || !data.properties.periods || !data.properties.periods.length) { if (!data || !data.properties || !data.properties.periods || !data.properties.periods.length) {
// Did not receive usable new data. // Did not receive usable new data.
// Maybe this needs a better check?
return; return;
} }
const forecast = this.generateWeatherObjectsFromForecast(data.properties.periods); const forecast = this.generateWeatherObjectsFromForecast(data.properties.periods);
this.setWeatherForecast(forecast); this.setWeatherForecast(forecast);
}) })
.catch(function (request) { .catch(function (request) {
Log.error("Could not load data ... ", request); Log.error("Could not load forecast hourly data ... ", request);
}) })
.finally(() => this.updateAvailable()); .finally(() => this.updateAvailable());
}, },
/** Weather.gov Specific Methods - These are not part of the default provider methods */ /** Weather.gov Specific Methods - These are not part of the default provider methods */
/* /*
* Gets the complete url for the request * Get specific URLs
*/ */
getUrl() { fetchWxGovURLs(config) {
return this.config.apiBase + this.config.lat + "," + this.config.lon + this.config.weatherEndpoint; this.fetchData(`${config.apiBase}/points/${config.lat},${config.lon}`)
.then((data) => {
if (!data || !data.properties) {
// points URL did not respond with usable data.
return;
}
this.fetchedLocationName = data.properties.relativeLocation.properties.city + ", " + data.properties.relativeLocation.properties.state;
Log.log("Forecast location is " + this.fetchedLocationName);
this.forecastURL = data.properties.forecast;
this.forecastHourlyURL = data.properties.forecastHourly;
this.forecastGridDataURL = data.properties.forecastGridData;
this.observationStationsURL = data.properties.observationStations;
// with this URL, we chain another promise for the station obs URL
return this.fetchData(data.properties.observationStations);
})
.then((obsData) => {
if (!obsData || !obsData.features) {
// obs station URL did not respond with usable data.
return;
}
this.stationObsURL = obsData.features[0].id + "/observations/latest";
})
.catch((err) => {
Log.error(err);
})
.finally(() => {
// excellent, let's fetch some actual wx data
this.configURLs = true;
this.fetchCurrentWeather();
});
}, },
/* /*
* Generate a WeatherObject based on currentWeatherInformation * Generate a WeatherObject based on currentWeatherInformation
* Weather.gov API uses specific units; API does not include choice of units
* ... object needs data in units based on config!
*/ */
generateWeatherObjectFromCurrentWeather(currentWeatherData) { generateWeatherObjectFromCurrentWeather(currentWeatherData) {
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits); const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
currentWeather.temperature = currentWeatherData.temperature; currentWeather.date = moment(currentWeatherData.timestamp);
currentWeather.windSpeed = currentWeatherData.windSpeed.split(" ", 1); currentWeather.temperature = this.convertTemp(currentWeatherData.temperature.value);
currentWeather.windDirection = this.convertWindDirection(currentWeatherData.windDirection); currentWeather.windSpeed = this.covertSpeed(currentWeatherData.windSpeed.value);
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.shortForecast, currentWeatherData.isDaytime); currentWeather.windDirection = currentWeatherData.windDirection.value;
currentWeather.minTemperature = this.convertTemp(currentWeatherData.minTemperatureLast24Hours.value);
currentWeather.maxTemperature = this.convertTemp(currentWeatherData.maxTemperatureLast24Hours.value);
currentWeather.humidity = Math.round(currentWeatherData.relativeHumidity.value);
currentWeather.rain = null;
currentWeather.snow = null;
currentWeather.precipitation = this.convertLength(currentWeatherData.precipitationLastHour.value);
currentWeather.feelsLikeTemp = this.convertTemp(currentWeatherData.heatIndex.value);
let isDaytime = true;
if (currentWeatherData.icon.includes("day")) {
isDaytime = true;
} else {
isDaytime = false;
}
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.textDescription, isDaytime);
// determine the sunrise/sunset times - not supplied in weather.gov data // determine the sunrise/sunset times - not supplied in weather.gov data
let times = this.calcAstroData(this.config.lat, this.config.lon); let times = this.calcAstroData(this.config.lat, this.config.lon);
@ -124,7 +203,7 @@ WeatherProvider.register("weathergov", {
// specify date // specify date
weather.date = moment(forecast.startTime); weather.date = moment(forecast.startTime);
// If the first value of today is later than 17:00, we have an icon at least! // use the forecast isDayTime attribute to help build the weatherType label
weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime); weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime);
} }
@ -148,6 +227,34 @@ WeatherProvider.register("weathergov", {
return days.slice(1); return days.slice(1);
}, },
/*
* Unit conversions
*/
// conversion to fahrenheit
convertTemp(temp) {
if (this.config.tempUnits === "imperial") {
return (9 / 5) * temp + 32;
} else {
return temp;
}
},
// conversion to mph
covertSpeed(metSec) {
if (this.config.windUnits === "imperial") {
return metSec * 2.23694;
} else {
return metSec;
}
},
// conversion to inches
convertLength(meters) {
if (this.config.units === "imperial") {
return meters * 39.3701;
} else {
return meters;
}
},
/* /*
* Calculate the astronomical data * Calculate the astronomical data
*/ */

View File

@ -1,83 +0,0 @@
{% if wData %}
{% set current = wData.current %}
{% if not config.onlyTemp %}
<div class="normal medium">
<span class="wi wi-strong-wind dimmed"></span>
<span>
{% if config.useBeaufort %}
{{ current.beaufortWindSpeed() | round }}
{% else %}
{{ current.windSpeed | round }}
{% endif %}
{% if config.showWindDirection %}
<sup>
{% if config.showWindDirectionAsArrow %}
<i class="fa fa-long-arrow-up" style="transform:rotate({{ current.windDirection }}deg);"></i>
{% else %}
{{ current.cardinalWindDirection() | translate }}
{% endif %}
&nbsp;
</sup>
{% endif %}
</span>
{% if config.showHumidity and current.humidity %}
<span>{{ current.humidity | decimalSymbol }}</span><sup>&nbsp;<i class="wi wi-humidity humidityIcon"></i></sup>
{% endif %}
{% if config.showSun %}
<span class="wi dimmed wi-{{ current.nextSunAction() }}"></span>
<span>
{% if current.nextSunAction() === "sunset" %}
{{ current.sunset | formatTime }}
{% else %}
{{ current.sunrise | formatTime }}
{% endif %}
</span>
{% endif %}
</div>
{% endif %}
<div class="large light">
<span class="wi weathericon wi-{{current.weatherType}}"></span>
<span class="bright">
{{ current.temperature | roundValue | unit("temperature") | decimalSymbol }}
</span>
</div>
<div class="normal light indoor">
{% if config.showIndoorTemperature and indoor.temperature %}
<div>
<span class="fa fa-home"></span>
<span class="bright">
{{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }}
</span>
</div>
{% endif %}
{% if config.showIndoorHumidity and indoor.humidity %}
<div>
<span class="fa fa-tint"></span>
<span class="bright">
{{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }}
</span>
</div>
{% endif %}
</div>
{% if (config.showFeelsLike or config.showPrecipitationAmount) and not config.onlyTemp %}
<div class="normal medium">
{% if config.showFeelsLike %}
<span class="dimmed">
{{ "FEELS" | translate }} {{ current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }}
</span>
{% endif %}
{% if config.showPrecipitationAmount %}
<span class="dimmed">
{{ "PRECIP" | translate }} {{ current.precipitation | unit("precip") }}
</span>
{% endif %}
</div>
{% endif %}
{% else %}
<div class="dimmed light small">
{{ "LOADING" | translate | safe }}
</div>
{% endif %}
<!-- Uncomment the line below to see the contents of the `wData` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{wData | dump}}</div> -->

View File

@ -1,32 +0,0 @@
{% if wData %}
{% set numSteps = wData.days | calcNumEntries %}
{% set currentStep = 0 %}
<table class="{{ config.tableClass }}">
{% set days = wData.days.slice(0, numSteps) %}
{% for day in days %}
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
<td class="day">{{ day.date.format('ddd') }}</td>
<td class="bright weather-icon"><span class="wi weathericon wi-{{ day.weatherType }}"></span></td>
<td class="align-right bright max-temp">
{{ day.maxTemperature | roundValue | unit("temperature") }}
</td>
<td class="align-right min-temp">
{{ day.minTemperature | roundValue | unit("temperature") }}
</td>
{% if config.showPrecipitationAmount %}
<td class="align-right bright precipitation">
{{ day.precipitation | unit("precip") }}
</td>
{% endif %}
</tr>
{% set currentStep = currentStep + 1 %}
{% endfor %}
</table>
{% else %}
<div class="dimmed light small">
{{ "LOADING" | translate | safe }}
</div>
{% endif %}
<!-- Uncomment the line below to see the contents of the `wData` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{wData | dump}}</div> -->

View File

@ -11,7 +11,7 @@ Module.register("weather", {
defaults: { defaults: {
weatherProvider: "openweathermap", weatherProvider: "openweathermap",
roundTemp: false, roundTemp: false,
type: "current", //current, forecast, wDataCurrent, wDataHourly, wDataDaily type: "current",
lat: 0, lat: 0,
lon: 0, lon: 0,
@ -127,6 +127,9 @@ Module.register("weather", {
// Select the template depending on the display type. // Select the template depending on the display type.
getTemplate: function () { getTemplate: function () {
if (this.config.type === "daily") {
return `forecast.njk`;
}
return `${this.config.type.toLowerCase()}.njk`; return `${this.config.type.toLowerCase()}.njk`;
}, },
@ -136,7 +139,7 @@ Module.register("weather", {
config: this.config, config: this.config,
current: this.weatherProvider.currentWeather(), current: this.weatherProvider.currentWeather(),
forecast: this.weatherProvider.weatherForecast(), forecast: this.weatherProvider.weatherForecast(),
wData: this.weatherProvider.weatherData(), weatherData: this.weatherProvider.weatherData(),
indoor: { indoor: {
humidity: this.indoorHumidity, humidity: this.indoorHumidity,
temperature: this.indoorTemperature temperature: this.indoorTemperature
@ -158,12 +161,12 @@ Module.register("weather", {
} }
setTimeout(() => { setTimeout(() => {
if (this.config.type === "forecast") { if (this.config.weatherEndpoint === "/onecall") {
this.weatherProvider.fetchWeatherForecast();
} else if (this.config.type === "current") {
this.weatherProvider.fetchCurrentWeather();
} else {
this.weatherProvider.fetchWeatherData(); this.weatherProvider.fetchWeatherData();
} else if (this.config.type === "forecast") {
this.weatherProvider.fetchWeatherForecast();
} else {
this.weatherProvider.fetchCurrentWeather();
} }
}, nextLoad); }, nextLoad);
}, },

View File

@ -9,6 +9,8 @@ Module.register("weatherforecast", {
defaults: { defaults: {
location: false, location: false,
locationID: false, locationID: false,
lat: false,
lon: false,
appid: "", appid: "",
units: config.units, units: config.units,
maxNumberOfDays: 7, maxNumberOfDays: 7,
@ -29,6 +31,7 @@ Module.register("weatherforecast", {
apiVersion: "2.5", apiVersion: "2.5",
apiBase: "https://api.openweathermap.org/data/", apiBase: "https://api.openweathermap.org/data/",
forecastEndpoint: "forecast/daily", forecastEndpoint: "forecast/daily",
excludes: false,
appendLocationNameToHeader: true, appendLocationNameToHeader: true,
calendarClass: "calendar", calendarClass: "calendar",
@ -283,6 +286,8 @@ Module.register("weatherforecast", {
var params = "?"; var params = "?";
if (this.config.locationID) { if (this.config.locationID) {
params += "id=" + this.config.locationID; params += "id=" + this.config.locationID;
} else if (this.config.lat && this.config.lon) {
params += "lat=" + this.config.lat + "&lon=" + this.config.lon;
} else if (this.config.location) { } else if (this.config.location) {
params += "q=" + this.config.location; params += "q=" + this.config.location;
} else if (this.firstEvent && this.firstEvent.geo) { } else if (this.firstEvent && this.firstEvent.geo) {
@ -297,13 +302,14 @@ Module.register("weatherforecast", {
let numberOfDays; let numberOfDays;
if (this.config.forecastEndpoint === "forecast") { if (this.config.forecastEndpoint === "forecast") {
numberOfDays = this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 5 ? 5 : this.config.maxNumberOfDays; numberOfDays = this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 5 ? 5 : this.config.maxNumberOfDays;
// don't get forecasts for the 6th day, as it would not represent the whole day // don't get forecasts for the next day, as it would not represent the whole day
numberOfDays = numberOfDays * 8 - (Math.floor(new Date().getHours() / 3) % 8); numberOfDays = numberOfDays * 8 - (Math.round(new Date().getHours() / 3) % 8);
} else { } else {
numberOfDays = this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 17 ? 7 : this.config.maxNumberOfDays; numberOfDays = this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 17 ? 7 : this.config.maxNumberOfDays;
} }
params += "&cnt=" + numberOfDays; params += "&cnt=" + numberOfDays;
params += "&exclude=" + this.config.excludes;
params += "&units=" + this.config.units; params += "&units=" + this.config.units;
params += "&lang=" + this.config.lang; params += "&lang=" + this.config.lang;
params += "&APPID=" + this.config.appid; params += "&APPID=" + this.config.appid;
@ -331,15 +337,34 @@ Module.register("weatherforecast", {
* argument data object - Weather information received form openweather.org. * argument data object - Weather information received form openweather.org.
*/ */
processWeather: function (data) { processWeather: function (data) {
// Forcast16 (paid) API endpoint provides this data. Onecall endpoint
// does not.
if (data.city) {
this.fetchedLocationName = data.city.name + ", " + data.city.country; this.fetchedLocationName = data.city.name + ", " + data.city.country;
} else if (this.config.location) {
this.fetchedLocationName = this.config.location;
} else {
this.fetchedLocationName = "Unknown";
}
this.forecast = []; this.forecast = [];
var lastDay = null; var lastDay = null;
var forecastData = {}; var forecastData = {};
for (var i = 0, count = data.list.length; i < count; i++) { // Handle different structs between forecast16 and onecall endpoints
var forecast = data.list[i]; var forecastList = null;
this.parserDataWeather(forecast); // hack issue #1017 if (data.list) {
forecastList = data.list;
} else if (data.daily) {
forecastList = data.daily;
} else {
Log.error("Unexpected forecast data");
return undefined;
}
for (var i = 0, count = forecastList.length; i < count; i++) {
var forecast = forecastList[i];
forecast = this.parserDataWeather(forecast); // hack issue #1017
var day; var day;
var hour; var hour;
@ -357,7 +382,7 @@ Module.register("weatherforecast", {
icon: this.config.iconTable[forecast.weather[0].icon], icon: this.config.iconTable[forecast.weather[0].icon],
maxTemp: this.roundValue(forecast.temp.max), maxTemp: this.roundValue(forecast.temp.max),
minTemp: this.roundValue(forecast.temp.min), minTemp: this.roundValue(forecast.temp.min),
rain: this.processRain(forecast, data.list) rain: this.processRain(forecast, forecastList)
}; };
this.forecast.push(forecastData); this.forecast.push(forecastData);

941
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,9 +9,10 @@
"install": "echo \"Installing vendor files ...\n\" && cd vendor && npm install --loglevel=error", "install": "echo \"Installing vendor files ...\n\" && cd vendor && npm install --loglevel=error",
"install-fonts": "echo \"Installing fonts ...\n\" && cd fonts && npm install --loglevel=error", "install-fonts": "echo \"Installing fonts ...\n\" && cd fonts && npm install --loglevel=error",
"postinstall": "npm run install-fonts && echo \"MagicMirror installation finished successfully! \n\"", "postinstall": "npm run install-fonts && echo \"MagicMirror installation finished successfully! \n\"",
"test": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests --recursive", "test": "NODE_ENV=test mocha tests --recursive",
"test:unit": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests/unit --recursive", "test:coverage": "NODE_ENV=test nyc mocha tests --recursive --timeout=3000",
"test:e2e": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests/e2e --recursive", "test:e2e": "NODE_ENV=test mocha tests/e2e --recursive",
"test:unit": "NODE_ENV=test mocha tests/unit --recursive",
"test:prettier": "prettier --check **/*.{js,css,json,md,yml}", "test:prettier": "prettier --check **/*.{js,css,json,md,yml}",
"test:js": "eslint *.js js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --quiet", "test:js": "eslint *.js js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --quiet",
"test:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json", "test:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json",
@ -54,6 +55,7 @@
"mocha": "^7.1.2", "mocha": "^7.1.2",
"mocha-each": "^2.0.1", "mocha-each": "^2.0.1",
"mocha-logger": "^1.0.6", "mocha-logger": "^1.0.6",
"nyc": "^15.1.0",
"prettier": "^2.0.5", "prettier": "^2.0.5",
"pretty-quick": "^2.0.1", "pretty-quick": "^2.0.1",
"spectron": "^8.0.0", "spectron": "^8.0.0",
@ -68,18 +70,18 @@
"dependencies": { "dependencies": {
"colors": "^1.1.2", "colors": "^1.1.2",
"console-stamp": "^0.2.9", "console-stamp": "^0.2.9",
"eslint": "^7.3.0", "eslint": "^7.4.0",
"express": "^4.16.2", "express": "^4.16.2",
"express-ipfilter": "^1.0.1", "express-ipfilter": "^1.0.1",
"feedme": "latest", "feedme": "latest",
"helmet": "^3.21.2", "helmet": "^3.23.3",
"ical": "^0.8.0", "ical": "^0.8.0",
"iconv-lite": "latest", "iconv-lite": "latest",
"lodash": "^4.17.15", "lodash": "^4.17.19",
"module-alias": "^2.2.2", "module-alias": "^2.2.2",
"moment": "latest", "moment": "latest",
"request": "^2.88.2", "request": "^2.88.2",
"rrule": "^2.6.2", "rrule": "^2.6.4",
"rrule-alt": "^2.2.8", "rrule-alt": "^2.2.8",
"simple-git": "^1.85.0", "simple-git": "^1.85.0",
"socket.io": "^2.1.1", "socket.io": "^2.1.1",

View File

@ -1,13 +0,0 @@
{
// Escaped
"FOO\"BAR": "Today",
/*
* The following lines
* represent cardinal directions
*/
"N": "N",
"E": "E",
"S": "S",
"W": "W"
}

View File

@ -0,0 +1,56 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//ical.marudot.com//iCal Event Maker
X-WR-CALNAME:TestEvents
NAME:TestEvents
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
TZURL:http://tzurl.org/zoneinfo-outlook/Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTAMP:20200719T094531Z
UID:20200719T094531Z-1871115387@marudot.com
DTSTART;TZID=Europe/Berlin:20300101T120000
DTEND;TZID=Europe/Berlin:20300101T130000
SUMMARY:TestEvent
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20200719T094531Z
UID:20200719T094531Z-1929725136@marudot.com
DTSTART;TZID=Europe/Berlin:20300701T120000
RRULE:FREQ=YEARLY;BYMONTH=7;BYMONTHDAY=1
DTEND;TZID=Europe/Berlin:20300701T130000
SUMMARY:TestEventRepeat
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20200719T094531Z
UID:20200719T094531Z-371801474@marudot.com
DTSTART;VALUE=DATE:20300401
DTEND;VALUE=DATE:20300402
SUMMARY:TestEventDay
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20200719T094531Z
UID:20200719T094531Z-133401084@marudot.com
DTSTART;VALUE=DATE:20301001
RRULE:FREQ=YEARLY;BYMONTH=10;BYMONTHDAY=1
DTEND;VALUE=DATE:20301002
SUMMARY:TestEventRepeatDay
END:VEVENT
END:VCALENDAR

View File

@ -0,0 +1,41 @@
/* Magic Mirror Test config custom calendar
*
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true
}
},
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
calendars: [
{
symbol: "birthday-cake",
fullDaySymbol: "calendar-day",
recurringSymbol: "undo",
maximumEntries: 4,
maximumNumberOfDays: 10000,
url: "http://localhost:8080/tests/configs/data/calendar_test_icons.ics"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@ -1,5 +1,6 @@
const helpers = require("../global-setup"); const helpers = require("../global-setup");
const serverBasicAuth = require("../../servers/basic-auth.js"); const serverBasicAuth = require("../../servers/basic-auth.js");
const expect = require("chai").expect;
const describe = global.describe; const describe = global.describe;
const it = global.it; const it = global.it;
@ -31,8 +32,47 @@ describe("Calendar module", function () {
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/default.js"; process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/default.js";
}); });
it("Should return TestEvents", function () { it("should show the default maximumEntries of 10", async () => {
return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000); await app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
const events = await app.client.$$(".calendar .event");
return expect(events.length).equals(10);
});
it("should show the default calendar symbol in each event", async () => {
await app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
const icons = await app.client.$$(".calendar .event .fa-calendar");
return expect(icons.length).not.equals(0);
});
});
describe("Custom configuration", function () {
before(function () {
// Set config sample for use in test
process.env.MM_CONFIG_FILE = "tests/configs/modules/calendar/custom.js";
});
it("should show the custom maximumEntries of 4", async () => {
await app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
const events = await app.client.$$(".calendar .event");
return expect(events.length).equals(4);
});
it("should show the custom calendar symbol in each event", async () => {
await app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
const icons = await app.client.$$(".calendar .event .fa-birthday-cake");
return expect(icons.length).equals(4);
});
it("should show two custom icons for repeating events", async () => {
await app.client.waitUntilTextExists(".calendar", "TestEventRepeat", 10000);
const icons = await app.client.$$(".calendar .event .fa-undo");
return expect(icons.length).equals(2);
});
it("should show two custom icons for day events", async () => {
await app.client.waitUntilTextExists(".calendar", "TestEventDay", 10000);
const icons = await app.client.$$(".calendar .event .fa-calendar-day");
return expect(icons.length).equals(2);
}); });
}); });
@ -47,7 +87,7 @@ describe("Calendar module", function () {
serverBasicAuth.close(done()); serverBasicAuth.close(done());
}); });
it("Should return TestEvents", function () { it("should return TestEvents", function () {
return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000); return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
}); });
}); });
@ -63,7 +103,7 @@ describe("Calendar module", function () {
serverBasicAuth.close(done()); serverBasicAuth.close(done());
}); });
it("Should return TestEvents", function () { it("should return TestEvents", function () {
return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000); return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
}); });
}); });
@ -79,7 +119,7 @@ describe("Calendar module", function () {
serverBasicAuth.close(done()); serverBasicAuth.close(done());
}); });
it("Should return TestEvents", function () { it("should return TestEvents", function () {
return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000); return app.client.waitUntilTextExists(".calendar", "TestEvent", 10000);
}); });
}); });
@ -95,7 +135,7 @@ describe("Calendar module", function () {
serverBasicAuth.close(done()); serverBasicAuth.close(done());
}); });
it("Should return No upcoming events", function () { it("should return No upcoming events", function () {
return app.client.waitUntilTextExists(".calendar", "No upcoming events.", 10000); return app.client.waitUntilTextExists(".calendar", "No upcoming events.", 10000);
}); });
}); });

View File

@ -1,7 +1,7 @@
var path = require("path"); const path = require("path");
var auth = require("http-auth"); const auth = require("http-auth");
var express = require("express"); const express = require("express");
var app = express(); const app = express();
var server; var server;

View File

@ -171,25 +171,6 @@ describe("Translator", function () {
}; };
}); });
it("should strip comments", function (done) {
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = function () {
const { Translator } = dom.window;
const file = "StripComments.json";
Translator.load(mmm, file, false, function () {
expect(Translator.translations[mmm.name]).to.be.deep.equal({
'FOO"BAR': "Today",
N: "N",
E: "E",
S: "S",
W: "W"
});
done();
});
};
});
it("should not load translations, if module fallback exists", function (done) { it("should not load translations, if module fallback exists", function (done) {
const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" }); const dom = new JSDOM(`<script>var Log = {log: function(){}};</script><script src="${path.join(__dirname, "..", "..", "..", "js", "translator.js")}">`, { runScripts: "dangerously", resources: "usable" });
dom.window.onload = function () { dom.window.onload = function () {
@ -205,7 +186,7 @@ describe("Translator", function () {
}; };
Translator.load(mmm, file, false, function () { Translator.load(mmm, file, false, function () {
expect(Translator.translations[mmm.name]).to.be.undefined; expect(Translator.translations[mmm.name]).to.be.equal(undefined);
expect(Translator.translationsFallback[mmm.name]).to.be.deep.equal({ expect(Translator.translationsFallback[mmm.name]).to.be.deep.equal({
Hello: "Hallo" Hello: "Hallo"
}); });

View File

@ -1,6 +1,6 @@
var expect = require("chai").expect; const expect = require("chai").expect;
var Utils = require("../../../js/utils.js"); const Utils = require("../../../js/utils.js");
var colors = require("colors/safe"); const colors = require("colors/safe");
describe("Utils", function () { describe("Utils", function () {
describe("colors", function () { describe("colors", function () {

View File

@ -32,54 +32,54 @@ describe("Functions into modules/default/calendar/calendar.js", function () {
}); });
describe("getLocaleSpecification", function () { describe("getLocaleSpecification", function () {
it("Should return a valid moment.LocaleSpecification for a 12-hour format", function () { it("should return a valid moment.LocaleSpecification for a 12-hour format", function () {
expect(Module.definitions.calendar.getLocaleSpecification(12)).to.deep.equal({ longDateFormat: { LT: "h:mm A" } }); expect(Module.definitions.calendar.getLocaleSpecification(12)).to.deep.equal({ longDateFormat: { LT: "h:mm A" } });
}); });
it("Should return a valid moment.LocaleSpecification for a 24-hour format", function () { it("should return a valid moment.LocaleSpecification for a 24-hour format", function () {
expect(Module.definitions.calendar.getLocaleSpecification(24)).to.deep.equal({ longDateFormat: { LT: "HH:mm" } }); expect(Module.definitions.calendar.getLocaleSpecification(24)).to.deep.equal({ longDateFormat: { LT: "HH:mm" } });
}); });
it("Should return the current system locale when called without timeFormat number", function () { it("should return the current system locale when called without timeFormat number", function () {
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: moment.localeData().longDateFormat("LT") } }); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: moment.localeData().longDateFormat("LT") } });
}); });
it("Should return a 12-hour longDateFormat when using the 'en' locale", function () { it("should return a 12-hour longDateFormat when using the 'en' locale", function () {
var localeBackup = moment.locale(); var localeBackup = moment.locale();
moment.locale("en"); moment.locale("en");
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } }); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } });
moment.locale(localeBackup); moment.locale(localeBackup);
}); });
it("Should return a 12-hour longDateFormat when using the 'au' locale", function () { it("should return a 12-hour longDateFormat when using the 'au' locale", function () {
var localeBackup = moment.locale(); var localeBackup = moment.locale();
moment.locale("au"); moment.locale("au");
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } }); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } });
moment.locale(localeBackup); moment.locale(localeBackup);
}); });
it("Should return a 12-hour longDateFormat when using the 'eg' locale", function () { it("should return a 12-hour longDateFormat when using the 'eg' locale", function () {
var localeBackup = moment.locale(); var localeBackup = moment.locale();
moment.locale("eg"); moment.locale("eg");
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } }); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "h:mm A" } });
moment.locale(localeBackup); moment.locale(localeBackup);
}); });
it("Should return a 24-hour longDateFormat when using the 'nl' locale", function () { it("should return a 24-hour longDateFormat when using the 'nl' locale", function () {
var localeBackup = moment.locale(); var localeBackup = moment.locale();
moment.locale("nl"); moment.locale("nl");
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } }); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } });
moment.locale(localeBackup); moment.locale(localeBackup);
}); });
it("Should return a 24-hour longDateFormat when using the 'fr' locale", function () { it("should return a 24-hour longDateFormat when using the 'fr' locale", function () {
var localeBackup = moment.locale(); var localeBackup = moment.locale();
moment.locale("fr"); moment.locale("fr");
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } }); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } });
moment.locale(localeBackup); moment.locale(localeBackup);
}); });
it("Should return a 24-hour longDateFormat when using the 'uk' locale", function () { it("should return a 24-hour longDateFormat when using the 'uk' locale", function () {
var localeBackup = moment.locale(); var localeBackup = moment.locale();
moment.locale("uk"); moment.locale("uk");
expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } }); expect(Module.definitions.calendar.getLocaleSpecification()).to.deep.equal({ longDateFormat: { LT: "HH:mm" } });
@ -120,5 +120,11 @@ describe("Functions into modules/default/calendar/calendar.js", function () {
"This is a wrapEvent <br>test. Should wrap the string <br>instead of shorten it if called <br>with wrapEvent = true" "This is a wrapEvent <br>test. Should wrap the string <br>instead of shorten it if called <br>with wrapEvent = true"
); );
}); });
it("should wrap and shorten the string in the second line if called with wrapEvents = true and maxTitleLines = 2", function () {
expect(Module.definitions.calendar.shorten("This is a wrapEvent and maxTitleLines test. Should wrap and shorten the string in the second line if called with wrapEvents = true and maxTitleLines = 2", undefined, true, 2)).to.equal(
"This is a wrapEvent and <br>maxTitleLines test. Should wrap and &hellip;"
);
});
}); });
}); });

View File

@ -1,5 +1,5 @@
/* eslint no-multi-spaces: 0 */ /* eslint no-multi-spaces: 0 */
var expect = require("chai").expect; const expect = require("chai").expect;
describe("Functions module currentweather", function () { describe("Functions module currentweather", function () {
// Fake for use by currentweather.js // Fake for use by currentweather.js

View File

@ -1,29 +1,15 @@
var expect = require("chai").expect; const expect = require("chai").expect;
describe("Functions into modules/default/newsfeed/newsfeed.js", function () { describe("Functions into modules/default/newsfeed/newsfeed.js", function () {
// Fake for use by newsletter.js
Module = {}; Module = {};
Module.definitions = {}; Module.definitions = {};
Module.register = function (name, moduleDefinition) { Module.register = function (name, moduleDefinition) {
Module.definitions[name] = moduleDefinition; Module.definitions[name] = moduleDefinition;
}; };
before(function () {
// load newsfeed.js // load newsfeed.js
require("../../../modules/default/newsfeed/newsfeed.js"); require("../../../modules/default/newsfeed/newsfeed.js");
describe("capitalizeFirstLetter", function () {
const words = {
rodrigo: "Rodrigo",
"123m": "123m",
"magic mirror": "Magic mirror",
",a": ",a",
ñandú: "Ñandú",
".!": ".!"
};
Object.keys(words).forEach((word) => {
it(`for ${word} should return ${words[word]}`, function () {
expect(Module.definitions.newsfeed.capitalizeFirstLetter(word)).to.equal(words[word]);
});
});
}); });
}); });

View File

@ -1,5 +1,5 @@
/* eslint no-multi-spaces: 0 */ /* eslint no-multi-spaces: 0 */
var expect = require("chai").expect; const expect = require("chai").expect;
describe("Functions module weatherforecast", function () { describe("Functions module weatherforecast", function () {
before(function () { before(function () {

View File

@ -1,7 +1,7 @@
var fs = require("fs"); const fs = require("fs");
var path = require("path"); const path = require("path");
var expect = require("chai").expect; const expect = require("chai").expect;
var vm = require("vm"); const vm = require("vm");
before(function () { before(function () {
var basedir = path.join(__dirname, "../../.."); var basedir = path.join(__dirname, "../../..");

View File

@ -1,7 +1,7 @@
var fs = require("fs"); const fs = require("fs");
var path = require("path"); const path = require("path");
var expect = require("chai").expect; const expect = require("chai").expect;
var vm = require("vm"); const vm = require("vm");
before(function () { before(function () {
var basedir = path.join(__dirname, "../../.."); var basedir = path.join(__dirname, "../../..");

View File

@ -26,6 +26,8 @@
"NW": "NW", "NW": "NW",
"NNW": "NNW", "NNW": "NNW",
"MODULE_CONFIG_CHANGED": "Die Konfigurationsoptionen für das {MODULE_NAME} Modul haben sich geändert. \nBitte überprüfen Sie die Dokumentation.",
"UPDATE_NOTIFICATION": "Aktualisierung für MagicMirror² verfügbar.", "UPDATE_NOTIFICATION": "Aktualisierung für MagicMirror² verfügbar.",
"UPDATE_NOTIFICATION_MODULE": "Aktualisierung für das {MODULE_NAME} Modul verfügbar.", "UPDATE_NOTIFICATION_MODULE": "Aktualisierung für das {MODULE_NAME} Modul verfügbar.",
"UPDATE_INFO_SINGLE": "Die aktuelle Installation ist {COMMIT_COUNT} Commit hinter dem {BRANCH_NAME} Branch.", "UPDATE_INFO_SINGLE": "Die aktuelle Installation ist {COMMIT_COUNT} Commit hinter dem {BRANCH_NAME} Branch.",

View File

@ -26,6 +26,8 @@
"NW": "NW", "NW": "NW",
"NNW": "NNW", "NNW": "NNW",
"MODULE_CONFIG_CHANGED": "The configuration options for the {MODULE_NAME} module have changed.\nPlease check the documentation.",
"UPDATE_NOTIFICATION": "MagicMirror² update available.", "UPDATE_NOTIFICATION": "MagicMirror² update available.",
"UPDATE_NOTIFICATION_MODULE": "Update available for {MODULE_NAME} module.", "UPDATE_NOTIFICATION_MODULE": "Update available for {MODULE_NAME} module.",
"UPDATE_INFO_SINGLE": "The current installation is {COMMIT_COUNT} commit behind on the {BRANCH_NAME} branch.", "UPDATE_INFO_SINGLE": "The current installation is {COMMIT_COUNT} commit behind on the {BRANCH_NAME} branch.",

View File

@ -26,6 +26,8 @@
"NW": "NO", "NW": "NO",
"NNW": "NNO", "NNW": "NNO",
"MODULE_CONFIG_CHANGED": "Las opciones de configuración para el módulo {MODULE_NAME} han cambiado. \nVerifique la documentación.",
"UPDATE_NOTIFICATION": "MagicMirror² actualización disponible.", "UPDATE_NOTIFICATION": "MagicMirror² actualización disponible.",
"UPDATE_NOTIFICATION_MODULE": "Disponible una actualización para el módulo {MODULE_NAME}.", "UPDATE_NOTIFICATION_MODULE": "Disponible una actualización para el módulo {MODULE_NAME}.",
"UPDATE_INFO_SINGLE": "Tu actual instalación está {COMMIT_COUNT} commit cambios detrás de la rama {BRANCH_NAME}.", "UPDATE_INFO_SINGLE": "Tu actual instalación está {COMMIT_COUNT} commit cambios detrás de la rama {BRANCH_NAME}.",

View File

@ -26,6 +26,8 @@
"NW": "NO", "NW": "NO",
"NNW": "NNO", "NNW": "NNO",
"MODULE_CONFIG_CHANGED": "Les options de configuration du module {MODULE_NAME} ont changé. \nVeuillez consulter la documentation.",
"UPDATE_NOTIFICATION": "Une mise à jour de MagicMirror² est disponible", "UPDATE_NOTIFICATION": "Une mise à jour de MagicMirror² est disponible",
"UPDATE_NOTIFICATION_MODULE": "Une mise à jour est disponible pour le module {MODULE_NAME} .", "UPDATE_NOTIFICATION_MODULE": "Une mise à jour est disponible pour le module {MODULE_NAME} .",
"UPDATE_INFO_SINGLE": "L'installation actuelle est {COMMIT_COUNT} commit en retard sur la branche {BRANCH_NAME} .", "UPDATE_INFO_SINGLE": "L'installation actuelle est {COMMIT_COUNT} commit en retard sur la branche {BRANCH_NAME} .",

36
translations/lt.json Normal file
View File

@ -0,0 +1,36 @@
{
"LOADING": "Kraunasi &hellip;",
"TODAY": "Šiandien",
"TOMORROW": "Rytoj",
"DAYAFTERTOMORROW": "Už 2 dienų",
"RUNNING": "Pasibaigs už",
"EMPTY": "Nėra artimų įvykių.",
"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",
"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",
"PRECIP": "Krituliai"
}

1138
vendor/package-lock.json generated vendored

File diff suppressed because it is too large Load Diff

8
vendor/package.json vendored
View File

@ -10,10 +10,10 @@
"url": "https://github.com/MichMich/MagicMirror/issues" "url": "https://github.com/MichMich/MagicMirror/issues"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.3.1", "@fortawesome/fontawesome-free": "^5.13.1",
"moment": "^2.17.1", "moment": "^2.27.0",
"moment-timezone": "^0.5.11", "moment-timezone": "^0.5.31",
"nunjucks": "^3.0.1", "nunjucks": "^3.2.1",
"suncalc": "^1.8.0", "suncalc": "^1.8.0",
"weathericons": "^2.1.0" "weathericons": "^2.1.0"
} }