@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.
Files changed (95) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +11 -0
  3. package/dist/esm/controller/AttributeDataCache.d.ts +49 -0
  4. package/dist/esm/controller/AttributeDataCache.d.ts.map +1 -0
  5. package/dist/esm/controller/AttributeDataCache.js +154 -0
  6. package/dist/esm/controller/AttributeDataCache.js.map +6 -0
  7. package/dist/esm/controller/ControllerCommandHandler.d.ts +118 -0
  8. package/dist/esm/controller/ControllerCommandHandler.d.ts.map +1 -0
  9. package/dist/esm/controller/ControllerCommandHandler.js +1015 -0
  10. package/dist/esm/controller/ControllerCommandHandler.js.map +6 -0
  11. package/dist/esm/controller/LegacyDataInjector.d.ts +95 -0
  12. package/dist/esm/controller/LegacyDataInjector.d.ts.map +1 -0
  13. package/dist/esm/controller/LegacyDataInjector.js +196 -0
  14. package/dist/esm/controller/LegacyDataInjector.js.map +6 -0
  15. package/dist/esm/controller/MatterController.d.ts +59 -0
  16. package/dist/esm/controller/MatterController.d.ts.map +1 -0
  17. package/dist/esm/controller/MatterController.js +212 -0
  18. package/dist/esm/controller/MatterController.js.map +6 -0
  19. package/dist/esm/controller/Nodes.d.ts +62 -0
  20. package/dist/esm/controller/Nodes.d.ts.map +1 -0
  21. package/dist/esm/controller/Nodes.js +85 -0
  22. package/dist/esm/controller/Nodes.js.map +6 -0
  23. package/dist/esm/controller/TestNodeCommandHandler.d.ts +84 -0
  24. package/dist/esm/controller/TestNodeCommandHandler.d.ts.map +1 -0
  25. package/dist/esm/controller/TestNodeCommandHandler.js +225 -0
  26. package/dist/esm/controller/TestNodeCommandHandler.js.map +6 -0
  27. package/dist/esm/data/VendorIDs.d.ts +7 -0
  28. package/dist/esm/data/VendorIDs.d.ts.map +1 -0
  29. package/dist/esm/data/VendorIDs.js +1237 -0
  30. package/dist/esm/data/VendorIDs.js.map +6 -0
  31. package/dist/esm/example/send-command.d.ts +7 -0
  32. package/dist/esm/example/send-command.d.ts.map +1 -0
  33. package/dist/esm/example/send-command.js +60 -0
  34. package/dist/esm/example/send-command.js.map +6 -0
  35. package/dist/esm/index.d.ts +21 -0
  36. package/dist/esm/index.d.ts.map +1 -0
  37. package/dist/esm/index.js +26 -0
  38. package/dist/esm/index.js.map +6 -0
  39. package/dist/esm/model/ModelMapper.d.ts +34 -0
  40. package/dist/esm/model/ModelMapper.d.ts.map +1 -0
  41. package/dist/esm/model/ModelMapper.js +62 -0
  42. package/dist/esm/model/ModelMapper.js.map +6 -0
  43. package/dist/esm/package.json +3 -0
  44. package/dist/esm/server/ConfigStorage.d.ts +29 -0
  45. package/dist/esm/server/ConfigStorage.d.ts.map +1 -0
  46. package/dist/esm/server/ConfigStorage.js +84 -0
  47. package/dist/esm/server/ConfigStorage.js.map +6 -0
  48. package/dist/esm/server/Converters.d.ts +53 -0
  49. package/dist/esm/server/Converters.d.ts.map +1 -0
  50. package/dist/esm/server/Converters.js +343 -0
  51. package/dist/esm/server/Converters.js.map +6 -0
  52. package/dist/esm/server/WebSocketControllerHandler.d.ts +21 -0
  53. package/dist/esm/server/WebSocketControllerHandler.d.ts.map +1 -0
  54. package/dist/esm/server/WebSocketControllerHandler.js +767 -0
  55. package/dist/esm/server/WebSocketControllerHandler.js.map +6 -0
  56. package/dist/esm/types/CommandHandler.d.ts +258 -0
  57. package/dist/esm/types/CommandHandler.d.ts.map +1 -0
  58. package/dist/esm/types/CommandHandler.js +6 -0
  59. package/dist/esm/types/CommandHandler.js.map +6 -0
  60. package/dist/esm/types/WebServer.d.ts +12 -0
  61. package/dist/esm/types/WebServer.d.ts.map +1 -0
  62. package/dist/esm/types/WebServer.js +6 -0
  63. package/dist/esm/types/WebServer.js.map +6 -0
  64. package/dist/esm/types/WebSocketMessageTypes.d.ts +478 -0
  65. package/dist/esm/types/WebSocketMessageTypes.d.ts.map +1 -0
  66. package/dist/esm/types/WebSocketMessageTypes.js +77 -0
  67. package/dist/esm/types/WebSocketMessageTypes.js.map +6 -0
  68. package/dist/esm/util/matterVersion.d.ts +12 -0
  69. package/dist/esm/util/matterVersion.d.ts.map +1 -0
  70. package/dist/esm/util/matterVersion.js +32 -0
  71. package/dist/esm/util/matterVersion.js.map +6 -0
  72. package/dist/esm/util/network.d.ts +14 -0
  73. package/dist/esm/util/network.d.ts.map +1 -0
  74. package/dist/esm/util/network.js +63 -0
  75. package/dist/esm/util/network.js.map +6 -0
  76. package/package.json +45 -0
  77. package/src/controller/AttributeDataCache.ts +194 -0
  78. package/src/controller/ControllerCommandHandler.ts +1256 -0
  79. package/src/controller/LegacyDataInjector.ts +314 -0
  80. package/src/controller/MatterController.ts +265 -0
  81. package/src/controller/Nodes.ts +115 -0
  82. package/src/controller/TestNodeCommandHandler.ts +305 -0
  83. package/src/data/VendorIDs.ts +1234 -0
  84. package/src/example/send-command.ts +82 -0
  85. package/src/index.ts +33 -0
  86. package/src/model/ModelMapper.ts +87 -0
  87. package/src/server/ConfigStorage.ts +112 -0
  88. package/src/server/Converters.ts +483 -0
  89. package/src/server/WebSocketControllerHandler.ts +917 -0
  90. package/src/tsconfig.json +7 -0
  91. package/src/types/CommandHandler.ts +270 -0
  92. package/src/types/WebServer.ts +14 -0
  93. package/src/types/WebSocketMessageTypes.ts +525 -0
  94. package/src/util/matterVersion.ts +45 -0
  95. package/src/util/network.ts +85 -0
