@matter/protocol 0.12.2-alpha.0-20250130-d012e3082 → 0.12.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.
Files changed (49) hide show
  1. package/dist/cjs/interaction/AccessControlManager.d.ts +8 -5
  2. package/dist/cjs/interaction/AccessControlManager.d.ts.map +1 -1
  3. package/dist/cjs/interaction/AccessControlManager.js +1 -1
  4. package/dist/cjs/interaction/AccessControlManager.js.map +1 -1
  5. package/dist/cjs/interaction/AttributeDataEncoder.d.ts +8 -1
  6. package/dist/cjs/interaction/AttributeDataEncoder.d.ts.map +1 -1
  7. package/dist/cjs/interaction/AttributeDataEncoder.js.map +1 -1
  8. package/dist/cjs/interaction/InteractionMessenger.d.ts +7 -4
  9. package/dist/cjs/interaction/InteractionMessenger.d.ts.map +1 -1
  10. package/dist/cjs/interaction/InteractionMessenger.js +33 -26
  11. package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
  12. package/dist/cjs/interaction/InteractionServer.d.ts +22 -6
  13. package/dist/cjs/interaction/InteractionServer.d.ts.map +1 -1
  14. package/dist/cjs/interaction/InteractionServer.js +178 -165
  15. package/dist/cjs/interaction/InteractionServer.js.map +1 -1
  16. package/dist/cjs/interaction/ServerSubscription.d.ts +13 -3
  17. package/dist/cjs/interaction/ServerSubscription.d.ts.map +1 -1
  18. package/dist/cjs/interaction/ServerSubscription.js +121 -91
  19. package/dist/cjs/interaction/ServerSubscription.js.map +2 -2
  20. package/dist/cjs/peer/ControllerCommissioningFlow.js +7 -6
  21. package/dist/cjs/peer/ControllerCommissioningFlow.js.map +1 -1
  22. package/dist/esm/interaction/AccessControlManager.d.ts +8 -5
  23. package/dist/esm/interaction/AccessControlManager.d.ts.map +1 -1
  24. package/dist/esm/interaction/AccessControlManager.js +8 -2
  25. package/dist/esm/interaction/AccessControlManager.js.map +1 -1
  26. package/dist/esm/interaction/AttributeDataEncoder.d.ts +8 -1
  27. package/dist/esm/interaction/AttributeDataEncoder.d.ts.map +1 -1
  28. package/dist/esm/interaction/AttributeDataEncoder.js.map +1 -1
  29. package/dist/esm/interaction/InteractionMessenger.d.ts +7 -4
  30. package/dist/esm/interaction/InteractionMessenger.d.ts.map +1 -1
  31. package/dist/esm/interaction/InteractionMessenger.js +41 -27
  32. package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
  33. package/dist/esm/interaction/InteractionServer.d.ts +22 -6
  34. package/dist/esm/interaction/InteractionServer.d.ts.map +1 -1
  35. package/dist/esm/interaction/InteractionServer.js +178 -165
  36. package/dist/esm/interaction/InteractionServer.js.map +1 -1
  37. package/dist/esm/interaction/ServerSubscription.d.ts +13 -3
  38. package/dist/esm/interaction/ServerSubscription.d.ts.map +1 -1
  39. package/dist/esm/interaction/ServerSubscription.js +121 -91
  40. package/dist/esm/interaction/ServerSubscription.js.map +2 -2
  41. package/dist/esm/peer/ControllerCommissioningFlow.js +7 -6
  42. package/dist/esm/peer/ControllerCommissioningFlow.js.map +1 -1
  43. package/package.json +6 -6
  44. package/src/interaction/AccessControlManager.ts +20 -8
  45. package/src/interaction/AttributeDataEncoder.ts +11 -1
  46. package/src/interaction/InteractionMessenger.ts +65 -33
  47. package/src/interaction/InteractionServer.ts +224 -188
  48. package/src/interaction/ServerSubscription.ts +155 -108
  49. package/src/peer/ControllerCommissioningFlow.ts +7 -7
