From d9665b35df989bfb73f23a37f3ae26af67152362 Mon Sep 17 00:00:00 2001 From: sam detweiler Date: Mon, 15 Jul 2024 12:51:05 -0500 Subject: [PATCH] Update compliments with support for cron type date/time for selections, addition to just date. (#3481) > - What does the pull request accomplish? Use a list if needed. this change allows uses to configure date/time events for compliments.. also linked site that will build the cron entry.. and example was Happy hour in a pub, on fri/sat between 5 and 7 pm. or just after midnight on Halloween (Boooooo!) I also added testcases for #3478 (and added support for this in MMM-Config), with a custom, drop down selection list of the types.. ) | if this is approved I will update the module doc --- CHANGELOG.md | 1 + modules/default/compliments/compliments.js | 100 +++++++++++++++--- .../compliments/compliments_cron_entry.js | 18 ++++ .../compliments/compliments_e2e_cron_entry.js | 18 ++++ tests/e2e/modules/compliments_spec.js | 11 ++ tests/electron/modules/compliments_spec.js | 36 +++++++ vendor/package-lock.json | 9 ++ vendor/package.json | 1 + vendor/vendor.js | 3 +- 9 files changed, 180 insertions(+), 17 deletions(-) create mode 100644 tests/configs/modules/compliments/compliments_cron_entry.js create mode 100644 tests/configs/modules/compliments/compliments_e2e_cron_entry.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 39b2d7e6e9..67b593ac6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Thanks to: @btoconnor, @bugsounet, @JasonStieber, @khassel, @kleinmantara and @W ### Added +- [compliments] Added support for cron type date/time format entries.. mm hh DD MM dow (minutes/hours/days/months and day of week) see https://crontab.cronhub.io for construction - [calendar] Added config option "showEndsOnlyWithDuration" for default calendar - [compliments] Added `specialDayUnique` config option, defaults to `false` (#3465) - [weather] Provider weathergov: Use `precipitationLast3Hours` if `precipitationLastHour` is `null` (#3124) diff --git a/modules/default/compliments/compliments.js b/modules/default/compliments/compliments.js index c249f9915f..4c4bef471f 100644 --- a/modules/default/compliments/compliments.js +++ b/modules/default/compliments/compliments.js @@ -1,3 +1,5 @@ +/* global Cron */ + Module.register("compliments", { // Module config defaults. defaults: { @@ -21,10 +23,12 @@ Module.register("compliments", { 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 ["moment.js"]; + return ["croner.js", "moment.js"]; }, // Define start sequence. @@ -38,11 +42,45 @@ Module.register("compliments", { this.config.compliments = JSON.parse(response); this.updateDom(); } + 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(" "); + }, - // Schedule update timer. - setInterval(() => { - this.updateDom(this.config.fadeSpeed); - }, this.config.updateInterval); + /** + * @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; }, /** @@ -75,8 +113,9 @@ Module.register("compliments", { * @returns {string[]} array with compliments for the time of the day. */ complimentArray () { - const hour = moment().hour(); - const date = moment().format("YYYY-MM-DD"); + const now = moment(); + const hour = now.hour(); + const date = now.format("YYYY-MM-DD"); let compliments = []; // Add time of day compliments @@ -91,20 +130,49 @@ Module.register("compliments", { // 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); - // Add compliments for special days - for (let entry in this.config.compliments) { - if (new RegExp(entry).test(date)) { - // Only display compliments configured for the day if specialDayUnique is set to true - if (this.config.specialDayUnique) { - compliments.length = 0; - } - Array.prototype.push.apply(compliments, this.config.compliments[entry]); + // 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; diff --git a/tests/configs/modules/compliments/compliments_cron_entry.js b/tests/configs/modules/compliments/compliments_cron_entry.js new file mode 100644 index 0000000000..59a6659293 --- /dev/null +++ b/tests/configs/modules/compliments/compliments_cron_entry.js @@ -0,0 +1,18 @@ +let config = { + modules: [ + { + module: "compliments", + position: "middle_center", + config: { + specialDayUnique: true, + compliments: { + anytime: ["just a test"], + "00-10 16-19 * * fri": ["just pub time"] + } + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { module.exports = config; } diff --git a/tests/configs/modules/compliments/compliments_e2e_cron_entry.js b/tests/configs/modules/compliments/compliments_e2e_cron_entry.js new file mode 100644 index 0000000000..a4d1c5d8f2 --- /dev/null +++ b/tests/configs/modules/compliments/compliments_e2e_cron_entry.js @@ -0,0 +1,18 @@ +let config = { + modules: [ + { + module: "compliments", + position: "middle_center", + config: { + specialDayUnique: true, + compliments: { + anytime: ["just a test"], + "* * * * *": ["anytime cron"] + } + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { module.exports = config; } diff --git a/tests/e2e/modules/compliments_spec.js b/tests/e2e/modules/compliments_spec.js index 95b342577e..7763dc689b 100644 --- a/tests/e2e/modules/compliments_spec.js +++ b/tests/e2e/modules/compliments_spec.js @@ -77,5 +77,16 @@ describe("Compliments module", () => { await expect(doTest(["Special day message"])).resolves.toBe(true); }); }); + + describe("cron type key", () => { + beforeAll(async () => { + await helpers.startApplication("tests/configs/modules/compliments/compliments_e2e_cron_entry.js"); + await helpers.getDocument(); + }); + + it("compliments array contains only special value", async () => { + await expect(doTest(["anytime cron"])).resolves.toBe(true); + }); + }); }); }); diff --git a/tests/electron/modules/compliments_spec.js b/tests/electron/modules/compliments_spec.js index b4c4ed845b..ac162fd9ce 100644 --- a/tests/electron/modules/compliments_spec.js +++ b/tests/electron/modules/compliments_spec.js @@ -43,5 +43,41 @@ describe("Compliments module", () => { await expect(doTest(["Happy new year!"])).resolves.toBe(true); }); }); + + describe("Test only custom date events shown with new property", () => { + it("shows 'Special day message' on May 6", async () => { + await helpers.startApplication("tests/configs/modules/compliments/compliments_specialDayUnique_true.js", "06 May 2022 10:00:00 GMT"); + await expect(doTest(["Special day message"])).resolves.toBe(true); + }); + }); + + describe("Test all date events shown without neww property", () => { + it("shows 'any message' on May 6", async () => { + await helpers.startApplication("tests/configs/modules/compliments/compliments_specialDayUnique_false.js", "06 May 2022 10:00:00 GMT"); + await expect(doTest(["Special day message", "Typical message 1", "Typical message 2", "Typical message 3"])).resolves.toBe(true); + }); + }); + + describe("Test only custom cron date event shown with new property", () => { + it("shows 'any message' on May 6", async () => { + await helpers.startApplication("tests/configs/modules/compliments/compliments_cron_entry.js", "06 May 2022 17:03:00 GMT"); + await expect(doTest(["just pub time"])).resolves.toBe(true); + }); + }); + + describe("Test any event shows after time window", () => { + it("shows 'any message' on May 6", async () => { + await helpers.startApplication("tests/configs/modules/compliments/compliments_cron_entry.js", "06 May 2022 17:11:00 GMT"); + await expect(doTest(["just a test"])).resolves.toBe(true); + }); + }); + + describe("Test any event shows different day", () => { + it("shows 'any message' on May 5", async () => { + await helpers.startApplication("tests/configs/modules/compliments/compliments_cron_entry.js", "05 May 2022 17:00:00 GMT"); + await expect(doTest(["just a test"])).resolves.toBe(true); + }); + }); + }); }); diff --git a/vendor/package-lock.json b/vendor/package-lock.json index 2df570b22a..058dae08df 100644 --- a/vendor/package-lock.json +++ b/vendor/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@fortawesome/fontawesome-free": "^6.5.2", "animate.css": "^4.1.1", + "croner": "^8.0.2", "moment": "^2.30.1", "moment-timezone": "^0.5.45", "nunjucks": "^3.2.4", @@ -50,6 +51,14 @@ "node": ">= 6" } }, + "node_modules/croner": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/croner/-/croner-8.0.2.tgz", + "integrity": "sha512-HgSdlSUX8mIgDTTiQpWUP4qY4IFRMsduPCYdca34Pelt8MVdxdaDOzreFtCscA6R+cRZd7UbD1CD3uyx6J3X1A==", + "engines": { + "node": ">=18.0" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", diff --git a/vendor/package.json b/vendor/package.json index efaa4f151e..e61f8bf380 100644 --- a/vendor/package.json +++ b/vendor/package.json @@ -13,6 +13,7 @@ "dependencies": { "@fortawesome/fontawesome-free": "^6.5.2", "animate.css": "^4.1.1", + "croner": "^8.0.2", "moment": "^2.30.1", "moment-timezone": "^0.5.45", "nunjucks": "^3.2.4", diff --git a/vendor/vendor.js b/vendor/vendor.js index b2d912d4de..ee3f0bb777 100644 --- a/vendor/vendor.js +++ b/vendor/vendor.js @@ -5,7 +5,8 @@ const vendor = { "weather-icons-wind.css": "node_modules/weathericons/css/weather-icons-wind.css", "font-awesome.css": "css/font-awesome.css", "nunjucks.js": "node_modules/nunjucks/browser/nunjucks.min.js", - "suncalc.js": "node_modules/suncalc/suncalc.js" + "suncalc.js": "node_modules/suncalc/suncalc.js", + "croner.js": "node_modules/croner/dist/croner.umd.min.js" }; if (typeof module !== "undefined") {