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.
Files changed (147) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/FUNDING.yml +1 -0
  3. package/README.md +14 -17
  4. package/dist/accessories/AutoSyncStationAccessory.js +26 -30
  5. package/dist/accessories/AutoSyncStationAccessory.js.map +1 -1
  6. package/dist/accessories/BaseAccessory.js +33 -46
  7. package/dist/accessories/BaseAccessory.js.map +1 -1
  8. package/dist/accessories/CameraAccessory.js +92 -167
  9. package/dist/accessories/CameraAccessory.js.map +1 -1
  10. package/dist/accessories/Device.js +11 -15
  11. package/dist/accessories/Device.js.map +1 -1
  12. package/dist/accessories/EntrySensorAccessory.js +7 -12
  13. package/dist/accessories/EntrySensorAccessory.js.map +1 -1
  14. package/dist/accessories/LockAccessory.js +20 -24
  15. package/dist/accessories/LockAccessory.js.map +1 -1
  16. package/dist/accessories/MotionSensorAccessory.js +7 -12
  17. package/dist/accessories/MotionSensorAccessory.js.map +1 -1
  18. package/dist/accessories/StationAccessory.js +40 -45
  19. package/dist/accessories/StationAccessory.js.map +1 -1
  20. package/dist/config.js +1 -4
  21. package/dist/config.js.map +1 -1
  22. package/dist/controller/LocalLivestreamManager.js +85 -89
  23. package/dist/controller/LocalLivestreamManager.js.map +1 -1
  24. package/dist/controller/recordingDelegate.js +116 -108
  25. package/dist/controller/recordingDelegate.js.map +1 -1
  26. package/dist/controller/snapshotDelegate.js +379 -0
  27. package/dist/controller/snapshotDelegate.js.map +1 -0
  28. package/dist/controller/streamingDelegate.js +202 -207
  29. package/dist/controller/streamingDelegate.js.map +1 -1
  30. package/dist/index.js +9 -7
  31. package/dist/index.js.map +1 -1
  32. package/dist/interfaces.js +1 -2
  33. package/dist/interfaces.js.map +1 -1
  34. package/dist/platform.js +145 -165
  35. package/dist/platform.js.map +1 -1
  36. package/dist/settings.js +26 -19
  37. package/dist/settings.js.map +1 -1
  38. package/dist/utils/Talkback.js +9 -13
  39. package/dist/utils/Talkback.js.map +1 -1
  40. package/dist/utils/configTypes.js +12 -7
  41. package/dist/utils/configTypes.js.map +1 -1
  42. package/dist/utils/ffmpeg.js +44 -52
  43. package/dist/utils/ffmpeg.js.map +1 -1
  44. package/dist/utils/interfaces.js +1 -5
  45. package/dist/utils/interfaces.js.map +1 -1
  46. package/dist/utils/utils.js +32 -134
  47. package/dist/utils/utils.js.map +1 -1
  48. package/dist/version.js +1 -4
  49. package/dist/version.js.map +1 -1
  50. package/eslint.config.mjs +25 -47
  51. package/homebridge-eufy-security.png +0 -0
  52. package/homebridge-ui/public/app.js +221 -0
  53. package/homebridge-ui/public/assets/devices/eufycam3pro_large.png +0 -0
  54. package/homebridge-ui/public/assets/devices/homebase3_large.png +0 -0
  55. package/homebridge-ui/public/assets/devices/homebasemini_large.jpg +0 -0
  56. package/homebridge-ui/public/assets/devices/indoorcamC210_large.png +0 -0
  57. package/homebridge-ui/public/assets/devices/indoorcamC220_large.png +0 -0
  58. package/homebridge-ui/public/assets/devices/indoorcamE30_large.png +0 -0
  59. package/homebridge-ui/public/assets/devices/solocamc35_large.png +0 -0
  60. package/homebridge-ui/public/assets/devices/solocame30_large.png +0 -0
  61. package/homebridge-ui/public/components/device-card.js +141 -0
  62. package/homebridge-ui/public/components/guard-modes.js +88 -0
  63. package/homebridge-ui/public/components/number-input.js +121 -0
  64. package/homebridge-ui/public/components/select.js +73 -0
  65. package/homebridge-ui/public/components/toggle.js +68 -0
  66. package/homebridge-ui/public/index.html +24 -14
  67. package/homebridge-ui/public/services/api.js +129 -0
  68. package/homebridge-ui/public/services/config.js +144 -0
  69. package/homebridge-ui/public/style.css +705 -0
  70. package/homebridge-ui/public/utils/countries.js +73 -0
  71. package/homebridge-ui/public/utils/device-images.js +69 -0
  72. package/homebridge-ui/public/utils/helpers.js +47 -0
  73. package/homebridge-ui/public/views/dashboard.js +211 -0
  74. package/homebridge-ui/public/views/device-detail.js +631 -0
  75. package/homebridge-ui/public/views/login.js +625 -0
  76. package/homebridge-ui/public/views/settings.js +389 -0
  77. package/homebridge-ui/public/views/unsupported-detail.js +216 -0
  78. package/homebridge-ui/server.js +567 -450
  79. package/package.json +19 -59
  80. package/dist/accessories/AutoSyncStationAccessory.d.ts +0 -42
  81. package/dist/accessories/AutoSyncStationAccessory.d.ts.map +0 -1
  82. package/dist/accessories/BaseAccessory.d.ts +0 -56
  83. package/dist/accessories/BaseAccessory.d.ts.map +0 -1
  84. package/dist/accessories/CameraAccessory.d.ts +0 -84
  85. package/dist/accessories/CameraAccessory.d.ts.map +0 -1
  86. package/dist/accessories/Device.d.ts +0 -19
  87. package/dist/accessories/Device.d.ts.map +0 -1
  88. package/dist/accessories/EntrySensorAccessory.d.ts +0 -25
  89. package/dist/accessories/EntrySensorAccessory.d.ts.map +0 -1
  90. package/dist/accessories/LockAccessory.d.ts +0 -45
  91. package/dist/accessories/LockAccessory.d.ts.map +0 -1
  92. package/dist/accessories/MotionSensorAccessory.d.ts +0 -25
  93. package/dist/accessories/MotionSensorAccessory.d.ts.map +0 -1
  94. package/dist/accessories/StationAccessory.d.ts +0 -86
  95. package/dist/accessories/StationAccessory.d.ts.map +0 -1
  96. package/dist/config.d.ts +0 -31
  97. package/dist/config.d.ts.map +0 -1
  98. package/dist/controller/LocalLivestreamManager.d.ts +0 -31
  99. package/dist/controller/LocalLivestreamManager.d.ts.map +0 -1
  100. package/dist/controller/SnapshotManager.d.ts +0 -64
  101. package/dist/controller/SnapshotManager.d.ts.map +0 -1
  102. package/dist/controller/SnapshotManager.js +0 -434
  103. package/dist/controller/SnapshotManager.js.map +0 -1
  104. package/dist/controller/recordingDelegate.d.ts +0 -27
  105. package/dist/controller/recordingDelegate.d.ts.map +0 -1
  106. package/dist/controller/streamingDelegate.d.ts +0 -52
  107. package/dist/controller/streamingDelegate.d.ts.map +0 -1
  108. package/dist/index.d.ts +0 -7
  109. package/dist/index.d.ts.map +0 -1
  110. package/dist/interfaces.d.ts +0 -15
  111. package/dist/interfaces.d.ts.map +0 -1
  112. package/dist/platform.d.ts +0 -94
  113. package/dist/platform.d.ts.map +0 -1
  114. package/dist/settings.d.ts +0 -23
  115. package/dist/settings.d.ts.map +0 -1
  116. package/dist/utils/Talkback.d.ts +0 -21
  117. package/dist/utils/Talkback.d.ts.map +0 -1
  118. package/dist/utils/configTypes.d.ts +0 -68
  119. package/dist/utils/configTypes.d.ts.map +0 -1
  120. package/dist/utils/ffmpeg.d.ts +0 -111
  121. package/dist/utils/ffmpeg.d.ts.map +0 -1
  122. package/dist/utils/interfaces.d.ts +0 -8
  123. package/dist/utils/interfaces.d.ts.map +0 -1
  124. package/dist/utils/utils.d.ts +0 -35
  125. package/dist/utils/utils.d.ts.map +0 -1
  126. package/dist/version.d.ts +0 -2
  127. package/dist/version.d.ts.map +0 -1
  128. package/homebridge-ui/configui/app/util/types.d.ts +0 -65
  129. package/homebridge-ui/configui/app/util/types.d.ts.map +0 -1
  130. package/homebridge-ui/configui/app/util/types.js +0 -18
  131. package/homebridge-ui/configui/app/util/types.js.map +0 -1
  132. package/homebridge-ui/public/3rdpartylicenses.txt +0 -561
  133. package/homebridge-ui/public/assets/devices/homebase2_large.jpg +0 -0
  134. package/homebridge-ui/public/assets/devices/homebase3_large.jpg +0 -0
  135. package/homebridge-ui/public/assets/images/homebridge-eufy-security.png +0 -0
  136. package/homebridge-ui/public/favicon.ico +0 -0
  137. package/homebridge-ui/public/main.f6423287e0411c87.js +0 -1
  138. package/homebridge-ui/public/polyfills.efa2f09ba3e49167.js +0 -1
  139. package/homebridge-ui/public/runtime.ccde331d62c423b3.js +0 -1
  140. package/homebridge-ui/public/styles.13e635bf7a488639.css +0 -5
  141. package/homebridge-ui/server.d.ts +0 -2
  142. package/homebridge-ui/server.d.ts.map +0 -1
  143. package/homebridge-ui/server.js.map +0 -1
  144. package/homebridge-ui/version.d.ts +0 -2
  145. package/homebridge-ui/version.d.ts.map +0 -1
  146. package/homebridge-ui/version.js +0 -5
  147. package/homebridge-ui/version.js.map +0 -1
