smart-nodes
Version:
Controls light, shutters and more. Includes common used logic and statistic nodes to control your home.
376 lines (319 loc) • 13 kB
JavaScript
module.exports = function (RED)
{
"use strict";
function TextExecNode(config)
{
const node = this;
RED.nodes.createNode(node, config);
// ###################
// # Class constants #
// ###################
// #######################
// # Global help objects #
// #######################
const smart_context = require("../persistence.js")(RED);
const helper = require("../smart_helper.js");
// #####################
// # persistent values #
// #####################
// ##################
// # Dynamic config #
// ##################
// ##################
// # Runtime values #
// ##################
// Here the ouput message for logging purposes is saved.
// It will be cleared with every new node input
let log = null;
// Holds a list with all room names, sorted from the longest to the shortest
// Format: Index => RoomName
let rooms = [];
// Holds references to the nodes to access them by room name
// Format: RoomName => [Node1, Node2, ..., NodeX]
let lookup = new Map();
node.status({});
// ###############
// # Node events #
// ###############
node.on("input", function (msg)
{
let mode = "light"; // light || shutter
let action = null; // on || off || up || stop || down
let number = null;
let without = false;
let message = null;
if (typeof msg.payload == "string")
{
message = msg.payload;
}
else if (typeof msg.payload?.event?.command == "string")
{
message = msg.payload?.event?.command;
}
// Check if message was found
if (typeof message != "string")
{
node.error("Couldn't detect sended message");
return;
}
node.status({ fill: "green", shape: "dot", text: helper.getCurrentTimeForStatus() + ": " + message });
log = {
rooms: rooms,
lookup: lookup,
actions: []
};
log.initMessage = message;
message = prepareMessage(message);
log.preparedMessage = message;
let affectedNodes = [];
for (const word of message.trim().split(/[\s,.]+/))
{
switch (word)
{
case "licht":
case "steckdose":
case "light":
case "power":
case "outlet":
// node.log("Set mode to light");
mode = "light";
break;
case "rollladen":
case "rolladen":
case "beschattung":
case "fenster":
case "cover":
case "shutter":
case "shading":
case "window":
case "windows":
// node.log("Set mode to shutter");
mode = "shutter";
break;
case "an":
case "on":
case "ein":
case "einschalten":
// node.log("Set action to on");
mode = "light";
action = "on";
if (performAction(mode, action, number, affectedNodes))
{
action = null;
affectedNodes = [];
number = null;
without = false;
}
break;
case "aus":
case "off":
case "ausschalten":
// node.log("Set action to off");
mode = "light";
action = "off";
if (performAction(mode, action, number, affectedNodes))
{
action = null;
affectedNodes = [];
number = null;
without = false;
}
break;
case "hoch":
case "oben":
case "up":
case "auf":
case "öffne":
case "open":
// node.log("Set action to up");
mode = "shutter";
action = "up";
break;
case "runter":
case "unten":
case "down":
case "ab":
case "schließe":
case "close":
// node.log("Set action to down");
mode = "shutter";
action = "down";
break;
case "prozent":
case "%":
case "percent":
case "percentage":
// node.log("Set action to position");
mode = "shutter";
action = "position";
break;
case "stoppe":
case "stopp":
case "stop":
case "anhalten":
// node.log("Set action to stop");
mode = "shutter";
action = "stop";
break;
case "außer":
case "ohne":
case "without":
case "but":
case "except":
without = true;
break;
case "und":
case "and":
if (performAction(mode, action, number, affectedNodes))
{
action = null;
affectedNodes = [];
number = null;
without = false;
}
break;
default:
if (word[0] == "$")
{
// node.log("Found word " + word);
let room = rooms[parseInt(word.substring(1), 10)];
// node.log("Found room " + room);
if (lookup.has(room))
{
for (const node of lookup.get(room))
{
if (without && affectedNodes.includes(node))
affectedNodes.splice(affectedNodes.indexOf(node), 1);
else if (!without && !affectedNodes.includes(node))
affectedNodes.push(node);
}
}
}
else if (isFinite(word))
{
number = parseInt(word, 10);
// node.log("Found number " + number);
}
else if (word[word.length - 1] == "%" && isFinite(word.substr(0, word.length - 1)))
{
number = parseInt(word.substr(0, word.length - 1), 10);
mode = "shutter";
action = "position";
// node.log("Found number " + number + " with %");
}
else
{
// node.log("Ignore word " + word);
}
break;
}
}
performAction(mode, action, number, affectedNodes);
// node.log("Finished");
// node.log(log);
// Show what happened in debug bar
node.send(log);
});
node.on("close", function ()
{
});
/**
* This function prepares a list of the rooms as well as the lookup tables for lights and shutters
*/
let prepareRoomList = () =>
{
for (const link of config.links)
{
// node.log("Check node = " + link);
let linkedNode = RED.nodes.getNode(link);
if (linkedNode == null || linkedNode.exec_text_names == null || linkedNode.exec_text_names.length == 0)
continue;
for (const name of linkedNode.exec_text_names)
{
// console.log(linkedNode);
switch (linkedNode.type)
{
case "smart_light-control":
case "smart_scene-control":
case "smart_shutter-control":
case "smart_shutter-complex-control":
// node.log("Add room: " + name);
if (!lookup.has(name))
lookup.set(name, []);
if (!lookup.get(name).includes(linkedNode))
lookup.get(name).push(linkedNode);
if (!rooms.includes(name))
{
// node.log("Add room to room list: " + name);
rooms.push(name);
}
break;
default:
break;
}
}
}
// Sort by length, longest names first
rooms.sort((a, b) => b.length - a.length);
// node.log(rooms);
// console.log(lookup);
}
/**
* This function replaces all rooms with a placeholder for the position within the rooms array
* The reason for that is, that the names could contain spaces which won't work when splitting by a space.
*
* e.g.: let rooms = ["Room 1", "Room 2"];
* let message = "Turn on light in Room 1 and off in Room 2";
*
* prepareMessage(message) will return "Turn on light in $0 and off in $1";
*/
let prepareMessage = message =>
{
message = message.toLowerCase();
for (let index = 0; index < rooms.length; index++)
{
const room = rooms[index];
message = message.replaceAll(room, "$" + index);
}
return message;
}
let performAction = (mode, action, number, affectedNodes) =>
{
if (action != null && affectedNodes.length > 0)
{
for (const targetNode of affectedNodes)
{
if (mode == "light" && !["smart_light-control", "smart_scene-control"].includes(targetNode.type))
continue;
if (mode == "shutter" && !["smart_shutter-control", "smart_shutter-complex-control"].includes(targetNode.type))
continue;
// node.log("Notify node " + node.id);
if (action == "position")
{
// console.log({ "topic": action, "payload": number });
if (number != null)
{
// console.log(node.id + " -> " + targetNode.id);
// console.log({ "topic": action, "payload": number });
RED.events.emit("node:" + targetNode.id, { "topic": action, "payload": number });
}
}
else
{
// console.log(node.id + " -> " + targetNode.id);
// console.log({ "topic": action });
RED.events.emit("node:" + targetNode.id, { "topic": action });
}
}
log.actions.push({ mode, action, nodes: affectedNodes.map(n => n.name || n.id) });
return true;
}
return false;
}
setTimeout(() =>
{
prepareRoomList();
}, 1000);
}
RED.nodes.registerType("smart_text-exec", TextExecNode);
};