UNPKG

homebridge-sense-energy-monitor

Version:

Enhanced Homebridge plugin for Sense Home Energy Monitor with comprehensive API integration and real-time monitoring

219 lines 8.33 kB
import { normalizeConfig } from './config.js'; import { EnergyMonitorAccessory } from './energyMonitorAccessory.js'; import { SenseApi } from './senseApi.js'; import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'; // Never change this seed: it keys the accessory in the Homebridge cache, and // keeping it lets v2.x installs upgrade without losing their accessory. const MAIN_ACCESSORY_SEED = 'sense-main-monitor-v2'; const INIT_RETRY_MS = 2 * 60 * 1000; const STATUS_LOG_INTERVAL_MS = 30 * 1000; export class SenseEnergyMonitorPlatform { log; api; accessories = new Map(); pluginConfig; senseApi = null; fakeGatoService = null; accessoryHandler = null; pollingTimer = null; deviceLogTimer = null; initRetryTimer = null; lastStatusLogAt = 0; shuttingDown = false; constructor(log, config, api) { this.log = log; this.api = api; const { config: normalized, errors } = normalizeConfig(config); this.pluginConfig = normalized; if (!normalized) { for (const error of errors) { this.log.error(`Configuration error: ${error}`); } this.log.error('Plugin not configured correctly. Please check your configuration.'); return; } this.api.on('didFinishLaunching', () => { void this.initialize(); }); this.api.on('shutdown', () => { this.shuttingDown = true; this.cleanup(); }); } configureAccessory(accessory) { this.log.debug(`Restoring cached accessory: ${accessory.displayName}`); this.accessories.set(accessory.UUID, accessory); } async initialize() { const config = this.pluginConfig; if (!config) { return; } try { await this.loadFakeGato(); this.senseApi = new SenseApi({ log: this.makeApiLogger(), username: config.username, password: config.password, monitorId: config.monitorId, mfaEnabled: config.mfaEnabled, mfaSecret: config.mfaSecret, storagePath: this.api.user.storagePath(), }); this.wireApiEvents(this.senseApi); if (!this.senseApi.isAuthenticated) { await this.senseApi.authenticate(); } // Validates a cached token (self-healing on 401). await this.senseApi.validateSession(); if (this.shuttingDown) { this.senseApi.destroy(); return; } this.discoverAccessories(); if (config.useWebSocket) { this.senseApi.openStream(); } this.startPeriodicUpdates(); } catch (error) { this.senseApi?.destroy(); this.senseApi = null; if (this.shuttingDown) { return; } this.log.error(`Failed to initialize platform: ${error.message}`); this.log.info(`Retrying initialization in ${INIT_RETRY_MS / 60000} minutes`); this.initRetryTimer = setTimeout(() => { this.initRetryTimer = null; void this.initialize(); }, INIT_RETRY_MS); } } async loadFakeGato() { if (!this.pluginConfig?.enableHistory || this.fakeGatoService) { return; } try { const fakegato = (await import('fakegato-history')).default; this.fakeGatoService = fakegato(this.api); } catch { this.log.debug('fakegato-history not available; Eve history disabled'); } } wireApiEvents(senseApi) { senseApi.on('authenticated', () => { this.log.info('Sense API authenticated successfully'); }); senseApi.on('authentication_failed', (error) => { this.log.error(`Sense authentication failed: ${error.message}`); }); senseApi.on('data', (data) => { this.accessoryHandler?.update(data); this.logStatus(data); }); senseApi.on('trend_update', (trends) => { this.log.debug(`Daily trends: ${trends.dailyUsageKwh.toFixed(2)} kWh used, ${trends.dailyProductionKwh.toFixed(2)} kWh produced`); }); } discoverAccessories() { const config = this.pluginConfig; if (!config) { return; } const uuid = this.api.hap.uuid.generate(MAIN_ACCESSORY_SEED); let accessory = this.accessories.get(uuid); if (accessory) { this.log.info(`Restoring cached accessory: ${accessory.displayName}`); this.accessoryHandler = new EnergyMonitorAccessory(this, accessory); this.api.updatePlatformAccessories([accessory]); } else { this.log.info(`Creating accessory: ${config.name}`); accessory = new this.api.platformAccessory(config.name, uuid); this.accessoryHandler = new EnergyMonitorAccessory(this, accessory); this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); this.accessories.set(uuid, accessory); } // Unregister only orphans (e.g. leftovers from v2.x experiments). const orphans = [...this.accessories.values()].filter((cached) => cached.UUID !== uuid); if (orphans.length > 0) { this.log.info(`Removing ${orphans.length} orphaned cached accessorie(s)`); this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, orphans); for (const orphan of orphans) { this.accessories.delete(orphan.UUID); } } } startPeriodicUpdates() { const config = this.pluginConfig; const senseApi = this.senseApi; if (!config || !senseApi) { return; } this.pollingTimer = setInterval(() => { void (async () => { try { await senseApi.updateTrends(); if (!config.useWebSocket) { await senseApi.updateRealtime(); } } catch (error) { this.log.warn(`Periodic update failed: ${error.message}`); } })(); }, config.pollingIntervalMs); if (config.verbose) { this.deviceLogTimer = setInterval(() => { const devices = senseApi.realtime.devices; if (devices.length > 0) { this.log.info(`Active devices: ${devices.map((d) => `${d.name}(${d.power}W)`).join(', ')}`); } }, config.deviceLoggingIntervalMs); } } logStatus(data) { if (!this.pluginConfig?.verbose) { return; } const now = Date.now(); if (now - this.lastStatusLogAt >= STATUS_LOG_INTERVAL_MS) { this.lastStatusLogAt = now; this.log.info(`Power: ${data.power}W, Solar: ${data.solarPower}W, Active devices: ${data.devices.length}`); } } /** When verbose is on, surface the API client's debug logging as info. */ makeApiLogger() { if (!this.pluginConfig?.verbose) { return this.log; } const base = this.log; const wrapped = (message, ...parameters) => { base.info(message, ...parameters); }; return Object.assign(wrapped, { prefix: base.prefix, info: base.info.bind(base), warn: base.warn.bind(base), error: base.error.bind(base), debug: base.info.bind(base), log: base.log.bind(base), success: base.success.bind(base), }); } cleanup() { this.log.info('Shutting down: cleaning up timers and connections'); for (const timer of [this.pollingTimer, this.deviceLogTimer, this.initRetryTimer]) { if (timer) { clearTimeout(timer); } } this.pollingTimer = null; this.deviceLogTimer = null; this.initRetryTimer = null; this.senseApi?.destroy(); } } //# sourceMappingURL=platform.js.map