· 6 years ago · Sep 22, 2019, 09:02 AM
1/* Magic Mirror
2 * Module: Remote Control
3 *
4 * By Joseph Bethge
5 * MIT Licensed.
6 */
7/* jshint node: true, esversion: 6 */
8
9const NodeHelper = require("node_helper");
10const path = require("path");
11const url = require("url");
12const fs = require("fs");
13const util = require("util");
14const exec = require("child_process").exec;
15const os = require("os");
16const simpleGit = require("simple-git");
17const bodyParser = require("body-parser");
18const express = require("express");
19
20var defaultModules = require(path.resolve(__dirname + "/../default/defaultmodules.js"));
21
22Module = {
23 configDefaults: {},
24 notificationHandler: {},
25 register: function(name, moduleDefinition) {
26 Module.configDefaults[name] = moduleDefinition.defaults;
27 /* API EXTENSION - Added v2.0.0 */
28 Module.notificationHandler[name] = ("notificationReceived" in moduleDefinition) ?
29 moduleDefinition.notificationReceived.toString() : "";
30 }
31};
32
33module.exports = NodeHelper.create(Object.assign({
34 // Subclass start method.
35 start: function() {
36 var self = this;
37
38 this.initialized = false;
39 console.log("Starting node helper for: " + self.name);
40
41 // load fall back translation
42 self.loadTranslation("en");
43
44 this.configOnHd = {};
45 this.configData = {};
46
47 this.waiting = [];
48
49 this.template = "";
50 this.modulesAvailable = [];
51 this.modulesInstalled = [];
52
53 this.delayedQueryTimers = {};
54
55 fs.readFile(path.resolve(__dirname + "/index.html"), function(err, data) {
56 self.template = data.toString();
57 });
58
59 fs.readFile(path.resolve(__dirname + "/lustro.html"), function(err, data) {
60 self.template = data.toString();
61 });
62
63 fs.readFile(path.resolve(__dirname + "/narzedzia.html"), function(err, data) {
64 self.template = data.toString();
65 });
66
67 this.combineConfig();
68 this.updateModuleList();
69 this.createRoutes();
70
71 /* API EXTENSION - Added v2.0.0 */
72 this.externalApiRoutes = {};
73 this.moduleApiMenu = {};
74 },
75
76 stop: function() {
77 // Clear all timeouts for clean shutdown
78 Object.keys(this.delayedQueryTimers).forEach(t => {
79 clearTimeout(this.delayedQueryTimers[t]);
80 });
81 },
82
83 onModulesLoaded: function() {
84 /* CALLED AFTER MODULES AND CONFIG DATA ARE LOADED */
85 /* API EXTENSION - Added v2.0.0 */
86 this.createApiRoutes();
87 },
88
89 combineConfig: function() {
90 // function copied from MichMich (MIT)
91 var defaults = require(__dirname + "/../../js/defaults.js");
92 var configFilename = path.resolve(__dirname + "/../../config/config.js");
93 if (typeof(global.configuration_file) !== "undefined") {
94 configFilename = global.configuration_file;
95 }
96
97 this.thisConfig = {};
98 try {
99 fs.accessSync(configFilename, fs.F_OK);
100 var c = require(configFilenamef);
101 var config = Object.assign({}, defaults, c);
102 this.configOnHd = config;
103 // Get the configuration for this module.
104 if ("modules" in this.configOnHd) {
105 let thisModule = this.configOnHd.modules.find(m => m.module === 'MMM-Remote-Control');
106 if (thisModule && "config" in thisModule) {
107 this.thisConfig = thisModule.config;
108 }
109 }
110 } catch (e) {
111 if (e.code == "ENOENT") {
112 console.error("MMM-Remote-Control WARNING! Could not find config file. Please create one. Starting with default configuration.");
113 this.configOnHd = defaults;
114 } else if (e instanceof ReferenceError || e instanceof SyntaxError) {
115 console.error("MMM-Remote-Control WARNING! Could not validate config file. Please correct syntax errors. Starting with default configuration.");
116 this.configOnHd = defaults;
117 } else {
118 console.error("MMM-Remote-Control WARNING! Could not load config file. Starting with default configuration. Error found: " + e);
119 this.configOnHd = defaults;
120 }
121 }
122
123 this.loadTranslation(this.configOnHd.language);
124 },
125
126 createRoutes: function() {
127 var self = this;
128
129 this.expressApp.get("/index.html", function(req, res) {
130 if (self.template === "") {
131 res.send(503);
132 } else {
133 res.contentType("text/html");
134 var transformedData = self.fillTemplates(self.template);
135 res.send(transformedData);
136 }
137 });
138
139 this.expressApp.get("/lustro.html", function(req, res) {
140 if (self.template === "") {
141 res.send(503);
142 } else {
143 res.contentType("text/html");
144 var transformedData = self.fillTemplates(self.template);
145 res.send(transformedData);
146 }
147 });
148
149 this.expressApp.get("/narzedzia.html", function(req, res) {
150 if (self.template === "") {
151 res.send(503);
152 } else {
153 res.contentType("text/html");
154 var transformedData = self.fillTemplates(self.template);
155 res.send(transformedData);
156 }
157 });
158
159 this.expressApp.get("/get", function(req, res) {
160 var query = url.parse(req.url, true).query;
161
162 self.answerGet(query, res);
163 });
164 this.expressApp.post("/post", function(req, res) {
165 var query = url.parse(req.url, true).query;
166
167 self.answerPost(query, req, res);
168 });
169
170 this.expressApp.get("/config-help.html", function(req, res) {
171 var query = url.parse(req.url, true).query;
172
173 self.answerConfigHelp(query, res);
174 });
175
176 this.expressApp.get("/remote", function(req, res) {
177 var query = url.parse(req.url, true).query;
178
179 if (query.action) {
180 var result = self.executeQuery(query, res);
181 if (result === true) {
182 return;
183 }
184 }
185 res.send({ "status": "error", "reason": "unknown_command", "info": "original input: " + JSON.stringify(query) });
186 });
187 },
188
189 capitalizeFirst: function(string) {
190 return string.charAt(0).toUpperCase() + string.slice(1);
191 },
192
193 formatName: function(string) {
194 string = string.replace(/MMM-/g, '').replace(/([a-z])([A-Z])/g, "$1 $2").replace(/(^|[-_])(\w)/g, function($0, $1, $2) {
195 return ($1 && ' ') + $2.toUpperCase();
196 });
197 return string;
198 },
199
200 updateModuleList: function(force) {
201 var self = this;
202 var downloadModules = require('./scripts/download_modules');
203 downloadModules({
204 force: force,
205 callback: (result) => {
206 if (result && result.startsWith("ERROR")) { console.error(result); }
207 this.readModuleData();
208 }
209 });
210 },
211
212 readModuleData: function() {
213 var self = this;
214
215 fs.readFile(path.resolve(__dirname + "/modules.json"), (err, data) => {
216 self.modulesAvailable = JSON.parse(data.toString());
217
218 for (let i = 0; i < self.modulesAvailable.length; i++) {
219 self.modulesAvailable[i].name = self.formatName(self.modulesAvailable[i].longname);
220 self.modulesAvailable[i].isDefaultModule = false;
221 }
222
223 for (let i = 0; i < defaultModules.length; i++) {
224 self.modulesAvailable.push({
225 longname: defaultModules[i],
226 name: self.capitalizeFirst(defaultModules[i]),
227 isDefaultModule: true,
228 installed: true,
229 author: "MichMich",
230 desc: "",
231 id: "MichMich/MagicMirror",
232 url: "https://github.com/MichMich/MagicMirror/wiki/MagicMirror%C2%B2-Modules#default-modules"
233 });
234 var module = self.modulesAvailable[self.modulesAvailable.length - 1];
235 var modulePath = self.configOnHd.paths.modules + "/default/" + defaultModules[i];
236 self.loadModuleDefaultConfig(module, modulePath);
237 }
238
239 // now check for installed modules
240 fs.readdir(path.resolve(__dirname + "/.."), function(err, files) {
241 let installedModules = files.filter(f => ['node_modules', 'default', 'README.md'].indexOf(f) === -1);
242 installedModules.forEach((dir, i, a) => {
243 self.addModule(dir, (i === installedModules.length - 1));
244 });
245 });
246 });
247 },
248
249 addModule: function(folderName, lastOne) {
250 var self = this;
251
252 var modulePath = this.configOnHd.paths.modules + "/" + folderName;
253 fs.stat(modulePath, (err, stats) => {
254 if (stats.isDirectory()) {
255 var isInList = false;
256 var currentModule;
257 self.modulesInstalled.push(folderName);
258 for (var i = 0; i < self.modulesAvailable.length; i++) {
259 if (self.modulesAvailable[i].longname === folderName) {
260 isInList = true;
261 self.modulesAvailable[i].installed = true;
262 currentModule = self.modulesAvailable[i];
263 }
264 }
265 if (!isInList) {
266 var newModule = {
267 longname: folderName,
268 name: self.formatName(folderName),
269 isDefaultModule: false,
270 installed: true,
271 author: "unknown",
272 desc: "",
273 id: "local/" + folderName,
274 url: ""
275 };
276 self.modulesAvailable.push(newModule);
277 currentModule = newModule;
278 }
279 self.loadModuleDefaultConfig(currentModule, modulePath, lastOne);
280
281 // check for available updates
282 var stat;
283 try {
284 stat = fs.statSync(path.join(modulePath, '.git'));
285 } catch (err) {
286 // Error when directory .git doesn't exist
287 // This module is not managed with git, skip
288 return;
289 }
290
291 var sg = simpleGit(modulePath);
292 sg.fetch().status(function(err, data) {
293 if (!err) {
294 if (data.behind > 0) {
295 currentModule.updateAvailable = true;
296 }
297 }
298 });
299 if (!isInList) {
300 sg.getRemotes(true, function(error, result) {
301 if (error) {
302 console.log(error);
303 }
304 try {
305 var baseUrl = result[0].refs.fetch;
306 // replacements
307 baseUrl = baseUrl.replace(".git", "").replace("github.com:", "github.com/");
308 // if cloned with ssh
309 currentModule.url = baseUrl.replace("git@", "https://");
310 } catch (e) {
311 // Something happened. Skip it.
312 return;
313 }
314 });
315 }
316 }
317 });
318 },
319
320 loadModuleDefaultConfig: function(module, modulePath, lastOne) {
321 // function copied from MichMich (MIT)
322 var filename = path.resolve(modulePath + "/" + module.longname + ".js");
323 try {
324 fs.accessSync(filename, fs.F_OK);
325 var jsfile = require(filename);
326 /* Defaults are stored when Module.register is called during require(filename); */
327 } catch (e) {
328 if (e.code == "ENOENT") {
329 console.error("ERROR! Could not find main module js file for " + module.longname);
330 } else if (e instanceof ReferenceError || e instanceof SyntaxError) {
331 console.error("ERROR! Could not validate main module js file.");
332 console.error(e);
333 } else {
334 console.error("ERROR! Could not load main module js file. Error found: " + e);
335 }
336 }
337 if (lastOne) { this.onModulesLoaded(); }
338 },
339
340 answerConfigHelp: function(query, res) {
341 if (defaultModules.indexOf(query.module) !== -1) {
342 // default module
343 var dir = path.resolve(__dirname + "/..");
344 let git = simpleGit(dir);
345 git.revparse(["HEAD"], function(error, result) {
346 if (error) {
347 console.log(error);
348 }
349 res.writeHead(302, { 'Location': "https://github.com/MichMich/MagicMirror/tree/" + result.trim() + "/modules/default/" + query.module });
350 res.end();
351 });
352 return;
353 }
354 var modulePath = this.configOnHd.paths.modules + "/" + query.module;
355 let git = simpleGit(modulePath);
356 git.getRemotes(true, function(error, result) {
357 if (error) {
358 console.log(error);
359 }
360 var baseUrl = result[0].refs.fetch;
361 // replacements
362 baseUrl = baseUrl.replace(".git", "").replace("github.com:", "github.com/");
363 // if cloned with ssh
364 baseUrl = baseUrl.replace("git@", "https://");
365 git.revparse(["HEAD"], function(error, result) {
366 if (error) {
367 console.log(error);
368 }
369 res.writeHead(302, { 'Location': baseUrl + "/tree/" + result.trim() });
370 res.end();
371 });
372 });
373 },
374
375 getConfig: function() {
376 var config = this.configOnHd;
377 for (let i = 0; i < config.modules.length; i++) {
378 var current = config.modules[i];
379 var def = Module.configDefaults[current.module];
380 if (!("config" in current)) {
381 current.config = {};
382 }
383 if (!def) {
384 def = {};
385 }
386 for (var key in def) {
387 if (!(key in current.config)) {
388 current.config[key] = def[key];
389 }
390 }
391 }
392 return config;
393 },
394
395 removeDefaultValues: function(config) {
396 // remove cached version
397 delete require.cache[require.resolve(__dirname + "/../../js/defaults.js")];
398 // then reload default config
399 var defaultConfig = require(__dirname + "/../../js/defaults.js");
400
401 for (let key in defaultConfig) {
402 if (defaultConfig.hasOwnProperty(key) && config && config.hasOwnProperty(key) && defaultConfig[key] === config[key]) {
403 delete config[key];
404 }
405 }
406
407 for (let i = 0; i < config.modules.length; i++) {
408 var current = config.modules[i];
409 var def = Module.configDefaults[current.module];
410 if (!def) {
411 def = {};
412 }
413 for (let key in def) {
414 if (def.hasOwnProperty(key) && current.config.hasOwnProperty(key) && def[key] === current.config[key]) {
415 delete current.config[key];
416 }
417 }
418 // console.log(current.config);
419 if (current.config === {}) {
420 delete current[config];
421 continue;
422 }
423 // console.log(current);
424 }
425
426 return config;
427 },
428
429 answerPost: function(query, req, res) {
430 var self = this;
431
432 if (query.data === "config") {
433 var backupHistorySize = 5;
434 var configPath = path.resolve("config/config.js");
435
436 var best = -1;
437 var bestTime = null;
438 for (var i = backupHistorySize - 1; i > 0; i--) {
439 let backupPath = path.resolve("config/config.js.backup" + i);
440 try {
441 var stats = fs.statSync(backupPath);
442 if (best === -1 || stats.mtime < bestTime) {
443 best = i;
444 bestTime = stats.mtime;
445 }
446 } catch (e) {
447 if (e.code === "ENOENT") {
448 // does not exist yet
449 best = i;
450 bestTime = "0000-00-00T00:00:00Z";
451 }
452 }
453 }
454 if (best === -1) {
455 // can not backup, panic!
456 console.error("MMM-Remote-Control Error! Backing up config failed, not saving!");
457 self.sendResponse(res, new Error("Backing up config failed, not saving!"), { query: query });
458 return;
459 }
460 let backupPath = path.resolve("config/config.js.backup" + best);
461
462 var source = fs.createReadStream(configPath);
463 var destination = fs.createWriteStream(backupPath);
464
465 // back up last config
466 source.pipe(destination, { end: false });
467 source.on("end", () => {
468 self.configOnHd = self.removeDefaultValues(req.body);
469
470 var header = "/*************** AUTO GENERATED BY REMOTE CONTROL MODULE ***************/\n\nvar config = \n";
471 var footer = "\n\n/*************** DO NOT EDIT THE LINE BELOW ***************/\nif (typeof module !== 'undefined') {module.exports = config;}\n";
472
473 fs.writeFile(configPath, header + util.inspect(self.configOnHd, {
474 showHidden: false,
475 depth: null,
476 maxArrayLength: null,
477 compact: false
478 }) + footer,
479 (error) => {
480 query.data = "config_update";
481 if (error) {
482 self.sendResponse(res, error, { query: query, backup: backupPath, config: self.configOnHd });
483 }
484 console.info("MMM-Remote-Control saved new config!");
485 console.info("Used backup: " + backupPath);
486 self.sendResponse(res, undefined, { query: query, backup: backupPath, config: self.configOnHd });
487 }
488 );
489 });
490 }
491 },
492
493 answerGet: function(query, res) {
494 var self = this;
495
496 if (query.data === "modulesAvailable") {
497 this.modulesAvailable.sort(function(a, b) { return a.name.localeCompare(b.name); });
498 this.sendResponse(res, undefined, { query: query, data: this.modulesAvailable });
499 return;
500 }
501 if (query.data === "modulesInstalled") {
502 var filterInstalled = function(value) {
503 return value.installed && !value.isDefaultModule;
504 };
505 var installed = self.modulesAvailable.filter(filterInstalled);
506 installed.sort(function(a, b) {
507 return a.name.localeCompare(b.name);
508 });
509 this.sendResponse(res, undefined, { query: query, data: installed });
510 return;
511 }
512 if (query.data === "translations") {
513 this.sendResponse(res, undefined, { query: query, data: this.translation });
514 return;
515 }
516 if (query.data === "mmUpdateAvailable") {
517 var sg = simpleGit(__dirname + "/..");
518 sg.fetch().status((err, data) => {
519 if (!err) {
520 if (data.behind > 0) {
521 this.sendResponse(res, undefined, { query: query, result: true });
522 return;
523 }
524 }
525 this.sendResponse(res, undefined, { query: query, result: false });
526 });
527 return;
528 }
529 if (query.data === "config") {
530 this.sendResponse(res, undefined, { query: query, data: this.getConfig() });
531 return;
532 }
533 if (query.data === "defaultConfig") {
534 if (!(query.module in Module.configDefaults)) {
535 this.sendResponse(res, undefined, { query: query, data: {} });
536 } else {
537 this.sendResponse(res, undefined, { query: query, data: Module.configDefaults[query.module] });
538 }
539 return;
540 }
541 if (query.data === "modules") {
542 if (!this.checkInititialized(res)) { return; }
543 this.callAfterUpdate(() => {
544 this.sendResponse(res, undefined, { query: query, data: self.configData.moduleData });
545 });
546 return;
547 }
548 if (query.data === "brightness") {
549 if (!this.checkInititialized(res)) { return; }
550 this.callAfterUpdate(() => {
551 this.sendResponse(res, undefined, { query: query, result: self.configData.brightness });
552 });
553 return;
554 }
555 if (query.data === "userPresence") {
556 this.sendResponse(res, undefined, { query: query, result: this.userPresence });
557 return;
558 }
559 // Unknown Command, Return Error
560 this.sendResponse(res, "Unknown or Bad Command.", query);
561 },
562
563 callAfterUpdate: function(callback, timeout) {
564 if (timeout === undefined) {
565 timeout = 3000;
566 }
567
568 var waitObject = {
569 finished: false,
570 run: function() {
571 if (this.finished) {
572 return;
573 }
574 this.finished = true;
575 this.callback();
576 },
577 callback: callback
578 };
579
580 this.waiting.push(waitObject);
581 this.sendSocketNotification("UPDATE");
582 setTimeout(function() {
583 waitObject.run();
584 }, timeout);
585 },
586
587 delayedQuery: function(query, res) {
588 if (query.did in this.delayedQueryTimers) {
589 clearTimeout(this.delayedQueryTimers[query.did]);
590 delete this.delayedQueryTimers[query.did];
591 }
592 if (!query.abort) {
593 this.delayedQueryTimers[query.did] = setTimeout(() => {
594 this.executeQuery(query.query);
595 delete this.delayedQueryTimers[query.did];
596 }, (("timeout" in query) ? query.timeout : 10) * 1000);
597 }
598 this.sendResponse(res, undefined, query);
599 },
600
601
602 sendResponse: function(res, error, data) {
603 let response = { success: true };
604 let status = 200;
605 let result = true;
606 if (error) {
607 console.log(error);
608 response = { success: false, status: "error", reason: "unknown", info: error };
609 status = 400;
610 result = false;
611 }
612 if (data) {
613 response = Object.assign({}, response, data);
614 }
615 if (res) {
616 if ("isSocket" in res && res.isSocket) {
617 this.sendSocketNotification("REMOTE_ACTION_RESULT", response);
618 } else {
619 res.status(status).json(response);
620 }
621 }
622 return result;
623 },
624
625 monitorControl: function(action, opts, res) {
626 let status = "unknown";
627 let monitorOnCommand = (this.initialized && "monitorOnCommand" in this.thisConfig.customCommand) ?
628 this.thisConfig.customCommand.monitorOnCommand :
629 "tvservice --preferred && sudo chvt 6 && sudo chvt 7";
630 let monitorOffCommand = (this.initialized && "monitorOffCommand" in this.thisConfig.customCommand) ?
631 this.thisConfig.customCommand.monitorOffCommand :
632 "tvservice -o";
633 let monitorStatusCommand = (this.initialized && "monitorStatusCommand" in this.thisConfig.customCommand) ?
634 this.thisConfig.customCommand.monitorStatusCommand :
635 "tvservice --status";
636 if (["MONITORTOGGLE", "MONITORSTATUS", "MONITORON"].indexOf(action) !== -1) {
637 screenStatus = exec(monitorStatusCommand, opts, (error, stdout, stderr) => {
638 if (stdout.indexOf("TV is off") !== -1 || stdout.indexOf("false") !== -1) {
639 // Screen is OFF, turn it ON
640 status = "off";
641 if (action === "MONITORTOGGLE" || action === "MONITORON") {
642 exec(monitorOnCommand, opts, (error, stdout, stderr) => {
643 this.checkForExecError(error, stdout, stderr, res, { monitor: "on" });
644 });
645 this.sendSocketNotification("USER_PRESENCE", true);
646 return;
647 }
648 } else if (stdout.indexOf("HDMI") !== -1 || stdout.indexOf("true") !== -1) {
649 // Screen is ON, turn it OFF
650 status = "on";
651 if (action === "MONITORTOGGLE") {
652 this.monitorControl("MONITOROFF", opts, res);
653 return;
654 }
655 }
656 this.checkForExecError(error, stdout, stderr, res, { monitor: status });
657 return;
658 });
659 }
660 if (action === "MONITOROFF") {
661 exec(monitorOffCommand, (error, stdout, stderr) => {
662 this.checkForExecError(error, stdout, stderr, res, { monitor: "off" });
663 });
664 this.sendSocketNotification("USER_PRESENCE", false);
665 return;
666 }
667 },
668
669 executeQuery: function(query, res) {
670 var self = this;
671 var opts = { timeout: 15000 };
672
673 if (query.action === "SHUTDOWN") {
674 exec("sudo shutdown -h now", opts, (error, stdout, stderr) => { self.checkForExecError(error, stdout, stderr, res); });
675 return true;
676 }
677 if (query.action === "REBOOT") {
678 exec("sudo shutdown -r now", opts, (error, stdout, stderr) => { self.checkForExecError(error, stdout, stderr, res); });
679 return true;
680 }
681 if (query.action === "RESTART" || query.action === "STOP") {
682 this.controlPm2(res, query);
683 return true;
684 }
685 if (query.action === "USER_PRESENCE") {
686 this.sendSocketNotification("USER_PRESENCE", query.value);
687 this.userPresence = query.value;
688 this.sendResponse(res, undefined, query);
689 return true;
690 }
691 if (["MONITORON", "MONITOROFF", "MONITORTOGGLE", "MONITORSTATUS"].indexOf(query.action) !== -1) {
692 this.monitorControl(query.action, opts, res);
693 return true;
694 }
695 if (query.action === "HIDE" || query.action === "SHOW" || query.action === "TOGGLE") {
696 self.sendSocketNotification(query.action, query);
697 self.sendResponse(res);
698 return true;
699 }
700 if (query.action === "BRIGHTNESS") {
701 self.sendResponse(res);
702 self.sendSocketNotification(query.action, query.value);
703 return true;
704 }
705 if (query.action === "SAVE") {
706 self.sendResponse(res);
707 self.callAfterUpdate(function() { self.saveDefaultSettings(); });
708 return true;
709 }
710 if (query.action === "MODULE_DATA") {
711 self.callAfterUpdate(function() {
712 self.sendResponse(res, undefined, self.configData);
713 });
714 return true;
715 }
716 if (query.action === "INSTALL") {
717 self.installModule(query.url, res, query);
718 return true;
719 }
720 if (query.action === "REFRESH") {
721 self.sendResponse(res);
722 self.sendSocketNotification(query.action);
723 return true;
724 }
725 if (query.action === "HIDE_ALERT") {
726 self.sendResponse(res);
727 self.sendSocketNotification(query.action);
728 return true;
729 }
730 if (query.action === "SHOW_ALERT") {
731 self.sendResponse(res);
732
733 var type = query.type ? query.type : "alert";
734 var title = query.title ? query.title : "Note";
735 var message = query.message ? query.message : "Attention!";
736 var timer = query.timer ? query.timer : 4;
737
738 self.sendSocketNotification(query.action, {
739 type: type,
740 title: title,
741 message: message,
742 timer: timer * 1000
743 });
744 return true;
745 }
746 if (query.action === "UPDATE") {
747 self.updateModule(decodeURI(query.module), res);
748 return true;
749 }
750 if (query.action === 'NOTIFICATION') {
751 try {
752 var payload = {}; // Assume empty JSON-object if no payload is provided
753 if (typeof query.payload === 'undefined') {
754 payload = query.payload;
755 } else if (typeof query.payload === 'object') {
756 payload = query.payload;
757 } else if (typeof query.payload === 'string') {
758 if (query.payload.startsWith("{")) {
759 payload = JSON.parse(query.payload);
760 } else {
761 payload = query.payload;
762 }
763 }
764 this.sendSocketNotification(query.action, { 'notification': query.notification, 'payload': payload });
765 this.sendResponse(res);
766 return true;
767 } catch (err) {
768 console.log("ERROR: ", err);
769 this.sendResponse(res, err, { reason: err.message });
770 return true;
771 }
772 }
773 if (["MINIMIZE", "TOGGLEFULLSCREEN", "DEVTOOLS"].indexOf(query.action) !== -1) {
774 try {
775 let electron = require("electron").BrowserWindow;
776 if (!electron) { throw "Could not get Electron window instance."; }
777 let win = electron.getFocusedWindow();
778 switch (query.action) {
779 case "MINIMIZE":
780 win.minimize();
781 break;
782 case "TOGGLEFULLSCREEN":
783 win.setFullScreen(!win.isFullScreen());
784 break;
785 case "DEVTOOLS":
786 win.webContents.openDevTools();
787 break;
788 default:
789 }
790 this.sendResponse(res);
791 } catch (err) {
792 this.sendResponse(res, err);
793 }
794 return;
795 }
796 if (query.action === "DELAYED") {
797 /* Expects a nested query object
798 * {
799 * action: "DELAYED",
800 * did: "SOME_UNIQUE_ID",
801 * timeout: 10000, // Optional; Default 10000ms
802 * abort: false, // Optional; send to cancel
803 * query: {
804 * action: "SHOW_ALERT",
805 * title: "Delayed Alert!",
806 * message: "This is a delayed alert test."
807 * }
808 * }
809 * Resending with same ID resets delay, unless abort:true
810 */
811 this.delayedQuery(query, res);
812 return;
813 }
814 self.sendResponse(res, new Error(`Invalid Option: ${ query.action }`));
815 return false;
816 },
817
818 installModule: function(url, res, data) {
819 var self = this;
820
821 simpleGit(path.resolve(__dirname + "/..")).clone(url, path.basename(url), function(error, result) {
822 if (error) {
823 console.log(error);
824 self.sendResponse(res, error);
825 } else {
826 var workDir = path.resolve(__dirname + "/../" + path.basename(url));
827 exec("npm install", { cwd: workDir, timeout: 120000 }, (error, stdout, stderr) => {
828 if (error) {
829 console.log(error);
830 self.sendResponse(res, error, Object.assign({ stdout: stdout, stderr: stderr }, data));
831 } else {
832 // success part
833 self.readModuleData();
834 self.sendResponse(res, undefined, Object.assign({ stdout: stdout }, data));
835 }
836 });
837 }
838 });
839 },
840
841 updateModule: function(module, res) {
842 console.log("UPDATE " + (module || "MagicMirror"));
843
844 var self = this;
845
846 var path = __dirname + "/../../";
847 var name = "MM";
848
849 if (typeof module !== 'undefined' && module !== 'undefined') {
850 if (self.modulesAvailable) {
851 var modData = self.modulesAvailable.find(m => m.longname === module);
852 if (modData === undefined) {
853 this.sendResponse(res, new Error("Unknown Module"), { info: modules });
854 return;
855 }
856
857 path = __dirname + "/../" + modData.longname;
858 name = modData.name;
859 }
860 }
861
862 console.log("path: " + path + " name: " + name);
863
864 var git = simpleGit(path);
865 git.pull((error, result) => {
866 if (error) {
867 console.log(error);
868 self.sendResponse(res, error);
869 return;
870 }
871 if (result.summary.changes) {
872 exec("npm install", { cwd: path, timeout: 120000 }, (error, stdout, stderr) => {
873 if (error) {
874 console.log(error);
875 self.sendResponse(res, error, { stdout: stdout, stderr: stderr });
876 } else {
877 // success part
878 self.readModuleData();
879 self.sendResponse(res, undefined, { code: "restart", info: name + " updated." });
880 }
881 });
882 } else {
883 self.sendResponse(res, undefined, { code: "up-to-date", info: name + " already up to date." });
884 }
885 });
886 },
887
888 checkForExecError: function(error, stdout, stderr, res, data) {
889 console.log(stdout);
890 console.log(stderr);
891 this.sendResponse(res, error, data);
892 },
893
894 controlPm2: function(res, query) {
895 var pm2 = require('pm2');
896 let processName = query.processName || this.thisConfig.pm2ProcessName || "mm";
897
898 pm2.connect((err) => {
899 if (err) {
900 this.sendResponse(res, err);
901 return;
902 }
903 console.log(`PM2 process: ${query.action.toLowerCase()} ${processName}`);
904
905 pm2.stop(processName, (err, apps) => {
906 this.sendResponse(res, undefined, { action: action, processName: processName });
907 pm2.disconnect();
908 if (err) { this.sendResponse(res, err); }
909 });
910 });
911 },
912
913 translate: function(data) {
914 Object.keys(this.translation).forEach(t => {
915 let pattern = "%%TRANSLATE:" + t + "%%";
916 let re = new RegExp(pattern, "g");
917 data = data.replace(re, this.translation[t]);
918 });
919 return data;
920 },
921
922 saveDefaultSettings: function() {
923 var moduleData = this.configData.moduleData;
924 var simpleModuleData = [];
925 for (var k = 0; k < moduleData.length; k++) {
926 simpleModuleData.push({});
927 simpleModuleData[k].identifier = moduleData[k].identifier;
928 simpleModuleData[k].hidden = moduleData[k].hidden;
929 simpleModuleData[k].lockStrings = moduleData[k].lockStrings;
930 }
931
932 var text = JSON.stringify({
933 moduleData: simpleModuleData,
934 brightness: this.configData.brightness,
935 settingsVersion: this.configData.settingsVersion
936 });
937
938 fs.writeFile(path.resolve(__dirname + "/settings.json"), text, function(err) {
939 if (err) {
940 throw err;
941 }
942 });
943 },
944
945 in: function(pattern, string) {
946 return string.indexOf(pattern) !== -1;
947 },
948
949 loadDefaultSettings: function() {
950 var self = this;
951
952 fs.readFile(path.resolve(__dirname + "/settings.json"), function(err, data) {
953 if (err) {
954 if (self.in("no such file or directory", err.message)) {
955 return;
956 }
957 console.log(err);
958 } else {
959 data = JSON.parse(data.toString());
960 self.sendSocketNotification("DEFAULT_SETTINGS", data);
961 }
962 });
963 },
964
965 fillTemplates: function(data) {
966 return this.translate(data);
967 },
968
969 loadTranslation: function(language) {
970 var self = this;
971
972 fs.readFile(path.resolve(__dirname + "/translations/" + language + ".json"), function(err, data) {
973 if (err) {
974 return;
975 } else {
976 self.translation = Object.assign({}, self.translation, JSON.parse(data.toString()));
977 }
978 });
979 },
980
981 loadCustomMenus: function() {
982 if ("customMenu" in this.thisConfig) {
983 let menuPath = path.resolve(__dirname + "/../../config/" + this.thisConfig.customMenu);
984 if (!fs.existsSync(menuPath)) {
985 console.log(`MMM-Remote-Control customMenu Requested, but file:${menuPath} was not found`);
986 return;
987 }
988 fs.readFile(menuPath, (err, data) => {
989 if (err) {
990 return;
991 } else {
992 this.customMenu = Object.assign({}, this.customMenu, JSON.parse(this.translate(data.toString())));
993 this.sendSocketNotification("REMOTE_CLIENT_CUSTOM_MENU", this.customMenu);
994 }
995 });
996 }
997 },
998
999 getIpAddresses: function() {
1000 // module started, answer with current IP address
1001 var interfaces = os.networkInterfaces();
1002 var addresses = [];
1003 for (var k in interfaces) {
1004 for (var k2 in interfaces[k]) {
1005 var address = interfaces[k][k2];
1006 if (address.family === "IPv4" && !address.internal) {
1007 addresses.push(address.address);
1008 }
1009 }
1010 }
1011 return addresses;
1012 },
1013
1014 socketNotificationReceived: function(notification, payload) {
1015 var self = this;
1016
1017 if (notification === "CURRENT_STATUS") {
1018 this.configData = payload;
1019 this.thisConfig = payload.remoteConfig;
1020 if (!this.initialized) {
1021 // Do anything else required to initialize
1022 this.initialized = true;
1023 } else {
1024 this.waiting.forEach(o => { o.run(); });
1025 this.waiting = [];
1026 }
1027 }
1028 if (notification === "REQUEST_DEFAULT_SETTINGS") {
1029 // module started, answer with current ip addresses
1030 self.sendSocketNotification("IP_ADDRESSES", self.getIpAddresses());
1031
1032 // check if we have got saved default settings
1033 self.loadDefaultSettings();
1034 }
1035 if (notification === "REMOTE_ACTION") {
1036 if ("action" in payload) {
1037 this.executeQuery(payload, { isSocket: true });
1038 } else if ("data" in payload) {
1039 this.answerGet(payload, { isSocket: true });
1040 }
1041 }
1042 if (notification === "NEW_CONFIG") {
1043 this.answerPost({ data: "config" }, { body: payload }, { isSocket: true });
1044 }
1045 if (notification === "REMOTE_CLIENT_CONNECTED") {
1046 this.sendSocketNotification("REMOTE_CLIENT_CONNECTED");
1047 this.loadCustomMenus();
1048 if ("id" in this.moduleApiMenu) {
1049 this.sendSocketNotification("REMOTE_CLIENT_MODULEAPI_MENU", this.moduleApiMenu);
1050 }
1051 }
1052 if (notification === "REMOTE_NOTIFICATION_ECHO_IN") {
1053 this.sendSocketNotification("REMOTE_NOTIFICATION_ECHO_OUT", payload);
1054 }
1055 if (notification === "USER_PRESENCE") {
1056 this.userPresence = payload;
1057 }
1058 /* API EXTENSION -- added v2.0.0 */
1059 if (notification === "REGISTER_API") {
1060 if ("module" in payload) {
1061 if ("actions" in payload && payload.actions !== {}) {
1062 this.externalApiRoutes[payload.path] = payload;
1063 } else {
1064 // Blank actions means the module has requested to be removed from API
1065 delete this.externalApiRoutes[payload.path];
1066 }
1067 this.updateModuleApiMenu();
1068 }
1069 }
1070 }
1071 },
1072 require('./API/api.js')));