2023-10-02 21:33:26 +03:00
|
|
|
module.exports = function(RED) {
|
2023-10-02 22:32:55 +03:00
|
|
|
function thermostat(config) {
|
2023-10-02 21:33:26 +03:00
|
|
|
RED.nodes.createNode(this,config);
|
2023-10-03 16:13:30 +03:00
|
|
|
this.room = config.room;
|
|
|
|
this.zone = config.zone;
|
2023-10-02 21:33:26 +03:00
|
|
|
var node = this;
|
2023-10-03 00:26:45 +03:00
|
|
|
var context = node.context();
|
|
|
|
|
2023-10-03 16:09:12 +03:00
|
|
|
|
2023-10-03 00:26:45 +03:00
|
|
|
const HEAT_ON_THRESHOLD = 0.2
|
|
|
|
const HEAT_OFF_THRESHOLD = 0.2
|
|
|
|
let debug = ""
|
|
|
|
|
2023-10-05 23:41:35 +03:00
|
|
|
|
2023-10-05 23:29:05 +03:00
|
|
|
let pwm_timer = null;
|
2023-10-05 23:41:35 +03:00
|
|
|
let last_state = null;
|
2023-10-05 23:34:03 +03:00
|
|
|
context.set("pwm_period", 10000); // 10 seconds
|
|
|
|
context.set("pwm_tick", 1000); // 1 second
|
2023-10-05 23:21:13 +03:00
|
|
|
|
|
|
|
function manage_pwm(duty_cycle_percent) {
|
|
|
|
if (duty_cycle_percent === -1) {
|
|
|
|
if (pwm_timer !== null) {
|
2023-10-05 23:29:05 +03:00
|
|
|
clearInterval(pwm_timer);
|
|
|
|
pwm_timer = null;
|
2023-10-05 23:41:35 +03:00
|
|
|
last_state = null;
|
2023-10-05 23:34:03 +03:00
|
|
|
node.log("PWM stopped");
|
2023-10-05 23:21:13 +03:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const pwm_duty_cycle = Math.max(0, Math.min(1, duty_cycle_percent / 100));
|
|
|
|
context.set("pwm_duty_cycle", pwm_duty_cycle);
|
2023-10-05 23:34:03 +03:00
|
|
|
node.log(`PWM duty cycle set to ${pwm_duty_cycle}`);
|
2023-10-05 23:21:13 +03:00
|
|
|
|
2023-10-05 23:26:17 +03:00
|
|
|
if (pwm_timer === null) {
|
2023-10-05 23:21:13 +03:00
|
|
|
let cycle_start_time = Date.now();
|
|
|
|
const pwm_tick = context.get("pwm_tick");
|
|
|
|
|
2023-10-05 23:26:17 +03:00
|
|
|
pwm_timer = setInterval(() => {
|
2023-10-05 23:21:13 +03:00
|
|
|
const pwm_period = context.get("pwm_period");
|
2023-10-05 23:41:35 +03:00
|
|
|
const pwm_duty_cycle = context.get("pwm_duty_cycle");
|
2023-10-05 23:21:13 +03:00
|
|
|
const elapsed_time = Date.now() - cycle_start_time;
|
2023-10-05 23:34:03 +03:00
|
|
|
const on_time = pwm_period * pwm_duty_cycle;
|
|
|
|
|
|
|
|
node.log(`Elapsed: ${elapsed_time}, On Time: ${on_time}`);
|
2023-10-05 23:21:13 +03:00
|
|
|
|
|
|
|
if (elapsed_time >= pwm_period) {
|
|
|
|
cycle_start_time = Date.now();
|
|
|
|
}
|
|
|
|
|
2023-10-05 23:41:35 +03:00
|
|
|
let current_state = elapsed_time < on_time ? 1 : 0;
|
|
|
|
|
|
|
|
if (current_state !== last_state) {
|
|
|
|
node.send([null, { payload: current_state }]);
|
|
|
|
node.log(`Sending ${current_state}`);
|
|
|
|
last_state = current_state;
|
2023-10-05 23:21:13 +03:00
|
|
|
}
|
2023-10-03 00:55:42 +03:00
|
|
|
|
2023-10-05 23:21:13 +03:00
|
|
|
}, pwm_tick);
|
|
|
|
}
|
|
|
|
}
|
2023-10-03 00:55:42 +03:00
|
|
|
|
2023-10-05 23:29:05 +03:00
|
|
|
|
2023-10-05 23:34:03 +03:00
|
|
|
|
2023-10-05 23:41:35 +03:00
|
|
|
|
2023-10-03 00:26:45 +03:00
|
|
|
function debug_text(text) {
|
2023-10-03 00:55:42 +03:00
|
|
|
if (text === "return_debug") {
|
|
|
|
let debug_tmp = debug
|
|
|
|
debug = ""
|
2023-10-03 01:31:10 +03:00
|
|
|
return debug_tmp
|
2023-10-03 00:55:42 +03:00
|
|
|
}
|
2023-10-03 00:26:45 +03:00
|
|
|
debug = debug ? `${debug}, ${text}` : text
|
2023-10-03 00:55:42 +03:00
|
|
|
|
2023-10-03 00:26:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-10-03 00:58:52 +03:00
|
|
|
node.on('input', function(msg) {
|
|
|
|
let topic = msg.topic
|
|
|
|
let payload = msg.payload
|
2023-10-03 00:26:45 +03:00
|
|
|
|
2023-10-03 00:58:52 +03:00
|
|
|
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") {
|
2023-10-03 00:26:45 +03:00
|
|
|
heater_status = 0
|
2023-10-03 00:58:52 +03:00
|
|
|
debug_text("Heater: force/off")
|
2023-10-03 00:26:45 +03:00
|
|
|
return
|
|
|
|
}
|
2023-10-03 00:58:52 +03:00
|
|
|
|
|
|
|
if (control_mode === "on") {
|
2023-10-03 00:26:45 +03:00
|
|
|
heater_status = 1
|
2023-10-03 00:58:52 +03:00
|
|
|
debug_text("Heater: force/on")
|
2023-10-03 00:26:45 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-10-03 00:58:52 +03:00
|
|
|
if (control_mode === "auto" && current_temp !== null && target_temp !== null) {
|
|
|
|
if (window_state === "open" && window_mode === "cool") {
|
2023-10-03 00:26:45 +03:00
|
|
|
heater_status = 0
|
2023-10-03 00:58:52 +03:00
|
|
|
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"}`)
|
|
|
|
}
|
2023-10-03 00:26:45 +03:00
|
|
|
}
|
2023-10-03 00:58:52 +03:00
|
|
|
return
|
2023-10-03 00:26:45 +03:00
|
|
|
}
|
2023-10-03 00:58:52 +03:00
|
|
|
|
2023-10-05 23:21:13 +03:00
|
|
|
if (control_mode === "pwm" && current_temp !== null && target_temp !== null) {
|
|
|
|
manage_pwm(context.get("pwm_duty_cycle"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2023-10-03 00:55:42 +03:00
|
|
|
|
2023-10-03 00:26:45 +03:00
|
|
|
debug_text(`W-mode: ${window_mode}`)
|
2023-10-03 16:36:12 +03:00
|
|
|
|
|
|
|
|
|
|
|
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") {
|
2023-10-05 23:21:13 +03:00
|
|
|
if (mqtt_control[1] === "window_mode_control" || mqtt_control[1] === "heat_mode_control" || mqtt_control[1] === "pwm_control") {
|
2023-10-03 16:36:12 +03:00
|
|
|
topic = mqtt_control[1]
|
|
|
|
}
|
2023-10-03 16:39:56 +03:00
|
|
|
else return
|
2023-10-03 16:36:12 +03:00
|
|
|
}
|
2023-10-03 16:39:56 +03:00
|
|
|
else return
|
2023-10-03 16:36:12 +03:00
|
|
|
}
|
|
|
|
|
2023-10-03 00:26:45 +03:00
|
|
|
if (topic === "window_mode_control" && (payload === "cool" || payload === "heat" || payload === "off")) {
|
|
|
|
window_mode = payload
|
|
|
|
context.set("window_mode", window_mode)
|
|
|
|
}
|
|
|
|
|
2023-10-05 23:21:13 +03:00
|
|
|
if (topic === "heat_mode_control" && (payload === "on" || payload === "off" || payload === "auto" || payload === "pwm")) {
|
2023-10-03 00:26:45 +03:00
|
|
|
control_mode = payload
|
|
|
|
context.set("control_mode", control_mode)
|
|
|
|
}
|
|
|
|
|
2023-10-05 23:21:13 +03:00
|
|
|
if (topic === "pwm_control") {
|
|
|
|
context.set("pwm_duty_cycle", payload)
|
|
|
|
}
|
|
|
|
|
2023-10-03 00:26:45 +03:00
|
|
|
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 }
|
2023-10-03 16:59:59 +03:00
|
|
|
let msg_extended_stats = { payload: extended_stats, topic: node.zone + "_" + node.room }
|
2023-10-03 00:26:45 +03:00
|
|
|
|
|
|
|
if (heater_status === null || typeof heater_status === "undefined") {
|
|
|
|
msg_heater_status = null
|
|
|
|
}
|
|
|
|
|
2023-10-04 17:41:53 +03:00
|
|
|
node.send([msg_extended_stats, msg_heater_status]);
|
2023-10-02 21:33:26 +03:00
|
|
|
});
|
2023-10-03 00:26:45 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-10-02 21:33:26 +03:00
|
|
|
}
|
2023-10-02 22:53:03 +03:00
|
|
|
RED.nodes.registerType("c-thermostat", thermostat );
|
2023-10-02 21:33:26 +03:00
|
|
|
}
|
2023-10-03 00:26:45 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|