• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

homebridge / HAP-NodeJS / 24964733939

26 Apr 2026 07:10PM UTC coverage: 64.646% (+1.2%) from 63.43%
24964733939

Pull #1113

github

web-flow
Merge 472e7d100 into 6098a0053
Pull Request #1113: v2.1.3

1863 of 3360 branches covered (55.45%)

Branch coverage included in aggregate %.

53 of 133 new or added lines in 8 files covered. (39.85%)

6413 of 9442 relevant lines covered (67.92%)

308.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.6
/src/lib/Accessory.ts
1
import assert from "assert";
9✔
2
import crypto from "crypto";
9✔
3
import createDebug from "debug";
9✔
4
import { EventEmitter } from "events";
9✔
5
import net from "net";
9✔
6
import {
7
  AccessoryJsonObject,
8
  CharacteristicId,
9
  CharacteristicReadData,
10
  CharacteristicsReadRequest,
11
  CharacteristicsReadResponse,
12
  CharacteristicsWriteRequest,
13
  CharacteristicsWriteResponse,
14
  CharacteristicValue,
15
  CharacteristicWrite,
16
  CharacteristicWriteData,
17
  ConstructorArgs,
18
  HAPPincode,
19
  InterfaceName,
20
  IPAddress,
21
  MacAddress,
22
  Nullable,
23
  PartialCharacteristicReadData,
24
  PartialCharacteristicWriteData,
25
  ResourceRequest,
26
  ResourceRequestType,
27
  VoidCallback,
28
  WithUUID,
29
} from "../types";
30
import { Advertiser, AdvertiserEvent, AvahiAdvertiser, BonjourHAPAdvertiser, CiaoAdvertiser, ResolvedAdvertiser } from "./Advertiser";
9✔
31
// noinspection JSDeprecatedSymbols
32
import {
9✔
33
  Access,
34
  ChangeReason,
35
  Characteristic,
36
  CharacteristicEventTypes,
37
  CharacteristicOperationContext,
38
  CharacteristicSetCallback,
39
  Perms,
40
} from "./Characteristic";
41
import {
9✔
42
  CameraController,
43
  Controller,
44
  ControllerConstructor,
45
  ControllerIdentifier,
46
  ControllerServiceMap,
47
  isSerializableController,
48
} from "./controller";
49
import {
9✔
50
  AccessoriesCallback,
51
  AddPairingCallback,
52
  HAPHTTPCode,
53
  HAPServer,
54
  HAPServerEventTypes,
55
  HAPStatus,
56
  IdentifyCallback,
57
  ListPairingsCallback,
58
  PairCallback,
59
  ReadCharacteristicsCallback,
60
  RemovePairingCallback,
61
  ResourceRequestCallback,
62
  TLVErrorCode,
63
  WriteCharacteristicsCallback,
64
} from "./HAPServer";
65
import { AccessoryInfo, PermissionTypes } from "./model/AccessoryInfo";
9✔
66
import { ControllerStorage } from "./model/ControllerStorage";
9✔
67
import { IdentifierCache } from "./model/IdentifierCache";
9✔
68
import { SerializedService, Service, ServiceCharacteristicChange, ServiceEventTypes, ServiceId } from "./Service";
9✔
69
import { clone } from "./util/clone";
9✔
70
import { EventName, HAPConnection, HAPUsername } from "./util/eventedhttp";
71
import { formatOutgoingCharacteristicValue } from "./util/request-util";
9✔
72
import * as uuid from "./util/uuid";
9✔
73
import { toShortForm } from "./util/uuid";
9✔
74
import { checkName } from "./util/checkName";
9✔
75

76
const debug = createDebug("HAP-NodeJS:Accessory");
9✔
77
const MAX_ACCESSORIES = 149; // Maximum number of bridged accessories per bridge.
9✔
78
const MAX_SERVICES = 100;
9✔
79

80
/**
81
 * Known category values. Category is a hint to iOS clients about what "type" of Accessory this represents, for UI only.
82
 *
83
 * @group Accessory
84
 */
85
export const enum Categories {
9✔
86
  // noinspection JSUnusedGlobalSymbols
87
  OTHER = 1,
9✔
88
  BRIDGE = 2,
9✔
89
  FAN = 3,
9✔
90
  GARAGE_DOOR_OPENER = 4,
9✔
91
  LIGHTBULB = 5,
9✔
92
  DOOR_LOCK = 6,
9✔
93
  OUTLET = 7,
9✔
94
  SWITCH = 8,
9✔
95
  THERMOSTAT = 9,
9✔
96
  SENSOR = 10,
9✔
97
  ALARM_SYSTEM = 11,
9✔
98
  // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
99
  SECURITY_SYSTEM = 11, //Added to conform to HAP naming
9✔
100
  DOOR = 12,
9✔
101
  WINDOW = 13,
9✔
102
  WINDOW_COVERING = 14,
9✔
103
  PROGRAMMABLE_SWITCH = 15,
9✔
104
  RANGE_EXTENDER = 16,
9✔
105
  CAMERA = 17,
9✔
106
  // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
107
  IP_CAMERA = 17, //Added to conform to HAP naming
9✔
108
  VIDEO_DOORBELL = 18,
9✔
109
  AIR_PURIFIER = 19,
9✔
110
  AIR_HEATER = 20,
9✔
111
  AIR_CONDITIONER = 21,
9✔
112
  AIR_HUMIDIFIER = 22,
9✔
113
  AIR_DEHUMIDIFIER = 23,
9✔
114
  APPLE_TV = 24,
9✔
115
  HOMEPOD = 25,
9✔
116
  SPEAKER = 26,
9✔
117
  AIRPORT = 27,
9✔
118
  SPRINKLER = 28,
9✔
119
  FAUCET = 29,
9✔
120
  SHOWER_HEAD = 30,
9✔
121
  TELEVISION = 31,
9✔
122
  TARGET_CONTROLLER = 32, // Remote Control
9✔
123
  ROUTER = 33,
9✔
124
  AUDIO_RECEIVER = 34,
9✔
125
  TV_SET_TOP_BOX = 35,
9✔
126
  TV_STREAMING_STICK = 36,
9✔
127
}
128

129
/**
130
 * @group Accessory
131
 */
132
export interface SerializedAccessory {
133
  displayName: string,
134
  UUID: string,
135
  lastKnownUsername?: MacAddress,
136
  category: Categories,
137

138
  services: SerializedService[],
139
  linkedServices?: Record<ServiceId, ServiceId[]>,
140
  controllers?: SerializedControllerContext[],
141
}
142

143
/**
144
 * @group Controller API
145
 */
146
export interface SerializedControllerContext {
147
  type: ControllerIdentifier, // this field is called type out of history
148
  services: SerializedServiceMap,
149
}
150

151
/**
152
 * @group Controller API
153
 */
154
export type SerializedServiceMap = Record<string, ServiceId>; // maps controller defined name (from the ControllerServiceMap) to serviceId
155

156
/**
157
 * @group Controller API
158
 */
159
export interface ControllerContext {
160
  controller: Controller
161
  serviceMap: ControllerServiceMap,
162
}
163

164
/**
165
 * @group Accessory
166
 */
167
export const enum CharacteristicWarningType {
9✔
168
  SLOW_WRITE = "slow-write",
9✔
169
  TIMEOUT_WRITE = "timeout-write",
9✔
170
  SLOW_READ = "slow-read",
9✔
171
  TIMEOUT_READ = "timeout-read",
9✔
172
  WARN_MESSAGE = "warn-message",
9✔
173
  ERROR_MESSAGE = "error-message",
9✔
174
  DEBUG_MESSAGE = "debug-message",
9✔
175
}
176

177
/**
178
 * @group Accessory
179
 */
180
export interface CharacteristicWarning {
181
  characteristic: Characteristic,
182
  type: CharacteristicWarningType,
183
  message: string,
184
  originatorChain: string[],
185
  stack?: string,
186
}
187

188
/**
189
 * @group Accessory
190
 */
191
export interface PublishInfo {
192
  username: MacAddress;
193
  pincode: HAPPincode;
194
  /**
195
   * Specify the category for the HomeKit accessory.
196
   * The category is used only in the mdns advertisement and specifies the devices type
197
   * for the HomeKit controller.
198
   * Currently, this only affects the icon shown in the pairing screen.
199
   * For the Television and Smart Speaker service it also affects the icon shown in
200
   * the Home app when paired.
201
   */
202
  category?: Categories;
203
  setupID?: string;
204
  /**
205
   * Defines the host where the HAP server will be bound to.
206
   * When undefined the HAP server will bind to all available interfaces
207
   * (see https://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback).
208
   *
209
   * This property accepts a mixture of IPAddresses and network interface names.
210
   * Depending on the mixture of supplied addresses/names hap-nodejs will bind differently.
211
   *
212
   * It is advised to not just bind to a specific address, but specifying the interface name
213
   * in oder to bind on all address records (and ip version) available.
214
   *
215
   * HAP-NodeJS (or the underlying ciao library) will not report about misspelled interface names,
216
   * as it could be that the interface is currently just down and will come up later.
217
   *
218
   * Here are a few examples:
219
   *  - bind: "::"
220
   *      Pretty much identical to not specifying anything, as most systems (with ipv6 support)
221
   *      will default to the unspecified ipv6 address (with dual stack support).
222
   *
223
   *  - bind: "0.0.0.0"
224
   *      Binding TCP socket to the unspecified ipv4 address.
225
   *      The mdns advertisement will exclude any ipv6 address records.
226
   *
227
   *  - bind: ["en0", "lo0"]
228
   *      The mdns advertising will advertise all records of the en0 and loopback interface (if available) and
229
   *      will also react to address changes on those interfaces.
230
   *      In order for the HAP server to accept all those address records (which may contain ipv6 records)
231
   *      it will bind on the unspecified ipv6 address "::" (assuming dual stack is supported).
232
   *
233
   *  - bind: ["en0", "lo0", "0.0.0.0"]
234
   *      Same as above, only that the HAP server will bind on the unspecified ipv4 address "0.0.0.0".
235
   *      The mdns advertisement will not advertise any ipv6 records.
236
   *
237
   *  - bind: "169.254.104.90"
238
   *      This will bind the HAP server to the address 0.0.0.0.
239
   *      The mdns advertisement will only advertise the A record 169.254.104.90.
240
   *      If the given network interface of that address encounters an ip address change (to a different address),
241
   *      the mdns advertisement will result in not advertising an address at all.
242
   *      So it is advised to specify an interface name instead of a specific address.
243
   *      This is identical with ipv6 addresses.
244
   *
245
   *  - bind: ["169.254.104.90", "192.168.1.4"]
246
   *      As the HAP TCP socket can only bind to a single address, when specifying multiple ip addresses
247
   *      the HAP server will bind to the unspecified ip address (0.0.0.0 if only ipv4 addresses are supplied,
248
   *      :: if a mixture or only ipv6 addresses are supplied).
249
   *      The mdns advertisement will only advertise the specified ip addresses.
250
   *      If the given network interface of that address encounters an ip address change (to different addresses),
251
   *      the mdns advertisement will result in not advertising an address at all.
252
   *      So it is advised to specify an interface name instead of a specific address.
253
   *
254
   */
255
  bind?: (InterfaceName | IPAddress) | (InterfaceName | IPAddress)[];
256
  /**
257
   * Defines the port where the HAP server will be bound to.
258
   * When undefined port 0 will be used resulting in a random port.
259
   */
260
  port?: number;
261
  /**
262
   * If this option is set to true, HAP-NodeJS will add identifying material (based on {@link username})
263
   * to the end of the accessory display name (and bonjour instance name).
264
   * Default: true
265
   */
266
  addIdentifyingMaterial?: boolean;
267
  /**
268
   * Defines the advertiser used with the published Accessory.
269
   */
270
  advertiser?: MDNSAdvertiser;
271
}
272

273
/**
274
 * @group Accessory
275
 */
276
export const enum MDNSAdvertiser {
9✔
277
  /**
278
   * Use the `@homebridge/ciao` module as advertiser.
279
   */
280
  CIAO = "ciao",
9✔
281
  /**
282
   * Use the `bonjour-hap` module as advertiser.
283
   */
284
  BONJOUR = "bonjour-hap",
9✔
285
  /**
286
   * Use Avahi/D-Bus as advertiser.
287
   */
288
  AVAHI = "avahi",
9✔
289
  /**
290
   * Use systemd-resolved/D-Bus as advertiser.
291
   *
292
   * Note: The systemd-resolved D-Bus interface doesn't provide means to detect restarts of the service.
293
   * Therefore, we can't detect if our advertisement might be lost due to a restart of the systemd-resolved daemon restart.
294
   * Consequentially, treat this feature as an experimental feature.
295
   */
296
  RESOLVED = "resolved",
9✔
297
}
298

299
/**
300
 * @group Accessory
301
 */
302
export type AccessoryCharacteristicChange = ServiceCharacteristicChange &  {
303
  service: Service;
304
};
305

306
/**
307
 * @group Service
308
 */
309
export interface ServiceConfigurationChange {
310
  service: Service;
311
}
312

313
const enum WriteRequestState {
9✔
314
  REGULAR_REQUEST,
9✔
315
  TIMED_WRITE_AUTHENTICATED,
9✔
316
  TIMED_WRITE_REJECTED
9✔
317
}
318

319
/**
320
 * @group Accessory
321
 */
322
export const enum AccessoryEventTypes {
9✔
323
  /**
324
   * Emitted when an iOS device wishes for this Accessory to identify itself. If `paired` is false, then
325
   * this device is currently browsing for Accessories in the system-provided "Add Accessory" screen. If
326
   * `paired` is true, then this is a device that has already paired with us. Note that if `paired` is true,
327
   * listening for this event is a shortcut for the underlying mechanism of setting the `Identify` Characteristic:
328
   * `getService(Service.AccessoryInformation).getCharacteristic(Characteristic.Identify).on('set', ...)`
329
   * You must call the callback for identification to be successful.
330
   */
331
  IDENTIFY = "identify",
9✔
332
  /**
333
   * This event is emitted once the HAP TCP socket is bound.
334
   * At this point the mdns advertisement isn't yet available. Use the {@link ADVERTISED} if you require the accessory to be discoverable.
335
   */
336
  LISTENING = "listening",
9✔
337
  /**
338
   * This event is emitted once the mDNS suite has fully advertised the presence of the accessory.
339
   * This event is guaranteed to be called after {@link LISTENING}.
340
   */
341
  ADVERTISED = "advertised",
9✔
342
  SERVICE_CONFIGURATION_CHANGE = "service-configurationChange",
9✔
343
  /**
344
   * Emitted after a change in the value of one of the provided Service's Characteristics.
345
   */
346
  SERVICE_CHARACTERISTIC_CHANGE = "service-characteristic-change",
9✔
347
  PAIRED = "paired",
9✔
348
  UNPAIRED = "unpaired",
9✔
349

350
  CHARACTERISTIC_WARNING = "characteristic-warning",
9✔
351
}
352

353
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
354
export declare interface Accessory {
355
  on(event: "identify", listener: (paired: boolean, callback: VoidCallback) => void): this;
356
  on(event: "listening", listener: (port: number, address: string) => void): this;
357
  on(event: "advertised", listener: () => void): this;
358

359
  on(event: "service-configurationChange", listener: (change: ServiceConfigurationChange) => void): this;
360
  on(event: "service-characteristic-change", listener: (change: AccessoryCharacteristicChange) => void): this;
361

362
  on(event: "paired", listener: () => void): this;
363
  on(event: "unpaired", listener: () => void): this;
364

365
  on(event: "characteristic-warning", listener: (warning: CharacteristicWarning) => void): this;
366

367

368
  emit(event: "identify", paired: boolean, callback: VoidCallback): boolean;
369
  emit(event: "listening", port: number, address: string): boolean;
370
  emit(event: "advertised"): boolean;
371

372
  emit(event: "service-configurationChange", change: ServiceConfigurationChange): boolean;
373
  emit(event: "service-characteristic-change", change: AccessoryCharacteristicChange): boolean;
374

375
  emit(event: "paired"): boolean;
376
  emit(event: "unpaired"): boolean;
377

378
  emit(event: "characteristic-warning", warning: CharacteristicWarning): boolean;
379
}
380

381
/**
382
 * Accessory is a virtual HomeKit device. It can publish an associated HAP server for iOS devices to communicate
383
 * with - or it can run behind another "Bridge" Accessory server.
384
 *
385
 * Bridged Accessories in this implementation must have a UUID that is unique among all other Accessories that
386
 * are hosted by the Bridge. This UUID must be "stable" and unchanging, even when the server is restarted. This
387
 * is required so that the Bridge can provide consistent "Accessory IDs" (aid) and "Instance IDs" (iid) for all
388
 * Accessories, Services, and Characteristics for iOS clients to reference later.
389
 *
390
 * @group Accessory
391
 */
392
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
393
export class Accessory extends EventEmitter {
9✔
394
  // Timeout in milliseconds until a characteristic warning is issue
395
  private static readonly TIMEOUT_WARNING = 3000;
9✔
396
  // Timeout in milliseconds after `TIMEOUT_WARNING` until the operation on the characteristic is considered timed out.
397
  private static readonly TIMEOUT_AFTER_WARNING = 6000;
9✔
398

399
  // NOTICE: when adding/changing properties, remember to possibly adjust the serialize/deserialize functions
400
  aid: Nullable<number> = null; // assigned by us in assignIDs() or by a Bridge
339✔
401
  _isBridge = false; // true if we are a Bridge (creating a new instance of the Bridge subclass sets this to true)
339✔
402
  bridged = false; // true if we are hosted "behind" a Bridge Accessory
339✔
403
  bridge?: Accessory; // if accessory is bridged, this property points to the bridge which bridges this accessory
404
  bridgedAccessories: Accessory[] = []; // If we are a Bridge, these are the Accessories we are bridging
339✔
405
  reachable = true;
339✔
406
  lastKnownUsername?: MacAddress;
407
  category: Categories = Categories.OTHER;
339✔
408
  services: Service[] = [];
339✔
409
  private primaryService?: Service;
410
  shouldPurgeUnusedIDs = true; // Purge unused ids by default
339✔
411
  /**
412
   * Captures if initialization steps inside {@link publish} have been called.
413
   * This is important when calling {@link publish} multiple times (e.g. after calling {@link unpublish}).
414
   * @private Private API
415
   */
416
  private initialized = false;
339✔
417

418
  private controllers: Record<ControllerIdentifier, ControllerContext> = {};
339✔
419
  private serializedControllers?: Record<ControllerIdentifier, ControllerServiceMap>; // store uninitialized controller data after a Accessory.deserialize call
420
  private activeCameraController?: CameraController;
421

422
  /**
423
   * @private Private API.
424
   */
425
  _accessoryInfo?: Nullable<AccessoryInfo>;
426
  /**
427
   * @private Private API.
428
   */
429
  _setupID: Nullable<string> = null;
339✔
430
  /**
431
   * @private Private API.
432
   */
433
  _identifierCache?: Nullable<IdentifierCache>;
434
  /**
435
   * @private Private API.
436
   */
437
  controllerStorage: ControllerStorage = new ControllerStorage(this);
339✔
438
  /**
439
   * @private Private API.
440
   */
441
  _advertiser?: Advertiser;
442
  /**
443
   * @private Private API.
444
   */
445
  _server?: HAPServer;
446
  /**
447
   * @private Private API.
448
   */
449
  _setupURI?: string;
450

451
  private configurationChangeDebounceTimeout?: NodeJS.Timeout;
452
  /**
453
   * This property captures the time when we last served a /accessories request.
454
   * For multiple bursts of /accessories request we don't want to always contact GET handlers
455
   */
456
  private lastAccessoriesRequest = 0;
339✔
457

458
  constructor(public displayName: string, public UUID: string) {
339✔
459
    super();
339✔
460
    assert(displayName, "Accessories must be created with a non-empty displayName.");
339✔
461
    assert(UUID, "Accessories must be created with a valid UUID.");
336✔
462
    assert(uuid.isValid(UUID), "UUID '" + UUID + "' is not a valid UUID. Try using the provided 'generateUUID' function to create a " +
333✔
463
      "valid UUID from any arbitrary string, like a serial number.");
464

465
    // create our initial "Accessory Information" Service that all Accessories are expected to have
466
    checkName(this.displayName, "Name", displayName);
330✔
467
    this.addService(Service.AccessoryInformation)
330✔
468
      .setCharacteristic(Characteristic.Name, displayName);
469

470
    // sign up for when iOS attempts to "set" the `Identify` characteristic - this means a paired device wishes
471
    // for us to identify ourselves (as opposed to an unpaired device - that case is handled by HAPServer 'identify' event)
472
    this.getService(Service.AccessoryInformation)!
330✔
473
      .getCharacteristic(Characteristic.Identify)!
474
      .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => {
475
        if (value) {
6!
476
          const paired = true;
6✔
477
          this.identificationRequest(paired, callback);
6✔
478
        }
479
      });
480
  }
481

482
  private identificationRequest(paired: boolean, callback: IdentifyCallback) {
483
    debug("[%s] Identification request", this.displayName);
6✔
484

485
    if (this.listeners(AccessoryEventTypes.IDENTIFY).length > 0) {
6✔
486
      // allow implementors to identify this Accessory in whatever way is appropriate, and pass along
487
      // the standard callback for completion.
488
      this.emit(AccessoryEventTypes.IDENTIFY, paired, callback);
3✔
489
    } else {
490
      debug("[%s] Identification request ignored; no listeners to 'identify' event", this.displayName);
3✔
491
      callback();
3✔
492
    }
493
  }
494

495
  /**
496
   * Add the given service instance to the Accessory.
497
   *
498
   * @param service - A {@link Service} instance.
499
   * @returns Returns the service instance passed to the method call.
500
   */
501
  public addService(service: Service): Service
502
  /**
503
   * Adds a given service by calling the provided {@link Service} constructor with the provided constructor arguments.
504
   * @param serviceConstructor - A {@link Service} service constructor (e.g. {@link Service.Switch}).
505
   * @param constructorArgs - The arguments passed to the given constructor.
506
   * @returns Returns the constructed service instance.
507
   */
508
  public addService<S extends typeof Service>(serviceConstructor: S, ...constructorArgs: ConstructorArgs<S>): Service
509
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
510
  public addService(serviceParam: Service | typeof Service, ...constructorArgs: any[]): Service {
511
    // service might be a constructor like `Service.AccessoryInformation` instead of an instance
512
    // of Service. Coerce if necessary.
513
    const service: Service = typeof serviceParam === "function"
846✔
514
      ? new serviceParam(constructorArgs[0], constructorArgs[1], constructorArgs[2])
515
      : serviceParam;
516

517
    // check for UUID+subtype conflict
518
    for (const existing of this.services) {
846✔
519
      if (existing.UUID === service.UUID) {
1,707✔
520
        // OK we have two Services with the same UUID. Check that each defines a `subtype` property and that each is unique.
521
        if (!service.subtype) {
33✔
522
          throw new Error("Cannot add a Service with the same UUID '" + existing.UUID +
3✔
523
            "' as another Service in this Accessory without also defining a unique 'subtype' property.");
524
        }
525

526
        if (service.subtype === existing.subtype) {
30✔
527
          throw new Error("Cannot add a Service with the same UUID '" + existing.UUID +
3✔
528
            "' and subtype '" + existing.subtype + "' as another Service in this Accessory.");
529
        }
530
      }
531
    }
532

533
    if (this.services.length >= MAX_SERVICES) {
840!
534
      throw new Error("Cannot add more than " + MAX_SERVICES + " services to a single accessory!");
×
535
    }
536

537
    this.services.push(service);
840✔
538

539
    if (service.isPrimaryService) { // check if a primary service was added
840✔
540
      if (this.primaryService !== undefined) {
9✔
541
        this.primaryService.isPrimaryService = false;
3✔
542
      }
543

544
      this.primaryService = service;
9✔
545
    }
546

547
    if (!this.bridged) {
840✔
548
      this.enqueueConfigurationUpdate();
837✔
549
    } else {
550
      this.emit(AccessoryEventTypes.SERVICE_CONFIGURATION_CHANGE, { service: service });
3✔
551
    }
552

553
    this.setupServiceEventHandlers(service);
840✔
554

555
    return service;
840✔
556
  }
557

558
  public removeService(service: Service): void {
559
    const index = this.services.indexOf(service);
63✔
560

561
    if (index >= 0) {
63!
562
      this.services.splice(index, 1);
63✔
563

564
      if (this.primaryService === service) { // check if we are removing out primary service
63✔
565
        this.primaryService = undefined;
3✔
566
      }
567
      this.removeLinkedService(service); // remove it from linked service entries on the local accessory
63✔
568

569
      if (!this.bridged) {
63!
570
        this.enqueueConfigurationUpdate();
63✔
571
      } else {
572
        this.emit(AccessoryEventTypes.SERVICE_CONFIGURATION_CHANGE, { service: service });
×
573
      }
574

575
      service.removeAllListeners();
63✔
576
    }
577
  }
578

579
  private removeLinkedService(removed: Service) {
580
    for (const service of this.services) {
63✔
581
      service.removeLinkedService(removed);
396✔
582
    }
583
  }
584

585
  public getService<T extends WithUUID<typeof Service>>(name: string | T): Service | undefined {
586
    for (const service of this.services) {
972✔
587
      if (typeof name === "string" && (service.displayName === name || service.name === name || service.subtype === name)) {
1,155✔
588
        return service;
3✔
589
      } else {
590
        // @ts-expect-error ('UUID' does not exist on type 'never')
591
        if (typeof name === "function" && ((service instanceof name) || (name.UUID === service.UUID))) {
1,152✔
592
          return service;
816✔
593
        }
594
      }
595
    }
596

597
    return undefined;
153✔
598
  }
599

600
  public getServiceById<T extends WithUUID<typeof Service>>(uuid: string | T, subType: string): Service | undefined {
601
    for (const service of this.services) {
9✔
602
      if (typeof uuid === "string" && (service.displayName === uuid || service.name === uuid) && service.subtype === subType) {
36✔
603
        return service;
3✔
604
      } else {
605
        // @ts-expect-error ('UUID' does not exist on type 'never')
606
        if (typeof uuid === "function" && ((service instanceof uuid) || (uuid.UUID === service.UUID)) && service.subtype === subType) {
33✔
607
          return service;
3✔
608
        }
609
      }
610
    }
611

612
    return undefined;
3✔
613
  }
614

615
  /**
616
   * Returns the bridging accessory if this accessory is bridged.
617
   * Otherwise, returns itself.
618
   *
619
   * @returns the primary accessory
620
   */
621
  public getPrimaryAccessory = (): Accessory => {
339✔
622
    return this.bridged? this.bridge!: this;
24✔
623
  };
624

625
  public addBridgedAccessory(accessory: Accessory, deferUpdate = false): Accessory {
18✔
626
    if (accessory._isBridge || accessory === this) {
30✔
627
      throw new Error("Illegal state: either trying to bridge a bridge or trying to bridge itself!");
3✔
628
    }
629

630
    if (accessory.initialized) {
27!
631
      throw new Error("Tried to bridge an accessory which was already published once!");
×
632
    }
633

634
    if (accessory.bridge != null) {
27✔
635
      // this also prevents that we bridge the same accessory twice!
636
      throw new Error("Tried to bridge " + accessory.displayName + " while it was already bridged by " + accessory.bridge.displayName);
3✔
637
    }
638

639
    if (this.bridgedAccessories.length >= MAX_ACCESSORIES) {
24!
640
      throw new Error("Cannot Bridge more than " + MAX_ACCESSORIES + " Accessories");
×
641
    }
642

643
    // listen for changes in ANY characteristics of ANY services on this Accessory
644
    accessory.on(AccessoryEventTypes.SERVICE_CHARACTERISTIC_CHANGE, change => this.handleCharacteristicChangeEvent(accessory, change.service, change));
24✔
645
    accessory.on(AccessoryEventTypes.SERVICE_CONFIGURATION_CHANGE, this.enqueueConfigurationUpdate.bind(this));
24✔
646
    accessory.on(AccessoryEventTypes.CHARACTERISTIC_WARNING, this.handleCharacteristicWarning.bind(this));
24✔
647

648
    accessory.bridged = true;
24✔
649
    accessory.bridge = this;
24✔
650

651
    this.bridgedAccessories.push(accessory);
24✔
652

653
    this.controllerStorage.linkAccessory(accessory); // init controllers of bridged accessory
24✔
654

655
    if(!deferUpdate) {
24✔
656
      this.enqueueConfigurationUpdate();
12✔
657
    }
658

659
    return accessory;
24✔
660
  }
661

662
  public addBridgedAccessories(accessories: Accessory[]): void {
663
    for (const accessory of accessories) {
12✔
664
      this.addBridgedAccessory(accessory, true);
12✔
665
    }
666

667
    this.enqueueConfigurationUpdate();
12✔
668
  }
669

670
  public removeBridgedAccessory(accessory: Accessory, deferUpdate = false): void {
6✔
671
    // check for UUID conflict
672
    const accessoryIndex = this.bridgedAccessories.indexOf(accessory);
12✔
673
    if (accessoryIndex === -1) {
12✔
674
      throw new Error("Cannot find the bridged Accessory to remove.");
3✔
675
    }
676

677
    this.bridgedAccessories.splice(accessoryIndex, 1);
9✔
678

679
    accessory.bridged = false;
9✔
680
    accessory.bridge = undefined;
9✔
681
    accessory.removeAllListeners();
9✔
682

683
    if(!deferUpdate) {
9✔
684
      this.enqueueConfigurationUpdate();
3✔
685
    }
686
  }
687

688
  public removeBridgedAccessories(accessories: Accessory[]): void {
689
    for (const accessory of accessories) {
3✔
690
      this.removeBridgedAccessory(accessory, true);
3✔
691
    }
692

693
    this.enqueueConfigurationUpdate();
3✔
694
  }
695

696
  public removeAllBridgedAccessories(): void {
697
    for (let i = this.bridgedAccessories.length - 1; i >= 0; i --) {
3✔
698
      this.removeBridgedAccessory(this.bridgedAccessories[i], true);
3✔
699
    }
700
    this.enqueueConfigurationUpdate();
3✔
701
  }
702

703
  private getCharacteristicByIID(iid: number): Characteristic | undefined {
704
    for (const service of this.services) {
186✔
705
      const characteristic = service.getCharacteristicByIID(iid);
357✔
706

707
      if (characteristic) {
357✔
708
        return characteristic;
180✔
709
      }
710
    }
711
  }
712

713
  protected getAccessoryByAID(aid: number): Accessory | undefined {
714
    if (this.aid === aid) {
213✔
715
      return this;
201✔
716
    }
717

718
    return this.bridgedAccessories.find(value => value.aid === aid);
12✔
719
  }
720

721
  protected findCharacteristic(aid: number, iid: number): Characteristic | undefined {
722
    const accessory = this.getAccessoryByAID(aid);
174✔
723
    return accessory && accessory.getCharacteristicByIID(iid);
174✔
724
  }
725

726
  /**
727
   * This method is used to set up a new Controller for this accessory. See {@link Controller} for a more detailed
728
   * explanation what a Controller is and what it is capable of.
729
   *
730
   * The controller can be passed as an instance of the class or as a constructor (without any necessary parameters)
731
   * for a new Controller.
732
   * Only one Controller of a given {@link ControllerIdentifier} can be configured for a given Accessory.
733
   *
734
   * When called, it will be checked if there are any services and persistent data the Controller (for the given
735
   * {@link ControllerIdentifier}) can be restored from. Otherwise, the Controller will be created with new services.
736
   *
737
   *
738
   * @param controllerConstructor - The Controller instance or constructor to the Controller with no required arguments.
739
   */
740
  public configureController(controllerConstructor: Controller | ControllerConstructor): void {
741
    const controller = typeof controllerConstructor === "function"
27!
742
      ? new controllerConstructor() // any custom constructor arguments should be passed before using .bind(...)
743
      : controllerConstructor;
744
    const id = controller.controllerId();
27✔
745

746
    if (this.controllers[id]) {
27!
747
      throw new Error(`A Controller with the type/id '${id}' was already added to the accessory ${this.displayName}`);
×
748
    }
749

750
    const savedServiceMap = this.serializedControllers && this.serializedControllers[id];
27✔
751
    let serviceMap: ControllerServiceMap;
752

753
    if (savedServiceMap) { // we found data to restore from
27✔
754
      const clonedServiceMap = clone(savedServiceMap);
3✔
755
      const updatedServiceMap = controller.initWithServices(savedServiceMap); // init controller with existing services
3✔
756
      serviceMap = updatedServiceMap || savedServiceMap; // initWithServices could return an updated serviceMap, otherwise just use the existing one
3!
757

758
      if (updatedServiceMap) { // controller returned a ServiceMap and thus signaled an updated set of services
3!
759
        // clonedServiceMap is altered by this method, should not be touched again after this call (for the future people)
760
        this.handleUpdatedControllerServiceMap(clonedServiceMap, updatedServiceMap);
3✔
761
      }
762

763
      controller.configureServices(); // let the controller setup all its handlers
3✔
764

765
      // remove serialized data from our dictionary:
766
      delete this.serializedControllers![id];
3✔
767
      if (Object.entries(this.serializedControllers!).length === 0) {
3!
768
        this.serializedControllers = undefined;
3✔
769
      }
770
    } else {
771
      serviceMap = controller.constructServices(); // let the controller create his services
24✔
772
      controller.configureServices(); // let the controller setup all its handlers
24✔
773

774
      Object.values(serviceMap).forEach(service => {
24✔
775
        if (service && !this.services.includes(service)) {
186!
776
          this.addService(service);
186✔
777
        }
778
      });
779
    }
780

781

782
    // --- init handlers and setup context ---
783
    const context: ControllerContext = {
27✔
784
      controller: controller,
785
      serviceMap: serviceMap,
786
    };
787

788
    if (isSerializableController(controller)) {
27✔
789
      this.controllerStorage.trackController(controller);
21✔
790
    }
791

792
    this.controllers[id] = context;
27✔
793

794
    if (controller instanceof CameraController) { // save CameraController for Snapshot handling
27✔
795
      this.activeCameraController = controller;
21✔
796
    }
797
  }
798

799
  /**
800
   * This method will remove a given Controller from this accessory.
801
   * The controller object will be restored to its initial state.
802
   * This also means that any event handlers setup for the controller will be removed.
803
   *
804
   * @param controller - The controller which should be removed from the accessory.
805
   */
806
  public removeController(controller: Controller): void {
807
    const id = controller.controllerId();
6✔
808

809
    const storedController = this.controllers[id];
6✔
810
    if (storedController) {
6!
811
      if (storedController.controller !== controller) {
6!
812
        throw new Error("[" + this.displayName + "] tried removing a controller with the id/type '" + id +
×
813
          "' though provided controller isn't the same instance that is registered!");
814
      }
815

816
      if (isSerializableController(controller)) {
6!
817
        // this will reset the state change delegate before we call handleControllerRemoved()
818
        this.controllerStorage.untrackController(controller);
6✔
819
      }
820

821
      if (controller.handleFactoryReset) {
6!
822
        controller.handleFactoryReset();
6✔
823
      }
824
      controller.handleControllerRemoved();
6✔
825

826
      delete this.controllers[id];
6✔
827

828
      if (this.activeCameraController === controller) {
6!
829
        this.activeCameraController = undefined;
6✔
830
      }
831

832
      Object.values(storedController.serviceMap).forEach(service => {
6✔
833
        if (service) {
54!
834
          this.removeService(service);
54✔
835
        }
836
      });
837
    }
838

839
    if (this.serializedControllers) {
6!
840
      delete this.serializedControllers[id];
×
841
    }
842
  }
843

844
  private handleAccessoryUnpairedForControllers(): void {
845
    for (const context of Object.values(this.controllers)) {
3✔
846
      const controller = context.controller;
×
847
      if (controller.handleFactoryReset) { // if the controller implements handleFactoryReset, setup event handlers for this controller
×
848
        controller.handleFactoryReset();
×
849
      }
850

851
      if (isSerializableController(controller)) {
×
852
        this.controllerStorage.purgeControllerData(controller);
×
853
      }
854
    }
855
  }
856

857
  private handleUpdatedControllerServiceMap(originalServiceMap: ControllerServiceMap, updatedServiceMap: ControllerServiceMap) {
858
    updatedServiceMap = clone(updatedServiceMap); // clone it so we can alter it
3✔
859

860
    Object.keys(originalServiceMap).forEach(name => { // this loop removed any services contained in both ServiceMaps
3✔
861
      const service = originalServiceMap[name];
6✔
862
      const updatedService = updatedServiceMap[name];
6✔
863

864
      if (service && updatedService) { // we check all names contained in both ServiceMaps for changes
6✔
865
        delete originalServiceMap[name]; // delete from original ServiceMap, so it will only contain deleted services at the end
3✔
866
        delete updatedServiceMap[name]; // delete from updated ServiceMap, so it will only contain added services at the end
3✔
867

868
        if (service !== updatedService) {
3!
869
          this.removeService(service);
3✔
870
          this.addService(updatedService);
3✔
871
        }
872
      }
873
    });
874

875
    // now originalServiceMap contains only deleted services and updateServiceMap only added services
876

877
    Object.values(originalServiceMap).forEach(service => {
3✔
878
      if (service) {
3!
879
        this.removeService(service);
3✔
880
      }
881
    });
882
    Object.values(updatedServiceMap).forEach(service => {
3✔
883
      if (service) {
3!
884
        this.addService(service);
3✔
885
      }
886
    });
887
  }
888

889
  setupURI(): string {
890
    if (this._setupURI) {
6✔
891
      return this._setupURI;
3✔
892
    }
893

894
    assert(!!this._accessoryInfo, "Cannot generate setupURI on an accessory that isn't published yet!");
3✔
895

896
    const buffer = Buffer.alloc(8);
3✔
897
    let value_low = parseInt(this._accessoryInfo.pincode.replace(/-/g, ""), 10);
3✔
898
    const value_high = this._accessoryInfo.category >> 1;
3✔
899

900
    value_low |= 1 << 28; // Supports IP;
3✔
901

902
    buffer.writeUInt32BE(value_low, 4);
3✔
903

904
    if (this._accessoryInfo.category & 1) {
3!
905
      buffer[4] = buffer[4] | 1 << 7;
×
906
    }
907

908
    buffer.writeUInt32BE(value_high, 0);
3✔
909

910
    let encodedPayload = (buffer.readUInt32BE(4) + (buffer.readUInt32BE(0) * 0x100000000)).toString(36).toUpperCase();
3✔
911

912
    if (encodedPayload.length !== 9) {
3!
913
      for (let i = 0; i <= 9 - encodedPayload.length; i++) {
3✔
914
        encodedPayload = "0" + encodedPayload;
6✔
915
      }
916
    }
917

918
    this._setupURI = "X-HM://" + encodedPayload + this._setupID;
3✔
919
    return this._setupURI;
3✔
920
  }
921

922
  /**
923
   * This method is called right before the accessory is published. It should be used to check for common
924
   * mistakes in Accessory structured, which may lead to HomeKit rejecting the accessory when pairing.
925
   * If it is called on a bridge it will call this method for all bridged accessories.
926
   */
927
  private validateAccessory(mainAccessory?: boolean) {
928
    const service = this.getService(Service.AccessoryInformation);
153✔
929
    if (!service) {
153!
930
      console.log("HAP-NodeJS WARNING: The accessory '" + this.displayName + "' is getting published without a AccessoryInformation service. " +
×
931
        "This might prevent the accessory from being added to the Home app or leading to the accessory being unresponsive!");
932
    } else {
933
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
934
      const checkValue = (name: string, value?: any) => {
153✔
935
        if (!value) {
765!
936
          console.log("HAP-NodeJS WARNING: The accessory '" + this.displayName + "' is getting published with the characteristic '" + name + "'" +
×
937
            " (of the AccessoryInformation service) not having a value set. " +
938
            "This might prevent the accessory from being added to the Home App or leading to the accessory being unresponsive!");
939
        }
940
      };
941

942
      checkName(this.displayName, "Name", service.getCharacteristic(Characteristic.Name).value);
153✔
943
      checkValue("FirmwareRevision", service.getCharacteristic(Characteristic.FirmwareRevision).value);
153✔
944
      checkValue("Manufacturer", service.getCharacteristic(Characteristic.Manufacturer).value);
153✔
945
      checkValue("Model", service.getCharacteristic(Characteristic.Model).value);
153✔
946
      checkValue("Name", service.getCharacteristic(Characteristic.Name).value);
153✔
947
      checkValue("SerialNumber", service.getCharacteristic(Characteristic.SerialNumber).value);
153✔
948
    }
949

950
    if (mainAccessory) {
153!
951
      // the main accessory which is advertised via bonjour must have a name with length <= 63 (limitation of DNS FQDN names)
952
      assert(Buffer.from(this.displayName, "utf8").length <= 63, "Accessory displayName cannot be longer than 63 bytes!");
153✔
953
    }
954

955
    if (this.bridged) {
153!
956
      this.bridgedAccessories.forEach(accessory => accessory.validateAccessory());
×
957
    }
958
  }
959

960
  /**
961
   * Assigns aid/iid to ourselves, any Accessories we are bridging, and all associated Services+Characteristics. Uses
962
   * the provided identifierCache to keep IDs stable.
963
   * @private Private API
964
   */
965
  _assignIDs(identifierCache: IdentifierCache): void {
966

967
    // if we are responsible for our own identifierCache, start the expiration process
968
    // also check weather we want to have an expiration process
969
    if (this._identifierCache && this.shouldPurgeUnusedIDs) {
191✔
970
      this._identifierCache.startTrackingUsage();
182✔
971
    }
972

973
    if (this.bridged) {
191✔
974
      // This Accessory is bridged, so it must have an aid > 1. Use the provided identifierCache to
975
      // fetch or assign one based on our UUID.
976
      this.aid = identifierCache.getAID(this.UUID);
9✔
977
    } else {
978
      // Since this Accessory is the server (as opposed to any Accessories that may be bridged behind us),
979
      // we must have aid = 1
980
      this.aid = 1;
182✔
981
    }
982

983
    for (const service of this.services) {
191✔
984
      if (this._isBridge) {
580✔
985
        service._assignIDs(identifierCache, this.UUID, 2000000000);
3✔
986
      } else {
987
        service._assignIDs(identifierCache, this.UUID);
577✔
988
      }
989
    }
990

991
    // now assign IDs for any Accessories we are bridging
992
    for (const accessory of this.bridgedAccessories) {
191✔
993
      accessory._assignIDs(identifierCache);
9✔
994
    }
995

996
    // expire any now-unused cache keys (for Accessories, Services, or Characteristics
997
    // that have been removed since the last call to assignIDs())
998
    if (this._identifierCache) {
191✔
999
      //Check weather we want to purge the unused ids
1000
      if (this.shouldPurgeUnusedIDs) {
182!
1001
        this._identifierCache.stopTrackingUsageAndExpireUnused();
182✔
1002
      }
1003
      //Save in case we have new ones
1004
      this._identifierCache.save();
182✔
1005
    }
1006
  }
1007

1008
  disableUnusedIDPurge(): void {
1009
    this.shouldPurgeUnusedIDs = false;
3✔
1010
  }
1011

1012
  enableUnusedIDPurge(): void {
1013
    this.shouldPurgeUnusedIDs = true;
3✔
1014
  }
1015

1016
  /**
1017
   * Manually purge the unused ids if you like, comes handy
1018
   * when you have disabled auto purge, so you can do it manually
1019
   */
1020
  purgeUnusedIDs(): void {
1021
    //Cache the state of the purge mechanism and set it to true
1022
    const oldValue = this.shouldPurgeUnusedIDs;
6✔
1023
    this.shouldPurgeUnusedIDs = true;
6✔
1024

1025
    //Reassign all ids
1026
    this._assignIDs(this._identifierCache!);
6✔
1027

1028
    // Revert the purge mechanism state
1029
    this.shouldPurgeUnusedIDs = oldValue;
6✔
1030
  }
1031

1032
  /**
1033
   * Returns a JSON representation of this accessory suitable for delivering to HAP clients.
1034
   */
1035
  private async toHAP(connection: HAPConnection, contactGetHandlers = true): Promise<AccessoryJsonObject[]> {
×
1036
    assert(this.aid, "aid cannot be undefined for accessory '" + this.displayName + "'");
3✔
1037
    assert(this.services.length, "accessory '" + this.displayName + "' does not have any services!");
3✔
1038

1039
    const accessory: AccessoryJsonObject = {
3✔
1040
      aid: this.aid!,
1041
      services: await Promise.all(this.services.map(service => service.toHAP(connection, contactGetHandlers))),
9✔
1042
    };
1043

1044
    const accessories: AccessoryJsonObject[] = [accessory];
3✔
1045

1046
    if (!this.bridged) {
3!
1047
      accessories.push(... await Promise.all(
3✔
1048
        this.bridgedAccessories
1049
          .map(accessory => accessory.toHAP(connection, contactGetHandlers).then(value => value[0])),
×
1050
      ));
1051
    }
1052

1053
    return accessories;
3✔
1054
  }
1055

1056
  /**
1057
   * Returns a JSON representation of this accessory without characteristic values.
1058
   */
1059
  private internalHAPRepresentation(assignIds = true): AccessoryJsonObject[] {
8✔
1060
    if (assignIds) {
161✔
1061
      this._assignIDs(this._identifierCache!); // make sure our aid/iid's are all assigned
8✔
1062
    }
1063
    assert(this.aid, "aid cannot be undefined for accessory '" + this.displayName + "'");
161✔
1064
    assert(this.services.length, "accessory '" + this.displayName + "' does not have any services!");
161✔
1065

1066
    const accessory: AccessoryJsonObject = {
161✔
1067
      aid: this.aid!,
1068
      services: this.services.map(service => service.internalHAPRepresentation()),
469✔
1069
    };
1070

1071
    const accessories: AccessoryJsonObject[] = [accessory];
161✔
1072

1073
    if (!this.bridged) {
161!
1074
      for (const accessory of this.bridgedAccessories) {
161✔
1075
        accessories.push(accessory.internalHAPRepresentation(false)[0]);
×
1076
      }
1077
    }
1078

1079
    return accessories;
161✔
1080
  }
1081

1082
  /**
1083
   * Publishes this accessory on the local network for iOS clients to communicate with.
1084
   * - `info.username` - formatted as a MAC address, like `CC:22:3D:E3:CE:F6`, of this accessory.
1085
   *   Must be globally unique from all Accessories on your local network.
1086
   * - `info.pincode` - the 8-digit pin code for clients to use when pairing this Accessory.
1087
   *   Must be formatted as a string like `031-45-154`.
1088
   * - `info.category` - one of the values of the `Accessory.Category` enum, like `Accessory.Category.SWITCH`.
1089
   *   This is a hint to iOS clients about what "type" of Accessory this represents, so
1090
   *   that for instance an appropriate icon can be drawn for the user while adding a
1091
   *   new Accessory.
1092
   * @param {{
1093
   *   username: string;
1094
   *   pincode: string;
1095
   *   category: Accessory.Categories;
1096
   * }} info - Required info for publishing.
1097
   * @param {boolean} allowInsecureRequest - Will allow unencrypted and unauthenticated access to the http server
1098
   */
1099
  public async publish(info: PublishInfo, allowInsecureRequest?: boolean): Promise<void> {
1100
    if (this.bridged) {
153!
1101
      throw new Error("Can't publish in accessory which is bridged by another accessory. Bridged by " + this.bridge?.displayName);
×
1102
    }
1103

1104
    let service = this.getService(Service.ProtocolInformation);
153✔
1105
    if (!service) {
153✔
1106
      service = this.addService(Service.ProtocolInformation); // add the protocol information service to the primary accessory
147✔
1107
    }
1108
    service.setCharacteristic(Characteristic.Version, CiaoAdvertiser.protocolVersionService);
153✔
1109

1110
    if (this.lastKnownUsername && this.lastKnownUsername !== info.username) { // username changed since last publish
153!
1111
      Accessory.cleanupAccessoryData(this.lastKnownUsername); // delete old Accessory data
×
1112
    }
1113

1114
    if (!this.initialized && (info.addIdentifyingMaterial ?? true)) {
153✔
1115
      // adding some identifying material to our displayName if it's our first publish() call
1116
      this.displayName = this.displayName + " " + crypto.createHash("sha512")
147✔
1117
        .update(info.username, "utf8")
1118
        .digest("hex").slice(0, 4).toUpperCase();
1119
      this.getService(Service.AccessoryInformation)!.updateCharacteristic(Characteristic.Name, this.displayName);
147✔
1120
    }
1121

1122
    // attempt to load existing AccessoryInfo from disk
1123
    this._accessoryInfo = AccessoryInfo.load(info.username);
153✔
1124

1125
    // if we don't have one, create a new one.
1126
    if (!this._accessoryInfo) {
153✔
1127
      debug("[%s] Creating new AccessoryInfo for our HAP server", this.displayName);
51✔
1128
      this._accessoryInfo = AccessoryInfo.create(info.username);
51✔
1129
    }
1130

1131
    if (info.setupID) {
153!
1132
      this._setupID = info.setupID;
×
1133
    } else if (this._accessoryInfo.setupID === undefined || this._accessoryInfo.setupID === "") {
153✔
1134
      this._setupID = Accessory._generateSetupID();
51✔
1135
    } else {
1136
      this._setupID = this._accessoryInfo.setupID;
102✔
1137
    }
1138

1139
    this._accessoryInfo.setupID = this._setupID;
153✔
1140

1141
    // make sure we have up-to-date values in AccessoryInfo, then save it in case they changed (or if we just created it)
1142
    this._accessoryInfo.displayName = this.displayName;
153✔
1143
    this._accessoryInfo.model = this.getService(Service.AccessoryInformation)!.getCharacteristic(Characteristic.Model).value as string;
153✔
1144
    this._accessoryInfo.category = info.category || Categories.OTHER;
153!
1145
    this._accessoryInfo.pincode = info.pincode;
153✔
1146
    this._accessoryInfo.save();
153✔
1147

1148
    // create our IdentifierCache, so we can provide clients with stable aid/iid's
1149
    this._identifierCache = IdentifierCache.load(info.username);
153✔
1150

1151
    // if we don't have one, create a new one.
1152
    if (!this._identifierCache) {
153!
1153
      debug("[%s] Creating new IdentifierCache", this.displayName);
153✔
1154
      this._identifierCache = new IdentifierCache(info.username);
153✔
1155
    }
1156

1157
    // If it's bridge and there are no accessories already assigned to the bridge
1158
    // probably purge is not needed since it's going to delete all the ids
1159
    // of accessories that might be added later. Useful when dynamically adding
1160
    // accessories.
1161
    if (this._isBridge && this.bridgedAccessories.length === 0) {
153!
1162
      this.disableUnusedIDPurge();
×
1163
      this.controllerStorage.purgeUnidentifiedAccessoryData = false;
×
1164
    }
1165

1166
    if (!this.initialized) { // controller storage is only loaded from disk the first time we publish!
153✔
1167
      this.controllerStorage.load(info.username); // initializing controller data
147✔
1168
    }
1169

1170
    // assign aid/iid
1171
    this._assignIDs(this._identifierCache);
153✔
1172

1173
    // get our accessory information in HAP format and determine if our configuration (that is, our
1174
    // Accessories/Services/Characteristics) has changed since the last time we were published. make
1175
    // sure to omit actual values since these are not part of the "configuration".
1176
    const config = this.internalHAPRepresentation(false); // TODO ensure this stuff is ordered
153✔
1177
    // TODO queue this check until about 5 seconds after startup, allowing some last changes after the publish call
1178
    //   without constantly incrementing the current config number
1179
    this._accessoryInfo.checkForCurrentConfigurationNumberIncrement(config, true);
153✔
1180

1181
    this.validateAccessory(true);
153✔
1182

1183
    // create our Advertiser which broadcasts our presence over mdns
1184
    const parsed = Accessory.parseBindOption(info);
153✔
1185

1186
    info.advertiser ??= MDNSAdvertiser.CIAO;
153✔
1187
    if (
153!
1188
      (info.advertiser === MDNSAdvertiser.AVAHI && !await AvahiAdvertiser.isAvailable()) ||
306!
1189
      (info.advertiser === MDNSAdvertiser.RESOLVED && !await ResolvedAdvertiser.isAvailable())
1190
    ) {
1191
      console.error(
×
1192
        `[${this.displayName}] The selected advertiser, "${info.advertiser}", isn't available on this platform. ` +
1193
        `Reverting to "${MDNSAdvertiser.CIAO}"`,
1194
      );
1195
      info.advertiser = MDNSAdvertiser.CIAO;
×
1196
    }
1197

1198
    switch (info.advertiser) {
153!
1199
    case MDNSAdvertiser.CIAO:
1200
      this._advertiser = new CiaoAdvertiser(this._accessoryInfo, {
42✔
1201
        interface: parsed.advertiserAddress,
1202
      }, {
1203
        restrictedAddresses: parsed.serviceRestrictedAddress,
1204
        disabledIpv6: parsed.serviceDisableIpv6,
1205
      });
1206
      break;
42✔
1207
    case MDNSAdvertiser.BONJOUR:
1208
      this._advertiser = new BonjourHAPAdvertiser(this._accessoryInfo, {
111✔
1209
        restrictedAddresses: parsed.serviceRestrictedAddress,
1210
        disabledIpv6: parsed.serviceDisableIpv6,
1211
      });
1212
      break;
111✔
1213
    case MDNSAdvertiser.AVAHI:
1214
      this._advertiser = new AvahiAdvertiser(this._accessoryInfo);
×
1215
      break;
×
1216
    case MDNSAdvertiser.RESOLVED:
1217
      this._advertiser = new ResolvedAdvertiser(this._accessoryInfo);
×
1218
      break;
×
1219
    default:
1220
      throw new Error("Unsupported advertiser setting: '" + info.advertiser + "'");
×
1221
    }
1222
    this._advertiser.on(AdvertiserEvent.UPDATED_NAME, name => {
153✔
1223
      this.displayName = name;
×
1224
      if (this._accessoryInfo) {
×
1225
        this._accessoryInfo.displayName = name;
×
1226
        this._accessoryInfo.save();
×
1227
      }
1228

1229
      // bonjour service name MUST match the name in the accessory information service
1230
      this.getService(Service.AccessoryInformation)!
×
1231
        .updateCharacteristic(Characteristic.Name, name);
1232
    });
1233

1234
    // create our HAP server which handles all communication between iOS devices and us
1235
    this._server = new HAPServer(this._accessoryInfo);
153✔
1236
    this._server.allowInsecureRequest = !!allowInsecureRequest;
153✔
1237
    this._server.on(HAPServerEventTypes.LISTENING, this.onListening.bind(this));
153✔
1238
    this._server.on(HAPServerEventTypes.IDENTIFY, this.identificationRequest.bind(this, false));
153✔
1239
    this._server.on(HAPServerEventTypes.PAIR, this.handleInitialPairSetupFinished.bind(this));
153✔
1240
    this._server.on(HAPServerEventTypes.ADD_PAIRING, this.handleAddPairing.bind(this));
153✔
1241
    this._server.on(HAPServerEventTypes.REMOVE_PAIRING, this.handleRemovePairing.bind(this));
153✔
1242
    this._server.on(HAPServerEventTypes.LIST_PAIRINGS, this.handleListPairings.bind(this));
153✔
1243
    this._server.on(HAPServerEventTypes.ACCESSORIES, this.handleAccessories.bind(this));
153✔
1244
    this._server.on(HAPServerEventTypes.GET_CHARACTERISTICS, this.handleGetCharacteristics.bind(this));
153✔
1245
    this._server.on(HAPServerEventTypes.SET_CHARACTERISTICS, this.handleSetCharacteristics.bind(this));
153✔
1246
    this._server.on(HAPServerEventTypes.CONNECTION_CLOSED, this.handleHAPConnectionClosed.bind(this));
153✔
1247
    this._server.on(HAPServerEventTypes.REQUEST_RESOURCE, this.handleResource.bind(this));
153✔
1248

1249
    this._server.listen(info.port, parsed.serverAddress);
153✔
1250

1251
    this.initialized = true;
153✔
1252
  }
1253

1254
  /**
1255
   * Removes this Accessory from the local network
1256
   * Accessory object will no longer valid after invoking this method
1257
   * Trying to invoke publish() on the object will result undefined behavior
1258
   */
1259
  public destroy(): Promise<void> {
1260
    const promise = this.unpublish();
300✔
1261

1262
    if (this._accessoryInfo) {
300✔
1263
      Accessory.cleanupAccessoryData(this._accessoryInfo.username);
183✔
1264

1265
      this._accessoryInfo = undefined;
183✔
1266
      this._identifierCache = undefined;
183✔
1267
      this.controllerStorage = new ControllerStorage(this);
183✔
1268
    }
1269
    this.removeAllListeners();
300✔
1270

1271
    return promise;
300✔
1272
  }
1273

1274
  public async unpublish(): Promise<void> {
1275
    if (this._server) {
612✔
1276
      this._server.destroy();
153✔
1277
      this._server = undefined;
153✔
1278
    }
1279
    if (this._advertiser) {
612✔
1280
      // noinspection JSIgnoredPromiseFromCall
1281
      await this._advertiser.destroy();
159✔
1282
      this._advertiser = undefined;
159✔
1283
    }
1284
  }
1285

1286
  private enqueueConfigurationUpdate(): void {
1287
    if (this.configurationChangeDebounceTimeout) {
1,344✔
1288
      return; // already enqueued
1,014✔
1289
    }
1290

1291
    this.configurationChangeDebounceTimeout = setTimeout(() => {
330✔
1292
      this.configurationChangeDebounceTimeout = undefined;
250✔
1293

1294
      if (this._advertiser && this._advertiser) {
250✔
1295
        // get our accessory information in HAP format and determine if our configuration (that is, our
1296
        // Accessories/Services/Characteristics) has changed since the last time we were published. make
1297
        // sure to omit actual values since these are not part of the "configuration".
1298
        const config = this.internalHAPRepresentation(); // TODO ensure this stuff is ordered
8✔
1299
        if (this._accessoryInfo?.checkForCurrentConfigurationNumberIncrement(config)) {
8!
1300
          this._advertiser.updateAdvertisement();
×
1301
        }
1302
      }
1303
    }, 1000);
1304
    this.configurationChangeDebounceTimeout.unref();
330✔
1305
    // 1s is fine, HomeKit is built that with configuration updates no iid or aid conflicts occur.
1306
    // Thus, the only thing happening when the txt update arrives late is already removed accessories/services
1307
    // not responding or new accessories/services not yet shown
1308
  }
1309

1310
  private onListening(port: number, hostname: string): void {
1311
    assert(this._advertiser, "Advertiser wasn't created at onListening!");
63✔
1312
    // the HAP server is listening, so we can now start advertising our presence.
1313
    this._advertiser!.initPort(port);
63✔
1314

1315
    this._advertiser!.startAdvertising()
63✔
1316
      .then(() => this.emit(AccessoryEventTypes.ADVERTISED))
63✔
1317
      .catch(reason => {
1318
        console.error("Could not create mDNS advertisement. The HAP-Server won't be discoverable: " + reason);
×
1319
        if (reason.stack) {
×
1320
          debug("Detailed error: " + reason.stack);
×
1321
        }
1322
      });
1323

1324
    this.emit(AccessoryEventTypes.LISTENING, port, hostname);
63✔
1325
  }
1326

1327
  private handleInitialPairSetupFinished(username: string, publicKey: Buffer, callback: PairCallback): void {
1328
    debug("[%s] Paired with client %s", this.displayName, username);
3✔
1329

1330
    if (this._accessoryInfo) {
3!
1331
      this._accessoryInfo.addPairedClient(username, publicKey, PermissionTypes.ADMIN);
3✔
1332
      this._accessoryInfo.save();
3✔
1333
    }
1334

1335
    // update our advertisement, so it can pick up on the paired status of AccessoryInfo
1336
    if (this._advertiser) {
3!
1337
      this._advertiser.updateAdvertisement();
3✔
1338
    }
1339

1340
    callback();
3✔
1341

1342
    this.emit(AccessoryEventTypes.PAIRED);
3✔
1343
  }
1344

1345
  private handleAddPairing(connection: HAPConnection, username: string, publicKey: Buffer, permission: PermissionTypes, callback: AddPairingCallback): void {
1346
    if (!this._accessoryInfo) {
27✔
1347
      callback(TLVErrorCode.UNAVAILABLE);
3✔
1348
      return;
3✔
1349
    }
1350

1351
    if (!this._accessoryInfo.hasAdminPermissions(connection.username!)) {
24✔
1352
      callback(TLVErrorCode.AUTHENTICATION);
3✔
1353
      return;
3✔
1354
    }
1355

1356
    const existingKey = this._accessoryInfo.getClientPublicKey(username);
21✔
1357
    if (existingKey) {
21✔
1358
      if (existingKey.toString() !== publicKey.toString()) {
15✔
1359
        callback(TLVErrorCode.UNKNOWN);
3✔
1360
        return;
3✔
1361
      }
1362

1363
      this._accessoryInfo.updatePermission(username, permission);
12✔
1364
    } else {
1365
      this._accessoryInfo.addPairedClient(username, publicKey, permission);
6✔
1366
    }
1367

1368
    this._accessoryInfo.save();
18✔
1369
    // there should be no need to update advertisement
1370
    callback(0);
18✔
1371
  }
1372

1373
  private handleRemovePairing(connection: HAPConnection, username: HAPUsername, callback: RemovePairingCallback): void {
1374
    if (!this._accessoryInfo) {
15✔
1375
      callback(TLVErrorCode.UNAVAILABLE);
3✔
1376
      return;
3✔
1377
    }
1378

1379
    if (!this._accessoryInfo.hasAdminPermissions(connection.username!)) {
12✔
1380
      callback(TLVErrorCode.AUTHENTICATION);
3✔
1381
      return;
3✔
1382
    }
1383

1384
    this._accessoryInfo.removePairedClient(connection, username);
9✔
1385
    this._accessoryInfo.save();
9✔
1386

1387
    callback(0); // first of all ensure the pairing is removed before we advertise availability again
9✔
1388

1389
    if (!this._accessoryInfo.paired()) {
9✔
1390
      if (this._advertiser) {
3!
1391
        this._advertiser.updateAdvertisement();
3✔
1392
      }
1393
      this.emit(AccessoryEventTypes.UNPAIRED);
3✔
1394

1395
      this.handleAccessoryUnpairedForControllers();
3✔
1396
      for (const accessory of this.bridgedAccessories) {
3✔
1397
        accessory.handleAccessoryUnpairedForControllers();
×
1398
      }
1399
    }
1400
  }
1401

1402
  private handleListPairings(connection: HAPConnection, callback: ListPairingsCallback): void {
1403
    if (!this._accessoryInfo) {
12✔
1404
      callback(TLVErrorCode.UNAVAILABLE);
3✔
1405
      return;
3✔
1406
    }
1407

1408
    if (!this._accessoryInfo.hasAdminPermissions(connection.username!)) {
9✔
1409
      callback(TLVErrorCode.AUTHENTICATION);
6✔
1410
      return;
6✔
1411
    }
1412

1413
    callback(0, this._accessoryInfo.listPairings());
3✔
1414
  }
1415

1416
  private handleAccessories(connection: HAPConnection, callback: AccessoriesCallback): void {
1417
    this._assignIDs(this._identifierCache!); // make sure our aid/iid's are all assigned
3✔
1418

1419
    const now = Date.now();
3✔
1420
    const contactGetHandlers = now - this.lastAccessoriesRequest > 5_000; // we query the latest value if last /accessories was more than 5s ago
3✔
1421
    this.lastAccessoriesRequest = now;
3✔
1422

1423
    this.toHAP(connection, contactGetHandlers).then(value => {
3✔
1424
      callback(undefined, {
3✔
1425
        accessories: value,
1426
      });
1427
    }, reason => {
1428
      console.error("[" + this.displayName + "] /accessories request error with: " + reason.stack);
×
1429
      callback({ httpCode: HAPHTTPCode.INTERNAL_SERVER_ERROR, status: HAPStatus.SERVICE_COMMUNICATION_FAILURE });
×
1430
    });
1431
  }
1432

1433
  private handleGetCharacteristics(connection: HAPConnection, request: CharacteristicsReadRequest, callback: ReadCharacteristicsCallback): void {
1434
    const characteristics: CharacteristicReadData[] = [];
51✔
1435
    const response: CharacteristicsReadResponse = { characteristics: characteristics };
51✔
1436

1437
    const missingCharacteristics: Set<EventName> = new Set(request.ids.map(id => id.aid + "." + id.iid));
51✔
1438
    if (missingCharacteristics.size !== request.ids.length) {
51!
1439
      // if those sizes differ, we have duplicates and can't properly handle that
1440
      callback({ httpCode: HAPHTTPCode.UNPROCESSABLE_ENTITY, status: HAPStatus.INVALID_VALUE_IN_REQUEST });
×
1441
      return;
×
1442
    }
1443

1444
    let timeout: NodeJS.Timeout | undefined = setTimeout(() => {
51✔
1445
      for (const id of missingCharacteristics) {
6✔
1446
        const split = id.split(".");
6✔
1447
        const aid = parseInt(split[0], 10);
6✔
1448
        const iid = parseInt(split[1], 10);
6✔
1449

1450
        const accessory = this.getAccessoryByAID(aid);
6✔
1451
        const characteristic = accessory?.getCharacteristicByIID(iid);
6✔
1452
        if (!accessory || !characteristic) {
6!
NEW
1453
          continue;
×
1454
        }
1455
        this.sendCharacteristicWarning(characteristic, CharacteristicWarningType.SLOW_READ, "The read handler for the characteristic '" +
6✔
1456
          characteristic.displayName + "' on the accessory '" + accessory.displayName + "' was slow to respond!");
1457
      }
1458

1459
      // after a total of 10s we do no longer wait for a request to appear and just return status code timeout
1460
      timeout = setTimeout(() => {
6✔
1461
        timeout = undefined;
3✔
1462

1463
        for (const id of missingCharacteristics) {
3✔
1464
          const split = id.split(".");
3✔
1465
          const aid = parseInt(split[0], 10);
3✔
1466
          const iid = parseInt(split[1], 10);
3✔
1467

1468
          const accessory = this.getAccessoryByAID(aid);
3✔
1469
          const characteristic = accessory?.getCharacteristicByIID(iid);
3✔
1470
          if (!accessory || !characteristic) {
3!
NEW
1471
            continue;
×
1472
          }
1473
          this.sendCharacteristicWarning(characteristic, CharacteristicWarningType.TIMEOUT_READ, "The read handler for the characteristic '" +
3✔
1474
            characteristic.displayName + "' on the accessory '" + accessory.displayName + "' didn't respond at all!. " +
1475
            "Please check that you properly call the callback!");
1476

1477
          characteristics.push({
3✔
1478
            aid: aid,
1479
            iid: iid,
1480
            status: HAPStatus.OPERATION_TIMED_OUT,
1481
          });
1482
        }
1483
        missingCharacteristics.clear();
3✔
1484

1485
        callback(undefined, response);
3✔
1486
      }, Accessory.TIMEOUT_AFTER_WARNING);
1487
      timeout.unref();
6✔
1488
    }, Accessory.TIMEOUT_WARNING);
1489
    timeout.unref();
51✔
1490

1491
    for (const id of request.ids) {
51✔
1492
      const name = id.aid + "." + id.iid;
51✔
1493
      this.handleCharacteristicRead(connection, id, request).then(value => {
51✔
1494
        return {
51✔
1495
          aid: id.aid,
1496
          iid: id.iid,
1497
          ...value,
1498
        };
1499
      }, reason => { // this error block is only called if hap-nodejs itself messed up
1500
        console.error(`[${this.displayName}] Read request for characteristic ${name} encountered an error: ${reason.stack}`);
×
1501

1502
        return {
×
1503
          aid: id.aid,
1504
          iid: id.iid,
1505
          status: HAPStatus.SERVICE_COMMUNICATION_FAILURE,
1506
        };
1507
      }).then(value => {
1508
        if (!timeout) {
51✔
1509
          return; // if timeout is undefined, response was already sent out
3✔
1510
        }
1511

1512
        missingCharacteristics.delete(name);
48✔
1513
        characteristics.push(value);
48✔
1514

1515
        if (missingCharacteristics.size === 0) {
48!
1516
          if (timeout) {
48!
1517
            clearTimeout(timeout);
48✔
1518
            timeout = undefined;
48✔
1519
          }
1520
          callback(undefined, response);
48✔
1521
        }
1522
      });
1523
    }
1524
  }
1525

1526
  private async handleCharacteristicRead(
1527
    connection: HAPConnection,
1528
    id: CharacteristicId,
1529
    request: CharacteristicsReadRequest,
1530
  ): Promise<PartialCharacteristicReadData> {
1531
    const characteristic = this.findCharacteristic(id.aid, id.iid);
51✔
1532

1533
    if (!characteristic) {
51✔
1534
      debug("[%s] Could not find a Characteristic with aid of %s and iid of %s", this.displayName, id.aid, id.iid);
6✔
1535
      return { status: HAPStatus.INVALID_VALUE_IN_REQUEST };
6✔
1536
    }
1537

1538
    if (!characteristic.props.perms.includes(Perms.PAIRED_READ)) { // check if read is allowed for this characteristic
45✔
1539
      debug("[%s] Tried reading from characteristic which does not allow reading (aid of %s and iid of %s)", this.displayName, id.aid, id.iid);
3✔
1540
      return { status: HAPStatus.WRITE_ONLY_CHARACTERISTIC };
3✔
1541
    }
1542

1543
    if (characteristic.props.adminOnlyAccess && characteristic.props.adminOnlyAccess.includes(Access.READ)) {
42✔
1544
      const verifiable = this._accessoryInfo && connection.username;
9✔
1545
      if (!verifiable) {
9✔
1546
        debug("[%s] Could not verify admin permissions for Characteristic which requires admin permissions for reading (aid of %s and iid of %s)",
3✔
1547
          this.displayName, id.aid, id.iid);
1548
      }
1549

1550
      if (!verifiable || !this._accessoryInfo!.hasAdminPermissions(connection.username!)) {
9✔
1551
        return { status: HAPStatus.INSUFFICIENT_PRIVILEGES };
6✔
1552
      }
1553
    }
1554

1555
    return characteristic.handleGetRequest(connection).then(value => {
36✔
1556
      value = formatOutgoingCharacteristicValue(value, characteristic.props);
33✔
1557
      debug("[%s] Got Characteristic \"%s\" value: \"%s\"", this.displayName, characteristic!.displayName, value);
33✔
1558

1559
      const data: PartialCharacteristicReadData = {
33✔
1560
        value: value == null ? null: value,
33!
1561
      };
1562

1563
      if (request.includeMeta) {
33✔
1564
        data.format = characteristic.props.format;
9✔
1565
        data.unit = characteristic.props.unit;
9✔
1566
        data.minValue = characteristic.props.minValue;
9✔
1567
        data.maxValue = characteristic.props.maxValue;
9✔
1568
        data.minStep = characteristic.props.minStep;
9✔
1569
        data.maxLen = characteristic.props.maxLen || characteristic.props.maxDataLen;
9✔
1570
      }
1571
      if (request.includePerms) {
33✔
1572
        data.perms = characteristic.props.perms;
3✔
1573
      }
1574
      if (request.includeType) {
33✔
1575
        data.type = toShortForm(characteristic.UUID);
3✔
1576
      }
1577
      if (request.includeEvent) {
33✔
1578
        data.ev = connection.hasEventNotifications(id.aid, id.iid);
6✔
1579
      }
1580

1581
      return data;
33✔
1582
    }, (reason: HAPStatus) => {
1583
      // @ts-expect-error: preserveConstEnums compiler option
1584
      debug("[%s] Error getting value for characteristic \"%s\": %s", this.displayName, characteristic.displayName, HAPStatus[reason]);
3✔
1585
      return { status: reason };
3✔
1586
    });
1587
  }
1588

1589
  private handleSetCharacteristics(connection: HAPConnection, writeRequest: CharacteristicsWriteRequest, callback: WriteCharacteristicsCallback): void {
1590
    debug("[%s] Processing characteristic set: %s", this.displayName, JSON.stringify(writeRequest));
120✔
1591

1592
    let writeState: WriteRequestState = WriteRequestState.REGULAR_REQUEST;
120✔
1593
    if (writeRequest.pid !== undefined) { // check for timed writes
120✔
1594
      if (connection.timedWritePid === writeRequest.pid) {
24✔
1595
        writeState = WriteRequestState.TIMED_WRITE_AUTHENTICATED;
6✔
1596
        clearTimeout(connection.timedWriteTimeout!);
6✔
1597
        connection.timedWritePid = undefined;
6✔
1598
        connection.timedWriteTimeout = undefined;
6✔
1599

1600
        debug("[%s] Timed write request got acknowledged for pid %d", this.displayName, writeRequest.pid);
6✔
1601
      } else {
1602
        writeState = WriteRequestState.TIMED_WRITE_REJECTED;
18✔
1603
        debug("[%s] TTL for timed write request has probably expired for pid %d", this.displayName, writeRequest.pid);
18✔
1604
      }
1605
    }
1606

1607
    const characteristics: CharacteristicWriteData[] = [];
120✔
1608
    const response: CharacteristicsWriteResponse = { characteristics: characteristics };
120✔
1609

1610
    const missingCharacteristics: Set<EventName> = new Set(
120✔
1611
      writeRequest.characteristics
1612
        .map(characteristic => characteristic.aid + "." + characteristic.iid),
120✔
1613
    );
1614
    if (missingCharacteristics.size !== writeRequest.characteristics.length) {
120!
1615
      // if those sizes differ, we have duplicates and can't properly handle that
1616
      callback({ httpCode: HAPHTTPCode.UNPROCESSABLE_ENTITY, status: HAPStatus.INVALID_VALUE_IN_REQUEST });
×
1617
      return;
×
1618
    }
1619

1620
    let timeout: NodeJS.Timeout | undefined = setTimeout(() => {
120✔
1621
      for (const id of missingCharacteristics) {
6✔
1622
        const split = id.split(".");
6✔
1623
        const aid = parseInt(split[0], 10);
6✔
1624
        const iid = parseInt(split[1], 10);
6✔
1625

1626
        const accessory = this.getAccessoryByAID(aid);
6✔
1627
        const characteristic = accessory?.getCharacteristicByIID(iid);
6✔
1628
        if (!accessory || !characteristic) {
6!
NEW
1629
          continue;
×
1630
        }
1631
        this.sendCharacteristicWarning(characteristic, CharacteristicWarningType.SLOW_WRITE, "The write handler for the characteristic '" +
6✔
1632
          characteristic.displayName + "' on the accessory '" + accessory.displayName + "' was slow to respond!");
1633
      }
1634

1635
      // after a total of 10s we do no longer wait for a request to appear and just return status code timeout
1636
      timeout = setTimeout(() => {
6✔
1637
        timeout = undefined;
3✔
1638

1639
        for (const id of missingCharacteristics) {
3✔
1640
          const split = id.split(".");
3✔
1641
          const aid = parseInt(split[0], 10);
3✔
1642
          const iid = parseInt(split[1], 10);
3✔
1643

1644
          const accessory = this.getAccessoryByAID(aid);
3✔
1645
          const characteristic = accessory?.getCharacteristicByIID(iid);
3✔
1646
          if (!accessory || !characteristic) {
3!
NEW
1647
            continue;
×
1648
          }
1649
          this.sendCharacteristicWarning(characteristic, CharacteristicWarningType.TIMEOUT_WRITE, "The write handler for the characteristic '" +
3✔
1650
            characteristic.displayName + "' on the accessory '" + accessory.displayName + "' didn't respond at all!. " +
1651
            "Please check that you properly call the callback!");
1652

1653
          characteristics.push({
3✔
1654
            aid: aid,
1655
            iid: iid,
1656
            status: HAPStatus.OPERATION_TIMED_OUT,
1657
          });
1658
        }
1659
        missingCharacteristics.clear();
3✔
1660

1661
        callback(undefined, response);
3✔
1662
      }, Accessory.TIMEOUT_AFTER_WARNING);
1663
      timeout.unref();
6✔
1664
    }, Accessory.TIMEOUT_WARNING);
1665
    timeout.unref();
120✔
1666

1667
    for (const data of writeRequest.characteristics) {
120✔
1668
      const name = data.aid + "." + data.iid;
120✔
1669
      this.handleCharacteristicWrite(connection, data, writeState).then(value => {
120✔
1670
        return {
120✔
1671
          aid: data.aid,
1672
          iid: data.iid,
1673
          ...value,
1674
        };
1675
      }, reason => { // this error block is only called if hap-nodejs itself messed up
1676
        console.error(`[${this.displayName}] Write request for characteristic ${name} encountered an error: ${reason.stack}`);
×
1677

1678
        return {
×
1679
          aid: data.aid,
1680
          iid: data.iid,
1681
          status: HAPStatus.SERVICE_COMMUNICATION_FAILURE,
1682
        };
1683
      }).then(value => {
1684
        if (!timeout) {
120✔
1685
          return; // if timeout is undefined, response was already sent out
3✔
1686
        }
1687

1688
        missingCharacteristics.delete(name);
117✔
1689
        characteristics.push(value);
117✔
1690

1691
        if (missingCharacteristics.size === 0) { // if everything returned send the response
117!
1692
          if (timeout) {
117!
1693
            clearTimeout(timeout);
117✔
1694
            timeout = undefined;
117✔
1695
          }
1696
          callback(undefined, response);
117✔
1697
        }
1698
      });
1699
    }
1700
  }
1701

1702
  private async handleCharacteristicWrite(
1703
    connection: HAPConnection,
1704
    data: CharacteristicWrite,
1705
    writeState: WriteRequestState,
1706
  ): Promise<PartialCharacteristicWriteData> {
1707
    const characteristic = this.findCharacteristic(data.aid, data.iid);
120✔
1708

1709
    if (!characteristic) {
120✔
1710
      debug("[%s] Could not find a Characteristic with aid of %s and iid of %s", this.displayName, data.aid, data.iid);
6✔
1711
      return { status: HAPStatus.INVALID_VALUE_IN_REQUEST };
6✔
1712
    }
1713

1714
    if (writeState === WriteRequestState.TIMED_WRITE_REJECTED) {
114✔
1715
      return { status: HAPStatus.INVALID_VALUE_IN_REQUEST };
18✔
1716
    }
1717

1718
    if (data.ev == null && data.value == null) {
96✔
1719
      return { status: HAPStatus.INVALID_VALUE_IN_REQUEST };
3✔
1720
    }
1721

1722
    if (data.ev != null) { // register/unregister event notifications
93✔
1723
      if (!characteristic.props.perms.includes(Perms.NOTIFY)) { // check if notify is allowed for this characteristic
33✔
1724
        debug("[%s] Tried %s notifications for Characteristic which does not allow notify (aid of %s and iid of %s)",
6✔
1725
          this.displayName, data.ev? "enabling": "disabling", data.aid, data.iid);
6✔
1726
        return { status: HAPStatus.NOTIFICATION_NOT_SUPPORTED };
6✔
1727
      }
1728

1729
      if (characteristic.props.adminOnlyAccess && characteristic.props.adminOnlyAccess.includes(Access.NOTIFY)) {
27✔
1730
        const verifiable = connection.username && this._accessoryInfo;
12✔
1731
        if (!verifiable) {
12✔
1732
          debug("[%s] Could not verify admin permissions for Characteristic which requires admin permissions for notify (aid of %s and iid of %s)",
3✔
1733
            this.displayName, data.aid, data.iid);
1734
        }
1735

1736
        if (!verifiable || !this._accessoryInfo!.hasAdminPermissions(connection.username!)) {
12✔
1737
          return { status: HAPStatus.INSUFFICIENT_PRIVILEGES };
9✔
1738
        }
1739
      }
1740

1741
      const notificationsEnabled = connection.hasEventNotifications(data.aid, data.iid);
18✔
1742
      if (data.ev && !notificationsEnabled) {
18✔
1743
        connection.enableEventNotifications(data.aid, data.iid);
9✔
1744
        characteristic.subscribe();
9✔
1745
        debug("[%s] Registered Characteristic \"%s\" on \"%s\" for events", connection.remoteAddress, characteristic.displayName, this.displayName);
9✔
1746
      } else if (!data.ev && notificationsEnabled) {
9✔
1747
        characteristic.unsubscribe();
3✔
1748
        connection.disableEventNotifications(data.aid, data.iid);
3✔
1749
        debug("[%s] Unregistered Characteristic \"%s\" on \"%s\" for events", connection.remoteAddress, characteristic.displayName, this.displayName);
3✔
1750
      }
1751

1752
      // response is returned below in the else block
1753
    }
1754

1755
    if (data.value != null) {
78✔
1756
      if (!characteristic.props.perms.includes(Perms.PAIRED_WRITE)) { // check if write is allowed for this characteristic
60✔
1757
        debug("[%s] Tried writing to Characteristic which does not allow writing (aid of %s and iid of %s)", this.displayName, data.aid, data.iid);
3✔
1758
        return { status: HAPStatus.READ_ONLY_CHARACTERISTIC };
3✔
1759
      }
1760

1761
      if (characteristic.props.adminOnlyAccess && characteristic.props.adminOnlyAccess.includes(Access.WRITE)) {
57✔
1762
        const verifiable = connection.username && this._accessoryInfo;
9✔
1763
        if (!verifiable) {
9✔
1764
          debug("[%s] Could not verify admin permissions for Characteristic which requires admin permissions for write (aid of %s and iid of %s)",
3✔
1765
            this.displayName, data.aid, data.iid);
1766
        }
1767

1768
        if (!verifiable || !this._accessoryInfo!.hasAdminPermissions(connection.username!)) {
9✔
1769
          return { status: HAPStatus.INSUFFICIENT_PRIVILEGES };
6✔
1770
        }
1771
      }
1772

1773
      if (characteristic.props.perms.includes(Perms.ADDITIONAL_AUTHORIZATION) && characteristic.additionalAuthorizationHandler) {
51✔
1774
        // if the characteristic "supports additional authorization" but doesn't define a handler for the check
1775
        // we conclude that the characteristic doesn't want to check the authData (currently) and just allows access for everybody
1776

1777
        let allowWrite;
1778
        try {
9✔
1779
          allowWrite = characteristic.additionalAuthorizationHandler(data.authData);
9✔
1780
        } catch (error) {
1781
          console.warn("[" + this.displayName + "] Additional authorization handler has thrown an error when checking authData: " + error.stack);
3✔
1782
          allowWrite = false;
3✔
1783
        }
1784

1785
        if (!allowWrite) {
9✔
1786
          return { status: HAPStatus.INSUFFICIENT_AUTHORIZATION };
6✔
1787
        }
1788
      }
1789

1790
      if (characteristic.props.perms.includes(Perms.TIMED_WRITE) && writeState !== WriteRequestState.TIMED_WRITE_AUTHENTICATED) {
45✔
1791
        debug("[%s] Tried writing to a timed write only Characteristic without properly preparing (iid of %s and aid of %s)",
3✔
1792
          this.displayName, data.aid, data.iid);
1793
        return { status: HAPStatus.INVALID_VALUE_IN_REQUEST };
3✔
1794
      }
1795

1796
      return characteristic.handleSetRequest(data.value, connection).then(value => {
42✔
1797
        debug("[%s] Setting Characteristic \"%s\" to value %s", this.displayName, characteristic.displayName, data.value);
39✔
1798
        return {
39✔
1799
          // if write response is requests and value is provided, return that
1800
          value: data.r && value? formatOutgoingCharacteristicValue(value, characteristic.props): undefined,
84✔
1801

1802
          status: HAPStatus.SUCCESS,
1803
        };
1804
      }, (status: HAPStatus) => {
1805
        // @ts-expect-error: forceConsistentCasingInFileNames compiler option
1806
        debug("[%s] Error setting Characteristic \"%s\" to value %s: ", this.displayName, characteristic.displayName, data.value, HAPStatus[status]);
3✔
1807

1808
        return { status: status };
3✔
1809
      });
1810
    }
1811

1812
    return { status: HAPStatus.SUCCESS };
18✔
1813
  }
1814

1815
  private handleResource(data: ResourceRequest, callback: ResourceRequestCallback): void {
1816
    if (data["resource-type"] === ResourceRequestType.IMAGE) {
30✔
1817
      const aid = data.aid; // aid is optionally supplied by HomeKit (for example when camera is bridged, multiple cams, etc)
27✔
1818

1819
      let accessory: Accessory | undefined = undefined;
27✔
1820
      let controller: CameraController | undefined = undefined;
27✔
1821
      if (aid) {
27✔
1822
        accessory = this.getAccessoryByAID(aid);
12✔
1823
        if (accessory && accessory.activeCameraController) {
12✔
1824
          controller = accessory.activeCameraController;
9✔
1825
        }
1826
      } else if (this.activeCameraController) { // aid was not supplied, check if this accessory is a camera
15✔
1827
        // eslint-disable-next-line @typescript-eslint/no-this-alias
1828
        accessory = this;
9✔
1829
        controller = this.activeCameraController;
9✔
1830
      }
1831

1832
      if (!controller) {
27✔
1833
        debug("[%s] received snapshot request though no camera controller was associated!");
9✔
1834
        callback({ httpCode: HAPHTTPCode.NOT_FOUND, status: HAPStatus.RESOURCE_DOES_NOT_EXIST });
9✔
1835
        return;
9✔
1836
      }
1837

1838
      controller.handleSnapshotRequest(data["image-height"], data["image-width"], accessory?.displayName, data.reason)
18✔
1839
        .then(buffer => {
1840
          callback(undefined, buffer);
15✔
1841
        }, (status: HAPStatus) => {
1842
          callback({ httpCode: HAPHTTPCode.MULTI_STATUS, status: status });
3✔
1843
        });
1844

1845
      return;
18✔
1846
    }
1847

1848
    debug("[%s] received request for unsupported image type: " + data["resource-type"], this._accessoryInfo?.username);
3✔
1849
    callback({ httpCode: HAPHTTPCode.NOT_FOUND, status: HAPStatus.RESOURCE_DOES_NOT_EXIST });
3✔
1850
  }
1851

1852
  private handleHAPConnectionClosed(connection: HAPConnection): void {
1853
    for (const event of connection.getRegisteredEvents()) {
3✔
1854
      const ids = event.split(".");
3✔
1855
      if (ids.length < 2) {
3!
NEW
1856
        continue;
×
1857
      }
1858
      const aid = parseInt(ids[0], 10);
3✔
1859
      const iid = parseInt(ids[1], 10);
3✔
1860

1861
      const characteristic = this.findCharacteristic(aid, iid);
3✔
1862
      if (characteristic) {
3!
1863
        characteristic.unsubscribe();
3✔
1864
      }
1865
    }
1866
    connection.clearRegisteredEvents();
3✔
1867
  }
1868

1869
  private handleServiceConfigurationChangeEvent(service: Service): void {
1870
    if (!service.isPrimaryService && service === this.primaryService) {
408✔
1871
      // service changed form primary to non-primary service
1872
      this.primaryService = undefined;
3✔
1873
    } else if (service.isPrimaryService && service !== this.primaryService) {
405✔
1874
      // service changed from non-primary to primary service
1875
      if (this.primaryService !== undefined) {
3!
1876
        this.primaryService.isPrimaryService = false;
3✔
1877
      }
1878

1879
      this.primaryService = service;
3✔
1880
    }
1881

1882
    if (this.bridged) {
408!
1883
      this.emit(AccessoryEventTypes.SERVICE_CONFIGURATION_CHANGE, { service: service });
×
1884
    } else {
1885
      this.enqueueConfigurationUpdate();
408✔
1886
    }
1887
  }
1888

