homebridge-unifi-protect 6.19.0 → 6.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/devices/protect-camera.d.ts +0 -2
  2. package/dist/devices/protect-camera.js +114 -189
  3. package/dist/devices/protect-camera.js.map +1 -1
  4. package/dist/devices/protect-chime.d.ts +3 -0
  5. package/dist/devices/protect-chime.js +100 -28
  6. package/dist/devices/protect-chime.js.map +1 -1
  7. package/dist/devices/protect-device.d.ts +7 -2
  8. package/dist/devices/protect-device.js +147 -98
  9. package/dist/devices/protect-device.js.map +1 -1
  10. package/dist/devices/protect-doorbell.js +48 -81
  11. package/dist/devices/protect-doorbell.js.map +1 -1
  12. package/dist/devices/protect-light.js +33 -43
  13. package/dist/devices/protect-light.js.map +1 -1
  14. package/dist/devices/protect-liveviews.js +10 -20
  15. package/dist/devices/protect-liveviews.js.map +1 -1
  16. package/dist/devices/protect-nvr-systeminfo.js +2 -8
  17. package/dist/devices/protect-nvr-systeminfo.js.map +1 -1
  18. package/dist/devices/protect-securitysystem.d.ts +1 -2
  19. package/dist/devices/protect-securitysystem.js +31 -58
  20. package/dist/devices/protect-securitysystem.js.map +1 -1
  21. package/dist/devices/protect-sensor.d.ts +2 -1
  22. package/dist/devices/protect-sensor.js +137 -184
  23. package/dist/devices/protect-sensor.js.map +1 -1
  24. package/dist/devices/protect-viewer.js +21 -28
  25. package/dist/devices/protect-viewer.js.map +1 -1
  26. package/dist/protect-mqtt.d.ts +2 -2
  27. package/dist/protect-mqtt.js +17 -3
  28. package/dist/protect-mqtt.js.map +1 -1
  29. package/dist/protect-nvr.js +5 -2
  30. package/dist/protect-nvr.js.map +1 -1
  31. package/dist/protect-options.js +6 -5
  32. package/dist/protect-options.js.map +1 -1
  33. package/dist/protect-record.js +8 -8
  34. package/dist/protect-record.js.map +1 -1
  35. package/dist/protect-snapshot.js +8 -0
  36. package/dist/protect-snapshot.js.map +1 -1
  37. package/dist/protect-types.d.ts +3 -0
  38. package/dist/protect-types.js +6 -0
  39. package/dist/protect-types.js.map +1 -1
  40. package/dist/settings.d.ts +1 -0
  41. package/dist/settings.js +2 -0
  42. package/dist/settings.js.map +1 -1
  43. package/homebridge-ui/public/index.html +2 -0
  44. package/package.json +9 -9
