@project-chip/matter.js 0.16.0-alpha.0-20250912-0d12bf718 → 0.16.0-alpha.0-20250916-d577beb1d

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/cjs/CommissioningController.d.ts +2 -1
  2. package/dist/cjs/CommissioningController.d.ts.map +1 -1
  3. package/dist/cjs/CommissioningController.js +10 -10
  4. package/dist/cjs/CommissioningController.js.map +1 -1
  5. package/dist/cjs/device/Endpoint.d.ts +42 -1
  6. package/dist/cjs/device/Endpoint.d.ts.map +1 -1
  7. package/dist/cjs/device/Endpoint.js +73 -14
  8. package/dist/cjs/device/Endpoint.js.map +1 -1
  9. package/dist/cjs/device/EndpointPropertiesProxy.d.ts +33 -0
  10. package/dist/cjs/device/EndpointPropertiesProxy.d.ts.map +1 -0
  11. package/dist/cjs/device/EndpointPropertiesProxy.js +109 -0
  12. package/dist/cjs/device/EndpointPropertiesProxy.js.map +6 -0
  13. package/dist/cjs/device/PairedNode.d.ts +43 -2
  14. package/dist/cjs/device/PairedNode.d.ts.map +1 -1
  15. package/dist/cjs/device/PairedNode.js +44 -4
  16. package/dist/cjs/device/PairedNode.js.map +1 -1
  17. package/dist/esm/CommissioningController.d.ts +2 -1
  18. package/dist/esm/CommissioningController.d.ts.map +1 -1
  19. package/dist/esm/CommissioningController.js +10 -11
  20. package/dist/esm/CommissioningController.js.map +1 -1
  21. package/dist/esm/device/Endpoint.d.ts +42 -1
  22. package/dist/esm/device/Endpoint.d.ts.map +1 -1
  23. package/dist/esm/device/Endpoint.js +73 -14
  24. package/dist/esm/device/Endpoint.js.map +1 -1
  25. package/dist/esm/device/EndpointPropertiesProxy.d.ts +33 -0
  26. package/dist/esm/device/EndpointPropertiesProxy.d.ts.map +1 -0
  27. package/dist/esm/device/EndpointPropertiesProxy.js +89 -0
  28. package/dist/esm/device/EndpointPropertiesProxy.js.map +6 -0
  29. package/dist/esm/device/PairedNode.d.ts +43 -2
  30. package/dist/esm/device/PairedNode.d.ts.map +1 -1
  31. package/dist/esm/device/PairedNode.js +44 -4
  32. package/dist/esm/device/PairedNode.js.map +1 -1
  33. package/package.json +8 -8
  34. package/src/CommissioningController.ts +11 -11
  35. package/src/device/Endpoint.ts +82 -15
  36. package/src/device/EndpointPropertiesProxy.ts +114 -0
  37. package/src/device/PairedNode.ts +53 -6
@@ -10,7 +10,8 @@ import {
10
10
  FixedLabelCluster,
11
11
  UserLabelCluster,
12
12
  } from "#clusters";
13
- import { AtLeastOne, Diagnostic, ImplementationError, InternalError, NotImplementedError } from "#general";
13
+ import { AtLeastOne, Diagnostic, Immutable, ImplementationError, InternalError, NotImplementedError } from "#general";
14
+ import { Behavior, Commands as BehaviorCommands } from "#node";
14
15
  import { ClusterClientObj, SupportedAttributeClient, UnknownSupportedAttributeClient } from "#protocol";
