Skip to content

Matter: composed device types are re-registered on every restart, changing the bridged device's uniqueId #4018

Description

@thecloudseeker

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

  1. npm i homebridge@2.4.0
  2. Save the script below as repro.mjs next to node_modules.
  3. Run node repro.mjs twice with the same matter-store.
  4. 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:

  1. Carry the uniqueId over from the endpoint being replaced (or derive it from the accessory UUID).
  2. Treat a plugin type that only adds behaviors to the restored base type as the same shape, so it's attached in place.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions