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

595 lines 29.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Service = exports.ServiceEventTypes = void 0; var tslib_1 = require("tslib"); var assert_1 = (0, tslib_1.__importDefault)(require("assert")); var debug_1 = (0, tslib_1.__importDefault)(require("debug")); var events_1 = require("events"); var Characteristic_1 = require("./Characteristic"); var uuid_1 = require("./util/uuid"); var debug = (0, debug_1.default)("HAP-NodeJS:Service"); /** * HAP spec allows a maximum of 100 characteristics per service! */ var MAX_CHARACTERISTICS = 100; var ServiceEventTypes; (function (ServiceEventTypes) { ServiceEventTypes["CHARACTERISTIC_CHANGE"] = "characteristic-change"; ServiceEventTypes["SERVICE_CONFIGURATION_CHANGE"] = "service-configurationChange"; ServiceEventTypes["CHARACTERISTIC_WARNING"] = "characteristic-warning"; })(ServiceEventTypes = exports.ServiceEventTypes || (exports.ServiceEventTypes = {})); /** * Service represents a set of grouped values necessary to provide a logical function. For instance, a * "Door Lock Mechanism" service might contain two values, one for the "desired lock state" and one for the * "current lock state". A particular Service is distinguished from others by its "type", which is a UUID. * HomeKit provides a set of known Service UUIDs defined in HomeKit.ts along with a corresponding * concrete subclass that you can instantiate directly to setup the necessary values. These natively-supported * Services are expected to contain a particular set of Characteristics. * * Unlike Characteristics, where you cannot have two Characteristics with the same UUID in the same Service, * you can actually have multiple Services with the same UUID in a single Accessory. For instance, imagine * a Garage Door Opener with both a "security light" and a "backlight" for the display. Each light could be * a "Lightbulb" Service with the same UUID. To account for this situation, we define an extra "subtype" * property on Service, that can be a string or other string-convertible object that uniquely identifies the * Service among its peers in an Accessory. For instance, you might have `service1.subtype = 'security_light'` * for one and `service2.subtype = 'backlight'` for the other. * * You can also define custom Services by providing your own UUID for the type that you generate yourself. * Custom Services can contain an arbitrary set of Characteristics, but Siri will likely not be able to * work with these. */ var Service = /** @class */ (function (_super) { (0, tslib_1.__extends)(Service, _super); function Service(displayName, UUID, subtype) { if (displayName === void 0) { displayName = ""; } var _this = _super.call(this) || this; _this.iid = null; // assigned later by our containing Accessory _this.name = null; _this.characteristics = []; _this.optionalCharacteristics = []; /** * @private */ _this.isHiddenService = false; /** * @private */ _this.isPrimaryService = false; // do not write to this directly /** * @private */ _this.linkedServices = []; (0, assert_1.default)(UUID, "Services must be created with a valid UUID."); _this.displayName = displayName; _this.UUID = UUID; _this.subtype = subtype; // every service has an optional Characteristic.Name property - we'll set it to our displayName // if one was given // if you don't provide a display name, some HomeKit apps may choose to hide the device. if (displayName) { // create the characteristic if necessary var nameCharacteristic = _this.getCharacteristic(Characteristic_1.Characteristic.Name) || _this.addCharacteristic(Characteristic_1.Characteristic.Name); nameCharacteristic.updateValue(displayName); } return _this; } /** * Returns an id which uniquely identifies an service on the associated accessory. * The serviceId is a concatenation of the UUID for the service (defined by HAP) and the subtype (could be empty) * which is programmatically defined by the programmer. * * @returns the serviceId */ Service.prototype.getServiceId = function () { return this.UUID + (this.subtype || ""); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any Service.prototype.addCharacteristic = function (input) { // characteristic might be a constructor like `Characteristic.Brightness` instead of an instance of Characteristic. Coerce if necessary. var e_1, _a; var constructorArgs = []; for (var _i = 1; _i < arguments.length; _i++) { constructorArgs[_i - 1] = arguments[_i]; } var characteristic = typeof input === "function" ? new (input.bind.apply(input, (0, tslib_1.__spreadArray)([void 0], (0, tslib_1.__read)(constructorArgs), false)))() : input; try { // check for UUID conflict for (var _b = (0, tslib_1.__values)(this.characteristics), _c = _b.next(); !_c.done; _c = _b.next()) { var existing = _c.value; if (existing.UUID === characteristic.UUID) { if (characteristic.UUID === "00000052-0000-1000-8000-0026BB765291") { //This is a special workaround for the Firmware Revision characteristic. return existing; } throw new Error("Cannot add a Characteristic with the same UUID as another Characteristic in this Service: " + existing.UUID); } } } 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.characteristics.length >= MAX_CHARACTERISTICS) { throw new Error("Cannot add more than " + MAX_CHARACTERISTICS + " characteristics to a single service!"); } this.setupCharacteristicEventHandlers(characteristic); this.characteristics.push(characteristic); this.emit("service-configurationChange" /* SERVICE_CONFIGURATION_CHANGE */); return characteristic; }; /** * Sets this service as the new primary service. * Any currently active primary service will be reset to be not primary. * This will happen immediately, if the service was already added to an accessory, or later * when the service gets added to an accessory. * * @param isPrimary {boolean} - optional boolean (default true) if the service should be the primary service */ Service.prototype.setPrimaryService = function (isPrimary) { if (isPrimary === void 0) { isPrimary = true; } this.isPrimaryService = isPrimary; this.emit("service-configurationChange" /* SERVICE_CONFIGURATION_CHANGE */); }; /** * Marks the service as hidden * * @param isHidden {boolean} - optional boolean (default true) if the service should be marked hidden */ Service.prototype.setHiddenService = function (isHidden) { if (isHidden === void 0) { isHidden = true; } this.isHiddenService = isHidden; this.emit("service-configurationChange" /* SERVICE_CONFIGURATION_CHANGE */); }; /** * Adds a new link to the specified service. The service MUST be already added to * the SAME accessory. * * @param service - The service this service should link to */ Service.prototype.addLinkedService = function (service) { //TODO: Add a check if the service is on the same accessory. if (!this.linkedServices.includes(service)) { this.linkedServices.push(service); } this.emit("service-configurationChange" /* SERVICE_CONFIGURATION_CHANGE */); }; /** * Removes a link to the specified service which was previously added with {@link addLinkedService} * * @param service - Previously linked service */ Service.prototype.removeLinkedService = function (service) { //TODO: Add a check if the service is on the same accessory. var index = this.linkedServices.indexOf(service); if (index !== -1) { this.linkedServices.splice(index, 1); } this.emit("service-configurationChange" /* SERVICE_CONFIGURATION_CHANGE */); }; Service.prototype.removeCharacteristic = function (characteristic) { var index = this.characteristics.indexOf(characteristic); if (index !== -1) { this.characteristics.splice(index, 1); characteristic.removeAllListeners(); this.emit("service-configurationChange" /* SERVICE_CONFIGURATION_CHANGE */); } }; Service.prototype.getCharacteristic = function (name) { // returns a characteristic object from the service // If Service.prototype.getCharacteristic(Characteristic.Type) does not find the characteristic, // but the type is in optionalCharacteristics, it adds the characteristic.type to the service and returns it. var e_2, _a, e_3, _b; try { for (var _c = (0, tslib_1.__values)(this.characteristics), _d = _c.next(); !_d.done; _d = _c.next()) { var characteristic = _d.value; if (typeof name === "string" && characteristic.displayName === name) { return characteristic; // @ts-expect-error: UUID field } else if (typeof name === "function" && ((characteristic instanceof name) || (name.UUID === characteristic.UUID))) { return characteristic; } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_d && !_d.done && (_a = _c.return)) _a.call(_c); } finally { if (e_2) throw e_2.error; } } if (typeof name === "function") { try { for (var _e = (0, tslib_1.__values)(this.optionalCharacteristics), _f = _e.next(); !_f.done; _f = _e.next()) { var characteristic = _f.value; // @ts-expect-error: UUID field if ((characteristic instanceof name) || (name.UUID === characteristic.UUID)) { return this.addCharacteristic(name); } } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (_f && !_f.done && (_b = _e.return)) _b.call(_e); } finally { if (e_3) throw e_3.error; } } var instance = this.addCharacteristic(name); // Not found in optional Characteristics. Adding anyway, but warning about it if it isn't the Name. // @ts-expect-error: UUID field if (name.UUID !== Characteristic_1.Characteristic.Name.UUID) { this.emitCharacteristicWarningEvent(instance, "warn-message" /* WARN_MESSAGE */, "Characteristic not in required or optional characteristic section for service " + this.constructor.name + ". Adding anyway."); } return instance; } }; Service.prototype.testCharacteristic = function (name) { var e_4, _a; try { // checks for the existence of a characteristic object in the service for (var _b = (0, tslib_1.__values)(this.characteristics), _c = _b.next(); !_c.done; _c = _b.next()) { var characteristic = _c.value; if (typeof name === "string" && characteristic.displayName === name) { return true; // @ts-expect-error: UUID field } else if (typeof name === "function" && ((characteristic instanceof name) || (name.UUID === characteristic.UUID))) { return true; } } } 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 false; }; Service.prototype.setCharacteristic = function (name, value) { this.getCharacteristic(name).setValue(value); return this; // for chaining }; // A function to only updating the remote value, but not firing the 'set' event. Service.prototype.updateCharacteristic = function (name, value) { this.getCharacteristic(name).updateValue(value); return this; }; Service.prototype.addOptionalCharacteristic = function (characteristic) { // characteristic might be a constructor like `Characteristic.Brightness` instead of an instance // of Characteristic. Coerce if necessary. if (typeof characteristic === "function") { characteristic = new characteristic(); } this.optionalCharacteristics.push(characteristic); }; // noinspection JSUnusedGlobalSymbols /** * This method was created to copy all characteristics from another service to this. * It's only adopting is currently in homebridge to merge the AccessoryInformation service. So some things * my be explicitly tailored towards this use case. * * It will not remove characteristics which are present currently but not added on the other characteristic. * It will not replace the characteristic if the value is falsy (except of '0' or 'false') * @param service * @private used by homebridge */ Service.prototype.replaceCharacteristicsFromService = function (service) { var _this = this; if (this.UUID !== service.UUID) { throw new Error("Incompatible services. Tried replacing characteristics of ".concat(this.UUID, " with characteristics from ").concat(service.UUID)); } var foreignCharacteristics = {}; // index foreign characteristics by UUID service.characteristics.forEach(function (characteristic) { return foreignCharacteristics[characteristic.UUID] = characteristic; }); this.characteristics.forEach(function (characteristic) { var foreignCharacteristic = foreignCharacteristics[characteristic.UUID]; if (foreignCharacteristic) { delete foreignCharacteristics[characteristic.UUID]; if (!foreignCharacteristic.value && foreignCharacteristic.value !== 0 && foreignCharacteristic.value !== false) { return; // ignore falsy values except if its the number zero or literally false } characteristic.replaceBy(foreignCharacteristic); } }); // add all additional characteristics which where not present already Object.values(foreignCharacteristics).forEach(function (characteristic) { return _this.addCharacteristic(characteristic); }); }; /** * @private */ Service.prototype.getCharacteristicByIID = function (iid) { var e_5, _a; try { for (var _b = (0, tslib_1.__values)(this.characteristics), _c = _b.next(); !_c.done; _c = _b.next()) { var characteristic = _c.value; if (characteristic.iid === iid) { return characteristic; } } } 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; } } }; /** * @private */ Service.prototype._assignIDs = function (identifierCache, accessoryName, baseIID) { var e_6, _a; if (baseIID === void 0) { baseIID = 0; } // the Accessory Information service must have a (reserved by IdentifierCache) ID of 1 if (this.UUID === "0000003E-0000-1000-8000-0026BB765291") { this.iid = 1; } else { // assign our own ID based on our UUID this.iid = baseIID + identifierCache.getIID(accessoryName, this.UUID, this.subtype); } try { // assign IIDs to our Characteristics for (var _b = (0, tslib_1.__values)(this.characteristics), _c = _b.next(); !_c.done; _c = _b.next()) { var characteristic = _c.value; characteristic._assignID(identifierCache, accessoryName, this.UUID, this.subtype); } } catch (e_6_1) { e_6 = { error: e_6_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_6) throw e_6.error; } } }; /** * Returns a JSON representation of this service suitable for delivering to HAP clients. * @private used to generate response to /accessories query */ Service.prototype.toHAP = function (connection, contactGetHandlers) { var _this = this; if (contactGetHandlers === void 0) { contactGetHandlers = true; } return new Promise(function (resolve) { var e_7, _a, e_8, _b; (0, assert_1.default)(_this.iid, "iid cannot be undefined for service '" + _this.displayName + "'"); (0, assert_1.default)(_this.characteristics.length, "service '" + _this.displayName + "' does not have any characteristics!"); var service = { type: (0, uuid_1.toShortForm)(_this.UUID), iid: _this.iid, characteristics: [], hidden: _this.isHiddenService ? true : undefined, primary: _this.isPrimaryService ? true : undefined, }; if (_this.linkedServices.length) { service.linked = []; try { for (var _c = (0, tslib_1.__values)(_this.linkedServices), _d = _c.next(); !_d.done; _d = _c.next()) { var linked = _d.value; if (!linked.iid) { // we got a linked service which is not added to the accessory // as it doesn't "exists" we just ignore it. // we have some (at least one) plugins on homebridge which link to the AccessoryInformation service. // homebridge always creates its own AccessoryInformation service and ignores the user supplied one // thus the link is automatically broken. debug("iid of linked service '".concat(linked.displayName, "' ").concat(linked.UUID, " is undefined on service '").concat(_this.displayName, "'")); continue; } service.linked.push(linked.iid); } } catch (e_7_1) { e_7 = { error: e_7_1 }; } finally { try { if (_d && !_d.done && (_a = _c.return)) _a.call(_c); } finally { if (e_7) throw e_7.error; } } } var missingCharacteristics = new Set(); var timeout = setTimeout(function () { var e_9, _a; try { for (var missingCharacteristics_1 = (0, tslib_1.__values)(missingCharacteristics), missingCharacteristics_1_1 = missingCharacteristics_1.next(); !missingCharacteristics_1_1.done; missingCharacteristics_1_1 = missingCharacteristics_1.next()) { var characteristic = missingCharacteristics_1_1.value; _this.emitCharacteristicWarningEvent(characteristic, "slow-read" /* SLOW_READ */, "The read handler for the characteristic '".concat(characteristic.displayName, "' was slow to respond!")); } } catch (e_9_1) { e_9 = { error: e_9_1 }; } finally { try { if (missingCharacteristics_1_1 && !missingCharacteristics_1_1.done && (_a = missingCharacteristics_1.return)) _a.call(missingCharacteristics_1); } finally { if (e_9) throw e_9.error; } } timeout = setTimeout(function () { var e_10, _a; timeout = undefined; try { for (var missingCharacteristics_2 = (0, tslib_1.__values)(missingCharacteristics), missingCharacteristics_2_1 = missingCharacteristics_2.next(); !missingCharacteristics_2_1.done; missingCharacteristics_2_1 = missingCharacteristics_2.next()) { var characteristic = missingCharacteristics_2_1.value; _this.emitCharacteristicWarningEvent(characteristic, "timeout-read" /* TIMEOUT_READ */, "The read handler for the characteristic '" + (characteristic === null || characteristic === void 0 ? void 0 : characteristic.displayName) + "' didn't respond at all!. Please check that you properly call the callback!"); service.characteristics.push(characteristic.internalHAPRepresentation()); // value is set to null } } catch (e_10_1) { e_10 = { error: e_10_1 }; } finally { try { if (missingCharacteristics_2_1 && !missingCharacteristics_2_1.done && (_a = missingCharacteristics_2.return)) _a.call(missingCharacteristics_2); } finally { if (e_10) throw e_10.error; } } missingCharacteristics.clear(); resolve(service); }, 6000); }, 3000); var _loop_1 = function (characteristic) { missingCharacteristics.add(characteristic); characteristic.toHAP(connection, contactGetHandlers).then(function (value) { if (!timeout) { return; // if timeout is undefined, response was already sent out } missingCharacteristics.delete(characteristic); service.characteristics.push(value); if (missingCharacteristics.size === 0) { if (timeout) { clearTimeout(timeout); timeout = undefined; } resolve(service); } }); }; try { for (var _e = (0, tslib_1.__values)(_this.characteristics), _f = _e.next(); !_f.done; _f = _e.next()) { var characteristic = _f.value; _loop_1(characteristic); } } catch (e_8_1) { e_8 = { error: e_8_1 }; } finally { try { if (_f && !_f.done && (_b = _e.return)) _b.call(_e); } finally { if (e_8) throw e_8.error; } } }); }; /** * Returns a JSON representation of this service without characteristic values. * @private used to generate the config hash */ Service.prototype.internalHAPRepresentation = function () { var e_11, _a; (0, assert_1.default)(this.iid, "iid cannot be undefined for service '" + this.displayName + "'"); (0, assert_1.default)(this.characteristics.length, "service '" + this.displayName + "' does not have any characteristics!"); var service = { type: (0, uuid_1.toShortForm)(this.UUID), iid: this.iid, characteristics: this.characteristics.map(function (characteristic) { return characteristic.internalHAPRepresentation(); }), hidden: this.isHiddenService ? true : undefined, primary: this.isPrimaryService ? true : undefined, }; if (this.linkedServices.length) { service.linked = []; try { for (var _b = (0, tslib_1.__values)(this.linkedServices), _c = _b.next(); !_c.done; _c = _b.next()) { var linked = _c.value; if (!linked.iid) { // we got a linked service which is not added to the accessory // as it doesn't "exists" we just ignore it. // we have some (at least one) plugins on homebridge which link to the AccessoryInformation service. // homebridge always creates its own AccessoryInformation service and ignores the user supplied one // thus the link is automatically broken. debug("iid of linked service '".concat(linked.displayName, "' ").concat(linked.UUID, " is undefined on service '").concat(this.displayName, "'")); continue; } service.linked.push(linked.iid); } } catch (e_11_1) { e_11 = { error: e_11_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_11) throw e_11.error; } } } return service; }; /** * @private */ Service.prototype.setupCharacteristicEventHandlers = function (characteristic) { var _this = this; // listen for changes in characteristics and bubble them up characteristic.on("change" /* CHANGE */, function (change) { _this.emit("characteristic-change" /* CHARACTERISTIC_CHANGE */, (0, tslib_1.__assign)((0, tslib_1.__assign)({}, change), { characteristic: characteristic })); }); characteristic.on("characteristic-warning" /* CHARACTERISTIC_WARNING */, this.emitCharacteristicWarningEvent.bind(this, characteristic)); }; /** * @private */ Service.prototype.emitCharacteristicWarningEvent = function (characteristic, type, message, stack) { this.emit("characteristic-warning" /* CHARACTERISTIC_WARNING */, { characteristic: characteristic, type: type, message: message, originatorChain: [this.displayName, characteristic.displayName], stack: stack, }); }; /** * @private */ Service.prototype._sideloadCharacteristics = function (targetCharacteristics) { var e_12, _a; try { for (var targetCharacteristics_1 = (0, tslib_1.__values)(targetCharacteristics), targetCharacteristics_1_1 = targetCharacteristics_1.next(); !targetCharacteristics_1_1.done; targetCharacteristics_1_1 = targetCharacteristics_1.next()) { var target = targetCharacteristics_1_1.value; this.setupCharacteristicEventHandlers(target); } } catch (e_12_1) { e_12 = { error: e_12_1 }; } finally { try { if (targetCharacteristics_1_1 && !targetCharacteristics_1_1.done && (_a = targetCharacteristics_1.return)) _a.call(targetCharacteristics_1); } finally { if (e_12) throw e_12.error; } } this.characteristics = targetCharacteristics.slice(); }; /** * @private */ Service.serialize = function (service) { var constructorName; if (service.constructor.name !== "Service") { constructorName = service.constructor.name; } return { displayName: service.displayName, UUID: service.UUID, subtype: service.subtype, constructorName: constructorName, hiddenService: service.isHiddenService, primaryService: service.isPrimaryService, characteristics: service.characteristics.map(function (characteristic) { return Characteristic_1.Characteristic.serialize(characteristic); }), optionalCharacteristics: service.optionalCharacteristics.map(function (characteristic) { return Characteristic_1.Characteristic.serialize(characteristic); }), }; }; /** * @private */ Service.deserialize = function (json) { var service; if (json.constructorName && json.constructorName.charAt(0).toUpperCase() === json.constructorName.charAt(0) && Service[json.constructorName]) { // MUST start with uppercase character and must exist on Service object var constructor = Service[json.constructorName]; service = new constructor(json.displayName, json.subtype); } else { service = new Service(json.displayName, json.UUID, json.subtype); } service.isHiddenService = !!json.hiddenService; service.isPrimaryService = !!json.primaryService; var characteristics = json.characteristics.map(function (serialized) { return Characteristic_1.Characteristic.deserialize(serialized); }); service._sideloadCharacteristics(characteristics); if (json.optionalCharacteristics) { service.optionalCharacteristics = json.optionalCharacteristics.map(function (serialized) { return Characteristic_1.Characteristic.deserialize(serialized); }); } return service; }; return Service; }(events_1.EventEmitter)); exports.Service = Service; //# sourceMappingURL=Service.js.map