@matter-server/ws-controller 0.2.0-alpha.0-00000000-000000000
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/LICENSE +201 -0
- package/README.md +11 -0
- package/dist/esm/controller/AttributeDataCache.d.ts +49 -0
- package/dist/esm/controller/AttributeDataCache.d.ts.map +1 -0
- package/dist/esm/controller/AttributeDataCache.js +154 -0
- package/dist/esm/controller/AttributeDataCache.js.map +6 -0
- package/dist/esm/controller/ControllerCommandHandler.d.ts +118 -0
- package/dist/esm/controller/ControllerCommandHandler.d.ts.map +1 -0
- package/dist/esm/controller/ControllerCommandHandler.js +1015 -0
- package/dist/esm/controller/ControllerCommandHandler.js.map +6 -0
- package/dist/esm/controller/LegacyDataInjector.d.ts +95 -0
- package/dist/esm/controller/LegacyDataInjector.d.ts.map +1 -0
- package/dist/esm/controller/LegacyDataInjector.js +196 -0
- package/dist/esm/controller/LegacyDataInjector.js.map +6 -0
- package/dist/esm/controller/MatterController.d.ts +59 -0
- package/dist/esm/controller/MatterController.d.ts.map +1 -0
- package/dist/esm/controller/MatterController.js +212 -0
- package/dist/esm/controller/MatterController.js.map +6 -0
- package/dist/esm/controller/Nodes.d.ts +62 -0
- package/dist/esm/controller/Nodes.d.ts.map +1 -0
- package/dist/esm/controller/Nodes.js +85 -0
- package/dist/esm/controller/Nodes.js.map +6 -0
- package/dist/esm/controller/TestNodeCommandHandler.d.ts +84 -0
- package/dist/esm/controller/TestNodeCommandHandler.d.ts.map +1 -0
- package/dist/esm/controller/TestNodeCommandHandler.js +225 -0
- package/dist/esm/controller/TestNodeCommandHandler.js.map +6 -0
- package/dist/esm/data/VendorIDs.d.ts +7 -0
- package/dist/esm/data/VendorIDs.d.ts.map +1 -0
- package/dist/esm/data/VendorIDs.js +1237 -0
- package/dist/esm/data/VendorIDs.js.map +6 -0
- package/dist/esm/example/send-command.d.ts +7 -0
- package/dist/esm/example/send-command.d.ts.map +1 -0
- package/dist/esm/example/send-command.js +60 -0
- package/dist/esm/example/send-command.js.map +6 -0
- package/dist/esm/index.d.ts +21 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +26 -0
- package/dist/esm/index.js.map +6 -0
- package/dist/esm/model/ModelMapper.d.ts +34 -0
- package/dist/esm/model/ModelMapper.d.ts.map +1 -0
- package/dist/esm/model/ModelMapper.js +62 -0
- package/dist/esm/model/ModelMapper.js.map +6 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/server/ConfigStorage.d.ts +29 -0
- package/dist/esm/server/ConfigStorage.d.ts.map +1 -0
- package/dist/esm/server/ConfigStorage.js +84 -0
- package/dist/esm/server/ConfigStorage.js.map +6 -0
- package/dist/esm/server/Converters.d.ts +53 -0
- package/dist/esm/server/Converters.d.ts.map +1 -0
- package/dist/esm/server/Converters.js +343 -0
- package/dist/esm/server/Converters.js.map +6 -0
- package/dist/esm/server/WebSocketControllerHandler.d.ts +21 -0
- package/dist/esm/server/WebSocketControllerHandler.d.ts.map +1 -0
- package/dist/esm/server/WebSocketControllerHandler.js +767 -0
- package/dist/esm/server/WebSocketControllerHandler.js.map +6 -0
- package/dist/esm/types/CommandHandler.d.ts +258 -0
- package/dist/esm/types/CommandHandler.d.ts.map +1 -0
- package/dist/esm/types/CommandHandler.js +6 -0
- package/dist/esm/types/CommandHandler.js.map +6 -0
- package/dist/esm/types/WebServer.d.ts +12 -0
- package/dist/esm/types/WebServer.d.ts.map +1 -0
- package/dist/esm/types/WebServer.js +6 -0
- package/dist/esm/types/WebServer.js.map +6 -0
- package/dist/esm/types/WebSocketMessageTypes.d.ts +478 -0
- package/dist/esm/types/WebSocketMessageTypes.d.ts.map +1 -0
- package/dist/esm/types/WebSocketMessageTypes.js +77 -0
- package/dist/esm/types/WebSocketMessageTypes.js.map +6 -0
- package/dist/esm/util/matterVersion.d.ts +12 -0
- package/dist/esm/util/matterVersion.d.ts.map +1 -0
- package/dist/esm/util/matterVersion.js +32 -0
- package/dist/esm/util/matterVersion.js.map +6 -0
- package/dist/esm/util/network.d.ts +14 -0
- package/dist/esm/util/network.d.ts.map +1 -0
- package/dist/esm/util/network.js +63 -0
- package/dist/esm/util/network.js.map +6 -0
- package/package.json +45 -0
- package/src/controller/AttributeDataCache.ts +194 -0
- package/src/controller/ControllerCommandHandler.ts +1256 -0
- package/src/controller/LegacyDataInjector.ts +314 -0
- package/src/controller/MatterController.ts +265 -0
- package/src/controller/Nodes.ts +115 -0
- package/src/controller/TestNodeCommandHandler.ts +305 -0
- package/src/data/VendorIDs.ts +1234 -0
- package/src/example/send-command.ts +82 -0
- package/src/index.ts +33 -0
- package/src/model/ModelMapper.ts +87 -0
- package/src/server/ConfigStorage.ts +112 -0
- package/src/server/Converters.ts +483 -0
- package/src/server/WebSocketControllerHandler.ts +917 -0
- package/src/tsconfig.json +7 -0
- package/src/types/CommandHandler.ts +270 -0
- package/src/types/WebServer.ts +14 -0
- package/src/types/WebSocketMessageTypes.ts +525 -0
- package/src/util/matterVersion.ts +45 -0
- package/src/util/network.ts +85 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { AttributeId, Bytes, camelize, ClusterId, isObject, Logger } from "@matter/main";
|
|
8
|
+
import { ClusterModel, FieldValue, ValueModel } from "@matter/main/model";
|
|
9
|
+
import { EndpointNumber, MATTER_EPOCH_OFFSET_S, MATTER_EPOCH_OFFSET_US } from "@matter/main/types";
|
|
10
|
+
|
|
11
|
+
const logger = new Logger("ChipToolWebSocketHandler");
|
|
12
|
+
|
|
13
|
+
/** Convert stringified numbers in hex and normal style to either number or bigint. */
|
|
14
|
+
export function parseNumber(number: string): number | bigint {
|
|
15
|
+
const parsed = number.startsWith("0x") ? BigInt(number) : parseInt(number);
|
|
16
|
+
if (typeof parsed === "number" && isNaN(parsed)) {
|
|
17
|
+
throw new Error(`Failed to parse number: ${number}`);
|
|
18
|
+
}
|
|
19
|
+
return parsed;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function convertWebSocketGenericToMatter(value: unknown, model: ValueModel, clusterModel: ClusterModel) {
|
|
23
|
+
// Handle bitmaps - convert number to object with boolean flags
|
|
24
|
+
if (typeof value === "number" && model.metabase?.metatype === "bitmap") {
|
|
25
|
+
const bitmapValue: { [key: string]: boolean | number } = {};
|
|
26
|
+
|
|
27
|
+
for (const member of clusterModel.scope.membersOf(model)) {
|
|
28
|
+
const memberName =
|
|
29
|
+
member.name !== undefined && model.name !== "FeatureMap"
|
|
30
|
+
? camelize(member.name)
|
|
31
|
+
: member.title !== undefined
|
|
32
|
+
? camelize(member.title)
|
|
33
|
+
: undefined;
|
|
34
|
+
|
|
35
|
+
if (memberName === undefined) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const constraintValue = FieldValue.numericValue(member.constraint.value);
|
|
40
|
+
if (constraintValue !== undefined) {
|
|
41
|
+
// Single bit - extract as boolean
|
|
42
|
+
bitmapValue[memberName] = (value & (1 << constraintValue)) !== 0;
|
|
43
|
+
} else {
|
|
44
|
+
const minBit = FieldValue.numericValue(member.constraint.min) ?? 0;
|
|
45
|
+
const maxBit = FieldValue.numericValue(member.constraint.max);
|
|
46
|
+
if (maxBit !== undefined) {
|
|
47
|
+
// Multi-bit field - extract value
|
|
48
|
+
const mask = ((1 << (maxBit - minBit + 1)) - 1) << minBit;
|
|
49
|
+
bitmapValue[memberName] = (value & mask) >> minBit;
|
|
50
|
+
} else {
|
|
51
|
+
// Single bit at minBit position
|
|
52
|
+
bitmapValue[memberName] = (value & (1 << minBit)) !== 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return bitmapValue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Handle bytes - convert base64 string to Uint8Array
|
|
61
|
+
if (typeof value === "string" && model.metabase?.metatype === "bytes") {
|
|
62
|
+
return Bytes.fromBase64(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Handle epoch timestamps - convert from Unix timestamps to Matter epoch
|
|
66
|
+
if (model.metabase?.metatype === "integer") {
|
|
67
|
+
if (model.type === "epoch-s" && typeof value === "number") {
|
|
68
|
+
return value + MATTER_EPOCH_OFFSET_S;
|
|
69
|
+
} else if (model.type === "epoch-us" && (typeof value === "number" || typeof value === "bigint")) {
|
|
70
|
+
return BigInt(value) + MATTER_EPOCH_OFFSET_US;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Return primitives as-is
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Converts tag-based WebSocket data (with numeric keys) back to Matter.js data format (with camelCased names).
|
|
80
|
+
* This is the reverse of convertMatterToWebSocketTagBased.
|
|
81
|
+
*/
|
|
82
|
+
export function convertWebSocketTagBasedToMatter(
|
|
83
|
+
value: unknown,
|
|
84
|
+
model: ValueModel | undefined,
|
|
85
|
+
clusterModel: ClusterModel,
|
|
86
|
+
): unknown {
|
|
87
|
+
if (model === undefined || value === null) {
|
|
88
|
+
return value; // Return null/undefined values as-is
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Handle lists
|
|
92
|
+
if (Array.isArray(value) && model.type === "list") {
|
|
93
|
+
return value.map(v => convertWebSocketTagBasedToMatter(v, model.members[0], clusterModel));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Handle structs - convert numeric keys to camelCased member names
|
|
97
|
+
if (isObject(value) && model.metabase?.name === "struct") {
|
|
98
|
+
const valueKeys = Object.keys(value);
|
|
99
|
+
const result: { [key: string]: unknown } = {};
|
|
100
|
+
|
|
101
|
+
// Build a map of member ID to member for efficient lookup
|
|
102
|
+
const memberById: { [id: number]: ValueModel } = {};
|
|
103
|
+
for (const member of model.members) {
|
|
104
|
+
if (member.id !== undefined) {
|
|
105
|
+
memberById[member.id] = member;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const key of valueKeys) {
|
|
110
|
+
const memberId = parseInt(key);
|
|
111
|
+
if (!isNaN(memberId) && memberById[memberId]) {
|
|
112
|
+
const member = memberById[memberId];
|
|
113
|
+
result[camelize(member.name)] = convertWebSocketTagBasedToMatter(value[key], member, clusterModel);
|
|
114
|
+
} else {
|
|
115
|
+
// Keep unknown keys as-is (fallback for unknown attributes)
|
|
116
|
+
result[key] = value[key];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return convertWebSocketGenericToMatter(value, model, clusterModel);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Converts camelized name-based WebSocket data to Matter.js data format. Mainly to ensure binary and epoch data
|
|
127
|
+
*/
|
|
128
|
+
export function convertCommandDataToMatter(
|
|
129
|
+
value: unknown,
|
|
130
|
+
model: ValueModel | undefined,
|
|
131
|
+
clusterModel: ClusterModel,
|
|
132
|
+
): unknown {
|
|
133
|
+
if (model === undefined || value === null) {
|
|
134
|
+
return value; // Return null/undefined values as-is
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Handle lists
|
|
138
|
+
if (Array.isArray(value) && model.type === "list") {
|
|
139
|
+
return value.map(v => convertCommandDataToMatter(v, model.members[0], clusterModel));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Handle structs - convert numeric keys to camelCased member names
|
|
143
|
+
if (isObject(value) && model.metabase?.name === "struct") {
|
|
144
|
+
const valueKeys = Object.keys(value);
|
|
145
|
+
const result: { [key: string]: unknown } = {};
|
|
146
|
+
|
|
147
|
+
// Build a map of member ID to member for efficient lookup
|
|
148
|
+
const memberByName: { [name: string]: ValueModel } = {};
|
|
149
|
+
for (const member of model.members) {
|
|
150
|
+
if (member.name !== undefined) {
|
|
151
|
+
memberByName[camelize(member.name)] = member;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const key of valueKeys) {
|
|
156
|
+
if (memberByName[key]) {
|
|
157
|
+
const member = memberByName[key];
|
|
158
|
+
result[key] = convertCommandDataToMatter(value[key], member, clusterModel);
|
|
159
|
+
} else {
|
|
160
|
+
// Keep unknown keys as-is (fallback for unknown attributes)
|
|
161
|
+
result[key] = value[key];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return convertWebSocketGenericToMatter(value, model, clusterModel);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Uses the matter.js Model to convert the response data for read, subscribe and invoke into a tag-based response
|
|
172
|
+
* including conversion of data types.
|
|
173
|
+
*/
|
|
174
|
+
export function convertMatterToWebSocketTagBased(
|
|
175
|
+
value: unknown,
|
|
176
|
+
model: ValueModel | undefined,
|
|
177
|
+
clusterModel: ClusterModel | undefined,
|
|
178
|
+
): unknown {
|
|
179
|
+
if (value === null) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (model === undefined) {
|
|
183
|
+
// Do some simple conversions when we have unknown attributes
|
|
184
|
+
if (Bytes.isBytes(value)) {
|
|
185
|
+
return `${Bytes.toBase64(value)}`;
|
|
186
|
+
}
|
|
187
|
+
if (isObject(value) || !["string", "number", "bigint", "boolean", "undefined"].includes(typeof value)) {
|
|
188
|
+
return null; // We cannot convert this
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
if (Array.isArray(value) && model.type === "list") {
|
|
194
|
+
return value.map(v => convertMatterToWebSocketTagBased(v, model.members[0], clusterModel));
|
|
195
|
+
}
|
|
196
|
+
if (isObject(value) && model.metabase?.name === "struct") {
|
|
197
|
+
const valueKeys = Object.keys(value);
|
|
198
|
+
const result: { [key: string]: any } = {};
|
|
199
|
+
for (const member of model.members) {
|
|
200
|
+
const name = camelize(member.name);
|
|
201
|
+
if (member.name !== undefined && member.id !== undefined && valueKeys.includes(name)) {
|
|
202
|
+
result[member.id] = convertMatterToWebSocketTagBased(value[name], member, clusterModel);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
if (isObject(value) && model.metabase?.metatype === "bitmap") {
|
|
208
|
+
if (clusterModel !== undefined) {
|
|
209
|
+
let numberValue = 0;
|
|
210
|
+
|
|
211
|
+
for (const member of clusterModel.scope.membersOf(model)) {
|
|
212
|
+
const memberValue =
|
|
213
|
+
member.name !== undefined && value[camelize(member.name)]
|
|
214
|
+
? value[camelize(member.name)]
|
|
215
|
+
: member.title !== undefined && value[camelize(member.title)]
|
|
216
|
+
? value[camelize(member.title)]
|
|
217
|
+
: undefined;
|
|
218
|
+
|
|
219
|
+
if (!memberValue) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (typeof memberValue !== "boolean" && typeof memberValue !== "number") {
|
|
223
|
+
throw new Error("Invalid bitmap value", memberValue);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const constraintValue = FieldValue.numericValue(member.constraint.value);
|
|
227
|
+
if (constraintValue !== undefined) {
|
|
228
|
+
numberValue |= 1 << constraintValue;
|
|
229
|
+
} else {
|
|
230
|
+
const minBit = FieldValue.numericValue(member.constraint.min) ?? 0;
|
|
231
|
+
numberValue |= typeof memberValue === "boolean" ? 1 : memberValue << minBit;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return numberValue;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (value instanceof Uint8Array && model.metabase?.metatype === "bytes") {
|
|
240
|
+
value = `${Bytes.toBase64(value)}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (model.metabase?.metatype === "integer") {
|
|
244
|
+
// Convert Epoch timestamps to Unix timestamps we use internally
|
|
245
|
+
if (model.type === "epoch-s" && typeof value === "number") {
|
|
246
|
+
value -= MATTER_EPOCH_OFFSET_S;
|
|
247
|
+
} else if (model.type === "epoch-us" && (typeof value === "number" || typeof value === "bigint")) {
|
|
248
|
+
value = BigInt(value) - MATTER_EPOCH_OFFSET_US;
|
|
249
|
+
}
|
|
250
|
+
return value;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Serialize to JSON with BigInt support.
|
|
258
|
+
* - BigInt values within safe integer range are converted to numbers
|
|
259
|
+
* - Large BigInt values are output as raw decimal numbers (not quoted strings)
|
|
260
|
+
*/
|
|
261
|
+
export function toBigIntAwareJson(object: object, spaces?: number): string {
|
|
262
|
+
const replacements = new Array<{ from: string; to: string }>();
|
|
263
|
+
let result = JSON.stringify(
|
|
264
|
+
object,
|
|
265
|
+
(_key, value) => {
|
|
266
|
+
if (typeof value === "bigint") {
|
|
267
|
+
if (value > Number.MAX_SAFE_INTEGER) {
|
|
268
|
+
// Store replacement: quoted hex string -> raw decimal number
|
|
269
|
+
replacements.push({ from: `"0x${value.toString(16)}"`, to: value.toString() });
|
|
270
|
+
return `0x${value.toString(16)}`;
|
|
271
|
+
} else {
|
|
272
|
+
return Number(value);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return value;
|
|
276
|
+
},
|
|
277
|
+
spaces,
|
|
278
|
+
);
|
|
279
|
+
// Large numbers need to be raw (not quoted) in the output, so replace hex placeholders with decimal
|
|
280
|
+
// This handles both object values and array elements
|
|
281
|
+
if (replacements.length > 0) {
|
|
282
|
+
replacements.forEach(({ from, to }) => {
|
|
283
|
+
result = result.replaceAll(from, to);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Marker prefix for large numbers that need BigInt conversion */
|
|
291
|
+
const BIGINT_MARKER = "__BIGINT__";
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Parse JSON with BigInt support for large numbers that exceed JavaScript precision.
|
|
295
|
+
* Numbers with 15+ digits that exceed MAX_SAFE_INTEGER are converted to BigInt.
|
|
296
|
+
*/
|
|
297
|
+
export function parseBigIntAwareJson(json: string): unknown {
|
|
298
|
+
// Pre-process: Replace large numbers (15+ digits) with marked string placeholders
|
|
299
|
+
// This must happen before JSON.parse to preserve precision
|
|
300
|
+
// Match numbers after colon (object values) or after [ or , (array elements)
|
|
301
|
+
const processed = json.replace(/([:,[])\s*(\d{15,})(?=[,}\]\s])/g, (match, prefix, number) => {
|
|
302
|
+
const num = BigInt(number);
|
|
303
|
+
if (num > Number.MAX_SAFE_INTEGER) {
|
|
304
|
+
return `${prefix}"${BIGINT_MARKER}${number}"`;
|
|
305
|
+
}
|
|
306
|
+
return match;
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Parse with reviver to convert marked strings back to BigInt
|
|
310
|
+
return JSON.parse(processed, (_key, value) => {
|
|
311
|
+
if (typeof value === "string" && value.startsWith(BIGINT_MARKER)) {
|
|
312
|
+
return BigInt(value.slice(BIGINT_MARKER.length));
|
|
313
|
+
}
|
|
314
|
+
return value;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Chip JSON-like data strings can contain long numbers that are not supported by JSON.parse */
|
|
319
|
+
function parseChipJSON(json: string) {
|
|
320
|
+
json = json.replace(/: (\d{15,})[,}]/g, (match, number) => {
|
|
321
|
+
const num = BigInt(number);
|
|
322
|
+
if (num > Number.MAX_SAFE_INTEGER) {
|
|
323
|
+
return match.replace(number, `"0x${num.toString(16)}"`);
|
|
324
|
+
}
|
|
325
|
+
return match;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return JSON.parse(json);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Use the matter.js model to convert the incoming data for write and invoke commands into the expected format. */
|
|
332
|
+
export function convertWebsocketDataToMatter(value: any, model: ValueModel): any {
|
|
333
|
+
if (value === undefined) {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
if (value === "null" || value === null) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (model.type === "list") {
|
|
341
|
+
if (typeof value === "string") {
|
|
342
|
+
value = parseChipJSON(value);
|
|
343
|
+
}
|
|
344
|
+
if (Array.isArray(value)) {
|
|
345
|
+
return value.map(v => convertWebsocketDataToMatter(v, model.members[0]));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (model.metabase?.name === "struct") {
|
|
350
|
+
if (typeof value === "string") {
|
|
351
|
+
value = parseChipJSON(value);
|
|
352
|
+
}
|
|
353
|
+
if (typeof value === "object") {
|
|
354
|
+
const members = model.members.reduce(
|
|
355
|
+
(acc, member) => {
|
|
356
|
+
if (member.name !== undefined) {
|
|
357
|
+
acc[member.name.toLowerCase()] = member;
|
|
358
|
+
}
|
|
359
|
+
return acc;
|
|
360
|
+
},
|
|
361
|
+
{} as { [key: string]: ValueModel },
|
|
362
|
+
);
|
|
363
|
+
const valueKeys = Object.keys(value);
|
|
364
|
+
const result: { [key: string]: unknown } = {};
|
|
365
|
+
valueKeys.forEach(key => {
|
|
366
|
+
const member = members[camelize(key).toLowerCase()];
|
|
367
|
+
if (member !== undefined) {
|
|
368
|
+
result[camelize(member.name)] = convertWebsocketDataToMatter(value[key], member);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (
|
|
376
|
+
(typeof value === "number" || typeof value === "bigint") &&
|
|
377
|
+
(model.metabase?.metatype === "integer" || model.metabase?.metatype === "enum")
|
|
378
|
+
) {
|
|
379
|
+
// Convert Epoch timestamps to Unix timestamps we use internally
|
|
380
|
+
if (model.type === "epoch-s" && typeof value === "number") {
|
|
381
|
+
value += MATTER_EPOCH_OFFSET_S;
|
|
382
|
+
} else if (model.type === "epoch-us") {
|
|
383
|
+
value = BigInt(value) + MATTER_EPOCH_OFFSET_US;
|
|
384
|
+
}
|
|
385
|
+
return value;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (typeof value === "string") {
|
|
389
|
+
if (model.metabase?.metatype === "bytes" && value.startsWith("hex:")) {
|
|
390
|
+
return Bytes.fromHex(value.slice(4));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (model.metabase?.metatype === "bitmap") {
|
|
394
|
+
const numberValue = parseInt(value);
|
|
395
|
+
if (isNaN(numberValue)) {
|
|
396
|
+
throw new Error("Invalid bitmap value");
|
|
397
|
+
}
|
|
398
|
+
const bitmapValue: { [key: string]: boolean } = {};
|
|
399
|
+
model.members.forEach(member => {
|
|
400
|
+
if (
|
|
401
|
+
member.constraint !== undefined &&
|
|
402
|
+
member.name !== undefined &&
|
|
403
|
+
numberValue & (1 << parseInt(member.constraint as unknown as string))
|
|
404
|
+
) {
|
|
405
|
+
bitmapValue[camelize(member.name)] = true;
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
return bitmapValue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (
|
|
412
|
+
((model.metabase?.metatype === "integer" || model.metabase?.metatype === "enum") &&
|
|
413
|
+
value.startsWith("0x") &&
|
|
414
|
+
value.match(/^0x[\da-fA-F]+$/)) ||
|
|
415
|
+
value.match(/^-?[1-9]\d*$/) ||
|
|
416
|
+
value === "0"
|
|
417
|
+
) {
|
|
418
|
+
let numberValue = parseNumber(value);
|
|
419
|
+
if (model.type === "epoch-s" && typeof numberValue === "number") {
|
|
420
|
+
numberValue += MATTER_EPOCH_OFFSET_S;
|
|
421
|
+
} else if (model.type === "epoch-us") {
|
|
422
|
+
numberValue = BigInt(value) + MATTER_EPOCH_OFFSET_US;
|
|
423
|
+
}
|
|
424
|
+
return numberValue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (model.metabase?.metatype === "boolean") {
|
|
428
|
+
return value === "true" || value === "1" || value === "True";
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (model.metabase?.metatype === "string") {
|
|
432
|
+
return value;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
logger.warn("UNHANDLED value ...", value, model.type, model.metatype, model.metabase?.metatype);
|
|
437
|
+
|
|
438
|
+
return value;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function getDateAsString(date: Date) {
|
|
442
|
+
const year = date.getFullYear();
|
|
443
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
444
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
445
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
446
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
447
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
448
|
+
const milliseconds = String(date.getMilliseconds()).padStart(3, "0");
|
|
449
|
+
const microseconds = "000"; // JavaScript Date object does not support microseconds
|
|
450
|
+
|
|
451
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}${microseconds}`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function buildAttributePath(endpointId: number, clusterId: number, attributeId: number): string {
|
|
455
|
+
return `${endpointId}/${clusterId}/${attributeId}`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Parse an attribute path string into its components.
|
|
460
|
+
* Supports wildcards (*) for endpoint, cluster, and attribute IDs.
|
|
461
|
+
* Non-numeric values are treated as wildcards and returned as undefined.
|
|
462
|
+
*
|
|
463
|
+
* @param path - Attribute path string in format "endpoint/cluster/attribute"
|
|
464
|
+
* @returns Object with endpointId, clusterId, attributeId - each undefined if wildcard
|
|
465
|
+
*/
|
|
466
|
+
export function splitAttributePath(path: string): {
|
|
467
|
+
endpointId: EndpointNumber | undefined;
|
|
468
|
+
clusterId: ClusterId | undefined;
|
|
469
|
+
attributeId: AttributeId | undefined;
|
|
470
|
+
} {
|
|
471
|
+
const [endpointStr, clusterStr, attributeStr] = path.split("/");
|
|
472
|
+
|
|
473
|
+
// Non-numeric values (like "*") are treated as wildcards (undefined)
|
|
474
|
+
const endpointNum = /^\d+$/.test(endpointStr) ? parseInt(endpointStr, 10) : undefined;
|
|
475
|
+
const clusterNum = /^\d+$/.test(clusterStr) ? parseInt(clusterStr, 10) : undefined;
|
|
476
|
+
const attributeNum = /^\d+$/.test(attributeStr) ? parseInt(attributeStr, 10) : undefined;
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
endpointId: endpointNum !== undefined ? EndpointNumber(endpointNum) : undefined,
|
|
480
|
+
clusterId: clusterNum !== undefined ? ClusterId(clusterNum) : undefined,
|
|
481
|
+
attributeId: attributeNum !== undefined ? AttributeId(attributeNum) : undefined,
|
|
482
|
+
};
|
|
483
|
+
}
|