UNPKG

homebridge-eufy-security

Version:
206 lines 8.87 kB
import { Device, PropertyName, CommandName, DeviceType, } from 'eufy-security-client'; import fs from 'fs'; import { log } from './utils.js'; import { LIB_VERSION } from '../version.js'; /** Seconds between heartbeat writes so the UI can detect a running plugin. */ const HEARTBEAT_SEC = 60; /** * Manages the accessories.json file that the Eufy Plugin UI reads. * * Builds the station/device tree from live eufy-security-client objects * and writes it to disk. A periodic heartbeat keeps `storedAt` fresh so * the UI can detect whether the plugin is still running. */ export class AccessoriesStore { eufyClient; config; storagePath; heartbeatInterval; constructor(eufyClient, config, storagePath) { this.eufyClient = eufyClient; this.config = config; this.storagePath = storagePath; } /** Write accessories.json immediately and start the heartbeat. */ async persistNow() { this.cancelPending(); await this.persist(); this.startHeartbeat(); } /** Stop the heartbeat. */ cancelPending() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = undefined; } } /** Start a periodic heartbeat that rewrites accessories.json every HEARTBEAT_SEC seconds. */ startHeartbeat() { if (this.heartbeatInterval) { return; // already running } this.heartbeatInterval = setInterval(() => this.persist(), HEARTBEAT_SEC * 1000); } // --------------------------------------------------------------------------- // Internal // --------------------------------------------------------------------------- async persist() { try { if (!this.eufyClient?.isConnected?.()) { return; } const [stations, devices] = await Promise.all([ this.eufyClient.getStations(), this.eufyClient.getDevices(), ]); // Group devices by their parent station serial const devicesByStation = new Map(); for (const device of devices) { const stationSN = device.getStationSerial(); if (!devicesByStation.has(stationSN)) { devicesByStation.set(stationSN, []); } devicesByStation.get(stationSN).push(device); } const storedStations = []; for (const station of stations) { const stationType = station.getDeviceType(); const stationSerial = station.getSerial(); const isKnownStation = Device.isStation(stationType); const stationRecord = { uniqueId: stationSerial, displayName: station.getName(), type: stationType, typename: DeviceType[stationType], disabled: false, devices: [], properties: { ...station.getProperties() }, unsupported: false, ignored: (this.config.ignoreStations ?? []).includes(stationSerial), power: computePower(station.getProperties()), }; try { delete stationRecord.properties.picture; } catch { /* ignore */ } if (!isKnownStation) { if (!Device.isSupported(stationType)) { stationRecord.unsupported = true; } else { const hasMatchingDevice = devicesByStation.has(stationSerial); if (hasMatchingDevice) { stationRecord.standalone = true; stationRecord.disabled = true; if (Device.isLock(stationType) || Device.isDoorbell(stationType) || Device.isSmartDrop(stationType)) { stationRecord.noSecurityControl = true; } } else { stationRecord.unsupported = true; } } } // Attach devices to this station const stationDevices = devicesByStation.get(stationSerial) ?? []; for (const device of stationDevices) { const devType = device.getDeviceType(); const deviceRecord = { uniqueId: device.getSerial(), displayName: device.getName(), type: devType, typename: DeviceType[devType], standalone: device.getSerial() === device.getStationSerial(), hasBattery: device.hasBattery(), isCamera: device.isCamera() || Device.isLockWifiVideo(devType), isDoorbell: device.isDoorbell(), isKeypad: device.isKeyPad(), isMotionSensor: Device.isMotionSensor(devType), isEntrySensor: Device.isEntrySensor(devType), isLock: Device.isLock(devType), isSmartDrop: Device.isSmartDrop(devType), supportsRTSP: device.hasPropertyValue(PropertyName.DeviceRTSPStream), supportsTalkback: device.hasCommand(CommandName.DeviceStartTalkback), DeviceEnabled: device.hasProperty(PropertyName.DeviceEnabled), DeviceMotionDetection: device.hasProperty(PropertyName.DeviceMotionDetection), DeviceLight: device.hasProperty(PropertyName.DeviceLight), DeviceChimeIndoor: device.hasProperty(PropertyName.DeviceChimeIndoor), disabled: false, properties: { ...device.getProperties() }, unsupported: !Device.isSupported(devType), ignored: (this.config.ignoreDevices ?? []).includes(device.getSerial()), power: computePower(device.getProperties()), }; try { delete deviceRecord.properties.picture; } catch { /* ignore */ } if (stationRecord.unsupported) { deviceRecord.unsupported = true; } stationRecord.devices.push(deviceRecord); } storedStations.push(stationRecord); } if (!fs.existsSync(this.storagePath)) { fs.mkdirSync(this.storagePath, { recursive: true }); } const filePath = this.storagePath + '/accessories.json'; const data = { version: LIB_VERSION, storedAt: new Date().toISOString(), stations: storedStations }; fs.writeFileSync(filePath, JSON.stringify(data)); log.debug(`Persisted ${storedStations.length} station(s) to accessories.json`); } catch (error) { log.error(`Failed to write accessories.json: ${error}`); } } } /** * Compute a unified power descriptor from a properties object. * Works for both devices and stations. */ function computePower(props) { const power = { source: null, icon: null, label: null }; if (props.battery !== undefined) { power.battery = props.battery; } else if (props.batteryLow !== undefined) { power.batteryLow = props.batteryLow; } if (props.chargingStatus !== undefined) { const cs = props.chargingStatus; const isSolar = ((cs >> 2) & 1) === 1; const isPlugSolar = ((cs >> 3) & 1) === 1; const isUsb = (cs & 1) === 1; if (isSolar || isPlugSolar) { power.source = 'solar'; power.icon = 'solar_power.svg'; power.label = 'Solar Charging'; return power; } if (isUsb) { power.source = 'plugged'; power.icon = 'bolt.svg'; power.label = 'Charging'; return power; } } if (props.powerSource === 1) { power.source = 'solar'; power.icon = 'solar_power.svg'; power.label = 'Solar'; } else if (props.powerSource === 0) { power.source = 'battery'; } else if (power.battery === undefined && power.batteryLow === undefined) { power.source = 'plugged'; power.icon = 'bolt.svg'; power.label = 'Plugged In'; } else { power.source = 'battery'; } return power; } //# sourceMappingURL=accessoriesStore.js.map