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