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 ); }