@phystack/hub-device 4.4.30

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 (82) hide show
  1. package/.prettierignore +10 -0
  2. package/.prettierrc +10 -0
  3. package/CHANGELOG.md +1202 -0
  4. package/README.md +12 -0
  5. package/dist/index.d.ts +114 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +967 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/storage/browser.d.ts +2 -0
  10. package/dist/storage/browser.d.ts.map +1 -0
  11. package/dist/storage/browser.js +20 -0
  12. package/dist/storage/browser.js.map +1 -0
  13. package/dist/storage/index.d.ts +6 -0
  14. package/dist/storage/index.d.ts.map +1 -0
  15. package/dist/storage/index.js +31 -0
  16. package/dist/storage/index.js.map +1 -0
  17. package/dist/storage/node.d.ts +2 -0
  18. package/dist/storage/node.d.ts.map +1 -0
  19. package/dist/storage/node.js +47 -0
  20. package/dist/storage/node.js.map +1 -0
  21. package/dist/sysinfo/browser.d.ts +3 -0
  22. package/dist/sysinfo/browser.d.ts.map +1 -0
  23. package/dist/sysinfo/browser.js +194 -0
  24. package/dist/sysinfo/browser.js.map +1 -0
  25. package/dist/sysinfo/index.d.ts +3 -0
  26. package/dist/sysinfo/index.d.ts.map +1 -0
  27. package/dist/sysinfo/index.js +34 -0
  28. package/dist/sysinfo/index.js.map +1 -0
  29. package/dist/sysinfo/node.d.ts +3 -0
  30. package/dist/sysinfo/node.d.ts.map +1 -0
  31. package/dist/sysinfo/node.js +53 -0
  32. package/dist/sysinfo/node.js.map +1 -0
  33. package/dist/sysinfo/tizen.d.ts +8 -0
  34. package/dist/sysinfo/tizen.d.ts.map +1 -0
  35. package/dist/sysinfo/tizen.js +168 -0
  36. package/dist/sysinfo/tizen.js.map +1 -0
  37. package/dist/types/command.types.d.ts +8 -0
  38. package/dist/types/command.types.d.ts.map +1 -0
  39. package/dist/types/command.types.js +8 -0
  40. package/dist/types/command.types.js.map +1 -0
  41. package/dist/types/container.types.d.ts +10 -0
  42. package/dist/types/container.types.d.ts.map +1 -0
  43. package/dist/types/container.types.js +3 -0
  44. package/dist/types/container.types.js.map +1 -0
  45. package/dist/types/job.types.d.ts +31 -0
  46. package/dist/types/job.types.d.ts.map +1 -0
  47. package/dist/types/job.types.js +15 -0
  48. package/dist/types/job.types.js.map +1 -0
  49. package/dist/types/twin.types.d.ts +653 -0
  50. package/dist/types/twin.types.d.ts.map +1 -0
  51. package/dist/types/twin.types.js +21 -0
  52. package/dist/types/twin.types.js.map +1 -0
  53. package/dist/utilities/get-device-identifier.utility.d.ts +2 -0
  54. package/dist/utilities/get-device-identifier.utility.d.ts.map +1 -0
  55. package/dist/utilities/get-device-identifier.utility.js +140 -0
  56. package/dist/utilities/get-device-identifier.utility.js.map +1 -0
  57. package/dist/utilities/get-hub-credentials.utility.d.ts +8 -0
  58. package/dist/utilities/get-hub-credentials.utility.d.ts.map +1 -0
  59. package/dist/utilities/get-hub-credentials.utility.js +47 -0
  60. package/dist/utilities/get-hub-credentials.utility.js.map +1 -0
  61. package/dist/utilities/get-provisioning-code.utility.d.ts +3 -0
  62. package/dist/utilities/get-provisioning-code.utility.d.ts.map +1 -0
  63. package/dist/utilities/get-provisioning-code.utility.js +49 -0
  64. package/dist/utilities/get-provisioning-code.utility.js.map +1 -0
  65. package/package.json +39 -0
  66. package/src/hub-device.d.ts +0 -0
  67. package/src/index.ts +1228 -0
  68. package/src/storage/browser.ts +16 -0
  69. package/src/storage/index.ts +46 -0
  70. package/src/storage/node.ts +42 -0
  71. package/src/sysinfo/browser.ts +217 -0
  72. package/src/sysinfo/index.ts +29 -0
  73. package/src/sysinfo/node.ts +387 -0
  74. package/src/sysinfo/tizen.ts +203 -0
  75. package/src/types/command.types.ts +8 -0
  76. package/src/types/container.types.ts +12 -0
  77. package/src/types/job.types.ts +36 -0
  78. package/src/types/twin.types.ts +751 -0
  79. package/src/utilities/get-device-identifier.utility.ts +179 -0
  80. package/src/utilities/get-hub-credentials.utility.ts +56 -0
  81. package/src/utilities/get-provisioning-code.utility.ts +55 -0
  82. package/tsconfig.json +45 -0
