node-red-contrib-knx-ultimate 4.2.3 → 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 (30) hide show
  1. package/CHANGELOG.md +16 -2
  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/knxUltimateViewer.html +16 -3
  7. package/nodes/knxUltimateViewer.js +279 -90
  8. package/nodes/locales/de/knxUltimate-config.html +1 -1
  9. package/nodes/locales/de/knxUltimate-config.json +10 -0
  10. package/nodes/locales/de/knxUltimateViewer.html +18 -0
  11. package/nodes/locales/en/knxUltimate-config.html +1 -1
  12. package/nodes/locales/en/knxUltimate-config.json +10 -0
  13. package/nodes/locales/en/knxUltimateViewer.html +20 -4
  14. package/nodes/locales/es/knxUltimate-config.html +1 -1
  15. package/nodes/locales/es/knxUltimate-config.json +10 -0
  16. package/nodes/locales/es/knxUltimateViewer.html +27 -11
  17. package/nodes/locales/fr/knxUltimate-config.html +1 -1
  18. package/nodes/locales/fr/knxUltimate-config.json +10 -0
  19. package/nodes/locales/fr/knxUltimateViewer.html +27 -11
  20. package/nodes/locales/it/knxUltimate-config.html +1 -1
  21. package/nodes/locales/it/knxUltimate-config.json +10 -0
  22. package/nodes/locales/it/knxUltimateViewer.html +18 -0
  23. package/nodes/locales/zh-CN/knxUltimate-config.html +1 -1
  24. package/nodes/locales/zh-CN/knxUltimate-config.json +10 -0
  25. package/nodes/locales/zh-CN/knxUltimateViewer.html +18 -0
  26. package/nodes/plugins/knxUltimate-flow-bubbles-plugin.html +279 -0
  27. package/nodes/plugins/knxUltimateViewer-vue/assets/app.css +1 -0
  28. package/nodes/plugins/knxUltimateViewer-vue/assets/app.js +1 -0
  29. package/nodes/plugins/knxUltimateViewer-vue/index.html +13 -0
  30. package/package.json +7 -4
@@ -1,7 +1,210 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
1
3
  const KNXAddress = require('knxultimate').KNXAddress
2
4
  const _ = require('lodash')
3
5
 
