@bldgblocks/node-red-contrib-control 0.1.37 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +38 -5
- package/README.md +10 -0
- package/nodes/accumulate-block.html +3 -3
- package/nodes/alarm-collector.html +11 -0
- package/nodes/alarm-collector.js +31 -5
- package/nodes/alarm-config.html +11 -6
- package/nodes/alarm-config.js +34 -35
- package/nodes/alarm-service.js +2 -2
- package/nodes/and-block.js +1 -1
- package/nodes/boolean-switch-block.html +27 -14
- package/nodes/boolean-switch-block.js +22 -12
- package/nodes/call-status-block.html +83 -56
- package/nodes/call-status-block.js +335 -248
- package/nodes/changeover-block.html +30 -31
- package/nodes/changeover-block.js +287 -389
- package/nodes/contextual-label-block.js +3 -3
- package/nodes/delay-block.js +74 -13
- package/nodes/global-getter.html +173 -12
- package/nodes/global-getter.js +29 -14
- package/nodes/global-setter.html +37 -0
- package/nodes/global-setter.js +96 -14
- package/nodes/history-buffer.js +32 -27
- package/nodes/history-collector.html +3 -1
- package/nodes/history-collector.js +4 -4
- package/nodes/history-config.html +8 -2
- package/nodes/network-point-read.js +6 -1
- package/nodes/network-point-register.html +1 -1
- package/nodes/network-service-bridge.js +43 -11
- package/nodes/network-service-registry.html +236 -27
- package/nodes/network-service-registry.js +1 -1
- package/nodes/or-block.js +1 -1
- package/nodes/priority-block.js +1 -1
- package/nodes/tstat-block.html +34 -79
- package/nodes/tstat-block.js +223 -345
- package/nodes/utils.js +1 -1
- package/package.json +90 -75
|
@@ -8,19 +8,38 @@
|
|
|
8
8
|
<p>This node maintains the mapping between Network Point IDs (integers) and Global Variables.</p>
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
|
-
<div class="form-row">
|
|
11
|
+
<div class="form-row" style="margin-bottom:4px;">
|
|
12
12
|
<label><i class="fa fa-list-ul"></i> Points</label>
|
|
13
|
+
<div style="display:inline-block; vertical-align:top;">
|
|
14
|
+
<button type="button" id="node-input-renumber-btn" class="editor-button editor-button-small" title="Renumber selected points sequentially">
|
|
15
|
+
<i class="fa fa-sort-numeric-asc"></i> Renumber...
|
|
16
|
+
</button>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="form-row">
|
|
13
20
|
<div id="node-input-point-list-div"
|
|
14
21
|
style="
|
|
15
22
|
border:1px solid #ccc;
|
|
16
|
-
height:
|
|
17
|
-
overflow-y:auto;
|
|
23
|
+
height:300px;
|
|
24
|
+
overflow-y:auto;
|
|
18
25
|
padding:5px;
|
|
19
26
|
box-sizing:border-box;">
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
<table id="node-input-point-table" style="width:100%;border-collapse:collapse;font-size:0.9em;">
|
|
28
|
+
<thead>
|
|
29
|
+
<tr style="border-bottom:2px solid #ccc;text-align:left;">
|
|
30
|
+
<th style="padding:4px;width:28px;"><input type="checkbox" id="node-input-select-all" title="Select all"></th>
|
|
31
|
+
<th style="padding:4px;width:70px;">ID</th>
|
|
32
|
+
<th style="padding:4px;">Path</th>
|
|
33
|
+
<th style="padding:4px;width:30px;"></th>
|
|
34
|
+
</tr>
|
|
35
|
+
</thead>
|
|
36
|
+
<tbody id="node-input-point-list"></tbody>
|
|
37
|
+
</table>
|
|
22
38
|
</div>
|
|
23
39
|
</div>
|
|
40
|
+
<div class="form-row">
|
|
41
|
+
<span id="node-input-point-status" style="font-size:0.85em;color:#666;"></span>
|
|
42
|
+
</div>
|
|
24
43
|
</script>
|
|
25
44
|
|
|
26
45
|
<script type="text/javascript">
|
|
@@ -33,44 +52,227 @@
|
|
|
33
52
|
return this.name || "Point Registry";
|
|
34
53
|
},
|
|
35
54
|
oneditprepare: function() {
|
|
36
|
-
const
|
|
37
|
-
const $
|
|
55
|
+
const configNode = this;
|
|
56
|
+
const $tbody = $("#node-input-point-list");
|
|
57
|
+
const $status = $("#node-input-point-status");
|
|
58
|
+
const $selectAll = $("#node-input-select-all");
|
|
38
59
|
|
|
60
|
+
// Track pending changes: { editorNodeId: newPointId }
|
|
61
|
+
const pendingChanges = {};
|
|
62
|
+
|
|
63
|
+
// ============================================================
|
|
64
|
+
// Load deployed points + merge in any editor-only register nodes
|
|
65
|
+
// ============================================================
|
|
39
66
|
function loadPoints() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
67
|
+
// Gather all register nodes in the editor that reference this registry
|
|
68
|
+
const editorNodes = {};
|
|
69
|
+
RED.nodes.eachNode(function(n) {
|
|
70
|
+
if ((n.type === "network-point-register" || n.type === "network-register") && n.registry === configNode.id) {
|
|
71
|
+
editorNodes[n.id] = {
|
|
72
|
+
nodeId: n.id,
|
|
73
|
+
id: parseInt(n.pointId),
|
|
74
|
+
path: null, // will be filled from deployed data if available
|
|
75
|
+
store: null,
|
|
76
|
+
writable: !!n.writable,
|
|
77
|
+
editorName: n.name || ""
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
$.getJSON(`network-point-registry/list/${configNode.id}`, function(deployedData) {
|
|
83
|
+
// Merge deployed data into editor nodes (deployed has runtime path/store)
|
|
84
|
+
deployedData.forEach(function(pt) {
|
|
85
|
+
if (editorNodes[pt.nodeId]) {
|
|
86
|
+
editorNodes[pt.nodeId].path = pt.path;
|
|
87
|
+
editorNodes[pt.nodeId].store = pt.store;
|
|
88
|
+
} else {
|
|
89
|
+
// Deployed but not in editor (unusual — maybe just deleted)
|
|
90
|
+
editorNodes[pt.nodeId] = pt;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
43
93
|
|
|
44
|
-
|
|
45
|
-
|
|
94
|
+
renderTable(Object.values(editorNodes));
|
|
95
|
+
}).fail(function() {
|
|
96
|
+
// Fallback: render from editor nodes only
|
|
97
|
+
renderTable(Object.values(editorNodes));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
46
100
|
|
|
47
|
-
|
|
48
|
-
|
|
101
|
+
// ============================================================
|
|
102
|
+
// Render the editable table
|
|
103
|
+
// ============================================================
|
|
104
|
+
function renderTable(points) {
|
|
105
|
+
$tbody.empty();
|
|
106
|
+
$selectAll.prop('checked', false);
|
|
49
107
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
108
|
+
if (!points.length) {
|
|
109
|
+
$tbody.append('<tr><td colspan="4" style="padding:8px;color:#999;">No points defined (deploy first if newly added)</td></tr>');
|
|
110
|
+
$status.text("");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
54
113
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
114
|
+
points.sort((a, b) => (a.id || 0) - (b.id || 0));
|
|
115
|
+
|
|
116
|
+
points.forEach(function(pt) {
|
|
117
|
+
const $tr = $('<tr>').css({ borderBottom: '1px solid #eee' }).attr('data-node-id', pt.nodeId);
|
|
118
|
+
|
|
119
|
+
// Checkbox
|
|
120
|
+
const $cb = $('<input type="checkbox" class="point-select">');
|
|
121
|
+
const $tdCb = $('<td>').css({ padding: '3px 4px' }).append($cb);
|
|
122
|
+
|
|
123
|
+
// Editable ID input
|
|
124
|
+
const $idInput = $('<input type="number" class="point-id-input">')
|
|
125
|
+
.val(pt.id)
|
|
126
|
+
.attr('data-original-id', pt.id)
|
|
127
|
+
.css({ width: '55px', padding: '2px 4px', textAlign: 'right' });
|
|
128
|
+
|
|
129
|
+
$idInput.on('change input', function() {
|
|
130
|
+
const nid = pt.nodeId;
|
|
131
|
+
const newVal = parseInt($(this).val());
|
|
132
|
+
const origVal = parseInt($(this).attr('data-original-id'));
|
|
133
|
+
|
|
134
|
+
if (!isNaN(newVal) && newVal !== origVal) {
|
|
135
|
+
pendingChanges[nid] = newVal;
|
|
136
|
+
} else {
|
|
137
|
+
delete pendingChanges[nid];
|
|
138
|
+
}
|
|
139
|
+
validateIds();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const $tdId = $('<td>').css({ padding: '3px 4px' }).append($idInput);
|
|
143
|
+
|
|
144
|
+
// Path display
|
|
145
|
+
const displayPath = pt.path && pt.path !== "not ready" ? pt.path : (pt.editorName || 'not deployed');
|
|
146
|
+
const $tdPath = $('<td>').css({ padding: '3px 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '200px' })
|
|
147
|
+
.attr('title', displayPath)
|
|
148
|
+
.text(displayPath);
|
|
149
|
+
|
|
150
|
+
// Reveal button
|
|
151
|
+
const $btn = $('<button type="button" class="editor-button editor-button-small">')
|
|
152
|
+
.attr('title', 'Find node on canvas')
|
|
58
153
|
.html('<i class="fa fa-search"></i>')
|
|
59
|
-
.on('click', function
|
|
154
|
+
.on('click', function(e) {
|
|
60
155
|
e.stopPropagation();
|
|
61
|
-
|
|
62
156
|
RED.view.reveal(pt.nodeId);
|
|
63
157
|
});
|
|
158
|
+
const $tdBtn = $('<td>').css({ padding: '3px 4px' }).append($btn);
|
|
64
159
|
|
|
65
|
-
|
|
66
|
-
$
|
|
160
|
+
$tr.append($tdCb, $tdId, $tdPath, $tdBtn);
|
|
161
|
+
$tbody.append($tr);
|
|
67
162
|
});
|
|
68
|
-
|
|
69
|
-
|
|
163
|
+
|
|
164
|
+
$status.text(points.length + " point" + (points.length !== 1 ? "s" : ""));
|
|
165
|
+
validateIds();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ============================================================
|
|
169
|
+
// Validate all IDs — highlight duplicates
|
|
170
|
+
// ============================================================
|
|
171
|
+
function validateIds() {
|
|
172
|
+
const idCounts = {};
|
|
173
|
+
$tbody.find('.point-id-input').each(function() {
|
|
174
|
+
const val = parseInt($(this).val());
|
|
175
|
+
if (!isNaN(val)) {
|
|
176
|
+
idCounts[val] = (idCounts[val] || 0) + 1;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
$tbody.find('.point-id-input').each(function() {
|
|
181
|
+
const val = parseInt($(this).val());
|
|
182
|
+
const orig = parseInt($(this).attr('data-original-id'));
|
|
183
|
+
const isDup = !isNaN(val) && idCounts[val] > 1;
|
|
184
|
+
const isChanged = !isNaN(val) && val !== orig;
|
|
185
|
+
|
|
186
|
+
$(this).css({
|
|
187
|
+
border: isDup ? '2px solid #d32f2f' : (isChanged ? '2px solid #1976d2' : ''),
|
|
188
|
+
backgroundColor: isDup ? '#f8a0a0' : (isChanged ? '#a0c8f0' : ''),
|
|
189
|
+
color: isDup ? '#7a0000' : (isChanged ? '#003060' : '')
|
|
190
|
+
});
|
|
70
191
|
});
|
|
71
192
|
}
|
|
72
193
|
|
|
194
|
+
// ============================================================
|
|
195
|
+
// Select All checkbox
|
|
196
|
+
// ============================================================
|
|
197
|
+
$selectAll.on('change', function() {
|
|
198
|
+
const checked = $(this).is(':checked');
|
|
199
|
+
$tbody.find('.point-select').prop('checked', checked);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ============================================================
|
|
203
|
+
// Renumber button
|
|
204
|
+
// ============================================================
|
|
205
|
+
$("#node-input-renumber-btn").on('click', function() {
|
|
206
|
+
const $checked = $tbody.find('.point-select:checked');
|
|
207
|
+
if ($checked.length === 0) {
|
|
208
|
+
RED.notify("Select points to renumber using the checkboxes.", "warning");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Prompt for starting number
|
|
213
|
+
const startStr = prompt("Enter starting ID number for " + $checked.length + " selected point(s):", "1");
|
|
214
|
+
if (startStr === null) return;
|
|
215
|
+
|
|
216
|
+
const startId = parseInt(startStr);
|
|
217
|
+
if (isNaN(startId) || startId < 0) {
|
|
218
|
+
RED.notify("Invalid starting ID.", "error");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Renumber selected rows in current table order (top to bottom)
|
|
223
|
+
let nextId = startId;
|
|
224
|
+
$checked.each(function() {
|
|
225
|
+
const $row = $(this).closest('tr');
|
|
226
|
+
const $input = $row.find('.point-id-input');
|
|
227
|
+
const nid = $row.attr('data-node-id');
|
|
228
|
+
const origVal = parseInt($input.attr('data-original-id'));
|
|
229
|
+
|
|
230
|
+
$input.val(nextId);
|
|
231
|
+
|
|
232
|
+
if (nextId !== origVal) {
|
|
233
|
+
pendingChanges[nid] = nextId;
|
|
234
|
+
} else {
|
|
235
|
+
delete pendingChanges[nid];
|
|
236
|
+
}
|
|
237
|
+
nextId++;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
validateIds();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ============================================================
|
|
244
|
+
// Load on open
|
|
245
|
+
// ============================================================
|
|
73
246
|
loadPoints();
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
oneditsave: function() {
|
|
250
|
+
// Apply pending ID changes to editor nodes
|
|
251
|
+
const $tbody = $("#node-input-point-list");
|
|
252
|
+
let changeCount = 0;
|
|
253
|
+
|
|
254
|
+
$tbody.find('tr[data-node-id]').each(function() {
|
|
255
|
+
const nid = $(this).attr('data-node-id');
|
|
256
|
+
const $input = $(this).find('.point-id-input');
|
|
257
|
+
const newVal = parseInt($input.val());
|
|
258
|
+
const origVal = parseInt($input.attr('data-original-id'));
|
|
259
|
+
|
|
260
|
+
if (!isNaN(newVal) && newVal !== origVal) {
|
|
261
|
+
// Update the editor node config
|
|
262
|
+
const editorNode = RED.nodes.node(nid);
|
|
263
|
+
if (editorNode) {
|
|
264
|
+
editorNode.pointId = newVal;
|
|
265
|
+
editorNode.changed = true;
|
|
266
|
+
editorNode.dirty = true;
|
|
267
|
+
changeCount++;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (changeCount > 0) {
|
|
273
|
+
RED.nodes.dirty(true);
|
|
274
|
+
RED.notify(changeCount + " point ID" + (changeCount !== 1 ? "s" : "") + " updated. Deploy to apply.", "success");
|
|
275
|
+
}
|
|
74
276
|
}
|
|
75
277
|
});
|
|
76
278
|
</script>
|
|
@@ -83,6 +285,13 @@ Maintains the mapping between integer Point IDs and global variable paths. This
|
|
|
83
285
|
|
|
84
286
|
Create a single registry for your network of nodes and reference it from each network-register node.
|
|
85
287
|
|
|
288
|
+
### Bulk ID Editing
|
|
289
|
+
The config panel lets you edit Point IDs directly in the table:
|
|
290
|
+
|
|
291
|
+
1. **Inline editing**: Change any ID by typing in its input field. Changed IDs highlight blue; conflicts highlight red.
|
|
292
|
+
2. **Renumber**: Select points with checkboxes, click **Renumber...**, enter a starting ID — selected points get sequential IDs in table order.
|
|
293
|
+
3. **Deploy**: Click Done, then Deploy to apply changes. The register nodes update automatically.
|
|
294
|
+
|
|
86
295
|
**API for Developers:**
|
|
87
296
|
* `register(id, meta)`: Claim an ID.
|
|
88
297
|
* `lookup(id)`: Find the path/store for an ID.
|
|
@@ -83,7 +83,7 @@ module.exports = function(RED) {
|
|
|
83
83
|
|
|
84
84
|
RED.httpAdmin.get('/network-point-registry/list/:registryId', RED.auth.needsPermission('network-point-registry.read'), function(req, res) {
|
|
85
85
|
const reg = RED.nodes.getNode(req.params.registryId);
|
|
86
|
-
if (!reg) return res.
|
|
86
|
+
if (!reg) return res.json([]); // Not deployed yet — return empty list
|
|
87
87
|
|
|
88
88
|
// Convert Map to array
|
|
89
89
|
const arr = [];
|
package/nodes/or-block.js
CHANGED
|
@@ -48,7 +48,7 @@ module.exports = function(RED) {
|
|
|
48
48
|
node.inputs[slotVal.index - 1] = Boolean(msg.payload);
|
|
49
49
|
const result = node.inputs.some(v => v === true);
|
|
50
50
|
const isUnchanged = result === lastResult && node.inputs.every((v, i) => v === lastInputs[i]);
|
|
51
|
-
const statusText = `
|
|
51
|
+
const statusText = `[${node.inputs.join(", ")}] -> ${result}`;
|
|
52
52
|
|
|
53
53
|
// ================================================================
|
|
54
54
|
// Debounce: Suppress consecutive same outputs within 500ms
|
package/nodes/priority-block.js
CHANGED
|
@@ -174,7 +174,7 @@ module.exports = function(RED) {
|
|
|
174
174
|
send(currentOutput);
|
|
175
175
|
const inDisplay = typeof msg.payload === "number" ? msg.payload.toFixed(2) : typeof msg.payload === "object" ? JSON.stringify(msg.payload).slice(0, 20) : msg.payload;
|
|
176
176
|
const outDisplay = currentOutput.payload === null ? "null" : typeof currentOutput.payload === "number" ? currentOutput.payload.toFixed(2) : currentOutput.payload;
|
|
177
|
-
const statusText = `
|
|
177
|
+
const statusText = `out: ${outDisplay}, slot: ${currentOutput.diagnostics.activePriority || "none"}`;
|
|
178
178
|
utils.setStatusChanged(node, statusText);
|
|
179
179
|
|
|
180
180
|
if (done) done();
|
package/nodes/tstat-block.html
CHANGED
|
@@ -63,6 +63,10 @@
|
|
|
63
63
|
<input type="text" id="node-input-isHeating" style="width: auto; vertical-align: middle;">
|
|
64
64
|
<input type="hidden" id="node-input-isHeatingType">
|
|
65
65
|
</div>
|
|
66
|
+
<div class="form-row">
|
|
67
|
+
<label for="node-input-startupDelay" title="Seconds to suppress heating/cooling calls after deploy (0 = disabled)"><i class="fa fa-hourglass-start"></i> Startup Delay</label>
|
|
68
|
+
<input type="number" id="node-input-startupDelay" placeholder="30" min="0" step="1">
|
|
69
|
+
</div>
|
|
66
70
|
</script>
|
|
67
71
|
|
|
68
72
|
<script type="text/javascript">
|
|
@@ -94,7 +98,8 @@
|
|
|
94
98
|
ignoreAnticipatorCycles: { value: "1" },
|
|
95
99
|
ignoreAnticipatorCyclesType: { value: "num" },
|
|
96
100
|
isHeating: { value: false },
|
|
97
|
-
isHeatingType: { value: "bool" }
|
|
101
|
+
isHeatingType: { value: "bool" },
|
|
102
|
+
startupDelay: { value: 30 }
|
|
98
103
|
},
|
|
99
104
|
inputs: 1,
|
|
100
105
|
outputs: 3,
|
|
@@ -223,100 +228,50 @@
|
|
|
223
228
|
</script>
|
|
224
229
|
|
|
225
230
|
<script type="text/markdown" data-help-name="tstat-block">
|
|
226
|
-
Thermostat controller for heating/cooling with single, split, or specified setpoint operation, hysteresis, and anticipation
|
|
231
|
+
Thermostat controller for heating/cooling with single, split, or specified setpoint operation, hysteresis, and anticipation.
|
|
227
232
|
|
|
228
233
|
### Inputs
|
|
229
|
-
:
|
|
230
|
-
:
|
|
234
|
+
: payload (number) : Temperature reading.
|
|
235
|
+
: isHeating (boolean) : Optional. Overrides the configured heating mode (typically wired from a changeover node).
|
|
231
236
|
|
|
232
237
|
### Outputs
|
|
233
|
-
|
|
234
|
-
:
|
|
235
|
-
:
|
|
236
|
-
: below (boolean) : `true` if input is below heating on threshold.
|
|
237
|
-
: status (object) : Contains detailed runtime information including:
|
|
238
|
-
- `algorithm`: Current algorithm in use
|
|
239
|
-
- `input`: Current temperature input value
|
|
240
|
-
- `isHeating`: Current heating mode
|
|
241
|
-
- `above/below`: Current output states
|
|
242
|
-
- Algorithm-specific setpoints and values
|
|
243
|
-
- `modeChanged`: If mode recently changed
|
|
244
|
-
- `cyclesSinceModeChange`: Count since last mode change
|
|
245
|
-
- `effectiveAnticipator`: Current anticipator value after mode change adjustments
|
|
238
|
+
: isHeating (boolean) : Output 1. Current heating mode. Includes `msg.context = "isHeating"`.
|
|
239
|
+
: above (boolean) : Output 2. `true` when cooling call is active (temperature exceeded cooling threshold).
|
|
240
|
+
: below (boolean) : Output 3. `true` when heating call is active (temperature dropped below heating threshold).
|
|
246
241
|
|
|
247
|
-
|
|
248
|
-
All outputs include comprehensive status information in `msg.status`. Example:
|
|
249
|
-
```json
|
|
250
|
-
{
|
|
251
|
-
"status": {
|
|
252
|
-
"algorithm": "single",
|
|
253
|
-
"input": 68.5,
|
|
254
|
-
"isHeating": true,
|
|
255
|
-
"above": false,
|
|
256
|
-
"below": true,
|
|
257
|
-
"setpoint": 70,
|
|
258
|
-
"diff": 2,
|
|
259
|
-
"anticipator": 0.5,
|
|
260
|
-
"modeChanged": false,
|
|
261
|
-
"cyclesSinceModeChange": 3,
|
|
262
|
-
"effectiveAnticipator": 0.5
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
```
|
|
242
|
+
All outputs include a `msg.status` object with runtime diagnostics: `algorithm`, `input`, `isHeating`, `above`, `below`, `activeSetpoint`, `onThreshold`, `offThreshold`, `diff`, `anticipator`, `effectiveAnticipator`, `modeChanged`, `cyclesSinceModeChange`.
|
|
266
243
|
|
|
267
244
|
### Algorithms
|
|
268
|
-
- **Single Setpoint**:
|
|
269
|
-
- Uses `setpoint`, `diff`, and `anticipator`.
|
|
270
|
-
- Sets `above` if `input > setpoint + diff/2`, clears when `input < setpoint + anticipator`.
|
|
271
|
-
- Sets `below` if `input < setpoint - diff/2`, clears when `input > setpoint - anticipator`.
|
|
272
|
-
- For positive `anticipator`, stops early to prevent overshoot. For negative `anticipator` (testing only), delays turn-off to overshoot setpoint.
|
|
273
|
-
- Example: `setpoint=70`, `diff=2`, `anticipator=-0.5`, `above` if `input > 71`, clears at `< 69.5` (overshoots); `below` if `input < 69`, clears at `> 70.5` (overshoots).
|
|
274
|
-
|
|
275
|
-
- **Split Setpoint**:
|
|
276
|
-
- Uses `heatingSetpoint`, `coolingSetpoint`, `diff`, and `anticipator`.
|
|
277
|
-
- For `isHeating = true`:
|
|
278
|
-
- Sets `below` if `input < heatingSetpoint - diff/2`, clears when `input > heatingSetpoint - anticipator`.
|
|
279
|
-
- `above` is `false`.
|
|
280
|
-
- For `isHeating = false`:
|
|
281
|
-
- Sets `above` if `input > coolingSetpoint + diff/2`, clears when `input < coolingSetpoint + anticipator`.
|
|
282
|
-
- `below` is `false`.
|
|
283
|
-
- Ensures `heatingSetpoint < coolingSetpoint`.
|
|
284
|
-
- For negative `anticipator`, delays turn-off (e.g., heating off above `heatingSetpoint`).
|
|
285
|
-
- Example: `heatingSetpoint=68`, `coolingSetpoint=74`, `diff=2`, `anticipator=-0.5`, heating mode sets `below` if `input < 67`, clears at `> 68.5`; cooling mode sets `above` if `input > 75`, clears at `< 73.5`.
|
|
286
245
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
- Sets `above` if `input > coolingOn`, clears when `input < coolingOff + anticipator`.
|
|
291
|
-
- `below` is `false`.
|
|
292
|
-
- For `isHeating = true`:
|
|
293
|
-
- Sets `below` if `input < heatingOn`, clears when `input > heatingOff - anticipator`.
|
|
294
|
-
- `above` is `false`.
|
|
295
|
-
- Validates `coolingOn >= coolingOff >= heatingOff >= heatingOn`.
|
|
296
|
-
- For negative `anticipator`, delays turn-off (e.g., heating off above `heatingOff`).
|
|
297
|
-
- Example: `coolingOn=74`, `coolingOff=72`, `heatingOff=68`, `heatingOn=66`, `anticipator=-0.5`, cooling mode sets `above` if `input > 74`, clears at `< 71.5`; heating mode sets `below` if `input < 66`, clears at `> 68.5`.
|
|
246
|
+
**Single** — One setpoint with differential hysteresis.
|
|
247
|
+
- Heating: call ON when `temp < setpoint - diff/2`, OFF when `temp > setpoint - anticipator`
|
|
248
|
+
- Cooling: call ON when `temp > setpoint + diff/2`, OFF when `temp < setpoint + anticipator`
|
|
298
249
|
|
|
299
|
-
|
|
300
|
-
|
|
250
|
+
**Split** — Separate heating/cooling setpoints with differential.
|
|
251
|
+
- Heating: call ON when `temp < heatingSetpoint - diff/2`, OFF when `temp > heatingSetpoint - anticipator`
|
|
252
|
+
- Cooling: call ON when `temp > coolingSetpoint + diff/2`, OFF when `temp < coolingSetpoint + anticipator`
|
|
301
253
|
|
|
302
|
-
|
|
254
|
+
**Specified** — Explicit on/off trigger temperatures.
|
|
255
|
+
- Heating: call ON when `temp < heatingOn`, OFF when `temp > heatingOff - anticipator`
|
|
256
|
+
- Cooling: call ON when `temp > coolingOn`, OFF when `temp < coolingOff + anticipator`
|
|
303
257
|
|
|
304
|
-
|
|
305
|
-
|
|
258
|
+
### Anticipator
|
|
259
|
+
Positive values stop early to prevent overshoot. Negative values (≥ -2, testing only) delay turn-off to allow overshoot.
|
|
306
260
|
|
|
307
|
-
The `
|
|
261
|
+
The `ignoreAnticipatorCycles` setting disables the anticipator for N cycles after a mode change to reduce short-cycling.
|
|
308
262
|
|
|
309
|
-
|
|
263
|
+
### Startup Delay
|
|
264
|
+
Configurable delay (default 30s, 0 = disabled) suppresses `above`/`below` calls after deployment. Prevents false calls before upstream mode selection has initialized. During delay, `isHeating` passes through normally and internal state is tracked. Status shows `[startup]` during suppression.
|
|
310
265
|
|
|
311
|
-
|
|
312
|
-
|
|
266
|
+
### Configuration
|
|
267
|
+
All numeric inputs support `num`, `msg`, `flow`, or `global` types via typed inputs. The `isHeating` flag supports `bool`, `msg`, `flow`, or `global`. Algorithm supports dropdown, `msg`, `flow`, or `global`.
|
|
313
268
|
|
|
314
269
|
### Status
|
|
315
|
-
- Green (dot): Configuration
|
|
316
|
-
- Blue (dot):
|
|
317
|
-
- Blue (ring):
|
|
318
|
-
- Red (ring):
|
|
319
|
-
- Yellow (ring):
|
|
270
|
+
- Green (dot): Configuration update
|
|
271
|
+
- Blue (dot): State changed
|
|
272
|
+
- Blue (ring): State unchanged
|
|
273
|
+
- Red (ring): Error
|
|
274
|
+
- Yellow (ring): Warning / startup delay
|
|
320
275
|
|
|
321
276
|
### References
|
|
322
277
|
- [Node-RED Documentation](https://nodered.org/docs/)
|