Merge branch 'develop' into cal-again2

This commit is contained in:
Michael Teeuw
2020-12-08 15:17:45 +01:00
committed by GitHub
12 changed files with 881 additions and 734 deletions

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,
coloredEvents: [], // Array of Keyword/Color where Keyword is a regexp and color the color code to use when regexp matches
tableClass: "small",
calendars: [
{
@@ -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;
@@ -248,6 +256,18 @@ Module.register("calendar", {
}
}
if (this.config.coloredEvents.length > 0) {
for (var ev in this.config.coloredEvents) {
var needle = new RegExp(this.config.coloredEvents[ev].keyword, "gi");
if (needle.test(event.title)) {
eventWrapper.style.cssText = "color:" + this.config.coloredEvents[ev].color;
titleWrapper.style.cssText = "color:" + this.config.coloredEvents[ev].color;
if (this.config.displaySymbol) symbolWrapper.style.cssText = "color:" + this.config.coloredEvents[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,82 +300,56 @@ 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 (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;
if (event.today) {
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
} else {
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) {
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
}
}
}
} else {
// Show relative times
if (event.startDate >= now) {
// Use relative time
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
if (event.startDate - now < this.config.getRelative * oneHour) {
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
}
} 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'
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
}
}
} else {
/* Check to see if the user displays absolute or relative dates with their events
* Also check to see if an event is happening within an 'urgency' time frameElement
* For example, if the user set an .urgency of 7 days, those events that fall within that
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
*
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim
*/
if (this.config.timeFormat === "absolute") {
if (this.config.urgency > 1 && event.startDate - now < this.config.urgency * oneDay) {
// This event falls within the config.urgency period that the user has set
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
} else {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
}
} else {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
}
}
} else {
// Ongoing event
timeWrapper.innerHTML = this.capFirst(
this.translate("RUNNING", {
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
@@ -363,12 +357,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 +510,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);
},

View File

@@ -76,8 +76,18 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
return;
}
const data = ical.parseICS(requestData);
Log.debug(" parsed data=" + JSON.stringify(data));
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
@@ -385,7 +395,21 @@ 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();

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