@homebridge-plugins/homebridge-eufy-security 4.6.2-beta.4 → 4.6.2-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/version.js +1 -1
- package/homebridge-ui/server.js +163 -232
- package/package.json +1 -1
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const LIB_VERSION = "4.6.2-beta.
|
|
1
|
+
export const LIB_VERSION = "4.6.2-beta.5";
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/homebridge-ui/server.js
CHANGED
|
@@ -7,6 +7,8 @@ import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils';
|
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import { createRequire } from 'module';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
10
12
|
import { encryptDiagnostics } from './diagnosticsCrypto.js';
|
|
11
13
|
|
|
12
14
|
const require = createRequire(import.meta.url);
|
|
@@ -22,14 +24,22 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
22
24
|
log;
|
|
23
25
|
tsLog;
|
|
24
26
|
storagePath;
|
|
25
|
-
|
|
27
|
+
storedAccessoriesPath;
|
|
28
|
+
unsupportedPath;
|
|
26
29
|
|
|
27
30
|
adminAccountUsed = false;
|
|
28
31
|
|
|
29
32
|
// Batch processing for stations and devices
|
|
30
33
|
pendingStations = [];
|
|
31
34
|
pendingDevices = [];
|
|
32
|
-
|
|
35
|
+
|
|
36
|
+
// Timer handles — declared here for visibility; cleared via scoped helpers below
|
|
37
|
+
/** @type {ReturnType<typeof setTimeout>|null} */ _loginTimeout = null;
|
|
38
|
+
/** @type {ReturnType<typeof setTimeout>|null} */ processingTimeout = null;
|
|
39
|
+
/** @type {ReturnType<typeof setTimeout>|null} */ _closeTimeout = null;
|
|
40
|
+
/** @type {ReturnType<typeof setInterval>|null} */ _debounceTickInterval = null;
|
|
41
|
+
/** @type {ReturnType<typeof setTimeout>|null} */ _discoveryInactivityTimeout = null;
|
|
42
|
+
/** @type {ReturnType<typeof setInterval>|null} */ _discoveryInactivityTickInterval = null;
|
|
33
43
|
|
|
34
44
|
/** Set to true when the user clicks "Skip" in the UI to abort the unsupported intel wait. */
|
|
35
45
|
_skipIntelWait = false;
|
|
@@ -69,8 +79,8 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
69
79
|
super();
|
|
70
80
|
|
|
71
81
|
this.storagePath = this.homebridgeStoragePath + '/eufysecurity';
|
|
72
|
-
this.
|
|
73
|
-
this.
|
|
82
|
+
this.storedAccessoriesPath = this.storagePath + '/accessories.json';
|
|
83
|
+
this.unsupportedPath = this.storagePath + '/unsupported.json';
|
|
74
84
|
this.config.persistentDir = this.storagePath;
|
|
75
85
|
|
|
76
86
|
this.initLogger();
|
|
@@ -169,9 +179,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
169
179
|
}
|
|
170
180
|
|
|
171
181
|
initTransportStreams() {
|
|
172
|
-
|
|
173
|
-
fs.mkdirSync(this.storagePath, { recursive: true });
|
|
174
|
-
}
|
|
182
|
+
fs.mkdirSync(this.storagePath, { recursive: true });
|
|
175
183
|
|
|
176
184
|
const logStreams = [
|
|
177
185
|
{ name: 'configui-server.log', logger: this.log },
|
|
@@ -246,6 +254,17 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
246
254
|
};
|
|
247
255
|
}
|
|
248
256
|
|
|
257
|
+
/** Emit a discovery progress event to the UI. */
|
|
258
|
+
_emitProgress(phase, progress, message) {
|
|
259
|
+
this.pushEvent('discoveryProgress', {
|
|
260
|
+
phase,
|
|
261
|
+
progress,
|
|
262
|
+
stations: this.pendingStations.length,
|
|
263
|
+
devices: this.pendingDevices.length,
|
|
264
|
+
message,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
249
268
|
async deleteFileIfExists(filePath) {
|
|
250
269
|
try {
|
|
251
270
|
await fs.promises.unlink(filePath);
|
|
@@ -261,7 +280,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
261
280
|
}
|
|
262
281
|
|
|
263
282
|
async resetAccessoryData() {
|
|
264
|
-
return this.deleteFileIfExists(this.
|
|
283
|
+
return this.deleteFileIfExists(this.storedAccessoriesPath);
|
|
265
284
|
}
|
|
266
285
|
|
|
267
286
|
async checkCache() {
|
|
@@ -282,11 +301,13 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
282
301
|
}
|
|
283
302
|
|
|
284
303
|
async login(options) {
|
|
304
|
+
this._clearLoginTimeout();
|
|
305
|
+
|
|
285
306
|
// Block login if the plugin is already running (accessories updated within 90s)
|
|
286
307
|
if (!this.eufyClient) {
|
|
287
308
|
try {
|
|
288
|
-
if (fs.existsSync(this.
|
|
289
|
-
const data = JSON.parse(fs.readFileSync(this.
|
|
309
|
+
if (fs.existsSync(this.storedAccessoriesPath)) {
|
|
310
|
+
const data = JSON.parse(fs.readFileSync(this.storedAccessoriesPath, 'utf-8'));
|
|
290
311
|
if (data?.storedAt) {
|
|
291
312
|
const ageMs = Date.now() - new Date(data.storedAt).getTime();
|
|
292
313
|
if (ageMs < 90_000) {
|
|
@@ -353,8 +374,8 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
353
374
|
this.eufyClient?.on('connect', () => this.log.debug('Connected!'));
|
|
354
375
|
this.eufyClient?.on('close', () => this.log.debug('Closed!'));
|
|
355
376
|
} catch (error) {
|
|
356
|
-
this.log.error(error);
|
|
357
|
-
this.pushEvent('authError', { message:
|
|
377
|
+
this.log.error('EufySecurity init failed:', error);
|
|
378
|
+
this.pushEvent('authError', { message: 'Initialization failed. Check the logs for details.' });
|
|
358
379
|
this._discoveryPhase = 'idle';
|
|
359
380
|
return { success: false };
|
|
360
381
|
}
|
|
@@ -373,52 +394,46 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
373
394
|
.then(() => this.log.debug('connected?: ' + this.eufyClient?.isConnected()))
|
|
374
395
|
.catch((error) => this.log.error(error));
|
|
375
396
|
} catch (error) {
|
|
376
|
-
this.log.error(error);
|
|
377
|
-
|
|
378
|
-
this.pushEvent('authError', { message: 'Login
|
|
397
|
+
this.log.error('Login error:', error);
|
|
398
|
+
this._clearLoginTimeout();
|
|
399
|
+
this.pushEvent('authError', { message: 'Login failed. Check the logs for details.' });
|
|
379
400
|
}
|
|
380
401
|
} else if (options && options.verifyCode) {
|
|
381
402
|
this.log.debug('login with TFA code');
|
|
382
|
-
this.
|
|
383
|
-
phase: 'authenticating',
|
|
384
|
-
progress: 10,
|
|
385
|
-
message: 'Verifying TFA code...',
|
|
386
|
-
});
|
|
403
|
+
this._emitProgress('authenticating', 10, 'Verifying TFA code...');
|
|
387
404
|
try {
|
|
388
405
|
this._registerOneTimeAuthHandlers();
|
|
389
406
|
this.eufyClient?.connect({ verifyCode: options.verifyCode, force: false })
|
|
390
407
|
.then(() => this.log.debug('TFA connect resolved, connected?: ' + this.eufyClient?.isConnected()))
|
|
391
408
|
.catch((error) => {
|
|
392
|
-
this.log.error('TFA connect error:
|
|
393
|
-
|
|
394
|
-
this.pushEvent('authError', { message: 'TFA verification failed
|
|
409
|
+
this.log.error('TFA connect error:', error);
|
|
410
|
+
this._clearLoginTimeout();
|
|
411
|
+
this.pushEvent('authError', { message: 'TFA verification failed. Check the logs for details.' });
|
|
395
412
|
});
|
|
396
413
|
} catch (error) {
|
|
397
|
-
|
|
398
|
-
this.
|
|
414
|
+
this.log.error('TFA verification error:', error);
|
|
415
|
+
this._clearLoginTimeout();
|
|
416
|
+
this.pushEvent('authError', { message: 'TFA verification failed. Check the logs for details.' });
|
|
399
417
|
}
|
|
400
418
|
} else if (options && options.captcha) {
|
|
401
419
|
this.log.debug('login with captcha');
|
|
402
|
-
this.
|
|
403
|
-
phase: 'authenticating',
|
|
404
|
-
progress: 10,
|
|
405
|
-
message: 'Verifying captcha...',
|
|
406
|
-
});
|
|
420
|
+
this._emitProgress('authenticating', 10, 'Verifying captcha...');
|
|
407
421
|
try {
|
|
408
422
|
this._registerOneTimeAuthHandlers();
|
|
409
423
|
this.eufyClient?.connect({ captcha: { captchaCode: options.captcha.captchaCode, captchaId: options.captcha.captchaId }, force: false })
|
|
410
424
|
.then(() => this.log.debug('Captcha connect resolved, connected?: ' + this.eufyClient?.isConnected()))
|
|
411
425
|
.catch((error) => {
|
|
412
|
-
this.log.error('Captcha connect error:
|
|
413
|
-
|
|
414
|
-
this.pushEvent('authError', { message: 'Captcha verification failed
|
|
426
|
+
this.log.error('Captcha connect error:', error);
|
|
427
|
+
this._clearLoginTimeout();
|
|
428
|
+
this.pushEvent('authError', { message: 'Captcha verification failed. Check the logs for details.' });
|
|
415
429
|
});
|
|
416
430
|
} catch (error) {
|
|
417
|
-
|
|
418
|
-
this.
|
|
431
|
+
this.log.error('Captcha verification error:', error);
|
|
432
|
+
this._clearLoginTimeout();
|
|
433
|
+
this.pushEvent('authError', { message: 'Captcha verification failed. Check the logs for details.' });
|
|
419
434
|
}
|
|
420
435
|
} else {
|
|
421
|
-
|
|
436
|
+
this._clearLoginTimeout();
|
|
422
437
|
this.pushEvent('authError', { message: 'Unsupported login method.' });
|
|
423
438
|
}
|
|
424
439
|
|
|
@@ -429,24 +444,20 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
429
444
|
/** Register once-only auth event handlers (TFA, captcha, connect) on the eufy client. */
|
|
430
445
|
_registerOneTimeAuthHandlers() {
|
|
431
446
|
this.eufyClient?.once('tfa request', () => {
|
|
432
|
-
|
|
447
|
+
this._clearLoginTimeout();
|
|
433
448
|
this.pushEvent('tfaRequest', {});
|
|
434
449
|
});
|
|
435
450
|
this.eufyClient?.once('captcha request', (id, captcha) => {
|
|
436
|
-
|
|
451
|
+
this._clearLoginTimeout();
|
|
437
452
|
this.pushEvent('captchaRequest', { id, captcha });
|
|
438
453
|
});
|
|
439
454
|
this.eufyClient?.once('connect', () => {
|
|
440
|
-
|
|
455
|
+
this._clearLoginTimeout();
|
|
441
456
|
if (this.adminAccountUsed) {
|
|
442
457
|
return;
|
|
443
458
|
}
|
|
444
459
|
this.pushEvent('authSuccess', {});
|
|
445
|
-
this.
|
|
446
|
-
phase: 'authenticating',
|
|
447
|
-
progress: 15,
|
|
448
|
-
message: 'Authenticated — waiting for devices...',
|
|
449
|
-
});
|
|
460
|
+
this._emitProgress('authenticating', 15, 'Authenticated — waiting for devices...');
|
|
450
461
|
this._startDiscoveryInactivityTimer();
|
|
451
462
|
});
|
|
452
463
|
}
|
|
@@ -465,11 +476,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
465
476
|
const elapsed = Math.floor((Date.now() - start) / 1000);
|
|
466
477
|
const remaining = Math.max(0, totalSec - elapsed);
|
|
467
478
|
const pct = Math.min(95, 15 + Math.floor((elapsed / totalSec) * 80));
|
|
468
|
-
this.
|
|
469
|
-
phase: 'waitingForDevices',
|
|
470
|
-
progress: pct,
|
|
471
|
-
message: `Authenticated — waiting for devices... ${remaining}s`,
|
|
472
|
-
});
|
|
479
|
+
this._emitProgress('waitingForDevices', pct, `Authenticated — waiting for devices... ${remaining}s`);
|
|
473
480
|
}, 1000);
|
|
474
481
|
|
|
475
482
|
this._discoveryInactivityTimeout = setTimeout(() => {
|
|
@@ -486,11 +493,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
486
493
|
} catch (error) {
|
|
487
494
|
this.log.error('Error storing empty accessories:', error);
|
|
488
495
|
}
|
|
489
|
-
this.
|
|
490
|
-
phase: 'done',
|
|
491
|
-
progress: 100,
|
|
492
|
-
message: 'No devices found.',
|
|
493
|
-
});
|
|
496
|
+
this._emitProgress('done', 100, 'No devices found.');
|
|
494
497
|
this.pushEvent('addAccessory', { stations: [], noDevices: true });
|
|
495
498
|
this.eufyClient?.removeAllListeners();
|
|
496
499
|
this.eufyClient?.close();
|
|
@@ -522,35 +525,35 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
522
525
|
`);
|
|
523
526
|
}
|
|
524
527
|
|
|
525
|
-
/** Clear
|
|
526
|
-
|
|
528
|
+
/** Clear the login timeout. */
|
|
529
|
+
_clearLoginTimeout() {
|
|
527
530
|
clearTimeout(this._loginTimeout);
|
|
528
531
|
this._loginTimeout = null;
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
this._debounceTickInterval = null;
|
|
540
|
-
}
|
|
541
|
-
this._clearDiscoveryInactivityTimer();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Clear debounce-related timers (processing, close, tick interval). */
|
|
535
|
+
_clearDebounceTimers() {
|
|
536
|
+
clearTimeout(this.processingTimeout);
|
|
537
|
+
this.processingTimeout = null;
|
|
538
|
+
clearTimeout(this._closeTimeout);
|
|
539
|
+
this._closeTimeout = null;
|
|
540
|
+
clearInterval(this._debounceTickInterval);
|
|
541
|
+
this._debounceTickInterval = null;
|
|
542
542
|
}
|
|
543
543
|
|
|
544
544
|
/** Clear the post-auth discovery inactivity timer. */
|
|
545
545
|
_clearDiscoveryInactivityTimer() {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
546
|
+
clearInterval(this._discoveryInactivityTickInterval);
|
|
547
|
+
this._discoveryInactivityTickInterval = null;
|
|
548
|
+
clearTimeout(this._discoveryInactivityTimeout);
|
|
549
|
+
this._discoveryInactivityTimeout = null;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Clear all pending timers. */
|
|
553
|
+
_clearAllTimers() {
|
|
554
|
+
this._clearLoginTimeout();
|
|
555
|
+
this._clearDebounceTimers();
|
|
556
|
+
this._clearDiscoveryInactivityTimer();
|
|
554
557
|
}
|
|
555
558
|
|
|
556
559
|
/** Parse a semver string (e.g. '4.4.2-beta.18') into [major, minor, patch]. */
|
|
@@ -560,12 +563,12 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
560
563
|
|
|
561
564
|
async loadStoredAccessories() {
|
|
562
565
|
try {
|
|
563
|
-
if (!fs.existsSync(this.
|
|
566
|
+
if (!fs.existsSync(this.storedAccessoriesPath)) {
|
|
564
567
|
this.log.debug('Stored accessories file does not exist.');
|
|
565
568
|
return [];
|
|
566
569
|
}
|
|
567
570
|
|
|
568
|
-
const storedData = await fs.promises.readFile(this.
|
|
571
|
+
const storedData = await fs.promises.readFile(this.storedAccessoriesPath, { encoding: 'utf-8' });
|
|
569
572
|
const { version: storedVersion, storedAt, stations: storedAccessories } = JSON.parse(storedData);
|
|
570
573
|
|
|
571
574
|
// --- Cache age check (30 days) ---
|
|
@@ -610,10 +613,6 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
610
613
|
}
|
|
611
614
|
}
|
|
612
615
|
|
|
613
|
-
async delay(ms) {
|
|
614
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
615
|
-
}
|
|
616
|
-
|
|
617
616
|
async _onStationDiscovered(station) {
|
|
618
617
|
if (this.adminAccountUsed) {
|
|
619
618
|
return;
|
|
@@ -628,13 +627,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
628
627
|
this.pendingStations.push(station);
|
|
629
628
|
this.log.debug(`${station.getName()}: Station queued for processing`);
|
|
630
629
|
this._discoveryPhase = 'queuing';
|
|
631
|
-
this.
|
|
632
|
-
phase: 'queuing',
|
|
633
|
-
progress: 30,
|
|
634
|
-
stations: this.pendingStations.length,
|
|
635
|
-
devices: this.pendingDevices.length,
|
|
636
|
-
message: `Discovered ${this.pendingStations.length} station(s), ${this.pendingDevices.length} device(s)...`,
|
|
637
|
-
});
|
|
630
|
+
this._emitProgress('queuing', 30, `Discovered ${this.pendingStations.length} station(s), ${this.pendingDevices.length} device(s)...`);
|
|
638
631
|
this._restartDiscoveryDebounce();
|
|
639
632
|
}
|
|
640
633
|
|
|
@@ -654,19 +647,14 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
654
647
|
this.pendingDevices.push(device);
|
|
655
648
|
this.log.debug(`${device.getName()}: Device queued for processing`);
|
|
656
649
|
this._discoveryPhase = 'queuing';
|
|
657
|
-
this.
|
|
658
|
-
phase: 'queuing',
|
|
659
|
-
progress: 30,
|
|
660
|
-
stations: this.pendingStations.length,
|
|
661
|
-
devices: this.pendingDevices.length,
|
|
662
|
-
message: `Discovered ${this.pendingStations.length} station(s), ${this.pendingDevices.length} device(s)...`,
|
|
663
|
-
});
|
|
650
|
+
this._emitProgress('queuing', 30, `Discovered ${this.pendingStations.length} station(s), ${this.pendingDevices.length} device(s)...`);
|
|
664
651
|
this._restartDiscoveryDebounce();
|
|
665
652
|
}
|
|
666
653
|
|
|
667
654
|
/** Restart the debounce timer — processing fires after DISCOVERY_DEBOUNCE_SEC of silence. */
|
|
668
655
|
_restartDiscoveryDebounce() {
|
|
669
|
-
this.
|
|
656
|
+
this._clearDebounceTimers();
|
|
657
|
+
this._clearDiscoveryInactivityTimer();
|
|
670
658
|
const delaySec = UiServer.DISCOVERY_DEBOUNCE_SEC;
|
|
671
659
|
this.log.debug(
|
|
672
660
|
`Discovery debounce reset — will process in ${delaySec}s if no more devices arrive ` +
|
|
@@ -679,13 +667,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
679
667
|
const elapsed = (Date.now() - debounceStart) / 1000;
|
|
680
668
|
const pct = Math.min(95, 30 + Math.floor((elapsed / delaySec) * 65));
|
|
681
669
|
const remaining = Math.max(0, Math.ceil(delaySec - elapsed));
|
|
682
|
-
this.
|
|
683
|
-
phase: 'queuing',
|
|
684
|
-
progress: pct,
|
|
685
|
-
stations: this.pendingStations.length,
|
|
686
|
-
devices: this.pendingDevices.length,
|
|
687
|
-
message: `Discovered ${this.pendingStations.length} station(s), ${this.pendingDevices.length} device(s) — waiting for more... ${remaining}s`,
|
|
688
|
-
});
|
|
670
|
+
this._emitProgress('queuing', pct, `Discovered ${this.pendingStations.length} station(s), ${this.pendingDevices.length} device(s) — waiting for more... ${remaining}s`);
|
|
689
671
|
}, 1000);
|
|
690
672
|
|
|
691
673
|
this.processingTimeout = setTimeout(() => {
|
|
@@ -706,13 +688,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
706
688
|
this.log.debug(`Processing ${this.pendingStations.length} stations and ${this.pendingDevices.length} devices`);
|
|
707
689
|
|
|
708
690
|
this._discoveryPhase = 'processing';
|
|
709
|
-
this.
|
|
710
|
-
phase: 'processing',
|
|
711
|
-
progress: 95,
|
|
712
|
-
stations: this.pendingStations.length,
|
|
713
|
-
devices: this.pendingDevices.length,
|
|
714
|
-
message: `Processing ${this.pendingStations.length} station(s) and ${this.pendingDevices.length} device(s)...`,
|
|
715
|
-
});
|
|
691
|
+
this._emitProgress('processing', 95, `Processing ${this.pendingStations.length} station(s) and ${this.pendingDevices.length} device(s)...`);
|
|
716
692
|
|
|
717
693
|
if (this.pendingStations.length === 0 || this.pendingDevices.length === 0) {
|
|
718
694
|
this.log.warn(
|
|
@@ -754,15 +730,11 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
754
730
|
const pollMs = 1000;
|
|
755
731
|
let waited = 0;
|
|
756
732
|
while (waited < UNSUPPORTED_INTEL_WAIT_MS && !this._skipIntelWait) {
|
|
757
|
-
await
|
|
733
|
+
await delay(pollMs);
|
|
758
734
|
waited += pollMs;
|
|
759
735
|
const pct = Math.min(95, 50 + Math.floor((waited / UNSUPPORTED_INTEL_WAIT_MS) * 45));
|
|
760
736
|
const remaining = Math.max(0, Math.ceil((UNSUPPORTED_INTEL_WAIT_MS - waited) / 1000));
|
|
761
|
-
this.
|
|
762
|
-
phase: 'unsupportedWait',
|
|
763
|
-
progress: pct,
|
|
764
|
-
message: `Collecting data for ${unsupportedItems.length} unsupported device(s)... ${remaining}s`,
|
|
765
|
-
});
|
|
737
|
+
this._emitProgress('unsupportedWait', pct, `Collecting data for ${unsupportedItems.length} unsupported device(s)... ${remaining}s`);
|
|
766
738
|
}
|
|
767
739
|
|
|
768
740
|
if (this._skipIntelWait) {
|
|
@@ -772,11 +744,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
772
744
|
}
|
|
773
745
|
}
|
|
774
746
|
|
|
775
|
-
this.
|
|
776
|
-
phase: 'buildingStations',
|
|
777
|
-
progress: 96,
|
|
778
|
-
message: 'Building station list...',
|
|
779
|
-
});
|
|
747
|
+
this._emitProgress('buildingStations', 96, 'Building station list...');
|
|
780
748
|
|
|
781
749
|
// Process queued stations
|
|
782
750
|
for (const station of this.pendingStations) {
|
|
@@ -844,11 +812,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
844
812
|
this.stations.push(s);
|
|
845
813
|
}
|
|
846
814
|
|
|
847
|
-
this.
|
|
848
|
-
phase: 'buildingDevices',
|
|
849
|
-
progress: 98,
|
|
850
|
-
message: 'Building device list...',
|
|
851
|
-
});
|
|
815
|
+
this._emitProgress('buildingDevices', 98, 'Building device list...');
|
|
852
816
|
|
|
853
817
|
// Process queued devices and attach them to stations
|
|
854
818
|
for (const device of this.pendingDevices) {
|
|
@@ -931,22 +895,16 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
931
895
|
this.log.error('Error storing accessories:', error);
|
|
932
896
|
}
|
|
933
897
|
|
|
934
|
-
this.
|
|
935
|
-
phase: 'done',
|
|
936
|
-
progress: 100,
|
|
937
|
-
message: 'Discovery complete!',
|
|
938
|
-
});
|
|
898
|
+
this._emitProgress('done', 100, 'Discovery complete!');
|
|
939
899
|
|
|
940
900
|
this.pushEvent('addAccessory', { stations: this.stations, extendedDiscovery: unsupportedItems.length > 0 });
|
|
941
901
|
}
|
|
942
902
|
|
|
943
903
|
/** Persist discovered stations/devices to accessories.json. */
|
|
944
904
|
storeAccessories() {
|
|
945
|
-
|
|
946
|
-
fs.mkdirSync(this.storagePath, { recursive: true });
|
|
947
|
-
}
|
|
905
|
+
fs.mkdirSync(this.storagePath, { recursive: true });
|
|
948
906
|
const dataToStore = { version: LIB_VERSION, storedAt: new Date().toISOString(), stations: this.stations };
|
|
949
|
-
fs.writeFileSync(this.
|
|
907
|
+
fs.writeFileSync(this.storedAccessoriesPath, JSON.stringify(dataToStore));
|
|
950
908
|
}
|
|
951
909
|
|
|
952
910
|
// ── Sensitive-field redaction ──────────────────────────────────────────────
|
|
@@ -1049,70 +1007,47 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1049
1007
|
for (const station of pendingStations) {
|
|
1050
1008
|
const stationType = station.getDeviceType();
|
|
1051
1009
|
if (!Device.isStation(stationType) && !Device.isSupported(stationType)) {
|
|
1052
|
-
unsupportedEntries.push(this.
|
|
1010
|
+
unsupportedEntries.push(this._buildUnsupportedEntry(station, 'station'));
|
|
1053
1011
|
}
|
|
1054
1012
|
}
|
|
1055
1013
|
|
|
1056
1014
|
// Collect unsupported devices
|
|
1057
1015
|
for (const device of pendingDevices) {
|
|
1058
1016
|
if (!Device.isSupported(device.getDeviceType())) {
|
|
1059
|
-
unsupportedEntries.push(this.
|
|
1017
|
+
unsupportedEntries.push(this._buildUnsupportedEntry(device, 'device'));
|
|
1060
1018
|
}
|
|
1061
1019
|
}
|
|
1062
1020
|
|
|
1063
|
-
|
|
1064
|
-
fs.mkdirSync(this.storagePath, { recursive: true });
|
|
1065
|
-
}
|
|
1021
|
+
fs.mkdirSync(this.storagePath, { recursive: true });
|
|
1066
1022
|
|
|
1067
1023
|
const dataToStore = { version: LIB_VERSION, storedAt: new Date().toISOString(), devices: unsupportedEntries };
|
|
1068
|
-
fs.writeFileSync(this.
|
|
1024
|
+
fs.writeFileSync(this.unsupportedPath, JSON.stringify(dataToStore));
|
|
1069
1025
|
this.log.debug(`Persisted ${unsupportedEntries.length} unsupported device(s) to unsupported.json`);
|
|
1070
1026
|
}
|
|
1071
1027
|
|
|
1072
|
-
/** Build a triage-ready intel object for an unsupported device. */
|
|
1073
|
-
|
|
1074
|
-
const
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
return {
|
|
1081
|
-
uniqueId: device.getSerial(),
|
|
1082
|
-
displayName: device.getName(),
|
|
1083
|
-
type: device.getDeviceType(),
|
|
1084
|
-
typename: DeviceType[device.getDeviceType()] || undefined,
|
|
1085
|
-
stationSerialNumber: device.getStationSerial(),
|
|
1086
|
-
model: rawDevice.device_model,
|
|
1087
|
-
hardwareVersion: rawDevice.main_hw_version,
|
|
1088
|
-
softwareVersion: rawDevice.main_sw_version,
|
|
1089
|
-
wifiSsid: wifiSsid ? UiServer._partialMask(wifiSsid, 3, 0) : undefined,
|
|
1090
|
-
localIp: localIp ? UiServer._partialMask(localIp, 3, 0) : undefined,
|
|
1091
|
-
rawDevice: UiServer._redactSensitiveFields(rawDevice),
|
|
1092
|
-
rawProperties: rawProps,
|
|
1093
|
-
};
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
/** Build a triage-ready intel object for an unsupported standalone station. */
|
|
1097
|
-
_buildUnsupportedStationEntry(station) {
|
|
1098
|
-
const rawStation = station.getRawStation ? station.getRawStation() : {};
|
|
1099
|
-
const rawProps = station.getRawProperties ? station.getRawProperties() : {};
|
|
1028
|
+
/** Build a triage-ready intel object for an unsupported device or station. */
|
|
1029
|
+
_buildUnsupportedEntry(item, kind) {
|
|
1030
|
+
const isStation = kind === 'station';
|
|
1031
|
+
const rawData = isStation
|
|
1032
|
+
? (item.getRawStation ? item.getRawStation() : {})
|
|
1033
|
+
: (item.getRawDevice ? item.getRawDevice() : {});
|
|
1034
|
+
const rawProps = item.getRawProperties ? item.getRawProperties() : {};
|
|
1100
1035
|
|
|
1101
|
-
const wifiSsid =
|
|
1102
|
-
const localIp =
|
|
1036
|
+
const wifiSsid = rawData.wifi_ssid || undefined;
|
|
1037
|
+
const localIp = rawData.ip_addr || rawData.local_ip || undefined;
|
|
1103
1038
|
|
|
1104
1039
|
return {
|
|
1105
|
-
uniqueId:
|
|
1106
|
-
displayName:
|
|
1107
|
-
type:
|
|
1108
|
-
typename: DeviceType[
|
|
1109
|
-
stationSerialNumber:
|
|
1110
|
-
model:
|
|
1111
|
-
hardwareVersion:
|
|
1112
|
-
softwareVersion:
|
|
1040
|
+
uniqueId: item.getSerial(),
|
|
1041
|
+
displayName: item.getName(),
|
|
1042
|
+
type: item.getDeviceType(),
|
|
1043
|
+
typename: DeviceType[item.getDeviceType()] || undefined,
|
|
1044
|
+
stationSerialNumber: isStation ? item.getSerial() : item.getStationSerial(),
|
|
1045
|
+
model: rawData[isStation ? 'station_model' : 'device_model'],
|
|
1046
|
+
hardwareVersion: rawData.main_hw_version,
|
|
1047
|
+
softwareVersion: rawData.main_sw_version,
|
|
1113
1048
|
wifiSsid: wifiSsid ? UiServer._partialMask(wifiSsid, 3, 0) : undefined,
|
|
1114
1049
|
localIp: localIp ? UiServer._partialMask(localIp, 3, 0) : undefined,
|
|
1115
|
-
rawDevice: UiServer._redactSensitiveFields(
|
|
1050
|
+
rawDevice: UiServer._redactSensitiveFields(rawData),
|
|
1116
1051
|
rawProperties: rawProps,
|
|
1117
1052
|
};
|
|
1118
1053
|
}
|
|
@@ -1120,10 +1055,10 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1120
1055
|
/** Load unsupported device intel from disk. */
|
|
1121
1056
|
async loadUnsupportedDevices() {
|
|
1122
1057
|
try {
|
|
1123
|
-
if (!fs.existsSync(this.
|
|
1058
|
+
if (!fs.existsSync(this.unsupportedPath)) {
|
|
1124
1059
|
return { devices: [] };
|
|
1125
1060
|
}
|
|
1126
|
-
const data = JSON.parse(await fs.promises.readFile(this.
|
|
1061
|
+
const data = JSON.parse(await fs.promises.readFile(this.unsupportedPath, 'utf-8'));
|
|
1127
1062
|
return { devices: data.devices || [] };
|
|
1128
1063
|
} catch (error) {
|
|
1129
1064
|
this.log.error('Could not load unsupported devices: ' + error);
|
|
@@ -1179,13 +1114,13 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1179
1114
|
const filesToArchive = [...finalLogFiles];
|
|
1180
1115
|
|
|
1181
1116
|
// Include accessories.json for diagnostics
|
|
1182
|
-
if (fs.existsSync(this.
|
|
1183
|
-
filesToArchive.push(path.basename(this.
|
|
1117
|
+
if (fs.existsSync(this.storedAccessoriesPath)) {
|
|
1118
|
+
filesToArchive.push(path.basename(this.storedAccessoriesPath));
|
|
1184
1119
|
}
|
|
1185
1120
|
|
|
1186
1121
|
// Include unsupported.json for diagnostics
|
|
1187
|
-
if (fs.existsSync(this.
|
|
1188
|
-
filesToArchive.push(path.basename(this.
|
|
1122
|
+
if (fs.existsSync(this.unsupportedPath)) {
|
|
1123
|
+
filesToArchive.push(path.basename(this.unsupportedPath));
|
|
1189
1124
|
}
|
|
1190
1125
|
|
|
1191
1126
|
this.pushEvent('diagnosticsProgress', { progress: 40, status: 'Checking archive content' });
|
|
@@ -1203,7 +1138,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1203
1138
|
// Snapshot files to a temp directory before archiving.
|
|
1204
1139
|
// Log files are actively written to — reading them directly with tar
|
|
1205
1140
|
// causes "did not encounter expected EOF" when file size changes mid-read.
|
|
1206
|
-
|
|
1141
|
+
|
|
1207
1142
|
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'eufy-diag-'));
|
|
1208
1143
|
let fileBuffer;
|
|
1209
1144
|
try {
|
|
@@ -1250,17 +1185,17 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1250
1185
|
'configui-server.log',
|
|
1251
1186
|
'configui-lib.log',
|
|
1252
1187
|
]);
|
|
1253
|
-
const
|
|
1188
|
+
const entries = await fs.promises.readdir(this.storagePath, { withFileTypes: true });
|
|
1254
1189
|
let deleted = 0;
|
|
1255
|
-
for (const
|
|
1256
|
-
if (preserved.has(
|
|
1257
|
-
const filePath = path.join(this.storagePath,
|
|
1190
|
+
for (const entry of entries) {
|
|
1191
|
+
if (!entry.isFile() || preserved.has(entry.name)) continue;
|
|
1192
|
+
const filePath = path.join(this.storagePath, entry.name);
|
|
1258
1193
|
try {
|
|
1259
1194
|
await fs.promises.unlink(filePath);
|
|
1260
1195
|
deleted++;
|
|
1261
|
-
this.log.debug(`Deleted: ${
|
|
1196
|
+
this.log.debug(`Deleted: ${entry.name}`);
|
|
1262
1197
|
} catch (error) {
|
|
1263
|
-
this.log.warn(`Failed to delete ${
|
|
1198
|
+
this.log.warn(`Failed to delete ${entry.name}: ${error}`);
|
|
1264
1199
|
}
|
|
1265
1200
|
}
|
|
1266
1201
|
this.log.info(`Cleaned ${deleted} file(s)`);
|
|
@@ -1268,7 +1203,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1268
1203
|
}
|
|
1269
1204
|
|
|
1270
1205
|
async getSystemInfo() {
|
|
1271
|
-
|
|
1206
|
+
|
|
1272
1207
|
let homebridgeVersion = 'unknown';
|
|
1273
1208
|
try {
|
|
1274
1209
|
const hbPkg = require('homebridge/package.json');
|
|
@@ -1279,8 +1214,8 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1279
1214
|
|
|
1280
1215
|
let deviceSummary = [];
|
|
1281
1216
|
try {
|
|
1282
|
-
if (fs.existsSync(this.
|
|
1283
|
-
const storedData = JSON.parse(fs.readFileSync(this.
|
|
1217
|
+
if (fs.existsSync(this.storedAccessoriesPath)) {
|
|
1218
|
+
const storedData = JSON.parse(fs.readFileSync(this.storedAccessoriesPath, 'utf-8'));
|
|
1284
1219
|
if (storedData.stations) {
|
|
1285
1220
|
deviceSummary = storedData.stations.map(s => ({
|
|
1286
1221
|
name: s.displayName,
|
|
@@ -1308,6 +1243,23 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1308
1243
|
};
|
|
1309
1244
|
}
|
|
1310
1245
|
|
|
1246
|
+
/** Resolve and validate a recording filename, returning the absolute path. */
|
|
1247
|
+
_resolveRecordingPath(filename) {
|
|
1248
|
+
if (!filename || typeof filename !== 'string') {
|
|
1249
|
+
throw new Error('Missing or invalid filename parameter.');
|
|
1250
|
+
}
|
|
1251
|
+
const recordingsDir = path.join(this.storagePath, 'recordings');
|
|
1252
|
+
const sanitized = path.basename(filename);
|
|
1253
|
+
if (!sanitized.endsWith('.mp4')) {
|
|
1254
|
+
throw new Error('Invalid filename.');
|
|
1255
|
+
}
|
|
1256
|
+
const resolved = path.resolve(path.join(recordingsDir, sanitized));
|
|
1257
|
+
if (!resolved.startsWith(path.resolve(recordingsDir))) {
|
|
1258
|
+
throw new Error('Invalid filename.');
|
|
1259
|
+
}
|
|
1260
|
+
return resolved;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1311
1263
|
/**
|
|
1312
1264
|
* List all debug recording files available for download.
|
|
1313
1265
|
*/
|
|
@@ -1347,8 +1299,8 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1347
1299
|
.sort((a, b) => b.createdAt - a.createdAt);
|
|
1348
1300
|
return { recordings: files };
|
|
1349
1301
|
} catch (error) {
|
|
1350
|
-
this.log.error('Error listing debug recordings:
|
|
1351
|
-
throw
|
|
1302
|
+
this.log.error('Error listing debug recordings:', error);
|
|
1303
|
+
throw new Error('Failed to list debug recordings. Check the logs for details.');
|
|
1352
1304
|
}
|
|
1353
1305
|
}
|
|
1354
1306
|
|
|
@@ -1359,26 +1311,14 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1359
1311
|
*/
|
|
1360
1312
|
async downloadDebugRecording(options = {}) {
|
|
1361
1313
|
const { filename, offset: rawOffset = 0, chunkSize: rawChunkSize = 256 * 1024 } = options;
|
|
1362
|
-
|
|
1363
|
-
if (!filename || typeof filename !== 'string') {
|
|
1364
|
-
throw new Error('Missing or invalid filename parameter.');
|
|
1365
|
-
}
|
|
1314
|
+
const resolved = this._resolveRecordingPath(filename);
|
|
1366
1315
|
|
|
1367
1316
|
// Coerce and clamp parameters
|
|
1368
1317
|
const numOffset = Number(rawOffset) || 0;
|
|
1318
|
+
if (numOffset < 0) throw new Error('Invalid offset.');
|
|
1369
1319
|
const MAX_CHUNK = 2 * 1024 * 1024; // 2 MB
|
|
1370
1320
|
const numChunkSize = Math.min(Number(rawChunkSize) || 256 * 1024, MAX_CHUNK);
|
|
1371
1321
|
|
|
1372
|
-
const recordingsDir = path.join(this.storagePath, 'recordings');
|
|
1373
|
-
const sanitized = path.basename(filename);
|
|
1374
|
-
const filePath = path.join(recordingsDir, sanitized);
|
|
1375
|
-
const resolved = path.resolve(filePath);
|
|
1376
|
-
|
|
1377
|
-
// Path traversal guard
|
|
1378
|
-
if (!resolved.startsWith(path.resolve(recordingsDir))) {
|
|
1379
|
-
throw new Error('Invalid filename.');
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
1322
|
if (!fs.existsSync(resolved)) {
|
|
1383
1323
|
throw new Error('Recording file not found.');
|
|
1384
1324
|
}
|
|
@@ -1391,10 +1331,13 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1391
1331
|
return { data: null, totalSize, offset: numOffset, chunkSize: 0, done: true };
|
|
1392
1332
|
}
|
|
1393
1333
|
|
|
1394
|
-
const
|
|
1334
|
+
const fh = await fs.promises.open(resolved, 'r');
|
|
1395
1335
|
const buf = Buffer.alloc(readSize);
|
|
1396
|
-
|
|
1397
|
-
|
|
1336
|
+
try {
|
|
1337
|
+
await fh.read(buf, 0, readSize, numOffset);
|
|
1338
|
+
} finally {
|
|
1339
|
+
await fh.close();
|
|
1340
|
+
}
|
|
1398
1341
|
|
|
1399
1342
|
const newOffset = numOffset + readSize;
|
|
1400
1343
|
|
|
@@ -1404,7 +1347,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1404
1347
|
offset: newOffset,
|
|
1405
1348
|
chunkSize: readSize,
|
|
1406
1349
|
done: newOffset >= totalSize,
|
|
1407
|
-
filename:
|
|
1350
|
+
filename: path.basename(resolved),
|
|
1408
1351
|
};
|
|
1409
1352
|
}
|
|
1410
1353
|
|
|
@@ -1412,26 +1355,14 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1412
1355
|
* Delete a specific debug recording file.
|
|
1413
1356
|
*/
|
|
1414
1357
|
async deleteDebugRecording(options = {}) {
|
|
1415
|
-
const
|
|
1416
|
-
|
|
1417
|
-
if (!filename || typeof filename !== 'string') {
|
|
1418
|
-
throw new Error('Missing or invalid filename parameter.');
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
const recordingsDir = path.join(this.storagePath, 'recordings');
|
|
1422
|
-
const sanitized = path.basename(filename);
|
|
1423
|
-
const filePath = path.join(recordingsDir, sanitized);
|
|
1424
|
-
const resolved = path.resolve(filePath);
|
|
1425
|
-
|
|
1426
|
-
if (!resolved.startsWith(path.resolve(recordingsDir))) {
|
|
1427
|
-
throw new Error('Invalid filename.');
|
|
1428
|
-
}
|
|
1358
|
+
const resolved = this._resolveRecordingPath(options.filename);
|
|
1429
1359
|
|
|
1430
1360
|
if (!fs.existsSync(resolved)) {
|
|
1431
1361
|
throw new Error('Recording file not found.');
|
|
1432
1362
|
}
|
|
1433
1363
|
|
|
1434
1364
|
fs.unlinkSync(resolved);
|
|
1365
|
+
const sanitized = path.basename(resolved);
|
|
1435
1366
|
this.log.info(`Deleted debug recording: ${sanitized}`);
|
|
1436
1367
|
return { deleted: true, filename: sanitized };
|
|
1437
1368
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "Homebridge Eufy Security",
|
|
3
3
|
"name": "@homebridge-plugins/homebridge-eufy-security",
|
|
4
|
-
"version": "4.6.2-beta.
|
|
4
|
+
"version": "4.6.2-beta.5",
|
|
5
5
|
"description": "Control Eufy Security from homebridge.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "Apache-2.0",
|