node-red-contrib-knx-ultimate 8.0.2 → 8.0.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 -0
- package/MIGRATION.md +2 -0
- package/README.md +2 -0
- package/nodes/locales/de/knxUltimateUtility.json +1 -0
- package/nodes/locales/en/knxUltimateUtility.json +1 -0
- package/nodes/locales/es/knxUltimateUtility.json +1 -0
- package/nodes/locales/fr/knxUltimateUtility.json +1 -0
- package/nodes/locales/it/knxUltimateUtility.json +1 -0
- package/nodes/locales/zh-CN/knxUltimateUtility.json +1 -0
- package/nodes/plugins/knxUltimate-v8-migration-recovery-plugin.html +1 -0
- package/package.json +1 -1
- package/resources/editorNotifications.js +101 -0
- package/resources/upgradeToV8.js +20 -12
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
+
**Version 8.0.5** - September 2026
|
|
10
|
+
|
|
11
|
+
- Added the KNX Ultimate 7-to-8 migration video to the package README, migration guide and all localized documentation pages.
|
|
12
|
+
|
|
13
|
+
**Version 8.0.4** - September 2026
|
|
14
|
+
|
|
15
|
+
- Automatic migration now validates only the HUE, Matter and KNX Utility nodes it converts. Invalid nodes from unrelated packages no longer block the upgrade.
|
|
16
|
+
- Unrelated invalid nodes are preserved unchanged while the converted flow is deployed and verified.
|
|
17
|
+
- Added regression coverage for the reported Alarm Ultimate flow with unconfigured Alarm State and Alarm Siren nodes.
|
|
18
|
+
|
|
19
|
+
**Version 8.0.3** - September 2026
|
|
20
|
+
|
|
21
|
+
- Allows incomplete legacy HUE and Matter nodes that belong to the active migration plan to be converted without weakening validation for unrelated invalid nodes.
|
|
22
|
+
- Adds an explicit **OK** button to persistent migration-error notifications so they can always be dismissed.
|
|
23
|
+
- Adds an end-to-end regression fixture for direct migration of the Alarm Ultimate flow reported by a user.
|
|
24
|
+
|
|
9
25
|
**Version 8.0.2** - September 2026
|
|
10
26
|
|
|
11
27
|
- Published version 8 on the stable `latest` channel for installations that only need the current KNX nodes.
|
package/MIGRATION.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Version 8 contains the current KNX nodes and gateway. HUE and Matter are provided by `node-red-contrib-hue-ultimate` and `node-red-contrib-matter-ultimate`; the old dedicated KNX utility nodes are replaced by KNX Utility.
|
|
4
4
|
|
|
5
|
+
> ▶️ [Watch the KNX Ultimate 7 → 8 migration video](https://youtu.be/fpNNi1jZZSc) *(Italian)*
|
|
6
|
+
|
|
5
7
|
## Recommended path from version 7
|
|
6
8
|
|
|
7
9
|
Install KNX Ultimate 7.1.4 first, open any KNX Ultimate node and press **Upgrade to v8**. The guided operation backs up the flows, installs the required HUE/Matter packages, converts compatible nodes, performs and verifies a full Deploy, and installs version 8. Restart the Node-RED service when requested.
|
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ The palette contains KNX Device, KNX Utility, KNX Viewer, KNX IoT Bridge, KNX Mu
|
|
|
22
22
|
>
|
|
23
23
|
> If version 8 was installed directly from an older v7, restart Node-RED and open the editor: the built-in recovery bridge offers the same protected backup, package installation, conversion and Deploy. Legacy KNX AI nodes still require manual replacement or removal.
|
|
24
24
|
>
|
|
25
|
+
> ▶️ **[Watch the KNX Ultimate 7 → 8 migration video](https://youtu.be/fpNNi1jZZSc)** *(Italian)*
|
|
26
|
+
>
|
|
25
27
|
> **Follow the [upgrade steps](https://supergiovane.github.io/node-red-contrib-knx-ultimate/wiki/Migration-8) to keep your flows working.**
|
|
26
28
|
|
|
27
29
|
### Native seamless integrations
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"cancel": "Abbrechen",
|
|
35
35
|
"start": "Sichern und aktualisieren",
|
|
36
36
|
"close": "Schließen",
|
|
37
|
+
"ok": "OK",
|
|
37
38
|
"backup_failed": "Die Flow-Sicherung konnte nicht gestartet werden. Es wurde nichts geändert:",
|
|
38
39
|
"progress_start": "Automatisches Update wird vorbereitet…",
|
|
39
40
|
"progress_install_hue": "HUE Ultimate wird installiert…",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"cancel": "Cancel",
|
|
35
35
|
"start": "Back up and upgrade",
|
|
36
36
|
"close": "Close",
|
|
37
|
+
"ok": "OK",
|
|
37
38
|
"backup_failed": "Flow backup could not be started. Nothing was changed:",
|
|
38
39
|
"progress_start": "Preparing the automatic upgrade…",
|
|
39
40
|
"progress_install_hue": "Installing HUE Ultimate…",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"cancel": "Cancelar",
|
|
35
35
|
"start": "Copia y actualización",
|
|
36
36
|
"close": "Cerrar",
|
|
37
|
+
"ok": "OK",
|
|
37
38
|
"backup_failed": "No se pudo iniciar la copia de seguridad de los flujos. No se modificó nada:",
|
|
38
39
|
"progress_start": "Preparando la actualización automática…",
|
|
39
40
|
"progress_install_hue": "Instalando HUE Ultimate…",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"cancel": "Annuler",
|
|
35
35
|
"start": "Sauvegarder et mettre à jour",
|
|
36
36
|
"close": "Fermer",
|
|
37
|
+
"ok": "OK",
|
|
37
38
|
"backup_failed": "Impossible de démarrer la sauvegarde des flux. Rien n’a été modifié :",
|
|
38
39
|
"progress_start": "Préparation de la mise à jour automatique…",
|
|
39
40
|
"progress_install_hue": "Installation de HUE Ultimate…",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"cancel": "Annulla",
|
|
35
35
|
"start": "Backup e aggiornamento",
|
|
36
36
|
"close": "Chiudi",
|
|
37
|
+
"ok": "OK",
|
|
37
38
|
"backup_failed": "Impossibile avviare il backup dei flow. Non è stato modificato nulla:",
|
|
38
39
|
"progress_start": "Preparazione dell’aggiornamento automatico…",
|
|
39
40
|
"progress_install_hue": "Installazione di HUE Ultimate…",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/editorNotifications.js"></script>
|
|
1
2
|
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/flowMigrationBackup.js"></script>
|
|
2
3
|
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/hueControllerMigration.js"></script>
|
|
3
4
|
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/knxUtilityMigration.js"></script>
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=20.18.1"
|
|
5
5
|
},
|
|
6
|
-
"version": "8.0.
|
|
6
|
+
"version": "8.0.5",
|
|
7
7
|
"description": "KNX integration for Node-RED with KNX/IP and KNX Secure, ETS import, routing, diagnostics, automation utilities, MQTT and Modbus adapters. Optional Hue Ultimate and Matter Ultimate packages connect through the KNX gateway.",
|
|
8
8
|
"files": [
|
|
9
9
|
"nodes/",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/* global RED */
|
|
2
|
+
/* eslint-disable no-undef */
|
|
3
|
+
|
|
4
|
+
;(function (root, factory) {
|
|
5
|
+
const api = factory()
|
|
6
|
+
if (typeof module === 'object' && module.exports) module.exports = api
|
|
7
|
+
if (root) root.UltimateEditorNotifications = api
|
|
8
|
+
try {
|
|
9
|
+
const editor = typeof RED !== 'undefined' ? RED : (root && root.RED)
|
|
10
|
+
if (editor) api.install(editor)
|
|
11
|
+
} catch (error) { /* Node-RED will call install when the resource loads again. */ }
|
|
12
|
+
}(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
13
|
+
'use strict'
|
|
14
|
+
|
|
15
|
+
const PATCH_MARKER = '__ultimateCloseButtonPatched'
|
|
16
|
+
const CLOSE_BUTTON_CLASS = 'ultimate-notification-close'
|
|
17
|
+
|
|
18
|
+
const closeLabel = (RED) => {
|
|
19
|
+
const key = 'common.label.close'
|
|
20
|
+
try {
|
|
21
|
+
const translated = RED && typeof RED._ === 'function' ? RED._(key) : undefined
|
|
22
|
+
if (translated && translated !== key) return translated
|
|
23
|
+
} catch (error) { /* use the language-neutral fallback below */ }
|
|
24
|
+
return 'Close'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const normalizeOptions = (options, fixed, timeout) => {
|
|
28
|
+
if (options && typeof options === 'object') return { ...options }
|
|
29
|
+
const normalized = {}
|
|
30
|
+
if (options !== undefined && options !== null) normalized.type = options
|
|
31
|
+
if (fixed !== undefined) normalized.fixed = fixed
|
|
32
|
+
if (timeout !== undefined) normalized.timeout = timeout
|
|
33
|
+
return normalized
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const closesNotification = (button) => {
|
|
37
|
+
if (!button || typeof button !== 'object') return false
|
|
38
|
+
if (String(button.class || '').split(/\s+/).includes(CLOSE_BUTTON_CLASS)) return true
|
|
39
|
+
if (typeof button.click !== 'function') return false
|
|
40
|
+
|
|
41
|
+
// Existing project buttons close either the notification directly or a
|
|
42
|
+
// named close/dismiss helper. Recognising them avoids duplicate buttons.
|
|
43
|
+
const source = Function.prototype.toString.call(button.click)
|
|
44
|
+
return /\.close\s*\(|\.dialog\s*\(\s*['"]close['"]|\b(?:close|dismiss)\w*\s*\(/i.test(source)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const withCloseButton = (RED, options, getNotification) => {
|
|
48
|
+
const normalized = { ...options }
|
|
49
|
+
const buttons = Array.isArray(normalized.buttons) ? normalized.buttons.slice() : []
|
|
50
|
+
if (!buttons.some(closesNotification)) {
|
|
51
|
+
buttons.push({
|
|
52
|
+
text: closeLabel(RED),
|
|
53
|
+
class: CLOSE_BUTTON_CLASS,
|
|
54
|
+
click: function () {
|
|
55
|
+
const notification = getNotification()
|
|
56
|
+
if (notification && typeof notification.close === 'function') notification.close()
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
normalized.buttons = buttons
|
|
61
|
+
return normalized
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const install = (RED) => {
|
|
65
|
+
if (!RED || typeof RED.notify !== 'function') return false
|
|
66
|
+
if (RED.notify[PATCH_MARKER]) return true
|
|
67
|
+
|
|
68
|
+
const originalNotify = RED.notify
|
|
69
|
+
const notifyWithClose = function (message, options, fixed, timeout) {
|
|
70
|
+
const notificationRef = { current: null }
|
|
71
|
+
const normalized = withCloseButton(
|
|
72
|
+
RED,
|
|
73
|
+
normalizeOptions(options, fixed, timeout),
|
|
74
|
+
() => notificationRef.current
|
|
75
|
+
)
|
|
76
|
+
notificationRef.current = originalNotify.call(this, message, normalized)
|
|
77
|
+
const notification = notificationRef.current
|
|
78
|
+
|
|
79
|
+
if (notification && typeof notification.update === 'function' && !notification.update[PATCH_MARKER]) {
|
|
80
|
+
const originalUpdate = notification.update
|
|
81
|
+
const updateWithClose = function (updatedMessage, updatedOptions) {
|
|
82
|
+
const options = typeof updatedOptions === 'number'
|
|
83
|
+
? { timeout: updatedOptions }
|
|
84
|
+
: normalizeOptions(updatedOptions)
|
|
85
|
+
return originalUpdate.call(this, updatedMessage, withCloseButton(RED, options, () => notification))
|
|
86
|
+
}
|
|
87
|
+
Object.defineProperty(updateWithClose, PATCH_MARKER, { value: true })
|
|
88
|
+
notification.update = updateWithClose
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return notification
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Object.defineProperty(notifyWithClose, PATCH_MARKER, { value: true })
|
|
95
|
+
Object.defineProperty(notifyWithClose, '__ultimateOriginalNotify', { value: originalNotify })
|
|
96
|
+
RED.notify = notifyWithClose
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return Object.freeze({ install })
|
|
101
|
+
}))
|
package/resources/upgradeToV8.js
CHANGED
|
@@ -339,19 +339,23 @@
|
|
|
339
339
|
if (remaining.length) throw new Error('Legacy node types remain after conversion: ' + remaining.map(node => node.type).join(', '))
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
function validateDeployable (RED) {
|
|
343
|
-
const
|
|
342
|
+
function validateDeployable (RED, plan) {
|
|
343
|
+
const migrating = new Set((plan && plan.entries ? plan.entries : []).map(entry => entry.node))
|
|
344
344
|
const unknown = []
|
|
345
345
|
collectAllNodes(RED).forEach(node => {
|
|
346
346
|
if (node.type === 'unknown' || (node._def && node._def.category === 'unknown')) {
|
|
347
347
|
unknown.push(node.id)
|
|
348
348
|
return
|
|
349
349
|
}
|
|
350
|
-
if (RED.editor && typeof RED.editor.validateNode === 'function') RED.editor.validateNode(node)
|
|
351
|
-
|
|
350
|
+
if (migrating.has(node) && RED.editor && typeof RED.editor.validateNode === 'function') RED.editor.validateNode(node)
|
|
351
|
+
// A legacy node can be incomplete (for example, a placeholder without a
|
|
352
|
+
// HUE/Matter config reference). It must first be persisted with its new
|
|
353
|
+
// type before the user can open and complete it. Keep its invalid marker
|
|
354
|
+
// in the editor, but do not let it deadlock the migration. Nodes outside
|
|
355
|
+
// the migration plan are preserved verbatim and are deliberately not
|
|
356
|
+
// validated: their configuration is unrelated to this upgrade.
|
|
352
357
|
})
|
|
353
358
|
if (unknown.length) throw new Error('Unknown nodes must be resolved before the automatic upgrade: ' + unknown.join(', '))
|
|
354
|
-
if (invalid.length) throw new Error('Invalid nodes must be corrected before the automatic upgrade: ' + invalid.join(', '))
|
|
355
359
|
}
|
|
356
360
|
|
|
357
361
|
function validatePreflight (RED, plan) {
|
|
@@ -360,19 +364,14 @@
|
|
|
360
364
|
...((plan && plan.missingStandalone && plan.missingStandalone.hue) || []),
|
|
361
365
|
...((plan && plan.missingStandalone && plan.missingStandalone.matter) || [])
|
|
362
366
|
])
|
|
363
|
-
const invalid = []
|
|
364
367
|
const unknown = []
|
|
365
368
|
collectAllNodes(RED).forEach(node => {
|
|
366
369
|
if (migrating.has(node) || expectedUnknown.has(node) || (plan && plan.ai && plan.ai.includes(node))) return
|
|
367
370
|
if (node.type === 'unknown' || (node._def && node._def.category === 'unknown')) {
|
|
368
371
|
unknown.push(node.id)
|
|
369
|
-
return
|
|
370
372
|
}
|
|
371
|
-
if (RED.editor && typeof RED.editor.validateNode === 'function') RED.editor.validateNode(node)
|
|
372
|
-
if (node.valid === false && node.d !== true) invalid.push(node.id)
|
|
373
373
|
})
|
|
374
374
|
if (unknown.length) throw new Error('Unknown nodes must be resolved before the automatic upgrade: ' + unknown.join(', '))
|
|
375
|
-
if (invalid.length) throw new Error('Invalid nodes must be corrected before the automatic upgrade: ' + invalid.join(', '))
|
|
376
375
|
}
|
|
377
376
|
|
|
378
377
|
function requestError (xhr, fallback) {
|
|
@@ -877,7 +876,7 @@
|
|
|
877
876
|
let deployed = false
|
|
878
877
|
try {
|
|
879
878
|
assertNoLegacyNodes(RED)
|
|
880
|
-
validateDeployable(RED)
|
|
879
|
+
validateDeployable(RED, plan)
|
|
881
880
|
progress('deploy')
|
|
882
881
|
await deployFlows(RED, {
|
|
883
882
|
...options,
|
|
@@ -1183,7 +1182,16 @@
|
|
|
1183
1182
|
? t('recovery_failed', 'Automatic migration stopped safely:') + ' ' + error.message
|
|
1184
1183
|
: t('failed', 'Automatic upgrade stopped safely. KNX Ultimate 8 was not activated:') + ' ' + error.message
|
|
1185
1184
|
}
|
|
1186
|
-
|
|
1185
|
+
let failureNotice
|
|
1186
|
+
failureNotice = RED.notify(failureMessage, {
|
|
1187
|
+
type: 'error',
|
|
1188
|
+
fixed: true,
|
|
1189
|
+
buttons: [{
|
|
1190
|
+
text: t('ok', 'OK'),
|
|
1191
|
+
class: 'primary',
|
|
1192
|
+
click: () => failureNotice.close()
|
|
1193
|
+
}]
|
|
1194
|
+
})
|
|
1187
1195
|
if (!keepDeployBlocked) offerRecoveryRetry()
|
|
1188
1196
|
})
|
|
1189
1197
|
.finally(() => unlockEditor({ keepDeployBlocked }))
|