homebridge-eufy-security 4.4.2-beta.2 → 4.4.2-beta.20
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/CHANGELOG.md +1 -1
- package/FUNDING.yml +1 -0
- package/README.md +14 -17
- package/dist/accessories/AutoSyncStationAccessory.js +26 -30
- package/dist/accessories/AutoSyncStationAccessory.js.map +1 -1
- package/dist/accessories/BaseAccessory.js +33 -46
- package/dist/accessories/BaseAccessory.js.map +1 -1
- package/dist/accessories/CameraAccessory.js +92 -167
- package/dist/accessories/CameraAccessory.js.map +1 -1
- package/dist/accessories/Device.js +11 -15
- package/dist/accessories/Device.js.map +1 -1
- package/dist/accessories/EntrySensorAccessory.js +7 -12
- package/dist/accessories/EntrySensorAccessory.js.map +1 -1
- package/dist/accessories/LockAccessory.js +20 -24
- package/dist/accessories/LockAccessory.js.map +1 -1
- package/dist/accessories/MotionSensorAccessory.js +7 -12
- package/dist/accessories/MotionSensorAccessory.js.map +1 -1
- package/dist/accessories/StationAccessory.js +40 -45
- package/dist/accessories/StationAccessory.js.map +1 -1
- package/dist/config.js +1 -4
- package/dist/config.js.map +1 -1
- package/dist/controller/LocalLivestreamManager.js +85 -89
- package/dist/controller/LocalLivestreamManager.js.map +1 -1
- package/dist/controller/recordingDelegate.js +116 -108
- package/dist/controller/recordingDelegate.js.map +1 -1
- package/dist/controller/snapshotDelegate.js +379 -0
- package/dist/controller/snapshotDelegate.js.map +1 -0
- package/dist/controller/streamingDelegate.js +202 -207
- package/dist/controller/streamingDelegate.js.map +1 -1
- package/dist/index.js +9 -7
- package/dist/index.js.map +1 -1
- package/dist/interfaces.js +1 -2
- package/dist/interfaces.js.map +1 -1
- package/dist/platform.js +145 -165
- package/dist/platform.js.map +1 -1
- package/dist/settings.js +26 -19
- package/dist/settings.js.map +1 -1
- package/dist/utils/Talkback.js +9 -13
- package/dist/utils/Talkback.js.map +1 -1
- package/dist/utils/configTypes.js +12 -7
- package/dist/utils/configTypes.js.map +1 -1
- package/dist/utils/ffmpeg.js +44 -52
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/interfaces.js +1 -5
- package/dist/utils/interfaces.js.map +1 -1
- package/dist/utils/utils.js +32 -134
- package/dist/utils/utils.js.map +1 -1
- package/dist/version.js +1 -4
- package/dist/version.js.map +1 -1
- package/eslint.config.mjs +25 -47
- package/homebridge-eufy-security.png +0 -0
- package/homebridge-ui/public/app.js +221 -0
- package/homebridge-ui/public/assets/devices/eufycam3pro_large.png +0 -0
- package/homebridge-ui/public/assets/devices/homebase3_large.png +0 -0
- package/homebridge-ui/public/assets/devices/homebasemini_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/indoorcamC210_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcamC220_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcamE30_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocamc35_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocame30_large.png +0 -0
- package/homebridge-ui/public/components/device-card.js +141 -0
- package/homebridge-ui/public/components/guard-modes.js +88 -0
- package/homebridge-ui/public/components/number-input.js +121 -0
- package/homebridge-ui/public/components/select.js +73 -0
- package/homebridge-ui/public/components/toggle.js +68 -0
- package/homebridge-ui/public/index.html +24 -14
- package/homebridge-ui/public/services/api.js +129 -0
- package/homebridge-ui/public/services/config.js +144 -0
- package/homebridge-ui/public/style.css +705 -0
- package/homebridge-ui/public/utils/countries.js +73 -0
- package/homebridge-ui/public/utils/device-images.js +69 -0
- package/homebridge-ui/public/utils/helpers.js +47 -0
- package/homebridge-ui/public/views/dashboard.js +211 -0
- package/homebridge-ui/public/views/device-detail.js +631 -0
- package/homebridge-ui/public/views/login.js +625 -0
- package/homebridge-ui/public/views/settings.js +389 -0
- package/homebridge-ui/public/views/unsupported-detail.js +216 -0
- package/homebridge-ui/server.js +567 -450
- package/package.json +19 -59
- package/dist/accessories/AutoSyncStationAccessory.d.ts +0 -42
- package/dist/accessories/AutoSyncStationAccessory.d.ts.map +0 -1
- package/dist/accessories/BaseAccessory.d.ts +0 -56
- package/dist/accessories/BaseAccessory.d.ts.map +0 -1
- package/dist/accessories/CameraAccessory.d.ts +0 -84
- package/dist/accessories/CameraAccessory.d.ts.map +0 -1
- package/dist/accessories/Device.d.ts +0 -19
- package/dist/accessories/Device.d.ts.map +0 -1
- package/dist/accessories/EntrySensorAccessory.d.ts +0 -25
- package/dist/accessories/EntrySensorAccessory.d.ts.map +0 -1
- package/dist/accessories/LockAccessory.d.ts +0 -45
- package/dist/accessories/LockAccessory.d.ts.map +0 -1
- package/dist/accessories/MotionSensorAccessory.d.ts +0 -25
- package/dist/accessories/MotionSensorAccessory.d.ts.map +0 -1
- package/dist/accessories/StationAccessory.d.ts +0 -86
- package/dist/accessories/StationAccessory.d.ts.map +0 -1
- package/dist/config.d.ts +0 -31
- package/dist/config.d.ts.map +0 -1
- package/dist/controller/LocalLivestreamManager.d.ts +0 -31
- package/dist/controller/LocalLivestreamManager.d.ts.map +0 -1
- package/dist/controller/SnapshotManager.d.ts +0 -64
- package/dist/controller/SnapshotManager.d.ts.map +0 -1
- package/dist/controller/SnapshotManager.js +0 -434
- package/dist/controller/SnapshotManager.js.map +0 -1
- package/dist/controller/recordingDelegate.d.ts +0 -27
- package/dist/controller/recordingDelegate.d.ts.map +0 -1
- package/dist/controller/streamingDelegate.d.ts +0 -52
- package/dist/controller/streamingDelegate.d.ts.map +0 -1
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/interfaces.d.ts +0 -15
- package/dist/interfaces.d.ts.map +0 -1
- package/dist/platform.d.ts +0 -94
- package/dist/platform.d.ts.map +0 -1
- package/dist/settings.d.ts +0 -23
- package/dist/settings.d.ts.map +0 -1
- package/dist/utils/Talkback.d.ts +0 -21
- package/dist/utils/Talkback.d.ts.map +0 -1
- package/dist/utils/configTypes.d.ts +0 -68
- package/dist/utils/configTypes.d.ts.map +0 -1
- package/dist/utils/ffmpeg.d.ts +0 -111
- package/dist/utils/ffmpeg.d.ts.map +0 -1
- package/dist/utils/interfaces.d.ts +0 -8
- package/dist/utils/interfaces.d.ts.map +0 -1
- package/dist/utils/utils.d.ts +0 -35
- package/dist/utils/utils.d.ts.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +0 -1
- package/homebridge-ui/configui/app/util/types.d.ts +0 -65
- package/homebridge-ui/configui/app/util/types.d.ts.map +0 -1
- package/homebridge-ui/configui/app/util/types.js +0 -18
- package/homebridge-ui/configui/app/util/types.js.map +0 -1
- package/homebridge-ui/public/3rdpartylicenses.txt +0 -561
- package/homebridge-ui/public/assets/devices/homebase2_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/homebase3_large.jpg +0 -0
- package/homebridge-ui/public/assets/images/homebridge-eufy-security.png +0 -0
- package/homebridge-ui/public/favicon.ico +0 -0
- package/homebridge-ui/public/main.f6423287e0411c87.js +0 -1
- package/homebridge-ui/public/polyfills.efa2f09ba3e49167.js +0 -1
- package/homebridge-ui/public/runtime.ccde331d62c423b3.js +0 -1
- package/homebridge-ui/public/styles.13e635bf7a488639.css +0 -5
- package/homebridge-ui/server.d.ts +0 -2
- package/homebridge-ui/server.d.ts.map +0 -1
- package/homebridge-ui/server.js.map +0 -1
- package/homebridge-ui/version.d.ts +0 -2
- package/homebridge-ui/version.d.ts.map +0 -1
- package/homebridge-ui/version.js +0 -5
- package/homebridge-ui/version.js.map +0 -1
package/homebridge-ui/server.js
CHANGED
|
@@ -1,483 +1,600 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
import { EufySecurity, libVersion, Device, PropertyName, CommandName, DeviceType, UserType } from 'eufy-security-client';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { Logger as TsLogger } from 'tslog';
|
|
4
|
+
import { createStream } from 'rotating-file-stream';
|
|
5
|
+
import { Zip } from 'zip-lib';
|
|
6
|
+
import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const { version: LIB_VERSION } = require('../package.json');
|
|
12
|
+
|
|
13
|
+
const LoginFailReason = Object.freeze({
|
|
14
|
+
UNKNOWN: 0,
|
|
15
|
+
CAPTCHA: 1,
|
|
16
|
+
TFA: 2,
|
|
17
|
+
TIMEOUT: 3,
|
|
17
18
|
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
homebridgeStoragePath;
|
|
53
|
-
storagePath;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
ready;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
minLevel: 2,
|
|
101
|
-
prettyLogTimeZone: 'local',
|
|
102
|
-
prettyLogStyles: {
|
|
103
|
-
logLevelName: {
|
|
104
|
-
'*': ['bold', 'black', 'bgWhiteBright', 'dim'],
|
|
105
|
-
SILLY: ['bold', 'white'],
|
|
106
|
-
TRACE: ['bold', 'whiteBright'],
|
|
107
|
-
DEBUG: ['bold', 'green'],
|
|
108
|
-
INFO: ['bold', 'blue'],
|
|
109
|
-
WARN: ['bold', 'yellow'],
|
|
110
|
-
ERROR: ['bold', 'red'],
|
|
111
|
-
FATAL: ['bold', 'redBright'],
|
|
112
|
-
},
|
|
113
|
-
dateIsoStr: 'gray',
|
|
114
|
-
filePathWithLine: 'white',
|
|
115
|
-
name: 'green',
|
|
116
|
-
nameWithDelimiterPrefix: ['white', 'bold'],
|
|
117
|
-
nameWithDelimiterSuffix: ['white', 'bold'],
|
|
118
|
-
errorName: ['bold', 'bgRedBright', 'whiteBright'],
|
|
119
|
-
fileName: ['yellow'],
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
this.tsLog = new tslog_1.Logger({ type: 'hidden', minLevel: 2 });
|
|
123
|
-
}
|
|
124
|
-
initTransportStreams() {
|
|
125
|
-
if (!fs.existsSync(this.storagePath)) {
|
|
126
|
-
fs.mkdirSync(this.storagePath);
|
|
127
|
-
}
|
|
128
|
-
const pluginLogStream = (0, rotating_file_stream_1.createStream)('configui-server.log', { path: this.storagePath, interval: '1d', rotate: 3, maxSize: '200M' });
|
|
129
|
-
const pluginLogLibStream = (0, rotating_file_stream_1.createStream)('configui-lib.log', { path: this.storagePath, interval: '1d', rotate: 3, maxSize: '200M' });
|
|
130
|
-
this.log.attachTransport((logObj) => pluginLogStream.write(JSON.stringify(logObj) + '\n'));
|
|
131
|
-
this.tsLog.attachTransport((logObj) => pluginLogLibStream.write(JSON.stringify(logObj) + '\n'));
|
|
132
|
-
this.log.debug('Using bropats eufy-security-client library in version ' + eufy_security_client_1.libVersion);
|
|
19
|
+
|
|
20
|
+
class UiServer extends HomebridgePluginUiServer {
|
|
21
|
+
|
|
22
|
+
stations = [];
|
|
23
|
+
eufyClient = null;
|
|
24
|
+
log;
|
|
25
|
+
tsLog;
|
|
26
|
+
storagePath;
|
|
27
|
+
storedAccessories_file;
|
|
28
|
+
logZipFilePath;
|
|
29
|
+
|
|
30
|
+
adminAccountUsed = false;
|
|
31
|
+
|
|
32
|
+
// Batch processing for stations and devices
|
|
33
|
+
pendingStations = [];
|
|
34
|
+
pendingDevices = [];
|
|
35
|
+
processingTimeout;
|
|
36
|
+
|
|
37
|
+
config = {
|
|
38
|
+
username: '',
|
|
39
|
+
password: '',
|
|
40
|
+
language: 'en',
|
|
41
|
+
country: 'US',
|
|
42
|
+
trustedDeviceName: 'My Phone',
|
|
43
|
+
persistentDir: '',
|
|
44
|
+
p2pConnectionSetup: 0,
|
|
45
|
+
pollingIntervalMinutes: 99,
|
|
46
|
+
eventDurationSeconds: 10,
|
|
47
|
+
acceptInvitations: true,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
constructor() {
|
|
51
|
+
super();
|
|
52
|
+
|
|
53
|
+
this.storagePath = this.homebridgeStoragePath + '/eufysecurity';
|
|
54
|
+
this.storedAccessories_file = this.storagePath + '/accessories.json';
|
|
55
|
+
this.logZipFilePath = this.storagePath + '/logs.zip';
|
|
56
|
+
this.config.persistentDir = this.storagePath;
|
|
57
|
+
|
|
58
|
+
this.initLogger();
|
|
59
|
+
this.initTransportStreams();
|
|
60
|
+
this.initEventListeners();
|
|
61
|
+
this.ready();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
initLogger() {
|
|
65
|
+
this.log = new TsLogger({
|
|
66
|
+
name: `[${LIB_VERSION}]`,
|
|
67
|
+
prettyLogTemplate: '{{name}}\t{{logLevelName}}\t',
|
|
68
|
+
prettyErrorTemplate: '\n{{errorName}} {{errorMessage}}\nerror stack:\n{{errorStack}}',
|
|
69
|
+
prettyErrorStackTemplate: ' • {{fileName}}\t{{method}}\n\t{{fileNameWithLine}}',
|
|
70
|
+
prettyErrorParentNamesSeparator: ':',
|
|
71
|
+
prettyErrorLoggerNameDelimiter: '\t',
|
|
72
|
+
stylePrettyLogs: true,
|
|
73
|
+
minLevel: 2,
|
|
74
|
+
prettyLogTimeZone: 'local',
|
|
75
|
+
prettyLogStyles: {
|
|
76
|
+
logLevelName: {
|
|
77
|
+
'*': ['bold', 'black', 'bgWhiteBright', 'dim'],
|
|
78
|
+
SILLY: ['bold', 'white'],
|
|
79
|
+
TRACE: ['bold', 'whiteBright'],
|
|
80
|
+
DEBUG: ['bold', 'green'],
|
|
81
|
+
INFO: ['bold', 'blue'],
|
|
82
|
+
WARN: ['bold', 'yellow'],
|
|
83
|
+
ERROR: ['bold', 'red'],
|
|
84
|
+
FATAL: ['bold', 'redBright'],
|
|
85
|
+
},
|
|
86
|
+
dateIsoStr: 'gray',
|
|
87
|
+
filePathWithLine: 'white',
|
|
88
|
+
name: 'green',
|
|
89
|
+
nameWithDelimiterPrefix: ['white', 'bold'],
|
|
90
|
+
nameWithDelimiterSuffix: ['white', 'bold'],
|
|
91
|
+
errorName: ['bold', 'bgRedBright', 'whiteBright'],
|
|
92
|
+
fileName: ['yellow'],
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
this.tsLog = new TsLogger({ type: 'hidden', minLevel: 2 });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
initTransportStreams() {
|
|
99
|
+
if (!fs.existsSync(this.storagePath)) {
|
|
100
|
+
fs.mkdirSync(this.storagePath);
|
|
133
101
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
102
|
+
const pluginLogStream = createStream('configui-server.log', { path: this.storagePath, interval: '1d', rotate: 3, maxSize: '200M' });
|
|
103
|
+
const pluginLogLibStream = createStream('configui-lib.log', { path: this.storagePath, interval: '1d', rotate: 3, maxSize: '200M' });
|
|
104
|
+
this.log.attachTransport((logObj) => pluginLogStream.write(JSON.stringify(logObj) + '\n'));
|
|
105
|
+
this.tsLog.attachTransport((logObj) => pluginLogLibStream.write(JSON.stringify(logObj) + '\n'));
|
|
106
|
+
this.log.debug('Using bropats eufy-security-client library in version ' + libVersion);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
initEventListeners() {
|
|
110
|
+
this.onRequest('/login', this.login.bind(this));
|
|
111
|
+
this.onRequest('/checkCache', this.checkCache.bind(this));
|
|
112
|
+
this.onRequest('/storedAccessories', this.loadStoredAccessories.bind(this));
|
|
113
|
+
this.onRequest('/reset', this.resetPlugin.bind(this));
|
|
114
|
+
this.onRequest('/downloadLogs', this.downloadLogs.bind(this));
|
|
115
|
+
this.onRequest('/systemInfo', this.getSystemInfo.bind(this));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async deleteFileIfExists(filePath) {
|
|
119
|
+
try {
|
|
120
|
+
await fs.promises.unlink(filePath);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error.code !== 'ENOENT') {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
139
125
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async resetPersistentData() {
|
|
129
|
+
return this.deleteFileIfExists(this.storagePath + '/persistent.json');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async resetAccessoryData() {
|
|
133
|
+
return this.deleteFileIfExists(this.storedAccessories_file);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async checkCache() {
|
|
137
|
+
const persistentFile = this.storagePath + '/persistent.json';
|
|
138
|
+
try {
|
|
139
|
+
if (fs.existsSync(persistentFile)) {
|
|
140
|
+
const data = JSON.parse(await fs.promises.readFile(persistentFile, 'utf-8'));
|
|
141
|
+
// Basic validity check: ensure it has some expected content
|
|
142
|
+
if (data && Object.keys(data).length > 0) {
|
|
143
|
+
this.log.debug('Persistent cache file found and valid.');
|
|
144
|
+
return { valid: true };
|
|
155
145
|
}
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
this.log.warn('Error checking persistent cache: ' + error);
|
|
156
149
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
150
|
+
return { valid: false };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async login(options) {
|
|
154
|
+
try {
|
|
155
|
+
if (options && options.username && options.password && !options.reconnect) {
|
|
156
|
+
this.log.info('deleting persistent.json and accessories due to new login');
|
|
157
|
+
await this.resetAccessoryData();
|
|
158
|
+
await this.resetPersistentData();
|
|
159
|
+
} else if (options && options.reconnect) {
|
|
160
|
+
this.log.info('Reconnecting using persistent cache (skipping data reset)');
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
this.log.error('Could not delete persistent.json due to error: ' + error);
|
|
164
164
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
165
|
+
|
|
166
|
+
if (!this.eufyClient && options && options.username && options.password && options.country) {
|
|
167
|
+
// Clear any pending timeouts from a previous login attempt
|
|
168
|
+
if (this.processingTimeout) {
|
|
169
|
+
clearTimeout(this.processingTimeout);
|
|
170
|
+
this.processingTimeout = null;
|
|
171
|
+
}
|
|
172
|
+
if (this._closeTimeout) {
|
|
173
|
+
clearTimeout(this._closeTimeout);
|
|
174
|
+
this._closeTimeout = null;
|
|
175
|
+
}
|
|
176
|
+
this.stations = [];
|
|
177
|
+
this.pendingStations = [];
|
|
178
|
+
this.pendingDevices = [];
|
|
179
|
+
this.log.debug('init eufyClient');
|
|
180
|
+
this.config.username = options.username;
|
|
181
|
+
this.config.password = options.password;
|
|
182
|
+
this.config.country = options.country;
|
|
183
|
+
this.config.trustedDeviceName = options.deviceName;
|
|
184
|
+
try {
|
|
185
|
+
this.eufyClient = await EufySecurity.initialize(this.config, this.tsLog);
|
|
186
|
+
this.eufyClient?.on('station added', this.addStation.bind(this));
|
|
187
|
+
this.eufyClient?.on('device added', this.addDevice.bind(this));
|
|
188
|
+
// Wait for 45 seconds to gather all stations and devices, then process them
|
|
189
|
+
this.processingTimeout = setTimeout(() => {
|
|
190
|
+
this.processPendingAccessories().catch(error => this.log.error('Error processing pending accessories:', error));
|
|
191
|
+
}, 45 * 1000);
|
|
192
|
+
// Close connection after 50 seconds to allow processing time
|
|
193
|
+
this._closeTimeout = setTimeout(() => {
|
|
194
|
+
this.eufyClient?.removeAllListeners();
|
|
195
|
+
this.eufyClient?.close();
|
|
196
|
+
}, 50 * 1000);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
this.log.error(error);
|
|
199
|
+
}
|
|
172
200
|
}
|
|
173
|
-
|
|
201
|
+
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
resolve({ success: false, failReason: LoginFailReason.TIMEOUT });
|
|
205
|
+
}, 25 * 1000);
|
|
206
|
+
|
|
207
|
+
if (options && options.username && options.password && options.country) {
|
|
208
|
+
this.log.debug('login with credentials');
|
|
174
209
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
this.log.error('Could not delete persistent.json due to error: ' + error);
|
|
210
|
+
this.loginHandlers(resolve);
|
|
211
|
+
this.eufyClient?.connect()
|
|
212
|
+
.then(() => this.log.debug('connected?: ' + this.eufyClient?.isConnected()))
|
|
213
|
+
.catch((error) => this.log.error(error));
|
|
214
|
+
} catch (error) {
|
|
215
|
+
this.log.error(error);
|
|
216
|
+
resolve({ success: false, failReason: LoginFailReason.UNKNOWN, data: { error } });
|
|
183
217
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
this.config.trustedDeviceName = options.deviceName;
|
|
191
|
-
try {
|
|
192
|
-
this.eufyClient = await eufy_security_client_1.EufySecurity.initialize(this.config, this.tsLog);
|
|
193
|
-
this.eufyClient?.on('station added', await this.addStation.bind(this));
|
|
194
|
-
this.eufyClient?.on('device added', await this.addDevice.bind(this));
|
|
195
|
-
// Close connection after 40 seconds enough time to get all devices
|
|
196
|
-
setTimeout(() => {
|
|
197
|
-
this.eufyClient?.removeAllListeners();
|
|
198
|
-
this.eufyClient?.close();
|
|
199
|
-
}, 40 * 1000);
|
|
200
|
-
}
|
|
201
|
-
catch (error) {
|
|
202
|
-
this.log.error(error);
|
|
203
|
-
}
|
|
218
|
+
} else if (options && options.verifyCode) {
|
|
219
|
+
try {
|
|
220
|
+
this.loginHandlers(resolve);
|
|
221
|
+
this.eufyClient?.connect({ verifyCode: options.verifyCode, force: false });
|
|
222
|
+
} catch (error) {
|
|
223
|
+
resolve({ success: false, failReason: LoginFailReason.UNKNOWN, data: { error } });
|
|
204
224
|
}
|
|
205
|
-
|
|
206
|
-
setTimeout(() => {
|
|
207
|
-
resolve({ success: false, failReason: types_js_1.LoginFailReason.TIMEOUT });
|
|
208
|
-
}, 25 * 1000);
|
|
209
|
-
if (options && options.username && options.password && options.country) {
|
|
210
|
-
this.log.debug('login with credentials');
|
|
211
|
-
try {
|
|
212
|
-
this.loginHandlers(resolve);
|
|
213
|
-
this.eufyClient?.connect()
|
|
214
|
-
.then(() => this.log.debug('connected?: ' + this.eufyClient?.isConnected()))
|
|
215
|
-
.catch((error) => this.log.error(error));
|
|
216
|
-
}
|
|
217
|
-
catch (error) {
|
|
218
|
-
this.log.error(error);
|
|
219
|
-
resolve({ success: false, failReason: types_js_1.LoginFailReason.UNKNOWN, data: { error: error } });
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
else if (options && options.verifyCode) {
|
|
223
|
-
try {
|
|
224
|
-
this.loginHandlers(resolve);
|
|
225
|
-
this.eufyClient?.connect({ verifyCode: options.verifyCode, force: false });
|
|
226
|
-
}
|
|
227
|
-
catch (error) {
|
|
228
|
-
resolve({ success: false, failReason: types_js_1.LoginFailReason.UNKNOWN, data: { error: error } });
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
else if (options && options.captcha) {
|
|
232
|
-
try {
|
|
233
|
-
this.loginHandlers(resolve);
|
|
234
|
-
this.eufyClient?.connect({ captcha: { captchaCode: options.captcha.captchaCode, captchaId: options.captcha.captchaId }, force: false });
|
|
235
|
-
}
|
|
236
|
-
catch (error) {
|
|
237
|
-
resolve({ success: false, failReason: types_js_1.LoginFailReason.UNKNOWN, data: { error: error } });
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
reject('unsupported login method');
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
loginHandlers(resolveCallback) {
|
|
246
|
-
this.eufyClient?.once('tfa request', () => resolveCallback({ success: false, failReason: types_js_1.LoginFailReason.TFA }));
|
|
247
|
-
this.eufyClient?.once('captcha request', (id, captcha) => resolveCallback({ success: false, failReason: types_js_1.LoginFailReason.CAPTCHA, data: { id: id, captcha: captcha } }));
|
|
248
|
-
this.eufyClient?.once('connect', () => resolveCallback({ success: true }));
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Asynchronously loads stored accessories from a file.
|
|
252
|
-
* @returns A promise resolving to an array of accessories.
|
|
253
|
-
*/
|
|
254
|
-
async loadStoredAccessories() {
|
|
225
|
+
} else if (options && options.captcha) {
|
|
255
226
|
try {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return [];
|
|
261
|
-
}
|
|
262
|
-
// Read the content of the stored accessories file asynchronously
|
|
263
|
-
const storedData = await fs.promises.readFile(this.storedAccessories_file, { encoding: 'utf-8' });
|
|
264
|
-
// Parse the JSON data obtained from the file
|
|
265
|
-
const { version: storedVersion, stations: storedAccessories } = JSON.parse(storedData);
|
|
266
|
-
// Compare the stored version with the current version
|
|
267
|
-
if (storedVersion !== version_js_1.LIB_VERSION) {
|
|
268
|
-
// If the versions do not match, log a warning and push an event
|
|
269
|
-
this.pushEvent('versionUnmatched', { currentVersion: version_js_1.LIB_VERSION, storedVersion: storedVersion });
|
|
270
|
-
this.log.warn(`Stored version (${storedVersion}) does not match current version (${version_js_1.LIB_VERSION})`);
|
|
271
|
-
}
|
|
272
|
-
// Return the parsed accessories
|
|
273
|
-
return storedAccessories;
|
|
227
|
+
this.loginHandlers(resolve);
|
|
228
|
+
this.eufyClient?.connect({ captcha: { captchaCode: options.captcha.captchaCode, captchaId: options.captcha.captchaId }, force: false });
|
|
229
|
+
} catch (error) {
|
|
230
|
+
resolve({ success: false, failReason: LoginFailReason.UNKNOWN, data: { error } });
|
|
274
231
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
232
|
+
} else {
|
|
233
|
+
reject('unsupported login method');
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
loginHandlers(resolveCallback) {
|
|
239
|
+
this.eufyClient?.once('tfa request', () => resolveCallback({ success: false, failReason: LoginFailReason.TFA }));
|
|
240
|
+
this.eufyClient?.once('captcha request', (id, captcha) => resolveCallback({ success: false, failReason: LoginFailReason.CAPTCHA, data: { id, captcha } }));
|
|
241
|
+
this.eufyClient?.once('connect', () => resolveCallback({ success: true }));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Parse a semver string into [major, minor, patch].
|
|
246
|
+
* @param {string} ver - e.g. '4.4.2-beta.18'
|
|
247
|
+
* @returns {number[]}
|
|
248
|
+
*/
|
|
249
|
+
_parseSemver(ver) {
|
|
250
|
+
return (ver || '0.0.0').replace(/-.*$/, '').split('.').map(Number);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async loadStoredAccessories() {
|
|
254
|
+
try {
|
|
255
|
+
if (!fs.existsSync(this.storedAccessories_file)) {
|
|
256
|
+
this.log.debug('Stored accessories file does not exist.');
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const storedData = await fs.promises.readFile(this.storedAccessories_file, { encoding: 'utf-8' });
|
|
261
|
+
const { version: storedVersion, storedAt, stations: storedAccessories } = JSON.parse(storedData);
|
|
262
|
+
|
|
263
|
+
// --- Cache age check (30 days) ---
|
|
264
|
+
if (storedAt) {
|
|
265
|
+
const ageMs = Date.now() - new Date(storedAt).getTime();
|
|
266
|
+
const ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
|
|
267
|
+
if (ageDays >= 30) {
|
|
268
|
+
this.pushEvent('cacheWarning', { reason: 'stale', ageDays });
|
|
269
|
+
this.log.warn(`Stored accessories are ${ageDays} days old. User should re-login to refresh.`);
|
|
279
270
|
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// --- Version branch check ---
|
|
274
|
+
if (storedVersion && storedVersion !== LIB_VERSION) {
|
|
275
|
+
const [curMajor, curMinor] = this._parseSemver(LIB_VERSION);
|
|
276
|
+
const [stoMajor, stoMinor] = this._parseSemver(storedVersion);
|
|
277
|
+
|
|
278
|
+
if (curMajor !== stoMajor || curMinor !== stoMinor) {
|
|
279
|
+
// Different minor (or major) branch → force re-login
|
|
280
|
+
this.pushEvent('cacheWarning', {
|
|
281
|
+
reason: 'versionForce',
|
|
282
|
+
currentVersion: LIB_VERSION,
|
|
283
|
+
storedVersion,
|
|
284
|
+
});
|
|
285
|
+
this.log.warn(`Stored version (${storedVersion}) is on a different branch than current (${LIB_VERSION}). Forcing re-login.`);
|
|
286
|
+
return { stations: [], storedAt: null }; // Return empty to force login flow
|
|
287
|
+
} else {
|
|
288
|
+
// Same minor branch, different patch → soft warning
|
|
289
|
+
this.pushEvent('cacheWarning', {
|
|
290
|
+
reason: 'versionWarn',
|
|
291
|
+
currentVersion: LIB_VERSION,
|
|
292
|
+
storedVersion,
|
|
293
|
+
});
|
|
294
|
+
this.log.warn(`Stored version (${storedVersion}) differs from current (${LIB_VERSION}) but same branch. Consider re-login.`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return { stations: storedAccessories, storedAt: storedAt || null };
|
|
299
|
+
} catch (error) {
|
|
300
|
+
this.log.error('Could not get stored accessories. Most likely no stored accessories yet: ' + error);
|
|
301
|
+
return { stations: [], storedAt: null };
|
|
280
302
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async delay(ms) {
|
|
306
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async addStation(station) {
|
|
310
|
+
// Check if creds are guest admin
|
|
311
|
+
const rawStation = station.getRawStation();
|
|
312
|
+
if (rawStation.member.member_type !== UserType.ADMIN) {
|
|
313
|
+
this.adminAccountUsed = true;
|
|
314
|
+
this.eufyClient?.close();
|
|
315
|
+
this.pushEvent('AdminAccountUsed', true);
|
|
316
|
+
this.resetPlugin();
|
|
317
|
+
this.log.error(`
|
|
293
318
|
#########################
|
|
294
319
|
######### ERROR #########
|
|
295
320
|
#########################
|
|
296
321
|
You're not using a guest admin account with this plugin! You must use a guest admin account!
|
|
297
322
|
Please look here for more details:
|
|
298
|
-
https://github.com/homebridge-eufy-security/
|
|
323
|
+
https://github.com/homebridge-plugins/homebridge-eufy-security/wiki/Create-a-dedicated-admin-account-for-Homebridge-Eufy-Security-Plugin
|
|
299
324
|
#########################
|
|
300
325
|
`);
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
await this.delay(1000);
|
|
304
|
-
const s = {
|
|
305
|
-
uniqueId: station.getSerial(),
|
|
306
|
-
displayName: station.getName(),
|
|
307
|
-
type: station.getDeviceType(),
|
|
308
|
-
typename: eufy_security_client_1.DeviceType[station.getDeviceType()],
|
|
309
|
-
disabled: false,
|
|
310
|
-
devices: [],
|
|
311
|
-
};
|
|
312
|
-
s.ignored = (this.config['ignoreStations'] ?? []).includes(s.uniqueId);
|
|
313
|
-
// Standalone Lock or Doorbell doesn't have Security Control
|
|
314
|
-
if (eufy_security_client_1.Device.isLock(s.type) || eufy_security_client_1.Device.isDoorbell(s.type)) {
|
|
315
|
-
s.disabled = true;
|
|
316
|
-
s.ignored = true;
|
|
317
|
-
}
|
|
318
|
-
this.stations.push(s);
|
|
319
|
-
this.storeAccessories();
|
|
320
|
-
this.pushEvent('addAccessory', this.stations);
|
|
326
|
+
return;
|
|
321
327
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
displayName: device.getName(),
|
|
332
|
-
type: device.getDeviceType(),
|
|
333
|
-
typename: eufy_security_client_1.DeviceType[device.getDeviceType()],
|
|
334
|
-
standalone: device.getSerial() === device.getStationSerial(),
|
|
335
|
-
hasBattery: device.hasBattery(),
|
|
336
|
-
isCamera: device.isCamera(),
|
|
337
|
-
isDoorbell: device.isDoorbell(),
|
|
338
|
-
isKeypad: device.isKeyPad(),
|
|
339
|
-
supportsRTSP: device.hasPropertyValue(eufy_security_client_1.PropertyName.DeviceRTSPStream),
|
|
340
|
-
supportsTalkback: device.hasCommand(eufy_security_client_1.CommandName.DeviceStartTalkback),
|
|
341
|
-
DeviceEnabled: device.hasProperty(eufy_security_client_1.PropertyName.DeviceEnabled),
|
|
342
|
-
DeviceMotionDetection: device.hasProperty(eufy_security_client_1.PropertyName.DeviceMotionDetection),
|
|
343
|
-
DeviceLight: device.hasProperty(eufy_security_client_1.PropertyName.DeviceLight),
|
|
344
|
-
DeviceChimeIndoor: device.hasProperty(eufy_security_client_1.PropertyName.DeviceChimeIndoor),
|
|
345
|
-
disabled: false,
|
|
346
|
-
properties: device.getProperties(),
|
|
347
|
-
};
|
|
348
|
-
if (device.hasProperty(eufy_security_client_1.PropertyName.DeviceChargingStatus)) {
|
|
349
|
-
d.chargingStatus = device.getPropertyValue(eufy_security_client_1.PropertyName.DeviceChargingStatus);
|
|
350
|
-
}
|
|
351
|
-
try {
|
|
352
|
-
delete d.properties.picture;
|
|
353
|
-
}
|
|
354
|
-
catch (error) {
|
|
355
|
-
this.log.error(error);
|
|
356
|
-
}
|
|
357
|
-
d.ignored = (this.config['ignoreDevices'] ?? []).includes(d.uniqueId);
|
|
358
|
-
const stationUniqueId = device.getStationSerial();
|
|
359
|
-
const stationIndex = this.stations.findIndex(station => station.uniqueId === stationUniqueId);
|
|
360
|
-
if (stationIndex !== -1) {
|
|
361
|
-
if (!this.stations[stationIndex].devices) {
|
|
362
|
-
this.stations[stationIndex].devices = [];
|
|
363
|
-
}
|
|
364
|
-
this.stations[stationIndex].devices.push(d);
|
|
365
|
-
this.storeAccessories();
|
|
366
|
-
this.pushEvent('addAccessory', this.stations);
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
this.log.error('Station not found for device:', d.displayName);
|
|
370
|
-
}
|
|
328
|
+
|
|
329
|
+
this.pendingStations.push(station);
|
|
330
|
+
this.log.debug(`${station.getName()}: Station queued for processing`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async addDevice(device) {
|
|
334
|
+
if (this.adminAccountUsed) {
|
|
335
|
+
this.pushEvent('AdminAccountUsed', true);
|
|
336
|
+
return;
|
|
371
337
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
338
|
+
|
|
339
|
+
const deviceType = device.getDeviceType();
|
|
340
|
+
if (Device.isKeyPad(deviceType)) {
|
|
341
|
+
this.log.warn(`${device.getName()}: The keypad is ignored as it serves no purpose in this plugin. You can ignore this message.`);
|
|
342
|
+
return;
|
|
375
343
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
344
|
+
|
|
345
|
+
this.pendingDevices.push(device);
|
|
346
|
+
this.log.debug(`${device.getName()}: Device queued for processing`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async processPendingAccessories() {
|
|
350
|
+
this.log.debug(`Processing ${this.pendingStations.length} stations and ${this.pendingDevices.length} devices`);
|
|
351
|
+
|
|
352
|
+
if (this.pendingStations.length === 0 || this.pendingDevices.length === 0) {
|
|
353
|
+
this.log.warn(
|
|
354
|
+
`Discovery finished with ${this.pendingStations.length} station(s) and ${this.pendingDevices.length} device(s). ` +
|
|
355
|
+
'If this is unexpected, please verify your Eufy account has devices and the credentials used are for a guest admin account.',
|
|
356
|
+
);
|
|
385
357
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
// Filter the list of files to include only those with names ending in .log, .log.0.
|
|
392
|
-
const logFiles = files.filter(file => {
|
|
393
|
-
return file.endsWith('.log') || file.endsWith('.log.0');
|
|
394
|
-
});
|
|
395
|
-
// Step 3: Filter out Empty Log Files
|
|
396
|
-
const nonEmptyLogFiles = await Promise.all(logFiles.map(async (file) => {
|
|
397
|
-
const filePath = path_1.default.join(this.storagePath, file);
|
|
398
|
-
const stats = await fs.promises.stat(filePath);
|
|
399
|
-
if (stats.size > 0) {
|
|
400
|
-
return file;
|
|
401
|
-
}
|
|
402
|
-
return null;
|
|
403
|
-
}));
|
|
404
|
-
// Step 4: Remove null entries (empty log files) from the array
|
|
405
|
-
return nonEmptyLogFiles.filter(file => file !== null);
|
|
358
|
+
|
|
359
|
+
// Build set of stations that have at least one device
|
|
360
|
+
const stationsWithDevices = new Set();
|
|
361
|
+
for (const device of this.pendingDevices) {
|
|
362
|
+
stationsWithDevices.add(device.getStationSerial());
|
|
406
363
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
throw error;
|
|
449
|
-
}
|
|
450
|
-
finally {
|
|
451
|
-
// Step 11: Cleanup
|
|
452
|
-
// Ensure to remove any compressed log files after the operation, regardless of success or failure.
|
|
453
|
-
this.removeCompressedLogs();
|
|
364
|
+
|
|
365
|
+
// Process queued stations
|
|
366
|
+
for (const station of this.pendingStations) {
|
|
367
|
+
const stationType = station.getDeviceType();
|
|
368
|
+
const stationSerial = station.getSerial();
|
|
369
|
+
|
|
370
|
+
const s = {
|
|
371
|
+
uniqueId: stationSerial,
|
|
372
|
+
displayName: station.getName(),
|
|
373
|
+
type: stationType,
|
|
374
|
+
typename: DeviceType[stationType],
|
|
375
|
+
disabled: false,
|
|
376
|
+
devices: [],
|
|
377
|
+
properties: station.getProperties(),
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
delete s.properties.picture;
|
|
382
|
+
} catch (error) {
|
|
383
|
+
// ignore
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
s.ignored = (this.config['ignoreStations'] ?? []).includes(s.uniqueId);
|
|
387
|
+
|
|
388
|
+
if (stationType !== DeviceType.STATION) {
|
|
389
|
+
// Non-HomeBase station — the station IS a device (station.type == device.type)
|
|
390
|
+
// Check if the matching device was emitted by the client
|
|
391
|
+
const hasMatchingDevice = this.pendingDevices.some(d => d.getSerial() === stationSerial);
|
|
392
|
+
|
|
393
|
+
if (hasMatchingDevice) {
|
|
394
|
+
s.standalone = true;
|
|
395
|
+
s.disabled = true; // No separate station card; settings accessible via device card
|
|
396
|
+
|
|
397
|
+
// Standalone Locks and Doorbells don't have Security Control
|
|
398
|
+
if (Device.isLock(s.type) || Device.isDoorbell(s.type)) {
|
|
399
|
+
s.noSecurityControl = true;
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
// Station exists but no device counterpart was emitted — unsupported
|
|
403
|
+
s.unsupported = true;
|
|
404
|
+
this.log.warn(`Station "${station.getName()}" (${DeviceType[stationType]}) has no matching device and will be marked as unsupported`);
|
|
454
405
|
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
this.stations.push(s);
|
|
455
409
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
410
|
+
|
|
411
|
+
// Process queued devices and attach them to stations
|
|
412
|
+
for (const device of this.pendingDevices) {
|
|
413
|
+
const devType = device.getDeviceType();
|
|
414
|
+
const d = {
|
|
415
|
+
uniqueId: device.getSerial(),
|
|
416
|
+
displayName: device.getName(),
|
|
417
|
+
type: devType,
|
|
418
|
+
typename: DeviceType[devType],
|
|
419
|
+
standalone: device.getSerial() === device.getStationSerial(),
|
|
420
|
+
hasBattery: device.hasBattery(),
|
|
421
|
+
isCamera: device.isCamera(),
|
|
422
|
+
isDoorbell: device.isDoorbell(),
|
|
423
|
+
isKeypad: device.isKeyPad(),
|
|
424
|
+
isMotionSensor: Device.isMotionSensor(devType),
|
|
425
|
+
isEntrySensor: Device.isEntrySensor(devType),
|
|
426
|
+
isLock: Device.isLock(devType),
|
|
427
|
+
supportsRTSP: device.hasPropertyValue(PropertyName.DeviceRTSPStream),
|
|
428
|
+
supportsTalkback: device.hasCommand(CommandName.DeviceStartTalkback),
|
|
429
|
+
DeviceEnabled: device.hasProperty(PropertyName.DeviceEnabled),
|
|
430
|
+
DeviceMotionDetection: device.hasProperty(PropertyName.DeviceMotionDetection),
|
|
431
|
+
DeviceLight: device.hasProperty(PropertyName.DeviceLight),
|
|
432
|
+
DeviceChimeIndoor: device.hasProperty(PropertyName.DeviceChimeIndoor),
|
|
433
|
+
disabled: false,
|
|
434
|
+
properties: device.getProperties(),
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// Mark device as unsupported if it doesn't match any known accessory type
|
|
438
|
+
if (!device.isCamera() && !Device.isMotionSensor(devType) && !Device.isEntrySensor(devType) && !Device.isLock(devType)) {
|
|
439
|
+
d.unsupported = true;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (device.hasProperty(PropertyName.DeviceChargingStatus)) {
|
|
443
|
+
d.chargingStatus = device.getPropertyValue(PropertyName.DeviceChargingStatus);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
delete d.properties.picture;
|
|
448
|
+
} catch (error) {
|
|
449
|
+
this.log.error(error);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
d.ignored = (this.config['ignoreDevices'] ?? []).includes(d.uniqueId);
|
|
453
|
+
|
|
454
|
+
const stationUniqueId = device.getStationSerial();
|
|
455
|
+
const stationIndex = this.stations.findIndex(station => station.uniqueId === stationUniqueId);
|
|
456
|
+
|
|
457
|
+
if (stationIndex !== -1) {
|
|
458
|
+
if (!this.stations[stationIndex].devices) {
|
|
459
|
+
this.stations[stationIndex].devices = [];
|
|
465
460
|
}
|
|
461
|
+
this.stations[stationIndex].devices.push(d);
|
|
462
|
+
} else {
|
|
463
|
+
this.log.error('Station not found for device:', d.displayName);
|
|
464
|
+
}
|
|
466
465
|
}
|
|
467
|
-
|
|
468
|
-
//
|
|
469
|
-
|
|
466
|
+
|
|
467
|
+
// Clear pending queues
|
|
468
|
+
this.pendingStations = [];
|
|
469
|
+
this.pendingDevices = [];
|
|
470
|
+
|
|
471
|
+
// Always send the final list to the UI, even if empty
|
|
470
472
|
try {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
// Create an instance of HomebridgePluginUiServer
|
|
475
|
-
const server = new HomebridgePluginUiServer();
|
|
476
|
-
// Initialize our UiServer with the server instance
|
|
477
|
-
new UiServer(server);
|
|
473
|
+
this.storeAccessories();
|
|
474
|
+
} catch (error) {
|
|
475
|
+
this.log.error('Error storing accessories:', error);
|
|
478
476
|
}
|
|
479
|
-
|
|
480
|
-
|
|
477
|
+
this.pushEvent('addAccessory', this.stations);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
storeAccessories() {
|
|
481
|
+
const dataToStore = { version: LIB_VERSION, storedAt: new Date().toISOString(), stations: this.stations };
|
|
482
|
+
fs.writeFileSync(this.storedAccessories_file, JSON.stringify(dataToStore));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async resetPlugin() {
|
|
486
|
+
try {
|
|
487
|
+
fs.rmSync(this.storagePath, { recursive: true, force: true });
|
|
488
|
+
return { result: 1 };
|
|
489
|
+
} catch (error) {
|
|
490
|
+
this.log.error('Could not reset plugin: ' + error);
|
|
491
|
+
return { result: 0 };
|
|
481
492
|
}
|
|
482
|
-
}
|
|
483
|
-
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async getLogs() {
|
|
496
|
+
const files = await fs.promises.readdir(this.storagePath);
|
|
497
|
+
|
|
498
|
+
const logFiles = files.filter(file => {
|
|
499
|
+
return file.endsWith('.log') || file.endsWith('.log.0');
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const nonEmptyLogFiles = await Promise.all(logFiles.map(async file => {
|
|
503
|
+
const filePath = path.join(this.storagePath, file);
|
|
504
|
+
const stats = await fs.promises.stat(filePath);
|
|
505
|
+
if (stats.size > 0) {
|
|
506
|
+
return file;
|
|
507
|
+
}
|
|
508
|
+
return null;
|
|
509
|
+
}));
|
|
510
|
+
|
|
511
|
+
return nonEmptyLogFiles.filter(file => file !== null);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async downloadLogs() {
|
|
515
|
+
this.pushEvent('downloadLogsProgress', { progress: 10, status: 'Gets non-empty log files' });
|
|
516
|
+
const finalLogFiles = await this.getLogs();
|
|
517
|
+
|
|
518
|
+
this.pushEvent('downloadLogsProgress', { progress: 30, status: 'Add Log Files to Zip' });
|
|
519
|
+
const zip = new Zip();
|
|
520
|
+
let numberOfFiles = 0;
|
|
521
|
+
finalLogFiles.forEach(logFile => {
|
|
522
|
+
const filePath = path.join(this.storagePath, logFile);
|
|
523
|
+
zip.addFile(filePath);
|
|
524
|
+
numberOfFiles++;
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
this.pushEvent('downloadLogsProgress', { progress: 40, status: 'No Log Files Found' });
|
|
528
|
+
if (numberOfFiles === 0) {
|
|
529
|
+
throw new Error('No log files were found');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
this.pushEvent('downloadLogsProgress', { progress: 45, status: `Compressing ${numberOfFiles} files` });
|
|
534
|
+
await zip.archive(this.logZipFilePath);
|
|
535
|
+
|
|
536
|
+
this.pushEvent('downloadLogsProgress', { progress: 80, status: 'Reading content' });
|
|
537
|
+
const fileBuffer = fs.readFileSync(this.logZipFilePath);
|
|
538
|
+
|
|
539
|
+
this.pushEvent('downloadLogsProgress', { progress: 90, status: 'Returning zip file' });
|
|
540
|
+
return fileBuffer;
|
|
541
|
+
} catch (error) {
|
|
542
|
+
this.log.error('Error while generating log files: ' + error);
|
|
543
|
+
throw error;
|
|
544
|
+
} finally {
|
|
545
|
+
this.removeCompressedLogs();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
removeCompressedLogs() {
|
|
550
|
+
try {
|
|
551
|
+
if (fs.existsSync(this.logZipFilePath)) {
|
|
552
|
+
fs.unlinkSync(this.logZipFilePath);
|
|
553
|
+
}
|
|
554
|
+
return true;
|
|
555
|
+
} catch {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async getSystemInfo() {
|
|
561
|
+
const os = await import('os');
|
|
562
|
+
let homebridgeVersion = 'unknown';
|
|
563
|
+
try {
|
|
564
|
+
const hbPkg = require('homebridge/package.json');
|
|
565
|
+
homebridgeVersion = hbPkg.version;
|
|
566
|
+
} catch {
|
|
567
|
+
// Homebridge package not resolvable from here
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
let deviceSummary = [];
|
|
571
|
+
try {
|
|
572
|
+
if (fs.existsSync(this.storedAccessories_file)) {
|
|
573
|
+
const storedData = JSON.parse(fs.readFileSync(this.storedAccessories_file, 'utf-8'));
|
|
574
|
+
if (storedData.stations) {
|
|
575
|
+
deviceSummary = storedData.stations.map(s => ({
|
|
576
|
+
name: s.displayName,
|
|
577
|
+
type: s.typename,
|
|
578
|
+
devices: (s.devices || []).map(d => ({
|
|
579
|
+
name: d.displayName,
|
|
580
|
+
type: d.typename,
|
|
581
|
+
})),
|
|
582
|
+
}));
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
} catch {
|
|
586
|
+
// ignore
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
pluginVersion: LIB_VERSION,
|
|
591
|
+
eufyClientVersion: libVersion,
|
|
592
|
+
homebridgeVersion,
|
|
593
|
+
nodeVersion: process.version,
|
|
594
|
+
os: `${os.type()} ${os.release()} (${os.arch()})`,
|
|
595
|
+
devices: deviceSummary,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
(() => new UiServer())();
|