@@ -22,6 +22,7 @@ import { PeerAddress } from "#peer/PeerAddress.js";
22
22
  import type { MessageExchange } from "#protocol/MessageExchange.js";
23
23
  import { SecureSession } from "#session/SecureSession.js";
24
24
  import {
25
+ EndpointNumber,
25
26
  EventNumber,
26
27
  INTERACTION_PROTOCOL_ID,
27
28
  StatusCode,
@@ -37,7 +38,7 @@ import {
37
38
  import { AnyAttributeServer, FabricScopedAttributeServer } from "../cluster/server/AttributeServer.js";
38
39
  import { AnyEventServer, FabricSensitiveEventServer } from "../cluster/server/EventServer.js";
39
40
  import { NoChannelError } from "../protocol/ChannelManager.js";
40
- import { AttributeReportPayload, EventReportPayload } from "./AttributeDataEncoder.js";
41
+ import { EventReportPayload } from "./AttributeDataEncoder.js";
41
42
  import { InteractionEndpointStructure } from "./InteractionEndpointStructure.js";
42
43
  import { InteractionServerMessenger } from "./InteractionMessenger.js";
43
44
  import {
@@ -133,7 +134,11 @@ export interface ServerSubscriptionContext {
133
134
  path: AttributePath,
134
135
  attribute: AnyAttributeServer<unknown>,
135
136
  offline?: boolean,
136
- ): Promise<{ version: number; value: unknown }>;
137
+ ): { version: number; value: unknown };
138
+ readEndpointAttributesForSubscription(
139
+ endpointId: EndpointNumber,
140
+ attributes: { path: AttributePath; attribute: AnyAttributeServer<unknown>; offline?: boolean }[],
141
+ ): { path: AttributePath; attribute: AnyAttributeServer<unknown>; version: number; value: unknown }[];
137
142
  readEvent(
138
143
  path: EventPath,
139
144
  event: AnyEventServer<any, any>,
@@ -425,7 +430,7 @@ export class ServerSubscription extends Subscription {
425
430
  const { newAttributes } = this.registerNewAttributes();
426
431
 
427
432
  for (const { path, attribute } of newAttributes) {
428
- const { version, value } = await this.#context.readAttribute(path, attribute);
433
+ const { version, value } = this.#context.readAttribute(path, attribute);
429
434
 
430
435
  // We do not do any version filtering for attributes that are newly added to make sure controller gets
431
436
  // most current state
@@ -639,69 +644,7 @@ export class ServerSubscription extends Subscription {
639
644
  }
640
645
  }
641
646
 
642
- async sendInitialReport(messenger: InteractionServerMessenger) {
643
- this.#updateTimer.stop();
644
-
645
- const { newAttributes, attributeErrors } = this.registerNewAttributes();
646
-
647
- const dataVersionFilterMap = new Map<string, number>(
648
- this.criteria.dataVersionFilters?.map(({ path, dataVersion }) => [clusterPathToId(path), dataVersion]) ??
649
- [],
650
- );
651
-
652
- let attributesFilteredWithVersion = false;
653
- const attributes = new Array<{
654
- path: TypeFromSchema<typeof TlvAttributePath>;
655
- value: any;
656
- version: number;
657
- schema: TlvSchema<any>;
658
- attribute: AnyAttributeServer<any>;
659
- }>();
660
- for (const { path, attribute } of newAttributes) {
661
- try {
662
- const { value, version } = await this.#context.readAttribute(path, attribute);
663
- if (value === undefined) continue;
664
-
665
- const { nodeId, endpointId, clusterId } = path;
666
-
667
- const versionFilterValue =
668
- endpointId !== undefined && clusterId !== undefined
669
- ? dataVersionFilterMap.get(clusterPathToId({ nodeId, endpointId, clusterId }))
670
- : undefined;
671
- if (versionFilterValue !== undefined && versionFilterValue === version) {
672
- attributesFilteredWithVersion = true;
673
- continue;
674
- }
675
-
676
- attributes.push({ path, value, version, schema: attribute.schema, attribute });
677
- } catch (error) {
678
- if (StatusResponseError.is(error, StatusCode.UnsupportedAccess)) {
679
- logger.warn(`Permission denied reading attribute ${this.#structure.resolveAttributeName(path)}`);
680
- } else {
681
- logger.warn(`Error reading attribute ${this.#structure.resolveAttributeName(path)}:`, error);
682
- }
683
- }
684
- }
685
- const attributeReportsPayload: AttributeReportPayload[] = attributes.map(
686
- ({ path, schema, value, version, attribute }) => ({
687
- hasFabricSensitiveData: attribute.hasFabricSensitiveData,
688
- attributeData: {
689
- path,
690
- dataVersion: version,
691
- payload: value,
692
- schema,
693
- },
694
- }),
695
- );
696
- attributeErrors.forEach(attributeStatus =>
697
- attributeReportsPayload.push({
698
- hasFabricSensitiveData: false,
699
- attributeStatus,
700
- }),
701
- );
702
-
703
- const { newEvents, eventErrors } = this.#registerNewEvents();
704
-
647
+ async #collectInitialEventReportPayloads(newEvents: EventWithPath[]) {
705
648
  let eventsFiltered = false;
706
649
  const eventReportsPayload = new Array<EventReportPayload>();
707
650
  for (const { path, event } of newEvents) {
@@ -745,8 +688,91 @@ export class ServerSubscription extends Subscription {
745
688
  }
746
689
  });
747
690
 
691
+ return { eventReportsPayload, eventsFiltered };
692
+ }
693
+
694
+ /**
695
+ * Returns an iterator that yields the initial subscription data to be sent to the controller.
696
+ * The iterator will yield all attributes and events that match the subscription criteria.
697
+ * A thrown exception will cancel the sending process immediately.
698
+ * TODO: Streamline all this with the normal Read flow to also handle Concrete Path subscriptions with errors correctly
699
+ */
700
+ *#iterateInitialSubscriptionData(
701
+ attributesToSend: {
702
+ newAttributes: AttributeWithPath[];
703
+ attributeErrors: TypeFromSchema<typeof TlvAttributeStatus>[];
704
+ },
705
+ eventsToSend: {
706
+ eventReportsPayload: EventReportPayload[];
707
+ eventsFiltered: boolean;
708
+ eventErrors: TypeFromSchema<typeof TlvEventStatus>[];
709
+ },
710
+ ) {
711
+ const dataVersionFilterMap = new Map<string, number>(
712
+ this.criteria.dataVersionFilters?.map(({ path, dataVersion }) => [clusterPathToId(path), dataVersion]) ??
713
+ [],
714
+ );
715
+
716
+ const { newAttributes, attributeErrors } = attributesToSend;
717
+ const { eventReportsPayload, eventsFiltered, eventErrors } = eventsToSend;
718
+
719
+ logger.debug(
720
+ `Initializes Subscription with ${newAttributes.length} attributes and ${eventReportsPayload.length} events.`,
721
+ );
722
+
723
+ let attributesFilteredWithVersion = false;
724
+
725
+ const attributesPerCluster = new Map<EndpointNumber, AttributeWithPath[]>();
726
+ for (const { path, attribute } of newAttributes) {
727
+ const { endpointId } = path;
728
+ const endpointAttributes = attributesPerCluster.get(endpointId) ?? new Array<AttributeWithPath>();
729
+ endpointAttributes.push({ path, attribute });
730
+ attributesPerCluster.set(endpointId, endpointAttributes);
731
+ }
732
+
733
+ let attributesCounter = 0;
734
+ for (const endpointId of attributesPerCluster.keys()) {
735
+ const endpointAttributes = attributesPerCluster.get(endpointId)!;
736
+ attributesPerCluster.delete(endpointId);
737
+ for (const { path, attribute, value, version } of this.#context.readEndpointAttributesForSubscription(
738
+ endpointId,
739
+ endpointAttributes,
740
+ )) {
741
+ if (value === undefined) continue;
742
+
743
+ const { nodeId, endpointId, clusterId } = path;
744
+
745
+ const versionFilterValue =
746
+ endpointId !== undefined && clusterId !== undefined
747
+ ? dataVersionFilterMap.get(clusterPathToId({ nodeId, endpointId, clusterId }))
748
+ : undefined;
749
+ if (versionFilterValue !== undefined && versionFilterValue === version) {
750
+ attributesFilteredWithVersion = true;
751
+ continue;
752
+ }
753
+
754
+ attributesCounter++;
755
+ yield {
756
+ hasFabricSensitiveData: attribute.hasFabricSensitiveData,
757
+ attributeData: {
758
+ path,
759
+ dataVersion: version,
760
+ payload: value,
761
+ schema: attribute.schema,
762
+ },
763
+ };
764
+ }
765
+ }
766
+
767
+ for (const attributeStatus of attributeErrors) {
768
+ yield {
769
+ hasFabricSensitiveData: false,
770
+ attributeStatus,
771
+ };
772
+ }
773
+
748
774
  if (
749
- attributes.length === 0 &&
775
+ attributesCounter === 0 &&
750
776
  !attributesFilteredWithVersion &&
751
777
  eventReportsPayload.length === 0 &&
752
778
  !eventsFiltered
@@ -757,27 +783,38 @@ export class ServerSubscription extends Subscription {
757
783
  );
758
784
  }
759
785
 
760
- eventErrors.forEach(eventStatus =>
761
- eventReportsPayload.push({
786
+ for (const eventReport of eventReportsPayload) {
787
+ yield eventReport;
788
+ }
789
+
790
+ for (const eventStatus of eventErrors) {
791
+ yield {
762
792
  hasFabricSensitiveData: false,
763
793
  eventStatus,
764
- }),
765
- );
794
+ };
795
+ }
766
796
 
767
- logger.debug(
768
- `Initialize Subscription with ${attributes.length} attributes and ${eventReportsPayload.length} events.`,
769
- );
770
797
  this.#lastUpdateTimeMs = Time.nowMs();
798
+ }
799
+
800
+ async sendInitialReport(messenger: InteractionServerMessenger) {
801
+ this.#updateTimer.stop();
802
+
803
+ const { newAttributes, attributeErrors } = this.registerNewAttributes();
804
+ const { newEvents, eventErrors } = this.#registerNewEvents();
805
+ const { eventReportsPayload, eventsFiltered } = await this.#collectInitialEventReportPayloads(newEvents);
771
806
 
772
807
  await messenger.sendDataReport(
773
808
  {
774
809
  suppressResponse: false, // we always need proper response for initial report
775
810
  subscriptionId: this.id,
776
811
  interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
777
- attributeReportsPayload,
778
- eventReportsPayload,
779
812
  },
780
813
  this.criteria.isFabricFiltered,
814
+ this.#iterateInitialSubscriptionData(
815
+ { newAttributes, attributeErrors },
816
+ { eventReportsPayload, eventsFiltered, eventErrors },
817
+ ),
781
818
  );
782
819
  }
