indigo-matter

Matter, Thread, and where Indigo + Domio fit

A plain-English guide to the Matter landscape and how the pieces of this ecosystem — Indigo, the indigo-matter plugin, matter-server, Domio, and Apple Home — fit together. If you want install steps, see INSTALL.md; if you want the wire contract, see API.md. This document is the “why does it work this way”.

The plugin works in both directions, and most of this page is about the first: Matter devices becoming Indigo devices. The second — selected Indigo devices published outward as Matter accessories — has its own section, Indigo as a Matter bridge.


What is Matter?

Matter is a smart-home standard backed by Apple, Google, Amazon, Samsung and hundreds of device makers under the Connectivity Standards Alliance. Its promise: buy a device with the Matter logo and it works with every major ecosystem, locally, without a vendor cloud.

The key thing to understand is that Matter is not a radio. It is a control protocol that runs on top of ordinary IP networking. A Matter device talks over one of:

Either way, control traffic is local IP (UDP, IPv6 link-local, discovered via mDNS/Bonjour — the same multicast machinery as AirPlay and HomeKit). No cloud round-trips, no vendor app needed for control.

What does Thread have to do with Matter?

Thread is one of the transports Matter can run over. It’s a low-power, self-healing mesh radio (802.15.4, the same silicon family as Zigbee) designed for battery devices — sensors, buttons, locks — that can’t afford Wi-Fi’s power budget.

Thread devices can’t talk IP to your LAN directly; they need a Thread Border Router (TBR) to bridge the mesh onto your network. You probably already own one — by 2026 almost every ecosystem’s hub is a TBR:

Two distinctions that save a lot of confusion:

Thread status in this plugin

Thread devices work via the normal share-model flow — the same Apple Home → pairing code path as Wi-Fi. Validated with real hardware on both transports: a Tapo P110M (Wi-Fi) and an Aqara FP300 presence multi-sensor (Thread, via a HomePod border router).

Here’s why. The one Thread operation the plugin’s controller stack (matter-server, matter.js, Beta) can’t do is first-admin commissioning — handing a factory-fresh device the Thread network credentials over BLE. In the share model that step is always the admin-1 ecosystem’s job: Apple Home (or Alexa, or Google Home — see below) provisions the device onto its own Thread mesh using its own credentials and radios. From then on the device is just an IP endpoint — that ecosystem’s border router routes IPv6 between the Thread mesh and the LAN and proxies the device’s mDNS records, so when the plugin joins as a second admin over IP it neither knows nor cares that the last hop is Thread.

Practical caveats: matter-server is Beta, and battery Thread devices are “sleepy” (they wake on long intervals), which is where a beta controller is most likely to be flaky — though the first validated Thread device was exactly such a sleepy battery sensor and behaved (live unprompted attribute reports). Thread devices bridged into Matter by a hub you own (Aqara, Hue, SwitchBot) are a separate, hub-dependent route — see Bridges below. And should a true gap surface, the WebSocket protocol the plugin speaks is deliberately compatible with Home Assistant’s python-matter-server, so a different backend is a swap, not a rewrite.

Bluetooth, commissioning, and why this plugin doesn’t need either

When you pair a brand-new Matter device (“commissioning”), the very first step usually happens over Bluetooth LE: the commissioner sends the device your Wi-Fi credentials (or Thread network key) over BLE, the device joins the network, and BLE is never used again.

This is why phones and ecosystem hubs make good first commissioners — they have BLE radios and (for Thread) the network credentials. A headless Mac running Indigo is a poor first commissioner: matter-server runs without BLE on macOS, and macOS has no Thread credential store.

This plugin sidesteps the problem entirely with the share model — the plugin’s founding architecture decision:

An ecosystem you already own — Apple Home, or equally Alexa / Google Home — commissions the device first: it owns the BLE step and gets the device onto your network. Then the device is shared to Indigo over plain IP. Indigo never needs BLE, Thread credentials, or proximity to the device.

That’s only possible because of Matter’s best feature: multi-admin.

Fabrics and multi-admin: one device, many controllers

A Matter fabric is a controller’s trust domain — a set of cryptographic credentials a controller installs on a device. The crucial design choice in Matter is that a device can belong to several fabrics at once. The spec floor is five — and since each fabric slot costs the device persistent storage, the floor is also what most devices ship. Treat five as your planning number. Each controller talks to the device directly and locally; none of them knows or cares about the others.

A single plug in this house happily serves four admins simultaneously: Apple Home, Indigo (this plugin’s fabric), the vendor’s app, and Home Assistant — with no cross-interference. Removing one fabric (e.g. “Remove from Indigo”) leaves the others untouched; only a factory reset wipes them all.

The plugin’s fabric lives inside matter-server’s storage directory. It is the single point of total loss — if it’s destroyed, every device must be re-commissioned — which is why the plugin ships fabric backup and restore (menu items: Back up the Matter fabric… / Restore a fabric backup…).

