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.
- package/CHANGELOG.md +20 -1
- 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/knxUltimateAI.js +5 -2
- 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/knxUltimateAI-vue/assets/app.css +1 -1
- package/nodes/plugins/knxUltimateAI-vue/assets/app.js +3 -3
- 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
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.
|
|
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/>
|
package/nodes/hue-config.html
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
var $clientKeyInput = $("#node-config-input-clientkey");
|
|
30
30
|
var $manualCredentialsButton = $("#manualCredentials");
|
|
31
31
|
|
|
32
|
-
function renderHueBridgeOptions
|
|
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
|
|
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
|
|
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
|
|
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
|
package/nodes/knxUltimate.js
CHANGED
|
@@ -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 {
|
package/nodes/knxUltimateAI.js
CHANGED
|
@@ -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>
|