├── .prettierignore ├── .prettierrc.json ├── .github ├── FUNDING.yml ├── example.gif └── workflows │ └── node.js.yml ├── .editorconfig ├── LICENSE ├── .gitignore ├── .eslint.config.js ├── package.json ├── tests └── node_helper.test.js ├── MMM-ModuleScheduler.js ├── node_helper.js └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | package-lock.json -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none" 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ianperrin 2 | custom: ["https://paypal.me/ianperrin01"] 3 | -------------------------------------------------------------------------------- /.github/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianperrin/MMM-ModuleScheduler/HEAD/.github/example.gif -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 250 11 | trim_trailing_whitespace = true 12 | 13 | [*.{js,json}] 14 | indent_size = 4 15 | indent_style = tab 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Ian Perrin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Various Windows ignoramuses. 40 | Thumbs.db 41 | ehthumbs.db 42 | Desktop.ini 43 | $RECYCLE.BIN/ 44 | *.cab 45 | *.msi 46 | *.msm 47 | *.msp 48 | *.lnk 49 | 50 | # Various OSX ignoramuses. 51 | .DS_Store 52 | .AppleDouble 53 | .LSOverride 54 | Icon 55 | ._* 56 | .DocumentRevisions-V100 57 | .fseventsd 58 | .Spotlight-V100 59 | .TemporaryItems 60 | .Trashes 61 | .VolumeIcon.icns 62 | .AppleDB 63 | .AppleDesktop 64 | Network Trash Folder 65 | Temporary Items 66 | .apdisk 67 | 68 | # Various Linux ignoramuses. 69 | .fuse_hidden* 70 | .directory 71 | .Trash-* 72 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Tests 5 | 6 | on: 7 | push: 8 | branches: [master, develop] 9 | pull_request: 10 | branches: [master, develop] 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [22.x] 19 | 20 | steps: 21 | - name: Checkout MagicMirror repo 22 | uses: actions/checkout@v2 23 | with: 24 | repository: MichMich/MagicMirror 25 | path: MagicMirror 26 | 27 | - name: Checkout Module repo 28 | uses: actions/checkout@v2 29 | with: 30 | path: MagicMirror/modules/MMM-ModuleScheduler 31 | 32 | - name: Use Node.js ${{ matrix.node-version }} 33 | uses: actions/setup-node@v2 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | 37 | - name: Install MagicMirror 38 | run: | 39 | cd MagicMirror 40 | Xvfb :99 -screen 0 1024x768x16 & 41 | export DISPLAY=:99 42 | npm ci 43 | 44 | - name: Run tests 45 | run: | 46 | cd MagicMirror/modules/MMM-ModuleScheduler 47 | npm ci 48 | npm run test 49 | -------------------------------------------------------------------------------- /.eslint.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import eslintPluginPrettier from "eslint-plugin-prettier"; 3 | import eslintPluginJsdoc from "eslint-plugin-jsdoc"; 4 | import globals from "globals"; 5 | import path from "node:path"; 6 | import { fileURLToPath } from "node:url"; 7 | import js from "@eslint/js"; 8 | import { FlatCompat } from "@eslint/eslintrc"; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | const compat = new FlatCompat({ 13 | baseDirectory: __dirname, 14 | recommendedConfig: js.configs.recommended, 15 | allConfig: js.configs.all 16 | }); 17 | 18 | export default defineConfig([ 19 | { 20 | extends: compat.extends("eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"), 21 | 22 | plugins: { 23 | eslintPluginPrettier, 24 | eslintPluginJsdoc 25 | }, 26 | 27 | languageOptions: { 28 | globals: { 29 | ...globals.browser, 30 | ...globals.mocha, 31 | ...globals.node, 32 | config: true, 33 | Log: true, 34 | MM: true, 35 | Module: true, 36 | moment: true 37 | }, 38 | 39 | ecmaVersion: 2017, 40 | sourceType: "module", 41 | 42 | parserOptions: { 43 | ecmaFeatures: { 44 | globalReturn: true 45 | } 46 | } 47 | }, 48 | 49 | rules: { 50 | "prettier/prettier": "error", 51 | eqeqeq: "error", 52 | "no-prototype-builtins": "off", 53 | "no-unused-vars": "off" 54 | } 55 | } 56 | ]); 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MMM-ModuleScheduler", 3 | "version": "1.5.1", 4 | "description": "A MagicMirror helper module to schedule when modules should be shown, hidden or dimmed and when notifications should be sent.", 5 | "main": "MMM-ModuleScheduler.js", 6 | "scripts": { 7 | "test": "npm run test:unit && npm run test:prettier && npm run test:js", 8 | "test:unit": "NODE_ENV=test mocha tests --recursive", 9 | "test:prettier": "prettier --check **/*.{js,css,json,md,yml}", 10 | "test:js": "eslint *.js tests/**/*.js --config .eslint.config.js", 11 | "lint:prettier": "prettier --write **/*.{js,css,json,md,yml}", 12 | "lint:js": "eslint *.js tests/**/*.js --config .eslint.config.js --fix" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/ianperrin/MMM-ModuleScheduler.git" 17 | }, 18 | "keywords": [ 19 | "MagicMirror", 20 | "Module", 21 | "Scheduler" 22 | ], 23 | "author": "Ian Perrin", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/ianperrin/MMM-ModuleScheduler/issues" 27 | }, 28 | "homepage": "https://github.com/ianperrin/MMM-ModuleScheduler#readme", 29 | "devDependencies": { 30 | "chai": "^5.2.0", 31 | "eslint": "^9.24.0", 32 | "eslint-config-prettier": "^10.1.2", 33 | "eslint-plugin-jsdoc": "^50.6.9", 34 | "eslint-plugin-prettier": "^5.2.6", 35 | "mocha": "^11.1.0", 36 | "module-alias": "^2.2.3", 37 | "prettier": "^3.5.3" 38 | }, 39 | "dependencies": { 40 | "cron": "^4.3.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/node_helper.test.js: -------------------------------------------------------------------------------- 1 | const expect = require("chai").expect; 2 | const moduleAlias = require("module-alias"); 3 | moduleAlias.addAliases({ node_helper: "../../js/node_helper.js", logger: "../js/logger.js" }); 4 | var Module = require("../node_helper.js"); 5 | var helper = new Module(); 6 | helper.setName("MMM-ModuleScheduler"); 7 | 8 | describe("Functions in node_helper.js", function () { 9 | describe("getOrMakeArray", function () { 10 | it(`for a string should return an array`, function () { 11 | expect(helper.getOrMakeArray("dsg")).to.eql(["dsg"]); 12 | }); 13 | it(`for an array should return an array`, function () { 14 | expect(helper.getOrMakeArray(["dsg"])).to.eql(["dsg"]); 15 | }); 16 | it(`for a multiple value array should return an array`, function () { 17 | expect(helper.getOrMakeArray(["dsg", "sss"])).to.eql(["dsg", "sss"]); 18 | }); 19 | }); 20 | describe("isValidSchedule", function () { 21 | ["global", "group", "module"].forEach((scheduleType) => { 22 | const validSchedules = { 23 | "from-to": { from: "0 5 * * *", to: "0 9 * * *" } 24 | }; 25 | describe(`for '${scheduleType}' valid schedules`, function () { 26 | Object.keys(validSchedules).forEach((schedule) => { 27 | it(`for '${schedule}' should return true`, function () { 28 | expect(helper.isValidSchedule(validSchedules[schedule], scheduleType)).to.be.true; 29 | }); 30 | }); 31 | }); 32 | }); 33 | ["notification"].forEach((scheduleType) => { 34 | const validSchedules = { 35 | "from-to": { schedule: "0 5 * * *", notification: "TEST" } 36 | }; 37 | describe(`for '${scheduleType}' valid schedules`, function () { 38 | Object.keys(validSchedules).forEach((schedule) => { 39 | it(`for '${schedule}' should return true`, function () { 40 | expect(helper.isValidSchedule(validSchedules[schedule], scheduleType)).to.be.true; 41 | }); 42 | }); 43 | }); 44 | }); 45 | const invalidSchedules = { 46 | "from-hide": { from: "0 5 * * *", hide: "0 9 * * *" }, 47 | "show-to": { show: "0 5 * * *", to: "0 9 * * *" }, 48 | "show-hide": { show: "0 5 * * *", hide: "0 9 * * *" }, 49 | "schedule-id": { schedule: "0 5 * * *", id: "TEST" }, 50 | "from-notification": { from: "0 5 * * *", notification: "TEST" }, 51 | "from-id": { from: "0 5 * * *", id: "TEST" }, 52 | empty: {} 53 | }; 54 | ["global", "group", "module", "notification"].forEach((scheduleType) => { 55 | describe(`for '${scheduleType}' invalid schedules`, function () { 56 | Object.keys(invalidSchedules).forEach((schedule) => { 57 | it(`for '${schedule}' should return false`, function () { 58 | expect(helper.isValidSchedule(invalidSchedules[schedule], scheduleType)).to.be.false; 59 | }); 60 | }); 61 | }); 62 | }); 63 | }); 64 | describe("getRequiredPropertiesForType", function () { 65 | const scheduleTypeProps = { 66 | global: ["from", "to"], 67 | group: ["from", "to"], 68 | module: ["from", "to"], 69 | notification: ["schedule", "notification"] 70 | }; 71 | Object.keys(scheduleTypeProps).forEach((scheduleTypeProp) => { 72 | it(`for '${scheduleTypeProp}' should return '${scheduleTypeProps[scheduleTypeProp]}'`, function () { 73 | expect(helper.getRequiredPropertiesForType(scheduleTypeProp)).to.eql(scheduleTypeProps[scheduleTypeProp]); 74 | }); 75 | }); 76 | }); 77 | describe("isValidAction", function () { 78 | const actions = { 79 | show: true, 80 | hide: true, 81 | dim: true, 82 | send: true, 83 | "anything else": false 84 | }; 85 | Object.keys(actions).forEach((action) => { 86 | it(`for '${action}' should return '${actions[action]}'`, function () { 87 | expect(helper.isValidAction(action)).to.eql(actions[action]); 88 | }); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /MMM-ModuleScheduler.js: -------------------------------------------------------------------------------- 1 | /* Magic Mirror 2 | * Module: MMM-ModuleScheduler 3 | * 4 | * By Ian Perrin http://ianperrin.com 5 | * MIT Licensed. 6 | */ 7 | Module.register("MMM-ModuleScheduler", { 8 | // Set the minimum MagicMirror module version for this module. 9 | requiresVersion: "2.0.0", 10 | 11 | // Module config defaults. 12 | defaults: { 13 | schedulerClass: "scheduler", 14 | animationSpeed: 1000, 15 | notification_schedule: false, 16 | global_schedule: false, 17 | debug: false, 18 | uselock: true 19 | }, 20 | 21 | // Define start sequence. 22 | start: function () { 23 | Log.info("Starting module: " + this.name); 24 | this.sendSocketNotification("INITIALISE_SCHEDULER", this.config); 25 | }, 26 | 27 | notificationReceived: function (notification, payload, sender) { 28 | var self = this; 29 | if (sender === undefined && notification === "ALL_MODULES_STARTED") { 30 | // Create notification schedules 31 | if (this.config.notification_schedule) { 32 | this.sendSocketNotification("CREATE_NOTIFICATION_SCHEDULE", this.config.notification_schedule); 33 | } 34 | return; 35 | } 36 | if (sender === undefined && notification === "DOM_OBJECTS_CREATED") { 37 | // Create global schedules 38 | if (typeof this.config.global_schedule === "object") { 39 | this.sendSocketNotification("CREATE_GLOBAL_SCHEDULE", this.config.global_schedule); 40 | } 41 | // Create module schedules 42 | MM.getModules() 43 | .exceptModule(this) 44 | .withClass(this.config.schedulerClass) 45 | .enumerate(function (module) { 46 | Log.log(self.name + " wants to schedule the display of " + module.name); 47 | if (typeof module.config.module_schedule === "object") { 48 | self.sendSocketNotification("CREATE_MODULE_SCHEDULE", { name: module.name, id: module.identifier, schedule: module.config.module_schedule }); 49 | } else { 50 | Log.error(module.name + " is configured to be scheduled, but the module_schedule option is undefined"); 51 | } 52 | }); 53 | return; 54 | } 55 | }, 56 | 57 | socketNotificationReceived: function (notification, payload) { 58 | var self = this; 59 | if (notification === "SHOW_MODULE" || notification === "HIDE_MODULE" || notification === "DIM_MODULE") { 60 | Log.log(this.name + " received a " + notification + " notification for " + payload.target); 61 | MM.getModules() 62 | .exceptModule(this) 63 | .withClass(this.config.schedulerClass) 64 | .enumerate(function (module) { 65 | if (payload.target === module.identifier) { 66 | self.setModuleDisplay(module, notification, payload.dimLevel ? payload.dimLevel : "25"); 67 | return; 68 | } 69 | }); 70 | } 71 | if (notification === "SHOW_MODULES" || notification === "HIDE_MODULES" || notification === "DIM_MODULES") { 72 | Log.log(this.name + " received a " + notification + " notification for " + (payload.target ? payload.target : "all") + " modules"); 73 | // Get all modules except this one 74 | var modules = MM.getModules().exceptModule(this); 75 | // Restrict to group of modules with specified class 76 | if (payload.target) { 77 | modules = modules.withClass(payload.target); 78 | } 79 | // Ignore specified modules 80 | if (payload.ignoreModules) { 81 | modules = modules.filter(function (module) { 82 | if (payload.ignoreModules.indexOf(module.name) === -1) { 83 | return true; 84 | } 85 | Log.log(self.name + " is ignoring " + module.name + " from the " + notification + " notification for " + (payload.target ? payload.target : "all") + " modules"); 86 | return false; 87 | }); 88 | } 89 | // Process the notification request 90 | var action = notification.replace("_MODULES", "_MODULE"); 91 | var brightness = payload.dimLevel ? payload.dimLevel : "25"; 92 | for (var i = 0; i < modules.length; i++) { 93 | this.setModuleDisplay(modules[i], action, brightness); 94 | } 95 | return; 96 | } 97 | if (notification === "SEND_NOTIFICATION") { 98 | Log.log(this.name + " received a request to send a " + payload.target + " notification"); 99 | this.sendNotification(payload.target, payload.payload); 100 | return; 101 | } 102 | }, 103 | 104 | setModuleDisplay: function (module, action, brightness) { 105 | const options = this.config.uselock ? { lockString: this.identifier } : ""; 106 | Log.log(this.name + " is processing the " + action + (action === "DIM_MODULE" ? " (" + brightness + "%)" : "") + " request for " + module.identifier); 107 | 108 | if (action === "SHOW_MODULE") { 109 | module["show"]( 110 | this.config.animationSpeed, 111 | () => { 112 | Log.log(this.name + " has shown " + module.identifier); 113 | this.setModuleBrightness(module.identifier, 100); 114 | }, 115 | options 116 | ); 117 | return true; 118 | } 119 | 120 | if (action === "HIDE_MODULE") { 121 | module.hide(this.config.animationSpeed, Log.log(this.name + " has hidden " + module.identifier), options); 122 | return true; 123 | } 124 | 125 | if (action === "DIM_MODULE") { 126 | this.setModuleBrightness(module.identifier, brightness); 127 | return true; 128 | } 129 | 130 | return false; 131 | }, 132 | setModuleBrightness(moduleIdentifier, brightness = 100) { 133 | const moduleDiv = document.getElementById(moduleIdentifier); 134 | if (moduleDiv) { 135 | moduleDiv.style.filter = "brightness(" + brightness + "%)"; 136 | Log.log(this.name + " has set the brightness of " + moduleIdentifier + " to " + brightness + "%"); 137 | } 138 | } 139 | }); 140 | -------------------------------------------------------------------------------- /node_helper.js: -------------------------------------------------------------------------------- 1 | /* Magic Mirror 2 | * Node Helper: MMM-ModuleScheduler 3 | * 4 | * By Ian Perrin http://ianperrin.com 5 | * MIT Licensed. 6 | */ 7 | 8 | var NodeHelper = require("node_helper"); 9 | var CronJob = require("cron").CronJob; 10 | 11 | // MODULE CONSTANTS 12 | const SCHEDULE_TYPE_GLOBAL = "global"; 13 | const SCHEDULE_TYPE_GROUP = "group"; 14 | const SCHEDULE_TYPE_MODULE = "module"; 15 | const SCHEDULE_TYPE_NOTIFICATION = "notification"; 16 | const JOB_ACTION_SHOW = "show"; 17 | const JOB_ACTION_HIDE = "hide"; 18 | const JOB_ACTION_DIM = "dim"; 19 | const JOB_ACTION_SEND = "send"; 20 | 21 | module.exports = NodeHelper.create({ 22 | scheduledJobs: [], 23 | 24 | // Override start method. 25 | start: function () { 26 | console.log("Starting node helper for: " + this.name); 27 | }, 28 | 29 | // Override socketNotificationReceived method. 30 | socketNotificationReceived: function (notification, payload) { 31 | this.log(this.name + " received " + notification); 32 | 33 | if (notification === "INITIALISE_SCHEDULER") { 34 | this.log(this.name + " is setting the config"); 35 | this.config = payload; 36 | this.removeScheduledJobs(); 37 | return true; 38 | } 39 | if (notification === "CREATE_NOTIFICATION_SCHEDULE") { 40 | this.createScheduleForNotifications(payload); 41 | return true; 42 | } 43 | if (notification === "CREATE_GLOBAL_SCHEDULE") { 44 | this.createGlobalSchedules(payload); 45 | return true; 46 | } 47 | if (notification === "CREATE_MODULE_SCHEDULE") { 48 | this.createScheduleForModule(payload); 49 | return true; 50 | } 51 | }, 52 | 53 | removeScheduledJobs: function () { 54 | this.log(this.name + " is removing all scheduled jobs"); 55 | for (var i = 0; i < this.scheduledJobs.length; i++) { 56 | var scheduledJob = this.scheduledJobs[i]; 57 | if (typeof scheduledJob.showJob === "object") { 58 | this.stopCronJob(scheduledJob.showJob); 59 | } 60 | if (typeof scheduledJob.hideJob === "object") { 61 | this.stopCronJob(scheduledJob.hideJob); 62 | } 63 | if (typeof scheduledJob.notificationJob === "object") { 64 | this.stopCronJob(scheduledJob.notificationJob); 65 | } 66 | } 67 | this.scheduledJobs.length = 0; 68 | }, 69 | 70 | stopCronJob: function (cronJob) { 71 | try { 72 | cronJob.stop(); 73 | } catch (ex) { 74 | this.log(this.name + " could not stop cronJob"); 75 | } 76 | }, 77 | 78 | createScheduleForNotifications: function (notification_schedule) { 79 | var notificationSchedules = this.getOrMakeArray(notification_schedule); 80 | 81 | for (var i = 0; i < notificationSchedules.length; i++) { 82 | var notificationSchedule = notificationSchedules[i]; 83 | 84 | // Validate Schedule Definition 85 | if (!this.isValidSchedule(notificationSchedule, SCHEDULE_TYPE_NOTIFICATION)) { 86 | break; 87 | } 88 | 89 | // Create cronJobs 90 | this.log(this.name + " is scheduling " + notificationSchedule.notification + ' using "' + notificationSchedule.schedule); 91 | var notificationJob = this.createCronJob(SCHEDULE_TYPE_NOTIFICATION, notificationSchedule.schedule, JOB_ACTION_SEND, { target: notificationSchedule.notification, payload: notificationSchedule.payload }); 92 | if (!notificationJob) { 93 | break; 94 | } 95 | 96 | // Store scheduledJobs 97 | this.scheduledJobs.push({ notificationJob: notificationJob }); 98 | 99 | this.log(this.name + " has scheduled " + notificationSchedule.notification); 100 | this.log(this.name + " will next send " + notificationSchedule.notification + " at " + new Date(notificationJob.nextDate())); 101 | } 102 | }, 103 | 104 | createScheduleForModule: function (module) { 105 | var moduleSchedules = this.getOrMakeArray(module.schedule); 106 | var nextShowDate, nextHideDate, nextDimLevel; 107 | 108 | for (var i = 0; i < moduleSchedules.length; i++) { 109 | var moduleSchedule = moduleSchedules[i]; 110 | 111 | // Validate Schedule Definition 112 | if (!this.isValidSchedule(moduleSchedule, SCHEDULE_TYPE_MODULE)) { 113 | break; 114 | } 115 | 116 | // Create cronJobs 117 | this.log(this.name + " is scheduling " + module.name + ' using "' + moduleSchedule.from + '" and "' + moduleSchedule.to + '" with dim level ' + moduleSchedule.dimLevel); 118 | var showJob = this.createCronJob(SCHEDULE_TYPE_MODULE, moduleSchedule.from, JOB_ACTION_SHOW, { target: module.id }); 119 | 120 | if (!showJob) { 121 | break; 122 | } 123 | var hideJobAction = moduleSchedule.dimLevel ? JOB_ACTION_DIM : JOB_ACTION_HIDE; 124 | var hideJob = this.createCronJob(SCHEDULE_TYPE_MODULE, moduleSchedule.to, hideJobAction, { target: module.id, dimLevel: moduleSchedule.dimLevel }); 125 | if (!hideJob) { 126 | showJob.stop(); 127 | break; 128 | } 129 | 130 | // Store scheduledJobs 131 | this.scheduledJobs.push({ module: module, schedule: moduleSchedule, showJob: showJob, hideJob: hideJob }); 132 | 133 | // Store next dates 134 | if (i === 0 || new Date(showJob.nextDate()) < nextShowDate) { 135 | nextShowDate = new Date(showJob.nextDate()); 136 | } 137 | if (i === 0 || new Date(hideJob.nextDate()) < nextShowDate) { 138 | nextHideDate = new Date(hideJob.nextDate()); 139 | nextDimLevel = moduleSchedule.dimLevel; 140 | } 141 | } 142 | 143 | if (nextHideDate && nextShowDate) { 144 | var now = new Date(); 145 | if (nextShowDate > now && nextHideDate > nextShowDate) { 146 | if (nextDimLevel > 0) { 147 | this.log(this.name + " is dimming " + module.name); 148 | this.sendSocketNotification("DIM_MODULE", { target: module.id, dimLevel: nextDimLevel }); 149 | } else { 150 | this.log(this.name + " is hiding " + module.name); 151 | this.sendSocketNotification("HIDE_MODULE", { target: module.id }); 152 | } 153 | } 154 | this.log(this.name + " has scheduled " + module.name); 155 | this.log(this.name + " will next show " + module.name + " at " + nextShowDate); 156 | this.log(this.name + " will next " + (nextDimLevel ? JOB_ACTION_DIM : JOB_ACTION_HIDE) + " " + module.name + " at " + nextHideDate); 157 | } 158 | }, 159 | 160 | createGlobalSchedules: function (global_schedule) { 161 | var globalSchedules = this.getOrMakeArray(global_schedule); 162 | 163 | for (var i = 0; i < globalSchedules.length; i++) { 164 | var globalSchedule = globalSchedules[i]; 165 | var groupOrAll = globalSchedule.groupClass ? globalSchedule.groupClass : "all"; 166 | 167 | // Validate Schedule Definition 168 | if (!this.isValidSchedule(globalSchedule, SCHEDULE_TYPE_GLOBAL)) { 169 | break; 170 | } 171 | 172 | // Create cronJobs 173 | this.log(this.name + " is creating a global schedule for " + groupOrAll + ' modules using "' + globalSchedule.from + '" and "' + globalSchedule.to + '" with dim level ' + globalSchedule.dimLevel); 174 | var showJob = this.createCronJob(SCHEDULE_TYPE_GLOBAL, globalSchedule.from, JOB_ACTION_SHOW, { target: globalSchedule.groupClass, ignoreModules: globalSchedule.ignoreModules }); 175 | if (!showJob) { 176 | break; 177 | } 178 | var hideJobAction = globalSchedule.dimLevel ? JOB_ACTION_DIM : JOB_ACTION_HIDE; 179 | var hideJob = this.createCronJob(SCHEDULE_TYPE_GLOBAL, globalSchedule.to, hideJobAction, { dimLevel: globalSchedule.dimLevel, target: globalSchedule.groupClass, ignoreModules: globalSchedule.ignoreModules }); 180 | if (!hideJob) { 181 | showJob.stop(); 182 | break; 183 | } 184 | 185 | // Store scheduledJobs 186 | this.scheduledJobs.push({ schedule: globalSchedule, showJob: showJob, hideJob: hideJob }); 187 | 188 | // Check next dates 189 | var nextShowDate = new Date(showJob.nextDate()); 190 | var nextHideDate = new Date(hideJob.nextDate()); 191 | var now = new Date(); 192 | if (nextShowDate > now && nextHideDate > nextShowDate) { 193 | if (globalSchedule.dimLevel > 0) { 194 | this.log(this.name + " is dimming " + groupOrAll + " modules"); 195 | this.sendSocketNotification("DIM_MODULES", { dimLevel: globalSchedule.dimLevel, target: globalSchedule.groupClass, ignoreModules: globalSchedule.ignoreModules }); 196 | } else { 197 | this.log(this.name + " is hiding " + groupOrAll + " modules"); 198 | this.sendSocketNotification("HIDE_MODULES", { target: globalSchedule.groupClass, ignoreModules: globalSchedule.ignoreModules }); 199 | } 200 | } 201 | this.log(this.name + " has created the global schedule for " + groupOrAll + " modules"); 202 | this.log(this.name + " will next show " + groupOrAll + " modules at " + nextShowDate); 203 | this.log(this.name + " will next " + (globalSchedule.dimLevel ? JOB_ACTION_DIM : JOB_ACTION_HIDE) + " " + groupOrAll + " modules at " + nextHideDate); 204 | } 205 | }, 206 | 207 | /** 208 | * Returns a CronJob object that has been scheduled to trigger the 209 | * specified action based on the supplied cronTime and options 210 | * @param {string} type the type of schedule to be created (either global, module or notification) 211 | * @param {object} cronTime a cron expression which determines when the job will fire 212 | * @param {string} action the action which should be performed (either show, hide, dim or send) 213 | * @param {object} options an object containing the options for the job (e.g. target, dimLevel, ignoreModules) 214 | * @returns {object} the scheduled cron job 215 | * @see CronJob 216 | */ 217 | createCronJob: function (type, cronTime, action, options) { 218 | var self = this; 219 | 220 | // Validate Action 221 | if (!this.isValidAction(action)) { 222 | return false; 223 | } 224 | 225 | // Build notification 226 | var notification = action.toUpperCase(); 227 | notification += type === SCHEDULE_TYPE_NOTIFICATION ? "_NOTIFICATION" : "_MODULE"; 228 | notification += type === SCHEDULE_TYPE_GLOBAL ? "S" : ""; 229 | 230 | try { 231 | var job = new CronJob( 232 | cronTime, 233 | function () { 234 | self.log(self.name + " is sending " + notification + " to " + options.target); 235 | self.sendSocketNotification(notification, options); 236 | self.log(self.name + " will next send " + notification + " to " + options.target + " at " + new Date(this.nextDate()) + ' based on "' + cronTime + '"'); 237 | }, 238 | function () { 239 | self.log(self.name + " has completed the " + action + " job for " + options.target + ' based on "' + cronTime + '"'); 240 | }, 241 | true 242 | ); 243 | return job; 244 | } catch (ex) { 245 | this.log(this.name + " could not create " + type + " schedule - check " + action + ' expression: "' + cronTime + '"'); 246 | return null; 247 | } 248 | }, 249 | 250 | /** 251 | * Returns either the original array or a new array holding the supplied value 252 | * @param {object} arrayOrString either an existing array or value to be used to create the new array 253 | * @returns {object} an array 254 | * @see Array 255 | */ 256 | getOrMakeArray: function (arrayOrString) { 257 | if (Array.isArray(arrayOrString)) { 258 | return arrayOrString; 259 | } else { 260 | return [arrayOrString]; 261 | } 262 | }, 263 | 264 | /** 265 | * Validates a schedule definition by determining whether it has the required 266 | * properties defined 267 | * @param {object} schedule_definition The schedule definition to be validated 268 | * @param {string} type The type of schedule to be created (either global, module or notification) 269 | * @returns {boolean} true or false 270 | */ 271 | isValidSchedule: function (schedule_definition, type) { 272 | var requiredProperties = this.getRequiredPropertiesForType(type); 273 | if (!requiredProperties) { 274 | this.log(this.name + " cannot validate required properties for `" + type + "_schedule`"); 275 | return false; 276 | } 277 | for (var i = 0; i < requiredProperties.length; i++) { 278 | var prop = requiredProperties[i]; 279 | if (!Object.prototype.hasOwnProperty.call(schedule_definition, prop)) { 280 | this.log(this.name + " cannot create schedule. Missing `" + prop + "` in `" + type + "_schedule`: " + JSON.stringify(schedule_definition)); 281 | return false; 282 | } 283 | } 284 | return true; 285 | }, 286 | 287 | /** 288 | * Determine whether a string is a valid action 289 | * @param {string} action The string to be validated 290 | * @returns {boolean} true or false 291 | */ 292 | isValidAction: function (action) { 293 | if (action !== JOB_ACTION_SHOW && action !== JOB_ACTION_HIDE && action !== JOB_ACTION_DIM && action !== JOB_ACTION_SEND) { 294 | this.log(this.name + " cannot create schedule. Expected show/hide/dim/send, not " + action); 295 | return false; 296 | } 297 | return true; 298 | }, 299 | 300 | /** 301 | * Gets an array of names for the properties required by the given schedule type 302 | * @param {string} type The scheduled type for which properties are required 303 | * @returns {object} An Array of property names 304 | */ 305 | getRequiredPropertiesForType: function (type) { 306 | if (type === SCHEDULE_TYPE_MODULE || type === SCHEDULE_TYPE_GLOBAL || type === SCHEDULE_TYPE_GROUP) { 307 | return ["from", "to"]; 308 | } else if (type === SCHEDULE_TYPE_NOTIFICATION) { 309 | return ["schedule", "notification"]; 310 | } else { 311 | return false; 312 | } 313 | }, 314 | 315 | /** 316 | * Outputs a message to the console/log when debugging is enabled 317 | * @param {string} msg A string containing the message to be output 318 | */ 319 | log: function (msg) { 320 | if (this.config && this.config.debug) { 321 | console.log(msg); 322 | } 323 | } 324 | }); 325 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MMM-ModuleScheduler 2 | 3 | A [MagicMirror²](https://magicmirror.builders) helper module to schedule when modules should be shown, hidden or dimmed and when notifications should be sent. 4 | 5 | [![Platform](https://img.shields.io/badge/platform-MagicMirror-informational)](https://MagicMirror.builders) 6 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://raw.githubusercontent.com/ianperrin/MMM-ModuleScheduler/master/LICENSE) 7 | ![Test Status](https://github.com/ianperrin/MMM-ModuleScheduler/actions/workflows/node.js.yml/badge.svg) 8 | [![Known Vulnerabilities](https://snyk.io/test/github/ianperrin/MMM-ModuleScheduler/badge.svg)](https://snyk.io/test/github/ianperrin/MMM-ModuleScheduler) 9 | 10 | ![Example Scheduling](.github/example.gif) 11 | 12 | ## Installation 13 | 14 | In your terminal, go to your MagicMirror's Module folder, clone this repository, go to the modules folder, install the dependencies: 15 | 16 | ``` 17 | cd ~/MagicMirror/modules 18 | git clone https://github.com/ianperrin/MMM-ModuleScheduler.git 19 | cd MMM-ModuleScheduler 20 | npm install --omit=dev 21 | ``` 22 | 23 | Add the module to the modules array in the `config/config.js` file: 24 | 25 | ```javascript 26 | { 27 | module: 'MMM-ModuleScheduler' 28 | }, 29 | ``` 30 | 31 | ## Config Options 32 | 33 | | **Option** | **Default** | **Description** | 34 | | ----------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | 35 | | `schedulerClass` | 'scheduler' | **Optional** The name of the class which should be used to identify the modules which have an individual schedule. | 36 | | `animationSpeed` | 1000 | **Optional** The speed of the show and hide animations in milliseconds | 37 | | `notification_schedule` | | **Optional** A single, or array of multiple definitions to schedule when notifications should be sent. See [Scheduling Notifications](#scheduling-notifications) | 38 | | `global_schedule` | | **Optional** A single, or array of multiple definitions to schedule when all modules should be shown/hidden/dimmed. See [Global Schedules](#global-schedules) | 39 | | `uselock` | `true` | **Optional** If set to `false`, scheduler don't lock the hidden modules. Other modules can then be used to show the modules if they are hidden by the scheduler. | 40 | | `debug` | `true` | **Optional** Outputs messages to the console/log when set to `true` | 41 | 42 | ## Config Examples 43 | 44 | Sending notifications 45 | 46 | - [Basic example](#scheduling-notifications) 47 | - [Multiple notifications](#scheduling-multiple-notifications) 48 | - [Remote control notifications](#scheduling-actions-to-control-your-magicmirror-pi-and-monitorscreen) 49 | 50 | Module Display 51 | 52 | - [Hide or show all modules](#global-schedules) 53 | - [Hide or show a group of modules](#group-schedules) 54 | - [Hide or show individual modules](#individual-module-schedules) 55 | - [Dimming modules](#dimming-modules) 56 | - [Multiple schedules](#multiple-schedules) 57 | - [Ignoring Modules](#ignoring-modules) 58 | 59 | ### Scheduling Notifications 60 | 61 | To schedule the sending of a notification to other modules, add a `notification_schedule` definition to the MMM-ModuleScheduler config, e.g. 62 | 63 | ```javascript 64 | { 65 | module: 'MMM-ModuleScheduler', 66 | config: { 67 | // SHOW AN ALERT AT 09:30 EVERY DAY (see https://github.com/MichMich/MagicMirror/tree/develop/modules/default/alert) 68 | notification_schedule: { 69 | notification: 'SHOW_ALERT', 70 | schedule: '30 9 * * *', 71 | payload: { 72 | type: "notification", 73 | title: 'Scheduled alert!' 74 | } 75 | } 76 | } 77 | }, 78 | ``` 79 | 80 | **Notes** 81 | 82 | - `notification` is required and should be the identifier of the notification to be sent to all other modules. 83 | - `schedule` is required and determines when the notification will be sent. It should be a valid cron expression - see [crontab.guru](http://crontab.guru/). 84 | - `payload` is optional and its contents will be determined by the module receiving the notification. 85 | 86 | * When specifying your schedule values make sure that your values fall within the ranges below. 87 | 88 | | **Unit** | **Format** | 89 | | -------------- | -------------- | 90 | | `Seconds` | 0-59 | 91 | | `Minutes` | 0-59 | 92 | | `Hours` | 0-23 | 93 | | `Day of Month` | 1-31 | 94 | | `Months` | 0-11 (Jan-Dec) | 95 | | `Day of Week` | 0-6 (Sun-Sat) | 96 | 97 | #### Scheduling Multiple Notifications 98 | 99 | Multiple `notification_schedule` definitions can be added using an array, e.g. 100 | 101 | ```javascript 102 | { 103 | module: 'MMM-ModuleScheduler', 104 | config: { 105 | notification_schedule: [ 106 | // SHOW AN ALERT AT 07:30 EVERY DAY 107 | {notification: 'SHOW_ALERT', schedule: '30 7 * * *', payload: {type: "notification", title: 'Good morning!'}}, 108 | // SHOW AN ALERT AT 17:45 EVERY DAY 109 | {notification: 'SHOW_ALERT', schedule: '17 45 * * *', payload: {type: "notification", title: 'Good afternoon!'}} 110 | ] 111 | } 112 | } 113 | ``` 114 | 115 | #### Scheduling actions to control your MagicMirror, Pi and monitor/screen 116 | 117 | Used in conjunction with [MMM-Remote-Control](https://github.com/Jopyth/MMM-Remote-Control) module, the `notification_schedule` definitions can create schedules to control your MagicMirror, Pi and monitor/screen, e.g. 118 | 119 | ```javascript 120 | { 121 | module: 'MMM-ModuleScheduler', 122 | config: { 123 | notification_schedule: [ 124 | // TURN THE MONITOR/SCREEN ON AT 07:30 EVERY DAY 125 | {notification: 'REMOTE_ACTION', schedule: '30 7 * * *', payload: {action: "MONITORON"}}, 126 | // TURN THE MONITOR/SCREEN OFF AT 22:30 EVERY DAY 127 | {notification: 'REMOTE_ACTION', schedule: '30 22 * * *', payload: {action: "MONITOROFF"}}, 128 | // RESTART THE MAGICMIRROR PROCESS AT 2am EVERY SUNDAY 129 | {notification: 'REMOTE_ACTION', schedule: '0 2 * * SUN', payload: {action: "RESTART"}} 130 | ] 131 | } 132 | }, 133 | ``` 134 | 135 | **Notes** 136 | 137 | - A full list of remote actions available for controlling your MagicMirror, Pi and monitor/screen are available in the [MMM-Remote-Control module documentation](https://github.com/Jopyth/MMM-Remote-Control#list-of-actions) 138 | - If you simply want to hide and show modules, it is recommended to use the module display scheduling options defined below, rather than the `SHOW` and `HIDE` remote actions. 139 | 140 | ### Scheduling Module Display 141 | 142 | #### Global Schedules 143 | 144 | To schedule when all modules are shown (or hidden) by the Magic Mirror, add a `global_schedule` definition to the MMM-ModuleScheduler config, e.g. 145 | 146 | ```javascript 147 | { 148 | module: 'MMM-ModuleScheduler', 149 | config: { 150 | // SHOW ALL MODULES AT 06:00 AND HIDE AT 22:00 EVERY DAY 151 | global_schedule: {from: '0 6 * * *', to: '0 22 * * *' }, 152 | } 153 | }, 154 | ``` 155 | 156 | #### Group Schedules 157 | 158 | To apply a schedule to a group of modules, add the `groupClass` option to the `global_schedule` definition, e.g. 159 | 160 | ```javascript 161 | { 162 | module: 'MMM-ModuleScheduler', 163 | config: { 164 | // SHOW MODULES WITH THE CLASS 'daytime_scheduler' AT 06:00 AND HIDE AT 22:00 EVERY DAY 165 | global_schedule: {from: '0 6 * * *', to: '0 22 * * *', groupClass: 'daytime_scheduler'}, 166 | } 167 | }, 168 | { 169 | module: 'clock', 170 | position: 'top_left', 171 | classes: 'daytime_scheduler' 172 | } 173 | { 174 | module: 'compliments', 175 | position: 'lower_third', 176 | classes: 'daytime_scheduler' 177 | }, 178 | 179 | ``` 180 | 181 | **Notes** 182 | 183 | - Modules scheduled as a group, only need the `groupClass` adding to the `classes` option in their config. The `schedulerClass` option can be omitted unless indiviudal schedules also exist. 184 | 185 | #### Individual Module Schedules 186 | 187 | To schedule when an individual module is shown (or hidden) by the Magic Mirror, modify the configuration for that module so that it includes the `classes` and `module_schedule` options. e.g. 188 | 189 | ```javascript 190 | { 191 | module: 'calendar', 192 | header: 'US Holidays', 193 | position: 'top_left', 194 | classes: 'scheduler', 195 | config: { 196 | // DISPLAY THE CALENDAR BETWEEN 09:00 and 18:00 ON WEDNESDAYS 197 | module_schedule: {from: '0 9 * * 3', to: '0 18 * * 3' }, 198 | calendars: [ 199 | { 200 | symbol: 'calendar-check-o ', 201 | url: 'webcal://www.calendarlabs.com/templates/ical/US-Holidays.ics' 202 | } 203 | ] 204 | } 205 | }, 206 | ``` 207 | 208 | **Notes** 209 | 210 | - `from` is required and determines when the module will be shown. It should be a valid cron expression - see [crontab.guru](http://crontab.guru/). 211 | - `to` is required and determines when the module will be hidden. It should be a valid cron expression - see [crontab.guru](http://crontab.guru/). 212 | 213 | #### Dimming Modules 214 | 215 | To dim modules, rather than hide them, add the `dimLevel` option (as a percentage between 0 and 100) to the `global_schedule` and `module_schedule` definitions. e.g. 216 | 217 | ```javascript 218 | { 219 | module: 'MMM-ModuleScheduler', 220 | config: { 221 | // SHOW ALL MODULES AT 06:00 AND DIM THEM TO 40% AT 22:00 222 | global_schedule: {from: '0 6 * * *', to: '0 22 * * *', dimLevel: '40' }, 223 | } 224 | }, 225 | { 226 | module: 'clock', 227 | position: 'top_left', 228 | classes: 'scheduler', 229 | config: { 230 | // SHOW THE CLOCK AT 06:30 AND DIM IT TO 25% AT 22:30 231 | module_schedule: {from: '30 6 * * *', to: '30 22 * * *', dimLevel: '25'} 232 | } 233 | }, 234 | ``` 235 | 236 | **Note:** 237 | 238 | - The modules will be shown (full brightness) based on the `from` expression 239 | - The modules will then either be dimmed (if the `dimLevel` option is set) based on the `to` expression. 240 | - Take care when adding both `global_schedule` and `module_schedule` definitions as MMM-ModuleScheduler performs no validation that they will be compatible. 241 | 242 | #### Multiple Schedules 243 | 244 | For more complex scheduling, multiple `global_schedule` and `module_schedule` definitions can be added using an array, e.g. 245 | 246 | ```javascript 247 | { 248 | module: 'MMM-ModuleScheduler', 249 | config: { 250 | global_schedule: [ 251 | // SHOW MODULES WITH THE CLASS 'morning_scheduler' AT 06:00 AND HIDE AT 09:00 EVERY DAY 252 | {from: '0 6 * * *', to: '0 9 * * *', groupClass: 'morning_scheduler'}, 253 | // SHOW MODULES WITH THE CLASS 'evening_scheduler' AT 17:00 AND HIDE AT 23:00 EVERY DAY 254 | {from: '0 17 * * *', to: '0 22 * * *', groupClass: 'evening_scheduler'}, 255 | ] 256 | } 257 | }, 258 | { 259 | module: 'clock', 260 | position: 'top_left', 261 | classes: 'scheduler', 262 | config: { 263 | // DISPLAY BETWEEN 09:30 ON SATURDAYS AND 22:30 ON SUNDAYS, 264 | // THEN AGAIN BETWEEN 20:00 AND 23:00 ON TUESDAYS AND WEDNESDAYS 265 | module_schedule: [ 266 | {from: '30 9 * * SAT', to: '30 22 * * SUN'}, 267 | {from: '0 20 * * 2-3', to: '0 23 * * 2-3'} 268 | ] 269 | } 270 | }, 271 | ``` 272 | 273 | **Note:** 274 | 275 | - Take care when adding both `global_schedule` and `module_schedule` definitions as MMM-ModuleScheduler performs no validation that they will be compatible. 276 | 277 | #### Ignoring Modules 278 | 279 | To ignore modules from being shown, hidden or dimmed by a global schedules, add the `ignoreModules` option to the `global_schedule` definition e.g. 280 | 281 | ```javascript 282 | { 283 | module: 'MMM-ModuleScheduler', 284 | config: { 285 | // SHOW ALL MODULES EXCEPT clock AND calender BETWEEN 06:00 AND 22:00 286 | global_schedule: {from: '0 6 * * *', to: '0 22 * * *', ignoreModules: ['clock', 'calendar'] }, 287 | } 288 | }, 289 | ``` 290 | 291 | **Note:** 292 | 293 | - Modules are ignored based on their name, as defined in the config file. If multiple instances of a single module are defined in the `config.js` file, all instances will be ignored using this option. 294 | 295 | ## Updating 296 | 297 | To update the module to the latest version, use your terminal to go to your MMM-ModuleScheduler module folder and type the following command: 298 | 299 | ``` 300 | git pull 301 | ``` 302 | 303 | If you haven't changed the modules, this should work without any problems. 304 | Type `git status` to see your changes, if there are any, you can reset them with `git reset --hard`. After that, git pull should be possible. 305 | --------------------------------------------------------------------------------