15
16
  import {
16
17
  Attributes,
@@ -28,6 +29,7 @@ import {
28
29
  import { ClusterServer } from "../cluster/server/ClusterServer.js";
29
30
  import { ClusterServerObj, asClusterServerInternal } from "../cluster/server/ClusterServerTypes.js";
30
31
  import { DeviceTypeDefinition } from "./DeviceTypes.js";
32
+ import { EndpointPropertiesProxy } from "./EndpointPropertiesProxy.js";
31
33
 
32
34
  export interface EndpointOptions {
33
35
  endpointId?: EndpointNumber;
@@ -37,13 +39,15 @@ export interface EndpointOptions {
37
39
  export class Endpoint {
38
40
  private readonly clusterServers = new Map<ClusterId, ClusterServerObj>();
39
41
  private readonly clusterClients = new Map<ClusterId, ClusterClientObj>();
40
- private readonly childEndpoints: Endpoint[] = [];
42
+ private readonly childEndpoints = new Map<number, Endpoint>();
41
43
  number: EndpointNumber | undefined;
42
44
  uniqueStorageKey: string | undefined;
43
45
  name = "";
44
46
  private structureChangedCallback: () => void = () => {
45
47
  /** noop until officially set **/
46
48
  };
49
+ #stateProxy?: EndpointPropertiesProxy.State;
50
+ #commandsProxy?: EndpointPropertiesProxy.Commands;
47
51
 
48
52
  /**
49
53
  * Create a new Endpoint instance.
@@ -65,13 +69,74 @@ export class Endpoint {
65
69
  }
66
70
  }
67
71
 
72
+ /**
73
+ * Access to cached cluster state values using endpoint.state.clusterNameOrId.attributeNameOrId
74
+ * Returns immutable cached attribute values from cluster clients
75
+ */
76
+ get state() {
77
+ if (this.#stateProxy === undefined) {
78
+ this.#stateProxy = EndpointPropertiesProxy.state(this.clusterClients);
79
+ }
80
+ return this.#stateProxy;
81
+ }
82
+
83
+ /**
84
+ * Access to cluster commands using endpoint.commands.clusterNameOrId.commandName
85
+ * Returns async functions that can be called to invoke commands on cluster clients
86
+ */
87
+ get commands() {
88
+ if (this.#commandsProxy === undefined) {
89
+ this.#commandsProxy = EndpointPropertiesProxy.commands(this.clusterClients);
90
+ }
91
+ return this.#commandsProxy;
92
+ }
93
+
94
+ /**
95
+ * Access to typed cached cluster state values
96
+ * Returns immutable cached attribute values from cluster clients
97
+ */
98
+ stateOf<T extends Behavior.Type>(type: T) {
99
+ this.#clusterClientForBehaviorType(type); // just use to verify the existence of the cluster
100
+
101
+ return this.state[type.name] as Immutable<Behavior.StateOf<T>>;
102
+ }
103
+
104
+ /**
105
+ * Access to typed cluster commands
106
+ * Returns async functions that can be called to invoke commands on cluster clients
107
+ */
108
+ commandsOf<T extends Behavior.Type>(type: T) {
109
+ return this.#clusterClientForBehaviorType(type).commands as BehaviorCommands.OfBehavior<T>;
110
+ }
111
+
112
+ #clusterClientForBehaviorType<T extends Behavior.Type>(type: T): ClusterClientObj {
113
+ const clusterId = type.schema?.tag === "cluster" ? (type.schema.id as ClusterId) : undefined;
114
+ if (clusterId === undefined) {
115
+ throw new ImplementationError(`Behavior ${type.id} is not backed by a cluster`);
116
+ }
117
+ const clusterClient = this.clusterClients.get(clusterId);
118
+ if (!clusterClient) {
119
+ throw new ImplementationError(
120
+ `Cluster ${type.id} (0x${clusterId.toString(16)}) is not present on endpoint ${this.number}`,
121
+ );
122
+ }
123
+ return clusterClient;
124
+ }
125
+
126
+ /** Get all child endpoints aka parts */
127
+ get parts() {
128
+ return this.childEndpoints;
129
+ }
130
+
68
131
  get deviceType(): DeviceTypeId {
69
132
  return this.deviceTypes[0].code;
70
133
  }
71
134
 
72
135
  setStructureChangedCallback(callback: () => void) {
73
136
  this.structureChangedCallback = callback;
74
- this.childEndpoints.forEach(endpoint => endpoint.setStructureChangedCallback(callback));
137
+ for (const endpoint of this.childEndpoints.values()) {
138
+ endpoint.setStructureChangedCallback(callback);
139
+ }
75
140
  }
76
141
 
77
142
  removeFromStructure() {
@@ -79,7 +144,9 @@ export class Endpoint {
79
144
  this.structureChangedCallback = () => {
80
145
  /** noop **/
81
146
  };
82
- this.childEndpoints.forEach(endpoint => endpoint.removeFromStructure());
147
+ for (const endpoint of this.childEndpoints.values()) {
148
+ endpoint.removeFromStructure();
149
+ }
83
150
  }
84
151
 
85
152
  close() {
@@ -217,34 +284,34 @@ export class Endpoint {
217
284
 
218
285
  addChildEndpoint(endpoint: Endpoint): void {
219
286
  if (!(endpoint instanceof Endpoint)) {
220
- throw new Error("Only supported EndpointInterface implementation is Endpoint");
287
+ throw new InternalError("Only supported EndpointInterface implementation is Endpoint");
221
288
  }
289
+ const id = endpoint.getNumber();
222
290
 
223
- if (endpoint.number !== undefined && this.getChildEndpoint(endpoint.number) !== undefined) {
224
- throw new ImplementationError(
225
- `Endpoint with id ${endpoint.number} already exists as child from ${this.number}.`,
226
- );
291
+ if (this.childEndpoints.has(id)) {
292
+ throw new ImplementationError(`Endpoint with id ${id} already exists as child from ${this.number}.`);
227
293
  }
228
294
 
229
- this.childEndpoints.push(endpoint);
295
+ this.childEndpoints.set(id, endpoint);
230
296
  endpoint.setStructureChangedCallback(this.structureChangedCallback);
231
297
  this.structureChangedCallback(); // Inform parent about structure change
232
298
  }
233
299
 
234
300
  getChildEndpoint(id: EndpointNumber): Endpoint | undefined {
235
- return this.childEndpoints.find(endpoint => endpoint.number === id);
301
+ return this.childEndpoints.get(id);
236
302
  }
237
303
 
238
304
  getChildEndpoints(): Endpoint[] {
239
- return this.childEndpoints;
305
+ return Array.from(this.childEndpoints.values());
240
306
  }
241
307
 
242
308
  protected removeChildEndpoint(endpoint: Endpoint): void {
243
- const index = this.childEndpoints.indexOf(endpoint);
244
- if (index === -1) {
309
+ const id = endpoint.getNumber();
310
+ const knownEndpoint = this.childEndpoints.get(id);
311
+ if (knownEndpoint === undefined) {
245
312
  throw new ImplementationError(`Provided endpoint for deletion does not exist as child endpoint.`);
246
313
  }
247
- this.childEndpoints.splice(index, 1);
314
+ this.childEndpoints.delete(id);
248
315
  endpoint.removeFromStructure();
249
316
  this.structureChangedCallback(); // Inform parent about structure change
250
317
  }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Immutable } from "#general";
8
+ import { ClusterClientObj } from "#protocol";
9
+ import { AttributeId, ClusterId, CommandId } from "#types";
10
+
11
+ enum EndpointPropertiesProxyType {
12
+ State = "state",
13
+ Commands = "commands",
14
+ }
15
+
16
+ /**
17
+ * Factory for creating proxy-based access to cached cluster state values for legacy Endpoint.
18
+ * This enables ClientNode-style state access patterns.
19
+ */
20
+ export class EndpointPropertiesProxy {
21
+ #clusterClients: Map<ClusterId, ClusterClientObj>;
22
+ #nameIdMap?: Map<string, ClusterId>;
23
+
24
+ /**
25
+ * Create a state proxy that allows access via cluster names/IDs
26
+ */
27
+ static state(clusterClients: Map<ClusterId, ClusterClientObj>) {
28
+ return new EndpointPropertiesProxy(clusterClients).#proxy(
29
+ EndpointPropertiesProxyType.State,
30
+ ) as EndpointPropertiesProxy.State;
31
+ }
32
+
33
+ /**
34
+ * Create a commands proxy that allows access via cluster names/IDs
35
+ */
36
+ static commands(clusterClients: Map<ClusterId, ClusterClientObj>) {
37
+ return new EndpointPropertiesProxy(clusterClients).#proxy(
38
+ EndpointPropertiesProxyType.Commands,
39
+ ) as EndpointPropertiesProxy.Commands;
40
+ }
41
+
42
+ constructor(clusterClients: Map<ClusterId, ClusterClientObj>) {
43
+ this.#clusterClients = clusterClients;
44
+ }
45
+
46
+ #proxy(type: EndpointPropertiesProxyType) {
47
+ return new Proxy(
48
+ {},
49
+ {
50
+ get: (_target, prop) => {
51
+ if (typeof prop !== "string") {
52
+ return undefined;
53
+ }
54
+
55
+ // Try to find cluster by name first, then by id
56
+ let clusterId = this.#clusterIdForName(prop);
57
+ if (clusterId === undefined) {
58
+ const id = parseInt(prop, 10);
59
+ clusterId = Number.isFinite(id) ? ClusterId(id) : undefined;
60
+ }
61
+ if (clusterId !== undefined) {
62
+ const clusterClient = this.#clusterClients.get(clusterId);
63
+
64
+ if (clusterClient !== undefined) {
65
+ switch (type) {
66
+ case EndpointPropertiesProxyType.Commands:
67
+ return clusterClient.commands;
68
+ case EndpointPropertiesProxyType.State:
69
+ return this.#createClusterStateProxy(clusterClient);
70
+ }
71
+ }
72
+ }
73
+ return undefined;
74
+ },
75
+ },
76
+ );
77
+ }
78
+
79
+ #createClusterStateProxy(clusterClient: ClusterClientObj): Immutable<Record<string | AttributeId, any>> {
80
+ return new Proxy(
81
+ {},
82
+ {
83
+ get: (_target, prop) => {
84
+ if (typeof prop !== "string") {
85
+ return undefined;
86
+ }
87
+
88
+ return clusterClient.attributes[prop]?.getLocal();
89
+ },
90
+ },
91
+ ) as Immutable<Record<string | AttributeId, any>>;
92
+ }
93
+
94
+ #clusterIdForName(name: string): ClusterId | undefined {
95
+ // Initialize map if not done yet or cluster structure changed
96
+ if (this.#nameIdMap === undefined || this.#nameIdMap.size !== this.#clusterClients.size) {
97
+ this.#nameIdMap = new Map<string, ClusterId>();
98
+ for (const [id, client] of this.#clusterClients) {
99
+ this.#nameIdMap.set(client.name.toLowerCase(), id);
100
+ }
101
+ }
102
+ return this.#nameIdMap.get(name.toLowerCase());
103
+ }
104
+ }
105
+
106
+ export namespace EndpointPropertiesProxy {
107
+ export type State = Immutable<{
108
+ [key: string | ClusterId]: Record<string | AttributeId, any> | undefined;
109
+ }>;
110
+
111
+ export type Commands = Immutable<{
112
+ [key: string | ClusterId]: Record<string | CommandId, (data: any) => Promise<unknown>> | undefined;
113
+ }>;
114
+ }
@@ -24,6 +24,7 @@ import {
24
24
  Time,
25
25
  Timer,
26
26
  } from "#general";
27
+ import { Behavior, Commands } from "#node";
27
28
  import {
28
29
  AttributeClientValues,
29
30
  ChannelStatusResponseError,
@@ -70,6 +71,7 @@ import {
70
71
  getDeviceTypeDefinitionFromModelByCode,
71
72
  } from "./DeviceTypes.js";
72
73
  import { Endpoint } from "./Endpoint.js";
74
+ import { EndpointPropertiesProxy } from "./EndpointPropertiesProxy.js";
73
75
  import { asClusterClientInternal, isClusterClient } from "./TypeHelpers.js";
74
76
 
75
77
  const logger = Logger.get("PairedNode");
@@ -235,7 +237,7 @@ interface SubscriptionHandlerCallbacks {
235
237
  * the CommissioningController on commissioning or when connecting.
236
238
  */
237
239
  export class PairedNode {
238
- readonly #endpoints = new Map<EndpointNumber, Endpoint>();
240
+ readonly #endpoints = new Map<number, Endpoint>();
239
241
  #interactionClient: InteractionClient;
240
242
  #reconnectDelayTimer?: Timer;
241
243
  #newChannelReconnectDelayTimer = Time.getTimer(
@@ -395,7 +397,7 @@ export class PairedNode {
395
397
  if (
396
398
  session.isInitiator || // If we initiated the session we do not need to react on it
397
399
  session.peerNodeId !== this.nodeId || // no session for this node
398
- this.state !== NodeStates.WaitingForDeviceDiscovery
400
+ this.connectionState !== NodeStates.WaitingForDeviceDiscovery
399
401
  ) {
400
402
  return;
401
403
  }
@@ -412,7 +414,7 @@ export class PairedNode {
412
414
  // This kicks of the remote initialization and automatic reconnection handling if it can not be connected
413
415
  this.#initialize().catch(error => {
414
416
  logger.info(`Node ${nodeId}: Error during remote initialization`, error);
415
- if (this.state !== NodeStates.Disconnected) {
417
+ if (this.connectionState !== NodeStates.Disconnected) {
416
418
  this.#setConnectionState(NodeStates.WaitingForDeviceDiscovery);
417
419
  this.#scheduleReconnect();
418
420
  }
@@ -430,7 +432,7 @@ export class PairedNode {
430
432
  }
431
433
 
432
434
  /** Returns the Node connection state. */
433
- get state() {
435
+ get connectionState() {
434
436
  return this.#connectionState;
435
437
  }
436
438
 
@@ -955,7 +957,7 @@ export class PairedNode {
955
957
  }
956
958
 
957
959
  #scheduleReconnect(delay?: Duration) {
958
- if (this.state !== NodeStates.WaitingForDeviceDiscovery) {
960
+ if (this.connectionState !== NodeStates.WaitingForDeviceDiscovery) {
959
961
  this.#setConnectionState(NodeStates.Reconnecting);
960
962
  }
961
963
 
@@ -988,7 +990,7 @@ export class PairedNode {
988
990
 
989
991
  if (updateStructure) {
990
992
  // Find out what we need to remove or retain
991
- const endpointsToRemove = new Set<EndpointNumber>(this.#endpoints.keys());
993
+ const endpointsToRemove = new Set<number>(this.#endpoints.keys());
992
994
  for (const [endpointId] of Object.entries(allData)) {
993
995
  const endpointIdNumber = EndpointNumber(parseInt(endpointId));
994
996
  if (this.#endpoints.has(endpointIdNumber)) {
@@ -1198,6 +1200,11 @@ export class PairedNode {
1198
1200
  }
1199
1201
  }
1200
1202
 
1203
+ /** Returns all parts (endpoints) known for the Root Endpoint of this node. */
1204
+ get parts() {
1205
+ return this.getRootEndpoint()?.parts ?? new Map<number, Endpoint>();
1206
+ }
1207
+
1201
1208
  /** Returns the functional devices/endpoints (the "childs" of the Root Endpoint) known for this node. */
1202
1209
  getDevices(): Endpoint[] {
1203
1210
  return this.#endpoints.get(EndpointNumber(0))?.getChildEndpoints() ?? [];
@@ -1432,4 +1439,44 @@ export class PairedNode {
1432
1439
  ],
1433
1440
  });
1434
1441
  }
1442
+
1443
+ /**
1444
+ * Access to cached cluster state values of the root endpoint using node.state.clusterNameOrId.attributeNameOrId
1445
+ * Returns immutable cached attribute values from cluster clients
1446
+ */
1447
+ get state() {
1448
+ return this.getRootEndpoint()?.state ?? ({} as EndpointPropertiesProxy.State);
1449
+ }
1450
+
1451
+ /**
1452
+ * Access to cluster commands of the root endpoint using node.commands.clusterNameOrId.commandName
1453
+ * Returns async functions that can be called to invoke commands on cluster clients
1454
+ */
1455
+ get commands() {
1456
+ return this.getRootEndpoint()?.commands ?? ({} as EndpointPropertiesProxy.Commands);
1457
+ }
1458
+
1459
+ /**
1460
+ * Access to typed cached cluster state values of the root endpoint
1461
+ * Returns immutable cached attribute values from cluster clients
1462
+ */
1463
+ stateOf<T extends Behavior.Type>(type: T) {
1464
+ const root = this.getRootEndpoint();
1465
+ if (root === undefined) {
1466
+ throw new ImplementationError(`Root endpoint for node ${this.nodeId} not found.`);
1467
+ }
1468
+ return root.stateOf(type);
1469
+ }
1470
+
1471
+ /**
1472
+ * Access to typed cluster commands of the root endpoint
1473
+ * Returns async functions that can be called to invoke commands on cluster clients
1474
+ */
1475
+ commandsOf<T extends Behavior.Type>(type: T): Commands.OfBehavior<T> {
1476
+ const root = this.getRootEndpoint();
1477
+ if (root === undefined) {
1478
+ throw new ImplementationError(`Root endpoint for node ${this.nodeId} not found.`);
1479
+ }
1480
+ return root.commandsOf(type);
1481
+ }
1435
1482
  }