@matter/node 0.16.0-alpha.0-20251101-70c8d51d7 → 0.16.0-alpha.0-20251103-b47ffa15b

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 (159) hide show
  1. package/dist/cjs/behavior/Behavior.d.ts +2 -2
  2. package/dist/cjs/behavior/Behavior.d.ts.map +1 -1
  3. package/dist/cjs/behavior/Behavior.js +1 -1
  4. package/dist/cjs/behavior/Behavior.js.map +1 -1
  5. package/dist/cjs/behavior/Events.js.map +1 -1
  6. package/dist/cjs/behavior/cluster/ClusterBehavior.d.ts +3 -2
  7. package/dist/cjs/behavior/cluster/ClusterBehavior.d.ts.map +1 -1
  8. package/dist/cjs/behavior/cluster/ClusterBehavior.js.map +1 -1
  9. package/dist/cjs/behavior/cluster/ClusterBehaviorUtil.js +1 -1
  10. package/dist/cjs/behavior/cluster/ClusterBehaviorUtil.js.map +1 -1
  11. package/dist/cjs/behavior/internal/BehaviorBacking.js +1 -1
  12. package/dist/cjs/behavior/internal/BehaviorBacking.js.map +1 -1
  13. package/dist/cjs/behavior/system/mqtt/MqttInterface.js +1 -1
  14. package/dist/cjs/behavior/system/mqtt/MqttInterface.js.map +1 -1
  15. package/dist/cjs/behaviors/basic-information/BasicInformationServer.d.ts +2 -2
  16. package/dist/cjs/behaviors/basic-information/BasicInformationServer.d.ts.map +1 -1
  17. package/dist/cjs/behaviors/basic-information/BasicInformationServer.js +0 -3
  18. package/dist/cjs/behaviors/basic-information/BasicInformationServer.js.map +1 -1
  19. package/dist/cjs/behaviors/bridged-device-basic-information/BridgedDeviceBasicInformationServer.d.ts +1 -1
  20. package/dist/cjs/behaviors/bridged-device-basic-information/BridgedDeviceBasicInformationServer.d.ts.map +1 -1
  21. package/dist/cjs/behaviors/color-control/ColorControlServer.d.ts +3 -3
  22. package/dist/cjs/behaviors/color-control/ColorControlServer.d.ts.map +1 -1
  23. package/dist/cjs/behaviors/color-control/ColorControlServer.js +118 -3
  24. package/dist/cjs/behaviors/color-control/ColorControlServer.js.map +1 -1
  25. package/dist/cjs/behaviors/general-diagnostics/GeneralDiagnosticsServer.d.ts +1 -1
  26. package/dist/cjs/behaviors/general-diagnostics/GeneralDiagnosticsServer.d.ts.map +1 -1
  27. package/dist/cjs/behaviors/general-diagnostics/GeneralDiagnosticsServer.js.map +1 -1
  28. package/dist/cjs/behaviors/group-key-management/GroupKeyManagementServer.d.ts +1 -1
  29. package/dist/cjs/behaviors/group-key-management/GroupKeyManagementServer.d.ts.map +1 -1
  30. package/dist/cjs/behaviors/group-key-management/GroupKeyManagementServer.js.map +1 -1
  31. package/dist/cjs/behaviors/groups/GroupsServer.d.ts.map +1 -1
  32. package/dist/cjs/behaviors/groups/GroupsServer.js +9 -1
  33. package/dist/cjs/behaviors/groups/GroupsServer.js.map +1 -1
  34. package/dist/cjs/behaviors/level-control/LevelControlServer.d.ts +5 -5
  35. package/dist/cjs/behaviors/level-control/LevelControlServer.d.ts.map +1 -1
  36. package/dist/cjs/behaviors/level-control/LevelControlServer.js +54 -27
  37. package/dist/cjs/behaviors/level-control/LevelControlServer.js.map +1 -1
  38. package/dist/cjs/behaviors/on-off/OnOffServer.d.ts +3 -6
  39. package/dist/cjs/behaviors/on-off/OnOffServer.d.ts.map +1 -1
  40. package/dist/cjs/behaviors/on-off/OnOffServer.js +59 -7
  41. package/dist/cjs/behaviors/on-off/OnOffServer.js.map +1 -1
  42. package/dist/cjs/behaviors/scenes-management/ScenesManagementServer.d.ts +122 -3
  43. package/dist/cjs/behaviors/scenes-management/ScenesManagementServer.d.ts.map +1 -1
  44. package/dist/cjs/behaviors/scenes-management/ScenesManagementServer.js +909 -1
  45. package/dist/cjs/behaviors/scenes-management/ScenesManagementServer.js.map +2 -2
  46. package/dist/cjs/behaviors/switch/SwitchServer.d.ts +1 -1
  47. package/dist/cjs/behaviors/switch/SwitchServer.d.ts.map +1 -1
  48. package/dist/cjs/behaviors/switch/SwitchServer.js.map +1 -1
  49. package/dist/cjs/behaviors/thermostat/AtomicWriteHandler.js +2 -2
  50. package/dist/cjs/behaviors/thermostat/AtomicWriteHandler.js.map +1 -1
  51. package/dist/cjs/behaviors/thermostat/ThermostatServer.d.ts +1 -1
  52. package/dist/cjs/behaviors/thermostat/ThermostatServer.d.ts.map +1 -1
  53. package/dist/cjs/behaviors/thermostat/ThermostatServer.js.map +1 -1
  54. package/dist/cjs/devices/color-temperature-light.d.ts +4 -4
  55. package/dist/cjs/devices/dimmable-light.d.ts +4 -4
  56. package/dist/cjs/devices/dimmable-plug-in-unit.d.ts +4 -4
  57. package/dist/cjs/devices/extended-color-light.d.ts +4 -4
  58. package/dist/cjs/devices/mounted-dimmable-load-control.d.ts +4 -4
  59. package/dist/cjs/devices/mounted-on-off-control.d.ts +4 -4
  60. package/dist/cjs/devices/on-off-light.d.ts +4 -4
  61. package/dist/cjs/devices/on-off-plug-in-unit.d.ts +4 -4
  62. package/dist/cjs/endpoint/Agent.d.ts +5 -0
  63. package/dist/cjs/endpoint/Agent.d.ts.map +1 -1
  64. package/dist/cjs/endpoint/Agent.js +9 -0
  65. package/dist/cjs/endpoint/Agent.js.map +1 -1
  66. package/dist/cjs/endpoint/properties/Behaviors.js +1 -1
  67. package/dist/cjs/endpoint/properties/Behaviors.js.map +1 -1
  68. package/dist/cjs/node/integration/ProtocolService.js +1 -1
  69. package/dist/cjs/node/integration/ProtocolService.js.map +1 -1
  70. package/dist/esm/behavior/Behavior.d.ts +2 -2
  71. package/dist/esm/behavior/Behavior.d.ts.map +1 -1
  72. package/dist/esm/behavior/Behavior.js +1 -1
  73. package/dist/esm/behavior/Behavior.js.map +1 -1
  74. package/dist/esm/behavior/Events.js.map +1 -1
  75. package/dist/esm/behavior/cluster/ClusterBehavior.d.ts +3 -2
  76. package/dist/esm/behavior/cluster/ClusterBehavior.d.ts.map +1 -1
  77. package/dist/esm/behavior/cluster/ClusterBehavior.js.map +1 -1
  78. package/dist/esm/behavior/cluster/ClusterBehaviorUtil.js +1 -1
  79. package/dist/esm/behavior/cluster/ClusterBehaviorUtil.js.map +1 -1
  80. package/dist/esm/behavior/internal/BehaviorBacking.js +1 -1
  81. package/dist/esm/behavior/internal/BehaviorBacking.js.map +1 -1
  82. package/dist/esm/behavior/system/mqtt/MqttInterface.js +1 -1
  83. package/dist/esm/behavior/system/mqtt/MqttInterface.js.map +1 -1
  84. package/dist/esm/behaviors/basic-information/BasicInformationServer.d.ts +2 -2
  85. package/dist/esm/behaviors/basic-information/BasicInformationServer.d.ts.map +1 -1
  86. package/dist/esm/behaviors/basic-information/BasicInformationServer.js +1 -4
  87. package/dist/esm/behaviors/basic-information/BasicInformationServer.js.map +1 -1
  88. package/dist/esm/behaviors/bridged-device-basic-information/BridgedDeviceBasicInformationServer.d.ts +1 -1
  89. package/dist/esm/behaviors/bridged-device-basic-information/BridgedDeviceBasicInformationServer.d.ts.map +1 -1
  90. package/dist/esm/behaviors/color-control/ColorControlServer.d.ts +3 -3
  91. package/dist/esm/behaviors/color-control/ColorControlServer.d.ts.map +1 -1
  92. package/dist/esm/behaviors/color-control/ColorControlServer.js +118 -3
  93. package/dist/esm/behaviors/color-control/ColorControlServer.js.map +1 -1
  94. package/dist/esm/behaviors/general-diagnostics/GeneralDiagnosticsServer.d.ts +1 -1
  95. package/dist/esm/behaviors/general-diagnostics/GeneralDiagnosticsServer.d.ts.map +1 -1
  96. package/dist/esm/behaviors/general-diagnostics/GeneralDiagnosticsServer.js.map +1 -1
  97. package/dist/esm/behaviors/group-key-management/GroupKeyManagementServer.d.ts +1 -1
  98. package/dist/esm/behaviors/group-key-management/GroupKeyManagementServer.d.ts.map +1 -1
  99. package/dist/esm/behaviors/group-key-management/GroupKeyManagementServer.js.map +1 -1
  100. package/dist/esm/behaviors/groups/GroupsServer.d.ts.map +1 -1
  101. package/dist/esm/behaviors/groups/GroupsServer.js +9 -1
  102. package/dist/esm/behaviors/groups/GroupsServer.js.map +1 -1
  103. package/dist/esm/behaviors/level-control/LevelControlServer.d.ts +5 -5
  104. package/dist/esm/behaviors/level-control/LevelControlServer.d.ts.map +1 -1
  105. package/dist/esm/behaviors/level-control/LevelControlServer.js +55 -28
  106. package/dist/esm/behaviors/level-control/LevelControlServer.js.map +1 -1
  107. package/dist/esm/behaviors/on-off/OnOffServer.d.ts +3 -6
  108. package/dist/esm/behaviors/on-off/OnOffServer.d.ts.map +1 -1
  109. package/dist/esm/behaviors/on-off/OnOffServer.js +59 -7
  110. package/dist/esm/behaviors/on-off/OnOffServer.js.map +1 -1
  111. package/dist/esm/behaviors/scenes-management/ScenesManagementServer.d.ts +122 -3
  112. package/dist/esm/behaviors/scenes-management/ScenesManagementServer.d.ts.map +1 -1
  113. package/dist/esm/behaviors/scenes-management/ScenesManagementServer.js +960 -1
  114. package/dist/esm/behaviors/scenes-management/ScenesManagementServer.js.map +2 -2
  115. package/dist/esm/behaviors/switch/SwitchServer.d.ts +1 -1
  116. package/dist/esm/behaviors/switch/SwitchServer.d.ts.map +1 -1
  117. package/dist/esm/behaviors/switch/SwitchServer.js.map +1 -1
  118. package/dist/esm/behaviors/thermostat/AtomicWriteHandler.js +2 -2
  119. package/dist/esm/behaviors/thermostat/AtomicWriteHandler.js.map +1 -1
  120. package/dist/esm/behaviors/thermostat/ThermostatServer.d.ts +1 -1
  121. package/dist/esm/behaviors/thermostat/ThermostatServer.d.ts.map +1 -1
  122. package/dist/esm/behaviors/thermostat/ThermostatServer.js.map +1 -1
  123. package/dist/esm/devices/color-temperature-light.d.ts +4 -4
  124. package/dist/esm/devices/dimmable-light.d.ts +4 -4
  125. package/dist/esm/devices/dimmable-plug-in-unit.d.ts +4 -4
  126. package/dist/esm/devices/extended-color-light.d.ts +4 -4
  127. package/dist/esm/devices/mounted-dimmable-load-control.d.ts +4 -4
  128. package/dist/esm/devices/mounted-on-off-control.d.ts +4 -4
  129. package/dist/esm/devices/on-off-light.d.ts +4 -4
  130. package/dist/esm/devices/on-off-plug-in-unit.d.ts +4 -4
  131. package/dist/esm/endpoint/Agent.d.ts +5 -0
  132. package/dist/esm/endpoint/Agent.d.ts.map +1 -1
  133. package/dist/esm/endpoint/Agent.js +9 -0
  134. package/dist/esm/endpoint/Agent.js.map +1 -1
  135. package/dist/esm/endpoint/properties/Behaviors.js +1 -1
  136. package/dist/esm/endpoint/properties/Behaviors.js.map +1 -1
  137. package/dist/esm/node/integration/ProtocolService.js +1 -1
  138. package/dist/esm/node/integration/ProtocolService.js.map +1 -1
  139. package/package.json +7 -7
  140. package/src/behavior/Behavior.ts +2 -2
  141. package/src/behavior/Events.ts +1 -1
  142. package/src/behavior/cluster/ClusterBehavior.ts +4 -2
  143. package/src/behavior/cluster/ClusterBehaviorUtil.ts +1 -1
  144. package/src/behavior/internal/BehaviorBacking.ts +1 -1
  145. package/src/behavior/system/mqtt/MqttInterface.ts +1 -1
  146. package/src/behaviors/basic-information/BasicInformationServer.ts +3 -7
  147. package/src/behaviors/color-control/ColorControlServer.ts +132 -3
  148. package/src/behaviors/general-diagnostics/GeneralDiagnosticsServer.ts +1 -1
  149. package/src/behaviors/group-key-management/GroupKeyManagementServer.ts +2 -2
  150. package/src/behaviors/groups/GroupsServer.ts +13 -1
  151. package/src/behaviors/level-control/LevelControlServer.ts +72 -29
  152. package/src/behaviors/on-off/OnOffServer.ts +78 -9
  153. package/src/behaviors/scenes-management/ScenesManagementServer.ts +1123 -3
  154. package/src/behaviors/switch/SwitchServer.ts +1 -1
  155. package/src/behaviors/thermostat/AtomicWriteHandler.ts +4 -4
  156. package/src/behaviors/thermostat/ThermostatServer.ts +1 -1
  157. package/src/endpoint/Agent.ts +10 -0
  158. package/src/endpoint/properties/Behaviors.ts +1 -1
  159. package/src/node/integration/ProtocolService.ts +1 -1
