smart-nodes
Version:
Controls light, shutters and more. Includes common used logic and statistic nodes to control your home.
340 lines (270 loc) • 11.4 kB
JavaScript
module.exports = function (RED)
{
"use strict";
function SceneControlNode(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");
// ############################
// # Used from text-exec node #
// ############################
if (typeof config.exec_text_names == "string")
node.exec_text_names = config.exec_text_names.split(",").map(n => n.trim().toLowerCase());
else
node.exec_text_names = [];
// #####################
// # persistent values #
// #####################
var node_settings = helper.cloneObject({
last_values: [], // light is on or off for a scene
}, smart_context.get(node.id));
// ##################
// # Dynamic config #
// ##################
let max_time_on = helper.getTimeInMs(config.max_time_on, config.max_time_on_unit);
// ##################
// # Runtime values #
// ##################
// Here the setTimeout return value is stored to turn off the light.
// That means if it is null, the light will not be turned off automatically.
let timeout = null;
// If isPermanent is true, then a default on time value is ignored
// Also if the motion sensor turns off, no timeout is started.
let isPermanent = false;
// Here the date is stored, when the light should go off.
// This is used to calculate the node status.
let timeout_end_date = null;
// #########################
// # Central node handling #
// #########################
var event = "node:" + config.id;
var handler = function (msg)
{
node.receive(msg);
}
RED.events.on(event, handler);
// Per default expect that all outputs are off
if (node_settings.last_values.length != config.scenes.length)
node_settings.last_values = new Array(config.outputs).fill(false);
// ###############
// # Node events #
// ###############
node.on("input", function (msg)
{
handleTopic(msg);
// At least one light is on, now
if (getCurrentScene() != 0)
startAutoOffIfNeeded(helper.getTimeInMsFromString(msg.time_on ?? max_time_on));
setStatus();
smart_context.set(node.id, node_settings);
});
node.on("close", function ()
{
stopAutoOff();
RED.events.off(event, handler);
});
// #####################
// # Private functions #
// #####################
// This is the main function which handles all topics that was received.
let handleTopic = msg =>
{
let currentScene = getCurrentScene();
let [real_topic, scenes] = helper.getTopicName(msg.topic).split("_");
let number = helper.getTopicNumber(msg.topic) - 1; // number should be used 0-based
switch (real_topic)
{
case "debug":
helper.nodeDebug(node, {
node_settings,
max_time_on,
isPermanent,
currentScene,
});
break;
case "status":
// Make sure it is bool
msg.payload = !!msg.payload;
node_settings.last_values[number] = msg.payload;
notifyCentral();
// All off? Stop permanent
if (getCurrentScene() == 0)
isPermanent = false;
// never forward status message to next node
return;
case "off":
node_settings.last_values = new Array(config.outputs).fill(false);
break;
case "on":
node_settings.last_values = new Array(config.outputs).fill(true);
break;
case "set":
// Make sure it is bool
msg.payload = !!msg.payload;
node_settings.last_values = new Array(config.outputs).fill(msg.payload);
// This happens because of splitting by _ for scenes
if (scenes == "permanent")
isPermanent = msg.payload;
break;
case "scene":
// Skip if button is released;
if (msg.payload === false)
return;
if (typeof scenes === "undefined")
{
node.error("called topic=scene without scene(s) set. Try topic=scene_0,1");
return;
}
scenes = scenes.split(",").map(s => parseInt(s, 10));
if (scenes.length == 0)
{
node.error("called topic=scene without scene(s) set. Try topic=scene_0,1");
return;
}
let nextSceneIndex = scenes.indexOf(currentScene);
if (nextSceneIndex === -1 || nextSceneIndex == scenes.length - 1)
nextSceneIndex = scenes[0];
else
nextSceneIndex = scenes[nextSceneIndex + 1];
// To be able to toggle if only one scene is set
if (currentScene == nextSceneIndex)
nextSceneIndex = 0;
if (nextSceneIndex == 0)
{
node_settings.last_values = new Array(config.outputs).fill(false);
}
else
{
const scene = config.scenes[nextSceneIndex - 1]; // scene numbers are 1 based
const expectedOn = scene.outputs.split(",");
for (let o = 0; o < node_settings.last_values.length; o++)
{
const output = node_settings.last_values[o];
// Check if output has to be changed
if ((output && !expectedOn.includes("" + (o + 1))) || (!output && expectedOn.includes("" + (o + 1))))
{
node_settings.last_values[o] = !output;
}
}
}
break;
case "toggle":
// Skip if button is released;
if (msg.payload === false)
return;
node_settings.last_values = new Array(config.outputs).fill(currentScene == 0);
break;
}
stopAutoOff();
node.send(node_settings.last_values.map(val => { return { payload: val }; }));
notifyCentral();
}
let getCurrentScene = () =>
{
// All off ist scene 0
if (!node_settings.last_values.includes(true))
return 0;
for (let s = 0; s < config.scenes.length; s++)
{
const scene = config.scenes[s];
const expectedOn = scene.outputs.split(",");
let skipScene = false;
for (let o = 0; o < node_settings.last_values.length; o++)
{
const output = node_settings.last_values[o];
// Check if one condition fails
if ((output && !expectedOn.includes("" + (o + 1))) || (!output && expectedOn.includes("" + (o + 1))))
{
skipScene = true;
break;
}
}
if (skipScene)
continue;
return s + 1; // Scene number is 1 based
}
// Not a scene
return null;
}
let startAutoOffIfNeeded = origTimeMs =>
{
let timeMs = parseInt(origTimeMs, 10);
if (isNaN(timeMs))
{
node.error("Invalid time_on value send: " + origTimeMs);
timeMs = max_time_on;
}
// calculate end date for status message
if (timeMs > 0)
{
timeout_end_date = new Date();
timeout_end_date.setMilliseconds(timeout_end_date.getMilliseconds() + timeMs);
}
else
{
timeout_end_date = null;
}
// Stop if any timeout is set
stopAutoOff();
// 0 = Always on or already off
if (timeMs <= 0 || isPermanent || getCurrentScene() == 0)
return;
timeout = setTimeout(() =>
{
timeout = null;
node_settings.last_values = new Array(config.outputs).fill(false);
node.send(node_settings.last_values.map(val => { return { payload: val }; }));
notifyCentral();
setStatus();
smart_context.set(node.id, node_settings);
}, timeMs);
}
let stopAutoOff = () =>
{
if (timeout != null)
{
clearTimeout(timeout);
timeout = null;
}
}
let setStatus = () =>
{
let scene = getCurrentScene();
if (scene != 0)
{
if (isPermanent || timeout_end_date == null)
node.status({ fill: "green", shape: "dot", text: helper.getCurrentTimeForStatus() + ": Scene " + scene + " active" });
else if (timeout)
node.status({ fill: "yellow", shape: "ring", text: helper.getCurrentTimeForStatus() + ": Scene " + scene + " active, wait " + helper.formatDateToStatus(timeout_end_date, "until") + " for auto off" });
}
else
{
node.status({ fill: "red", shape: "dot", text: helper.getCurrentTimeForStatus() + ": Off" });
}
}
let notifyCentral = () =>
{
if (!config.links)
return;
let state = getCurrentScene() !== 0;
config.links.forEach(link =>
{
helper.log(node, link, { source: node.id, state: state });
RED.events.emit("node:" + link, { source: node.id, state: state });
});
}
// After node red restart, start also the timeout
if (getCurrentScene() != 0)
startAutoOffIfNeeded(helper.getTimeInMsFromString(max_time_on));
setStatus();
}
RED.nodes.registerType("smart_scene-control", SceneControlNode);
};