node-red-contrib-knx-ultimate 4.2.2 → 4.2.5

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/nodes/hue-config.html +12 -9
  3. package/nodes/knxUltimate-config.html +24 -1
  4. package/nodes/knxUltimate-config.js +1 -0
  5. package/nodes/knxUltimate.js +79 -0
  6. package/nodes/knxUltimateAI.js +5 -2
  7. package/nodes/knxUltimateViewer.html +16 -3
  8. package/nodes/knxUltimateViewer.js +279 -90
  9. package/nodes/locales/de/knxUltimate-config.html +1 -1
  10. package/nodes/locales/de/knxUltimate-config.json +10 -0
  11. package/nodes/locales/de/knxUltimateViewer.html +18 -0
  12. package/nodes/locales/en/knxUltimate-config.html +1 -1
  13. package/nodes/locales/en/knxUltimate-config.json +10 -0
  14. package/nodes/locales/en/knxUltimateViewer.html +20 -4
  15. package/nodes/locales/es/knxUltimate-config.html +1 -1
  16. package/nodes/locales/es/knxUltimate-config.json +10 -0
  17. package/nodes/locales/es/knxUltimateViewer.html +27 -11
  18. package/nodes/locales/fr/knxUltimate-config.html +1 -1
  19. package/nodes/locales/fr/knxUltimate-config.json +10 -0
  20. package/nodes/locales/fr/knxUltimateViewer.html +27 -11
  21. package/nodes/locales/it/knxUltimate-config.html +1 -1
  22. package/nodes/locales/it/knxUltimate-config.json +10 -0
  23. package/nodes/locales/it/knxUltimateViewer.html +18 -0
  24. package/nodes/locales/zh-CN/knxUltimate-config.html +1 -1
  25. package/nodes/locales/zh-CN/knxUltimate-config.json +10 -0
  26. package/nodes/locales/zh-CN/knxUltimateViewer.html +18 -0
  27. package/nodes/plugins/knxUltimate-flow-bubbles-plugin.html +279 -0
  28. package/nodes/plugins/knxUltimateAI-vue/assets/app.css +1 -1
  29. package/nodes/plugins/knxUltimateAI-vue/assets/app.js +3 -3
  30. package/nodes/plugins/knxUltimateViewer-vue/assets/app.css +1 -0
  31. package/nodes/plugins/knxUltimateViewer-vue/assets/app.js +1 -0
  32. package/nodes/plugins/knxUltimateViewer-vue/index.html +13 -0
  33. package/package.json +7 -4
package/CHANGELOG.md CHANGED
@@ -6,6 +6,25 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
+ **Version 4.2.5** - March 2026<br/>
10
+
11
+ - NEW: added the **KNX Flow Bubbles** editor plugin to visualize live KNX Device state directly on the workspace.<br/>
12
+ - FIX: **KNX Flow Bubbles** are now removed correctly when disabled, and their visibility is evaluated per linked **KNX Gateway** config-node instead of globally.<br/>
13
+ - UI: **KNX Flow Bubbles** shell is now fully round for a cleaner visual appearance.<br/>
14
+ - Docs/help/wiki: documented the **Enable flow bubbles plugin** option in the gateway editor and supported documentation languages.<br/>
15
+ - Docs: removed obsolete references to **Echo sent message to all node with same Group Address** and aligned the text with the current automatic local mirroring behavior.<br/>
16
+
17
+ **Version 4.2.4** - March 2026<br/>
18
+
19
+ - NEW: **KNX Viewer Web** added a new Vue-based web page, opened directly from the **KNXViewer** node editor, to visualize KNX lights and dimmers in a modern dashboard.<br/>
20
+ - UI: **KNX Viewer Web** uses a visual style aligned with **KNX AI**, including live light/dimmer cards, search, node selection and auto-refresh.<br/>
21
+ - IMPROVE: **KNX Viewer Web** ON lights now use a much stronger yellow highlight for clearer visual feedback.<br/>
22
+ - CHANGE: package build/publish flow now also includes the **KNX Viewer Web** Vue bundle.<br/>
23
+
24
+ **Version 4.2.3** - March 2026<br/>
25
+
26
+ - IMPROVE: **KNX AI Flow Map** added a new toggle to show or hide `knxUltimate` nodes running in **Universal Mode**; they are now hidden by default to keep the topology cleaner.<br/>
27
+
9
28
  **Version 4.2.2** - March 2026<br/>
10
29
 
