node-red-contrib-vib-smart-valve
Version:
Smart Valve Managemeent
534 lines (454 loc) • 24.6 kB
HTML
<script type="text/javascript">
// Terminal command node-red -v -D logging.console.level=trace
RED.nodes.registerType('smart-valve',{
category: 'vib-node',
color: '#bdeeff',
defaults: {
name: {value:"",required:true},
mqttSettings: {value: "", type: "smart-valve-settings"},
temperatureSensorStateTopic: {value:"",required: true},
temperatureSensorKey: {value:"",required: true},
valveSetpointStateTopic: {value:"",required: true},
valveSetpointSetTopic: {value:"",required: true},
valveSetpointKey: {value:"",required: true},
valveTemperatureSetTopic: {value:"",required: true},
valveTemperatureKey: {value:"",required: true},
valveModeStateTopic: {value:"",required: true},
valveModeSetTopic: {value:"",required: true},
valveModeKey: {value:"",required: true},
OverrideDuration: {value:"60",required: true,validate:function(v) {
if (isNaN(v)){
return false;
}
if (v<1 || v> 1440){
return false;
}
return true;
}},
valveTemperatureSensorTypeSetTopic: {value:"",required: false},
valveTemperatureSensorTypePayload: {value:"",required: false},
msgStormMaxMsg:{value:"20",required: true,validate:function(v) {
if (isNaN(v)){
return false;
}
if (v<1 || v> 100){
return false;
}
return true;
}},
groupId:{value:"1",required: true,validate:function(v) {
if (isNaN(v)){
return false;
}
if (v<0 || v> 100){
return false;
}
return true;
}},
cycleDuration: {value:"5"},
offSp: {value:"5",required:true,validate:function(v){
if (isNaN(v)){
return false;
}
if (v<0 || v> 30){
return false;
}
return true;
}},
spUpdateMode:{value:"spUpdateMode.statechange.startup"},
adjustValveTempMode:{value:"adjustValveTempMode.noAdjust"},
debugInfo:{value:false},
allowOverride:{value:false},
},
inputs:1,
outputs:1,
outputLabels: ["Boiler/Scheduler"],
icon: "font-awesome/fa-magnet",
button: {
onclick: function() {
let node=this;
$.ajax({
url: "smartvalve/" + node.id,
type: "POST",
data: JSON.stringify({payload:{command:"trigger"}}),
contentType: "application/json; charset=utf-8",
success: function (resp) {
RED.notify(node._("smart-valve 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-valve";
},
oneditprepare: function() {
const node = this;
},
oneditsave: function() {
const node = this;
},
oneditresize: function() {
}
});
</script>
<script type="text/html" data-template-name="smart-valve">
<style>
ol#node-input-climate-container .red-ui-typedInput-container {
flex:1;
}
</style>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"> </i>Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-mqttSettings"><i class="fa fa-globe"> </i>MQTT</label>
<input type="text" id="node-input-mqttSettings"></input>
</div>
<div class="form-row">
<label for="node-input-groupId"><i class="fa fa-tasks"></i>Group id</label>
<input type="text" id="node-input-groupId" placeholder="id"></input>
</div>
<div class="form-row">
<span><i class="fa fa-tasks"> </i>Temperature sensor</span>
</div>
<div class="form-row">
<label for="node-input-temperatureSensorTopic">State</label>
<input type="text" id="node-input-temperatureSensorStateTopic" placeholder="mqtt topic"></input>
</div>
<div class="form-row">
<label for="node-input-temperatureSensorKey">key</label>
<input type="text" id="node-input-temperatureSensorKey" placeholder="mqtt key"></input>
</div>
<div class="form-row">
<span><i class="fa fa-tasks"> </i>Valve set point</span>
</div>
<div class="form-row">
<label for="node-input-valveSetpointTopic">State</label>
<input type="text" id="node-input-valveSetpointStateTopic" placeholder="mqtt topic"></input>
</div>
<div class="form-row">
<label for="node-input-valveSetpointTopic">Set</label>
<input type="text" id="node-input-valveSetpointSetTopic" placeholder="mqtt topic"></input>
</div>
<div class="form-row">
<label for="node-input-valveSetpointKey">key</label>
<input type="text" id="node-input-valveSetpointKey" placeholder="mqtt key"></input>
</div>
<div class="form-row">
<span><i class="fa fa-tasks"> </i>Valve temperature</span>
</div>
<div class="form-row">
<label for="node-input-valveTemperatureTopic">Set</label>
<input type="text" id="node-input-valveTemperatureSetTopic" placeholder="mqtt topic"></input>
</div>
<div class="form-row">
<label for="node-input-valveTemperatureKey">key</label>
<input type="text" id="node-input-valveTemperatureKey" placeholder="mqtt key"></input>
</div>
<div class="form-row">
<span><i class="fa fa-tasks"> </i>Valve Sensor Internal/External</span>
</div>
<div class="form-row">
<label for="node-input-valveTemperatureSensorTypeSetTopic">Set</label>
<input type="text" id="node-input-valveTemperatureSensorTypeSetTopic" placeholder="mqtt topic"></input>
</div>
<div class="form-row">
<label for="node-input-valveTemperaturePayload">payload</label>
<input type="text" id="node-input-valveTemperatureSensorTypePayload" placeholder="mqtt payload"></input>
</div>
<div class="form-row">
<span><i class="fa fa-tasks"> </i>Valve mode</span>
</div>
<div class="form-row">
<label for="node-input-valveModeStateTopic">State</label>
<input type="text" id="node-input-valveModeStateTopic" placeholder="mqtt topic"></input>
</div>
<div class="form-row">
<label for="node-input-valveModeSetTopic">Set</label>
<input type="text" id="node-input-valveModeSetTopic" placeholder="mqtt topic"></input>
</div>
<div class="form-row">
<label for="node-input-valveModeKey">key</label>
<input type="text" id="node-input-valveModeKey" placeholder="mqtt key"></input>
</div>
<div class="form-row">
<label for="node-input-offSp"><i class="fa fa-tasks"> </i> Off setpoint</label>
<input type="text" id="node-input-offSp" placeholder="temperature"></input>
<label for="node-input-offSp">°C</label>
</div>
<div class="form-row">
<span><i class="fa fa-tasks"> </i>Update</span>
</div>
<div class="form-row">
<label for="node-input-spUpdateMode"><i class="fa fa-play"> </i>Mode </label>
<select id="node-input-spUpdateMode">
<option value="spUpdateMode.statechange">when state changes</option>
<option value="spUpdateMode.statechange.startup">when state changes + startup</option>
<option value="spUpdateMode.cycle">every cycle</option>
</select>
</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"></input> min
</div>
<div class="form-row">
<label for="node-input-allowOverride"><i class="fa fa-chain-broken"> </i>Manual valve</label>
<input type="checkbox" title="allow override mode" id="node-input-allowOverride"></input>
</div>
<div class="form-row">
<label for="node-input-OverrideDuration"><i class="fa fa-chain-broken"> </i>Manual </label>
<input type="text" title="storm Message limit" style="width:60px !important; text-align: right;" id="node-input-OverrideDuration"></input>
<span>min duration</span>
</div>
<div class="form-row">
<label for="node-input-msgStormMaxMsg"><i class="fa fa-chain-broken"> </i>Storm</label>
<input type="text" title="storm Message limit" style="width:60px !important; text-align: right;" id="node-input-msgStormMaxMsg"></input>
<span>msg limit / 10s</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-valve">
<!--
╔══════════════════════════════════════════════════════════════════════════════╗
║ SMART-VALVE NODE ║
╚══════════════════════════════════════════════════════════════════════════════╝
Part of a heating control suite of nodes:
- Smart-Scheduler: Multi-zone smart scheduler
- Smart-Valve: Valve grouping, auto-calibration, manual override
- Smart-Boiler: Boiler OpenTherm, multi-valve management
OVERVIEW:
---------
This node manages multiple valves (climate entities) in a room as a single unit.
FEATURES:
---------
- External temperature sensor support
- Multiple valve synchronization
- TRV temperature recalibration based on external sensor
- Manual override support with automatic propagation to other valves and scheduler
INPUTS:
-------
- payload (string): "trigger" - Trigger an update
- sp (number): 0-35 - Set point temperature in °C
OUTPUTS:
--------
1. Home Assistant updates - Updates climate entities via service calls
2. Smart-Boiler/Scheduler - Sends set point to smart-boiler or override messages to smart-scheduler
CONFIGURATION:
--------------
Basic Settings:
- Name (string): Node name and group identifier for smart-boiler
- Mqtt: MQTT settings configuration node
- Group Id (number): 0-100 - Unique identifier for this valve group
- Temperature topic (string): MQTT topic for external temperature sensor
- Off setpoint (number): 0-30°C - Temperature threshold for "off" state
Climate Entities:
Add one or more climate entities with the following fields:
- Climate (string): Home Assistant climate entity (e.g., climate.kitchen)
- Ext temp topic (string): MQTT topic for valve external temperature
- Entity Topic (string): MQTT topic for climate entity state
- SetPoint Topic (string): MQTT topic for set point updates
Update Behavior:
- Update mode:
- when state changes - Update only on state changes
- when state changes + startup - Update on state changes and node startup
- every cycle - Update at regular intervals
- Update cycle (number): Duration in minutes between cycles (default: 5)
- Allow manual update (checkbox): Enable manual set point changes on valves
- When enabled, changing one valve updates all others and sends override to scheduler
Calibration:
- Recalibration:
- No - Disable recalibration
- Yes - Enable TRV calibration based on external temperature sensor
Advanced:
- Debug info (checkbox): Output debug information to Node-RED console
OPERATION:
----------
Initialization (Step 0):
- Group set point is initialized from valve states
Cycle Execution:
1. Manual Override Detection: Check for manual valve adjustments, propagate to all valves in group
2. Recalibration: Adjust TRV temperatures based on external sensor (if enabled)
3. Output: Send updates to smart-boiler based on configured update mode
On Input Message:
- Updates requested set point on all valves in the group
- Sends output message to smart-boiler/scheduler
NOTES:
------
- Each valve group requires a unique Group Id
- External temperature sensor improves accuracy for multi-valve rooms
- Manual override allows integration with physical valve controls
-->
<div style="font-family: monospace;"></div>
<h2 style="border: 2px solid #333; padding: 10px; text-align: center; background-color: #f0f0f0;">SMART-VALVE 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>This node manages multiple valves (climate entities) in a room as a single unit. It handles synchronization, external temperature sensor integration, and manual overrides.</p>
<p> a nodered JSON file with examples is available in the project example folder</p>
<h3>FEATURES:</h3>
<ul>
<li>External temperature sensor support</li>
<li>Multiple valve synchronization</li>
<li>Full MQTT & Home assistant integration</li>
<li>Loop message storm protection</li>
<li>Manual override support with automatic propagation to other valves and scheduler</li>
</ul>
<h3>INPUT MESSAGE FORMAT:</h3>
<pre><code></code> msg.payload = {
command: [number], [1|set|on|0|off|trigger|override] // Input message command
setpoint: [number], // Requested temperature set point
temperature: [number], // Current valve temperature
requestedBy: [string], // Command requester (in case of override valve name)
groupId: [number], // Unique valve identifier
noOut: [boolen] // trigger after evaluation output message ?
}</code></pre>
<br>
<h5> MESSAGE INPUT/OUTPUT PARAM DESCRIPTION</h5>
<table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse; width: 100%;">
<thead>
<tr style="background-color: #e0e0e0;">
<th>Parameter</th>
<th>Type</th>
<th>Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Command</td>
<td>[string],mandatory</td>
<td>[1|set|on|0|off|trigger|override] </td>
<td>
<ul>
<li><strong>"1"</strong>: equivalent to "trigger", will generate a new valve cycle (only if internal node variable <b>executionMode</b> is true)</li>
<li><strong>"trigger"</strong>: same as "1"</li>
<li><strong>"set"</strong>: trigger a new valve cycle and set valve / boiler temperature set point </li>
<li><strong>"on"</strong>: trigger a new valve cycle and change executionMode to true, it enables the valve </li>
<li><strong>"off"</strong>: set executionMoe to false, and disable the valve </li>
<li><strong>"0"</strong>: same as "off" to disable the valve node</li>
<li><strong>"override"</strong>: is used to force the valve / scheduler to override the current settings of the set point and share this valve/sechduler of the same groupId</li>
</ul>
</td>
</tr>
<tr>
<td>setpoint</td>
<td>[number],mandatory</td>
<td>[0,40]</td>
<td>set point temperature for input and output message</td>
</tr>
<tr>
<td>temperature</td>
<td>[number],[optionnal]</td>
<td>[0,40]</td>
<td>set the current temperature of the valve (only within "set" and "override" command messages)</td>
</tr>
<tr>
<td>requestedBy</td>
<td>[string],[optional]</td>
<td>n/a</td>
<td>Unique id/name of the command requester, this parameter is used in input for override message and in output for the boiler</td>
</tr>
<tr>
<td>groupId</td>
<td>[number],mandatory</td>
<td>any</td>
<td>Groupid of valve regrouped in the same location or linked together, ex: 3 valves in the same room should have the same groupId to be updated together </td>
</tr>
<tr>
<td>noOut</td>
<td>[boolean],[optionnal]</td>
<td>true/false</td>
<td>enable/disable output message, this is useful in case of override message to avoid message loop storm</td>
</tr>
</tbody>
</table>
<h3>OUTPUTS:</h3>
<ol>
<li><strong>MQTT valve updates:</strong> Updates climate entities via service calls</li>
<li><strong>Valve group</strong> override messages to valve in the sames group with the same GroupId</li>
<li><strong>Smart-Boiler/Scheduler:</strong> Sends set point to smart-boiler or override messages to smart-scheduler</li>
</ol>
<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>Name</td><td>Unique identifier of the valve, and used for the boiler to build an array of unique entries</td></tr>
<tr><td>MQTT</td><td>MQTT broker connection configuration</td></tr>
<tr><td>Group Id</td><td>Unique identifier (0-100) for this valve group</td></tr>
<tr><td>Temperature sensor</td><td>MQTT topic for external temperature sensor, this can be set to a specific valve temperature sensor as a master or using an external sensor for more accuracy, the [topic] is the MQTT topic, [key] is the value template.</td></tr>
<tr><td>Valve set point</td><td>MQTT topics to read and write setpoint,[state] is to read the set point value with value template, [set] is to write the set point, [key] is the value template.</td></tr>
<tr><td>Valve mode</td><td>Current mode of the valve [heat|off], this is linked with home assistant climate current mode</td></tr>
<tr><td>Valve Sensor Internal/External</td><td>Some valve manufacturer requires a specific MQTT message to use external sensor (such as aqara), this enable a MQTT topic with a specific payload</td></tr>
<tr><td>Off setpoint</td><td>Temperature threshold (0-35°C) for "off" state</td></tr>
<tr><td>Update mode</td><td>Trigger condition for updates (state change, startup, cycle)</td></tr>
<tr><td>Update cycle</td><td>Duration in minutes between cycles</td></tr>
<tr><td>Manual update</td><td>Enable manual set point changes on valves</td></tr>
<tr><td>Msg Storm</td><td>Protection to avoid loop of message from / to other valves or MQTT updates</td></tr>
<tr><td>Debug info</td><td>Output debug information to Node-RED console</td></tr>
</tbody>
</table>
<h3>OPERATION:</h3>
<h4>Initialization:</h4>
<p>By default the valve is <b>not ready</b> and needs to be initialized with valve current temperature and set point.
The current temperature is coming either from MQTT or INPUT messages, set point can be coming either from MQTT or INPUT message </p>
<h4>Cycle Execution:</h4>
<ol>
<li><strong>Manual Override Detection:</strong> Check for manual valve adjustments, propagate to all valves in group</li>
<li><strong>Output:</strong> Send updates to smart-boiler based on configured update mode</li>
</ol>
<h4>On Input Message:</h4>
<ul>
<li>Updates requested set point on all valves in the group</li>
<li>Sends output message to smart-boiler/scheduler/valve set/overrride</li>
</ul>
<h3>BUSINESS RULES:</h3>
<h4>Valve Activation Condition:</h4>
<p>A valve is considered <strong>ACTIVE</strong> (heating required) when:</p>
<pre><code>setpoint > temperature</code></pre>
<p>This means heating is requested when the target temperature exceeds the current room temperature.</p>
<h4>Target Conditions on Input:</h4>
<ul>
<li><strong>Command "set" or "on":</strong> Valve processes input if <code>executionMode = true</code></li>
<li><strong>Command "trigger" or "1":</strong> Only triggers evaluation if <code>executionMode = true</code></li>
<li><strong>Command "override":</strong> Always processed regardless of executionMode, forces setpoint update across all valves in group</li>
<li><strong>Command "off" or "0":</strong> Sets <code>executionMode = false</code>, disables valve operation</li>
</ul>
<h3>NOTES:</h3>
<ul>
<li>Each valve group requires a unique Group Id</li>
<li>External temperature sensor improves accuracy for multi-valve rooms</li>
<li>Manual override allows integration with physical valve controls</li>
<li><strong>State Persistence:</strong> The node automatically saves setpoint and temperature values to <code>~/.node-red/.node-red-state/valve-{node-id}.json</code>. This state is restored on Node-RED restarts and flow redeployments, ensuring valve settings persist across all system changes.</li>
</ul>
</script>