Analysis
If a plugin composes its own device type (e.g. TemperatureSensor.with(HumiditySensor.behaviors.relativeHumidityMeasurement)), Homebridge replaces the accessory on every restart, and the new endpoint gets a new random BridgedDeviceBasicInformation.uniqueId.
- The Matter cache stores the device type as
{ name, code } only, so the restore rebuilds the base type.
- When the plugin registers again,
sameShape in AccessoryManager.registerAccessory() compares behavior keys, finds that they differ, logs changed structure - re-registering and recreates the endpoint.
- The endpoint number survives, but
uniqueId is regenerated. Plugins can't pin it: createEndpointOptions() doesn't pass one through.
So a controller that identifies bridged devices by uniqueId may see a new device after every restart. A plain type, or a BridgedNode with parts, is attached in place and keeps its uniqueId.
Related: #4015 (same re-registration path).
Expected Behavior
Restarting without any plugin change keeps the bridged device's uniqueId the same.
Steps To Reproduce
npm i homebridge@2.4.0
- Save the script below as
repro.mjs next to node_modules.
- Run
node repro.mjs twice with the same matter-store.
- The endpoint number stays the same;
uniqueId changes on every run after the first, which logs changed structure - re-registering.
import { MatterServer } from "./node_modules/homebridge/dist/matter/server.js";
import { deviceTypes } from "./node_modules/homebridge/dist/matter/index.js";
import { uuid } from "@homebridge/hap-nodejs";
const { TemperatureSensor: T, HumiditySensor: H } = deviceTypes;
const server = new MatterServer({ port: 5597, uniqueId: "REPRO", storagePath: process.cwd() + "/matter-store" });
await server.start();
const UUID = uuid.generate("repro");
await server.registerPlatformAccessories("p", "P", [{
UUID, displayName: "Sensor", serialNumber: "1", manufacturer: "x", model: "y",
deviceType: T.with(H.behaviors.relativeHumidityMeasurement),
clusters: {
temperatureMeasurement: { measuredValue: null, minMeasuredValue: -5000, maxMeasuredValue: 10000 },
relativeHumidityMeasurement: { measuredValue: null, minMeasuredValue: 0, maxMeasuredValue: 10000 },
},
}]);
const ep = server.accessories.get(UUID).endpoint;
console.log("number:", ep.number, "uniqueId:", ep.state.bridgedDeviceBasicInformation.uniqueId);
await server.stop();
Logs
run 1: number: 2 uniqueId: 6o1E3j2yGbKNS5ir3aKHA0DJzovYSMpi
run 2: number: 2 uniqueId: cmZF382QA2i3cMWntwIohVBMblNifXYo (logged "changed structure - re-registering")
run 3: number: 2 uniqueId: ZKbotzSvG3cdRJRkXD5GPg8Wbd2j1oBJ (logged "changed structure - re-registering")
Same on a child bridge with a real plugin:
[Matter/Server] Restored accessory Temperature & Humidity changed structure - re-registering
[Matter/Server] Unregistered Matter accessory: Temperature & Humidity (adc34eaf-43a1-4e71-83db-cad86f0eb7e4)
[Matter/Server] Registered Matter accessory: Temperature & Humidity (adc34eaf-43a1-4e71-83db-cad86f0eb7e4)
Configuration
{
"platform": "HygrotermographCGDK2",
"_bridge": {
"username": "0E:D8:3D:82:7E:70",
"port": 35144,
"matter": { "port": 5531 }
}
}
Environment
- OS: Debian GNU/Linux 12 (bookworm), Raspberry Pi
- Software: Homebridge 2.4.0 (Matter.js 0.17.9); same logic in 2.4.1-beta.11
- Node: 24.21.0
- npm: 11.19.0
Process Supervisor
hb-service
Additional Context
Use case: a temperature + humidity sensor on one endpoint. IKEA Dirigera lists every sensor endpoint as a separate product, so this is the only layout it shows as one device (plugin: @thecloudseeker/homebridge-hygrothermograph-cgdk2).
Possible fixes:
- Carry the
uniqueId over from the endpoint being replaced (or derive it from the accessory UUID).
- Treat a plugin type that only adds behaviors to the restored base type as the same shape, so it's attached in place.
Analysis
If a plugin composes its own device type (e.g.
TemperatureSensor.with(HumiditySensor.behaviors.relativeHumidityMeasurement)), Homebridge replaces the accessory on every restart, and the new endpoint gets a new randomBridgedDeviceBasicInformation.uniqueId.{ name, code }only, so the restore rebuilds the base type.sameShapeinAccessoryManager.registerAccessory()compares behavior keys, finds that they differ, logschanged structure - re-registeringand recreates the endpoint.uniqueIdis regenerated. Plugins can't pin it:createEndpointOptions()doesn't pass one through.So a controller that identifies bridged devices by
uniqueIdmay see a new device after every restart. A plain type, or aBridgedNodewithparts, is attached in place and keeps itsuniqueId.Related: #4015 (same re-registration path).
Expected Behavior
Restarting without any plugin change keeps the bridged device's
uniqueIdthe same.Steps To Reproduce
npm i homebridge@2.4.0repro.mjsnext tonode_modules.node repro.mjstwice with the samematter-store.uniqueIdchanges on every run after the first, which logschanged structure - re-registering.Logs
Configuration
{ "platform": "HygrotermographCGDK2", "_bridge": { "username": "0E:D8:3D:82:7E:70", "port": 35144, "matter": { "port": 5531 } } }Environment
Process Supervisor
hb-service
Additional Context
Use case: a temperature + humidity sensor on one endpoint. IKEA Dirigera lists every sensor endpoint as a separate product, so this is the only layout it shows as one device (plugin: @thecloudseeker/homebridge-hygrothermograph-cgdk2).
Possible fixes:
uniqueIdover from the endpoint being replaced (or derive it from the accessory UUID).