Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
814a572
feat(node): add Behaviors.forCluster and cluster name/id resolver
Apollon77 Jul 14, 2026
d5fe0d3
feat(node): add Peers.commissioned view
Apollon77 Jul 14, 2026
187bd20
feat(node): add NodeLifecycle.isSeeded/seeded
Apollon77 Jul 14, 2026
181b4f4
feat(node): add commissioning-window helpers on CommissioningClient/C…
Apollon77 Jul 14, 2026
218f6a4
feat(protocol): emit establishment-unresponsive signal after MRP budg…
Apollon77 Jul 14, 2026
de8771b
feat(node): connection-state engine on NetworkClient exposed via Node…
Apollon77 Jul 14, 2026
3a45073
fix(node): redefine connection-state Disconnected as stopped/not-started
Apollon77 Jul 14, 2026
3b9fad2
fix(node): dispose seeded listener after latch; document startup-time…
Apollon77 Jul 14, 2026
5f9e8b1
refactor(types): relocate cluster name/id resolver to ClusterHelper w…
Apollon77 Jul 14, 2026
e8a0504
chore(matter.js): deprecate legacy controller/device API for 0.19 rem…
Apollon77 Jul 14, 2026
df9bfec
docs: add controller migration guide (MIGRATION_CONTROLLER_018, WIP)
Apollon77 Jul 14, 2026
b1ea69c
fix(node): register peer BasicInformation handlers once to prevent du…
Apollon77 Jul 15, 2026
fd2faeb
fix(node): register bridged ConfigurationVersion handler once
Apollon77 Jul 15, 2026
8c4d016
docs: fill migration guide subscription/event-bus/FAQ sections with c…
Apollon77 Jul 15, 2026
24e07fc
Potential fix for pull request finding
Apollon77 Jul 15, 2026
b68c500
docs: map legacy autoConnect/autoSubscribe to NetworkClient state in …
Apollon77 Jul 15, 2026
e4a4504
docs: document node enable/disable and automatic bulk connect in migr…
Apollon77 Jul 15, 2026
89adb50
docs: lead event-bus migration with ChangeNotificationService aggrega…
Apollon77 Jul 15, 2026
4a1ee17
refactor(node): split client-only lifecycle surface into ClientNodeLi…
Apollon77 Jul 15, 2026
2fa68ae
refactor(types): group cluster name/id resolvers under ClusterLookup …
Apollon77 Jul 16, 2026
590234d
chore: stop tracking local plan doc, gitignore docs/superpowers
Apollon77 Jul 16, 2026
41e9e47
feat(shell): add ServerNode/peers-backed ClientNode methods to Matter…
Apollon77 Jul 16, 2026
2b62169
fix(shell): tear down subscription via disable() for autoConnect:fals…
Apollon77 Jul 16, 2026
819712b
refactor(shell): migrate cmd_nodes to ClientNode/peers
Apollon77 Jul 16, 2026
e7a5f6b
fix(shell): migrate cmd_nodes fabric lookup + OTA off legacy Commissi…
Apollon77 Jul 16, 2026
b7fc1a9
refactor(shell): migrate cmd_commission to ClientNode API
Apollon77 Jul 16, 2026
303bdc3
refactor(shell): migrate cluster attribute/command/event commands to …
Apollon77 Jul 16, 2026
db06cad
refactor(shell): migrate cmd_subscribe to auto-subscribe + ChangeNoti…
Apollon77 Jul 16, 2026
e873862
refactor(shell): migrate cmd_icd to ClientNode lifecycle/IcdClient
Apollon77 Jul 16, 2026
9bf817e
refactor(shell): migrate cmd_identify/session/discover to ClientNode/…
Apollon77 Jul 16, 2026
d24f902
refactor(shell): remove CommissioningController backing, finalize Cli…
Apollon77 Jul 16, 2026
a992c0d
fix(shell): tidy cmd_icd watch teardown and restore cmd_discover dedupe
Apollon77 Jul 16, 2026
a726192
fix(shell): defer controller node creation/start to first use (restor…
Apollon77 Jul 17, 2026
6cb04a6
feat(node): add NetworkServer.autoStartCommissionedPeers opt-out for …
Apollon77 Jul 17, 2026
6dbed91
fix(shell): opt out of peer auto-connect, connect/subscribe on demand
Apollon77 Jul 17, 2026
a7c6ef4
docs: document NetworkServer.autoStartCommissionedPeers in controller…
Apollon77 Jul 17, 2026
5264c27
fix(shell): normalize autoSubscribe once at startup so connect no lon…
Apollon77 Jul 17, 2026
57271a3
fix(shell): enable disabled peers on connect
Apollon77 Jul 17, 2026
46bbe96
docs: clarify autoStartCommissionedPeers=false requires explicit enab…
Apollon77 Jul 17, 2026
90ed9ad
fix(shell): controller uses ephemeral operational port, not standard …
Apollon77 Jul 17, 2026
e5b05e7
feat(node): default controller operational port to ephemeral when unset
Apollon77 Jul 17, 2026
b2d7be0
fix(shell): drop port:0 workaround now handled by the library
Apollon77 Jul 17, 2026
b26b1ed
fix(shell): restore OTA field guards, promise-cache node lifecycle, r…
Apollon77 Jul 18, 2026
51151a3
docs: complete controller migration guide from the shell blueprint (d…
Apollon77 Jul 18, 2026
9b023d7
Merge remote-tracking branch 'origin/main' into worktree-feat+clientn…
Apollon77 Jul 18, 2026
eff26ae
feat(shell): close phase-2 ClientNode migration gaps
Apollon77 Jul 18, 2026
76677ff
refactor(shell): format seeded-wait timeout with Duration.format
Apollon77 Jul 18, 2026
d9fba6c
Merge branch 'main' into feat/clientnode-controller-api
Apollon77 Jul 18, 2026
ae6f2ad
docs: clarify autoStartCommissionedPeers vs per-node disable use cases
Apollon77 Jul 18, 2026
64844c8
fix: address #4091 review comments (passcode logging, establishment o…
Apollon77 Jul 18, 2026
273be0e
docs(migration): fabric-label propagation recipe, negotiated interval…
Apollon77 Jul 18, 2026
362e79d
feat(node): add Peers.completeCommissioning for split commissioning
Apollon77 Jul 19, 2026
dbae66e
fix(node): seed discoveryData on live peer + clean up on completeComm…
Apollon77 Jul 19, 2026
fe5c3d4
test(node): cover completeCommissioning non-Ok CommissioningComplete …
Apollon77 Jul 19, 2026
8d5fce8
docs: document finalizeCommissioning PASE-only + split-commissioning …
Apollon77 Jul 19, 2026
8638943
fix(node): guard completeCommissioning against deleting an already-co…
Apollon77 Jul 19, 2026
850d730
test(node): deterministically await re-announcement in failsafe commi…
Apollon77 Jul 19, 2026
277449f
feat(node): send CommissioningComplete with extended failsafe respons…
Apollon77 Jul 19, 2026
6c46f76
fix(node): persist fabric before the node in completeCommissioning (b…
Apollon77 Jul 19, 2026
cca8dfd
fix(node): require an existing shared fabric in completeCommissioning
Apollon77 Jul 19, 2026
28b2531
test(node): cover completeCommissioning rejecting when the shared fab…
Apollon77 Jul 19, 2026
a4b80b2
test(node): comment audit — drop WHAT-restating comments, fix stale i…
Apollon77 Jul 20, 2026
6ff62ca
docs(node): finalizeCommissioning is awaited — document return/throw …
Apollon77 Jul 20, 2026
19e9038
Merge branch 'main' into feat/clientnode-controller-api
Apollon77 Jul 20, 2026
5c6d828
fix(node): address final review — completeCommissioning seeding + polish
Apollon77 Jul 20, 2026
80d7979
fix: address #4091 PR review comments
Apollon77 Jul 20, 2026
a5c301a
Merge branch 'main' into feat/clientnode-controller-api
Apollon77 Jul 21, 2026
547483c
fix(shell): reject partial ip/port in commission command
Apollon77 Jul 21, 2026
6c6b4f6
Merge branch 'main' into feat/clientnode-controller-api
Apollon77 Jul 21, 2026
062facd
Merge branch 'main' into feat/clientnode-controller-api
Apollon77 Jul 27, 2026
b762c2b
Merge remote-tracking branch 'origin/main' into feat/clientnode-contr…
Apollon77 Jul 27, 2026
533ef8a
docs(changelog): move unreleased controller-API entries to WORK IN PR…
Apollon77 Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ The main work (all changes without a GitHub username in brackets in the below li
## __WORK IN PROGRESS__

- @matter/node
- Feature: Added `openBasicCommissioningWindow`/`openEnhancedCommissioningWindow` on `CommissioningClient`/`ClientNode` to open a commissioning window on a commissioned peer
- Feature: Added `NodeLifecycle.isSeeded` and the `seeded` event, indicating a peer node's structure has been read at least once
- Fix: `endpoints.size` no longer double-counts the root endpoint
- Fix: Attributes added when a peer cluster gains features at runtime are now readable

- @matter/protocol
Expand All @@ -23,6 +26,9 @@ The main work (all changes without a GitHub username in brackets in the below li
- @matter/types
- Fix: `ObjectSchema.injectField`/`removeField` no longer crash on fabric-scoped commands that omit an optional nested struct field

- @project-chip/matter.js
- Deprecation: The legacy controller/device API (CommissioningController, PairedNode, Device/Endpoint/Aggregator, cluster clients) is deprecated and will be removed in 0.19; migrate to @matter/node

## 0.17.5 (2026-07-13)

- @matter/\*:
Expand Down
135 changes: 135 additions & 0 deletions docs/MIGRATION_CONTROLLER_018.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Migration guide: Controller API to 0.18 (PairedNode/CommissioningController → ClientNode)

> **Status: WORK IN PROGRESS.** This guide is written alongside the actual migration of the
> matter.js shell and the matter.js WebSocket server. Sections are filled with verified before/after
> code as each capability is migrated. Open items are marked `TODO`.

With 0.18 the legacy controller API is being deprecated. The two legacy entry points are:

- **`CommissioningController`** — the controller wrapper (commission, enumerate/get nodes, discovery,
fabric). It is being replaced by a controller **`ServerNode`** whose commissioned peers are
**`ClientNode`** instances in `serverNode.peers`.
- **`PairedNode`** — the per-peer wrapper returned by the controller. It is being replaced by the
peer's **`ClientNode`** directly. Most `PairedNode` members already just delegate to the underlying
`ClientNode` (`PairedNode.node`), so a large part of the migration is removing the `.node` hop.

The legacy API stays functional through the deprecation period but will be removed. New code and
migrating consumers should target the `ClientNode` API.

---

## Quick map: what moved how

### Already 1:1 (PairedNode already delegates — just drop `PairedNode`, use the `ClientNode`)

`state`, `commands`, `stateOf`, `maybeStateOf`, `commandsOf`, `featuresOf`, `maybeFeaturesOf`,
`globalsOf`, `maybeGlobalsOf`, `get`, `getStateOf`, `eventsOf`, `id`, `decommission`, `close`,
`disconnect` (→ `disable()`).

Device info: `basicInformation` / `deviceInformation` → `new DeviceInformation(clientNode).basicInformation` / `.meta`
(or read the `BasicInformationClient` behavior state directly).

### Paradigm shift (no more `ClusterClient` objects)

| Legacy | New |
| --- | --- |
| `node.getDevices()` / `getDeviceById(id)` | `clientNode.endpoints` / `clientNode.endpoints.for(id)` |
| `node.getRootEndpoint()` | the `ClientNode` **is** the root endpoint |
| `node.getRootClusterClient(Cluster)` | root endpoint `stateOf(XxxClient)` / `commandsOf(XxxClient)` / `act(agent => agent.get(XxxClient)…)` |
| `node.getClusterClientForDevice(id, Cluster)` | `clientNode.endpoints.for(id).stateOf/commandsOf(XxxClient)` |
| `clusterClient.attributes[x].get()` / `.set()` | behavior `stateOf(...)` read / `set({...})` write |
| `clusterClient.commands[x](req)` | `endpoint.act(a => a.get(XxxClient).x(req))` |
| `node.getInteractionClient()` | `clientNode.interaction` (`Interactable`) |
| `node.logStructure()` | `logger.info(clientNode)` (node is directly loggable) |

### Controller: `CommissioningController` → controller `ServerNode`

| Legacy | New |
| --- | --- |
| `new CommissioningController({...})` | controller `ServerNode` (+ `ControllerBehavior`) |
| `controller.commissionNode(opts)` | `serverNode.peers.commission(opts)` / discovered → `clientNode.commission(opts)` |
| `controller.getNode(nodeId)` / `getPairedNode(nodeId)` | `serverNode.peers.get(nodeId \| address)` (returns `ClientNode`) |
| `controller.getNode(nodeId, allowUnknown)` | `serverNode.peers.forAddress(address)` |
| `controller.getCommissionedNodes()` | `serverNode.peers.commissioned` → map `peerAddress.nodeId` |
| `controller.getCommissionedNodesDetails()` | `serverNode.peers.commissioned` + per-node `stateOf(CommissioningClient)` / `BasicInformationClient` |
| `controller.isNodeCommissioned(id)` | `serverNode.peers.get(id)?.lifecycle.isCommissioned` |
| `controller.removeNode(id, tryDecommission)` | `clientNode.decommission()` (fallback `clientNode.delete()` for force) |
| `controller.disconnectNode(id)` | `clientNode.disable()` |
| `controller.connectNode(id)` | `clientNode.enable()` / `clientNode.start()` |
| `controller.discoverCommissionableDevices(opts)` | `serverNode.peers.discover(opts)` / `peers.locate(opts)` |
| `controller.getActiveSessionInformation()` | `serverNode.stateOf(SessionsBehavior).sessions` |
| `controller.updateFabricLabel(label)` | `fabric.setLabel(label)` (+ auto per-node sync on online) — **moves to node-management layer** |
| `controller.fabric` / `nodeId` | multi-fabric via `env.get(FabricAuthority)`; common case `FabricAuthority.defaultFabric` |
| `controller.resetStorage()` | `serverNode.erase()` |
| `controller.start()` / `close()` | `serverNode.start()` / `serverNode.close()` |

> Note: a controller `ServerNode` is **multi-fabric** (`FabricAuthority.fabrics`). Legacy `fabric`/`nodeId`
> singular getters existed because the legacy controller was single-fabric. Pick a fabric explicitly, or
> use `FabricAuthority.defaultFabric` for the single-fabric case.

---

## Subscriptions & reconnection are automatic now

`connect()`, `reconnect()`, `triggerReconnect()`, `subscribeAllAttributesAndEvents()` are **no longer
needed** — the `NetworkClient`'s `SustainedSubscription` establishes and re-establishes the subscription
automatically.

TODO: before/after examples — `enable()`/`disable()`, `set({ network: { autoSubscribe, defaultSubscription: { minIntervalFloor, maxIntervalCeiling } } })`,
and reading the current interval.

---

## Connection state

`ClientNode` now exposes the 4-state connection status on `lifecycle`, replacing PairedNode's `NodeStates`:

| Legacy (`PairedNode`) | New (`clientNode.lifecycle`) |
| --- | --- |
| `node.state` (`NodeStates`) | `lifecycle.connectionState` (`NodeConnectionState`) |
| `node.events.stateChanged` | `lifecycle.connectionStateChanged` |
| — | `lifecycle.isConnected` (convenience) |

`NodeConnectionState` uses the same numeric values as legacy `NodeStates`
(`Connected=0, Disconnected=1, Reconnecting=2, WaitingForDeviceDiscovery=3`), so a consumer migrates by
type-swap. Semantics: `Connected` = live subscription; `Disconnected` = node stopped / not started /
disabled; `Reconnecting` = started and (re-)establishing; `WaitingForDeviceDiscovery` = peer appears
offline (establishment past the MRP budget, or a missed ICD check-in for registered LIT ICDs). The
2-state `lifecycle.isOnline` still exists for simple online/offline needs.

---

## Attribute / event changes (the `PairedNode.events` bus)

TODO: PairedNode aggregated all changes into flat per-node observables (`attributeChanged`,
`eventTriggered`, `structureChanged`, `deviceInformationChanged`, `nodeEndpoint{Added,Removed,Changed}`,
`connectionAlive`). Document the sanctioned new-API pattern (per-behavior `reactTo` / interaction
subscription / `ChangeNotificationService`) with before/after code once verified against the shell and
server migrations.

---

## Commissioning windows

Available on both `ClientNode` and its `CommissioningClient` behavior:

- `openBasicCommissioningWindow(commissioningTimeout?: Duration): Promise<void>` (default `Seconds(900)`)
- `openEnhancedCommissioningWindow(commissioningTimeout?: Duration): Promise<{ manualPairingCode: string; qrPairingCode: string }>`

Both revoke any stale window first (tolerating `WindowNotOpen`). The enhanced window generates the
discriminator/passcode/salt/iterations and PAKE verifier and returns the pairing codes; basic throws
`ImplementationError` if the peer's `AdministratorCommissioning` cluster lacks the `Basic` feature.

---

## "Is the node ready?"

Legacy `initialized` / `localInitializationDone` / `remoteInitializationDone` → `clientNode.lifecycle.isSeeded`
(the peer's structure has been read: BasicInformation present and more than just the root endpoint), with
`lifecycle.seeded` emitting once on the false→true transition.

---

## FAQ

TODO.
Loading
Loading