Files
SRS IT e8b365e5a7 feat: persist and cache all known devices; discovery only adds/updates
- Store.mergeDevices(): updates existing by UDN, adds new, keeps offline devices
- platform.js: merges discovered into cache; registers cached-offline devices
  in HomeKit so they remain visible; only removes truly orphaned accessories
- server.js: discover endpoint merges and returns full known device list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 22:28:11 -04:00

200 lines
6.9 KiB
JavaScript

'use strict';
/**
* WemoPlatform
*
* Homebridge platform plugin. Discovers Wemo devices via SSDP (and any
* manually-configured hosts), registers each as a Switch accessory, and
* runs the DWM local scheduler for time-based automation rules.
*/
const DwmStore = require('./store');
const wemoClient = require('./wemo-client');
const DwmScheduler = require('./scheduler');
const WemoSwitchAccessory = require('./accessory');
const PLUGIN_NAME = 'homebridge-dibby-wemo';
const PLATFORM_NAME = 'DibbyWemo';
class WemoPlatform {
/**
* @param {object} log - Homebridge logger
* @param {object} config - Platform config from config.json
* @param {object} api - Homebridge API
*/
constructor(log, config, api) {
this.log = log;
this.config = config ?? {};
this.api = api;
this._accessories = new Map(); // uuid → PlatformAccessory
this._handlers = new Map(); // uuid → WemoSwitchAccessory
// Store in Homebridge's user storage directory
this._store = new DwmStore(api.user.storagePath());
// Location is set via the custom UI settings panel (city search) and stored
// in the plugin's DwmStore — no raw lat/lng in config.json needed.
// DWM Scheduler
this._scheduler = new DwmScheduler({
store: this._store,
wemoClient,
log,
});
this._scheduler.onFire(({ success, msg }) => {
if (success) log.info('[DWM] ' + msg);
else log.warn('[DWM] ' + msg);
});
// Homebridge calls didFinishLaunching once the restore cache is ready
api.on('didFinishLaunching', () => {
this._discoverDevices();
this._scheduler.start().catch((e) => log.error('[DWM Scheduler] Start failed: ' + e.message));
});
log.info('DibbyWemo platform initialised');
}
// ── Homebridge lifecycle ──────────────────────────────────────────────────
/**
* Called for each accessory restored from cache on startup.
* We immediately attach handlers using the device context stored in the
* accessory so HomeKit requests don't time out during the SSDP window.
*/
configureAccessory(accessory) {
this.log.info('Restoring cached accessory: ' + accessory.displayName);
this._accessories.set(accessory.UUID, accessory);
// Re-attach handlers right away if we have saved device context
const device = accessory.context?.device;
if (device?.host && device?.port) {
const pollInterval = this.config.pollInterval ?? 30;
this._handlers.get(accessory.UUID)?.stopPolling();
const handler = new WemoSwitchAccessory({
platform: this,
accessory,
device,
wemoClient,
pollInterval,
});
this._handlers.set(accessory.UUID, handler);
}
}
// ── Discovery ─────────────────────────────────────────────────────────────
async _discoverDevices() {
const timeout = this.config.discoveryTimeout ?? 10_000;
const pollInterval = this.config.pollInterval ?? 30;
this.log.info('Starting Wemo device discovery…');
let discovered = [];
try {
discovered = await wemoClient.discoverDevices(timeout);
} catch (e) {
this.log.error('SSDP discovery failed: ' + e.message);
}
// Merge in manually-configured devices
const manual = (this.config.manualDevices ?? []).map(({ host, port }) => ({
host, port: port ?? 49153,
}));
for (const m of manual) {
if (!discovered.find((d) => d.host === m.host && d.port === m.port)) {
try {
const info = await wemoClient.getDeviceInfo(m.host, m.port);
discovered.push({ ...m, ...info });
} catch {
discovered.push(m);
}
}
}
this.log.info(`Found ${discovered.length} Wemo device(s)`);
// Merge discovered devices into the cached list — keep offline devices too
const freshForStore = discovered.map((d) => ({
host: d.host,
port: d.port,
udn: d.udn ?? `${d.host}:${d.port}`,
friendlyName: d.friendlyName ?? d.host,
productModel: d.productModel ?? 'Wemo Device',
firmwareVersion: d.firmwareVersion ?? null,
}));
const allKnown = this._store.mergeDevices(freshForStore);
// Register newly discovered devices in HomeKit
for (const device of discovered) {
this._registerDevice(device, pollInterval);
}
// Register previously cached devices that weren't discovered (may be offline)
// so HomeKit still knows about them
const discoveredUDNs = new Set(discovered.map((d) => d.udn ?? `${d.host}:${d.port}`));
for (const cached of allKnown) {
if (!discoveredUDNs.has(cached.udn)) {
this.log.info(`Device offline/not found during discovery, keeping cached: ${cached.friendlyName}`);
this._registerDevice(cached, pollInterval);
}
}
// Remove orphaned accessories — those with no device context at all
for (const [uuid, acc] of this._accessories) {
if (!acc.context?.device?.host) {
this.log.info('Removing orphaned accessory (no device context): ' + acc.displayName);
this._handlers.get(uuid)?.stopPolling();
this._handlers.delete(uuid);
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [acc]);
this._accessories.delete(uuid);
}
}
}
_uuidForDevice(device) {
const id = device.udn ?? `${device.host}:${device.port}`;
return this.api.hap.uuid.generate(id);
}
_registerDevice(device, pollInterval) {
const uuid = this._uuidForDevice(device);
const name = device.friendlyName ?? device.host;
let accessory = this._accessories.get(uuid);
if (!accessory) {
this.log.info('Adding new accessory: ' + name);
accessory = new this.api.platformAccessory(name, uuid);
this._accessories.set(uuid, accessory);
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
} else {
accessory.displayName = name;
}
// Persist device connection info so configureAccessory can restore it on
// the next restart without waiting for SSDP to complete.
accessory.context.device = {
host: device.host,
port: device.port,
udn: device.udn ?? `${device.host}:${device.port}`,
friendlyName: device.friendlyName ?? device.host,
productModel: device.productModel ?? 'Wemo Device',
firmwareVersion: device.firmwareVersion ?? null,
};
// (Re)create handler so device info is up to date
this._handlers.get(uuid)?.stopPolling();
const handler = new WemoSwitchAccessory({
platform: this,
accessory,
device,
wemoClient,
pollInterval,
});
this._handlers.set(uuid, handler);
}
}
module.exports = { WemoPlatform, PLUGIN_NAME, PLATFORM_NAME };