node-red-contrib-vib-smart-boiler
Version:
Smart boiler node to control multiple thermostat
510 lines (441 loc) • 24.4 kB
HTML
<script type="text/javascript">
// Terminal command node-red -v -D logging.console.level=trace
RED.nodes.registerType('smart-boiler',{
category: 'vib-node',
color: '#bdeeff',
defaults: {
name: {value:""},
mqttSettings: {value: "", type: "smart-boiler-settings"},
boilerCurrentTemperatureSetTopic:{value:""},
boilerCurrentTemperatureKey:{value:""},
boilerSpSetTopic:{value:""},
boilerSpKey:{value:""},
boilerLeadingDeviceSetTopic:{value:""},
boilerLeadingDeviceIdSetTopic:{value:""},
boilerLeadingDeviceKey:{value:""},
boilerSwCentralHeatingSetTopic:{value:""},
boilerSwCentralHeatingKey:{value:""},
boilerEntityID:{value:""},
mqttUpdates:{value:false},
debugInfo:{value:"none"},
boilerSecurity:{value:false},
triggerMode:{value:"triggerMode.statechange.startup",required: true},
cycleDuration: {value:60,required: true,validate:function(v) {
if (isNaN(v)){
return false;
}
if (v<0 || v> 600){
return false;
}
return true;
}},
outputUpdates:{value:true},
defaultSp:{value:5,required: true, validate:function(v) {
if (isNaN(v)){
return false;
}
if (v<0 || v> 35){
//this.warn("overrideSp need to be >0 et <= 35")
return false;
}
return true;
}},
defaultTemp:{value:10,required: true, validate:function(v) {
if (isNaN(v)){
return false;
}
if (v<0 || v> 35){
//this.warn("overrideSp need to be >0 et <= 35")
return false;
}
return true;
}},
maxDurationSinceLastInput:{value:"5",required: true, validate:function(v) {
if (isNaN(v)){
return false;
}
if (v<0 || v> 120){
//this.warn("overrideSp need to be >0 et <= 35")
return false;
}
return true;
}}
},
inputs:1,
outputs:1,
icon: "font-awesome/fa-fire",
button: {
onclick: function() {
let node=this;
$.ajax({
url: "smartboiler/" + node.id,
type: "POST",
data: JSON.stringify({payload:{command:"trigger"}}),
contentType: "application/json; charset=utf-8",
success: function (resp) {
RED.notify(node._("smart-boiler trigger success", { label: "ss" }), { type: "success", id: "inject", timeout: 2000 });
},
error: function (jqXHR, textStatus, errorThrown) {
if (jqXHR.status == 404) {
RED.notify(node._("common.notification.error", { message: node._("common.notification.errors.not-deployed") }), "error");
} else if (jqXHR.status == 500) {
RED.notify(node._("common.notification.error", { message: node._("inject.errors.failed") }), "error");
} else if (jqXHR.status == 0) {
RED.notify(node._("common.notification.error", { message: node._("common.notification.errors.no-response") }), "error");
} else {
RED.notify(node._("common.notification.error", { message: node._("common.notification.errors.unexpected", { status: jqXHR.status, message: textStatus }) }), "error");
}
}
});
}
},
label: function() {
return this.name||"smart-boiler";
},
oneditprepare: function() {
var node = this;
$("#node-input-boilerEntityID").on('change', function() {
console.log("boilerEntityID changed, updating topics");
node.boilerEntityID = $("#node-input-boilerEntityID").val();
let prefix = (node.mqttSettings && node.mqttSettings.mqttRootPath) ? node.mqttSettings.mqttRootPath : "homeassistant";
if ($("#node-input-boilerSpSetTopic").val().length==0){
let setpointTopic = prefix + "/" + node.boilerEntityID + "/setpoint-temperature/set";
$("#node-input-boilerSpSetTopic").val(setpointTopic);
}
if ($("#node-input-boilerSwCentralHeatingSetTopic").val().length==0){
let swCentralHeatingTopic = prefix + "/" + node.boilerEntityID + "/swCentralHeating/set";
$("#node-input-boilerSwCentralHeatingSetTopic").val(swCentralHeatingTopic);
}
if ($("#node-input-boilerCurrentTemperatureSetTopic").val().length==0){
let tempTopic = prefix + "/" + node.boilerEntityID + "/current-temperature/state";
$("#node-input-boilerCurrentTemperatureSetTopic").val(tempTopic);
}
if ($("#node-input-boilerLeadingDeviceSetTopic").val().length==0){
let leadingDeviceTopic = prefix + "/" + node.boilerEntityID + "/leadingDevice/state";
$("#node-input-boilerLeadingDeviceSetTopic").val(leadingDeviceTopic);
}
if ($("#node-input-boilerLeadingDeviceIdSetTopic").val().length==0){
let leadingDeviceIdTopic = prefix + "/" + node.boilerEntityID + "/leadingDeviceId/state";
$("#node-input-boilerLeadingDeviceIdSetTopic").val(leadingDeviceIdTopic);
}
});
},
oneditsave: function() {
var node = this;
},
oneditresize: function() {
}
});
</script>
<script type="text/html" data-template-name="smart-boiler">
<style>
.form-row label {
display: inline-block;
width: 180px ;
white-space: nowrap;
}
.form-row input[type="text"] {
width: 250px ;
}
.form-row input[type="checkbox"] {
width: 120px ;
}
</style>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"> </i> Name</label>
<input type="text" title="node & home asssitant device name" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"> </i> Boiler entity</label>
<input type="text" title="node & home asssitant device name" id="node-input-boilerEntityID" placeholder="Smart boiler ID ">
</div>
<div class="form-row">
<label for="node-input-mqttSettings"><i class="fa fa-wrench"> </i> MQTT</label>
<input type="text" id="node-input-mqttSettings"></input>
</div>
<div class="form-row">
<label for="node-input-mqttUpdates"><i class="fa fa-tasks"> </i> Mqtt updates</label>
<input type="checkbox" title="" id="node-input-mqttUpdates"></span>
</div>
<div class="form-row">
<label for="node-input-outputUpdates"><i class="fa fa-tasks"> </i> Output updates</label>
<input type="checkbox" title="" id="node-input-outputUpdates">
</div>
<div class="form-row">
<span><i class="fa fa-tasks"> </i>Current temperature</span>
</div>
<div class="form-row">
<label for="node-input-boilerCurrentTemperatureSetTopic">Set</label>
<input type="text" id="node-input-boilerCurrentTemperatureSetTopic" placeholder="mqtt topic"></input>
</div>
<div class="form-row">
<label for="node-input-boilerCurrentTemperatureKey">Key</label>
<input type="text" id="node-input-boilerCurrentTemperatureKey" placeholder="mqtt key"></input>
</div>
<div class="form-row">
<span><i class="fa fa-tasks"> </i>Set point temperature</span>
</div>
<div class="form-row">
<label for="node-input-boilerSpSetTopic">Set</label>
<input type="text" id="node-input-boilerSpSetTopic" placeholder="mqtt topic"></input>
</div>
<div class="form-row">
<label for="node-input-boilerSpKey">Key</label>
<input type="text" id="node-input-boilerSpKey" placeholder="mqtt key"></input>
</div>
<div class="form-row">
<span><i class="fa fa-tasks"> </i>Leading Device</span>
</div>
<div class="form-row">
<label for="node-input-boilerLeadingDeviceSetTopic">Set name</label>
<input type="text" title="" id="node-input-boilerLeadingDeviceSetTopic" placeholder="mqtt topic">
</div>
<div class="form-row">
<label for="node-input-boilerLeadingDeviceIdSetTopic">Set Id</label>
<input type="text" title="" id="node-input-boilerLeadingDeviceIdSetTopic" placeholder="mqtt topic">
</div>
<div class="form-row">
<label for="node-input-boilerLeadingDeviceKey">Key</label>
<input type="text" title="" id="node-input-boilerLeadingDeviceKey" placeholder="mqtt topic">
</div>
<div class="form-row">
<span><i class="fa fa-tasks"> </i>Central Heating Switch</span>
</div>
<div class="form-row">
<label for="node-input-boilerSwCentralHeatingSetTopic">Set</label>
<input type="text" title="" id="node-input-boilerSwCentralHeatingSetTopic" placeholder="mqtt topic">
</div>
<div class="form-row">
<label for="node-input-boilerSwCentralHeatingKey">Key</label>
<input type="text" title="" id="node-input-boilerSwCentralHeatingKey" placeholder="mqtt topic">
</div>
<div class="form-row">
<span><i class="fa fa-tasks"> </i>Update</span>
</div>
<div class="form-row">
<label for="node-input-cycleDuration"><i class="fa fa-tasks"> </i>Cycle</label>
<input type="text" id="node-input-cycleDuration" style="width:60px !important; text-align: right;" placeholder="duration">
<span>s</span>
</div>
<div class="form-row">
<label for="node-input-triggerMode"><i class="fa fa-play"> </i> Trigger mode </label>
<select id="node-input-triggerMode">
<option value="triggerMode.statechange">When state changes</option>
<option value="triggerMode.statechange.startup">When state changes + startup</option>
<option value="triggerMode.everyCycle">Every cycle</option>
</select>
</div>
<div class="form-row">
<label for="node-input-maxDurationSinceLastInput"><i class="fa fa-tasks"> </i> Max duration</label>
<input type="text" id="node-input-maxDurationSinceLastInput" style="width:60px !important; text-align: right;" placeholder="duration">
<span> min since last input</span>
</div>
<div class="form-row">
<label for="node-input-boilerSecurity"><i class="fa fa-tasks"> </i>Security</label>
<input type="checkbox" title="" id="node-input-boilerSecurity" placeholder="">
</div>
<div class="form-row">
<label for="node-input-defaultTemp"><i class="fa fa-tasks"></i> Default temp</label>
<input type="text" id="node-input-defaultTemp" style="width:60px !important; text-align: right;" placeholder="duration">
<span> °C</span>
</div>
<div class="form-row">
<label for="node-input-defaultSp"><i class="fa fa-tasks"></i> Default set point</label>
<input type="text" id="node-input-defaultSp" style="width:60px !important; text-align: right;" placeholder="duration">
<span> °C</span>
</div>
<div class="form-row">
<label for="node-input-debugInfo"><i class="fa fa-bug"> </i> Debug Level</label>
<select id="node-input-debugInfo">
<option value="none">None</option>
<option value="error">Error</option>
<option value="warn">Warning</option>
<option value="info">Info</option>
<option value="debug">Debug</option>
</select>
</div>
</script>
<script type="text/html" data-help-name="smart-boiler">
<!--
╔══════════════════════════════════════════════════════════════════════════════╗
║ SMART-BOILER NODE ║
╚══════════════════════════════════════════════════════════════════════════════╝
OVERVIEW:
---------
This node is part of an intelligent heating control system consisting of three
main components:
• Smart-Scheduler: Manages multiple schedules and temperature set-points
• Smart-Valve: Controls and groups individual valves by room/zone
• Smart-Boiler: Central controller that determines optimal boiler temperature
based on all connected valve states
PURPOSE:
--------
Smart-Boiler aggregates data from multiple Smart-Valve nodes to intelligently
control the central heating boiler, ensuring efficient heating by prioritizing
the valve with the greatest heating demand.
INPUT MESSAGE FORMAT:
--------------------
msg.payload = {
command: "set", // Required
setpoint: [number], // Set-point (target temperature) in °C
temperature: [number], // Current valve temperature in °C
requestedBy: [string], // Valve description/ID (e.g., "Valve kitchen")
groupId: [number] // Group ID
}
Example:
msg.payload = { command: "set", setpoint: 23, temperature: 20, requestedBy: "Valve kitchen", groupId: 1 }
VALVE PROCESSING:
----------------
• Each input message creates or updates a valve entry in the internal stack
• Valves are evaluated on each update cycle
ACTIVE VALVE SELECTION LOGIC:
-----------------------------
A valve is considered ACTIVE when: sp > temp (heating required)
The boiler is controlled by the valve with the HIGHEST temperature gap:
gap = sp - temp
Example Scenario:
• Valve A: sp=19°C, temp=21°C → INACTIVE (sp < temp)
• Valve B: sp=23°C, temp=20°C → gap=3°C (ACTIVE)
• Valve C: sp=24°C, temp=19°C → gap=5°C (ACTIVE - CONTROLS BOILER)
Result: Valve C drives the boiler with sp=24°C, temp=19°C
FALLBACK RULES:
--------------
• No active valves: The highest set-point among inactive valves is used as
the boiler reference
• Input timeout: If no input received within max duration, a safety message
is sent with default values to prevent continuous heating
OUTPUT MODES:
------------
1. MQTT Output (Recommended):
- Set-point topic: Publishes boiler target temperature
- Temperature topic: Publishes current reference temperature
- Leading device topic: Publishes name of controlling valve
2. Standard Node Output:
msg.payload = {
sp: [number],
temp: [number],
name: [string],
id: [string],
groupId: [number],
lastupdate: [ISO 8601 timestamp]
}
CONFIGURATION PARAMETERS:
------------------------
┌─────────────────────────────────┬──────────────────────────────────────────┐
│ Parameter │ Description │
├─────────────────────────────────┼──────────────────────────────────────────┤
│ MQTT │ MQTT broker connection configuration │
│ MQTT updates │ Enable direct MQTT publishing │
│ Output updates │ Enable standard node output │
│ Set point topic │ MQTT topic for boiler set-point │
│ Temperature topic │ MQTT topic for boiler current temp │
│ Leading Device topic │ MQTT topic for active valve name │
│ Update cycle duration │ Interval for valve stack evaluation │
│ Max duration since last input │ Timeout before safety mode activation │
│ Default temp │ Safety mode default temperature │
│ Default set point │ Safety mode default set-point │
└─────────────────────────────────┴──────────────────────────────────────────┘
SAFETY FEATURES:
---------------
• Watchdog timer prevents endless heating if valve communication fails
• Configurable default values ensure safe operation during failures
• Timeout triggers automatic fallback to safe temperature settings
</pre>
-->
<div style="font-family: monospace;"></div>
<h2 style="border: 2px solid #333; padding: 10px; text-align: center; background-color: #f0f0f0;">SMART-BOILER NODE</h2>
<h3>OVERVIEW:</h3>
<p>This node is part of an intelligent heating control system consisting of three main components:</p>
<ul>
<li><strong>Smart-Scheduler:</strong> Manages multiple schedules and temperature set-points</li>
<li><strong>Smart-Valve:</strong> Controls and groups individual valves by room/zone</li>
<li><strong>Smart-Boiler:</strong> Central controller that determines optimal boiler temperature based on all connected valve states</li>
</ul>
<h3>PURPOSE:</h3>
<p>Smart-Boiler aggregates data from multiple Smart-Valve nodes to intelligently control the central heating boiler, ensuring efficient heating by prioritizing the valve with the greatest heating demand.</p>
<h3>INPUT MESSAGE FORMAT:</h3>
<pre><code>msg.payload = {
command: "set", // Required
setpoint: [number], // Set-point (target temperature) in °C
temperature: [number], // Current valve temperature in °C
requestedBy: [string], // Valve description/ID (e.g., "Valve kitchen")
groupId: [number] // Group ID
}
Example:
msg.payload = { command: "set", setpoint: 23, temperature: 20, requestedBy: "Valve kitchen", groupId: 1 }</code></pre>
<h3>VALVE PROCESSING:</h3>
<ul>
<li>Each input message creates or updates a valve entry in the internal stack</li>
<li>Valves are evaluated on each update cycle</li>
</ul>
<h3>BUSINESS RULES:</h3>
<h4>Valve Activation Condition:</h4>
<p>A valve is considered <strong>ACTIVE</strong> when: <code>sp > temp</code> (heating required)</p>
<p>The boiler is controlled by the valve with the <strong>HIGHEST</strong> temperature gap:</p>
<pre><code>gap = sp - temp</code></pre>
<h4>Target Conditions on Input:</h4>
<ul>
<li><strong>Valid input requires:</strong> <code>msg.payload.command === "set"</code> AND <code>msg.topic</code> matches configured pattern</li>
<li><strong>Message processing:</strong> Each input creates or updates a valve entry in the internal stack (liveStack)</li>
<li><strong>Stack management:</strong> Valve entries are indexed by <code>requestedBy</code> (valve name/ID)</li>
<li><strong>Cycle trigger:</strong> Every valid input triggers an evaluation cycle to determine the active valve</li>
<li><strong>Timestamp tracking:</strong> Each valve entry records <code>lastupdate</code> for timeout monitoring</li>
</ul>
<h4>Example Scenario:</h4>
<ul>
<li>Valve A: sp=19°C, temp=21°C → <strong>INACTIVE</strong> (sp < temp)</li>
<li>Valve B: sp=23°C, temp=20°C → gap=3°C (<strong>ACTIVE</strong>)</li>
<li>Valve C: sp=24°C, temp=19°C → gap=5°C (<strong>ACTIVE - CONTROLS BOILER</strong>)</li>
</ul>
<p><strong>Result:</strong> Valve C drives the boiler with sp=24°C, temp=19°C</p>
<h3>FALLBACK RULES:</h3>
<ul>
<li><strong>No active valves:</strong> The highest set-point among inactive valves is used as the boiler reference</li>
<li><strong>Input timeout:</strong> If no input received within max duration, a safety message is sent with default values to prevent continuous heating</li>
</ul>
<h3>OUTPUT MODES:</h3>
<h4>1. MQTT Output (Recommended):</h4>
<ul>
<li>Set-point topic: Publishes boiler target temperature</li>
<li>Temperature topic: Publishes current reference temperature</li>
<li>Leading device topic: Publishes name of controlling valve</li>
</ul>
<h4>2. Standard Node Output:</h4>
<pre><code>msg.payload = {
sp: [number],
temp: [number],
name: [string],
id: [string],
groupId: [number],
lastupdate: [ISO 8601 timestamp]
}</code></pre>
<h3>CONFIGURATION PARAMETERS:</h3>
<table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse; width: 100%;">
<thead>
<tr style="background-color: #e0e0e0;">
<th>Parameter</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr><td>MQTT</td><td>MQTT broker connection configuration</td></tr>
<tr><td>MQTT updates</td><td>Enable direct MQTT publishing</td></tr>
<tr><td>Output updates</td><td>Enable standard node output</td></tr>
<tr><td>Set point topic</td><td>MQTT topic for boiler set-point</td></tr>
<tr><td>Temperature topic</td><td>MQTT topic for boiler current temp</td></tr>
<tr><td>Leading Device topic</td><td>MQTT topic for active valve name</td></tr>
<tr><td>Update cycle duration</td><td>Interval for valve stack evaluation</td></tr>
<tr><td>Max duration since last input</td><td>Timeout before safety mode activation</td></tr>
<tr><td>Default temp</td><td>Safety mode default temperature</td></tr>
<tr><td>Default set point</td><td>Safety mode default set-point</td></tr>
</tbody>
</table>
<h3>SAFETY FEATURES:</h3>
<ul>
<li>Watchdog timer prevents endless heating if valve communication fails</li>
<li>Configurable default values ensure safe operation during failures</li>
<li>Timeout triggers automatic fallback to safe temperature settings</li>
<li><strong>State Persistence:</strong> The node automatically saves the complete valve stack and boiler state to <code>~/.node-red/.node-red-state/boiler-{node-id}.json</code>. This includes all connected valves, their temperatures, setpoints, and the active valve selection. State is restored on Node-RED restarts and flow redeployments, ensuring seamless operation continuity.</li>
</ul>
</div>
</script>