UNPKG

86 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.Accessory = exports.AccessoryEventTypes = exports.MDNSAdvertiser = exports.CharacteristicWarningType = exports.Categories = void 0;
4const tslib_1 = require("tslib");
5const assert_1 = tslib_1.__importDefault(require("assert"));
6const crypto_1 = tslib_1.__importDefault(require("crypto"));
7const debug_1 = tslib_1.__importDefault(require("debug"));
8const events_1 = require("events");
9const net_1 = tslib_1.__importDefault(require("net"));
10const Advertiser_1 = require("./Advertiser");
11// noinspection JSDeprecatedSymbols
12const Characteristic_1 = require("./Characteristic");
13const controller_1 = require("./controller");
14const HAPServer_1 = require("./HAPServer");
15const AccessoryInfo_1 = require("./model/AccessoryInfo");
16const ControllerStorage_1 = require("./model/ControllerStorage");
17const IdentifierCache_1 = require("./model/IdentifierCache");
18const Service_1 = require("./Service");
19const clone_1 = require("./util/clone");
20const request_util_1 = require("./util/request-util");
21const uuid = tslib_1.__importStar(require("./util/uuid"));
22const uuid_1 = require("./util/uuid");
23const checkName_1 = require("./util/checkName");
24const debug = (0, debug_1.default)("HAP-NodeJS:Accessory");
25const MAX_ACCESSORIES = 149; // Maximum number of bridged accessories per bridge.
26const MAX_SERVICES = 100;
27/**
28 * Known category values. Category is a hint to iOS clients about what "type" of Accessory this represents, for UI only.
29 *
30 * @group Accessory
31 */
32var Categories;
33(function (Categories) {
34 // noinspection JSUnusedGlobalSymbols
35 Categories[Categories["OTHER"] = 1] = "OTHER";
36 Categories[Categories["BRIDGE"] = 2] = "BRIDGE";
37 Categories[Categories["FAN"] = 3] = "FAN";
38 Categories[Categories["GARAGE_DOOR_OPENER"] = 4] = "GARAGE_DOOR_OPENER";
39 Categories[Categories["LIGHTBULB"] = 5] = "LIGHTBULB";
40 Categories[Categories["DOOR_LOCK"] = 6] = "DOOR_LOCK";
41 Categories[Categories["OUTLET"] = 7] = "OUTLET";
42 Categories[Categories["SWITCH"] = 8] = "SWITCH";
43 Categories[Categories["THERMOSTAT"] = 9] = "THERMOSTAT";
44 Categories[Categories["SENSOR"] = 10] = "SENSOR";
45 Categories[Categories["ALARM_SYSTEM"] = 11] = "ALARM_SYSTEM";
46 // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
47 Categories[Categories["SECURITY_SYSTEM"] = 11] = "SECURITY_SYSTEM";
48 Categories[Categories["DOOR"] = 12] = "DOOR";
49 Categories[Categories["WINDOW"] = 13] = "WINDOW";
50 Categories[Categories["WINDOW_COVERING"] = 14] = "WINDOW_COVERING";
51 Categories[Categories["PROGRAMMABLE_SWITCH"] = 15] = "PROGRAMMABLE_SWITCH";
52 Categories[Categories["RANGE_EXTENDER"] = 16] = "RANGE_EXTENDER";
53 Categories[Categories["CAMERA"] = 17] = "CAMERA";
54 // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
55 Categories[Categories["IP_CAMERA"] = 17] = "IP_CAMERA";
56 Categories[Categories["VIDEO_DOORBELL"] = 18] = "VIDEO_DOORBELL";
57 Categories[Categories["AIR_PURIFIER"] = 19] = "AIR_PURIFIER";
58 Categories[Categories["AIR_HEATER"] = 20] = "AIR_HEATER";
59 Categories[Categories["AIR_CONDITIONER"] = 21] = "AIR_CONDITIONER";
60 Categories[Categories["AIR_HUMIDIFIER"] = 22] = "AIR_HUMIDIFIER";
61 Categories[Categories["AIR_DEHUMIDIFIER"] = 23] = "AIR_DEHUMIDIFIER";
62 Categories[Categories["APPLE_TV"] = 24] = "APPLE_TV";
63 Categories[Categories["HOMEPOD"] = 25] = "HOMEPOD";
64 Categories[Categories["SPEAKER"] = 26] = "SPEAKER";
65 Categories[Categories["AIRPORT"] = 27] = "AIRPORT";
66 Categories[Categories["SPRINKLER"] = 28] = "SPRINKLER";
67 Categories[Categories["FAUCET"] = 29] = "FAUCET";
68 Categories[Categories["SHOWER_HEAD"] = 30] = "SHOWER_HEAD";
69 Categories[Categories["TELEVISION"] = 31] = "TELEVISION";
70 Categories[Categories["TARGET_CONTROLLER"] = 32] = "TARGET_CONTROLLER";
71 Categories[Categories["ROUTER"] = 33] = "ROUTER";
72 Categories[Categories["AUDIO_RECEIVER"] = 34] = "AUDIO_RECEIVER";
73 Categories[Categories["TV_SET_TOP_BOX"] = 35] = "TV_SET_TOP_BOX";
74 Categories[Categories["TV_STREAMING_STICK"] = 36] = "TV_STREAMING_STICK";
75})(Categories || (exports.Categories = Categories = {}));
76/**
77 * @group Accessory
78 */
79var CharacteristicWarningType;
80(function (CharacteristicWarningType) {
81 CharacteristicWarningType["SLOW_WRITE"] = "slow-write";
82 CharacteristicWarningType["TIMEOUT_WRITE"] = "timeout-write";
83 CharacteristicWarningType["SLOW_READ"] = "slow-read";
84 CharacteristicWarningType["TIMEOUT_READ"] = "timeout-read";
85 CharacteristicWarningType["WARN_MESSAGE"] = "warn-message";
86 CharacteristicWarningType["ERROR_MESSAGE"] = "error-message";
87 CharacteristicWarningType["DEBUG_MESSAGE"] = "debug-message";
88})(CharacteristicWarningType || (exports.CharacteristicWarningType = CharacteristicWarningType = {}));
89/**
90 * @group Accessory
91 */
92var MDNSAdvertiser;
93(function (MDNSAdvertiser) {
94 /**
95 * Use the `@homebridge/ciao` module as advertiser.
96 */
97 MDNSAdvertiser["CIAO"] = "ciao";
98 /**
99 * Use the `bonjour-hap` module as advertiser.
100 */
101 MDNSAdvertiser["BONJOUR"] = "bonjour-hap";
102 /**
103 * Use Avahi/D-Bus as advertiser.
104 */
105 MDNSAdvertiser["AVAHI"] = "avahi";
106 /**
107 * Use systemd-resolved/D-Bus as advertiser.
108 *
109 * Note: The systemd-resolved D-Bus interface doesn't provide means to detect restarts of the service.
110 * Therefore, we can't detect if our advertisement might be lost due to a restart of the systemd-resolved daemon restart.
111 * Consequentially, treat this feature as an experimental feature.
112 */
113 MDNSAdvertiser["RESOLVED"] = "resolved";
114})(MDNSAdvertiser || (exports.MDNSAdvertiser = MDNSAdvertiser = {}));
115var WriteRequestState;
116(function (WriteRequestState) {
117 WriteRequestState[WriteRequestState["REGULAR_REQUEST"] = 0] = "REGULAR_REQUEST";
118 WriteRequestState[WriteRequestState["TIMED_WRITE_AUTHENTICATED"] = 1] = "TIMED_WRITE_AUTHENTICATED";
119 WriteRequestState[WriteRequestState["TIMED_WRITE_REJECTED"] = 2] = "TIMED_WRITE_REJECTED";
120})(WriteRequestState || (WriteRequestState = {}));
121/**
122 * @group Accessory
123 */
124var AccessoryEventTypes;
125(function (AccessoryEventTypes) {
126 /**
127 * Emitted when an iOS device wishes for this Accessory to identify itself. If `paired` is false, then
128 * this device is currently browsing for Accessories in the system-provided "Add Accessory" screen. If
129 * `paired` is true, then this is a device that has already paired with us. Note that if `paired` is true,
130 * listening for this event is a shortcut for the underlying mechanism of setting the `Identify` Characteristic:
131 * `getService(Service.AccessoryInformation).getCharacteristic(Characteristic.Identify).on('set', ...)`
132 * You must call the callback for identification to be successful.
133 */
134 AccessoryEventTypes["IDENTIFY"] = "identify";
135 /**
136 * This event is emitted once the HAP TCP socket is bound.
137 * At this point the mdns advertisement isn't yet available. Use the {@link ADVERTISED} if you require the accessory to be discoverable.
138 */
139 AccessoryEventTypes["LISTENING"] = "listening";
140 /**
141 * This event is emitted once the mDNS suite has fully advertised the presence of the accessory.
142 * This event is guaranteed to be called after {@link LISTENING}.
143 */
144 AccessoryEventTypes["ADVERTISED"] = "advertised";
145 AccessoryEventTypes["SERVICE_CONFIGURATION_CHANGE"] = "service-configurationChange";
146 /**
147 * Emitted after a change in the value of one of the provided Service's Characteristics.
148 */
149 AccessoryEventTypes["SERVICE_CHARACTERISTIC_CHANGE"] = "service-characteristic-change";
150 AccessoryEventTypes["PAIRED"] = "paired";
151 AccessoryEventTypes["UNPAIRED"] = "unpaired";
152 AccessoryEventTypes["CHARACTERISTIC_WARNING"] = "characteristic-warning";
153})(AccessoryEventTypes || (exports.AccessoryEventTypes = AccessoryEventTypes = {}));
154/**
155 * Accessory is a virtual HomeKit device. It can publish an associated HAP server for iOS devices to communicate
156 * with - or it can run behind another "Bridge" Accessory server.
157 *
158 * Bridged Accessories in this implementation must have a UUID that is unique among all other Accessories that
159 * are hosted by the Bridge. This UUID must be "stable" and unchanging, even when the server is restarted. This
160 * is required so that the Bridge can provide consistent "Accessory IDs" (aid) and "Instance IDs" (iid) for all
161 * Accessories, Services, and Characteristics for iOS clients to reference later.
162 *
163 * @group Accessory
164 */
165// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
166class Accessory extends events_1.EventEmitter {
167 displayName;
168 UUID;
169 // Timeout in milliseconds until a characteristic warning is issue
170 static TIMEOUT_WARNING = 3000;
171 // Timeout in milliseconds after `TIMEOUT_WARNING` until the operation on the characteristic is considered timed out.
172 static TIMEOUT_AFTER_WARNING = 6000;
173 // NOTICE: when adding/changing properties, remember to possibly adjust the serialize/deserialize functions
174 aid = null; // assigned by us in assignIDs() or by a Bridge
175 _isBridge = false; // true if we are a Bridge (creating a new instance of the Bridge subclass sets this to true)
176 bridged = false; // true if we are hosted "behind" a Bridge Accessory
177 bridge; // if accessory is bridged, this property points to the bridge which bridges this accessory
178 bridgedAccessories = []; // If we are a Bridge, these are the Accessories we are bridging
179 reachable = true;
180 lastKnownUsername;
181 category = 1 /* Categories.OTHER */;
182 services = [];
183 primaryService;
184 shouldPurgeUnusedIDs = true; // Purge unused ids by default
185 /**
186 * Captures if initialization steps inside {@link publish} have been called.
187 * This is important when calling {@link publish} multiple times (e.g. after calling {@link unpublish}).
188 * @private Private API
189 */
190 initialized = false;
191 controllers = {};
192 serializedControllers; // store uninitialized controller data after a Accessory.deserialize call
193 activeCameraController;
194 /**
195 * @private Private API.
196 */
197 _accessoryInfo;
198 /**
199 * @private Private API.
200 */
201 _setupID = null;
202 /**
203 * @private Private API.
204 */
205 _identifierCache;
206 /**
207 * @private Private API.
208 */
209 controllerStorage = new ControllerStorage_1.ControllerStorage(this);
210 /**
211 * @private Private API.
212 */
213 _advertiser;
214 /**
215 * @private Private API.
216 */
217 _server;
218 /**
219 * @private Private API.
220 */
221 _setupURI;
222 configurationChangeDebounceTimeout;
223 /**
224 * This property captures the time when we last served a /accessories request.
225 * For multiple bursts of /accessories request we don't want to always contact GET handlers
226 */
227 lastAccessoriesRequest = 0;
228 constructor(displayName, UUID) {
229 super();
230 this.displayName = displayName;
231 this.UUID = UUID;
232 (0, assert_1.default)(displayName, "Accessories must be created with a non-empty displayName.");
233 (0, assert_1.default)(UUID, "Accessories must be created with a valid UUID.");
234 (0, assert_1.default)(uuid.isValid(UUID), "UUID '" + UUID + "' is not a valid UUID. Try using the provided 'generateUUID' function to create a " +
235 "valid UUID from any arbitrary string, like a serial number.");
236 // create our initial "Accessory Information" Service that all Accessories are expected to have
237 (0, checkName_1.checkName)(this.displayName, "Name", displayName);
238 this.addService(Service_1.Service.AccessoryInformation)
239 .setCharacteristic(Characteristic_1.Characteristic.Name, displayName);
240 // sign up for when iOS attempts to "set" the `Identify` characteristic - this means a paired device wishes
241 // for us to identify ourselves (as opposed to an unpaired device - that case is handled by HAPServer 'identify' event)
242 this.getService(Service_1.Service.AccessoryInformation)
243 .getCharacteristic(Characteristic_1.Characteristic.Identify)
244 .on("set" /* CharacteristicEventTypes.SET */, (value, callback) => {
245 if (value) {
246 const paired = true;
247 this.identificationRequest(paired, callback);
248 }
249 });
250 }
251 identificationRequest(paired, callback) {
252 debug("[%s] Identification request", this.displayName);
253 if (this.listeners("identify" /* AccessoryEventTypes.IDENTIFY */).length > 0) {
254 // allow implementors to identify this Accessory in whatever way is appropriate, and pass along
255 // the standard callback for completion.
256 this.emit("identify" /* AccessoryEventTypes.IDENTIFY */, paired, callback);
257 }
258 else {
259 debug("[%s] Identification request ignored; no listeners to 'identify' event", this.displayName);
260 callback();
261 }
262 }
263 // eslint-disable-next-line @typescript-eslint/no-explicit-any
264 addService(serviceParam, ...constructorArgs) {
265 // service might be a constructor like `Service.AccessoryInformation` instead of an instance
266 // of Service. Coerce if necessary.
267 const service = typeof serviceParam === "function"
268 ? new serviceParam(constructorArgs[0], constructorArgs[1], constructorArgs[2])
269 : serviceParam;
270 // check for UUID+subtype conflict
271 for (const existing of this.services) {
272 if (existing.UUID === service.UUID) {
273 // OK we have two Services with the same UUID. Check that each defines a `subtype` property and that each is unique.
274 if (!service.subtype) {
275 throw new Error("Cannot add a Service with the same UUID '" + existing.UUID +
276 "' as another Service in this Accessory without also defining a unique 'subtype' property.");
277 }
278 if (service.subtype === existing.subtype) {
279 throw new Error("Cannot add a Service with the same UUID '" + existing.UUID +
280 "' and subtype '" + existing.subtype + "' as another Service in this Accessory.");
281 }
282 }
283 }
284 if (this.services.length >= MAX_SERVICES) {
285 throw new Error("Cannot add more than " + MAX_SERVICES + " services to a single accessory!");
286 }
287 this.services.push(service);
288 if (service.isPrimaryService) { // check if a primary service was added
289 if (this.primaryService !== undefined) {
290 this.primaryService.isPrimaryService = false;
291 }
292 this.primaryService = service;
293 }
294 if (!this.bridged) {
295 this.enqueueConfigurationUpdate();
296 }
297 else {
298 this.emit("service-configurationChange" /* AccessoryEventTypes.SERVICE_CONFIGURATION_CHANGE */, { service: service });
299 }
300 this.setupServiceEventHandlers(service);
301 return service;
302 }
303 removeService(service) {
304 const index = this.services.indexOf(service);
305 if (index >= 0) {
306 this.services.splice(index, 1);
307 if (this.primaryService === service) { // check if we are removing out primary service
308 this.primaryService = undefined;
309 }
310 this.removeLinkedService(service); // remove it from linked service entries on the local accessory
311 if (!this.bridged) {
312 this.enqueueConfigurationUpdate();
313 }
314 else {
315 this.emit("service-configurationChange" /* AccessoryEventTypes.SERVICE_CONFIGURATION_CHANGE */, { service: service });
316 }
317 service.removeAllListeners();
318 }
319 }
320 removeLinkedService(removed) {
321 for (const service of this.services) {
322 service.removeLinkedService(removed);
323 }
324 }
325 getService(name) {
326 for (const service of this.services) {
327 if (typeof name === "string" && (service.displayName === name || service.name === name || service.subtype === name)) {
328 return service;
329 }
330 else {
331 // @ts-expect-error ('UUID' does not exist on type 'never')
332 if (typeof name === "function" && ((service instanceof name) || (name.UUID === service.UUID))) {
333 return service;
334 }
335 }
336 }
337 return undefined;
338 }
339 getServiceById(uuid, subType) {
340 for (const service of this.services) {
341 if (typeof uuid === "string" && (service.displayName === uuid || service.name === uuid) && service.subtype === subType) {
342 return service;
343 }
344 else {
345 // @ts-expect-error ('UUID' does not exist on type 'never')
346 if (typeof uuid === "function" && ((service instanceof uuid) || (uuid.UUID === service.UUID)) && service.subtype === subType) {
347 return service;
348 }
349 }
350 }
351 return undefined;
352 }
353 /**
354 * Returns the bridging accessory if this accessory is bridged.
355 * Otherwise, returns itself.
356 *
357 * @returns the primary accessory
358 */
359 getPrimaryAccessory = () => {
360 return this.bridged ? this.bridge : this;
361 };
362 addBridgedAccessory(accessory, deferUpdate = false) {
363 if (accessory._isBridge || accessory === this) {
364 throw new Error("Illegal state: either trying to bridge a bridge or trying to bridge itself!");
365 }
366 if (accessory.initialized) {
367 throw new Error("Tried to bridge an accessory which was already published once!");
368 }
369 if (accessory.bridge != null) {
370 // this also prevents that we bridge the same accessory twice!
371 throw new Error("Tried to bridge " + accessory.displayName + " while it was already bridged by " + accessory.bridge.displayName);
372 }
373 if (this.bridgedAccessories.length >= MAX_ACCESSORIES) {
374 throw new Error("Cannot Bridge more than " + MAX_ACCESSORIES + " Accessories");
375 }
376 // listen for changes in ANY characteristics of ANY services on this Accessory
377 accessory.on("service-characteristic-change" /* AccessoryEventTypes.SERVICE_CHARACTERISTIC_CHANGE */, change => this.handleCharacteristicChangeEvent(accessory, change.service, change));
378 accessory.on("service-configurationChange" /* AccessoryEventTypes.SERVICE_CONFIGURATION_CHANGE */, this.enqueueConfigurationUpdate.bind(this));
379 accessory.on("characteristic-warning" /* AccessoryEventTypes.CHARACTERISTIC_WARNING */, this.handleCharacteristicWarning.bind(this));
380 accessory.bridged = true;
381 accessory.bridge = this;
382 this.bridgedAccessories.push(accessory);
383 this.controllerStorage.linkAccessory(accessory); // init controllers of bridged accessory
384 if (!deferUpdate) {
385 this.enqueueConfigurationUpdate();
386 }
387 return accessory;
388 }
389 addBridgedAccessories(accessories) {
390 for (const accessory of accessories) {
391 this.addBridgedAccessory(accessory, true);
392 }
393 this.enqueueConfigurationUpdate();
394 }
395 removeBridgedAccessory(accessory, deferUpdate = false) {
396 // check for UUID conflict
397 const accessoryIndex = this.bridgedAccessories.indexOf(accessory);
398 if (accessoryIndex === -1) {
399 throw new Error("Cannot find the bridged Accessory to remove.");
400 }
401 this.bridgedAccessories.splice(accessoryIndex, 1);
402 accessory.bridged = false;
403 accessory.bridge = undefined;
404 accessory.removeAllListeners();
405 if (!deferUpdate) {
406 this.enqueueConfigurationUpdate();
407 }
408 }
409 removeBridgedAccessories(accessories) {
410 for (const accessory of accessories) {
411 this.removeBridgedAccessory(accessory, true);
412 }
413 this.enqueueConfigurationUpdate();
414 }
415 removeAllBridgedAccessories() {
416 for (let i = this.bridgedAccessories.length - 1; i >= 0; i--) {
417 this.removeBridgedAccessory(this.bridgedAccessories[i], true);
418 }
419 this.enqueueConfigurationUpdate();
420 }
421 getCharacteristicByIID(iid) {
422 for (const service of this.services) {
423 const characteristic = service.getCharacteristicByIID(iid);
424 if (characteristic) {
425 return characteristic;
426 }
427 }
428 }
429 getAccessoryByAID(aid) {
430 if (this.aid === aid) {
431 return this;
432 }
433 return this.bridgedAccessories.find(value => value.aid === aid);
434 }
435 findCharacteristic(aid, iid) {
436 const accessory = this.getAccessoryByAID(aid);
437 return accessory && accessory.getCharacteristicByIID(iid);
438 }
439 /**
440 * This method is used to set up a new Controller for this accessory. See {@link Controller} for a more detailed
441 * explanation what a Controller is and what it is capable of.
442 *
443 * The controller can be passed as an instance of the class or as a constructor (without any necessary parameters)
444 * for a new Controller.
445 * Only one Controller of a given {@link ControllerIdentifier} can be configured for a given Accessory.
446 *
447 * When called, it will be checked if there are any services and persistent data the Controller (for the given
448 * {@link ControllerIdentifier}) can be restored from. Otherwise, the Controller will be created with new services.
449 *
450 *
451 * @param controllerConstructor - The Controller instance or constructor to the Controller with no required arguments.
452 */
453 configureController(controllerConstructor) {
454 const controller = typeof controllerConstructor === "function"
455 ? new controllerConstructor() // any custom constructor arguments should be passed before using .bind(...)
456 : controllerConstructor;
457 const id = controller.controllerId();
458 if (this.controllers[id]) {
459 throw new Error(`A Controller with the type/id '${id}' was already added to the accessory ${this.displayName}`);
460 }
461 const savedServiceMap = this.serializedControllers && this.serializedControllers[id];
462 let serviceMap;
463 if (savedServiceMap) { // we found data to restore from
464 const clonedServiceMap = (0, clone_1.clone)(savedServiceMap);
465 const updatedServiceMap = controller.initWithServices(savedServiceMap); // init controller with existing services
466 serviceMap = updatedServiceMap || savedServiceMap; // initWithServices could return an updated serviceMap, otherwise just use the existing one
467 if (updatedServiceMap) { // controller returned a ServiceMap and thus signaled an updated set of services
468 // clonedServiceMap is altered by this method, should not be touched again after this call (for the future people)
469 this.handleUpdatedControllerServiceMap(clonedServiceMap, updatedServiceMap);
470 }
471 controller.configureServices(); // let the controller setup all its handlers
472 // remove serialized data from our dictionary:
473 delete this.serializedControllers[id];
474 if (Object.entries(this.serializedControllers).length === 0) {
475 this.serializedControllers = undefined;
476 }
477 }
478 else {
479 serviceMap = controller.constructServices(); // let the controller create his services
480 controller.configureServices(); // let the controller setup all its handlers
481 Object.values(serviceMap).forEach(service => {
482 if (service && !this.services.includes(service)) {
483 this.addService(service);
484 }
485 });
486 }
487 // --- init handlers and setup context ---
488 const context = {
489 controller: controller,
490 serviceMap: serviceMap,
491 };
492 if ((0, controller_1.isSerializableController)(controller)) {
493 this.controllerStorage.trackController(controller);
494 }
495 this.controllers[id] = context;
496 if (controller instanceof controller_1.CameraController) { // save CameraController for Snapshot handling
497 this.activeCameraController = controller;
498 }
499 }
500 /**
501 * This method will remove a given Controller from this accessory.
502 * The controller object will be restored to its initial state.
503 * This also means that any event handlers setup for the controller will be removed.
504 *
505 * @param controller - The controller which should be removed from the accessory.
506 */
507 removeController(controller) {
508 const id = controller.controllerId();
509 const storedController = this.controllers[id];
510 if (storedController) {
511 if (storedController.controller !== controller) {
512 throw new Error("[" + this.displayName + "] tried removing a controller with the id/type '" + id +
513 "' though provided controller isn't the same instance that is registered!");
514 }
515 if ((0, controller_1.isSerializableController)(controller)) {
516 // this will reset the state change delegate before we call handleControllerRemoved()
517 this.controllerStorage.untrackController(controller);
518 }
519 if (controller.handleFactoryReset) {
520 controller.handleFactoryReset();
521 }
522 controller.handleControllerRemoved();
523 delete this.controllers[id];
524 if (this.activeCameraController === controller) {
525 this.activeCameraController = undefined;
526 }
527 Object.values(storedController.serviceMap).forEach(service => {
528 if (service) {
529 this.removeService(service);
530 }
531 });
532 }
533 if (this.serializedControllers) {
534 delete this.serializedControllers[id];
535 }
536 }
537 handleAccessoryUnpairedForControllers() {
538 for (const context of Object.values(this.controllers)) {
539 const controller = context.controller;
540 if (controller.handleFactoryReset) { // if the controller implements handleFactoryReset, setup event handlers for this controller
541 controller.handleFactoryReset();
542 }
543 if ((0, controller_1.isSerializableController)(controller)) {
544 this.controllerStorage.purgeControllerData(controller);
545 }
546 }
547 }
548 handleUpdatedControllerServiceMap(originalServiceMap, updatedServiceMap) {
549 updatedServiceMap = (0, clone_1.clone)(updatedServiceMap); // clone it so we can alter it
550 Object.keys(originalServiceMap).forEach(name => {
551 const service = originalServiceMap[name];
552 const updatedService = updatedServiceMap[name];
553 if (service && updatedService) { // we check all names contained in both ServiceMaps for changes
554 delete originalServiceMap[name]; // delete from original ServiceMap, so it will only contain deleted services at the end
555 delete updatedServiceMap[name]; // delete from updated ServiceMap, so it will only contain added services at the end
556 if (service !== updatedService) {
557 this.removeService(service);
558 this.addService(updatedService);
559 }
560 }
561 });
562 // now originalServiceMap contains only deleted services and updateServiceMap only added services
563 Object.values(originalServiceMap).forEach(service => {
564 if (service) {
565 this.removeService(service);
566 }
567 });
568 Object.values(updatedServiceMap).forEach(service => {
569 if (service) {
570 this.addService(service);
571 }
572 });
573 }
574 setupURI() {
575 if (this._setupURI) {
576 return this._setupURI;
577 }
578 (0, assert_1.default)(!!this._accessoryInfo, "Cannot generate setupURI on an accessory that isn't published yet!");
579 const buffer = Buffer.alloc(8);
580 let value_low = parseInt(this._accessoryInfo.pincode.replace(/-/g, ""), 10);
581 const value_high = this._accessoryInfo.category >> 1;
582 value_low |= 1 << 28; // Supports IP;
583 buffer.writeUInt32BE(value_low, 4);
584 if (this._accessoryInfo.category & 1) {
585 buffer[4] = buffer[4] | 1 << 7;
586 }
587 buffer.writeUInt32BE(value_high, 0);
588 let encodedPayload = (buffer.readUInt32BE(4) + (buffer.readUInt32BE(0) * 0x100000000)).toString(36).toUpperCase();
589 if (encodedPayload.length !== 9) {
590 for (let i = 0; i <= 9 - encodedPayload.length; i++) {
591 encodedPayload = "0" + encodedPayload;
592 }
593 }
594 this._setupURI = "X-HM://" + encodedPayload + this._setupID;
595 return this._setupURI;
596 }
597 /**
598 * This method is called right before the accessory is published. It should be used to check for common
599 * mistakes in Accessory structured, which may lead to HomeKit rejecting the accessory when pairing.
600 * If it is called on a bridge it will call this method for all bridged accessories.
601 */
602 validateAccessory(mainAccessory) {
603 const service = this.getService(Service_1.Service.AccessoryInformation);
604 if (!service) {
605 console.log("HAP-NodeJS WARNING: The accessory '" + this.displayName + "' is getting published without a AccessoryInformation service. " +
606 "This might prevent the accessory from being added to the Home app or leading to the accessory being unresponsive!");
607 }
608 else {
609 // eslint-disable-next-line @typescript-eslint/no-explicit-any
610 const checkValue = (name, value) => {
611 if (!value) {
612 console.log("HAP-NodeJS WARNING: The accessory '" + this.displayName + "' is getting published with the characteristic '" + name + "'" +
613 " (of the AccessoryInformation service) not having a value set. " +
614 "This might prevent the accessory from being added to the Home App or leading to the accessory being unresponsive!");
615 }
616 };
617 (0, checkName_1.checkName)(this.displayName, "Name", service.getCharacteristic(Characteristic_1.Characteristic.Name).value);
618 checkValue("FirmwareRevision", service.getCharacteristic(Characteristic_1.Characteristic.FirmwareRevision).value);
619 checkValue("Manufacturer", service.getCharacteristic(Characteristic_1.Characteristic.Manufacturer).value);
620 checkValue("Model", service.getCharacteristic(Characteristic_1.Characteristic.Model).value);
621 checkValue("Name", service.getCharacteristic(Characteristic_1.Characteristic.Name).value);
622 checkValue("SerialNumber", service.getCharacteristic(Characteristic_1.Characteristic.SerialNumber).value);
623 }
624 if (mainAccessory) {
625 // the main accessory which is advertised via bonjour must have a name with length <= 63 (limitation of DNS FQDN names)
626 (0, assert_1.default)(Buffer.from(this.displayName, "utf8").length <= 63, "Accessory displayName cannot be longer than 63 bytes!");
627 }
628 if (this.bridged) {
629 this.bridgedAccessories.forEach(accessory => accessory.validateAccessory());
630 }
631 }
632 /**
633 * Assigns aid/iid to ourselves, any Accessories we are bridging, and all associated Services+Characteristics. Uses
634 * the provided identifierCache to keep IDs stable.
635 * @private Private API
636 */
637 _assignIDs(identifierCache) {
638 // if we are responsible for our own identifierCache, start the expiration process
639 // also check weather we want to have an expiration process
640 if (this._identifierCache && this.shouldPurgeUnusedIDs) {
641 this._identifierCache.startTrackingUsage();
642 }
643 if (this.bridged) {
644 // This Accessory is bridged, so it must have an aid > 1. Use the provided identifierCache to
645 // fetch or assign one based on our UUID.
646 this.aid = identifierCache.getAID(this.UUID);
647 }
648 else {
649 // Since this Accessory is the server (as opposed to any Accessories that may be bridged behind us),
650 // we must have aid = 1
651 this.aid = 1;
652 }
653 for (const service of this.services) {
654 if (this._isBridge) {
655 service._assignIDs(identifierCache, this.UUID, 2000000000);
656 }
657 else {
658 service._assignIDs(identifierCache, this.UUID);
659 }
660 }
661 // now assign IDs for any Accessories we are bridging
662 for (const accessory of this.bridgedAccessories) {
663 accessory._assignIDs(identifierCache);
664 }
665 // expire any now-unused cache keys (for Accessories, Services, or Characteristics
666 // that have been removed since the last call to assignIDs())
667 if (this._identifierCache) {
668 //Check weather we want to purge the unused ids
669 if (this.shouldPurgeUnusedIDs) {
670 this._identifierCache.stopTrackingUsageAndExpireUnused();
671 }
672 //Save in case we have new ones
673 this._identifierCache.save();
674 }
675 }
676 disableUnusedIDPurge() {
677 this.shouldPurgeUnusedIDs = false;
678 }
679 enableUnusedIDPurge() {
680 this.shouldPurgeUnusedIDs = true;
681 }
682 /**
683 * Manually purge the unused ids if you like, comes handy
684 * when you have disabled auto purge, so you can do it manually
685 */
686 purgeUnusedIDs() {
687 //Cache the state of the purge mechanism and set it to true
688 const oldValue = this.shouldPurgeUnusedIDs;
689 this.shouldPurgeUnusedIDs = true;
690 //Reassign all ids
691 this._assignIDs(this._identifierCache);
692 // Revert the purge mechanism state
693 this.shouldPurgeUnusedIDs = oldValue;
694 }
695 /**
696 * Returns a JSON representation of this accessory suitable for delivering to HAP clients.
697 */
698 async toHAP(connection, contactGetHandlers = true) {
699 (0, assert_1.default)(this.aid, "aid cannot be undefined for accessory '" + this.displayName + "'");
700 (0, assert_1.default)(this.services.length, "accessory '" + this.displayName + "' does not have any services!");
701 const accessory = {
702 aid: this.aid,
703 services: await Promise.all(this.services.map(service => service.toHAP(connection, contactGetHandlers))),
704 };
705 const accessories = [accessory];
706 if (!this.bridged) {
707 accessories.push(...await Promise.all(this.bridgedAccessories
708 .map(accessory => accessory.toHAP(connection, contactGetHandlers).then(value => value[0]))));
709 }
710 return accessories;
711 }
712 /**
713 * Returns a JSON representation of this accessory without characteristic values.
714 */
715 internalHAPRepresentation(assignIds = true) {
716 if (assignIds) {
717 this._assignIDs(this._identifierCache); // make sure our aid/iid's are all assigned
718 }
719 (0, assert_1.default)(this.aid, "aid cannot be undefined for accessory '" + this.displayName + "'");
720 (0, assert_1.default)(this.services.length, "accessory '" + this.displayName + "' does not have any services!");
721 const accessory = {
722 aid: this.aid,
723 services: this.services.map(service => service.internalHAPRepresentation()),
724 };
725 const accessories = [accessory];
726 if (!this.bridged) {
727 for (const accessory of this.bridgedAccessories) {
728 accessories.push(accessory.internalHAPRepresentation(false)[0]);
729 }
730 }
731 return accessories;
732 }
733 /**
734 * Publishes this accessory on the local network for iOS clients to communicate with.
735 * - `info.username` - formatted as a MAC address, like `CC:22:3D:E3:CE:F6`, of this accessory.
736 * Must be globally unique from all Accessories on your local network.
737 * - `info.pincode` - the 8-digit pin code for clients to use when pairing this Accessory.
738 * Must be formatted as a string like `031-45-154`.
739 * - `info.category` - one of the values of the `Accessory.Category` enum, like `Accessory.Category.SWITCH`.
740 * This is a hint to iOS clients about what "type" of Accessory this represents, so
741 * that for instance an appropriate icon can be drawn for the user while adding a
742 * new Accessory.
743 * @param {{
744 * username: string;
745 * pincode: string;
746 * category: Accessory.Categories;
747 * }} info - Required info for publishing.
748 * @param {boolean} allowInsecureRequest - Will allow unencrypted and unauthenticated access to the http server
749 */
750 async publish(info, allowInsecureRequest) {
751 if (this.bridged) {
752 throw new Error("Can't publish in accessory which is bridged by another accessory. Bridged by " + this.bridge?.displayName);
753 }
754 let service = this.getService(Service_1.Service.ProtocolInformation);
755 if (!service) {
756 service = this.addService(Service_1.Service.ProtocolInformation); // add the protocol information service to the primary accessory
757 }
758 service.setCharacteristic(Characteristic_1.Characteristic.Version, Advertiser_1.CiaoAdvertiser.protocolVersionService);
759 if (this.lastKnownUsername && this.lastKnownUsername !== info.username) { // username changed since last publish
760 Accessory.cleanupAccessoryData(this.lastKnownUsername); // delete old Accessory data
761 }
762 if (!this.initialized && (info.addIdentifyingMaterial ?? true)) {
763 // adding some identifying material to our displayName if it's our first publish() call
764 this.displayName = this.displayName + " " + crypto_1.default.createHash("sha512")
765 .update(info.username, "utf8")
766 .digest("hex").slice(0, 4).toUpperCase();
767 this.getService(Service_1.Service.AccessoryInformation).updateCharacteristic(Characteristic_1.Characteristic.Name, this.displayName);
768 }
769 // attempt to load existing AccessoryInfo from disk
770 this._accessoryInfo = AccessoryInfo_1.AccessoryInfo.load(info.username);
771 // if we don't have one, create a new one.
772 if (!this._accessoryInfo) {
773 debug("[%s] Creating new AccessoryInfo for our HAP server", this.displayName);
774 this._accessoryInfo = AccessoryInfo_1.AccessoryInfo.create(info.username);
775 }
776 if (info.setupID) {
777 this._setupID = info.setupID;
778 }
779 else if (this._accessoryInfo.setupID === undefined || this._accessoryInfo.setupID === "") {
780 this._setupID = Accessory._generateSetupID();
781 }
782 else {
783 this._setupID = this._accessoryInfo.setupID;
784 }
785 this._accessoryInfo.setupID = this._setupID;
786 // make sure we have up-to-date values in AccessoryInfo, then save it in case they changed (or if we just created it)
787 this._accessoryInfo.displayName = this.displayName;
788 this._accessoryInfo.model = this.getService(Service_1.Service.AccessoryInformation).getCharacteristic(Characteristic_1.Characteristic.Model).value;
789 this._accessoryInfo.category = info.category || 1 /* Categories.OTHER */;
790 this._accessoryInfo.pincode = info.pincode;
791 this._accessoryInfo.save();
792 // create our IdentifierCache, so we can provide clients with stable aid/iid's
793 this._identifierCache = IdentifierCache_1.IdentifierCache.load(info.username);
794 // if we don't have one, create a new one.
795 if (!this._identifierCache) {
796 debug("[%s] Creating new IdentifierCache", this.displayName);
797 this._identifierCache = new IdentifierCache_1.IdentifierCache(info.username);
798 }
799 // If it's bridge and there are no accessories already assigned to the bridge
800 // probably purge is not needed since it's going to delete all the ids
801 // of accessories that might be added later. Useful when dynamically adding
802 // accessories.
803 if (this._isBridge && this.bridgedAccessories.length === 0) {
804 this.disableUnusedIDPurge();
805 this.controllerStorage.purgeUnidentifiedAccessoryData = false;
806 }
807 if (!this.initialized) { // controller storage is only loaded from disk the first time we publish!
808 this.controllerStorage.load(info.username); // initializing controller data
809 }
810 // assign aid/iid
811 this._assignIDs(this._identifierCache);
812 // get our accessory information in HAP format and determine if our configuration (that is, our
813 // Accessories/Services/Characteristics) has changed since the last time we were published. make
814 // sure to omit actual values since these are not part of the "configuration".
815 const config = this.internalHAPRepresentation(false); // TODO ensure this stuff is ordered
816 // TODO queue this check until about 5 seconds after startup, allowing some last changes after the publish call
817 // without constantly incrementing the current config number
818 this._accessoryInfo.checkForCurrentConfigurationNumberIncrement(config, true);
819 this.validateAccessory(true);
820 // create our Advertiser which broadcasts our presence over mdns
821 const parsed = Accessory.parseBindOption(info);
822 let selectedAdvertiser = info.advertiser ?? "bonjour-hap" /* MDNSAdvertiser.BONJOUR */;
823 if ((info.advertiser === "avahi" /* MDNSAdvertiser.AVAHI */ && !await Advertiser_1.AvahiAdvertiser.isAvailable()) ||
824 (info.advertiser === "resolved" /* MDNSAdvertiser.RESOLVED */ && !await Advertiser_1.ResolvedAdvertiser.isAvailable())) {
825 console.error(`[${this.displayName}] The selected advertiser, "${info.advertiser}", isn't available on this platform. ` +
826 `Reverting to "${"bonjour-hap" /* MDNSAdvertiser.BONJOUR */}"`);
827 selectedAdvertiser = "bonjour-hap" /* MDNSAdvertiser.BONJOUR */;
828 }
829 switch (selectedAdvertiser) {
830 case "ciao" /* MDNSAdvertiser.CIAO */:
831 this._advertiser = new Advertiser_1.CiaoAdvertiser(this._accessoryInfo, {
832 interface: parsed.advertiserAddress,
833 }, {
834 restrictedAddresses: parsed.serviceRestrictedAddress,
835 disabledIpv6: parsed.serviceDisableIpv6,
836 });
837 break;
838 case "bonjour-hap" /* MDNSAdvertiser.BONJOUR */:
839 this._advertiser = new Advertiser_1.BonjourHAPAdvertiser(this._accessoryInfo, {
840 restrictedAddresses: parsed.serviceRestrictedAddress,
841 disabledIpv6: parsed.serviceDisableIpv6,
842 });
843 break;
844 case "avahi" /* MDNSAdvertiser.AVAHI */:
845 this._advertiser = new Advertiser_1.AvahiAdvertiser(this._accessoryInfo);
846 break;
847 case "resolved" /* MDNSAdvertiser.RESOLVED */:
848 this._advertiser = new Advertiser_1.ResolvedAdvertiser(this._accessoryInfo);
849 break;
850 default:
851 throw new Error("Unsupported advertiser setting: '" + info.advertiser + "'");
852 }
853 this._advertiser.on("updated-name" /* AdvertiserEvent.UPDATED_NAME */, name => {
854 this.displayName = name;
855 if (this._accessoryInfo) {
856 this._accessoryInfo.displayName = name;
857 this._accessoryInfo.save();
858 }
859 // bonjour service name MUST match the name in the accessory information service
860 this.getService(Service_1.Service.AccessoryInformation)
861 .updateCharacteristic(Characteristic_1.Characteristic.Name, name);
862 });
863 // create our HAP server which handles all communication between iOS devices and us
864 this._server = new HAPServer_1.HAPServer(this._accessoryInfo);
865 this._server.allowInsecureRequest = !!allowInsecureRequest;
866 this._server.on("listening" /* HAPServerEventTypes.LISTENING */, this.onListening.bind(this));
867 this._server.on("identify" /* HAPServerEventTypes.IDENTIFY */, this.identificationRequest.bind(this, false));
868 this._server.on("pair" /* HAPServerEventTypes.PAIR */, this.handleInitialPairSetupFinished.bind(this));
869 this._server.on("add-pairing" /* HAPServerEventTypes.ADD_PAIRING */, this.handleAddPairing.bind(this));
870 this._server.on("remove-pairing" /* HAPServerEventTypes.REMOVE_PAIRING */, this.handleRemovePairing.bind(this));
871 this._server.on("list-pairings" /* HAPServerEventTypes.LIST_PAIRINGS */, this.handleListPairings.bind(this));
872 this._server.on("accessories" /* HAPServerEventTypes.ACCESSORIES */, this.handleAccessories.bind(this));
873 this._server.on("get-characteristics" /* HAPServerEventTypes.GET_CHARACTERISTICS */, this.handleGetCharacteristics.bind(this));
874 this._server.on("set-characteristics" /* HAPServerEventTypes.SET_CHARACTERISTICS */, this.handleSetCharacteristics.bind(this));
875 this._server.on("connection-closed" /* HAPServerEventTypes.CONNECTION_CLOSED */, this.handleHAPConnectionClosed.bind(this));
876 this._server.on("request-resource" /* HAPServerEventTypes.REQUEST_RESOURCE */, this.handleResource.bind(this));
877 this._server.listen(info.port, parsed.serverAddress);
878 this.initialized = true;
879 }
880 /**
881 * Removes this Accessory from the local network
882 * Accessory object will no longer valid after invoking this method
883 * Trying to invoke publish() on the object will result undefined behavior
884 */
885 destroy() {
886 const promise = this.unpublish();
887 if (this._accessoryInfo) {
888 Accessory.cleanupAccessoryData(this._accessoryInfo.username);
889 this._accessoryInfo = undefined;
890 this._identifierCache = undefined;
891 this.controllerStorage = new ControllerStorage_1.ControllerStorage(this);
892 }
893 this.removeAllListeners();
894 return promise;
895 }
896 async unpublish() {
897 if (this._server) {
898 this._server.destroy();
899 this._server = undefined;
900 }
901 if (this._advertiser) {
902 // noinspection JSIgnoredPromiseFromCall
903 await this._advertiser.destroy();
904 this._advertiser = undefined;
905 }
906 }
907 enqueueConfigurationUpdate() {
908 if (this.configurationChangeDebounceTimeout) {
909 return; // already enqueued
910 }
911 this.configurationChangeDebounceTimeout = setTimeout(() => {
912 this.configurationChangeDebounceTimeout = undefined;
913 if (this._advertiser && this._advertiser) {
914 // get our accessory information in HAP format and determine if our configuration (that is, our
915 // Accessories/Services/Characteristics) has changed since the last time we were published. make
916 // sure to omit actual values since these are not part of the "configuration".
917 const config = this.internalHAPRepresentation(); // TODO ensure this stuff is ordered
918 if (this._accessoryInfo?.checkForCurrentConfigurationNumberIncrement(config)) {
919 this._advertiser.updateAdvertisement();
920 }
921 }
922 }, 1000);
923 this.configurationChangeDebounceTimeout.unref();
924 // 1s is fine, HomeKit is built that with configuration updates no iid or aid conflicts occur.
925 // Thus, the only thing happening when the txt update arrives late is already removed accessories/services
926 // not responding or new accessories/services not yet shown
927 }
928 onListening(port, hostname) {
929 (0, assert_1.default)(this._advertiser, "Advertiser wasn't created at onListening!");
930 // the HAP server is listening, so we can now start advertising our presence.
931 this._advertiser.initPort(port);
932 this._advertiser.startAdvertising()
933 .then(() => this.emit("advertised" /* AccessoryEventTypes.ADVERTISED */))
934 .catch(reason => {
935 console.error("Could not create mDNS advertisement. The HAP-Server won't be discoverable: " + reason);
936 if (reason.stack) {
937 debug("Detailed error: " + reason.stack);
938 }
939 });
940 this.emit("listening" /* AccessoryEventTypes.LISTENING */, port, hostname);
941 }
942 handleInitialPairSetupFinished(username, publicKey, callback) {
943 debug("[%s] Paired with client %s", this.displayName, username);
944 this._accessoryInfo && this._accessoryInfo.addPairedClient(username, publicKey, 1 /* PermissionTypes.ADMIN */);
945 this._accessoryInfo && this._accessoryInfo.save();
946 // update our advertisement, so it can pick up on the paired status of AccessoryInfo
947 this._advertiser && this._advertiser.updateAdvertisement();
948 callback();
949 this.emit("paired" /* AccessoryEventTypes.PAIRED */);
950 }
951 handleAddPairing(connection, username, publicKey, permission, callback) {
952 if (!this._accessoryInfo) {
953 callback(6 /* TLVErrorCode.UNAVAILABLE */);
954 return;
955 }
956 if (!this._accessoryInfo.hasAdminPermissions(connection.username)) {
957 callback(2 /* TLVErrorCode.AUTHENTICATION */);
958 return;
959 }
960 const existingKey = this._accessoryInfo.getClientPublicKey(username);
961 if (existingKey) {
962 if (existingKey.toString() !== publicKey.toString()) {
963 callback(1 /* TLVErrorCode.UNKNOWN */);
964 return;
965 }
966 this._accessoryInfo.updatePermission(username, permission);
967 }
968 else {
969 this._accessoryInfo.addPairedClient(username, publicKey, permission);
970 }
971 this._accessoryInfo.save();
972 // there should be no need to update advertisement
973 callback(0);
974 }
975 handleRemovePairing(connection, username, callback) {
976 if (!this._accessoryInfo) {
977 callback(6 /* TLVErrorCode.UNAVAILABLE */);
978 return;
979 }
980 if (!this._accessoryInfo.hasAdminPermissions(connection.username)) {
981 callback(2 /* TLVErrorCode.AUTHENTICATION */);
982 return;
983 }
984 this._accessoryInfo.removePairedClient(connection, username);
985 this._accessoryInfo.save();
986 callback(0); // first of all ensure the pairing is removed before we advertise availability again
987 if (!this._accessoryInfo.paired()) {
988 this._advertiser && this._advertiser.updateAdvertisement();
989 this.emit("unpaired" /* AccessoryEventTypes.UNPAIRED */);
990 this.handleAccessoryUnpairedForControllers();
991 for (const accessory of this.bridgedAccessories) {
992 accessory.handleAccessoryUnpairedForControllers();
993 }
994 }
995 }
996 handleListPairings(connection, callback) {
997 if (!this._accessoryInfo) {
998 callback(6 /* TLVErrorCode.UNAVAILABLE */);
999 return;
1000 }
1001 if (!this._accessoryInfo.hasAdminPermissions(connection.username)) {
1002 callback(2 /* TLVErrorCode.AUTHENTICATION */);
1003 return;
1004 }
1005 callback(0, this._accessoryInfo.listPairings());
1006 }
1007 handleAccessories(connection, callback) {
1008 this._assignIDs(this._identifierCache); // make sure our aid/iid's are all assigned
1009 const now = Date.now();
1010 const contactGetHandlers = now - this.lastAccessoriesRequest > 5_000; // we query the latest value if last /accessories was more than 5s ago
1011 this.lastAccessoriesRequest = now;
1012 this.toHAP(connection, contactGetHandlers).then(value => {
1013 callback(undefined, {
1014 accessories: value,
1015 });
1016 }, reason => {
1017 console.error("[" + this.displayName + "] /accessories request error with: " + reason.stack);
1018 callback({ httpCode: 500 /* HAPHTTPCode.INTERNAL_SERVER_ERROR */, status: -70402 /* HAPStatus.SERVICE_COMMUNICATION_FAILURE */ });
1019 });
1020 }
1021 handleGetCharacteristics(connection, request, callback) {
1022 const characteristics = [];
1023 const response = { characteristics: characteristics };
1024 const missingCharacteristics = new Set(request.ids.map(id => id.aid + "." + id.iid));
1025 if (missingCharacteristics.size !== request.ids.length) {
1026 // if those sizes differ, we have duplicates and can't properly handle that
1027 callback({ httpCode: 422 /* HAPHTTPCode.UNPROCESSABLE_ENTITY */, status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ });
1028 return;
1029 }
1030 let timeout = setTimeout(() => {
1031 for (const id of missingCharacteristics) {
1032 const split = id.split(".");
1033 const aid = parseInt(split[0], 10);
1034 const iid = parseInt(split[1], 10);
1035 const accessory = this.getAccessoryByAID(aid);
1036 const characteristic = accessory.getCharacteristicByIID(iid);
1037 this.sendCharacteristicWarning(characteristic, "slow-read" /* CharacteristicWarningType.SLOW_READ */, "The read handler for the characteristic '" +
1038 characteristic.displayName + "' on the accessory '" + accessory.displayName + "' was slow to respond!");
1039 }
1040 // after a total of 10s we do no longer wait for a request to appear and just return status code timeout
1041 timeout = setTimeout(() => {
1042 timeout = undefined;
1043 for (const id of missingCharacteristics) {
1044 const split = id.split(".");
1045 const aid = parseInt(split[0], 10);
1046 const iid = parseInt(split[1], 10);
1047 const accessory = this.getAccessoryByAID(aid);
1048 const characteristic = accessory.getCharacteristicByIID(iid);
1049 this.sendCharacteristicWarning(characteristic, "timeout-read" /* CharacteristicWarningType.TIMEOUT_READ */, "The read handler for the characteristic '" +
1050 characteristic.displayName + "' on the accessory '" + accessory.displayName + "' didn't respond at all!. " +
1051 "Please check that you properly call the callback!");
1052 characteristics.push({
1053 aid: aid,
1054 iid: iid,
1055 status: -70408 /* HAPStatus.OPERATION_TIMED_OUT */,
1056 });
1057 }
1058 missingCharacteristics.clear();
1059 callback(undefined, response);
1060 }, Accessory.TIMEOUT_AFTER_WARNING);
1061 timeout.unref();
1062 }, Accessory.TIMEOUT_WARNING);
1063 timeout.unref();
1064 for (const id of request.ids) {
1065 const name = id.aid + "." + id.iid;
1066 this.handleCharacteristicRead(connection, id, request).then(value => {
1067 return {
1068 aid: id.aid,
1069 iid: id.iid,
1070 ...value,
1071 };
1072 }, reason => {
1073 console.error(`[${this.displayName}] Read request for characteristic ${name} encountered an error: ${reason.stack}`);
1074 return {
1075 aid: id.aid,
1076 iid: id.iid,
1077 status: -70402 /* HAPStatus.SERVICE_COMMUNICATION_FAILURE */,
1078 };
1079 }).then(value => {
1080 if (!timeout) {
1081 return; // if timeout is undefined, response was already sent out
1082 }
1083 missingCharacteristics.delete(name);
1084 characteristics.push(value);
1085 if (missingCharacteristics.size === 0) {
1086 if (timeout) {
1087 clearTimeout(timeout);
1088 timeout = undefined;
1089 }
1090 callback(undefined, response);
1091 }
1092 });
1093 }
1094 }
1095 async handleCharacteristicRead(connection, id, request) {
1096 const characteristic = this.findCharacteristic(id.aid, id.iid);
1097 if (!characteristic) {
1098 debug("[%s] Could not find a Characteristic with aid of %s and iid of %s", this.displayName, id.aid, id.iid);
1099 return { status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ };
1100 }
1101 if (!characteristic.props.perms.includes("pr" /* Perms.PAIRED_READ */)) { // check if read is allowed for this characteristic
1102 debug("[%s] Tried reading from characteristic which does not allow reading (aid of %s and iid of %s)", this.displayName, id.aid, id.iid);
1103 return { status: -70405 /* HAPStatus.WRITE_ONLY_CHARACTERISTIC */ };
1104 }
1105 if (characteristic.props.adminOnlyAccess && characteristic.props.adminOnlyAccess.includes(0 /* Access.READ */)) {
1106 const verifiable = this._accessoryInfo && connection.username;
1107 if (!verifiable) {
1108 debug("[%s] Could not verify admin permissions for Characteristic which requires admin permissions for reading (aid of %s and iid of %s)", this.displayName, id.aid, id.iid);
1109 }
1110 if (!verifiable || !this._accessoryInfo.hasAdminPermissions(connection.username)) {
1111 return { status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ };
1112 }
1113 }
1114 return characteristic.handleGetRequest(connection).then(value => {
1115 value = (0, request_util_1.formatOutgoingCharacteristicValue)(value, characteristic.props);
1116 debug("[%s] Got Characteristic \"%s\" value: \"%s\"", this.displayName, characteristic.displayName, value);
1117 const data = {
1118 value: value == null ? null : value,
1119 };
1120 if (request.includeMeta) {
1121 data.format = characteristic.props.format;
1122 data.unit = characteristic.props.unit;
1123 data.minValue = characteristic.props.minValue;
1124 data.maxValue = characteristic.props.maxValue;
1125 data.minStep = characteristic.props.minStep;
1126 data.maxLen = characteristic.props.maxLen || characteristic.props.maxDataLen;
1127 }
1128 if (request.includePerms) {
1129 data.perms = characteristic.props.perms;
1130 }
1131 if (request.includeType) {
1132 data.type = (0, uuid_1.toShortForm)(characteristic.UUID);
1133 }
1134 if (request.includeEvent) {
1135 data.ev = connection.hasEventNotifications(id.aid, id.iid);
1136 }
1137 return data;
1138 }, (reason) => {
1139 // @ts-expect-error: preserveConstEnums compiler option
1140 debug("[%s] Error getting value for characteristic \"%s\": %s", this.displayName, characteristic.displayName, HAPServer_1.HAPStatus[reason]);
1141 return { status: reason };
1142 });
1143 }
1144 handleSetCharacteristics(connection, writeRequest, callback) {
1145 debug("[%s] Processing characteristic set: %s", this.displayName, JSON.stringify(writeRequest));
1146 let writeState = 0 /* WriteRequestState.REGULAR_REQUEST */;
1147 if (writeRequest.pid !== undefined) { // check for timed writes
1148 if (connection.timedWritePid === writeRequest.pid) {
1149 writeState = 1 /* WriteRequestState.TIMED_WRITE_AUTHENTICATED */;
1150 clearTimeout(connection.timedWriteTimeout);
1151 connection.timedWritePid = undefined;
1152 connection.timedWriteTimeout = undefined;
1153 debug("[%s] Timed write request got acknowledged for pid %d", this.displayName, writeRequest.pid);
1154 }
1155 else {
1156 writeState = 2 /* WriteRequestState.TIMED_WRITE_REJECTED */;
1157 debug("[%s] TTL for timed write request has probably expired for pid %d", this.displayName, writeRequest.pid);
1158 }
1159 }
1160 const characteristics = [];
1161 const response = { characteristics: characteristics };
1162 const missingCharacteristics = new Set(writeRequest.characteristics
1163 .map(characteristic => characteristic.aid + "." + characteristic.iid));
1164 if (missingCharacteristics.size !== writeRequest.characteristics.length) {
1165 // if those sizes differ, we have duplicates and can't properly handle that
1166 callback({ httpCode: 422 /* HAPHTTPCode.UNPROCESSABLE_ENTITY */, status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ });
1167 return;
1168 }
1169 let timeout = setTimeout(() => {
1170 for (const id of missingCharacteristics) {
1171 const split = id.split(".");
1172 const aid = parseInt(split[0], 10);
1173 const iid = parseInt(split[1], 10);
1174 const accessory = this.getAccessoryByAID(aid);
1175 const characteristic = accessory.getCharacteristicByIID(iid);
1176 this.sendCharacteristicWarning(characteristic, "slow-write" /* CharacteristicWarningType.SLOW_WRITE */, "The write handler for the characteristic '" +
1177 characteristic.displayName + "' on the accessory '" + accessory.displayName + "' was slow to respond!");
1178 }
1179 // after a total of 10s we do no longer wait for a request to appear and just return status code timeout
1180 timeout = setTimeout(() => {
1181 timeout = undefined;
1182 for (const id of missingCharacteristics) {
1183 const split = id.split(".");
1184 const aid = parseInt(split[0], 10);
1185 const iid = parseInt(split[1], 10);
1186 const accessory = this.getAccessoryByAID(aid);
1187 const characteristic = accessory.getCharacteristicByIID(iid);
1188 this.sendCharacteristicWarning(characteristic, "timeout-write" /* CharacteristicWarningType.TIMEOUT_WRITE */, "The write handler for the characteristic '" +
1189 characteristic.displayName + "' on the accessory '" + accessory.displayName + "' didn't respond at all!. " +
1190 "Please check that you properly call the callback!");
1191 characteristics.push({
1192 aid: aid,
1193 iid: iid,
1194 status: -70408 /* HAPStatus.OPERATION_TIMED_OUT */,
1195 });
1196 }
1197 missingCharacteristics.clear();
1198 callback(undefined, response);
1199 }, Accessory.TIMEOUT_AFTER_WARNING);
1200 timeout.unref();
1201 }, Accessory.TIMEOUT_WARNING);
1202 timeout.unref();
1203 for (const data of writeRequest.characteristics) {
1204 const name = data.aid + "." + data.iid;
1205 this.handleCharacteristicWrite(connection, data, writeState).then(value => {
1206 return {
1207 aid: data.aid,
1208 iid: data.iid,
1209 ...value,
1210 };
1211 }, reason => {
1212 console.error(`[${this.displayName}] Write request for characteristic ${name} encountered an error: ${reason.stack}`);
1213 return {
1214 aid: data.aid,
1215 iid: data.iid,
1216 status: -70402 /* HAPStatus.SERVICE_COMMUNICATION_FAILURE */,
1217 };
1218 }).then(value => {
1219 if (!timeout) {
1220 return; // if timeout is undefined, response was already sent out
1221 }
1222 missingCharacteristics.delete(name);
1223 characteristics.push(value);
1224 if (missingCharacteristics.size === 0) { // if everything returned send the response
1225 if (timeout) {
1226 clearTimeout(timeout);
1227 timeout = undefined;
1228 }
1229 callback(undefined, response);
1230 }
1231 });
1232 }
1233 }
1234 async handleCharacteristicWrite(connection, data, writeState) {
1235 const characteristic = this.findCharacteristic(data.aid, data.iid);
1236 if (!characteristic) {
1237 debug("[%s] Could not find a Characteristic with aid of %s and iid of %s", this.displayName, data.aid, data.iid);
1238 return { status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ };
1239 }
1240 if (writeState === 2 /* WriteRequestState.TIMED_WRITE_REJECTED */) {
1241 return { status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ };
1242 }
1243 if (data.ev == null && data.value == null) {
1244 return { status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ };
1245 }
1246 if (data.ev != null) { // register/unregister event notifications
1247 if (!characteristic.props.perms.includes("ev" /* Perms.NOTIFY */)) { // check if notify is allowed for this characteristic
1248 debug("[%s] Tried %s notifications for Characteristic which does not allow notify (aid of %s and iid of %s)", this.displayName, data.ev ? "enabling" : "disabling", data.aid, data.iid);
1249 return { status: -70406 /* HAPStatus.NOTIFICATION_NOT_SUPPORTED */ };
1250 }
1251 if (characteristic.props.adminOnlyAccess && characteristic.props.adminOnlyAccess.includes(2 /* Access.NOTIFY */)) {
1252 const verifiable = connection.username && this._accessoryInfo;
1253 if (!verifiable) {
1254 debug("[%s] Could not verify admin permissions for Characteristic which requires admin permissions for notify (aid of %s and iid of %s)", this.displayName, data.aid, data.iid);
1255 }
1256 if (!verifiable || !this._accessoryInfo.hasAdminPermissions(connection.username)) {
1257 return { status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ };
1258 }
1259 }
1260 const notificationsEnabled = connection.hasEventNotifications(data.aid, data.iid);
1261 if (data.ev && !notificationsEnabled) {
1262 connection.enableEventNotifications(data.aid, data.iid);
1263 characteristic.subscribe();
1264 debug("[%s] Registered Characteristic \"%s\" on \"%s\" for events", connection.remoteAddress, characteristic.displayName, this.displayName);
1265 }
1266 else if (!data.ev && notificationsEnabled) {
1267 characteristic.unsubscribe();
1268 connection.disableEventNotifications(data.aid, data.iid);
1269 debug("[%s] Unregistered Characteristic \"%s\" on \"%s\" for events", connection.remoteAddress, characteristic.displayName, this.displayName);
1270 }
1271 // response is returned below in the else block
1272 }
1273 if (data.value != null) {
1274 if (!characteristic.props.perms.includes("pw" /* Perms.PAIRED_WRITE */)) { // check if write is allowed for this characteristic
1275 debug("[%s] Tried writing to Characteristic which does not allow writing (aid of %s and iid of %s)", this.displayName, data.aid, data.iid);
1276 return { status: -70404 /* HAPStatus.READ_ONLY_CHARACTERISTIC */ };
1277 }
1278 if (characteristic.props.adminOnlyAccess && characteristic.props.adminOnlyAccess.includes(1 /* Access.WRITE */)) {
1279 const verifiable = connection.username && this._accessoryInfo;
1280 if (!verifiable) {
1281 debug("[%s] Could not verify admin permissions for Characteristic which requires admin permissions for write (aid of %s and iid of %s)", this.displayName, data.aid, data.iid);
1282 }
1283 if (!verifiable || !this._accessoryInfo.hasAdminPermissions(connection.username)) {
1284 return { status: -70401 /* HAPStatus.INSUFFICIENT_PRIVILEGES */ };
1285 }
1286 }
1287 if (characteristic.props.perms.includes("aa" /* Perms.ADDITIONAL_AUTHORIZATION */) && characteristic.additionalAuthorizationHandler) {
1288 // if the characteristic "supports additional authorization" but doesn't define a handler for the check
1289 // we conclude that the characteristic doesn't want to check the authData (currently) and just allows access for everybody
1290 let allowWrite;
1291 try {
1292 allowWrite = characteristic.additionalAuthorizationHandler(data.authData);
1293 }
1294 catch (error) {
1295 console.warn("[" + this.displayName + "] Additional authorization handler has thrown an error when checking authData: " + error.stack);
1296 allowWrite = false;
1297 }
1298 if (!allowWrite) {
1299 return { status: -70411 /* HAPStatus.INSUFFICIENT_AUTHORIZATION */ };
1300 }
1301 }
1302 if (characteristic.props.perms.includes("tw" /* Perms.TIMED_WRITE */) && writeState !== 1 /* WriteRequestState.TIMED_WRITE_AUTHENTICATED */) {
1303 debug("[%s] Tried writing to a timed write only Characteristic without properly preparing (iid of %s and aid of %s)", this.displayName, data.aid, data.iid);
1304 return { status: -70410 /* HAPStatus.INVALID_VALUE_IN_REQUEST */ };
1305 }
1306 return characteristic.handleSetRequest(data.value, connection).then(value => {
1307 debug("[%s] Setting Characteristic \"%s\" to value %s", this.displayName, characteristic.displayName, data.value);
1308 return {
1309 // if write response is requests and value is provided, return that
1310 value: data.r && value ? (0, request_util_1.formatOutgoingCharacteristicValue)(value, characteristic.props) : undefined,
1311 status: 0 /* HAPStatus.SUCCESS */,
1312 };
1313 }, (status) => {
1314 // @ts-expect-error: forceConsistentCasingInFileNames compiler option
1315 debug("[%s] Error setting Characteristic \"%s\" to value %s: ", this.displayName, characteristic.displayName, data.value, HAPServer_1.HAPStatus[status]);
1316 return { status: status };
1317 });
1318 }
1319 return { status: 0 /* HAPStatus.SUCCESS */ };
1320 }
1321 handleResource(data, callback) {
1322 if (data["resource-type"] === "image" /* ResourceRequestType.IMAGE */) {
1323 const aid = data.aid; // aid is optionally supplied by HomeKit (for example when camera is bridged, multiple cams, etc)
1324 let accessory = undefined;
1325 let controller = undefined;
1326 if (aid) {
1327 accessory = this.getAccessoryByAID(aid);
1328 if (accessory && accessory.activeCameraController) {
1329 controller = accessory.activeCameraController;
1330 }
1331 }
1332 else if (this.activeCameraController) { // aid was not supplied, check if this accessory is a camera
1333 // eslint-disable-next-line @typescript-eslint/no-this-alias
1334 accessory = this;
1335 controller = this.activeCameraController;
1336 }
1337 if (!controller) {
1338 debug("[%s] received snapshot request though no camera controller was associated!");
1339 callback({ httpCode: 404 /* HAPHTTPCode.NOT_FOUND */, status: -70409 /* HAPStatus.RESOURCE_DOES_NOT_EXIST */ });
1340 return;
1341 }
1342 controller.handleSnapshotRequest(data["image-height"], data["image-width"], accessory?.displayName, data.reason)
1343 .then(buffer => {
1344 callback(undefined, buffer);
1345 }, (status) => {
1346 callback({ httpCode: 207 /* HAPHTTPCode.MULTI_STATUS */, status: status });
1347 });
1348 return;
1349 }
1350 debug("[%s] received request for unsupported image type: " + data["resource-type"], this._accessoryInfo?.username);
1351 callback({ httpCode: 404 /* HAPHTTPCode.NOT_FOUND */, status: -70409 /* HAPStatus.RESOURCE_DOES_NOT_EXIST */ });
1352 }
1353 handleHAPConnectionClosed(connection) {
1354 for (const event of connection.getRegisteredEvents()) {
1355 const ids = event.split(".");
1356 const aid = parseInt(ids[0], 10);
1357 const iid = parseInt(ids[1], 10);
1358 const characteristic = this.findCharacteristic(aid, iid);
1359 if (characteristic) {
1360 characteristic.unsubscribe();
1361 }
1362 }
1363 connection.clearRegisteredEvents();
1364 }
1365 handleServiceConfigurationChangeEvent(service) {
1366 if (!service.isPrimaryService && service === this.primaryService) {
1367 // service changed form primary to non-primary service
1368 this.primaryService = undefined;
1369 }
1370 else if (service.isPrimaryService && service !== this.primaryService) {
1371 // service changed from non-primary to primary service
1372 if (this.primaryService !== undefined) {
1373 this.primaryService.isPrimaryService = false;
1374 }
1375 this.primaryService = service;
1376 }
1377 if (this.bridged) {
1378 this.emit("service-configurationChange" /* AccessoryEventTypes.SERVICE_CONFIGURATION_CHANGE */, { service: service });
1379 }
1380 else {
1381 this.enqueueConfigurationUpdate();
1382 }
1383 }
1384 handleCharacteristicChangeEvent(accessory, service, change) {
1385 if (this.bridged) { // forward this to our main accessory
1386 this.emit("service-characteristic-change" /* AccessoryEventTypes.SERVICE_CHARACTERISTIC_CHANGE */, { ...change, service: service });
1387 }
1388 else {
1389 if (!this._server) {
1390 return; // we're not running a HAPServer, so there's no one to notify about this event
1391 }
1392 if (accessory.aid == null || change.characteristic.iid == null) {
1393 debug("[%s] Muting event notification for %s as ids aren't yet assigned!", accessory.displayName, change.characteristic.displayName);
1394 return;
1395 }
1396 if (change.context != null && typeof change.context === "object" && change.context.omitEventUpdate) {
1397 debug("[%s] Omitting event updates for %s as specified in the context object!", accessory.displayName, change.characteristic.displayName);
1398 return;
1399 }
1400 if (!(change.reason === "event" /* ChangeReason.EVENT */ || change.oldValue !== change.newValue
1401 || change.characteristic.UUID === Characteristic_1.Characteristic.ProgrammableSwitchEvent.UUID // those specific checks are out of backwards compatibility
1402 || change.characteristic.UUID === Characteristic_1.Characteristic.ButtonEvent.UUID // new characteristics should use sendEventNotification call
1403 )) {
1404 // we only emit a change event if the reason was a call to sendEventNotification, if the value changed
1405 // as of a write request or a read request or if the change happened on dedicated event characteristics
1406 // otherwise we ignore this change event (with the return below)
1407 return;
1408 }
1409 const uuid = change.characteristic.UUID;
1410 const immediateDelivery = uuid === Characteristic_1.Characteristic.ButtonEvent.UUID || uuid === Characteristic_1.Characteristic.ProgrammableSwitchEvent.UUID
1411 || uuid === Characteristic_1.Characteristic.MotionDetected.UUID || uuid === Characteristic_1.Characteristic.ContactSensorState.UUID;
1412 const value = (0, request_util_1.formatOutgoingCharacteristicValue)(change.newValue, change.characteristic.props);
1413 this._server.sendEventNotifications(accessory.aid, change.characteristic.iid, value, change.originator, immediateDelivery);
1414 }
1415 }
1416 sendCharacteristicWarning(characteristic, type, message) {
1417 this.handleCharacteristicWarning({
1418 characteristic: characteristic,
1419 type: type,
1420 message: message,
1421 originatorChain: [characteristic.displayName], // we are missing the service displayName, but that's okay
1422 stack: new Error().stack,
1423 });
1424 }
1425 handleCharacteristicWarning(warning) {
1426 warning.originatorChain = [this.displayName, ...warning.originatorChain];
1427 const emitted = this.emit("characteristic-warning" /* AccessoryEventTypes.CHARACTERISTIC_WARNING */, warning);
1428 if (!emitted) {
1429 const message = `[${warning.originatorChain.join("@")}] ${warning.message}`;
1430 if (warning.type === "error-message" /* CharacteristicWarningType.ERROR_MESSAGE */
1431 || warning.type === "timeout-read" /* CharacteristicWarningType.TIMEOUT_READ */ || warning.type === "timeout-write" /* CharacteristicWarningType.TIMEOUT_WRITE */) {
1432 console.error(message);
1433 }
1434 else {
1435 console.warn(message);
1436 }
1437 debug("[%s] Above characteristic warning was thrown at: %s", this.displayName, warning.stack ?? "unknown");
1438 }
1439 }
1440 setupServiceEventHandlers(service) {
1441 service.on("service-configurationChange" /* ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE */, this.handleServiceConfigurationChangeEvent.bind(this, service));
1442 service.on("characteristic-change" /* ServiceEventTypes.CHARACTERISTIC_CHANGE */, this.handleCharacteristicChangeEvent.bind(this, this, service));
1443 service.on("characteristic-warning" /* ServiceEventTypes.CHARACTERISTIC_WARNING */, this.handleCharacteristicWarning.bind(this));
1444 }
1445 _sideloadServices(targetServices) {
1446 for (const service of targetServices) {
1447 this.setupServiceEventHandlers(service);
1448 }
1449 this.services = targetServices.slice();
1450 // Fix Identify
1451 this
1452 .getService(Service_1.Service.AccessoryInformation)
1453 .getCharacteristic(Characteristic_1.Characteristic.Identify)
1454 .on("set" /* CharacteristicEventTypes.SET */, (value, callback) => {
1455 if (value) {
1456 const paired = true;
1457 this.identificationRequest(paired, callback);
1458 }
1459 });
1460 }
1461 static _generateSetupID() {
1462 const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1463 const max = chars.length;
1464 let setupID = "";
1465 for (let i = 0; i < 4; i++) {
1466 const index = Math.floor(Math.random() * max);
1467 setupID += chars.charAt(index);
1468 }
1469 return setupID;
1470 }
1471 // serialization and deserialization functions, mainly designed for homebridge to create a json copy to store on disk
1472 static serialize(accessory) {
1473 const json = {
1474 displayName: accessory.displayName,
1475 UUID: accessory.UUID,
1476 lastKnownUsername: accessory._accessoryInfo ? accessory._accessoryInfo.username : undefined,
1477 category: accessory.category,
1478 services: [],
1479 };
1480 const linkedServices = {};
1481 let hasLinkedServices = false;
1482 accessory.services.forEach(service => {
1483 json.services.push(Service_1.Service.serialize(service));
1484 const linkedServicesPresentation = [];
1485 service.linkedServices.forEach(linkedService => {
1486 linkedServicesPresentation.push(linkedService.getServiceId());
1487 });
1488 if (linkedServicesPresentation.length > 0) {
1489 linkedServices[service.getServiceId()] = linkedServicesPresentation;
1490 hasLinkedServices = true;
1491 }
1492 });
1493 if (hasLinkedServices) {
1494 json.linkedServices = linkedServices;
1495 }
1496 const controllers = [];
1497 // save controllers
1498 Object.values(accessory.controllers).forEach((context) => {
1499 controllers.push({
1500 type: context.controller.controllerId(),
1501 services: Accessory.serializeServiceMap(context.serviceMap),
1502 });
1503 });
1504 // also save controller which didn't get initialized (could lead to service duplication if we throw that data away)
1505 accessory.serializedControllers && Object.entries(accessory.serializedControllers).forEach(([id, serviceMap]) => {
1506 controllers.push({
1507 type: id,
1508 services: Accessory.serializeServiceMap(serviceMap),
1509 });
1510 });
1511 if (controllers.length > 0) {
1512 json.controllers = controllers;
1513 }
1514 return json;
1515 }
1516 static deserialize(json) {
1517 const accessory = new Accessory(json.displayName, json.UUID);
1518 accessory.lastKnownUsername = json.lastKnownUsername;
1519 accessory.category = json.category;
1520 const services = [];
1521 const servicesMap = {};
1522 json.services.forEach(serialized => {
1523 const service = Service_1.Service.deserialize(serialized);
1524 services.push(service);
1525 servicesMap[service.getServiceId()] = service;
1526 });
1527 if (json.linkedServices) {
1528 for (const [serviceId, linkedServicesKeys] of Object.entries(json.linkedServices)) {
1529 const primaryService = servicesMap[serviceId];
1530 if (!primaryService) {
1531 continue;
1532 }
1533 linkedServicesKeys.forEach(linkedServiceKey => {
1534 const linkedService = servicesMap[linkedServiceKey];
1535 if (linkedService) {
1536 primaryService.addLinkedService(linkedService);
1537 }
1538 });
1539 }
1540 }
1541 if (json.controllers) { // just save it for later if it exists {@see configureController}
1542 accessory.serializedControllers = {};
1543 json.controllers.forEach(serializedController => {
1544 accessory.serializedControllers[serializedController.type] = Accessory.deserializeServiceMap(serializedController.services, servicesMap);
1545 });
1546 }
1547 accessory._sideloadServices(services);
1548 return accessory;
1549 }
1550 static cleanupAccessoryData(username) {
1551 IdentifierCache_1.IdentifierCache.remove(username);
1552 AccessoryInfo_1.AccessoryInfo.remove(username);
1553 ControllerStorage_1.ControllerStorage.remove(username);
1554 }
1555 static serializeServiceMap(serviceMap) {
1556 const serialized = {};
1557 Object.entries(serviceMap).forEach(([name, service]) => {
1558 if (!service) {
1559 return;
1560 }
1561 serialized[name] = service.getServiceId();
1562 });
1563 return serialized;
1564 }
1565 static deserializeServiceMap(serializedServiceMap, servicesMap) {
1566 const controllerServiceMap = {};
1567 Object.entries(serializedServiceMap).forEach(([name, serviceId]) => {
1568 const service = servicesMap[serviceId];
1569 if (service) {
1570 controllerServiceMap[name] = service;
1571 }
1572 });
1573 return controllerServiceMap;
1574 }
1575 static parseBindOption(info) {
1576 let advertiserAddress = undefined;
1577 let disableIpv6 = undefined;
1578 let serverAddress = undefined;
1579 if (info.bind) {
1580 const entries = new Set(Array.isArray(info.bind) ? info.bind : [info.bind]);
1581 if (entries.has("::")) {
1582 serverAddress = "::";
1583 entries.delete("::");
1584 if (entries.size) {
1585 advertiserAddress = Array.from(entries);
1586 }
1587 }
1588 else if (entries.has("0.0.0.0")) {
1589 disableIpv6 = true;
1590 serverAddress = "0.0.0.0";
1591 entries.delete("0.0.0.0");
1592 if (entries.size) {
1593 advertiserAddress = Array.from(entries);
1594 }
1595 }
1596 else if (entries.size === 1) {
1597 advertiserAddress = Array.from(entries);
1598 const entry = entries.values().next().value; // grab the first one
1599 const version = net_1.default.isIP(entry); // check if ip address was specified or an interface name
1600 if (version) {
1601 serverAddress = version === 4 ? "0.0.0.0" : "::"; // we currently bind to unspecified addresses so config-ui always has a connection via loopback
1602 }
1603 else {
1604 serverAddress = "::"; // the interface could have both ipv4 and ipv6 addresses
1605 }
1606 }
1607 else if (entries.size > 1) {
1608 advertiserAddress = Array.from(entries);
1609 let bindUnspecifiedIpv6 = false; // we bind on "::" if there are interface names, or we detect ipv6 addresses
1610 for (const entry of entries) {
1611 const version = net_1.default.isIP(entry);
1612 if (version === 0 || version === 6) {
1613 bindUnspecifiedIpv6 = true;
1614 break;
1615 }
1616 }
1617 if (bindUnspecifiedIpv6) {
1618 serverAddress = "::";
1619 }
1620 else {
1621 serverAddress = "0.0.0.0";
1622 }
1623 }
1624 }
1625 return {
1626 advertiserAddress: advertiserAddress,
1627 serviceRestrictedAddress: advertiserAddress,
1628 serviceDisableIpv6: disableIpv6,
1629 serverAddress: serverAddress,
1630 };
1631 }
1632}
1633exports.Accessory = Accessory;
1634//# sourceMappingURL=Accessory.js.map
\No newline at end of file