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
JavaScript
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