@@ -4,11 +4,1131 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- /*** THIS FILE WILL BE REGENERATED IF YOU DO NOT REMOVE THIS MESSAGE ***/
8
-
7
+ import { Events } from "#behavior/Events.js";
8
+ import { ClusterBehavior } from "#behavior/cluster/ClusterBehavior.js";
9
+ import { ClusterEvents } from "#behavior/cluster/ClusterEvents.js";
10
+ import { ScenesManagement } from "#clusters/scenes-management";
11
+ import { BasicSet, camelize, deepCopy, InternalError, Logger, ObserverGroup, serialize } from "#general";
12
+ import {
13
+ AccessLevel,
14
+ any,
15
+ bool,
16
+ fabricIdx,
17
+ field,
18
+ groupId,
19
+ int16,
20
+ int32,
21
+ int40,
22
+ int48,
23
+ int56,
24
+ int64,
25
+ int8,
26
+ listOf,
27
+ mandatory,
28
+ map16,
29
+ map32,
30
+ map64,
31
+ map8,
32
+ nonvolatile,
33
+ string,
34
+ uint16,
35
+ uint24,
36
+ uint32,
37
+ uint40,
38
+ uint48,
39
+ uint56,
40
+ uint64,
41
+ uint8,
42
+ } from "#model";
43
+ import { assertRemoteActor, Fabric, FabricManager, GroupSession, Val } from "#protocol";
44
+ import {
45
+ AttributeId,
46
+ ClusterId,
47
+ Command,
48
+ FabricIndex,
49
+ GroupId,
50
+ NullableSchema,
51
+ OptionalCommand,
52
+ Status,
53
+ StatusResponse,
54
+ TlvArray,
55
+ TlvBitmap,
56
+ TlvField,
57
+ TlvGroupId,
58
+ TlvNumericSchema,
59
+ TlvObject,
60
+ TlvSchema,
61
+ TlvString,
62
+ TlvUInt32,
63
+ TlvUInt8,
64
+ ValidationOutOfBoundsError,
65
+ } from "#types";
66
+ import { MaybePromise } from "@matter/general";
9
67
  import { ScenesManagementBehavior } from "./ScenesManagementBehavior.js";
10
68
 
