UNPKG

hap-nodejs

Version:

HAP-NodeJS is a Node.js implementation of HomeKit Accessory Server.

243 lines 9.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ControllerStorage = void 0; const tslib_1 = require("tslib"); const util_1 = tslib_1.__importDefault(require("util")); const debug_1 = tslib_1.__importDefault(require("debug")); const HAPStorage_1 = require("./HAPStorage"); const debug = (0, debug_1.default)("HAP-NodeJS:ControllerStorage"); /** * @group Model */ class ControllerStorage { accessoryUUID; initialized = false; // ----- properties only set in parent storage object ------ username; fileCreated = false; purgeUnidentifiedAccessoryData = true; // --------------------------------------------------------- trackedControllers = []; // used to track controllers before data was loaded from disk controllerData = {}; restoredAccessories; // indexed by accessory UUID parent; linkedAccessories; queuedSaveTimeout; queuedSaveTime; constructor(accessory) { this.accessoryUUID = accessory.UUID; } enqueueSaveRequest(timeout = 0) { if (this.parent) { this.parent.enqueueSaveRequest(timeout); return; } const plannedTime = Date.now() + timeout; if (this.queuedSaveTimeout) { if (plannedTime <= (this.queuedSaveTime ?? 0)) { return; } clearTimeout(this.queuedSaveTimeout); } this.queuedSaveTimeout = setTimeout(() => { this.queuedSaveTimeout = this.queuedSaveTime = undefined; this.save(); }, timeout).unref(); this.queuedSaveTime = Date.now() + timeout; } /** * Links a bridged accessory to the ControllerStorage of the bridge accessory. * * @param accessory */ linkAccessory(accessory) { if (!this.linkedAccessories) { this.linkedAccessories = []; } const storage = accessory.controllerStorage; this.linkedAccessories.push(storage); storage.parent = this; const saved = this.restoredAccessories && this.restoredAccessories[accessory.UUID]; if (this.initialized) { storage.init(saved); } } trackController(controller) { controller.setupStateChangeDelegate(this.handleStateChange.bind(this, controller)); // setup delegate if (!this.initialized) { // track controller if data isn't loaded yet this.trackedControllers.push(controller); } else { this.restoreController(controller); } } untrackController(controller) { const index = this.trackedControllers.indexOf(controller); if (index !== -1) { // remove from trackedControllers if storage wasn't initialized yet this.trackedControllers.splice(index, 1); } controller.setupStateChangeDelegate(undefined); // remove association with this storage object this.purgeControllerData(controller); } purgeControllerData(controller) { delete this.controllerData[controller.controllerId()]; if (this.initialized) { this.enqueueSaveRequest(100); } } handleStateChange(controller) { const id = controller.controllerId(); const serialized = controller.serialize(); if (!serialized) { // can be undefined when controller wishes to delete data delete this.controllerData[id]; } else { const controllerData = this.controllerData[id]; if (!controllerData) { this.controllerData[id] = { data: serialized, }; } else { controllerData.data = serialized; } } if (this.initialized) { // only save if data was loaded // run save data "async", as handleStateChange call will probably always be caused by a http request // this should improve our response time this.enqueueSaveRequest(100); } } restoreController(controller) { if (!this.initialized) { throw new Error("Illegal state. Controller data wasn't loaded yet!"); } const controllerData = this.controllerData[controller.controllerId()]; if (controllerData) { try { controller.deserialize(controllerData.data); } catch (error) { console.warn(`Could not initialize controller of type '${controller.controllerId()}' from data stored on disk. Resetting to default: ${error.stack}`); controller.handleFactoryReset(); } controllerData.purgeOnNextLoad = undefined; } } /** * Called when this particular Storage object is feed with data loaded from disk. * This method is only called once. * * @param data - array of {@link StoredControllerData}. undefined if nothing was stored on disk for this particular storage object */ init(data) { if (this.initialized) { throw new Error(`ControllerStorage for accessory ${this.accessoryUUID} was already initialized!`); } this.initialized = true; // storing data into our local controllerData Record data && data.forEach(saved => this.controllerData[saved.type] = saved.controllerData); const restoredControllers = []; this.trackedControllers.forEach(controller => { this.restoreController(controller); restoredControllers.push(controller.controllerId()); }); this.trackedControllers.splice(0, this.trackedControllers.length); // clear tracking list let purgedData = false; Object.entries(this.controllerData).forEach(([id, data]) => { if (data.purgeOnNextLoad) { delete this.controllerData[id]; purgedData = true; return; } if (!restoredControllers.includes(id)) { data.purgeOnNextLoad = true; } }); if (purgedData) { this.enqueueSaveRequest(500); } } load(username) { if (this.username) { throw new Error("ControllerStorage was already loaded!"); } this.username = username; const key = ControllerStorage.persistKey(username); const saved = HAPStorage_1.HAPStorage.storage().getItem(key); let ownData; if (saved) { this.fileCreated = true; ownData = saved.accessories[this.accessoryUUID]; delete saved.accessories[this.accessoryUUID]; } this.init(ownData); if (this.linkedAccessories) { this.linkedAccessories.forEach(linkedStorage => { const savedData = saved && saved.accessories[linkedStorage.accessoryUUID]; linkedStorage.init(savedData); if (saved) { delete saved.accessories[linkedStorage.accessoryUUID]; } }); } if (saved && Object.keys(saved.accessories).length > 0) { if (!this.purgeUnidentifiedAccessoryData) { this.restoredAccessories = saved.accessories; // save data for controllers which aren't linked yet } else { debug("Purging unidentified controller data for bridge %s", username); } } } save() { if (this.parent) { this.parent.save(); return; } if (!this.initialized) { throw new Error("ControllerStorage has not yet been loaded!"); } if (!this.username) { throw new Error("Cannot save controllerData for a storage without a username!"); } const accessories = { [this.accessoryUUID]: this.controllerData, }; if (this.linkedAccessories) { // grab data from all linked storage objects this.linkedAccessories.forEach(accessory => accessories[accessory.accessoryUUID] = accessory.controllerData); } // TODO removed accessories won't ever be deleted? const accessoryData = this.restoredAccessories || {}; Object.entries(accessories).forEach(([uuid, controllerData]) => { const entries = Object.entries(controllerData); if (entries.length > 0) { accessoryData[uuid] = entries.map(([id, data]) => ({ type: id, controllerData: data, })); } }); const key = ControllerStorage.persistKey(this.username); if (Object.keys(accessoryData).length > 0) { const saved = { accessories: accessoryData, }; this.fileCreated = true; HAPStorage_1.HAPStorage.storage().setItemSync(key, saved); } else if (this.fileCreated) { this.fileCreated = false; HAPStorage_1.HAPStorage.storage().removeItemSync(key); } } static persistKey(username) { return util_1.default.format("ControllerStorage.%s.json", username.replace(/:/g, "").toUpperCase()); } static remove(username) { const key = ControllerStorage.persistKey(username); HAPStorage_1.HAPStorage.storage().removeItemSync(key); } } exports.ControllerStorage = ControllerStorage; //# sourceMappingURL=ControllerStorage.js.map