Skip to content

Commit 0f23c12

Browse files
committed
.
1 parent 0dac250 commit 0f23c12

24 files changed

+398
-109
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66

77
# CHANGELOG
88

9+
**Version 4.0.18** - October 2025<br/>
10+
- Hue config node now exposes `refreshHueResources()` and `getHueResourceSnapshot()` so Hue devices always reload directly from the bridge after a refresh or reconnect, preventing stale caches.<br/>
11+
- Hue Light and Hue Plug nodes request the live Hue snapshot before applying KNX commands, queue pending writes during synchronisation (with a 10 s TTL), and replay only the fresh ones as soon as the bridge answers.<br/>
12+
- Hue Scene node now behaves like the other actuators: it waits for the bridge snapshot before serving KNX/flow recalls, replays deferred commands, and shows the actual flow payload in the status text.<br/>
13+
- All Hue editor dialogs automatically show flow pins whenever no KNX gateway is configured and hide them when one is selected, keeping the Flow output consistent with the gateway state.<br/>
14+
- General Hue editor polish: the refresh buttons and status messages now reflect the effective payload sent to Hue, and the UI stays responsive while the bridge syncs in the background.<br/>
15+
916
**Version 4.0.16** - October 2025<br/>
1017
- Fixed Gateway Discover, that sometimes donesn't show some KNX Gateways.<br/>
1118
- HUE Lights Refresh button in the Light node, now ask for a refresh from the HUE bridge directly.<br/>

nodes/knxUltimate.html

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,32 @@
5050
return ((this.outputRBE === "true" || this.outputRBE === true) ? "|rbe| " : "") + functionSendMsgToKNXCode + (this.name || this.topic || "KNX Device") + (this.setTopicType === 'str' || this.setTopicType === undefined ? '' : ' [' + (this.setTopicType === 'listenAllGA' ? 'Universal' : this.setTopicType) + ']') + functionreceiveMsgFromKNXCode + ((this.inputRBE === "true" || this.inputRBE === true) ? " |rbe|" : "")
5151
},
5252
paletteLabel: "KNX DEVICE",
53-
// button: {
54-
// enabled: function() {
55-
// // return whether or not the button is enabled, based on the current
56-
// // configuration of the node
57-
// return !this.changed
58-
// },
59-
// visible: function() {
60-
// // return whether or not the button is visible, based on the current
61-
// // configuration of the node
62-
// return this.hasButton
63-
// },
64-
// //toggle: "buttonState",
65-
// onclick: function() {}
66-
// },
53+
button: {
54+
enabled: function () {
55+
return !this.changed;
56+
},
57+
visible: function () {
58+
return true;
59+
},
60+
onclick: function () {
61+
const node = this;
62+
$.ajax({
63+
type: "POST",
64+
url: "knxUltimate/manualRead",
65+
data: { id: node.id },
66+
success: function () {
67+
RED.notify(RED._("node-red-contrib-knx-ultimate/knxUltimate.manualReadOk") || "KNX read request sent", "success");
68+
},
69+
error: function (xhr) {
70+
let message;
71+
if (xhr && xhr.responseJSON && xhr.responseJSON.error) {
72+
message = xhr.responseJSON.error;
73+
}
74+
RED.notify(message || (RED._("node-red-contrib-knx-ultimate/knxUltimate.manualReadError") || "Unable to send KNX read request"), "error");
75+
}
76+
});
77+
}
78+
},
6779
oneditprepare: function () {
6880
// Go to the help panel
6981
try {

nodes/knxUltimate.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,65 @@
11
const loggerClass = require('./utils/sysLogger')
22

3+
let manualReadEndpointRegistered = false;
4+
35
/* eslint-disable max-len */
46
module.exports = function (RED) {
7+
if (!manualReadEndpointRegistered) {
8+
RED.httpAdmin.post('/knxUltimate/manualRead', RED.auth.needsPermission('knxUltimate-config.write'), (req, res) => {
9+
try {
10+
const { id } = req.body || {};
11+
if (!id) {
12+
res.status(400).json({ error: 'Missing node id' });
13+
return;
14+
}
15+
const targetNode = RED.nodes.getNode(id);
16+
if (!targetNode) {
17+
res.status(404).json({ error: 'KNX node not found' });
18+
return;
19+
}
20+
if (!targetNode.serverKNX) {
21+
res.status(400).json({ error: 'KNX gateway not configured' });
22+
return;
23+
}
24+
if (targetNode.listenallga === true || targetNode.listenallga === 'true') {
25+
res.status(400).json({ error: 'Manual read is not available when universal mode is enabled' });
26+
return;
27+
}
28+
const grpaddr = targetNode.topic;
29+
if (grpaddr === undefined || grpaddr === null || String(grpaddr).trim() === '') {
30+
res.status(400).json({ error: 'Group address not set' });
31+
return;
32+
}
33+
targetNode.serverKNX.sendKNXTelegramToKNXEngine({
34+
grpaddr,
35+
payload: '',
36+
dpt: '',
37+
outputtype: 'read',
38+
nodecallerid: targetNode.id,
39+
});
40+
try {
41+
if (typeof targetNode.setNodeStatus === 'function') {
42+
targetNode.setNodeStatus({
43+
fill: 'blue',
44+
shape: 'ring',
45+
text: 'BTN->KNX READ',
46+
payload: '',
47+
GA: grpaddr,
48+
dpt: targetNode.dpt,
49+
devicename: targetNode.name || '',
50+
});
51+
}
52+
targetNode.sysLogger?.info(`Manual KNX read triggered via editor button for ${grpaddr}`);
53+
} catch (error) {
54+
targetNode.sysLogger?.warn(`Manual KNX read status update failed: ${error.message}`);
55+
}
56+
res.json({ status: 'ok' });
57+
} catch (error) {
58+
res.status(500).json({ error: error.message || 'KNX read failed' });
59+
}
60+
});
61+
manualReadEndpointRegistered = true;
62+
}
563
const _ = require('lodash');
664
const KNXUtils = require('knxultimate');
765
const payloadRounder = require('./utils/payloadManipulation');

nodes/knxUltimateHueBattery.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,12 @@
342342
$outputInfo.show();
343343
}
344344
}
345+
if ($enablePinsSelect && $enablePinsSelect.length) {
346+
const desiredPins = knxSelected ? 'no' : 'yes';
347+
if ($enablePinsSelect.val() !== desiredPins) {
348+
$enablePinsSelect.val(desiredPins).trigger('change');
349+
}
350+
}
345351
};
346352

