node-red-contrib-knx-ultimate 6.2.3-beta.3 → 6.2.3-beta.4
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 +2 -1
- package/nodes/commonFunctions.js +46 -2
- package/nodes/locales/de/matter-config.html +1 -1
- package/nodes/locales/en/matter-config.html +1 -1
- package/nodes/locales/es/matter-config.html +1 -1
- package/nodes/locales/fr/matter-config.html +1 -1
- package/nodes/locales/it/matter-config.html +1 -1
- package/nodes/locales/zh-CN/matter-config.html +1 -1
- package/nodes/matter-config.html +67 -6
- package/nodes/matter-config.js +51 -0
- package/nodes/utils/matterEngine.mjs +111 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
-
**Version 6.2.3-beta.
|
|
9
|
+
**Version 6.2.3-beta.4** - August 2026<br/>
|
|
10
10
|
|
|
11
|
+
- **Matter Controller — live commissioning progress**: replaced the pairing overlay spinner with a milestone-based progress bar driven by the actual matter.js commissioning flow. The blocking panel now describes the active discovery, PASE, device-information, fail-safe, regulatory, time-sync, attestation, credential, access-control, CASE reconnect and completion phase in English; when exposed by the commissionee, its product name, Vendor ID and Product ID appear below the current operation. Progress polling is local to the Node-RED editor, stops with the pairing request and is isolated by operation ID across editor tabs.<br/>
|
|
11
12
|
- **HUE Controller — unified Hue API v2 node**: added one controller node covering the complete set of existing Hue functions: lights and grouped lights, plugs, buttons, Tap dial, motion and camera motion, contacts, light level, temperature, humidity, scenes, battery, Zigbee connectivity and device software update. The compact device-function selector uses one third of the available width and opens the established dedicated editor with its localized labels; KNX mapping fields are hidden whenever no **KNX Gateway** is selected, including `none`, an empty value and **Add new...**. The YouTube tutorial playlist now appears once at the top of the editor, while routine inline `form-tips` are removed from the unified UI and a compact migration privacy disclaimer remains directly below the conversion button. The migration action runs entirely inside the browser and changes only the legacy HUE nodes requiring conversion, preserving every existing property, config reference, group, position and link; no flow or node data leaves the editor and the workspace remains undeployed for user review. After a successful conversion, an editable email draft addressed to the author opens without navigating the top-level Node-RED page and contains only the converted-node count and space for optional notes; the user decides whether to send it and nothing is sent automatically. The final Node-RED message offers an optional support button, and the PayPal donation page opens only when the user clicks it. Each profile executes the same mature runtime contract, preserving KNX telegram types, Hue event/status direction, flow pins and loop protection. Runtime implementations, editor definitions, templates and all six translation catalogs are embedded as package-internal HUE Controller profiles; registration order no longer matters, the KNX and Hue gateway fields remain real config-node selectors, and the controller has no runtime or editor dependency on deprecated node types. The original Hue Light node remains unchanged. Existing dedicated Hue nodes remain fully compatible and registered, use Node-RED's special `deprecated` category so they no longer appear in the palette, retain the lighter `#E7E9F6` color and `(deprecated)` canvas suffix without changing saved names, and show a localized HUE Controller migration notice at the top of their editors.<br/>
|
|
12
13
|
- **HUE Controller — complete per-function help**: the established help of every dedicated Hue node is now incorporated under the corresponding HUE Controller function key, with an anchor index and automatic sidebar focus when the function changes. The same 15-section reference is included in the Controller wiki documentation in EN, IT, DE, FR, ES and zh-CN; legacy deprecation notices are deliberately excluded from the embedded copies.<br/>
|
|
13
14
|
- **HUE Controller — legacy-independent maintenance boundary**: private runtime profiles and the private editor/template/translation sources are now the canonical implementation of HUE Controller. Its generator and consistency check read only those private sources and never inspect the frozen legacy nodes, so future Controller fixes remain isolated and the dedicated deprecated nodes can eventually be removed without breaking either runtime or UI. Added detailed architecture and lifecycle comments around runtime constructor capture, RED facades, editor mounting, profile drafts, dynamic pins, translations and the Hue Light bootstrap path.<br/>
|
package/nodes/commonFunctions.js
CHANGED
|
@@ -800,18 +800,51 @@ module.exports = (RED) => {
|
|
|
800
800
|
}
|
|
801
801
|
})
|
|
802
802
|
|
|
803
|
+
// MATTER: current commissioning milestone for the blocking editor overlay.
|
|
804
|
+
// The operation id keeps simultaneous editor tabs from displaying each other's progress.
|
|
805
|
+
RED.httpAdmin.get('/KNXUltimateMatterPairProgress', RED.auth.needsPermission('matter-config.read'), (req, res) => {
|
|
806
|
+
try {
|
|
807
|
+
const matterServer = RED.nodes.getNode(req.query.serverId)
|
|
808
|
+
if (matterServer === null || matterServer === undefined) {
|
|
809
|
+
res.json({ active: false, percent: 0, error: 'PLEASE DEPLOY FIRST: then try again.' })
|
|
810
|
+
return
|
|
811
|
+
}
|
|
812
|
+
res.json(matterServer.getCommissioningProgress(req.query.operationId))
|
|
813
|
+
} catch (error) {
|
|
814
|
+
RED.log.error(`Err KNXUltimateMatterPairProgress: ${error.message}`)
|
|
815
|
+
res.json({ active: false, percent: 0, error: error.message })
|
|
816
|
+
}
|
|
817
|
+
})
|
|
818
|
+
|
|
803
819
|
// MATTER: commissions (pairs) a new Matter device using a pairing code or QR code string
|
|
804
820
|
RED.httpAdmin.get('/KNXUltimateMatterPair', RED.auth.needsPermission('matter-config.write'), async (req, res) => {
|
|
821
|
+
let matterServer
|
|
822
|
+
const requestedOperationId = String(req.query.operationId || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 80)
|
|
823
|
+
// Keep direct callers of the pre-progress endpoint backward compatible. The editor
|
|
824
|
+
// supplies its own id so it can poll; legacy callers receive a server-only id.
|
|
825
|
+
const operationId = requestedOperationId || `server-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
|
805
826
|
try {
|
|
806
|
-
|
|
827
|
+
matterServer = RED.nodes.getNode(req.query.serverId)
|
|
807
828
|
if (matterServer === null || matterServer === undefined) {
|
|
808
829
|
res.json({ error: 'PLEASE DEPLOY FIRST: then try again.' })
|
|
809
830
|
return
|
|
810
831
|
}
|
|
811
|
-
|
|
832
|
+
if (!matterServer.beginCommissioningProgress(operationId)) {
|
|
833
|
+
res.json({ error: 'Another Matter commissioning operation is already in progress.' })
|
|
834
|
+
return
|
|
835
|
+
}
|
|
836
|
+
const nodeId = await matterServer.commission(req.query.code, {
|
|
837
|
+
targetHost: req.query.targetHost,
|
|
838
|
+
onProgress: (progress) => matterServer.reportCommissioningProgress(operationId, progress)
|
|
839
|
+
})
|
|
812
840
|
const requestedName = String(req.query.name || '').trim()
|
|
813
841
|
let renameError = null
|
|
814
842
|
if (requestedName !== '') {
|
|
843
|
+
matterServer.reportCommissioningProgress(operationId, {
|
|
844
|
+
phase: 'naming',
|
|
845
|
+
percent: 99,
|
|
846
|
+
message: 'Applying the requested device name…'
|
|
847
|
+
})
|
|
815
848
|
try {
|
|
816
849
|
await matterServer.renameCommissionedNode(nodeId, requestedName)
|
|
817
850
|
} catch (error) {
|
|
@@ -823,8 +856,19 @@ module.exports = (RED) => {
|
|
|
823
856
|
try {
|
|
824
857
|
device = matterServer.getCommissionedNodesDetails().find((item) => String(item.nodeId) === String(nodeId)) || null
|
|
825
858
|
} catch (error) { /* empty */ }
|
|
859
|
+
matterServer.endCommissioningProgress(operationId, {
|
|
860
|
+
phase: 'complete',
|
|
861
|
+
percent: 100,
|
|
862
|
+
message: 'Matter commissioning completed successfully.'
|
|
863
|
+
})
|
|
826
864
|
res.json({ nodeId, name: device?.name, productName: device?.productName, vendorName: device?.vendorName, renameError })
|
|
827
865
|
} catch (error) {
|
|
866
|
+
try {
|
|
867
|
+
matterServer?.endCommissioningProgress(operationId, {
|
|
868
|
+
phase: 'error',
|
|
869
|
+
message: `Commissioning failed: ${error.message}`
|
|
870
|
+
})
|
|
871
|
+
} catch (progressError) { /* empty */ }
|
|
828
872
|
const targetHost = req.query.targetHost ? ` targetHost=${req.query.targetHost}` : ''
|
|
829
873
|
RED.log.error(`Err KNXUltimateMatterPair:${targetHost} ${error.stack || error.message}`)
|
|
830
874
|
res.json({ error: error.message })
|
|
@@ -15,7 +15,7 @@ Der Controller kommuniziert mit den Geräten über das **IP-Netzwerk** (WLAN, Et
|
|
|
15
15
|
|
|
16
16
|
Statt den QR-Payload einzutippen, klicke auf **Webcam**, um ihn live zu scannen, oder auf **Bild**, um ihn aus einem lokalen Foto zu lesen. Sowohl normale dunkle QR-Codes auf hellem Grund als auch invertierte weiße Codes auf dunklem Grund werden unterstützt. Die Dekodierung erfolgt vollständig im Browser; nach dem Lesen eines gültigen Matter-QR-Codes füllt der Editor das Kopplungscode-Feld und startet die Kopplung sofort. Gib den optionalen Gerätenamen vor dem Scannen ein. Ein manuell eingegebener Code startet weiterhin erst mit **KOPPELN**. Der Live-Zugriff auf die Webcam setzt voraus, dass der Editor über HTTPS oder von `localhost` geöffnet wurde; andernfalls erklärt der Editor die Einschränkung und das Laden eines Bildes bleibt verfügbar.
|
|
17
17
|
|
|
18
|
-
Während der Kommissionierung überdeckt ein blockierender
|
|
18
|
+
Während der Kommissionierung überdeckt ein blockierender Bildschirm den Editor und verhindert weitere Klicks, bis der Vorgang erfolgreich abgeschlossen wird oder fehlschlägt. Der Fortschrittsbalken folgt den tatsächlichen matter.js-Meilensteinen und zeigt den aktuellen Vorgang auf Englisch. Wenn das Gerät seine Produktidentität bereitstellt, werden außerdem Produktname, Vendor ID und Product ID angezeigt.
|
|
19
19
|
|
|
20
20
|
Wenn das Gerät fabrikneu ist und nur Bluetooth-Kommissionierung unterstützt, kopple es zuerst mit der Hersteller-App oder einem anderen Matter-Controller (Alexa, Google Home, Apple Home) und nutze dann dessen Funktion **"mit weiterem Hub teilen"**, um einen neuen Kopplungscode für KNX-Ultimate zu erzeugen. So tritt das Gerät mehreren Fabrics gleichzeitig bei.
|
|
21
21
|
|
|
@@ -15,7 +15,7 @@ The controller talks to the devices over the **IP network** (WiFi, Ethernet, or
|
|
|
15
15
|
|
|
16
16
|
Instead of typing the QR payload, click **Webcam** to scan it live or **Image** to read it from a local picture. Both standard dark-on-light and inverted white-on-dark QR codes are supported. Decoding takes place entirely in the browser; after a valid Matter QR is read, the editor fills the pairing-code field and immediately starts pairing. Enter the optional device name before scanning if desired. A manually typed code still starts only when you click **PAIR**. Live webcam access requires the editor to be opened over HTTPS or from `localhost`; when that is not possible, the editor explains the limitation and image loading remains available.
|
|
17
17
|
|
|
18
|
-
While commissioning is in progress, a blocking
|
|
18
|
+
While commissioning is in progress, a blocking panel covers the editor and prevents further clicks until the operation succeeds or fails. Its progress bar follows the actual matter.js milestones and shows the current operation in English. When the device exposes its product identity, the panel also shows its product name, Vendor ID and Product ID.
|
|
19
19
|
|
|
20
20
|
If the device is brand new and only supports Bluetooth commissioning, first pair it with its vendor app or another Matter controller (Alexa, Google Home, Apple Home), then use that controller's **"share / pair with another hub"** function to generate a new pairing code for KNX-Ultimate. This way the device joins multiple fabrics at once.
|
|
21
21
|
|
|
@@ -15,7 +15,7 @@ El controlador se comunica con los dispositivos a través de la **red IP** (WiFi
|
|
|
15
15
|
|
|
16
16
|
En lugar de escribir el payload QR, pulsa **Webcam** para escanearlo en directo o **Imagen** para leerlo desde una foto local. Se admiten tanto los códigos QR estándar oscuros sobre fondo claro como los invertidos blancos sobre fondo oscuro. La decodificación se realiza íntegramente en el navegador; tras leer un QR Matter válido, el editor rellena el campo del código e inicia inmediatamente el emparejamiento. Introduce antes el nombre opcional del dispositivo si lo deseas. Un código escrito manualmente sigue iniciándose solo al pulsar **EMPAREJAR**. El acceso en directo a la webcam requiere que el editor se abra mediante HTTPS o desde `localhost`; si no es posible, el editor explica la limitación y la carga de imágenes sigue disponible.
|
|
17
17
|
|
|
18
|
-
Durante el comisionado, un panel
|
|
18
|
+
Durante el comisionado, un panel bloqueante cubre el editor e impide más clics hasta que la operación finaliza correctamente o con un error. La barra de progreso sigue los hitos reales de matter.js y muestra en inglés la operación actual. Cuando el dispositivo expone su identidad de producto, el panel también muestra el nombre del producto, Vendor ID y Product ID.
|
|
19
19
|
|
|
20
20
|
Si el dispositivo es nuevo de fábrica y solo admite emparejamiento por Bluetooth, emparéjalo primero con la app del fabricante o con otro controlador Matter (Alexa, Google Home, Apple Home) y usa después su función **"compartir / emparejar con otro hub"** para generar un nuevo código para KNX-Ultimate. Así el dispositivo se une a varias fabrics a la vez.
|
|
21
21
|
|
|
@@ -15,7 +15,7 @@ Le contrôleur communique avec les appareils via le **réseau IP** (WiFi, Ethern
|
|
|
15
15
|
|
|
16
16
|
Au lieu de saisir le payload QR, cliquez sur **Webcam** pour le scanner en direct ou sur **Image** pour le lire depuis une photo locale. Les QR codes standards sombres sur fond clair et les codes inversés blancs sur fond sombre sont pris en charge. Le décodage s'effectue entièrement dans le navigateur ; après la lecture d'un QR Matter valide, l'éditeur remplit le champ du code et démarre immédiatement l'appairage. Saisissez auparavant le nom facultatif de l'appareil si nécessaire. Un code saisi manuellement ne démarre toujours qu'après un clic sur **APPAIRER**. L'accès direct à la webcam exige que l'éditeur soit ouvert via HTTPS ou depuis `localhost` ; sinon l'éditeur explique cette limitation et le chargement d'une image reste disponible.
|
|
17
17
|
|
|
18
|
-
Pendant le commissionnement, un panneau
|
|
18
|
+
Pendant le commissionnement, un panneau bloquant recouvre l'éditeur et empêche tout autre clic jusqu'à la réussite ou l'échec de l'opération. La barre de progression suit les étapes réelles de matter.js et décrit l'opération en cours en anglais. Lorsque l'appareil expose son identité produit, le panneau affiche aussi le nom du produit, le Vendor ID et le Product ID.
|
|
19
19
|
|
|
20
20
|
Si l'appareil est neuf et ne prend en charge que l'appairage Bluetooth, appairez-le d'abord avec l'app du fabricant ou un autre contrôleur Matter (Alexa, Google Home, Apple Home), puis utilisez sa fonction **« partager / appairer avec un autre hub »** pour générer un nouveau code pour KNX-Ultimate. L'appareil rejoint ainsi plusieurs fabrics à la fois.
|
|
21
21
|
|
|
@@ -15,7 +15,7 @@ Il controller comunica con i dispositivi tramite la **rete IP** (WiFi, Ethernet,
|
|
|
15
15
|
|
|
16
16
|
In alternativa alla digitazione del payload QR, clicca **Webcam** per leggerlo in tempo reale oppure **Immagine** per ricavarlo da una foto locale. Sono supportati sia i QR standard scuri su fondo chiaro sia quelli invertiti bianchi su fondo scuro. La decodifica avviene interamente nel browser; dopo la lettura di un QR Matter valido, l'editor compila il campo del codice e avvia immediatamente l'associazione. Se desideri assegnare un nome, inseriscilo prima della scansione. Un codice digitato manualmente parte ancora soltanto quando clicchi **ASSOCIA**. L'accesso live alla webcam richiede che l'editor sia aperto tramite HTTPS o da `localhost`; quando non è possibile, l'editor spiega il limite e il caricamento dell'immagine rimane disponibile.
|
|
17
17
|
|
|
18
|
-
Durante il commissioning, un pannello
|
|
18
|
+
Durante il commissioning, un pannello bloccante copre l'editor e impedisce ulteriori clic finché l'operazione non termina con successo o con un errore. La barra di avanzamento segue le fasi reali di matter.js e mostra in inglese l'operazione corrente. Quando il device espone la propria identità di prodotto, il pannello mostra anche nome prodotto, Vendor ID e Product ID.
|
|
19
19
|
|
|
20
20
|
Se il dispositivo è nuovo di fabbrica e supporta solo il commissioning Bluetooth, associalo prima con l'app del produttore o con un altro controller Matter (Alexa, Google Home, Apple Home), poi usa la funzione **"condividi / abbina con altro hub"** di quel controller per generare un nuovo codice di abbinamento per KNX-Ultimate. In questo modo il dispositivo entra in più fabric contemporaneamente.
|
|
21
21
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
无需手动输入二维码 payload:点击**摄像头**可实时扫描,点击**图片**可从本地照片读取。支持浅色背景上的标准深色二维码以及深色背景上的反白二维码。解码完全在浏览器中进行;读取到有效的 Matter 二维码后,编辑器会填写配对码并立即自动开始配对。如需设置可选设备名称,请在扫描前填写。手动输入的配对码仍需点击**配对**才会开始。实时使用摄像头要求通过 HTTPS 或从 `localhost` 打开编辑器;若条件不满足,编辑器会说明此限制,而图片加载功能仍然可用。
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
配对期间,阻塞式面板会覆盖编辑器并阻止进一步点击,直到操作成功完成或失败。进度条跟随 matter.js 的实际阶段,并以英文显示当前操作。当设备公开产品身份时,面板还会显示产品名称、Vendor ID 和 Product ID。
|
|
19
19
|
|
|
20
20
|
如果设备是全新的且仅支持蓝牙配对,请先用厂商 App 或其他 Matter 控制器(Alexa、Google Home、Apple Home)配对,然后使用其**"与其他中枢共享/配对"**功能为 KNX-Ultimate 生成新的配对码。这样设备可以同时加入多个 fabric。
|
|
21
21
|
|
package/nodes/matter-config.html
CHANGED
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
var $devicesBody = $("#matter-devices-body");
|
|
23
23
|
var $refreshDevices = $("#matterRefreshDevices");
|
|
24
24
|
var $pairSpinner = $("#matterPairSpinner");
|
|
25
|
+
var $pairProgress = $("#matter-pairing-progress");
|
|
26
|
+
var $pairProgressBar = $("#matter-pairing-progress-bar");
|
|
27
|
+
var $pairProgressPercent = $("#matter-pairing-progress-percent");
|
|
28
|
+
var $pairProgressDescription = $("#matter-pairing-progress-description");
|
|
29
|
+
var $pairProgressDevice = $("#matter-pairing-progress-device");
|
|
25
30
|
var $storageExport = $('#matter-storage-export');
|
|
26
31
|
var $storageImport = $('#matter-storage-import');
|
|
27
32
|
var $storageFile = $('#matter-storage-file');
|
|
@@ -35,6 +40,8 @@
|
|
|
35
40
|
var qrScannerControls = null;
|
|
36
41
|
var qrScannerStarting = false;
|
|
37
42
|
var qrUtils = window.KNXUltimateMatterQrScanner;
|
|
43
|
+
var pairingProgressTimer = null;
|
|
44
|
+
var pairingOperationId = '';
|
|
38
45
|
|
|
39
46
|
function qrText(key) {
|
|
40
47
|
return RED._("node-red-contrib-knx-ultimate/matter-config:matter-config.properties." + key);
|
|
@@ -183,7 +190,10 @@
|
|
|
183
190
|
this.value = '';
|
|
184
191
|
scanQrImage(file);
|
|
185
192
|
});
|
|
186
|
-
node._matterQrCleanup =
|
|
193
|
+
node._matterQrCleanup = function () {
|
|
194
|
+
stopQrCamera();
|
|
195
|
+
stopPairingProgress();
|
|
196
|
+
};
|
|
187
197
|
|
|
188
198
|
function storageExportUrl() {
|
|
189
199
|
var adminRoot = (RED.settings && typeof RED.settings.httpAdminRoot === 'string') ? RED.settings.httpAdminRoot : '/';
|
|
@@ -209,6 +219,44 @@
|
|
|
209
219
|
});
|
|
210
220
|
}
|
|
211
221
|
|
|
222
|
+
function setPairingProgress(data) {
|
|
223
|
+
if (!data) return;
|
|
224
|
+
var percent = Number(data.percent);
|
|
225
|
+
if (!isFinite(percent)) percent = 0;
|
|
226
|
+
percent = Math.max(0, Math.min(100, Math.round(percent)));
|
|
227
|
+
$pairProgress.attr('aria-valuenow', percent);
|
|
228
|
+
$pairProgressBar.css('width', percent + '%');
|
|
229
|
+
$pairProgressPercent.text(percent + '%');
|
|
230
|
+
if (data.message) $pairProgressDescription.text(data.message);
|
|
231
|
+
if (data.device && data.device.productName) {
|
|
232
|
+
var details = data.device.productName;
|
|
233
|
+
if (data.device.vendorId !== undefined) details += ' · VID ' + data.device.vendorId;
|
|
234
|
+
if (data.device.productId !== undefined) details += ' · PID ' + data.device.productId;
|
|
235
|
+
$pairProgressDevice.text(details).show();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function stopPairingProgress() {
|
|
240
|
+
if (pairingProgressTimer !== null) clearInterval(pairingProgressTimer);
|
|
241
|
+
pairingProgressTimer = null;
|
|
242
|
+
pairingOperationId = '';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function startPairingProgress(operationId) {
|
|
246
|
+
stopPairingProgress();
|
|
247
|
+
pairingOperationId = operationId;
|
|
248
|
+
$pairProgressDevice.empty().hide();
|
|
249
|
+
setPairingProgress({ percent: 2, message: 'Preparing Matter commissioning…' });
|
|
250
|
+
pairingProgressTimer = setInterval(function () {
|
|
251
|
+
var activeOperationId = pairingOperationId;
|
|
252
|
+
if (!activeOperationId) return;
|
|
253
|
+
$.getJSON('KNXUltimateMatterPairProgress?serverId=' + encodeURIComponent(node.id) + '&operationId=' + encodeURIComponent(activeOperationId) + '&_=' + Date.now(), function (data) {
|
|
254
|
+
if (activeOperationId !== pairingOperationId || !data || data.operationId !== activeOperationId) return;
|
|
255
|
+
setPairingProgress(data);
|
|
256
|
+
});
|
|
257
|
+
}, 400);
|
|
258
|
+
}
|
|
259
|
+
|
|
212
260
|
function renderDevices(devices) {
|
|
213
261
|
$devicesBody.empty();
|
|
214
262
|
if (!Array.isArray(devices) || devices.length === 0) {
|
|
@@ -293,15 +341,20 @@
|
|
|
293
341
|
return;
|
|
294
342
|
}
|
|
295
343
|
$pairButton.prop('disabled', true);
|
|
344
|
+
var operationId = 'matter-' + Date.now() + '-' + Math.random().toString(36).slice(2, 10);
|
|
345
|
+
var pairingSucceeded = false;
|
|
346
|
+
startPairingProgress(operationId);
|
|
296
347
|
$pairSpinner.show();
|
|
297
348
|
$.ajax({
|
|
298
|
-
url: 'KNXUltimateMatterPair?serverId=' + node.id + '&code=' + encodeURIComponent(code) + (desiredName !== '' ? '&name=' + encodeURIComponent(desiredName) : ''),
|
|
349
|
+
url: 'KNXUltimateMatterPair?serverId=' + encodeURIComponent(node.id) + '&operationId=' + encodeURIComponent(operationId) + '&code=' + encodeURIComponent(code) + (desiredName !== '' ? '&name=' + encodeURIComponent(desiredName) : ''),
|
|
299
350
|
dataType: 'json',
|
|
300
351
|
timeout: 180000
|
|
301
352
|
}).done(function (data) {
|
|
302
353
|
if (data && data.error) {
|
|
303
354
|
notifyPairingError(data.error);
|
|
304
355
|
} else {
|
|
356
|
+
pairingSucceeded = true;
|
|
357
|
+
setPairingProgress({ percent: 100, message: 'Matter commissioning completed successfully.' });
|
|
305
358
|
var pairedName = data.name || data.productName || data.nodeId;
|
|
306
359
|
RED.notify(RED._("node-red-contrib-knx-ultimate/matter-config:matter-config.properties.pairing_ok") + ' ' + pairedName + ' (Node ID ' + data.nodeId + ')', { type: 'success' });
|
|
307
360
|
if (data.renameError) RED.notify(data.renameError, { type: 'warning', fixed: true });
|
|
@@ -314,8 +367,10 @@
|
|
|
314
367
|
try { msg = jqXHR.responseJSON.error; } catch (e) { msg = errorThrown || textStatus; }
|
|
315
368
|
notifyPairingError(msg);
|
|
316
369
|
}).always(function () {
|
|
370
|
+
stopPairingProgress();
|
|
317
371
|
$pairButton.prop('disabled', false);
|
|
318
|
-
$pairSpinner.hide();
|
|
372
|
+
if (pairingSucceeded) setTimeout(function () { $pairSpinner.hide(); }, 350);
|
|
373
|
+
else $pairSpinner.hide();
|
|
319
374
|
});
|
|
320
375
|
});
|
|
321
376
|
|
|
@@ -496,9 +551,15 @@
|
|
|
496
551
|
|
|
497
552
|
<div id="matterPairSpinner" role="alertdialog" aria-modal="true" aria-labelledby="matter-pairing-overlay-message"
|
|
498
553
|
style="display:none; position:fixed; inset:0; z-index:100000; background:rgba(20, 24, 28, 0.72); cursor:wait;">
|
|
499
|
-
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%, -50%);
|
|
500
|
-
<
|
|
501
|
-
<
|
|
554
|
+
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%, -50%); width:460px; max-width:80vw; padding:28px 34px; border-radius:8px; background:#fff; color:#333; text-align:center; box-shadow:0 8px 30px rgba(0,0,0,0.4);">
|
|
555
|
+
<strong id="matter-pairing-overlay-message" data-i18n="matter-config.properties.pairing_wait" style="display:block; margin-bottom:18px; font-size:16px;"></strong>
|
|
556
|
+
<div id="matter-pairing-progress" role="progressbar" aria-label="Matter commissioning progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"
|
|
557
|
+
style="position:relative; width:100%; height:20px; overflow:hidden; border-radius:10px; background:#66717a; box-shadow:inset 0 1px 3px rgba(0,0,0,0.22);">
|
|
558
|
+
<div id="matter-pairing-progress-bar" style="width:0; height:100%; border-radius:10px; background:#1b7d33; transition:width 250ms ease;"></div>
|
|
559
|
+
<span id="matter-pairing-progress-percent" style="position:absolute; inset:0; color:#fff; font-size:12px; font-weight:bold; line-height:20px; text-shadow:0 1px 2px #000;">0%</span>
|
|
560
|
+
</div>
|
|
561
|
+
<div id="matter-pairing-progress-description" aria-live="polite" style="min-height:20px; margin-top:14px; font-size:14px; line-height:20px;">Preparing Matter commissioning…</div>
|
|
562
|
+
<div id="matter-pairing-progress-device" style="display:none; margin-top:7px; color:#666; font-size:12px;"></div>
|
|
502
563
|
</div>
|
|
503
564
|
</div>
|
|
504
565
|
|
package/nodes/matter-config.js
CHANGED
|
@@ -22,6 +22,13 @@ module.exports = (RED) => {
|
|
|
22
22
|
node.matterManager = null
|
|
23
23
|
node.timerMatterConfigCheckState = null
|
|
24
24
|
node.storageOperation = null
|
|
25
|
+
node.commissioningProgress = {
|
|
26
|
+
active: false,
|
|
27
|
+
operationId: '',
|
|
28
|
+
sequence: 0,
|
|
29
|
+
percent: 0,
|
|
30
|
+
message: 'Waiting for commissioning to start.'
|
|
31
|
+
}
|
|
25
32
|
try {
|
|
26
33
|
node.sysLogger = loggerSetup({ loglevel: node.loglevel, setPrefix: 'matter-config.js' })
|
|
27
34
|
} catch (error) { console.log(error.stack) }
|
|
@@ -247,6 +254,50 @@ module.exports = (RED) => {
|
|
|
247
254
|
return node.matterManager.commission(_pairingCode, _options)
|
|
248
255
|
}
|
|
249
256
|
|
|
257
|
+
node.beginCommissioningProgress = (_operationId) => {
|
|
258
|
+
if (node.commissioningProgress.active === true) return false
|
|
259
|
+
node.commissioningProgress = {
|
|
260
|
+
active: true,
|
|
261
|
+
operationId: String(_operationId || ''),
|
|
262
|
+
sequence: node.commissioningProgress.sequence + 1,
|
|
263
|
+
percent: 2,
|
|
264
|
+
phase: 'starting',
|
|
265
|
+
message: 'Preparing Matter commissioning…',
|
|
266
|
+
updatedAt: Date.now()
|
|
267
|
+
}
|
|
268
|
+
return true
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
node.reportCommissioningProgress = (_operationId, _progress = {}) => {
|
|
272
|
+
if (node.commissioningProgress.operationId !== String(_operationId || '')) return false
|
|
273
|
+
const numericPercent = Number(_progress.percent)
|
|
274
|
+
const previousPercent = Number(node.commissioningProgress.percent) || 0
|
|
275
|
+
node.commissioningProgress = {
|
|
276
|
+
...node.commissioningProgress,
|
|
277
|
+
..._progress,
|
|
278
|
+
active: true,
|
|
279
|
+
operationId: node.commissioningProgress.operationId,
|
|
280
|
+
sequence: node.commissioningProgress.sequence + 1,
|
|
281
|
+
percent: Number.isFinite(numericPercent) ? Math.max(previousPercent, Math.min(100, Math.max(0, numericPercent))) : previousPercent,
|
|
282
|
+
updatedAt: Date.now()
|
|
283
|
+
}
|
|
284
|
+
return true
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
node.endCommissioningProgress = (_operationId, _progress = {}) => {
|
|
288
|
+
if (node.commissioningProgress.operationId !== String(_operationId || '')) return false
|
|
289
|
+
node.reportCommissioningProgress(_operationId, _progress)
|
|
290
|
+
node.commissioningProgress.active = false
|
|
291
|
+
return true
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
node.getCommissioningProgress = (_operationId) => {
|
|
295
|
+
if (node.commissioningProgress.operationId !== String(_operationId || '')) {
|
|
296
|
+
return { active: false, percent: 0, message: 'Waiting for commissioning to start.' }
|
|
297
|
+
}
|
|
298
|
+
return { ...node.commissioningProgress }
|
|
299
|
+
}
|
|
300
|
+
|
|
250
301
|
node.removeCommissionedNode = async (_nodeId) => {
|
|
251
302
|
if (node.matterManager === null) throw new Error('Matter controller not started')
|
|
252
303
|
return node.matterManager.removeNode(_nodeId)
|
|
@@ -88,15 +88,95 @@ class classMatter extends EventEmitter {
|
|
|
88
88
|
const { Environment, StorageService, Logger, LogLevel } = await import('@matter/main')
|
|
89
89
|
const { Seconds } = await import('@matter/general')
|
|
90
90
|
const { CommissioningController } = await import('@project-chip/matter.js')
|
|
91
|
+
const { ControllerCommissioningFlow } = await import('@matter/protocol')
|
|
91
92
|
const { NodeStates } = await import('@project-chip/matter.js/device')
|
|
92
93
|
const { ManualPairingCodeCodec, QrPairingCodeCodec, NodeId } = await import('@matter/main/types')
|
|
93
94
|
const { BasicInformation, GeneralCommissioning } = await import('@matter/main/clusters')
|
|
94
95
|
this._api = {
|
|
95
|
-
Environment, StorageService, Logger, LogLevel, Seconds, CommissioningController, NodeStates, ManualPairingCodeCodec, QrPairingCodeCodec, NodeId, BasicInformation, GeneralCommissioning
|
|
96
|
+
Environment, StorageService, Logger, LogLevel, Seconds, CommissioningController, ControllerCommissioningFlow, NodeStates, ManualPairingCodeCodec, QrPairingCodeCodec, NodeId, BasicInformation, GeneralCommissioning
|
|
96
97
|
}
|
|
97
98
|
return this._api
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
_reportCommissioningProgress = (_callback, progress) => {
|
|
102
|
+
if (typeof _callback !== 'function') return
|
|
103
|
+
try {
|
|
104
|
+
_callback(progress)
|
|
105
|
+
} catch (error) {
|
|
106
|
+
this._log('warn', `classMatter: commissioning progress callback: ${error.message}`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_createProgressCommissioningFlow = (_callback) => {
|
|
111
|
+
const BaseFlow = this._api.ControllerCommissioningFlow
|
|
112
|
+
const report = (progress) => this._reportCommissioningProgress(_callback, progress)
|
|
113
|
+
const descriptions = {
|
|
114
|
+
GetInitialData: { percent: 22, message: 'Reading commissioning information from the device…' },
|
|
115
|
+
'GeneralCommissioning.ArmFailsafe': { percent: 30, message: 'Arming the device fail-safe timer…' },
|
|
116
|
+
'GeneralCommissioning.ConfigureRegulatoryInformation': { percent: 36, message: 'Applying regulatory configuration…' },
|
|
117
|
+
'TimeSynchronization.SynchronizeTime': { percent: 42, message: 'Synchronizing the device clock…' },
|
|
118
|
+
'OperationalCredentials.DeviceAttestation': { percent: 50, message: 'Verifying device identity and attestation…' },
|
|
119
|
+
'OperationalCredentials.Certificates': { percent: 62, message: 'Installing operational credentials…' },
|
|
120
|
+
AccessControl: { percent: 72, message: 'Configuring Matter access control…' },
|
|
121
|
+
'NetworkCommissioning.Validate': { percent: 76, message: 'Checking the device network configuration…' },
|
|
122
|
+
'NetworkCommissioning.Wifi': { percent: 78, message: 'Connecting the device to the Wi-Fi network…' },
|
|
123
|
+
'NetworkCommissioning.Thread': { percent: 78, message: 'Connecting the device to the Thread network…' },
|
|
124
|
+
Reconnect: { percent: 84, message: 'Reconnecting through the operational CASE session…' },
|
|
125
|
+
'GeneralCommissioning.Complete': { percent: 92, message: 'Completing commissioning on the device…' },
|
|
126
|
+
'OperationalCredentials.UpdateFabricLabel': { percent: 96, message: 'Applying the Matter fabric label…' },
|
|
127
|
+
'AdditionalLogic.AddDefaultOtaProvider': { percent: 97, message: 'Configuring the default OTA provider…' }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return class ProgressCommissioningFlow extends BaseFlow {
|
|
131
|
+
constructor (...args) {
|
|
132
|
+
super(...args)
|
|
133
|
+
this.commissioningSteps.forEach((step) => {
|
|
134
|
+
const stepLogic = step.stepLogic
|
|
135
|
+
step.stepLogic = async () => {
|
|
136
|
+
const description = descriptions[step.name] || {
|
|
137
|
+
percent: 20,
|
|
138
|
+
message: `Running Matter commissioning step ${step.stepNumber}.${step.subStepNumber}: ${step.name}…`
|
|
139
|
+
}
|
|
140
|
+
const device = this.collectedCommissioningData?.productName
|
|
141
|
+
? {
|
|
142
|
+
productName: this.collectedCommissioningData.productName,
|
|
143
|
+
vendorId: this.collectedCommissioningData.vendorId,
|
|
144
|
+
productId: this.collectedCommissioningData.productId
|
|
145
|
+
}
|
|
146
|
+
: undefined
|
|
147
|
+
report({
|
|
148
|
+
phase: 'commissioning',
|
|
149
|
+
step: `${step.stepNumber}.${step.subStepNumber}`,
|
|
150
|
+
stepName: step.name,
|
|
151
|
+
percent: description.percent,
|
|
152
|
+
message: description.message,
|
|
153
|
+
device
|
|
154
|
+
})
|
|
155
|
+
const result = await stepLogic()
|
|
156
|
+
// GetInitialData is the first point where the commissionee may have exposed
|
|
157
|
+
// its product identity. Publish it immediately instead of waiting for the
|
|
158
|
+
// following step to make the editor show it.
|
|
159
|
+
if (step.name === 'GetInitialData' && this.collectedCommissioningData?.productName) {
|
|
160
|
+
report({
|
|
161
|
+
phase: 'commissioning',
|
|
162
|
+
step: `${step.stepNumber}.${step.subStepNumber}`,
|
|
163
|
+
stepName: step.name,
|
|
164
|
+
percent: 28,
|
|
165
|
+
message: 'Device identified. Preparing secure commissioning…',
|
|
166
|
+
device: {
|
|
167
|
+
productName: this.collectedCommissioningData.productName,
|
|
168
|
+
vendorId: this.collectedCommissioningData.vendorId,
|
|
169
|
+
productId: this.collectedCommissioningData.productId
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
return result
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
100
180
|
_resolveTargetHosts = async (_targetHost) => {
|
|
101
181
|
const host = normalizeHostForCompare(_targetHost)
|
|
102
182
|
if (host === '') return []
|
|
@@ -286,6 +366,12 @@ class classMatter extends EventEmitter {
|
|
|
286
366
|
let passcode
|
|
287
367
|
let identifierData
|
|
288
368
|
const discoveryCapabilities = { onIpNetwork: true }
|
|
369
|
+
const onProgress = _options?.onProgress
|
|
370
|
+
this._reportCommissioningProgress(onProgress, {
|
|
371
|
+
phase: 'preparing',
|
|
372
|
+
percent: 5,
|
|
373
|
+
message: 'Validating the Matter pairing code…'
|
|
374
|
+
})
|
|
289
375
|
if (code.toUpperCase().startsWith('MT:')) {
|
|
290
376
|
const qr = api.QrPairingCodeCodec.decode(code)[0]
|
|
291
377
|
passcode = qr.passcode
|
|
@@ -305,6 +391,11 @@ class classMatter extends EventEmitter {
|
|
|
305
391
|
}
|
|
306
392
|
const targetHost = (_options?.targetHost || '').toString().trim()
|
|
307
393
|
if (targetHost !== '') {
|
|
394
|
+
this._reportCommissioningProgress(onProgress, {
|
|
395
|
+
phase: 'discovery',
|
|
396
|
+
percent: 10,
|
|
397
|
+
message: `Looking for the Matter device at ${targetHost}…`
|
|
398
|
+
})
|
|
308
399
|
const commissionableDevice = await this._discoverCommissionableDeviceAtHost(identifierData, discoveryCapabilities, targetHost)
|
|
309
400
|
discovery = commissionableDevice !== undefined
|
|
310
401
|
? {
|
|
@@ -319,6 +410,11 @@ class classMatter extends EventEmitter {
|
|
|
319
410
|
let lastError
|
|
320
411
|
for (const discoveryAttempt of discoveryAttempts) {
|
|
321
412
|
try {
|
|
413
|
+
this._reportCommissioningProgress(onProgress, {
|
|
414
|
+
phase: 'pase',
|
|
415
|
+
percent: 15,
|
|
416
|
+
message: 'Discovering the device and establishing a secure PASE session…'
|
|
417
|
+
})
|
|
322
418
|
nodeId = await this.controller.commissionNode({
|
|
323
419
|
commissioning: {
|
|
324
420
|
nodeId: commissioningNodeId,
|
|
@@ -326,7 +422,10 @@ class classMatter extends EventEmitter {
|
|
|
326
422
|
},
|
|
327
423
|
discovery: discoveryAttempt,
|
|
328
424
|
passcode
|
|
329
|
-
}, {
|
|
425
|
+
}, {
|
|
426
|
+
connectNodeAfterCommissioning: false,
|
|
427
|
+
commissioningFlowImpl: this._createProgressCommissioningFlow(onProgress)
|
|
428
|
+
})
|
|
330
429
|
lastError = undefined
|
|
331
430
|
break
|
|
332
431
|
} catch (error) {
|
|
@@ -355,12 +454,22 @@ class classMatter extends EventEmitter {
|
|
|
355
454
|
throw error
|
|
356
455
|
}
|
|
357
456
|
try {
|
|
457
|
+
this._reportCommissioningProgress(onProgress, {
|
|
458
|
+
phase: 'attaching',
|
|
459
|
+
percent: 98,
|
|
460
|
+
message: 'Loading the commissioned device structure…'
|
|
461
|
+
})
|
|
358
462
|
await this._attachNode(nodeId)
|
|
359
463
|
} catch (error) {
|
|
360
464
|
// Pairing succeeded: never report it as failed just because the first connection
|
|
361
465
|
// attempt errored. The node will be attached again at the next controller start.
|
|
362
466
|
this._log('warn', `classMatter: commission: node ${nodeId} paired but not yet attached: ${error.message}`)
|
|
363
467
|
}
|
|
468
|
+
this._reportCommissioningProgress(onProgress, {
|
|
469
|
+
phase: 'complete',
|
|
470
|
+
percent: 100,
|
|
471
|
+
message: 'Matter commissioning completed successfully.'
|
|
472
|
+
})
|
|
364
473
|
return nodeId.toString()
|
|
365
474
|
}
|
|
366
475
|
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=20.18.1"
|
|
5
5
|
},
|
|
6
|
-
"version": "6.2.3-beta.
|
|
6
|
+
"version": "6.2.3-beta.4",
|
|
7
7
|
"description": "KNX Ultimate is the most advanced KNX integration for Node-RED, providing secure KNX/IP communication, routing, ETS project import, Philips Hue, Matter Controller and Matter Bridge (control matter device via KNX and expose KNX GA via Matter), MQTT, diagnostics with AI, virtual devices, and powerful automation nodes. Build professional, reliable, and scalable smart home and building automation projects with minimal effort.",
|
|
8
8
|
"files": [
|
|
9
9
|
"nodes/",
|