@@ -1,483 +1,600 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
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
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- const eufy_security_client_1 = require("eufy-security-client");
40
- const fs = __importStar(require("fs"));
41
- const tslog_1 = require("tslog");
42
- const rotating_file_stream_1 = require("rotating-file-stream");
43
- const zip_lib_1 = require("zip-lib");
44
- const types_js_1 = require("./configui/app/util/types.js");
45
- const version_js_1 = require("./version.js");
46
- const path_1 = __importDefault(require("path"));
47
- class UiServer {
48
- stations = [];
49
- eufyClient = null;
50
- log;
51
- tsLog;
52
- homebridgeStoragePath;
53
- storagePath;
54
- storedAccessories_file;
55
- logZipFilePath;
56
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
- pushEvent;
58
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
- onRequest;
60
- ready;
61
- adminAccountUsed = false;
62
- config = {
63
- username: '',
64
- password: '',
65
- language: 'en',
66
- country: 'US',
67
- trustedDeviceName: 'My Phone',
68
- persistentDir: '',
69
- p2pConnectionSetup: 0,
70
- pollingIntervalMinutes: 99,
71
- eventDurationSeconds: 10,
72
- acceptInvitations: true,
73
- };
74
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
- constructor(server) {
76
- // Copy over properties from the HomebridgePluginUiServer instance
77
- this.homebridgeStoragePath = server.homebridgeStoragePath;
78
- this.pushEvent = server.pushEvent.bind(server);
79
- this.onRequest = server.onRequest.bind(server);
80
- this.ready = server.ready.bind(server);
81
- // Initialize paths now that we have homebridgeStoragePath
82
- this.storagePath = this.homebridgeStoragePath + '/eufysecurity';
83
- this.storedAccessories_file = this.storagePath + '/accessories.json';
84
- this.logZipFilePath = this.storagePath + '/logs.zip';
85
- this.config.persistentDir = this.storagePath;
86
- this.initLogger();
87
- this.initTransportStreams();
88
- this.initEventListeners();
89
- this.ready();
90
- }
91
- initLogger() {
92
- this.log = new tslog_1.Logger({
93
- name: `[${version_js_1.LIB_VERSION}]`,
94
- prettyLogTemplate: '{{name}}\t{{logLevelName}}\t[{{fileNameWithLine}}]\t',
95
- prettyErrorTemplate: '\n{{errorName}} {{errorMessage}}\nerror stack:\n{{errorStack}}',
96
- prettyErrorStackTemplate: ' • {{fileName}}\t{{method}}\n\t{{fileNameWithLine}}',
97
- prettyErrorParentNamesSeparator: ':',
98
- prettyErrorLoggerNameDelimiter: '\t',
99
- stylePrettyLogs: true,
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
- initEventListeners() {
135
- this.onRequest('/login', this.login.bind(this));
136
- this.onRequest('/storedAccessories', this.loadStoredAccessories.bind(this));
137
- this.onRequest('/reset', this.resetPlugin.bind(this));
138
- this.onRequest('/downloadLogs', this.downloadLogs.bind(this));
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
- * Deletes a file if it exists.
142
- *
143
- * @param filePath The path to the file to be deleted.
144
- * @returns A Promise that resolves when the operation is complete, or rejects if an error occurs.
145
- * @throws Will reject the Promise with the error if file deletion fails.
146
- */
147
- async deleteFileIfExists(filePath) {
148
- try {
149
- if (fs.existsSync(filePath)) {
150
- fs.unlinkSync(filePath);
151
- }
152
- }
153
- catch (error) {
154
- return Promise.reject(error);
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
- * Resets the persistent data by removing the persistent.json file.
159
- *
160
- * @returns A Promise that resolves when the operation is complete, or rejects if an error occurs.
161
- */
162
- async resetPersistentData() {
163
- return this.deleteFileIfExists(this.storagePath + '/persistent.json');
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
- * Resets the accessory data by removing the stored accessories file.
167
- *
168
- * @returns A Promise that resolves when the operation is complete, or rejects if an error occurs.
169
- */
170
- async resetAccessoryData() {
171
- return this.deleteFileIfExists(this.storedAccessories_file);
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
- async login(options) {
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
- if (options && options.username && options.password) {
176
- this.log.info('deleting persistent.json and accessories due to new login');
177
- this.resetAccessoryData();
178
- await this.resetPersistentData(); // To be commented for testing purpose
179
- }
180
- }
181
- catch (error) {
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
- if (!this.eufyClient && options && options.username && options.password && options.country) {
185
- this.stations = [];
186
- this.log.debug('init eufyClient');
187
- this.config.username = options.username;
188
- this.config.password = options.password;
189
- this.config.country = options.country;
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
- return new Promise((resolve, reject) => {
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
- // Check if the stored accessories file exists
257
- if (!fs.existsSync(this.storedAccessories_file)) {
258
- // If the file doesn't exist, log a warning and return an empty array
259
- this.log.debug('Stored accessories file does not exist.');
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
- catch (error) {
276
- // If an error occurs during the process, log an error message and return an empty array
277
- this.log.error('Could not get stored accessories. Most likely no stored accessories yet: ' + error);
278
- return [];
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
- async delay(ms) {
282
- return new Promise(resolve => setTimeout(resolve, ms));
283
- }
284
- async addStation(station) {
285
- // Before doing anything check if creds are guest admin
286
- const rawStation = station.getRawStation();
287
- if (rawStation.member.member_type !== eufy_security_client_1.UserType.ADMIN) {
288
- this.adminAccountUsed = true;
289
- this.eufyClient?.close();
290
- this.pushEvent('AdminAccountUsed', true);
291
- this.resetPlugin();
292
- this.log.error(`
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/plugin/wiki/Create-a-dedicated-admin-account-for-Homebridge-Eufy-Security-Plugin
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
- return;
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
- async addDevice(device) {
323
- // Before doing anything check if creds are guest admin
324
- if (this.adminAccountUsed) {
325
- this.pushEvent('AdminAccountUsed', true);
326
- return;
327
- }
328
- await this.delay(2000);
329
- const d = {
330
- uniqueId: device.getSerial(),
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
- storeAccessories() {
373
- const dataToStore = { version: version_js_1.LIB_VERSION, stations: this.stations };
374
- fs.writeFileSync(this.storedAccessories_file, JSON.stringify(dataToStore));
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
- async resetPlugin() {
377
- try {
378
- fs.rmSync(this.storagePath, { recursive: true });
379
- return { result: 1 };
380
- }
381
- catch (error) {
382
- this.log.error('Could not reset plugin: ' + error);
383
- return { result: 0 };
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
- async getLogs() {
387
- // Step 1: List Files in Directory
388
- // Asynchronously list all files in the directory specified by this.storagePath.
389
- const files = await fs.promises.readdir(this.storagePath);
390
- // Step 2: Filter Log Files
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
- * Asynchronously compresses log files from a directory and returns a Promise that resolves to a Buffer.
409
- * @returns {Promise<Buffer>} A Promise resolving to a Buffer containing compressed log files.
410
- */
411
- async downloadLogs() {
412
- this.pushEvent('downloadLogsProgress', { progress: 10, status: 'Gets non-empty log files' });
413
- const finalLogFiles = await this.getLogs();
414
- // Step 5: Add Log Files to Zip
415
- // Initialize a Zip instance and add each log file to the archive.
416
- this.pushEvent('downloadLogsProgress', { progress: 30, status: 'Add Log Files to Zip' });
417
- const zip = new zip_lib_1.Zip();
418
- let numberOfFiles = 0;
419
- finalLogFiles.forEach(logFile => {
420
- const filePath = path_1.default.join(this.storagePath, logFile);
421
- zip.addFile(filePath);
422
- numberOfFiles++;
423
- });
424
- // Step 6: Handle No Log Files Found
425
- // If no log files are found after filtering, throw an error.
426
- this.pushEvent('downloadLogsProgress', { progress: 40, status: 'No Log Files Found' });
427
- if (numberOfFiles === 0) {
428
- throw new Error('No log files were found');
429
- }
430
- try {
431
- // Step 7: Archive Zip
432
- // Archive the Zip instance to the specified log zip file.
433
- this.pushEvent('downloadLogsProgress', { progress: 45, status: `Compressing ${numberOfFiles} files` });
434
- await zip.archive(this.logZipFilePath);
435
- // Step 8: Read Zip File
436
- // Read the content of the generated log zip file into a Buffer.
437
- this.pushEvent('downloadLogsProgress', { progress: 80, status: 'Reading content' });
438
- const fileBuffer = fs.readFileSync(this.logZipFilePath);
439
- // Step 9: Return Buffer
440
- // Return the Buffer containing the compressed log files.
441
- this.pushEvent('downloadLogsProgress', { progress: 90, status: 'Returning zip file' });
442
- return fileBuffer;
443
- }
444
- catch (error) {
445
- // Step 10: Error Handling
446
- // Log an error if archiving the zip file fails and propagate the error.
447
- this.log.error('Error while generating log files: ' + error);
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
- removeCompressedLogs() {
457
- try {
458
- if (fs.existsSync(this.logZipFilePath)) {
459
- fs.unlinkSync(this.logZipFilePath);
460
- }
461
- return true;
462
- }
463
- catch {
464
- return false;
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
- // Start the instance of the server with dynamic import
469
- (async () => {
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
- // Dynamically import the ESM module
472
- const pluginUiUtils = await import('@homebridge/plugin-ui-utils');
473
- const HomebridgePluginUiServer = pluginUiUtils.HomebridgePluginUiServer;
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
- catch (error) {
480
- console.error('Failed to initialize server:', error);
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
- //# sourceMappingURL=server.js.map
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())();