@homebridge-plugins/homebridge-eufy-security 4.4.4-beta.2 → 4.4.4-beta.4
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/public/assets/devices/walllight_s100_large.png +0 -0
- package/homebridge-ui/public/assets/devices/walllight_s120_large.png +0 -0
- package/homebridge-ui/public/utils/device-images.js +2 -2
- package/homebridge-ui/server.js +121 -147
- package/package.json +1 -1
- package/homebridge-ui/public/assets/devices/walllight_s100_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/walllight_s120_large.jpg +0 -0
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const LIB_VERSION = "4.4.4-beta.
|
|
1
|
+
export const LIB_VERSION = "4.4.4-beta.4";
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
|
Binary file
|
|
Binary file
|
|
@@ -69,12 +69,12 @@ const DeviceImages = {
|
|
|
69
69
|
case 140: return 'smartsafe_s10_t7400_large.png';
|
|
70
70
|
case 141: return 'smartsafe_s12_t7401_large.png';
|
|
71
71
|
case 142: case 143: return 'smartsafe_s10_t7400_large.png';
|
|
72
|
-
case 151: return 'walllight_s100_large.
|
|
72
|
+
case 151: return 'walllight_s100_large.png';
|
|
73
73
|
case 157: return 'smarttrack_link_t87B0_large.png';
|
|
74
74
|
case 159: return 'smarttrack_card_t87B2_large.png';
|
|
75
75
|
case 180: return 'smartlock_touch_and_wifi_t8502_large.png';
|
|
76
76
|
case 184: return 'smartlock_touch_and_wifi_t8506_large.png';
|
|
77
|
-
case 10005: return 'walllight_s120_large.
|
|
77
|
+
case 10005: return 'walllight_s120_large.png';
|
|
78
78
|
case 10008: return 'indoorcamC220_large.png';
|
|
79
79
|
case 10009: return 'indoorcamC210_large.png';
|
|
80
80
|
case 10010: return 'indoorcamC220_large.png';
|
package/homebridge-ui/server.js
CHANGED
|
@@ -65,7 +65,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
65
65
|
this.storagePath = this.homebridgeStoragePath + '/eufysecurity';
|
|
66
66
|
this.storedAccessories_file = this.storagePath + '/accessories.json';
|
|
67
67
|
this.unsupported_file = this.storagePath + '/unsupported.json';
|
|
68
|
-
this.diagnosticsZipFilePath = null;
|
|
68
|
+
this.diagnosticsZipFilePath = null;
|
|
69
69
|
this.config.persistentDir = this.storagePath;
|
|
70
70
|
|
|
71
71
|
this.initLogger();
|
|
@@ -74,17 +74,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
74
74
|
this.ready();
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/**
|
|
78
|
-
* Compute a unified power descriptor from a properties object.
|
|
79
|
-
* Works for both devices and stations.
|
|
80
|
-
* @param {object} props - the properties object (from device.getProperties() or station.getProperties())
|
|
81
|
-
* @returns {{ source: string, icon: string, label: string, battery?: number, batteryLow?: boolean }}
|
|
82
|
-
* source: 'battery' | 'solar' | 'plugged' | null
|
|
83
|
-
* icon: icon filename for the UI
|
|
84
|
-
* label: display text for the UI
|
|
85
|
-
* battery: percentage (0-100) if available
|
|
86
|
-
* batteryLow: true/false for simple sensors without percentage
|
|
87
|
-
*/
|
|
77
|
+
/** Build a unified power descriptor (source, icon, label, battery) from a properties object. */
|
|
88
78
|
_computePower(props) {
|
|
89
79
|
const power = { source: null, icon: null, label: null };
|
|
90
80
|
|
|
@@ -140,35 +130,35 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
140
130
|
|
|
141
131
|
initLogger() {
|
|
142
132
|
const logOptions = {
|
|
143
|
-
name: `[UI-${LIB_VERSION}]`,
|
|
144
|
-
prettyLogTemplate: '[{{mm}}/{{dd}}/{{yyyy}}, {{hh}}:{{MM}}:{{ss}}]\t{{name}}\t{{logLevelName}}\t',
|
|
145
|
-
prettyErrorTemplate: '\n{{errorName}} {{errorMessage}}\nerror stack:\n{{errorStack}}',
|
|
146
|
-
prettyErrorStackTemplate: ' • {{fileName}}\t{{method}}\n\t{{fileNameWithLine}}',
|
|
147
|
-
prettyErrorParentNamesSeparator: '',
|
|
148
|
-
prettyErrorLoggerNameDelimiter: '\t',
|
|
149
|
-
stylePrettyLogs: true,
|
|
150
|
-
minLevel: 2,
|
|
151
|
-
prettyLogTimeZone: 'local',
|
|
152
|
-
prettyLogStyles: {
|
|
153
|
-
logLevelName: {
|
|
154
|
-
'*': ['bold', 'black', 'bgWhiteBright', 'dim'],
|
|
155
|
-
SILLY: ['bold', 'white'],
|
|
156
|
-
TRACE: ['bold', 'whiteBright'],
|
|
157
|
-
DEBUG: ['bold', 'green'],
|
|
158
|
-
INFO: ['bold', 'blue'],
|
|
159
|
-
WARN: ['bold', 'yellow'],
|
|
160
|
-
ERROR: ['bold', 'red'],
|
|
161
|
-
FATAL: ['bold', 'redBright'],
|
|
133
|
+
name: `[UI-${LIB_VERSION}]`,
|
|
134
|
+
prettyLogTemplate: '[{{mm}}/{{dd}}/{{yyyy}}, {{hh}}:{{MM}}:{{ss}}]\t{{name}}\t{{logLevelName}}\t',
|
|
135
|
+
prettyErrorTemplate: '\n{{errorName}} {{errorMessage}}\nerror stack:\n{{errorStack}}',
|
|
136
|
+
prettyErrorStackTemplate: ' • {{fileName}}\t{{method}}\n\t{{fileNameWithLine}}',
|
|
137
|
+
prettyErrorParentNamesSeparator: '',
|
|
138
|
+
prettyErrorLoggerNameDelimiter: '\t',
|
|
139
|
+
stylePrettyLogs: true,
|
|
140
|
+
minLevel: 2,
|
|
141
|
+
prettyLogTimeZone: 'local',
|
|
142
|
+
prettyLogStyles: {
|
|
143
|
+
logLevelName: {
|
|
144
|
+
'*': ['bold', 'black', 'bgWhiteBright', 'dim'],
|
|
145
|
+
SILLY: ['bold', 'white'],
|
|
146
|
+
TRACE: ['bold', 'whiteBright'],
|
|
147
|
+
DEBUG: ['bold', 'green'],
|
|
148
|
+
INFO: ['bold', 'blue'],
|
|
149
|
+
WARN: ['bold', 'yellow'],
|
|
150
|
+
ERROR: ['bold', 'red'],
|
|
151
|
+
FATAL: ['bold', 'redBright'],
|
|
162
152
|
},
|
|
163
|
-
dateIsoStr: 'gray',
|
|
164
|
-
filePathWithLine: 'white',
|
|
165
|
-
name: 'green',
|
|
166
|
-
nameWithDelimiterPrefix: ['white', 'bold'],
|
|
167
|
-
nameWithDelimiterSuffix: ['white', 'bold'],
|
|
168
|
-
errorName: ['bold', 'bgRedBright', 'whiteBright'],
|
|
169
|
-
fileName: ['yellow'],
|
|
153
|
+
dateIsoStr: 'gray',
|
|
154
|
+
filePathWithLine: 'white',
|
|
155
|
+
name: 'green',
|
|
156
|
+
nameWithDelimiterPrefix: ['white', 'bold'],
|
|
157
|
+
nameWithDelimiterSuffix: ['white', 'bold'],
|
|
158
|
+
errorName: ['bold', 'bgRedBright', 'whiteBright'],
|
|
159
|
+
fileName: ['yellow'],
|
|
170
160
|
},
|
|
171
|
-
maskValuesOfKeys: [
|
|
161
|
+
maskValuesOfKeys: [
|
|
172
162
|
'username',
|
|
173
163
|
'password',
|
|
174
164
|
'token',
|
|
@@ -248,11 +238,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
248
238
|
return { ok: true };
|
|
249
239
|
}
|
|
250
240
|
|
|
251
|
-
/**
|
|
252
|
-
* Load valid country codes from the shared countries.js file.
|
|
253
|
-
* Parsed lazily and cached for subsequent calls.
|
|
254
|
-
* @returns {Set<string>}
|
|
255
|
-
*/
|
|
241
|
+
/** Lazily load and cache valid ISO 3166-1 alpha-2 country codes from countries.js. */
|
|
256
242
|
_getValidCountryCodes() {
|
|
257
243
|
if (!this._validCountryCodes) {
|
|
258
244
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -310,9 +296,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
310
296
|
}
|
|
311
297
|
|
|
312
298
|
async login(options) {
|
|
313
|
-
//
|
|
314
|
-
// If the plugin is running (accessories.json updated within the last 90s),
|
|
315
|
-
// block login to prevent a competing eufy-security-client instance.
|
|
299
|
+
// Block login if the plugin is already running (accessories updated within 90s)
|
|
316
300
|
if (!this.eufyClient) {
|
|
317
301
|
try {
|
|
318
302
|
if (fs.existsSync(this.storedAccessories_file)) {
|
|
@@ -346,22 +330,14 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
346
330
|
}
|
|
347
331
|
|
|
348
332
|
if (!this.eufyClient && options && options.username && options.password && options.country) {
|
|
349
|
-
|
|
350
|
-
if (this.processingTimeout) {
|
|
351
|
-
clearTimeout(this.processingTimeout);
|
|
352
|
-
this.processingTimeout = null;
|
|
353
|
-
}
|
|
354
|
-
if (this._closeTimeout) {
|
|
355
|
-
clearTimeout(this._closeTimeout);
|
|
356
|
-
this._closeTimeout = null;
|
|
357
|
-
}
|
|
333
|
+
this._clearAllTimers();
|
|
358
334
|
this.stations = [];
|
|
359
335
|
this.pendingStations = [];
|
|
360
336
|
this.pendingDevices = [];
|
|
361
337
|
this._discoveryPhase = 'authenticating';
|
|
362
338
|
this.log.debug('init eufyClient');
|
|
363
339
|
|
|
364
|
-
// Validate country code
|
|
340
|
+
// Validate country code
|
|
365
341
|
const country = typeof options.country === 'string' ? options.country.trim().toUpperCase() : '';
|
|
366
342
|
if (!this._getValidCountryCodes().has(country)) {
|
|
367
343
|
const raw = typeof options.country === 'object' ? JSON.stringify(options.country) : String(options.country);
|
|
@@ -377,8 +353,8 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
377
353
|
this.config.trustedDeviceName = options.deviceName;
|
|
378
354
|
try {
|
|
379
355
|
this.eufyClient = await EufySecurity.initialize(this.config, this.tsLog);
|
|
380
|
-
this.eufyClient?.on('station added', this.
|
|
381
|
-
this.eufyClient?.on('device added', this.
|
|
356
|
+
this.eufyClient?.on('station added', this._onStationDiscovered.bind(this));
|
|
357
|
+
this.eufyClient?.on('device added', this._onDeviceDiscovered.bind(this));
|
|
382
358
|
this.eufyClient?.on('push connect', () => this.log.debug('Push Connected!'));
|
|
383
359
|
this.eufyClient?.on('push close', () => this.log.debug('Push Closed!'));
|
|
384
360
|
this.eufyClient?.on('connect', () => this.log.debug('Connected!'));
|
|
@@ -399,7 +375,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
399
375
|
if (options && options.username && options.password && options.country) {
|
|
400
376
|
this.log.debug('login with credentials');
|
|
401
377
|
try {
|
|
402
|
-
this.
|
|
378
|
+
this._registerOneTimeAuthHandlers();
|
|
403
379
|
this.eufyClient?.connect()
|
|
404
380
|
.then(() => this.log.debug('connected?: ' + this.eufyClient?.isConnected()))
|
|
405
381
|
.catch((error) => this.log.error(error));
|
|
@@ -416,7 +392,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
416
392
|
message: 'Verifying TFA code...',
|
|
417
393
|
});
|
|
418
394
|
try {
|
|
419
|
-
this.
|
|
395
|
+
this._registerOneTimeAuthHandlers();
|
|
420
396
|
this.eufyClient?.connect({ verifyCode: options.verifyCode, force: false })
|
|
421
397
|
.then(() => this.log.debug('TFA connect resolved, connected?: ' + this.eufyClient?.isConnected()))
|
|
422
398
|
.catch((error) => {
|
|
@@ -436,7 +412,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
436
412
|
message: 'Verifying captcha...',
|
|
437
413
|
});
|
|
438
414
|
try {
|
|
439
|
-
this.
|
|
415
|
+
this._registerOneTimeAuthHandlers();
|
|
440
416
|
this.eufyClient?.connect({ captcha: { captchaCode: options.captcha.captchaCode, captchaId: options.captcha.captchaId }, force: false })
|
|
441
417
|
.then(() => this.log.debug('Captcha connect resolved, connected?: ' + this.eufyClient?.isConnected()))
|
|
442
418
|
.catch((error) => {
|
|
@@ -457,11 +433,8 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
457
433
|
return { pending: true };
|
|
458
434
|
}
|
|
459
435
|
|
|
460
|
-
/**
|
|
461
|
-
|
|
462
|
-
* All outcomes are delivered to the UI via push events.
|
|
463
|
-
*/
|
|
464
|
-
_registerAuthHandlers() {
|
|
436
|
+
/** Register once-only auth event handlers (TFA, captcha, connect) on the eufy client. */
|
|
437
|
+
_registerOneTimeAuthHandlers() {
|
|
465
438
|
this.eufyClient?.once('tfa request', () => {
|
|
466
439
|
clearTimeout(this._loginTimeout);
|
|
467
440
|
this.pushEvent('tfaRequest', {});
|
|
@@ -472,32 +445,29 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
472
445
|
});
|
|
473
446
|
this.eufyClient?.once('connect', () => {
|
|
474
447
|
clearTimeout(this._loginTimeout);
|
|
448
|
+
if (this.adminAccountUsed) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
475
451
|
this.pushEvent('authSuccess', {});
|
|
476
452
|
this.pushEvent('discoveryProgress', {
|
|
477
453
|
phase: 'authenticating',
|
|
478
454
|
progress: 15,
|
|
479
455
|
message: 'Authenticated — waiting for devices...',
|
|
480
456
|
});
|
|
481
|
-
this.
|
|
457
|
+
this._startDiscoveryInactivityTimer();
|
|
482
458
|
});
|
|
483
459
|
}
|
|
484
460
|
|
|
485
|
-
/**
|
|
486
|
-
|
|
487
|
-
* If no station or device is discovered within DISCOVERY_INACTIVITY_SEC seconds
|
|
488
|
-
* after authentication, save the account and send an empty result to the UI.
|
|
489
|
-
*/
|
|
490
|
-
_startDiscoveryInactivityTimeout() {
|
|
491
|
-
// If stations or devices were already discovered before connect fired, skip
|
|
461
|
+
/** Start a timer that gives up on device discovery after DISCOVERY_INACTIVITY_SEC seconds. */
|
|
462
|
+
_startDiscoveryInactivityTimer() {
|
|
492
463
|
if (this.pendingStations.length > 0 || this.pendingDevices.length > 0) {
|
|
493
464
|
this.log.debug('Devices already discovered before connect event — skipping inactivity timeout');
|
|
494
465
|
return;
|
|
495
466
|
}
|
|
496
|
-
this.
|
|
467
|
+
this._clearDiscoveryInactivityTimer();
|
|
497
468
|
const totalSec = UiServer.DISCOVERY_INACTIVITY_SEC;
|
|
498
469
|
const start = Date.now();
|
|
499
470
|
|
|
500
|
-
// Tick every second: progress 15 → 95 during the wait, with countdown
|
|
501
471
|
this._discoveryInactivityTickInterval = setInterval(() => {
|
|
502
472
|
const elapsed = Math.floor((Date.now() - start) / 1000);
|
|
503
473
|
const remaining = Math.max(0, totalSec - elapsed);
|
|
@@ -534,10 +504,52 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
534
504
|
}, totalSec * 1000);
|
|
535
505
|
}
|
|
536
506
|
|
|
537
|
-
/**
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
507
|
+
/** Return true if the station's account is the main admin (not a guest admin). */
|
|
508
|
+
_isMainAdminAccount(station) {
|
|
509
|
+
const rawStation = station.getRawStation();
|
|
510
|
+
return rawStation.member.member_type !== UserType.ADMIN;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Abort login: tear down client, cancel timers, notify UI, and reset plugin storage. */
|
|
514
|
+
_abortNonGuestAdminLogin() {
|
|
515
|
+
this.adminAccountUsed = true;
|
|
516
|
+
this._clearAllTimers();
|
|
517
|
+
this.eufyClient?.removeAllListeners();
|
|
518
|
+
this.eufyClient?.close();
|
|
519
|
+
this.pushEvent('AdminAccountUsed', true);
|
|
520
|
+
this.resetPlugin();
|
|
521
|
+
this.log.error(`
|
|
522
|
+
#########################
|
|
523
|
+
######### ERROR #########
|
|
524
|
+
#########################
|
|
525
|
+
You're not using a guest admin account with this plugin! You must use a guest admin account!
|
|
526
|
+
Please look here for more details:
|
|
527
|
+
https://github.com/homebridge-plugins/homebridge-eufy-security/wiki/Create-a-dedicated-admin-account-for-Homebridge-Eufy-Security-Plugin
|
|
528
|
+
#########################
|
|
529
|
+
`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Clear all pending timers (login, processing, close, debounce tick, discovery inactivity). */
|
|
533
|
+
_clearAllTimers() {
|
|
534
|
+
clearTimeout(this._loginTimeout);
|
|
535
|
+
this._loginTimeout = null;
|
|
536
|
+
if (this.processingTimeout) {
|
|
537
|
+
clearTimeout(this.processingTimeout);
|
|
538
|
+
this.processingTimeout = null;
|
|
539
|
+
}
|
|
540
|
+
if (this._closeTimeout) {
|
|
541
|
+
clearTimeout(this._closeTimeout);
|
|
542
|
+
this._closeTimeout = null;
|
|
543
|
+
}
|
|
544
|
+
if (this._debounceTickInterval) {
|
|
545
|
+
clearInterval(this._debounceTickInterval);
|
|
546
|
+
this._debounceTickInterval = null;
|
|
547
|
+
}
|
|
548
|
+
this._clearDiscoveryInactivityTimer();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Clear the post-auth discovery inactivity timer. */
|
|
552
|
+
_clearDiscoveryInactivityTimer() {
|
|
541
553
|
if (this._discoveryInactivityTickInterval) {
|
|
542
554
|
clearInterval(this._discoveryInactivityTickInterval);
|
|
543
555
|
this._discoveryInactivityTickInterval = null;
|
|
@@ -548,11 +560,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
548
560
|
}
|
|
549
561
|
}
|
|
550
562
|
|
|
551
|
-
/**
|
|
552
|
-
* Parse a semver string into [major, minor, patch].
|
|
553
|
-
* @param {string} ver - e.g. '4.4.2-beta.18'
|
|
554
|
-
* @returns {number[]}
|
|
555
|
-
*/
|
|
563
|
+
/** Parse a semver string (e.g. '4.4.2-beta.18') into [major, minor, patch]. */
|
|
556
564
|
_parseSemver(ver) {
|
|
557
565
|
return (ver || '0.0.0').replace(/-.*$/, '').split('.').map(Number);
|
|
558
566
|
}
|
|
@@ -613,27 +621,17 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
613
621
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
614
622
|
}
|
|
615
623
|
|
|
616
|
-
async
|
|
617
|
-
|
|
618
|
-
const rawStation = station.getRawStation();
|
|
619
|
-
if (rawStation.member.member_type !== UserType.ADMIN) {
|
|
620
|
-
this.adminAccountUsed = true;
|
|
621
|
-
this.eufyClient?.close();
|
|
622
|
-
this.pushEvent('AdminAccountUsed', true);
|
|
623
|
-
this.resetPlugin();
|
|
624
|
-
this.log.error(`
|
|
625
|
-
#########################
|
|
626
|
-
######### ERROR #########
|
|
627
|
-
#########################
|
|
628
|
-
You're not using a guest admin account with this plugin! You must use a guest admin account!
|
|
629
|
-
Please look here for more details:
|
|
630
|
-
https://github.com/homebridge-plugins/homebridge-eufy-security/wiki/Create-a-dedicated-admin-account-for-Homebridge-Eufy-Security-Plugin
|
|
631
|
-
#########################
|
|
632
|
-
`);
|
|
624
|
+
async _onStationDiscovered(station) {
|
|
625
|
+
if (this.adminAccountUsed) {
|
|
633
626
|
return;
|
|
634
627
|
}
|
|
635
628
|
|
|
636
|
-
this.
|
|
629
|
+
if (this._isMainAdminAccount(station)) {
|
|
630
|
+
this._abortNonGuestAdminLogin();
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
this._clearDiscoveryInactivityTimer();
|
|
637
635
|
this.pendingStations.push(station);
|
|
638
636
|
this.log.debug(`${station.getName()}: Station queued for processing`);
|
|
639
637
|
this._discoveryPhase = 'queuing';
|
|
@@ -644,10 +642,10 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
644
642
|
devices: this.pendingDevices.length,
|
|
645
643
|
message: `Discovered ${this.pendingStations.length} station(s), ${this.pendingDevices.length} device(s)...`,
|
|
646
644
|
});
|
|
647
|
-
this.
|
|
645
|
+
this._restartDiscoveryDebounce();
|
|
648
646
|
}
|
|
649
647
|
|
|
650
|
-
async
|
|
648
|
+
async _onDeviceDiscovered(device) {
|
|
651
649
|
if (this.adminAccountUsed) {
|
|
652
650
|
this.pushEvent('AdminAccountUsed', true);
|
|
653
651
|
return;
|
|
@@ -659,7 +657,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
659
657
|
return;
|
|
660
658
|
}
|
|
661
659
|
|
|
662
|
-
this.
|
|
660
|
+
this._clearDiscoveryInactivityTimer();
|
|
663
661
|
this.pendingDevices.push(device);
|
|
664
662
|
this.log.debug(`${device.getName()}: Device queued for processing`);
|
|
665
663
|
this._discoveryPhase = 'queuing';
|
|
@@ -670,24 +668,12 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
670
668
|
devices: this.pendingDevices.length,
|
|
671
669
|
message: `Discovered ${this.pendingStations.length} station(s), ${this.pendingDevices.length} device(s)...`,
|
|
672
670
|
});
|
|
673
|
-
this.
|
|
671
|
+
this._restartDiscoveryDebounce();
|
|
674
672
|
}
|
|
675
673
|
|
|
676
|
-
/**
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
* Processing begins once no new events arrive for DISCOVERY_DEBOUNCE_SEC seconds.
|
|
680
|
-
*/
|
|
681
|
-
resetDiscoveryDebounce() {
|
|
682
|
-
if (this.processingTimeout) {
|
|
683
|
-
clearTimeout(this.processingTimeout);
|
|
684
|
-
}
|
|
685
|
-
if (this._closeTimeout) {
|
|
686
|
-
clearTimeout(this._closeTimeout);
|
|
687
|
-
}
|
|
688
|
-
if (this._debounceTickInterval) {
|
|
689
|
-
clearInterval(this._debounceTickInterval);
|
|
690
|
-
}
|
|
674
|
+
/** Restart the debounce timer — processing fires after DISCOVERY_DEBOUNCE_SEC of silence. */
|
|
675
|
+
_restartDiscoveryDebounce() {
|
|
676
|
+
this._clearAllTimers();
|
|
691
677
|
const delaySec = UiServer.DISCOVERY_DEBOUNCE_SEC;
|
|
692
678
|
this.log.debug(
|
|
693
679
|
`Discovery debounce reset — will process in ${delaySec}s if no more devices arrive ` +
|
|
@@ -712,7 +698,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
712
698
|
this.processingTimeout = setTimeout(() => {
|
|
713
699
|
clearInterval(this._debounceTickInterval);
|
|
714
700
|
this._debounceTickInterval = null;
|
|
715
|
-
this.
|
|
701
|
+
this._processPendingAccessories().catch(error => this.log.error('Error processing pending accessories:', error));
|
|
716
702
|
}, delaySec * 1000);
|
|
717
703
|
// Close connection after processing + potential 2-min unsupported intel wait
|
|
718
704
|
const closeAfterSec = delaySec + (UNSUPPORTED_INTEL_WAIT_MS / 1000) + 15;
|
|
@@ -722,7 +708,8 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
722
708
|
}, closeAfterSec * 1000);
|
|
723
709
|
}
|
|
724
710
|
|
|
725
|
-
|
|
711
|
+
/** Process all queued stations and devices after the debounce window closes. */
|
|
712
|
+
async _processPendingAccessories() {
|
|
726
713
|
this.log.debug(`Processing ${this.pendingStations.length} stations and ${this.pendingDevices.length} devices`);
|
|
727
714
|
|
|
728
715
|
this._discoveryPhase = 'processing';
|
|
@@ -741,9 +728,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
741
728
|
);
|
|
742
729
|
}
|
|
743
730
|
|
|
744
|
-
//
|
|
745
|
-
// Hub/base stations (type 0, HB3, etc.) are not in DeviceProperties so
|
|
746
|
-
// Device.isSupported() returns false for them — exclude known station types.
|
|
731
|
+
// Collect unsupported items (exclude hub/base station types that aren't in DeviceProperties)
|
|
747
732
|
const unsupportedItems = [];
|
|
748
733
|
|
|
749
734
|
for (const station of this.pendingStations) {
|
|
@@ -758,7 +743,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
758
743
|
} catch (e) { /* ignore */ }
|
|
759
744
|
}
|
|
760
745
|
|
|
761
|
-
//
|
|
746
|
+
// Wait for raw data on unsupported items (user can skip via /skipIntelWait)
|
|
762
747
|
if (unsupportedItems.length > 0) {
|
|
763
748
|
const names = unsupportedItems.map(i => `${i.getName()} (type ${i.getDeviceType()})`).join(', ');
|
|
764
749
|
this._skipIntelWait = false;
|
|
@@ -772,7 +757,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
772
757
|
|
|
773
758
|
this.log.info(`Unsupported intel: waiting up to ${UNSUPPORTED_INTEL_WAIT_MS / 1000}s for raw data (user can skip)`);
|
|
774
759
|
|
|
775
|
-
//
|
|
760
|
+
// Poll every second, ticking progress 50 → 95
|
|
776
761
|
const pollMs = 1000;
|
|
777
762
|
let waited = 0;
|
|
778
763
|
while (waited < UNSUPPORTED_INTEL_WAIT_MS && !this._skipIntelWait) {
|
|
@@ -962,6 +947,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
962
947
|
this.pushEvent('addAccessory', { stations: this.stations, extendedDiscovery: unsupportedItems.length > 0 });
|
|
963
948
|
}
|
|
964
949
|
|
|
950
|
+
/** Persist discovered stations/devices to accessories.json. */
|
|
965
951
|
storeAccessories() {
|
|
966
952
|
if (!fs.existsSync(this.storagePath)) {
|
|
967
953
|
fs.mkdirSync(this.storagePath, { recursive: true });
|
|
@@ -971,11 +957,8 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
971
957
|
}
|
|
972
958
|
|
|
973
959
|
// ── Sensitive-field redaction ──────────────────────────────────────────────
|
|
974
|
-
// Keys whose string values must be partially masked before persisting to
|
|
975
|
-
// unsupported.json. Values are [keepStart, keepEnd] — the number of
|
|
976
|
-
// characters to leave visible at the beginning and end of the string.
|
|
977
|
-
// An empty / falsy string is left as-is so we can tell the field is blank.
|
|
978
960
|
|
|
961
|
+
/** Keys whose string values are partially masked before persisting to unsupported.json. */
|
|
979
962
|
static SENSITIVE_KEYS = new Map([
|
|
980
963
|
// Serial numbers — keep model prefix (e.g. T8170)
|
|
981
964
|
['station_sn', [5, 0]],
|
|
@@ -1065,10 +1048,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1065
1048
|
|
|
1066
1049
|
// ── Unsupported device storage ──────────────────────────────────────────
|
|
1067
1050
|
|
|
1068
|
-
/**
|
|
1069
|
-
* Collect raw intel for all unsupported devices/stations and write to unsupported.json.
|
|
1070
|
-
* This data is only used by the Plugin UI for triage and diagnostics.
|
|
1071
|
-
*/
|
|
1051
|
+
/** Collect raw intel for unsupported devices/stations and write to unsupported.json. */
|
|
1072
1052
|
storeUnsupportedDevices(pendingStations, pendingDevices) {
|
|
1073
1053
|
const unsupportedEntries = [];
|
|
1074
1054
|
|
|
@@ -1096,9 +1076,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1096
1076
|
this.log.debug(`Persisted ${unsupportedEntries.length} unsupported device(s) to unsupported.json`);
|
|
1097
1077
|
}
|
|
1098
1078
|
|
|
1099
|
-
/**
|
|
1100
|
-
* Build a triage-ready intel object for an unsupported device.
|
|
1101
|
-
*/
|
|
1079
|
+
/** Build a triage-ready intel object for an unsupported device. */
|
|
1102
1080
|
_buildUnsupportedDeviceEntry(device) {
|
|
1103
1081
|
const rawDevice = device.getRawDevice ? device.getRawDevice() : {};
|
|
1104
1082
|
const rawProps = device.getRawProperties ? device.getRawProperties() : {};
|
|
@@ -1122,9 +1100,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1122
1100
|
};
|
|
1123
1101
|
}
|
|
1124
1102
|
|
|
1125
|
-
/**
|
|
1126
|
-
* Build a triage-ready intel object for an unsupported standalone station.
|
|
1127
|
-
*/
|
|
1103
|
+
/** Build a triage-ready intel object for an unsupported standalone station. */
|
|
1128
1104
|
_buildUnsupportedStationEntry(station) {
|
|
1129
1105
|
const rawStation = station.getRawStation ? station.getRawStation() : {};
|
|
1130
1106
|
const rawProps = station.getRawProperties ? station.getRawProperties() : {};
|
|
@@ -1148,9 +1124,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
1148
1124
|
};
|
|
1149
1125
|
}
|
|
1150
1126
|
|
|
1151
|
-
/**
|
|
1152
|
-
* Load unsupported device intel from disk.
|
|
1153
|
-
*/
|
|
1127
|
+
/** Load unsupported device intel from disk. */
|
|
1154
1128
|
async loadUnsupportedDevices() {
|
|
1155
1129
|
try {
|
|
1156
1130
|
if (!fs.existsSync(this.unsupported_file)) {
|
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.4.4-beta.
|
|
4
|
+
"version": "4.4.4-beta.4",
|
|
5
5
|
"description": "Control Eufy Security from homebridge.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "Apache-2.0",
|
|
Binary file
|
|
Binary file
|