783
820
 
@@ -805,16 +842,15 @@ export class ServerSubscription extends Subscription {
805
842
  // We cannot be sure what value we got for fabric filtered attributes (and from which fabric),
806
843
  // so get it again for this relevant fabric. This also makes sure that fabric sensitive fields are filtered
807
844
  // TODO: Remove this once we remove the legacy API and go away from using AttributeServers in the background
808
- return this.#context.readAttribute(path, attribute, true).then(({ value }) => {
809
- this.#outstandingAttributeUpdates.set(attributePathToId(path), {
810
- attribute,
811
- path,
812
- schema,
813
- version,
814
- value,
815
- });
816
- this.#prepareDataUpdate();
845
+ const { value } = this.#context.readAttribute(path, attribute, true);
846
+ this.#outstandingAttributeUpdates.set(attributePathToId(path), {
847
+ attribute,
848
+ path,
849
+ schema,
850
+ version,
851
+ value,
817
852
  });
853
+ this.#prepareDataUpdate();
818
854
  }
819
855
  this.#outstandingAttributeUpdates.set(attributePathToId(path), { attribute, path, schema, version, value });
820
856
  this.#prepareDataUpdate();
@@ -878,6 +914,38 @@ export class ServerSubscription extends Subscription {
878
914
  await super.close();
879
915
  }