6
+ let viewerAdminEndpointsRegistered = false
7
+ const viewerRuntimeNodes = new Map()
8
+ const knxUltimateViewerVueDistDir = path.join(__dirname, 'plugins', 'knxUltimateViewer-vue')
9
+
10
+ const sendKnxUltimateViewerVueIndex = (res) => {
11
+ const entryPath = path.join(knxUltimateViewerVueDistDir, 'index.html')
12
+ fs.stat(entryPath, (error, stats) => {
13
+ if (error || !stats || !stats.isFile()) {
14
+ res.status(503).type('text/plain').send('KNX Viewer Vue build not found. Run "npm run knx-viewer:build" in the module root.')
15
+ return
16
+ }
17
+ res.sendFile(entryPath, (sendError) => {
18
+ if (!sendError || res.headersSent) return
19
+ res.status(sendError.statusCode || 500).type('text/plain').send(sendError.message || String(sendError))
20
+ })
21
+ })
22
+ }
23
+
24
+ const sendStaticFileSafe = ({ rootDir, relativePath, res }) => {
25
+ const rootPath = path.resolve(rootDir)
26
+ const requestedPath = String(relativePath || '').replace(/^\/+/, '')
27
+ const fullPath = path.resolve(rootPath, requestedPath)
28
+ if (!fullPath.startsWith(rootPath + path.sep) && fullPath !== rootPath) {
29
+ res.status(403).type('text/plain').send('Forbidden')
30
+ return
31
+ }
32
+ fs.stat(fullPath, (statError, stats) => {
33
+ if (statError || !stats || !stats.isFile()) {
34
+ res.status(404).type('text/plain').send('File not found')
35
+ return
36
+ }
37
+ res.sendFile(fullPath, (sendError) => {
38
+ if (!sendError || res.headersSent) return
39
+ res.status(sendError.statusCode || 500).type('text/plain').send(sendError.message || String(sendError))
40
+ })
41
+ })
42
+ }
43
+
44
+ const normalizeDpt = (value) => String(value || '').trim().toUpperCase()
45
+
46
+ const isBooleanLikeDpt = (value) => {
47
+ const dpt = normalizeDpt(value)
48
+ return dpt === '1' || dpt.startsWith('1.') || dpt.includes('DPT-1') || dpt.includes('DPST-1-')
49
+ }
50
+
51
+ const isDimmerLikeDpt = (value) => {
52
+ const dpt = normalizeDpt(value)
53
+ return dpt === '5.001' || dpt.includes('5.001') || dpt.includes('DPST-5-1') || dpt.includes('DPT-5')
54
+ }
55
+
56
+ const clamp = (value, min, max) => Math.max(min, Math.min(max, value))
57
+
58
+ const normalizePayloadText = (value) => {
59
+ if (value === undefined || value === null) return ''
60
+ if (Buffer.isBuffer(value)) return value.toString('hex')
61
+ if (typeof value === 'object') {
62
+ try {
63
+ return JSON.stringify(value)
64
+ } catch (error) {
65
+ return String(value)
66
+ }
67
+ }
68
+ return String(value)
69
+ }
70
+
71
+ const sortViewerEntries = (a, b) => {
72
+ if (a.addressRAW !== undefined && b.addressRAW !== undefined) {
73
+ return a.addressRAW > b.addressRAW ? 1 : -1
74
+ }
75
+ return a.addressRAW !== undefined ? 1 : -1
76
+ }
77
+
78
+ const classifyViewerEntry = (entry) => {
79
+ const payloadNumber = Number(entry && entry.payload)
80
+ const numericPayload = Number.isFinite(payloadNumber) ? clamp(payloadNumber, 0, 100) : null
81
+ const dpt = String(entry && entry.dpt ? entry.dpt : '').trim()
82
+ const isBooleanPayload = typeof (entry && entry.payload) === 'boolean'
83
+
84
+ if (isBooleanPayload || isBooleanLikeDpt(dpt)) {
85
+ return {
86
+ kind: 'light',
87
+ isOn: entry && entry.payload === true,
88
+ level: entry && entry.payload === true ? 100 : 0
89
+ }
90
+ }
91
+
92
+ if (numericPayload !== null && isDimmerLikeDpt(dpt)) {
93
+ return {
94
+ kind: 'dimmer',
95
+ isOn: numericPayload > 0,
96
+ level: Math.round(numericPayload)
97
+ }
98
+ }
99
+
100
+ return {
101
+ kind: 'other',
102
+ isOn: false,
103
+ level: null
104
+ }
105
+ }
106
+
107
+ const buildViewerWebState = (node) => {
108
+ const entries = Array.isArray(node && node.exposedGAs) ? node.exposedGAs.slice().sort(sortViewerEntries) : []
109
+ const items = entries.map((entry) => {
110
+ const classification = classifyViewerEntry(entry)
111
+ const lastUpdateMs = entry && entry.lastupdate ? new Date(entry.lastupdate).getTime() : 0
112
+ return {
113
+ address: String(entry && entry.address ? entry.address : '').trim(),
114
+ addressRAW: Number(entry && entry.addressRAW ? entry.addressRAW : 0),
115
+ dpt: String(entry && entry.dpt ? entry.dpt : '').trim(),
116
+ payload: entry ? entry.payload : undefined,
117
+ payloadText: normalizePayloadText(entry ? entry.payload : ''),
118
+ devicename: String(entry && entry.devicename ? entry.devicename : '').trim(),
119
+ lastUpdate: entry && entry.lastupdate ? new Date(entry.lastupdate).toISOString() : '',
120
+ lastUpdateMs: Number.isFinite(lastUpdateMs) ? lastUpdateMs : 0,
121
+ rawPayload: String(entry && entry.rawPayload ? entry.rawPayload : '').trim(),
122
+ payloadmeasureunit: String(entry && entry.payloadmeasureunit ? entry.payloadmeasureunit : '').trim(),
123
+ kind: classification.kind,
124
+ isOn: classification.isOn,
125
+ level: classification.level
126
+ }
127
+ })
128
+
129
+ const lights = items.filter(item => item.kind === 'light')
130
+ const dimmers = items.filter(item => item.kind === 'dimmer')
131
+ const others = items.filter(item => item.kind === 'other')
132
+ const lastUpdateMs = Math.max(...items.map(item => Number(item.lastUpdateMs || 0)), 0)
133
+
134
+ return {
135
+ node: {
136
+ id: node.id,
137
+ name: node.name || 'KNXViewer',
138
+ gatewayId: node.serverKNX ? node.serverKNX.id : '',
139
+ gatewayName: (node.serverKNX && node.serverKNX.name) ? node.serverKNX.name : ''
140
+ },
141
+ summary: {
142
+ totalItems: items.length,
143
+ lightCount: lights.length,
144
+ dimmerCount: dimmers.length,
145
+ otherCount: others.length,
146
+ lightOnCount: lights.filter(item => item.isOn).length,
147
+ lightOffCount: lights.filter(item => !item.isOn).length,
148
+ dimmerActiveCount: dimmers.filter(item => Number(item.level || 0) > 0).length,
149
+ averageDimmerLevel: dimmers.length
150
+ ? Math.round(dimmers.reduce((acc, item) => acc + Number(item.level || 0), 0) / dimmers.length)
151
+ : 0,
152
+ lastUpdate: lastUpdateMs > 0 ? new Date(lastUpdateMs).toISOString() : ''
153
+ },
154
+ items
155
+ }
156
+ }
157
+
4
158
  module.exports = function (RED) {
159
+ if (!viewerAdminEndpointsRegistered) {
160
+ RED.httpAdmin.get('/knxUltimateViewer/page', RED.auth.needsPermission('knxUltimate-config.read'), (req, res) => {
161
+ sendKnxUltimateViewerVueIndex(res)
162
+ })
163
+
164
+ RED.httpAdmin.get('/knxUltimateViewer/page/assets/:file', RED.auth.needsPermission('knxUltimate-config.read'), (req, res) => {
165
+ sendStaticFileSafe({
166
+ rootDir: path.join(knxUltimateViewerVueDistDir, 'assets'),
167
+ relativePath: req.params.file,
168
+ res
169
+ })
170
+ })
171
+
172
+ RED.httpAdmin.get('/knxUltimateViewer/nodes', RED.auth.needsPermission('knxUltimate-config.read'), (req, res) => {
173
+ try {
174
+ const nodes = Array.from(viewerRuntimeNodes.values())
175
+ .map((node) => ({
176
+ id: node.id,
177
+ name: node.name || 'KNXViewer',
178
+ gatewayId: node.serverKNX ? node.serverKNX.id : '',
179
+ gatewayName: (node.serverKNX && node.serverKNX.name) ? node.serverKNX.name : ''
180
+ }))
181
+ .sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')))
182
+ res.json({ nodes })
183
+ } catch (error) {
184
+ res.status(500).json({ error: error.message || String(error) })
185
+ }
186
+ })
187
+
188
+ RED.httpAdmin.get('/knxUltimateViewer/state', RED.auth.needsPermission('knxUltimate-config.read'), (req, res) => {
189
+ try {
190
+ const nodeId = String(req.query.nodeId || '').trim()
191
+ let viewerNode = nodeId ? viewerRuntimeNodes.get(nodeId) : null
192
+ if (!viewerNode) {
193
+ viewerNode = Array.from(viewerRuntimeNodes.values())[0]
194
+ }
195
+ if (!viewerNode) {
196
+ res.status(404).json({ error: 'KNX Viewer node not found' })
197
+ return
198
+ }
199
+ res.json(buildViewerWebState(viewerNode))
200
+ } catch (error) {
201
+ res.status(500).json({ error: error.message || String(error) })
202
+ }
203
+ })
204
+
205
+ viewerAdminEndpointsRegistered = true
206
+ }
207
+
5
208
  function knxUltimateViewer (config) {
6
209
  RED.nodes.createNode(this, config)
7
210
  const node = this
@@ -37,9 +240,9 @@ module.exports = function (RED) {
37
240
  node.initialread = false
38
241
  node.listenallga = true
39
242
  node.outputtype = 'write'
40
- node.outputRBE = 'false' // Apply or not RBE to the output (Messages coming from flow)
41
- node.inputRBE = 'false' // Apply or not RBE to the input (Messages coming from BUS)
42
- node.currentPayload = '' // Current value for the RBE input and for the .previouspayload msg
243
+ node.outputRBE = 'false'
244
+ node.inputRBE = 'false'
245
+ node.currentPayload = ''
43
246
  node.passthrough = 'no'
44
247
  node.formatmultiplyvalue = 1
45
248
  node.formatnegativevalue = 'leave'
@@ -47,58 +250,61 @@ module.exports = function (RED) {
47
250
  node.timerPIN3 = null
48
251
  node.exposedGAs = []
49
252
 
50
- // Used to call the status update from the config node.
51
- node.setNodeStatus = ({ fill, shape, text, payload, GA, dpt, devicename }) => {
253
+ viewerRuntimeNodes.set(node.id, node)
254
+
255
+ node.setNodeStatus = ({ fill, shape, text, payload, GA }) => {
52
256
  try {
53
257
  if (node.serverKNX === null) { updateStatus({ fill: 'red', shape: 'dot', text: '[NO GATEWAY SELECTED]' }); return }
54
- GA = GA === undefined ? '' : GA
55
- payload = payload === undefined ? '' : payload
56
- payload = typeof payload === 'object' ? JSON.stringify(payload) : payload
258
+ const gaValue = GA === undefined ? '' : GA
259
+ let payloadValue = payload === undefined ? '' : payload
260
+ payloadValue = typeof payloadValue === 'object' ? JSON.stringify(payloadValue) : payloadValue
57
261
  const dDate = new Date()
58
262
  const ts = (node.serverKNX && typeof node.serverKNX.formatStatusTimestamp === 'function')
59
263
  ? node.serverKNX.formatStatusTimestamp(dDate)
60
264
  : `${dDate.getDate()}, ${dDate.toLocaleTimeString()}`
61
- updateStatus({ fill, shape, text: GA + ' ' + payload + ' ' + text + ' (' + ts + ')' })
265
+ updateStatus({ fill, shape, text: gaValue + ' ' + payloadValue + ' ' + text + ' (' + ts + ')' })
62
266
  } catch (error) { /* empty */ }
63
267
  }
64
268
 
65
- // This function is called by the knx-ultimate config node, to output a msg.payload.
66
- node.handleSend = msg => {
269
+ node.handleSend = (msg) => {
270
+ let gaEntry
67
271
  try {
68
- var oGa = node.exposedGAs.find(ga => ga.address === msg.knx.destination)
272
+ gaEntry = node.exposedGAs.find(ga => ga.address === msg.knx.destination)
69
273
  } catch (error) {
70
-
71
274
  }
72
- const sDeviceName = msg.devicename === node.name ? 'Import ETS file to view the group address name' : msg.devicename // The ETS file hasn't been imported
73
- const sAddressRAW = KNXAddress.createFromString(msg.knx.destination, KNXAddress.TYPE_GROUP).get() // Address as number (for ordering later)
74
- if (oGa === undefined) {
75
- node.exposedGAs.push({ address: msg.knx.destination, addressRAW: sAddressRAW, dpt: msg.knx.dpt, payload: msg.payload, devicename: sDeviceName, lastupdate: new Date(), rawPayload: 'HEX Raw: ' + msg.knx.rawValue.toString('hex') || '?', payloadmeasureunit: (msg.payloadmeasureunit !== 'unknown' ? ' ' + msg.payloadmeasureunit : '') })
275
+ const deviceName = msg.devicename === node.name ? 'Import ETS file to view the group address name' : msg.devicename
276
+ const addressRAW = KNXAddress.createFromString(msg.knx.destination, KNXAddress.TYPE_GROUP).get()
277
+ if (gaEntry === undefined) {
278
+ node.exposedGAs.push({
279
+ address: msg.knx.destination,
280
+ addressRAW,
281
+ dpt: msg.knx.dpt,
282
+ payload: msg.payload,
283
+ devicename: deviceName,
284
+ lastupdate: new Date(),
285
+ rawPayload: 'HEX Raw: ' + msg.knx.rawValue.toString('hex') || '?',
286
+ payloadmeasureunit: (msg.payloadmeasureunit !== 'unknown' ? ' ' + msg.payloadmeasureunit : '')
287
+ })
76
288
  } else {
77
- oGa.dpt = msg.knx.dpt
78
- oGa.payload = msg.payload
79
- oGa.devicename = sDeviceName
80
- oGa.lastupdate = new Date()
81
- oGa.rawPayload = 'HEX Raw: ' + msg.knx.rawValue.toString('hex') || '?'
82
- oGa.payloadmeasureunit = (msg.payloadmeasureunit !== 'unknown' ? ' ' + msg.payloadmeasureunit : '')
289
+ gaEntry.dpt = msg.knx.dpt
290
+ gaEntry.payload = msg.payload
291
+ gaEntry.devicename = deviceName
292
+ gaEntry.lastupdate = new Date()
293
+ gaEntry.rawPayload = 'HEX Raw: ' + msg.knx.rawValue.toString('hex') || '?'
294
+ gaEntry.payloadmeasureunit = (msg.payloadmeasureunit !== 'unknown' ? ' ' + msg.payloadmeasureunit : '')
83
295
  }
84
- // Output the payload
85
- const PIN1 = node.createPayloadPIN1()
86
- const PIN2 = node.createPayloadPIN2()
87
- const PIN3 = node.createPayloadPIN3()
88
- node.send([PIN1, PIN2, PIN3])
296
+
297
+ const pin1 = node.createPayloadPIN1()
298
+ const pin2 = node.createPayloadPIN2()
299
+ const pin3 = node.createPayloadPIN3()
300
+ node.send([pin1, pin2, pin3])
89
301
  }
90
302
 
91
303
  node.createPayloadPIN1 = () => {
92
- const aSorted = node.exposedGAs.sort((a, b) => {
93
- if (a.addressRAW !== undefined && b.addressRAW !== undefined) {
94
- return a.addressRAW > b.addressRAW ? 1 : -1
95
- } else {
96
- return a.addressRAW !== undefined ? 1 : -1
97
- }
98
- })
99
- let sPayload = ''
304
+ const sorted = node.exposedGAs.sort(sortViewerEntries)
305
+ let payload = ''
100
306
 
101
- const sHead = `<div class="main"><table><caption>Current received KNX Group address values</caption>
307
+ const head = `<div class="main"><table><caption>Current received KNX Group address values</caption>
102
308
  <thead>
103
309
  <tr>
104
310
  <th> GA </th>
@@ -109,65 +315,55 @@ module.exports = function (RED) {
109
315
  </tr>
110
316
  </thead>
111
317
  <tbody>`
112
- const sFooter = `</tbody><tfoot>
318
+ const footer = `</tbody><tfoot>
113
319
  <tr>
114
320
  <th scope="row">Count</th>
115
- <td>` + aSorted.length + `</td>
321
+ <td>` + sorted.length + `</td>
116
322
  </tr>
117
323
  </tfoot>
118
324
  </table></div>`
119
325
 
120
326
  try {
121
- for (let index = 0; index < aSorted.length; index++) {
122
- const element = aSorted[index]
123
- sPayload += '<tr><td>' + element.address + '</td>'
327
+ for (let index = 0; index < sorted.length; index++) {
328
+ const element = sorted[index]
329
+ payload += '<tr><td>' + element.address + '</td>'
124
330
  if (typeof element.payload === 'boolean' && element.payload === true) {
125
- sPayload += '<td><b><font color=green>True</font></b></td>'
331
+ payload += '<td><b><font color=green>True</font></b></td>'
126
332
  } else if (typeof element.payload === 'boolean' && element.payload === false) {
127
- sPayload += '<td><font color=red>False</font></td>'
333
+ payload += '<td><font color=red>False</font></td>'
128
334
  } else if (typeof element.payload === 'object' && !isNaN(Date.parse(element.payload))) {
129
- // The payload is a datetime
130
- sPayload += '<td>' + element.payload.toLocaleString() + '</td>'
335
+ payload += '<td>' + element.payload.toLocaleString() + '</td>'
131
336
  } else if (typeof element.payload === 'object') {
132
- // Is maybe a JSON?
133
337
  try {
134
- // sPayload += '<td>' + JSON.stringify(element.payload) + '</td>'
135
- sPayload += '<td><i>' + element.rawPayload + '</i></td>'
338
+ payload += '<td><i>' + element.rawPayload + '</i></td>'
136
339
  } catch (error) {
137
- sPayload += '<td>' + element.payload + '</td>'
340
+ payload += '<td>' + element.payload + '</td>'
138
341
  }
139
342
  } else {
140
- sPayload += '<td>' + element.payload + element.payloadmeasureunit + '</td>'
343
+ payload += '<td>' + element.payload + element.payloadmeasureunit + '</td>'
141
344
  }
142
- sPayload += '<td>' + element.dpt + '</td>'
143
- sPayload += '<td>' + element.lastupdate.toLocaleString() + '</td>'
144
- sPayload += '<td><font style="font-size: smaller;">' + element.devicename + '</font></td></tr>'
345
+ payload += '<td>' + element.dpt + '</td>'
346
+ payload += '<td>' + element.lastupdate.toLocaleString() + '</td>'
347
+ payload += '<td><font style="font-size: smaller;">' + element.devicename + '</font></td></tr>'
145
348
  }
146
349
  } catch (error) {
147
-
148
350
  }
149
- return { topic: node.name, payload: sHead + sPayload + sFooter }
351
+ return { topic: node.name, payload: head + payload + footer }
150
352
  }
353
+
151
354
  node.createPayloadPIN2 = () => {
152
355
  return { topic: node.name, payload: node.exposedGAs }
153
356
  }
357
+
154
358
  node.createPayloadPIN3 = () => {
155
- // Object containing the telegram in the queue
156
- // node.writeQueueAdd({
157
- // grpaddr: _oClient.topic,
158
- // payload: "",
159
- // dpt: "",
160
- // outputtype: "read",
161
- // nodecallerid: _oClient.id,
162
- // });
163
- let sHead = ''
164
- let sFooter = ''
165
- let sPayload = ''
359
+ let head = ''
360
+ let footer = ''
361
+ let payload = ''
166
362
  try {
167
- const aItems = _.clone(node.serverKNX.knxConnection.commandQueue)
168
- if (aItems === undefined) return
363
+ const items = _.clone(node.serverKNX.knxConnection.commandQueue)
364
+ if (items === undefined) return
169
365
 
170
- sHead = `<div class="main"><table><caption>Queue of outgoing telegrams to the KNX BUS. The more the count,</br>the more congested is the KNX BUS.</caption>
366
+ head = `<div class="main"><table><caption>Queue of outgoing telegrams to the KNX BUS. The more the count,</br>the more congested is the KNX BUS.</caption>
171
367
  <thead>
172
368
  <tr>
173
369
  <th> Channel ID </th>
@@ -177,50 +373,43 @@ module.exports = function (RED) {
177
373
  </tr>
178
374
  </thead>
179
375
  <tbody>`
180
- sFooter = `</tbody><tfoot>
376
+ footer = `</tbody><tfoot>
181
377
  <tr>
182
378
  <th scope="row">Count</th>
183
- <td>` + aItems.length + `</td>
379
+ <td>` + items.length + `</td>
184
380
  </tr>
185
381
  </tfoot>
186
382
  </table></div>`
187
383
 
188
- for (let index = 0; index < aItems.length; index++) {
189
- const element = aItems[index]
190
- sPayload += '<tr><td>' + element.knxPacket.channelID + '</td>'
191
- sPayload += '<td><b><font color=green>' + element.knxPacket.seqCounter + '</font></b></td>'
192
- sPayload += '<td>' + element.knxPacket.type + '</td>'
193
- sPayload += '<td>' + element.knxPacket.status + '</td></tr>'
384
+ for (let index = 0; index < items.length; index++) {
385
+ const element = items[index]
386
+ payload += '<tr><td>' + element.knxPacket.channelID + '</td>'
387
+ payload += '<td><b><font color=green>' + element.knxPacket.seqCounter + '</font></b></td>'
388
+ payload += '<td>' + element.knxPacket.type + '</td>'
389
+ payload += '<td>' + element.knxPacket.status + '</td></tr>'
194
390
  }
195
391
  } catch (error) {
196
-
197
392
  }
198
- return { topic: node.name, payload: sHead + sPayload + sFooter }
393
+ return { topic: node.name, payload: head + payload + footer }
199
394
  }
200
395
 
201
- // if (timerPIN3 !== null) clearInterval(timerPIN3);
202
- // timerPIN3 = setInterval(() => {
203
- // let PIN3 = node.createPayloadPIN3();
204
- // node.send([null, null, PIN3]);
205
- // }, 200);
206
-
207
- node.on('input', function (msg) {
208
-
396
+ node.on('input', function () {
209
397
  })
210
398
 
211
399
  node.on('close', function (done) {
212
- if (timerPIN3 !== null) clearInterval(timerPIN3)
400
+ if (node.timerPIN3 !== null) clearInterval(node.timerPIN3)
401
+ viewerRuntimeNodes.delete(node.id)
213
402
  if (node.serverKNX) {
214
403
  node.serverKNX.removeClient(node)
215
404
  }
216
405
  done()
217
406
  })
218
407
 
219
- // On each deploy, unsubscribe+resubscribe
220
408
  if (node.serverKNX) {
221
409
  node.serverKNX.removeClient(node)
222
410
  node.serverKNX.addClient(node)
223
411
  }
224
412
  }
413
+
225
414
  RED.nodes.registerType('knxUltimateViewer', knxUltimateViewer)
226
415
  }
@@ -38,9 +38,9 @@ Dieser Node stellt die Verbindung zu deinem KNX/IP-Gateway her.
38
38
 
39
39
  |Eigenschaft|Beschreibung|
40
40
  |--|--|
41
- | Echo sent message to all node with same Group Address | Leitet Flow-Nachrichten an alle Nodes mit derselben GA weiter, so als kämen sie vom BUS. Nützlich bei KNX-Emulation oder fehlender BUS-Verbindung. **Wird künftig standardmäßig aktiviert und dann entfernt.** |
42
41
  | Suppress repeated (R-Flag) telegrams fom BUS | Wiederholte BUS-Telegramme (R-Flag) ignorieren. |
43
42
  | Suppress ACK request in tunneling mode | Für sehr alte KNX/IP-Gateways: ACK ignorieren, alle Telegramme akzeptieren. |
43
+ | Flow-Bubbles-Plugin aktivieren | Zeigt im Editor neben jedem KNX Device Node eine kleine farbige Bubble an. Die Bubble spiegelt den zuletzt vom Gateway empfangenen Live-Status wider und wird abgeblendet, wenn die Daten veraltet sind. Nützlich bei Inbetriebnahme und Debugging. Standard: deaktiviert. |
44
44
  | Delay between each telegram (ms) | KNX erlaubt max. 50 Telegramme/s. 25-50 ms sind üblich; bei langsamen Verbindungen höher (z. B. 200-500 ms). |
45
45
  | Loglevel | Log-Detailgrad. Standard: "Error". |
46
46
  | Aktualisierung der Status-Badges | Legt fest, wie oft die Statusanzeige der Nodes erneuert wird. Mit einer Verzögerung werden Zwischenstände verworfen und nur der letzte Wert nach dem gewählten Intervall angezeigt. Wählen Sie **Sofort**, um das bisherige Echtzeitverhalten beizubehalten. |
@@ -72,6 +72,8 @@
72
72
  "suppress_ack": "ACK-Anfrage unterdrücken in Tunnel Modus",
73
73
  "suppress_ack_help": "Diese Option unterstützt die Kompatibilität mit der alten Firmware des alten IP-Interfaces.Achtung: Der Knoten kann die Trennung vom Gateway möglicherweise nicht bemerken. Verwenden Sie in diesem Fall den Watchdog im Ethernet+KNX-Twisted-Pair Modus.",
74
74
  "ignoreTelegramsWithRepeatedFlag": "Wiederholt (R-Flag) telegramme vom BUS unterdrücken",
75
+ "enable_flow_bubbles": "Flow-Bubbles-Plugin aktivieren",
76
+ "enable_flow_bubbles_help": "Zeigt Live-Status-Bubbles neben KNX Device Nodes im Editor an. Nützlich bei Inbetriebnahme und Debugging.",
75
77
  "log_level": "Loglevel",
76
78
  "status_datetime_format": "Status Datum/Uhrzeit-Format",
77
79
  "status_datetime_format_legacy": "Legacy (System-Locale)",
@@ -98,6 +100,14 @@
98
100
  "exposeAsVariableREADONLY": "Nur Lesen",
99
101
  "exposeAsVariableREADWRITE": "Lesen/Schreiben"
100
102
  },
103
+ "help": {
104
+ "intro": "Konfigurieren Sie die KNX-Gateway-Verbindung, den ETS-Import und das erweiterte Verhalten des Editors.",
105
+ "flow_bubbles_title": "Flow-Bubbles-Plugin",
106
+ "flow_bubbles_enable": "Aktivieren Sie diese Option, um neben jedem KNX Device Node im Editor eine kleine farbige Bubble anzuzeigen.",
107
+ "flow_bubbles_scope": "Die Bubbles spiegeln den zuletzt im Editor empfangenen Live-Status der KNX Device Nodes wider, die mit diesem Gateway verknüpft sind.",
108
+ "flow_bubbles_tooltip": "Fahren Sie mit der Maus über die Bubble, um Knotenname oder Topic, den letzten Payload und den aktuellen Statustext zu sehen.",
109
+ "flow_bubbles_stale": "Wenn kein aktueller KNX-Verkehr vorliegt, bleibt die Bubble abgeblendet, sodass alte Daten von frischen Aktualisierungen unterschieden werden können."
110
+ },
101
111
  "ets": {
102
112
  "description": "Sie können entweder eine ETS CSV-Gruppenadressliste oder eine ESF-Gruppenadressliste importieren. Bitte bevorzugen Sie die Adressliste der ETS CSV-Gruppe, da die Datenpunkte vollständiger sind.",
103
113
  "youtube": "&nbsp<i class=\"fa fa-youtube-play\"></i>&nbsp<a target=\"_blank\" href=\"https://youtu.be/egRbR_KwP9I\"><u>Anleitung des ETS CSV Exports auf Youtube.</u></a>",
@@ -10,6 +10,18 @@ Zeigt alle Gruppenadressen und deren Werte in einem Dashboard-Widget an.<br/>
10
10
  | Gateway | KNX-Gateway. |
11
11
  | Name | Node-Name. |
12
12
 
13
+ # WEBSEITE
14
+
15
+ Der **KNX Viewer** enthält jetzt eine eigene **Vue-basierte Webseite**, die direkt aus dem Node-Editor geöffnet werden kann.
16
+
17
+ Sie kann verwendet werden, um:
18
+ - **Lichter** aus booleschen KNX-Werten live anzuzeigen
19
+ - **Dimmer** aus Werten im Stil `DPT 5.001` anzuzeigen
20
+ - Einträge zu filtern, zwischen Viewer-Nodes zu wechseln und Auto-Refresh aktiv zu halten
21
+ - eine optisch mit **KNX AI** abgestimmte Oberfläche zu nutzen
22
+
23
+ Die Webseite wird direkt von Node-RED bereitgestellt und folgt daher demselben Authentifizierungsmodell wie Editor und Admin-Endpunkte.
24
+
13
25
  # AUSGÄNGE
14
26
 
15
27
  1. Gruppenadressen (Dashboard)
@@ -19,6 +31,12 @@ Zeigt alle Gruppenadressen und deren Werte in einem Dashboard-Widget an.<br/>
19
31
  3. Telegramm-Warteschlange (Dashboard)
20
32
  : payload (html): mit <b>Template</b> verbinden; zeigt die KNX-Sendequeue zur BUS-Überwachung.
21
33
 
34
+ # HINWEISE
35
+
36
+ - Die **Webseite** ersetzt die bestehenden Ausgänge nicht; sie bleiben für klassische Dashboard-/Template-Flows verfügbar.
37
+ - Licht- und Dimmer-Karten werden aus den aktuell vom Viewer gesehenen KNX-Werten abgeleitet.
38
+ - Wenn noch keine Telegramme empfangen wurden, bleibt die Webseite leer, bis der Viewer Live-Verkehr gesammelt hat.
39
+
22
40
  # BEISPIEL
23
41
 
24
42
  <img src="https://raw.githubusercontent.com/Supergiovane/node-red-contrib-knx-ultimate/master/img/wiki/viewer2.png" width="90%"><br/>
@@ -38,9 +38,9 @@
38
38
 
39
39
  |Property|Description|
40
40
  |--|--|
41
- | Echo sent message to all node with same Group Address | Send the msg input coming from the flow, to all nodes having the same group address. The nodes will receive the new msg as if it's coming from the KNX bus. This is useful in case of using the KNX emulation and in case the connection to the KNX bus is not established. **This option will be deprecated in the next version and defaulted to checked.** Default is checked. |
42
41
  | Suppress repeated (R-Flag) telegrams fom BUS | Ignore repeated KNX telegrams coming from the bus. Default is unchecked. |
43
42
  | Suppress ACK request in tunneling mode | Enable it if you have a very old KNX/IP gateway. It ignores the ACK procedure and accepts all telegrams. Default is unchecked.|
43
+ | Enable flow bubbles plugin | Draw a small coloured bubble next to each KNX Device node in the editor. The bubble reflects the latest live state received from the gateway and becomes dim when the data is stale. Useful during commissioning and debugging. Default is unchecked. |
44
44
  | Delay between each telegram (in milliseconds) | KNX specs states, that the maximum telegram sending speed is 50 telegrams per second. A speed between 25 and 50ms should be fine, unless you're connecting to a remote KNX Gateway via a slow internet connection (in this case, you should increase the value by, for example, 200 to 500ms or more). |
45
45
  | Loglevel | Log level, in case you need to debug something with the dev. Default is "Error", |
46
46
  | Node status throttle | Delay how often node badges are refreshed. When a delay is selected, intermediate status changes are discarded and the last one is shown after the chosen interval. Select **Immediate** to keep the legacy real-time behaviour. |
@@ -73,6 +73,8 @@
73
73
  "suppress_ack": "Suppress ACK request in tunneling mode",
74
74
  "suppress_ack_help": "The option above helps old KNX/IP Interfaces compatibility. Warning: the node may not notice the disconnection from the gateway. In this case, use the watchdog in Ethernet+KNX twisted pair mode.",
75
75
  "ignoreTelegramsWithRepeatedFlag": "Suppress repeated (R-Flag) telegrams fom BUS",
76
+ "enable_flow_bubbles": "Enable flow bubbles plugin",
77
+ "enable_flow_bubbles_help": "Draw live status bubbles next to KNX Device nodes in the editor. Useful during commissioning and debugging.",
76
78
  "log_level": "Loglevel",
77
79
  "status_throttle": "Throttle node status updates",
78
80
  "status_throttle_none": "Immediate (no throttle)",
@@ -102,6 +104,14 @@
102
104
  "delaybetweentelegrams": "Delay between each telegram (in milliseconds)",
103
105
  "delaybetweentelegramsfurtherdelayREAD": "and further multiply delay only between -read- telegrams"
104
106
  },
107
+ "help": {
108
+ "intro": "Configure the KNX gateway connection, ETS import and advanced editor behaviour.",
109
+ "flow_bubbles_title": "Flow bubbles plugin",
110
+ "flow_bubbles_enable": "Enable this option to draw a small coloured bubble next to each KNX Device node in the editor.",
111
+ "flow_bubbles_scope": "Bubbles reflect the latest live state received by the editor from KNX Device nodes linked to this gateway.",
112
+ "flow_bubbles_tooltip": "Hover the bubble to inspect the node name/topic, the last payload and the current status text.",
113
+ "flow_bubbles_stale": "If no recent KNX traffic is available, the bubble stays dimmed so stale data can be distinguished from fresh updates."
114
+ },
105
115
  "ets": {
106
116
  "description": "You can import either an ETS CSV group address list, or an ESF group address list. Please prefer the ETS CSV group address list, as the datapoints are more complete.",
107
117
  "youtube": "&nbsp<i class=\"fa fa-youtube-play\"></i>&nbsp<a target=\"_blank\" href=\"https://youtu.be/egRbR_KwP9I\"><u>See how to export the CSV on Youtube.</u></a>",
@@ -10,16 +10,32 @@
10
10
  | Gateway | The KNX gateway. |
11
11
  | Name | The node name. |
12
12
 
13
+ # WEB PAGE
14
+
15
+ The **KNX Viewer** node now includes a dedicated **Vue-based Web Page** that you can open directly from the node editor.
16
+
17
+ Use it to:
18
+ - browse live **lights** detected from boolean-style KNX values
19
+ - browse live **dimmers** detected from `DPT 5.001` style values
20
+ - filter items, switch between Viewer nodes and keep the page auto-refreshed
21
+ - get a UI visually aligned with **KNX AI**
22
+
23
+ The web page is served directly by Node-RED, so it follows the same authentication model used by the editor/admin endpoints.
24
+
13
25
  # OUTPUT PINS
14
26
 
15
27
  1. Dashboard Group Addresses
16
- : payload (html) : Connect it directly with the Template UI node. It creates a table with all Group Addresses and their
17
- values.
28
+ : payload (html) : Connect it directly with the Template UI node. It creates a table with all Group Addresses and their values.
18
29
  2. Simple Array JSON for data recording
19
30
  : payload (array) : An array containing all the GA. You can use the array to do your own format and reordering.
20
31
  3. Dashboard telegrams queue
21
- : payload (html) : Connect it directly with the Template UI node. It creates a table with the KNX transmission queue for
22
- monitoring BUS congestion.
32
+ : payload (html) : Connect it directly with the Template UI node. It creates a table with the KNX transmission queue for monitoring BUS congestion.
33
+
34
+ # NOTES
35
+
36
+ - The **Web Page** does not replace the existing outputs: they remain available for dashboard/template flows.
37
+ - Light and dimmer cards are inferred from the KNX values currently seen by the Viewer node.
38
+ - If no telegrams have been seen yet, the web page will stay empty until the Viewer collects live traffic.
23
39
 
24
40
  # SAMPLE
25
41
 
@@ -33,9 +33,9 @@
33
33
  <br/> **Avanzado** | Propiedad | Descripción |
34
34
 
35
35
  |-|-|
36
- | Echo enviado mensaje a todos los nodos con la misma dirección de grupo | Envíe la entrada MSG que viene del flujo a todos los nodos que tienen la misma dirección de grupo. Los nodos recibirán el nuevo MSG como si viniera del autobús KNX. Esto es útil en el caso de usar la emulación KNX y en caso de que no se establezca la conexión con el bus KNX. ** Esta opción estará en desuso en la próxima versión y se predeterminó en verificación.** Se verifica el valor predeterminado. |
37
36
  | Suprimir los telegramas repetidos (r-flag) FOM BUS | Ignore los repetidos telegramas de KNX que provengan del autobús. El valor predeterminado no está marcado. |
38
37
  | Suprimir la solicitud ACK en el modo de túnel | Habilite si tiene una puerta de enlace KNX/IP muy antigua. Ignora el procedimiento ACK y acepta todos los telegramas. El valor predeterminado no está marcado. |
38
+ | Habilitar plugin flow bubbles | Dibuja una pequeña burbuja de color junto a cada nodo KNX Device en el editor. La burbuja refleja el último estado en vivo recibido desde el gateway y se atenúa cuando los datos ya no son recientes. Útil durante la puesta en marcha y la depuración. Valor predeterminado: desactivado. |
39
39
  | Retraso entre cada telegrama (en milisegundos) | Las especificaciones de KNX establecen que la velocidad máxima de envío de telegrama es de 50 telegramas por segundo. Una velocidad entre 25 y 50 ms debe estar bien, a menos que se conecte a una puerta de enlace KNX remota a través de una conexión a Internet lenta (en este caso, debe aumentar el valor, por ejemplo, de 200 a 500 ms o más). |
40
40
  | Loglevel | Nivel de registro, en caso de que necesite depurar algo con el desarrollo. El valor predeterminado es "error", |
41
41
  | Temporización de actualizaciones de estado | Define cada cuánto se actualiza la insignia de estado de los nodos. Con un retardo activo se descartan los estados intermedios y solo se muestra el último después del intervalo elegido. Seleccione **Inmediato** para mantener el comportamiento en tiempo real. |