From 08f73c1b5899ed78d226fe4efaa3b4801eea1398 Mon Sep 17 00:00:00 2001 From: vvzvlad Date: Tue, 3 Oct 2023 16:59:59 +0300 Subject: [PATCH] Bump version to 0.1.11 --- package.json | 5 +- thermostat-analyzer.html | 31 ++++++ thermostat-analyzer.js | 218 +++++++++++++++++++++++++++++++++++++++ thermostat.js | 6 +- 4 files changed, 253 insertions(+), 7 deletions(-) create mode 100644 thermostat-analyzer.html create mode 100644 thermostat-analyzer.js diff --git a/package.json b/package.json index a136436..4e4926f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vvzvlad/node-red-contrib-rn-combined-nodes", - "version": "0.1.10", + "version": "0.1.11", "description": "", "main": "index.js", "keywords": [ @@ -15,7 +15,8 @@ }, "node-red": { "nodes": { - "thermostat": "thermostat.js" + "thermostat": "thermostat.js", + "thermostat-analyzer": "thermostat-analyzer.js" } }, "author": "vvzvlad", diff --git a/thermostat-analyzer.html b/thermostat-analyzer.html new file mode 100644 index 0000000..22534ad --- /dev/null +++ b/thermostat-analyzer.html @@ -0,0 +1,31 @@ + + + + + + diff --git a/thermostat-analyzer.js b/thermostat-analyzer.js new file mode 100644 index 0000000..4d5fa0e --- /dev/null +++ b/thermostat-analyzer.js @@ -0,0 +1,218 @@ +module.exports = function(RED) { + function thermostat_analyzer(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 = "" + + + + + function debug_text(text) { + if (text === "return_debug") { + let debug_tmp = debug + debug = "" + return debug_tmp + } + debug = debug ? `${debug}, ${text}` : text + + } + + function predict_temp_change(current_temp, time_interval) { + let last_temp = context.get('last_temp') || null + let last_temp_time = context.get('last_temp_time') || null + if (last_temp === null || last_temp_time === null) { + last_temp = current_temp + last_temp_time = new Date().getTime() + context.set('last_temp', last_temp) + context.set('last_temp_time', last_temp_time) + return 0 + } + + const temp_diff = current_temp - last_temp + const time_diff = (new Date().getTime() - last_temp_time) / 1000 + const rate_of_change = temp_diff / time_diff + + last_temp = current_temp + last_temp_time = new Date().getTime() + context.set('last_temp', last_temp) + context.set('last_temp_time', last_temp_time) + + return current_temp + rate_of_change * time_interval + } + + + + 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-predict" && current_temp !== null && target_temp !== null) { + const predicted_temp = predict_temp_change(current_temp, 40) + 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 (predicted_temp < target_temp) { + heater_status = 1 + debug_text(`Heater: auto-predict/${"on"}`) + } else if (predicted_temp >= target_temp) { + heater_status = 0 + debug_text(`Heater: auto-predict/${"off"}`) + } + } + 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 + } + } + + + + + 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") { + 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 === "auto-predict")) { + control_mode = payload + context.set("control_mode", control_mode) + } + + 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_heater_status, msg_extended_stats]); + }); + + + + + } + RED.nodes.registerType("c-thermostat-analyzer", thermostat_analyzer ); +} + + + + + + + + + + + + diff --git a/thermostat.js b/thermostat.js index 0e51ade..a8d596c 100644 --- a/thermostat.js +++ b/thermostat.js @@ -173,10 +173,8 @@ module.exports = function(RED) { debug_text(`Target: ${target_temp}`) } - logic() - let extended_stats = { "control_mode": control_mode, "current_temp": current_temp, @@ -188,12 +186,10 @@ module.exports = function(RED) { "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, room: node.room, zone: node.zone } + let msg_extended_stats = { payload: extended_stats, topic: node.zone + "_" + node.room } if (heater_status === null || typeof heater_status === "undefined") { msg_heater_status = null