package/src/index.ts ADDED
@@ -0,0 +1,1228 @@
1
+ import { Socket, SocketOptions, ManagerOptions } from 'socket.io-client';
2
+ import { getSocketIOWithProxy } from '@phystack/socket.io-proxy';
3
+ import getStorage from './storage';
4
+ import getSysinfo, { isTizen } from './sysinfo';
5
+ import getDeviceIdentifier from './utilities/get-device-identifier.utility';
6
+ import getHubCredentials from './utilities/get-hub-credentials.utility';
7
+ import getProvisioningCode from './utilities/get-provisioning-code.utility';
8
+ import {
9
+ DeviceIp,
10
+ DeviceStatus,
11
+ DeviceTwinReportedProperties,
12
+ DeviceTwinResponse,
13
+ EdgeTwinReportedProperties,
14
+ ScreenTwinReportedProperties,
15
+ TwinResponse,
16
+ TwinStatusEnum,
17
+ TwinTypeEnum,
18
+ PeripheralTwinReportedProperties,
19
+ } from './types/twin.types';
20
+ import { Job } from './types/job.types';
21
+ import path from 'path';
22
+ import fs from 'fs';
23
+ import { Mutex } from 'async-mutex';
24
+
25
+ export type MyIoOptions = Partial<ManagerOptions & SocketOptions>;
26
+ interface EventPayload<T = any> {
27
+ twinId?: string;
28
+ deviceId?: string;
29
+ status?: TwinStatusEnum;
30
+ data?: T;
31
+ }
32
+
33
+ interface MigratedCredentials {
34
+ deviceId: string;
35
+ accessKey: string;
36
+ deviceSerial: string;
37
+ phyhubUrl: string;
38
+ region: string;
39
+ }
40
+
41
+ enum PhyHubResponseStatus {
42
+ success = 'success',
43
+ error = 'error',
44
+ }
45
+
46
+ const logger = console;
47
+
48
+ function getVersion(): string {
49
+ const pkgPath = path.resolve(__dirname, '../package.json');
50
+ try {
51
+ const content = fs.readFileSync(pkgPath, 'utf-8');
52
+ const pkg = JSON.parse(content);
53
+ return pkg.version || 'N/A';
54
+ } catch (error) {
55
+ console.error('Failed to read version from package.json:', error);
56
+ return 'N/A';
57
+ }
58
+ }
59
+
60
+ const phyhubDeviceProvisioningBaseUrls: {[env: string]: string} = {
61
+ LOCAL: 'https://phyhub-prov.dev.omborigrid.net/api',
62
+ DEV: 'https://phyhub-prov.dev.omborigrid.net/api',
63
+ QA: 'https://phyhub-prov.qa.omborigrid.net/api',
64
+ PROD: 'https://phyhub-prov.eu.omborigrid.net/api',
65
+ };
66
+
67
+ let phyHubSingleton: HubDevice | null = null;
68
+
69
+ export const connectToPhyHub = async (
70
+ gridEnv: string = 'PROD',
71
+ osVersion: string = 'N/A',
72
+ migratedCredentials: MigratedCredentials | undefined = undefined
73
+ ) => {
74
+ const storage = await getStorage();
75
+ let deviceId = await storage.get('deviceId');
76
+ let accessKey = await storage.get('accessKey');
77
+ let phyhubUrl = await storage.get('phyhubUrl');
78
+ let deviceSerial = await storage.get('deviceSerial');
79
+ let provisioningCode = await storage.get('provisioningCode');
80
+ let region = await storage.get('region');
81
+
82
+ // migrate credentials if legacy device
83
+ if (migratedCredentials) {
84
+ try {
85
+ logger.info(`connectToPhyHub(): Using migrated credentials `, JSON.stringify(migratedCredentials, null, 2));
86
+ const {
87
+ deviceId: migratedDeviceId,
88
+ deviceSerial: migratedDeviceSerial,
89
+ accessKey: migratedAccessKey,
90
+ region: migratedRegion
91
+ } = migratedCredentials;
92
+ if (migratedDeviceId) {
93
+ await storage.set('deviceId', migratedDeviceId);
94
+ deviceId = migratedDeviceId;
95
+ }
96
+ if (migratedDeviceSerial) {
97
+ await storage.set('deviceSerial', migratedDeviceSerial);
98
+ deviceSerial = migratedDeviceSerial;
99
+ }
100
+ if (migratedAccessKey) {
101
+ await storage.set('accessKey', migratedAccessKey);
102
+ accessKey = migratedAccessKey;
103
+ }
104
+ if (migratedRegion) {
105
+ await storage.set('region', migratedRegion);
106
+ region = migratedRegion;
107
+ const migratedPhyhubUrl = `https://phyhub.${migratedRegion}.omborigrid.net`;
108
+ await storage.set('phyhubUrl', migratedPhyhubUrl);
109
+ phyhubUrl = migratedPhyhubUrl;
110
+ }
111
+ } catch (error) {
112
+ logger.error(`connectToPhyHub(): Unable to migrate credentials:`, error);
113
+ }
114
+ }
115
+
116
+ // Ensure we have a device serial and it's properly stored
117
+ if (!deviceSerial) {
118
+ logger.info('connectToPhyHub(): Getting device identifier');
119
+ deviceSerial = await getDeviceIdentifier();
120
+ // Wait for storage to complete before continuing
121
+ await storage.set('deviceSerial', deviceSerial);
122
+ }
123
+
124
+ // Always store updates to gridEnv, osVersion (for future use)
125
+ await storage.set('gridEnv', gridEnv);
126
+ await storage.set('osVersion', osVersion);
127
+
128
+ logger.info(`connectToPhyHub(): Connecting device`, {
129
+ deviceId,
130
+ accessKey,
131
+ deviceSerial,
132
+ gridEnv,
133
+ provisioningCode,
134
+ osVersion,
135
+ phyhubUrl,
136
+ region,
137
+ });
138
+
139
+ // We are already connected, just return the instance
140
+ if (phyHubSingleton) {
141
+ // logger.info(
142
+ // `connectToPhyHub(): HubDevice singleton instance found for device ${deviceId}.`
143
+ // );
144
+ return { phyHub: phyHubSingleton, deviceSerial };
145
+ }
146
+
147
+ // If we have credentials, do not wait for provisioning code
148
+ if (deviceId && accessKey && deviceSerial && gridEnv && phyhubUrl) {
149
+ logger.info(
150
+ `connectToPhyHub(): Credentials found. Connecting device ${deviceId} to phyhub.`
151
+ );
152
+ phyHubSingleton = new HubDevice(
153
+ phyhubUrl,
154
+ deviceId,
155
+ accessKey,
156
+ deviceSerial,
157
+ gridEnv,
158
+ osVersion,
159
+ region,
160
+ );
161
+
162
+ logger.info(
163
+ `connectToPhyHub(): Device ${deviceId} connected to phyhub.`
164
+ );
165
+ return { phyHub: phyHubSingleton, deviceSerial };
166
+ }
167
+
168
+ const storedDeviceSerial = await storage.get('deviceSerial');
169
+ // check if deviceSerial is not set, set it.
170
+ if (!storedDeviceSerial) {
171
+ await storage.set('deviceSerial', deviceSerial);
172
+ }
173
+
174
+ // logger.info(`connectToPhyHub(): Getting credentials for device ${deviceId}.`);
175
+ const credentials = await getHubCredentials(phyhubDeviceProvisioningBaseUrls[gridEnv]);
176
+
177
+ if (credentials) {
178
+ const { deviceId, accessKey, phyhubUrl, region } = credentials;
179
+
180
+
181
+ await storage.set('region', region);
182
+ await storage.set('deviceId', deviceId);
183
+ await storage.set('accessKey', accessKey);
184
+ await storage.set('phyhubUrl', phyhubUrl);
185
+
186
+ // logger.info(`connectToPhyHub(): Credentials fetched for device ${deviceId}.`);
187
+ phyHubSingleton = new HubDevice(
188
+ phyhubUrl,
189
+ deviceId,
190
+ accessKey,
191
+ deviceSerial,
192
+ gridEnv,
193
+ osVersion,
194
+ region
195
+ );
196
+ logger.info(
197
+ `connectToPhyHub(): Device ${deviceId} connected to phyhub.`
198
+ );
199
+ return { phyHub: phyHubSingleton, deviceSerial };
200
+ }
201
+
202
+ await getProvisioningCode(phyhubDeviceProvisioningBaseUrls[gridEnv]);
203
+ const provisioningCodeKey = `provisioningCode_${gridEnv}`;
204
+ provisioningCode = await storage.get(provisioningCodeKey);
205
+ logger.info(
206
+ `connectToPhyHub(): Returning provisioning code ${provisioningCode} for device ${deviceSerial}`
207
+ );
208
+ return provisioningCode;
209
+ };
210
+
211
+ export const getPhyHubInstance = async () => {
212
+ return phyHubSingleton;
213
+ };
214
+
215
+ export const getProvisioningInfo = async () => {
216
+ const storage = await getStorage();
217
+ const deviceSerial = await storage.get('deviceSerial');
218
+ let provisioningCode = await storage.get('provisioningCode');
219
+
220
+ // If we don't have a provisioning code, try to get a new one
221
+ if (!provisioningCode) {
222
+ logger.info('getProvisioningInfo(): No provisioning code found, fetching new one');
223
+ const gridEnv = await storage.get('gridEnv') || 'PROD';
224
+ const apiUrl = phyhubDeviceProvisioningBaseUrls[gridEnv];
225
+
226
+ const result = await getProvisioningCode(apiUrl);
227
+ if (result?.provisioningCode) {
228
+ provisioningCode = result.provisioningCode;
229
+ logger.info('getProvisioningInfo(): Successfully fetched new provisioning code');
230
+ } else {
231
+ logger.error('getProvisioningInfo(): Failed to fetch provisioning code');
232
+ }
233
+ }
234
+
235
+ return {
236
+ provisioningCode,
237
+ deviceSerial,
238
+ };
239
+ };
240
+
241
+ export class HubDevice {
242
+ public gridEnv: string;
243
+ public osVersion: string;
244
+ public phyhubUrl: string | null = null;
245
+ public deviceId: string | null = null;
246
+ public accessKey: string | null = null;
247
+ public deviceSerial: string | null = null;
248
+ public region: string | null = null;
249
+
250
+ public socket: Socket | null = null;
251
+ private socketConnected: boolean = false;
252
+ private socketAuthenticated: boolean = false;
253
+ private onTwinUpdate: ((twins: TwinResponse) => void) | null = null;
254
+ private onTwinRemove: ((twins: TwinResponse) => void) | null = null;
255
+ private emitQueue: any[] = [];
256
+ private emitAuthQueue: any[] = [];
257
+ private scheduler: any | null = null;
258
+ private deletedTwins: string[] = [];
259
+
260
+ private intervals: any = {};
261
+ private peripheralTwinMutex = new Mutex();
262
+ private eventCallbacks: Map<string, Array<(data: any) => void>> = new Map();
263
+ private reconnectListeners: Array<() => void> = [];
264
+ private authReconnectListeners: Array<() => void> = [];
265
+
266
+ constructor(
267
+ phyhubUrl: string,
268
+ deviceId: string,
269
+ accessKey: string,
270
+ deviceSerial: string,
271
+ gridEnv: string = 'PROD',
272
+ osVersion: string = 'N/A',
273
+ region: string,
274
+ ) {
275
+ this.phyhubUrl = phyhubUrl;
276
+ this.deviceId = deviceId;
277
+ this.accessKey = accessKey;
278
+ this.deviceSerial = deviceSerial;
279
+ this.gridEnv = gridEnv.toUpperCase();
280
+ this.osVersion = osVersion;
281
+ this.region = region.toUpperCase();
282
+ }
283
+
284
+ public setTwinUpdateHandler(callback: (twins: TwinResponse) => void) {
285
+ this.onTwinUpdate = callback;
286
+ // logger.info('setTwinUpdateHandler(): Twin update handler set');
287
+ }
288
+
289
+ public setTwinRemoveHandler(callback: (twins: TwinResponse) => void) {
290
+ this.onTwinRemove = callback;
291
+ // logger.info('setTwinRemoveHandler(): Twin remove handler set');
292
+ }
293
+
294
+ public isConnected() {
295
+ return this.socketConnected;
296
+ }
297
+
298
+ public isAuthenticated() {
299
+ return this.socketAuthenticated;
300
+ }
301
+
302
+ public async connect() {
303
+ try {
304
+ const { deviceId, accessKey, gridEnv, osVersion, phyhubUrl } = this;
305
+ if (!deviceId || !accessKey) {
306
+ logger.error('connect(): DeviceId or AccessKey not set');
307
+ return;
308
+ }
309
+
310
+
311
+ if (!phyhubUrl) {
312
+ logger.error('connect(): Phyhub URL not set');
313
+ return;
314
+ }
315
+
316
+ logger.info(`connect(): Connecting device `, { deviceId, accessKey, gridEnv, osVersion });
317
+ this.socket = await this.connectSocket(deviceId, accessKey, phyhubUrl);
318
+ } catch (error) {
319
+ logger.error(`connect(): Failed to connect socket`, error);
320
+ }
321
+
322
+ return this;
323
+ }
324
+
325
+ public on(event: string, callback: (data: any) => void) {
326
+ const { socket, deviceId } = this;
327
+ // logger.info('SOCKET on(): Registering event handler', { event, deviceId });
328
+
329
+ if (!this.eventCallbacks.has(event)) {
330
+ this.eventCallbacks.set(event, []);
331
+ }
332
+ this.eventCallbacks.get(event)!.push(callback);
333
+
334
+ if (!socket || !deviceId) {
335
+ logger.error('SOCKET on(): No socket or deviceId available', { socket: !!socket, deviceId });
336
+ return callback({ error: `connectDevice ${deviceId}: Not connected` });
337
+ }
338
+
339
+ socket.on(event, (data: any) => {
340
+ if (this.isConnected()) {
341
+ // logger.info(`SOCKET on(): Received ${event} event`, { deviceId, data });
342
+ this.eventCallbacks.get(event)!.forEach(cb => cb(data));
343
+ } else {
344
+ logger.warn(`SOCKET on(): Ignoring ${event} event as socket is not connected`, { deviceId });
345
+ }
346
+ });
347
+
348
+ logger.info('SOCKET on(): Successfully registered callback for event', { event, deviceId });
349
+ }
350
+
351
+ public emit(...args: any) {
352
+ const { socket, emitQueue } = this;
353
+
354
+ if (!socket) {
355
+ logger.error('emit(): Socket not created');
356
+ return;
357
+ }
358
+ if (!this.isConnected()) {
359
+ logger.info('SOCKET emit(): Pushing to emitQueue');
360
+ emitQueue.push(args);
361
+ return;
362
+ }
363
+ socket.emit.apply(socket, args);
364
+ }
365
+
366
+ public emitAuth(...args: any) {
367
+ const { socket, emitAuthQueue } = this;
368
+ if (!socket) {
369
+ logger.error('emitAuth(): Socket not created');
370
+ return;
371
+ }
372
+ if (!this.isConnected() || !this.isAuthenticated()) {
373
+ // logger.info('SOCKET emitAuth(): Pushing to emitAuthQueue');
374
+ emitAuthQueue.push(args);
375
+ return;
376
+ }
377
+
378
+ // const [event, payload, ...rest] = args;
379
+ // if (event === 'getPeripheralTwins') {
380
+ // this.getPeripheralTwins(payload);
381
+ // return;
382
+ // }
383
+ socket.emit.apply(socket, args);
384
+ }
385
+
386
+ private processEmitQueue() {
387
+ logger.info('SOCKET processEmitQueue(): processing emit queue');
388
+ const { socket, emitQueue } = this;
389
+
390
+ if (!socket || !this.isConnected()) {
391
+ logger.info('SOCKET processEmitQueue(): Aborting', {
392
+ socket: !!socket,
393
+ socketConnected: this.isConnected(),
394
+ });
395
+ return;
396
+ }
397
+
398
+ while (emitQueue.length > 0) {
399
+ const args = emitQueue.shift();
400
+ socket.emit.apply(socket, args);
401
+ }
402
+ this.processEmitAuthQueue();
403
+ }
404
+
405
+ private processEmitAuthQueue() {
406
+ logger.info('SOCKET processEmitAuthQueue(): processing emit auth queue');
407
+ const { socket, emitAuthQueue } = this;
408
+
409
+ if (!socket || !this.isConnected() || !this.isAuthenticated()) {
410
+ logger.info('SOCKET processEmitAuthQueue(): Aborting', {
411
+ socket: !!socket,
412
+ socketConnected: this.isConnected(),
413
+ socketAuthenticated: this.isAuthenticated(),
414
+ });
415
+ return;
416
+ }
417
+
418
+ while (emitAuthQueue.length > 0) {
419
+ const args = emitAuthQueue.shift();
420
+ socket.emit.apply(socket, args);
421
+ }
422
+ }
423
+
424
+ private executeReconnectListeners() {
425
+ this.reconnectListeners.forEach(listener => {
426
+ try {
427
+ listener();
428
+ } catch (error) {
429
+ logger.error('Error executing reconnect listener:', error);
430
+ }
431
+ });
432
+ }
433
+
434
+ private executeAuthReconnectListeners() {
435
+ this.authReconnectListeners.forEach(listener => {
436
+ try {
437
+ listener();
438
+ } catch (error) {
439
+ logger.error('Error executing reconnect listener:', error);
440
+ }
441
+ });
442
+ }
443
+
444
+ private async connectSocket(
445
+ deviceId: string,
446
+ accessKey: string,
447
+ phyhubUrl: string
448
+ ): Promise<Socket | null> {
449
+ // logger.info('SOCKET connectSocket(): Creating new socket', { deviceId, accessKey, phyhubUrl });
450
+
451
+ // Updated to use getSocketIOWithProxy
452
+ const socket: Socket = await getSocketIOWithProxy(phyhubUrl, {
453
+ auth: { deviceId, accessKey },
454
+ reconnection: true,
455
+ reconnectionDelayMax: 1000,
456
+ reconnectionAttempts: Infinity,
457
+ });
458
+
459
+ socket.on('connect', () => {
460
+ logger.info(
461
+ `SOCKET connectSocket() on connect: Connected to phyhub ${phyhubUrl} 🟢-🟢-🟢-🟢-⚪`
462
+ );
463
+ this.socketConnected = true;
464
+ this.processEmitQueue();
465
+ if (this.socketConnected) {
466
+ this.executeReconnectListeners();
467
+ }
468
+ // If we're already authenticated, call reconnect listeners and process auth queue
469
+ if (this.socketAuthenticated) {
470
+ logger.info('SOCKET connectSocket() on reconnect: Socket already authenticated, notifying listeners');
471
+ this.executeAuthReconnectListeners();
472
+ this.processEmitAuthQueue();
473
+ }
474
+ });
475
+
476
+ socket.on('disconnect', () => {
477
+ logger.info('SOCKET connectSocket() on disconnect: Disconnected from server');
478
+ this.socketConnected = false;
479
+ });
480
+
481
+ socket.on('deviceAuthenticated', (response: any) => {
482
+ logger.info('SOCKET connectSocket() on deviceAuthenticated: Device authenticated', response);
483
+ this.socketAuthenticated = true;
484
+ if (response.status === 'success') {
485
+ // Execute reconnect listeners when device is authenticated
486
+ this.executeReconnectListeners();
487
+ this.processEmitAuthQueue();
488
+ } else {
489
+ logger.error('SOCKET connectSocket() on deviceAuthenticated: Device authentication failed', response);
490
+ }
491
+ });
492
+
493
+ socket.io.on('reconnect', (attempt: number) => {
494
+ logger.info(`SOCKET connectSocket() on reconnect: Reconnected to server attempt: ${attempt}`);
495
+ this.socketConnected = true;
496
+ this.processEmitQueue();
497
+ });
498
+
499
+ socket.on('twinUpdated', async (payload: EventPayload<TwinResponse>) => {
500
+ logger.info('SOCKET connectSocket() on twinUpdated: syncing twin on device');
501
+ if (!payload.data) return;
502
+
503
+ const twin = payload.data;
504
+ await this.cacheTwins([twin]);
505
+
506
+ if (this.onTwinUpdate) {
507
+ this.onTwinUpdate(twin);
508
+ }
509
+ });
510
+
511
+ socket.on('twinCreated', async (payload: EventPayload<TwinResponse>) => {
512
+ logger.info('SOCKET connectSocket() on twinCreated: Adding new twin to device');
513
+ if (payload.data) {
514
+ await this.cacheTwins([payload.data]);
515
+ if (this.onTwinUpdate) {
516
+ this.onTwinUpdate(payload.data);
517
+ }
518
+ }
519
+ });
520
+
521
+ socket.on('twinDeleted', async (payload: EventPayload<TwinResponse>) => {
522
+ logger.info('SOCKET connectSocket() on twinDeleted: Removing twin from device');
523
+ if (this.onTwinRemove && payload.data) {
524
+ await this.removeCachedTwin([payload.data]);
525
+ this.onTwinRemove(payload.data);
526
+ }
527
+ });
528
+
529
+ // socket.on('twinMessage', async (payload: EventPayload) => {
530
+ // logger.info('SOCKET connectSocket() on twinMessage: received twin message', JSON.stringify(payload, null, 2));
531
+ // });
532
+
533
+ socket.io.on('reconnect_attempt', (attempt: number) => {
534
+ logger.info(`SOCKET connectSocket() on reconnect_attempt: Reconnect attempt: ${attempt}`);
535
+ });
536
+
537
+ socket.io.on('reconnect_error', (error: Error) => {
538
+ logger.info('SOCKET connectSocket() on reconnect_error: ', error);
539
+ });
540
+
541
+ socket.io.on('reconnect_failed', () => {
542
+ logger.info('SOCKET connectSocket() on reconnect_failed: Reconnect failed');
543
+ });
544
+
545
+ return socket;
546
+ }
547
+
548
+ public getDeviceStatus = async (): Promise<DeviceStatus> => {
549
+ const storage = await getStorage();
550
+ const deviceId = await storage.get('deviceId');
551
+ const accessKey = await storage.get('accessKey');
552
+ const deviceSerial = await storage.get('deviceSerial');
553
+ const provisioningCode = accessKey ? 'N/A' : await storage.get('provisioningCode');
554
+ const gridEnv = await storage.get('gridEnv');
555
+ const osVersion = await storage.get('osVersion');
556
+ const dataResidency = await storage.get('region');
557
+ const twins = await this.getCachedTwins();
558
+
559
+ let displayName = '';
560
+ let deviceEnv= '';
561
+ let spaceId = '';
562
+ let tenantId = '';
563
+ let ip: DeviceIp[] = [];
564
+
565
+ const cachedTwins = await this.getCachedTwins();
566
+ try {
567
+ const deviceTwinInstanceId = cachedTwins?.[TwinTypeEnum.Device]?.[0];
568
+ if (deviceTwinInstanceId) {
569
+ const deviceTwinInstance: DeviceTwinResponse = await this.getCachedTwins(deviceTwinInstanceId);
570
+ if (deviceTwinInstance) {
571
+ displayName = deviceTwinInstance.properties.desired.displayName;
572
+ deviceEnv = deviceTwinInstance.properties.desired.env;
573
+ spaceId = deviceTwinInstance.properties.desired.spaceId;
574
+ tenantId = deviceTwinInstance.tenantId;
575
+ ip = deviceTwinInstance.properties.reported.ip
576
+ }
577
+ }
578
+ } catch (error) {
579
+ logger.error(`Device twin not found for device ${deviceId}`, error);
580
+ }
581
+
582
+ const response = {
583
+ deviceId,
584
+ accessKey,
585
+ deviceSerial,
586
+ gridEnv,
587
+ osVersion,
588
+ provisioningCode,
589
+ displayName,
590
+ deviceEnv,
591
+ spaceId,
592
+ tenantId,
593
+ dataResidency,
594
+ ip,
595
+ socketConnected: this.isConnected(),
596
+ socketAuthenticated: this.isAuthenticated(),
597
+ twins,
598
+ };
599
+ return response;
600
+ };
601
+
602
+ public getDeviceInstance = async () => {
603
+ const storage = await getStorage();
604
+ const deviceId = await storage.get('deviceId');
605
+ const twins = await this.getCachedTwins();
606
+
607
+ try {
608
+ const deviceTwinInstanceId = twins[TwinTypeEnum.Device][0];
609
+ if (deviceTwinInstanceId) {
610
+ const deviceTwinInstance = await this.getCachedTwins(deviceTwinInstanceId);
611
+ if (deviceTwinInstance) {
612
+ return { twin: deviceTwinInstance };
613
+ }
614
+ }
615
+ return {};
616
+ } catch (error) {
617
+ logger.error(`Device twin not found for device ${deviceId}`, error);
618
+ return {};
619
+ }
620
+ };
621
+
622
+ public getDeviceNetworks = async () => {
623
+ try {
624
+ const sysinfo = await getSysinfo();
625
+ const { networkInterfaces, wifiInterfaces, wifiNetworks, wifiConnections } = sysinfo;
626
+ return {
627
+ networkInterfaces,
628
+ wifiInterfaces,
629
+ wifiNetworks,
630
+ wifiConnections,
631
+ };
632
+ } catch (error) {
633
+ logger.error(`Unable to get network information of the device`, error);
634
+ return {
635
+ wifiNetworks: [],
636
+ wifiConnections: [],
637
+ networkInterfaces: [],
638
+ wifiInterfaces: [],
639
+ };
640
+ }
641
+ };
642
+
643
+ public getScreenInstance = async () => {
644
+ const storage = await getStorage();
645
+ const deviceId = await storage.get('deviceId');
646
+
647
+ try {
648
+ const twins = await this.getCachedTwins();
649
+ console.log('getScreenInstance(): twins', JSON.stringify(twins, null, 2));
650
+ if(!twins[TwinTypeEnum.Screen]) {
651
+ return {};
652
+ }
653
+ const screenTwinInstanceId = twins[TwinTypeEnum.Screen][0];
654
+
655
+ if (screenTwinInstanceId) {
656
+ const screenTwinInstance = await this.getCachedTwins(screenTwinInstanceId);
657
+ if (screenTwinInstance) {
658
+ return { twin: screenTwinInstance };
659
+ }
660
+ }
661
+ return {};
662
+ } catch (error) {
663
+ logger.error(`Screen instance not found for device ${deviceId}`, error);
664
+ return {};
665
+ }
666
+ };
667
+
668
+ public async connectDevice(callback: (data: any) => void) {
669
+ const { socket, deviceId } = this;
670
+ logger.info(`connectDevice(): Connecting device ${deviceId}`);
671
+
672
+ if (!socket || !deviceId) {
673
+ return callback({ error: `connectDevice ${deviceId}: Not connected` });
674
+ }
675
+
676
+ let deviceTwin: any = undefined;
677
+ let cachedTwinsInfo: Record<string, string[]> | undefined;
678
+ let shouldUseCachedResponse = false;
679
+
680
+ try {
681
+ cachedTwinsInfo = await this.getCachedTwins();
682
+ if (cachedTwinsInfo && cachedTwinsInfo[TwinTypeEnum.Device] && cachedTwinsInfo[TwinTypeEnum.Device].length > 0) {
683
+ const deviceTwinId = cachedTwinsInfo[TwinTypeEnum.Device][0];
684
+ deviceTwin = await this.getCachedTwins(deviceTwinId);
685
+ // Only use cached response if device twin exists and has valid properties
686
+ shouldUseCachedResponse = deviceTwin && deviceTwin.properties && deviceTwin.properties.desired;
687
+ }
688
+ } catch (error) {
689
+ logger.info(`connectDevice(): No cached twins found, will fetch fresh twins`);
690
+ }
691
+
692
+ const reportSystemInformation = async () => {
693
+ logger.info('reportSystemInformation(): About to fetch system information');
694
+ const deviceInfo = await getSysinfo();
695
+ let ips: Array<{interface: string; ipv4: string; ipv6: string}> = [];
696
+
697
+ try {
698
+ const { networkInterfaces } = deviceInfo;
699
+ for (const iface of networkInterfaces) {
700
+ if (iface.ip4 && iface.ip4 !== '127.0.0.1') { // Skip loopback address
701
+ ips.push({
702
+ interface: iface.ifaceName,
703
+ ipv4: iface.ip4,
704
+ ipv6: iface.ip6,
705
+ });
706
+ }
707
+ }
708
+ } catch (error) {
709
+ logger.error('Failed to detect IP address of device', error);
710
+ }
711
+
712
+ // If we're in the browser, pick the osVersion from the last path segment.
713
+ // Otherwise (Node environment), fallback to whatever was stored.
714
+ const storage = await getStorage();
715
+ const isBrowser = typeof window !== 'undefined';
716
+
717
+ let finalOsVersion;
718
+ if (isTizen) {
719
+ finalOsVersion = deviceInfo?.os?.osVersion || '';
720
+ } else if (isBrowser && window.location && window.location.pathname) {
721
+ const segments = window.location.pathname.split('/').filter(Boolean);
722
+ finalOsVersion = segments.length ? segments[segments.length - 1] : 'N/A';
723
+ } else {
724
+ finalOsVersion = (await storage.get('osVersion')) || 'N/A';
725
+ }
726
+
727
+ const gridEnv = await storage.get('gridEnv');
728
+
729
+ const deviceTwinReportedProperties: EventPayload<DeviceTwinReportedProperties> = {
730
+ data: {
731
+ ...deviceInfo,
732
+ ip: deviceInfo?.ip && deviceInfo?.ip?.length > 0 ? deviceInfo.ip : ips,
733
+ os: {
734
+ osVersion: finalOsVersion,
735
+ },
736
+ env: {
737
+ gridEnv,
738
+ },
739
+ phydeviceVersion: getVersion(),
740
+ },
741
+ };
742
+
743
+ logger.info('reportSystemInformation(): About to emit system information');
744
+ socket.emit(
745
+ 'reportDeviceTwinProperties',
746
+ deviceTwinReportedProperties,
747
+ async (_response: any) => {
748
+ logger.info('reportSystemInformation(): Emitted system information');
749
+ }
750
+ );
751
+ };
752
+
753
+ // Immediately send system information upon a new socket connection
754
+ reportSystemInformation();
755
+
756
+ if (!this.intervals['reportSystemInformation']) {
757
+ // Changed the interval from 60000 (1 minute) to 300000 (5 minutes)
758
+ this.intervals['reportSystemInformation'] = setInterval(async () => {
759
+ // Only emit if connected and authenticated
760
+ if (!this.isConnected() || !this.isAuthenticated()) {
761
+ return;
762
+ }
763
+ reportSystemInformation();
764
+ }, 300000);
765
+ }
766
+
767
+ const connector = async () => {
768
+ this.emitAuth('connectDevice', async (response: any) => {
769
+ console.log('connector connectDevice(): response', response);
770
+
771
+ // If server response is valid, use server response
772
+ if (response.status === 'success' && response.twins) {
773
+ callback(response);
774
+ const { twins }: { twins: TwinResponse[] } = response;
775
+ logger.info(
776
+ `SOCKET connector(): received ${twins.length} twins in response. 🟢-🟢-🟢-🟢-🟢`
777
+ );
778
+ await this.cacheTwins(twins);
779
+ } else if (shouldUseCachedResponse && deviceTwin && cachedTwinsInfo) {
780
+ // Fallback to cached response if server fails
781
+ const storage = await getStorage();
782
+ const region = await storage.get('region');
783
+ const phyhubUrl = await storage.get('phyhubUrl');
784
+ const transformedTwins = await this.transformCachedTwinsToArray(cachedTwinsInfo);
785
+ logger.info('connectDevice(): Using cached twins as fallback due to server error');
786
+ callback({ status: 'success', twins: transformedTwins, region, phyhubUrl });
787
+ } else {
788
+ // No cached twins available and server failed
789
+ logger.error(
790
+ `SOCKET connector(): No twins received in response and no cached fallback available. 🟢-🟢-🟢-🟢-🔴`
791
+ );
792
+ callback(response);
793
+ }
794
+ });
795
+ };
796
+ connector();
797
+ socket.on('reconnect', connector);
798
+ }
799
+
800
+ // Add new helper method to transform cached twins format
801
+ private async transformCachedTwinsToArray(cachedTwins: Record<string, string[]>): Promise<TwinResponse[]> {
802
+ const transformedTwins: TwinResponse[] = [];
803
+
804
+ for (const [_type, twinIds] of Object.entries(cachedTwins)) {
805
+ for (const twinId of twinIds) {
806
+ const twin = await this.getCachedTwins(twinId);
807
+ if (twin) {
808
+ transformedTwins.push(twin);
809
+ }
810
+ }
811
+ }
812
+
813
+ return transformedTwins;
814
+ }
815
+
816
+ private cacheTwins = async (twins: TwinResponse[]) => {
817
+ // logger.info(`cacheTwins(): Caching twins`);
818
+
819
+ for (const twin of twins) {
820
+ try {
821
+ // Handle peripheral twin separately
822
+ if (twin.type === TwinTypeEnum.Peripheral) {
823
+ await this.cachePeripheralTwins(twin);
824
+ continue;
825
+ }
826
+
827
+ // Existing job scheduling logic
828
+ const { type, id } = twin;
829
+ if (type === TwinTypeEnum.Device) {
830
+ const reported = twin.properties?.reported as DeviceTwinReportedProperties | undefined;
831
+ const jobs = reported?.jobs ?? [];
832
+ if (jobs.length > 0 && this.scheduler) {
833
+ this.scheduler.processJobs(jobs, twin?.properties?.desired?.timezone ?? 'UTC', id);
834
+ }
835
+ }
836
+ } catch (error) {
837
+ logger.error(`cacheTwins(): Failed to schedule job for device twin ${twin.id}`, error);
838
+ }
839
+ }
840
+
841
+ const storage = await getStorage();
842
+ const twinsInfo: Record<string, string[]> = {};
843
+ let twinsInfoCachePath = ``;
844
+ for (const twin of twins) {
845
+ if (twin && !this.deletedTwins.includes(twin.id)) {
846
+ const { type, deviceId, id } = twin;
847
+ twinsInfoCachePath = `twins-device-${deviceId}`;
848
+
849
+ const twinCachePath = `${id}-device-${deviceId}`;
850
+ // logger.info(`cacheTwins(): Caching ${type} twin ${id}`);
851
+ await storage.set(twinCachePath, twin);
852
+
853
+ if (!twinsInfo[type]) {
854
+ twinsInfo[type] = [];
855
+ }
856
+ twinsInfo[type].push(id);
857
+ }
858
+ }
859
+
860
+ let cachedTwinsInfo = undefined;
861
+ try {
862
+ cachedTwinsInfo = await storage.get(twinsInfoCachePath);
863
+ } catch (error) {
864
+ // logger.info(`cacheTwins(): ${twinsInfoCachePath} not found, creating cache`);
865
+ }
866
+
867
+ if (cachedTwinsInfo) {
868
+ // merge cached twins info with fresh twins info
869
+ const updatedTwinsInfo = { ...cachedTwinsInfo };
870
+ for (const key in twinsInfo) {
871
+ if (Object.prototype.hasOwnProperty.call(updatedTwinsInfo, key)) {
872
+ updatedTwinsInfo[key] = Array.from(
873
+ new Set([...updatedTwinsInfo[key], ...twinsInfo[key]])
874
+ );
875
+ } else {
876
+ updatedTwinsInfo[key] = twinsInfo[key];
877
+ }
878
+ }
879
+
880
+ // logger.info(`cacheTwins(): Updating twins info cache`);
881
+ await storage.set(twinsInfoCachePath, updatedTwinsInfo);
882
+ } else {
883
+ // logger.info(`cacheTwins(): Updating twins info cache`);
884
+ await storage.set(twinsInfoCachePath, twinsInfo);
885
+ }
886
+ };
887
+
888
+ private async removeCachedTwin(twins: TwinResponse[] | TwinResponse | string) {
889
+ const storage = await getStorage();
890
+ const twinsArray = Array.isArray(twins) ? twins : [twins];
891
+
892
+ for (const twin of twinsArray) {
893
+ const twinData = typeof twin === 'string'
894
+ ? await this.getCachedTwins(twin)
895
+ : twin;
896
+
897
+ if (!twinData) continue;
898
+
899
+ const { type, deviceId, id, instanceId } = twinData;
900
+
901
+ // Handle peripheral twins differently
902
+ if (type === TwinTypeEnum.Peripheral && instanceId) {
903
+ await this.peripheralTwinMutex.runExclusive(async () => {
904
+ const cachePath = `peripheral-twins-${instanceId}`;
905
+ const peripheralTwins = await storage.get(cachePath) || {};
906
+ delete peripheralTwins[id];
907
+ await storage.set(cachePath, peripheralTwins);
908
+ });
909
+ continue;
910
+ }
911
+
912
+ // Existing logic for other twin types
913
+ const twinCachePath = `${id}-device-${deviceId}`;
914
+ const twinsInfoCachePath = `twins-device-${deviceId}`;
915
+ try {
916
+ // Remove individual twin cache
917
+ logger.info(`removeCachedTwin(): Removing ${type} twin ${id}`);
918
+ await storage.remove(twinCachePath);
919
+
920
+ // Update twins info cache
921
+ const cachedTwinsInfo = await storage.get(twinsInfoCachePath);
922
+ if (cachedTwinsInfo && cachedTwinsInfo[type]) {
923
+ cachedTwinsInfo[type] = cachedTwinsInfo[type].filter(
924
+ (cachedId: string) => cachedId !== id
925
+ );
926
+
927
+ if (cachedTwinsInfo[type].length === 0) {
928
+ delete cachedTwinsInfo[type];
929
+ }
930
+
931
+ if (Object.keys(cachedTwinsInfo).length > 0) {
932
+ await storage.set(twinsInfoCachePath, cachedTwinsInfo);
933
+ } else {
934
+ await storage.remove(twinsInfoCachePath);
935
+ }
936
+ }
937
+ this.deletedTwins.push(id);
938
+ } catch (error) {
939
+ logger.info(`removeCachedTwin(): No twins info cache found for ${twinsInfoCachePath}`);
940
+ }
941
+ }
942
+ }
943
+
944
+ public getCachedTwins = async (twinId: string | undefined = undefined) => {
945
+ const storage = await getStorage();
946
+ if (twinId) {
947
+ const twinCachePath = `${twinId}-device-${this.deviceId}`;
948
+ const cachedTwin = await storage.get(twinCachePath);
949
+ return cachedTwin;
950
+ } else {
951
+ const twinsInfoCachePath = `twins-device-${this.deviceId}`;
952
+ const cachedTwinsInfo = await storage.get(twinsInfoCachePath) || {};
953
+ return cachedTwinsInfo;
954
+ }
955
+ };
956
+
957
+ public async reportScreenInstanceProperties(
958
+ properties: EventPayload<ScreenTwinReportedProperties>,
959
+ callback: (data: any) => void
960
+ ) {
961
+ const { socket, deviceId } = this;
962
+ if (!socket || !deviceId) {
963
+ return callback({ error: `reportScreenInstanceProperties() ${deviceId}: Not connected` });
964
+ }
965
+
966
+ this.emitAuth('reportScreenTwinProperties', properties, async (response: any) => {
967
+ if (response.status === PhyHubResponseStatus.success) {
968
+ // logger.info(
969
+ // `reportScreenInstanceProperties() on reportScreenProperties: device ${deviceId} screen app properties reported`
970
+ // );
971
+ const { twin } = response;
972
+ if (!twin) {
973
+ logger.info(
974
+ `reportScreenInstanceProperties() on reportScreenProperties: failed to cache screen twin updates for device ${deviceId}`
975
+ );
976
+ } else {
977
+ await this.cacheTwins([twin]);
978
+ }
979
+ } else {
980
+ logger.info(
981
+ `reportScreenInstanceProperties() on reportScreenProperties: failed to report screen app properties of device ${deviceId}`
982
+ );
983
+ }
984
+ callback(response);
985
+ });
986
+ }
987
+
988
+ public async getEdgeInstance(
989
+ payload: EventPayload
990
+ ) {
991
+ const storage = await getStorage();
992
+ const deviceId = await storage.get('deviceId');
993
+
994
+ try {
995
+ const { twinId } = payload;
996
+ const twins = await this.getCachedTwins();
997
+ const edgeTwin = twins[TwinTypeEnum.Edge].filter((id: string) => id === twinId);
998
+
999
+ if (edgeTwin && edgeTwin.length > 0) {
1000
+ const edgeTwinInstance = await this.getCachedTwins(edgeTwin[0]);
1001
+ if (edgeTwinInstance) {
1002
+ return { twin: edgeTwinInstance };
1003
+ }
1004
+ }
1005
+ return {};
1006
+ } catch (error) {
1007
+ logger.error(`Edge instance not found for device ${deviceId}`, error);
1008
+ return {};
1009
+ }
1010
+ }
1011
+
1012
+ public async sendEventSignal(
1013
+ payload: EventPayload<Record<string, any>>
1014
+ ) {
1015
+ logger.info(`sendEventSignal(): sending Event Signal from device ${this.deviceId}`, payload);
1016
+ const { socket, deviceId } = this;
1017
+ if (!socket || !deviceId) {
1018
+ logger.error(`sendEventSignal(): Device or socket not connected`);
1019
+ return;
1020
+ }
1021
+
1022
+ this.emitAuth('signalEvent', payload);
1023
+ }
1024
+
1025
+ public async sendClientSignal(
1026
+ payload: EventPayload<Record<string, any>>
1027
+ ) {
1028
+ logger.info(`sendClientSignal(): sending Client Signal from device ${this.deviceId}`, payload);
1029
+ const { socket, deviceId } = this;
1030
+ if (!socket || !deviceId) {
1031
+ logger.error(`sendClientSignal(): Device or socket not connected`);
1032
+ return;
1033
+ }
1034
+
1035
+ this.emitAuth('signalClient', payload);
1036
+ }
1037
+
1038
+ public async sendSessionSignal(
1039
+ payload: EventPayload<Record<string, any>>
1040
+ ) {
1041
+ logger.info(`sendSessionSignal(): sending Session Signal from device ${this.deviceId}`, payload);
1042
+ const { socket, deviceId } = this;
1043
+ if (!socket || !deviceId) {
1044
+ logger.error(`sendSessionSignal(): Device or socket not connected`);
1045
+ return;
1046
+ }
1047
+
1048
+ this.emitAuth('signalSession', payload);
1049
+ }
1050
+
1051
+ public async reportEdgeTwinProperties(
1052
+ instanceTwinId: string,
1053
+ properties: EdgeTwinReportedProperties,
1054
+ callback: (data: any) => void
1055
+ ) {
1056
+ logger.info(`reportEdgeTwinProperties(): Reporting twin ${instanceTwinId} properties`);
1057
+ const { socket, deviceId } = this;
1058
+ if (!socket || !deviceId) {
1059
+ return callback({ error: `reportEdgeTwinProperties(): Device or socket not connected` });
1060
+ }
1061
+
1062
+ const payload: EventPayload = {
1063
+ twinId: instanceTwinId,
1064
+ data: properties,
1065
+ };
1066
+
1067
+ this.emitAuth('reportEdgeTwinProperties', payload, async (response: any) => {
1068
+ const { twin } = response;
1069
+ if (twin) {
1070
+ await this.cacheTwins([twin]);
1071
+ }
1072
+ });
1073
+ }
1074
+
1075
+ public async reportDeviceTwinProperties(
1076
+ instanceTwinId: string,
1077
+ properties: DeviceTwinReportedProperties,
1078
+ callback: (data: any) => void
1079
+ ) {
1080
+ // logger.info(`reportDeviceTwinProperties(): Reporting device twin ${instanceTwinId} properties`);
1081
+ const { socket, deviceId } = this;
1082
+ if (!socket || !deviceId) {
1083
+ return callback({ error: `reportDeviceTwinProperties(): Device or socket not connected` });
1084
+ }
1085
+
1086
+ const payload: EventPayload<DeviceTwinReportedProperties> = {
1087
+ twinId: instanceTwinId,
1088
+ data: properties,
1089
+ };
1090
+
1091
+ this.emitAuth('reportDeviceTwinProperties', payload, async (response: any) => {
1092
+ const { twin } = response;
1093
+ if (twin) {
1094
+ await this.cacheTwins([twin]);
1095
+ }
1096
+ });
1097
+ }
1098
+
1099
+ public async reportDeviceJob(instanceTwinId: string, job: Job, callback: (data: any) => void) {
1100
+ // logger.info(`reportDeviceJob(): Reporting device twin ${instanceTwinId} job ${job.id}`);
1101
+ const { socket, deviceId } = this;
1102
+ if (!socket || !deviceId) {
1103
+ return callback({ error: `reportDeviceJob(): Device or socket not connected` });
1104
+ }
1105
+
1106
+ const payload: EventPayload<Job> = {
1107
+ twinId: instanceTwinId,
1108
+ data: job,
1109
+ };
1110
+
1111
+ this.emitAuth('reportDeviceJob', payload, async (response: any) => {
1112
+ const { twin } = response;
1113
+ if (twin) {
1114
+ await this.cacheTwins([twin]);
1115
+ }
1116
+ });
1117
+ }
1118
+
1119
+ public async reportPeripheralTwinProperties(
1120
+ instanceTwinId: string,
1121
+ properties: PeripheralTwinReportedProperties,
1122
+ callback?: (data: any) => void
1123
+ ) {
1124
+ // logger.info(`reportPeripheralTwinProperties(): Reporting twin ${instanceTwinId} properties`);
1125
+ const { socket, deviceId } = this;
1126
+ if (!socket || !deviceId) {
1127
+ return callback?.({ error: `reportPeripheralTwinProperties(): Device or socket not connected` });
1128
+ }
1129
+
1130
+ const payload: EventPayload = {
1131
+ twinId: instanceTwinId,
1132
+ data: properties,
1133
+ };
1134
+
1135
+ this.emitAuth('reportPeripheralTwinProperties', payload, async (response: any) => {
1136
+ const { twin } = response;
1137
+ if (twin) {
1138
+ await this.cacheTwins([twin]);
1139
+ }
1140
+ callback?.(response);
1141
+ });
1142
+ }
1143
+
1144
+ public setScheduler(scheduler: any) {
1145
+ this.scheduler = scheduler;
1146
+ if (scheduler && typeof scheduler.setHubDevice === 'function') {
1147
+ scheduler.setHubDevice(this);
1148
+ }
1149
+ logger.info('setScheduler(): Scheduler has been set');
1150
+ }
1151
+
1152
+ private async cachePeripheralTwins(twin: TwinResponse) {
1153
+ if (twin.type !== TwinTypeEnum.Peripheral || !twin.instanceId) return;
1154
+
1155
+ return await this.peripheralTwinMutex.runExclusive(async () => {
1156
+ const storage = await getStorage();
1157
+ const cachePath = `peripheral-twins-${twin.instanceId}`;
1158
+ let peripheralTwins = await storage.get(cachePath) || {};
1159
+
1160
+ peripheralTwins[twin.id] = twin;
1161
+ await storage.set(cachePath, peripheralTwins);
1162
+ return peripheralTwins;
1163
+ });
1164
+ }
1165
+
1166
+ public async getPeripheralTwins(payload: EventPayload<{ instanceId: string }>) {
1167
+ const { data } = payload;
1168
+ // console.log('getPeripheralTwins(): payload', payload);
1169
+ if (!data?.instanceId) {
1170
+ logger.error('getPeripheralTwins(): Missing instanceId in payload');
1171
+ return { peripheralTwins: {} };
1172
+ }
1173
+ return await this.peripheralTwinMutex.runExclusive(async () => {
1174
+ // Get current cached twins
1175
+ const storage = await getStorage();
1176
+ const cachePath = `peripheral-twins-${data.instanceId}`;
1177
+ const cachedTwins = await storage.get(cachePath) || {};
1178
+
1179
+ try {
1180
+ // Get fresh twins from server
1181
+ const response = await new Promise<{ peripheralTwins: Record<string, TwinResponse> }>((resolve, reject) => {
1182
+ this.emitAuth('getPeripheralTwins', payload, resolve);
1183
+ this.socket?.on('error', reject);
1184
+ });
1185
+
1186
+ // Ensure peripheralTwins exists and is an object
1187
+ const peripheralTwins = response?.peripheralTwins || {};
1188
+
1189
+ // Cache new twins using cachePeripheralTwins
1190
+ for (const twin of Object.values(peripheralTwins)) {
1191
+ await this.cachePeripheralTwins(twin);
1192
+ }
1193
+
1194
+ // Find and remove deleted twins
1195
+ const cachedTwinIds = Object.keys(cachedTwins);
1196
+ const currentTwinIds = Object.keys(peripheralTwins);
1197
+ const deletedTwinIds = cachedTwinIds.filter(id => !currentTwinIds.includes(id));
1198
+
1199
+ if (deletedTwinIds.length > 0) {
1200
+ await this.removeCachedTwin(deletedTwinIds.map(id => cachedTwins[id]));
1201
+ }
1202
+
1203
+ return { peripheralTwins };
1204
+ } catch (error) {
1205
+ logger.error('getPeripheralTwins(): Error during execution', error);
1206
+ return { peripheralTwins: {} };
1207
+ }
1208
+ });
1209
+ }
1210
+
1211
+ public onReconnect(listener: () => void) {
1212
+ this.reconnectListeners.push(listener);
1213
+ // logger.info('onReconnect(): Added new reconnect listener');
1214
+ }
1215
+
1216
+ public onAuthReconnect(listener: () => void) {
1217
+ this.authReconnectListeners.push(listener);
1218
+ // logger.info('onAuthReconnect(): Added new auth reconnect listener');
1219
+ }
1220
+ }
1221
+
1222
+
1223
+
1224
+ export default {
1225
+ connectToPhyHub,
1226
+ getPhyHubInstance,
1227
+ getProvisioningInfo,
1228
+ };