homebridge-unifi-protect 5.3.2 → 5.4.1

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.
@@ -26,6 +26,12 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
26
26
  if (this.accessory.context.detectMotion !== undefined) {
27
27
  detectMotion = this.accessory.context.detectMotion;
28
28
  }
29
+ // Default to disabling the dynamic bitrate setting.
30
+ let dynamicBitrate = false;
31
+ // Save the dynamic bitrate switch state before we wipeout the context.
32
+ if (this.accessory.context.dynamicBitrate !== undefined) {
33
+ dynamicBitrate = this.accessory.context.dynamicBitrate;
34
+ }
29
35
  // Default to enabling HKSV recording.
30
36
  let hksvRecording = true;
31
37
  // Save the HKSV recording switch state before we wipeout the context.
@@ -37,10 +43,11 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
37
43
  this.accessory.context.device = device;
38
44
  this.accessory.context.nvr = (_a = this.nvr.nvrApi.bootstrap) === null || _a === void 0 ? void 0 : _a.nvr.mac;
39
45
  this.accessory.context.detectMotion = detectMotion;
46
+ this.accessory.context.dynamicBitrate = dynamicBitrate;
40
47
  this.accessory.context.hksvRecording = hksvRecording;
41
- // Inform the user if we have enabled dynamic bitrates.
42
- if ((_b = this.nvr) === null || _b === void 0 ? void 0 : _b.optionEnabled(this.accessory.context.device, "Video.Dynamic.Bitrates", false)) {
43
- this.log.info("%s: Dynamic adjustment of bitrates using the UniFi Protect controller enabled.", this.name());
48
+ // Inform the user if we have enabled the dynamic bitrate setting.
49
+ if ((_b = this.nvr) === null || _b === void 0 ? void 0 : _b.optionEnabled(this.accessory.context.device, "Video.DynamicBitrate", false)) {
50
+ this.log.info("%s: Dynamic streaming bitrate adjustment on the UniFi Protect controller enabled.", this.name());
44
51
  }
45
52
  // If the camera supports it, check to see if we have smart motion events enabled.
46
53
  if (device.featureFlags.hasSmartDetect && ((_c = this.nvr) === null || _c === void 0 ? void 0 : _c.optionEnabled(device, "Motion.SmartDetect", false))) {
@@ -74,6 +81,10 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
74
81
  await this.configureVideoStream();
75
82
  // Configure our camera details.
76
83
  this.configureCameraDetails();
84
+ // Configure our bitrate switch.
85
+ this.configureDynamicBitrateSwitch();
86
+ // Configure our NVR recording switches.
87
+ this.configureNvrRecordingSwitch();
77
88
  // Configure the doorbell trigger.
78
89
  this.configureDoorbellTrigger();
79
90
  return true;
@@ -83,7 +94,7 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
83
94
  var _a, _b, _c, _d, _e, _f;
84
95
  const device = this.accessory.context.device;
85
96
  // Check for object-centric contact sensors that are no longer enabled and remove them.
86
- for (const objectService of this.accessory.services.filter(x => { var _a; return (_a = x.subtype) === null || _a === void 0 ? void 0 : _a.startsWith(protect_accessory_1.PROTECT_CONTACT_MOTION_SMARTDETECT + "."); })) {
97
+ for (const objectService of this.accessory.services.filter(x => { var _a; return (_a = x.subtype) === null || _a === void 0 ? void 0 : _a.startsWith(protect_accessory_1.ProtectReservedNames.CONTACT_MOTION_SMARTDETECT + "."); })) {
87
98
  // If we have motion sensors as well as object contact sensors enabled, and we have this object type enabled on this camera, we're good here.
88
99
  if (((_a = this.nvr) === null || _a === void 0 ? void 0 : _a.optionEnabled(device, "Motion.Sensor")) &&
89
100
  ((_b = this.nvr) === null || _b === void 0 ? void 0 : _b.optionEnabled(device, "Motion.SmartDetect.ObjectSensors", false))) {
@@ -104,10 +115,10 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
104
115
  // Add individual contact sensors for each object detection type, if needed.
105
116
  for (const smartDetectType of device.featureFlags.smartDetectTypes) {
106
117
  // See if we already have this contact sensor configured.
107
- let contactService = this.accessory.getServiceById(this.hap.Service.ContactSensor, protect_accessory_1.PROTECT_CONTACT_MOTION_SMARTDETECT + "." + smartDetectType);
118
+ let contactService = this.accessory.getServiceById(this.hap.Service.ContactSensor, protect_accessory_1.ProtectReservedNames.CONTACT_MOTION_SMARTDETECT + "." + smartDetectType);
108
119
  // If not, let's add it.
109
120
  if (!contactService) {
110
- contactService = new this.hap.Service.ContactSensor(this.accessory.displayName + " " + smartDetectType.charAt(0).toUpperCase() + smartDetectType.slice(1), protect_accessory_1.PROTECT_CONTACT_MOTION_SMARTDETECT + "." + smartDetectType);
121
+ contactService = new this.hap.Service.ContactSensor(this.accessory.displayName + " " + smartDetectType.charAt(0).toUpperCase() + smartDetectType.slice(1), protect_accessory_1.ProtectReservedNames.CONTACT_MOTION_SMARTDETECT + "." + smartDetectType);
111
122
  // Something went wrong, we're done here.
112
123
  if (!contactService) {
113
124
  this.log.error("%s: Unable to add smart motion contact sensor for %s detection.", this.name(), smartDetectType);
@@ -126,7 +137,7 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
126
137
  configureMotionTrigger() {
127
138
  var _a, _b, _c;
128
139
  // Find the switch service, if it exists.
129
- let triggerService = this.accessory.getServiceById(this.hap.Service.Switch, protect_accessory_1.PROTECT_SWITCH_MOTION_TRIGGER);
140
+ let triggerService = this.accessory.getServiceById(this.hap.Service.Switch, protect_accessory_1.ProtectReservedNames.SWITCH_MOTION_TRIGGER);
130
141
  // Motion triggers are disabled by default and primarily exist for automation purposes.
131
142
  if (!((_a = this.nvr) === null || _a === void 0 ? void 0 : _a.optionEnabled(this.accessory.context.device, "Motion.Sensor")) ||
132
143
  !((_b = this.nvr) === null || _b === void 0 ? void 0 : _b.optionEnabled(this.accessory.context.device, "Motion.Trigger", false))) {
@@ -137,7 +148,7 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
137
148
  }
138
149
  // Add the switch to the camera, if needed.
139
150
  if (!triggerService) {
140
- triggerService = new this.hap.Service.Switch(this.accessory.displayName + " Motion Trigger", protect_accessory_1.PROTECT_SWITCH_MOTION_TRIGGER);
151
+ triggerService = new this.hap.Service.Switch(this.accessory.displayName + " Motion Trigger", protect_accessory_1.ProtectReservedNames.SWITCH_MOTION_TRIGGER);
141
152
  if (!triggerService) {
142
153
  this.log.error("%s: Unable to add motion sensor trigger.", this.name());
143
154
  return false;
@@ -145,7 +156,7 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
145
156
  this.accessory.addService(triggerService);
146
157
  }
147
158
  const motionService = this.accessory.getService(this.hap.Service.MotionSensor);
148
- const switchService = this.accessory.getServiceById(this.hap.Service.Switch, protect_accessory_1.PROTECT_SWITCH_MOTION_SENSOR);
159
+ const switchService = this.accessory.getServiceById(this.hap.Service.Switch, protect_accessory_1.ProtectReservedNames.SWITCH_MOTION_SENSOR);
149
160
  // Activate or deactivate motion detection.
150
161
  (_c = triggerService
151
162
  .getCharacteristic(this.hap.Characteristic.On)) === null || _c === void 0 ? void 0 : _c.onGet(() => {
@@ -183,7 +194,7 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
183
194
  var _a, _b;
184
195
  const camera = this.accessory.context.device;
185
196
  // Find the switch service, if it exists.
186
- let triggerService = this.accessory.getServiceById(this.hap.Service.Switch, protect_accessory_1.PROTECT_SWITCH_DOORBELL_TRIGGER);
197
+ let triggerService = this.accessory.getServiceById(this.hap.Service.Switch, protect_accessory_1.ProtectReservedNames.SWITCH_DOORBELL_TRIGGER);
187
198
  // See if we have a doorbell service configured.
188
199
  let doorbellService = this.accessory.getService(this.hap.Service.Doorbell);
189
200
  // Doorbell switches are disabled by default and primarily exist for automation purposes.
@@ -213,7 +224,7 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
213
224
  }
214
225
  // Add the switch to the camera, if needed.
215
226
  if (!triggerService) {
216
- triggerService = new this.hap.Service.Switch(this.accessory.displayName + " Doorbell Trigger", protect_accessory_1.PROTECT_SWITCH_DOORBELL_TRIGGER);
227
+ triggerService = new this.hap.Service.Switch(this.accessory.displayName + " Doorbell Trigger", protect_accessory_1.ProtectReservedNames.SWITCH_DOORBELL_TRIGGER);
217
228
  if (!triggerService) {
218
229
  this.log.error("%s: Unable to add the doorbell trigger.", this.name());
219
230
  return false;
@@ -305,69 +316,6 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
305
316
  }
306
317
  return true;
307
318
  }
308
- // Find an RTSP configuration for a given target resolution.
309
- findRtsp(width, height, camera = null, address = "", rtspEntries = this.rtspEntries) {
310
- var _a, _b;
311
- // No RTSP entries to choose from, we're done.
312
- if (!rtspEntries || !rtspEntries.length) {
313
- return null;
314
- }
315
- // First, we check to see if we've set an explicit preference for the target address.
316
- if (camera && address) {
317
- // If we don't have this address cached, look it up and cache it.
318
- if (!this.rtspQuality[address]) {
319
- // Check to see if there's an explicit preference set and cache the result.
320
- if (this.nvr.optionEnabled(camera, "Video.Stream.Only.Low", false, address, true)) {
321
- this.rtspQuality[address] = "LOW";
322
- }
323
- else if (this.nvr.optionEnabled(camera, "Video.Stream.Only.Medium", false, address, true)) {
324
- this.rtspQuality[address] = "MEDIUM";
325
- }
326
- else if (this.nvr.optionEnabled(camera, "Video.Stream.Only.High", false, address, true)) {
327
- this.rtspQuality[address] = "HIGH";
328
- }
329
- else {
330
- this.rtspQuality[address] = "None";
331
- }
332
- }
333
- // If it's set to none, we default to our normal lookup logic.
334
- if (this.rtspQuality[address] !== "None") {
335
- return (_a = rtspEntries.find(x => x.channel.name.toUpperCase() === this.rtspQuality[address])) !== null && _a !== void 0 ? _a : null;
336
- }
337
- }
338
- // Second, we check to see if we've set an explicit preference for stream quality.
339
- if (this.rtspQuality.Default) {
340
- return (_b = rtspEntries.find(x => x.channel.name.toUpperCase() === this.rtspQuality.Default)) !== null && _b !== void 0 ? _b : null;
341
- }
342
- // See if we have a match for our desired resolution on the camera. We ignore FPS - HomeKit clients seem
343
- // to be able to handle it just fine.
344
- const exactRtsp = rtspEntries.find(x => (x.resolution[0] === width) && (x.resolution[1] === height));
345
- if (exactRtsp) {
346
- return exactRtsp;
347
- }
348
- // No match found, let's see what we have that's closest. We try to be a bit smart about how we select our
349
- // stream - if it's an HD quality stream request (720p+), we want to try to return something that's HD quality
350
- // before looking for something lower resolution.
351
- if ((width >= 1280) && (height >= 720)) {
352
- for (const entry of rtspEntries) {
353
- // Make sure we're looking at an HD resolution.
354
- if (entry.resolution[0] < 1280) {
355
- continue;
356
- }
357
- // Return the first one we find.
358
- return entry;
359
- }
360
- }
361
- // If we didn't request an HD resolution, or we couldn't find anything HD to use, we try to find whatever we
362
- // can find that's close.
363
- for (const entry of rtspEntries) {
364
- if (width >= entry.resolution[0]) {
365
- return entry;
366
- }
367
- }
368
- // We couldn't find a close match, return the lowest resolution we found.
369
- return rtspEntries[rtspEntries.length - 1];
370
- }
371
319
  // Configure a camera accessory for HomeKit.
372
320
  async configureVideoStream() {
373
321
  var _a, _b;
@@ -448,19 +396,24 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
448
396
  this.log.info("%s: Mapping resolution: %s.", this.name(), this.getResolution(entry.resolution) + " => " + entry.name);
449
397
  }
450
398
  }
451
- // Check to see if the user has requested a specific stream quality for this camera.
452
- if (nvr.optionEnabled(device, "Video.Stream.Only.Low", false)) {
453
- this.rtspQuality.Default = "LOW";
454
- }
455
- else if (nvr.optionEnabled(device, "Video.Stream.Only.Medium", false)) {
456
- this.rtspQuality.Default = "MEDIUM";
399
+ // Check for explicit RTSP profile preferences.
400
+ for (const rtspProfile of ["LOW", "MEDIUM", "HIGH"]) {
401
+ // Check to see if the user has requested a specific streaming profile for this camera.
402
+ if (nvr.optionEnabled(device, "Video.Stream.Only." + rtspProfile, false)) {
403
+ this.rtspQuality.StreamingDefault = rtspProfile;
404
+ }
405
+ // Check to see if the user has requested a specific recording profile for this camera.
406
+ if (nvr.optionEnabled(device, "Video.HKSV.Recording.Only." + rtspProfile, false)) {
407
+ this.rtspQuality.RecordingDefault = rtspProfile;
408
+ }
457
409
  }
458
- else if (nvr.optionEnabled(device, "Video.Stream.Only.High", false)) {
459
- this.rtspQuality.Default = "HIGH";
410
+ // Inform the user if we've set a streaming default.
411
+ if (this.rtspQuality.StreamingDefault) {
412
+ this.log.info("%s: Video streaming configured to use only: %s.", this.name(), this.rtspQuality.StreamingDefault.charAt(0) + this.rtspQuality.StreamingDefault.slice(1).toLowerCase());
460
413
  }
461
- // Inform the user if we've set a default.
462
- if (this.rtspQuality.Default) {
463
- this.log.info("%s: Configured to use only RTSP stream profile: %s.", this.name(), this.rtspQuality.Default.charAt(0) + this.rtspQuality.Default.slice(1).toLowerCase());
414
+ // Inform the user if we've set a recording default.
415
+ if (this.rtspQuality.RecordingDefault) {
416
+ this.log.info("%s: HomeKit Secure Video event recording configured to use only: %s.", this.name(), this.rtspQuality.RecordingDefault.charAt(0) + this.rtspQuality.RecordingDefault.slice(1).toLowerCase());
464
417
  }
465
418
  // Configure the video stream with our resolutions.
466
419
  this.stream = new protect_stream_1.ProtectStreamingDelegate(this, this.rtspEntries.map(x => x.resolution));
@@ -498,20 +451,20 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
498
451
  configureHksvRecordingSwitch() {
499
452
  var _a, _b, _c;
500
453
  // Find the switch service, if it exists.
501
- let switchService = this.accessory.getServiceById(this.hap.Service.Switch, protect_accessory_1.PROTECT_SWITCH_HKSV_RECORDING);
502
- // If we don't have HKSV or the HKSV recording switch, disable the switch and we're done.
454
+ let switchService = this.accessory.getServiceById(this.hap.Service.Switch, protect_accessory_1.ProtectReservedNames.SWITCH_HKSV_RECORDING);
455
+ // If we don't have HKSV or the HKSV recording switch enabled, disable it and we're done.
503
456
  if (!((_a = this.nvr) === null || _a === void 0 ? void 0 : _a.optionEnabled(this.accessory.context.device, "Video.HKSV")) ||
504
457
  !((_b = this.nvr) === null || _b === void 0 ? void 0 : _b.optionEnabled(this.accessory.context.device, "Video.HKSV.Recording.Switch", false))) {
505
458
  if (switchService) {
506
459
  this.accessory.removeService(switchService);
507
460
  }
508
461
  // We want to default this back to recording whenever we disable the recording switch.
509
- this.accessory.context.hksvRecording;
462
+ this.accessory.context.hksvRecording = true;
510
463
  return false;
511
464
  }
512
465
  // Add the switch to the camera, if needed.
513
466
  if (!switchService) {
514
- switchService = new this.hap.Service.Switch(this.accessory.displayName + " HKSV Recording", protect_accessory_1.PROTECT_SWITCH_HKSV_RECORDING);
467
+ switchService = new this.hap.Service.Switch(this.accessory.displayName + " HKSV Recording", protect_accessory_1.ProtectReservedNames.SWITCH_HKSV_RECORDING);
515
468
  if (!switchService) {
516
469
  this.log.error("%s: Unable to add the HomeKit Secure Video recording switch.", this.name());
517
470
  return false;
@@ -533,6 +486,139 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
533
486
  this.log.info("%s: Enabling HomeKit Secure Video recording switch.", this.name());
534
487
  return true;
535
488
  }
489
+ // Configure a switch to manually enable or disable dynamic bitrate capabilities for a camera.
490
+ configureDynamicBitrateSwitch() {
491
+ var _a, _b;
492
+ // Find the switch service, if it exists.
493
+ let switchService = this.accessory.getServiceById(this.hap.Service.Switch, protect_accessory_1.ProtectReservedNames.SWITCH_DYNAMIC_BITRATE);
494
+ // If we don't want a dynamic bitrate switch, disable it and we're done.
495
+ if (!((_a = this.nvr) === null || _a === void 0 ? void 0 : _a.optionEnabled(this.accessory.context.device, "Video.DynamicBitrate.Switch", false))) {
496
+ if (switchService) {
497
+ this.accessory.removeService(switchService);
498
+ }
499
+ // We want to default this back to off by default whenever we disable the dynamic bitrate switch.
500
+ this.accessory.context.dynamicBitrate = false;
501
+ return false;
502
+ }
503
+ // Add the switch to the camera, if needed.
504
+ if (!switchService) {
505
+ switchService = new this.hap.Service.Switch(this.accessory.displayName + " Dynamic Bitrate", protect_accessory_1.ProtectReservedNames.SWITCH_DYNAMIC_BITRATE);
506
+ if (!switchService) {
507
+ this.log.error("%s: Unable to add the dynamic bitrate switch.", this.name());
508
+ return false;
509
+ }
510
+ this.accessory.addService(switchService);
511
+ }
512
+ // Activate or deactivate dynamic bitrate for this device.
513
+ (_b = switchService
514
+ .getCharacteristic(this.hap.Characteristic.On)) === null || _b === void 0 ? void 0 : _b.onGet(() => {
515
+ return this.accessory.context.dynamicBitrate;
516
+ }).onSet(async (value) => {
517
+ if (this.accessory.context.dynamicBitrate === value) {
518
+ return;
519
+ }
520
+ // We're enabling dynamic bitrate for this device.
521
+ if (value) {
522
+ this.accessory.context.dynamicBitrate = true;
523
+ this.log.info("%s: Dynamic streaming bitrate adjustment on the UniFi Protect controller enabled.", this.name());
524
+ return;
525
+ }
526
+ // We're disabling dynamic bitrate for this device.
527
+ const device = this.accessory.context.device;
528
+ const updatedChannels = device.channels;
529
+ // Update the channels JSON.
530
+ for (const channel of updatedChannels) {
531
+ channel.bitrate = channel.maxBitrate;
532
+ }
533
+ // Send the channels JSON to Protect.
534
+ const newDevice = await this.nvrApi.updateCamera(device, { channels: updatedChannels });
535
+ // We failed.
536
+ if (!newDevice) {
537
+ this.log.error("%s: Unable to set the streaming bitrate to %s.", this.name(), value);
538
+ }
539
+ else {
540
+ this.accessory.context.device = newDevice;
541
+ }
542
+ this.accessory.context.dynamicBitrate = false;
543
+ this.log.info("%s: Dynamic streaming bitrate adjustment on the UniFi Protect controller disabled.", this.name());
544
+ });
545
+ // Initialize the switch.
546
+ switchService.updateCharacteristic(this.hap.Characteristic.On, this.accessory.context.dynamicBitrate);
547
+ this.log.info("%s: Enabling the dynamic streaming bitrate adjustment switch.", this.name());
548
+ return true;
549
+ }
550
+ // Configure a series of switches to manually enable or disable recording on the UniFi Protect controller for a camera.
551
+ configureNvrRecordingSwitch() {
552
+ var _a, _b;
553
+ const switchesEnabled = [];
554
+ // The Protect controller supports three modes for recording on a camera: always, detections, and never. We create switches for each of the modes.
555
+ for (const ufpRecordingSwitchType of [protect_accessory_1.ProtectReservedNames.SWITCH_UFP_RECORDING_ALWAYS, protect_accessory_1.ProtectReservedNames.SWITCH_UFP_RECORDING_DETECTIONS, protect_accessory_1.ProtectReservedNames.SWITCH_UFP_RECORDING_NEVER]) {
556
+ const ufpRecordingSetting = ufpRecordingSwitchType.slice(ufpRecordingSwitchType.lastIndexOf(".") + 1);
557
+ // Find the switch service, if it exists.
558
+ let switchService = this.accessory.getServiceById(this.hap.Service.Switch, ufpRecordingSwitchType);
559
+ // If we don't have the feature option enabled, disable the switch and we're done.
560
+ if (!((_a = this.nvr) === null || _a === void 0 ? void 0 : _a.optionEnabled(this.accessory.context.device, "Nvr.Recording.Switch", false))) {
561
+ if (switchService) {
562
+ this.accessory.removeService(switchService);
563
+ }
564
+ continue;
565
+ }
566
+ // Add the switch to the camera, if needed.
567
+ if (!switchService) {
568
+ switchService = new this.hap.Service.Switch(this.accessory.displayName + " UFP Recording " + ufpRecordingSetting.charAt(0).toUpperCase() + ufpRecordingSetting.slice(1), ufpRecordingSwitchType);
569
+ if (!switchService) {
570
+ this.log.error("%s: Unable to add the UniFi Protect recording switches.", this.name());
571
+ continue;
572
+ }
573
+ this.accessory.addService(switchService);
574
+ }
575
+ // Activate or deactivate the appropriate recording mode on the Protect controller.
576
+ (_b = switchService
577
+ .getCharacteristic(this.hap.Characteristic.On)) === null || _b === void 0 ? void 0 : _b.onGet(() => {
578
+ return this.accessory.context.device.recordingSettings.mode === ufpRecordingSetting;
579
+ }).onSet(async (value) => {
580
+ var _a;
581
+ // We only want to do something if we're being activated. Turning off the switch would really be an undefined state given that
582
+ // there are three different settings one can choose from. Instead, we do nothing and leave it to the user to choose what state
583
+ // they really want to set.
584
+ if (!value) {
585
+ setTimeout(() => {
586
+ this.updateDevice();
587
+ }, 50);
588
+ return;
589
+ }
590
+ // Set our recording mode.
591
+ const device = this.accessory.context.device;
592
+ device.recordingSettings.mode = ufpRecordingSetting;
593
+ // Tell Protect about it.
594
+ const newDevice = await this.nvr.nvrApi.updateCamera(device, { recordingSettings: device.recordingSettings });
595
+ if (!newDevice) {
596
+ this.log.error("%s: Unable to set the UniFi Protect recording mode to %s.", this.name(), ufpRecordingSetting);
597
+ return false;
598
+ }
599
+ // Save our updated device context.
600
+ this.accessory.context.device = newDevice;
601
+ // Update all the other recording switches.
602
+ for (const otherUfpSwitch of [protect_accessory_1.ProtectReservedNames.SWITCH_UFP_RECORDING_ALWAYS, protect_accessory_1.ProtectReservedNames.SWITCH_UFP_RECORDING_DETECTIONS, protect_accessory_1.ProtectReservedNames.SWITCH_UFP_RECORDING_NEVER]) {
603
+ // Don't update ourselves a second time.
604
+ if (ufpRecordingSwitchType === otherUfpSwitch) {
605
+ continue;
606
+ }
607
+ // Update the other recording switches.
608
+ (_a = this.accessory.getServiceById(this.hap.Service.Switch, otherUfpSwitch)) === null || _a === void 0 ? void 0 : _a.updateCharacteristic(this.hap.Characteristic.On, false);
609
+ }
610
+ // Inform the user, and we're done.
611
+ this.log.info("%s: UniFi Protect recording mode set to %s.", this.name(), ufpRecordingSetting);
612
+ });
613
+ // Initialize the recording switch state.
614
+ switchService.updateCharacteristic(this.hap.Characteristic.On, this.accessory.context.device.recordingSettings.mode === ufpRecordingSetting);
615
+ switchesEnabled.push(ufpRecordingSetting);
616
+ }
617
+ if (switchesEnabled.length) {
618
+ this.log.info("%s: Enabling UniFi Protect recording switches: %s.", this.name(), switchesEnabled.join(", "));
619
+ }
620
+ return true;
621
+ }
536
622
  // Configure MQTT capabilities of this camera.
537
623
  configureMqtt() {
538
624
  var _a, _b;
@@ -572,16 +658,29 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
572
658
  }
573
659
  // Refresh camera-specific characteristics.
574
660
  updateDevice() {
661
+ var _a, _b, _c;
575
662
  // Grab our device context.
576
663
  const device = this.accessory.context.device;
664
+ // Update the camera state.
665
+ (_a = this.accessory.getService(this.hap.Service.MotionSensor)) === null || _a === void 0 ? void 0 : _a.updateCharacteristic(this.hap.Characteristic.StatusActive, device.state === "CONNECTED");
577
666
  // Find the service, if it exists.
578
667
  const service = this.accessory.getService(this.hap.Service.CameraOperatingMode);
579
668
  // Check to see if this device has a status light.
580
669
  if (device.featureFlags.hasLedStatus) {
581
670
  service === null || service === void 0 ? void 0 : service.updateCharacteristic(this.hap.Characteristic.CameraOperatingModeIndicator, device.ledSettings.isEnabled === true);
582
671
  }
672
+ // Check for updates to the recording state, if we have the switches configured.
673
+ if ((_b = this.nvr) === null || _b === void 0 ? void 0 : _b.optionEnabled(device, "Nvr.Recording.Switch", false)) {
674
+ // Update all the switch states.
675
+ for (const ufpRecordingSwitchType of [protect_accessory_1.ProtectReservedNames.SWITCH_UFP_RECORDING_ALWAYS, protect_accessory_1.ProtectReservedNames.SWITCH_UFP_RECORDING_DETECTIONS, protect_accessory_1.ProtectReservedNames.SWITCH_UFP_RECORDING_NEVER]) {
676
+ const ufpRecordingSetting = ufpRecordingSwitchType.slice(ufpRecordingSwitchType.lastIndexOf(".") + 1);
677
+ // Update state based on the recording mode.
678
+ (_c = this.accessory.getServiceById(this.hap.Service.Switch, ufpRecordingSwitchType)) === null || _c === void 0 ? void 0 : _c.updateCharacteristic(this.hap.Characteristic.On, ufpRecordingSetting === device.recordingSettings.mode);
679
+ }
680
+ }
583
681
  return true;
584
682
  }
683
+ // Get the current bitrate for a specific camera channel.
585
684
  getBitrate(channelId) {
586
685
  var _a;
587
686
  // Grab the device JSON.
@@ -592,9 +691,15 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
592
691
  }
593
692
  // Set the bitrate for a specific camera channel.
594
693
  async setBitrate(channelId, value) {
595
- var _a;
596
- // If we've disabled the ability to set bitrates dynamically, silently fail.
597
- if (!((_a = this.nvr) === null || _a === void 0 ? void 0 : _a.optionEnabled(this.accessory.context.device, "Video.Dynamic.Bitrates", false))) {
694
+ var _a, _b, _c;
695
+ // If we've disabled the ability to set the bitrate dynamically, silently fail. We prioritize switches over the global
696
+ // setting here, in case the user enabled both, using the principle that the most specific setting always wins. If the
697
+ // user has both the global setting and the switch enabled, the switch setting will take precedence.
698
+ if ((!this.accessory.context.dynamicBitrate &&
699
+ !((_a = this.nvr) === null || _a === void 0 ? void 0 : _a.optionEnabled(this.accessory.context.device, "Video.DynamicBitrate", false))) ||
700
+ (!this.accessory.context.dynamicBitrate &&
701
+ ((_b = this.nvr) === null || _b === void 0 ? void 0 : _b.optionEnabled(this.accessory.context.device, "Video.DynamicBitrate", false)) &&
702
+ ((_c = this.nvr) === null || _c === void 0 ? void 0 : _c.optionEnabled(this.accessory.context.device, "Video.DynamicBitrate.Switch", false)))) {
598
703
  return true;
599
704
  }
600
705
  // Grab the device JSON.
@@ -621,6 +726,77 @@ class ProtectCamera extends protect_accessory_1.ProtectAccessory {
621
726
  this.accessory.context.device = newDevice;
622
727
  return true;
623
728
  }
729
+ // Find an RTSP configuration for a given target resolution.
730
+ findRtspEntry(width, height, camera, address, rtspEntries, defaultStream = this.rtspQuality.StreamingDefault) {
731
+ var _a, _b;
732
+ // No RTSP entries to choose from, we're done.
733
+ if (!rtspEntries || !rtspEntries.length) {
734
+ return null;
735
+ }
736
+ // First, we check to see if we've set an explicit preference for the target address.
737
+ if (camera && address) {
738
+ // If we don't have this address cached, look it up and cache it.
739
+ if (!this.rtspQuality[address]) {
740
+ // Check to see if there's an explicit preference set and cache the result.
741
+ if (this.nvr.optionEnabled(camera, "Video.Stream.Only.Low", false, address, true)) {
742
+ this.rtspQuality[address] = "LOW";
743
+ }
744
+ else if (this.nvr.optionEnabled(camera, "Video.Stream.Only.Medium", false, address, true)) {
745
+ this.rtspQuality[address] = "MEDIUM";
746
+ }
747
+ else if (this.nvr.optionEnabled(camera, "Video.Stream.Only.High", false, address, true)) {
748
+ this.rtspQuality[address] = "HIGH";
749
+ }
750
+ else {
751
+ this.rtspQuality[address] = "None";
752
+ }
753
+ }
754
+ // If it's set to none, we default to our normal lookup logic.
755
+ if (this.rtspQuality[address] !== "None") {
756
+ return (_a = rtspEntries.find(x => x.channel.name.toUpperCase() === this.rtspQuality[address])) !== null && _a !== void 0 ? _a : null;
757
+ }
758
+ }
759
+ // Second, we check to see if we've set an explicit preference for stream quality.
760
+ if (defaultStream) {
761
+ return (_b = rtspEntries.find(x => x.channel.name.toUpperCase() === defaultStream)) !== null && _b !== void 0 ? _b : null;
762
+ }
763
+ // See if we have a match for our desired resolution on the camera. We ignore FPS - HomeKit clients seem
764
+ // to be able to handle it just fine.
765
+ const exactRtsp = rtspEntries.find(x => (x.resolution[0] === width) && (x.resolution[1] === height));
766
+ if (exactRtsp) {
767
+ return exactRtsp;
768
+ }
769
+ // No match found, let's see what we have that's closest. We try to be a bit smart about how we select our
770
+ // stream - if it's an HD quality stream request (720p+), we want to try to return something that's HD quality
771
+ // before looking for something lower resolution.
772
+ if ((width >= 1280) && (height >= 720)) {
773
+ for (const entry of rtspEntries) {
774
+ // Make sure we're looking at an HD resolution.
775
+ if (entry.resolution[0] < 1280) {
776
+ continue;
777
+ }
778
+ // Return the first one we find.
779
+ return entry;
780
+ }
781
+ }
782
+ // If we didn't request an HD resolution, or we couldn't find anything HD to use, we try to find whatever we
783
+ // can find that's close.
784
+ for (const entry of rtspEntries) {
785
+ if (width >= entry.resolution[0]) {
786
+ return entry;
787
+ }
788
+ }
789
+ // We couldn't find a close match, return the lowest resolution we found.
790
+ return rtspEntries[rtspEntries.length - 1];
791
+ }
792
+ // Find a streaming RTSP configuration for a given target resolution.
793
+ findRtsp(width, height, camera = null, address = "", rtspEntries = this.rtspEntries) {
794
+ return this.findRtspEntry(width, height, camera, address, rtspEntries);
795
+ }
796
+ // Find a recording RTSP configuration for a given target resolution.
797
+ findRecordingRtsp(width, height, camera = null, rtspEntries = this.rtspEntries) {
798
+ return this.findRtspEntry(width, height, camera, "", rtspEntries, this.rtspQuality.RecordingDefault);
799
+ }
624
800
  // Utility function for sorting by resolution.
625
801
  sortByResolutions(a, b) {
626
802
  // Check width.