node-red-contrib-knx-ultimate 4.0.14 → 4.0.15

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/CHANGELOG.md CHANGED
@@ -6,6 +6,12 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
+ **Version 4.0.15** - October 2025<br/>
10
+ - NEW: Introduced the KNX Monitor sidebar plugin with live GA table, 1 s auto-refresh, highlight on fresh telegrams and row toggles for boolean values.<br/>
11
+ - NEW: Added inline filtering, draggable column widths, persistent gateway selection and reorder controls to speed up commissioning and diagnosis.<br/>
12
+ - Added `/knxUltimateMonitor` and `/knxUltimateMonitorToggle` admin endpoints to feed the sidebar and allow direct write/toggle actions.<br/>
13
+ - Docs: documented the KNX Monitor panel across all supported wiki languages and refreshed screenshots/tooltips.<br/>
14
+
9
15
  **Version 4.0.14** - October 2025<br/>
10
16
  - NEW: Added KNX ↔ IoT Bridge node to orchestrate bidirectional KNX↔MQTT/REST/Modbus mappings with scaling, templating and ack metadata.<br/>
11
17
 
@@ -61,6 +61,52 @@ module.exports = (RED) => {
61
61
  function commonFunctions() {
62
62
  var node = this;
63
63
 
64
+ const ensureBuffer = (rawValue) => {
65
+ if (!rawValue) return null;
66
+ if (Buffer.isBuffer(rawValue)) return rawValue;
67
+ if (Array.isArray(rawValue)) return Buffer.from(rawValue);
68
+ if (typeof rawValue === 'object' && Array.isArray(rawValue.data)) return Buffer.from(rawValue.data);
69
+ if (rawValue.type === 'Buffer' && Array.isArray(rawValue.data)) return Buffer.from(rawValue.data);
70
+ return null;
71
+ };
72
+
73
+ const guessDptFromRawValue = (rawBuffer) => {
74
+ if (!rawBuffer || !rawBuffer.length) return null;
75
+ if (rawBuffer.length === 1) {
76
+ if (rawBuffer[0] === 0 || rawBuffer[0] === 1) return '1.001';
77
+ return '5.001';
78
+ }
79
+ if (rawBuffer.length === 4) return '14.056';
80
+ if (rawBuffer.length === 2) return '9.001';
81
+ if (rawBuffer.length === 3) return '11.001';
82
+ if (rawBuffer.length === 14) return '16.001';
83
+
84
+ const dpts = Object.entries(dptlib).filter(onlyDptKeys).map(extractBaseNo).sort(sortBy('base')).reduce(toConcattedSubtypes, []);
85
+ for (let index = 0; index < dpts.length; index++) {
86
+ try {
87
+ const resolved = dptlib.resolve(dpts[index].value);
88
+ if (!resolved) continue;
89
+ const jsValue = dptlib.fromBuffer(rawBuffer, resolved);
90
+ if (typeof jsValue !== 'undefined') return dpts[index].value;
91
+ } catch (error) { /* empty */ }
92
+ }
93
+ return null;
94
+ };
95
+
96
+ const formatDisplayValue = (value) => {
97
+ if (value === null || value === undefined) return '';
98
+ if (value instanceof Date) return value.toISOString();
99
+ if (typeof value === 'object') {
100
+ try {
101
+ return JSON.stringify(value);
102
+ } catch (error) {
103
+ return String(value);
104
+ }
105
+ }
106
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
107
+ return String(value);
108
+ };
109
+
64
110
  // // Gather infos about all interfaces on the lan and provides a static variable utils.aDiscoveredknxGateways
65
111
  // try {
66
112
  // require('./utils/utils').DiscoverKNXGateways()
@@ -499,6 +545,110 @@ module.exports = (RED) => {
499
545
  }
500
546
  });
501
547
 
