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.
- package/CHANGELOG.md +16 -2
- package/nodes/hue-config.html +12 -9
- package/nodes/knxUltimate-config.html +24 -1
- package/nodes/knxUltimate-config.js +1 -0
- package/nodes/knxUltimate.js +79 -0
- package/nodes/knxUltimateViewer.html +16 -3
- package/nodes/knxUltimateViewer.js +279 -90
- package/nodes/locales/de/knxUltimate-config.html +1 -1
- package/nodes/locales/de/knxUltimate-config.json +10 -0
- package/nodes/locales/de/knxUltimateViewer.html +18 -0
- package/nodes/locales/en/knxUltimate-config.html +1 -1
- package/nodes/locales/en/knxUltimate-config.json +10 -0
- package/nodes/locales/en/knxUltimateViewer.html +20 -4
- package/nodes/locales/es/knxUltimate-config.html +1 -1
- package/nodes/locales/es/knxUltimate-config.json +10 -0
- package/nodes/locales/es/knxUltimateViewer.html +27 -11
- package/nodes/locales/fr/knxUltimate-config.html +1 -1
- package/nodes/locales/fr/knxUltimate-config.json +10 -0
- package/nodes/locales/fr/knxUltimateViewer.html +27 -11
- package/nodes/locales/it/knxUltimate-config.html +1 -1
- package/nodes/locales/it/knxUltimate-config.json +10 -0
- package/nodes/locales/it/knxUltimateViewer.html +18 -0
- package/nodes/locales/zh-CN/knxUltimate-config.html +1 -1
- package/nodes/locales/zh-CN/knxUltimate-config.json +10 -0
- package/nodes/locales/zh-CN/knxUltimateViewer.html +18 -0
- package/nodes/plugins/knxUltimate-flow-bubbles-plugin.html +279 -0
- package/nodes/plugins/knxUltimateViewer-vue/assets/app.css +1 -0
- package/nodes/plugins/knxUltimateViewer-vue/assets/app.js +1 -0
- package/nodes/plugins/knxUltimateViewer-vue/index.html +13 -0
- 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'
|
|
41
|
-
node.inputRBE = 'false'
|
|
42
|
-
node.currentPayload = ''
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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:
|
|
265
|
+
updateStatus({ fill, shape, text: gaValue + ' ' + payloadValue + ' ' + text + ' (' + ts + ')' })
|
|
62
266
|
} catch (error) { /* empty */ }
|
|
63
267
|
}
|
|
64
268
|
|
|
65
|
-
|
|
66
|
-
|
|
269
|
+
node.handleSend = (msg) => {
|
|
270
|
+
let gaEntry
|
|
67
271
|
try {
|
|
68
|
-
|
|
272
|
+
gaEntry = node.exposedGAs.find(ga => ga.address === msg.knx.destination)
|
|
69
273
|
} catch (error) {
|
|
70
|
-
|
|
71
274
|
}
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
75
|
-
node.exposedGAs.push({
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
node.send([
|
|
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
|
|
93
|
-
|
|
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
|
|
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
|
|
318
|
+
const footer = `</tbody><tfoot>
|
|
113
319
|
<tr>
|
|
114
320
|
<th scope="row">Count</th>
|
|
115
|
-
<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 <
|
|
122
|
-
const element =
|
|
123
|
-
|
|
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
|
-
|
|
331
|
+
payload += '<td><b><font color=green>True</font></b></td>'
|
|
126
332
|
} else if (typeof element.payload === 'boolean' && element.payload === false) {
|
|
127
|
-
|
|
333
|
+
payload += '<td><font color=red>False</font></td>'
|
|
128
334
|
} else if (typeof element.payload === 'object' && !isNaN(Date.parse(element.payload))) {
|
|
129
|
-
|
|
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
|
-
|
|
135
|
-
sPayload += '<td><i>' + element.rawPayload + '</i></td>'
|
|
338
|
+
payload += '<td><i>' + element.rawPayload + '</i></td>'
|
|
136
339
|
} catch (error) {
|
|
137
|
-
|
|
340
|
+
payload += '<td>' + element.payload + '</td>'
|
|
138
341
|
}
|
|
139
342
|
} else {
|
|
140
|
-
|
|
343
|
+
payload += '<td>' + element.payload + element.payloadmeasureunit + '</td>'
|
|
141
344
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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:
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
168
|
-
if (
|
|
363
|
+
const items = _.clone(node.serverKNX.knxConnection.commandQueue)
|
|
364
|
+
if (items === undefined) return
|
|
169
365
|
|
|
170
|
-
|
|
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
|
-
|
|
376
|
+
footer = `</tbody><tfoot>
|
|
181
377
|
<tr>
|
|
182
378
|
<th scope="row">Count</th>
|
|
183
|
-
<td>` +
|
|
379
|
+
<td>` + items.length + `</td>
|
|
184
380
|
</tr>
|
|
185
381
|
</tfoot>
|
|
186
382
|
</table></div>`
|
|
187
383
|
|
|
188
|
-
for (let index = 0; index <
|
|
189
|
-
const element =
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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:
|
|
393
|
+
return { topic: node.name, payload: head + payload + footer }
|
|
199
394
|
}
|
|
200
395
|
|
|
201
|
-
|
|
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": " <i class=\"fa fa-youtube-play\"></i> <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": " <i class=\"fa fa-youtube-play\"></i> <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
|
-
|
|
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. |
|