69
+ const logger = Logger.get("ScenesManagementServer");
70
+
71
+ /** Used in FabricSceneInfo to denote that it is unknown which scene is or was last active */
72
+ const UNDEFINED_SCENE_ID = 0xff;
73
+
74
+ /** Defines the Global Scene together with UNDEFINED_GROUP */
75
+ const GLOBAL_SCENE_ID = 0;
76
+
77
+ /** Defines the undefined group together with GLOBAL_SCENE_ID */
78
+ const UNDEFINED_GROUP = GroupId(0);
79
+
80
+ /** Internal meta information for sceneable attributes on the endpoint */
81
+ type AttributeDetails = {
82
+ id: AttributeId;
83
+ name: string;
84
+ schema: TlvSchema<any>;
85
+ type: string;
86
+ mappedType: AttributeValuePairDataFields;
87
+ nullable: boolean;
88
+ };
89
+
90
+ /** Enum for the allowed fields in AttributeValuePair */
91
+ const enum AttributeValuePairDataFields {
92
+ ValueUnsigned8 = "valueUnsigned8",
93
+ ValueSigned8 = "valueSigned8",
94
+ ValueUnsigned16 = "valueUnsigned16",
95
+ ValueSigned16 = "valueSigned16",
96
+ ValueUnsigned32 = "valueUnsigned32",
97
+ ValueSigned32 = "valueSigned32",
98
+ ValueUnsigned64 = "valueUnsigned64",
99
+ ValueSigned64 = "valueSigned64",
100
+ }
101
+
102
+ /** Mapping from Datatypes to the AttributeValuePair field to use and expect */
103
+ export const DataTypeToSceneAttributeDataMap: Record<string, AttributeValuePairDataFields | undefined> = {
104
+ [bool.name]: AttributeValuePairDataFields.ValueUnsigned8,
105
+ [map8.name]: AttributeValuePairDataFields.ValueUnsigned8,
106
+ [uint8.name]: AttributeValuePairDataFields.ValueUnsigned8,
107
+ [int8.name]: AttributeValuePairDataFields.ValueSigned8,
108
+ [uint16.name]: AttributeValuePairDataFields.ValueUnsigned16,
109
+ [map16.name]: AttributeValuePairDataFields.ValueUnsigned16,
110
+ [int16.name]: AttributeValuePairDataFields.ValueSigned16,
111
+ [uint24.name]: AttributeValuePairDataFields.ValueUnsigned32,
112
+ [uint32.name]: AttributeValuePairDataFields.ValueUnsigned32,
113
+ [map32.name]: AttributeValuePairDataFields.ValueUnsigned32,
114
+ [int32.name]: AttributeValuePairDataFields.ValueSigned32,
115
+ [uint40.name]: AttributeValuePairDataFields.ValueUnsigned64,
116
+ [uint48.name]: AttributeValuePairDataFields.ValueUnsigned64,
117
+ [uint56.name]: AttributeValuePairDataFields.ValueUnsigned64,
118
+ [uint64.name]: AttributeValuePairDataFields.ValueUnsigned64,
119
+ [map64.name]: AttributeValuePairDataFields.ValueUnsigned64,
120
+ [int40.name]: AttributeValuePairDataFields.ValueSigned64,
121
+ [int48.name]: AttributeValuePairDataFields.ValueSigned64,
122
+ [int56.name]: AttributeValuePairDataFields.ValueSigned64,
123
+ [int64.name]: AttributeValuePairDataFields.ValueSigned64,
124
+ };
125
+
126
+ /**
127
+ * Monkey patching Tlv Structure of some commands to prevent data validation of the sceneId, groupId, sceneName and
128
+ * transitionTime field to be handled as ConstraintError because we need to return errors as a special response.
129
+ * We do this to leave the model in fact for other validations and only apply the change for our Schema-aware Tlv parsing.
130
+ */
131
+ ScenesManagement.Cluster.commands = {
132
+ ...ScenesManagement.Cluster.commands,
133
+ addScene: Command(
134
+ 0x0,
135
+ TlvObject({
136
+ groupId: TlvField(0, TlvGroupId),
137
+ sceneId: TlvField(1, TlvUInt8),
138
+ transitionTime: TlvField(2, TlvUInt32),
139
+ sceneName: TlvField(3, TlvString),
140
+ extensionFieldSetStructs: TlvField(4, TlvArray(ScenesManagement.TlvExtensionFieldSet)),
141
+ }),
142
+ 0x0,
143
+ ScenesManagement.TlvAddSceneResponse,
144
+ { invokeAcl: AccessLevel.Manage },
145
+ ),
146
+ viewScene: Command(
147
+ 0x1,
148
+ TlvObject({
149
+ groupId: TlvField(0, TlvGroupId),
150
+ sceneId: TlvField(1, TlvUInt8),
151
+ }),
152
+ 0x1,
153
+ ScenesManagement.TlvViewSceneResponse,
154
+ ),
155
+ removeScene: Command(
156
+ 0x2,
157
+ TlvObject({
158
+ groupId: TlvField(0, TlvGroupId),
159
+ sceneId: TlvField(1, TlvUInt8),
160
+ }),
161
+ 0x2,
162
+ ScenesManagement.TlvRemoveSceneResponse,
163
+ { invokeAcl: AccessLevel.Manage },
164
+ ),
165
+ storeScene: Command(
166
+ 0x4,
167
+ TlvObject({
168
+ groupId: TlvField(0, TlvGroupId),
169
+ sceneId: TlvField(1, TlvUInt8),
170
+ }),
171
+ 0x4,
172
+ ScenesManagement.TlvStoreSceneResponse,
173
+ {
174
+ invokeAcl: AccessLevel.Manage,
175
+ },
176
+ ),
177
+ copyScene: OptionalCommand(
178
+ 0x40,
179
+ TlvObject({
180
+ mode: TlvField(0, TlvBitmap(TlvUInt8, ScenesManagement.CopyMode)),
181
+ groupIdentifierFrom: TlvField(1, TlvGroupId),
182
+ sceneIdentifierFrom: TlvField(2, TlvUInt8),
183
+ groupIdentifierTo: TlvField(3, TlvGroupId),
184
+ sceneIdentifierTo: TlvField(4, TlvUInt8),
185
+ }),
186
+ 0x40,
187
+ ScenesManagement.TlvCopySceneResponse,
188
+ {
189
+ invokeAcl: AccessLevel.Manage,
190
+ },
191
+ ),
192
+ };
193
+
194
+ // We enable group names by default
195
+ const ScenesManagementBase = ScenesManagementBehavior.with(ScenesManagement.Feature.SceneNames);
196
+
11
197
  /**
12
198
  * This is the default server implementation of {@link ScenesManagementBehavior}.
199
+ * We implement the full Scenes Management cluster as specified in the Matter Spec.
200
+ * The SceneName feature is enabled by default.
201
+ *
202
+ * When a scene is applied/recalled then the relevant clusters are informed via the "applySceneValues" event they need
203
+ * to implement. If they do not implement the scene is not applied for that cluster.
13
204
  */
