mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 11:50:00 +00:00
here is an updated test version of the fixes for all kinds of calendar date problems. NOTE: the changed branch name NOTE: this used the node-cal@0.19.0 library UNCHANGED best to make a new folder and git clone there git clone https://github.com/sdetweil/MagicMirror cd MagicMirror git checkout fixcaldates2 // <------ note this is a changed branch name npm run install-mm copy your config.js and custom.css from the prior folder and the non-default modules you have installed… this ONLY changes the default calendar but DOES ship an updated node-ical library too if you need to fall back, just rename the folders around again so that your original is called MagicMirror all the testcases for node-ical and MagicMirror execute successfully. the ‘BIG’ change here is to get the local NON-TZ dates for the rrule.between() all the checking and conversion code is commented out or not used the node-ical fixes are for excluded dates (exdate) values being adjusted for DST/STD time… waiting to submit that PR one fix in calendar.js for checking if a past date was too far back, but it never checked to see IF the event date was in the past… (before today) so it chopped off too many and one change in calendarfetcher.js to put out a better diagnostic message of the parsed data… (exdate was excluded cause JSON stringify couldn’t convert the complex structure) I added the tests you all have documented please re-pull and checkout the new branch (I deleted the old branch) and npm run install-mm again --------- Co-authored-by: Veeck <github@veeck.de>
300 lines
10 KiB
JavaScript
300 lines
10 KiB
JavaScript
/* global Cron */
|
|
|
|
Module.register("compliments", {
|
|
// Module config defaults.
|
|
defaults: {
|
|
compliments: {
|
|
anytime: ["Hey there sexy!"],
|
|
morning: ["Good morning, handsome!", "Enjoy your day!", "How was your sleep?"],
|
|
afternoon: ["Hello, beauty!", "You look sexy!", "Looking good today!"],
|
|
evening: ["Wow, you look hot!", "You look nice!", "Hi, sexy!"],
|
|
"....-01-01": ["Happy new year!"]
|
|
},
|
|
updateInterval: 30000,
|
|
remoteFile: null,
|
|
remoteFileRefreshInterval: 0,
|
|
fadeSpeed: 4000,
|
|
morningStartTime: 3,
|
|
morningEndTime: 12,
|
|
afternoonStartTime: 12,
|
|
afternoonEndTime: 17,
|
|
random: true,
|
|
specialDayUnique: false
|
|
},
|
|
urlSuffix: "",
|
|
compliments_new: null,
|
|
refreshMinimumDelay: 15 * 60 * 60 * 1000, // 15 minutes
|
|
lastIndexUsed: -1,
|
|
// Set currentweather from module
|
|
currentWeatherType: "",
|
|
cron_regex: /^(((\d+,)+\d+|((\d+|[*])[/]\d+|((JAN|FEB|APR|MA[RY]|JU[LN]|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|APR|MA[RY]|JU[LN]|AUG|SEP|OCT|NOV|DEC))?))|(\d+-\d+)|\d+(-\d+)?[/]\d+(-\d+)?|\d+|[*]|(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?) ?){5}$/i,
|
|
date_regex: "[1-9.][0-9.][0-9.]{2}-([0][1-9]|[1][0-2])-([1-2][0-9]|[0][1-9]|[3][0-1])",
|
|
pre_defined_types: ["anytime", "morning", "afternoon", "evening"],
|
|
// Define required scripts.
|
|
getScripts () {
|
|
return ["croner.js", "moment.js"];
|
|
},
|
|
|
|
// Define start sequence.
|
|
async start () {
|
|
Log.info(`Starting module: ${this.name}`);
|
|
|
|
this.lastComplimentIndex = -1;
|
|
|
|
if (this.config.remoteFile !== null) {
|
|
const response = await this.loadComplimentFile();
|
|
this.config.compliments = JSON.parse(response);
|
|
this.updateDom();
|
|
if (this.config.remoteFileRefreshInterval !== 0) {
|
|
if ((this.config.remoteFileRefreshInterval >= this.refreshMinimumDelay) || window.mmTestMode === "true") {
|
|
setInterval(async () => {
|
|
const response = await this.loadComplimentFile();
|
|
if (response) {
|
|
this.compliments_new = JSON.parse(response);
|
|
}
|
|
else {
|
|
Log.error(`${this.name} remoteFile refresh failed`);
|
|
}
|
|
},
|
|
this.config.remoteFileRefreshInterval);
|
|
} else {
|
|
Log.error(`${this.name} remoteFileRefreshInterval less than minimum`);
|
|
}
|
|
}
|
|
}
|
|
let minute_sync_delay = 1;
|
|
// loop thru all the configured when events
|
|
for (let m of Object.keys(this.config.compliments)) {
|
|
// if it is a cron entry
|
|
if (this.isCronEntry(m)) {
|
|
// we need to synch our interval cycle to the minute
|
|
minute_sync_delay = (60 - (moment().second())) * 1000;
|
|
break;
|
|
}
|
|
}
|
|
// Schedule update timer. sync to the minute start (if needed), so minute based events happen on the minute start
|
|
setTimeout(() => {
|
|
setInterval(() => {
|
|
this.updateDom(this.config.fadeSpeed);
|
|
}, this.config.updateInterval);
|
|
},
|
|
minute_sync_delay);
|
|
},
|
|
|
|
// check to see if this entry could be a cron entry wich contains spaces
|
|
isCronEntry (entry) {
|
|
return entry.includes(" ");
|
|
},
|
|
|
|
/**
|
|
* @param {string} cronExpression The cron expression. See https://croner.56k.guru/usage/pattern/
|
|
* @param {Date} [timestamp] The timestamp to check. Defaults to the current time.
|
|
* @returns {number} The number of seconds until the next cron run.
|
|
*/
|
|
getSecondsUntilNextCronRun (cronExpression, timestamp = new Date()) {
|
|
// Required for seconds precision
|
|
const adjustedTimestamp = new Date(timestamp.getTime() - 1000);
|
|
|
|
// https://www.npmjs.com/package/croner
|
|
const cronJob = new Cron(cronExpression);
|
|
const nextRunTime = cronJob.nextRun(adjustedTimestamp);
|
|
|
|
const secondsDelta = (nextRunTime - adjustedTimestamp) / 1000;
|
|
return secondsDelta;
|
|
},
|
|
|
|
/**
|
|
* Generate a random index for a list of compliments.
|
|
* @param {string[]} compliments Array with compliments.
|
|
* @returns {number} a random index of given array
|
|
*/
|
|
randomIndex (compliments) {
|
|
if (compliments.length <= 1) {
|
|
return 0;
|
|
}
|
|
|
|
const generate = function () {
|
|
return Math.floor(Math.random() * compliments.length);
|
|
};
|
|
|
|
let complimentIndex = generate();
|
|
|
|
while (complimentIndex === this.lastComplimentIndex) {
|
|
complimentIndex = generate();
|
|
}
|
|
|
|
this.lastComplimentIndex = complimentIndex;
|
|
|
|
return complimentIndex;
|
|
},
|
|
|
|
/**
|
|
* Retrieve an array of compliments for the time of the day.
|
|
* @returns {string[]} array with compliments for the time of the day.
|
|
*/
|
|
complimentArray () {
|
|
const now = moment();
|
|
const hour = now.hour();
|
|
const date = now.format("YYYY-MM-DD");
|
|
let compliments = [];
|
|
|
|
// Add time of day compliments
|
|
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime && this.config.compliments.hasOwnProperty("morning")) {
|
|
compliments = [...this.config.compliments.morning];
|
|
} else if (hour >= this.config.afternoonStartTime && hour < this.config.afternoonEndTime && this.config.compliments.hasOwnProperty("afternoon")) {
|
|
compliments = [...this.config.compliments.afternoon];
|
|
} else if (this.config.compliments.hasOwnProperty("evening")) {
|
|
compliments = [...this.config.compliments.evening];
|
|
}
|
|
|
|
// Add compliments based on weather
|
|
if (this.currentWeatherType in this.config.compliments) {
|
|
Array.prototype.push.apply(compliments, this.config.compliments[this.currentWeatherType]);
|
|
// if the predefine list doesn't include it (yet)
|
|
if (!this.pre_defined_types.includes(this.currentWeatherType)) {
|
|
// add it
|
|
this.pre_defined_types.push(this.currentWeatherType);
|
|
}
|
|
}
|
|
|
|
// Add compliments for anytime
|
|
Array.prototype.push.apply(compliments, this.config.compliments.anytime);
|
|
|
|
// get the list of just date entry keys
|
|
let temp_list = Object.keys(this.config.compliments).filter((k) => {
|
|
if (this.pre_defined_types.includes(k)) return false;
|
|
else return true;
|
|
});
|
|
|
|
let date_compliments = [];
|
|
// Add compliments for special day/times
|
|
for (let entry of temp_list) {
|
|
// check if this could be a cron type entry
|
|
if (this.isCronEntry(entry)) {
|
|
// make sure the regex is valid
|
|
if (new RegExp(this.cron_regex).test(entry)) {
|
|
// check if we are in the time range for the cron entry
|
|
if (this.getSecondsUntilNextCronRun(entry, now.set("seconds", 0).toDate()) <= 1) {
|
|
// if so, use its notice entries
|
|
Array.prototype.push.apply(date_compliments, this.config.compliments[entry]);
|
|
}
|
|
} else Log.error(`compliments cron syntax invalid=${JSON.stringify(entry)}`);
|
|
} else if (new RegExp(entry).test(date)) {
|
|
Array.prototype.push.apply(date_compliments, this.config.compliments[entry]);
|
|
}
|
|
}
|
|
|
|
// if we found any date compliments
|
|
if (date_compliments.length) {
|
|
// and the special flag is true
|
|
if (this.config.specialDayUnique) {
|
|
// clear the non-date compliments if any
|
|
compliments.length = 0;
|
|
}
|
|
// put the date based compliments on the list
|
|
Array.prototype.push.apply(compliments, date_compliments);
|
|
}
|
|
|
|
return compliments;
|
|
},
|
|
|
|
/**
|
|
* Retrieve a file from the local filesystem
|
|
* @returns {Promise} Resolved when the file is loaded
|
|
*/
|
|
async loadComplimentFile () {
|
|
const isRemote = this.config.remoteFile.indexOf("http://") === 0 || this.config.remoteFile.indexOf("https://") === 0,
|
|
url = isRemote ? this.config.remoteFile : this.file(this.config.remoteFile);
|
|
// because we may be fetching the same url,
|
|
// we need to force the server to not give us the cached result
|
|
// create an extra property (ignored by the server handler) just so the url string is different
|
|
// that will never be the same, using the ms value of date
|
|
if (isRemote && this.config.remoteFileRefreshInterval !== 0) this.urlSuffix = `?dummy=${Date.now()}`;
|
|
//
|
|
try {
|
|
const response = await fetch(url + this.urlSuffix);
|
|
return await response.text();
|
|
} catch (error) {
|
|
Log.info(`${this.name} fetch failed error=`, error);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Retrieve a random compliment.
|
|
* @returns {string} a compliment
|
|
*/
|
|
getRandomCompliment () {
|
|
// get the current time of day compliments list
|
|
const compliments = this.complimentArray();
|
|
// variable for index to next message to display
|
|
let index;
|
|
// are we randomizing
|
|
if (this.config.random) {
|
|
// yes
|
|
index = this.randomIndex(compliments);
|
|
} else {
|
|
// no, sequential
|
|
// if doing sequential, don't fall off the end
|
|
index = this.lastIndexUsed >= compliments.length - 1 ? 0 : ++this.lastIndexUsed;
|
|
}
|
|
|
|
return compliments[index] || "";
|
|
},
|
|
|
|
// Override dom generator.
|
|
getDom () {
|
|
const wrapper = document.createElement("div");
|
|
wrapper.className = this.config.classes ? this.config.classes : "thin xlarge bright pre-line";
|
|
// get the compliment text
|
|
const complimentText = this.getRandomCompliment();
|
|
// split it into parts on newline text
|
|
const parts = complimentText.split("\n");
|
|
// create a span to hold the compliment
|
|
const compliment = document.createElement("span");
|
|
// process all the parts of the compliment text
|
|
for (const part of parts) {
|
|
if (part !== "") {
|
|
// create a text element for each part
|
|
compliment.appendChild(document.createTextNode(part));
|
|
// add a break
|
|
compliment.appendChild(document.createElement("BR"));
|
|
}
|
|
}
|
|
// only add compliment to wrapper if there is actual text in there
|
|
if (compliment.children.length > 0) {
|
|
// remove the last break
|
|
compliment.lastElementChild.remove();
|
|
wrapper.appendChild(compliment);
|
|
}
|
|
// if a new set of compliments was loaded from the refresh task
|
|
// we do this here to make sure no other function is using the compliments list
|
|
if (this.compliments_new) {
|
|
// use them
|
|
if (JSON.stringify(this.config.compliments) !== JSON.stringify(this.compliments_new)) {
|
|
// only reset if the contents changes
|
|
this.config.compliments = this.compliments_new;
|
|
// reset the index
|
|
this.lastIndexUsed = -1;
|
|
}
|
|
// clear new file list so we don't waste cycles comparing between refreshes
|
|
this.compliments_new = null;
|
|
}
|
|
// only in test mode
|
|
if (window.mmTestMode === "true") {
|
|
// check for (undocumented) remoteFile2 to test new file load
|
|
if (this.config.remoteFile2 !== null && this.config.remoteFileRefreshInterval !== 0) {
|
|
// switch the file so that next time it will be loaded from a changed file
|
|
this.config.remoteFile = this.config.remoteFile2;
|
|
}
|
|
}
|
|
return wrapper;
|
|
},
|
|
|
|
// Override notification handler.
|
|
notificationReceived (notification, payload, sender) {
|
|
if (notification === "CURRENTWEATHER_TYPE") {
|
|
this.currentWeatherType = payload.type;
|
|
}
|
|
}
|
|
});
|