node-red-contrib-vib-smart-valve
Version:
Smart Valve Managemeent
333 lines (278 loc) • 16.4 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:""},
topic:{value:""},
mqttSettings: {value: "", type: "smart-valve-settings"},
tempEntity: {value:"",required: true},
tempEntityTopic: {value:"",required: true},
groupId:{value:"1",required: true,validate:function(v) {
if (isNaN(v)){
return false;
}
if (v<0 || v> 100){
return false;
}
return true;
}},
climates:{value:[{entity:"",valveExtTempTopic:""}],validate: function(rules, opt) {
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:2,
outputLabels: ["HomeAssistant Service","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 sucess", { 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() {
var node = this;
if (!this.climates) {
var climate = {
entity:"",
entityTopic:"",
valveExtTempTopic:"",
valveSpTopic:""
};
this.climates = [climate];
}
$('#node-input-climate-container').css('min-height','150px').css('min-width','400px').editableList({
removable: true,
sortable: false,
addItem: function(container,i,opt) {
var climate = opt;
if ( typeof node.climates[i] === 'undefined'){
node.climates[i]={};
}
node.climates[i].ruleIdx=i;
if (!climate.hasOwnProperty('entity')) {
climate = {entity:"",valveExtTempTopic:"",entityTopic:"",valveSpTopic:""};
}
$(container).data('data',i);
let fragment = document.createDocumentFragment();
var row1 = $('<div/>',{style:"display:flex; align-items: baseline"}).appendTo(fragment);
var row2 = $('<div/>',{style:"margin-top:8px;"}).appendTo(fragment);
var row3 = $('<div/>',{style:"margin-top:8px;"}).appendTo(fragment);
var row4 = $('<div/>',{style:"margin-top:8px;"}).appendTo(fragment);
$('<label style="width:20px !important; text-align: left;"><span><i class="fa fa-share-alt"/><b>'+(i+1)+'</b></span></label>').appendTo(row1);
$('<label style="text-align: right; margin-right:5px;"><span>Climate:</span></label>').appendTo(row1);
$('<input type="text" id="node-input-climateEntity_'+i+'" idx="'+i+'" class="node-input-climateEntity" placeholder="Climate entity" >').appendTo(row1);
var row2_1 = $('<div/>', {style:"display:flex;align-items: baseline"}).appendTo(row2);
$('<label style="width:20px !important; text-align: left;"><span></span></label>').appendTo(row2_1);
$('<label style="text-align: right; margin-right:5px;"><span>Ext temp topic:</span></label>').appendTo(row2_1);
$('<input type="text" id="node-input-valveExtTempTopic_'+i+'" idx="'+i+'" class="node-input-valveExtTempTopic" placeholder="Valve Ext temp topic" >').appendTo(row2_1);
var row3_1 = $('<div/>', {style:"display:flex;align-items: baseline"}).appendTo(row3);
$('<label style="width:20px !important; text-align: left;"><span><i class="fa fa-share-alt"/><b>'+(i+1)+'</b></span></label>').appendTo(row3_1);
$('<label style="text-align: right; margin-right:5px;"><span>Entity Topic:</span></label>').appendTo(row3_1);
$('<input type="text" id="node-input-climateEntityTopic_'+i+'" idx="'+i+'" class="node-input-climateEntityTopic" placeholder="Entity topic" >').appendTo(row3_1);
var row4_1 = $('<div/>', {style:"display:flex;align-items: baseline"}).appendTo(row4);
$('<label style="width:20px !important; text-align: left;"><span><i class="fa fa-share-alt"/><b>'+(i+1)+'</b></span></label>').appendTo(row4_1);
$('<label style="text-align: right; margin-right:5px;"><span>SetPoint Topic:</span></label>').appendTo(row4_1);
$('<input type="text" id="node-input-valveSpTopic_'+i+'" idx="'+i+'" class="node-input-valveSpTopic" placeholder="Sp topic" >').appendTo(row4_1);
container[0].appendChild(fragment);
$('#node-input-climateEntity_'+i).val(climate.entity);
$('#node-input-climateEntityTopic_'+i).val(climate.entityTopic);
$('#node-input-valveExtTempTopic_'+i).val(climate.valveExtTempTopic);
$('#node-input-valveSpTopic_'+i).val(climate.valveSpTopic);
$('#node-input-climateEntity_'+i).keyup(function (event) {
ctrl_i=$(event.target).attr('idx');
node.climates[ctrl_i].entity=$('#node-input-climateEntity_'+ctrl_i).val();
});
$('#node-input-valveSpTopic_'+i).keyup(function (event) {
ctrl_i=$(event.target).attr('idx');
node.climates[ctrl_i].valveSpTopic=$('#node-input-valveSpTopic_'+ctrl_i).val();
});
$('#node-input-climateEntityTopic_'+i).keyup(function (event) {
ctrl_i=$(event.target).attr('idx');
node.climates[ctrl_i].entityTopic=$('#node-input-climateEntityTopic_'+ctrl_i).val();
});
$('#node-input-valveExtTempTopic_'+i).keyup(function (event) {
ctrl_i=$(event.target).attr('idx');
node.climates[ctrl_i].valveExtTempTopic=$('#node-input-valveExtTempTopic_'+ctrl_i).val();
});
},
removeItem:function(data) {
},
resizeItem: function(row,index) {
var originalData = $(row).data('data');
console.log("Resize the row for item:", originalData)
},
resize: function() {
}
});
for (var i=0; i<this.climates.length; i++) {
var climate = this.climates[i];
$("#node-input-climate-container").editableList('addItem',climate);
}
},
oneditsave: function() {
var node = this;
var climates = $("#node-input-climate-container").editableList('items');
node.climates= [];
climates.each(function(i){
var climate = $(this);
var r = {
entity:climate.find(".node-input-climateEntity").val(),
valveExtTempTopic:climate.find(".node-input-valveExtTempTopic").val(),
valveSpTopic:climate.find(".node-input-valveSpTopic").val(),
entityTopic:climate.find(".node-input-climateEntityTopic").val(),
}
node.climates.push(r);
});
console.log(node.climates);
},
oneditresize: function() {
}
});
</script>
<script type="text/html" data-template-name="smart-valve">
<style>
ol#node-input-rule-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-topic"><i class="fa fa-tasks"></i> Topic</label>
<input type="text" id="node-input-topic" placeholder="Topic"></input>
</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-topic"><i class="fa fa-tasks"></i> Group id </label>
<input type="text" id="node-input-groupId" placeholder="id"></input>
</div>
<div class="form-row">
<label for="node-input-tempEntity"><i class="fa fa-tasks"></i> Temperature</label>
<input type="text" id="node-input-tempEntity" placeholder="Temperature entity"></input>
</div>
<div class="form-row">
<label for="node-input-tempEntityTopic"><i class="fa fa-tasks"></i> Temperature topic</label>
<input type="text" id="node-input-tempEntityTopic" placeholder="Temperature topic"></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" style="width:60px !important; text-align: right;" placeholder="temperature"></input>
<label for="node-input-offSp">°C</label>
</div>
<div class="form-row node-input-climate-container-row">
<ol id="node-input-climate-container"></ol>
</div>
<div class="form-row">
<label style="width:120px !important;" for="node-input-spUpdateMode"><i class="fa fa-play"></i> Update 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 style="width:120px !important;" for="node-input-cycleDuration"><i class="fa fa-tasks"></i> Update cycle</label>
<input type="text" id="node-input-cycleDuration" style="width:60px !important; text-align: right;" placeholder="duration"></input>
</div>
<div class="form-row">
<label for="node-input-allowOverride"><i class="fa fa-chain-broken"></i> Allow manual</label>
<span><input type="checkbox" title="allow override mode" id="node-input-allowOverride"></input></span>
<label for="node-input-allowOverride2">update</label>
</div>
<div class="form-row">
<label style="width:120px !important;" for="node-input-adjustValveTempMode"><i class="fa fa-play"></i> Recalibration</label>
<select id="node-input-adjustValveTempMode">
<option value="adjustValveTempMode.noAdjust">No</option>
<option value="adjustValveTempMode.adjust.startup">Yes</option>
</select>
</div>
<div class="form-row">
<label style="width:120px !important;" for="node-input-debugInfo"><i class="fa fa-tasks"></i> Debug</label>
<input type="checkbox" title="send debug information to the console" id="node-input-debugInfo" ></input>
<label style="width:120px !important;" for="node-input-debugInfo">info</label>
</div>
</script>
<script type="text/markdown" data-help-name="smart-valve">
# Smart-valve is part of a suite of node
- Smart-Scheduler: multi-zonning SmartScheduler,
- Smart-Valve: Valve grouping, auto-calibration, manual override,
- Smart-Boiler: Boiler OpenTherm, multi valve management.
## Smart-valve
This node enables to manage multiple valve (climate) in a same room like one.
It support the following features :
- External temperature sensor,
- Multiple valves updates,
- TRV temperture Recalibration based on the external temperature sensor,
- Manual update directly on the valve to trigger override message to the scheduler and update the other valve
### Inputs
: payload (string):[1|on|trigger]
: sp (integer): [0-35]
## Outputs
1. Update home assistant via call service
2. Update of SP to the boiler (smart-Boiler) or override message to the smart-scheduler
### Settings
- Name: [string], name of the node and also the name of the group sent to the smart-boiler node
- Topic: [string], not used,
- Group Id: [integer], used by the smart-boiler node to identify this group of valves, need to be unique
- Temperature: [string], is the name of the external temperature sensor entity in home assistant ex: sensor.temp9
- Update mode: [state changed|state changed+startup|every cycle], define how frequently updates are sent to the smart-boiler node
- Update cycle: [integer], duration in minute between two cycle. default is 5
- Allow manual updates: [true|false], enable direct set point (target temperature) change on the valve or home assistant. If true when a valve set point is changed all the other valves are updated and a override message is sent to the smart-scheduler node.
- Recalibration: [No|Yes|Yes+threshold], enable to adjust the valve (TRV) current temperature based on the external temperature sensor,
- Delta threshold: [integer] [0-9], threshold delta between external temperature sensor and the TRV current temperature to trigger recalibration,
- Debug: [true|false], send debug info to the node-red console
- Climate: each valve entry has 2 field:
- climate: [string], home assitant climate entity of the valve ex: climate.kitchen
- calibration: [string], home assistant calibration entity of the valve ex: number.kitchen_calibration
### Execution rules
- Step 0: update current group setpoint with the sp of the valve,
- Cycle :
Step 1: identify if manual updates on valve, then update all valves,
Step 2: check and execute recalibration
Step 3: based on the execution mode, output to the smart-boiler node
- On input:
Update the requested set-point on each valves