module.exports = function(RED) { function thermostat(config) { RED.nodes.createNode(this,config); this.room = config.room; this.zone = config.zone; var node = this; var context = node.context(); const HEAT_ON_THRESHOLD = 0.2 const HEAT_OFF_THRESHOLD = 0.2 let debug = "" let pwm_timer = null; context.set("pwm_period", 10000); // 10 seconds context.set("pwm_tick", 1000); // 1 second function manage_pwm(duty_cycle_percent) { if (duty_cycle_percent === -1) { if (pwm_timer !== null) { clearInterval(pwm_timer); pwm_timer = null; node.log("PWM stopped"); } return; } const pwm_duty_cycle = Math.max(0, Math.min(1, duty_cycle_percent / 100)); context.set("pwm_duty_cycle", pwm_duty_cycle); node.log(`PWM duty cycle set to ${pwm_duty_cycle}`); if (pwm_timer === null) { let cycle_start_time = Date.now(); const pwm_tick = context.get("pwm_tick"); pwm_timer = setInterval(() => { const pwm_period = context.get("pwm_period"); const pwm_duty_cycle = context.get("pwm_duty_cycle"); // Retrieve each time const elapsed_time = Date.now() - cycle_start_time; const on_time = pwm_period * pwm_duty_cycle; node.log(`Elapsed: ${elapsed_time}, On Time: ${on_time}`); if (elapsed_time >= pwm_period) { cycle_start_time = Date.now(); } if (elapsed_time < on_time) { node.send([null, { payload: 1 }]); console.log("Sending 1"); } else { node.send([null, { payload: 0 }]); console.log("Sending 0"); } }, pwm_tick); } } function debug_text(text) { if (text === "return_debug") { let debug_tmp = debug debug = "" return debug_tmp } debug = debug ? `${debug}, ${text}` : text } node.on('input', function(msg) { let topic = msg.topic let payload = msg.payload let control_mode = context.get("control_mode") || "auto" //auto, on, off let current_temp = context.get("current_temp") let target_temp = context.get("target_temp") || 19 let window_state = context.get("window_state") || "close" //open, close let window_mode = context.get("window_mode") || "cool" //heat, cool, off let heater_status = null function logic() { if (control_mode === "off") { heater_status = 0 debug_text("Heater: force/off") return } if (control_mode === "on") { heater_status = 1 debug_text("Heater: force/on") return } if (control_mode === "auto" && current_temp !== null && target_temp !== null) { if (window_state === "open" && window_mode === "cool") { heater_status = 0 debug_text("Heater: w-open-cool/off") return } if (window_state === "open" && window_mode === "heat") { heater_status = 1 debug_text("Heater: w-open-heat/on") return } if (window_state === "close") { if (current_temp <= target_temp - HEAT_ON_THRESHOLD) { heater_status = 1 debug_text(`Heater: auto/${"on"}`) } else if (current_temp >= target_temp + HEAT_OFF_THRESHOLD) { heater_status = 0 debug_text(`Heater: auto/${"off"}`) } else { debug_text(`Heater: auto/${"hyst"}`) } } return } if (control_mode === "pwm" && current_temp !== null && target_temp !== null) { manage_pwm(context.get("pwm_duty_cycle")) return } } debug_text(`W-mode: ${window_mode}`) let mqtt_control = topic.split('/') if (mqtt_control.length === 3 && mqtt_control[0] === 'control') { if (mqtt_control[2] === node.zone || mqtt_control[2] === node.zone + "_" + node.room || mqtt_control[2] === "all") { if (mqtt_control[1] === "window_mode_control" || mqtt_control[1] === "heat_mode_control" || mqtt_control[1] === "pwm_control") { topic = mqtt_control[1] } else return } else return } if (topic === "window_mode_control" && (payload === "cool" || payload === "heat" || payload === "off")) { window_mode = payload context.set("window_mode", window_mode) } if (topic === "heat_mode_control" && (payload === "on" || payload === "off" || payload === "auto" || payload === "pwm")) { control_mode = payload context.set("control_mode", control_mode) } if (topic === "pwm_control") { context.set("pwm_duty_cycle", payload) } debug_text(`W-state: ${window_state}`) if (topic === "window" && (payload === "open" || payload === "close")) { window_state = payload context.set("window_state", window_state) } if (topic === "target") { target_temp = parseFloat(msg.payload) context.set("target_temp", target_temp) debug_text(`Current: ${current_temp}`) debug_text(`Target: ${target_temp}`) } if (msg.topic === "current") { current_temp = parseFloat(msg.payload) context.set("current_temp", current_temp) debug_text(`Current: ${current_temp}`) debug_text(`Target: ${target_temp}`) } logic() let extended_stats = { "control_mode": control_mode, "current_temp": current_temp, "target_temp": target_temp, "window_state": window_state, "window_mode": window_mode, "heater_status": heater_status, "HEAT_ON_THRESHOLD": HEAT_ON_THRESHOLD, "HEAT_OFF_THRESHOLD": HEAT_OFF_THRESHOLD, } node.status({ text: debug_text("return_debug") }) let msg_heater_status = { payload: heater_status } let msg_extended_stats = { payload: extended_stats, topic: node.zone + "_" + node.room } if (heater_status === null || typeof heater_status === "undefined") { msg_heater_status = null } node.send([msg_extended_stats, msg_heater_status]); }); } RED.nodes.registerType("c-thermostat", thermostat ); }