Why Apple Home calls our fabric “Matter Test” — this is expected. When the plugin joins a device, Apple Home shows a “joined a new network” notification and lists the Indigo fabric under a name like “Matter Test” rather than “Indigo”. Nothing is wrong. The fabric label is correctly set to “Indigo” (the plugin sets it, and it’s what the device stores), but in that notification Apple displays the fabric’s Vendor ID, not its label. matter-server (matter.js) commissions under the Matter test vendor ID 0xFFF1, whose registered name is “Matter Test”. Showing a custom name there would require a CSA-allocated Vendor ID — paid CSA membership plus product certification — the same limitation every non-certified Matter controller has (Home Assistant shows up the same way). It is purely cosmetic: control, security, and which fabrics the device belongs to are all unaffected.

The pieces, end to end

                       ┌── commissioning (one-off per device) ──┐

   Matter device ◀─BLE+Wi-Fi── Apple Home (admin 1, iPhone/HomePod/Apple TV)
        ▲                              │
        │                              │ "Turn On Pairing Mode" → setup code
        │                              ▼
        │                      Domio iOS app ──HTTPS (API.md)──▶ indigo-matter plugin
        │                                                              │
        └────── IP (admin 2: the Indigo fabric) ◀── matter-server ◀────┘ WebSocket
                                                                       │
                       ┌── runtime control (for the device's life) ────┘
                       ▼
        Indigo devices: triggers, schedules, action groups,
        control pages, HTTP/WebSocket API, Domio control
Piece Role
Apple Home First commissioner (admin 1). Does the BLE onboarding and, for Thread devices, provides the border router (HomePod/Apple TV). Alexa or Google Home + their TBRs can play this role instead — see “Don’t have Apple Home?” below.
Domio (iOS app) Owns the add device UX. Relays the pairing code from Apple Home to the plugin. One-shot per device — not in the control path afterwards.
indigo-matter (this plugin) Holds the Indigo fabric, translates Matter clusters ↔ Indigo device types, serves the Domio HTTP API, supervises matter-server.
matter-server (Node.js) The actual Matter controller stack (matter.js). Speaks Matter on the network; the plugin drives it over a local WebSocket.
Indigo Where the devices live. A Matter plug is a relay, a Matter bulb is a dimmer — first-class citizens in every Indigo feature.

Adding a Matter device

The normal flow (Wi-Fi device):

  1. Commission in Apple Home first. Scan the device’s QR code in the Home app. If your main Wi-Fi uses “advanced” features (WPA3-only, 802.11r fast roaming, band steering), a basic 2.4 GHz IoT SSID is far more reliable for onboarding — but it must be the same subnet as the Indigo Mac (see Troubleshooting).
  2. Open pairing mode. In the Home app: device → settings → Turn On Pairing Mode. Apple Home shows a fresh setup code (the printed QR code will not work for this — pairing mode generates a new one-time code).
  3. Enter the code, in whichever of the two is closer to hand:
    • Domio → add Matter device → code, name, room. Domio POSTs it to the plugin.
    • Indigo → Plugins ▸ Matter ▸ Commission device by setup code… → the same code, name and room.

    Either way the plugin joins as a second admin over IP and creates the Indigo device(s). Discovery can take a couple of minutes on a busy LAN — the plugin waits up to 5.

  4. Done. The device appears in Indigo (the room becomes a device folder) and is controllable from everything Indigo offers, plus Domio.

A point worth being explicit about: once the pairing-mode code exists, the join itself is pure IP — no Bluetooth, no phone, no proximity to the device. Opening pairing mode makes the device re-announce itself over mDNS on the network it’s already on; the plugin then does PASE/CASE over IP. This is equally true for Thread devices (the join rides the border router exactly like control traffic does). The phone is only needed for the two things that genuinely require it: the out-of-box BLE commissioning, and tapping “Turn On Pairing Mode”. Everything downstream of the code is ecosystem-free.

The two entry points are equals, and for the same reason: once the code exists, the rest is plain IP. The plugin menu’s Commission device by setup code… does exactly what Domio does — paste the pairing-mode code, Wi-Fi or Thread alike. Neither is a fallback for the other, and Domio is not required.

Pick on convenience, not capability. Domio wins when you’re standing at the new device with the Home app open and the code has just appeared — it’s the next tap on the same phone, and you never go near the Mac. The plugin menu wins when you’re already in Indigo, or have the code some other way.

And if a device is already on your network and was never commissioned by anyone (some vendor apps get devices onto Wi-Fi without Matter), its printed code works directly in either entry point.

Removing: plugin menu → Decommission Matter device… — this removes only the Indigo fabric; the device stays in Apple Home and any other ecosystem. (Deleting the Indigo device alone does not work — by design, the plugin recreates it the next time it reconciles with matter-server, e.g. after a plugin restart or reconnect. Only a factory reset on the device itself removes everything.)

Don’t have Apple Home? Alexa or Google Home work too

The plugin and Domio never care which ecosystem is admin 1 — they just consume a pairing code. Apple Home is the smoothest path for Domio users (you’re on an iPhone already), but any Matter ecosystem can play the role:

One rule to remember for Thread devices: the device joins the mesh of whichever ecosystem commissions it, so that ecosystem’s border router must stay online for the device to be reachable — an Apple-commissioned device rides the Apple TBR, an Alexa-commissioned one rides the Echo. (Pre-Thread-1.4, different vendors’ meshes in one house are separate networks.) For Wi-Fi devices none of this applies — no TBR is involved at all.

Sharing with other platforms (and vendor apps)

Multi-admin works in every direction. The same pairing-mode trick adds the device to Google Home, Alexa, or Home Assistant alongside Indigo.

The Indigo-first direction. For a device Indigo itself commissioned, you don’t need to put it back into its own pairing mode by hand: Plugins ▸ Matter ▸ Share a Matter device with another ecosystem… opens a fresh commissioning window on it directly, and writes the code to the Indigo Event Log (the same “codes can’t go in a dialog” constraint as pairing the export bridge — see below). Add the device in the other ecosystem’s app as you would any Matter accessory. There is no way to ask matter-server whether a window is already open, and no way to re-read a code once it’s issued, so a code you lose or a window you let expire (up to 15 minutes) means running the menu item again for a fresh one.

One thing to expect here too: an Apple Home pairing uses two fabric slots, not one — Apple Home and Apple Keychain (iCloud Keychain sync). Most Matter devices support only a handful of fabrics in total, so sharing a device that already has several admins can leave no room for Apple, or for anything after it. The share menu warns (never blocks) when a device is getting close.

Adding the device to its vendor app is often worth doing once, because firmware updates usually ship through it (see below). Caution with reset buttons during vendor onboarding — e.g. on TP-Link Tapo plugs a ~5 s hold is a Wi-Fi-only reset (Matter fabrics survive), while a ~10 s hold is a factory reset that wipes every fabric and undoes all your commissioning.

Bridges

A Matter bridge exposes non-Matter devices (Zigbee, proprietary RF) as Matter endpoints — Aqara, SwitchBot, and Hue hubs all do this. The plugin supports bridges: each bridged child appears as its own Indigo device. It’s also a practical route for Zigbee and other non-Matter sensors you already own behind such a hub.

What’s supported (device classes)

The plugin maps Matter clusters to native Indigo device types:

Matter capability Indigo device
On/Off (plugs, switches, lights) Relay
Dimming (Level Control) Dimmer
Colour & colour-temperature Colour dimmer (RGB + white-temp UI)
Sensors: temperature, humidity, occupancy, contact, illuminance, pressure, flow Sensor (one device per measurement)
Thermostats (incl. attached fan) Thermostat
Standalone fans Speed-controlled dimmer
Window coverings (blinds, shades) Dimmer (100% = open)
Door locks Relay with lock UI
Valves (water/irrigation) Relay (with flood-safe toggle behaviour)
Buttons / scene switches (Generic Switch) Button device firing Indigo trigger events
Smoke / CO alarms Sensor (alarm latch)
Air quality (AQI, CO₂, PM2.5, TVOC) Sensors (one per metric)
Power & energy metering curEnergyLevel (W) / accumEnergyTotal (kWh) states on the primary device
Battery level batteryLevel on every device the power source says it powers (usually the whole node)
Bridges One Indigo device per bridged child endpoint

A node exposing several capabilities gets several Indigo devices (e.g. a multi-sensor becomes one device per measurement); secondary capabilities like energy metering and battery merge into the primary device’s states. When a device reports its power/energy measurement on the node’s own root endpoint rather than alongside a specific function, the reading is attributed to that function’s device when there is exactly one candidate (e.g. a single relay), or otherwise lands on the node device described below.

One extra device per node — and the node’s devices group together

Every Matter device also has a node device in Indigo: one per physical product, carrying the things that belong to the whole thing rather than to any one of its functions — vendor and product, firmware version, whether it is reachable, its battery level where the device says the battery powers the whole node, and a power/energy reading when the device measures it on the node’s own root endpoint and there is no single function to attribute it to (see above).

A node’s devices are put in one Indigo device group together, node device included. So an Aqara FP300 called “Landing sensor” reads as “Landing sensor” (the node device) together with “Landing sensor - Motion”, “Landing sensor - Temperature” and the rest, and a plug called “Office Plug” reads as “Office Plug” together with “Office Plug - Switch”. Indigo shows the family as one group in the device list.

Which device leads the group is Indigo’s decision, not this plugin’s: Indigo lists a group’s members oldest first, and the oldest one leads. For anything you commission from now on that is the node device — it is created before the others deliberately, so that it comes out in front. For devices you already had before node devices existed, the group is led by whichever of that node’s devices you have had longest, because nothing can make an existing device younger. Either way every device is in the group, which is what makes the family show together; and battery level, which Indigo reads from the device leading the group, still lands on a device that has one — the node device when it leads, and otherwise the sensor or plug whose own battery reading the hardware reports.

Devices commissioned before this existed are brought into that shape automatically, the first time the plugin reconciles with them — grouped, and renamed to match their family. A device you renamed yourself is never renamed, which sometimes means a name is left slightly out of step with its siblings; that is deliberate. The plugin works out what a node’s family is called by reading the names its existing devices already wear, so one thing is worth knowing: if you rename a device to something shaped exactly like the names the plugin generates — “Landing sensor - Temperature” — that name counts as evidence. When it disagrees with its siblings there is no single answer, and the plugin then leaves every name on that node alone, including the node device’s. Nothing of yours is overwritten either way; the automatic renaming just stops (grouping and folder tidying still run). Renaming to anything else (“Hall thermometer”) avoids it entirely.

Deleting: what to do instead, and the one exception

The endpoint devices are not the plugin’s to keep or yours to remove: each one exists because the node reports the cluster behind it, so deleting one brings it straight back on the next reconcile. That is not the plugin overruling you, it is the same self-healing that repairs a device you lost to a mishap.

So if a device is simply in your way — an endpoint you will never automate — move it to a folder you do not look at. It keeps working, it stops cluttering the list, and nothing fights you over it. And if you want the hardware genuinely gone from Indigo, decommission the node (Remove from Indigo): that drops Indigo from the device’s Matter fabric and takes its Indigo devices with it, which is the operation that actually means “gone”.

What you should not do is delete a grouped device and confirm. Indigo will tell you the rest of the family goes too, and it means it — and because the plugin then recreates them, they come back as new devices with new ids, so any trigger, schedule or control page pointing at the old ones quietly stops working. Indigo does not warn about that part, and there is no undo. If you confirm it anyway, expect to rebind whatever referred to them.

The node device is the exception, because it is the one device here the plugin invented rather than derived: delete it and it stays deleted, with Plugins → Matter → Recreate Matter node devices… as the way back.

Indigo as a Matter bridge — the other direction

Everything up to here is Indigo as a Matter controller: other people’s Matter devices becoming Indigo devices. The plugin also runs the reverse. A set of Indigo devices you choose — Z-Wave, Insteon, Zigbee behind a plugin, MQTT, anything with an Indigo device record — can be exported: published outward as Matter accessories, so Apple Home sees and controls them like any other Matter kit. Locally, with no cloud relay, and with no per-ecosystem bridge plugin.

If you have run Homebridge, this is the same idea over Matter’s protocol instead of HomeKit’s — with the difference that Matter is one implementation reaching every ecosystem that speaks it, rather than one bridge per ecosystem.

The bridge is a second npm package, indigo-matter-bridge, published on the npm registry (0.5.0 onward) and exact-pinned by the plugin, so the Install/update the Matter bridge menu item resolves it. Setup steps are in INSTALL.html; this section is the shape of it.

The two questions this page gets asked most are “can I export my lock?” and “why isn’t my X in the list?”, so those tables come first; the reasoning follows them.

What can be exported (v1)

Indigo does not record what a device is. A relay may be a lamp, a plug, a lock, a valve, a fan or a garage door, and nothing in the device model tells them apart. So export asks you to declare a role per device, and defaults to the safest reading rather than guessing. Only roles a device can legitimately take are offered.

Indigo device Roles offered Appears in ecosystems as
Relay Plug (default), Light, Lock On/Off Plug-in Unit · On/Off Light · Door Lock
Dimmer Dimmable light (default), Window covering Dimmable Light · Window Covering
Dimmer, colour-temperature capable Colour-temperature light (default), Dimmable light, Window covering Color Temperature Light
Dimmer, full colour Full-colour light (default), Dimmable light, Colour-temperature light, Window covering Extended Color Light
Sensor, on/off Occupancy (PIR/motion), Contact, Water leak, Freeze, Rain, Smoke, Carbon monoxide — the default is read from the device’s name, and you can correct it Occupancy Sensor · Contact Sensor · Water Leak Detector · Water Freeze Detector · Rain Sensor · Smoke CO Alarm
Sensor, numeric Temperature, Humidity, Light (lux), Pressure, Flow — the default is guessed from the device’s units, and you can correct it the matching Matter sensor type
Thermostat Thermostat Thermostat (setpoints and modes; no fan in v1)

Two notes on that table. Window covering is offered for any dimmer because Indigo represents blinds as dimmers; an exported position always means 100 % = fully open in your ecosystem app (the bridge node handles Matter’s own inverted WindowCovering attribute for you), and there is a per-export tick-box if your device runs the other way. And the numeric sensor guess reads whatever unit hints the device carries (its plugin’s properties, its displayed value, then its name) — it is only a default, and all five roles stay selectable.

On/off sensors now cover the safety family, and the default is read from the device’s name (issue #236). A leak sensor exports as Matter’s own Water Leak Detector, so Apple Home shows it as a leak sensor with leak alerts rather than as a mis-labelled occupancy tile; freeze and rain sensors get the adjacent Matter types. Because a binary sensor publishes one boolean and no unit, its name is the only evidence there is about what it detects — so “Utility Leak Sensor” defaults to Water leak, “Front Door” to Contact, “Study PIR” to Occupancy. It is only ever a default: all five roles stay selectable, and picking the right one at first export matters because changing a role later costs the accessory’s room (see below).

Devices from plugins that keep their reading in their own state can be exported too — you just have to say which state (issue #252). Some plugins define their devices in a way that gives Indigo no on/off flag to read: alarm panels are the common case, where every zone is a “custom” device whose detection sits in a state the plugin named itself. Those devices used to be listed as no resolvable Matter role, which was misleading — they are ordinary motion, door, smoke and CO sensors, and Matter models all of them.

Select one in the export dialog and it now asks which state is the reading, offering the boolean states the device actually publishes. Pick that, then choose the role as normal — the role is suggested from the device’s name, so a zone called “Study PIR” arrives as an occupancy sensor and “Front Door” as a contact.

Two things make a large panel bearable. The state list hides Indigo’s own expansions of a multi-value state, so a Texecom zone offers status rather than status plus four statusText.* entries. And once you have mapped the first zone, every other device of the same type from the same plugin pre-selects the same state — so the remaining zones only need their role, which is the part that genuinely differs. The role is still yours to choose: nothing in a zone tells the plugin whether it is a PIR or a smoke detector except the name you gave it, so name-based pre-selection is a suggestion and never a decision.

If the plugin later renames or drops the state you mapped, the export stops publishing and says so in the log rather than reporting a fabricated “nothing detected” — the one outcome worth failing loudly for on a smoke alarm.

Only on/off states can be mapped in this version. A custom device whose only readings are numbers says so in the picker.

Which way round a sensor reads is a per-export tick-box, and contact sensors start ticked. Indigo’s on/off state means tripped for every binary sensor — motion seen, leak seen, door open. Matter reads all of those the same way but one: its contact sensor treats true as closed. So a door sensor exported straight through would report every shut door as open, and a new contact export therefore arrives with Reading is inverted already ticked. Nothing else does, because nothing else disagrees.

Change the role and the tick re-derives, since the same boolean means something different under a different role. Untick it and that is the last word — and an export you have already saved is never re-derived, so a polarity you corrected by hand stays corrected. If you exported a contact sensor before this behaviour existed and it reads backwards in your ecosystem, open it in Manage Matter Exports… and tick the box.

Smoke and carbon monoxide sensors export too, as separate roles (issue #179). Matter gives both one device type and picks the sensing half from a feature flag, so the plugin offers Smoke alarm and Carbon monoxide alarm as two roles rather than one combined accessory: an Indigo sensor is a single on/off reading meaning a single thing, and exporting a smoke-only sensor as both would leave it permanently telling your ecosystem that its CO reading is fine — a reassurance nothing actually measured. If you have a combined smoke/CO unit that Indigo presents as two devices, export each one under its own role. There is deliberately no self-test: Matter makes that command optional, and the plugin has nothing to run.

Heat alarms have no Matter equivalent and are not offered a role. Matter’s alarm device type senses smoke and carbon monoxide only, so a heat zone would have to be exported as one of those, and an ecosystem would then show — and automate on — a smoke alarm that cannot detect smoke. The device stays exportable under any role you declare for it; there just isn’t an honest one in this family.

A motion sensor exports as “Occupancy” — that is Matter’s own ceiling, not a gap in this plugin (issue #252). Matter 1.x defines no motion device type. Occupancy Sensor (0x0107) is the Matter representation of a PIR — the standard’s own sensor-type bitmap lists passive infrared as one of the things an occupancy sensor is — so an Indigo motion sensor has exactly one honest export, and the picker names it “Occupancy sensor (PIR/motion)” so you can find it. Measured in Apple Home on 2026-08-18: the exported accessory shows as an Occupancy tile, and its automations trigger on occupancy detected/cleared, which is the same edge your Indigo motion sensor reports. The asymmetry is only in the naming: coming the other way, this plugin renders a Matter occupancy sensor as an Indigo motion sensor, because that is the Indigo device type users expect.

Battery level appears automatically, for any exported device that reports one. If the underlying Indigo device has a battery reading, your ecosystem shows it — no extra setting, no tick-box; the plugin never invents a role for this, it just reads what the device already publishes. Battery changes flow through immediately, but Matter itself rate-limits how often controllers are told about a battery change (at most every ~10s, by the standard’s own design) — so it reads slightly quieter than other exported readings, which have no such limit. A brand-new device that has never taken a reading does not show a false “low battery” warning: a battery-level of exactly 0% (or below) is treated as “not read yet” rather than “flat”, since Indigo initialises new devices’ numeric states to 0.

If you are updating from a version before battery support: every already-exported device that has a battery is re-created once, the first time the plugin reconnects after the update. The accessory’s identity (endpoint number) is preserved, but its name and room assignment in your paired ecosystem(s) may need re-assigning — the same one-time hedge a role change already asks for.

Renaming an exported device in Indigo will not rename it in your ecosystem (issue #221, measured 2026-08-19). The plugin does push the new name — it updates the accessory’s nodeLabel and the bridge records it within seconds — but Apple Home, and every other controller, stores an accessory’s name in your home, set once when the accessory was added, and treats it as yours from then on. A later change coming up from the bridge does not override it. This is the same reason renaming a bulb in its vendor’s own app does not rename it in Home.

So the name you set in Indigo — and the optional Name in ecosystems field in the export dialog — decides what an accessory is called when it first appears, and nothing after that. To rename one you already have, rename it in the ecosystem. The only thing that forces a fresh name is a role change, because that publishes a genuinely new accessory — and it costs you the room and the automations along with it, which is far too much to pay for a typo.

Changing a role now costs the room, on purpose (issue #240). Matter does not allow an endpoint to change device type, so changing an export’s role removes the old accessory from every ecosystem and adds a new one under a fresh accessory number — you will need to put it back in its room, but it is one deliberate, predictable re-room every time. The dialog warns you at the time. This is deliberately NOT an in-place recreate at the same number any more: that used to be able to leave Apple Home stuck on “could not change settings”, because its cached accessory structure no longer matched what the bridge advertised under a number it believed was stable. Because the old accessory is deleted and a new one created, your ecosystem’s own “new accessory” flow handles it the same way it handles any other addition — no home-hub restart needed.

If you are already stuck from an older version (live-observed 2026-08-17, pre-2026.21.0): the fix is to restart your Apple home hub (Apple TV: Settings → System → Restart, or power-cycle the HomePod); force-quitting the Home app is not enough. After the reboot the hub re-reads the bridge — expect a couple of minutes of “No Response” across all bridged accessories while it rebuilds its sessions, then room changes work again. This is a one-time recovery for a wedge an older plugin version could leave behind; a role change on 2026.21.0 or later does not create it in the first place.

Locks export, and they do not auto-confirm anything. A lock or unlock from an ecosystem is passed to Indigo and nothing else: no optimistic state, no synthesised confirmation. What the ecosystem shows moves only when Indigo’s own state moves, because the bolt is the authority.

Switches, plugs and lights work the same way now. Tapping a tile in Apple Home, Google Home or Alexa sends the request to Indigo and waits for Indigo to report back before the tile itself moves — the same no-optimistic-state rule the lock has always followed. For a fast relay this is imperceptible; for a slower Z-Wave device, a cloud-backed plugin, or a device that is simply offline, the tile may sit still for a moment (or not move at all, which is the honest answer for a device that never responded). One side effect you may notice: a command that matches what the accessory already believes — tapping “on” on a light your ecosystem already shows as on — is now forwarded to Indigo instead of being silently dropped. (If the real device was off, that tap now turns it on; if Indigo already agreed with the tile, Indigo treats it as a no-op, so a tile that itself drifted out of sync still waits for the next real state change to catch up.) Brightness now behaves the same way: dragging the slider on a lamp that is showing off turns it on to that level, the same turn-on-to-that-level meaning Indigo itself gives a brightness command on an off device — the slider waits on Indigo’s confirmation the same way the on/off tile does, rather than moving immediately. Colour works the same way: picking a new colour or colour temperature on a lamp that is showing off is forwarded to Indigo too, and Indigo decides what that means for the device — on many RGB lamps a colour write lights the lamp at the new colour, while on lamps with a white channel a colour-temperature change is normally stored without turning the lamp on. Either way the accessory’s controls only move once Indigo reports what actually happened. One boundary to know about: if an ecosystem starts a timed “turn off in N minutes” countdown and you turn the device off in Indigo before it fires, the countdown is cancelled rather than firing late on a device that is already off.

What cannot be exported, and why

Excluded devices still appear in the picker with their reason — they are never silently missing, so you are never left hunting for a device that will never show up. (The picker shows up to 300 matching devices at a time; past that it says so and asks you to narrow the name filter.)

Not exportable Why
Anything this plugin created — including its own energy-meter devices Loop guard: a Matter device is not re-exported over Matter. Checked on the owning plugin id, before the device’s type is looked at, so it catches every one of the plugin’s device types alike. These are filtered out of the picker entirely rather than shown with a reason.
Valves (as a relay role) matter.js’s valve cluster is an empty stub — the whole command surface would have to be written from scratch — and ecosystem support for the type is poor. v2 candidate.
Fans — both dimmer-backed and speed-control devices Same reason: matter.js’s fan cluster only seeds a default mode, so all fan behaviour would be ours to implement. v2 candidate. This is also why the exported thermostat has no fan control.
Garage doors (as a relay role) Needs polarity data Indigo does not carry (onState true meaning closed, “on” meaning close), and getting it backwards is a physical-safety problem, not a cosmetic one. Blocked on the device-catalog work.
Heat alarms Matter’s alarm device type senses smoke and carbon monoxide only. Exporting a heat zone as either would put a “smoke alarm” in your ecosystem that cannot detect smoke — and anything automating on smoke would then include it. The device stays exportable under any role you declare; there just is not an honest one in this family.
Sprinkler devices Matter has no irrigation-controller type. Per-zone water valves would be a lossy fit, and water valves are descoped anyway.
MultiIO devices No coherent way to represent one as a single accessory.
Sensors whose units aren’t in the table above No faithful Matter sensor type to map them to.
Sensors that report neither an on/off state nor a value Nothing to publish.
Devices with no resolvable role The device is not a relay, dimmer, sensor, thermostat, speed-control, sprinkler or MultiIO — typically another plugin’s custom device class. There is nothing in the Indigo device model to map, so there is no role to offer.

Beyond the device types, four v1 limits worth knowing up front:

Two processes, opposite jobs

   Matter devices  ──▶  matter-server  ──▶ ┐
   (in the house)        (controller,      │
                          Indigo's fabric) │
                                           ├──▶  indigo-matter plugin  ──▶  Indigo
   Apple Home  ◀──  bridge node  ◀─────────┘        (Python)
   (and any other  (device role,
    ecosystem)      matter.js)

There are now two Node processes, and they share a Node runtime and nothing else. matter-server is the controller: it holds Indigo’s own fabric and commissions other people’s devices. The bridge node is a Matter device — it holds no fabric of its own, and instead gets commissioned into other ecosystems’ fabrics, exactly as a Matter plug does. Separate process, separate storage, separate launchd job, separate port. One can be broken while the other works, and an export failure never touches the devices Indigo controls.

Why a separate process at all, rather than the plugin doing Matter itself: Indigo plugins are Python and the Matter library the plugin uses (matter.js) is TypeScript. Keeping the whole Matter stack behind a process boundary is what lets the plugin stay plain Python and speak Indigo devices, and it is the same discipline the controller side already follows. The two halves talk over a loopback WebSocket that nothing outside this Mac can reach, and the protocol is versioned: a bridge node speaking a different protocol version is refused rather than guessed at. That version is bumped only when the wire contract itself changes, so most plugin upgrades need no new bridge node — but when it does change, an old node left running across an upgrade is halted with a reason rather than talked to on a guess.

Both halves present the bridge as one accessory containing an aggregator with one child endpoint per exported device — the standard Matter bridge topology, and the same shape the plugin already consumes inbound from Aqara, Hue and SwitchBot hubs. One bridge, one pairing operation, one uncertified prompt, however many devices you export.

Nothing is exported until you say so

Export is opt-in per device, default empty, and that is a deliberate policy rather than a UI convenience.

Exporting a device is publishing it. It becomes visible and operable from every ecosystem the bridge is paired with, and from those ecosystems’ accounts — which means, for a voice assistant, from outside the house. Locks, valves, garage doors and alarm-adjacent devices make an export-everything default indefensible, so there isn’t one: a fresh install exports nothing, pairs nothing and runs no bridge process at all.

The consequence worth planning around: there is no per-ecosystem export set in v1. There is one bridge and one allow-list, so a device you export is exported to every ecosystem the bridge is paired with. If you want a device in one place and not another, do not export it. (Per-ecosystem sets, and multiple bridges, are a v2 idea.)

Devices this plugin created itself can never be exported — they are filtered out of the picker entirely, so a Matter device cannot be re-exported over Matter into a loop.

Fabrics, again — and Apple takes two slots

The same multi-admin machinery described above works in this direction too. Each ecosystem that pairs the bridge installs its credentials as a fabric on it, and the bridge serves them all at once and independently. There is no small fabric limit here of the kind physical devices have: the bridge is software and its ceiling is far above any realistic number of ecosystems.

One thing to expect: an Apple Home pairing creates two fabrics, not one — Apple Home and Apple Keychain (iCloud Keychain sync). Both show up in the plugin’s readouts and in the unpair picker. Neither is a stray, and removing either is not a tidy-up.

Unpairing the last ecosystem resets the bridge completely: Matter’s rules make the node factory-reset itself when its fabric set empties, so it goes back to advertising for commissioning. That is correct behaviour, and it is the same outcome as Reset Matter Bridge Pairings… — worth knowing before you remove what turns out to be the only one.

Accessory identity, and why it must never move

Back up ~/Library/Application Support/com.simons-plugins.indigo-matter/bridge-node/, and read the recovery notes in INSTALL.html before using either of the two destructive repair actions. That is the whole practical instruction; the rest of this subsection is why it matters more than it looks.

Every exported device gets a stable identity derived from its immutable Indigo device ID, and a Matter endpoint number allocated once against that identity — never from its position in a list. Ecosystems remember both. The name you gave an accessory in Apple Home, the room you put it in, the scenes and automations you built on it: all of that is keyed to the identity, not to the accessory’s name.

So the bridge treats its storage directory as sacred, and keeps an independent record of which number belongs to which device purely so it can notice if they ever disagree. If they do, it says so loudly and does not repair it — an automatic repair would bless whatever went wrong and make the next occurrence invisible too. And if that record is unreadable on a bridge that has been paired, it refuses to export anything at all rather than quietly renumbering — silent renumbering is how every accessory ends up duplicated in every ecosystem, with the originals dead and removable only by hand. (Before the bridge has ever been paired there is nothing to protect, so it just carries on.)

There is one everyday case that changes identity on purpose: changing an export’s role. Matter does not allow an endpoint to change device type, so the accessory is removed and re-added, and every ecosystem treats it as brand new — losing the name and room it had. The dialog warns you at the time.

Replacing hardware, without losing anything

If the Indigo device you are replacing still exists — a socket swapped for a newer one, a Zigbee dimmer moved onto a Matter-native module, a device rebuilt under a new id while the old one is still sitting there — use Plugin ▸ Migrate an exported accessory… rather than deleting anything. Pick the currently-exported accessory, pick the Indigo device that should drive it from now on, and confirm. The accessory’s identity never lapses: the old device’s entry simply leaves the export list, and the new device takes over under the SAME identity and the SAME accessory number, so no ecosystem ever processes a removal — nothing is lost, not the room, not the name, not a single scene or automation. The old Indigo device stays in Indigo, just no longer exported.

If the device you migrate onto already has its own exported accessory, THAT accessory is removed from every ecosystem and replaced by the migrated one — the dialog marks it and warns you before you confirm.

It only works onto a device that can take the accessory’s role — export the device normally instead if it cannot, and accept that it becomes a new accessory.

The old Indigo device is not locked out afterwards — you can export it again later — but re-exporting it makes a NEW accessory under a fresh identity; it does not steal back the identity you just migrated away.

If you delete and recreate an exported device

Deleting an Indigo device that is exported removes its accessory from every paired ecosystem — this happens as soon as the plugin notices the deletion (immediately if it is running, at the next reconnect if it was not). There is nothing left behind to manage: the accessory is gone, not merely un-driven.

Re-creating the device (or restoring it a different way) gets you a NEW Indigo device with a NEW id, and Indigo has no way to know it is “the same” device you deleted — exporting it normally builds a brand-new accessory, in the Default Room under a default name, with any scene or automation that referenced the old one needing to be rebuilt. If the old device still exists at the time you want to move its accessory onto a replacement, use Migrate (above) instead — it is the only way to carry room, name, scenes and automations across, and only works while the old device has not yet been deleted.

Which ecosystems this actually works with

Honestly stated, because the difference matters:

What has and has not been proven on live hardware is stated once, in the project README, rather than restated here where the two copies would drift apart. The short version: the bridge has been commissioned into Apple Home and Alexa, exported devices have been controlled both ways, and accessory identity has survived both an upgrade and a full host reboot without duplicating or renumbering. Treat the export half as new.

Alexa conformance: what a code sweep found (issue #222)

Alexa’s Matter support is stricter and less documented than Apple’s, so a completed sweep of the export side against Alexa’s known requirements is recorded here rather than left implicit in the code:

Why every ecosystem calls it “uncertified”

Because it is, deliberately, and there is nothing the plugin can do about it.

Matter attestation runs one way: the commissioner checks the device’s certificate. Inbound, the plugin is the commissioner, so it has a setting for this (Allow test/development device certificates). Outbound the roles are swapped — the bridge is the device and Apple is the commissioner — so the trust policy belongs to Apple and there is no flag we could ship for someone else’s ecosystem, ever.

The bridge advertises with the specification’s test vendor ID (0xFFF1), the same posture Homebridge, matterbridge and Home Assistant’s bridge all ship with. Certification means CSA membership plus per-product testing, which is not proportionate for a free community plugin. So: expect the warning, choose “Add Anyway”, and know that it says nothing about whether the bridge works.

(That test vendor ID is also why Apple Home shows the controller fabric as “Matter Test”, described earlier. Same number, two entirely different uses — one is our controller’s fabric identity, the other is the bridge’s attestation identity — and neither is a claim about the other.)

An on/off accessory forgets it was on across a bridge restart (issue #202)

Matter’s StartUpOnOff attribute lets an on/off device declare what it should do when it powers back up — stay off, come on, or restore its last state. On an exported endpoint, as of matter.js 0.17.8 (the version this bridge pins), OnOffServer.initialize() explicitly skips that power-on-behaviour restoration for any endpoint that lives under an AggregatorEndpoint — which every exported accessory does, by construction. The attribute is still there and still writable; it is just inert.

Not worked around, on purpose. Two ways around it were considered and both make things worse:

So the attribute is left as matter.js ships it: present, spec-conformant, and not the thing that actually decides what an exported device does when the bridge restarts.

Firmware updates (and why they matter more than usual)

Matter firmware arrives by two routes: the device vendor’s app, and the ecosystems themselves — those that act as Matter OTA providers push updates too (Apple Home auto-updates Matter accessory firmware; observed on this house’s Tapo plug). The vendor app typically gets releases first, so check there when you’re waiting on a feature. And vendors are still actively adding Matter features via firmware. Real example from this house: a Tapo P110M energy plug shipped exposing only on/off over Matter — the energy-measurement clusters (a Matter 1.3 feature) appeared only after a firmware update via the Tapo app. The plugin re-reads a node’s capabilities whenever it rejoins, so the existing Indigo device gained its energy states automatically — no restart, no re-pairing, no recreation.

Rule of thumb: if a device seems to be missing a capability you know it has, check for firmware first. The plugin’s diagnostics endpoint (see API.md §3.5) lists exactly which clusters a node is advertising, which settles the “device or plugin?” question in one request.

Troubleshooting

Network first. Matter assumes a flat residential network. The plugin, the device, and matter-server must share one subnet/VLAN: discovery is mDNS multicast and transport is link-local IPv6, and neither crosses routers. You do not need “IPv6 from your ISP” — link-local addresses are self-assigned and always present.

Common failure modes, in rough order of likelihood:

Backups. Back up the fabric after commissioning anything you’d hate to re-pair. The plugin keeps rotating zips and a restore is menu-driven and reversible.

Exporting devices has its own failure modes, its own logs and its own recovery actions — see the export bridge table in INSTALL.html → Troubleshooting.


Further reading: INSTALL.md (setup) · API.md (Domio ↔ plugin contract) · IMPLEMENTATION.md (internals) · Home Assistant’s Matter docs (an excellent ecosystem-neutral primer that inspired this page).