From cb810ab60fc1b1f51385cbc83b7408dbe0fd0ab0 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie <7365207+Pierre-Gilles@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:08:41 +0100 Subject: [PATCH 01/16] Update Matter.js to 0.16.10 and adapt code --- server/package-lock.json | 204 +++++++++--------- server/package.json | 2 +- server/services/matter/lib/matter.getNodes.js | 10 +- server/services/matter/lib/matter.init.js | 5 +- server/services/matter/lib/matter.setValue.js | 21 +- server/services/matter/package-lock.json | 124 +++++------ server/services/matter/package.json | 4 +- .../matter/utils/convertToGladysDevice.js | 6 +- 8 files changed, 191 insertions(+), 185 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 55ef046d84..047b10e7fe 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -53,7 +53,7 @@ "ws": "^6.2.2" }, "devDependencies": { - "@matter/main": "^0.13.0", + "@matter/main": "^0.16.10", "apidoc": "^1.0.3", "chai": "^4.3.7", "chai-as-promised": "^7.1.1", @@ -1062,93 +1062,93 @@ } }, "node_modules/@matter/general": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.13.0.tgz", - "integrity": "sha512-PZ+FVJotKWgtoBvorqN+PLCuTBBbTCJTCss2P5C6n9r/ZAIcCgW7LDTFmRXNNNMJEri8JUVR0REvHaa92zVj2Q==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.16.10.tgz", + "integrity": "sha512-/qytvaxvDDhEdHLaEoxlEFVBWg982jL+XXvOmAFgIv92yGDQvN4U+VcNW7S5dueJuv/L+gi0zDqhl8LUzHHAlg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@noble/curves": "^1.8.2" + "@noble/curves": "^2.0.1" } }, "node_modules/@matter/main": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.13.0.tgz", - "integrity": "sha512-Qu60G05f821bEtp2yU+rJ7Xpe66GHDn9NdSPGrAn1EJH/iWplmVeLS1nSXzyGE28iPVPYNLcMX0edY/FejAhmQ==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.16.10.tgz", + "integrity": "sha512-QKoQrmMnt6cB893+Oezk3DdIVgQJj5spt/ikEszF8pjyTuvB69zlzq1wnrn52N3mi57R6UWwk6+BPEbQE95FSw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.13.0", - "@matter/model": "0.13.0", - "@matter/node": "0.13.0", - "@matter/protocol": "0.13.0", - "@matter/types": "0.13.0" + "@matter/general": "0.16.10", + "@matter/model": "0.16.10", + "@matter/node": "0.16.10", + "@matter/protocol": "0.16.10", + "@matter/types": "0.16.10" }, "optionalDependencies": { - "@matter/nodejs": "0.13.0" + "@matter/nodejs": "0.16.10" } }, "node_modules/@matter/model": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.13.0.tgz", - "integrity": "sha512-rcXu7OdMctlOGVOkClH6/+11ct6FMDSK2/Yu05+J7BJfohJY5mQbo0jnz8l0FPbwRrk5ADbqfAJ5jh/1OKHR2Q==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.16.10.tgz", + "integrity": "sha512-6Ei8gETAkcKGEMRW+z8Mak55Y1Jl1TKGQIboC/4vvsrqcvB8zhIvGBS3GaAllxzvF0qjE7ihCPpgXXr6HuTLyg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.13.0" + "@matter/general": "0.16.10" } }, "node_modules/@matter/node": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.13.0.tgz", - "integrity": "sha512-HQkzpRnhAUfj9u2ijkT4qgyFzEfaNqyAxh+dgMpLKnbbGQoHTskJ/wEgkRRI7B0Q57mr6VJ5KgXYdbXUYA7xFA==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.16.10.tgz", + "integrity": "sha512-Hb2AxuEf0DlfN8yHxeahZGYurUUu/UDWJkmdvpDKuwSR0eIHhMweOG2RBO55/anyRFObANaUr1gr3DnyocB/0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.13.0", - "@matter/model": "0.13.0", - "@matter/protocol": "0.13.0", - "@matter/types": "0.13.0" + "@matter/general": "0.16.10", + "@matter/model": "0.16.10", + "@matter/protocol": "0.16.10", + "@matter/types": "0.16.10" } }, "node_modules/@matter/nodejs": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.13.0.tgz", - "integrity": "sha512-ThWrLZJo7UH4Ebanf3gxkhe2p8vq4X3PxyRG+ICDRGPfzSAJZ3znh2hD/zdvcdyzQlSoXeLpFbLpTkJu7aRMHg==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.16.10.tgz", + "integrity": "sha512-o8e9tGZVsiqmEtD1osSL9+gRMIuNjtwXt29I3YDRzSqb3N5Dvd4Khj3HN68YhaFFeBlM4YEV2TS0/9VF3C06/w==", "dev": true, "license": "Apache-2.0", "optional": true, "dependencies": { - "@matter/general": "0.13.0", - "@matter/node": "0.13.0", - "@matter/protocol": "0.13.0", - "@matter/types": "0.13.0" + "@matter/general": "0.16.10", + "@matter/node": "0.16.10", + "@matter/protocol": "0.16.10", + "@matter/types": "0.16.10" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.19.0 <22.0.0 || >=22.13.0" } }, "node_modules/@matter/protocol": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.13.0.tgz", - "integrity": "sha512-wFxU6+LG5ygzruOV5JF30hmLkk88hz8PqAMkQ6ow81ZvoDFBevsW0OyqxXMCNwYd/zmGXmvr3mpC0mi9/troHg==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.16.10.tgz", + "integrity": "sha512-vPQEMl8Wf4vc3tauwsGlLZRrDx+VrCPfccw3n50Lvyucws4UBt+SuBszSyIO2+QPnJcr4MB4L+EgCRI14U4zRA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.13.0", - "@matter/model": "0.13.0", - "@matter/types": "0.13.0" + "@matter/general": "0.16.10", + "@matter/model": "0.16.10", + "@matter/types": "0.16.10" } }, "node_modules/@matter/types": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.13.0.tgz", - "integrity": "sha512-8mH7hRC4MBSy4KUs8zb6uDTr0MfRYGtGBFq9t2uedlsiHA5lMUP3L1fitszoeCbpJh4EeqKHWSvF6RxWzKNueA==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.16.10.tgz", + "integrity": "sha512-AW86tGgL3sN1Vb76+03VPdQMsW9vWsaVGHe9agcED1sjtWRJBV17tcLDymAQ8k7fEeVqiy3qf9CUwFK1bhfXKQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.13.0", - "@matter/model": "0.13.0" + "@matter/general": "0.16.10", + "@matter/model": "0.16.10" } }, "node_modules/@microsoft/recognizers-text": { @@ -1776,29 +1776,29 @@ } }, "node_modules/@noble/curves": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.0.tgz", - "integrity": "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" + "@noble/hashes": "2.0.1" }, "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", "dev": true, "license": "MIT", "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -13146,81 +13146,81 @@ } }, "@matter/general": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.13.0.tgz", - "integrity": "sha512-PZ+FVJotKWgtoBvorqN+PLCuTBBbTCJTCss2P5C6n9r/ZAIcCgW7LDTFmRXNNNMJEri8JUVR0REvHaa92zVj2Q==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.16.10.tgz", + "integrity": "sha512-/qytvaxvDDhEdHLaEoxlEFVBWg982jL+XXvOmAFgIv92yGDQvN4U+VcNW7S5dueJuv/L+gi0zDqhl8LUzHHAlg==", "dev": true, "requires": { - "@noble/curves": "^1.8.2" + "@noble/curves": "^2.0.1" } }, "@matter/main": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.13.0.tgz", - "integrity": "sha512-Qu60G05f821bEtp2yU+rJ7Xpe66GHDn9NdSPGrAn1EJH/iWplmVeLS1nSXzyGE28iPVPYNLcMX0edY/FejAhmQ==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.16.10.tgz", + "integrity": "sha512-QKoQrmMnt6cB893+Oezk3DdIVgQJj5spt/ikEszF8pjyTuvB69zlzq1wnrn52N3mi57R6UWwk6+BPEbQE95FSw==", "dev": true, "requires": { - "@matter/general": "0.13.0", - "@matter/model": "0.13.0", - "@matter/node": "0.13.0", - "@matter/nodejs": "0.13.0", - "@matter/protocol": "0.13.0", - "@matter/types": "0.13.0" + "@matter/general": "0.16.10", + "@matter/model": "0.16.10", + "@matter/node": "0.16.10", + "@matter/nodejs": "0.16.10", + "@matter/protocol": "0.16.10", + "@matter/types": "0.16.10" } }, "@matter/model": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.13.0.tgz", - "integrity": "sha512-rcXu7OdMctlOGVOkClH6/+11ct6FMDSK2/Yu05+J7BJfohJY5mQbo0jnz8l0FPbwRrk5ADbqfAJ5jh/1OKHR2Q==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.16.10.tgz", + "integrity": "sha512-6Ei8gETAkcKGEMRW+z8Mak55Y1Jl1TKGQIboC/4vvsrqcvB8zhIvGBS3GaAllxzvF0qjE7ihCPpgXXr6HuTLyg==", "dev": true, "requires": { - "@matter/general": "0.13.0" + "@matter/general": "0.16.10" } }, "@matter/node": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.13.0.tgz", - "integrity": "sha512-HQkzpRnhAUfj9u2ijkT4qgyFzEfaNqyAxh+dgMpLKnbbGQoHTskJ/wEgkRRI7B0Q57mr6VJ5KgXYdbXUYA7xFA==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.16.10.tgz", + "integrity": "sha512-Hb2AxuEf0DlfN8yHxeahZGYurUUu/UDWJkmdvpDKuwSR0eIHhMweOG2RBO55/anyRFObANaUr1gr3DnyocB/0w==", "dev": true, "requires": { - "@matter/general": "0.13.0", - "@matter/model": "0.13.0", - "@matter/protocol": "0.13.0", - "@matter/types": "0.13.0" + "@matter/general": "0.16.10", + "@matter/model": "0.16.10", + "@matter/protocol": "0.16.10", + "@matter/types": "0.16.10" } }, "@matter/nodejs": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.13.0.tgz", - "integrity": "sha512-ThWrLZJo7UH4Ebanf3gxkhe2p8vq4X3PxyRG+ICDRGPfzSAJZ3znh2hD/zdvcdyzQlSoXeLpFbLpTkJu7aRMHg==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.16.10.tgz", + "integrity": "sha512-o8e9tGZVsiqmEtD1osSL9+gRMIuNjtwXt29I3YDRzSqb3N5Dvd4Khj3HN68YhaFFeBlM4YEV2TS0/9VF3C06/w==", "dev": true, "optional": true, "requires": { - "@matter/general": "0.13.0", - "@matter/node": "0.13.0", - "@matter/protocol": "0.13.0", - "@matter/types": "0.13.0" + "@matter/general": "0.16.10", + "@matter/node": "0.16.10", + "@matter/protocol": "0.16.10", + "@matter/types": "0.16.10" } }, "@matter/protocol": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.13.0.tgz", - "integrity": "sha512-wFxU6+LG5ygzruOV5JF30hmLkk88hz8PqAMkQ6ow81ZvoDFBevsW0OyqxXMCNwYd/zmGXmvr3mpC0mi9/troHg==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.16.10.tgz", + "integrity": "sha512-vPQEMl8Wf4vc3tauwsGlLZRrDx+VrCPfccw3n50Lvyucws4UBt+SuBszSyIO2+QPnJcr4MB4L+EgCRI14U4zRA==", "dev": true, "requires": { - "@matter/general": "0.13.0", - "@matter/model": "0.13.0", - "@matter/types": "0.13.0" + "@matter/general": "0.16.10", + "@matter/model": "0.16.10", + "@matter/types": "0.16.10" } }, "@matter/types": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.13.0.tgz", - "integrity": "sha512-8mH7hRC4MBSy4KUs8zb6uDTr0MfRYGtGBFq9t2uedlsiHA5lMUP3L1fitszoeCbpJh4EeqKHWSvF6RxWzKNueA==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.16.10.tgz", + "integrity": "sha512-AW86tGgL3sN1Vb76+03VPdQMsW9vWsaVGHe9agcED1sjtWRJBV17tcLDymAQ8k7fEeVqiy3qf9CUwFK1bhfXKQ==", "dev": true, "requires": { - "@matter/general": "0.13.0", - "@matter/model": "0.13.0" + "@matter/general": "0.16.10", + "@matter/model": "0.16.10" } }, "@microsoft/recognizers-text": { @@ -13824,18 +13824,18 @@ } }, "@noble/curves": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.0.tgz", - "integrity": "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", "dev": true, "requires": { - "@noble/hashes": "1.8.0" + "@noble/hashes": "2.0.1" } }, "@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", "dev": true }, "@nodelib/fs.scandir": { diff --git a/server/package.json b/server/package.json index 594741446b..b5ad5cc1e0 100644 --- a/server/package.json +++ b/server/package.json @@ -46,7 +46,7 @@ ] }, "devDependencies": { - "@matter/main": "^0.13.0", + "@matter/main": "^0.16.10", "apidoc": "^1.0.3", "chai": "^4.3.7", "chai-as-promised": "^7.1.1", diff --git a/server/services/matter/lib/matter.getNodes.js b/server/services/matter/lib/matter.getNodes.js index 5ec7a37a83..bdbe3c9591 100644 --- a/server/services/matter/lib/matter.getNodes.js +++ b/server/services/matter/lib/matter.getNodes.js @@ -5,7 +5,8 @@ const convertDevice = (device) => { const clusterClients = []; // Each cluster client is a feature of the device - device.clusterClients.forEach((clusterClient, clusterIndex) => { + const allClusterClients = device.getAllClusterClients(); + allClusterClients.forEach((clusterClient) => { clusterClients.push({ id: clusterClient.id.toString(), name: clusterClient.name, @@ -16,7 +17,8 @@ const convertDevice = (device) => { }); // Convert child endpoints (child devices) - const childEndpoints = device.childEndpoints.map((childDeviceEndpoint) => { + const childEndpointsList = device.getChildEndpoints(); + const childEndpoints = childEndpointsList.map((childDeviceEndpoint) => { return convertDevice(childDeviceEndpoint); }); @@ -37,8 +39,8 @@ const convertDevice = (device) => { async function getNodes() { const nodeDetails = this.commissioningController.getCommissionedNodesDetails(); const filteredNodeDetails = nodeDetails.filter((nodeDetail) => { - if (!nodeDetail.deviceData) { - logger.warn(`Matter: Node ${nodeDetail.nodeId} has no device data`); + if (!nodeDetail.deviceData || !nodeDetail.deviceData.basicInformation) { + logger.warn(`Matter: Node ${nodeDetail.nodeId} has no device data or basic information`); return false; } return true; diff --git a/server/services/matter/lib/matter.init.js b/server/services/matter/lib/matter.init.js index 0b566e76a0..9e52e9d03f 100644 --- a/server/services/matter/lib/matter.init.js +++ b/server/services/matter/lib/matter.init.js @@ -45,9 +45,10 @@ async function init() { environment, id: 'matter-controller-data', }, - autoConnect: true, + // Set autoConnect to undefined to fix matter-js bug (https://github.com/matter-js/matter.js/pull/3436) + // Once the fix is live in matter-js, we can switch back to true + autoConnect: undefined, adminFabricLabel: 'Gladys Assistant', - storage: storageService, }); await this.commissioningController.start(); diff --git a/server/services/matter/lib/matter.setValue.js b/server/services/matter/lib/matter.setValue.js index 7e74b8de79..87d84c3156 100644 --- a/server/services/matter/lib/matter.setValue.js +++ b/server/services/matter/lib/matter.setValue.js @@ -27,8 +27,9 @@ function findDeviceRecursively(parentDevice, path) { const deviceNumber = Number(currentNumber); // Look in child endpoints - if (parentDevice.childEndpoints) { - const childDevice = parentDevice.childEndpoints.find((child) => child.number === deviceNumber); + const childEndpoints = parentDevice.getChildEndpoints(); + if (childEndpoints && childEndpoints.length > 0) { + const childDevice = childEndpoints.find((child) => child.number === deviceNumber); if (childDevice) { return findDeviceRecursively(childDevice, remainingPath); } @@ -85,7 +86,7 @@ async function setValue(gladysDevice, gladysFeature, value) { // Handle binary device if (gladysFeature.type === DEVICE_FEATURE_TYPES.SWITCH.BINARY) { - const onOff = targetDevice.clusterClients.get(OnOff.Complete.id); + const onOff = targetDevice.getClusterClientById(OnOff.Complete.id); if (!onOff) { throw new Error('Device does not support OnOff cluster'); @@ -101,7 +102,7 @@ async function setValue(gladysDevice, gladysFeature, value) { // Handle shutters if (gladysFeature.category === DEVICE_FEATURE_CATEGORIES.SHUTTER) { - const windowCovering = targetDevice.clusterClients.get(WindowCovering.Complete.id); + const windowCovering = targetDevice.getClusterClientById(WindowCovering.Complete.id); // Handle device feature shutter position if (gladysFeature.type === DEVICE_FEATURE_TYPES.SHUTTER.POSITION) { await windowCovering.goToLiftPercentage({ @@ -125,8 +126,8 @@ async function setValue(gladysDevice, gladysFeature, value) { gladysFeature.category === DEVICE_FEATURE_CATEGORIES.LIGHT && gladysFeature.type === DEVICE_FEATURE_TYPES.LIGHT.BRIGHTNESS ) { - const levelControl = targetDevice.clusterClients.get(LevelControl.Complete.id); - const onOff = targetDevice.clusterClients.get(OnOff.Complete.id); + const levelControl = targetDevice.getClusterClientById(LevelControl.Complete.id); + const onOff = targetDevice.getClusterClientById(OnOff.Complete.id); await levelControl.moveToLevel({ level: value, transitionTime: null, @@ -147,8 +148,8 @@ async function setValue(gladysDevice, gladysFeature, value) { gladysFeature.category === DEVICE_FEATURE_CATEGORIES.LIGHT && gladysFeature.type === DEVICE_FEATURE_TYPES.LIGHT.COLOR ) { - const colorControl = targetDevice.clusterClients.get(ColorControl.Complete.id); - const onOff = targetDevice.clusterClients.get(OnOff.Complete.id); + const colorControl = targetDevice.getClusterClientById(ColorControl.Complete.id); + const onOff = targetDevice.getClusterClientById(OnOff.Complete.id); const [hue, saturation] = intToHsb(value); // Convert from standard HSB ranges to Matter ranges @@ -175,7 +176,7 @@ async function setValue(gladysDevice, gladysFeature, value) { gladysFeature.category === DEVICE_FEATURE_CATEGORIES.THERMOSTAT && gladysFeature.type === DEVICE_FEATURE_TYPES.THERMOSTAT.TARGET_TEMPERATURE ) { - const thermostat = targetDevice.clusterClients.get(Thermostat.Complete.id); + const thermostat = targetDevice.getClusterClientById(Thermostat.Complete.id); await thermostat.setOccupiedHeatingSetpointAttribute(value * 100); } @@ -184,7 +185,7 @@ async function setValue(gladysDevice, gladysFeature, value) { gladysFeature.category === DEVICE_FEATURE_CATEGORIES.AIR_CONDITIONING && gladysFeature.type === DEVICE_FEATURE_TYPES.AIR_CONDITIONING.TARGET_TEMPERATURE ) { - const thermostat = targetDevice.clusterClients.get(Thermostat.Complete.id); + const thermostat = targetDevice.getClusterClientById(Thermostat.Complete.id); await thermostat.setOccupiedCoolingSetpointAttribute(value * 100); } } diff --git a/server/services/matter/package-lock.json b/server/services/matter/package-lock.json index 328c3bd4e0..210445bb14 100644 --- a/server/services/matter/package-lock.json +++ b/server/services/matter/package-lock.json @@ -16,133 +16,133 @@ "win32" ], "dependencies": { - "@matter/main": "^0.13.0", - "@project-chip/matter.js": "^0.13.0", + "@matter/main": "^0.16.10", + "@project-chip/matter.js": "^0.16.10", "bluebird": "^3.7.2", "fs-extra": "^11.3.0" } }, "node_modules/@matter/general": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.13.0.tgz", - "integrity": "sha512-PZ+FVJotKWgtoBvorqN+PLCuTBBbTCJTCss2P5C6n9r/ZAIcCgW7LDTFmRXNNNMJEri8JUVR0REvHaa92zVj2Q==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.16.10.tgz", + "integrity": "sha512-/qytvaxvDDhEdHLaEoxlEFVBWg982jL+XXvOmAFgIv92yGDQvN4U+VcNW7S5dueJuv/L+gi0zDqhl8LUzHHAlg==", "license": "Apache-2.0", "dependencies": { - "@noble/curves": "^1.8.2" + "@noble/curves": "^2.0.1" } }, "node_modules/@matter/main": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.13.0.tgz", - "integrity": "sha512-Qu60G05f821bEtp2yU+rJ7Xpe66GHDn9NdSPGrAn1EJH/iWplmVeLS1nSXzyGE28iPVPYNLcMX0edY/FejAhmQ==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.16.10.tgz", + "integrity": "sha512-QKoQrmMnt6cB893+Oezk3DdIVgQJj5spt/ikEszF8pjyTuvB69zlzq1wnrn52N3mi57R6UWwk6+BPEbQE95FSw==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.13.0", - "@matter/model": "0.13.0", - "@matter/node": "0.13.0", - "@matter/protocol": "0.13.0", - "@matter/types": "0.13.0" + "@matter/general": "0.16.10", + "@matter/model": "0.16.10", + "@matter/node": "0.16.10", + "@matter/protocol": "0.16.10", + "@matter/types": "0.16.10" }, "optionalDependencies": { - "@matter/nodejs": "0.13.0" + "@matter/nodejs": "0.16.10" } }, "node_modules/@matter/model": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.13.0.tgz", - "integrity": "sha512-rcXu7OdMctlOGVOkClH6/+11ct6FMDSK2/Yu05+J7BJfohJY5mQbo0jnz8l0FPbwRrk5ADbqfAJ5jh/1OKHR2Q==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.16.10.tgz", + "integrity": "sha512-6Ei8gETAkcKGEMRW+z8Mak55Y1Jl1TKGQIboC/4vvsrqcvB8zhIvGBS3GaAllxzvF0qjE7ihCPpgXXr6HuTLyg==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.13.0" + "@matter/general": "0.16.10" } }, "node_modules/@matter/node": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.13.0.tgz", - "integrity": "sha512-HQkzpRnhAUfj9u2ijkT4qgyFzEfaNqyAxh+dgMpLKnbbGQoHTskJ/wEgkRRI7B0Q57mr6VJ5KgXYdbXUYA7xFA==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.16.10.tgz", + "integrity": "sha512-Hb2AxuEf0DlfN8yHxeahZGYurUUu/UDWJkmdvpDKuwSR0eIHhMweOG2RBO55/anyRFObANaUr1gr3DnyocB/0w==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.13.0", - "@matter/model": "0.13.0", - "@matter/protocol": "0.13.0", - "@matter/types": "0.13.0" + "@matter/general": "0.16.10", + "@matter/model": "0.16.10", + "@matter/protocol": "0.16.10", + "@matter/types": "0.16.10" } }, "node_modules/@matter/nodejs": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.13.0.tgz", - "integrity": "sha512-ThWrLZJo7UH4Ebanf3gxkhe2p8vq4X3PxyRG+ICDRGPfzSAJZ3znh2hD/zdvcdyzQlSoXeLpFbLpTkJu7aRMHg==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.16.10.tgz", + "integrity": "sha512-o8e9tGZVsiqmEtD1osSL9+gRMIuNjtwXt29I3YDRzSqb3N5Dvd4Khj3HN68YhaFFeBlM4YEV2TS0/9VF3C06/w==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@matter/general": "0.13.0", - "@matter/node": "0.13.0", - "@matter/protocol": "0.13.0", - "@matter/types": "0.13.0" + "@matter/general": "0.16.10", + "@matter/node": "0.16.10", + "@matter/protocol": "0.16.10", + "@matter/types": "0.16.10" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.19.0 <22.0.0 || >=22.13.0" } }, "node_modules/@matter/protocol": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.13.0.tgz", - "integrity": "sha512-wFxU6+LG5ygzruOV5JF30hmLkk88hz8PqAMkQ6ow81ZvoDFBevsW0OyqxXMCNwYd/zmGXmvr3mpC0mi9/troHg==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.16.10.tgz", + "integrity": "sha512-vPQEMl8Wf4vc3tauwsGlLZRrDx+VrCPfccw3n50Lvyucws4UBt+SuBszSyIO2+QPnJcr4MB4L+EgCRI14U4zRA==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.13.0", - "@matter/model": "0.13.0", - "@matter/types": "0.13.0" + "@matter/general": "0.16.10", + "@matter/model": "0.16.10", + "@matter/types": "0.16.10" } }, "node_modules/@matter/types": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.13.0.tgz", - "integrity": "sha512-8mH7hRC4MBSy4KUs8zb6uDTr0MfRYGtGBFq9t2uedlsiHA5lMUP3L1fitszoeCbpJh4EeqKHWSvF6RxWzKNueA==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.16.10.tgz", + "integrity": "sha512-AW86tGgL3sN1Vb76+03VPdQMsW9vWsaVGHe9agcED1sjtWRJBV17tcLDymAQ8k7fEeVqiy3qf9CUwFK1bhfXKQ==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.13.0", - "@matter/model": "0.13.0" + "@matter/general": "0.16.10", + "@matter/model": "0.16.10" } }, "node_modules/@noble/curves": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.0.tgz", - "integrity": "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" + "@noble/hashes": "2.0.1" }, "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", "license": "MIT", "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@project-chip/matter.js": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@project-chip/matter.js/-/matter.js-0.13.0.tgz", - "integrity": "sha512-QWmfuDorGBW8dpgYk+3L9V4SdW80PQ4pW90s+ORxtWnpBcJoiP6a5skS7NJuW3kNRSMLtXQmpG+yI8oDXcULSg==", + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/@project-chip/matter.js/-/matter.js-0.16.10.tgz", + "integrity": "sha512-5SpMsaNftqrqHSppP8J1RTdWdtJgehs3k9CVf0cM0IrZLQMK67W2cWVlTbsJElV7zOfKatjU56FdRA2UgkbvMQ==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.13.0", - "@matter/model": "0.13.0", - "@matter/node": "0.13.0", - "@matter/protocol": "0.13.0", - "@matter/types": "0.13.0" + "@matter/general": "0.16.10", + "@matter/model": "0.16.10", + "@matter/node": "0.16.10", + "@matter/protocol": "0.16.10", + "@matter/types": "0.16.10" } }, "node_modules/bluebird": { diff --git a/server/services/matter/package.json b/server/services/matter/package.json index c4965473c9..b696541e9d 100644 --- a/server/services/matter/package.json +++ b/server/services/matter/package.json @@ -12,8 +12,8 @@ "arm64" ], "dependencies": { - "@matter/main": "^0.13.0", - "@project-chip/matter.js": "^0.13.0", + "@matter/main": "^0.16.10", + "@project-chip/matter.js": "^0.16.10", "bluebird": "^3.7.2", "fs-extra": "^11.3.0" } diff --git a/server/services/matter/utils/convertToGladysDevice.js b/server/services/matter/utils/convertToGladysDevice.js index 7975f66333..2059e1c813 100644 --- a/server/services/matter/utils/convertToGladysDevice.js +++ b/server/services/matter/utils/convertToGladysDevice.js @@ -101,8 +101,10 @@ async function convertToGladysDevice(serviceId, nodeId, device, nodeDetailDevice // Add endpoint number to the name so the user can identify the device gladysDevice.name += ` ${device.number}`; - if (device.clusterClients) { - await Promise.each(Array.from(device.clusterClients.entries()), async ([clusterIndex, clusterClient]) => { + const allClusterClients = device.getAllClusterClients(); + if (allClusterClients && allClusterClients.length > 0) { + await Promise.each(allClusterClients, async (clusterClient) => { + const clusterIndex = clusterClient.id; const commonNewFeature = { name: `${clusterClient.name} - ${clusterClient.endpointId}`, selector: slugify(`matter-${device.name}-${clusterClient.name}`, true), From 77373e25588cb240ac96b1b28fa4a34a297bb958 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie <7365207+Pierre-Gilles@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:19:29 +0100 Subject: [PATCH 02/16] adapt tests and listenToStateChange --- .../services/matter/lib/matter.handleNode.js | 7 +- .../matter/lib/matter.listenToStateChange.js | 38 ++-- .../matter/lib/convertToGladysDevice.test.js | 16 +- .../matter/lib/listenToStateChange.test.js | 193 +++++++++--------- .../matter/lib/matter.getNodes.test.js | 13 +- .../services/matter/lib/matter.init.test.js | 9 +- .../matter/lib/matter.pairDevice.test.js | 45 ++-- .../matter/lib/matter.refreshDevices.test.js | 9 +- .../matter/lib/matter.setValue.test.js | 45 ++-- 9 files changed, 203 insertions(+), 172 deletions(-) diff --git a/server/services/matter/lib/matter.handleNode.js b/server/services/matter/lib/matter.handleNode.js index 85cc866658..806fc3834d 100644 --- a/server/services/matter/lib/matter.handleNode.js +++ b/server/services/matter/lib/matter.handleNode.js @@ -26,7 +26,7 @@ const handleDevice = async ( }; // If we have this cluster, it means we are in a bridge device - const bridgedDeviceBasicInformationClusterClient = device.clusterClients.get( + const bridgedDeviceBasicInformationClusterClient = device.getClusterClientById( BridgedDeviceBasicInformation.Complete.id, ); @@ -82,8 +82,9 @@ const handleDevice = async ( devices.push(gladysDevice); } - if (device.childEndpoints) { - await Promise.each(device.childEndpoints, async (childDevice, index) => { + const childEndpoints = device.getChildEndpoints(); + if (childEndpoints && childEndpoints.length > 0) { + await Promise.each(childEndpoints, async (childDevice, index) => { await handleDevice( nodeId, childInformations, diff --git a/server/services/matter/lib/matter.listenToStateChange.js b/server/services/matter/lib/matter.listenToStateChange.js index 99b34f395c..3681c2e4cf 100644 --- a/server/services/matter/lib/matter.listenToStateChange.js +++ b/server/services/matter/lib/matter.listenToStateChange.js @@ -32,8 +32,8 @@ const { EVENTS, STATE, BUTTON_STATUS } = require('../../../utils/constants'); * @example matter.listenToStateChange(nodeId, device); */ async function listenToStateChange(nodeId, devicePath, device) { - // Get the OnOff cluster from clusterClients map - const onOff = device.clusterClients.get(OnOff.Complete.id); + // Get the OnOff cluster + const onOff = device.getClusterClientById(OnOff.Complete.id); // We only add the listener if it's not already added if (onOff && !this.stateChangeListeners.has(onOff)) { @@ -49,7 +49,7 @@ async function listenToStateChange(nodeId, devicePath, device) { }); } - const booleanState = device.clusterClients.get(BooleanState.Complete.id); + const booleanState = device.getClusterClientById(BooleanState.Complete.id); if (booleanState && !this.stateChangeListeners.has(booleanState)) { logger.debug(`Matter: Adding state change listener for BooleanState cluster ${booleanState.name}`); this.stateChangeListeners.add(booleanState); @@ -62,7 +62,7 @@ async function listenToStateChange(nodeId, devicePath, device) { }); } - const switchCluster = device.clusterClients.get(Switch.Complete.id); + const switchCluster = device.getClusterClientById(Switch.Complete.id); if (switchCluster && !this.stateChangeListeners.has(switchCluster)) { logger.debug(`Matter: Adding state change listener for Switch cluster ${switchCluster.name}`); this.stateChangeListeners.add(switchCluster); @@ -104,7 +104,7 @@ async function listenToStateChange(nodeId, devicePath, device) { } } - const occupancy = device.clusterClients.get(OccupancySensing.Complete.id); + const occupancy = device.getClusterClientById(OccupancySensing.Complete.id); if (occupancy && !this.stateChangeListeners.has(occupancy)) { logger.debug(`Matter: Adding state change listener for OccupancySensing cluster ${occupancy.name}`); this.stateChangeListeners.add(occupancy); @@ -118,7 +118,7 @@ async function listenToStateChange(nodeId, devicePath, device) { }); } - const illuminance = device.clusterClients.get(IlluminanceMeasurement.Complete.id); + const illuminance = device.getClusterClientById(IlluminanceMeasurement.Complete.id); if (illuminance && !this.stateChangeListeners.has(illuminance)) { logger.debug(`Matter: Adding state change listener for IlluminanceMeasurement cluster ${illuminance.name}`); this.stateChangeListeners.add(illuminance); @@ -133,7 +133,7 @@ async function listenToStateChange(nodeId, devicePath, device) { }); } - const temperatureSensor = device.clusterClients.get(TemperatureMeasurement.Complete.id); + const temperatureSensor = device.getClusterClientById(TemperatureMeasurement.Complete.id); if (temperatureSensor && !this.stateChangeListeners.has(temperatureSensor)) { logger.debug(`Matter: Adding state change listener for TemperatureMeasurement cluster ${temperatureSensor.name}`); this.stateChangeListeners.add(temperatureSensor); @@ -147,7 +147,7 @@ async function listenToStateChange(nodeId, devicePath, device) { }); } - const windowCover = device.clusterClients.get(WindowCovering.Complete.id); + const windowCover = device.getClusterClientById(WindowCovering.Complete.id); if (windowCover && !this.stateChangeListeners.has(windowCover)) { logger.debug(`Matter: Adding state change listener for WindowCovering cluster ${windowCover.name}`); @@ -162,7 +162,7 @@ async function listenToStateChange(nodeId, devicePath, device) { }); } - const levelControl = device.clusterClients.get(LevelControl.Complete.id); + const levelControl = device.getClusterClientById(LevelControl.Complete.id); if (levelControl && !this.stateChangeListeners.has(levelControl)) { logger.debug(`Matter: Adding state change listener for LevelControl cluster ${levelControl.name}`); this.stateChangeListeners.add(levelControl); @@ -176,7 +176,7 @@ async function listenToStateChange(nodeId, devicePath, device) { }); } - const colorControl = device.clusterClients.get(ColorControl.Complete.id); + const colorControl = device.getClusterClientById(ColorControl.Complete.id); if (colorControl && !this.stateChangeListeners.has(colorControl)) { logger.debug(`Matter: Adding state change listener for ColorControl cluster ${colorControl.name}`); this.stateChangeListeners.add(colorControl); @@ -220,7 +220,7 @@ async function listenToStateChange(nodeId, devicePath, device) { } } - const relativeHumidityMeasurement = device.clusterClients.get(RelativeHumidityMeasurement.Complete.id); + const relativeHumidityMeasurement = device.getClusterClientById(RelativeHumidityMeasurement.Complete.id); if (relativeHumidityMeasurement && !this.stateChangeListeners.has(relativeHumidityMeasurement)) { logger.debug( `Matter: Adding state change listener for RelativeHumidityMeasurement cluster ${relativeHumidityMeasurement.name}`, @@ -236,7 +236,7 @@ async function listenToStateChange(nodeId, devicePath, device) { }); } - const pm25ConcentrationMeasurement = device.clusterClients.get(Pm25ConcentrationMeasurement.Complete.id); + const pm25ConcentrationMeasurement = device.getClusterClientById(Pm25ConcentrationMeasurement.Complete.id); if (pm25ConcentrationMeasurement && !this.stateChangeListeners.has(pm25ConcentrationMeasurement)) { logger.debug( `Matter: Adding state change listener for Pm25ConcentrationMeasurement cluster ${pm25ConcentrationMeasurement.name}`, @@ -252,7 +252,7 @@ async function listenToStateChange(nodeId, devicePath, device) { }); } - const pm10ConcentrationMeasurement = device.clusterClients.get(Pm10ConcentrationMeasurement.Complete.id); + const pm10ConcentrationMeasurement = device.getClusterClientById(Pm10ConcentrationMeasurement.Complete.id); if (pm10ConcentrationMeasurement && !this.stateChangeListeners.has(pm10ConcentrationMeasurement)) { logger.debug( `Matter: Adding state change listener for Pm10ConcentrationMeasurement cluster ${pm10ConcentrationMeasurement.name}`, @@ -268,7 +268,7 @@ async function listenToStateChange(nodeId, devicePath, device) { }); } - const totalVolatileOrganicCompoundsConcentrationMeasurement = device.clusterClients.get( + const totalVolatileOrganicCompoundsConcentrationMeasurement = device.getClusterClientById( TotalVolatileOrganicCompoundsConcentrationMeasurement.Complete.id, ); if ( @@ -289,7 +289,7 @@ async function listenToStateChange(nodeId, devicePath, device) { }); } - const formaldehydeConcentrationMeasurement = device.clusterClients.get( + const formaldehydeConcentrationMeasurement = device.getClusterClientById( FormaldehydeConcentrationMeasurement.Complete.id, ); if (formaldehydeConcentrationMeasurement && !this.stateChangeListeners.has(formaldehydeConcentrationMeasurement)) { @@ -307,7 +307,7 @@ async function listenToStateChange(nodeId, devicePath, device) { }); } - const thermostat = device.clusterClients.get(Thermostat.Complete.id); + const thermostat = device.getClusterClientById(Thermostat.Complete.id); if (thermostat && !this.stateChangeListeners.has(thermostat)) { logger.debug(`Matter: Adding state change listener for Thermostat cluster ${thermostat.name}`); this.stateChangeListeners.add(thermostat); @@ -332,7 +332,7 @@ async function listenToStateChange(nodeId, devicePath, device) { } } - const electricalPowerMeasurement = device.clusterClients.get(ElectricalPowerMeasurement.Complete.id); + const electricalPowerMeasurement = device.getClusterClientById(ElectricalPowerMeasurement.Complete.id); if (electricalPowerMeasurement && !this.stateChangeListeners.has(electricalPowerMeasurement)) { logger.debug( `Matter: Adding state change listener for ElectricalPowerMeasurement cluster ${electricalPowerMeasurement.name}`, @@ -374,7 +374,7 @@ async function listenToStateChange(nodeId, devicePath, device) { } } - const electricalEnergyMeasurement = device.clusterClients.get(ElectricalEnergyMeasurement.Complete.id); + const electricalEnergyMeasurement = device.getClusterClientById(ElectricalEnergyMeasurement.Complete.id); if (electricalEnergyMeasurement && !this.stateChangeListeners.has(electricalEnergyMeasurement)) { logger.debug( `Matter: Adding state change listener for ElectricalEnergyMeasurement cluster ${electricalEnergyMeasurement.name}`, @@ -395,7 +395,7 @@ async function listenToStateChange(nodeId, devicePath, device) { } } - const hepaFilterMonitoring = device.clusterClients.get(HepaFilterMonitoring.Complete.id); + const hepaFilterMonitoring = device.getClusterClientById(HepaFilterMonitoring.Complete.id); if (hepaFilterMonitoring && !this.stateChangeListeners.has(hepaFilterMonitoring)) { logger.debug(`Matter: Adding state change listener for HepaFilterMonitoring cluster ${hepaFilterMonitoring.name}`); this.stateChangeListeners.add(hepaFilterMonitoring); diff --git a/server/test/services/matter/lib/convertToGladysDevice.test.js b/server/test/services/matter/lib/convertToGladysDevice.test.js index a241489ecf..d7983fdb62 100644 --- a/server/test/services/matter/lib/convertToGladysDevice.test.js +++ b/server/test/services/matter/lib/convertToGladysDevice.test.js @@ -13,17 +13,17 @@ describe('Matter.convertToGladysDevice', () => { }; it('should create a read-only binary feature for BooleanState cluster', async () => { - const clusterClients = new Map(); - clusterClients.set(BooleanState.Complete.id, { + const clusterClient = { id: BooleanState.Complete.id, name: 'BooleanState', endpointId: 1, - }); + }; const device = { name: 'Test Device', number: 1, - clusterClients, + getAllClusterClients: () => [clusterClient], + getChildEndpoints: () => [], }; const gladysDevice = await convertToGladysDevice(serviceId, nodeId, device, basicInformation, '1'); @@ -43,17 +43,17 @@ describe('Matter.convertToGladysDevice', () => { }); it('should create a button click feature for Switch cluster', async () => { - const clusterClients = new Map(); - clusterClients.set(Switch.Complete.id, { + const clusterClient = { id: Switch.Complete.id, name: 'Switch', endpointId: 2, - }); + }; const device = { name: 'Test Device', number: 2, - clusterClients, + getAllClusterClients: () => [clusterClient], + getChildEndpoints: () => [], }; const gladysDevice = await convertToGladysDevice(serviceId, nodeId, device, basicInformation, '1:child_endpoint:2'); diff --git a/server/test/services/matter/lib/listenToStateChange.test.js b/server/test/services/matter/lib/listenToStateChange.test.js index 2e95802e47..f7c093d5f3 100644 --- a/server/test/services/matter/lib/listenToStateChange.test.js +++ b/server/test/services/matter/lib/listenToStateChange.test.js @@ -48,15 +48,15 @@ describe('Matter.listenToStateChange', () => { }); it('should listen to state change (ON)', async () => { - const clusterClients = new Map(); - clusterClients.set(OnOff.Complete.id, { + const clusterClient = { + id: OnOff.Complete.id, addOnOffAttributeListener: (callback) => { callback(true); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -65,15 +65,15 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (OFF)', async () => { - const clusterClients = new Map(); - clusterClients.set(OnOff.Complete.id, { + const clusterClient = { + id: OnOff.Complete.id, addOnOffAttributeListener: (callback) => { callback(false); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -82,15 +82,15 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change in nested child endpoint (OFF)', async () => { - const clusterClients = new Map(); - clusterClients.set(OnOff.Complete.id, { + const clusterClient = { + id: OnOff.Complete.id, addOnOffAttributeListener: (callback) => { callback(false); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1:child_endpoint:2', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -99,15 +99,15 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (BooleanState = true)', async () => { - const clusterClients = new Map(); - clusterClients.set(BooleanState.Complete.id, { + const clusterClient = { + id: BooleanState.Complete.id, addStateValueAttributeListener: (callback) => { callback(true); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -116,8 +116,8 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to switch events', async () => { - const clusterClients = new Map(); - clusterClients.set(Switch.Complete.id, { + const clusterClient = { + id: Switch.Complete.id, addInitialPressEventListener: (callback) => { callback(); }, @@ -130,10 +130,10 @@ describe('Matter.listenToStateChange', () => { addLongReleaseEventListener: (callback) => { callback(); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -154,15 +154,15 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (Occupancy = true)', async () => { - const clusterClients = new Map(); - clusterClients.set(OccupancySensing.Complete.id, { + const clusterClient = { + id: OccupancySensing.Complete.id, addOccupancyAttributeListener: (callback) => { callback({ occupied: true }); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -171,15 +171,15 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (Occupancy = false)', async () => { - const clusterClients = new Map(); - clusterClients.set(OccupancySensing.Complete.id, { + const clusterClient = { + id: OccupancySensing.Complete.id, addOccupancyAttributeListener: (callback) => { callback({ occupied: false }); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -188,16 +188,16 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (IlluminanceMeasurement)', async () => { - const clusterClients = new Map(); // Matter: Illuminance attribute changed to 21327 (Converted to 136 lux) - clusterClients.set(IlluminanceMeasurement.Complete.id, { + const clusterClient = { + id: IlluminanceMeasurement.Complete.id, addMeasuredValueAttributeListener: (callback) => { callback(21327); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -206,15 +206,15 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (TemperatureMeasurement)', async () => { - const clusterClients = new Map(); - clusterClients.set(TemperatureMeasurement.Complete.id, { + const clusterClient = { + id: TemperatureMeasurement.Complete.id, addMeasuredValueAttributeListener: (callback) => { callback(2150); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -223,15 +223,15 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (RelativeHumidityMeasurement)', async () => { - const clusterClients = new Map(); - clusterClients.set(RelativeHumidityMeasurement.Complete.id, { + const clusterClient = { + id: RelativeHumidityMeasurement.Complete.id, addMeasuredValueAttributeListener: (callback) => { callback(5000); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -240,15 +240,15 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (Pm25ConcentrationMeasurement)', async () => { - const clusterClients = new Map(); - clusterClients.set(Pm25ConcentrationMeasurement.Complete.id, { + const clusterClient = { + id: Pm25ConcentrationMeasurement.Complete.id, addMeasuredValueAttributeListener: (callback) => { callback(100); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -257,15 +257,15 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (Pm10ConcentrationMeasurement)', async () => { - const clusterClients = new Map(); - clusterClients.set(Pm10ConcentrationMeasurement.Complete.id, { + const clusterClient = { + id: Pm10ConcentrationMeasurement.Complete.id, addMeasuredValueAttributeListener: (callback) => { callback(100); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -274,15 +274,15 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (TotalVolatileOrganicCompoundsConcentrationMeasurement)', async () => { - const clusterClients = new Map(); - clusterClients.set(TotalVolatileOrganicCompoundsConcentrationMeasurement.Complete.id, { + const clusterClient = { + id: TotalVolatileOrganicCompoundsConcentrationMeasurement.Complete.id, addLevelValueAttributeListener: (callback) => { callback(3); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -291,15 +291,15 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (FormaldehydeConcentrationMeasurement)', async () => { - const clusterClients = new Map(); - clusterClients.set(FormaldehydeConcentrationMeasurement.Complete.id, { + const clusterClient = { + id: FormaldehydeConcentrationMeasurement.Complete.id, addMeasuredValueAttributeListener: (callback) => { callback(100); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -308,18 +308,18 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (Thermostat heating)', async () => { - const clusterClients = new Map(); - clusterClients.set(Thermostat.Complete.id, { + const clusterClient = { + id: Thermostat.Complete.id, supportedFeatures: { heating: true, }, addOccupiedHeatingSetpointAttributeListener: (callback) => { callback(2000); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -328,18 +328,18 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (Thermostat cooling)', async () => { - const clusterClients = new Map(); - clusterClients.set(Thermostat.Complete.id, { + const clusterClient = { + id: Thermostat.Complete.id, supportedFeatures: { cooling: true, }, addOccupiedCoolingSetpointAttributeListener: (callback) => { callback(2000); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -348,15 +348,15 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (WindowCovering)', async () => { - const clusterClients = new Map(); - clusterClients.set(WindowCovering.Complete.id, { + const clusterClient = { + id: WindowCovering.Complete.id, addCurrentPositionLiftPercent100thsAttributeListener: (callback) => { callback(8400); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -365,15 +365,15 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (LevelControl)', async () => { - const clusterClients = new Map(); - clusterClients.set(LevelControl.Complete.id, { + const clusterClient = { + id: LevelControl.Complete.id, addCurrentLevelAttributeListener: (callback) => { callback(99); }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -382,7 +382,7 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (ColorControl)', async () => { - const clusterClients = new Map(); + let clusterClient; const promise = new Promise((resolve) => { let callCount = 0; const checkThatEveryThingWasCalled = () => { @@ -391,7 +391,8 @@ describe('Matter.listenToStateChange', () => { resolve(); } }; - clusterClients.set(ColorControl.Complete.id, { + clusterClient = { + id: ColorControl.Complete.id, supportedFeatures: { hueSaturation: true, }, @@ -411,11 +412,11 @@ describe('Matter.listenToStateChange', () => { checkThatEveryThingWasCalled(); return 40; }, - }); + }; }); const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); // We need to make sure that we called all 4 functions before checking the events @@ -431,17 +432,17 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (ElectricalPowerMeasurement - ActivePower)', async () => { - const clusterClients = new Map(); - clusterClients.set(ElectricalPowerMeasurement.Complete.id, { + const clusterClient = { + id: ElectricalPowerMeasurement.Complete.id, addActivePowerAttributeListener: (callback) => { callback(1500000); // 1500000 mW = 1500 W }, addVoltageAttributeListener: () => {}, addActiveCurrentAttributeListener: () => {}, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -450,8 +451,8 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (ElectricalPowerMeasurement - Voltage)', async () => { - const clusterClients = new Map(); - clusterClients.set(ElectricalPowerMeasurement.Complete.id, { + const clusterClient = { + id: ElectricalPowerMeasurement.Complete.id, supportedFeatures: { voltage: true, }, @@ -460,10 +461,10 @@ describe('Matter.listenToStateChange', () => { callback(230000); // 230000 mV = 230 V }, addActiveCurrentAttributeListener: () => {}, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -472,8 +473,8 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (ElectricalPowerMeasurement - ActiveCurrent)', async () => { - const clusterClients = new Map(); - clusterClients.set(ElectricalPowerMeasurement.Complete.id, { + const clusterClient = { + id: ElectricalPowerMeasurement.Complete.id, supportedFeatures: { current: true, }, @@ -482,10 +483,10 @@ describe('Matter.listenToStateChange', () => { addActiveCurrentAttributeListener: (callback) => { callback(6500); // 6500 mA = 6.5 A }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -494,18 +495,18 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (ElectricalEnergyMeasurement - CumulativeEnergy)', async () => { - const clusterClients = new Map(); - clusterClients.set(ElectricalEnergyMeasurement.Complete.id, { + const clusterClient = { + id: ElectricalEnergyMeasurement.Complete.id, supportedFeatures: { cumulativeEnergy: true, }, addCumulativeEnergyImportedAttributeListener: (callback) => { callback({ energy: 1500000000 }); // 1500000000 mWh = 1500 kWh }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { @@ -514,15 +515,15 @@ describe('Matter.listenToStateChange', () => { }); }); it('should listen to state change (HepaFilterMonitoring)', async () => { - const clusterClients = new Map(); - clusterClients.set(HepaFilterMonitoring.Complete.id, { + const clusterClient = { + id: HepaFilterMonitoring.Complete.id, addConditionAttributeListener: (callback) => { callback(75); // 75% filter life remaining }, - }); + }; const device = { number: 1, - clusterClients, + getClusterClientById: (id) => (id === clusterClient.id ? clusterClient : null), }; await matterHandler.listenToStateChange(1234n, '1', device); assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { diff --git a/server/test/services/matter/lib/matter.getNodes.test.js b/server/test/services/matter/lib/matter.getNodes.test.js index 2515b02a2d..2c8f186fe8 100644 --- a/server/test/services/matter/lib/matter.getNodes.test.js +++ b/server/test/services/matter/lib/matter.getNodes.test.js @@ -21,15 +21,14 @@ describe('Matter.getNodes', () => { }); it('should return all nodes', async () => { - const clusterClients = new Map(); - clusterClients.set(6, { + const clusterClient = { id: 2, name: 'OnOff', attributes: { onOff: {}, }, commands: {}, - }); + }; matterHandler.commissioningController = { getNode: fake.returns({ isConnected: true, @@ -37,13 +36,13 @@ describe('Matter.getNodes', () => { { number: 1, name: 'Test Device', - clusterClients: new Map(), - childEndpoints: [ + getAllClusterClients: () => [], + getChildEndpoints: () => [ { name: 'Test Device child', number: 2, - clusterClients, - childEndpoints: [], + getAllClusterClients: () => [clusterClient], + getChildEndpoints: () => [], }, ], }, diff --git a/server/test/services/matter/lib/matter.init.test.js b/server/test/services/matter/lib/matter.init.test.js index a9ae89572d..b1d3846b08 100644 --- a/server/test/services/matter/lib/matter.init.test.js +++ b/server/test/services/matter/lib/matter.init.test.js @@ -294,13 +294,16 @@ describe('Matter.init', () => { id: 'device-1', name: 'Test Device', number: 1, - clusterClients, - childEndpoints: [ + getAllClusterClients: () => Array.from(clusterClients.values()), + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [ { id: 'child-endpoint-1', name: 'Child Endpoint', number: 2, - clusterClients, + getAllClusterClients: () => Array.from(clusterClients.values()), + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [], }, ], }, diff --git a/server/test/services/matter/lib/matter.pairDevice.test.js b/server/test/services/matter/lib/matter.pairDevice.test.js index 82ef8a9ce4..ab63197d7b 100644 --- a/server/test/services/matter/lib/matter.pairDevice.test.js +++ b/server/test/services/matter/lib/matter.pairDevice.test.js @@ -24,10 +24,10 @@ describe('Matter.pairDevice', () => { it('should pair a device', async () => { const pairingCode = '1450-134-1614'; - const clusterClients = new Map(); - clusterClients.set(6, { + const clusterClient = { + id: 6, addOnOffAttributeListener: fake.returns(null), - }); + }; matterHandler.commissioningController = { commissionNode: fake.resolves(12345n), getCommissionedNodesDetails: fake.returns([ @@ -47,13 +47,16 @@ describe('Matter.pairDevice', () => { id: 'device-1', name: 'Test Device', number: 1, - clusterClients: new Map(), - childEndpoints: [ + getAllClusterClients: () => [], + getClusterClientById: () => null, + getChildEndpoints: () => [ { id: 'child-endpoint-1', name: 'Child Endpoint', number: 2, - clusterClients, + getAllClusterClients: () => [clusterClient], + getClusterClientById: () => null, + getChildEndpoints: () => [], }, ], }, @@ -66,12 +69,12 @@ describe('Matter.pairDevice', () => { }); it('should pair a bridge device', async () => { const pairingCode = '1450-134-1614'; - const clusterClients = new Map(); - clusterClients.set(6, { + const clusterClient = { + id: 6, addOnOffAttributeListener: fake.returns(null), - }); - const bridgeClusterClients = new Map(); - bridgeClusterClients.set(BridgedDeviceBasicInformation.Complete.id, { + }; + const bridgeClusterClient = { + id: BridgedDeviceBasicInformation.Complete.id, attributes: { vendorName: { get: fake.resolves('Test Vendor'), @@ -92,7 +95,7 @@ describe('Matter.pairDevice', () => { get: fake.resolves('serialNumber'), }, }, - }); + }; matterHandler.commissioningController = { commissionNode: fake.resolves(12345n), getCommissionedNodesDetails: fake.returns([ @@ -114,13 +117,16 @@ describe('Matter.pairDevice', () => { id: 'device-1', name: 'Test Device', number: 1, - clusterClients: bridgeClusterClients, - childEndpoints: [ + getAllClusterClients: () => [bridgeClusterClient], + getClusterClientById: (id) => (id === bridgeClusterClient.id ? bridgeClusterClient : null), + getChildEndpoints: () => [ { id: 'child-endpoint-1', name: 'Child Endpoint', number: 2, - clusterClients, + getAllClusterClients: () => [clusterClient], + getClusterClientById: () => null, + getChildEndpoints: () => [], }, ], }, @@ -128,13 +134,16 @@ describe('Matter.pairDevice', () => { id: 'device-2', name: 'Test Device 2', number: 2, - clusterClients: bridgeClusterClients, - childEndpoints: [ + getAllClusterClients: () => [bridgeClusterClient], + getClusterClientById: (id) => (id === bridgeClusterClient.id ? bridgeClusterClient : null), + getChildEndpoints: () => [ { id: 'child-endpoint-2', name: 'Child Endpoint 2', number: 2, - clusterClients, + getAllClusterClients: () => [clusterClient], + getClusterClientById: () => null, + getChildEndpoints: () => [], }, ], }, diff --git a/server/test/services/matter/lib/matter.refreshDevices.test.js b/server/test/services/matter/lib/matter.refreshDevices.test.js index 1e867e5fb6..2a16141b7d 100644 --- a/server/test/services/matter/lib/matter.refreshDevices.test.js +++ b/server/test/services/matter/lib/matter.refreshDevices.test.js @@ -53,13 +53,16 @@ describe('Matter.refreshDevices', () => { id: 'device-1', name: 'Test Device', number: 1, - clusterClients, - childEndpoints: [ + getAllClusterClients: () => Array.from(clusterClients.values()), + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [ { id: 'child-endpoint-1', name: 'Child Endpoint', number: 2, - clusterClients, + getAllClusterClients: () => Array.from(clusterClients.values()), + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [], }, ], }, diff --git a/server/test/services/matter/lib/matter.setValue.test.js b/server/test/services/matter/lib/matter.setValue.test.js index d05e24e9d0..5137dfcb3a 100644 --- a/server/test/services/matter/lib/matter.setValue.test.js +++ b/server/test/services/matter/lib/matter.setValue.test.js @@ -51,7 +51,8 @@ describe('Matter.setValue', () => { getDevices: fake.returns([ { number: 1, - clusterClients, + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [], }, ]), }); @@ -90,7 +91,8 @@ describe('Matter.setValue', () => { getDevices: fake.returns([ { number: 1, - clusterClients, + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [], }, ]), }); @@ -130,13 +132,16 @@ describe('Matter.setValue', () => { getDevices: fake.returns([ { number: 1, - childEndpoints: [ + getClusterClientById: () => null, + getChildEndpoints: () => [ { number: 2, - childEndpoints: [ + getClusterClientById: () => null, + getChildEndpoints: () => [ { number: 2, - clusterClients, + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [], }, ], }, @@ -179,7 +184,8 @@ describe('Matter.setValue', () => { getDevices: fake.returns([ { number: 1, - clusterClients, + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [], }, ]), }); @@ -218,7 +224,8 @@ describe('Matter.setValue', () => { getDevices: fake.returns([ { number: 1, - clusterClients, + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [], }, ]), }); @@ -265,7 +272,8 @@ describe('Matter.setValue', () => { getDevices: fake.returns([ { number: 1, - clusterClients, + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [], }, ]), }); @@ -319,7 +327,8 @@ describe('Matter.setValue', () => { getDevices: fake.returns([ { number: 1, - clusterClients, + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [], }, ]), }); @@ -366,7 +375,8 @@ describe('Matter.setValue', () => { getDevices: fake.returns([ { number: 1, - clusterClients, + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [], }, ]), }); @@ -404,7 +414,8 @@ describe('Matter.setValue', () => { getDevices: fake.returns([ { number: 1, - clusterClients, + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [], }, ]), }); @@ -475,7 +486,8 @@ describe('Matter.setValue', () => { getDevices: fake.returns([ { number: 1, - clusterClients, + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [], }, ]), }); @@ -507,7 +519,8 @@ describe('Matter.setValue', () => { getDevices: fake.returns([ { number: 1, - clusterClients, + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [], }, ]), }); @@ -539,10 +552,12 @@ describe('Matter.setValue', () => { getDevices: fake.returns([ { number: 1, - childEndpoints: [ + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [ { number: 1, - clusterClients, + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [], }, ], }, From 417aa1dd8ab63fde1151a45df3a7e8e683893325 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie <7365207+Pierre-Gilles@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:37:48 +0100 Subject: [PATCH 03/16] Add cleaner log to device.newStateEvent in case device was not added to Gladys --- server/lib/device/device.newStateEvent.js | 6 ++-- .../lib/device/device.newStateEvent.test.js | 32 +++++++------------ 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/server/lib/device/device.newStateEvent.js b/server/lib/device/device.newStateEvent.js index 341403385a..111eb6d33a 100644 --- a/server/lib/device/device.newStateEvent.js +++ b/server/lib/device/device.newStateEvent.js @@ -15,11 +15,13 @@ const logger = require('../../utils/logger'); async function newStateEvent(event) { const deviceFeature = this.stateManager.get('deviceFeatureByExternalId', event.device_feature_external_id); if (deviceFeature === null) { - throw new NotFoundError(`DeviceFeature ${event.device_feature_external_id} not found`); + logger.info(`DeviceFeature "${event.device_feature_external_id}" not found (or not added to Gladys), skipping state update.`); + return; } const device = this.stateManager.get('deviceById', deviceFeature.device_id); if (device === null) { - throw new NotFoundError(`Device ${deviceFeature.device_id} not found`); + logger.info(`Device "${deviceFeature.device_id}" not found, skipping state update.`); + return; } try { // If there is a "text" property in the even, we save as string diff --git a/server/test/lib/device/device.newStateEvent.test.js b/server/test/lib/device/device.newStateEvent.test.js index 4fe49897fe..dabc1c96a8 100644 --- a/server/test/lib/device/device.newStateEvent.test.js +++ b/server/test/lib/device/device.newStateEvent.test.js @@ -184,16 +184,12 @@ describe('Device.newStateEvent', () => { it('should not save state missing device feature', async () => { const stateManager = new StateManager(event); const device = new Device(event, {}, stateManager, {}, {}, {}, job); - try { - await device.newStateEvent({ - device_feature_external_id: 'hue:binary:1', - state: 12, - created_at: '2019-02-12 07:49:07.556 +00:00', - }); - assert.fail(); - } catch (e) { - expect(e).to.be.instanceOf(NotFoundError); - } + // Should not throw, just log and return early + await device.newStateEvent({ + device_feature_external_id: 'hue:binary:1', + state: 12, + created_at: '2019-02-12 07:49:07.556 +00:00', + }); const newDeviceFeature = stateManager.get('deviceFeatureByExternalId', 'hue:binary:1'); // eslint-disable-next-line no-unused-expressions expect(newDeviceFeature).to.be.null; @@ -209,16 +205,12 @@ describe('Device.newStateEvent', () => { }; stateManager.setState('deviceFeatureByExternalId', 'hue:binary:1', currentDeviceFeature); const device = new Device(event, {}, stateManager, {}, {}, {}, job); - try { - await device.newStateEvent({ - device_feature_external_id: 'hue:binary:1', - state: 12, - created_at: '2019-02-12 07:49:07.556 +00:00', - }); - assert.fail(); - } catch (e) { - expect(e).to.be.instanceOf(NotFoundError); - } + // Should not throw, just log and return early + await device.newStateEvent({ + device_feature_external_id: 'hue:binary:1', + state: 12, + created_at: '2019-02-12 07:49:07.556 +00:00', + }); const newDeviceFeature = stateManager.get('deviceFeatureByExternalId', 'hue:binary:1'); expect(newDeviceFeature).not.to.have.property('last_value'); expect(newDeviceFeature).not.to.have.property('last_value_changed'); From 2bdfd3d134c461bbe529ab34c6bd8714a47597e7 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie <7365207+Pierre-Gilles@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:40:46 +0100 Subject: [PATCH 04/16] refreshDevices should be non-blocking during startup to avoid blocking Gladys --- server/services/matter/lib/matter.init.js | 6 +++++- server/test/services/matter/lib/matter.init.test.js | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/server/services/matter/lib/matter.init.js b/server/services/matter/lib/matter.init.js index 9e52e9d03f..7da45e27a4 100644 --- a/server/services/matter/lib/matter.init.js +++ b/server/services/matter/lib/matter.init.js @@ -53,7 +53,11 @@ async function init() { await this.commissioningController.start(); logger.info('Matter controller started'); - await this.refreshDevices(); + // Refresh devices in the background to avoid blocking Gladys startup + // Store the promise so it can be awaited in tests + this.refreshDevicesPromise = this.refreshDevices().catch((err) => { + logger.error('Matter: Error refreshing devices in background:', err); + }); // Schedule reccurent job if not already scheduled if (!this.backupScheduledJob) { this.backupScheduledJob = this.gladys.scheduler.scheduleJob('0 0 4 * * *', () => this.backupController()); diff --git a/server/test/services/matter/lib/matter.init.test.js b/server/test/services/matter/lib/matter.init.test.js index b1d3846b08..c8cd06ea29 100644 --- a/server/test/services/matter/lib/matter.init.test.js +++ b/server/test/services/matter/lib/matter.init.test.js @@ -349,6 +349,8 @@ describe('Matter.init', () => { it('should initialize matter service successfully', async () => { await matterHandler.init(); + // Wait for background refreshDevices() to complete + await matterHandler.refreshDevicesPromise; expect(matterHandler.devices).to.have.lengthOf(2); // Device selector should be a slug of the name with 4 random characters at the end expect(matterHandler.devices[0].selector).to.satisfy( From 6aba08305d35617809ded7a9d102b085201376e4 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie <7365207+Pierre-Gilles@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:44:26 +0100 Subject: [PATCH 05/16] Fix linting & prettier --- server/lib/device/device.newStateEvent.js | 5 +++-- server/test/lib/device/device.newStateEvent.test.js | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/lib/device/device.newStateEvent.js b/server/lib/device/device.newStateEvent.js index 111eb6d33a..0738ef1548 100644 --- a/server/lib/device/device.newStateEvent.js +++ b/server/lib/device/device.newStateEvent.js @@ -1,4 +1,3 @@ -const { NotFoundError } = require('../../utils/coreErrors'); const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../utils/constants'); const logger = require('../../utils/logger'); @@ -15,7 +14,9 @@ const logger = require('../../utils/logger'); async function newStateEvent(event) { const deviceFeature = this.stateManager.get('deviceFeatureByExternalId', event.device_feature_external_id); if (deviceFeature === null) { - logger.info(`DeviceFeature "${event.device_feature_external_id}" not found (or not added to Gladys), skipping state update.`); + logger.info( + `DeviceFeature "${event.device_feature_external_id}" not found (or not added to Gladys), skipping state update.`, + ); return; } const device = this.stateManager.get('deviceById', deviceFeature.device_id); diff --git a/server/test/lib/device/device.newStateEvent.test.js b/server/test/lib/device/device.newStateEvent.test.js index dabc1c96a8..bd9e43a429 100644 --- a/server/test/lib/device/device.newStateEvent.test.js +++ b/server/test/lib/device/device.newStateEvent.test.js @@ -8,7 +8,6 @@ const { expect } = require('chai'); const Device = require('../../../lib/device'); const StateManager = require('../../../lib/state'); const Job = require('../../../lib/job'); -const { NotFoundError } = require('../../../utils/coreErrors'); const event = new EventEmitter(); const job = new Job(event); From 33f34ff41cc475d688cfb92c23f599b06b2c52ce Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie <7365207+Pierre-Gilles@users.noreply.github.com> Date: Mon, 23 Mar 2026 09:55:00 +0100 Subject: [PATCH 06/16] Add missing test for coverage --- server/test/services/matter/lib/matter.init.test.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/test/services/matter/lib/matter.init.test.js b/server/test/services/matter/lib/matter.init.test.js index c8cd06ea29..73731e0489 100644 --- a/server/test/services/matter/lib/matter.init.test.js +++ b/server/test/services/matter/lib/matter.init.test.js @@ -897,6 +897,16 @@ describe('Matter.init', () => { expect(matterHandler.nodesMap.size).to.equal(0); }); + it('should log error when refreshDevices fails in background', async () => { + const error = new Error('Test error'); + matterHandler.refreshDevices = fake.rejects(error); + await matterHandler.init(); + // Wait for background promise to complete + await matterHandler.refreshDevicesPromise; + // The error should be caught and logged, not thrown + expect(matterHandler.refreshDevicesPromise).to.be.fulfilled; + }); + it('should return PPM for 0', () => { expect(convertMeasurementUnitToDeviceFeatureUnits(0)).to.equal('ppm'); }); From 08fad078977bc3a979dd680e0238bb639d2eede2 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie <7365207+Pierre-Gilles@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:02:36 +0100 Subject: [PATCH 07/16] Fix linting --- server/test/services/matter/lib/matter.init.test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/test/services/matter/lib/matter.init.test.js b/server/test/services/matter/lib/matter.init.test.js index 73731e0489..c33973f986 100644 --- a/server/test/services/matter/lib/matter.init.test.js +++ b/server/test/services/matter/lib/matter.init.test.js @@ -901,10 +901,8 @@ describe('Matter.init', () => { const error = new Error('Test error'); matterHandler.refreshDevices = fake.rejects(error); await matterHandler.init(); - // Wait for background promise to complete + // Wait for background promise to complete - error should be caught and logged, not thrown await matterHandler.refreshDevicesPromise; - // The error should be caught and logged, not thrown - expect(matterHandler.refreshDevicesPromise).to.be.fulfilled; }); it('should return PPM for 0', () => { From 19faba388e1b022bdf3190bb0a3264efd0134380 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie <7365207+Pierre-Gilles@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:06:17 +0100 Subject: [PATCH 08/16] refreshDevices should not fail completely if one device fail --- .../matter/lib/matter.refreshDevices.js | 9 +++- .../matter/lib/matter.refreshDevices.test.js | 50 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/server/services/matter/lib/matter.refreshDevices.js b/server/services/matter/lib/matter.refreshDevices.js index 005e37b584..e875e410bc 100644 --- a/server/services/matter/lib/matter.refreshDevices.js +++ b/server/services/matter/lib/matter.refreshDevices.js @@ -1,5 +1,7 @@ const Promise = require('bluebird'); +const logger = require('../../../utils/logger'); + /** * @description Refresh the devices. * @example matter.refreshDevices(); @@ -10,7 +12,12 @@ async function refreshDevices() { const nodeDetails = this.commissioningController.getCommissionedNodesDetails(); await Promise.each(nodeDetails, async (nodeDetail) => { - await this.handleNode(nodeDetail); + try { + await this.handleNode(nodeDetail); + } catch (err) { + // Log error but continue with other devices - one unreachable device shouldn't break the others + logger.warn(`Matter: Error handling node ${nodeDetail.nodeId}: ${err.message}`); + } }); } diff --git a/server/test/services/matter/lib/matter.refreshDevices.test.js b/server/test/services/matter/lib/matter.refreshDevices.test.js index 2a16141b7d..46a98c0ed9 100644 --- a/server/test/services/matter/lib/matter.refreshDevices.test.js +++ b/server/test/services/matter/lib/matter.refreshDevices.test.js @@ -79,4 +79,54 @@ describe('Matter.refreshDevices', () => { expect(matterHandler.devices).to.have.lengthOf(2); expect(matterHandler.nodesMap.size).to.equal(1); }); + + it('should continue with other devices when one node fails', async () => { + let callCount = 0; + // First node throws an error, second node succeeds + matterHandler.commissioningController = { + getCommissionedNodesDetails: fake.returns([ + { + nodeId: 11111n, + deviceData: { + basicInformation: { + vendorName: 'Unreachable Vendor', + productName: 'Unreachable Product', + }, + }, + }, + { + nodeId: 22222n, + deviceData: { + basicInformation: { + vendorName: 'Reachable Vendor', + productName: 'Reachable Product', + }, + }, + }, + ]), + getNode: sinon.stub().callsFake(async () => { + callCount += 1; + if (callCount === 1) { + throw new Error('Node is not reachable right now'); + } + return { + getDevices: fake.returns([ + { + id: 'device-2', + name: 'Reachable Device', + number: 1, + getAllClusterClients: () => Array.from(clusterClients.values()), + getClusterClientById: (id) => clusterClients.get(id), + getChildEndpoints: () => [], + }, + ]), + }; + }), + }; + + await matterHandler.refreshDevices(); + // Should have 1 device from the second node (first node failed) + expect(matterHandler.devices).to.have.lengthOf(1); + expect(matterHandler.devices[0].name).to.include('Reachable'); + }); }); From 987f2d6efc4a5b25f74e54fcc6c93fbc4fef3cc8 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie <7365207+Pierre-Gilles@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:18:08 +0200 Subject: [PATCH 09/16] Upgrade Matter.js to v0.16.11 --- server/package-lock.json | 166 +++++++++++------------ server/package.json | 2 +- server/services/matter/package-lock.json | 102 +++++++------- server/services/matter/package.json | 4 +- 4 files changed, 137 insertions(+), 137 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 047b10e7fe..82491feb4a 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -53,7 +53,7 @@ "ws": "^6.2.2" }, "devDependencies": { - "@matter/main": "^0.16.10", + "@matter/main": "^0.16.11", "apidoc": "^1.0.3", "chai": "^4.3.7", "chai-as-promised": "^7.1.1", @@ -1062,9 +1062,9 @@ } }, "node_modules/@matter/general": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.16.10.tgz", - "integrity": "sha512-/qytvaxvDDhEdHLaEoxlEFVBWg982jL+XXvOmAFgIv92yGDQvN4U+VcNW7S5dueJuv/L+gi0zDqhl8LUzHHAlg==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.16.11.tgz", + "integrity": "sha512-iOnCR7azYgRzr4p/WQRRmbediZFYsqfaqSIp7yyGhnfn2gx386F/Wh96HGXYWdprTWet/7IsFV6uiA9jcDNHvA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1072,83 +1072,83 @@ } }, "node_modules/@matter/main": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.16.10.tgz", - "integrity": "sha512-QKoQrmMnt6cB893+Oezk3DdIVgQJj5spt/ikEszF8pjyTuvB69zlzq1wnrn52N3mi57R6UWwk6+BPEbQE95FSw==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.16.11.tgz", + "integrity": "sha512-FDFhTix4AcH7t7TP7PnU2smFayza2RrI3uppSRzEmJ+Vxy8btelJMpDjBcxBbNb2Iao7jX7W8DLPMMuH6AzMQg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.16.10", - "@matter/model": "0.16.10", - "@matter/node": "0.16.10", - "@matter/protocol": "0.16.10", - "@matter/types": "0.16.10" + "@matter/general": "0.16.11", + "@matter/model": "0.16.11", + "@matter/node": "0.16.11", + "@matter/protocol": "0.16.11", + "@matter/types": "0.16.11" }, "optionalDependencies": { - "@matter/nodejs": "0.16.10" + "@matter/nodejs": "0.16.11" } }, "node_modules/@matter/model": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.16.10.tgz", - "integrity": "sha512-6Ei8gETAkcKGEMRW+z8Mak55Y1Jl1TKGQIboC/4vvsrqcvB8zhIvGBS3GaAllxzvF0qjE7ihCPpgXXr6HuTLyg==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.16.11.tgz", + "integrity": "sha512-7a64fUnf3EFwfqt5C/p4aR9RrQBWgNYnP/FKPAI4wC6cp3OyOm9Yt7fqE//Q6ikPpU867kMLxhMHwmgvntMgtA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.16.10" + "@matter/general": "0.16.11" } }, "node_modules/@matter/node": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.16.10.tgz", - "integrity": "sha512-Hb2AxuEf0DlfN8yHxeahZGYurUUu/UDWJkmdvpDKuwSR0eIHhMweOG2RBO55/anyRFObANaUr1gr3DnyocB/0w==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.16.11.tgz", + "integrity": "sha512-Y+ji+A8iWRCaG2HI7yXlvgOizwePIt3lSZV+wVo+Pyf86vwtDLxxY225nfRcV/Iwawmm/l2WM8aoPaNh37C0iw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.16.10", - "@matter/model": "0.16.10", - "@matter/protocol": "0.16.10", - "@matter/types": "0.16.10" + "@matter/general": "0.16.11", + "@matter/model": "0.16.11", + "@matter/protocol": "0.16.11", + "@matter/types": "0.16.11" } }, "node_modules/@matter/nodejs": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.16.10.tgz", - "integrity": "sha512-o8e9tGZVsiqmEtD1osSL9+gRMIuNjtwXt29I3YDRzSqb3N5Dvd4Khj3HN68YhaFFeBlM4YEV2TS0/9VF3C06/w==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.16.11.tgz", + "integrity": "sha512-Q/kOWerSnHHUI5A/FnD6Tz61asO0C4Rz/hemF3L7DAWu4nEEq6O2msH+RTaj3NrHK9ef28PGpTMS35PbMpFOyg==", "dev": true, "license": "Apache-2.0", "optional": true, "dependencies": { - "@matter/general": "0.16.10", - "@matter/node": "0.16.10", - "@matter/protocol": "0.16.10", - "@matter/types": "0.16.10" + "@matter/general": "0.16.11", + "@matter/node": "0.16.11", + "@matter/protocol": "0.16.11", + "@matter/types": "0.16.11" }, "engines": { "node": ">=20.19.0 <22.0.0 || >=22.13.0" } }, "node_modules/@matter/protocol": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.16.10.tgz", - "integrity": "sha512-vPQEMl8Wf4vc3tauwsGlLZRrDx+VrCPfccw3n50Lvyucws4UBt+SuBszSyIO2+QPnJcr4MB4L+EgCRI14U4zRA==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.16.11.tgz", + "integrity": "sha512-+Q6s+Cmvcm5BJEFBtS6m0vUVrHdxTBteDcJ+VfpNna1ST910371lVqtDwYFclqECQMlDYxoNjkfcYNv4pZfNPg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.16.10", - "@matter/model": "0.16.10", - "@matter/types": "0.16.10" + "@matter/general": "0.16.11", + "@matter/model": "0.16.11", + "@matter/types": "0.16.11" } }, "node_modules/@matter/types": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.16.10.tgz", - "integrity": "sha512-AW86tGgL3sN1Vb76+03VPdQMsW9vWsaVGHe9agcED1sjtWRJBV17tcLDymAQ8k7fEeVqiy3qf9CUwFK1bhfXKQ==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.16.11.tgz", + "integrity": "sha512-RcZk5N4AeoqKaLzlC6M6+J8YGJgxnLYpF/3WL7CeIM1DpA9ebkPQB9B8of+RJnqy8kgr6eqL/3ATmPGDA46fwg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.16.10", - "@matter/model": "0.16.10" + "@matter/general": "0.16.11", + "@matter/model": "0.16.11" } }, "node_modules/@microsoft/recognizers-text": { @@ -13146,81 +13146,81 @@ } }, "@matter/general": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.16.10.tgz", - "integrity": "sha512-/qytvaxvDDhEdHLaEoxlEFVBWg982jL+XXvOmAFgIv92yGDQvN4U+VcNW7S5dueJuv/L+gi0zDqhl8LUzHHAlg==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.16.11.tgz", + "integrity": "sha512-iOnCR7azYgRzr4p/WQRRmbediZFYsqfaqSIp7yyGhnfn2gx386F/Wh96HGXYWdprTWet/7IsFV6uiA9jcDNHvA==", "dev": true, "requires": { "@noble/curves": "^2.0.1" } }, "@matter/main": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.16.10.tgz", - "integrity": "sha512-QKoQrmMnt6cB893+Oezk3DdIVgQJj5spt/ikEszF8pjyTuvB69zlzq1wnrn52N3mi57R6UWwk6+BPEbQE95FSw==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.16.11.tgz", + "integrity": "sha512-FDFhTix4AcH7t7TP7PnU2smFayza2RrI3uppSRzEmJ+Vxy8btelJMpDjBcxBbNb2Iao7jX7W8DLPMMuH6AzMQg==", "dev": true, "requires": { - "@matter/general": "0.16.10", - "@matter/model": "0.16.10", - "@matter/node": "0.16.10", - "@matter/nodejs": "0.16.10", - "@matter/protocol": "0.16.10", - "@matter/types": "0.16.10" + "@matter/general": "0.16.11", + "@matter/model": "0.16.11", + "@matter/node": "0.16.11", + "@matter/nodejs": "0.16.11", + "@matter/protocol": "0.16.11", + "@matter/types": "0.16.11" } }, "@matter/model": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.16.10.tgz", - "integrity": "sha512-6Ei8gETAkcKGEMRW+z8Mak55Y1Jl1TKGQIboC/4vvsrqcvB8zhIvGBS3GaAllxzvF0qjE7ihCPpgXXr6HuTLyg==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.16.11.tgz", + "integrity": "sha512-7a64fUnf3EFwfqt5C/p4aR9RrQBWgNYnP/FKPAI4wC6cp3OyOm9Yt7fqE//Q6ikPpU867kMLxhMHwmgvntMgtA==", "dev": true, "requires": { - "@matter/general": "0.16.10" + "@matter/general": "0.16.11" } }, "@matter/node": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.16.10.tgz", - "integrity": "sha512-Hb2AxuEf0DlfN8yHxeahZGYurUUu/UDWJkmdvpDKuwSR0eIHhMweOG2RBO55/anyRFObANaUr1gr3DnyocB/0w==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.16.11.tgz", + "integrity": "sha512-Y+ji+A8iWRCaG2HI7yXlvgOizwePIt3lSZV+wVo+Pyf86vwtDLxxY225nfRcV/Iwawmm/l2WM8aoPaNh37C0iw==", "dev": true, "requires": { - "@matter/general": "0.16.10", - "@matter/model": "0.16.10", - "@matter/protocol": "0.16.10", - "@matter/types": "0.16.10" + "@matter/general": "0.16.11", + "@matter/model": "0.16.11", + "@matter/protocol": "0.16.11", + "@matter/types": "0.16.11" } }, "@matter/nodejs": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.16.10.tgz", - "integrity": "sha512-o8e9tGZVsiqmEtD1osSL9+gRMIuNjtwXt29I3YDRzSqb3N5Dvd4Khj3HN68YhaFFeBlM4YEV2TS0/9VF3C06/w==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.16.11.tgz", + "integrity": "sha512-Q/kOWerSnHHUI5A/FnD6Tz61asO0C4Rz/hemF3L7DAWu4nEEq6O2msH+RTaj3NrHK9ef28PGpTMS35PbMpFOyg==", "dev": true, "optional": true, "requires": { - "@matter/general": "0.16.10", - "@matter/node": "0.16.10", - "@matter/protocol": "0.16.10", - "@matter/types": "0.16.10" + "@matter/general": "0.16.11", + "@matter/node": "0.16.11", + "@matter/protocol": "0.16.11", + "@matter/types": "0.16.11" } }, "@matter/protocol": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.16.10.tgz", - "integrity": "sha512-vPQEMl8Wf4vc3tauwsGlLZRrDx+VrCPfccw3n50Lvyucws4UBt+SuBszSyIO2+QPnJcr4MB4L+EgCRI14U4zRA==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.16.11.tgz", + "integrity": "sha512-+Q6s+Cmvcm5BJEFBtS6m0vUVrHdxTBteDcJ+VfpNna1ST910371lVqtDwYFclqECQMlDYxoNjkfcYNv4pZfNPg==", "dev": true, "requires": { - "@matter/general": "0.16.10", - "@matter/model": "0.16.10", - "@matter/types": "0.16.10" + "@matter/general": "0.16.11", + "@matter/model": "0.16.11", + "@matter/types": "0.16.11" } }, "@matter/types": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.16.10.tgz", - "integrity": "sha512-AW86tGgL3sN1Vb76+03VPdQMsW9vWsaVGHe9agcED1sjtWRJBV17tcLDymAQ8k7fEeVqiy3qf9CUwFK1bhfXKQ==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.16.11.tgz", + "integrity": "sha512-RcZk5N4AeoqKaLzlC6M6+J8YGJgxnLYpF/3WL7CeIM1DpA9ebkPQB9B8of+RJnqy8kgr6eqL/3ATmPGDA46fwg==", "dev": true, "requires": { - "@matter/general": "0.16.10", - "@matter/model": "0.16.10" + "@matter/general": "0.16.11", + "@matter/model": "0.16.11" } }, "@microsoft/recognizers-text": { diff --git a/server/package.json b/server/package.json index b5ad5cc1e0..65ff7411a6 100644 --- a/server/package.json +++ b/server/package.json @@ -46,7 +46,7 @@ ] }, "devDependencies": { - "@matter/main": "^0.16.10", + "@matter/main": "^0.16.11", "apidoc": "^1.0.3", "chai": "^4.3.7", "chai-as-promised": "^7.1.1", diff --git a/server/services/matter/package-lock.json b/server/services/matter/package-lock.json index 210445bb14..6402b1532b 100644 --- a/server/services/matter/package-lock.json +++ b/server/services/matter/package-lock.json @@ -16,93 +16,93 @@ "win32" ], "dependencies": { - "@matter/main": "^0.16.10", - "@project-chip/matter.js": "^0.16.10", + "@matter/main": "^0.16.11", + "@project-chip/matter.js": "^0.16.11", "bluebird": "^3.7.2", "fs-extra": "^11.3.0" } }, "node_modules/@matter/general": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.16.10.tgz", - "integrity": "sha512-/qytvaxvDDhEdHLaEoxlEFVBWg982jL+XXvOmAFgIv92yGDQvN4U+VcNW7S5dueJuv/L+gi0zDqhl8LUzHHAlg==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/general/-/general-0.16.11.tgz", + "integrity": "sha512-iOnCR7azYgRzr4p/WQRRmbediZFYsqfaqSIp7yyGhnfn2gx386F/Wh96HGXYWdprTWet/7IsFV6uiA9jcDNHvA==", "license": "Apache-2.0", "dependencies": { "@noble/curves": "^2.0.1" } }, "node_modules/@matter/main": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.16.10.tgz", - "integrity": "sha512-QKoQrmMnt6cB893+Oezk3DdIVgQJj5spt/ikEszF8pjyTuvB69zlzq1wnrn52N3mi57R6UWwk6+BPEbQE95FSw==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/main/-/main-0.16.11.tgz", + "integrity": "sha512-FDFhTix4AcH7t7TP7PnU2smFayza2RrI3uppSRzEmJ+Vxy8btelJMpDjBcxBbNb2Iao7jX7W8DLPMMuH6AzMQg==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.16.10", - "@matter/model": "0.16.10", - "@matter/node": "0.16.10", - "@matter/protocol": "0.16.10", - "@matter/types": "0.16.10" + "@matter/general": "0.16.11", + "@matter/model": "0.16.11", + "@matter/node": "0.16.11", + "@matter/protocol": "0.16.11", + "@matter/types": "0.16.11" }, "optionalDependencies": { - "@matter/nodejs": "0.16.10" + "@matter/nodejs": "0.16.11" } }, "node_modules/@matter/model": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.16.10.tgz", - "integrity": "sha512-6Ei8gETAkcKGEMRW+z8Mak55Y1Jl1TKGQIboC/4vvsrqcvB8zhIvGBS3GaAllxzvF0qjE7ihCPpgXXr6HuTLyg==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/model/-/model-0.16.11.tgz", + "integrity": "sha512-7a64fUnf3EFwfqt5C/p4aR9RrQBWgNYnP/FKPAI4wC6cp3OyOm9Yt7fqE//Q6ikPpU867kMLxhMHwmgvntMgtA==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.16.10" + "@matter/general": "0.16.11" } }, "node_modules/@matter/node": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.16.10.tgz", - "integrity": "sha512-Hb2AxuEf0DlfN8yHxeahZGYurUUu/UDWJkmdvpDKuwSR0eIHhMweOG2RBO55/anyRFObANaUr1gr3DnyocB/0w==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/node/-/node-0.16.11.tgz", + "integrity": "sha512-Y+ji+A8iWRCaG2HI7yXlvgOizwePIt3lSZV+wVo+Pyf86vwtDLxxY225nfRcV/Iwawmm/l2WM8aoPaNh37C0iw==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.16.10", - "@matter/model": "0.16.10", - "@matter/protocol": "0.16.10", - "@matter/types": "0.16.10" + "@matter/general": "0.16.11", + "@matter/model": "0.16.11", + "@matter/protocol": "0.16.11", + "@matter/types": "0.16.11" } }, "node_modules/@matter/nodejs": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.16.10.tgz", - "integrity": "sha512-o8e9tGZVsiqmEtD1osSL9+gRMIuNjtwXt29I3YDRzSqb3N5Dvd4Khj3HN68YhaFFeBlM4YEV2TS0/9VF3C06/w==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/nodejs/-/nodejs-0.16.11.tgz", + "integrity": "sha512-Q/kOWerSnHHUI5A/FnD6Tz61asO0C4Rz/hemF3L7DAWu4nEEq6O2msH+RTaj3NrHK9ef28PGpTMS35PbMpFOyg==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@matter/general": "0.16.10", - "@matter/node": "0.16.10", - "@matter/protocol": "0.16.10", - "@matter/types": "0.16.10" + "@matter/general": "0.16.11", + "@matter/node": "0.16.11", + "@matter/protocol": "0.16.11", + "@matter/types": "0.16.11" }, "engines": { "node": ">=20.19.0 <22.0.0 || >=22.13.0" } }, "node_modules/@matter/protocol": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.16.10.tgz", - "integrity": "sha512-vPQEMl8Wf4vc3tauwsGlLZRrDx+VrCPfccw3n50Lvyucws4UBt+SuBszSyIO2+QPnJcr4MB4L+EgCRI14U4zRA==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/protocol/-/protocol-0.16.11.tgz", + "integrity": "sha512-+Q6s+Cmvcm5BJEFBtS6m0vUVrHdxTBteDcJ+VfpNna1ST910371lVqtDwYFclqECQMlDYxoNjkfcYNv4pZfNPg==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.16.10", - "@matter/model": "0.16.10", - "@matter/types": "0.16.10" + "@matter/general": "0.16.11", + "@matter/model": "0.16.11", + "@matter/types": "0.16.11" } }, "node_modules/@matter/types": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.16.10.tgz", - "integrity": "sha512-AW86tGgL3sN1Vb76+03VPdQMsW9vWsaVGHe9agcED1sjtWRJBV17tcLDymAQ8k7fEeVqiy3qf9CUwFK1bhfXKQ==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@matter/types/-/types-0.16.11.tgz", + "integrity": "sha512-RcZk5N4AeoqKaLzlC6M6+J8YGJgxnLYpF/3WL7CeIM1DpA9ebkPQB9B8of+RJnqy8kgr6eqL/3ATmPGDA46fwg==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.16.10", - "@matter/model": "0.16.10" + "@matter/general": "0.16.11", + "@matter/model": "0.16.11" } }, "node_modules/@noble/curves": { @@ -133,16 +133,16 @@ } }, "node_modules/@project-chip/matter.js": { - "version": "0.16.10", - "resolved": "https://registry.npmjs.org/@project-chip/matter.js/-/matter.js-0.16.10.tgz", - "integrity": "sha512-5SpMsaNftqrqHSppP8J1RTdWdtJgehs3k9CVf0cM0IrZLQMK67W2cWVlTbsJElV7zOfKatjU56FdRA2UgkbvMQ==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/@project-chip/matter.js/-/matter.js-0.16.11.tgz", + "integrity": "sha512-FAgJuh1uSWY7o+Hn0rVKS7i6+5crv3lGekwp002JoU5joty8jdNqMKUiSfwNtPGdYAkm5aMa7KTOaewHkx5taQ==", "license": "Apache-2.0", "dependencies": { - "@matter/general": "0.16.10", - "@matter/model": "0.16.10", - "@matter/node": "0.16.10", - "@matter/protocol": "0.16.10", - "@matter/types": "0.16.10" + "@matter/general": "0.16.11", + "@matter/model": "0.16.11", + "@matter/node": "0.16.11", + "@matter/protocol": "0.16.11", + "@matter/types": "0.16.11" } }, "node_modules/bluebird": { diff --git a/server/services/matter/package.json b/server/services/matter/package.json index b696541e9d..97647f4661 100644 --- a/server/services/matter/package.json +++ b/server/services/matter/package.json @@ -12,8 +12,8 @@ "arm64" ], "dependencies": { - "@matter/main": "^0.16.10", - "@project-chip/matter.js": "^0.16.10", + "@matter/main": "^0.16.11", + "@project-chip/matter.js": "^0.16.11", "bluebird": "^3.7.2", "fs-extra": "^11.3.0" } From 03cd3b1f73d754b3dba6a3b7d8b1d6f178bd29e7 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie <7365207+Pierre-Gilles@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:42:04 +0200 Subject: [PATCH 10/16] Set log level to debug in matter.js --- server/services/matter/lib/matter.init.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/services/matter/lib/matter.init.js b/server/services/matter/lib/matter.init.js index 7da45e27a4..6f150c2b27 100644 --- a/server/services/matter/lib/matter.init.js +++ b/server/services/matter/lib/matter.init.js @@ -35,7 +35,7 @@ async function init() { // Set the log level to "notice" // Log levels are defined here: // https://github.com/project-chip/matter.js/blob/b0ffc2ff3c8acd7fef19918337d4fd95dfa466e6/packages/general/src/log/LogLevel.ts - Logger.level = LogLevel('notice'); + Logger.level = LogLevel('debug'); const storageService = environment.get(StorageService); storageService.location = storagePath; From c7256195f0034dacbfb8ee1bfb4caf681e8919e7 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie <7365207+Pierre-Gilles@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:18:18 +0200 Subject: [PATCH 11/16] Add new button to reset Matter integration --- front/src/config/i18n/en.json | 11 ++- front/src/config/i18n/fr.json | 11 ++- .../all/matter/MatterSettingsPage.jsx | 82 ++++++++++++++++- .../services/matter/api/matter.controller.js | 15 ++++ server/services/matter/lib/index.js | 2 + server/services/matter/lib/matter.reset.js | 43 +++++++++ .../matter/api/matter.controller.test.js | 11 +++ .../services/matter/lib/matter.reset.test.js | 90 +++++++++++++++++++ 8 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 server/services/matter/lib/matter.reset.js create mode 100644 server/test/services/matter/lib/matter.reset.test.js diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 9de1d41897..3469c9974e 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -2133,7 +2133,16 @@ "disabledWarningSettings": "Matter is not activated, please click on the button below to activate it.", "enableButton": "Enable Matter", "disableButton": "Disable Matter", - "downloadNodesJson": "Download Nodes JSON" + "downloadNodesJson": "Download Nodes JSON", + "reset": { + "title": "Reset Integration", + "description": "Use this option if you want to start from scratch with Matter. This will delete all controller data, backup, and Matter folder.", + "button": "Reset", + "confirmMessage": "Warning, resetting the integration will delete all Matter controller data, backup, and Matter folder. All Matter devices currently paired will need to be re-paired.", + "confirmButton": "Yes, reset everything", + "cancelButton": "Cancel", + "error": "An error occurred while resetting the integration." + } }, "device": { "title": "Matter Devices", diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index aae0b1fcaf..1cf37f933d 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -2164,7 +2164,16 @@ "disabledWarningSettings": "Matter n'est pas activé, veuillez cliquer sur le bouton ci-dessous pour l'activer.", "enableButton": "Activer Matter", "disableButton": "Désactiver Matter", - "downloadNodesJson": "Télécharger Noeuds Matter en JSON" + "downloadNodesJson": "Télécharger Noeuds Matter en JSON", + "reset": { + "title": "Réinitialiser l'intégration", + "description": "Utilisez cette option si vous souhaitez repartir de zéro avec Matter. Cela supprimera toutes les données du contrôleur, la sauvegarde et le dossier Matter.", + "button": "Réinitialiser", + "confirmMessage": "Attention, la réinitialisation de l'intégration supprimera toutes les données du contrôleur Matter, la sauvegarde et le dossier Matter. Tous les appareils Matter actuellement appairés devront être ré-appairés.", + "confirmButton": "Oui, tout réinitialiser", + "cancelButton": "Annuler", + "error": "Une erreur est survenue lors de la réinitialisation de l'intégration." + } } }, "mcp": { diff --git a/front/src/routes/integration/all/matter/MatterSettingsPage.jsx b/front/src/routes/integration/all/matter/MatterSettingsPage.jsx index 2dfe725d05..0d6f3e7b69 100644 --- a/front/src/routes/integration/all/matter/MatterSettingsPage.jsx +++ b/front/src/routes/integration/all/matter/MatterSettingsPage.jsx @@ -136,7 +136,10 @@ class MatterSettingsPage extends Component { loadingNodes: true, decommissioningNodes: {}, collapsedDevices: {}, - visibleKeys: {} + visibleKeys: {}, + showConfirmReset: false, + resetting: false, + resetError: null }; } @@ -319,6 +322,29 @@ class MatterSettingsPage extends Component { window.URL.revokeObjectURL(url); }; + showConfirmReset = () => { + this.setState({ showConfirmReset: true }); + }; + + cancelReset = () => { + this.setState({ showConfirmReset: false }); + }; + + confirmReset = async () => { + this.setState({ showConfirmReset: false, resetting: true, resetError: null }); + try { + await this.props.httpClient.post('/api/v1/service/matter/reset'); + this.setState({ + resetting: false, + matterEnabled: false, + nodes: [] + }); + } catch (e) { + console.error(e); + this.setState({ resetting: false, resetError: true }); + } + }; + render() { const { matterEnabled, @@ -330,7 +356,10 @@ class MatterSettingsPage extends Component { decommissioningNodes, collapsedDevices, visibleKeys, - hasIpv6 + hasIpv6, + showConfirmReset, + resetting, + resetError } = this.state; return ( @@ -464,6 +493,55 @@ class MatterSettingsPage extends Component { + +
+

+ +

+

+ +

+ + {resetError && ( +
+ +
+ )} + + {!showConfirmReset && ( + + )} + + {showConfirmReset && ( +
+
+ +
+
+ + +
+
+ )} +
)} diff --git a/server/services/matter/api/matter.controller.js b/server/services/matter/api/matter.controller.js index eb172f0a83..2413ce853f 100644 --- a/server/services/matter/api/matter.controller.js +++ b/server/services/matter/api/matter.controller.js @@ -59,6 +59,16 @@ module.exports = function MatterController(matterHandler) { res.json({ success: true }); } + /** + * @api {post} /api/v1/service/matter/reset Reset Matter integration + * @apiName reset + * @apiGroup Matter + */ + async function reset(req, res) { + await matterHandler.reset(); + res.json({ success: true }); + } + return { 'post /api/v1/service/matter/pair-device': { authenticated: true, @@ -80,5 +90,10 @@ module.exports = function MatterController(matterHandler) { authenticated: true, controller: asyncMiddleware(decommissionNode), }, + 'post /api/v1/service/matter/reset': { + authenticated: true, + admin: true, + controller: asyncMiddleware(reset), + }, }; }; diff --git a/server/services/matter/lib/index.js b/server/services/matter/lib/index.js index 676b18ee50..08428f7d62 100644 --- a/server/services/matter/lib/index.js +++ b/server/services/matter/lib/index.js @@ -14,6 +14,7 @@ const { checkIpv6 } = require('./matter.checkIpv6'); const { refreshDevices } = require('./matter.refreshDevices'); const { backupController } = require('./matter.backupController'); const { restoreBackup } = require('./matter.restoreBackup'); +const { reset } = require('./matter.reset'); /** * @description Matter handler. @@ -52,5 +53,6 @@ MatterHandler.prototype.checkIpv6 = checkIpv6; MatterHandler.prototype.refreshDevices = refreshDevices; MatterHandler.prototype.backupController = backupController; MatterHandler.prototype.restoreBackup = restoreBackup; +MatterHandler.prototype.reset = reset; module.exports = MatterHandler; diff --git a/server/services/matter/lib/matter.reset.js b/server/services/matter/lib/matter.reset.js new file mode 100644 index 0000000000..92b492f8c1 --- /dev/null +++ b/server/services/matter/lib/matter.reset.js @@ -0,0 +1,43 @@ +const path = require('path'); +const fse = require('fs-extra'); + +const logger = require('../../../utils/logger'); +const { VARIABLES } = require('../utils/constants'); + +/** + * @description Reset the Matter integration to factory defaults. + * This will delete all controller data, backup, and matter folder. + * @example + * await matter.reset(); + */ +async function reset() { + logger.info('Matter: resetting integration...'); + + // 1. Stop the Matter controller + await this.stop(); + + // 2. Delete the Matter backup variable from database + await this.gladys.variable.destroy(VARIABLES.MATTER_BACKUP, this.serviceId); + logger.info('Matter: backup variable deleted'); + + // 3. Set MATTER_ENABLED to false + await this.gladys.variable.setValue(VARIABLES.MATTER_ENABLED, 'false', this.serviceId); + logger.info('Matter: MATTER_ENABLED set to false'); + + // 4. Delete the Matter folder on disk + const storagePath = process.env.MATTER_FOLDER_PATH || path.join(path.dirname(this.gladys.config.storage), 'matter'); + await fse.remove(storagePath); + logger.info('Matter: folder deleted successfully'); + + // 5. Reset in-memory state + this.commissioningController = null; + this.devices = []; + this.nodesMap = new Map(); + this.stateChangeListeners = new Set(); + + logger.info('Matter: integration reset complete'); +} + +module.exports = { + reset, +}; diff --git a/server/test/services/matter/api/matter.controller.test.js b/server/test/services/matter/api/matter.controller.test.js index 0b9c40a403..26fbcc0366 100644 --- a/server/test/services/matter/api/matter.controller.test.js +++ b/server/test/services/matter/api/matter.controller.test.js @@ -28,6 +28,7 @@ describe('MatterController', () => { }), decommission: fake.resolves(null), listenToStateChange: fake.resolves(null), + reset: fake.resolves(null), }; controller = MatterController(matterHandler); sinon.reset(); @@ -122,4 +123,14 @@ describe('MatterController', () => { assert.calledOnce(matterHandler.decommission); assert.calledWith(res.json, { success: true }); }); + it('should reset matter integration', async () => { + const req = {}; + const res = { + json: fake.returns(null), + }; + + await controller['post /api/v1/service/matter/reset'].controller(req, res); + assert.calledOnce(matterHandler.reset); + assert.calledWith(res.json, { success: true }); + }); }); diff --git a/server/test/services/matter/lib/matter.reset.test.js b/server/test/services/matter/lib/matter.reset.test.js new file mode 100644 index 0000000000..d51b1fc892 --- /dev/null +++ b/server/test/services/matter/lib/matter.reset.test.js @@ -0,0 +1,90 @@ +const sinon = require('sinon'); +const path = require('path'); +const proxyquire = require('proxyquire').noCallThru(); + +const { fake, assert } = sinon; + +const MatterHandler = require('../../../../services/matter/lib'); + +describe('Matter.reset', () => { + let matterHandler; + let gladys; + + beforeEach(() => { + gladys = { + job: { + wrapper: fake.returns(null), + }, + variable: { + destroy: fake.resolves(null), + setValue: fake.resolves(null), + }, + config: { + storage: '/var/lib/gladys/gladys.db', + }, + }; + const MatterMain = {}; + const ProjectChipMatter = {}; + + matterHandler = new MatterHandler(gladys, MatterMain, ProjectChipMatter, 'service-1'); + matterHandler.stop = fake.resolves(null); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should reset matter integration', async () => { + const fseRemoveFake = fake.resolves(null); + + const { reset } = proxyquire('../../../../services/matter/lib/matter.reset', { + 'fs-extra': { + remove: fseRemoveFake, + }, + }); + + matterHandler.commissioningController = { + close: fake.resolves(null), + }; + matterHandler.devices = [{ name: 'device1' }]; + matterHandler.nodesMap = new Map([['node1', {}]]); + matterHandler.stateChangeListeners = new Set(['listener1']); + + await reset.call(matterHandler); + + assert.calledOnce(matterHandler.stop); + assert.calledOnceWithExactly(gladys.variable.destroy, 'MATTER_BACKUP', 'service-1'); + assert.calledOnceWithExactly(gladys.variable.setValue, 'MATTER_ENABLED', 'false', 'service-1'); + assert.calledOnceWithExactly(fseRemoveFake, path.join('/var/lib/gladys', 'matter')); + + // Verify in-memory state is reset + sinon.assert.match(matterHandler.commissioningController, null); + sinon.assert.match(matterHandler.devices, []); + sinon.assert.match(matterHandler.nodesMap.size, 0); + sinon.assert.match(matterHandler.stateChangeListeners.size, 0); + }); + + it('should reset matter integration with custom MATTER_FOLDER_PATH', async () => { + const originalEnv = process.env.MATTER_FOLDER_PATH; + process.env.MATTER_FOLDER_PATH = '/custom/matter/path'; + + const fseRemoveFake = fake.resolves(null); + + const { reset } = proxyquire('../../../../services/matter/lib/matter.reset', { + 'fs-extra': { + remove: fseRemoveFake, + }, + }); + + await reset.call(matterHandler); + + assert.calledOnceWithExactly(fseRemoveFake, '/custom/matter/path'); + + // Restore original env + if (originalEnv === undefined) { + delete process.env.MATTER_FOLDER_PATH; + } else { + process.env.MATTER_FOLDER_PATH = originalEnv; + } + }); +}); From f2ea5317eca087447c0fe045c3f969cdc760c97c Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie <7365207+Pierre-Gilles@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:21:18 +0200 Subject: [PATCH 12/16] Run prettier front --- .../integration/all/matter/MatterSettingsPage.jsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/front/src/routes/integration/all/matter/MatterSettingsPage.jsx b/front/src/routes/integration/all/matter/MatterSettingsPage.jsx index 0d6f3e7b69..ac20742886 100644 --- a/front/src/routes/integration/all/matter/MatterSettingsPage.jsx +++ b/front/src/routes/integration/all/matter/MatterSettingsPage.jsx @@ -524,18 +524,10 @@ class MatterSettingsPage extends Component {
- -
From a4b5a03838a2a758955ce9b8f88555e7a6c2b05f Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie <7365207+Pierre-Gilles@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:26:47 +0200 Subject: [PATCH 13/16] Add de.json --- front/src/config/i18n/de.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json index a7f017e1f2..70efc012ba 100644 --- a/front/src/config/i18n/de.json +++ b/front/src/config/i18n/de.json @@ -2133,7 +2133,16 @@ "disabledWarningSettings": "Matter ist nicht aktiviert, bitte klicke auf den Button unten, um es zu aktivieren.", "enableButton": "Matter aktivieren", "disableButton": "Matter deaktivieren", - "downloadNodesJson": "Knoten-JSON herunterladen" + "downloadNodesJson": "Knoten-JSON herunterladen", + "reset": { + "title": "Integration zurücksetzen", + "description": "Verwende diese Option, wenn du mit Matter von vorne beginnen möchtest. Dadurch werden alle Controller-Daten, Backups und der Matter-Ordner gelöscht.", + "button": "Zurücksetzen", + "confirmMessage": "Achtung, das Zurücksetzen der Integration löscht alle Matter-Controller-Daten, Backups und den Matter-Ordner. Alle derzeit gekoppelten Matter-Geräte müssen erneut gekoppelt werden.", + "confirmButton": "Ja, alles zurücksetzen", + "cancelButton": "Abbrechen", + "error": "Beim Zurücksetzen der Integration ist ein Fehler aufgetreten." + } }, "device": { "title": "Matter-Geräte", From a0a20213028f9b917f377ec4fe3c5a55ab73a883 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie <7365207+Pierre-Gilles@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:34:50 +0200 Subject: [PATCH 14/16] Fix test on CI --- server/test/services/matter/lib/matter.reset.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/test/services/matter/lib/matter.reset.test.js b/server/test/services/matter/lib/matter.reset.test.js index d51b1fc892..c416037bb1 100644 --- a/server/test/services/matter/lib/matter.reset.test.js +++ b/server/test/services/matter/lib/matter.reset.test.js @@ -55,7 +55,8 @@ describe('Matter.reset', () => { assert.calledOnce(matterHandler.stop); assert.calledOnceWithExactly(gladys.variable.destroy, 'MATTER_BACKUP', 'service-1'); assert.calledOnceWithExactly(gladys.variable.setValue, 'MATTER_ENABLED', 'false', 'service-1'); - assert.calledOnceWithExactly(fseRemoveFake, path.join('/var/lib/gladys', 'matter')); + assert.calledOnce(fseRemoveFake); + assert.calledWith(fseRemoveFake, path.join(path.dirname(gladys.config.storage), 'matter')); // Verify in-memory state is reset sinon.assert.match(matterHandler.commissioningController, null); From 1c8d67a0f91de92d8eda8defdabcdb80317307e8 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie <7365207+Pierre-Gilles@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:45:54 +0200 Subject: [PATCH 15/16] Fix tests --- .../services/matter/lib/matter.reset.test.js | 73 ++++++++----------- 1 file changed, 30 insertions(+), 43 deletions(-) diff --git a/server/test/services/matter/lib/matter.reset.test.js b/server/test/services/matter/lib/matter.reset.test.js index c416037bb1..38599b9193 100644 --- a/server/test/services/matter/lib/matter.reset.test.js +++ b/server/test/services/matter/lib/matter.reset.test.js @@ -1,6 +1,7 @@ const sinon = require('sinon'); const path = require('path'); -const proxyquire = require('proxyquire').noCallThru(); +const fse = require('fs-extra'); +const { expect } = require('chai'); const { fake, assert } = sinon; @@ -9,8 +10,18 @@ const MatterHandler = require('../../../../services/matter/lib'); describe('Matter.reset', () => { let matterHandler; let gladys; + let previousMatterPath; + let testMatterPath; + + beforeEach(async () => { + previousMatterPath = process.env.MATTER_FOLDER_PATH; + testMatterPath = '/tmp/gladys-matter-reset-test'; + process.env.MATTER_FOLDER_PATH = testMatterPath; + + // Create test directory with some files + await fse.ensureDir(testMatterPath); + await fse.writeFile(path.join(testMatterPath, 'test-file'), 'test content'); - beforeEach(() => { gladys = { job: { wrapper: fake.returns(null), @@ -30,19 +41,13 @@ describe('Matter.reset', () => { matterHandler.stop = fake.resolves(null); }); - afterEach(() => { - sinon.reset(); + afterEach(async () => { + await fse.remove(testMatterPath); + process.env.MATTER_FOLDER_PATH = previousMatterPath; + sinon.restore(); }); it('should reset matter integration', async () => { - const fseRemoveFake = fake.resolves(null); - - const { reset } = proxyquire('../../../../services/matter/lib/matter.reset', { - 'fs-extra': { - remove: fseRemoveFake, - }, - }); - matterHandler.commissioningController = { close: fake.resolves(null), }; @@ -50,42 +55,24 @@ describe('Matter.reset', () => { matterHandler.nodesMap = new Map([['node1', {}]]); matterHandler.stateChangeListeners = new Set(['listener1']); - await reset.call(matterHandler); + // Verify directory exists before reset + const existsBefore = await fse.pathExists(testMatterPath); + expect(existsBefore).to.equal(true); + + await matterHandler.reset(); assert.calledOnce(matterHandler.stop); assert.calledOnceWithExactly(gladys.variable.destroy, 'MATTER_BACKUP', 'service-1'); assert.calledOnceWithExactly(gladys.variable.setValue, 'MATTER_ENABLED', 'false', 'service-1'); - assert.calledOnce(fseRemoveFake); - assert.calledWith(fseRemoveFake, path.join(path.dirname(gladys.config.storage), 'matter')); - // Verify in-memory state is reset - sinon.assert.match(matterHandler.commissioningController, null); - sinon.assert.match(matterHandler.devices, []); - sinon.assert.match(matterHandler.nodesMap.size, 0); - sinon.assert.match(matterHandler.stateChangeListeners.size, 0); - }); + // Verify directory was deleted + const existsAfter = await fse.pathExists(testMatterPath); + expect(existsAfter).to.equal(false); - it('should reset matter integration with custom MATTER_FOLDER_PATH', async () => { - const originalEnv = process.env.MATTER_FOLDER_PATH; - process.env.MATTER_FOLDER_PATH = '/custom/matter/path'; - - const fseRemoveFake = fake.resolves(null); - - const { reset } = proxyquire('../../../../services/matter/lib/matter.reset', { - 'fs-extra': { - remove: fseRemoveFake, - }, - }); - - await reset.call(matterHandler); - - assert.calledOnceWithExactly(fseRemoveFake, '/custom/matter/path'); - - // Restore original env - if (originalEnv === undefined) { - delete process.env.MATTER_FOLDER_PATH; - } else { - process.env.MATTER_FOLDER_PATH = originalEnv; - } + // Verify in-memory state is reset + expect(matterHandler.commissioningController).to.equal(null); + expect(matterHandler.devices).to.deep.equal([]); + expect(matterHandler.nodesMap.size).to.equal(0); + expect(matterHandler.stateChangeListeners.size).to.equal(0); }); }); From 90ed64f36214c8c5ef07f8064e8cfe6d78216f6f Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie <7365207+Pierre-Gilles@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:52:40 +0200 Subject: [PATCH 16/16] Remove sinon.restore --- server/test/services/matter/lib/matter.reset.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/test/services/matter/lib/matter.reset.test.js b/server/test/services/matter/lib/matter.reset.test.js index 38599b9193..67805a9961 100644 --- a/server/test/services/matter/lib/matter.reset.test.js +++ b/server/test/services/matter/lib/matter.reset.test.js @@ -44,7 +44,6 @@ describe('Matter.reset', () => { afterEach(async () => { await fse.remove(testMatterPath); process.env.MATTER_FOLDER_PATH = previousMatterPath; - sinon.restore(); }); it('should reset matter integration', async () => {