@@ -0,0 +1,525 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ export type AttributesData = { [key: string]: unknown };
8
+
9
+ export interface MatterNode {
10
+ node_id: number | bigint;
11
+ date_commissioned: string;
12
+ last_interview: string;
13
+ interview_version: number;
14
+ available: boolean;
15
+ is_bridge: boolean;
16
+ attributes: AttributesData;
17
+ attribute_subscriptions: []; // ???
18
+ }
19
+
20
+ export interface APICommands {
21
+ start_listening: {
22
+ requestArgs: void;
23
+ response: Array<MatterNode>;
24
+ };
25
+ set_default_fabric_label: {
26
+ requestArgs: {
27
+ /** Fabric label (null to clear) */
28
+ label: string | null;
29
+ };
30
+ response: null;
31
+ };
32
+ diagnostics: {
33
+ requestArgs: {};
34
+ response: {
35
+ info: ServerInfoMessage;
36
+ nodes: Array<MatterNode>;
37
+ /** Last 25 node events */
38
+ events: Array<MatterNodeEvent>;
39
+ };
40
+ };
41
+ server_info: {
42
+ requestArgs: {};
43
+ response: ServerInfoMessage;
44
+ };
45
+ get_nodes: {
46
+ requestArgs: {
47
+ only_available?: boolean;
48
+ };
49
+ response: Array<MatterNode>;
50
+ };
51
+ get_node: {
52
+ requestArgs: {
53
+ node_id: number | bigint; // ????
54
+ };
55
+ response: MatterNode; // ????
56
+ };
57
+ commission_with_code: {
58
+ requestArgs: {
59
+ code: string;
60
+ network_only?: boolean;
61
+ };
62
+ response: MatterNode;
63
+ };
64
+ commission_on_network: {
65
+ requestArgs: {
66
+ setup_pin_code: number;
67
+ /** Discovery filter type: 0=None, 1=ShortDiscriminator, 2=LongDiscriminator, 3=VendorId, 4=DeviceType */
68
+ filter_type?: number;
69
+ /** Filter value (discriminator, vendor ID, or device type depending on filter_type) */
70
+ filter?: number;
71
+ /** Direct IP address for commissioning */
72
+ ip_addr?: string;
73
+ };
74
+ response: MatterNode;
75
+ };
76
+ set_wifi_credentials: {
77
+ requestArgs: {
78
+ ssid: string;
79
+ credentials: string;
80
+ };
81
+ response: {};
82
+ };
83
+ set_thread_dataset: {
84
+ requestArgs: {
85
+ dataset: string;
86
+ };
87
+ response: {};
88
+ };
89
+ open_commissioning_window: {
90
+ requestArgs: {
91
+ node_id: number | bigint; //????
92
+ timeout: number; // seconds
93
+ iteration?: number; // 1000
94
+ option?: number; // 1??
95
+ discriminator?: number | null; // ???
96
+ };
97
+ response: {
98
+ setup_pin_code: number;
99
+ setup_manual_code: string;
100
+ setup_qr_code: string;
101
+ };
102
+ };
103
+ discover: {
104
+ requestArgs: {};
105
+ response: CommissionableNodeData[];
106
+ };
107
+ interview_node: {
108
+ requestArgs: {
109
+ node_id: number | bigint; // ???
110
+ };
111
+ response: null;
112
+ };
113
+ device_command: {
114
+ requestArgs: {
115
+ node_id: number | bigint; // ??
116
+ endpoint_id: number;
117
+ cluster_id: number;
118
+ command_name: string;
119
+ payload: unknown;
120
+ response_type: unknown; // ????
121
+ timed_request_timeout_ms?: number | null;
122
+ interaction_timeout_ms?: number | null;
123
+ };
124
+ response: unknown;
125
+ };
126
+ remove_node: {
127
+ requestArgs: {
128
+ node_id: number | bigint; // ???
129
+ };
130
+ response: null;
131
+ };
132
+ get_vendor_names: {
133
+ requestArgs: {
134
+ filter_vendors?: Array<number>;
135
+ };
136
+ response: { [key: string]: string };
137
+ };
138
+ subscribe_attribute: {
139
+ requestArgs: {};
140
+ response: {};
141
+ };
142
+ read_attribute: {
143
+ requestArgs: {
144
+ node_id: number | bigint;
145
+ /** Single attribute path or array of paths. Supports wildcards (*) for cluster and attribute IDs. */
146
+ attribute_path: string | string[];
147
+ /** Filter by fabric (default: false) */
148
+ fabric_filtered?: boolean;
149
+ };
150
+ response: AttributesData;
151
+ };
152
+ write_attribute: {
153
+ requestArgs: {
154
+ node_id: number | bigint; //???,
155
+ attribute_path: string;
156
+ value: unknown;
157
+ };
158
+ response: Array<{
159
+ Path: {
160
+ EndpointId: number;
161
+ ClusterId: number;
162
+ AttributeId: number;
163
+ };
164
+ Status: number;
165
+ }>;
166
+ };
167
+ ping_node: {
168
+ requestArgs: {
169
+ node_id: number | bigint;
170
+ /** Number of ping attempts per IP address (default: 1) */
171
+ attempts?: number;
172
+ };
173
+ response: { [key: string]: boolean };
174
+ };
175
+ import_test_node: {
176
+ requestArgs: {
177
+ dump: string;
178
+ };
179
+ response: null;
180
+ };
181
+ get_node_ip_addresses: {
182
+ requestArgs: {
183
+ node_id: number | bigint; // ????
184
+ prefer_cache: boolean;
185
+ scoped: boolean;
186
+ };
187
+ response: Array<string>;
188
+ };
189
+ check_node_update: {
190
+ requestArgs: { node_id: number | bigint };
191
+ response: MatterSoftwareVersion | null;
192
+ };
193
+ update_node: {
194
+ requestArgs: { node_id: number | bigint; software_version: number | string };
195
+ response: MatterSoftwareVersion | null;
196
+ };
197
+ discover_commissionable_nodes: {
198
+ requestArgs: {};
199
+ response: CommissionableNodeData[];
200
+ };
201
+ get_matter_fabrics: {
202
+ requestArgs: { node_id: number | bigint };
203
+ response: MatterFabricData[];
204
+ };
205
+ remove_matter_fabric: {
206
+ requestArgs: {
207
+ node_id: number | bigint;
208
+ fabric_index: number;
209
+ };
210
+ response: {};
211
+ };
212
+ set_acl_entry: {
213
+ requestArgs: {
214
+ node_id: number | bigint;
215
+ entry: AccessControlEntry[];
216
+ };
217
+ response: AttributeWriteResult[] | null;
218
+ };
219
+ set_node_binding: {
220
+ requestArgs: {
221
+ node_id: number | bigint;
222
+ endpoint: number;
223
+ bindings: BindingTarget[];
224
+ };
225
+ response: AttributeWriteResult[] | null;
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Access Control Entry structure for set_acl_entry command.
231
+ * Uses snake_case field names matching our API convention.
232
+ */
233
+ export interface AccessControlEntry {
234
+ /** 1=View, 3=Operate, 4=Manage, 5=Administer */
235
+ privilege: number;
236
+ /** 1=PASE, 2=CASE, 3=Group */
237
+ auth_mode: number;
238
+ /** List of subject NodeIds or GroupIds */
239
+ subjects: Array<number | bigint> | null;
240
+ /** Optional target restrictions */
241
+ targets: AccessControlTarget[] | null;
242
+ }
243
+
244
+ export interface AccessControlTarget {
245
+ cluster: number | null;
246
+ endpoint: number | null;
247
+ device_type: number | null;
248
+ }
249
+
250
+ /**
251
+ * Binding target structure for set_node_binding command.
252
+ * Matches Python Matter Server's TargetStruct.
253
+ */
254
+ export interface BindingTarget {
255
+ /** Target node ID */
256
+ node: number | bigint | null;
257
+ /** Target group ID */
258
+ group: number | null;
259
+ /** Target endpoint */
260
+ endpoint: number | null;
261
+ /** Target cluster */
262
+ cluster: number | null;
263
+ }
264
+
265
+ /** Attribute write result for set_acl_entry and set_node_binding responses */
266
+ export interface AttributeWriteResult {
267
+ path: {
268
+ endpoint_id: number;
269
+ cluster_id: number;
270
+ attribute_id: number;
271
+ };
272
+ status: number;
273
+ }
274
+
275
+ export interface ServerInfoMessage {
276
+ fabric_id: bigint; // not number
277
+ compressed_fabric_id: bigint; // not number
278
+ schema_version: number;
279
+ min_supported_schema_version: number;
280
+ sdk_version: string;
281
+ wifi_credentials_set: boolean;
282
+ thread_credentials_set: boolean;
283
+ bluetooth_enabled: boolean;
284
+ }
285
+
286
+ /**
287
+ * Matter node event structure for node_event WebSocket event.
288
+ * Matches Python Matter Server's MatterNodeEvent.
289
+ */
290
+ export interface MatterNodeEvent {
291
+ node_id: number | bigint;
292
+ endpoint_id: number;
293
+ cluster_id: number;
294
+ event_id: number;
295
+ event_number: number | bigint;
296
+ priority: number; // 0=Debug, 1=Info, 2=Critical
297
+ timestamp: number | bigint;
298
+ timestamp_type: number; // 0=System, 1=Epoch, 2=POSIX
299
+ data: unknown | null;
300
+ }
301
+
302
+ interface APIEvents {
303
+ node_added: {
304
+ data: MatterNode;
305
+ };
306
+ node_updated: {
307
+ data: MatterNode;
308
+ };
309
+ node_removed: {
310
+ data: number | bigint;
311
+ };
312
+ node_event: {
313
+ data: MatterNodeEvent;
314
+ };
315
+ attribute_updated: {
316
+ data: [node_id: number | bigint, attribute_path: string, value: unknown];
317
+ };
318
+ server_shutdown: {
319
+ data: Record<string, never>;
320
+ };
321
+ endpoint_added: {
322
+ data: { node_id: number | bigint; endpoint_id: number };
323
+ };
324
+ endpoint_removed: {
325
+ data: { node_id: number | bigint; endpoint_id: number };
326
+ };
327
+ server_info_updated: {
328
+ data: ServerInfoMessage;
329
+ };
330
+ }
331
+
332
+ export type EventMessage<E extends keyof APIEvents> = {
333
+ event: E;
334
+ data: APIEvents[E]["data"];
335
+ };
336
+ export type EventTypes = keyof APIEvents;
337
+
338
+ export interface WebSocketConfig {
339
+ host: string;
340
+ port: string;
341
+ scheme: string;
342
+ path: string;
343
+ }
344
+
345
+ export enum UpdateSource {
346
+ MAIN_NET_DCL = "main-net-dcl",
347
+ TEST_NET_DCL = "test-net-dcl",
348
+ LOCAL = "local",
349
+ }
350
+
351
+ export interface MatterSoftwareVersion {
352
+ vid: number;
353
+ pid: number;
354
+ software_version: number;
355
+ software_version_string: string;
356
+ firmware_information?: string;
357
+ min_applicable_software_version: number;
358
+ max_applicable_software_version: number;
359
+ release_notes_url?: string;
360
+ update_source: UpdateSource;
361
+ }
362
+
363
+ export interface CommissioningParameters {
364
+ setup_pin_code: number;
365
+ setup_manual_code: string;
366
+ setup_qr_code: string;
367
+ }
368
+
369
+ export interface CommissionableNodeData {
370
+ instance_name?: string;
371
+ host_name?: string;
372
+ port?: number;
373
+ long_discriminator?: number;
374
+ vendor_id?: number;
375
+ product_id?: number;
376
+ commissioning_mode?: number;
377
+ device_type?: number;
378
+ device_name?: string;
379
+ pairing_instruction?: string;
380
+ pairing_hint?: number;
381
+ mrp_retry_interval_idle?: number;
382
+ mrp_retry_interval_active?: number;
383
+ supports_tcp?: boolean;
384
+ addresses?: string[];
385
+ rotating_id?: string;
386
+ }
387
+
388
+ export interface MatterFabricData {
389
+ fabric_id?: number | bigint;
390
+ vendor_id?: number;
391
+ fabric_index?: number;
392
+ fabric_label?: string;
393
+ vendor_name?: string;
394
+ }
395
+
396
+ export type NotificationType = "success" | "info" | "warning" | "error";
397
+ export type NodePingResult = Record<string, boolean>;
398
+
399
+ /**
400
+ * WebSocket Command Message generic type.
401
+ */
402
+ export interface CommandMessage {
403
+ message_id: string;
404
+ command: keyof APICommands;
405
+ args?: APICommands[keyof APICommands]["requestArgs"];
406
+ }
407
+
408
+ /** WebSocket Result Message base fields type */
409
+ interface ResultMessageBase {
410
+ message_id: string;
411
+ }
412
+
413
+ /** WebSocket Error Result Message type */
414
+ export interface ErrorResultMessage extends ResultMessageBase {
415
+ error_code: number;
416
+ details?: string;
417
+ }
418
+
419
+ /** WebSocket Success Result Message type */
420
+ export interface SuccessResultMessage<T extends keyof APICommands> extends ResultMessageBase {
421
+ result: APICommands[T]["response"];
422
+ }
423
+
424
+ export type ArgsOf<R extends keyof APICommands> = APICommands[R]["requestArgs"];
425
+ export type ResponseOf<R extends keyof APICommands> = APICommands[R]["response"];
426
+
427
+ /**
428
+ * Minimum test node ID. Node IDs >= this value are reserved for test nodes.
429
+ * Uses high 64-bit range (0xFFFF_FFFE_0000_0000) to avoid collision with real node IDs.
430
+ */
431
+ export const TEST_NODE_START = 0xffff_fffe_0000_0000n;
432
+
433
+ /**
434
+ * Error codes matching Python Matter Server for API compatibility.
435
+ * @see https://github.com/home-assistant-libs/python-matter-server/blob/main/matter_server/common/errors.py
436
+ */
437
+ export enum ServerErrorCode {
438
+ /** Generic/unknown error */
439
+ UnknownError = 0,
440
+ /** Node commissioning failed */
441
+ NodeCommissionFailed = 1,
442
+ /** Node interview failed */
443
+ NodeInterviewFailed = 2,
444
+ /** Node is not ready (offline or not yet interviewed) */
445
+ NodeNotReady = 3,
446
+ /** Node not resolving (CASE session establishment failed) */
447
+ NodeNotResolving = 4,
448
+ /** Node does not exist */
449
+ NodeNotExists = 5,
450
+ /** SDK version mismatch */
451
+ VersionMismatch = 6,
452
+ /** SDK/Stack error */
453
+ SDKStackError = 7,
454
+ /** Invalid command arguments */
455
+ InvalidArguments = 8,
456
+ /** Invalid/unknown command */
457
+ InvalidCommand = 9,
458
+ /** OTA update check failed */
459
+ UpdateCheckError = 10,
460
+ /** OTA update failed */
461
+ UpdateError = 11,
462
+ }
463
+
464
+ /**
465
+ * Custom error class for server errors with typed error codes.
466
+ * Use this to throw errors that will be properly mapped to Python-compatible error codes.
467
+ */
468
+ export class ServerError extends Error {
469
+ constructor(
470
+ public readonly code: ServerErrorCode,
471
+ message: string,
472
+ cause?: Error,
473
+ ) {
474
+ super(message, { cause });
475
+ this.name = "ServerError";
476
+ }
477
+
478
+ static unknownError(message: string, cause?: Error): ServerError {
479
+ return new ServerError(ServerErrorCode.UnknownError, message, cause);
480
+ }
481
+
482
+ static nodeCommissionFailed(message: string, cause?: Error): ServerError {
483
+ return new ServerError(ServerErrorCode.NodeCommissionFailed, message, cause);
484
+ }
485
+
486
+ static nodeInterviewFailed(message: string, cause?: Error): ServerError {
487
+ return new ServerError(ServerErrorCode.NodeInterviewFailed, message, cause);
488
+ }
489
+
490
+ static nodeNotReady(nodeId: number | bigint, cause?: Error): ServerError {
491
+ return new ServerError(ServerErrorCode.NodeNotReady, `Node ${nodeId} is not ready`, cause);
492
+ }
493
+
494
+ static nodeNotResolving(nodeId: number | bigint, cause?: Error): ServerError {
495
+ return new ServerError(ServerErrorCode.NodeNotResolving, `Node ${nodeId} is not resolving`, cause);
496
+ }
497
+
498
+ static nodeNotExists(nodeId: number | bigint): ServerError {
499
+ return new ServerError(ServerErrorCode.NodeNotExists, `Node ${nodeId} does not exist`);
500
+ }
501
+
502
+ static versionMismatch(message: string): ServerError {
503
+ return new ServerError(ServerErrorCode.VersionMismatch, message);
504
+ }
505
+
506
+ static sdkStackError(message: string, cause?: Error): ServerError {
507
+ return new ServerError(ServerErrorCode.SDKStackError, message, cause);
508
+ }
509
+
510
+ static invalidArguments(message: string): ServerError {
511
+ return new ServerError(ServerErrorCode.InvalidArguments, message);
512
+ }
513
+
514
+ static invalidCommand(command: string): ServerError {
515
+ return new ServerError(ServerErrorCode.InvalidCommand, `Unknown command: ${command}`);
516
+ }
517
+
518
+ static updateCheckError(message: string, cause?: Error): ServerError {
519
+ return new ServerError(ServerErrorCode.UpdateCheckError, message, cause);
520
+ }
521
+
522
+ static updateError(message: string, cause?: Error): ServerError {
523
+ return new ServerError(ServerErrorCode.UpdateError, message, cause);
524
+ }
525
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ /**
8
+ * Utility to get the Matter.js SDK version from @matter/main package.
9
+ */
10
+
11
+ import { readFileSync } from "node:fs";
12
+ import { createRequire } from "node:module";
13
+ import { dirname, join } from "node:path";
14
+
15
+ interface PackageJson {
16
+ version: string;
17
+ }
18
+
19
+ /**
20
+ * Get the version of the @matter/main package.
21
+ */
22
+ export function getMatterVersion(): string {
23
+ // Use createRequire to resolve the module path, then read the package.json
24
+ const require = createRequire(import.meta.url);
25
+ const matterMainPath = require.resolve("@matter/main");
26
+ // Navigate up from the resolved module to find package.json
27
+ let dir = dirname(matterMainPath);
28
+ while (dir !== "/") {
29
+ try {
30
+ const packageJsonPath = join(dir, "package.json");
31
+ const content = readFileSync(packageJsonPath, "utf-8");
32
+ const pkg = JSON.parse(content) as PackageJson & { name?: string };
33
+ if (pkg.name === "@matter/main") {
34
+ return pkg.version;
35
+ }
36
+ } catch {
37
+ // Continue searching
38
+ }
39
+ dir = dirname(dir);
40
+ }
41
+ throw new Error("Could not find @matter/main package.json");
42
+ }
43
+
44
+ /** Cached Matter.js SDK version */
45
+ export const MATTER_VERSION = getMatterVersion();
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { spawn } from "node:child_process";
8
+ import { platform } from "node:os";
9
+
10
+ const PLATFORM_MAC = platform() === "darwin";
11
+
12
+ /**
13
+ * Ping an IP address using the system ping command.
14
+ * @param ipAddress The IP address to ping (IPv4 or IPv6)
15
+ * @param timeout Timeout in seconds (default: 2)
16
+ * @param attempts Number of ping attempts (default: 1)
17
+ * @returns Promise resolving to true if ping succeeded, false otherwise
18
+ */
19
+ export async function pingIp(ipAddress: string, timeout = 2, attempts = 1): Promise<boolean> {
20
+ const isIpv6 = ipAddress.includes(":");
21
+
22
+ // Build the ping command based on platform and IP version
23
+ let command: string;
24
+ let args: string[];
25
+
26
+ if (isIpv6 && PLATFORM_MAC) {
27
+ // macOS doesn't support -W (timeout) on ping6
28
+ command = "ping6";
29
+ args = ["-c", "1", ipAddress];
30
+ } else if (isIpv6) {
31
+ // Linux IPv6
32
+ command = "ping";
33
+ args = ["-6", "-c", "1", "-W", timeout.toString(), ipAddress];
34
+ } else {
35
+ // IPv4 (works on both macOS and Linux)
36
+ command = "ping";
37
+ args = ["-c", "1", "-W", timeout.toString(), ipAddress];
38
+ }
39
+
40
+ while (attempts > 0) {
41
+ attempts--;
42
+ try {
43
+ const success = await runPingCommand(command, args, timeout + 2);
44
+ if (success || attempts === 0) {
45
+ return success;
46
+ }
47
+ } catch {
48
+ // Timeout or error, try again if attempts remaining
49
+ if (attempts === 0) {
50
+ return false;
51
+ }
52
+ }
53
+ // Sleep 10 seconds between attempts (like Python implementation)
54
+ if (attempts > 0) {
55
+ await new Promise(resolve => setTimeout(resolve, 10000));
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+
61
+ /**
62
+ * Run the ping command and return whether it succeeded.
63
+ */
64
+ function runPingCommand(command: string, args: string[], timeoutSeconds: number): Promise<boolean> {
65
+ return new Promise((resolve, reject) => {
66
+ const proc = spawn(command, args, {
67
+ stdio: ["ignore", "pipe", "pipe"],
68
+ });
69
+
70
+ const timer = setTimeout(() => {
71
+ proc.kill();
72
+ reject(new Error("Ping timeout"));
73
+ }, timeoutSeconds * 1000);
74
+
75
+ proc.on("close", code => {
76
+ clearTimeout(timer);
77
+ resolve(code === 0);
78
+ });
79
+
80
+ proc.on("error", err => {
81
+ clearTimeout(timer);
82
+ reject(err);
83
+ });
84
+ });
85
+ }