node-red-contrib-rn-combine.../thermostat-analyzer.js
2023-10-04 22:16:43 +03:00

160 lines
6.2 KiB
JavaScript

module.exports = function(RED) {
function thermostat_analyzer(config) {
RED.nodes.createNode(this,config);
this.status_window = config.status_window;
this.transitions_window = config.transitions_window;
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
if (window_state == "open") {
context.set('heater_reason', "(открыто окно)")
}
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 get_current_temp(msg) {
let current_temp = msg.payload.current_temp || "00"
return current_temp
}
function analyzer(msg) {
const status_window_h = node.status_window || 5
const transitions_window_h = node.transitions_window || 5
const max_temp_diff = 3
const status_window_ms = status_window_h * 60 * 60 * 1000
const transitions_window_ms = transitions_window_h * 60 * 60 * 1000
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_ms)
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_ms)
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_ms)
context.set('last_heater_status', new_heater_status)
context.set('transitions', transitions)
}
context.set('last_window_state', new_window_state)
return transitions.length / transitions_window_h * 24
}
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 msg_current_temp
let msg_state_text
if (msg.payload.heater_status === null && typeof msg.payload.heater_status === "undefined") msg_state_text = null
else msg_state_text = { payload: status_reason(msg) }
if (msg.payload.current_temp === null && typeof msg.payload.current_temp === "undefined") msg_current_temp = null
else msg_current_temp = { payload: get_current_temp(msg) }
let stats = analyzer(msg)
let msg_mqtt_debug = { payload: JSON.stringify(stats), topic: `debug/${msg.topic}/th`}
node.send([msg_mqtt_debug, msg_state_text, msg_current_temp]);
});
}
RED.nodes.registerType("c-thermostat-analyzer", thermostat_analyzer );
}