347353
const updatePinsState = () => {

nodes/knxUltimateHueCameraMotion.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,12 @@
325325
$outputInfo.show();
326326
}
327327
}
328+
if ($enablePinsSelect && $enablePinsSelect.length) {
329+
const desiredPins = knxSelected ? 'no' : 'yes';
330+
if ($enablePinsSelect.val() !== desiredPins) {
331+
$enablePinsSelect.val(desiredPins).trigger('change');
332+
}
333+
}
328334
};
329335

330336
const updateKNXVisibility = () => {

nodes/knxUltimateHueHumiditySensor.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,12 @@
323323
$outputInfo.show();
324324
}
325325
}
326+
if ($enablePinsSelect && $enablePinsSelect.length) {
327+
const desiredPins = knxSelected ? 'no' : 'yes';
328+
if ($enablePinsSelect.val() !== desiredPins) {
329+
$enablePinsSelect.val(desiredPins).trigger('change');
330+
}
331+
}
326332
};
327333

328334
const updateKNXVisibility = () => {

nodes/knxUltimateHueLight.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -834,8 +834,11 @@
834834
$tabs.hide();
835835
}
836836

837-
if (!knxSelected && $pinSelect.length && $pinSelect.val() !== 'yes') {
838-
$pinSelect.val('yes').trigger('change');
837+
if ($pinSelect.length) {
838+
const desiredPins = knxSelected ? 'no' : 'yes';
839+
if ($pinSelect.val() !== desiredPins) {
840+
$pinSelect.val(desiredPins).trigger('change');
841+
}
839842
}
840843

841844
if ($pinSectionRow.length) {

nodes/knxUltimateHueLight.js

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,34 @@ module.exports = function (RED) {
291291

292292
const pendingKnxMessages = [];
293293
const MAX_PENDING_KNX_MESSAGES = 5;
294+
const PENDING_KNX_TTL_MS = 10000;
294295
let pendingHueDeviceSnapshotPromise = null;
296+
297+
const prunePendingKnxMessages = (now = Date.now()) => {
298+
if (!pendingKnxMessages.length) return now;
299+
while (pendingKnxMessages.length > 0 && (now - pendingKnxMessages[0].enqueuedAt) > PENDING_KNX_TTL_MS) {
300+
pendingKnxMessages.shift();
301+
}
302+
return now;
303+
};
304+
305+
const enqueuePendingKnxMessage = (msg) => {
306+
const now = prunePendingKnxMessages();
307+
const snapshot = {
308+
msg: (() => {
309+
try {
310+
return cloneDeep(msg);
311+
} catch (error) {
312+
return msg;
313+
}
314+
})(),
315+
enqueuedAt: now,
316+
};
317+
if (pendingKnxMessages.length >= MAX_PENDING_KNX_MESSAGES) {
318+
pendingKnxMessages.shift();
319+
}
320+
pendingKnxMessages.push(snapshot);
321+
};
295322
const ensureCurrentHueDevice = async ({ forceRefresh = false } = {}) => {
296323
if (!node.serverHue || typeof node.serverHue.getHueResourceSnapshot !== 'function') return undefined;
297324
if (pendingHueDeviceSnapshotPromise) {
@@ -315,16 +342,15 @@ module.exports = function (RED) {
315342
RED.log.debug(`knxUltimateHueLight: ensureCurrentHueDevice handleSendHUE error ${error.message}`);
316343
}
317344
}
318-
if (pendingKnxMessages.length > 0) {
319-
const queued = pendingKnxMessages.splice(0);
320-
queued.forEach((queuedMsg) => {
321-
try {
322-
node.handleSend(queuedMsg);
323-
} catch (error) {
324-
RED.log.warn(`knxUltimateHueLight: replay queued KNX command error ${error.message}`);
325-
}
326-
});
327-
}
345+
const now = prunePendingKnxMessages();
346+
const queued = pendingKnxMessages.splice(0).filter((entry) => (now - entry.enqueuedAt) <= PENDING_KNX_TTL_MS);
347+
queued.forEach(({ msg }) => {
348+
try {
349+
node.handleSend(msg);
350+
} catch (error) {
351+
RED.log.warn(`knxUltimateHueLight: replay queued KNX command error ${error.message}`);
352+
}
353+
});
328354
return snapshot;
329355
} catch (error) {
330356
RED.log.warn(`knxUltimateHueLight: ensureCurrentHueDevice error ${error.message}`);
@@ -540,13 +566,7 @@ module.exports = function (RED) {
540566
text: "Syncing with HUE bridge",
541567
payload: "",
542568
});
543-
if (pendingKnxMessages.length < MAX_PENDING_KNX_MESSAGES) {
544-
try {
545-
pendingKnxMessages.push(cloneDeep(msg));
546-
} catch (error) {
547-
pendingKnxMessages.push(msg);
548-
}
549-
}
569+
enqueuePendingKnxMessage(msg);
550570
ensureCurrentHueDevice({ forceRefresh: true });
551571
} else {
552572
node.setNodeStatusHue({

nodes/knxUltimateHueLightSensor.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,12 @@
330330
$outputInfo.show();
331331
}
332332
}
333+
if ($enablePinsSelect && $enablePinsSelect.length) {
334+
const desiredPins = knxSelected ? 'no' : 'yes';
335+
if ($enablePinsSelect.val() !== desiredPins) {
336+
$enablePinsSelect.val(desiredPins).trigger('change');
337+
}
338+
}
333339
};
334340

335341
const updatePinsState = () => {

nodes/knxUltimateHueMotion.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,12 @@
341341
$outputInfo.show();
342342
}
343343
}
344+
if ($enablePinsSelect && $enablePinsSelect.length) {
345+
const desiredPins = knxSelected ? 'no' : 'yes';
346+
if ($enablePinsSelect.val() !== desiredPins) {
347+
$enablePinsSelect.val(desiredPins).trigger('change');
348+
}
349+
}
344350
};
345351

346352
const updatePinsState = () => {

0 commit comments

Comments
 (0)