880
916
 
917
+ /**
918
+ * Iterates over all attributes and events that have changed since the last update and sends them to
919
+ * the controller.
920
+ * A thrown exception will cancel the sending process immediately.
921
+ */
922
+ *#iterateDataUpdate(attributes: AttributePathWithValueVersion<any>[], events: EventPathWithEventData<any>[]) {
923
+ for (const {
924
+ path,
925
+ schema,
926
+ value: payload,
927
+ version: dataVersion,
928
+ attribute: { hasFabricSensitiveData },
929
+ } of attributes) {
930
+ yield {
931
+ hasFabricSensitiveData,
932
+ attributeData: { path, dataVersion, schema, payload },
933
+ };
934
+ }
935
+
936
+ for (const {
937
+ path,
938
+ schema,
939
+ event,
940
+ data: { number: eventNumber, priority, epochTimestamp, payload },
941
+ } of events) {
942
+ yield {
943
+ hasFabricSensitiveData: event.hasFabricSensitiveData,
944
+ eventData: { path, eventNumber, priority, epochTimestamp, schema, payload },
945
+ };
946
+ }
947
+ }
948
+
881
949
  private async sendUpdateMessage(
882
950
  attributes: AttributePathWithValueVersion<any>[],
883
951
  events: EventPathWithEventData<any>[],
@@ -905,6 +973,7 @@ export class ServerSubscription extends Subscription {
905
973
  interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
906
974
  },
907
975
  this.criteria.isFabricFiltered,
976
+ undefined,
908
977
  !this.isClosed, // Do not wait for ack when closed
909
978
  );