@@ -13,9 +13,7 @@ export interface RtspEntry {
13
13
  export declare class ProtectCamera extends ProtectDevice {
14
14
  hasHksv: boolean;
15
15
  private isDeleted;
16
- private isDoorbellConfigured;
17
16
  isRinging: boolean;
18
- private isVideoConfigured;
19
17
  detectLicensePlate: string[];
20
18
  private rtspEntries;
21
19
  private rtspQuality;
@@ -1,13 +1,11 @@
1
+ import { ProtectReservedNames, toCamelCase } from "../protect-types.js";
1
2
  import { PROTECT_HOMEKIT_IDR_INTERVAL } from "../settings.js";
2
3
  import { ProtectDevice } from "./protect-device.js";
3
- import { ProtectReservedNames } from "../protect-types.js";
4
4
  import { ProtectStreamingDelegate } from "../protect-stream.js";
5
5
  export class ProtectCamera extends ProtectDevice {
6
6
  hasHksv;
7
7
  isDeleted;
8
- isDoorbellConfigured;
9
8
  isRinging;
10
- isVideoConfigured;
11
9
  detectLicensePlate;
12
10
  rtspEntries;
13
11
  rtspQuality;
@@ -16,11 +14,9 @@ export class ProtectCamera extends ProtectDevice {
16
14
  // Create an instance.
17
15
  constructor(nvr, device, accessory) {
18
16
  super(nvr, accessory);
19
- this.isDoorbellConfigured = false;
20
17
  this.hasHksv = false;
21
18
  this.isDeleted = false;
22
19
  this.isRinging = false;
23
- this.isVideoConfigured = false;
24
20
  this.detectLicensePlate = [];
25
21
  this.rtspEntries = [];
26
22
  this.rtspQuality = {};
@@ -35,6 +31,7 @@ export class ProtectCamera extends ProtectDevice {
35
31
  this.hints.crop = this.hasFeature("Video.Crop");
36
32
  this.hints.hardwareDecoding = true;
37
33
  this.hints.hardwareTranscoding = this.hasFeature("Video.Transcode.Hardware");
34
+ this.hints.highResSnapshots = this.hasFeature("Video.HighResSnapshots");
38
35
  this.hints.ledStatus = this.ufp.featureFlags.hasLedStatus && this.hasFeature("Device.StatusLed");
39
36
  this.hints.logDoorbell = this.hasFeature("Log.Doorbell");
40
37
  this.hints.logHksv = this.hasFeature("Log.HKSV");
@@ -48,29 +45,13 @@ export class ProtectCamera extends ProtectDevice {
48
45
  }
49
46
  // Configure a camera accessory for HomeKit.
50
47
  async configureDevice() {
51
- // Default to enabling motion detection.
52
- let detectMotion = true;
53
- // Save the motion detection switch state before we wipeout the context.
54
- if ("detectMotion" in this.accessory.context) {
55
- detectMotion = this.accessory.context.detectMotion;
56
- }
57
- // Default to disabling the dynamic bitrate setting.
58
- let dynamicBitrate = false;
59
- // Save the dynamic bitrate switch state before we wipeout the context.
60
- if ("dynamicBitrate" in this.accessory.context) {
61
- dynamicBitrate = this.accessory.context.dynamicBitrate;
62
- }
63
- // Default to enabling HKSV recording.
64
- let hksvRecording = true;
65
- // Save the HKSV recording switch state before we wipeout the context.
66
- if ("hksvRecording" in this.accessory.context) {
67
- hksvRecording = this.accessory.context.hksvRecording;
68
- }
48
+ // Save our context for reference before we recreate it.
49
+ const savedContext = this.accessory.context;
69
50
  // Clean out the context object in case it's been polluted somehow.
70
51
  this.accessory.context = {};
71
- this.accessory.context.detectMotion = detectMotion;
72
- this.accessory.context.dynamicBitrate = dynamicBitrate;
73
- this.accessory.context.hksvRecording = hksvRecording;
52
+ this.accessory.context.detectMotion = savedContext.detectMotion ?? true;
53
+ this.accessory.context.dynamicBitrate = savedContext.dynamicBitrate ?? false;
54
+ this.accessory.context.hksvRecording = savedContext.hksvRecording ?? true;
74
55
  this.accessory.context.mac = this.ufp.mac;
75
56
  this.accessory.context.nvr = this.nvr.ufp.mac;
76
57
  // Inform the user that motion detection will suck.
@@ -202,30 +183,22 @@ export class ProtectCamera extends ProtectDevice {
202
183
  }
203
184
  // A utility for us to add contact sensors.
204
185
  const addSmartDetectContactSensor = (name, serviceId, errorMessage) => {
205
- // See if we already have this contact sensor configured.
206
- let contactService = this.accessory.getServiceById(this.hap.Service.ContactSensor, serviceId);
207
- // If not, let's add it.
208
- if (!contactService) {
209
- contactService = new this.hap.Service.ContactSensor(name, serviceId);
210
- // Something went wrong, we're done here.
211
- if (!contactService) {
212
- this.log.error(errorMessage);
213
- return false;
214
- }
215
- contactService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
216
- // Finally, add it to the camera.
217
- this.accessory.addService(contactService);
186
+ // Acquire the service.
187
+ const service = this.acquireService(this.hap.Service.ContactSensor, name, serviceId);
188
+ // Fail gracefully.
189
+ if (!service) {
190
+ this.log.error(errorMessage);
191
+ return false;
218
192
  }
219
193
  // Initialize the sensor.
220
- contactService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, name);
221
- contactService.updateCharacteristic(this.hap.Characteristic.ContactSensorState, false);
194
+ service.updateCharacteristic(this.hap.Characteristic.ContactSensorState, false);
222
195
  return true;
223
196
  };
224
197
  let enabledContactSensors = [];
225
198
  // Add individual contact sensors for each object detection type, if needed.
226
199
  if (this.hasFeature("Motion.SmartDetect.ObjectSensors")) {
227
200
  for (const smartDetectType of this.ufp.featureFlags.smartDetectTypes) {
228
- if (addSmartDetectContactSensor(this.accessoryName + " " + smartDetectType.charAt(0).toUpperCase() + smartDetectType.slice(1), ProtectReservedNames.CONTACT_MOTION_SMARTDETECT + "." + smartDetectType, "Unable to add smart motion contact sensor for " + smartDetectType + " detection.")) {
201
+ if (addSmartDetectContactSensor(this.accessoryName + " " + toCamelCase(smartDetectType), ProtectReservedNames.CONTACT_MOTION_SMARTDETECT + "." + smartDetectType, "Unable to add smart motion contact sensor for " + smartDetectType + " detection.")) {
229
202
  enabledContactSensors.push(smartDetectType);
230
203
  }
231
204
  }
@@ -248,17 +221,17 @@ export class ProtectCamera extends ProtectDevice {
248
221
  }
249
222
  // Configure a switch to manually trigger a doorbell ring event for HomeKit.
250
223
  configureDoorbellTrigger() {
251
- // Find the switch service, if it exists.
252
- let triggerService = this.accessory.getServiceById(this.hap.Service.Switch, ProtectReservedNames.SWITCH_DOORBELL_TRIGGER);
253
224
  // See if we have a doorbell service configured.
254
225
  let doorbellService = this.accessory.getService(this.hap.Service.Doorbell);
255
- // Doorbell switches are disabled by default and primarily exist for automation purposes.
256
- if (!this.hasFeature("Doorbell.Trigger")) {
257
- if (triggerService) {
258
- this.accessory.removeService(triggerService);
226
+ // Validate whether we should have this service enabled.
227
+ if (!this.validService(this.hap.Service.Switch, () => {
228
+ // Doorbell switches are disabled by default and primarily exist for automation purposes.
229
+ if (!this.hasFeature("Doorbell.Trigger")) {
230
+ return false;
259
231
  }
260
- // Since we aren't enabling the doorbell trigger on this camera, remove the doorbell service if the camera
261
- // isn't actually doorbell-capable hardware.
232
+ return true;
233
+ }, ProtectReservedNames.SWITCH_DOORBELL_TRIGGER)) {
234
+ // Since we aren't enabling the doorbell trigger on this camera, remove the doorbell service if the camera isn't actually doorbell-capable hardware.
262
235
  if (!this.ufp.featureFlags.isDoorbell && doorbellService) {
263
236
  this.accessory.removeService(doorbellService);
264
237
  }
@@ -276,16 +249,12 @@ export class ProtectCamera extends ProtectDevice {
276
249
  return false;
277
250
  }
278
251
  }
279
- const triggerName = this.accessoryName + " Doorbell Trigger";
280
252
  // Add the switch to the camera, if needed.
253
+ const triggerService = this.acquireService(this.hap.Service.Switch, this.accessoryName + " Doorbell Trigger", ProtectReservedNames.SWITCH_DOORBELL_TRIGGER);
254
+ // Fail gracefully.
281
255
  if (!triggerService) {
282
- triggerService = new this.hap.Service.Switch(triggerName, ProtectReservedNames.SWITCH_DOORBELL_TRIGGER);
283
- if (!triggerService) {
284
- this.log.error("Unable to add the doorbell trigger.");
285
- return false;
286
- }
287
- triggerService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
288
- this.accessory.addService(triggerService);
256
+ this.log.error("Unable to add the doorbell trigger.");
257
+ return false;
289
258
  }
290
259
  // Trigger the doorbell.
291
260
  triggerService.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => {
@@ -307,31 +276,21 @@ export class ProtectCamera extends ProtectDevice {
307
276
  }
308
277
  });
309
278
  // Initialize the switch.
310
- triggerService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, triggerName);
311
279
  triggerService.updateCharacteristic(this.hap.Characteristic.On, false);
312
280
  this.log.info("Enabling doorbell automation trigger.");
313
281
  return true;
314
282
  }
315
283
  // Configure the doorbell service for HomeKit.
316
284
  configureVideoDoorbell() {
317
- // Only configure the doorbell service if we haven't configured it before.
318
- if (this.isDoorbellConfigured) {
319
- return true;
320
- }
321
- // Find the doorbell service, if it exists.
322
- let doorbellService = this.accessory.getService(this.hap.Service.Doorbell);
323
- // Add the doorbell service to this Protect doorbell. HomeKit requires the doorbell service to be
324
- // marked as the primary service on the accessory.
325
- if (!doorbellService) {
326
- doorbellService = new this.hap.Service.Doorbell(this.accessoryName);
327
- if (!doorbellService) {
328
- this.log.error("Unable to add doorbell.");
329
- return false;
330
- }
331
- this.accessory.addService(doorbellService);
285
+ // Acquire the service.
286
+ const service = this.acquireService(this.hap.Service.Doorbell);
287
+ // Fail gracefully.
288
+ if (!service) {
289
+ this.log.error("Unable to add doorbell.");
290
+ return false;
332
291
  }
333
- doorbellService.setPrimaryService(true);
334
- this.isDoorbellConfigured = true;
292
+ // Add the doorbell service to this Protect doorbell. HomeKit requires the doorbell service to be marked as the primary service on the accessory.
293
+ service.setPrimaryService(true);
335
294
  return true;
336
295
  }
337
296
  // Configure additional camera-specific characteristics for HomeKit.
@@ -414,18 +373,15 @@ export class ProtectCamera extends ProtectDevice {
414
373
  // Figure out which camera channels are RTSP-enabled, and user-enabled.
415
374
  let cameraChannels = this.ufp.channels.filter(x => x.isRtspEnabled && this.hasFeature("Video.Stream." + x.name, true));
416
375
  // Make sure we've got a HomeKit compatible IDR frame interval. If not, let's take care of that.
417
- let idrChannels = cameraChannels.filter(x => x.idrInterval !== PROTECT_HOMEKIT_IDR_INTERVAL);
376
+ const idrChannels = cameraChannels.filter(x => x.idrInterval !== PROTECT_HOMEKIT_IDR_INTERVAL);
418
377
  if (idrChannels.length) {
419
- // Edit the channel map.
420
- idrChannels = idrChannels.map(x => {
421
- x.idrInterval = PROTECT_HOMEKIT_IDR_INTERVAL;
422
- return x;
423
- });
424
- this.ufp = await this.nvr.ufpApi.updateDevice(this.ufp, { channels: idrChannels }) ?? this.ufp;
378
+ // Edit the channel map and update the Protect controller.
379
+ this.ufp = await this.nvr.ufpApi.updateDevice(this.ufp, { channels: idrChannels.map(x => Object.assign(x, { idrInterval: PROTECT_HOMEKIT_IDR_INTERVAL })) }) ??
380
+ this.ufp;
425
381
  }
426
382
  // Set the camera and shapshot URLs.
427
383
  const cameraUrl = "rtsps://" + (this.nvr.config.overrideAddress ?? this.ufp.connectionHost) + ":" + this.nvr.ufp.ports.rtsps.toString() + "/";
428
- // Filter out any package camera entries.
384
+ // Filter out any package camera entries. We deal with those independently in the package camera class.
429
385
  cameraChannels = cameraChannels.filter(x => x.name !== "Package Camera");
430
386
  // No RTSP streams are available that meet our criteria - we're done.
431
387
  if (!cameraChannels.length) {
@@ -516,8 +472,8 @@ export class ProtectCamera extends ProtectDevice {
516
472
  }
517
473
  // Publish our updated list of supported resolutions and their URLs.
518
474
  this.rtspEntries = rtspEntries;
519
- // If we're already configured, we're done here.
520
- if (this.isVideoConfigured) {
475
+ // If we've already configured the HomeKit video streaming delegate, we're done here.
476
+ if (this.stream) {
521
477
  return true;
522
478
  }
523
479
  // Inform users about our RTSP entry mapping, if we're debugging.
@@ -539,17 +495,20 @@ export class ProtectCamera extends ProtectDevice {
539
495
  }
540
496
  // Inform the user if we've set a streaming default.
541
497
  if (this.rtspQuality.StreamingDefault) {
542
- this.log.info("Video streaming configured to use only: %s.", this.rtspQuality.StreamingDefault.charAt(0) + this.rtspQuality.StreamingDefault.slice(1).toLowerCase());
498
+ this.log.info("Video streaming configured to use only: %s.", toCamelCase(this.rtspQuality.StreamingDefault));
499
+ }
500
+ // Inform the user if they've selected the legacy snapshot API.
501
+ if (!this.hints.highResSnapshots) {
502
+ this.log.info("Disabling the use of higher quality snapshots.");
543
503
  }
544
504
  // Inform the user if we've set a recording default.
545
505
  if (this.rtspQuality.RecordingDefault) {
546
- this.log.info("HomeKit Secure Video event recording configured to use only: %s.", this.rtspQuality.RecordingDefault.charAt(0) + this.rtspQuality.RecordingDefault.slice(1).toLowerCase());
506
+ this.log.info("HomeKit Secure Video event recording configured to use only: %s.", toCamelCase(this.rtspQuality.RecordingDefault));
547
507
  }
548
508
  // Configure the video stream with our resolutions.
549
509
  this.stream = new ProtectStreamingDelegate(this, this.rtspEntries.map(x => x.resolution));
550
510
  // Fire up the controller and inform HomeKit about it.
551
511
  this.accessory.configureController(this.stream.controller);
552
- this.isVideoConfigured = true;
553
512
  return true;
554
513
  }
555
514
  // Configure HomeKit Secure Video support.
@@ -565,75 +524,66 @@ export class ProtectCamera extends ProtectDevice {
565
524
  }
566
525
  // Configure a switch to manually enable or disable HKSV recording for a camera.
567
526
  configureHksvRecordingSwitch() {
568
- // Find the switch service, if it exists.
569
- let switchService = this.accessory.getServiceById(this.hap.Service.Switch, ProtectReservedNames.SWITCH_HKSV_RECORDING);
570
- // If we don't have HKSV or the HKSV recording switch enabled, disable it and we're done.
571
- if (!this.hasFeature("Video.HKSV.Recording.Switch")) {
572
- if (switchService) {
573
- this.accessory.removeService(switchService);
527
+ // Validate whether we should have this service enabled.
528
+ if (!this.validService(this.hap.Service.Switch, () => {
529
+ // If we don't have HKSV or the HKSV recording switch enabled, disable it and we're done.
530
+ if (!this.hasFeature("Video.HKSV.Recording.Switch")) {
531
+ return false;
574
532
  }
533
+ return true;
534
+ }, ProtectReservedNames.SWITCH_HKSV_RECORDING)) {
575
535
  // We want to default this back to recording whenever we disable the recording switch.
576
536
  this.accessory.context.hksvRecording = true;
577
537
  return false;
578
538
  }
579
- const switchName = this.accessoryName + " HKSV Recording";
580
- // Add the switch to the camera, if needed.
581
- if (!switchService) {
582
- switchService = new this.hap.Service.Switch(switchName, ProtectReservedNames.SWITCH_HKSV_RECORDING);
583
- if (!switchService) {
584
- this.log.error("Unable to add the HomeKit Secure Video recording switch.");
585
- return false;
586
- }
587
- switchService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
588
- this.accessory.addService(switchService);
539
+ // Acquire the service.
540
+ const service = this.acquireService(this.hap.Service.Switch, this.accessoryName + " HKSV Recording", ProtectReservedNames.SWITCH_HKSV_RECORDING);
541
+ // Fail gracefully.
542
+ if (!service) {
543
+ this.log.error("Unable to add HKSV recording switch.");
544
+ return false;
589
545
  }
590
546
  // Activate or deactivate HKSV recording.
591
- switchService.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => {
592
- return this.accessory.context.hksvRecording;
547
+ service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => {
548
+ return this.accessory.context.hksvRecording ?? true;
593
549
  });
594
- switchService.getCharacteristic(this.hap.Characteristic.On)?.onSet((value) => {
550
+ service.getCharacteristic(this.hap.Characteristic.On)?.onSet((value) => {
595
551
  if (this.accessory.context.hksvRecording !== value) {
596
- this.log.info("HomeKit Secure Video event recording has been %s.", value === true ? "enabled" : "disabled");
552
+ this.log.info("HKSV event recording has been %s.", value === true ? "enabled" : "disabled");
597
553
  }
598
554
  this.accessory.context.hksvRecording = value === true;
599
555
  });
600
556
  // Initialize the switch.
601
- switchService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, switchName);
602
- switchService.updateCharacteristic(this.hap.Characteristic.On, this.accessory.context.hksvRecording);
603
- this.log.info("Enabling HomeKit Secure Video recording switch.");
557
+ service.updateCharacteristic(this.hap.Characteristic.On, this.accessory.context.hksvRecording);
558
+ this.log.info("Enabling HKSV recording switch.");
604
559
  return true;
605
560
  }
606
561
  // Configure a switch to manually enable or disable dynamic bitrate capabilities for a camera.
607
562
  configureDynamicBitrateSwitch() {
608
- // Find the switch service, if it exists.
609
- let switchService = this.accessory.getServiceById(this.hap.Service.Switch, ProtectReservedNames.SWITCH_DYNAMIC_BITRATE);
610
- // If we don't want a dynamic bitrate switch, disable it and we're done.
611
- if (!this.hasFeature("Video.DynamicBitrate.Switch")) {
612
- if (switchService) {
613
- this.accessory.removeService(switchService);
563
+ // Validate whether we should have this service enabled.
564
+ if (!this.validService(this.hap.Service.Switch, () => {
565
+ // If we don't want a dynamic bitrate switch, disable it and we're done.
566
+ if (!this.hasFeature("Video.DynamicBitrate.Switch")) {
567
+ return false;
614
568
  }
569
+ return true;
570
+ }, ProtectReservedNames.SWITCH_DYNAMIC_BITRATE)) {
615
571
  // We want to default this back to off by default whenever we disable the dynamic bitrate switch.
616
572
  this.accessory.context.dynamicBitrate = false;
617
573
  return false;
618
574
  }
619
- const switchName = this.accessoryName + " Dynamic Bitrate";
620
- // Add the switch to the camera, if needed.
621
- if (!switchService) {
622
- switchService = new this.hap.Service.Switch(switchName, ProtectReservedNames.SWITCH_DYNAMIC_BITRATE);
623
- if (!switchService) {
624
- this.log.error("Unable to add the dynamic bitrate switch.");
625
- return false;
626
- }
627
- switchService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
628
- this.accessory.addService(switchService);
575
+ // Acquire the service.
576
+ const service = this.acquireService(this.hap.Service.Switch, this.accessoryName + " Dynamic Bitrate", ProtectReservedNames.SWITCH_DYNAMIC_BITRATE);
577
+ // Fail gracefully.
578
+ if (!service) {
579
+ this.log.error("Unable to add dynamic bitrate switch.");
580
+ return false;
629
581
  }
630
582
  // Activate or deactivate dynamic bitrate for this device.
631
- switchService
632
- .getCharacteristic(this.hap.Characteristic.On)
633
- ?.onGet(() => {
634
- return this.accessory.context.dynamicBitrate;
635
- })
636
- .onSet(async (value) => {
583
+ service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => {
584
+ return this.accessory.context.dynamicBitrate ?? false;
585
+ });
586
+ service.getCharacteristic(this.hap.Characteristic.On)?.onSet(async (value) => {
637
587
  if (this.accessory.context.dynamicBitrate === value) {
638
588
  return;
639
589
  }
@@ -643,12 +593,8 @@ export class ProtectCamera extends ProtectDevice {
643
593
  this.log.info("Dynamic streaming bitrate adjustment on the UniFi Protect controller enabled.");
644
594
  return;
645
595
  }
646
- // We're disabling dynamic bitrate for this device.
647
- const updatedChannels = this.ufp.channels;
648
- // Update the channels JSON.
649
- for (const channel of updatedChannels) {
650
- channel.bitrate = channel.maxBitrate;
651
- }
596
+ // We're disabling dynamic bitrate for this device. Update the channels JSON to revert to the maximum bitrate we can.
597
+ const updatedChannels = this.ufp.channels.map(channel => ({ ...channel, bitrate: channel.maxBitrate }));
652
598
  // Send the channels JSON to Protect.
653
599
  const newDevice = await this.nvr.ufpApi.updateDevice(this.ufp, { channels: updatedChannels });
654
600
  // We failed.
@@ -662,8 +608,7 @@ export class ProtectCamera extends ProtectDevice {
662
608
  this.log.info("Dynamic streaming bitrate adjustment on the UniFi Protect controller disabled.");
663
609
  });
664
610
  // Initialize the switch.
665
- switchService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, switchName);
666
- switchService.updateCharacteristic(this.hap.Characteristic.On, this.accessory.context.dynamicBitrate);
611
+ service.updateCharacteristic(this.hap.Characteristic.On, this.accessory.context.dynamicBitrate);
667
612
  this.log.info("Enabling the dynamic streaming bitrate adjustment switch.");
668
613
  return true;
669
614
  }
@@ -673,31 +618,29 @@ export class ProtectCamera extends ProtectDevice {
673
618
  // The Protect controller supports three modes for recording on a camera: always, detections, and never. We create switches for each of the modes.
674
619
  for (const ufpRecordingSwitchType of [ProtectReservedNames.SWITCH_UFP_RECORDING_ALWAYS, ProtectReservedNames.SWITCH_UFP_RECORDING_DETECTIONS, ProtectReservedNames.SWITCH_UFP_RECORDING_NEVER]) {
675
620
  const ufpRecordingSetting = ufpRecordingSwitchType.slice(ufpRecordingSwitchType.lastIndexOf(".") + 1);
676
- // Find the switch service, if it exists.
677
- let switchService = this.accessory.getServiceById(this.hap.Service.Switch, ufpRecordingSwitchType);
678
- // If we don't have the feature option enabled, disable the switch and we're done.
679
- if (!this.hasFeature("Nvr.Recording.Switch")) {
680
- if (switchService) {
681
- this.accessory.removeService(switchService);
621
+ // Validate whether we should have this service enabled.
622
+ if (!this.validService(this.hap.Service.Switch, () => {
623
+ // If we don't have the feature option enabled, disable the switch and we're done.
624
+ if (!this.hasFeature("Nvr.Recording.Switch")) {
625
+ return false;
682
626
  }
627
+ return true;
628
+ }, ufpRecordingSwitchType)) {
683
629
  continue;
684
630
  }
685
- const switchName = this.accessoryName + " UFP Recording " + ufpRecordingSetting.charAt(0).toUpperCase() + ufpRecordingSetting.slice(1);
686
- // Add the switch to the camera, if needed.
687
- if (!switchService) {
688
- switchService = new this.hap.Service.Switch(switchName, ufpRecordingSwitchType);
689
- if (!switchService) {
690
- this.log.error("Unable to add the UniFi Protect recording switches.");
691
- continue;
692
- }
693
- switchService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
694
- this.accessory.addService(switchService);
631
+ const switchName = this.accessoryName + " UFP Recording " + toCamelCase(ufpRecordingSetting);
632
+ // Acquire the service.
633
+ const service = this.acquireService(this.hap.Service.Switch, switchName, ufpRecordingSwitchType);
634
+ // Fail gracefully.
635
+ if (!service) {
636
+ this.log.error("Unable to add UniFi Protect recording switches.");
637
+ continue;
695
638
  }
696
639
  // Activate or deactivate the appropriate recording mode on the Protect controller.
697
- switchService.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => {
640
+ service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => {
698
641
  return this.ufp.recordingSettings.mode === ufpRecordingSetting;
699
642
  });
700
- switchService.getCharacteristic(this.hap.Characteristic.On)?.onSet(async (value) => {
643
+ service.getCharacteristic(this.hap.Characteristic.On)?.onSet(async (value) => {
701
644
  // We only want to do something if we're being activated. Turning off the switch would really be an undefined state given that there are three different
702
645
  // settings one can choose from. Instead, we do nothing and leave it to the user to choose what state they really want to set.
703
646
  if (!value) {
@@ -729,8 +672,7 @@ export class ProtectCamera extends ProtectDevice {
729
672
  this.log.info("UniFi Protect recording mode set to %s.", ufpRecordingSetting);
730
673
  });
731
674
  // Initialize the recording switch state.
732
- switchService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, switchName);
733
- switchService.updateCharacteristic(this.hap.Characteristic.On, this.ufp.recordingSettings.mode === ufpRecordingSetting);
675
+ service.updateCharacteristic(this.hap.Characteristic.On, this.ufp.recordingSettings.mode === ufpRecordingSetting);
734
676
  switchesEnabled.push(ufpRecordingSetting);
735
677
  }
736
678
  if (switchesEnabled.length) {
@@ -741,44 +683,28 @@ export class ProtectCamera extends ProtectDevice {
741
683
  // Configure MQTT capabilities of this camera.
742
684
  configureMqtt() {
743
685
  // Return the RTSP URLs when requested.
744
- this.nvr.mqtt?.subscribe(this.accessory, "rtsp/get", (message) => {
745
- const value = message.toString();
746
- // When we get the right message, we trigger the snapshot request.
747
- if (value?.toLowerCase() !== "true") {
748
- return;
749
- }
750
- const urlInfo = {};
751
- // Grab all the available RTSP channels.
752
- for (const channel of this.ufp.channels) {
753
- if (!channel.isRtspEnabled) {
754
- continue;
755
- }
756
- urlInfo[channel.name] = "rtsps://" + this.nvr.ufp.host + ":" + this.nvr.ufp.ports.rtsp.toString() + "/" + channel.rtspAlias + "?enableSrtp";
757
- }
758
- this.nvr.mqtt?.publish(this.accessory, "rtsp", JSON.stringify(urlInfo));
759
- this.log.info("RTSP information published via MQTT.");
686
+ this.subscribeGet("rtsp", "RTSP information", () => {
687
+ // Grab all the available RTSP channels and return them as a JSON.
688
+ return JSON.stringify(Object.assign({}, ...this.ufp.channels.filter(channel => channel.isRtspEnabled)
689
+ .map(channel => ({ [channel.name]: "rtsps://" + this.nvr.ufp.host + ":" + this.nvr.ufp.ports.rtsp + "/" + channel.rtspAlias + "?enableSrtp" }))));
760
690
  });
761
691
  // Trigger snapshots when requested.
762
- this.nvr.mqtt?.subscribe(this.accessory, "snapshot/trigger", (message) => {
763
- const value = message.toString();
692
+ this.subscribeSet("snapshot", "snapshot trigger", (value) => {
764
693
  // When we get the right message, we trigger the snapshot request.
765
- if (value?.toLowerCase() !== "true") {
694
+ if (value !== "true") {
766
695
  return;
767
696
  }
768
697
  void this.stream?.handleSnapshotRequest();
769
- this.log.info("Snapshot triggered via MQTT.");
770
698
  });
771
699
  // Enable doorbell-specific MQTT capabilities only when we have a Protect doorbell or a doorbell trigger enabled.
772
700
  if (this.ufp.featureFlags.isDoorbell || this.hasFeature("Doorbell.Trigger")) {
773
701
  // Trigger doorbell when requested.
774
- this.nvr.mqtt?.subscribe(this.accessory, "doorbell/trigger", (message) => {
775
- const value = message.toString();
702
+ this.subscribeSet("doorbell", "doorbell ring trigger", (value) => {
776
703
  // When we get the right message, we trigger the doorbell request.
777
- if (value?.toLowerCase() !== "true") {
704
+ if (value !== "true") {
778
705
  return;
779
706
  }
780
707
  this.nvr.events.doorbellEventHandler(this, Date.now());
781
- this.log.info("Doorbell ring event triggered via MQTT.");
782
708
  });
783
709
  }
784
710
  return true;
@@ -812,9 +738,8 @@ export class ProtectCamera extends ProtectDevice {
812
738
  }
813
739
  // Set the bitrate for a specific camera channel.
814
740
  async setBitrate(channelId, value) {
815
- // If we've disabled the ability to set the bitrate dynamically, silently fail. We prioritize switches over the global
816
- // setting here, in case the user enabled both, using the principle that the most specific setting always wins. If the
817
- // user has both the global setting and the switch enabled, the switch setting will take precedence.
741
+ // If we've disabled the ability to set the bitrate dynamically, silently fail. We prioritize switches over the global setting here, in case the user enabled both,
742
+ // using the principle that the most specific setting always wins. If the user has both the global setting and the switch enabled, the switch will take precedence.
818
743
  if ((!this.accessory.context.dynamicBitrate && !this.hasFeature("Video.DynamicBitrate")) ||
819
744
  (!this.accessory.context.dynamicBitrate && this.hasFeature("Video.DynamicBitrate") && this.hasFeature("Video.DynamicBitrate.Switch"))) {
820
745
  return true;