@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.
@@ -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:200px; /* set a fixed height */
17
- overflow-y:auto; /* enable vertical scrolling */
23
+ height:300px;
24
+ overflow-y:auto;
18
25
  padding:5px;
19
26
  box-sizing:border-box;">
20
- <!-- The list will be injected by the JavaScript below -->
21
- <ul id="node-input-point-list" style="margin:0;padding-left:1.2em;"></ul>
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 node = this;
37
- const $list = $("#node-input-point-list");
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
- $.getJSON(`/network-point-registry/list/${node.id}`, function(data) {
41
- $list.empty();
42
- if (!data.length) return $list.append('<li>No points defined</li>');
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
- data.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10));
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
- data.forEach(pt => {
48
- const li = $('<li>').css({ display:'flex', alignItems:'center' });
101
+ // ============================================================
102
+ // Render the editable table
103
+ // ============================================================
104
+ function renderTable(points) {
105
+ $tbody.empty();
106
+ $selectAll.prop('checked', false);
49
107
 
50
- // Text part: ID + path
51
- const txt = $('<span>')
52
- .text(`ID ${pt.id}: ${pt.path ?? 'not set'}`)
53
- .css({ flexGrow:1 }); // push button to the right
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
- // Button part
56
- const btn = $('<button type="button" class="editor-button">')
57
- .attr('title','Find node')
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 (e) {
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
- li.append(txt, btn);
66
- $list.append(li);
160
+ $tr.append($tdCb, $tdId, $tdPath, $tdBtn);
161
+ $tbody.append($tr);
67
162
  });
68
- }).fail(function() {
69
- $list.html('<li style="color:red;">Could not load points</li>');
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.status(404).json({error:'not found'});
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 = `in: [${node.inputs.join(", ")}], out: ${result}`;
51
+ const statusText = `[${node.inputs.join(", ")}] -> ${result}`;
52
52
 
53
53
  // ================================================================
54
54
  // Debounce: Suppress consecutive same outputs within 500ms
@@ -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 = `in: ${inDisplay}, out: ${outDisplay}, slot: ${currentOutput.diagnostics.activePriority || "none"}`;
177
+ const statusText = `out: ${outDisplay}, slot: ${currentOutput.diagnostics.activePriority || "none"}`;
178
178
  utils.setStatusChanged(node, statusText);
179
179
 
180
180
  if (done) done();
@@ -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 to prevent or allow overshoot for testing.
231
+ Thermostat controller for heating/cooling with single, split, or specified setpoint operation, hysteresis, and anticipation.
227
232
 
228
233
  ### Inputs
229
- : context (string) : Configures node (`"algorithm"`, `"setpoint"`, `"heatingSetpoint"`, `"coolingSetpoint"`, `"coolingOn"`, `"coolingOff"`, `"heatingOff"`, `"heatingOn"`, `"diff"`, `"anticipator"`, `"ignoreAnticipatorCycles"`, `"isHeating"`, `"status"`).
230
- : payload (number | boolean | string) : Number for temperature or config values, boolean for `isHeating`, string for `algorithm` (`"single"`, `"split"`, `"specified"`).
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
- All output messages include a `msg.status` object containing runtime information
234
- : isHeating (boolean) : `true` for heating mode, `false` for cooling mode. Includes `msg.context = "isHeating"`.
235
- : above (boolean) : `true` if input exceeds cooling on threshold.
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
- ### Status Monitoring
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
- - **Specified Setpoint**:
288
- - Uses `coolingOn`, `coolingOff`, `heatingOff`, `heatingOn`, and `anticipator`.
289
- - For `isHeating = false`:
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
- ### Details
300
- Compares a numeric temperature input (`msg.payload`) against setpoints to control heating or cooling.
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
- The `differential` (`diff`) applies to the `single` and `split` setpoint algorithms, providing hysteresis.
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
- The `anticipator` adjusts turn-off points: positive values stop early to prevent overshoot (subtracts for heating, adds for cooling);
305
- negative values (allowed for testing, >= -2) delay turn-off to overshoot setpoint.
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 `isHeating` flag (typically from a changeover node) sets output 1 and selects the active setpoint(s).
261
+ The `ignoreAnticipatorCycles` setting disables the anticipator for N cycles after a mode change to reduce short-cycling.
308
262
 
309
- The `ignoreAnticipatorCycles` setting allows ignoring the anticipator for a specified number of cycles after a mode change to reduce short-cycling.
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
- All numeric inputs (`setpoint`, `heatingSetpoint`, `coolingSetpoint`, `coolingOn`, `coolingOff`, `heatingOff`, `heatingOn`, `diff`, `anticipator`,
312
- `ignoreAnticipatorCycles`) support `num`, `msg`, `flow`, or `global` types via `typedInput`.
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 updates (e.g., `setpoint: 70.0`, `anticipator: 0.5`, `isHeating: true`).
316
- - Blue (dot): Outputs when state changes (e.g., `in: 65.00, out: heating, above: false, below: true`).
317
- - Blue (ring): Outputs when state unchanged (e.g., `in: 66.00, out: heating, above: false, below: true`).
318
- - Red (ring): Errors (e.g., `missing input`, `invalid coolingOff`, `invalid diff, using 2`).
319
- - Yellow (ring): Warnings (e.g., `unknown context`).
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/)