Skip to content

Commit

Permalink
Update compliments with support for cron type date/time for selection…
Browse files Browse the repository at this point in the history
…s, 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
  • Loading branch information
sdetweil authored Jul 15, 2024
1 parent 974a1da commit d9665b3
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
100 changes: 84 additions & 16 deletions modules/default/compliments/compliments.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* global Cron */

Module.register("compliments", {
// Module config defaults.
defaults: {
Expand All @@ -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.
Expand All @@ -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;
},

/**
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down
18 changes: 18 additions & 0 deletions tests/configs/modules/compliments/compliments_cron_entry.js
Original file line number Diff line number Diff line change
@@ -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; }
18 changes: 18 additions & 0 deletions tests/configs/modules/compliments/compliments_e2e_cron_entry.js
Original file line number Diff line number Diff line change
@@ -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; }
11 changes: 11 additions & 0 deletions tests/e2e/modules/compliments_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
});
36 changes: 36 additions & 0 deletions tests/electron/modules/compliments_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

});
});
9 changes: 9 additions & 0 deletions vendor/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions vendor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion vendor/vendor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down

0 comments on commit d9665b3

Please sign in to comment.