11
30
  - CHANGE: **KNX AI Web Page** is now the official Vue-based dashboard served from `/knxUltimateAI/sidebar/page`.<br/>
@@ -2094,7 +2113,7 @@ This is an interim version, to quick fix some issues. Please report any issue wi
2094
2113
 
2095
2114
  **Version 1.1.45** - March 2020 in Italy, we're locked down for Coronavirus<br/>
2096
2115
 
2097
- - Update knxultimate-api. Yet the nodes connected to an IP Interface, behaves like the nodes connected to an IP Router. See option **Echo sent message to all node with same Group Address** in the Gateway configuration wiki.<br/>
2116
+ - Update knxultimate-api. Nodes connected to an IP Interface now behave like nodes connected to an IP Router, and local writes are mirrored automatically to nodes sharing the same Group Address.<br/>
2098
2117
  - I'm internationalizing the node **(Deutsch, Italiano, English)** with the help of **@svenflender**, so please be patient if some parts are still only in english. Internationalization is working with node-red version 1.0.3 and above. Versions below, does have issues in the i18n module, so knx-ultimate falls back to english. Please upgrade node-red.<br/>
2099
2118
 
2100
2119
  **Version 1.1.43** - March 2020 in the middle of Coronavirus emergency in Italy<br/>
@@ -29,7 +29,7 @@
29
29
  var $clientKeyInput = $("#node-config-input-clientkey");
30
30
  var $manualCredentialsButton = $("#manualCredentials");
31
31
 
