UNPKG

smart-nodes

Version:
425 lines (340 loc) 14.3 kB
module.exports = function (RED) { "use strict"; function MixingValveNode(config) { const node = this; RED.nodes.createNode(node, config); // ################### // # Class constants # // ################### const ADJUST_OPEN = 0; const ADJUST_CLOSE = 1; // ####################### // # Global help objects # // ####################### const smart_context = require("../persistence.js")(RED); const helper = require("../smart_helper.js"); // ##################### // # persistent values # // ##################### var node_settings = helper.cloneObject({ enabled: config.enabled, setpoint: config.setpoint, off_mode: config.off_mode, valve_mode: config.valve_mode, precision: config.precision || 1, max_change_percent: config.max_change_percent || 2, max_change_temp_difference: config.max_change_temp_difference || 20, last_position: null, min_change_time: config.min_change_time || 0 }, smart_context.get(node.id)); // ################## // # Dynamic config # // ################## let time_total_s = config.time_total; let time_sampling_s = config.time_sampling; // ################## // # Runtime values # // ################## // Here the setInterval return value is stored which is used to recheck the valve position let sampling_interval = null; // Here the setTimeout return value is stored to stop the calibration time. let calibration_timeout = null; // Here the setTimeout return value is stored to stop the valve adjustment. let changing_timeout = null; // Store the direction of the adjustment let adjusting = null; // Stores the current temperature which is used to determine the adjusting direction let current_temperature = null; // The start time of the adjustment to count the change let adjusting_start_time = null; // ############### // # Node events # // ############### node.on("input", function (msg) { handleTopic(msg); setStatus(); smart_context.set(node.id, node_settings); }); node.on("close", function () { stopSampling(); if (sampling_interval !== null) { clearInterval(sampling_interval); sampling_interval = null; } if (calibration_timeout !== null) { clearTimeout(calibration_timeout); calibration_timeout = null; } if (changing_timeout !== null) { clearTimeout(changing_timeout); changing_timeout = null; } stopChanging(); }); // ##################### // # Private functions # // ##################### // This is the main function which handles all topics that was received. let handleTopic = msg => { let real_topic = helper.getTopicName(msg.topic); if (real_topic == "set_state_inverted") { real_topic = "set_state"; msg.payload = !msg.payload; } if (real_topic == "set_state") real_topic = (!!msg.payload) ? "enable" : "disable"; switch (real_topic) { case "enable": if (node_settings.enabled) break; node_settings.enabled = true; stopChanging(); startSampling(); break; case "disable": if (!node_settings.enabled) break; node_settings.enabled = false; stopSampling(); doOffMode(); break; case "setpoint": let new_setpoint = parseFloat(msg.payload); if (isNaN(new_setpoint) && !isFinite(new_setpoint)) { // helper.warn(this, "Invalid payload: " + msg.payload); return; } node_settings.setpoint = new_setpoint; break; case "off_mode": switch (msg.payload) { case "NOTHING": case "OPEN": case "CLOSE": node_settings.off_mode = msg.payload; if (!node_settings.enabled) doOffMode(); break; default: helper.warn(this, "Invalid off_mode: " + msg.payload); return; } break; case "valve_mode": switch (msg.payload) { case "HEATING": case "COOLING": node_settings.valve_mode = msg.payload; setStatus(); break; default: helper.warn(this, "Invalid valve_mode: " + msg.payload); return; } startSampling(); break; case "current_temperature": let new_temp = parseFloat(msg.payload); if (isNaN(new_temp) && !isFinite(new_temp)) { // helper.warn(this, "Invalid payload for current_temperature: " + msg.payload); return; } current_temperature = new_temp; break; case "calibrate": calibrate(); break; default: helper.warn(this, "Invalid topic: " + real_topic); return; } } let startSampling = () => { // Wait for calibration first if (node_settings.last_position === null || calibration_timeout !== null || !node_settings.enabled) return; // sampling is already running, so do nothing if (sampling_interval !== null) return; // start sampling sampling_interval = setInterval(sample, time_sampling_s * 1000); setStatus(); } let stopSampling = () => { // Wait for calibration first if (node_settings.last_position === null || calibration_timeout !== null) return; // sampling is not running, so do nothing if (sampling_interval === null) return; // stop sampling clearInterval(sampling_interval); sampling_interval = null; setStatus(); } let sample = () => { // No current temperature available or in calibration => no action if (current_temperature === null || calibration_timeout !== null || !node_settings.enabled) { helper.log("No sample possible", { current_temperature, calibration_timeout, enabled: node_settings.enabled }); return; } // +/- 1°C => already good enough, do nothing let temp_diff = Math.abs(current_temperature - node_settings.setpoint); if (temp_diff < node_settings.precision) return; // Calculate change time // Change time in ms for 1% let moving_time = time_total_s * 1000 / 100; // 0 °C diff => 0% change // for max_change_temp_difference (default: 20 °C) diff => max_change_percent (default: 2%) change moving_time *= helper.scale( Math.min(temp_diff, node_settings.max_change_temp_difference), 0, node_settings.max_change_temp_difference, 0, node_settings.max_change_percent ); if (moving_time < node_settings.min_change_time) moving_time = node_settings.min_change_time; // calculate direction let adjustAction = ADJUST_CLOSE; if (current_temperature < node_settings.setpoint) { if (node_settings.valve_mode == "HEATING") adjustAction = ADJUST_OPEN; } else { if (node_settings.valve_mode == "COOLING") adjustAction = ADJUST_OPEN; } // start moving startChanging(adjustAction, moving_time); } let startChanging = (adjustAction, time_ms) => { helper.log("Start changing", adjustAction, time_ms) stopChanging(); // Already oppened/closed if (adjustAction == ADJUST_OPEN && node_settings.last_position == 100) time_ms = time_total_s * 1000 / 200; // Change at least 1/200 => 0.5 % else if (adjustAction == ADJUST_CLOSE && node_settings.last_position == 0) time_ms = time_total_s * 1000 / 200; // Change at least 1/200 => 0.5 % adjusting_start_time = Date.now(); if (adjustAction == ADJUST_OPEN) node.send([{ payload: true }, { payload: false }, null]); else node.send([{ payload: false }, { payload: true }, null]); adjusting = adjustAction; changing_timeout = setTimeout(() => { changing_timeout = null; stopChanging(); adjusting = null; setStatus(); }, time_ms); setStatus(); } let stopChanging = () => { // Stop any runnting timeout if (changing_timeout !== null) { clearTimeout(changing_timeout); changing_timeout = null; } // No changing in progress if (adjusting_start_time == null) return; // Calculate moved percentage let time_passed = (Date.now() - adjusting_start_time) / 1000; adjusting_start_time = null; let changed_value = (time_passed / time_total_s) * 100; // calculate in % value (0-100) if (adjusting == ADJUST_OPEN) node_settings.last_position += changed_value; else node_settings.last_position -= changed_value; // Only values from 0 to 100 are allowed node_settings.last_position = Math.min(Math.max(node_settings.last_position, 0), 100); node.send([{ payload: false }, { payload: false }, { payload: helper.toFixed(node_settings.last_position, 1) }]); setStatus(); } let calibrate = () => { // start closing node.send([{ payload: false }, { payload: true }, null]); calibration_timeout = setTimeout(() => { // stop closing node.send([{ payload: false }, { payload: false }, null]); node_settings.last_position = 0; smart_context.set(node.id, node_settings); // Calibration finished, start sampling if enabled calibration_timeout = null; if (node_settings.enabled) startSampling(); else stopSampling(); setStatus(); }, time_total_s * 1000); } let doOffMode = () => { switch (node_settings.off_mode) { case "OPEN": startChanging(ADJUST_OPEN, time_total_s * 1000); break; case "CLOSE": startChanging(ADJUST_CLOSE, time_total_s * 1000); break; case "NOTHING": default: break; } } let setStatus = () => { if (calibration_timeout !== null) node.status({ fill: "yellow", shape: "ring", text: helper.getCurrentTimeForStatus() + ": In calibration" }); else if (changing_timeout != null) node.status({ fill: node_settings.enabled ? "green" : "red", shape: "ring", text: helper.getCurrentTimeForStatus() + ": " + (node_settings.valve_mode == "HEATING" ? "🔥" : "❄️") + " " + (adjusting == ADJUST_OPEN ? "Opening" : "Closing") + ", Set: " + helper.toFixed(node_settings.setpoint, 1) + "°C, Cur: " + helper.toFixed(current_temperature, 1) + "°C, Pos: " + helper.toFixed(node_settings.last_position, 1) + "%" }); else node.status({ fill: node_settings.enabled ? "green" : "red", shape: "dot", text: helper.getCurrentTimeForStatus() + ": " + (node_settings.valve_mode == "HEATING" ? "🔥" : "❄️") + " Set: " + helper.toFixed(node_settings.setpoint, 1) + "°C, Cur: " + helper.toFixed(current_temperature, 1) + "°C, Pos: " + helper.toFixed(node_settings.last_position, 1) + "%" }); } if (node_settings.last_position == null) { // Start calibration after 10s setTimeout(calibrate, 10 * 1000); } else if (node_settings.enabled) { startSampling(); node.send([null, null, { payload: helper.toFixed(node_settings.last_position, 1) }]); } setStatus(); } RED.nodes.registerType("smart_mixing-valve", MixingValveNode); }