UNPKG

homebridge-plugin-wrapper

Version:

Wrapper for Homebridge and NodeJS-HAP with reduced dependencies that allows to intercept plugin values and also send to them

962 lines 109 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Accessory = exports.AccessoryEventTypes = exports.MDNSAdvertiser = exports.CharacteristicWarningType = exports.Categories = void 0; var tslib_1 = require("tslib"); var assert_1 = (0, tslib_1.__importDefault)(require("assert")); var crypto_1 = (0, tslib_1.__importDefault)(require("crypto")); var debug_1 = (0, tslib_1.__importDefault)(require("debug")); var events_1 = require("events"); var net_1 = (0, tslib_1.__importDefault)(require("net")); var Advertiser_1 = require("./Advertiser"); // noinspection JSDeprecatedSymbols var camera_1 = require("./camera"); var Characteristic_1 = require("./Characteristic"); var controller_1 = require("./controller"); var HAPServer_1 = require("./HAPServer"); var AccessoryInfo_1 = require("./model/AccessoryInfo"); var ControllerStorage_1 = require("./model/ControllerStorage"); var IdentifierCache_1 = require("./model/IdentifierCache"); var Service_1 = require("./Service"); var clone_1 = require("./util/clone"); var request_util_1 = require("./util/request-util"); var uuid = (0, tslib_1.__importStar)(require("./util/uuid")); var uuid_1 = require("./util/uuid"); var debug = (0, debug_1.default)("HAP-NodeJS:Accessory"); var MAX_ACCESSORIES = 1000000; // Maximum number of bridged accessories per bridge. var MAX_SERVICES = 1000000; // Known category values. Category is a hint to iOS clients about what "type" of Accessory this represents, for UI only. var Categories; (function (Categories) { // noinspection JSUnusedGlobalSymbols Categories[Categories["OTHER"] = 1] = "OTHER"; Categories[Categories["BRIDGE"] = 2] = "BRIDGE"; Categories[Categories["FAN"] = 3] = "FAN"; Categories[Categories["GARAGE_DOOR_OPENER"] = 4] = "GARAGE_DOOR_OPENER"; Categories[Categories["LIGHTBULB"] = 5] = "LIGHTBULB"; Categories[Categories["DOOR_LOCK"] = 6] = "DOOR_LOCK"; Categories[Categories["OUTLET"] = 7] = "OUTLET"; Categories[Categories["SWITCH"] = 8] = "SWITCH"; Categories[Categories["THERMOSTAT"] = 9] = "THERMOSTAT"; Categories[Categories["SENSOR"] = 10] = "SENSOR"; Categories[Categories["ALARM_SYSTEM"] = 11] = "ALARM_SYSTEM"; Categories[Categories["SECURITY_SYSTEM"] = 11] = "SECURITY_SYSTEM"; Categories[Categories["DOOR"] = 12] = "DOOR"; Categories[Categories["WINDOW"] = 13] = "WINDOW"; Categories[Categories["WINDOW_COVERING"] = 14] = "WINDOW_COVERING"; Categories[Categories["PROGRAMMABLE_SWITCH"] = 15] = "PROGRAMMABLE_SWITCH"; Categories[Categories["RANGE_EXTENDER"] = 16] = "RANGE_EXTENDER"; Categories[Categories["CAMERA"] = 17] = "CAMERA"; Categories[Categories["IP_CAMERA"] = 17] = "IP_CAMERA"; Categories[Categories["VIDEO_DOORBELL"] = 18] = "VIDEO_DOORBELL"; Categories[Categories["AIR_PURIFIER"] = 19] = "AIR_PURIFIER"; Categories[Categories["AIR_HEATER"] = 20] = "AIR_HEATER"; Categories[Categories["AIR_CONDITIONER"] = 21] = "AIR_CONDITIONER"; Categories[Categories["AIR_HUMIDIFIER"] = 22] = "AIR_HUMIDIFIER"; Categories[Categories["AIR_DEHUMIDIFIER"] = 23] = "AIR_DEHUMIDIFIER"; Categories[Categories["APPLE_TV"] = 24] = "APPLE_TV"; Categories[Categories["HOMEPOD"] = 25] = "HOMEPOD"; Categories[Categories["SPEAKER"] = 26] = "SPEAKER"; Categories[Categories["AIRPORT"] = 27] = "AIRPORT"; Categories[Categories["SPRINKLER"] = 28] = "SPRINKLER"; Categories[Categories["FAUCET"] = 29] = "FAUCET"; Categories[Categories["SHOWER_HEAD"] = 30] = "SHOWER_HEAD"; Categories[Categories["TELEVISION"] = 31] = "TELEVISION"; Categories[Categories["TARGET_CONTROLLER"] = 32] = "TARGET_CONTROLLER"; Categories[Categories["ROUTER"] = 33] = "ROUTER"; Categories[Categories["AUDIO_RECEIVER"] = 34] = "AUDIO_RECEIVER"; Categories[Categories["TV_SET_TOP_BOX"] = 35] = "TV_SET_TOP_BOX"; Categories[Categories["TV_STREAMING_STICK"] = 36] = "TV_STREAMING_STICK"; })(Categories = exports.Categories || (exports.Categories = {})); var CharacteristicWarningType; (function (CharacteristicWarningType) { CharacteristicWarningType["SLOW_WRITE"] = "slow-write"; CharacteristicWarningType["TIMEOUT_WRITE"] = "timeout-write"; CharacteristicWarningType["SLOW_READ"] = "slow-read"; CharacteristicWarningType["TIMEOUT_READ"] = "timeout-read"; CharacteristicWarningType["WARN_MESSAGE"] = "warn-message"; CharacteristicWarningType["ERROR_MESSAGE"] = "error-message"; CharacteristicWarningType["DEBUG_MESSAGE"] = "debug-message"; })(CharacteristicWarningType = exports.CharacteristicWarningType || (exports.CharacteristicWarningType = {})); var MDNSAdvertiser; (function (MDNSAdvertiser) { /** * Use the `@homebridge/ciao` module as advertiser. */ MDNSAdvertiser["CIAO"] = "ciao"; /** * Use the `bonjour-hap` module as advertiser. */ MDNSAdvertiser["BONJOUR"] = "bonjour-hap"; /** * Use Avahi/D-Bus as advertiser. */ MDNSAdvertiser["AVAHI"] = "avahi"; })(MDNSAdvertiser = exports.MDNSAdvertiser || (exports.MDNSAdvertiser = {})); var WriteRequestState; (function (WriteRequestState) { WriteRequestState[WriteRequestState["REGULAR_REQUEST"] = 0] = "REGULAR_REQUEST"; WriteRequestState[WriteRequestState["TIMED_WRITE_AUTHENTICATED"] = 1] = "TIMED_WRITE_AUTHENTICATED"; WriteRequestState[WriteRequestState["TIMED_WRITE_REJECTED"] = 2] = "TIMED_WRITE_REJECTED"; })(WriteRequestState || (WriteRequestState = {})); var AccessoryEventTypes; (function (AccessoryEventTypes) { /** * Emitted when an iOS device wishes for this Accessory to identify itself. If `paired` is false, then * this device is currently browsing for Accessories in the system-provided "Add Accessory" screen. If * `paired` is true, then this is a device that has already paired with us. Note that if `paired` is true, * listening for this event is a shortcut for the underlying mechanism of setting the `Identify` Characteristic: * `getService(Service.AccessoryInformation).getCharacteristic(Characteristic.Identify).on('set', ...)` * You must call the callback for identification to be successful. */ AccessoryEventTypes["IDENTIFY"] = "identify"; /** * This event is emitted once the HAP TCP socket is bound. * At this point the mdns advertisement isn't yet available. Use the {@link ADVERTISED} if you require the accessory to be discoverable. */ AccessoryEventTypes["LISTENING"] = "listening"; /** * This event is emitted once the mDNS suite has fully advertised the presence of the accessory. * This event is guaranteed to be called after {@link LISTENING}. */ AccessoryEventTypes["ADVERTISED"] = "advertised"; AccessoryEventTypes["SERVICE_CONFIGURATION_CHANGE"] = "service-configurationChange"; /** * Emitted after a change in the value of one of the provided Service's Characteristics. */ AccessoryEventTypes["SERVICE_CHARACTERISTIC_CHANGE"] = "service-characteristic-change"; AccessoryEventTypes["PAIRED"] = "paired"; AccessoryEventTypes["UNPAIRED"] = "unpaired"; AccessoryEventTypes["CHARACTERISTIC_WARNING"] = "characteristic-warning"; })(AccessoryEventTypes = exports.AccessoryEventTypes || (exports.AccessoryEventTypes = {})); /** * Accessory is a virtual HomeKit device. It can publish an associated HAP server for iOS devices to communicate * with - or it can run behind another "Bridge" Accessory server. * * Bridged Accessories in this implementation must have a UUID that is unique among all other Accessories that * are hosted by the Bridge. This UUID must be "stable" and unchanging, even when the server is restarted. This * is required so that the Bridge can provide consistent "Accessory IDs" (aid) and "Instance IDs" (iid) for all * Accessories, Services, and Characteristics for iOS clients to reference later. */ var Accessory = /** @class */ (function (_super) { (0, tslib_1.__extends)(Accessory, _super); function Accessory(displayName, UUID) { var _this = _super.call(this) || this; _this.displayName = displayName; _this.UUID = UUID; // NOTICE: when adding/changing properties, remember to possibly adjust the serialize/deserialize functions _this.aid = null; // assigned by us in assignIDs() or by a Bridge _this._isBridge = false; // true if we are a Bridge (creating a new instance of the Bridge subclass sets this to true) _this.bridged = false; // true if we are hosted "behind" a Bridge Accessory _this.bridgedAccessories = []; // If we are a Bridge, these are the Accessories we are bridging _this.reachable = true; _this.category = 1 /* OTHER */; _this.services = []; _this.shouldPurgeUnusedIDs = true; // Purge unused ids by default /** * Captures if initialization steps inside {@link publish} have been called. * This is important when calling {@link publish} multiple times (e.g. after calling {@link unpublish}). * @private Private API */ _this.initialized = false; _this.controllers = {}; _this._setupID = null; _this.controllerStorage = new ControllerStorage_1.ControllerStorage(_this); /** * This property captures the time when we last served a /accessories request. * For multiple bursts of /accessories request we don't want to always contact GET handlers */ _this.lastAccessoriesRequest = 0; /** * Returns the bridging accessory if this accessory is bridged. * Otherwise returns itself. * * @returns the primary accessory */ _this.getPrimaryAccessory = function () { return _this.bridged ? _this.bridge : _this; }; (0, assert_1.default)(displayName, "Accessories must be created with a non-empty displayName."); (0, assert_1.default)(UUID, "Accessories must be created with a valid UUID."); (0, assert_1.default)(uuid.isValid(UUID), "UUID '" + UUID + "' is not a valid UUID. Try using the provided 'generateUUID' function to create a " + "valid UUID from any arbitrary string, like a serial number."); // create our initial "Accessory Information" Service that all Accessories are expected to have _this.addService(Service_1.Service.AccessoryInformation) .setCharacteristic(Characteristic_1.Characteristic.Name, displayName); // sign up for when iOS attempts to "set" the `Identify` characteristic - this means a paired device wishes // for us to identify ourselves (as opposed to an unpaired device - that case is handled by HAPServer 'identify' event) _this.getService(Service_1.Service.AccessoryInformation) .getCharacteristic(Characteristic_1.Characteristic.Identify) .on("set" /* SET */, function (value, callback) { if (value) { var paired = true; _this.identificationRequest(paired, callback); } }); return _this; } Accessory.prototype.identificationRequest = function (paired, callback) { debug("[%s] Identification request", this.displayName); if (this.listeners("identify" /* IDENTIFY */).length > 0) { // allow implementors to identify this Accessory in whatever way is appropriate, and pass along // the standard callback for completion. this.emit("identify" /* IDENTIFY */, paired, callback); } else { debug("[%s] Identification request ignored; no listeners to 'identify' event", this.displayName); callback(); } }; // eslint-disable-next-line @typescript-eslint/no-explicit-any Accessory.prototype.addService = function (serviceParam) { var e_1, _a; var constructorArgs = []; for (var _i = 1; _i < arguments.length; _i++) { constructorArgs[_i - 1] = arguments[_i]; } // service might be a constructor like `Service.AccessoryInformation` instead of an instance // of Service. Coerce if necessary. var service = typeof serviceParam === "function" ? new serviceParam(constructorArgs[0], constructorArgs[1], constructorArgs[2]) : serviceParam; try { // check for UUID+subtype conflict for (var _b = (0, tslib_1.__values)(this.services), _c = _b.next(); !_c.done; _c = _b.next()) { var existing = _c.value; if (existing.UUID === service.UUID) { // OK we have two Services with the same UUID. Check that each defines a `subtype` property and that each is unique. if (!service.subtype) { throw new Error("Cannot add a Service with the same UUID '" + existing.UUID + "' as another Service in this Accessory without also defining a unique 'subtype' property."); } if (service.subtype === existing.subtype) { throw new Error("Cannot add a Service with the same UUID '" + existing.UUID + "' and subtype '" + existing.subtype + "' as another Service in this Accessory."); } } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } if (this.services.length >= MAX_SERVICES) { throw new Error("Cannot add more than " + MAX_SERVICES + " services to a single accessory!"); } this.services.push(service); if (service.isPrimaryService) { // check if a primary service was added if (this.primaryService !== undefined) { this.primaryService.isPrimaryService = false; } this.primaryService = service; } if (!this.bridged) { this.enqueueConfigurationUpdate(); } else { this.emit("service-configurationChange" /* SERVICE_CONFIGURATION_CHANGE */, { service: service }); } this.setupServiceEventHandlers(service); return service; }; /** * @deprecated use {@link Service.setPrimaryService} directly */ Accessory.prototype.setPrimaryService = function (service) { service.setPrimaryService(); }; Accessory.prototype.removeService = function (service) { var index = this.services.indexOf(service); if (index >= 0) { this.services.splice(index, 1); if (this.primaryService === service) { // check if we are removing out primary service this.primaryService = undefined; } this.removeLinkedService(service); // remove it from linked service entries on the local accessory if (!this.bridged) { this.enqueueConfigurationUpdate(); } else { this.emit("service-configurationChange" /* SERVICE_CONFIGURATION_CHANGE */, { service: service }); } service.removeAllListeners(); } }; Accessory.prototype.removeLinkedService = function (removed) { var e_2, _a; try { for (var _b = (0, tslib_1.__values)(this.services), _c = _b.next(); !_c.done; _c = _b.next()) { var service = _c.value; service.removeLinkedService(removed); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_2) throw e_2.error; } } }; Accessory.prototype.getService = function (name) { var e_3, _a; try { for (var _b = (0, tslib_1.__values)(this.services), _c = _b.next(); !_c.done; _c = _b.next()) { var service = _c.value; if (typeof name === "string" && (service.displayName === name || service.name === name || service.subtype === name)) { return service; // @ts-expect-error: UUID property } else if (typeof name === "function" && ((service instanceof name) || (name.UUID === service.UUID))) { return service; } } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_3) throw e_3.error; } } return undefined; }; Accessory.prototype.getServiceById = function (uuid, subType) { var e_4, _a; try { for (var _b = (0, tslib_1.__values)(this.services), _c = _b.next(); !_c.done; _c = _b.next()) { var service = _c.value; if (typeof uuid === "string" && (service.displayName === uuid || service.name === uuid) && service.subtype === subType) { return service; // @ts-expect-error: UUID property } else if (typeof uuid === "function" && ((service instanceof uuid) || (uuid.UUID === service.UUID)) && service.subtype === subType) { return service; } } } catch (e_4_1) { e_4 = { error: e_4_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_4) throw e_4.error; } } return undefined; }; /** * @deprecated Not supported anymore */ Accessory.prototype.updateReachability = function (reachable) { if (!this.bridged) { throw new Error("Cannot update reachability on non-bridged accessory!"); } this.reachable = reachable; debug("Reachability update is no longer being supported."); }; Accessory.prototype.addBridgedAccessory = function (accessory, deferUpdate) { var e_5, _a; var _this = this; if (deferUpdate === void 0) { deferUpdate = false; } if (accessory._isBridge) { throw new Error("Cannot Bridge another Bridge!"); } try { // check for UUID conflict for (var _b = (0, tslib_1.__values)(this.bridgedAccessories), _c = _b.next(); !_c.done; _c = _b.next()) { var existing = _c.value; if (existing.UUID === accessory.UUID) { throw new Error("Cannot add a bridged Accessory with the same UUID as another bridged Accessory: " + existing.UUID); } } } catch (e_5_1) { e_5 = { error: e_5_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_5) throw e_5.error; } } if (this.bridgedAccessories.length >= MAX_ACCESSORIES) { throw new Error("Cannot Bridge more than " + MAX_ACCESSORIES + " Accessories"); } // listen for changes in ANY characteristics of ANY services on this Accessory accessory.on("service-characteristic-change" /* SERVICE_CHARACTERISTIC_CHANGE */, function (change) { return _this.handleCharacteristicChangeEvent(accessory, change.service, change); }); accessory.on("service-configurationChange" /* SERVICE_CONFIGURATION_CHANGE */, this.enqueueConfigurationUpdate.bind(this)); accessory.on("characteristic-warning" /* CHARACTERISTIC_WARNING */, this.handleCharacteristicWarning.bind(this)); accessory.bridged = true; accessory.bridge = this; this.bridgedAccessories.push(accessory); this.controllerStorage.linkAccessory(accessory); // init controllers of bridged accessory if (!deferUpdate) { this.enqueueConfigurationUpdate(); } return accessory; }; Accessory.prototype.addBridgedAccessories = function (accessories) { var e_6, _a; try { for (var accessories_1 = (0, tslib_1.__values)(accessories), accessories_1_1 = accessories_1.next(); !accessories_1_1.done; accessories_1_1 = accessories_1.next()) { var accessory = accessories_1_1.value; this.addBridgedAccessory(accessory, true); } } catch (e_6_1) { e_6 = { error: e_6_1 }; } finally { try { if (accessories_1_1 && !accessories_1_1.done && (_a = accessories_1.return)) _a.call(accessories_1); } finally { if (e_6) throw e_6.error; } } this.enqueueConfigurationUpdate(); }; Accessory.prototype.removeBridgedAccessory = function (accessory, deferUpdate) { if (accessory._isBridge) { throw new Error("Cannot Bridge another Bridge!"); } // check for UUID conflict var foundMatchAccessory = this.bridgedAccessories.findIndex(function (existing) { return existing.UUID === accessory.UUID; }); if (foundMatchAccessory === -1) { throw new Error("Cannot find the bridged Accessory to remove."); } this.bridgedAccessories.splice(foundMatchAccessory, 1); accessory.removeAllListeners(); if (!deferUpdate) { this.enqueueConfigurationUpdate(); } }; Accessory.prototype.removeBridgedAccessories = function (accessories) { var e_7, _a; try { for (var accessories_2 = (0, tslib_1.__values)(accessories), accessories_2_1 = accessories_2.next(); !accessories_2_1.done; accessories_2_1 = accessories_2.next()) { var accessory = accessories_2_1.value; this.removeBridgedAccessory(accessory, true); } } catch (e_7_1) { e_7 = { error: e_7_1 }; } finally { try { if (accessories_2_1 && !accessories_2_1.done && (_a = accessories_2.return)) _a.call(accessories_2); } finally { if (e_7) throw e_7.error; } } this.enqueueConfigurationUpdate(); }; Accessory.prototype.removeAllBridgedAccessories = function () { for (var i = this.bridgedAccessories.length - 1; i >= 0; i--) { this.removeBridgedAccessory(this.bridgedAccessories[i], true); } this.enqueueConfigurationUpdate(); }; Accessory.prototype.getCharacteristicByIID = function (iid) { var e_8, _a; try { for (var _b = (0, tslib_1.__values)(this.services), _c = _b.next(); !_c.done; _c = _b.next()) { var service = _c.value; var characteristic = service.getCharacteristicByIID(iid); if (characteristic) { return characteristic; } } } catch (e_8_1) { e_8 = { error: e_8_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_8) throw e_8.error; } } }; Accessory.prototype.getAccessoryByAID = function (aid) { var e_9, _a; if (aid === 1) { return this; } try { for (var _b = (0, tslib_1.__values)(this.bridgedAccessories), _c = _b.next(); !_c.done; _c = _b.next()) { var accessory = _c.value; if (accessory.aid === aid) { return accessory; } } } catch (e_9_1) { e_9 = { error: e_9_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_9) throw e_9.error; } } return undefined; }; Accessory.prototype.findCharacteristic = function (aid, iid) { var accessory = this.getAccessoryByAID(aid); return accessory && accessory.getCharacteristicByIID(iid); }; // noinspection JSDeprecatedSymbols /** * Method is used to configure an old style CameraSource. * The CameraSource API was fully replaced by the new Controller API used by {@link CameraController}. * The {@link CameraStreamingDelegate} used by the CameraController is the equivalent to the old CameraSource. * * The new Controller API is much more refined and robust way of "grouping" services together. * It especially is intended to fully support serialization/deserialization to/from persistent storage. * This feature is also gained when using the old style CameraSource API. * The {@link CameraStreamingDelegate} improves on the overall camera API though and provides some reworked * type definitions and a refined callback interface to better signal errors to the requesting HomeKit device. * It is advised to update to it. * * Full backwards compatibility is currently maintained. A legacy CameraSource will be wrapped into an Adapter. * All legacy StreamControllers in the "streamControllers" property will be replaced by CameraRTPManagement instances. * Any services in the "services" property which are one of the following are ignored: * - CameraRTPStreamManagement * - CameraOperatingMode * - CameraEventRecordingManagement * * @param cameraSource {LegacyCameraSource} * @deprecated please refer to the new {@see CameraController} API and {@link configureController} */ Accessory.prototype.configureCameraSource = function (cameraSource) { var _this = this; if (cameraSource.streamControllers.length === 0) { throw new Error("Malformed legacy CameraSource. Did not expose any StreamControllers!"); } var options = cameraSource.streamControllers[0].options; // grab options from one of the StreamControllers var cameraControllerOptions = { cameraStreamCount: cameraSource.streamControllers.length, streamingOptions: options, delegate: new camera_1.LegacyCameraSourceAdapter(cameraSource), }; var cameraController = new controller_1.CameraController(cameraControllerOptions, true); // create CameraController in legacy mode this.configureController(cameraController); // we try here to be as good as possibly of keeping current behaviour cameraSource.services.forEach(function (service) { if (service.UUID === Service_1.Service.CameraRTPStreamManagement.UUID || service.UUID === Service_1.Service.CameraOperatingMode.UUID || service.UUID === Service_1.Service.CameraRecordingManagement.UUID) { return; // ignore those services, as they get replaced by the RTPStreamManagement } // all other services get added. We can't really control possibly linking to any of those ignored services // so this is really only half-baked stuff. _this.addService(service); }); // replace stream controllers; basically only to still support the "forceStop" call // noinspection JSDeprecatedSymbols cameraSource.streamControllers = cameraController.streamManagements; return cameraController; // return the reference for the controller (maybe this could be useful?) }; /** * This method is used to setup a new Controller for this accessory. See {@see Controller} for a more detailed * explanation what a Controller is and what it is capable of. * * The controller can be passed as an instance of the class or as a constructor (without any necessary parameters) * for a new Controller. * Only one Controller of a given {@link ControllerIdentifier} can be configured for a given Accessory. * * When called, it will be checked if there are any services and persistent data the Controller (for the given * {@link ControllerIdentifier}) can be restored from. Otherwise the Controller will be created with new services. * * * @param controllerConstructor {Controller | ControllerConstructor} */ Accessory.prototype.configureController = function (controllerConstructor) { var _this = this; var controller = typeof controllerConstructor === "function" ? new controllerConstructor() // any custom constructor arguments should be passed before using .bind(...) : controllerConstructor; var id = controller.controllerId(); if (this.controllers[id]) { throw new Error("A Controller with the type/id '".concat(id, "' was already added to the accessory ").concat(this.displayName)); } var savedServiceMap = this.serializedControllers && this.serializedControllers[id]; var serviceMap; if (savedServiceMap) { // we found data to restore from var clonedServiceMap = (0, clone_1.clone)(savedServiceMap); var updatedServiceMap = controller.initWithServices(savedServiceMap); // init controller with existing services serviceMap = updatedServiceMap || savedServiceMap; // initWithServices could return an updated serviceMap, otherwise just use the existing one if (updatedServiceMap) { // controller returned a ServiceMap and thus signaled an updated set of services // clonedServiceMap is altered by this method, should not be touched again after this call (for the future people) this.handleUpdatedControllerServiceMap(clonedServiceMap, updatedServiceMap); } controller.configureServices(); // let the controller setup all its handlers // remove serialized data from our dictionary: delete this.serializedControllers[id]; if (Object.entries(this.serializedControllers).length === 0) { this.serializedControllers = undefined; } } else { serviceMap = controller.constructServices(); // let the controller create his services controller.configureServices(); // let the controller setup all its handlers Object.values(serviceMap).forEach(function (service) { if (service && !_this.services.includes(service)) { _this.addService(service); } }); } // --- init handlers and setup context --- var context = { controller: controller, serviceMap: serviceMap, }; if ((0, controller_1.isSerializableController)(controller)) { this.controllerStorage.trackController(controller); } this.controllers[id] = context; if (controller instanceof controller_1.CameraController) { // save CameraController for Snapshot handling this.activeCameraController = controller; } }; /** * This method will remove a given Controller from this accessory. * The controller object will be restored to its initial state. * This also means that any event handlers setup for the controller will be removed. * * @param controller - The controller which should be removed from the accessory. */ Accessory.prototype.removeController = function (controller) { var _this = this; var id = controller.controllerId(); var storedController = this.controllers[id]; if (storedController) { if (storedController.controller !== controller) { throw new Error("[" + this.displayName + "] tried removing a controller with the id/type '" + id + "' though provided controller isn't the same which is registered!"); } if ((0, controller_1.isSerializableController)(controller)) { // this will reset the state change delegate before we call handleControllerRemoved() this.controllerStorage.untrackController(controller); } if (controller.handleFactoryReset) { controller.handleFactoryReset(); } controller.handleControllerRemoved(); delete this.controllers[id]; if (this.activeCameraController === controller) { this.activeCameraController = undefined; } Object.values(storedController.serviceMap).forEach(function (service) { if (service) { _this.removeService(service); } }); } if (this.serializedControllers) { delete this.serializedControllers[id]; } }; Accessory.prototype.handleAccessoryUnpairedForControllers = function () { var e_10, _a; try { for (var _b = (0, tslib_1.__values)(Object.values(this.controllers)), _c = _b.next(); !_c.done; _c = _b.next()) { var context = _c.value; var controller = context.controller; if (controller.handleFactoryReset) { // if the controller implements handleFactoryReset, setup event handlers for this controller controller.handleFactoryReset(); } if ((0, controller_1.isSerializableController)(controller)) { this.controllerStorage.purgeControllerData(controller); } } } catch (e_10_1) { e_10 = { error: e_10_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_10) throw e_10.error; } } }; Accessory.prototype.handleUpdatedControllerServiceMap = function (originalServiceMap, updatedServiceMap) { var _this = this; updatedServiceMap = (0, clone_1.clone)(updatedServiceMap); // clone it so we can alter it Object.keys(originalServiceMap).forEach(function (name) { var service = originalServiceMap[name]; var updatedService = updatedServiceMap[name]; if (service && updatedService) { // we check all names contained in both ServiceMaps for changes delete originalServiceMap[name]; // delete from original ServiceMap so it will only contain deleted services at the end delete updatedServiceMap[name]; // delete from updated ServiceMap so it will only contain added services at the end if (service !== updatedService) { _this.removeService(service); _this.addService(updatedService); } } }); // now originalServiceMap contains only deleted services and updateServiceMap only added services Object.values(originalServiceMap).forEach(function (service) { if (service) { _this.removeService(service); } }); Object.values(updatedServiceMap).forEach(function (service) { if (service) { _this.addService(service); } }); }; Accessory.prototype.setupURI = function () { if (this._setupURI) { return this._setupURI; } var buffer = Buffer.alloc(8); var setupCode = this._accessoryInfo && parseInt(this._accessoryInfo.pincode.replace(/-/g, ""), 10); var value_low = setupCode; var value_high = this._accessoryInfo && this._accessoryInfo.category >> 1; value_low |= 1 << 28; // Supports IP; buffer.writeUInt32BE(value_low, 4); if (this._accessoryInfo && this._accessoryInfo.category & 1) { buffer[4] = buffer[4] | 1 << 7; } buffer.writeUInt32BE(value_high, 0); var encodedPayload = (buffer.readUInt32BE(4) + (buffer.readUInt32BE(0) * Math.pow(2, 32))).toString(36).toUpperCase(); if (encodedPayload.length !== 9) { for (var i = 0; i <= 9 - encodedPayload.length; i++) { encodedPayload = "0" + encodedPayload; } } this._setupURI = "X-HM://" + encodedPayload + this._setupID; return this._setupURI; }; /** * This method is called right before the accessory is published. It should be used to check for common * mistakes in Accessory structured, which may lead to HomeKit rejecting the accessory when pairing. * If it is called on a bridge it will call this method for all bridged accessories. */ Accessory.prototype.validateAccessory = function (mainAccessory) { var _this = this; var service = this.getService(Service_1.Service.AccessoryInformation); if (!service) { console.log("HAP-NodeJS WARNING: The accessory '" + this.displayName + "' is getting published without a AccessoryInformation service. " + "This might prevent the accessory from being added to the Home app or leading to the accessory being unresponsive!"); } else { // eslint-disable-next-line @typescript-eslint/no-explicit-any var checkValue = function (name, value) { if (!value) { console.log("HAP-NodeJS WARNING: The accessory '" + _this.displayName + "' is getting published with the characteristic '" + name + "'" + " (of the AccessoryInformation service) not having a value set. " + "This might prevent the accessory from being added to the Home App or leading to the accessory being unresponsive!"); } }; var model = service.getCharacteristic(Characteristic_1.Characteristic.Model).value; var serialNumber = service.getCharacteristic(Characteristic_1.Characteristic.SerialNumber).value; var firmwareRevision = service.getCharacteristic(Characteristic_1.Characteristic.FirmwareRevision).value; var name = service.getCharacteristic(Characteristic_1.Characteristic.Name).value; checkValue("Model", model); checkValue("SerialNumber", serialNumber); checkValue("FirmwareRevision", firmwareRevision); checkValue("Name", name); } if (mainAccessory) { // the main accessory which is advertised via bonjour must have a name with length <= 63 (limitation of DNS FQDN names) (0, assert_1.default)(Buffer.from(this.displayName, "utf8").length <= 63, "Accessory displayName cannot be longer than 63 bytes!"); } if (this.bridged) { this.bridgedAccessories.forEach(function (accessory) { return accessory.validateAccessory(); }); } }; /** * Assigns aid/iid to ourselves, any Accessories we are bridging, and all associated Services+Characteristics. Uses * the provided identifierCache to keep IDs stable. */ Accessory.prototype._assignIDs = function (identifierCache) { var e_11, _a, e_12, _b; // if we are responsible for our own identifierCache, start the expiration process // also check weather we want to have an expiration process if (this._identifierCache && this.shouldPurgeUnusedIDs) { this._identifierCache.startTrackingUsage(); } if (this.bridged) { // This Accessory is bridged, so it must have an aid > 1. Use the provided identifierCache to // fetch or assign one based on our UUID. this.aid = identifierCache.getAID(this.UUID); } else { // Since this Accessory is the server (as opposed to any Accessories that may be bridged behind us), // we must have aid = 1 this.aid = 1; } try { for (var _c = (0, tslib_1.__values)(this.services), _d = _c.next(); !_d.done; _d = _c.next()) { var service = _d.value; if (this._isBridge) { service._assignIDs(identifierCache, this.UUID, 2000000000); } else { service._assignIDs(identifierCache, this.UUID); } } } catch (e_11_1) { e_11 = { error: e_11_1 }; } finally { try { if (_d && !_d.done && (_a = _c.return)) _a.call(_c); } finally { if (e_11) throw e_11.error; } } try { // now assign IDs for any Accessories we are bridging for (var _e = (0, tslib_1.__values)(this.bridgedAccessories), _f = _e.next(); !_f.done; _f = _e.next()) { var accessory = _f.value; accessory._assignIDs(identifierCache); } } catch (e_12_1) { e_12 = { error: e_12_1 }; } finally { try { if (_f && !_f.done && (_b = _e.return)) _b.call(_e); } finally { if (e_12) throw e_12.error; } } // expire any now-unused cache keys (for Accessories, Services, or Characteristics // that have been removed since the last call to assignIDs()) if (this._identifierCache) { //Check weather we want to purge the unused ids if (this.shouldPurgeUnusedIDs) { this._identifierCache.stopTrackingUsageAndExpireUnused(); } //Save in case we have new ones this._identifierCache.save(); } }; Accessory.prototype.disableUnusedIDPurge = function () { this.shouldPurgeUnusedIDs = false; }; Accessory.prototype.enableUnusedIDPurge = function () { this.shouldPurgeUnusedIDs = true; }; /** * Manually purge the unused ids if you like, comes handy * when you have disabled auto purge so you can do it manually */ Accessory.prototype.purgeUnusedIDs = function () { //Cache the state of the purge mechanism and set it to true var oldValue = this.shouldPurgeUnusedIDs; this.shouldPurgeUnusedIDs = true; //Reassign all ids this._assignIDs(this._identifierCache); // Revert the purge mechanism state this.shouldPurgeUnusedIDs = oldValue; }; /** * Returns a JSON representation of this accessory suitable for delivering to HAP clients. */ Accessory.prototype.toHAP = function (connection, contactGetHandlers) { if (contactGetHandlers === void 0) { contactGetHandlers = true; } return (0, tslib_1.__awaiter)(this, void 0, void 0, function () { var accessory, accessories, _a, _b, _c, _d; var _e; return (0, tslib_1.__generator)(this, function (_f) { switch (_f.label) { case 0: (0, assert_1.default)(this.aid, "aid cannot be undefined for accessory '" + this.displayName + "'"); (0, assert_1.default)(this.services.length, "accessory '" + this.displayName + "' does not have any services!"); _e = { aid: this.aid }; return [4 /*yield*/, Promise.all(this.services.map(function (service) { return service.toHAP(connection, contactGetHandlers); }))]; case 1: accessory = (_e.services = _f.sent(), _e); accessories = [accessory]; if (!!this.bridged) return [3 /*break*/, 3]; _b = (_a = accessories.push).apply; _c = [accessories]; _d = [[]]; return [4 /*yield*/, Promise.all(this.bridgedAccessories .map(function (accessory) { return accessory.toHAP(connection, contactGetHandlers).then(function (value) { return value[0]; }); }))]; case 2: _b.apply(_a, _c.concat([tslib_1.__spreadArray.apply(void 0, _d.concat([tslib_1.__read.apply(void 0, [_f.sent()]), false]))])); _f.label = 3; case 3: return [2 /*return*/, accessories]; } }); }); }; /** * Returns a JSON representation of this accessory without characteristic values. */ Accessory.prototype.internalHAPRepresentation = function (assignIds) { var e_13, _a; if (assignIds === void 0) { assignIds = true; } if (assignIds) { this._assignIDs(this._identifierCache); // make sure our aid/iid's are all assigned } (0, assert_1.default)(this.aid, "aid cannot be undefined for accessory '" + this.displayName + "'"); (0, assert_1.default)(this.services.length, "accessory '" + this.displayName + "' does not have any services!"); var accessory = { aid: this.aid, services: this.services.map(function (service) { return service.internalHAPRepresentation(); }), }; var accessories = [accessory]; if (!this.bridged) { try { for (var _b = (0, tslib_1.__values)(this.bridgedAccessories), _c = _b.next(); !_c.done; _c = _b.next()) { var accessory_1 = _c.value; accessories.push(accessory_1.internalHAPRepresentation(false)[0]); } } catch (e_13_1) { e_13 = { error: e_13_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_13) throw e_13.error; } } } return accessories; }; /** * Publishes this Accessory on the local network for iOS clients to communicate with. * * @param {Object} info - Required info for publishing. * @param allowInsecureRequest - Will allow unencrypted and unauthenticated access to the http server * @param {string} info.username - The "username" (formatted as a MAC address - like "CC:22:3D:E3:CE:F6") of * this Accessory. Must be globally unique from all Accessories on your local network. * @param {string} info.pincode - The 8-digit pincode for clients to use when pairing this Accessory. Must be formatted * as a string like "031-45-154". * @param {string} info.category - One of the values of the Accessory.Category enum, like Accessory.Category.SWITCH. * This is a hint to iOS clients about what "type" of Accessory this represents, so * that for instance an appropriate icon can be drawn for the user while adding a * new Accessory. */ Accessory.prototype.publish = function (info, allowInsecureRequest) { var _a, _b; return (0, tslib_1.__awaiter)(this, void 0, void 0, function () { var service, config, parsed, selectedAdvertiser, _c; var _this = this; return (0, tslib_1.__generator)(this, function (_d) { switch (_d.label) { case 0: // noinspection JSDeprecatedSymbols if (!info.advertiser && info.useLegacyAdvertiser != null) { // noinspection JSDeprecatedSymbols info.advertiser = info.useLegacyAdvertiser ? "bonjour-hap" /* BONJOUR */ : "ciao" /* CIAO */; console.warn("DEPRECATED The PublishInfo.useLegacyAdvertiser option has been removed. " + "Please use the PublishInfo.advertiser property to enable \"ciao\" (useLegacyAdvertiser=false) " + "or \"bonjour-hap\" (useLegacyAdvertiser=true) mdns advertiser libraries!"); } // noinspection JSDeprecatedSymbols if (info.mdns && info.advertiser !== "bonjour-hap" /* BONJOUR */) { console.log("DEPRECATED user supplied a custom 'mdns' option. This option is deprecated and ignored. " + "Please move to the new 'bind' option."); } service = this.getService(Service_1.Service.ProtocolInformation); if (!service) { service = this.addService(Service_1.Service.ProtocolInformation); // add the protocol information service to the primary accessory } service.setCharacteristic(Characteristic_1.Characteristic.Version, Advertiser_1.CiaoAdvertiser.protocolVersionService); if (this.lastKnownUsername && this.lastKnownUsername !== info.username) { // username changed since last publish Accessory.cleanupAccessoryData(this.lastKnownUsername); // delete old Accessory data } if (!this.initialized && ((_a = info.addIdentifyingMaterial) !== null && _a !== void 0 ? _a : true)) { // adding some identifying material to our displayName if its our first publish() call this.displayName = this.displayName + " " + crypto_1.default.createHash("sha512") .update(info.username, "utf8") .digest("hex").slice(0, 4).toUpperCase(); this.getService(Service_1.Service.AccessoryInformation).updateCharacteristic(Characteristic_1.Characteristic.Name, this.displayName); } // attempt to load existing AccessoryInfo f