910
979
  } else {
@@ -913,31 +982,9 @@ export class ServerSubscription extends Subscription {
913
982
  suppressResponse: false, // Non-empty data reports always need to send response
914
983
  subscriptionId: this.id,
915
984
  interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
916
- attributeReportsPayload: attributes.map(({ path, schema, value, version, attribute }) => ({
917
- hasFabricSensitiveData: attribute.hasFabricSensitiveData,
918
- attributeData: {
919
- path,
920
- dataVersion: version,
921
- schema,
922
- payload: value,
923
- },
924
- })),
925
- eventReportsPayload: events.map(({ path, schema, event, data }) => {
926
- const { number, priority, epochTimestamp, payload } = data;
927
- return {
928
- hasFabricSensitiveData: event.hasFabricSensitiveData,
929
- eventData: {
930
- path,
931
- eventNumber: number,
932
- priority,
933
- epochTimestamp,
934
- schema,
935
- payload,
936
- },
937
- };
938
- }),
939
985
  },
940
986
  this.criteria.isFabricFiltered,
987
+ this.#iterateDataUpdate(attributes, events),
941
988
  !this.isClosed, // Do not wait for ack when closed
942
989
  );
943
990
  }
@@ -350,13 +350,6 @@ export class ControllerCommissioningFlow {
350
350
  stepLogic: () => this.#certificates(),
351
351
  });
352
352
 
353
- this.#commissioningSteps.push({
354
- stepNumber: 9,
355
- subStepNumber: 2,
356
- name: "OperationalCredentials.UpdateFabricLabel",
357
- stepLogic: () => this.#updateFabricLabel(),
358
- });
359
-
360
353
  // TODO Step 10: TimeSynchronization.SetTrustedTimeSource if supported
361
354
 
362
355
  this.#commissioningSteps.push({
@@ -412,6 +405,13 @@ export class ControllerCommissioningFlow {
412
405
  name: "GeneralCommissioning.Complete",
413
406
  stepLogic: () => this.#completeCommissioning(),
414
407
  });
408
+
409
+ this.#commissioningSteps.push({
410
+ stepNumber: 17, // Should be allowed in Step 9, but Tasmota is not supporting this
411
+ subStepNumber: 1,
412
+ name: "OperationalCredentials.UpdateFabricLabel",
413
+ stepLogic: () => this.#updateFabricLabel(),
414
+ });
415
415
  }
416
416
 
417
417
  #sortSteps() {