@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.
- package/dist/cjs/behavior/Behavior.d.ts +2 -2
- package/dist/cjs/behavior/Behavior.d.ts.map +1 -1
- package/dist/cjs/behavior/Behavior.js +1 -1
- package/dist/cjs/behavior/Behavior.js.map +1 -1
- package/dist/cjs/behavior/Events.js.map +1 -1
- package/dist/cjs/behavior/cluster/ClusterBehavior.d.ts +3 -2
- package/dist/cjs/behavior/cluster/ClusterBehavior.d.ts.map +1 -1
- package/dist/cjs/behavior/cluster/ClusterBehavior.js.map +1 -1
- package/dist/cjs/behavior/cluster/ClusterBehaviorUtil.js +1 -1
- package/dist/cjs/behavior/cluster/ClusterBehaviorUtil.js.map +1 -1
- package/dist/cjs/behavior/internal/BehaviorBacking.js +1 -1
- package/dist/cjs/behavior/internal/BehaviorBacking.js.map +1 -1
- package/dist/cjs/behavior/system/mqtt/MqttInterface.js +1 -1
- package/dist/cjs/behavior/system/mqtt/MqttInterface.js.map +1 -1
- package/dist/cjs/behaviors/basic-information/BasicInformationServer.d.ts +2 -2
- package/dist/cjs/behaviors/basic-information/BasicInformationServer.d.ts.map +1 -1
- package/dist/cjs/behaviors/basic-information/BasicInformationServer.js +0 -3
- package/dist/cjs/behaviors/basic-information/BasicInformationServer.js.map +1 -1
- package/dist/cjs/behaviors/bridged-device-basic-information/BridgedDeviceBasicInformationServer.d.ts +1 -1
- package/dist/cjs/behaviors/bridged-device-basic-information/BridgedDeviceBasicInformationServer.d.ts.map +1 -1
- package/dist/cjs/behaviors/color-control/ColorControlServer.d.ts +3 -3
- package/dist/cjs/behaviors/color-control/ColorControlServer.d.ts.map +1 -1
- package/dist/cjs/behaviors/color-control/ColorControlServer.js +118 -3
- package/dist/cjs/behaviors/color-control/ColorControlServer.js.map +1 -1
- package/dist/cjs/behaviors/general-diagnostics/GeneralDiagnosticsServer.d.ts +1 -1
- package/dist/cjs/behaviors/general-diagnostics/GeneralDiagnosticsServer.d.ts.map +1 -1
- package/dist/cjs/behaviors/general-diagnostics/GeneralDiagnosticsServer.js.map +1 -1
- package/dist/cjs/behaviors/group-key-management/GroupKeyManagementServer.d.ts +1 -1
- package/dist/cjs/behaviors/group-key-management/GroupKeyManagementServer.d.ts.map +1 -1
- package/dist/cjs/behaviors/group-key-management/GroupKeyManagementServer.js.map +1 -1
- package/dist/cjs/behaviors/groups/GroupsServer.d.ts.map +1 -1
- package/dist/cjs/behaviors/groups/GroupsServer.js +9 -1
- package/dist/cjs/behaviors/groups/GroupsServer.js.map +1 -1
- package/dist/cjs/behaviors/level-control/LevelControlServer.d.ts +5 -5
- package/dist/cjs/behaviors/level-control/LevelControlServer.d.ts.map +1 -1
- package/dist/cjs/behaviors/level-control/LevelControlServer.js +54 -27
- package/dist/cjs/behaviors/level-control/LevelControlServer.js.map +1 -1
- package/dist/cjs/behaviors/on-off/OnOffServer.d.ts +3 -6
- package/dist/cjs/behaviors/on-off/OnOffServer.d.ts.map +1 -1
- package/dist/cjs/behaviors/on-off/OnOffServer.js +59 -7
- package/dist/cjs/behaviors/on-off/OnOffServer.js.map +1 -1
- package/dist/cjs/behaviors/scenes-management/ScenesManagementServer.d.ts +122 -3
- package/dist/cjs/behaviors/scenes-management/ScenesManagementServer.d.ts.map +1 -1
- package/dist/cjs/behaviors/scenes-management/ScenesManagementServer.js +909 -1
- package/dist/cjs/behaviors/scenes-management/ScenesManagementServer.js.map +2 -2
- package/dist/cjs/behaviors/switch/SwitchServer.d.ts +1 -1
- package/dist/cjs/behaviors/switch/SwitchServer.d.ts.map +1 -1
- package/dist/cjs/behaviors/switch/SwitchServer.js.map +1 -1
- package/dist/cjs/behaviors/thermostat/AtomicWriteHandler.js +2 -2
- package/dist/cjs/behaviors/thermostat/AtomicWriteHandler.js.map +1 -1
- package/dist/cjs/behaviors/thermostat/ThermostatServer.d.ts +1 -1
- package/dist/cjs/behaviors/thermostat/ThermostatServer.d.ts.map +1 -1
- package/dist/cjs/behaviors/thermostat/ThermostatServer.js.map +1 -1
- package/dist/cjs/devices/color-temperature-light.d.ts +4 -4
- package/dist/cjs/devices/dimmable-light.d.ts +4 -4
- package/dist/cjs/devices/dimmable-plug-in-unit.d.ts +4 -4
- package/dist/cjs/devices/extended-color-light.d.ts +4 -4
- package/dist/cjs/devices/mounted-dimmable-load-control.d.ts +4 -4
- package/dist/cjs/devices/mounted-on-off-control.d.ts +4 -4
- package/dist/cjs/devices/on-off-light.d.ts +4 -4
- package/dist/cjs/devices/on-off-plug-in-unit.d.ts +4 -4
- package/dist/cjs/endpoint/Agent.d.ts +5 -0
- package/dist/cjs/endpoint/Agent.d.ts.map +1 -1
- package/dist/cjs/endpoint/Agent.js +9 -0
- package/dist/cjs/endpoint/Agent.js.map +1 -1
- package/dist/cjs/endpoint/properties/Behaviors.js +1 -1
- package/dist/cjs/endpoint/properties/Behaviors.js.map +1 -1
- package/dist/cjs/node/integration/ProtocolService.js +1 -1
- package/dist/cjs/node/integration/ProtocolService.js.map +1 -1
- package/dist/esm/behavior/Behavior.d.ts +2 -2
- package/dist/esm/behavior/Behavior.d.ts.map +1 -1
- package/dist/esm/behavior/Behavior.js +1 -1
- package/dist/esm/behavior/Behavior.js.map +1 -1
- package/dist/esm/behavior/Events.js.map +1 -1
- package/dist/esm/behavior/cluster/ClusterBehavior.d.ts +3 -2
- package/dist/esm/behavior/cluster/ClusterBehavior.d.ts.map +1 -1
- package/dist/esm/behavior/cluster/ClusterBehavior.js.map +1 -1
- package/dist/esm/behavior/cluster/ClusterBehaviorUtil.js +1 -1
- package/dist/esm/behavior/cluster/ClusterBehaviorUtil.js.map +1 -1
- package/dist/esm/behavior/internal/BehaviorBacking.js +1 -1
- package/dist/esm/behavior/internal/BehaviorBacking.js.map +1 -1
- package/dist/esm/behavior/system/mqtt/MqttInterface.js +1 -1
- package/dist/esm/behavior/system/mqtt/MqttInterface.js.map +1 -1
- package/dist/esm/behaviors/basic-information/BasicInformationServer.d.ts +2 -2
- package/dist/esm/behaviors/basic-information/BasicInformationServer.d.ts.map +1 -1
- package/dist/esm/behaviors/basic-information/BasicInformationServer.js +1 -4
- package/dist/esm/behaviors/basic-information/BasicInformationServer.js.map +1 -1
- package/dist/esm/behaviors/bridged-device-basic-information/BridgedDeviceBasicInformationServer.d.ts +1 -1
- package/dist/esm/behaviors/bridged-device-basic-information/BridgedDeviceBasicInformationServer.d.ts.map +1 -1
- package/dist/esm/behaviors/color-control/ColorControlServer.d.ts +3 -3
- package/dist/esm/behaviors/color-control/ColorControlServer.d.ts.map +1 -1
- package/dist/esm/behaviors/color-control/ColorControlServer.js +118 -3
- package/dist/esm/behaviors/color-control/ColorControlServer.js.map +1 -1
- package/dist/esm/behaviors/general-diagnostics/GeneralDiagnosticsServer.d.ts +1 -1
- package/dist/esm/behaviors/general-diagnostics/GeneralDiagnosticsServer.d.ts.map +1 -1
- package/dist/esm/behaviors/general-diagnostics/GeneralDiagnosticsServer.js.map +1 -1
- package/dist/esm/behaviors/group-key-management/GroupKeyManagementServer.d.ts +1 -1
- package/dist/esm/behaviors/group-key-management/GroupKeyManagementServer.d.ts.map +1 -1
- package/dist/esm/behaviors/group-key-management/GroupKeyManagementServer.js.map +1 -1
- package/dist/esm/behaviors/groups/GroupsServer.d.ts.map +1 -1
- package/dist/esm/behaviors/groups/GroupsServer.js +9 -1
- package/dist/esm/behaviors/groups/GroupsServer.js.map +1 -1
- package/dist/esm/behaviors/level-control/LevelControlServer.d.ts +5 -5
- package/dist/esm/behaviors/level-control/LevelControlServer.d.ts.map +1 -1
- package/dist/esm/behaviors/level-control/LevelControlServer.js +55 -28
- package/dist/esm/behaviors/level-control/LevelControlServer.js.map +1 -1
- package/dist/esm/behaviors/on-off/OnOffServer.d.ts +3 -6
- package/dist/esm/behaviors/on-off/OnOffServer.d.ts.map +1 -1
- package/dist/esm/behaviors/on-off/OnOffServer.js +59 -7
- package/dist/esm/behaviors/on-off/OnOffServer.js.map +1 -1
- package/dist/esm/behaviors/scenes-management/ScenesManagementServer.d.ts +122 -3
- package/dist/esm/behaviors/scenes-management/ScenesManagementServer.d.ts.map +1 -1
- package/dist/esm/behaviors/scenes-management/ScenesManagementServer.js +960 -1
- package/dist/esm/behaviors/scenes-management/ScenesManagementServer.js.map +2 -2
- package/dist/esm/behaviors/switch/SwitchServer.d.ts +1 -1
- package/dist/esm/behaviors/switch/SwitchServer.d.ts.map +1 -1
- package/dist/esm/behaviors/switch/SwitchServer.js.map +1 -1
- package/dist/esm/behaviors/thermostat/AtomicWriteHandler.js +2 -2
- package/dist/esm/behaviors/thermostat/AtomicWriteHandler.js.map +1 -1
- package/dist/esm/behaviors/thermostat/ThermostatServer.d.ts +1 -1
- package/dist/esm/behaviors/thermostat/ThermostatServer.d.ts.map +1 -1
- package/dist/esm/behaviors/thermostat/ThermostatServer.js.map +1 -1
- package/dist/esm/devices/color-temperature-light.d.ts +4 -4
- package/dist/esm/devices/dimmable-light.d.ts +4 -4
- package/dist/esm/devices/dimmable-plug-in-unit.d.ts +4 -4
- package/dist/esm/devices/extended-color-light.d.ts +4 -4
- package/dist/esm/devices/mounted-dimmable-load-control.d.ts +4 -4
- package/dist/esm/devices/mounted-on-off-control.d.ts +4 -4
- package/dist/esm/devices/on-off-light.d.ts +4 -4
- package/dist/esm/devices/on-off-plug-in-unit.d.ts +4 -4
- package/dist/esm/endpoint/Agent.d.ts +5 -0
- package/dist/esm/endpoint/Agent.d.ts.map +1 -1
- package/dist/esm/endpoint/Agent.js +9 -0
- package/dist/esm/endpoint/Agent.js.map +1 -1
- package/dist/esm/endpoint/properties/Behaviors.js +1 -1
- package/dist/esm/endpoint/properties/Behaviors.js.map +1 -1
- package/dist/esm/node/integration/ProtocolService.js +1 -1
- package/dist/esm/node/integration/ProtocolService.js.map +1 -1
- package/package.json +7 -7
- package/src/behavior/Behavior.ts +2 -2
- package/src/behavior/Events.ts +1 -1
- package/src/behavior/cluster/ClusterBehavior.ts +4 -2
- package/src/behavior/cluster/ClusterBehaviorUtil.ts +1 -1
- package/src/behavior/internal/BehaviorBacking.ts +1 -1
- package/src/behavior/system/mqtt/MqttInterface.ts +1 -1
- package/src/behaviors/basic-information/BasicInformationServer.ts +3 -7
- package/src/behaviors/color-control/ColorControlServer.ts +132 -3
- package/src/behaviors/general-diagnostics/GeneralDiagnosticsServer.ts +1 -1
- package/src/behaviors/group-key-management/GroupKeyManagementServer.ts +2 -2
- package/src/behaviors/groups/GroupsServer.ts +13 -1
- package/src/behaviors/level-control/LevelControlServer.ts +72 -29
- package/src/behaviors/on-off/OnOffServer.ts +78 -9
- package/src/behaviors/scenes-management/ScenesManagementServer.ts +1123 -3
- package/src/behaviors/switch/SwitchServer.ts +1 -1
- package/src/behaviors/thermostat/AtomicWriteHandler.ts +4 -4
- package/src/behaviors/thermostat/ThermostatServer.ts +1 -1
- package/src/endpoint/Agent.ts +10 -0
- package/src/endpoint/properties/Behaviors.ts +1 -1
- 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
|
-
|
|
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
|
|
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
|
+
}
|