@homebridge-plugins/homebridge-eufy-security 0.0.1
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 +5 -0
- package/FUNDING.yml +1 -0
- package/LICENSE +176 -0
- package/README.md +67 -0
- package/config.schema.json +6 -0
- package/dist/accessories/AutoSyncStationAccessory.js +156 -0
- package/dist/accessories/AutoSyncStationAccessory.js.map +1 -0
- package/dist/accessories/BaseAccessory.js +247 -0
- package/dist/accessories/BaseAccessory.js.map +1 -0
- package/dist/accessories/CameraAccessory.js +431 -0
- package/dist/accessories/CameraAccessory.js.map +1 -0
- package/dist/accessories/Device.js +67 -0
- package/dist/accessories/Device.js.map +1 -0
- package/dist/accessories/EntrySensorAccessory.js +48 -0
- package/dist/accessories/EntrySensorAccessory.js.map +1 -0
- package/dist/accessories/LockAccessory.js +142 -0
- package/dist/accessories/LockAccessory.js.map +1 -0
- package/dist/accessories/MotionSensorAccessory.js +48 -0
- package/dist/accessories/MotionSensorAccessory.js.map +1 -0
- package/dist/accessories/SmartDropAccessory.js +145 -0
- package/dist/accessories/SmartDropAccessory.js.map +1 -0
- package/dist/accessories/StationAccessory.js +371 -0
- package/dist/accessories/StationAccessory.js.map +1 -0
- package/dist/config.js +25 -0
- package/dist/config.js.map +1 -0
- package/dist/controller/LocalLivestreamManager.js +116 -0
- package/dist/controller/LocalLivestreamManager.js.map +1 -0
- package/dist/controller/recordingDelegate.js +208 -0
- package/dist/controller/recordingDelegate.js.map +1 -0
- package/dist/controller/snapshotDelegate.js +345 -0
- package/dist/controller/snapshotDelegate.js.map +1 -0
- package/dist/controller/streamingDelegate.js +345 -0
- package/dist/controller/streamingDelegate.js.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces.js +2 -0
- package/dist/interfaces.js.map +1 -0
- package/dist/media/Snapshot-Unavailable.png +0 -0
- package/dist/media/Snapshot-Unavailable.xcf +0 -0
- package/dist/media/Snapshot-black.png +0 -0
- package/dist/media/camera-disabled.png +0 -0
- package/dist/media/camera-offline.png +0 -0
- package/dist/media/media/Snapshot-Unavailable.png +0 -0
- package/dist/media/media/Snapshot-Unavailable.xcf +0 -0
- package/dist/media/media/Snapshot-black.png +0 -0
- package/dist/media/media/camera-disabled.png +0 -0
- package/dist/media/media/camera-offline.png +0 -0
- package/dist/platform.js +716 -0
- package/dist/platform.js.map +1 -0
- package/dist/settings.js +38 -0
- package/dist/settings.js.map +1 -0
- package/dist/utils/Talkback.js +92 -0
- package/dist/utils/Talkback.js.map +1 -0
- package/dist/utils/accessoriesStore.js +206 -0
- package/dist/utils/accessoriesStore.js.map +1 -0
- package/dist/utils/configTypes.js +35 -0
- package/dist/utils/configTypes.js.map +1 -0
- package/dist/utils/ffmpeg.js +843 -0
- package/dist/utils/ffmpeg.js.map +1 -0
- package/dist/utils/interfaces.js +8 -0
- package/dist/utils/interfaces.js.map +1 -0
- package/dist/utils/utils.js +44 -0
- package/dist/utils/utils.js.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/eslint.config.mjs +18 -0
- package/homebridge-eufy-security.png +0 -0
- package/homebridge-ui/public/app.js +225 -0
- package/homebridge-ui/public/assets/devices/4g_lte_starlight_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C30.png +0 -0
- package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C31.png +0 -0
- package/homebridge-ui/public/assets/devices/batterydoorbell1080p_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/batterydoorbell2kdual_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/batterydoorbell_e340_large.png +0 -0
- package/homebridge-ui/public/assets/devices/eufy-security-client.png +0 -0
- package/homebridge-ui/public/assets/devices/eufycam2_large.png +0 -0
- package/homebridge-ui/public/assets/devices/eufycam2c_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycam2cpro_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycam2pro_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycam3_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycam3c_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycam3pro_large.png +0 -0
- package/homebridge-ui/public/assets/devices/eufycam_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycame330_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/floodlight2_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/floodlight2pro_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/floodlight_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/floodlightcame340_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/garage_camera_t8452_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/homebase2_large.png +0 -0
- package/homebridge-ui/public/assets/devices/homebase3_large.png +0 -0
- package/homebridge-ui/public/assets/devices/homebase_large.jpg +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/indoorcamc120_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcammini_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/indoorcamp24_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcams350_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/keypad_large.png +0 -0
- package/homebridge-ui/public/assets/devices/minibase_chime_T8023_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/motionsensor_large.png +0 -0
- package/homebridge-ui/public/assets/devices/sensor_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartdrop_t8790_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t8500_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t8500_wifibridge_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t8503_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t8504_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t8510P_t8520P_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8502_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8506_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8520_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_wifibridge_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_video_t8530_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlockwifibridge_t8021_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/smartsafe_s10_t7400_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartsafe_s12_t7401_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smarttrack_card_t87B2_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smarttrack_link_t87B0_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocamc210_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solocamc35_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocame20_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solocame30_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocame40_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solocaml20_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solocams220_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solocams340_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocams40_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/soloindoorcamc24_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solooutdoorcamc22_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solooutdoorcamc24_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/unknown.png +0 -0
- package/homebridge-ui/public/assets/devices/walllight_s100_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/walllight_s120_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/wireddoorbell1080p_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/wireddoorbell2k_large.png +0 -0
- package/homebridge-ui/public/assets/devices/wireddoorbelldual_large.jpg +0 -0
- package/homebridge-ui/public/assets/icons/attach.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_0.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_1.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_2.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_3.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_4.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_5.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_6.svg +1 -0
- package/homebridge-ui/public/assets/icons/bolt.svg +1 -0
- package/homebridge-ui/public/assets/icons/bug-report.svg +1 -0
- package/homebridge-ui/public/assets/icons/copy.svg +1 -0
- package/homebridge-ui/public/assets/icons/delete.svg +1 -0
- package/homebridge-ui/public/assets/icons/download.svg +1 -0
- package/homebridge-ui/public/assets/icons/info.svg +1 -0
- package/homebridge-ui/public/assets/icons/inventory.svg +1 -0
- package/homebridge-ui/public/assets/icons/refresh.svg +1 -0
- package/homebridge-ui/public/assets/icons/satellite_alt.svg +1 -0
- package/homebridge-ui/public/assets/icons/settings.svg +1 -0
- package/homebridge-ui/public/assets/icons/settings_backup_restore.svg +1 -0
- package/homebridge-ui/public/assets/icons/solar_power.svg +1 -0
- package/homebridge-ui/public/assets/icons/warning.svg +1 -0
- package/homebridge-ui/public/components/device-card.js +162 -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 +27 -0
- package/homebridge-ui/public/services/api.js +214 -0
- package/homebridge-ui/public/services/config.js +144 -0
- package/homebridge-ui/public/style.css +775 -0
- package/homebridge-ui/public/utils/countries.js +73 -0
- package/homebridge-ui/public/utils/device-images.js +89 -0
- package/homebridge-ui/public/utils/helpers.js +87 -0
- package/homebridge-ui/public/views/dashboard.js +226 -0
- package/homebridge-ui/public/views/device-detail.js +610 -0
- package/homebridge-ui/public/views/diagnostics.js +296 -0
- package/homebridge-ui/public/views/login.js +636 -0
- package/homebridge-ui/public/views/settings.js +192 -0
- package/homebridge-ui/public/views/unsupported-detail.js +296 -0
- package/homebridge-ui/server.js +1327 -0
- package/media/Snapshot-Unavailable.png +0 -0
- package/media/Snapshot-Unavailable.xcf +0 -0
- package/media/Snapshot-black.png +0 -0
- package/media/camera-disabled.png +0 -0
- package/media/camera-offline.png +0 -0
- package/package.json +64 -0
package/dist/platform.js
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
|
|
2
|
+
import { DEFAULT_CONFIG_VALUES } from './config.js';
|
|
3
|
+
import { StationAccessory } from './accessories/StationAccessory.js';
|
|
4
|
+
import { EntrySensorAccessory } from './accessories/EntrySensorAccessory.js';
|
|
5
|
+
import { MotionSensorAccessory } from './accessories/MotionSensorAccessory.js';
|
|
6
|
+
import { CameraAccessory } from './accessories/CameraAccessory.js';
|
|
7
|
+
import { LockAccessory } from './accessories/LockAccessory.js';
|
|
8
|
+
import { SmartDropAccessory } from './accessories/SmartDropAccessory.js';
|
|
9
|
+
import { AutoSyncStationAccessory } from './accessories/AutoSyncStationAccessory.js';
|
|
10
|
+
import { EufySecurity, Device, UserType, libVersion, LogLevel, } from 'eufy-security-client';
|
|
11
|
+
import { createStream } from 'rotating-file-stream';
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import { platform } from 'node:process';
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { initLog, log, tsLogger, ffmpegLogger, HAP } from './utils/utils.js';
|
|
17
|
+
import { hasFdkAac } from './utils/ffmpeg.js';
|
|
18
|
+
import { AccessoriesStore } from './utils/accessoriesStore.js';
|
|
19
|
+
import { LIB_VERSION } from './version.js';
|
|
20
|
+
export class EufySecurityPlatform {
|
|
21
|
+
config;
|
|
22
|
+
api;
|
|
23
|
+
eufyClient = {};
|
|
24
|
+
// this is used to track restored cached accessories
|
|
25
|
+
accessories = [];
|
|
26
|
+
already_shutdown = false;
|
|
27
|
+
eufyPath;
|
|
28
|
+
activeAccessoryIds = [];
|
|
29
|
+
discoveryDebounceTimeout;
|
|
30
|
+
pendingStations = [];
|
|
31
|
+
pendingDevices = [];
|
|
32
|
+
/** Seconds to wait after the last station/device event before processing. */
|
|
33
|
+
static DISCOVERY_DEBOUNCE_SEC = 15;
|
|
34
|
+
_hostSystem = '';
|
|
35
|
+
accessoriesStore;
|
|
36
|
+
constructor(hblog, config, api) {
|
|
37
|
+
this.config = config;
|
|
38
|
+
this.api = api;
|
|
39
|
+
this.eufyPath = this.api.user.storagePath() + '/eufysecurity';
|
|
40
|
+
if (!fs.existsSync(this.eufyPath)) {
|
|
41
|
+
fs.mkdirSync(this.eufyPath);
|
|
42
|
+
}
|
|
43
|
+
// Identify what we're running on so we can take advantage of hardware-specific features.
|
|
44
|
+
this.probeHwOs();
|
|
45
|
+
this.initConfig(config);
|
|
46
|
+
this.configureLogger();
|
|
47
|
+
this.initSetup();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Initializes the configuration object with default values where properties are not provided.
|
|
51
|
+
* If a property is provided in the config object, it overrides the default value.
|
|
52
|
+
* Additionally, certain numeric properties are parsed to ensure they are of the correct type.
|
|
53
|
+
* @param config - Partial configuration object with user-provided values.
|
|
54
|
+
*/
|
|
55
|
+
initConfig(config) {
|
|
56
|
+
// Assigns the provided config object to this.config, casting it to the EufySecurityPlatformConfig type.
|
|
57
|
+
this.config = config;
|
|
58
|
+
// Iterates over each key in the DEFAULT_CONFIG_VALUES object.
|
|
59
|
+
Object.keys(DEFAULT_CONFIG_VALUES).forEach(key => {
|
|
60
|
+
// Checks if the corresponding property in the config object is undefined or null.
|
|
61
|
+
// If it is, assigns the default value from DEFAULT_CONFIG_VALUES to it.
|
|
62
|
+
this.config[key] ??= DEFAULT_CONFIG_VALUES[key];
|
|
63
|
+
});
|
|
64
|
+
// List of properties that need to be parsed as numeric values
|
|
65
|
+
const numericProperties = [
|
|
66
|
+
'CameraMaxLivestreamDuration',
|
|
67
|
+
'pollingIntervalMinutes',
|
|
68
|
+
'hkHome',
|
|
69
|
+
'hkAway',
|
|
70
|
+
'hkNight',
|
|
71
|
+
'hkOff'
|
|
72
|
+
];
|
|
73
|
+
// Iterate over each property in the config object
|
|
74
|
+
Object.entries(this.config).forEach(([key, value]) => {
|
|
75
|
+
// Check if the property is one of the numeric properties
|
|
76
|
+
if (numericProperties.includes(key)) {
|
|
77
|
+
// Parse the value to ensure it is of the correct type (number)
|
|
78
|
+
this.config[key] = (typeof value === 'string') ? parseInt(value) : value;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Configures the logging mechanism for the plugin.
|
|
84
|
+
*/
|
|
85
|
+
configureLogger() {
|
|
86
|
+
// Define options for logging
|
|
87
|
+
const logOptions = {
|
|
88
|
+
name: '[EufySecurity]', // Name prefix for log messages
|
|
89
|
+
prettyLogTemplate: '[{{mm}}/{{dd}}/{{yyyy}}, {{hh}}:{{MM}}:{{ss}}]\t{{name}}\t{{logLevelName}}\t', // Template for pretty log output
|
|
90
|
+
prettyErrorTemplate: '\n{{errorName}} {{errorMessage}}\nerror stack:\n{{errorStack}}', // Template for pretty error output
|
|
91
|
+
prettyErrorStackTemplate: ' • {{fileName}}\t{{method}}\n\t{{fileNameWithLine}}', // Template for error stack trace
|
|
92
|
+
prettyErrorParentNamesSeparator: '', // Separator for parent names in error messages
|
|
93
|
+
prettyErrorLoggerNameDelimiter: '\t', // Delimiter for logger name in error messages
|
|
94
|
+
stylePrettyLogs: true, // Enable styling for logs
|
|
95
|
+
minLevel: 3, // Minimum log level to display (3 corresponds to INFO)
|
|
96
|
+
prettyLogTimeZone: 'local', // Time zone for log timestamps
|
|
97
|
+
prettyLogStyles: {
|
|
98
|
+
logLevelName: {
|
|
99
|
+
'*': ['bold', 'black', 'bgWhiteBright', 'dim'], // Default style
|
|
100
|
+
SILLY: ['bold', 'white'], // Style for SILLY level
|
|
101
|
+
TRACE: ['bold', 'whiteBright'], // Style for TRACE level
|
|
102
|
+
DEBUG: ['bold', 'green'], // Style for DEBUG level
|
|
103
|
+
INFO: ['bold', 'blue'], // Style for INFO level
|
|
104
|
+
WARN: ['bold', 'yellow'], // Style for WARN level
|
|
105
|
+
ERROR: ['bold', 'red'], // Style for ERROR level
|
|
106
|
+
FATAL: ['bold', 'redBright'], // Style for FATAL level
|
|
107
|
+
},
|
|
108
|
+
dateIsoStr: 'gray', // Style for ISO date strings
|
|
109
|
+
filePathWithLine: 'white', // Style for file paths with line numbers
|
|
110
|
+
name: 'green', // Style for logger names
|
|
111
|
+
nameWithDelimiterPrefix: ['white', 'bold'], // Style for logger names with delimiter prefix
|
|
112
|
+
nameWithDelimiterSuffix: ['white', 'bold'], // Style for logger names with delimiter suffix
|
|
113
|
+
errorName: ['bold', 'bgRedBright', 'whiteBright'], // Style for error names
|
|
114
|
+
fileName: ['yellow'], // Style for file names
|
|
115
|
+
},
|
|
116
|
+
maskValuesOfKeys: [
|
|
117
|
+
'username',
|
|
118
|
+
'password',
|
|
119
|
+
'token',
|
|
120
|
+
'clientPrivateKey',
|
|
121
|
+
'private_key',
|
|
122
|
+
'login_hash',
|
|
123
|
+
'serverPublicKey',
|
|
124
|
+
'cloud_token',
|
|
125
|
+
'refreshToken',
|
|
126
|
+
'p2p_conn',
|
|
127
|
+
'app_conn',
|
|
128
|
+
'address',
|
|
129
|
+
'latitude',
|
|
130
|
+
'longitude',
|
|
131
|
+
'serialnumber',
|
|
132
|
+
'serialNumber',
|
|
133
|
+
'stationSerialNumber',
|
|
134
|
+
'data',
|
|
135
|
+
'ignoreStations',
|
|
136
|
+
'ignoreDevices',
|
|
137
|
+
'pincode',
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
// Modify log options if detailed logging is enabled
|
|
141
|
+
if (this.config.enableDetailedLogging) {
|
|
142
|
+
logOptions.name = `[EufySecurity-${LIB_VERSION}]`; // Modify logger name with plugin version
|
|
143
|
+
logOptions.prettyLogTemplate = '[{{mm}}/{{dd}}/{{yyyy}} {{hh}}:{{MM}}:{{ss}}]\t{{name}}\t{{logLevelName}}\t[{{fileNameWithLine}}]\t'; // Modify log template
|
|
144
|
+
logOptions.minLevel = 2; // Adjust minimum log level
|
|
145
|
+
}
|
|
146
|
+
// Initialize the global logger with the configured options
|
|
147
|
+
initLog(logOptions);
|
|
148
|
+
// Configures log streams for various log files
|
|
149
|
+
this.configureLogStreams();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Configures log streams for various log files if log file storage is not omitted.
|
|
153
|
+
*/
|
|
154
|
+
configureLogStreams() {
|
|
155
|
+
// Log a message if log file storage will be omitted
|
|
156
|
+
if (this.config.omitLogFiles) {
|
|
157
|
+
log.info('log file storage will be omitted.');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Log streams configuration: main log keeps file info, lib logs omit it
|
|
161
|
+
const logStreamsWithFile = [
|
|
162
|
+
{ name: 'eufy-security.log', logger: log },
|
|
163
|
+
{ name: 'ffmpeg.log', logger: ffmpegLogger },
|
|
164
|
+
];
|
|
165
|
+
const logStreamsWithoutFile = [
|
|
166
|
+
{ name: 'eufy-lib.log', logger: tsLogger },
|
|
167
|
+
];
|
|
168
|
+
const parentName = log.settings.name;
|
|
169
|
+
// Attach transports for logs that include file info (eufy-security, ffmpeg)
|
|
170
|
+
for (const { name, logger } of logStreamsWithFile) {
|
|
171
|
+
const logStream = createStream(name, {
|
|
172
|
+
path: this.eufyPath,
|
|
173
|
+
interval: '1d',
|
|
174
|
+
rotate: 3,
|
|
175
|
+
maxSize: '200M',
|
|
176
|
+
compress: 'gzip',
|
|
177
|
+
});
|
|
178
|
+
logger.attachTransport((logObj) => {
|
|
179
|
+
const meta = logObj['_meta'];
|
|
180
|
+
const loggerName = meta.name || parentName;
|
|
181
|
+
const level = meta.logLevelName;
|
|
182
|
+
const date = meta.date.toISOString();
|
|
183
|
+
const fileNameWithLine = meta.path?.fileNameWithLine || '';
|
|
184
|
+
let message = '';
|
|
185
|
+
for (let i = 0; i <= 5; i++) {
|
|
186
|
+
if (logObj[i]) {
|
|
187
|
+
message += ' ' + (typeof logObj[i] === 'string' ? logObj[i] : JSON.stringify(logObj[i]));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
logStream.write(date + '\t' + loggerName + '\t' + level + '\t' + fileNameWithLine + '\t' + message + '\n');
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
// Attach transports for lib logs (no file column)
|
|
194
|
+
for (const { name, logger } of logStreamsWithoutFile) {
|
|
195
|
+
const logStream = createStream(name, {
|
|
196
|
+
path: this.eufyPath,
|
|
197
|
+
interval: '1d',
|
|
198
|
+
rotate: 3,
|
|
199
|
+
maxSize: '200M',
|
|
200
|
+
compress: 'gzip',
|
|
201
|
+
});
|
|
202
|
+
logger.attachTransport((logObj) => {
|
|
203
|
+
const meta = logObj['_meta'];
|
|
204
|
+
const loggerName = meta.name;
|
|
205
|
+
const level = meta.logLevelName;
|
|
206
|
+
const date = meta.date.toISOString();
|
|
207
|
+
let message = '';
|
|
208
|
+
for (let i = 0; i <= 5; i++) {
|
|
209
|
+
if (logObj[i]) {
|
|
210
|
+
message += ' ' + (typeof logObj[i] === 'string' ? logObj[i] : JSON.stringify(logObj[i]));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
logStream.write(date + '\t' + loggerName + '\t' + level + '\t' + message + '\n');
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// This function is responsible for identifying the hardware and operating system environment the application is running on.
|
|
218
|
+
probeHwOs() {
|
|
219
|
+
// Start off with a generic identifier.
|
|
220
|
+
this._hostSystem = 'generic';
|
|
221
|
+
// Take a look at the platform we're on for an initial hint of what we are.
|
|
222
|
+
switch (platform) {
|
|
223
|
+
// The beloved macOS.
|
|
224
|
+
case 'darwin':
|
|
225
|
+
// For macOS, we check the CPU model to determine if it's an Apple CPU or an Intel CPU.
|
|
226
|
+
this._hostSystem = 'macOS.' + (os.cpus()[0].model.includes('Apple') ? 'Apple' : 'Intel');
|
|
227
|
+
break;
|
|
228
|
+
// The indomitable Linux.
|
|
229
|
+
case 'linux':
|
|
230
|
+
// Let's further see if we're a small, but scrappy, Raspberry Pi.
|
|
231
|
+
try {
|
|
232
|
+
// As of the 4.9 kernel, Raspberry Pi prefers to be identified using this method and has deprecated cpuinfo.
|
|
233
|
+
const systemId = readFileSync('/sys/firmware/devicetree/base/model', { encoding: 'utf8' });
|
|
234
|
+
// Check if it's a Raspberry Pi 4.
|
|
235
|
+
if (/Raspberry Pi (Compute Module )?4/.test(systemId)) {
|
|
236
|
+
// If it's a Pi 4, we identify the system as running Raspbian.
|
|
237
|
+
this._hostSystem = 'raspbian';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// Errors encountered while attempting to identify the system are ignored.
|
|
242
|
+
// We prioritize getting system information through hints rather than comprehensive detection.
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
default:
|
|
246
|
+
// We aren't trying to solve for every system type.
|
|
247
|
+
// If the platform doesn't match macOS or Linux, we keep the generic identifier.
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Utility to return the hardware environment we're on.
|
|
252
|
+
get hostSystem() {
|
|
253
|
+
return this._hostSystem;
|
|
254
|
+
}
|
|
255
|
+
initSetup() {
|
|
256
|
+
log.debug('plugin data store:', this.eufyPath);
|
|
257
|
+
log.debug('OS is', this.hostSystem);
|
|
258
|
+
log.debug('Using bropats @homebridge-eufy-security/eufy-security-client library in version ', libVersion);
|
|
259
|
+
// Probe ffmpeg for libfdk_aac support early so the warning shows at boot.
|
|
260
|
+
hasFdkAac();
|
|
261
|
+
// Log the final configuration object for debugging purposes
|
|
262
|
+
log.debug('The config is:', this.config);
|
|
263
|
+
const eufyConfig = {
|
|
264
|
+
username: this.config.username,
|
|
265
|
+
password: this.config.password,
|
|
266
|
+
country: this.config.country,
|
|
267
|
+
trustedDeviceName: this.config.deviceName,
|
|
268
|
+
language: 'en',
|
|
269
|
+
persistentDir: this.eufyPath,
|
|
270
|
+
p2pConnectionSetup: 0,
|
|
271
|
+
pollingIntervalMinutes: this.config.pollingIntervalMinutes,
|
|
272
|
+
enableEmbeddedPKCS1Support: this.config.enableEmbeddedPKCS1Support,
|
|
273
|
+
eventDurationSeconds: 10,
|
|
274
|
+
logging: {
|
|
275
|
+
level: (this.config.enableDetailedLogging) ? LogLevel.Debug : LogLevel.Info,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
this.api.on("didFinishLaunching" /* APIEvent.DID_FINISH_LAUNCHING */, async () => {
|
|
279
|
+
await this.pluginSetup(eufyConfig);
|
|
280
|
+
});
|
|
281
|
+
this.api.on("shutdown" /* APIEvent.SHUTDOWN */, async () => {
|
|
282
|
+
await this.pluginShutdown();
|
|
283
|
+
});
|
|
284
|
+
log.debug('Finished booting!');
|
|
285
|
+
}
|
|
286
|
+
async pluginSetup(eufyConfig) {
|
|
287
|
+
try {
|
|
288
|
+
this.eufyClient = await EufySecurity.initialize(eufyConfig, (this.config.enableDetailedLogging) ? tsLogger : undefined);
|
|
289
|
+
// Each camera adds listeners (livestream, talkback) on top of the base ones.
|
|
290
|
+
// Raise limit to prevent MaxListenersExceededWarning in Node 22+.
|
|
291
|
+
this.eufyClient.setMaxListeners(30);
|
|
292
|
+
this.eufyClient.on('station added', this.stationAdded.bind(this));
|
|
293
|
+
this.eufyClient.on('station removed', this.stationRemoved.bind(this));
|
|
294
|
+
this.eufyClient.on('device added', this.deviceAdded.bind(this));
|
|
295
|
+
this.eufyClient.on('device removed', this.deviceRemoved.bind(this));
|
|
296
|
+
// Initialise the accessories store for UI consumption
|
|
297
|
+
this.accessoriesStore = new AccessoriesStore(this.eufyClient, this.config, this.eufyPath);
|
|
298
|
+
this.eufyClient.on('push connect', () => {
|
|
299
|
+
log.debug('Push Connected!');
|
|
300
|
+
});
|
|
301
|
+
this.eufyClient.on('push close', () => {
|
|
302
|
+
log.debug('Push Closed!');
|
|
303
|
+
});
|
|
304
|
+
this.eufyClient.on('connect', () => {
|
|
305
|
+
log.debug('Connected!');
|
|
306
|
+
});
|
|
307
|
+
this.eufyClient.on('close', () => {
|
|
308
|
+
log.debug('Closed!');
|
|
309
|
+
});
|
|
310
|
+
this.eufyClient.on('connection error', async (error) => {
|
|
311
|
+
log.debug(`Error: ${error}`);
|
|
312
|
+
await this.pluginShutdown();
|
|
313
|
+
});
|
|
314
|
+
this.eufyClient.once('captcha request', async () => {
|
|
315
|
+
log.error(`
|
|
316
|
+
***************************
|
|
317
|
+
***** WARNING MESSAGE *****
|
|
318
|
+
***************************
|
|
319
|
+
Important Notice: CAPTCHA Required
|
|
320
|
+
Your account seems to have triggered a security measure that requires CAPTCHA verification for the next 24 hours...
|
|
321
|
+
Please abstain from any activities until this period elapses...
|
|
322
|
+
Should your issue persist beyond this timeframe, you may need to consider setting up a new account.
|
|
323
|
+
For more detailed instructions, please consult:
|
|
324
|
+
https://github.com/homebridge-plugins/homebridge-eufy-security/wiki/Create-a-dedicated-admin-account-for-Homebridge-Eufy-Security-Plugin
|
|
325
|
+
***************************
|
|
326
|
+
`);
|
|
327
|
+
await this.pluginShutdown();
|
|
328
|
+
});
|
|
329
|
+
this.eufyClient.on('tfa request', async () => {
|
|
330
|
+
log.error(`
|
|
331
|
+
***************************
|
|
332
|
+
***** WARNING MESSAGE *****
|
|
333
|
+
***************************
|
|
334
|
+
Attention: Two-Factor Authentication (2FA) Requested
|
|
335
|
+
It appears that your account is currently under a temporary 24-hour flag for security reasons...
|
|
336
|
+
Kindly refrain from making any further attempts during this period...
|
|
337
|
+
If your concern remains unresolved after 24 hours, you may need to consider creating a new account.
|
|
338
|
+
For additional information, refer to:
|
|
339
|
+
https://github.com/homebridge-plugins/homebridge-eufy-security/wiki/Create-a-dedicated-admin-account-for-Homebridge-Eufy-Security-Plugin
|
|
340
|
+
***************************
|
|
341
|
+
`);
|
|
342
|
+
await this.pluginShutdown();
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
catch (e) {
|
|
346
|
+
log.error(`Error while setup : ${e}`);
|
|
347
|
+
log.error('Not connected can\'t continue!');
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
await this.eufyClient.connect();
|
|
352
|
+
log.debug('EufyClient connected ' + this.eufyClient.isConnected());
|
|
353
|
+
}
|
|
354
|
+
catch (e) {
|
|
355
|
+
log.error(`Error authenticating Eufy: ${e}`);
|
|
356
|
+
}
|
|
357
|
+
if (!this.eufyClient.isConnected()) {
|
|
358
|
+
log.error('Not connected can\'t continue!');
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
log.info('Connected to Eufy. Waiting for stations and devices to be discovered...');
|
|
362
|
+
if (this.config.CameraMaxLivestreamDuration > 86400) {
|
|
363
|
+
this.config.CameraMaxLivestreamDuration = 86400;
|
|
364
|
+
log.warn('Your maximum livestream duration value is too large. Since this can cause problems it was reset to 86400 seconds (1 day maximum).');
|
|
365
|
+
}
|
|
366
|
+
this.eufyClient.setCameraMaxLivestreamDuration(this.config.CameraMaxLivestreamDuration);
|
|
367
|
+
log.debug(`CameraMaxLivestreamDuration: ${this.eufyClient.getCameraMaxLivestreamDuration()}`);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Generates a UUID based on the given identifier and station flag.
|
|
371
|
+
* @param identifier The unique identifier.
|
|
372
|
+
* @param isStation Flag indicating whether the identifier belongs to a station.
|
|
373
|
+
* @returns The generated UUID.
|
|
374
|
+
*/
|
|
375
|
+
generateUUID(identifier, isStation) {
|
|
376
|
+
// Add prefix 's_' if it's a station identifier, otherwise, no prefix.
|
|
377
|
+
const prefix = isStation ? 's1_' : 'd1_';
|
|
378
|
+
// Generate UUID based on the prefix + identifier.
|
|
379
|
+
return HAP.uuid.generate(prefix + identifier);
|
|
380
|
+
}
|
|
381
|
+
async delay(ms) {
|
|
382
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Defines an accessory for a device or station.
|
|
386
|
+
*
|
|
387
|
+
* @param deviceContainer The container holding information about the device or station.
|
|
388
|
+
* @param isStation A boolean indicating whether the container represents a station.
|
|
389
|
+
* @returns A tuple containing the created or cached accessory and a boolean indicating whether the accessory was cached.
|
|
390
|
+
*/
|
|
391
|
+
defineAccessory(deviceContainer, isStation) {
|
|
392
|
+
// Generate UUID for the accessory based on device's unique identifier and whether it's a station
|
|
393
|
+
const uuid = this.generateUUID(deviceContainer.deviceIdentifier.uniqueId, isStation);
|
|
394
|
+
// Check if the accessory is already cached
|
|
395
|
+
const cachedAccessory = this.accessories.find((accessory) => accessory.UUID === uuid);
|
|
396
|
+
// If the accessory is cached, remove it from the accessories array
|
|
397
|
+
if (cachedAccessory) {
|
|
398
|
+
this.accessories.splice(this.accessories.indexOf(cachedAccessory), 1);
|
|
399
|
+
}
|
|
400
|
+
// Determine if the device is a camera
|
|
401
|
+
const isCamera = (deviceContainer.eufyDevice instanceof Device)
|
|
402
|
+
? deviceContainer.eufyDevice.isCamera()
|
|
403
|
+
: false;
|
|
404
|
+
// Create a new accessory if not cached, otherwise use the cached one
|
|
405
|
+
const accessory = cachedAccessory
|
|
406
|
+
|| new this.api.platformAccessory(deviceContainer.deviceIdentifier.displayName, uuid, isCamera ? 17 /* HAP.Categories.CAMERA */ : 11 /* HAP.Categories.SECURITY_SYSTEM */);
|
|
407
|
+
// Store device information in accessory context
|
|
408
|
+
accessory.context['device'] = deviceContainer.deviceIdentifier;
|
|
409
|
+
accessory.displayName = deviceContainer.deviceIdentifier.displayName;
|
|
410
|
+
return [accessory, !!cachedAccessory];
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Adds or updates an accessory for a device or station.
|
|
414
|
+
*
|
|
415
|
+
* @param deviceContainer The container holding information about the device or station.
|
|
416
|
+
* @param isStation A boolean indicating whether the container represents a station.
|
|
417
|
+
*/
|
|
418
|
+
async addOrUpdateAccessory(deviceContainer, isStation) {
|
|
419
|
+
try {
|
|
420
|
+
// Define the accessory and check if it already exists
|
|
421
|
+
const [accessory, isExist] = this.defineAccessory(deviceContainer, isStation);
|
|
422
|
+
// Register the accessory based on whether it's a station or device
|
|
423
|
+
try {
|
|
424
|
+
if (isStation) {
|
|
425
|
+
this.register_station(accessory, deviceContainer);
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
this.register_device(accessory, deviceContainer);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
// Remove station or device accessories created prior to plugin upgrade,
|
|
433
|
+
// which may have been subject to removal due to newly introduced logic.
|
|
434
|
+
if (isExist) {
|
|
435
|
+
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
|
|
436
|
+
}
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
// Add the accessory's UUID to activeAccessoryIds if it's not already present
|
|
440
|
+
if (this.activeAccessoryIds.indexOf(accessory.UUID) === -1) {
|
|
441
|
+
this.activeAccessoryIds.push(accessory.UUID);
|
|
442
|
+
}
|
|
443
|
+
// Update or register the accessory with the platform
|
|
444
|
+
if (isExist) {
|
|
445
|
+
this.api.updatePlatformAccessories([accessory]);
|
|
446
|
+
log.info(`Updating existing accessory: ${accessory.displayName}`);
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
|
|
450
|
+
log.info(`Registering new accessory: ${accessory.displayName}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
// Log any errors that occur during accessory addition or update
|
|
455
|
+
log.error(`Error in ${isStation ? 'stationAdded' : 'deviceAdded'}:`, error);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async stationAdded(station) {
|
|
459
|
+
try {
|
|
460
|
+
if (this.config.ignoreStations.includes(station.getSerial())) {
|
|
461
|
+
log.debug(`${station.getName()}: Station ignored`);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const rawStation = station.getRawStation();
|
|
465
|
+
if (rawStation.member.member_type !== UserType.ADMIN) {
|
|
466
|
+
await this.pluginShutdown();
|
|
467
|
+
log.error(`
|
|
468
|
+
#########################
|
|
469
|
+
######### ERROR #########
|
|
470
|
+
#########################
|
|
471
|
+
You're not using a guest admin account with this plugin! You must use a guest admin account!
|
|
472
|
+
Please look here for more details:
|
|
473
|
+
https://github.com/homebridge-plugins/homebridge-eufy-security/wiki/Create-a-dedicated-admin-account-for-Homebridge-Eufy-Security-Plugin
|
|
474
|
+
#########################
|
|
475
|
+
`);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
// Store station for batch processing later
|
|
479
|
+
this.pendingStations.push(station);
|
|
480
|
+
log.debug(`${station.getName()}: Station queued for processing`);
|
|
481
|
+
this.resetDiscoveryDebounce();
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
log.error(`Error in stationAdded:, ${error}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async deviceAdded(device) {
|
|
488
|
+
try {
|
|
489
|
+
if (this.config.ignoreDevices.includes(device.getSerial())) {
|
|
490
|
+
log.debug(`${device.getName()}: Device ignored`);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
// Check if the device is a keypad and ignore it from the start
|
|
494
|
+
const deviceType = device.getDeviceType();
|
|
495
|
+
if (Device.isKeyPad(deviceType)) {
|
|
496
|
+
log.warn(`${device.getName()}: The keypad is ignored as it serves no purpose in this plugin. You can ignore this message.`);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
// Store device for batch processing later
|
|
500
|
+
this.pendingDevices.push(device);
|
|
501
|
+
log.debug(`${device.getName()}: Device queued for processing`);
|
|
502
|
+
this.resetDiscoveryDebounce();
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
log.error(`Error in deviceAdded: ${error}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async stationRemoved(station) {
|
|
509
|
+
const serial = station.getSerial();
|
|
510
|
+
log.debug(`A device has been removed: ${serial}`);
|
|
511
|
+
}
|
|
512
|
+
async deviceRemoved(device) {
|
|
513
|
+
const serial = device.getSerial();
|
|
514
|
+
log.debug(`A device has been removed: ${serial}`);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Processes pending stations and devices in batch.
|
|
518
|
+
* Only creates station entities if:
|
|
519
|
+
* 1. It's a hub/base station (Device.isStation) OR
|
|
520
|
+
* 2. It has at least one device attached
|
|
521
|
+
*
|
|
522
|
+
* All discovered devices are processed since they are already verified by bropat/eufy-security-client.
|
|
523
|
+
*/
|
|
524
|
+
/**
|
|
525
|
+
* Resets the discovery debounce timer.
|
|
526
|
+
* Each time a station or device is emitted, the timer restarts.
|
|
527
|
+
* Processing begins once no new events arrive for DISCOVERY_DEBOUNCE_SEC seconds.
|
|
528
|
+
*/
|
|
529
|
+
resetDiscoveryDebounce() {
|
|
530
|
+
if (this.discoveryDebounceTimeout) {
|
|
531
|
+
clearTimeout(this.discoveryDebounceTimeout);
|
|
532
|
+
}
|
|
533
|
+
const delaySec = EufySecurityPlatform.DISCOVERY_DEBOUNCE_SEC;
|
|
534
|
+
log.debug(`Discovery debounce reset — will process in ${delaySec}s if no more devices arrive ` +
|
|
535
|
+
`(${this.pendingStations.length} station(s), ${this.pendingDevices.length} device(s) queued)`);
|
|
536
|
+
this.discoveryDebounceTimeout = setTimeout(async () => {
|
|
537
|
+
await this.processPendingDevices();
|
|
538
|
+
this.cleanCachedAccessories();
|
|
539
|
+
}, delaySec * 1000);
|
|
540
|
+
}
|
|
541
|
+
async processPendingDevices() {
|
|
542
|
+
log.debug(`[PROCESSING START] Processing ${this.pendingStations.length} stations and ${this.pendingDevices.length} devices`);
|
|
543
|
+
if (this.pendingStations.length === 0 || this.pendingDevices.length === 0) {
|
|
544
|
+
log.warn(`[DISCOVERY WARNING] Discovery finished with ${this.pendingStations.length} station(s) and ${this.pendingDevices.length} devices(s). ` +
|
|
545
|
+
'If this is unexpected, please verify your Eufy account has devices and the credentials used are for a guest admin account.');
|
|
546
|
+
}
|
|
547
|
+
// Build set of stations that have at least one device
|
|
548
|
+
const stationsWithDevices = new Set();
|
|
549
|
+
// Create all queued devices (they are already verified by bropat/eufy-security-client)
|
|
550
|
+
log.debug(`[DEVICES PROCESSING] Starting to process ${this.pendingDevices.length} devices`);
|
|
551
|
+
for (const device of this.pendingDevices) {
|
|
552
|
+
stationsWithDevices.add(device.getStationSerial());
|
|
553
|
+
try {
|
|
554
|
+
const deviceType = device.getDeviceType();
|
|
555
|
+
// Skip devices that the client declares as unsupported
|
|
556
|
+
if (!Device.isSupported(deviceType)) {
|
|
557
|
+
log.warn(`[DEVICE SKIP] "${device.getName()}" (type ${deviceType}) is unsupported by eufy-security-client — skipping accessory creation`);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
const deviceContainer = {
|
|
561
|
+
deviceIdentifier: {
|
|
562
|
+
uniqueId: device.getSerial(),
|
|
563
|
+
displayName: 'DEVICE ' + device.getName().replace(/[^a-zA-Z0-9]/g, ''),
|
|
564
|
+
type: deviceType,
|
|
565
|
+
},
|
|
566
|
+
eufyDevice: device,
|
|
567
|
+
};
|
|
568
|
+
log.debug(`[DEVICE] Processing: ${deviceContainer.deviceIdentifier.displayName}`);
|
|
569
|
+
await this.addOrUpdateAccessory(deviceContainer, false);
|
|
570
|
+
log.debug(`[DEVICE] Completed: ${deviceContainer.deviceIdentifier.displayName}`);
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
log.error(`[DEVICE ERROR] Error processing device "${device.getName()}": ${error}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
log.debug(`[DEVICES COMPLETE] All ${this.pendingDevices.length} devices processed`);
|
|
577
|
+
// Create queued stations that should be created
|
|
578
|
+
log.debug(`[STATIONS PROCESSING] Starting to process ${this.pendingStations.length} stations`);
|
|
579
|
+
let stationsSkipped = 0;
|
|
580
|
+
for (const station of this.pendingStations) {
|
|
581
|
+
const stationType = station.getDeviceType();
|
|
582
|
+
const stationSerial = station.getSerial();
|
|
583
|
+
// Hub/base stations (type 0, HB3, etc.) are always valid — they don't
|
|
584
|
+
// appear in DeviceProperties so Device.isSupported() returns false for them.
|
|
585
|
+
// For standalone cameras acting as their own station, fall back to Device.isSupported().
|
|
586
|
+
const isKnownStation = Device.isStation(stationType);
|
|
587
|
+
if (!isKnownStation && !Device.isSupported(stationType)) {
|
|
588
|
+
log.warn(`[STATION SKIP] "${station.getName()}" (type ${stationType}) is unsupported by eufy-security-client — skipping accessory creation`);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
// Create if: it's a hub/base station OR it has devices
|
|
592
|
+
const shouldCreate = isKnownStation || stationsWithDevices.has(stationSerial);
|
|
593
|
+
if (!shouldCreate) {
|
|
594
|
+
stationsSkipped++;
|
|
595
|
+
log.debug(`[STATION SKIP] "${station.getName()}" has no devices and will be skipped`);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
try {
|
|
599
|
+
const deviceContainer = {
|
|
600
|
+
deviceIdentifier: {
|
|
601
|
+
uniqueId: stationSerial,
|
|
602
|
+
displayName: 'STATION ' + station.getName().replace(/[^a-zA-Z0-9]/g, ''),
|
|
603
|
+
type: stationType,
|
|
604
|
+
},
|
|
605
|
+
eufyDevice: station,
|
|
606
|
+
};
|
|
607
|
+
log.debug(`[STATION] Processing: ${deviceContainer.deviceIdentifier.displayName}`);
|
|
608
|
+
await this.addOrUpdateAccessory(deviceContainer, true);
|
|
609
|
+
log.debug(`[STATION] Completed: ${deviceContainer.deviceIdentifier.displayName}`);
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
log.error(`[STATION ERROR] Error processing station "${station.getName()}": ${error}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
log.debug(`[STATIONS COMPLETE] ${this.pendingStations.length - stationsSkipped} stations created, ${stationsSkipped} skipped`);
|
|
616
|
+
// Persist the discovered accessories for the UI (immediate, no debounce)
|
|
617
|
+
this.accessoriesStore?.persistNow();
|
|
618
|
+
// Clear pending queues
|
|
619
|
+
this.pendingStations = [];
|
|
620
|
+
this.pendingDevices = [];
|
|
621
|
+
log.info(`[PROCESSING COMPLETE] All pending devices and stations processed`);
|
|
622
|
+
}
|
|
623
|
+
async pluginShutdown() {
|
|
624
|
+
// Ensure a single shutdown to prevent corruption of the persistent file.
|
|
625
|
+
// This also enables captcha through the GUI and prevents repeated captcha or 2FA prompts upon plugin restart.
|
|
626
|
+
if (this.already_shutdown) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
this.already_shutdown = true;
|
|
630
|
+
if (this.discoveryDebounceTimeout) {
|
|
631
|
+
clearTimeout(this.discoveryDebounceTimeout);
|
|
632
|
+
}
|
|
633
|
+
this.accessoriesStore?.cancelPending();
|
|
634
|
+
try {
|
|
635
|
+
if (this.eufyClient.isConnected()) {
|
|
636
|
+
this.eufyClient.close();
|
|
637
|
+
}
|
|
638
|
+
log.info('Finished shutdown!');
|
|
639
|
+
}
|
|
640
|
+
catch (e) {
|
|
641
|
+
log.error(`Error while shutdown: ${e}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* This function is invoked when homebridge restores cached accessories from disk at startup.
|
|
646
|
+
* It should be used to setup event handlers for characteristics and update respective values.
|
|
647
|
+
*/
|
|
648
|
+
configureAccessory(accessory) {
|
|
649
|
+
log.debug(`Loading accessory from cache: ${accessory.displayName}`);
|
|
650
|
+
// add the restored accessory to the accessories cache so we can track if it has already been registered
|
|
651
|
+
this.accessories.push(accessory);
|
|
652
|
+
}
|
|
653
|
+
cleanCachedAccessories() {
|
|
654
|
+
if (this.config.cleanCache) {
|
|
655
|
+
log.debug('Looking for old cached accessories that seem to be outdated...');
|
|
656
|
+
let num = 0;
|
|
657
|
+
const staleAccessories = this.accessories.filter((item) => {
|
|
658
|
+
return this.activeAccessoryIds.indexOf(item.UUID) === -1;
|
|
659
|
+
});
|
|
660
|
+
staleAccessories.forEach((staleAccessory) => {
|
|
661
|
+
log.info(`Removing cached accessory ${staleAccessory.UUID} ${staleAccessory.displayName}`);
|
|
662
|
+
num++;
|
|
663
|
+
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [staleAccessory]);
|
|
664
|
+
});
|
|
665
|
+
if (num > 0) {
|
|
666
|
+
log.info('Removed ' + num + ' cached accessories');
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
log.info('No outdated cached accessories found.');
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
register_station(accessory, container) {
|
|
674
|
+
log.debug(accessory.displayName + ' UUID:' + accessory.UUID);
|
|
675
|
+
const type = container.deviceIdentifier.type;
|
|
676
|
+
const station = container.eufyDevice;
|
|
677
|
+
if (!Device.isStation(type)) {
|
|
678
|
+
// Standalone Lock or Doorbell doesn't have Security Control
|
|
679
|
+
if (Device.isDoorbell(type) || Device.isLock(type)) {
|
|
680
|
+
throw new Error(`looks station but it's not could imply some errors! Type: ${type}. You can ignore this message.`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (this.config.autoSyncStation) {
|
|
684
|
+
new AutoSyncStationAccessory(this, accessory, station);
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
new StationAccessory(this, accessory, station);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
register_device(accessory, container) {
|
|
691
|
+
log.debug(accessory.displayName + ' UUID:' + accessory.UUID);
|
|
692
|
+
const device = container.eufyDevice;
|
|
693
|
+
const type = container.deviceIdentifier.type;
|
|
694
|
+
if (Device.isMotionSensor(type)) {
|
|
695
|
+
log.debug(accessory.displayName + ' isMotionSensor!');
|
|
696
|
+
new MotionSensorAccessory(this, accessory, device);
|
|
697
|
+
}
|
|
698
|
+
if (Device.isEntrySensor(type)) {
|
|
699
|
+
log.debug(accessory.displayName + ' isEntrySensor!');
|
|
700
|
+
new EntrySensorAccessory(this, accessory, device);
|
|
701
|
+
}
|
|
702
|
+
if (Device.isSmartDrop(type)) {
|
|
703
|
+
log.debug(accessory.displayName + ' isSmartDrop!');
|
|
704
|
+
new SmartDropAccessory(this, accessory, device);
|
|
705
|
+
}
|
|
706
|
+
if (Device.isLock(type)) {
|
|
707
|
+
log.debug(accessory.displayName + ' isLock!');
|
|
708
|
+
new LockAccessory(this, accessory, device);
|
|
709
|
+
}
|
|
710
|
+
if (Device.isCamera(type) || Device.isLockWifiVideo(type)) {
|
|
711
|
+
log.debug(accessory.displayName + ' isCamera!');
|
|
712
|
+
new CameraAccessory(this, accessory, device);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
//# sourceMappingURL=platform.js.map
|