14
- export class ScenesManagementServer extends ScenesManagementBehavior {}
205
+ export class ScenesManagementServer extends ScenesManagementBase {
206
+ declare state: ScenesManagementServer.State;
207
+ declare protected internal: ScenesManagementServer.Internal;
208
+
209
+ override initialize() {
210
+ if (!this.state.sceneTableSize) {
211
+ this.state.sceneTableSize = 128; // Let's use that as a meaningful max for now if not specified by the developer
212
+ }
213
+
214
+ const fabricManager = this.endpoint.env.get(FabricManager);
215
+
216
+ // Initialize fabric scene info field to match to the current state of the scene table
217
+ this.#initializeFabricSceneInfo(fabricManager);
218
+
219
+ // When a fabric git removed we need to check if the active scene is considered from that fabric
220
+ // Data cleanup happens automatically
221
+ this.reactTo(fabricManager.events.deleted, this.#handleDeleteFabric);
222
+ }
223
+
224
+ /**
225
+ * Handles removal of one group in a fabric.
226
+ * This method is called by the GroupsServer implementation and also internally by this cluster.
227
+ */
228
+ removeScenesForGroupOnFabric(fabricIndex: FabricIndex, groupId: GroupId) {
229
+ this.state.sceneTable = deepCopy(this.state.sceneTable).filter(
230
+ s => !(s.fabricIndex === fabricIndex && s.sceneGroupId === groupId),
231
+ );
232
+
233
+ // If the current active scene is on the removed group, invalidate it
234
+ if (this.internal.monitorSceneAttributesForFabric === fabricIndex) {
235
+ const fabricSceneInfo = this.#fabricSceneInfoForFabric(fabricIndex);
236
+ if (fabricSceneInfo !== undefined) {
237
+ if (fabricSceneInfo.currentGroup === groupId && fabricSceneInfo.sceneValid) {
238
+ fabricSceneInfo.sceneValid = false;
239
+ this.internal.monitorSceneAttributesForFabric = null;
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ /** Handles removal of all groups in a fabric. This method is called by the GroupsServer implementation. */
246
+ removeScenesForAllGroupsForFabric(fabricIndex: FabricIndex) {
247
+ this.state.sceneTable = deepCopy(this.state.sceneTable).filter(s => s.fabricIndex !== fabricIndex);
248
+ this.#invalidateFabricSceneInfoForFabric(fabricIndex);
249
+ }
250
+
251
+ /** Validates the groupId and sceneId parameters of scene commands and returns convenient data for further processing */
252
+ #assertSceneCommandParameter(groupIdToValidate: GroupId, sceneId?: number) {
253
+ assertRemoteActor(this.context);
254
+ const fabric = this.context.session.associatedFabric;
255
+ const fabricIndex = fabric.fabricIndex;
256
+ const isGroupSession = GroupSession.is(this.context.session);
257
+ let groupId: GroupId | undefined = undefined;
258
+ if (isGroupSession && groupIdToValidate === 0) {
259
+ // TODO check if the spec really mean it that way, in fact response will be ignored anyway
260
+ throw new StatusResponse.InvalidCommandError(`GroupId cannot be 0 in a Group Session`);
261
+ }
262
+ if (groupIdToValidate === 0 || this.#groupExistentInFabric(fabric, groupIdToValidate)) {
263
+ groupId = groupIdToValidate;
264
+ }
265
+ const existingSceneIndex =
266
+ groupId !== undefined && sceneId !== undefined ? this.#sceneIndexForId(fabricIndex, sceneId, groupId) : -1;
267
+
268
+ return {
269
+ fabric,
270
+ fabricIndex,
271
+ groupId,
272
+ existingSceneIndex,
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Adds or replaces a scene entry in the scene table.
278
+ * If existingSceneIndex is -1, a new entry is added, else replaces the existing entry at that index.
279
+ * It also checks if the scene is allowed to be added depending on the fabric scene capacity.
280
+ *
281
+ * @returns The AddSceneResponse compatible response of the action
282
+ */
283
+ #addOrReplaceSceneEntry(sceneData: ScenesManagementServer.ScenesTableEntry, existingSceneIndex = -1) {
284
+ const { fabricIndex, sceneGroupId: groupId, sceneId } = sceneData;
285
+ if (existingSceneIndex === -1) {
286
+ if (this.#scenesForFabric(fabricIndex).length >= this.#fabricSceneCapacity) {
287
+ return { status: Status.ResourceExhausted, groupId, sceneId };
288
+ }
289
+ this.state.sceneTable.push(sceneData);
290
+ logger.debug(`Added scene ${sceneId} in group ${groupId} for fabric ${fabricIndex}`);
291
+
292
+ this.#updateFabricSceneInfoCountsForFabric(fabricIndex);
293
+ } else {
294
+ // Scene already exists, we will overwrite it
295
+ this.state.sceneTable[existingSceneIndex] = sceneData;
296
+ logger.debug(`Updated scene ${sceneId} in group ${groupId} for fabric ${fabricIndex}`);
297
+ }
298
+
299
+ return { status: Status.Success, groupId, sceneId };
300
+ }
301
+
302
+ /** Implements the AddScene command */
303
+ override addScene({
304
+ groupId: reqGroupId,
305
+ sceneId,
306
+ sceneName,
307
+ transitionTime,
308
+ extensionFieldSetStructs,
309
+ }: ScenesManagement.AddSceneRequest): ScenesManagement.AddSceneResponse {
310
+ if (sceneId > 254 || transitionTime > 60000000) {
311
+ return { status: Status.ConstraintError, groupId: reqGroupId, sceneId };
312
+ }
313
+
314
+ const { fabricIndex, groupId, existingSceneIndex } = this.#assertSceneCommandParameter(reqGroupId, sceneId);
315
+ if (groupId === undefined) {
316
+ return { status: Status.InvalidCommand, groupId: reqGroupId, sceneId };
317
+ }
318
+
319
+ const sceneValues = this.#decodeExtensionFieldSets(extensionFieldSetStructs);
320
+ if (sceneValues === undefined) {
321
+ return { status: Status.InvalidCommand, groupId, sceneId };
322
+ }
323
+
324
+ return this.#addOrReplaceSceneEntry(
325
+ {
326
+ sceneGroupId: groupId,
327
+ sceneId,
328
+ sceneName,
329
+ sceneTransitionTime: transitionTime,
330
+ sceneValues,
331
+ fabricIndex,
332
+ },
333
+ existingSceneIndex,
334
+ );
335
+ }
336
+
337
+ /** Implements the ViewScene command */
338
+ override viewScene({
339
+ groupId: reqGroupId,
340
+ sceneId,
341
+ }: ScenesManagement.ViewSceneRequest): ScenesManagement.ViewSceneResponse {
342
+ if (sceneId > 254) {
343
+ return { status: Status.ConstraintError, groupId: reqGroupId, sceneId };
344
+ }
345
+
346
+ const { groupId, existingSceneIndex } = this.#assertSceneCommandParameter(reqGroupId, sceneId);
347
+ if (groupId === undefined) {
348
+ return { status: Status.InvalidCommand, groupId: reqGroupId, sceneId };
349
+ }
350
+ if (existingSceneIndex === -1) {
351
+ return { status: Status.NotFound, groupId, sceneId };
352
+ }
353
+
354
+ const scene = this.state.sceneTable[existingSceneIndex];
355
+
356
+ return {
357
+ status: Status.Success,
358
+ groupId: scene.sceneGroupId,
359
+ sceneId: scene.sceneId,
360
+ sceneName: scene.sceneName,
361
+ transitionTime: scene.sceneTransitionTime,
362
+ extensionFieldSetStructs: this.#encodeExtensionFieldSets(scene.sceneValues),
363
+ };
364
+ }
365
+
366
+ /** Implements the RemoveScene command */
367
+ override removeScene({
368
+ groupId: reqGroupId,
369
+ sceneId,
370
+ }: ScenesManagement.RemoveSceneRequest): ScenesManagement.RemoveSceneResponse {
371
+ if (sceneId > 254) {
372
+ return { status: Status.ConstraintError, groupId: reqGroupId, sceneId };
373
+ }
374
+
375
+ const { groupId, existingSceneIndex, fabricIndex } = this.#assertSceneCommandParameter(reqGroupId, sceneId);
376
+ if (groupId === undefined) {
377
+ return { status: Status.InvalidCommand, groupId: reqGroupId, sceneId };
378
+ }
379
+
380
+ if (existingSceneIndex === -1) {
381
+ return { status: Status.NotFound, groupId, sceneId };
382
+ }
383
+
384
+ this.state.sceneTable.splice(existingSceneIndex, 1);
385
+
386
+ if (this.internal.monitorSceneAttributesForFabric === fabricIndex) {
387
+ const info = this.#fabricSceneInfoForFabric(fabricIndex);
388
+ if (info) {
389
+ if (info.currentGroup === groupId && info.currentScene === sceneId && info.sceneValid) {
390
+ info.sceneValid = false;
391
+ this.internal.monitorSceneAttributesForFabric = null;
392
+ }
393
+ }
394
+ }
395
+
396
+ return { status: Status.Success, groupId, sceneId };
397
+ }
398
+
399
+ /** Implements the RemoveAllScenes command */
400
+ override removeAllScenes({
401
+ groupId: reqGroupId,
402
+ }: ScenesManagement.RemoveAllScenesRequest): ScenesManagement.RemoveAllScenesResponse {
403
+ const { groupId, fabricIndex } = this.#assertSceneCommandParameter(reqGroupId);
404
+ if (groupId === undefined) {
405
+ return { status: Status.InvalidCommand, groupId: reqGroupId };
406
+ }
407
+
408
+ this.removeScenesForGroupOnFabric(fabricIndex, groupId);
409
+
410
+ return { status: Status.Success, groupId };
411
+ }
412
+
413
+ /** Implements the StoreScene command */
414
+ override storeScene({
415
+ groupId: reqGroupId,
416
+ sceneId,
417
+ }: ScenesManagement.StoreSceneRequest): ScenesManagement.StoreSceneResponse {
418
+ if (sceneId > 254) {
419
+ return { status: Status.ConstraintError, groupId: reqGroupId, sceneId };
420
+ }
421
+
422
+ const { groupId, existingSceneIndex, fabricIndex } = this.#assertSceneCommandParameter(reqGroupId, sceneId);
423
+ if (groupId === undefined) {
424
+ return { status: Status.InvalidCommand, groupId: reqGroupId, sceneId };
425
+ }
426
+ const sceneValues: ScenesManagementServer.SceneAttributeData = this.#collectSceneAttributeValues();
427
+
428
+ let result: ScenesManagement.StoreSceneResponse;
429
+ // If a scene already exists under the same group/scene identifier pair, the ExtensionFieldSets of
430
+ // the stored scene SHALL be replaced with the ExtensionFieldSets corresponding to the current
431
+ // state of other clusters on the same endpoint and the other fields of the scene table entry SHALL
432
+ // remain unchanged.
433
+ if (existingSceneIndex !== -1) {
434
+ const scene = this.state.sceneTable[existingSceneIndex];
435
+ scene.sceneValues = sceneValues;
436
+ result = this.#addOrReplaceSceneEntry(scene, existingSceneIndex);
437
+ } else {
438
+ // Otherwise, a new entry SHALL be added to the scene table, using the provided GroupID and
439
+ // SceneID, with SceneTransitionTime set to 0, with SceneName set to the empty string, and with
440
+ // ExtensionFieldSets corresponding to the current state of other clusters on the same endpoint.
441
+ result = this.#addOrReplaceSceneEntry({
442
+ sceneGroupId: groupId,
443
+ sceneId,
444
+ sceneName: "",
445
+ sceneTransitionTime: 0,
446
+ sceneValues,
447
+ fabricIndex,
448
+ });
449
+ }
450
+
451
+ // IF scene was successfully added it is also the active one now
452
+ if (result.status === Status.Success) {
453
+ this.#activateSceneInFabricSceneInfo(fabricIndex, groupId, sceneId);
454
+ }
455
+ return result;
456
+ }
457
+
458
+ /** Implements the RecallScene command */
459
+ override async recallScene({ groupId: reqGroupId, sceneId, transitionTime }: ScenesManagement.RecallSceneRequest) {
460
+ if (sceneId > 254) {
461
+ throw new StatusResponse.ConstraintErrorError(`SceneId ${sceneId} exceeds maximum`);
462
+ }
463
+ if (transitionTime !== null && transitionTime !== undefined && transitionTime > 60000000) {
464
+ throw new StatusResponse.ConstraintErrorError(`TransitionTime ${transitionTime} exceeds maximum`);
465
+ }
466
+
467
+ const { groupId, existingSceneIndex, fabricIndex } = this.#assertSceneCommandParameter(reqGroupId, sceneId);
468
+ if (groupId === undefined) {
469
+ throw new StatusResponse.InvalidCommandError(`Invalid groupId ${reqGroupId}`);
470
+ }
471
+ if (existingSceneIndex === -1) {
472
+ throw new StatusResponse.NotFoundError(`SceneId ${sceneId} in groupId ${groupId} not found`);
473
+ }
474
+
475
+ // Recall the scene by setting all attributes to the stored values and marking it active
476
+ const scene = this.state.sceneTable[existingSceneIndex];
477
+ await this.#applySceneAttributeValues(scene.sceneValues, transitionTime);
478
+ this.#activateSceneInFabricSceneInfo(fabricIndex, groupId, sceneId);
479
+ }
480
+
481
+ /** Implements the GetSceneMembership command */
482
+ override getSceneMembership({
483
+ groupId: reqGroupId,
484
+ }: ScenesManagement.GetSceneMembershipRequest): ScenesManagement.GetSceneMembershipResponse {
485
+ const { groupId, fabricIndex } = this.#assertSceneCommandParameter(reqGroupId);
486
+ if (groupId === undefined) {
487
+ return { status: Status.InvalidCommand, groupId: reqGroupId, capacity: null };
488
+ }
489
+
490
+ // Capacity
491
+ // 0 - No further scenes MAY be added.
492
+ // • 0 < Capacity < 0xFE - Capacity holds the number of scenes that MAY be added.
493
+ // • 0xFE - At least 1 further scene MAY be added (exact number is unknown).
494
+ // Formally 0xfe can never happen because the scene capacity is bound to max 253 scenes per fabric
495
+ const capacity = Math.max(
496
+ Math.min(this.#fabricSceneCapacity - this.#scenesForFabric(fabricIndex).length, 0xfe),
497
+ 0,
498
+ );
499
+
500
+ return {
501
+ status: Status.Success,
502
+ groupId,
503
+ sceneList: this.#scenesForGroup(fabricIndex, groupId).map(({ sceneId }) => sceneId),
504
+ capacity,
505
+ };
506
+ }
507
+
508
+ /** Implements the CopyScene command */
509
+ override copyScene({
510
+ mode,
511
+ groupIdentifierFrom,
512
+ sceneIdentifierFrom,
513
+ groupIdentifierTo,
514
+ sceneIdentifierTo,
515
+ }: ScenesManagement.CopySceneRequest): ScenesManagement.CopySceneResponse {
516
+ const {
517
+ fabricIndex,
518
+ groupId: fromGroupId,
519
+ existingSceneIndex: fromSceneIndex,
520
+ } = this.#assertSceneCommandParameter(groupIdentifierFrom, sceneIdentifierFrom);
521
+ const { groupId: toGroupId, existingSceneIndex: toSceneIndex } = this.#assertSceneCommandParameter(
522
+ groupIdentifierTo,
523
+ sceneIdentifierTo,
524
+ );
525
+ if (fromGroupId === undefined || toGroupId === undefined) {
526
+ return { status: Status.InvalidCommand, groupIdentifierFrom, sceneIdentifierFrom };
527
+ }
528
+
529
+ // We want to copy all scenes from one group to another, as long as we have capacity free
530
+ if (mode.copyAllScenes) {
531
+ const groupScenes = deepCopy(this.#scenesForGroup(fabricIndex, fromGroupId));
532
+ for (const scene of groupScenes) {
533
+ scene.sceneGroupId = toGroupId;
534
+ const { status } = this.#addOrReplaceSceneEntry(
535
+ scene,
536
+ this.#sceneIndexForId(fabricIndex, scene.sceneId, toGroupId),
537
+ );
538
+ if (status !== Status.Success) {
539
+ return {
540
+ status,
541
+ groupIdentifierFrom,
542
+ sceneIdentifierFrom,
543
+ };
544
+ }
545
+ }
546
+ return {
547
+ status: Status.Success,
548
+ groupIdentifierFrom,
549
+ sceneIdentifierFrom,
550
+ };
551
+ }
552
+
553
+ // We want to copy a single scene
554
+ if (sceneIdentifierTo > 254 || sceneIdentifierFrom > 254) {
555
+ return { status: Status.ConstraintError, groupIdentifierFrom, sceneIdentifierFrom };
556
+ }
557
+ if (fromSceneIndex === -1) {
558
+ return { status: Status.NotFound, groupIdentifierFrom, sceneIdentifierFrom };
559
+ }
560
+
561
+ const scene = deepCopy(this.state.sceneTable[fromSceneIndex]);
562
+ scene.sceneGroupId = groupIdentifierTo;
563
+ scene.sceneId = sceneIdentifierTo;
564
+ const result = this.#addOrReplaceSceneEntry(scene, toSceneIndex);
565
+ return {
566
+ status: result.status,
567
+ groupIdentifierFrom,
568
+ sceneIdentifierFrom,
569
+ };
570
+ }
571
+
572
+ /** Close the observers */
573
+ override async [Symbol.asyncDispose]() {
574
+ this.internal.endpointSceneAttributeObservers.close();
575
+ }
576
+
577
+ /** Method used by the OnOff cluster to recall the global scene */
578
+ async recallGlobalScene(fabricIndex: FabricIndex) {
579
+ const existingSceneIndex = this.#sceneIndexForId(fabricIndex, GLOBAL_SCENE_ID, UNDEFINED_GROUP);
580
+ if (existingSceneIndex === -1) {
581
+ return;
582
+ }
583
+ const scene = this.state.sceneTable[existingSceneIndex];
584
+ await this.#applySceneAttributeValues(scene.sceneValues, scene.sceneTransitionTime);
585
+ this.#activateSceneInFabricSceneInfo(fabricIndex, UNDEFINED_GROUP, GLOBAL_SCENE_ID);
586
+ }
587
+
588
+ /** Method used by the OnOff cluster to store the global scene */
589
+ storeGlobalScene(fabricIndex: FabricIndex) {
590
+ const sceneValues: ScenesManagementServer.SceneAttributeData = this.#collectSceneAttributeValues();
591
+
592
+ const existingSceneIndex = this.#sceneIndexForId(fabricIndex, GLOBAL_SCENE_ID, UNDEFINED_GROUP);
593
+ if (existingSceneIndex === -1) {
594
+ this.#addOrReplaceSceneEntry({
595
+ sceneGroupId: UNDEFINED_GROUP,
596
+ sceneId: GLOBAL_SCENE_ID,
597
+ sceneName: "Global Scene",
598
+ sceneTransitionTime: 0,
599
+ sceneValues,
600
+ fabricIndex,
601
+ });
602
+ } else {
603
+ const scene = this.state.sceneTable[existingSceneIndex];
604
+ scene.sceneValues = sceneValues;
605
+ this.#addOrReplaceSceneEntry(scene, existingSceneIndex);
606
+ }
607
+ }
608
+
609
+ /**
610
+ * Decodes an ExtensionFieldSet struct into SceneAttributeData format including validation.
611
+ * Returns undefined if the data are considered invalid according to the Spec/SDK.
612
+ */
613
+ #decodeExtensionFieldSets(
614
+ fieldSet: ScenesManagement.ExtensionFieldSet[] = [],
615
+ ): ScenesManagementServer.SceneAttributeData | undefined {
616
+ const result: ScenesManagementServer.SceneAttributeData = {};
617
+ for (const { clusterId, attributeValueList } of fieldSet) {
618
+ const sceneClusterDetails = this.internal.endpointSceneableBehaviors.get("id", clusterId);
619
+ if (sceneClusterDetails === undefined) {
620
+ // Any ExtensionFieldSetStruct referencing a ClusterID that is not implemented on the endpoint
621
+ // MAY be omitted during processing.
622
+ continue;
623
+ }
624
+ const clusterName = sceneClusterDetails.name;
625
+
626
+ // If the ExtensionFieldSetStructs list has multiple entries listing the same ClusterID, the last
627
+ // one within the list SHALL be the one recorded.
628
+ if (result[clusterName]) {
629
+ delete result[clusterName];
630
+ }
631
+
632
+ for (const attributeValue of attributeValueList) {
633
+ const { attributeId } = attributeValue;
634
+ const attributeDetails = sceneClusterDetails.attributes.get("id", attributeId);
635
+ if (attributeDetails === undefined) {
636
+ // Effectively an SDK and/or Spec bug, at least both out of sync, but we need to do that to pass tests
637
+ // The AttributeID field SHALL NOT refer to an attribute without the Scenes ("S") designation in the
638
+ // Quality column of the cluster specification.
639
+ return undefined;
640
+ }
641
+ const value = this.#decodeValueFromAttributeValuePair(attributeValue, attributeDetails);
642
+ if (value == undefined) {
643
+ return undefined;
644
+ }
645
+ result[clusterName] = result[clusterName] || {};
646
+
647
+ // Within a single entry of the ExtensionFieldSetStructs list, if an ExtensionFieldSet contains
648
+ // the same AttributeID more than once, the last one within the ExtensionFieldSet SHALL be
649
+ // the one recorded.
650
+ result[clusterName][attributeDetails.name] = value;
651
+ }
652
+ }
653
+ return result;
654
+ }
655
+
656
+ /**
657
+ * Decodes and validates a single AttributeValuePair into the actual attribute value including validation.
658
+ */
659
+ #decodeValueFromAttributeValuePair(
660
+ attributeValuePair: ScenesManagement.AttributeValuePair,
661
+ { schema, type, mappedType, nullable }: AttributeDetails,
662
+ ): number | bigint | boolean | null | undefined {
663
+ let fieldCount = 0;
664
+ for (const value of Object.values(attributeValuePair)) {
665
+ if (value !== undefined) {
666
+ fieldCount++;
667
+ }
668
+ }
669
+ if (fieldCount !== 2) {
670
+ logger.warn(
671
+ `AttributeValuePair has invalid number (${fieldCount}) of fields (${serialize(attributeValuePair)})`,
672
+ );
673
+ return undefined;
674
+ }
675
+
676
+ const value = attributeValuePair[mappedType] as unknown;
677
+ if (value === undefined) {
678
+ logger.warn(
679
+ `AttributeValuePair missing value for mappedType ${mappedType} (${serialize(attributeValuePair)})`,
680
+ );
681
+ return undefined;
682
+ }
683
+ if (typeof value !== "number" && typeof value !== "bigint") {
684
+ logger.warn(
685
+ `AttributeValuePair has invalid non-numeric value for mappedType ${mappedType} (${serialize(attributeValuePair)})`, // Should never happen
686
+ );
687
+ return undefined;
688
+ }
689
+
690
+ // Handle Boolean values
691
+ if (type === bool.name) {
692
+ let boolValue: boolean | null;
693
+ if (value === 0 || value === 1) {
694
+ boolValue = !!value;
695
+ } else if (nullable) {
696
+ // For boolean nullable attributes, any value that is not 0 or 1 SHALL be considered to have the
697
+ // null value.
698
+ boolValue = null;
699
+ } else {
700
+ // For boolean non-nullable attributes, any value that is not 0 or 1 SHALL be considered to have
701
+ // the value FALSE.
702
+ boolValue = false;
703
+ }
704
+ schema.validate(boolValue);
705
+ return boolValue;
706
+ }
707
+
708
+ // Handle numbers
709
+ try {
710
+ schema.validate(value);
711
+ return value;
712
+ } catch (error) {
713
+ ValidationOutOfBoundsError.accept(error);
714
+ }
715
+
716
+ // We only came here if the value is out of bounds
717
+ // When nullable this means we return null
718
+ if (nullable) {
719
+ // For non-boolean nullable attributes, any value that is not a valid numeric value for the
720
+ // attribute’s type after accounting for range reductions due to being nullable and constraints
721
+ // SHALL be considered to have the null value for the type.
722
+ return null;
723
+ }
724
+
725
+ // Else we need to find the closest valid value within the constraints
726
+ if (!(schema instanceof TlvNumericSchema) || schema.min === undefined || schema.max === undefined) {
727
+ throw new InternalError(`Attribute schema for non-boolean non-nullable attribute is not TlvNumericSchema`);
728
+ }
729
+ const effectiveMin = schema.min as number | bigint;
730
+ const effectiveMax = schema.max as number | bigint;
731
+ // For non-boolean non-nullable attributes, any value that is not a valid numeric value for the
732
+ // attribute’s type after accounting for constraints SHALL be considered to be the valid attribute
733
+ // value that is closest to the provided value.
734
+ // ◦ In the event that an invalid provided value is of equal numerical distance to the two closest
735
+ // valid values, the lowest of those values SHALL be considered the closest valid attribute
736
+ // value.
737
+ let minDiff = BigInt(value) - BigInt(effectiveMin);
738
+ if (minDiff < 0) {
739
+ minDiff = -minDiff;
740
+ }
741
+ let maxDiff = BigInt(value) - BigInt(effectiveMax);
742
+ if (maxDiff < 0) {
743
+ maxDiff = -maxDiff;
744
+ }
745
+
746
+ let closestValue = effectiveMin;
747
+ if (maxDiff < minDiff || (maxDiff === minDiff && effectiveMax < closestValue)) {
748
+ closestValue = effectiveMax;
749
+ }
750
+ schema.validate(closestValue); // Just to be sure
751
+ return closestValue;
752
+ }
753
+
754
+ /** Encode the SceneAttributeData into ExtensionFieldSet structs for command responses */
755
+ #encodeExtensionFieldSets(
756
+ sceneValues: ScenesManagementServer.SceneAttributeData,
757
+ ): ScenesManagement.ExtensionFieldSet[] {
758
+ const extensionFieldSetStructs = new Array<ScenesManagement.ExtensionFieldSet>();
759
+
760
+ for (const [clusterName, clusterAttributes] of Object.entries(sceneValues)) {
761
+ const clusterData = this.internal.endpointSceneableBehaviors.get("name", clusterName);
762
+ if (clusterData === undefined) {
763
+ throw new InternalError(
764
+ `Scene Attribute cluster ${clusterName} not found on Endpoint ${this.endpoint.id} during encoding`,
765
+ );
766
+ }
767
+ const attributeValueList = new Array<ScenesManagement.AttributeValuePair>();
768
+ for (const [attributeName, value] of Object.entries(clusterAttributes)) {
769
+ const attributeDetails = clusterData.attributes.get("name", attributeName);
770
+ if (attributeDetails !== undefined) {
771
+ const encodedData = this.#encodeSceneAttributeValue(attributeDetails, value);
772
+ if (encodedData !== undefined) {
773
+ attributeValueList.push(encodedData);
774
+ }
775
+ }
776
+ }
777
+ if (attributeValueList.length) {
778
+ extensionFieldSetStructs.push({
779
+ clusterId: clusterData.id,
780
+ attributeValueList,
781
+ });
782
+ }
783
+ }
784
+ return extensionFieldSetStructs;
785
+ }
786
+
787
+ /** Encodes a single attribute value into an AttributeValuePair for command responses */
788
+ #encodeSceneAttributeValue(
789
+ { id: attributeId, schema, type, mappedType }: AttributeDetails,
790
+ value: number | bigint | boolean | null,
791
+ ): ScenesManagement.AttributeValuePair | undefined {
792
+ if (type === bool.name) {
793
+ if (value === null) {
794
+ return { attributeId, [mappedType]: 0xff };
795
+ }
796
+ return { attributeId, [mappedType]: value ? 1 : 0 };
797
+ }
798
+ if (value !== null) {
799
+ return { attributeId, [mappedType]: value };
800
+ }
801
+ if (!(schema instanceof NullableSchema)) {
802
+ throw new InternalError(`Attribute schema for non-nullable attribute is not NullableSchema`);
803
+ }
804
+ if (!(schema.schema instanceof TlvNumericSchema)) {
805
+ throw new InternalError(`Underlying schema for non-nullable attribute is not TlvNumericSchema`);
806
+ }
807
+ if (schema.schema.baseTypeMin === 0 && schema.schema.max < schema.schema.baseTypeMax) {
808
+ // unsigned integer null is represented by baseTypeMax
809
+ return { attributeId, [mappedType]: schema.schema.baseTypeMax };
810
+ } else if (schema.schema.baseTypeMin < 0 && schema.schema.min > schema.schema.baseTypeMin) {
811
+ // signed integer null is represented by baseTypeMin
812
+ return { attributeId, [mappedType]: schema.schema.baseTypeMin };
813
+ } else {
814
+ // Should never happen!
815
+ logger.warn(
816
+ `Cannot determine out-of-bounds value for attribute schema, returning min value of datatype schema`,
817
+ );
818
+ }
819
+ }
820
+
821
+ /** Collects the current values of all sceneable attributes on the endpoint */
822
+ #collectSceneAttributeValues() {
823
+ const sceneValues: ScenesManagementServer.SceneAttributeData = {};
824
+ this.endpoint.act(agent => {
825
+ for (const { name: clusterName, attributes } of this.internal.endpointSceneableBehaviors) {
826
+ const clusterState = (agent as any)[clusterName].state;
827
+ for (const attribute of attributes) {
828
+ const attributeName = attribute.name;
829
+ const currentValue = clusterState[attributeName];
830
+ if (currentValue !== undefined) {
831
+ sceneValues[clusterName] = sceneValues[clusterName] || {};
832
+ sceneValues[clusterName][attributeName] = deepCopy(currentValue);
833
+ }
834
+ }
835
+ }
836
+ });
837
+ logger.debug(`Collected scene attribute values on Endpoint ${this.endpoint.id}: ${serialize(sceneValues)}`);
838
+ return sceneValues;
839
+ }
840
+
841
+ /**
842
+ * Main method for Clusters to Register themselves with their "Apply Scenes Callback".
843
+ *
844
+ * @param behavior ClusterBehavior implementing a cluster with sceneable attributes
845
+ * @param applyFunc Function that applies scene values for that cluster
846
+ */
847
+ implementScenes<T extends ClusterBehavior>(behavior: T, applyFunc: ScenesManagementServer.ApplySceneValuesFunc<T>) {
848
+ const type = behavior.type as ClusterBehavior.Type;
849
+ if (!ClusterBehavior.is(type) || !type.schema || !type.schema.id) {
850
+ return;
851
+ }
852
+ const clusterName = camelize(type.schema.name);
853
+
854
+ const clusterId = ClusterId(type.schema.id);
855
+ let sceneClusterDetails;
856
+ for (const attribute of type.schema.conformant.attributes) {
857
+ if (!attribute.effectiveQuality.scene) {
858
+ continue; // Ignore non sceneable attributes
859
+ }
860
+
861
+ const attributeId = AttributeId(attribute.id);
862
+ const attributeName = camelize(attribute.name);
863
+
864
+ // Ignore attributes that are not present on the endpoint or do not have change events
865
+ const event = (this.endpoint.events as Events.Generic<ClusterEvents.ChangedObservable<any>>)[clusterName]?.[
866
+ `${attributeName}$Changed`
867
+ ];
868
+ const hasValue =
869
+ (this.endpoint.state as Record<string, Val.Struct>)[clusterName]?.[attributeName] !== undefined;
870
+ if (!hasValue || !event) {
871
+ continue;
872
+ }
873
+
874
+ // Register observer to reset scene validity on attribute changes
875
+ // Ideally we would do it right but SDK implementation is different, so for now we mimic SDK.
876
+ // This means that certain commands will reset the state manually
877
+ /*this.internal.endpointSceneAttributeObservers.on(
878
+ event,
879
+ this.callback(this.makeAllFabricSceneInfoEntriesInvalid),
880
+ );*/
881
+ if (!sceneClusterDetails) {
882
+ sceneClusterDetails = this.internal.endpointSceneableBehaviors.get("id", clusterId) ?? {
883
+ id: clusterId,
884
+ name: clusterName,
885
+ attributes: new BasicSet<AttributeDetails>(),
886
+ clusterBehaviorType: type,
887
+ applyFunc,
888
+ };
889
+ }
890
+ const attrType = attribute.primitiveBase?.name;
891
+ if (attrType === undefined || DataTypeToSceneAttributeDataMap[attrType] === undefined) {
892
+ logger.warn(
893
+ `Scene Attribute ${attribute.name} on Cluster ${clusterName} has unsupported datatype ${attrType} for scene management on Endpoint ${this.endpoint.id}`,
894
+ );
895
+ continue;
896
+ }
897
+
898
+ sceneClusterDetails.attributes.add({
899
+ id: attributeId,
900
+ name: attributeName,
901
+ schema: type.cluster.attributes[attributeName].schema,
902
+ type: attrType,
903
+ mappedType: DataTypeToSceneAttributeDataMap[attrType],
904
+ nullable: !!attribute.effectiveQuality.nullable,
905
+ });
906
+ }
907
+ if (sceneClusterDetails) {
908
+ logger.info(
909
+ `Registered ${sceneClusterDetails.attributes.size} scene attributes for Cluster ${clusterName} on Endpoint ${this.endpoint.id}`,
910
+ );
911
+ this.internal.endpointSceneableBehaviors.add(sceneClusterDetails);
912
+ }
913
+ }
914
+
915
+ /** Apply scene attribute values in the various clusters on the endpoint. */
916
+ #applySceneAttributeValues(
917
+ sceneValues: ScenesManagementServer.SceneAttributeData,
918
+ transitionTime: number | null = null,
919
+ ): MaybePromise {
920
+ logger.debug(`Recalling scene on Endpoint ${this.endpoint.id} with values: ${serialize(sceneValues)}`);
921
+ const agent = this.endpoint.agentFor(this.context);
922
+ const promises: Array<PromiseLike<void>> = [];
923
+ for (const [clusterName, clusterAttributes] of Object.entries(sceneValues)) {
924
+ const { applyFunc, clusterBehaviorType } =
925
+ this.internal.endpointSceneableBehaviors.get("name", clusterName) ?? {};
926
+ if (applyFunc && clusterBehaviorType) {
927
+ const result = applyFunc.call(agent.get(clusterBehaviorType), clusterAttributes, transitionTime ?? 0);
928
+ if (MaybePromise.is(result)) {
929
+ promises.push(result);
930
+ }
931
+ } else {
932
+ logger.warn(
933
+ `No scenes implementation found for cluster ${clusterName} on Endpoint ${this.endpoint.id} during scene recall. Values are ignored`,
934
+ );
935
+ }
936
+ }
937
+ if (promises.length) {
938
+ return Promise.all(promises).then(
939
+ () => undefined,
940
+ error => logger.warn(`Error applying scene attribute values on Endpoint ${this.endpoint.id}:`, error),
941
+ );
942
+ }
943
+ }
944
+
945
+ #groupExistentInFabric(fabric: Fabric, groupId: GroupId): boolean {
946
+ return fabric.groups.groupKeyIdMap.has(groupId);
947
+ }
948
+
949
+ /**
950
+ * The Scene Table capacity for a given fabric SHALL be less than half (rounded down towards 0) of the Scene Table
951
+ * entries (as indicated in the SceneTableSize attribute), with a maximum of 253 entries
952
+ */
953
+ get #fabricSceneCapacity(): number {
954
+ return Math.min(Math.floor((this.state.sceneTableSize - 1) / 2), 253);
955
+ }
956
+
957
+ #scenesForGroup(fabricIndex: FabricIndex, groupId: number): ScenesManagementServer.ScenesTableEntry[] {
958
+ return this.state.sceneTable.filter(s => s.fabricIndex === fabricIndex && s.sceneGroupId === groupId);
959
+ }
960
+
961
+ #scenesForFabric(fabricIndex: FabricIndex): ScenesManagementServer.ScenesTableEntry[] {
962
+ return this.state.sceneTable.filter(s => s.fabricIndex === fabricIndex);
963
+ }
964
+
965
+ #sceneIndexForId(fabricIndex: FabricIndex, sceneId: number, groupId: number): number {
966
+ return this.state.sceneTable.findIndex(
967
+ s => s.fabricIndex === fabricIndex && s.sceneId === sceneId && s.sceneGroupId === groupId,
968
+ );
969
+ }
970
+
971
+ #fabricSceneInfoForFabric(fabricIndex: FabricIndex): ScenesManagement.SceneInfo | undefined {
972
+ return this.state.fabricSceneInfo.find(f => f.fabricIndex === fabricIndex);
973
+ }
974
+
975
+ /** If the fabric is the one that currently has a valid scene being monitored, invalidate it. */
976
+ #invalidateFabricSceneInfoForFabric(fabricIndex: FabricIndex) {
977
+ if (this.internal.monitorSceneAttributesForFabric !== fabricIndex) {
978
+ return;
979
+ }
980
+ const infoEntry = this.#fabricSceneInfoForFabric(fabricIndex);
981
+ if (infoEntry && infoEntry.sceneValid) {
982
+ infoEntry.sceneValid = false;
983
+ }
984
+ this.internal.monitorSceneAttributesForFabric = null;
985
+ }
986
+
987
+ /**
988
+ * Invalidate all fabric scene info entries.
989
+ * Method will be called by relevant clusters when commands change the state.
990
+ */
991
+ makeAllFabricSceneInfoEntriesInvalid() {
992
+ if (this.internal.monitorSceneAttributesForFabric === null) {
993
+ return;
994
+ }
995
+ const infoEntry = this.#fabricSceneInfoForFabric(this.internal.monitorSceneAttributesForFabric);
996
+ if (infoEntry && infoEntry.sceneValid) {
997
+ infoEntry.sceneValid = false;
998
+ }
999
+ this.internal.monitorSceneAttributesForFabric = null;
1000
+ }
1001
+
1002
+ /** Initializes the fabric scene info entries based on existing fabrics and scene table. */
1003
+ #initializeFabricSceneInfo(fabric: FabricManager) {
1004
+ const existingEntries = new Map<FabricIndex, ScenesManagement.SceneInfo>();
1005
+ for (const entry of this.state.fabricSceneInfo) {
1006
+ existingEntries.set(entry.fabricIndex, entry);
1007
+ }
1008
+ const list = new Array<ScenesManagement.SceneInfo>();
1009
+ for (const { fabricIndex } of fabric.fabrics) {
1010
+ const entry = existingEntries.get(fabricIndex) ?? {
1011
+ sceneCount: 0, // Will be updated before it is set
1012
+ currentScene: UNDEFINED_SCENE_ID,
1013
+ currentGroup: UNDEFINED_GROUP,
1014
+ sceneValid: false,
1015
+ remainingCapacity: 0, // Will be updated before it is set
1016
+ fabricIndex,
1017
+ };
1018
+ entry.sceneValid = false;
1019
+ const { sceneCount, remainingCapacity } = this.#countsForFabric(fabricIndex);
1020
+ entry.sceneCount = sceneCount;
1021
+ entry.remainingCapacity = remainingCapacity;
1022
+ list.push(entry);
1023
+ }
1024
+ this.state.fabricSceneInfo = list;
1025
+ }
1026
+
1027
+ /** Updates the scene count and remaining capacity for a given fabric index */
1028
+ #updateFabricSceneInfoCountsForFabric(fabricIndex: FabricIndex) {
1029
+ const infoEntryIndex = this.state.fabricSceneInfo.findIndex(f => f.fabricIndex === fabricIndex);
1030
+ const entry: ScenesManagement.SceneInfo =
1031
+ infoEntryIndex !== -1
1032
+ ? this.state.fabricSceneInfo[infoEntryIndex]
1033
+ : {
1034
+ sceneCount: 0, // Will be updated before it is set
1035
+ currentScene: UNDEFINED_SCENE_ID,
1036
+ currentGroup: UNDEFINED_GROUP,
1037
+ sceneValid: false,
1038
+ remainingCapacity: 0, // Will be updated before it is set
1039
+ fabricIndex,
1040
+ };
1041
+ const { sceneCount, remainingCapacity } = this.#countsForFabric(fabricIndex);
1042
+ entry.sceneCount = sceneCount;
1043
+ entry.remainingCapacity = remainingCapacity;
1044
+ if (infoEntryIndex === -1) {
1045
+ this.state.fabricSceneInfo.push(entry);
1046
+ } else {
1047
+ this.state.fabricSceneInfo[infoEntryIndex] = entry;
1048
+ }
1049
+ }
1050
+
1051
+ #countsForFabric(fabricIndex: FabricIndex) {
1052
+ const sceneCount = this.#scenesForFabric(fabricIndex).length;
1053
+ return {
1054
+ sceneCount,
1055
+ remainingCapacity: Math.max(this.#fabricSceneCapacity - sceneCount, 0),
1056
+ };
1057
+ }
1058
+
1059
+ /** Activates the given scene in the fabric scene info, invalidating all others. */
1060
+ #activateSceneInFabricSceneInfo(fabricIndex: FabricIndex, groupId: GroupId, sceneId: number) {
1061
+ for (const infoEntry of this.state.fabricSceneInfo) {
1062
+ if (infoEntry.fabricIndex === fabricIndex) {
1063
+ infoEntry.currentGroup = groupId;
1064
+ infoEntry.currentScene = sceneId;
1065
+ infoEntry.sceneValid = true;
1066
+
1067
+ this.internal.monitorSceneAttributesForFabric = fabricIndex;
1068
+ } else if (infoEntry.sceneValid) {
1069
+ infoEntry.sceneValid = false;
1070
+ }
1071
+ }
1072
+ }
1073
+
1074
+ /** Removes all scenes for a given fabric when the fabric is deleted */
1075
+ #handleDeleteFabric({ fabricIndex }: Fabric) {
1076
+ if (this.internal.monitorSceneAttributesForFabric === fabricIndex) {
1077
+ this.internal.monitorSceneAttributesForFabric = null;
1078
+ }
1079
+ }
1080
+ }
1081
+
1082
+ export namespace ScenesManagementServer {
1083
+ /** Scene Attribute Data format used internally to store scene attribute values */
1084
+ export type SceneAttributeData = { [key: string]: { [key: string]: boolean | number | bigint | null } };
1085
+
1086
+ /** Scene Table Entry as decorated class for persistence */
1087
+ export class ScenesTableEntry implements Omit<ScenesManagement.LogicalSceneTable, "extensionFields"> {
1088
+ @field(groupId, mandatory)
1089
+ sceneGroupId!: GroupId;
1090
+
1091
+ @field(uint8.extend({ constraint: "max 254" }), mandatory)
1092
+ sceneId!: number;
1093
+
1094
+ @field(string.extend({ constraint: "max 16" }))
1095
+ sceneName?: string;
1096
+
1097
+ @field(uint32.extend({ constraint: "max 60000000" }), mandatory)
1098
+ sceneTransitionTime!: number;
1099
+
1100
+ @field(any, mandatory)
1101
+ sceneValues!: SceneAttributeData;
1102
+
1103
+ @field(fabricIdx, mandatory)
1104
+ fabricIndex!: FabricIndex;
1105
+ }
1106
+
1107
+ export class State extends ScenesManagementBase.State {
1108
+ @field(listOf(ScenesTableEntry), nonvolatile, mandatory)
1109
+ sceneTable = new Array<ScenesTableEntry>();
1110
+ }
1111
+
1112
+ export type ApplySceneValuesFunc<T extends ClusterBehavior> = (
1113
+ this: T,
1114
+ values: Val.Struct,
1115
+ transitionTime: number,
1116
+ ) => MaybePromise;
1117
+
1118
+ export class Internal {
1119
+ /** ObserverGroup for all $Changed events of sceneable attributes */
1120
+ endpointSceneAttributeObservers = new ObserverGroup();
1121
+
1122
+ /** Fabric index where a scene is currently valid, if any */
1123
+ monitorSceneAttributesForFabric: FabricIndex | null = null;
1124
+
1125
+ /** Map of sceneable behaviors/clusters and their sceneable attributes on the endpoint */
1126
+ endpointSceneableBehaviors = new BasicSet<{
1127
+ id: ClusterId;
1128
+ name: string;
1129
+ attributes: BasicSet<AttributeDetails>;
1130
+ clusterBehaviorType: ClusterBehavior.Type;
1131
+ applyFunc: ApplySceneValuesFunc<any>;
1132
+ }>();
1133
+ }
1134
+ }