1889
  private handleCharacteristicChangeEvent(accessory: Accessory, service: Service, change: ServiceCharacteristicChange): void {
1890
    if (this.bridged) { // forward this to our main accessory
864✔
1891
      this.emit(AccessoryEventTypes.SERVICE_CHARACTERISTIC_CHANGE, { ...change, service: service });
3✔
1892
    } else {
1893
      if (!this._server) {
861✔
1894
        return; // we're not running a HAPServer, so there's no one to notify about this event
639✔
1895
      }
1896

1897
      if (accessory.aid == null || change.characteristic.iid == null) {
222✔
1898
        debug("[%s] Muting event notification for %s as ids aren't yet assigned!", accessory.displayName, change.characteristic.displayName);
180✔
1899
        return;
180✔
1900
      }
1901

1902
      if (change.context != null && typeof change.context === "object" && (change.context as CharacteristicOperationContext).omitEventUpdate) {
42!
1903
        debug("[%s] Omitting event updates for %s as specified in the context object!", accessory.displayName, change.characteristic.displayName);
×
1904
        return;
×
1905
      }
1906

1907
      if (!(change.reason === ChangeReason.EVENT || change.oldValue !== change.newValue
42✔
1908
        || change.characteristic.UUID === Characteristic.ProgrammableSwitchEvent.UUID // those specific checks are out of backwards compatibility
1909
        || change.characteristic.UUID === Characteristic.ButtonEvent.UUID // new characteristics should use sendEventNotification call
1910
      )) {
1911
        // we only emit a change event if the reason was a call to sendEventNotification, if the value changed
1912
        // as of a write request or a read request or if the change happened on dedicated event characteristics
1913
        // otherwise we ignore this change event (with the return below)
1914
        return;
9✔
1915
      }
1916

1917
      const uuid = change.characteristic.UUID;
33✔
1918
      const immediateDelivery = uuid === Characteristic.ButtonEvent.UUID || uuid === Characteristic.ProgrammableSwitchEvent.UUID
33✔
1919
        || uuid === Characteristic.MotionDetected.UUID || uuid === Characteristic.ContactSensorState.UUID;
1920

1921
      const value = formatOutgoingCharacteristicValue(change.newValue, change.characteristic.props);
33✔
1922
      this._server.sendEventNotifications(accessory.aid, change.characteristic.iid, value, change.originator, immediateDelivery);
33✔
1923
    }
1924
  }
1925

1926
  private sendCharacteristicWarning(characteristic: Characteristic, type: CharacteristicWarningType, message: string): void {
1927
    this.handleCharacteristicWarning({
×
1928
      characteristic: characteristic,
1929
      type: type,
1930
      message: message,
1931
      originatorChain: [characteristic.displayName], // we are missing the service displayName, but that's okay
1932
      stack: new Error().stack,
1933
    });
1934
  }
1935

1936
  private handleCharacteristicWarning(warning: CharacteristicWarning): void {
1937
    warning.originatorChain = [this.displayName, ...warning.originatorChain];
15✔
1938

1939
    const emitted = this.emit(AccessoryEventTypes.CHARACTERISTIC_WARNING, warning);
15✔
1940
    if (!emitted) {
15✔
1941
      const message = `[${warning.originatorChain.join("@")}] ${warning.message}`;
6✔
1942

1943
      if (warning.type === CharacteristicWarningType.ERROR_MESSAGE
6!
1944
        || warning.type === CharacteristicWarningType.TIMEOUT_READ|| warning.type === CharacteristicWarningType.TIMEOUT_WRITE) {
1945
        console.error(message);
×
1946
      } else {
1947
        console.warn(message);
6✔
1948
      }
1949
      debug("[%s] Above characteristic warning was thrown at: %s", this.displayName, warning.stack ?? "unknown");
6!
1950
    }
1951
  }
1952

1953
  private setupServiceEventHandlers(service: Service): void {
1954
    service.on(ServiceEventTypes.SERVICE_CONFIGURATION_CHANGE, this.handleServiceConfigurationChangeEvent.bind(this, service));
864✔
1955
    service.on(ServiceEventTypes.CHARACTERISTIC_CHANGE, this.handleCharacteristicChangeEvent.bind(this, this, service));
864✔
1956
    service.on(ServiceEventTypes.CHARACTERISTIC_WARNING, this.handleCharacteristicWarning.bind(this));
864✔
1957
  }
1958

1959
  private _sideloadServices(targetServices: Service[]): void {
1960
    for (const service of targetServices) {
9✔
1961
      this.setupServiceEventHandlers(service);
24✔
1962
    }
1963

1964
    this.services = targetServices.slice();
9✔
1965

1966
    // Fix Identify
1967
    this
9✔
1968
      .getService(Service.AccessoryInformation)!
1969
      .getCharacteristic(Characteristic.Identify)!
1970
      .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => {
1971
        if (value) {
×
1972
          const paired = true;
×
1973
          this.identificationRequest(paired, callback);
×
1974
        }
1975
      });
1976
  }
1977

1978
  private static _generateSetupID(): string {
1979
    const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
771✔
1980
    const max = chars.length;
771✔
1981
    let setupID = "";
771✔
1982

1983
    for (let i = 0; i < 4; i++) {
771✔
1984
      const index = Math.floor(Math.random() * max);
3,084✔
1985
      setupID += chars.charAt(index);
3,084✔
1986
    }
1987

1988
    return setupID;
771✔
1989
  }
1990

1991
  // serialization and deserialization functions, mainly designed for homebridge to create a json copy to store on disk
1992
  public static serialize(accessory: Accessory): SerializedAccessory {
1993
    const json: SerializedAccessory = {
6✔
1994
      displayName: accessory.displayName,
1995
      UUID: accessory.UUID,
1996
      lastKnownUsername: accessory._accessoryInfo? accessory._accessoryInfo.username: undefined,
6!
1997
      category: accessory.category,
1998

1999
      services: [],
2000
    };
2001

2002
    const linkedServices: Record<ServiceId, ServiceId[]> = {};
6✔
2003
    let hasLinkedServices = false;
6✔
2004

2005
    accessory.services.forEach(service => {
6✔
2006
      json.services.push(Service.serialize(service));
18✔
2007

2008
      const linkedServicesPresentation: ServiceId[] = [];
18✔
2009
      service.linkedServices.forEach(linkedService => {
18✔
2010
        linkedServicesPresentation.push(linkedService.getServiceId());
3✔
2011
      });
2012
      if (linkedServicesPresentation.length > 0) {
18✔
2013
        linkedServices[service.getServiceId()] = linkedServicesPresentation;
3✔
2014
        hasLinkedServices = true;
3✔
2015
      }
2016
    });
2017

2018
    if (hasLinkedServices) {
6✔
2019
      json.linkedServices = linkedServices;
3✔
2020
    }
2021

2022
    const controllers: SerializedControllerContext[] = [];
6✔
2023

2024
    // save controllers
2025
    Object.values(accessory.controllers).forEach((context: ControllerContext)  => {
6✔
2026
      controllers.push({
3✔
2027
        type: context.controller.controllerId(),
2028
        services: Accessory.serializeServiceMap(context.serviceMap),
2029
      });
2030
    });
2031

2032
    // also save controller which didn't get initialized (could lead to service duplication if we throw that data away)
2033
    if (accessory.serializedControllers) {
6!
2034
      Object.entries(accessory.serializedControllers).forEach(([id, serviceMap]) => {
×
2035
        controllers.push({
×
2036
          type: id,
2037
          services: Accessory.serializeServiceMap(serviceMap),
2038
        });
2039
      });
2040
    }
2041

2042
    if (controllers.length > 0) {
6✔
2043
      json.controllers = controllers;
3✔
2044
    }
2045

2046
    return json;
6✔
2047
  }
2048

2049
  public static deserialize(json: SerializedAccessory): Accessory {
2050
    const accessory = new Accessory(json.displayName, json.UUID);
9✔
2051

2052
    accessory.lastKnownUsername = json.lastKnownUsername;
9✔
2053
    accessory.category = json.category;
9✔
2054

2055
    const services: Service[] = [];
9✔
2056
    const servicesMap: Record<ServiceId, Service> = {};
9✔
2057

2058
    json.services.forEach(serialized => {
9✔
2059
      const service = Service.deserialize(serialized);
24✔
2060

2061
      services.push(service);
24✔
2062
      servicesMap[service.getServiceId()] = service;
24✔
2063
    });
2064

2065
    if (json.linkedServices) {
9✔
2066
      for (const [ serviceId, linkedServicesKeys ] of Object.entries(json.linkedServices)) {
6✔
2067
        const primaryService = servicesMap[serviceId];
9✔
2068

2069
        if (!primaryService) {
9!
2070
          continue;
×
2071
        }
2072

2073
        linkedServicesKeys.forEach(linkedServiceKey => {
9✔
2074
          const linkedService = servicesMap[linkedServiceKey];
3✔
2075

2076
          if (linkedService) {
3!
2077
            primaryService.addLinkedService(linkedService);
3✔
2078
          }
2079
        });
2080
      }
2081
    }
2082

2083
    if (json.controllers) { // just save it for later if it exists {@see configureController}
9✔
2084
      accessory.serializedControllers = {};
3✔
2085

2086
      json.controllers.forEach(serializedController => {
3✔
2087
        accessory.serializedControllers![serializedController.type] = Accessory.deserializeServiceMap(serializedController.services, servicesMap);
3✔
2088
      });
2089
    }
2090

2091
    accessory._sideloadServices(services);
9✔
2092

2093
    return accessory;
9✔
2094
  }
2095

2096
  public static cleanupAccessoryData(username: MacAddress): void {
2097
    IdentifierCache.remove(username);
453✔
2098
    AccessoryInfo.remove(username);
453✔
2099
    ControllerStorage.remove(username);
453✔
2100
  }
2101

2102
  private static serializeServiceMap(serviceMap: ControllerServiceMap): SerializedServiceMap {
2103
    const serialized: SerializedServiceMap = {};
3✔
2104

2105
    Object.entries(serviceMap).forEach(([name, service]) => {
3✔
2106
      if (!service) {
6!
2107
        return;
×
2108
      }
2109

2110
      serialized[name] = service.getServiceId();
6✔
2111
    });
2112

2113
    return serialized;
3✔
2114
  }
2115

2116
  private static deserializeServiceMap(serializedServiceMap: SerializedServiceMap, servicesMap: Record<ServiceId, Service>): ControllerServiceMap {
2117
    const controllerServiceMap: ControllerServiceMap = {};
3✔
2118

2119
    Object.entries(serializedServiceMap).forEach(([name, serviceId]) => {
3✔
2120
      const service = servicesMap[serviceId];
6✔
2121
      if (service) {
6!
2122
        controllerServiceMap[name] = service;
6✔
2123
      }
2124
    });
2125

2126
    return controllerServiceMap;
3✔
2127
  }
2128

2129
  private static parseBindOption(info: PublishInfo): {
2130
    advertiserAddress?: string[],
2131
    serviceRestrictedAddress?: string[],
2132
    serviceDisableIpv6?: boolean,
2133
    serverAddress?: string,
2134
  } {
2135
    let advertiserAddress: string[] | undefined = undefined;
180✔
2136
    let disableIpv6: boolean | undefined = undefined;
180✔
2137
    let serverAddress: string | undefined = undefined;
180✔
2138

2139
    if (info.bind) {
180✔
2140
      const entries: Set<InterfaceName | IPAddress> = new Set(Array.isArray(info.bind)? info.bind: [info.bind]);
27✔
2141

2142
      if (entries.has("::")) {
27✔
2143
        serverAddress = "::";
6✔
2144

2145
        entries.delete("::");
6✔
2146
        if (entries.size) {
6✔
2147
          advertiserAddress = Array.from(entries);
3✔
2148
        }
2149
      } else if (entries.has("0.0.0.0")) {
21✔
2150
        disableIpv6 = true;
6✔
2151
        serverAddress = "0.0.0.0";
6✔
2152

2153
        entries.delete("0.0.0.0");
6✔
2154
        if (entries.size) {
6✔
2155
          advertiserAddress = Array.from(entries);
3✔
2156
        }
2157
      } else if (entries.size === 1) {
15✔
2158
        advertiserAddress = Array.from(entries);
6✔
2159

2160
        const entry = entries.values().next().value; // grab the first one
6✔
2161

2162
        const version = net.isIP(entry!); // check if ip address was specified or an interface name
6✔
2163
        if (version) {
6!
2164
          serverAddress = version === 4? "0.0.0.0": "::"; // we currently bind to unspecified addresses so config-ui always has a connection via loopback
6✔
2165
        } else {
2166
          serverAddress = "::"; // the interface could have both ipv4 and ipv6 addresses
×
2167
        }
2168
      } else if (entries.size > 1) {
9!
2169
        advertiserAddress = Array.from(entries);
9✔
2170

2171
        let bindUnspecifiedIpv6 = false; // we bind on "::" if there are interface names, or we detect ipv6 addresses
9✔
2172

2173
        for (const entry of entries) {
9✔
2174
          const version = net.isIP(entry);
12✔
2175
          if (version === 0 || version === 6) {
12✔
2176
            bindUnspecifiedIpv6 = true;
6✔
2177
            break;
6✔
2178
          }
2179
        }
2180

2181
        if (bindUnspecifiedIpv6) {
9✔
2182
          serverAddress = "::";
6✔
2183
        } else {
2184
          serverAddress = "0.0.0.0";
3✔
2185
        }
2186
      }
2187
    }
2188

2189
    return {
180✔
2190
      advertiserAddress: advertiserAddress,
2191
      serviceRestrictedAddress: advertiserAddress,
2192
      serviceDisableIpv6: disableIpv6,
2193
      serverAddress: serverAddress,
2194
    };
2195
  }
2196

2197
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc