hap-nodejs
Version:
HAP-NodeJS is a Node.js implementation of HomeKit Accessory Server.
762 lines • 29 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Service = exports.ServiceEventTypes = void 0;
const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert"));
const debug_1 = tslib_1.__importDefault(require("debug"));
const events_1 = require("events");
const Characteristic_1 = require("./Characteristic");
const uuid_1 = require("./util/uuid");
const checkName_1 = require("./util/checkName");
const debug = (0, debug_1.default)("HAP-NodeJS:Service");
/**
* HAP spec allows a maximum of 100 characteristics per service!
*/
const MAX_CHARACTERISTICS = 100;
/**
* @group Service
*/
var ServiceEventTypes;
(function (ServiceEventTypes) {
ServiceEventTypes["CHARACTERISTIC_CHANGE"] = "characteristic-change";
ServiceEventTypes["SERVICE_CONFIGURATION_CHANGE"] = "service-configurationChange";
ServiceEventTypes["CHARACTERISTIC_WARNING"] = "characteristic-warning";
})(ServiceEventTypes || (exports.ServiceEventTypes = 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 set up 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.
*
* @group Service
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
class Service extends events_1.EventEmitter {
// Service MUST NOT have any other static variables
// Pattern below is for automatic detection of the section of defined services. Used by the generator
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-
/**
* @group Service Definitions
*/
static AccessCode;
/**
* @group Service Definitions
*/
static AccessControl;
/**
* @group Service Definitions
*/
static AccessoryInformation;
/**
* @group Service Definitions
*/
static AccessoryMetrics;
/**
* @group Service Definitions
*/
static AccessoryRuntimeInformation;
/**
* @group Service Definitions
*/
static AirPurifier;
/**
* @group Service Definitions
*/
static AirQualitySensor;
/**
* @group Service Definitions
*/
static AssetUpdate;
/**
* @group Service Definitions
*/
static Assistant;
/**
* @group Service Definitions
*/
static AudioStreamManagement;
/**
* @group Service Definitions
*/
static Battery;
/**
* @group Service Definitions
*/
static CameraOperatingMode;
/**
* @group Service Definitions
*/
static CameraRecordingManagement;
/**
* @group Service Definitions
*/
static CameraRTPStreamManagement;
/**
* @group Service Definitions
*/
static CarbonDioxideSensor;
/**
* @group Service Definitions
*/
static CarbonMonoxideSensor;
/**
* @group Service Definitions
*/
static CloudRelay;
/**
* @group Service Definitions
*/
static ContactSensor;
/**
* @group Service Definitions
*/
static DataStreamTransportManagement;
/**
* @group Service Definitions
*/
static Diagnostics;
/**
* @group Service Definitions
*/
static Door;
/**
* @group Service Definitions
*/
static Doorbell;
/**
* @group Service Definitions
*/
static Fan;
/**
* @group Service Definitions
*/
static Fanv2;
/**
* @group Service Definitions
*/
static Faucet;
/**
* @group Service Definitions
*/
static FilterMaintenance;
/**
* @group Service Definitions
*/
static FirmwareUpdate;
/**
* @group Service Definitions
*/
static GarageDoorOpener;
/**
* @group Service Definitions
*/
static HeaterCooler;
/**
* @group Service Definitions
*/
static HumidifierDehumidifier;
/**
* @group Service Definitions
*/
static HumiditySensor;
/**
* @group Service Definitions
*/
static InputSource;
/**
* @group Service Definitions
*/
static IrrigationSystem;
/**
* @group Service Definitions
*/
static LeakSensor;
/**
* @group Service Definitions
*/
static Lightbulb;
/**
* @group Service Definitions
*/
static LightSensor;
/**
* @group Service Definitions
*/
static LockManagement;
/**
* @group Service Definitions
*/
static LockMechanism;
/**
* @group Service Definitions
*/
static Microphone;
/**
* @group Service Definitions
*/
static MotionSensor;
/**
* @group Service Definitions
*/
static NFCAccess;
/**
* @group Service Definitions
*/
static OccupancySensor;
/**
* @group Service Definitions
*/
static Outlet;
/**
* @group Service Definitions
*/
static Pairing;
/**
* @group Service Definitions
*/
static PowerManagement;
/**
* @group Service Definitions
*/
static ProtocolInformation;
/**
* @group Service Definitions
*/
static SecuritySystem;
/**
* @group Service Definitions
*/
static ServiceLabel;
/**
* @group Service Definitions
*/
static Siri;
/**
* @group Service Definitions
*/
static SiriEndpoint;
/**
* @group Service Definitions
*/
static Slats;
/**
* @group Service Definitions
*/
static SmartSpeaker;
/**
* @group Service Definitions
*/
static SmokeSensor;
/**
* @group Service Definitions
*/
static Speaker;
/**
* @group Service Definitions
*/
static StatefulProgrammableSwitch;
/**
* @group Service Definitions
*/
static StatelessProgrammableSwitch;
/**
* @group Service Definitions
*/
static Switch;
/**
* @group Service Definitions
*/
static TapManagement;
/**
* @group Service Definitions
*/
static TargetControl;
/**
* @group Service Definitions
*/
static TargetControlManagement;
/**
* @group Service Definitions
*/
static Television;
/**
* @group Service Definitions
*/
static TelevisionSpeaker;
/**
* @group Service Definitions
*/
static TemperatureSensor;
/**
* @group Service Definitions
*/
static Thermostat;
/**
* @group Service Definitions
*/
static ThreadTransport;
/**
* @group Service Definitions
*/
static TransferTransportManagement;
/**
* @group Service Definitions
*/
static Tunnel;
/**
* @group Service Definitions
*/
static Valve;
/**
* @group Service Definitions
*/
static WiFiRouter;
/**
* @group Service Definitions
*/
static WiFiSatellite;
/**
* @group Service Definitions
*/
static WiFiTransport;
/**
* @group Service Definitions
*/
static Window;
/**
* @group Service Definitions
*/
static WindowCovering;
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// NOTICE: when adding/changing properties, remember to possibly adjust the serialize/deserialize functions
displayName;
UUID;
subtype;
iid = null; // assigned later by our containing Accessory
name = null;
characteristics = [];
optionalCharacteristics = [];
/**
* @private
*/
isHiddenService = false;
/**
* @private
*/
isPrimaryService = false; // do not write to this directly
/**
* @private
*/
linkedServices = [];
constructor(displayName = "", UUID, subtype) {
super();
(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
(0, checkName_1.checkName)(this.displayName, "Name", displayName);
const nameCharacteristic = this.getCharacteristic(Characteristic_1.Characteristic.Name) ||
this.addCharacteristic(Characteristic_1.Characteristic.Name);
nameCharacteristic.updateValue(displayName);
}
}
/**
* Returns an id which uniquely identifies a 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
*/
getServiceId() {
return this.UUID + (this.subtype || "");
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
addCharacteristic(input, ...constructorArgs) {
// characteristic might be a constructor like `Characteristic.Brightness` instead of an instance of Characteristic. Coerce if necessary.
const characteristic = typeof input === "function" ? new input(...constructorArgs) : input;
// check for UUID conflict
for (const existing of this.characteristics) {
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);
}
}
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" /* ServiceEventTypes.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 - optional boolean (default true) if the service should be the primary service
*/
setPrimaryService(isPrimary = true) {
this.isPrimaryService = isPrimary;
this.emit("service-configurationChange" /* ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE */);
}
/**
* Marks the service as hidden
*
* @param isHidden - optional boolean (default true) if the service should be marked hidden
*/
setHiddenService(isHidden = true) {
this.isHiddenService = isHidden;
this.emit("service-configurationChange" /* ServiceEventTypes.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
*/
addLinkedService(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" /* ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE */);
}
/**
* Removes a link to the specified service which was previously added with {@link addLinkedService}
*
* @param service - Previously linked service
*/
removeLinkedService(service) {
//TODO: Add a check if the service is on the same accessory.
const index = this.linkedServices.indexOf(service);
if (index !== -1) {
this.linkedServices.splice(index, 1);
}
this.emit("service-configurationChange" /* ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE */);
}
removeCharacteristic(characteristic) {
const index = this.characteristics.indexOf(characteristic);
if (index !== -1) {
this.characteristics.splice(index, 1);
characteristic.removeAllListeners();
this.emit("service-configurationChange" /* ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE */);
}
}
getCharacteristic(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.
for (const characteristic of this.characteristics) {
if (typeof name === "string" && characteristic.displayName === name) {
return characteristic;
}
else {
// @ts-expect-error ('UUID' does not exist on type 'never')
if (typeof name === "function" && ((characteristic instanceof name) || (name.UUID === characteristic.UUID))) {
return characteristic;
}
}
}
if (typeof name === "function") {
for (const characteristic of this.optionalCharacteristics) {
// @ts-expect-error ('UUID' does not exist on type 'never')
if ((characteristic instanceof name) || (name.UUID === characteristic.UUID)) {
return this.addCharacteristic(name);
}
}
const instance = this.addCharacteristic(name);
// Not found in optional Characteristics. Adding anyway, but warning about it if it isn't the Name.
if (name.UUID !== Characteristic_1.Characteristic.Name.UUID) {
this.emitCharacteristicWarningEvent(instance, "warn-message" /* CharacteristicWarningType.WARN_MESSAGE */, "Characteristic not in required or optional characteristic section for service " + this.constructor.name + ". Adding anyway.");
}
return instance;
}
}
testCharacteristic(name) {
// checks for the existence of a characteristic object in the service
for (const characteristic of this.characteristics) {
if (typeof name === "string" && characteristic.displayName === name) {
return true;
}
else {
// @ts-expect-error ('UUID' does not exist on type 'never')
if (typeof name === "function" && ((characteristic instanceof name) || (name.UUID === characteristic.UUID))) {
return true;
}
}
}
return false;
}
setCharacteristic(name, value) {
// @ts-expect-error: We know that both overloads exists individually. There is just no publicly exposed type for that!
this.getCharacteristic(name).setValue(value);
return this; // for chaining
}
updateCharacteristic(name, value) {
this.getCharacteristic(name).updateValue(value);
return this;
}
addOptionalCharacteristic(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
* may 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
*/
replaceCharacteristicsFromService(service) {
if (this.UUID !== service.UUID) {
throw new Error(`Incompatible services. Tried replacing characteristics of ${this.UUID} with characteristics from ${service.UUID}`);
}
const foreignCharacteristics = {}; // index foreign characteristics by UUID
service.characteristics.forEach(characteristic => foreignCharacteristics[characteristic.UUID] = characteristic);
this.characteristics.forEach(characteristic => {
const 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 it's the number zero or literally false
}
characteristic.replaceBy(foreignCharacteristic);
}
});
// add all additional characteristics which where not present already
Object.values(foreignCharacteristics).forEach(characteristic => this.addCharacteristic(characteristic));
}
/**
* @private
*/
getCharacteristicByIID(iid) {
for (const characteristic of this.characteristics) {
if (characteristic.iid === iid) {
return characteristic;
}
}
}
/**
* @private
*/
_assignIDs(identifierCache, accessoryName, 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);
}
// assign IIDs to our Characteristics
for (const characteristic of this.characteristics) {
characteristic._assignID(identifierCache, accessoryName, this.UUID, this.subtype);
}
}
/**
* Returns a JSON representation of this service suitable for delivering to HAP clients.
* @private used to generate response to /accessories query
*/
toHAP(connection, contactGetHandlers = true) {
return new Promise(resolve => {
(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!");
const 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 = [];
for (const linked of this.linkedServices) {
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 '${linked.displayName}' ${linked.UUID} is undefined on service '${this.displayName}'`);
continue;
}
service.linked.push(linked.iid);
}
}
const missingCharacteristics = new Set();
let timeout = setTimeout(() => {
for (const characteristic of missingCharacteristics) {
this.emitCharacteristicWarningEvent(characteristic, "slow-read" /* CharacteristicWarningType.SLOW_READ */, `The read handler for the characteristic '${characteristic.displayName}' was slow to respond!`);
}
timeout = setTimeout(() => {
timeout = undefined;
for (const characteristic of missingCharacteristics) {
this.emitCharacteristicWarningEvent(characteristic, "timeout-read" /* CharacteristicWarningType.TIMEOUT_READ */, "The read handler for the characteristic '" + 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
}
missingCharacteristics.clear();
resolve(service);
}, 6000);
}, 3000);
for (const characteristic of this.characteristics) {
missingCharacteristics.add(characteristic);
characteristic.toHAP(connection, contactGetHandlers).then(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);
}
});
}
});
}
/**
* Returns a JSON representation of this service without characteristic values.
* @private used to generate the config hash
*/
internalHAPRepresentation() {
(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!");
const service = {
type: (0, uuid_1.toShortForm)(this.UUID),
iid: this.iid,
characteristics: this.characteristics.map(characteristic => characteristic.internalHAPRepresentation()),
hidden: this.isHiddenService ? true : undefined,
primary: this.isPrimaryService ? true : undefined,
};
if (this.linkedServices.length) {
service.linked = [];
for (const linked of this.linkedServices) {
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 '${linked.displayName}' ${linked.UUID} is undefined on service '${this.displayName}'`);
continue;
}
service.linked.push(linked.iid);
}
}
return service;
}
/**
* @private
*/
setupCharacteristicEventHandlers(characteristic) {
// listen for changes in characteristics and bubble them up
characteristic.on("change" /* CharacteristicEventTypes.CHANGE */, (change) => {
this.emit("characteristic-change" /* ServiceEventTypes.CHARACTERISTIC_CHANGE */, { ...change, characteristic: characteristic });
});
characteristic.on("characteristic-warning" /* CharacteristicEventTypes.CHARACTERISTIC_WARNING */, this.emitCharacteristicWarningEvent.bind(this, characteristic));
}
/**
* @private
*/
emitCharacteristicWarningEvent(characteristic, type, message, stack) {
this.emit("characteristic-warning" /* ServiceEventTypes.CHARACTERISTIC_WARNING */, {
characteristic: characteristic,
type: type,
message: message,
originatorChain: [this.displayName, characteristic.displayName],
stack: stack,
});
}
/**
* @private
*/
_sideloadCharacteristics(targetCharacteristics) {
for (const target of targetCharacteristics) {
this.setupCharacteristicEventHandlers(target);
}
this.characteristics = targetCharacteristics.slice();
}
/**
* @private
*/
static serialize(service) {
let 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(characteristic => Characteristic_1.Characteristic.serialize(characteristic)),
optionalCharacteristics: service.optionalCharacteristics.map(characteristic => Characteristic_1.Characteristic.serialize(characteristic)),
};
}
/**
* @private
*/
static deserialize(json) {
let 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
const 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;
const characteristics = json.characteristics.map(serialized => Characteristic_1.Characteristic.deserialize(serialized));
service._sideloadCharacteristics(characteristics);
if (json.optionalCharacteristics) {
service.optionalCharacteristics = json.optionalCharacteristics.map(serialized => Characteristic_1.Characteristic.deserialize(serialized));
}
return service;
}
}
exports.Service = Service;
// We have a cyclic dependency problem. Within this file we have the definitions of "./definitions" as
// type imports only (in order to define the static properties). Setting those properties is done outside
// this file, within the definition files. Therefore, we import it at the end of this file. Seems weird, but is important.
require("./definitions/ServiceDefinitions");
//# sourceMappingURL=Service.js.map