e8b365e5a7
- 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>
146 lines
5.1 KiB
JavaScript
146 lines
5.1 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* DWM Store — Homebridge edition.
|
|
*
|
|
* Stores devices, DWM rules, and location in a single JSON file inside
|
|
* Homebridge's storagePath (passed in at construction time, not via Electron).
|
|
*
|
|
* Schema mirrors the desktop store exactly so DWM rules created in the desktop
|
|
* app can be imported / shared.
|
|
*/
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const DEFAULTS = {
|
|
location: null,
|
|
devices: [],
|
|
deviceGroups: [],
|
|
deviceOrder: [],
|
|
disabledRules: {},
|
|
dwmRules: [],
|
|
schedulerHeartbeat: null,
|
|
};
|
|
|
|
class DwmStore {
|
|
constructor(storagePath) {
|
|
this._filePath = path.join(storagePath, 'dibby-wemo.json');
|
|
}
|
|
|
|
// ── Internal I/O ──────────────────────────────────────────────────────────
|
|
|
|
_load() {
|
|
try {
|
|
return { ...DEFAULTS, ...JSON.parse(fs.readFileSync(this._filePath, 'utf8')) };
|
|
} catch {
|
|
return { ...DEFAULTS };
|
|
}
|
|
}
|
|
|
|
_save(data) {
|
|
fs.writeFileSync(this._filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
}
|
|
|
|
// ── Location ──────────────────────────────────────────────────────────────
|
|
|
|
getLocation() { return this._load().location; }
|
|
setLocation(loc) { const d = this._load(); d.location = loc; this._save(d); }
|
|
|
|
// ── Devices ───────────────────────────────────────────────────────────────
|
|
|
|
getDevices() { return this._load().devices ?? []; }
|
|
saveDevices(list) { const d = this._load(); d.devices = list; this._save(d); }
|
|
getDeviceOrder() { return this._load().deviceOrder ?? []; }
|
|
saveDeviceOrder(order) { const d = this._load(); d.deviceOrder = order; this._save(d); }
|
|
getDeviceGroups() { return this._load().deviceGroups ?? []; }
|
|
saveDeviceGroups(groups) { const d = this._load(); d.deviceGroups = groups; this._save(d); }
|
|
|
|
/**
|
|
* Merge freshly discovered devices into the cached list.
|
|
* - Existing devices are updated with fresh data (host/port/name/firmware).
|
|
* - Previously cached devices NOT in the new scan are kept as-is (offline ≠ removed).
|
|
* - Newly found devices are appended.
|
|
* Returns the merged list.
|
|
*/
|
|
mergeDevices(fresh) {
|
|
const d = this._load();
|
|
const cached = d.devices ?? [];
|
|
const byUdn = new Map(cached.map((dev) => [dev.udn, dev]));
|
|
|
|
for (const f of fresh) {
|
|
const udn = f.udn ?? `${f.host}:${f.port}`;
|
|
if (byUdn.has(udn)) {
|
|
// Update existing entry with latest network data
|
|
byUdn.set(udn, { ...byUdn.get(udn), ...f, udn });
|
|
} else {
|
|
byUdn.set(udn, { ...f, udn });
|
|
}
|
|
}
|
|
|
|
d.devices = Array.from(byUdn.values());
|
|
this._save(d);
|
|
return d.devices;
|
|
}
|
|
|
|
// ── Disabled-rule backups ─────────────────────────────────────────────────
|
|
|
|
getDisabledRules() { return this._load().disabledRules ?? {}; }
|
|
setDisabledRule(key, ruleDevicesRows) {
|
|
const d = this._load();
|
|
if (!d.disabledRules) d.disabledRules = {};
|
|
d.disabledRules[key] = ruleDevicesRows;
|
|
this._save(d);
|
|
}
|
|
clearDisabledRule(key) {
|
|
const d = this._load();
|
|
if (!d.disabledRules) return;
|
|
delete d.disabledRules[key];
|
|
this._save(d);
|
|
}
|
|
|
|
// ── DWM Rules ─────────────────────────────────────────────────────────────
|
|
|
|
getDwmRules() { return this._load().dwmRules ?? []; }
|
|
|
|
createDwmRule(rule) {
|
|
const d = this._load();
|
|
if (!d.dwmRules) d.dwmRules = [];
|
|
const id = `dwm-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
const now = new Date().toISOString();
|
|
const newRule = { ...rule, id, createdAt: now, updatedAt: now };
|
|
d.dwmRules.push(newRule);
|
|
this._save(d);
|
|
return newRule;
|
|
}
|
|
|
|
updateDwmRule(id, updates) {
|
|
const d = this._load();
|
|
if (!d.dwmRules) d.dwmRules = [];
|
|
const idx = d.dwmRules.findIndex((r) => r.id === id);
|
|
if (idx === -1) throw new Error(`DWM rule not found: ${id}`);
|
|
d.dwmRules[idx] = { ...d.dwmRules[idx], ...updates, id, updatedAt: new Date().toISOString() };
|
|
this._save(d);
|
|
return d.dwmRules[idx];
|
|
}
|
|
|
|
deleteDwmRule(id) {
|
|
const d = this._load();
|
|
if (!d.dwmRules) return;
|
|
d.dwmRules = d.dwmRules.filter((r) => r.id !== id);
|
|
this._save(d);
|
|
}
|
|
|
|
// ── Scheduler heartbeat ───────────────────────────────────────────────────
|
|
|
|
getHeartbeat() { return this._load().schedulerHeartbeat ?? null; }
|
|
|
|
saveHeartbeat(hb) {
|
|
const d = this._load();
|
|
d.schedulerHeartbeat = { ...hb, ts: new Date().toISOString() };
|
|
this._save(d);
|
|
}
|
|
}
|
|
|
|
module.exports = DwmStore;
|