UNPKG

smart-nodes

Version:
474 lines (386 loc) 16.7 kB
module.exports = function (RED) { "use strict"; function LightControlNode(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_value: false, last_value_before_alarm: false, last_value_sended: false, alarm_active: false, }, smart_context.get(node.id)); // ################## // # Dynamic config # // ################## let max_time_on = helper.getTimeInMs(config.max_time_on, config.max_time_on_unit); let alarm_action = config.alarm_action || "NOTHING"; let alarm_off_action = config.alarm_off_action || "NOTHING"; let mode = config.mode || "BOOL"; // ################## // # 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; // If this is on, a motion sensor has detected a move, so the on time value is ignored. // The timeout starts only if the motion sensor goes off. let isMotion = false; // If this is on, ignore all state changes and don't restart any time measurement. let isBlinking = 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:" + node.id; var handler = function (msg) { node.receive(msg); } RED.events.on(event, handler); // ############### // # Node events # // ############### node.on("input", function (msg) { handleTopic(msg); 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 doRestartTimer = true; let real_topic = helper.getTopicName(msg.topic); switch (real_topic) { case "status": // Ignore if it is in blinking mode if (isBlinking) return; // Make sure it is bool or int if (mode == "BOOL") msg.payload = !!msg.payload; else if (mode == "PERCENTAGE") msg.payload = boolToInt(msg.payload); // Output is already in the state of the status value and the timeout is running. // No need to restart the timeout. if (node_settings.last_value == msg.payload && timeout != null) doRestartTimer = false; node_settings.last_value = msg.payload; break; case "off": // If button is released, don't handle this message if (msg.payload === false) return; isBlinking = false; if (mode == "BOOL") node_settings.last_value = false; else if (mode == "PERCENTAGE") node_settings.last_value = 0; node_settings.last_value_sended = node_settings.last_value; break; case "on": // If button is released, don't handle this message if (msg.payload === false) return; isBlinking = false; if (mode == "BOOL") node_settings.last_value = true; else if (mode == "PERCENTAGE") node_settings.last_value = 100; node_settings.last_value_sended = node_settings.last_value; break; case "set": // Make sure it is bool or int if (mode == "BOOL") msg.payload = !!msg.payload; else if (mode == "PERCENTAGE") msg.payload = boolToInt(msg.payload); isBlinking = false; node_settings.last_value = msg.payload; node_settings.last_value_sended = node_settings.last_value; break; case "set_permanent": // Make sure it is bool or int if (mode == "BOOL") msg.payload = !!msg.payload; else if (mode == "PERCENTAGE") msg.payload = boolToInt(msg.payload); isBlinking = false; isPermanent = !!msg.payload; // false or 0 => not permanent node_settings.last_value = msg.payload; node_settings.last_value_sended = node_settings.last_value; break; case "motion": // Make sure it is bool msg.payload = !!msg.payload; isMotion = msg.payload; isBlinking = false; if (msg.payload == false) { // It already was off, so don't turn on if (mode == "BOOL" && node_settings.last_value == false) return; if (mode == "PERCENTAGE" && node_settings.last_value == 0) return; // If time is set to 0, then turn off immediately if (helper.getTimeInMsFromString(msg.time_on ?? max_time_on) == 0) { if (mode == "BOOL") node_settings.last_value = false; else if (mode == "PERCENTAGE") node_settings.last_value = 0; } } else { if (mode == "BOOL") node_settings.last_value = true; else if (mode == "PERCENTAGE") node_settings.last_value = 100; } node_settings.last_value_sended = node_settings.last_value; break; case "alarm": isBlinking = false; // Make sure it is bool msg.payload = !!msg.payload; // No alarm change, do nothing if (node_settings.alarm_active == msg.payload) return; node_settings.alarm_active = msg.payload; if (node_settings.alarm_active) { // Alarm turned on node_settings.last_value_before_alarm = node_settings.last_value; } else { // Alarm turned off switch (alarm_off_action) { case "NOTHING": break; case "ON": if (mode == "BOOL") node_settings.last_value = true; else if (mode == "PERCENTAGE") node_settings.last_value = 100; break; case "OFF": if (mode == "BOOL") node_settings.last_value = false; else if (mode == "PERCENTAGE") node_settings.last_value = 0; break; case "LAST": node_settings.last_value = node_settings.last_value_before_alarm; break; case "LAST_SENDED": node_settings.last_value = node_settings.last_value_sended; break; } } break; case "blink": if (!node_settings.alarm_active) { isBlinking = true; if (mode == "BOOL") node.send({ payload: !node_settings.last_value }); else if (mode == "PERCENTAGE") node.send({ payload: node_settings.last_value == 100 ? 0 : 100 }); setStatus(); setTimeout( () => { isBlinking = false; if (!node_settings.alarm_active) { node.send({ payload: node_settings.last_value }); setStatus(); } }, helper.getTimeInMsFromString(msg.time_on, 500) ); } return; case "toggle": default: // If button is released, don't handle this message if (msg.payload === false) return; if (mode == "BOOL") node_settings.last_value = !node_settings.last_value; else if (mode == "PERCENTAGE") node_settings.last_value = node_settings.last_value == 100 ? 0 : 100; node_settings.last_value_sended = node_settings.last_value; break; } if (doRestartTimer) stopAutoOff(); // Check alarm values if (node_settings.alarm_active) { isPermanent = false; switch (alarm_action) { case "ON": if (mode == "BOOL") node_settings.last_value = true; else if (mode == "PERCENTAGE") node_settings.last_value = 100; break; case "OFF": if (mode == "BOOL") node_settings.last_value = false; else if (mode == "PERCENTAGE") node_settings.last_value = 0; break; case "NOTHING": default: break; } } if (node_settings.alarm_active || helper.getTopicName(msg.topic) != "status") node.send({ payload: node_settings.last_value }); // Output is on, now if (node_settings.last_value && doRestartTimer) startAutoOffIfNeeded(helper.getTimeInMsFromString(msg.time_on ?? max_time_on)); notifyCentral(!!node_settings.last_value); } /** * This function sets a timeout to turn off the light after the defined time is over. * @param {*} origTimeMs The on time */ let startAutoOffIfNeeded = origTimeMs => { // No timer when alarm is active if (node_settings.alarm_active) return; let timeMs = parseInt(origTimeMs); if (isNaN(timeMs)) { helper.warn(this, "Invalid time_on value send: " + origTimeMs); timeMs = max_time_on; } // calculate end date for status message timeout_end_date = new Date(); timeout_end_date.setMilliseconds(timeout_end_date.getMilliseconds() + timeMs); // Stop if any timeout is set stopAutoOff(); // 0 = Always on if (timeMs <= 0 || isPermanent || isMotion || !node_settings.last_value) return; timeout = setTimeout(() => { timeout = null; if (mode == "BOOL") node_settings.last_value = false; else if (mode == "PERCENTAGE") node_settings.last_value = 0; node.send({ payload: node_settings.last_value }); notifyCentral(false); setStatus(); smart_context.set(node.id, node_settings); }, timeMs); }; /** * Stops the current running timeout */ let stopAutoOff = () => { if (timeout != null) { clearTimeout(timeout); timeout = null; } }; /** * Set the current node status */ let setStatus = () => { if (node_settings.alarm_active) { node.status({ fill: "red", shape: "dot", text: helper.getCurrentTimeForStatus() + ": ALARM is active" }); } else if (isBlinking) { node.status({ fill: "yellow", shape: "dot", text: helper.getCurrentTimeForStatus() + ": Blink" }); } else if (node_settings.last_value) { if (isPermanent || isMotion || timeout == null) node.status({ fill: "green", shape: "dot", text: helper.getCurrentTimeForStatus() + ": On" }); else if (timeout) node.status({ fill: "yellow", shape: "ring", text: helper.getCurrentTimeForStatus() + ": Wait " + helper.formatDateToStatus(timeout_end_date, "until") + " for auto off" }); } else { node.status({ fill: "red", shape: "dot", text: helper.getCurrentTimeForStatus() + ": Off" }); } } let boolToInt = boolValue => { if (boolValue === true) return 100; if (boolValue === false) return 0; return parseInt(boolValue, 10); } /** * Notify all connected central nodes * @param {boolean} state The state if the light is on */ let notifyCentral = state => { if (!config.links) return; config.links.forEach(link => { helper.log(node.id + " -> " + link); helper.log({ source: node.id, state: state }); RED.events.emit("node:" + link, { source: node.id, state: state }); }); } // After node red restart, start also the timeout if (node_settings.last_value) startAutoOffIfNeeded(helper.getTimeInMsFromString(max_time_on)); setStatus(); } RED.nodes.registerType("smart_light-control", LightControlNode); };