/* eslint @typescript-eslint/no-var-requires: "off" */ import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic, } from 'homebridge'; import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; import { EufySecurityPlatformConfig } from './config'; import { DeviceIdentifier, DeviceContainer } from './interfaces'; import { StationAccessory } from './accessories/StationAccessory'; import { EntrySensorAccessory } from './accessories/EntrySensorAccessory'; import { MotionSensorAccessory } from './accessories/MotionSensorAccessory'; import { CameraAccessory } from './accessories/CameraAccessory'; import { DoorbellCameraAccessory } from './accessories/DoorbellCameraAccessory'; import { KeypadAccessory } from './accessories/KeypadAccessory'; import { SmartLockAccessory } from './accessories/SmartLockAccessory'; import { EufySecurity, EufySecurityConfig, DeviceType, Station, EntrySensor, MotionSensor, Camera, DoorbellCamera, Keypad, Lock, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore } from 'eufy-security-client'; import bunyan from 'bunyan'; import bunyanDebugStream from 'bunyan-debug-stream'; export class EufySecurityPlatform implements DynamicPlatformPlugin { public readonly Service: typeof Service = this.api.hap.Service; public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic; public eufyClient: EufySecurity; // this is used to track restored cached accessories public readonly accessories: PlatformAccessory[] = []; public readonly config: EufySecurityPlatformConfig; private eufyConfig: EufySecurityConfig; public log; constructor( public readonly hblog: Logger, config: PlatformConfig, public readonly api: API, ) { this.config = config as EufySecurityPlatformConfig; this.eufyConfig = { username: this.config.username, password: this.config.password, country: 'US', language: 'en', persistentDir: api.user.storagePath(), p2pConnectionSetup: 0, pollingIntervalMinutes: this.config.pollingIntervalMinutes ?? 10, eventDurationSeconds: 10, } as EufySecurityConfig; this.config.ignoreStations = this.config.ignoreStations || []; this.config.ignoreDevices = this.config.ignoreDevices || []; if (this.config.enableDetailedLogging >= 1) { const plugin = require('../package.json'); this.log = bunyan.createLogger({ name: '[EufySecurity-' + plugin.version + ']', hostname: '', streams: [{ level: (this.config.enableDetailedLogging === 2) ? 'trace' : 'debug', type: 'raw', stream: bunyanDebugStream({ forceColor: true, showProcess: false, showPid: false, showDate: (time) => { return '[' + time.toLocaleString('en-US') + ']'; }, }), }], serializers: bunyanDebugStream.serializers, }); this.log.info('Eufy Security Plugin: enableDetailedLogging on'); } else { this.log = hblog; } this.eufyClient = (this.config.enableDetailedLogging === 2) ? new EufySecurity(this.eufyConfig, this.log) : new EufySecurity(this.eufyConfig); // Removing the ability to set Off(6) waiting Bropat feedback bropat/eufy-security-client#27 this.config.hkOff = (this.config.hkOff === 6) ? 63 : this.config.hkOff; // When this event is fired it means Homebridge has restored all cached accessories from disk. // Dynamic Platform plugins should only register new accessories after this event was fired, // in order to ensure they weren't added to homebridge already. This event can also be used // to start discovery of new accessories. this.api.on('didFinishLaunching', async () => { // await this.createConnection(); // run the method to discover / register your devices as accessories await this.discoverDevices(); }); this.log.info('Finished initializing Eufy Security Platform'); } /** * This function is invoked when homebridge restores cached accessories from disk at startup. * It should be used to setup event handlers for characteristics and update respective values. */ configureAccessory(accessory: PlatformAccessory) { this.log.debug('Loading accessory from cache:', accessory.displayName); // add the restored accessory to the accessories cache so we can track if it has already been registered this.accessories.push(accessory); } async discoverDevices() { this.log.debug('discoveringDevices'); try { await this.eufyClient.connect(); this.log.debug('EufyClient connected ' + this.eufyClient.isConnected()); } catch (e) { this.log.error('Error authenticating Eufy : ', e); } if (!this.eufyClient.isConnected()) { this.log.error('Not connected can\'t continue!'); return; } await this.refreshData(this.eufyClient); this.eufyClient.on('push connect', () => { this.log.debug('Push Connected!'); }); this.eufyClient.on('push close', () => { this.log.warn('Push Closed!'); }); const eufyStations = await this.eufyClient.getStations(); this.log.debug('Found ' + eufyStations.length + ' stations.'); const devices: Array<DeviceContainer> = []; for (const station of eufyStations) { this.log.debug( 'Found Station', station.getSerial(), station.getName(), DeviceType[station.getDeviceType()], station.getLANIPAddress(), ); if (this.config.ignoreStations.indexOf(station.getSerial()) !== -1) { this.log.debug('Device ignored'); continue; } const deviceContainer: DeviceContainer = { deviceIdentifier: { uniqueId: station.getSerial(), displayName: station.getName(), type: station.getDeviceType(), station: true, } as DeviceIdentifier, eufyDevice: station, }; devices.push(deviceContainer); } const eufyDevices = await this.eufyClient.getDevices(); this.log.debug('Found ' + eufyDevices.length + ' devices.'); for (const device of eufyDevices) { this.log.debug( 'Found device', device.getSerial(), device.getName(), DeviceType[device.getDeviceType()], ); if (this.config.ignoreStations.indexOf(device.getStationSerial()) !== -1) { this.log.debug('Device ignored because station is ignored'); continue; } if (this.config.ignoreDevices.indexOf(device.getSerial()) !== -1) { this.log.debug('Device ignored'); continue; } const deviceContainer: DeviceContainer = { deviceIdentifier: { uniqueId: device.getSerial(), displayName: device.getName(), type: device.getDeviceType(), station: false, } as DeviceIdentifier, eufyDevice: device, }; devices.push(deviceContainer); } const activeAccessoryIds: string[] = []; // loop over the discovered devices and register each one if it has not already been registered for (const device of devices) { // generate a unique id for the accessory this should be generated from // something globally unique, but constant, for example, the device serial // number or MAC address let uuid = this.api.hap.uuid.generate(device.deviceIdentifier.uniqueId); // Checking Device Type if it's not a station, it will be the same serial number we will find // in Device list and it will create the same UUID if (device.deviceIdentifier.type !== DeviceType.STATION && device.deviceIdentifier.station) { uuid = this.api.hap.uuid.generate('s_' + device.deviceIdentifier.uniqueId); this.log.debug('This device is not a station. Generating a new UUID to avoid any duplicate issue'); } activeAccessoryIds.push(uuid); // see if an accessory with the same uuid has already been registered and restored from // the cached devices we stored in the `configureAccessory` method above const existingAccessory = this.accessories.find( (accessory) => accessory.UUID === uuid, ); if (existingAccessory) { // the accessory already exists if (this.register_accessory( existingAccessory, device.deviceIdentifier.type, device.eufyDevice, device.deviceIdentifier.station, ) ) { this.log.info( 'Restoring existing accessory from cache:', existingAccessory.displayName, ); // update accessory cache with any changes to the accessory details and information this.api.updatePlatformAccessories([existingAccessory]); } } else { // the accessory does not yet exist, so we need to create it // create a new accessory const accessory = new this.api.platformAccessory( device.deviceIdentifier.displayName, uuid, ); // store a copy of the device object in the `accessory.context` // the `context` property can be used to store any data about the accessory you may need accessory.context.device = device.deviceIdentifier; // create the accessory handler for the newly create accessory // this is imported from `platformAccessory.ts` if ( this.register_accessory( accessory, device.deviceIdentifier.type, device.eufyDevice, device.deviceIdentifier.station, ) ) { this.log.info( 'Adding new accessory:', device.deviceIdentifier.displayName, ); // link the accessory to your platform this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [ accessory, ]); } } } // Cleaning cached accessory which are no longer exist const staleAccessories = this.accessories.filter((item) => { return activeAccessoryIds.indexOf(item.UUID) === -1; }); staleAccessories.forEach((staleAccessory) => { this.log.info(`Removing cached accessory ${staleAccessory.UUID} ${staleAccessory.displayName}`); this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [staleAccessory]); }); } private register_accessory( accessory: PlatformAccessory, type: number, device, station: boolean, ) { this.log.debug(accessory.displayName, 'UUID:', accessory.UUID); /* Under development area This need to be rewrite */ if (station) { if (type !== DeviceType.STATION) { // Allowing camera but not the lock nor doorbell for now if (!(type === DeviceType.LOCK_BASIC || type === DeviceType.LOCK_ADVANCED || type === DeviceType.LOCK_BASIC_NO_FINGER || type === DeviceType.LOCK_ADVANCED_NO_FINGER || type === DeviceType.DOORBELL || type === DeviceType.BATTERY_DOORBELL || type === DeviceType.BATTERY_DOORBELL_2)) { this.log.warn(accessory.displayName, 'looks station but it\'s not could imply some errors', 'Type:', type); new StationAccessory(this, accessory, device as Station); return true; } else { return false; } } } switch (type) { case DeviceType.STATION: new StationAccessory(this, accessory, device as Station); break; case DeviceType.MOTION_SENSOR: new MotionSensorAccessory(this, accessory, device as MotionSensor); break; case DeviceType.CAMERA: case DeviceType.CAMERA2: case DeviceType.CAMERA_E: case DeviceType.CAMERA2C: case DeviceType.INDOOR_CAMERA: case DeviceType.INDOOR_PT_CAMERA: case DeviceType.FLOODLIGHT: case DeviceType.CAMERA2C_PRO: case DeviceType.CAMERA2_PRO: case DeviceType.INDOOR_CAMERA_1080: case DeviceType.INDOOR_PT_CAMERA_1080: case DeviceType.SOLO_CAMERA: case DeviceType.SOLO_CAMERA_PRO: case DeviceType.SOLO_CAMERA_SPOTLIGHT_1080: case DeviceType.SOLO_CAMERA_SPOTLIGHT_2K: case DeviceType.SOLO_CAMERA_SPOTLIGHT_SOLAR: case DeviceType.INDOOR_OUTDOOR_CAMERA_1080P: case DeviceType.INDOOR_OUTDOOR_CAMERA_1080P_NO_LIGHT: case DeviceType.INDOOR_OUTDOOR_CAMERA_2K: case DeviceType.FLOODLIGHT_CAMERA_8422: case DeviceType.FLOODLIGHT_CAMERA_8423: case DeviceType.FLOODLIGHT_CAMERA_8424: new CameraAccessory(this, accessory, device as Camera); break; case DeviceType.DOORBELL: case DeviceType.BATTERY_DOORBELL: case DeviceType.BATTERY_DOORBELL_2: new DoorbellCameraAccessory(this, accessory, device as DoorbellCamera); break; case DeviceType.SENSOR: new EntrySensorAccessory(this, accessory, device as EntrySensor); break; case DeviceType.KEYPAD: new KeypadAccessory(this, accessory, device as Keypad); break; case DeviceType.LOCK_BASIC: case DeviceType.LOCK_ADVANCED: case DeviceType.LOCK_BASIC_NO_FINGER: case DeviceType.LOCK_ADVANCED_NO_FINGER: new SmartLockAccessory(this, accessory, device as Lock); break; default: this.log.warn( 'This accessory is not compatible with HomeBridge Eufy Security plugin:', accessory.displayName, 'Type:', type, ); return false; } return true; } public async refreshData(client: EufySecurity): Promise<void> { this.log.debug( `PollingInterval: ${this.eufyConfig.pollingIntervalMinutes}`, ); if (client) { this.log.debug('Refresh data from cloud and schedule next refresh.'); try { await client.refreshCloudData(); } catch (error) { this.log.error('Error refreshing data from Eufy: ', error); } setTimeout(() => { try { this.refreshData(client); } catch (error) { this.log.error('Error refreshing data from Eufy: ', error); } }, this.eufyConfig.pollingIntervalMinutes * 60 * 1000); } } public getStationById(id: string) { return this.eufyClient.getStation(id); } }