From 6578d6bf71704cd750eb1f93b488a33a381db75b Mon Sep 17 00:00:00 2001 From: vvzvlad Date: Tue, 3 Oct 2023 17:15:20 +0300 Subject: [PATCH] Bump version to 0.1.12 --- package.json | 2 +- thermostat-analyzer.html | 6 +- thermostat-analyzer.js | 283 +++++++++++++++------------------------ 3 files changed, 112 insertions(+), 179 deletions(-) diff --git a/package.json b/package.json index 4e4926f..bc1b2a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vvzvlad/node-red-contrib-rn-combined-nodes", - "version": "0.1.11", + "version": "0.1.12", "description": "", "main": "index.js", "keywords": [ diff --git a/thermostat-analyzer.html b/thermostat-analyzer.html index 22534ad..9a6242f 100644 --- a/thermostat-analyzer.html +++ b/thermostat-analyzer.html @@ -4,13 +4,11 @@ color: '#F3B567', paletteLabel: 'Th-analyzer', defaults: { - name: {value:""}, - room: {value:"", required:true}, - zone: {value:"", required:true} + name: {value:""} }, inputs: 1, outputs: 3, - outputLabels: ["heater","stats"], + outputLabels: ["mqtt_debug","state_text","current_temp"], icon: "font-awesome/fa-snowflake-o", label: function() { return this.name||"Th-analyzer"; diff --git a/thermostat-analyzer.js b/thermostat-analyzer.js index 4d5fa0e..f77c44c 100644 --- a/thermostat-analyzer.js +++ b/thermostat-analyzer.js @@ -6,196 +6,131 @@ module.exports = function(RED) { var node = this; var context = node.context(); + function status_reason(msg) { + let heater = msg.payload.heater_status + let window_state = msg.payload.window_state + let control_mode = msg.payload.control_mode - 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 + if (window_state == "open") { + context.set('heater_reason', "(открыто окно)") } - debug = debug ? `${debug}, ${text}` : text + if (control_mode == "auto") { + context.set('heater_reason', "(поддержание температуры)") + } + + if (heater === 1) { + context.set('heater_status', "Включен") + } + + if (heater === 0) { + context.set('heater_status', "Выключен") + } + + let heater_reason = context.get('heater_reason') || "" + let heater_status = context.get('heater_status') || "" + + return heater_status + " " + heater_reason } - 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 + function get_current_temp(msg) { + let current_temp = msg.payload.current_temp || "00" + return current_temp } + function analyzer(msg) { + const status_window = 3 * 60 * 60 * 1000 + const transitions_window = 3 * 60 * 60 * 1000 + const max_temp_diff = 3 + + + function round_to_two_decimal_places(num) { + return Math.round((num + Number.EPSILON) * 100) / 100 + } + + function update_heater_status(value) { + let status_hist = context.get('status_hist') || [] + if (value === null) return + const current_time = new Date().getTime() + status_hist.push({ value, timestamp: current_time }) + status_hist = status_hist.filter(item => current_time - item.timestamp <= status_window) + let sum = 0 + let total_time = 0 + for (let i = 1; i < status_hist.length; i++) { + const time_diff = status_hist[i].timestamp - status_hist[i - 1].timestamp + sum += status_hist[i].value * time_diff + total_time += time_diff + } + context.set('status_hist', status_hist) + return round_to_two_decimal_places(total_time ? sum / total_time : null) + } + + function update_temp_diff(current_temp, target_temp) { + let diff_hist = context.get('diff_hist') || [] + const current_time = new Date().getTime() + const temp_diff = Math.abs(current_temp - target_temp) + diff_hist.push({ value: temp_diff, timestamp: current_time }) + diff_hist = diff_hist.filter(item => current_time - item.timestamp <= status_window) + let sum = 0 + let total_time = 0 + for (let i = 1; i < diff_hist.length; i++) { + const time_diff = diff_hist[i].timestamp - diff_hist[i - 1].timestamp + sum += diff_hist[i].value * time_diff + total_time += time_diff + } + context.set('diff_hist', diff_hist) + return round_to_two_decimal_places(total_time ? sum / total_time : null) + } + + function update_transitions(new_heater_status, new_window_state) { + let transitions = context.get('transitions') || [] + let last_heater_status = context.get('last_heater_status') + let last_window_state = context.get('last_window_state') + + if (new_heater_status !== last_heater_status && new_window_state === last_window_state) { + const current_time = new Date().getTime() + transitions.push({ timestamp: current_time }) + transitions = transitions.filter(item => current_time - item.timestamp <= transitions_window) + context.set('last_heater_status', new_heater_status) + context.set('transitions', transitions) + } + context.set('last_window_state', new_window_state) + return transitions.length * 8 + } + + const payload = msg.payload + + const avg_heater_status = update_heater_status(payload.heater_status) + const avg_temp_diff = update_temp_diff(payload.current_temp, payload.target_temp) + const transitions_count = update_transitions(payload.heater_status, payload.window_state) + + let malfunction = 0 + if (avg_temp_diff > max_temp_diff && (avg_heater_status > 0.9 || avg_heater_status < 0.1)) malfunction = 1 + + msg.payload.malfunction = malfunction + msg.payload.avg_temp_diff = avg_temp_diff + msg.payload.avg_heater_status = avg_heater_status + msg.payload.transitions_count = transitions_count + msg.payload.const_max_temp_diff = max_temp_diff + + node.status({ text: `malfunction: ${malfunction}, transitions: ${transitions_count}` }) + + return msg.payload + } 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 } + let key = "fake" + let mqtt_debug = { payload: analyzer(msg), topic: `debug/${node.zone}_${node.room}_/th/${key}`} + let state_text = { payload: status_reason(msg) } + let current_temp = { payload: get_current_temp(msg) } if (heater_status === null || typeof heater_status === "undefined") { msg_heater_status = null } - node.send([msg_heater_status, msg_extended_stats]); + node.send([mqtt_debug, state_text, current_temp]); });