548
+ RED.httpAdmin.get('/knxUltimateMonitor', RED.auth.needsPermission("knxUltimate-config.read"), (req, res) => {
549
+ try {
550
+ const server = RED.nodes.getNode(req.query.serverId);
551
+ if (!server) {
552
+ res.json({ items: [], error: 'NO_SERVER' });
553
+ return;
554
+ }
555
+ const items = Array.isArray(server.exposedGAs) ? server.exposedGAs.map((entry) => {
556
+ const rawBuffer = ensureBuffer(entry?.rawValue);
557
+ const rawHex = rawBuffer ? rawBuffer.toString('hex') : '';
558
+ let dpt = entry?.dpt;
559
+ let guessed = false;
560
+ if ((!dpt || dpt === '') && rawBuffer) {
561
+ const inferred = guessDptFromRawValue(rawBuffer);
562
+ if (inferred) {
563
+ dpt = inferred;
564
+ guessed = true;
565
+ }
566
+ }
567
+ let value = null;
568
+ if (rawBuffer && dpt) {
569
+ try {
570
+ const resolved = dptlib.resolve(dpt);
571
+ if (resolved) value = dptlib.fromBuffer(rawBuffer, resolved);
572
+ } catch (error) { /* empty */ }
573
+ }
574
+ return {
575
+ ga: entry?.ga || '',
576
+ devicename: entry?.devicename || '',
577
+ dpt: dpt || '',
578
+ dptGuessed: guessed,
579
+ rawHex,
580
+ value,
581
+ valueText: formatDisplayValue(value),
582
+ updatedAt: entry?.updatedAt || null,
583
+ };
584
+ }).sort((a, b) => a.ga.localeCompare(b.ga)) : [];
585
+ res.json({ items });
586
+ } catch (error) {
587
+ try { RED.log.error(`KNXUltimate: knxUltimateMonitor error: ${error.message}`); } catch (e) { }
588
+ res.json({ items: [], error: error.message });
589
+ }
590
+ });
591
+
592
+ RED.httpAdmin.post('/knxUltimateMonitorToggle', RED.auth.needsPermission("knxUltimate-config.write"), (req, res) => {
593
+ try {
594
+ const serverId = req.body?.serverId;
595
+ const ga = req.body?.ga;
596
+ const server = serverId ? RED.nodes.getNode(serverId) : null;
597
+ if (!server) {
598
+ res.json({ status: 'error', error: 'NO_SERVER' });
599
+ return;
600
+ }
601
+ if (!ga) {
602
+ res.json({ status: 'error', error: 'NO_GA' });
603
+ return;
604
+ }
605
+ const entry = Array.isArray(server.exposedGAs) ? server.exposedGAs.find((item) => item.ga === ga) : undefined;
606
+ if (!entry) {
607
+ res.json({ status: 'error', error: 'GA_NOT_FOUND' });
608
+ return;
609
+ }
610
+ const rawBuffer = ensureBuffer(entry.rawValue);
611
+ let dpt = entry.dpt;
612
+ if (!rawBuffer) {
613
+ res.json({ status: 'error', error: 'NO_CURRENT_VALUE' });
614
+ return;
615
+ }
616
+ if (!dpt) {
617
+ const inferred = guessDptFromRawValue(rawBuffer);
618
+ if (inferred) dpt = inferred;
619
+ }
620
+ let currentValue = null;
621
+ if (dpt) {
622
+ try {
623
+ const resolved = dptlib.resolve(dpt);
624
+ if (resolved) currentValue = dptlib.fromBuffer(rawBuffer, resolved);
625
+ } catch (error) { currentValue = null; }
626
+ }
627
+ if (typeof currentValue !== 'boolean') {
628
+ res.json({ status: 'error', error: 'NOT_BOOLEAN' });
629
+ return;
630
+ }
631
+ const nextValue = !currentValue;
632
+ const sendDpt = dpt || '1.001';
633
+ try {
634
+ server.sendKNXTelegramToKNXEngine({
635
+ grpaddr: ga,
636
+ payload: nextValue,
637
+ dpt: sendDpt,
638
+ outputtype: 'write',
639
+ nodecallerid: 'knxUltimateMonitor'
640
+ });
641
+ } catch (error) {
642
+ res.json({ status: 'error', error: error.message });
643
+ return;
644
+ }
645
+ res.json({ status: 'ok', value: nextValue });
646
+ } catch (error) {
647
+ try { RED.log.error(`KNXUltimate: knxUltimateMonitorToggle error: ${error.message}`); } catch (e) { }
648
+ res.json({ status: 'error', error: error.message });
649
+ }
650
+ });
651
+
502
652
  RED.httpAdmin.get("/KNXUltimateGetResourcesHUE", (req, res) => {
503
653
  try {
504
654
  // °°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°
@@ -1154,7 +1154,7 @@ module.exports = (RED) => {
1154
1154
  // 04/04/2021 Supergiovane: save value to node.exposedGAs
1155
1155
  if (typeof _dest === "string" && _rawValue !== undefined && (_evt === "GroupValue_Write" || _evt === "GroupValue_Response")) {
1156
1156
  try {
1157
- const ret = { ga: _dest, rawValue: _rawValue, dpt: undefined, devicename: undefined };
1157
+ const ret = { ga: _dest, rawValue: _rawValue, dpt: undefined, devicename: undefined, updatedAt: Date.now() };
1158
1158
  node.exposedGAs = node.exposedGAs.filter((item) => item.ga !== _dest); // Remove previous
1159
1159
  if (node.csv !== undefined && node.csv !== '' && node.csv.length !== 0) {
1160
1160
  // Add the dpt
@@ -3,7 +3,7 @@
3
3
  <script type="text/javascript">
4
4
  RED.nodes.registerType('knxUltimateGarage', {
5
5
  category: 'KNX Ultimate',
6
- color: '#7dd484',
6
+ color: '#C7E9C0',
7
7
  defaults: {
8
8
  server: { type: 'knxUltimate-config', required: true },
9
9
  name: { value: '' },
@@ -306,4 +306,4 @@
306
306
  </ul>
307
307
  <h3 data-i18n="knxUltimateGarage.help.events"></h3>
308
308
  <p data-i18n="knxUltimateGarage.help.events_desc"></p>
309
- </script>
309
+ </script>
@@ -384,4 +384,4 @@ Configure bidirectional maps between KNX group addresses and IoT backends such a
384
384
  - Use `msg.bridge.id` to route acknowledgements or correlate responses.
385
385
  - Enable "Read KNX values on deploy" to bootstrap dashboards after deploys.
386
386
 
387
- </script>
387
+ </script>
@@ -3,7 +3,7 @@
3
3
  <script type="text/javascript">
4
4
  RED.nodes.registerType('knxUltimateStaircase', {
5
5
  category: 'KNX Ultimate',
6
- color: '#7dd484',
6
+ color: '#C7E9C0',
7
7
  defaults: {
8
8
  server: { type: 'knxUltimate-config', required: true },
9
9
  name: { value: '' },
@@ -323,4 +323,4 @@
323
323
  </div>
324
324
 
325
325
  <br/><br/><br/><br/>
326
- </script>
326
+ </script>