32
- function renderHueBridgeOptions (bridges) {
32
+ function renderHueBridgeOptions(bridges) {
33
33
  discoveredHueBridges = Array.isArray(bridges) ? bridges : [];
34
34
  if ($bridgeDatalist.length) {
35
35
  $bridgeDatalist.empty();
@@ -66,7 +66,7 @@
66
66
  }
67
67
  }
68
68
 
69
- function runHueBridgeDiscovery (options) {
69
+ function runHueBridgeDiscovery(options) {
70
70
  var settings = options || {};
71
71
  if ($refreshHueBridges.length) {
72
72
  $refreshHueBridges.addClass("fa-spin");
@@ -117,7 +117,7 @@
117
117
 
118
118
  runHueBridgeDiscovery({ silent: true });
119
119
 
120
- function loadHueCredentials () {
120
+ function loadHueCredentials() {
121
121
  if (!node || !node.id) return;
122
122
  $.getJSON("KNXUltimateGetPlainHueBridgeCredentials?serverId=" + node.id, function (data) {
123
123
  if (data && !data.error) {
@@ -171,7 +171,7 @@
171
171
  var inflightRequest = null;
172
172
  var myNotification;
173
173
 
174
- function cleanupPolling () {
174
+ function cleanupPolling() {
175
175
  polling = false;
176
176
  if (retryTimer) {
177
177
  clearTimeout(retryTimer);
@@ -183,17 +183,17 @@
183
183
  inflightRequest = null;
184
184
  }
185
185
 
186
- function restoreEditorView () {
186
+ function restoreEditorView() {
187
187
  $("#mainWindow").show();
188
188
  $("#waitWindow").hide();
189
189
  }
190
190
 
191
- function scheduleRetry () {
191
+ function scheduleRetry() {
192
192
  if (!polling) return;
193
193
  retryTimer = setTimeout(attemptRegistration, 2000);
194
194
  }
195
195
 
196
- function handleRegistrationSuccess (registration) {
196
+ function handleRegistrationSuccess(registration) {
197
197
  cleanupPolling();
198
198
  if (registration && registration.bridge) {
199
199
  if (registration.bridge.data && registration.bridge.data.name) {
@@ -213,7 +213,7 @@
213
213
  if (myNotification) myNotification.close();
214
214
  }
215
215
 
216
- function handleRegistrationError (message, { retryAllowed = false } = {}) {
216
+ function handleRegistrationError(message, { retryAllowed = false } = {}) {
217
217
  if (retryAllowed) {
218
218
  scheduleRetry();
219
219
  return;
@@ -232,7 +232,7 @@
232
232
  });
233
233
  }
234
234
 
235
- function attemptRegistration () {
235
+ function attemptRegistration() {
236
236
  if (!polling) return;
237
237
  inflightRequest = $.getJSON("KNXUltimateRegisterToHueBridge?IP=" + $("#node-config-input-host").val() + "&serverId=" + node.id, function (data) {
238
238
  if (!polling) return;
@@ -358,7 +358,10 @@
358
358
  <label for="node-config-input-clientkey"> <span data-i18n="hue-config.properties.client_key"></span></label>
359
359
  <input type="text" id="node-config-input-clientkey" placeholder="">
360
360
  </div>
361
+
361
362
  </div>
363
+
364
+
362
365
  </div>
363
366
 
364
367
 
@@ -32,6 +32,7 @@
32
32
  tunnelUserPassword: { value: "" },
33
33
  tunnelUserId: { value: "" },
34
34
  autoReconnect: { value: "yes" },
35
+ enableFlowBubbles: { value: false },
35
36
  statusUpdateThrottle: { value: "0" },
36
37
  statusDateTimeFormat: { value: "legacy" },
37
38
  statusDateTimeCustom: { value: "DD MMM HH:mm" },
@@ -1415,6 +1416,17 @@
1415
1416
  </label>
1416
1417
  </div>
1417
1418
 
1419
+ <div class="form-row form-row-checkbox">
1420
+ <input type="checkbox" id="node-config-input-enableFlowBubbles"
1421
+ style="display:inline-block; width:auto; vertical-align:top;">
1422
+ <label for="node-config-input-enableFlowBubbles">
1423
+ <i class="fa fa-commenting-o"></i>
1424
+ <span data-i18n="knxUltimate-config.advanced.enable_flow_bubbles"></span>
1425
+ </label>
1426
+ </div>
1427
+ <div class="form-tips" data-i18n="knxUltimate-config.advanced.enable_flow_bubbles_help"></div>
1428
+
1429
+
1418
1430
  <div class="form-row">
1419
1431
  <label for="node-config-input-delaybetweentelegrams">
1420
1432
  <i class="fa fa-hourglass-start"></i>
@@ -1553,4 +1565,15 @@
1553
1565
 
1554
1566
  </div>
1555
1567
 
1556
- </script>
1568
+ </script>
1569
+
1570
+ <script type="text/html" data-help-name="knxUltimate-config">
1571
+ <div data-i18n="knxUltimate-config.help.intro"></div>
1572
+ <h3 data-i18n="knxUltimate-config.help.flow_bubbles_title"></h3>
1573
+ <ul>
1574
+ <li data-i18n="knxUltimate-config.help.flow_bubbles_enable"></li>
1575
+ <li data-i18n="knxUltimate-config.help.flow_bubbles_scope"></li>
1576
+ <li data-i18n="knxUltimate-config.help.flow_bubbles_tooltip"></li>
1577
+ <li data-i18n="knxUltimate-config.help.flow_bubbles_stale"></li>
1578
+ </ul>
1579
+ </script>
@@ -216,6 +216,7 @@ module.exports = (RED) => {
216
216
  } else {
217
217
  node.autoReconnect = true
218
218
  }
219
+ node.enableFlowBubbles = config.enableFlowBubbles === true || config.enableFlowBubbles === 'true'
219
220
  node.ignoreTelegramsWithRepeatedFlag = config.ignoreTelegramsWithRepeatedFlag === undefined ? false : config.ignoreTelegramsWithRepeatedFlag
220
221
  const throttleSecondsRaw = Number(config.statusUpdateThrottle)
221
222
  node.statusUpdateThrottleMs = Number.isFinite(throttleSecondsRaw) && throttleSecondsRaw > 0
@@ -3,6 +3,63 @@ const loggerClass = require('./utils/sysLogger')
3
3
  const coerceBoolean = (value) => (value === true || value === 'true')
4
4
 
5
5
  let buttonEndpointRegistered = false
6
+ let liveStateEndpointRegistered = false
7
+ const knxUltimateLiveState = new Map()
8
+
9
+ const resolveBubbleColor = (status = {}) => {
10
+ const fill = typeof status.fill === 'string' ? status.fill.trim().toLowerCase() : ''
11
+ if (typeof status.payload === 'boolean') return status.payload ? '#ffd84d' : '#a5afbf'
12
+ if (typeof status.payload === 'number' && Number.isFinite(status.payload)) {
13
+ if (status.payload > 0) return '#ffd84d'
14
+ if (status.payload === 0) return '#a5afbf'
15
+ }
16
+ switch (fill) {
17
+ case 'green':
18
+ return '#58c96a'
19
+ case 'yellow':
20
+ return '#ffd84d'
21
+ case 'blue':
22
+ return '#61a8ff'
23
+ case 'red':
24
+ return '#f06b6b'
25
+ case 'grey':
26
+ case 'gray':
27
+ return '#a5afbf'
28
+ default:
29
+ return '#d4d8e2'
30
+ }
31
+ }
32
+
33
+ const normalizePayloadForBubble = (payload) => {
34
+ if (payload === undefined || payload === null || payload === '') return ''
35
+ if (typeof payload === 'object') {
36
+ try {
37
+ return JSON.stringify(payload)
38
+ } catch (error) {
39
+ return String(payload)
40
+ }
41
+ }
42
+ return String(payload)
43
+ }
44
+
45
+ const storeKnxUltimateLiveState = (node, status = {}) => {
46
+ if (!node || !node.id) return
47
+ const payload = Object.prototype.hasOwnProperty.call(status, 'payload') ? status.payload : node.currentPayload
48
+ knxUltimateLiveState.set(node.id, {
49
+ id: node.id,
50
+ topic: node.topic || '',
51
+ name: node.name || '',
52
+ dpt: node.dpt || '',
53
+ listenallga: node.listenallga === true || node.listenallga === 'true',
54
+ fill: status.fill || '',
55
+ shape: status.shape || '',
56
+ text: status.text || '',
57
+ payload,
58
+ payloadText: normalizePayloadForBubble(payload),
59
+ bubbleColor: resolveBubbleColor({ fill: status.fill, payload }),
60
+ updatedAt: Date.now()
61
+ })
62
+ }
6
63
 
7
64
  /* eslint-disable max-len */
8
65
  module.exports = function (RED) {
@@ -225,6 +282,18 @@ module.exports = function (RED) {
225
282
  })
226
283
  buttonEndpointRegistered = true
227
284
  }
285
+
286
+ if (!liveStateEndpointRegistered) {
287
+ RED.httpAdmin.get('/knxUltimate/editorLiveState', RED.auth.needsPermission('knxUltimate-config.read'), (req, res) => {
288
+ try {
289
+ const nodes = Array.from(knxUltimateLiveState.values())
290
+ res.json({ nodes, updatedAt: Date.now() })
291
+ } catch (error) {
292
+ res.status(500).json({ error: error.message || String(error) })
293
+ }
294
+ })
295
+ liveStateEndpointRegistered = true
296
+ }
228
297
  const _ = require('lodash')
229
298
  const KNXUtils = require('knxultimate')
230
299
  const payloadRounder = require('./utils/payloadManipulation')
@@ -244,6 +313,7 @@ module.exports = function (RED) {
244
313
  }
245
314
 
246
315
  if (node.serverKNX === undefined) {
316
+ storeKnxUltimateLiveState(node, { fill: 'red', shape: 'dot', text: '[THE GATEWAY NODE HAS BEEN DISABLED]', payload: node.currentPayload })
247
317
  pushStatus({ fill: 'red', shape: 'dot', text: '[THE GATEWAY NODE HAS BEEN DISABLED]' })
248
318
  return
249
319
  }
@@ -254,6 +324,7 @@ module.exports = function (RED) {
254
324
  }) => {
255
325
  try {
256
326
  if (node.serverKNX === null) { pushStatus({ fill: 'red', shape: 'dot', text: '[NO GATEWAY SELECTED]' }); return }
327
+ const rawPayload = payload
257
328
  if (node.icountMessageInWindow == -999) return // Locked out, doesn't change status.
258
329
  const dDate = new Date()
259
330
  const ts = (node.serverKNX && typeof node.serverKNX.formatStatusTimestamp === 'function')
@@ -265,6 +336,7 @@ module.exports = function (RED) {
265
336
  dpt = (typeof dpt === 'undefined' || dpt == '') ? '' : ` DPT${dpt}`
266
337
  payload = typeof payload === 'object' ? JSON.stringify(payload) : payload
267
338
  const statusText = `${GA + payload + (node.listenallga === true ? ` ${devicename}` : '')} (${ts}) ${text}`
339
+ storeKnxUltimateLiveState(node, { fill, shape, text: statusText, payload: rawPayload })
268
340
  pushStatus({ fill, shape, text: statusText })
269
341
  // 16/02/2020 signal errors to the server
270
342
  if (fill.toUpperCase() === 'RED') {
@@ -356,6 +428,12 @@ module.exports = function (RED) {
356
428
  node.buttonStaticValue = config.buttonStaticValue || ''
357
429
  node.buttonToggleInitial = coerceBoolean(config.buttonToggleInitial)
358
430
  node._buttonToggleState = node.buttonToggleInitial
431
+ storeKnxUltimateLiveState(node, {
432
+ fill: 'grey',
433
+ shape: 'ring',
434
+ text: 'Waiting for KNX traffic',
435
+ payload: node.currentPayload
436
+ })
359
437
  node.periodicSend = coerceBoolean(config.periodicSend)
360
438
  node.periodicSendInterval = Number(config.periodicSendInterval)
361
439
  if (!Number.isFinite(node.periodicSendInterval) || node.periodicSendInterval <= 0) node.periodicSendInterval = 0
@@ -871,6 +949,7 @@ module.exports = function (RED) {
871
949
  if (node.timerTTLInputMessage !== null) clearTimeout(node.timerTTLInputMessage)
872
950
  clearPeriodicSendTimer()
873
951
  node.inputmessage = {}
952
+ knxUltimateLiveState.delete(node.id)
874
953
  if (node.serverKNX) {
875
954
  node.serverKNX.removeClient(node)
876
955
  try {
@@ -1766,7 +1766,8 @@ module.exports = function (RED) {
1766
1766
  payload: '',
1767
1767
  lastSeenAtMs: 0,
1768
1768
  inFlow: true,
1769
- anomalyCount: 0
1769
+ anomalyCount: 0,
1770
+ listenAllGA: false
1770
1771
  }, entry))
1771
1772
  return
1772
1773
  }
@@ -1778,6 +1779,7 @@ module.exports = function (RED) {
1778
1779
  cur.inFlow = entry.inFlow !== undefined ? !!entry.inFlow : cur.inFlow
1779
1780
  cur.lastSeenAtMs = Math.max(Number(cur.lastSeenAtMs || 0), Number(entry.lastSeenAtMs || 0))
1780
1781
  cur.anomalyCount = Math.max(Number(cur.anomalyCount || 0), Number(entry.anomalyCount || 0))
1782
+ cur.listenAllGA = entry.listenAllGA !== undefined ? !!entry.listenAllGA : !!cur.listenAllGA
1781
1783
  graphNodes.set(id, cur)
1782
1784
  }
1783
1785
 
@@ -1827,7 +1829,8 @@ module.exports = function (RED) {
1827
1829
  payload: String(nodeLastPayload[nid] || '').trim(),
1828
1830
  lastSeenAtMs: Number(nodeLastSeenMs[nid] || 0),
1829
1831
  inFlow: true,
1830
- anomalyCount: 0
1832
+ anomalyCount: 0,
1833
+ listenAllGA: !!(nodeInfo && nodeInfo.listenAllGA)
1831
1834
  })
1832
1835
  }
1833
1836
 
@@ -50,9 +50,13 @@
50
50
  try {
51
51
  RED.sidebar.show("help");
52
52
  } catch (error) { }
53
-
54
-
55
-
53
+ const nodeId = this.id;
54
+ $("#knx-viewer-open-web-page").on("click", function (evt) {
55
+ evt.preventDefault();
56
+ const target = "knxUltimateViewer/page" + (nodeId ? ("?nodeId=" + encodeURIComponent(nodeId)) : "");
57
+ const wnd = window.open(target, "_blank", "noopener,noreferrer");
58
+ try { if (wnd && typeof wnd.focus === "function") wnd.focus(); } catch (e) { }
59
+ });
56
60
  },
57
61
  oneditsave: function () {
58
62
  // Return to the info tab
@@ -97,6 +101,15 @@
97
101
  <input type="text" id="node-input-name" data-i18n="[placeholder]knxUltimateViewer.node-input-name" style="flex:1 1 240px; min-width:240px; max-width:240px;" />
98
102
  </div>
99
103
 
104
+ <div class="form-row">
105
+ <label><i class="fa fa-external-link"></i> Web UI</label>
106
+ <div style="display:flex; gap:8px; flex-wrap:wrap;">
107
+ <button type="button" class="red-ui-button" id="knx-viewer-open-web-page" style="background-color:#2bb673; border-color:#2bb673; color:#ffffff !important; -webkit-text-fill-color:#ffffff;">
108
+ <i class="fa fa-leaf" style="color:#10341f !important;"></i> <span style="color:#10341f !important;">Open KNX Viewer Web Page</span>
109
+ </button>
110
+ </div>
111
+ </div>
112
+
100
113
 
101
114
 
102
115
  </script>