@matter-server/ws-controller 0.6.2-alpha.0-20260425-025dd18 → 0.6.2
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.
- package/dist/esm/controller/ControllerCommandHandler.d.ts +1 -6
- package/dist/esm/controller/ControllerCommandHandler.d.ts.map +1 -1
- package/dist/esm/controller/ControllerCommandHandler.js +103 -226
- package/dist/esm/controller/ControllerCommandHandler.js.map +2 -2
- package/dist/esm/controller/Nodes.d.ts +1 -19
- package/dist/esm/controller/Nodes.d.ts.map +1 -1
- package/dist/esm/controller/Nodes.js +0 -31
- package/dist/esm/controller/Nodes.js.map +1 -1
- package/dist/esm/types/CommandHandler.d.ts +1 -117
- package/dist/esm/types/CommandHandler.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/controller/ControllerCommandHandler.ts +115 -251
- package/src/controller/Nodes.ts +1 -48
- package/src/types/CommandHandler.ts +1 -122
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
AsyncObservable,
|
|
10
10
|
camelize,
|
|
11
11
|
ClientNode,
|
|
12
|
-
ClientNodeInteraction,
|
|
13
12
|
CommissioningClient,
|
|
14
13
|
FabricId,
|
|
15
14
|
FabricIndex,
|
|
@@ -84,12 +83,6 @@ import {
|
|
|
84
83
|
MatterNodeData,
|
|
85
84
|
OpenCommissioningWindowRequest,
|
|
86
85
|
OpenCommissioningWindowResponse,
|
|
87
|
-
ReadEventRequest,
|
|
88
|
-
ReadEventResponse,
|
|
89
|
-
SubscribeAttributeRequest,
|
|
90
|
-
SubscribeAttributeResponse,
|
|
91
|
-
SubscribeEventRequest,
|
|
92
|
-
SubscribeEventResponse,
|
|
93
86
|
WriteAttributeRequest,
|
|
94
87
|
} from "../types/CommandHandler.js";
|
|
95
88
|
import {
|
|
@@ -370,7 +363,10 @@ export class ControllerCommandHandler {
|
|
|
370
363
|
basicInfoChangedInBatch = false;
|
|
371
364
|
this.events.nodeStructureChanged.emit(nodeId);
|
|
372
365
|
});
|
|
373
|
-
node.events.decommissioned.on(() =>
|
|
366
|
+
node.events.decommissioned.on(() => {
|
|
367
|
+
this.#cleanupNodeAfterRemoval(nodeId);
|
|
368
|
+
this.events.nodeDecommissioned.emit(nodeId);
|
|
369
|
+
});
|
|
374
370
|
node.events.nodeEndpointAdded.on(endpointId => this.events.nodeEndpointAdded.emit(nodeId, endpointId));
|
|
375
371
|
node.events.nodeEndpointRemoved.on(endpointId => this.events.nodeEndpointRemoved.emit(nodeId, endpointId));
|
|
376
372
|
|
|
@@ -465,7 +461,7 @@ export class ControllerCommandHandler {
|
|
|
465
461
|
}),
|
|
466
462
|
includeKnownVersions: true, // do not send DataVersionFilters, so we do a new clean read
|
|
467
463
|
};
|
|
468
|
-
for await (const _chunk of
|
|
464
|
+
for await (const _chunk of node.node.interaction.read(read));
|
|
469
465
|
}
|
|
470
466
|
|
|
471
467
|
/**
|
|
@@ -517,42 +513,44 @@ export class ControllerCommandHandler {
|
|
|
517
513
|
fabricFiltered = false,
|
|
518
514
|
): Promise<AttributesData> {
|
|
519
515
|
const result: AttributesData = {};
|
|
520
|
-
const
|
|
516
|
+
const node = this.#nodes.get(nodeId);
|
|
521
517
|
const batchSize = 9;
|
|
522
|
-
|
|
523
|
-
// Parse all paths (wildcards become undefined for that component)
|
|
524
518
|
const parsedPaths = attributePaths.map(path => splitAttributePath(path));
|
|
525
519
|
|
|
526
|
-
// Process in batches of up to 9
|
|
527
520
|
for (let i = 0; i < parsedPaths.length; i += batchSize) {
|
|
528
521
|
const batch = parsedPaths.slice(i, i + batchSize);
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
for (const { path: attrPath, value } of attributeData) {
|
|
541
|
-
const { pathStr, value: wsValue } = this.#convertAttributeToWebSocket(
|
|
542
|
-
{
|
|
543
|
-
endpointId: EndpointNumber(attrPath.endpointId),
|
|
544
|
-
clusterId: ClusterId(attrPath.clusterId),
|
|
545
|
-
attributeId: attrPath.attributeId,
|
|
546
|
-
},
|
|
547
|
-
value,
|
|
548
|
-
);
|
|
549
|
-
result[pathStr] = wsValue;
|
|
550
|
-
}
|
|
522
|
+
const readRequest = {
|
|
523
|
+
...Read({
|
|
524
|
+
fabricFilter: fabricFiltered,
|
|
525
|
+
attributes: batch.map(({ endpointId, clusterId, attributeId }) => ({
|
|
526
|
+
endpointId: endpointId !== undefined ? EndpointNumber(endpointId) : undefined,
|
|
527
|
+
clusterId: clusterId !== undefined ? ClusterId(clusterId) : undefined,
|
|
528
|
+
attributeId: attributeId !== undefined ? AttributeId(attributeId) : undefined,
|
|
529
|
+
})),
|
|
530
|
+
}),
|
|
531
|
+
includeKnownVersions: true,
|
|
532
|
+
};
|
|
551
533
|
|
|
552
|
-
|
|
553
|
-
for (const
|
|
554
|
-
|
|
555
|
-
|
|
534
|
+
for await (const chunk of node.node.interaction.read(readRequest)) {
|
|
535
|
+
for (const entry of chunk) {
|
|
536
|
+
if (entry.kind === "attr-value") {
|
|
537
|
+
const { pathStr, value: wsValue } = this.#convertAttributeToWebSocket(
|
|
538
|
+
{
|
|
539
|
+
endpointId: EndpointNumber(entry.path.endpointId),
|
|
540
|
+
clusterId: ClusterId(entry.path.clusterId),
|
|
541
|
+
attributeId: entry.path.attributeId,
|
|
542
|
+
},
|
|
543
|
+
entry.value,
|
|
544
|
+
);
|
|
545
|
+
result[pathStr] = wsValue;
|
|
546
|
+
} else if (entry.kind === "attr-status") {
|
|
547
|
+
const pathStr = buildAttributePath(
|
|
548
|
+
entry.path.endpointId,
|
|
549
|
+
entry.path.clusterId,
|
|
550
|
+
entry.path.attributeId,
|
|
551
|
+
);
|
|
552
|
+
logger.warn(`Failed to read attribute ${pathStr}: status=${entry.status}`);
|
|
553
|
+
}
|
|
556
554
|
}
|
|
557
555
|
}
|
|
558
556
|
}
|
|
@@ -582,6 +580,33 @@ export class ControllerCommandHandler {
|
|
|
582
580
|
};
|
|
583
581
|
}
|
|
584
582
|
|
|
583
|
+
/**
|
|
584
|
+
* Write a single attribute on a remote node. Uses `setStateOf(string, ...)` (not `set({...})`)
|
|
585
|
+
* because peer cluster behaviors are dynamically registered and aren't on the agent's cached property getters.
|
|
586
|
+
*/
|
|
587
|
+
async #writeAttribute(
|
|
588
|
+
nodeId: NodeId,
|
|
589
|
+
endpointId: EndpointNumber,
|
|
590
|
+
clusterId: ClusterId,
|
|
591
|
+
attributeName: string,
|
|
592
|
+
value: unknown,
|
|
593
|
+
): Promise<{ status: number; clusterStatus?: number }> {
|
|
594
|
+
const node = this.#nodes.get(nodeId);
|
|
595
|
+
const clusterEntry = ClusterMap[clusterId];
|
|
596
|
+
if (!clusterEntry) {
|
|
597
|
+
throw ServerError.invalidArguments(`Cluster Id "${clusterId}" unknown`);
|
|
598
|
+
}
|
|
599
|
+
const clusterProperty = clusterEntry.model.propertyName;
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
await node.node.endpoints.for(endpointId).setStateOf(clusterProperty, { [attributeName]: value });
|
|
603
|
+
return { status: 0 };
|
|
604
|
+
} catch (error) {
|
|
605
|
+
StatusResponseError.accept(error);
|
|
606
|
+
return { status: error.code, clusterStatus: error.clusterCode };
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
585
610
|
/**
|
|
586
611
|
* Set the fabric label. Pass null or empty string to reset to "Home".
|
|
587
612
|
* Note: matter.js requires non-empty labels (1-32 chars), so null/empty resets to default.
|
|
@@ -594,165 +619,27 @@ export class ControllerCommandHandler {
|
|
|
594
619
|
return this.#controller.disconnectNode(nodeId, true);
|
|
595
620
|
}
|
|
596
621
|
|
|
597
|
-
async handleReadEvent(data: ReadEventRequest): Promise<ReadEventResponse> {
|
|
598
|
-
const { nodeId, endpointId, clusterId, eventId, eventMin } = data;
|
|
599
|
-
const client = this.#nodes.interactionClientFor(nodeId);
|
|
600
|
-
const { eventData, eventStatus } = await client.getMultipleEventsAndStatus({
|
|
601
|
-
events: [
|
|
602
|
-
{
|
|
603
|
-
endpointId,
|
|
604
|
-
clusterId,
|
|
605
|
-
eventId,
|
|
606
|
-
},
|
|
607
|
-
],
|
|
608
|
-
eventFilters: eventMin ? [{ eventMin }] : undefined,
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
return {
|
|
612
|
-
values: eventData.flatMap(({ path: { endpointId, clusterId, eventId }, events }) =>
|
|
613
|
-
events.map(({ eventNumber, data }) => ({
|
|
614
|
-
eventId,
|
|
615
|
-
clusterId,
|
|
616
|
-
endpointId,
|
|
617
|
-
eventNumber,
|
|
618
|
-
value: data,
|
|
619
|
-
})),
|
|
620
|
-
),
|
|
621
|
-
status: eventStatus?.map(({ path: { endpointId, clusterId, eventId }, status, clusterStatus }) => ({
|
|
622
|
-
clusterId,
|
|
623
|
-
endpointId,
|
|
624
|
-
eventId,
|
|
625
|
-
status,
|
|
626
|
-
clusterStatus,
|
|
627
|
-
})),
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
async handleSubscribeAttribute(data: SubscribeAttributeRequest): Promise<SubscribeAttributeResponse> {
|
|
632
|
-
const { nodeId, endpointId, clusterId, attributeId, minInterval, maxInterval, changeListener } = data;
|
|
633
|
-
const client = this.#nodes.interactionClientFor(nodeId);
|
|
634
|
-
const updated = Observable<[void]>();
|
|
635
|
-
let ignoreData = true; // We ignore data coming in during initial seeding
|
|
636
|
-
const { attributeReports = [] } = await client.subscribeMultipleAttributesAndEvents({
|
|
637
|
-
attributes: [
|
|
638
|
-
{
|
|
639
|
-
endpointId,
|
|
640
|
-
clusterId,
|
|
641
|
-
attributeId,
|
|
642
|
-
},
|
|
643
|
-
],
|
|
644
|
-
minIntervalFloorSeconds: minInterval,
|
|
645
|
-
maxIntervalCeilingSeconds: maxInterval,
|
|
646
|
-
attributeListener: data => {
|
|
647
|
-
if (ignoreData) return;
|
|
648
|
-
changeListener({
|
|
649
|
-
attributeId: data.path.attributeId,
|
|
650
|
-
clusterId: data.path.clusterId,
|
|
651
|
-
endpointId: data.path.endpointId,
|
|
652
|
-
dataVersion: data.version,
|
|
653
|
-
value: data.value,
|
|
654
|
-
});
|
|
655
|
-
},
|
|
656
|
-
updateReceived: () => {
|
|
657
|
-
updated.emit();
|
|
658
|
-
},
|
|
659
|
-
keepSubscriptions: false,
|
|
660
|
-
});
|
|
661
|
-
ignoreData = false;
|
|
662
|
-
|
|
663
|
-
return {
|
|
664
|
-
values: attributeReports.map(
|
|
665
|
-
({ path: { endpointId, clusterId, attributeId }, value, version: dataVersion }) => ({
|
|
666
|
-
attributeId,
|
|
667
|
-
clusterId,
|
|
668
|
-
endpointId,
|
|
669
|
-
dataVersion,
|
|
670
|
-
value,
|
|
671
|
-
}),
|
|
672
|
-
),
|
|
673
|
-
updated,
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
async handleSubscribeEvent(data: SubscribeEventRequest): Promise<SubscribeEventResponse> {
|
|
678
|
-
const { nodeId, endpointId, clusterId, eventId, minInterval, maxInterval, changeListener } = data;
|
|
679
|
-
const client = this.#nodes.interactionClientFor(nodeId);
|
|
680
|
-
const updated = Observable<[void]>();
|
|
681
|
-
let ignoreData = true; // We ignore data coming in during initial seeding
|
|
682
|
-
const { eventReports = [] } = await client.subscribeMultipleAttributesAndEvents({
|
|
683
|
-
events: [
|
|
684
|
-
{
|
|
685
|
-
endpointId,
|
|
686
|
-
clusterId,
|
|
687
|
-
eventId,
|
|
688
|
-
},
|
|
689
|
-
],
|
|
690
|
-
minIntervalFloorSeconds: minInterval,
|
|
691
|
-
maxIntervalCeilingSeconds: maxInterval,
|
|
692
|
-
eventListener: data => {
|
|
693
|
-
if (ignoreData) return;
|
|
694
|
-
data.events.forEach(event =>
|
|
695
|
-
changeListener({
|
|
696
|
-
eventId: data.path.eventId,
|
|
697
|
-
clusterId: data.path.clusterId,
|
|
698
|
-
endpointId: data.path.endpointId,
|
|
699
|
-
eventNumber: event.eventNumber,
|
|
700
|
-
value: event.data,
|
|
701
|
-
}),
|
|
702
|
-
);
|
|
703
|
-
},
|
|
704
|
-
updateReceived: () => {
|
|
705
|
-
updated.emit();
|
|
706
|
-
},
|
|
707
|
-
keepSubscriptions: false,
|
|
708
|
-
});
|
|
709
|
-
ignoreData = false;
|
|
710
|
-
|
|
711
|
-
return {
|
|
712
|
-
values: eventReports.flatMap(({ path: { endpointId, clusterId, eventId }, events }) =>
|
|
713
|
-
events.map(({ eventNumber, data }) => ({
|
|
714
|
-
eventId,
|
|
715
|
-
clusterId,
|
|
716
|
-
endpointId,
|
|
717
|
-
eventNumber,
|
|
718
|
-
value: data,
|
|
719
|
-
})),
|
|
720
|
-
),
|
|
721
|
-
updated,
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
|
|
725
622
|
async handleWriteAttribute(data: WriteAttributeRequest): Promise<AttributeResponseStatus> {
|
|
726
623
|
const { nodeId, endpointId, clusterId, attributeId } = data;
|
|
727
624
|
let { value } = data;
|
|
728
625
|
|
|
729
|
-
const client = this.#nodes.clusterClientByIdFor(nodeId, endpointId, clusterId);
|
|
730
|
-
|
|
731
626
|
const clusterEntry = ClusterMap[clusterId];
|
|
732
|
-
const
|
|
733
|
-
if (
|
|
734
|
-
|
|
627
|
+
const attributeModel = clusterEntry?.attributes[attributeId];
|
|
628
|
+
if (!clusterEntry || !attributeModel) {
|
|
629
|
+
throw ServerError.invalidArguments(`Attribute ${attributeId} on cluster ${clusterId} unknown`);
|
|
735
630
|
}
|
|
736
631
|
|
|
632
|
+
value = convertWebSocketTagBasedToMatter(value, attributeModel, clusterEntry.model);
|
|
633
|
+
|
|
737
634
|
logger.info("Writing attribute", attributeId, "with value", value);
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
} catch (error) {
|
|
747
|
-
StatusResponseError.accept(error);
|
|
748
|
-
return {
|
|
749
|
-
attributeId,
|
|
750
|
-
clusterId,
|
|
751
|
-
endpointId,
|
|
752
|
-
status: error.code,
|
|
753
|
-
clusterStatus: error.clusterCode,
|
|
754
|
-
};
|
|
755
|
-
}
|
|
635
|
+
const { status, clusterStatus } = await this.#writeAttribute(
|
|
636
|
+
nodeId,
|
|
637
|
+
endpointId,
|
|
638
|
+
clusterId,
|
|
639
|
+
attributeModel.propertyName,
|
|
640
|
+
value,
|
|
641
|
+
);
|
|
642
|
+
return { attributeId, clusterId, endpointId, status, clusterStatus };
|
|
756
643
|
}
|
|
757
644
|
|
|
758
645
|
async #invokeCommand<const C extends Specifier.ClusterLike>(
|
|
@@ -1140,12 +1027,21 @@ export class ControllerCommandHandler {
|
|
|
1140
1027
|
throw ServerError.nodeNotExists(nodeId);
|
|
1141
1028
|
}
|
|
1142
1029
|
await this.#controller.removeNode(nodeId, !!node?.isConnected);
|
|
1030
|
+
this.#cleanupNodeAfterRemoval(nodeId);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Drop all references to a removed node so subsequent reads don't reach a
|
|
1035
|
+
* destroyed PairedNode. Idempotent — both the `decommissioned` listener
|
|
1036
|
+
* (external fabric leave) and `decommissionNode` invoke it, and the
|
|
1037
|
+
* listener may have run first.
|
|
1038
|
+
*/
|
|
1039
|
+
#cleanupNodeAfterRemoval(nodeId: NodeId) {
|
|
1143
1040
|
this.#reconnectTimers.get(nodeId)?.stop();
|
|
1144
1041
|
this.#reconnectTimers.delete(nodeId);
|
|
1145
|
-
// Remove node from storage (also clears attribute cache)
|
|
1146
1042
|
this.#nodes.delete(nodeId);
|
|
1147
|
-
// Unregister from custom cluster polling
|
|
1148
1043
|
this.#customClusterPoller.unregisterNode(nodeId);
|
|
1044
|
+
this.#availableUpdates.delete(nodeId);
|
|
1149
1045
|
}
|
|
1150
1046
|
|
|
1151
1047
|
async openCommissioningWindow(data: OpenCommissioningWindowRequest): Promise<OpenCommissioningWindowResponse> {
|
|
@@ -1172,7 +1068,7 @@ export class ControllerCommandHandler {
|
|
|
1172
1068
|
includeKnownVersions: true, // we want to read from device
|
|
1173
1069
|
};
|
|
1174
1070
|
|
|
1175
|
-
for await (const chunk of
|
|
1071
|
+
for await (const chunk of node.node.interaction.read(read)) {
|
|
1176
1072
|
for (const attr of chunk) {
|
|
1177
1073
|
if (attr.kind === "attr-value" && Array.isArray(attr.value)) {
|
|
1178
1074
|
// We only expect one array response
|
|
@@ -1197,12 +1093,8 @@ export class ControllerCommandHandler {
|
|
|
1197
1093
|
/**
|
|
1198
1094
|
* Set Access Control List entries on a node.
|
|
1199
1095
|
* Writes to the ACL attribute on the AccessControl cluster (endpoint 0).
|
|
1200
|
-
* TODO Migrate to new Node API
|
|
1201
1096
|
*/
|
|
1202
1097
|
async setAclEntry(nodeId: NodeId, entries: AccessControlEntry[]): Promise<AttributeWriteResult[] | null> {
|
|
1203
|
-
const client = this.#nodes.clusterClientFor(nodeId, EndpointNumber(0), AccessControl.Cluster);
|
|
1204
|
-
|
|
1205
|
-
// Convert from WebSocket format (snake_case) to Matter.js format (camelCase)
|
|
1206
1098
|
const aclEntries: AccessControl.AccessControlEntry[] = entries.map(entry => ({
|
|
1207
1099
|
privilege: entry.privilege as AccessControl.AccessControlEntryPrivilege,
|
|
1208
1100
|
authMode: entry.auth_mode as AccessControl.AccessControlEntryAuthMode,
|
|
@@ -1218,46 +1110,30 @@ export class ControllerCommandHandler {
|
|
|
1218
1110
|
|
|
1219
1111
|
logger.info("Setting ACL entries", aclEntries);
|
|
1220
1112
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
StatusResponseError.accept(error);
|
|
1235
|
-
return [
|
|
1236
|
-
{
|
|
1237
|
-
path: {
|
|
1238
|
-
endpoint_id: 0,
|
|
1239
|
-
cluster_id: AccessControl.Cluster.id,
|
|
1240
|
-
attribute_id: 0,
|
|
1241
|
-
},
|
|
1242
|
-
status: error.code,
|
|
1243
|
-
},
|
|
1244
|
-
];
|
|
1245
|
-
}
|
|
1113
|
+
const { status } = await this.#writeAttribute(
|
|
1114
|
+
nodeId,
|
|
1115
|
+
EndpointNumber(0),
|
|
1116
|
+
AccessControl.Cluster.id,
|
|
1117
|
+
"acl",
|
|
1118
|
+
aclEntries,
|
|
1119
|
+
);
|
|
1120
|
+
return [
|
|
1121
|
+
{
|
|
1122
|
+
path: { endpoint_id: 0, cluster_id: AccessControl.Cluster.id, attribute_id: 0 },
|
|
1123
|
+
status,
|
|
1124
|
+
},
|
|
1125
|
+
];
|
|
1246
1126
|
}
|
|
1247
1127
|
|
|
1248
1128
|
/**
|
|
1249
1129
|
* Set bindings on a specific endpoint of a node.
|
|
1250
1130
|
* Writes to the Binding attribute on the Binding cluster.
|
|
1251
|
-
* TODO Migrate to new Node API
|
|
1252
1131
|
*/
|
|
1253
1132
|
async setNodeBinding(
|
|
1254
1133
|
nodeId: NodeId,
|
|
1255
1134
|
endpointId: EndpointNumber,
|
|
1256
1135
|
bindings: BindingTarget[],
|
|
1257
1136
|
): Promise<AttributeWriteResult[] | null> {
|
|
1258
|
-
const client = this.#nodes.clusterClientFor(nodeId, endpointId, Binding.Cluster);
|
|
1259
|
-
|
|
1260
|
-
// Convert from WebSocket format to Matter.js format
|
|
1261
1137
|
const bindingEntries: Binding.Target[] = bindings.map(binding => ({
|
|
1262
1138
|
node: binding.node !== null ? NodeId(binding.node) : undefined,
|
|
1263
1139
|
group: binding.group !== null ? GroupId(binding.group) : undefined,
|
|
@@ -1268,31 +1144,19 @@ export class ControllerCommandHandler {
|
|
|
1268
1144
|
|
|
1269
1145
|
logger.info("Setting bindings on endpoint", endpointId, bindingEntries);
|
|
1270
1146
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
StatusResponseError.accept(error);
|
|
1285
|
-
return [
|
|
1286
|
-
{
|
|
1287
|
-
path: {
|
|
1288
|
-
endpoint_id: endpointId,
|
|
1289
|
-
cluster_id: Binding.Cluster.id,
|
|
1290
|
-
attribute_id: 0,
|
|
1291
|
-
},
|
|
1292
|
-
status: error.code,
|
|
1293
|
-
},
|
|
1294
|
-
];
|
|
1295
|
-
}
|
|
1147
|
+
const { status } = await this.#writeAttribute(
|
|
1148
|
+
nodeId,
|
|
1149
|
+
endpointId,
|
|
1150
|
+
Binding.Cluster.id,
|
|
1151
|
+
"binding",
|
|
1152
|
+
bindingEntries,
|
|
1153
|
+
);
|
|
1154
|
+
return [
|
|
1155
|
+
{
|
|
1156
|
+
path: { endpoint_id: endpointId, cluster_id: Binding.Cluster.id, attribute_id: 0 },
|
|
1157
|
+
status,
|
|
1158
|
+
},
|
|
1159
|
+
];
|
|
1296
1160
|
}
|
|
1297
1161
|
|
|
1298
1162
|
/**
|
package/src/controller/Nodes.ts
CHANGED
|
@@ -5,20 +5,16 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { NodeId } from "@matter/main";
|
|
8
|
-
import { ClusterClientObj } from "@matter/main/protocol";
|
|
9
|
-
import { ClusterId, ClusterType, EndpointNumber } from "@matter/main/types";
|
|
10
|
-
import { InteractionClient } from "@project-chip/matter.js/cluster";
|
|
11
8
|
import { NodeStates, PairedNode } from "@project-chip/matter.js/device";
|
|
12
9
|
import { ServerError } from "../types/WebSocketMessageTypes.js";
|
|
13
10
|
import { AttributeDataCache } from "./AttributeDataCache.js";
|
|
14
11
|
|
|
15
12
|
/**
|
|
16
|
-
* Manages node storage and
|
|
13
|
+
* Manages node storage and tracks per-node availability.
|
|
17
14
|
*
|
|
18
15
|
* This class handles:
|
|
19
16
|
* - Storage of PairedNode instances
|
|
20
17
|
* - Node retrieval and existence checking
|
|
21
|
-
* - Access to interaction clients and cluster clients
|
|
22
18
|
* - Attribute data caching
|
|
23
19
|
* - Connection state tracking for availability debouncing
|
|
24
20
|
*/
|
|
@@ -156,47 +152,4 @@ export class Nodes {
|
|
|
156
152
|
isAvailable(nodeId: NodeId): boolean {
|
|
157
153
|
return this.#lastAvailability.get(nodeId) ?? false;
|
|
158
154
|
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Get the interaction client for a node.
|
|
162
|
-
*/
|
|
163
|
-
interactionClientFor(nodeId: NodeId): InteractionClient {
|
|
164
|
-
return this.get(nodeId).getInteractionClient();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Get a cluster client by cluster ID for a specific endpoint on a node.
|
|
169
|
-
* @throws Error if endpoint or cluster not found
|
|
170
|
-
* TODO: Migrate to new node API
|
|
171
|
-
*/
|
|
172
|
-
clusterClientByIdFor(nodeId: NodeId, endpointId: EndpointNumber, clusterId: ClusterId): ClusterClientObj<any> {
|
|
173
|
-
const node = this.get(nodeId);
|
|
174
|
-
|
|
175
|
-
const endpoint = endpointId === 0 ? node.getRootEndpoint() : node.getDeviceById(endpointId);
|
|
176
|
-
|
|
177
|
-
if (endpoint === undefined) {
|
|
178
|
-
throw ServerError.invalidArguments(`Endpoint ${endpointId} on node ${nodeId} not found`);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const client = endpoint.getClusterClientById(clusterId);
|
|
182
|
-
|
|
183
|
-
if (client === undefined) {
|
|
184
|
-
throw ServerError.invalidArguments(
|
|
185
|
-
`Cluster ${clusterId} on endpoint ${endpointId} on node ${nodeId} not found`,
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return client;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Get a typed cluster client for a specific endpoint on a node.
|
|
194
|
-
*/
|
|
195
|
-
clusterClientFor<const T extends ClusterType>(
|
|
196
|
-
nodeId: NodeId,
|
|
197
|
-
endpointId: EndpointNumber,
|
|
198
|
-
cluster: T,
|
|
199
|
-
): ClusterClientObj<T["Typing"]> {
|
|
200
|
-
return this.clusterClientByIdFor(nodeId, endpointId, cluster.id!) as ClusterClientObj<T["Typing"]>;
|
|
201
|
-
}
|
|
202
155
|
}
|