UNPKG

smart-nodes

Version:
564 lines (460 loc) 19.9 kB
module.exports = function (RED) { "use strict"; function DimmerControlNode(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_on_value: 0.0, last_value: 0.0, last_value_before_alarm: 0.0, last_value_sended: 0.0, alarm_active: false, }, smart_context.get(node.id)); // ################## // # Dynamic config # // ################## let time_total = helper.getTimeInMs(config.time_total, config.time_total_unit); let long_press_ms = parseInt(config.long_press_ms || 1000, 10); let update_interval = parseInt(config.update_interval || 500, 10); let alarm_on_action = config.alarm_on_action || "NOTHING"; let alarm_on_percentage = parseFloat(config.alarm_on_percentage) || 0.0; let alarm_off_action = config.alarm_off_action || "NOTHING"; let alarm_off_percentage = parseFloat(config.alarm_off_percentage) || 0.0; // ################## // # Runtime values # // ################## // Here the setTimeout return value is stored to turn off the dimmer. // That means if it is null, the dimmer 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 dimmer should go off. // This is used to calculate the node status. let date_turn_off = null; // Here the setTimeout return value is stored when the toggle_dim button was pressed first. let timeout_button_pressed = null; // Here the date is stored, when the dimming was started let dim_start_date = null; // Here the value is stored, when the dimming was started let dim_start_value = null; // Here the dimming direction is stored, true = higher, false = lower let dim_direction = false; // Here the setInterval return interval ist stored, which is used for the dimming process let interval_dim = null; // Here the new dim value is saved for the status let new_dim_value = 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 payload is a float if (typeof msg.payload == "boolean") msg.payload = msg.payload ? 100.0 : 0.0; else msg.payload = parseFloat(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; node_settings.last_value = 0.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; node_settings.last_value = 100.0; node_settings.last_value_sended = node_settings.last_value; break; case "set": // Make sure payload is a float if (typeof msg.payload == "boolean") msg.payload = msg.payload ? 100.0 : 0.0; else msg.payload = parseFloat(msg.payload); node_settings.last_value = msg.payload; node_settings.last_value_sended = node_settings.last_value; break; case "set_permanent": // Make sure payload is a float if (typeof msg.payload == "boolean") msg.payload = msg.payload ? 100.0 : 0.0; else msg.payload = parseFloat(msg.payload); isPermanent = msg.payload > 0; 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; if (msg.payload == false) { // It already was off, so don't turn on if (node_settings.last_value == 0.0) return; // If time is set to 0, then turn off immediately if (helper.getTimeInMsFromString(msg.time_on ?? max_time_on) == 0) node_settings.last_value = 0.0; } else { node_settings.last_value = node_settings.last_on_value; } node_settings.last_value_sended = node_settings.last_value; break; case "alarm": // 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": node_settings.last_value = 100.0; break; case "OFF": node_settings.last_value = 0.0; break; case "PERCENTAGE": node_settings.last_value = alarm_off_percentage; 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; node.send({ payload: 0.0 }); 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 "dim": if (msg.payload) startDim(msg.direction); else stopDim(helper.getTimeInMsFromString(msg.time_on ?? max_time_on)); break; case "toggle_dim": if (msg.payload) { if (timeout_button_pressed != null) clearTimeout(timeout_button_pressed); timeout_button_pressed = setTimeout(() => { // Long press detected, start dimming timeout_button_pressed = null; startDim(msg.direction); }, long_press_ms); } else { if (timeout_button_pressed == null) { // Long press finished, stop dimming stopDim(helper.getTimeInMsFromString(msg.time_on ?? max_time_on)); } else { clearTimeout(timeout_button_pressed); timeout_button_pressed = null; // Short press, do a toggle. if (node_settings.last_value == 0.0) node_settings.last_value = node_settings.last_on_value; else node_settings.last_value = 0.0; node_settings.last_value_sended = node_settings.last_value; } } break; case "toggle": default: // If button is released, don't handle this message if (msg.payload === false) return; if (node_settings.last_value == 0.0) node_settings.last_value = node_settings.last_on_value; else node_settings.last_value = 0.0; 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_on_action) { case "ON": node_settings.last_value = 100.0; break; case "OFF": node_settings.last_value = 0.0; break; case "PERCENTAGE": node_settings.last_value = alarm_on_percentage; break; case "NOTHING": default: break; } } if (node_settings.alarm_active || helper.getTopicName(msg.topic) != "status") { node.send({ payload: node_settings.last_value }); if (node_settings.last_value > 0.0) node_settings.last_on_value = 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 > 0); } /** * Starts the dimming in the given direction or toggle direction if parameter is null * @param {bool|null} direction */ let startDim = (direction = null) => { if (direction != null || direction !== true || direction !== false || direction != "up" || direction != "down") { node.warn("Invalid direction received. direction = " + direction); return; } stopDim(); stopAutoOff(); // Auto toggle direction if (direction == null) direction = !dim_direction; // Save initial values dim_direction = direction === true || direction == "up"; dim_start_date = new Date(); dim_start_value = node_settings.last_value; interval_dim = setInterval(() => { let time_diff_ms = (new Date()).getTime() - dim_start_date.getTime(); let delta = time_diff_ms / time_total; new_dim_value = dim_start_value if (dim_direction) new_dim_value += delta; else new_dim_value -= delta; node.send({ payload: new_dim_value }); setStatus(); }, update_interval); } /** * This stops a running dim */ let stopDim = origTimeMs => { if (interval_dim != null) { clearTimeout(interval_dim); interval_dim = null; // Calculate new persistant values let time_diff_ms = (new Date()).getTime() - dim_start_date.getTime(); let delta = time_diff_ms / time_total; let new_dim_value = dim_start_value if (dim_direction) new_dim_value += delta; else new_dim_value -= delta; node.send({ payload: new_dim_value }); startAutoOffIfNeeded(origTimeMs); } } /** * This function sets a timeout to turn off the dimmer 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 date_turn_off = new Date(); date_turn_off.setMilliseconds(date_turn_off.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; node_settings.last_value = 0.0; node.send({ payload: 0.0 }); 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 (interval_dim != null) { node.status({ fill: "yellow", shape: "ring", text: helper.getCurrentTimeForStatus() + ": Dim " + helper.toFixed(new_dim_value, 0) + "%" }); } 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(date_turn_off, "until") + " for auto off" }); } else { node.status({ fill: "red", shape: "dot", text: helper.getCurrentTimeForStatus() + ": Off" }); } } /** * Notify all connected central nodes * @param {boolean} state The state if the dimmer 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_dimmer-control", DimmerControlNode); };