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